diff --git a/.agents/skills/minimizing-ty-ecosystem-changes/SKILL.md b/.agents/skills/minimizing-ty-ecosystem-changes/SKILL.md index a511397bb4..f6baa25367 100644 --- a/.agents/skills/minimizing-ty-ecosystem-changes/SKILL.md +++ b/.agents/skills/minimizing-ty-ecosystem-changes/SKILL.md @@ -7,19 +7,24 @@ description: Use when a user says "minimize this ty ecosystem change", "reproduc ## Invariants -1. Use the exact Ruff revisions, PR config, dependency cutoff, mypy-primer revision, and project Python version from the Actions run. +1. Use the exact Ruff revisions, user-level PR config, dependency cutoff, mypy-primer revision, project Python version, and strictness settings from the Actions run. 2. Reproduce the reported project difference before explaining it or writing a smaller example. 3. Treat copied binaries and config as read-only, and verify every reduction against both binaries. +4. Derive every candidate from the preceding verified candidate; NEVER substitute an independently constructed example. +5. Preserve the underlying trigger, not merely the diagnostic rule, message, or displayed type. Start each investigation from fresh artifacts. Do not trust retained memories, previous minimizations, current upstream project state, or the helper script's default lockfile. ## Collect Exact-Run Metadata -Run the bundled helper with the Actions run ID or URL and every affected mypy-primer project name: +If a primary agent supplied an existing run-metadata manifest, verify that its run ID and attempt match the frozen report and that it contains each assigned project. Reuse the manifest without modifying it. + +Otherwise, run the bundled helper with the Actions run ID or URL, matching attempt, and every affected mypy-primer project name: ```bash scripts/collect_ty_ecosystem_run_metadata.py \ ... \ + --attempt \ --output target/ty-ecosystem-run.json ``` @@ -29,7 +34,7 @@ The current workflow splits compilation into `Build ty (base)` and `Build ty (pr ## Prepare ty -If a primary agent supplied freshly copied base and PR binaries plus the PR ecosystem config, verify the paths exist and reuse them. Do not rebuild, switch Ruff refs, or overwrite the shared artifacts. +If a primary agent supplied freshly copied base and PR profiling binaries plus the PR ecosystem config, preserve their absolute paths as `TY_ECOSYSTEM_BASE_BINARY` and `TY_ECOSYSTEM_PR_BINARY`, verify they exist, and reuse them. Do not rebuild those binaries, switch shared Ruff refs, or overwrite the shared artifacts. An agent may build an exact-revision debug binary on demand to identify an ambiguous internal type, using an isolated worktree if necessary; the profiling binaries remain the behavioral oracle. Otherwise, require a clean working tree, copy `.github/ty-ecosystem.toml` from the PR revision, and build ty on the manifest's merge base and PR revision: @@ -55,7 +60,7 @@ cp target/profiling/ty target/ty-ecosystem-bins/ty-pr ## Reproduce -Create a unique temporary directory for each project. Read its Python version and the pinned mypy-primer revision from the manifest, then bypass the adjacent script lockfile: +Create a unique temporary directory for each project and use its absolute path. Read its Python version and the pinned mypy-primer revision from the manifest. Obtain the project revision from the `/blob//` component of the original diagnostic's source permalink, and check that links for the same project agree. If no diagnostic permalink exists, inspect the matching diagnostics shard or Actions logs; if the exact revision cannot be recovered, explicitly report that limitation. Then bypass the adjacent script lockfile: ```bash uv run \ @@ -63,32 +68,63 @@ uv run \ --with "mypy-primer @ git+https://github.com/hauntsaninja/mypy_primer@" \ --no-project \ python scripts/setup_primer_project.py \ - \ + \ --revision \ --exclude-newer ``` -Use absolute paths and re-export `TY_CONFIG_FILE` in every new shell before running either binary: +Use the ecosystem config as user-level configuration, matching CI without replacing project-level config discovery, and re-export `XDG_CONFIG_HOME` in each new shell. If a primary agent supplied `TY_ECOSYSTEM_CONFIG_HOME`, reuse its installed config without modifying it; otherwise, install the copied config locally. Read the project's `strict` or `non-strict` label from the frozen detailed report, or its `strict_settings` value from the matching diagnostics shard. Preserve that mode when running either binary: ```bash -export TY_CONFIG_FILE="$PWD/target/ty-ecosystem-bins/ty-ecosystem.toml" -test -f "$TY_CONFIG_FILE" -project_dir="$PWD/" -ty_base="$PWD/target/ty-ecosystem-bins/ty-base" -ty_pr="$PWD/target/ty-ecosystem-bins/ty-pr" +if [[ -n "${TY_ECOSYSTEM_CONFIG_HOME:-}" ]]; then + export XDG_CONFIG_HOME="$TY_ECOSYSTEM_CONFIG_HOME" + test -f "$XDG_CONFIG_HOME/ty/ty.toml" || exit 1 +else + export XDG_CONFIG_HOME="$PWD/target/ty-ecosystem-config" + mkdir -p "$XDG_CONFIG_HOME/ty" + cp "$PWD/target/ty-ecosystem-bins/ty-ecosystem.toml" "$XDG_CONFIG_HOME/ty/ty.toml" +fi +unset TY_CONFIG_FILE + +project_dir="" +ty_base="${TY_ECOSYSTEM_BASE_BINARY:-$PWD/target/ty-ecosystem-bins/ty-base}" +ty_pr="${TY_ECOSYSTEM_PR_BINARY:-$PWD/target/ty-ecosystem-bins/ty-pr}" +test -x "$ty_base" && test -x "$ty_pr" || exit 1 +ecosystem_analysis_mode="" + +if [[ "$ecosystem_analysis_mode" != strict && "$ecosystem_analysis_mode" != non-strict ]]; then + echo "Unknown ecosystem analysis mode: $ecosystem_analysis_mode" >&2 + exit 1 +fi + +run_ecosystem_ty() { + if [[ "$ecosystem_analysis_mode" == strict ]]; then + \ + --config analysis.strict-equality-semantics=true \ + --config analysis.strict-generic-narrowing=true + else + + fi +} cd "$project_dir" ty_binary="$ty_base" - +base_exit_status=0 +run_ecosystem_ty || base_exit_status=$? ty_binary="$ty_pr" - +pr_exit_status=0 +run_ecosystem_ty || pr_exit_status=$? ``` -Confirm the detailed report's difference exactly, including duplicate diagnostics when present. +Confirm the detailed report's difference exactly, including duplicate diagnostics and both exit statuses. Ordinary diagnostics can produce exit status 1; do not mistake that for a failed reproduction. ## Minimize -Reduce the reproduced project iteratively, using the base-versus-PR output as the oracle after every change. Prefer a single file, no third-party imports, and the least complex code that preserves the difference. For nontrivial reductions, follow [references/advanced-minimization.md](references/advanced-minimization.md). +Reduce the reproduced project toward a self-contained single-file reproducer with minimal code and dependencies. A reduction is trivial only when the difference already occurs in one self-contained file and can be preserved solely by deleting obviously unrelated code. Multiple files, imports or dependencies, inlining, replacing language constructs, ambiguous types such as `@Todo`, or an uncertain cause make a reduction nontrivial. Before attempting any nontrivial reduction, read and follow [references/advanced-minimization.md](references/advanced-minimization.md). If in doubt, treat the reduction as nontrivial. + +Matching diagnostics or displayed types do not establish a shared cause. When the output is ambiguous, identify the original and minimized triggers using exact-revision debug output, a targeted `reveal_type`, or the producing Rust call site. + +Record the original source permalink, accepted reductions, both binaries' results, and any causal fingerprint. If source provenance or a matching cause cannot be established, return the original project excerpt explicitly marked as unminimized. ## Return diff --git a/.agents/skills/minimizing-ty-ecosystem-changes/references/advanced-minimization.md b/.agents/skills/minimizing-ty-ecosystem-changes/references/advanced-minimization.md index 49c7de0654..056b8f0c45 100644 --- a/.agents/skills/minimizing-ty-ecosystem-changes/references/advanced-minimization.md +++ b/.agents/skills/minimizing-ty-ecosystem-changes/references/advanced-minimization.md @@ -8,7 +8,7 @@ Prefer a single-file reproducer with no third-party imports, few definitions, an ## Reduction Loop -Work systematically from the reproduced project. Do not skip ahead to an explanation, hand-written reproducer, or a guessed subset of relevant code. Follow the stages below in order and exhaust each stage before advancing. Try one controlled reduction at a time, run both copied ty binaries after every change, and keep the reduction only if the original difference remains. After every successful reduction, restart at step 1 because it may make earlier reductions possible. +Work systematically from the reproduced project. NEVER skip ahead to an explanation, hand-written reproducer, or a guessed subset of relevant code. Follow the stages below in order and exhaust each stage before advancing. Try one controlled reduction at a time, run both copied ty binaries after every change, and keep the reduction only if the original difference and underlying trigger remain. After every successful reduction, restart at step 1 because it may make earlier reductions possible. 1. Delete unrelated files. 2. Remove imports, definitions, decorators, annotations, statements, and branches. @@ -23,4 +23,6 @@ Repeat the full loop until an exhaustive pass through every stage finds no furth Attempt to remove every remaining import and inline every remaining third-party definition. Record why any surviving import is essential. Keep these notes as working evidence; the caller decides whether they belong in its final artifact. +Verify that the recorded reduction chain connects the final reproducer to the original ecosystem entry and, when diagnostic output is ambiguous, preserves the original causal fingerprint. If either check fails, return the original project excerpt as unminimized instead of substituting an unrelated example. + Delete transient project and dependency copies after the investigation. diff --git a/.agents/skills/summarise-ecosystem-results/SKILL.md b/.agents/skills/summarise-ecosystem-results/SKILL.md index 1ad7e61dfd..1b258a151b 100644 --- a/.agents/skills/summarise-ecosystem-results/SKILL.md +++ b/.agents/skills/summarise-ecosystem-results/SKILL.md @@ -8,7 +8,7 @@ description: Use when a user says "summarise ecosystem results", "summarize this ## Priorities 1. Reproduce every retained behavior with the exact environment used by the Actions run. -2. Lead the report with analysis of diagnostic changes and clear minimized examples. +2. Lead the report with new or changed project failures, then cover meaningful flaky behavior, diagnostic changes, and clear minimized examples. 3. Keep execution, audit, and traceability bookkeeping out of the report. ## Deliverable @@ -21,9 +21,11 @@ If summarising an ecosystem report is the only thing you're asked to do in a Cod ## Workflow -1. **Locate the evidence.** Normalize the input to a PR number, find the ty ecosystem-results comment, open the linked detailed HTML report, and identify the exact Actions run that produced it. Use the comment as the change list and the detailed report as evidence. -2. **Reproduce from scratch.** Ignore retained memories and previous local artifacts. Load the `minimizing-ty-ecosystem-changes` skill, use its metadata helper and exact-run workflow, and reproduce each report entry before explaining or minimizing it. -3. **Minimize and curate.** Retain the smallest clear reproducer for each distinct behavior change. Group entries only when the same base-to-PR behavior, explanation, and reproducer account for every entry in the group. -4. **Write and verify.** Fill the report template, check every link and diagnostic, then run `uvx prek run --files PR__ECOSYSTEM_SUMMARY.md`. Present the Markdown file as the finished product. +1. **Freeze the evidence.** Preserve any report URL or ecosystem-results comment explicitly supplied by the user before identifying the PR. For PR-only input, find its ecosystem-results comment and linked detailed report. Capture the matching Actions run and attempt as described in [references/evidence-acquisition.md](references/evidence-acquisition.md); never replace a supplied report with the PR's current report. Ignore later comment edits, PR updates, and workflow runs. Use the frozen detailed report as the authoritative change list and the comment for orientation when available. +2. **Identify changed outcomes.** Check the detailed report for new, fixed, or changed project failures, panics, timeouts, abnormal exits, and meaningful flaky diagnostic or exit-status changes. Omit unchanged persistent failures. If neither project outcomes nor diagnostics changed, say explicitly that the run had no ecosystem impact and omit project-specific sections and reproduction details. +3. **Reproduce from scratch.** Ignore retained memories and previous local artifacts. Load the `minimizing-ty-ecosystem-changes` skill, use its metadata helper and exact-run workflow, and reproduce each report entry before explaining or minimizing it. Reproduce flaky behavior with the reported run counts. +4. **Minimize with provenance.** Include a standalone reproducer only when a verified reduction chain connects it to a cited ecosystem entry and preserves the same underlying trigger. If either cannot be verified, retain the original source excerpt and identify it as unminimized. +5. **Group by cause.** Group entries only when the same base-to-PR behavior, underlying trigger, explanation, and reproducer account for every entry. Identical diagnostic text or displayed `@Todo` types do not establish equivalence. +6. **Write and verify.** Fill the report template, record each affected project's strict or non-strict analysis mode, and include both strict-analysis flags in the comparison method when applicable. Check every link, diagnostic, reproducer's source provenance, and causal fingerprint when required, then run `uv run --only-group dev --locked prek run --files PR__ECOSYSTEM_SUMMARY.md`. Present the Markdown file as the finished product. -When parallelizing step 2, read [references/subagent-handoff.md](references/subagent-handoff.md). Otherwise, keep batches small and work through them sequentially. +When parallelizing reproduction or minimization, read [references/subagent-handoff.md](references/subagent-handoff.md). Otherwise, keep batches small and work through them sequentially. diff --git a/.agents/skills/summarise-ecosystem-results/assets/report-template.md b/.agents/skills/summarise-ecosystem-results/assets/report-template.md index ff1435d568..c77654181a 100644 --- a/.agents/skills/summarise-ecosystem-results/assets/report-template.md +++ b/.agents/skills/summarise-ecosystem-results/assets/report-template.md @@ -1,8 +1,20 @@ - + # [PR #](https://github.com/astral-sh/ruff/pull/) ecosystem summary - + + + + +## + +**Affected projects:** + +- [](): merge base: ``; PR: ``. + + + + ## @@ -14,19 +26,37 @@ + + ```python -# Merge base: -# PR: ``` ## Reproduction - Detailed report: [ecosystem-analyzer report]() -- Actions run: [run ]() +- Actions run: [run , attempt ]() - Ruff comparison: [``](https://github.com/astral-sh/ruff/commit/) to [``](https://github.com/astral-sh/ruff/commit/) - `ecosystem-analyzer`: [``](https://github.com/astral-sh/ecosystem-analyzer/commit/) - `mypy-primer`: [``](https://github.com/hauntsaninja/mypy_primer/commit/) -- Project Python: `` - Dependency cutoff: `` -- Comparison method: `` +- Project Python: `` +- Project analysis mode: `` +- Comparison method: `` diff --git a/.agents/skills/summarise-ecosystem-results/references/evidence-acquisition.md b/.agents/skills/summarise-ecosystem-results/references/evidence-acquisition.md new file mode 100644 index 0000000000..4c05457817 --- /dev/null +++ b/.agents/skills/summarise-ecosystem-results/references/evidence-acquisition.md @@ -0,0 +1,31 @@ +# Freeze Ecosystem Evidence + +At the beginning of the summary request, preserve any detailed report or ecosystem-results comment explicitly supplied by the user. Only look up the PR's current comment when the user provided no specific report or comment. Save the selected deployed report immediately when it is accessible, then identify its matching PR, Actions run, and attempt; never substitute a newer comment, report, PR revision, workflow run, or reporting attempt. + +If no comment matching an explicitly supplied report remains, continue with the supplied report and record that the matching comment is unavailable. If the exact Actions run or attempt cannot be identified uniquely, report that uncertainty instead of selecting the current PR report or guessing from matching Ruff revisions. + +Create a unique snapshot directory, save the matching comment when available and the selected attempt's effective job graph, and inspect the run's available artifacts: + +```bash +snapshot_dir="$(mktemp -d "${TMPDIR:-/tmp}/ty-ecosystem-report.XXXXXX")" +ecosystem_comment_id="" +if [[ -n "$ecosystem_comment_id" ]]; then + gh api "repos/astral-sh/ruff/issues/comments/$ecosystem_comment_id" > "$snapshot_dir/comment.json" +fi +gh run view --repo astral-sh/ruff --attempt \ + --json attempt,headSha,jobs,startedAt,updatedAt,url > "$snapshot_dir/run.json" +gh api "repos/astral-sh/ruff/actions/runs//artifacts" > "$snapshot_dir/artifacts.json" +``` + +`gh run download` cannot select an attempt, and a newer rerun can replace an older attempt's artifacts without changing Ruff's revisions. Before downloading, verify that `full-report` was created during the selected attempt's report-generation job and that each diagnostics shard was created during its matching successful shard job. Use the effective job graph, not the attempt start time: partial reruns legitimately inherit successful jobs and artifacts from earlier attempts. + +Download only artifacts that pass these checks; use the shard glob only when every matching artifact belongs to the selected job graph: + +```bash +gh run download --repo astral-sh/ruff --name full-report --dir "$snapshot_dir/full-report" +gh run download --repo astral-sh/ruff --pattern 'diagnostics-shard-*' --dir "$snapshot_dir/shards" +``` + +Record the selected Actions attempt and pass it to `scripts/collect_ty_ecosystem_run_metadata.py` with `--attempt `. Verify that the frozen report's Ruff base and PR revisions agree with the resulting manifest, then use the saved report, shards, run, attempt, and matching comment when available throughout the investigation. + +If the selected report's artifacts were replaced or are unavailable, use its frozen deployed report and explicitly describe any unavailable shards or resulting verification limitations. Never silently substitute artifacts produced by an unrelated reporting attempt. diff --git a/.agents/skills/summarise-ecosystem-results/references/subagent-handoff.md b/.agents/skills/summarise-ecosystem-results/references/subagent-handoff.md index de9d946718..78f2bf6565 100644 --- a/.agents/skills/summarise-ecosystem-results/references/subagent-handoff.md +++ b/.agents/skills/summarise-ecosystem-results/references/subagent-handoff.md @@ -4,23 +4,26 @@ Use this reference only when parallelizing reproduction and minimization. ## Primary-Agent Responsibilities -Prepare the copied base binary, PR binary, PR ecosystem config, and run-metadata manifest once. Treat them as read-only shared inputs. Batch related entries without creating more assignments than can run concurrently. +Prepare the frozen evidence snapshot, copied base binary, PR binary, PR ecosystem config, and one run-metadata manifest covering every affected project. Record the binaries' absolute paths as `TY_ECOSYSTEM_BASE_BINARY` and `TY_ECOSYSTEM_PR_BINARY`. Choose an absolute `TY_ECOSYSTEM_CONFIG_HOME` and install the copied config once at `$TY_ECOSYSTEM_CONFIG_HOME/ty/ty.toml`. Treat the snapshot, binaries, manifest, copied config, and installed configuration as read-only shared inputs. Batch related entries without creating more assignments than can run concurrently. ## Assignment Checklist Give each subagent: -- The PR, ecosystem comment, and detailed report links. +- The PR and detailed report links, plus the ecosystem comment link when available. +- The paths to the frozen detailed report and available diagnostics shards, plus the frozen comment path when available and the selected Actions run and attempt; use these captured inputs instead of refetching live evidence. - The exact report entries assigned to it. -- The paths to the copied binaries, copied config, and metadata manifest. +- The copied-config and metadata-manifest paths, plus the shared `TY_ECOSYSTEM_BASE_BINARY`, `TY_ECOSYSTEM_PR_BINARY`, and `TY_ECOSYSTEM_CONFIG_HOME` values. - The instruction to follow the `minimizing-ty-ecosystem-changes` skill using a unique temporary directory. -- The instruction not to rebuild ty, switch Ruff refs, overwrite shared artifacts, trust previous local reproductions, or substitute current dependency metadata. +- The instruction to preserve a verified reduction chain and underlying trigger, or return the original source explicitly marked as unminimized. +- Permission to build an exact-revision debug binary on demand for causal inspection, using an isolated worktree if necessary and retaining the profiling binaries as the behavioral oracle. +- The instruction not to rebuild profiling binaries, regenerate the supplied manifest, rewrite the installed configuration, switch shared Ruff refs, overwrite shared artifacts, trust previous local reproductions, or substitute current dependency metadata. ## Required Return Request: - Report-ready GitHub-flavored Markdown describing the exact base-versus-PR behavior and minimized code. -- Separate working notes covering reproduction, reductions, and the import audit. +- Separate working notes covering the original source permalink, reproduction, accepted reductions, both binaries' results, any necessary causal fingerprint, and the import audit. If a later entry has exactly the same behavior change and cause as an already minimized entry, the subagent may classify it as a duplicate instead of repeating the full minimization, but it must explain the match. diff --git a/.config/nextest.toml b/.config/nextest.toml index 5a621e58ee..bd055013dd 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -33,9 +33,15 @@ final-status-level = "slow" # well clear of the current cost rather than just above it. `type def` blocks are # the most expensive kind, because transpiling one evaluates its type functions, # and each evaluation spawns an interpreter. +# +# It had drifted back to "just above it": 279.6s on main and 284.0s after the +# 2026-08-13 upstream merge, against a 360s ceiling. Runner speed varies by more +# than the 21% of headroom that left, and the same commit timed out at 360s on two +# runs out of three while passing at 284s on the third. 10 minutes puts the cost +# back under half the ceiling and still catches a deadlock. [[profile.ci.overrides]] filter = 'test(clean_mdtest_blocks_run)' -slow-timeout = { period = "60s", terminate-after = 6 } +slow-timeout = { period = "60s", terminate-after = 10 } # External-dependency mdtests provision a real virtualenv with `uv` (a network # install of the framework plus its stubs) before type-checking, so on the fork's diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml index 2528699099..c6411637e1 100644 --- a/.github/actionlint.yaml +++ b/.github/actionlint.yaml @@ -5,12 +5,13 @@ self-hosted-runner: # Various runners we use that aren't recognized out-of-the-box by actionlint: labels: - depot-ubuntu-24.04-4 - - depot-ubuntu-latest-8 + - depot-ubuntu-24.04-8 - depot-ubuntu-22.04-16 - depot-ubuntu-22.04-32 - namespace-profile-macos-15 - - depot-windows-2022-16 + - namespace-profile-windows-2022-x86-64-16x32 - depot-ubuntu-22.04-arm-4 + - depot-ubuntu-24.04-arm-8 - github-windows-2025-x86_64-8 - github-windows-2025-x86_64-16 - codspeed-macro diff --git a/.github/pr-reviewer-pools.toml b/.github/pr-reviewer-pools.toml index 549f48855d..490b917c8b 100644 --- a/.github/pr-reviewer-pools.toml +++ b/.github/pr-reviewer-pools.toml @@ -9,7 +9,7 @@ reviewers = ["ntBre"] [[pools]] name = "ty-semantic" paths = ["/crates/ty_python_core/**", "/crates/ty_python_semantic/**"] -reviewers = ["carljm", "charliermarsh", "sharkdp", "dcreager", "dhruvmanila", "ibraheemdev"] +reviewers = ["carljm", "charliermarsh", "dcreager", "dhruvmanila", "ibraheemdev"] [[pools]] name = "ty-module-resolver" diff --git a/.github/ty-ecosystem.toml b/.github/ty-ecosystem.toml index 69c2b83665..7d3f469aa7 100644 --- a/.github/ty-ecosystem.toml +++ b/.github/ty-ecosystem.toml @@ -3,8 +3,12 @@ # Enable off-by-default rules. [rules] +blanket-ignore-comment = "warn" division-by-zero = "warn" +missing-type-argument = "warn" possibly-missing-attribute = "warn" possibly-missing-import = "warn" possibly-unresolved-reference = "warn" +unsound-return-statement = "warn" +unsound-yield = "warn" unsupported-dynamic-base = "warn" diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index 90aabdfa76..096c70648b 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -23,6 +23,7 @@ on: - pyproject.toml # And when we change this workflow itself... - .github/workflows/build-binaries.yml + - scripts/build_ruff_pgo.py concurrency: group: build-binaries-${{ github.ref }} @@ -41,13 +42,13 @@ env: jobs: sdist: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} - runs-on: ubuntu-latest + runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-24.04-4' || 'ubuntu-latest' }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: submodules: recursive persist-credentials: false - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ env.PYTHON_VERSION }} - name: "Prep README.md" @@ -68,11 +69,11 @@ jobs: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} runs-on: ${{ github.repository == 'astral-sh/ruff' && 'namespace-profile-macos-15' || 'macos-15' }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: submodules: recursive persist-credentials: false - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ env.PYTHON_VERSION }} architecture: x64 @@ -97,15 +98,27 @@ jobs: macos-aarch64: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} runs-on: ${{ github.repository == 'astral-sh/ruff' && 'namespace-profile-macos-15' || 'macos-15' }} + env: + # Use Rust's bundled Mach-O LLD, which supports ICF. + # ICF reduces the macOS aarch64 ruff binary size by ~0.8%. + RUSTFLAGS: "-C linker=rust-lld -C linker-flavor=ld64.lld -C link-arg=--icf=safe" steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: submodules: recursive persist-credentials: false - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ env.PYTHON_VERSION }} architecture: arm64 + - name: "Install LLVM profiling tools" + run: rustup component add llvm-tools-preview + - name: "Train PGO Ruff" + run: | + python scripts/build_ruff_pgo.py \ + --target aarch64-apple-darwin \ + --target-dir "${{ github.workspace }}/target/ruff-pgo" \ + --train-only - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi - name: "Build wheels - aarch64" @@ -115,9 +128,10 @@ jobs: target: aarch64 args: --release --locked --out dist --compatibility pypi env: - # Use Rust's bundled Mach-O LLD, which supports ICF. - # ICF reduces the macOS aarch64 ruff binary size by ~0.8%. - RUSTFLAGS: "-C linker=rust-lld -C linker-flavor=ld64.lld -C link-arg=--icf=safe" + RUSTFLAGS: "${{ env.RUSTFLAGS }} -Cprofile-use=${{ github.workspace }}/target/ruff-pgo/ruff.profdata" + # Apple Clang cannot consume profile data from rustc's LLVM version. + CFLAGS: "-fno-profile-generate -fno-profile-use" + CXXFLAGS: "-fno-profile-generate -fno-profile-use" - name: "Upload wheels" uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: @@ -126,7 +140,7 @@ jobs: windows: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} - runs-on: windows-latest + runs-on: ${{ github.repository == 'astral-sh/ruff' && 'namespace-profile-windows-2022-x86-64-16x32' || 'windows-latest' }} strategy: matrix: platform: @@ -137,14 +151,29 @@ jobs: - target: aarch64-pc-windows-msvc arch: x64 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: submodules: recursive persist-credentials: false - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ env.PYTHON_VERSION }} architecture: ${{ matrix.platform.arch }} + - name: "Install LLVM profiling tools" + if: ${{ matrix.platform.target == 'x86_64-pc-windows-msvc' }} + run: rustup component add llvm-tools-preview + - name: "Train PGO Ruff" + if: ${{ matrix.platform.target == 'x86_64-pc-windows-msvc' }} + shell: bash + run: | + export RUSTFLAGS="${RUSTFLAGS:+${RUSTFLAGS} }-C target-feature=+crt-static" + + python scripts/build_ruff_pgo.py \ + --target "${{ matrix.platform.target }}" \ + --target-dir "${{ github.workspace }}/target/ruff-pgo" \ + --train-only + + echo "RUSTFLAGS=${RUSTFLAGS:+${RUSTFLAGS} }-Cprofile-use=${{ github.workspace }}/target/ruff-pgo/ruff.profdata" >> "$GITHUB_ENV" - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi - name: "Build wheels" @@ -156,6 +185,20 @@ jobs: env: # aarch64 build fails, see https://github.com/PyO3/maturin/issues/2110 XWIN_VERSION: 16 + - name: "Verify static Windows runtime" + if: ${{ matrix.platform.target == 'x86_64-pc-windows-msvc' }} + shell: bash + run: | + LLVM_READOBJ="$(rustc --print sysroot)/lib/rustlib/${{ matrix.platform.target }}/bin/llvm-readobj.exe" + IMPORTS_FILE="$RUNNER_TEMP/ruff-coff-imports.txt" + # the wheel is built from `crates/basedpython`, which is its own cargo workspace, so + # its binaries land beside that manifest rather than in the repository's `target` + "$LLVM_READOBJ" --coff-imports "crates/basedpython/target/${{ matrix.platform.target }}/release/buff.exe" > "$IMPORTS_FILE" + + if grep -Eiq 'vcruntime[[:digit:]_]*\.dll|api-ms-win-crt-' "$IMPORTS_FILE"; then + echo "Ruff must not dynamically link the Visual C++ runtime" >&2 + exit 1 + fi - name: "Upload wheels" uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: @@ -164,21 +207,33 @@ jobs: linux: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} - runs-on: ubuntu-latest + runs-on: ${{ github.repository != 'astral-sh/ruff' && 'ubuntu-latest' || (matrix.target == 'x86_64-unknown-linux-gnu' && 'depot-ubuntu-24.04-8' || 'depot-ubuntu-24.04-4') }} strategy: matrix: target: - x86_64-unknown-linux-gnu - i686-unknown-linux-gnu steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: submodules: recursive persist-credentials: false - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ env.PYTHON_VERSION }} architecture: x64 + - name: "Install LLVM profiling tools" + if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }} + run: rustup component add llvm-tools-preview + - name: "Train PGO Ruff" + if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }} + run: | + python scripts/build_ruff_pgo.py \ + --target "${{ matrix.target }}" \ + --target-dir "${{ github.workspace }}/target/ruff-pgo" \ + --train-only + + echo "RUSTFLAGS=${RUSTFLAGS:+${RUSTFLAGS} }-Cprofile-use=${{ github.workspace }}/target/ruff-pgo/ruff.profdata" >> "$GITHUB_ENV" - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi - name: "Build wheels" @@ -194,18 +249,83 @@ jobs: name: wheels-${{ matrix.target }} path: dist + # `linux-aarch64` is new from upstream and is the one job with no runner the fork can + # reach: it asks for depot's ARM machines, which basedpython does not have, so it sits + # queued forever rather than failing. Disabled until there is an ARM runner to give it. + # It also carries the binary-archive steps this workflow's header says the fork drops. + # linux-aarch64: + # if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} + # runs-on: depot-ubuntu-24.04-arm-8 + # env: + # # see https://github.com/astral-sh/ruff/issues/3791 + # # and https://github.com/gnzlbg/jemallocator/issues/170#issuecomment-1503228963 + # JEMALLOC_SYS_WITH_LG_PAGE: "16" + # steps: + # - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + # with: + # submodules: recursive + # persist-credentials: false + # - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + # with: + # python-version: ${{ env.PYTHON_VERSION }} + # - name: "Install LLVM profiling tools" + # run: rustup component add llvm-tools-preview + # - name: "Train PGO Ruff" + # run: | + # python scripts/build_ruff_pgo.py \ + # --target aarch64-unknown-linux-gnu \ + # --target-dir "${{ github.workspace }}/target/ruff-pgo" \ + # --train-only + # + # echo "RUSTFLAGS=${RUSTFLAGS:+${RUSTFLAGS} }-Cprofile-use=${{ github.workspace }}/target/ruff-pgo/ruff.profdata" >> "$GITHUB_ENV" + # - name: "Prep README.md" + # run: python scripts/transform_readme.py --target pypi + # - name: "Build wheels" + # uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 + # with: + # maturin-version: v1.14.1 + # target: aarch64-unknown-linux-gnu + # manylinux: 2_17 + # docker-options: -e JEMALLOC_SYS_WITH_LG_PAGE=16 + # args: --release --locked --out dist --compatibility pypi + # - name: "Test wheel" + # run: | + # pip install dist/"${PACKAGE_NAME}"-*.whl --force-reinstall + # "${MODULE_NAME}" --help + # python -m "${MODULE_NAME}" --help + # - name: "Upload wheels" + # uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + # with: + # name: wheels-aarch64-unknown-linux-gnu + # path: dist + # - name: "Archive binary" + # shell: bash + # run: | + # set -euo pipefail + # + # TARGET=aarch64-unknown-linux-gnu + # ARCHIVE_NAME=ruff-$TARGET + # ARCHIVE_FILE=$ARCHIVE_NAME.tar.gz + # + # mkdir -p $ARCHIVE_NAME + # # `crates/basedpython` is its own cargo workspace, so the wheel's binaries are there + # cp crates/basedpython/target/$TARGET/release/buff $ARCHIVE_NAME/buff + # tar czvf $ARCHIVE_FILE $ARCHIVE_NAME + # shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 + # - name: "Upload binary" + # uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + # with: + # name: artifacts-aarch64-unknown-linux-gnu + # path: | + # *.tar.gz + # *.sha256 + linux-cross: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} - runs-on: ubuntu-latest + runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-24.04-4' || 'ubuntu-latest' }} strategy: matrix: platform: - - target: aarch64-unknown-linux-gnu - arch: aarch64 - manylinux: 2_17 - # see https://github.com/astral-sh/ruff/issues/3791 - # and https://github.com/gnzlbg/jemallocator/issues/170#issuecomment-1503228963 - maturin_docker_options: -e JEMALLOC_SYS_WITH_LG_PAGE=16 - target: armv7-unknown-linux-gnueabihf arch: armv7 manylinux: 2_17 @@ -227,11 +347,11 @@ jobs: manylinux: 2_31 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: submodules: recursive persist-credentials: false - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ env.PYTHON_VERSION }} - name: "Prep README.md" @@ -252,18 +372,18 @@ jobs: musllinux: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} - runs-on: ubuntu-latest + runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-24.04-4' || 'ubuntu-latest' }} strategy: matrix: target: - x86_64-unknown-linux-musl - i686-unknown-linux-musl steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: submodules: recursive persist-credentials: false - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ env.PYTHON_VERSION }} architecture: x64 @@ -284,7 +404,7 @@ jobs: musllinux-cross: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} - runs-on: ubuntu-latest + runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-24.04-4' || 'ubuntu-latest' }} strategy: matrix: platform: @@ -295,11 +415,11 @@ jobs: arch: armv7 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: submodules: recursive persist-credentials: false - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ env.PYTHON_VERSION }} - name: "Prep README.md" diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 899cda4983..b0b12d9d1e 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -47,14 +47,14 @@ jobs: - linux/amd64 - linux/arm64 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: submodules: recursive persist-credentials: false - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 + - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }} with: registry: ghcr.io @@ -150,7 +150,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@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 + - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -212,7 +212,7 @@ jobs: steps: - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 + - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -330,7 +330,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@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 + - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ github.repository_owner }} diff --git a/.github/workflows/build-wasm.yml b/.github/workflows/build-wasm.yml index e615b31d64..982e42aa56 100644 --- a/.github/workflows/build-wasm.yml +++ b/.github/workflows/build-wasm.yml @@ -35,7 +35,7 @@ jobs: target: [web, bundler, nodejs] fail-fast: false steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: "Install Rust toolchain" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d09426b030..070026938e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -50,7 +50,7 @@ jobs: # Flag that is set to "true" when code related to the benchmarks changes. benchmarks: ${{ steps.check_benchmarks.outputs.changed }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false @@ -246,7 +246,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: "Install Rust toolchain" @@ -258,10 +258,10 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version: "1.26.5" - name: "Run ShellCheck" @@ -284,10 +284,10 @@ jobs: if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }} timeout-minutes: 20 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - name: "Install Rust toolchain" @@ -318,10 +318,10 @@ jobs: env: CARGO_PROFILE_DEV_DEBUG: line-tables-only steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: shared-key: ruff-linux-debug save-if: false @@ -354,7 +354,15 @@ jobs: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} # the fork's `ubuntu-latest` runner is far slower than astral's 16-core depot # runner, so the full suite overruns the depot-tuned 20-minute budget - timeout-minutes: 45 + # + # 45 was in turn too tight: the job was already taking 34m32s on main, and runner speed + # varies by more than the quarter of an hour that left. On 2026-08-13 the same commit range + # ran its tests in 1603s, 1645s and then 1942s — the slow one reporting 1455 tests over the + # one-second mark against 1263 for the fast one, which is a slower machine rather than more + # work — and the 1942s run reached 45m mid-way through the dogfood steps and was killed. + # Nextest's own total is the number to compare here: it stays flat across the merge, so the + # budget is for the runner's variance, not for the suite growing. + timeout-minutes: 75 env: # Line-tables-only debug info: faster builds, backtraces still work. CARGO_PROFILE_DEV_DEBUG: line-tables-only @@ -366,10 +374,10 @@ jobs: # windows runner's slow file I/O). MDTEST_EXTERNAL: "1" steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: shared-key: ruff-linux-debug save-if: ${{ github.ref == 'refs/heads/main' }} @@ -378,7 +386,7 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - name: "Install cargo nextest and insta" - uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 + uses: taiki-e/install-action@cb33e69fad06166ca28a42b2575e4dadabf62ee8 # v2.85.8 with: tool: | cargo-nextest @@ -386,7 +394,7 @@ jobs: - name: "Install uv" uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" + version: "0.12.3" enable-cache: "true" - name: ty mdtests (GitHub annotations) if: ${{ needs.determine_changes.outputs.ty == 'true' }} @@ -447,10 +455,10 @@ jobs: # Line-tables-only debug info: faster builds, backtraces still work. CARGO_PROFILE_PROFILING_DEBUG: line-tables-only steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - name: "Install Rust toolchain" @@ -458,13 +466,13 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - name: "Install cargo nextest" - uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 + uses: taiki-e/install-action@cb33e69fad06166ca28a42b2575e4dadabf62ee8 # v2.85.8 with: tool: cargo-nextest - name: "Install uv" uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" + version: "0.12.3" enable-cache: "true" - name: "Run tests" run: cargo nextest run --cargo-profile profiling --all-features @@ -475,7 +483,7 @@ jobs: strategy: matrix: platform: - - ${{ github.repository == 'astral-sh/ruff' && 'depot-windows-2022-16' || 'windows-latest' }} + - ${{ github.repository == 'astral-sh/ruff' && 'namespace-profile-windows-2022-x86-64-16x32' || 'windows-latest' }} - ${{ github.repository == 'astral-sh/ruff' && 'namespace-profile-macos-15' || 'macos-latest' }} name: "cargo test (${{ matrix.platform }})" runs-on: ${{ matrix.platform }} @@ -483,15 +491,19 @@ jobs: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} # the fork's `windows-latest`/`macos-latest` runners are slower than astral's # depot runners, so the full suite overruns the depot-tuned 20-minute budget - timeout-minutes: 45 + # + # windows is the slow half of this matrix and came within two minutes of the previous + # 45-minute budget on 2026-08-13 (42m54s, against 24m56s for macos on the same run), so it + # gets the same headroom the linux job needed for the same reason + timeout-minutes: 75 env: # Line-tables-only debug info: faster builds, backtraces still work. CARGO_PROFILE_DEV_DEBUG: line-tables-only steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} # Fix for https://github.com/Swatinem/rust-cache/issues/341 @@ -499,13 +511,13 @@ jobs: - name: "Install Rust toolchain" run: rustup show - name: "Install cargo nextest" - uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 + uses: taiki-e/install-action@cb33e69fad06166ca28a42b2575e4dadabf62ee8 # v2.85.8 with: tool: cargo-nextest - name: "Install uv" uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" + version: "0.12.3" enable-cache: "true" - name: "Run tests" run: | @@ -519,10 +531,10 @@ jobs: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} timeout-minutes: 10 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - name: "Install Rust toolchain" @@ -546,7 +558,7 @@ jobs: cargo-build-msrv: name: "cargo build (msrv)" - runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-latest-8' || 'ubuntu-latest' }} + runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-24.04-8' || 'ubuntu-latest' }} 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 @@ -554,7 +566,7 @@ jobs: # Line-tables-only debug info: faster builds, backtraces still work. CARGO_PROFILE_DEV_DEBUG: line-tables-only steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: SebRollen/toml-action@b1b3628f55fc3a28208d4203ada8b737e9687876 # v1.2.0 @@ -562,7 +574,7 @@ jobs: with: file: "Cargo.toml" field: "workspace.package.rust-version" - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - name: "Install Rust toolchain" @@ -611,13 +623,13 @@ jobs: env: FORCE_COLOR: 1 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + version: "0.12.3" + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: shared-key: ruff-linux-debug save-if: false @@ -653,15 +665,15 @@ jobs: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} timeout-minutes: 5 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" + version: "0.12.3" - name: "Install Rust toolchain" run: rustup component add rustfmt # Run all code generation scripts, and verify that the current output is @@ -689,7 +701,7 @@ jobs: ecosystem: name: "ecosystem" - runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-latest-8' || 'ubuntu-latest' }} + runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-24.04-8' || 'ubuntu-latest' }} needs: determine_changes # Only runs on pull requests, since that is the only we way we can find the base version for comparison. # Ecosystem check needs linter and/or formatter changes. @@ -706,7 +718,7 @@ jobs: # Line-tables-only debug info: faster builds, backtraces still work. CARGO_PROFILE_DEV_DEBUG: line-tables-only steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ github.event.pull_request.base.ref }} persist-credentials: false @@ -715,7 +727,7 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} activate-environment: true - version: "0.11.31" + version: "0.12.3" - name: "Install Rust toolchain" run: rustup show @@ -723,7 +735,7 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: shared-key: ruff-linux-debug save-if: false @@ -734,7 +746,7 @@ jobs: cargo build --bin buff mv target/debug/buff target/debug/buff-baseline - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false clean: false @@ -865,7 +877,7 @@ jobs: # full budget rather than cancelling a healthy run timeout-minutes: ${{ github.repository == 'astral-sh/ruff' && 10 || 360 }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false # Faster to do this separately than to use `fetch-depth: 0` with `actions/checkout` @@ -873,8 +885,8 @@ jobs: run: git fetch --no-tags --filter=blob:none --unshallow origin - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + version: "0.12.3" + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - name: "Install Rust toolchain" @@ -921,10 +933,10 @@ jobs: needs: determine_changes if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: cargo-bins/cargo-binstall@ead08b90bd7b2e6d81963fb9cf0b7239f66d5db4 # v1.21.0 + - uses: cargo-bins/cargo-binstall@e00d2c94cc0067b77737821097a62d91c0301baa # v1.21.1 - run: cargo binstall --no-confirm cargo-shear@1.12.4 - run: cargo shear --deny-warnings @@ -937,13 +949,13 @@ jobs: # Line-tables-only debug info: faster builds, backtraces still work. CARGO_PROFILE_PROFILING_DEBUG: line-tables-only steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + version: "0.12.3" + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - name: "Install Rust toolchain" @@ -961,14 +973,14 @@ jobs: timeout-minutes: 20 if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ env.PYTHON_VERSION }} architecture: x64 - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - name: "Prep README.md" @@ -990,12 +1002,12 @@ jobs: runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }} timeout-minutes: 10 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" + version: "0.12.3" - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 24 @@ -1020,10 +1032,10 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - name: "Install Rust toolchain" @@ -1033,7 +1045,7 @@ jobs: with: python-version: 3.13 activate-environment: true - version: "0.11.31" + version: "0.12.3" - name: "Install dependencies" run: uv pip install -r docs/requirements.txt - name: "Update README File" @@ -1071,10 +1083,10 @@ jobs: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.formatter == 'true' || github.ref == 'refs/heads/main') }} timeout-minutes: 10 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - name: "Install Rust toolchain" @@ -1096,12 +1108,12 @@ jobs: # # Line-tables-only debug info: faster builds, backtraces still work. # CARGO_PROFILE_DEV_DEBUG: line-tables-only # steps: - # - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + # - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # name: "Checkout ruff source" # with: # persist-credentials: false # - # - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + # - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 # with: # shared-key: ruff-linux-debug # save-if: false @@ -1115,19 +1127,19 @@ jobs: # - name: Build Ruff binary # run: cargo build -p ruff --bin ruff # - # - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + # - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # name: "Checkout ruff-lsp source" # with: # persist-credentials: false # repository: "astral-sh/ruff-lsp" # path: ruff-lsp # - # - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + # - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 # with: # # installation fails on 3.13 and newer # python-version: "3.12" # - # - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + # - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 # # - name: Install ruff-lsp dependencies # run: | @@ -1154,12 +1166,12 @@ jobs: - determine_changes if: ${{ (needs.determine_changes.outputs.playground == 'true') }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: "Install Rust toolchain" run: rustup target add wasm32-unknown-unknown - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 @@ -1208,22 +1220,22 @@ jobs: id-token: write # required for OIDC authentication with CodSpeed steps: - name: "Checkout Branch" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" + version: "0.12.3" - name: "Install Rust toolchain" run: rustup show - name: "Install codspeed" - uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 + uses: taiki-e/install-action@cb33e69fad06166ca28a42b2575e4dadabf62ee8 # v2.85.8 with: tool: cargo-codspeed @@ -1231,7 +1243,7 @@ jobs: run: cargo codspeed build -m simulation -m memory --features "codspeed,ruff_instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench formatter --bench lexer --bench linter --bench parser - name: "Run benchmarks" - uses: CodSpeedHQ/action@f99becdce5e5d51fd556489ebef684f4ecfd6286 # v4.18.5 + uses: CodSpeedHQ/action@0ca9cbbf4623b599a6c3ed4fc8a922942705d9f1 # v5.0.2 with: mode: "simulation,memory" run: cargo codspeed run @@ -1253,11 +1265,11 @@ jobs: timeout-minutes: 20 steps: - name: "Checkout Branch" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} @@ -1265,12 +1277,12 @@ jobs: run: rustup show - name: "Install codspeed" - uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 + uses: taiki-e/install-action@cb33e69fad06166ca28a42b2575e4dadabf62ee8 # v2.85.8 with: tool: cargo-codspeed - name: "Build benchmarks" - run: cargo codspeed build -m simulation -m memory --features "codspeed,module_resolution,ty_instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench module_resolution --bench ty + run: cargo codspeed build -m simulation -m memory --features "codspeed,module_resolution,ty_instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench module_resolution --bench ty --bench ty_constraint_set - name: "Upload benchmark binary" uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -1301,6 +1313,14 @@ jobs: target: ty filter: micro mode: memory + - name: constraint_set + target: ty_constraint_set + filter: micro + mode: simulation + - name: constraint_set + target: ty_constraint_set + filter: micro + mode: memory - name: projects target: ty filter: "check_file|anyio|attrs|hydra|datetype" @@ -1315,15 +1335,15 @@ jobs: mode: simulation steps: - name: "Checkout Branch" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" + version: "0.12.3" - name: "Install codspeed" - uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 + uses: taiki-e/install-action@cb33e69fad06166ca28a42b2575e4dadabf62ee8 # v2.85.8 with: tool: cargo-codspeed @@ -1338,7 +1358,7 @@ jobs: run: find target/codspeed -type f -exec chmod +x {} + - name: "Run benchmarks" - uses: CodSpeedHQ/action@f99becdce5e5d51fd556489ebef684f4ecfd6286 # v4.18.5 + uses: CodSpeedHQ/action@0ca9cbbf4623b599a6c3ed4fc8a922942705d9f1 # v5.0.2 with: mode: ${{ matrix.mode }} run: cargo codspeed run --bench "${{ matrix.target }}" "${{ matrix.filter }}" @@ -1364,22 +1384,22 @@ jobs: timeout-minutes: 20 steps: - name: "Checkout Branch" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" + version: "0.12.3" - name: "Install Rust toolchain" run: rustup show - name: "Install codspeed" - uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 + uses: taiki-e/install-action@cb33e69fad06166ca28a42b2575e4dadabf62ee8 # v2.85.8 with: tool: cargo-codspeed @@ -1420,16 +1440,16 @@ jobs: filter: "pydantic|freqtrade|multithreaded|altair" steps: - name: "Checkout Branch" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" + version: "0.12.3" - name: "Install codspeed" - uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 + uses: taiki-e/install-action@cb33e69fad06166ca28a42b2575e4dadabf62ee8 # v2.85.8 with: tool: cargo-codspeed @@ -1444,7 +1464,7 @@ jobs: run: find target/codspeed -type f -exec chmod +x {} + - name: "Run benchmarks" - uses: CodSpeedHQ/action@f99becdce5e5d51fd556489ebef684f4ecfd6286 # v4.18.5 + uses: CodSpeedHQ/action@0ca9cbbf4623b599a6c3ed4fc8a922942705d9f1 # v5.0.2 env: # enabling walltime flamegraphs adds ~6 minutes to the CI time, and they don't # appear to provide much useful insight for our walltime benchmarks right now diff --git a/.github/workflows/daily_fuzz.yaml b/.github/workflows/daily_fuzz.yaml index c7c67c1cfd..ef3a51df3d 100644 --- a/.github/workflows/daily_fuzz.yaml +++ b/.github/workflows/daily_fuzz.yaml @@ -33,17 +33,17 @@ jobs: # Don't run the cron job on forks: if: ${{ github.repository == 'astral-sh/ruff' || github.event_name != 'schedule' }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" + version: "0.12.3" - name: "Install Rust toolchain" run: rustup show - name: "Install mold" uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - 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/memory_report.yaml b/.github/workflows/memory_report.yaml index ff0dedeeff..bc17a67c2c 100644 --- a/.github/workflows/memory_report.yaml +++ b/.github/workflows/memory_report.yaml @@ -44,7 +44,7 @@ jobs: runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-32' || 'ubuntu-latest' }} timeout-minutes: 20 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: path: ruff persist-credentials: false @@ -53,14 +53,19 @@ jobs: - name: Fetch full history without tags run: git -C ruff fetch --no-tags --filter=blob:none --unshallow origin - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: workspaces: "ruff" - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ env.PYTHON_VERSION }} + - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + version: "0.12.3" + enable-cache: true + - name: Install Rust toolchain run: rustup show diff --git a/.github/workflows/publish-crates.yml b/.github/workflows/publish-crates.yml index b2e086d6ba..d16b76b5af 100644 --- a/.github/workflows/publish-crates.yml +++ b/.github/workflows/publish-crates.yml @@ -21,7 +21,7 @@ jobs: contents: read id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5 diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index 2094b6ec8a..e773a08a44 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -26,12 +26,12 @@ jobs: name: release runs-on: ubuntu-latest steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ inputs.ref }} persist-credentials: true - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: 3.12 @@ -65,7 +65,7 @@ jobs: - name: "Install Rust toolchain" run: rustup show - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - name: "Install dependencies" run: pip install -r docs/requirements.txt diff --git a/.github/workflows/publish-playground.yml b/.github/workflows/publish-playground.yml index 32b0f8f1cb..4f407c0618 100644 --- a/.github/workflows/publish-playground.yml +++ b/.github/workflows/publish-playground.yml @@ -27,7 +27,7 @@ jobs: env: CF_API_TOKEN_EXISTS: ${{ secrets.CF_API_TOKEN != '' }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: "Install Rust toolchain" diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 4501473458..438473568e 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -24,7 +24,7 @@ jobs: - name: "Install uv" uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" + version: "0.12.3" - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: wheels-* diff --git a/.github/workflows/publish-ty-playground.yml b/.github/workflows/publish-ty-playground.yml index e9ab1ff682..0dee4e2491 100644 --- a/.github/workflows/publish-ty-playground.yml +++ b/.github/workflows/publish-ty-playground.yml @@ -31,7 +31,7 @@ jobs: env: CF_API_TOKEN_EXISTS: ${{ secrets.CF_API_TOKEN != '' }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: "Install Rust toolchain" diff --git a/.github/workflows/publish-versions.yml b/.github/workflows/publish-versions.yml index 52e7ed28ec..fadeb2df69 100644 --- a/.github/workflows/publish-versions.yml +++ b/.github/workflows/publish-versions.yml @@ -20,7 +20,7 @@ jobs: env: VERSION: ${{ fromJson(inputs.plan).announcement_tag }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0a572c1534..760247636b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -117,7 +117,7 @@ jobs: needs: - plan if: ${{ needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload' || inputs.tag == 'dry-run' }} - uses: ./.github/workflows/build-binaries.yml + uses: $/.github/workflows/build-binaries.yml with: plan: ${{ needs.plan.outputs.val }} secrets: inherit @@ -127,7 +127,7 @@ jobs: - plan - release-gate if: ${{ always() && needs.plan.result == 'success' && (needs.release-gate.result == 'success' || needs.release-gate.result == 'skipped') && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload' || inputs.tag == 'dry-run') }} - uses: ./.github/workflows/build-docker.yml + uses: $/.github/workflows/build-docker.yml with: plan: ${{ needs.plan.outputs.val }} secrets: inherit @@ -141,7 +141,7 @@ jobs: needs: - plan if: ${{ needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload' || inputs.tag == 'dry-run' }} - uses: ./.github/workflows/build-wasm.yml + uses: $/.github/workflows/build-wasm.yml with: plan: ${{ needs.plan.outputs.val }} secrets: inherit @@ -266,7 +266,7 @@ jobs: - host - release-gate if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }} - uses: ./.github/workflows/publish-wasm.yml + uses: $/.github/workflows/publish-wasm.yml with: plan: ${{ needs.plan.outputs.val }} secrets: inherit @@ -284,7 +284,7 @@ jobs: - custom-publish-pypi # DIRTY: see #16989 - custom-publish-wasm # DIRTY: see #16989 if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }} - uses: ./.github/workflows/publish-crates.yml + uses: $/.github/workflows/publish-crates.yml with: plan: ${{ needs.plan.outputs.val }} secrets: inherit @@ -355,7 +355,7 @@ jobs: needs: - plan - announce - uses: ./.github/workflows/notify-dependents.yml + uses: $/.github/workflows/notify-dependents.yml with: plan: ${{ needs.plan.outputs.val }} secrets: inherit @@ -364,7 +364,7 @@ jobs: needs: - plan - announce - uses: ./.github/workflows/publish-docs.yml + uses: $/.github/workflows/publish-docs.yml with: plan: ${{ needs.plan.outputs.val }} secrets: inherit @@ -373,7 +373,7 @@ jobs: needs: - plan - announce - uses: ./.github/workflows/publish-playground.yml + uses: $/.github/workflows/publish-playground.yml with: plan: ${{ needs.plan.outputs.val }} secrets: inherit @@ -382,7 +382,7 @@ jobs: needs: - plan - announce - uses: ./.github/workflows/publish-versions.yml + uses: $/.github/workflows/publish-versions.yml with: plan: ${{ needs.plan.outputs.val }} secrets: inherit @@ -391,7 +391,7 @@ jobs: needs: - plan - announce - uses: ./.github/workflows/publish-mirror.yml + uses: $/.github/workflows/publish-mirror.yml with: plan: ${{ needs.plan.outputs.val }} secrets: inherit diff --git a/.github/workflows/sync_typeshed.yaml b/.github/workflows/sync_typeshed.yaml index 851d117002..6919d9f143 100644 --- a/.github/workflows/sync_typeshed.yaml +++ b/.github/workflows/sync_typeshed.yaml @@ -69,12 +69,12 @@ jobs: permissions: contents: write # to push back to the repository steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 name: Checkout Ruff with: path: ruff persist-credentials: true - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 name: Checkout typeshed with: repository: python/typeshed @@ -86,7 +86,7 @@ jobs: git config --global user.email '<>' - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" + version: "0.12.3" - name: Sync typeshed stubs run: | rm -rf "ruff/${VENDORED_TYPESHED}" @@ -135,14 +135,14 @@ jobs: permissions: contents: write # to push back to the repository steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 name: Checkout Ruff with: persist-credentials: true ref: ${{ env.UPSTREAM_BRANCH}} - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" + version: "0.12.3" - name: Setup git run: | git config --global user.name typeshedbot @@ -177,14 +177,14 @@ jobs: permissions: contents: write # to push back to the repository steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 name: Checkout Ruff with: persist-credentials: true ref: ${{ env.UPSTREAM_BRANCH}} - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" + version: "0.12.3" - name: Setup git run: | git config --global user.name typeshedbot @@ -248,7 +248,7 @@ jobs: permissions: contents: write # to push back to the repository steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 name: Checkout Ruff with: persist-credentials: true @@ -257,7 +257,7 @@ jobs: run: | git config --global user.name typeshedbot git config --global user.email '<>' - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: # Reuse the cache populated by the `cargo test (linux)` CI job on `main`. shared-key: ruff-linux-debug @@ -268,7 +268,7 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - name: "Install cargo nextest and insta" - uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 + uses: taiki-e/install-action@cb33e69fad06166ca28a42b2575e4dadabf62ee8 # v2.85.8 with: tool: | cargo-nextest diff --git a/.github/workflows/ty-ecosystem-analyzer.yaml b/.github/workflows/ty-ecosystem-analyzer.yaml index b5d8a56c22..452dfc62c5 100644 --- a/.github/workflows/ty-ecosystem-analyzer.yaml +++ b/.github/workflows/ty-ecosystem-analyzer.yaml @@ -42,7 +42,9 @@ env: RUST_BACKTRACE: 1 # Line-tables-only debug info: faster builds, backtraces still work. CARGO_PROFILE_PROFILING_DEBUG: line-tables-only - ECOSYSTEM_ANALYZER_COMMIT: 263b5500881186e8c918193577c23b341e5b7237 + # TODO: Update the mypy-primer revision in scripts/setup_primer_project.py + # and regenerate its lockfile when updating ecosystem-analyzer. + ECOSYSTEM_ANALYZER_COMMIT: 27b644f296d70fccacb7d7c23c91c5d6ccd8713d jobs: build-ty: @@ -54,12 +56,12 @@ jobs: # max (6h): the fork's slow `ubuntu-latest` runner overruns the depot-tuned budget timeout-minutes: 360 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false ref: ${{ github.sha }} - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: lookup-only: false @@ -137,7 +139,7 @@ jobs: - name: Install the latest version of uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" + version: "0.12.3" ignore-empty-workdir: true # The default of `enable-cache: auto` leads to warnings from setup-uv in this job, # since its cache-invalidation keys (pyproject.toml, uv.lock, etc.) aren't available @@ -198,7 +200,7 @@ jobs: - name: Install the latest version of uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" + version: "0.12.3" ignore-empty-workdir: true # The default of `enable-cache: auto` leads to warnings from setup-uv in this job, # since its cache-invalidation keys (pyproject.toml, uv.lock, etc.) aren't available diff --git a/.github/workflows/ty-ecosystem-report.yaml b/.github/workflows/ty-ecosystem-report.yaml index e06ae6da2e..9bcfc24f1f 100644 --- a/.github/workflows/ty-ecosystem-report.yaml +++ b/.github/workflows/ty-ecosystem-report.yaml @@ -20,7 +20,7 @@ env: RUST_BACKTRACE: 1 # Line-tables-only debug info: faster builds, backtraces still work. CARGO_PROFILE_PROFILING_DEBUG: line-tables-only - ECOSYSTEM_ANALYZER_COMMIT: e2c5b76149b147fae104a7d8fa0997a9eb7f7754 + ECOSYSTEM_ANALYZER_COMMIT: 27b644f296d70fccacb7d7c23c91c5d6ccd8713d jobs: ty-ecosystem-report: @@ -28,7 +28,7 @@ jobs: runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-32' || 'ubuntu-latest' }} timeout-minutes: 40 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false @@ -36,9 +36,9 @@ jobs: uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: enable-cache: true - version: "0.11.31" + version: "0.12.3" - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: lookup-only: false diff --git a/.github/workflows/typing_conformance.yaml b/.github/workflows/typing_conformance.yaml index c284e4d015..1db9694cba 100644 --- a/.github/workflows/typing_conformance.yaml +++ b/.github/workflows/typing_conformance.yaml @@ -45,7 +45,7 @@ jobs: runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-32' || 'ubuntu-latest' }} timeout-minutes: 10 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: path: ruff persist-credentials: false @@ -54,18 +54,18 @@ jobs: - name: Fetch full history without tags run: git -C ruff fetch --no-tags --filter=blob:none --unshallow origin - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: python/typing ref: ${{ env.CONFORMANCE_SUITE_COMMIT }} path: typing persist-credentials: false - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: workspaces: "ruff" - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ env.PYTHON_VERSION }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ffd7824c72..b6ffa14ade 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,7 +41,7 @@ repos: priority: 0 - repo: https://github.com/crate-ci/typos - rev: bee27e3a4fd1ea2111cf90ab89cd076c870fce14 # frozen: v1.48.0 + rev: 8a48f81b6c64dcfea44b3633223084c4be58ac5f # frozen: v1.49.0 hooks: - id: typos priority: 0 @@ -77,7 +77,7 @@ repos: priority: 0 # Prettier - repo: https://github.com/rbubley/mirrors-prettier - rev: 9337a74165b178ae2c766f60bee7252a0f06f3e8 # frozen: v3.9.5 + rev: 0ee178619d696787ca73d210cc191d720868c631 # frozen: v3.9.6 hooks: - id: prettier types: [yaml] @@ -86,7 +86,7 @@ repos: # zizmor detects security vulnerabilities in GitHub Actions workflows. # Additional configuration for the tool is found in `.github/zizmor.yml` - repo: https://github.com/zizmorcore/zizmor-pre-commit - rev: 64a97fb7fa63188393d3215c6e312f5f9c6d0f78 # frozen: v1.27.0 + rev: 451b56af716f9f0d0c2b816503a3fd0cf8b036fa # frozen: v1.29.0 hooks: - id: zizmor priority: 0 @@ -125,7 +125,7 @@ repos: - id: mdformat language: python # means renovate will also update `additional_dependencies` additional_dependencies: - - mdformat-mkdocs==5.2.1 + - mdformat-mkdocs==5.3.0 - mdformat-footnote==0.1.3 exclude: | (?x)^( @@ -136,13 +136,13 @@ repos: priority: 0 - repo: https://github.com/astral-sh/uv-pre-commit - rev: 69e5d7b46d7a93b633431a498a46cf3a8a2181f4 # frozen: 0.11.31 + rev: 8d582f54b8e4cc5a61a85eeed1f54bbdb1e294fc # frozen: 0.12.3 hooks: - id: uv-lock priority: 0 - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 2700fd5671c633760d912769c041bfcde2b9a01b # frozen: v0.15.22 + rev: 7c55798a78262d14b2074abf623d8a992ebb70d4 # frozen: v0.16.2 hooks: - id: ruff-format exclude: crates/ty_python_semantic/resources/corpus/ @@ -150,7 +150,7 @@ repos: # Priority 1: Second-pass fixers (e.g., markdownlint-fix runs after mdformat). - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 2700fd5671c633760d912769c041bfcde2b9a01b # frozen: v0.15.22 + rev: 7c55798a78262d14b2074abf623d8a992ebb70d4 # frozen: v0.16.2 hooks: - id: ruff-check args: [--fix, --exit-non-zero-on-fix] @@ -173,7 +173,7 @@ repos: # Priority 2: ruffen-docs runs after markdownlint-fix (both modify markdown). - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 2700fd5671c633760d912769c041bfcde2b9a01b # frozen: v0.15.22 + rev: 7c55798a78262d14b2074abf623d8a992ebb70d4 # frozen: v0.16.2 hooks: - id: ruff-format name: mdtest format diff --git a/AGENTS.md b/AGENTS.md index 4251a9bacf..6324173da2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ This repository contains both Ruff (a Python linter and formatter) and ty (a Python type checker). The crates follow a naming convention: `ruff_*` for Ruff-specific code and `ty_*` for ty-specific code. ty reuses several Ruff crates, including the Python parser (`ruff_python_parser`) and AST definitions (`ruff_python_ast`). -## Code reviews +## Code Review Rules When reviewing a branch or pull request, be deliberately nitpicky. Report not only bugs and regressions, but also architectural and maintenance risks, weak @@ -11,30 +11,35 @@ consistency issues. Order findings by severity, cite files and lines, and distinguish blockers from non-blocking improvements. Number each review point for easy reference in subsequent review discussion. +During code review, check the proposed changes against all applicable code, test, +documentation, and architectural conventions in this `AGENTS.md`. Report +meaningful violations introduced by the changes; do not apply agent-only workflow +instructions to PR authors or flag unrelated pre-existing issues. + ## Running Tests -Run all tests (using `nextest` for faster execution, setting `CARGO_PROFILE_DEV_OPT_LEVEL=1 CARGO_PROFILE_DEV_DEBUG="line-tables-only"` to enable optimizations while retaining some debug info, and setting `INSTA_FORCE_PASS=1 INSTA_UPDATE=always MDTEST_UPDATE_SNAPSHOTS=1` to ensure all snapshots are updated): +Run all tests (using `nextest` for faster execution and setting `INSTA_FORCE_PASS=1 INSTA_UPDATE=always MDTEST_UPDATE_SNAPSHOTS=1` to ensure all snapshots are updated): ```sh -CARGO_PROFILE_DEV_OPT_LEVEL=1 INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo nextest run +CARGO_PROFILE_DEV_OPT_LEVEL=1 CARGO_PROFILE_DEV_LTO=off INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo nextest run ``` Run tests for a specific crate: ```sh -CARGO_PROFILE_DEV_OPT_LEVEL=1 INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo nextest run -p ty_python_semantic +CARGO_PROFILE_DEV_OPT_LEVEL=1 CARGO_PROFILE_DEV_LTO=off INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo nextest run -p ty_python_semantic ``` -Run a single mdtest file. The path to the mdtest file should be relative to the `crates/ty_python_semantic/resources/mdtest` folder: +Run a single mdtest file. The path to the mdtest file should be relative to the `crates/ty_python_semantic/resources/mdtest` folder. Include `--test mdtest` to avoid building unrelated test binaries: ```sh -CARGO_PROFILE_DEV_OPT_LEVEL=1 INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo nextest run -p ty_python_semantic -- mdtest:: +CARGO_PROFILE_DEV_OPT_LEVEL=1 CARGO_PROFILE_DEV_LTO=off INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo nextest run -p ty_python_semantic --test mdtest -- mdtest:: ``` To run a specific mdtest within a file, use a substring of the Markdown header text as `MDTEST_TEST_FILTER`. Only use this if it's necessary to isolate a single test case: ```sh -MDTEST_TEST_FILTER="" CARGO_PROFILE_DEV_OPT_LEVEL=1 INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo nextest run -p ty_python_semantic -- mdtest:: +MDTEST_TEST_FILTER="" CARGO_PROFILE_DEV_OPT_LEVEL=1 CARGO_PROFILE_DEV_LTO=off INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo nextest run -p ty_python_semantic --test mdtest -- mdtest:: ``` ### Fallback without nextest @@ -43,16 +48,16 @@ If `cargo nextest` is not available, use `cargo test` with the same environment ```sh # Run all tests. -CARGO_PROFILE_DEV_OPT_LEVEL=1 INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo test +CARGO_PROFILE_DEV_OPT_LEVEL=1 CARGO_PROFILE_DEV_LTO=off INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo test # Run tests for a specific crate. -CARGO_PROFILE_DEV_OPT_LEVEL=1 INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo test -p ty_python_semantic +CARGO_PROFILE_DEV_OPT_LEVEL=1 CARGO_PROFILE_DEV_LTO=off INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo test -p ty_python_semantic # Run a single mdtest file. -CARGO_PROFILE_DEV_OPT_LEVEL=1 INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo test -p ty_python_semantic --test mdtest -- +CARGO_PROFILE_DEV_OPT_LEVEL=1 CARGO_PROFILE_DEV_LTO=off INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo test -p ty_python_semantic --test mdtest -- # Run a specific mdtest within a file. -MDTEST_TEST_FILTER="" CARGO_PROFILE_DEV_OPT_LEVEL=1 INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo test -p ty_python_semantic --test mdtest -- +MDTEST_TEST_FILTER="" CARGO_PROFILE_DEV_OPT_LEVEL=1 CARGO_PROFILE_DEV_LTO=off INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo test -p ty_python_semantic --test mdtest -- ``` ### Snapshot updates @@ -63,10 +68,19 @@ When running tests with `INSTA_FORCE_PASS=1`, check for `.pending-snap` files if Never edit snapshot files or inline snapshot bodies manually. Regenerate them by running the relevant tests with the snapshot-update environment variables documented above, then review the generated diff. +## Writing mdtests + +- Write mdtests as readable, literate specifications, and minimize the context a reader must hold in mind. Prefer short, focused code blocks, and define types, fixtures, and helpers close to the assertions that use them. Give independent scenarios separate sibling Markdown test headings at the same level; only introduce child headings if any existing code beneath their parent is first moved into child sections. When scenarios need shared setup, interleave short prose-and-code blocks under the same heading. Code blocks for the same file within a section are concatenated, so do not repeat imports or definitions. +- Prioritize document structure and readability over avoiding duplicated setup. Add a test to an existing section when its heading accurately describes the new scenario, adding or improving introductory prose as needed; otherwise, create a separate sibling section, even if that requires repeating a small fixture. +- Introduce each scenario with a short prose paragraph explaining the code immediately below. Use clear, precise terminology. Avoid using jargon where it's unnecessary, and avoid inventing new jargon if there's an existing term of art used in that file. Avoid long paragraphs covering multiple scenarios followed by a single long code block. +- Minimize regression examples to the behavior under test. When adapting real-world code or an issue reproducer, remove incidental types, methods, type parameters, imports, and domain-specific details. Preserve complexity only when necessary to reproduce the regression or distinguish the intended behavior, and reuse nearby fixtures or simple built-in types when doing so keeps the test easy to understand. +- Prefer a minimal, purpose-built custom type over a standard-library type when a regression depends on particular attributes, methods, bounds, or constraints. Define the relevant behavior in the test so readers do not need to look up the standard-library type to understand the scenario. For commonly used standard-library types, consider adding a separate regression using the real type to protect against changes in typeshed. +- Place each mdtest in a file for the behavior it actually tests, and assert that behavior directly. Prefer an existing file when one already covers that behavior; create a new file when no existing file is a good fit. Do not choose a file solely because its directive or helper can express the assertion. + ## Running Clippy ```sh -cargo clippy --workspace --all-targets --all-features -- -D warnings +CARGO_PROFILE_DEV_OPT_LEVEL=1 CARGO_PROFILE_DEV_LTO=off CARGO_PROFILE_DEV_DEBUG="line-tables-only" cargo clippy --workspace --all-targets --all-features -- -D warnings ``` ## Running Debug Builds @@ -76,13 +90,13 @@ Use debug builds (not `--release`) when developing, as release builds lack debug Run Ruff: ```sh -cargo run --bin ruff -- check path/to/file.py +CARGO_PROFILE_DEV_OPT_LEVEL=1 CARGO_PROFILE_DEV_LTO=off CARGO_PROFILE_DEV_DEBUG="line-tables-only" cargo run --bin ruff -- check path/to/file.py ``` Run ty: ```sh -cargo run --bin ty -- check path/to/file.py +CARGO_PROFILE_DEV_OPT_LEVEL=1 CARGO_PROFILE_DEV_LTO=off CARGO_PROFILE_DEV_DEBUG="line-tables-only" cargo run --bin ty -- check path/to/file.py ``` ## Working on ty @@ -97,6 +111,18 @@ When the task matches a more specific ty workflow, also read and follow that ski - Ecosystem report summaries: `.agents/skills/summarise-ecosystem-results/SKILL.md`. - Reproducing, investigating, or minimizing ecosystem or primer differences: `.agents/skills/minimizing-ty-ecosystem-changes/SKILL.md`. +### Completion ranking + +When changing ty autocomplete ranking, add or update evaluation fixtures under `crates/ty_completion_eval/truth/`. Extend an existing project when it is a good fit for the behavior being tested; otherwise, add a new one. Use `` directives to assert ranking, and include the expected module for auto-import completions. Add `completion.rs` unit tests only when the evaluation fixtures cannot adequately cover the behavior. + +Regenerate and review the committed evaluation results after changing ranking behavior or fixtures: + +```sh +CARGO_PROFILE_DEV_OPT_LEVEL=1 CARGO_PROFILE_DEV_LTO=off CARGO_PROFILE_DEV_DEBUG="line-tables-only" cargo run --package ty_completion_eval -- all --threshold 0.4 --tasks crates/ty_completion_eval/completion-evaluation-tasks.csv +``` + +To inspect one evaluation task, run `cargo run --package ty_completion_eval -- show-one --file-name --index `. + ### Ad hoc reproductions When running ty against a temporary Python reproduction file, create it outside the Ruff checkout (for example, under `/tmp`). A file inside the checkout discovers Ruff's root `pyproject.toml`, whose `requires-python = ">=3.7"` causes ty to infer Python 3.7 as the default Python version. @@ -136,19 +162,19 @@ Parts of `.github/workflows/release.yml` are generated by cargo-dist from `dist- ## Development Guidelines -- All changes must be tested. If you're not testing your changes, you're not done. +- All significant changes must be tested. Add or update focused tests for semantic changes when existing coverage does not already establish the intended behavior. - Look to see if your tests could go in an existing file before adding a new file for your tests. - Get your tests to pass. If you didn't run the tests, your code does not work. - Follow existing code style. Check neighboring files for patterns. - Prefer narrow visibility by default because this workspace is generally its own consumer. However, do not add workarounds solely to avoid `pub`: make an item public when another workspace crate needs it and that produces the cleaner implementation. - Rust imports should always go at the top of the file, never locally in functions. - Run `uv run --only-group dev --locked prek` at the end of a task if you changed files in the repo. This includes changes such as rebases or addressing review comments. Use `uv run --only-group dev --locked prek run --files ` and pass every file you changed. This keeps the hook run independent of staged state and avoids sweeping unrelated changes. Use `uv run --only-group dev --locked prek run --all-files` when a full-repository hook sweep is specifically needed. -- Avoid writing significant amounts of new code. This is often a sign that we're missing an existing method or mechanism that could help solve the problem. Look for existing utilities first. -- Try hard to avoid patterns that require `panic!`, `unreachable!`, or `.unwrap()`. Instead, try to encode those constraints in the type system. Don't be afraid to write code that's more verbose or requires largeish refactors if it enables you to avoid these unsafe calls. -- Prefer let chains (`if let` combined with `&&`) over nested `if let` statements to reduce indentation and improve readability. At the end of a task, always check your work to see if you missed opportunities to use `let` chains. -- If you *have* to suppress a Clippy lint, prefer to use `#[expect()]` over `[allow()]`, where possible. But if a lint is complaining about unused/dead code, it's usually best to just delete the unused code. -- Use comments purposefully. Don't use comments to narrate code, but do use them to explain invariants and why something unusual was done a particular way. -- Run `cargo dev generate-all` after changing configuration options, CLI arguments, lint rules, or environment variable definitions, as these changes require regeneration of schemas, docs, and CLI references. +- Before writing significant amounts of new code, look for existing utilities or mechanisms that could solve the problem. Avoid expanding the task to unrelated issues, but do not confuse keeping the task focused with minimizing the size of the implementation. Prefer addressing the underlying architectural problem over adding a localized workaround, even when doing so requires a substantial refactor or rearchitecture. Ask the user for guidance if in doubt about whether to attempt a larger refactor or not. +- Try hard to avoid patterns that require `panic!`, `unreachable!`, `.unwrap()` or `.expect()`. Instead, try to encode those constraints in the type system. Don't be afraid to write code that's more verbose or requires largeish refactors if it enables you to avoid these unsafe calls. +- Prefer let chains (`if let` combined with `&&`) and let guards (`PAT if let ... =>`) over nested `if let` statements to reduce indentation and improve readability. At the end of a task, always check your work to see if you missed opportunities to use `let` chains or `let` guards. +- If you _have_ to suppress a Clippy lint, prefer to use `#[expect()]` over `[allow()]`, where possible. But if a lint is complaining about unused/dead code, it's usually best to just delete the unused code. +- Don't use comments to narrate code, but do use them to explain invariants and why something unusual was done a particular way. Make sure that a comment will make sense to somebody who's reading the code for the first time. Prefer plain language, avoid jargon, and don't be afraid to be more verbose if it's necessary to explain something well. Giving examples of the kind of Python code we're trying to model at this particular point in Ruff or ty can often be very helpful for future readers of the code. +- Run `CARGO_PROFILE_DEV_OPT_LEVEL=1 CARGO_PROFILE_DEV_LTO=off CARGO_PROFILE_DEV_DEBUG="line-tables-only" cargo dev generate-all` after changing configuration options, CLI arguments, lint rules, or environment variable definitions, as these changes require regeneration of schemas, docs, and CLI references. - Don't prefix tests with `test_`. - Don't separate struct definitions from their `impl` blocks unless the `impl` is deliberately placed in a separate file, as for large structs. - Avoid running `uv run` for any scripts from the repository root unless you use `--no-project`, `--script` or similar. Using `uv run` from the Ruff repo root without these flags will build Ruff from source, which is very slow and usually unnecessary. diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md index d634cb306d..e21770e927 100644 --- a/BREAKING_CHANGES.md +++ b/BREAKING_CHANGES.md @@ -1,5 +1,71 @@ # Breaking Changes +## 0.16.0 + +- **New default rules** + + Ruff now enables a much larger set of rules by default (413, up from 59). See the blog post for + more details and the new [Default Rules](https://docs.astral.sh/ruff/default-rules/) page for a + full listing of the enabled rules. Note that this is primarily an expansion, but 18 of the more + opinionated pycodestyle (`E`) and pyflakes (`F`) rules have been removed from the default set: + `E401`, `E402`, `E701`, `E702`, `E703`, `E711`, `E712`, `E713`, `E714`, `E721`, `E731`, `E741`, + `E742`, `E743`, `F403`, `F405`, `F406`, and `F722`. + +- **Python code block formatting in Markdown files** + + Ruff can now format Python code blocks in Markdown files and will do this by default. See the + [documentation](https://docs.astral.sh/ruff/formatter/#markdown-code-formatting) for more details. + +- **`ruff: ignore` suppression comments** + + Ruff now supports `ruff: ignore` comments at the ends of lines, like `noqa` comments, or on the line preceding a diagnostic. For example, these both suppress an [`unused-import`](https://docs.astral.sh/ruff/rules/unused-import/) (`F401`) diagnostic: + + ```py + import math # ruff: ignore[F401] + + # ruff: ignore[F401] + import os + ``` + +- **Fix diffs in linter and formatter output** + + Fixes are now shown in `check` and `format --check` output: + + ````console + ❯ ruff format --check . + unformatted: File would be reformatted + --> try.md:1:1 + | + 1 | ```python + - import math + 2 + import math + 3 | ``` + | + + 1 file would be reformatted + ```` + + This example also shows off the Markdown formatting. + +- **Output format support in `format --check`** + + `format --check` now supports the same output formats as the linter, including the `github` and + `gitlab` outputs for rendering annotations in CI: + + ```console + ❯ ruff format --check --output-format github . + ::error title=ruff (unformatted),file=try.md,line=2,col=8,endLine=2,endColumn=10::try.md:2:8: unformatted: File would be reformatted + ``` + + See the CLI help or [documentation](https://docs.astral.sh/ruff/settings/#output-format) for the + full list of supported formats. + +- **Some fields are now optional in the JSON output** + + The `filename`, `location`, `end_location`, `fix.edits[].location`, and `fix.edits[].end_location` + fields in the JSON output format may now be `null` rather than defaulting to the empty string and + row 1, column 1, respectively. + ## 0.15.0 - **2026 formatter style guide** diff --git a/CHANGELOG.md b/CHANGELOG.md index e92d24f1b8..b871d0c132 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,1334 +1,229 @@ # Changelog -## 0.15.22 +## 0.16.2 -Released on 2026-07-16. - -### Preview features - -- \[`pycodestyle`\] Add an autofix for `E402` ([#22212](https://github.com/astral-sh/ruff/pull/22212)) -- \[`refurb`\] Allow subclassing builtins in stub files (`FURB189`) ([#26812](https://github.com/astral-sh/ruff/pull/26812)) -- \[`ruff`\] Add rule to replace `noqa` comments with `ruff:ignore` (`RUF105`) ([#26423](https://github.com/astral-sh/ruff/pull/26423)) -- \[`ruff`\] Add rule to use human-readable names in `ruff:ignore` comments (`RUF106`) ([#26682](https://github.com/astral-sh/ruff/pull/26682)) -- \[`ruff`\] Add rule to use human-readable names in configuration selectors (`RUF201`) ([#26772](https://github.com/astral-sh/ruff/pull/26772)) - -### Bug fixes - -- \[`flake8-pyi`\] Fix false positive in `__all__` (`PYI053`) ([#26872](https://github.com/astral-sh/ruff/pull/26872)) - -### Rule changes - -- \[`pylint`\] Ignore mutable type updates in `redefined-loop-name` (`PLW2901`) ([#25733](https://github.com/astral-sh/ruff/pull/25733)) - -### Performance - -- Avoid redundant lexer token bookkeeping ([#26765](https://github.com/astral-sh/ruff/pull/26765)) -- Avoid redundant pending-indentation writes ([#26774](https://github.com/astral-sh/ruff/pull/26774)) -- Avoid unnecessary identifier lookahead ([#26525](https://github.com/astral-sh/ruff/pull/26525)) -- Reuse parser scratch buffers ([#26798](https://github.com/astral-sh/ruff/pull/26798)) - -### Documentation - -- Document argfile support ([#26803](https://github.com/astral-sh/ruff/pull/26803)) -- \[`flake8-datetimez`\] Clarify naming guidance for `datetime.today` (`DTZ002`) ([#26658](https://github.com/astral-sh/ruff/pull/26658)) -- \[`pycodestyle`\] Document `E731` fix safety ([#26847](https://github.com/astral-sh/ruff/pull/26847)) -- \[`ruff`\] Clarify intentional async contexts for `unused-async` (`RUF029`) ([#26641](https://github.com/astral-sh/ruff/pull/26641)) - -### Contributors - -- [@dwego](https://github.com/dwego) -- [@MichaReiser](https://github.com/MichaReiser) -- [@Joosboy](https://github.com/Joosboy) -- [@KaufmanDmitriy](https://github.com/KaufmanDmitriy) -- [@PeterJCLaw](https://github.com/PeterJCLaw) -- [@ntBre](https://github.com/ntBre) -- [@charliermarsh](https://github.com/charliermarsh) - -## 0.15.21 - -Released on 2026-07-09. - -### Preview features - -- Add `--add-ignore` for adding `ruff:ignore` comments ([#26346](https://github.com/astral-sh/ruff/pull/26346)) -- \[`flake8-comprehensions`\] Drop `C409` tuple comprehension preview behavior ([#25707](https://github.com/astral-sh/ruff/pull/25707)) -- Avoid whitespace normalization when formatting comments ([#26455](https://github.com/astral-sh/ruff/pull/26455)) -- \[`pyupgrade`\] Lint and fix use of deprecated `abc` decorators (`UP051`) ([#26417](https://github.com/astral-sh/ruff/pull/26417)) +Released on 2026-08-06. ### Bug fixes -- Refine non-empty f-string detection ([#26526](https://github.com/astral-sh/ruff/pull/26526)) -- Detect syntax errors in individual notebook cells ([#26419](https://github.com/astral-sh/ruff/pull/26419)) -- \[`flake8-implicit-str-concat`\] Fix `ISC003` autofix incorrectly stripping `+` from comments ([#26554](https://github.com/astral-sh/ruff/pull/26554)) - -### Rule changes - -- \[`flake8-executable`\] Mark `EXE004` fix as unsafe ([#26033](https://github.com/astral-sh/ruff/pull/26033)) -- \[`flake8-pyi`\] Mark `PYI061` fixes as unsafe in Python files ([#26533](https://github.com/astral-sh/ruff/pull/26533)) -- \[`pydocstyle`\] Skip `overload-with-docstring` in stub files (`D418`) ([#26318](https://github.com/astral-sh/ruff/pull/26318)) - -### Performance - -- Avoid per-token source index visitor calls ([#26506](https://github.com/astral-sh/ruff/pull/26506)) -- Cache parenthesized expression boundaries in the formatter ([#26344](https://github.com/astral-sh/ruff/pull/26344)) -- Improve performance of rendering edits in preview mode ([#26565](https://github.com/astral-sh/ruff/pull/26565)) -- Inline `fits_element` in formatter ([#26429](https://github.com/astral-sh/ruff/pull/26429)) -- Inline formatter printing hot paths ([#26504](https://github.com/astral-sh/ruff/pull/26504)) -- Lazily create builtin bindings ([#26510](https://github.com/astral-sh/ruff/pull/26510)) -- Skip empty trivia scans in the source indexer ([#26507](https://github.com/astral-sh/ruff/pull/26507)) -- Use ICF for macOS release builds ([#25780](https://github.com/astral-sh/ruff/pull/25780)) - -### Formatter +- \[`flake8-pyi`\] Avoid false positives on `singledispatch` functions (`PYI041`) ([#27335](https://github.com/astral-sh/ruff/pull/27335)) -- Add `--extend-exclude` to `ruff format` ([#26372](https://github.com/astral-sh/ruff/pull/26372)) - -### Documentation +### Server -- Add "How does Ruff's import sorting compare to isort?" link to README ([#26530](https://github.com/astral-sh/ruff/pull/26530)) -- Fix Mozilla Firefox repository link in README ([#26537](https://github.com/astral-sh/ruff/pull/26537)) -- \[`flake8-bandit`\] Fix misleading docstring for `mako-templates` (`S702`) ([#26432](https://github.com/astral-sh/ruff/pull/26432)) -- \[`ruff`\] Fix non-triggering example for `if-key-in-dict-del` (`RUF051`) ([#26433](https://github.com/astral-sh/ruff/pull/26433)) +- Register formatting capabilities dynamically to exclude TOML files ([#27332](https://github.com/astral-sh/ruff/pull/27332)) ### Contributors -- [@EkriirkE](https://github.com/EkriirkE) -- [@tingerrr](https://github.com/tingerrr) -- [@s-rigaud](https://github.com/s-rigaud) -- [@nikolauspschuetz](https://github.com/nikolauspschuetz) -- [@Avasam](https://github.com/Avasam) -- [@ntBre](https://github.com/ntBre) -- [@omar-y-abdi](https://github.com/omar-y-abdi) -- [@AlexWaygood](https://github.com/AlexWaygood) -- [@sylvestre](https://github.com/sylvestre) -- [@shaanmajid](https://github.com/shaanmajid) -- [@lerebear](https://github.com/lerebear) -- [@baltasarblanco](https://github.com/baltasarblanco) -- [@Sanjays2402](https://github.com/Sanjays2402) -- [@ZedThree](https://github.com/ZedThree) -- [@servusdei2018](https://github.com/servusdei2018) +- [@MeGaGiGaGon](https://github.com/MeGaGiGaGon) - [@charliermarsh](https://github.com/charliermarsh) -- [@jesco-absolute](https://github.com/jesco-absolut) -- [@velikodniy](https://github.com/velikodniy) -- [@zaniebot](https://github.com/zaniebot) - [@epage](https://github.com/epage) - -## 0.15.20 - -Released on 2026-06-25. - -### Preview features - -- Allow human-readable names in rule selectors ([#25887](https://github.com/astral-sh/ruff/pull/25887)) -- Emit a warning instead of an error for unknown rule selectors ([#26113](https://github.com/astral-sh/ruff/pull/26113)) -- Match `noqa` shebang handling in `ruff:ignore` comments ([#26286](https://github.com/astral-sh/ruff/pull/26286)) -- \[`ruff`\] Remove `pytest-fixture-autouse` (`RUF076`) ([#26240](https://github.com/astral-sh/ruff/pull/26240), [#26371](https://github.com/astral-sh/ruff/pull/26371)) - -### Documentation - -- Add versioning sections to custom crate READMEs ([#26317](https://github.com/astral-sh/ruff/pull/26317)) -- Update `ruff_python_parser` README for crates.io ([#26315](https://github.com/astral-sh/ruff/pull/26315)) -- \[`perflint`\] Clarify that `PERF402` applies to any iterable ([#26242](https://github.com/astral-sh/ruff/pull/26242)) - -### Contributors - -- [@dhruvmanila](https://github.com/dhruvmanila) -- [@MichaReiser](https://github.com/MichaReiser) -- [@ntBre](https://github.com/ntBre) -- [@trilamsr](https://github.com/trilamsr) - -## 0.15.19 - -Released on 2026-06-23. - -### Preview features - -- Support human-readable names when hovering suppression comments and in code actions ([#26114](https://github.com/astral-sh/ruff/pull/26114)) - -### Bug fixes - -- Fall back to default settings when editor-only settings are invalid ([#26244](https://github.com/astral-sh/ruff/pull/26244)) -- Fix panic when inserting text at a notebook cell boundary ([#26111](https://github.com/astral-sh/ruff/pull/26111)) - -### Rule changes - -- \[`pylint`\] Update fix suggestions for `__floor__`, `__trunc__`, `__length_hint__`, and `__matmul__` variants (`PLC2801`) ([#26239](https://github.com/astral-sh/ruff/pull/26239)) - -### Performance - -- Avoid allocating when parsing single string literals ([#26200](https://github.com/astral-sh/ruff/pull/26200)) -- Avoid reallocating singleton call arguments ([#26223](https://github.com/astral-sh/ruff/pull/26223)) -- Lazily create source files for lint diagnostics ([#26226](https://github.com/astral-sh/ruff/pull/26226)) -- Optimize formatter text width and indentation ([#26236](https://github.com/astral-sh/ruff/pull/26236)) -- Reserve capacity for builtin bindings ([#26229](https://github.com/astral-sh/ruff/pull/26229)) -- Skip repeated-key checks for singleton dictionaries ([#26228](https://github.com/astral-sh/ruff/pull/26228)) -- Use ArrayVec for qualified name segments ([#26224](https://github.com/astral-sh/ruff/pull/26224)) - -### Documentation - -- \[`flake8-pyi`\] Note that `PYI051` is an opinionated stylistic rule ([#26179](https://github.com/astral-sh/ruff/pull/26179)) -- \[`pyupgrade`\] Clarify `UP029` as a Python 2 compatibility rule ([#26243](https://github.com/astral-sh/ruff/pull/26243)) - -### Other changes - -- Publish Ruff crates to crates.io ([#26271](https://github.com/astral-sh/ruff/pull/26271)) - -### Contributors - -- [@MakenRosa](https://github.com/MakenRosa) -- [@MichaReiser](https://github.com/MichaReiser) -- [@trilamsr](https://github.com/trilamsr) +- [@sharkdp](https://github.com/sharkdp) - [@ntBre](https://github.com/ntBre) -- [@sanjibani](https://github.com/sanjibani) -- [@charliermarsh](https://github.com/charliermarsh) -## 0.15.18 +## 0.16.1 -Released on 2026-06-18. +Released on 2026-07-30. ### Preview features -- Handle nested `ruff:ignore` comments ([#25791](https://github.com/astral-sh/ruff/pull/25791)) -- Stop displaying severity in output ([#26050](https://github.com/astral-sh/ruff/pull/26050)) -- Use human-readable names in CLI output ([#25937](https://github.com/astral-sh/ruff/pull/25937)) -- Use human-readable names in LSP and playground diagnostics ([#26058](https://github.com/astral-sh/ruff/pull/26058)) -- \[`pydocstyle`\] Prevent property docstrings starting with verbs (`D421`) ([#23775](https://github.com/astral-sh/ruff/pull/23775)) -- \[`flake8-pyi`\] Extend `PYI033` to Python files ([#26129](https://github.com/astral-sh/ruff/pull/26129)) +- Add an option to opt out of human-readable names ([#27160](https://github.com/astral-sh/ruff/pull/27160)) +- \[`flake8-pytest-style`\] Make fixes safe by default and unsafe only when comments are present (`PT018`) ([#27201](https://github.com/astral-sh/ruff/pull/27201)) +- \[`pyupgrade`\] Skip fix when a defaulted `TypeVar` precedes a non-defaulted one (`UP040`, `UP046`, `UP047`) ([#27133](https://github.com/astral-sh/ruff/pull/27133)) +- \[`ruff`\] Fix false positive with unpacked arguments (`RUF065`) ([#26959](https://github.com/astral-sh/ruff/pull/26959)) ### Bug fixes -- Detect equivalent numeric mapping keys ([#26009](https://github.com/astral-sh/ruff/pull/26009)) -- Detect mapping keys equivalent to booleans ([#25982](https://github.com/astral-sh/ruff/pull/25982)) -- Detect repeated signed and complex dictionary keys ([#26007](https://github.com/astral-sh/ruff/pull/26007)) +- Bump `gen-lsp-types` to gracefully handle unknown enumeration values in LSP messages ([#27230](https://github.com/astral-sh/ruff/pull/27230)) +- \[`flake8-bugbear`\] Mark `range` as immutable (`B008`) ([#27247](https://github.com/astral-sh/ruff/pull/27247)) +- \[`flake8-comprehensions`\] NFKC-normalize keyword names in `C408` fix ([#26813](https://github.com/astral-sh/ruff/pull/26813)) +- \[`flake8-return`\] Fix false positive when variable is read in `finally` clause (`RET504`) ([#25441](https://github.com/astral-sh/ruff/pull/25441)) +- \[`pydocstyle`\] Skip section detection inside RST directive bodies (`D214`, `D405`, `D413`) ([#23635](https://github.com/astral-sh/ruff/pull/23635)) +- \[`refurb`\] Parenthesize `yield` arguments in the `FURB192` fix ([#27192](https://github.com/astral-sh/ruff/pull/27192)) ### Rule changes -- \[`flake8-pyi`\] Rename `PYI033` to `legacy-type-comment` ([#26131](https://github.com/astral-sh/ruff/pull/26131)) - -### Performance - -- Use `ThinVec` for call keywords ([#25999](https://github.com/astral-sh/ruff/pull/25999)) -- Inline parser recovery context checks ([#26038](https://github.com/astral-sh/ruff/pull/26038)) -- Match parser keywords as bytes ([#26037](https://github.com/astral-sh/ruff/pull/26037)) -- Move value parsing out of lexing ([#25360](https://github.com/astral-sh/ruff/pull/25360)) +- \[`flake8-pytest-style`\] Mark `PT022` fixes as unsafe ([#26440](https://github.com/astral-sh/ruff/pull/26440)) +- \[`refurb`\] Mark fixes that remove unknown separators as unsafe (`FURB105`) ([#27200](https://github.com/astral-sh/ruff/pull/27200)) ### Server -- Render subdiagnostics and secondary annotations as related information ([#26011](https://github.com/astral-sh/ruff/pull/26011)) +- Fix indexing of excluded nested Ruff workspaces ([#27303](https://github.com/astral-sh/ruff/pull/27303)) +- Lint TOML files in the LSP ([#26862](https://github.com/astral-sh/ruff/pull/26862)) ### Documentation -- Update fix availability for always-fixable rules ([#26091](https://github.com/astral-sh/ruff/pull/26091)) -- \[`flake8-tidy-imports`\] Add fix safety section (`TID252`) ([#17491](https://github.com/astral-sh/ruff/pull/17491)) - -### Parser - -- Reject `__debug__` lambda parameters ([#26022](https://github.com/astral-sh/ruff/pull/26022)) -- Reject `_` as a match-pattern target ([#25977](https://github.com/astral-sh/ruff/pull/25977)) -- Reject multiple starred names in sequence patterns ([#25976](https://github.com/astral-sh/ruff/pull/25976)) -- Reject parenthesized star imports ([#26021](https://github.com/astral-sh/ruff/pull/26021)) -- Reject starred comprehension targets ([#26023](https://github.com/astral-sh/ruff/pull/26023)) -- Reject unparenthesized generator expressions in class bases ([#25978](https://github.com/astral-sh/ruff/pull/25978)) -- Reject `yield` expressions after commas ([#26024](https://github.com/astral-sh/ruff/pull/26024)) -- Validate function type parameter default order ([#25981](https://github.com/astral-sh/ruff/pull/25981)) - -### Playground - -- Make diagnostic links clickable ([#26104](https://github.com/astral-sh/ruff/pull/26104)) -- Use diagnostic tags ([#26105](https://github.com/astral-sh/ruff/pull/26105)) - -### Contributors - -- [@AlexWaygood](https://github.com/AlexWaygood) -- [@ntBre](https://github.com/ntBre) -- [@gtkacz](https://github.com/gtkacz) -- [@MichaReiser](https://github.com/MichaReiser) -- [@charliermarsh](https://github.com/charliermarsh) -- [@Kalmaegi](https://github.com/Kalmaegi) - -## 0.15.17 - -Released on 2026-06-11. - -### Preview features - -- Allow human-readable names in suppression comments ([#25614](https://github.com/astral-sh/ruff/pull/25614)) -- Fix handling of `ignore` comments within a `disable`/`enable` pair ([#25845](https://github.com/astral-sh/ruff/pull/25845)) -- Prioritize human-readable names in CLI output ([#25869](https://github.com/astral-sh/ruff/pull/25869)) -- Respect diagnostic start and parent ranges and trailing comments in `ruff:ignore` suppressions ([#25673](https://github.com/astral-sh/ruff/pull/25673)) -- \[`flake8-async`\] Add `trio.as_safe_channel` to safe decorators (`ASYNC119`) ([#25775](https://github.com/astral-sh/ruff/pull/25775)) -- \[`flake8-pytest-style`\] Also check `pytest_asyncio` fixtures ([#25375](https://github.com/astral-sh/ruff/pull/25375)) -- \[`ruff`\] Ban `pytest` autouse fixtures (`RUF076`) ([#25477](https://github.com/astral-sh/ruff/pull/25477)) -- \[`pyupgrade`\] Add `from __future__ import annotations` automatically (`UP007`, `UP045`) ([#23259](https://github.com/astral-sh/ruff/pull/23259)) - -### Bug fixes - -- Fix diagnostic when `ruff:enable` or `ruff:disable` appears where `ruff:ignore` is expected ([#25700](https://github.com/astral-sh/ruff/pull/25700)) -- \[`pyupgrade`\] Preserve leading empty literals to avoid syntax errors (`UP032`) ([#25491](https://github.com/astral-sh/ruff/pull/25491)) - -### Rule changes - -- \[`flake8-pytest-style`\] Clarify diagnostic message for single parameters (`PT007`) ([#25592](https://github.com/astral-sh/ruff/pull/25592)) -- \[`numpy`\] Drop autofix for `np.in1d` (`NPY201`) ([#25612](https://github.com/astral-sh/ruff/pull/25612)) -- \[`pylint`\] Exempt Python version comparisons (`PLR2004`) ([#25743](https://github.com/astral-sh/ruff/pull/25743)) - -### Performance - -- Reserve AST `Vec`s with correct capacity for common cases ([#25451](https://github.com/astral-sh/ruff/pull/25451)) - -### Formatter - -- Preserve whitespace for Quarto cell option comments ([#25641](https://github.com/astral-sh/ruff/pull/25641)) - -### CLI - -- Allow rule names in `ruff rule` ([#25640](https://github.com/astral-sh/ruff/pull/25640)) +- Cover `pycon` Markdown formatting ([#27153](https://github.com/astral-sh/ruff/pull/27153)) +- \[`flake8-bandit`\] Document `TYPE_CHECKING` exception (`S101`) ([#27004](https://github.com/astral-sh/ruff/pull/27004)) +- \[`flake8-import-conventions`\] Document that `extend-aliases` can override default aliases ([#27191](https://github.com/astral-sh/ruff/pull/27191)) +- \[`pylint`\] Add missing fix safety gotchas for `non-augmented-assignment` (`PLR6104`) ([#27250](https://github.com/astral-sh/ruff/pull/27250)) ### Other changes -- Fix playground diagnostics scrollbars ([#25642](https://github.com/astral-sh/ruff/pull/25642)) +- Reduce syntax error noise by swallowing dedents like indents ([#27170](https://github.com/astral-sh/ruff/pull/27170)) +- Vendor latest annotate-snippets ([#27033](https://github.com/astral-sh/ruff/pull/27033)) ### Contributors -- [@SuryanshSS1011](https://github.com/SuryanshSS1011) +- [@bxff](https://github.com/bxff) - [@anishgirianish](https://github.com/anishgirianish) -- [@romero-deshaw](https://github.com/romero-deshaw) -- [@karlhillx](https://github.com/karlhillx) -- [@carljm](https://github.com/carljm) -- [@ntBre](https://github.com/ntBre) -- [@11happy](https://github.com/11happy) -- [@Kilo59](https://github.com/Kilo59) -- [@oconnor663](https://github.com/oconnor663) -- [@LeonidasZhak](https://github.com/LeonidasZhak) -- [@DavisVaughan](https://github.com/DavisVaughan) -- [@MeGaGiGaGon](https://github.com/MeGaGiGaGon) -- [@jonathandung](https://github.com/jonathandung) +- [@Avasam](https://github.com/Avasam) +- [@epage](https://github.com/epage) +- [@LHMQ878](https://github.com/LHMQ878) - [@MichaReiser](https://github.com/MichaReiser) -- [@brianmego](https://github.com/brianmego) - -## 0.15.16 - -Released on 2026-06-04. - -### Preview features - -- \[`flake8-async`\] Implement `yield-in-context-manager-in-async-generator` (`ASYNC119`) ([#24644](https://github.com/astral-sh/ruff/pull/24644)) -- \[`pylint`\] Narrow diagnostic range and exclude cases without exception handlers (`PLW0717`) ([#25440](https://github.com/astral-sh/ruff/pull/25440)) -- \[`ruff`\] Treat `yield` before `break` from a terminal loop as terminal (`RUF075`) ([#25447](https://github.com/astral-sh/ruff/pull/25447)) - -### Bug fixes - -- \[`eradicate`\] Avoid flagging `ruff:ignore` comments as code (`ERA001`) ([#25537](https://github.com/astral-sh/ruff/pull/25537)) -- \[`eradicate`\] Fix `ERA001`/`RUF100` conflict when `noqa` is on commented-out code ([#25414](https://github.com/astral-sh/ruff/pull/25414)) -- \[`pyflakes`\] Avoid removing the `format` call when it would change behavior (`F523`) ([#25320](https://github.com/astral-sh/ruff/pull/25320)) -- \[`pylint`\] Avoid syntax errors in invalid character replacements in f-strings before Python 3.12 (`PLE2510`, `PLE2512`, `PLE2513`, `PLE2514`, `PLE2515`) ([#25544](https://github.com/astral-sh/ruff/pull/25544)) -- \[`pyupgrade`\] Avoid converting `format` calls with more kinds of side effects (`UP032`) ([#25484](https://github.com/astral-sh/ruff/pull/25484)) - -### Rule changes - -- \[`flake8-pytest-style`\] Avoid fixes for ambiguous `argnames` and `argvalues` combinations (`PT006`) ([#24776](https://github.com/astral-sh/ruff/pull/24776)) - -### Performance - -- Drop excess capacity from statement suites during parsing ([#25368](https://github.com/astral-sh/ruff/pull/25368)) - -### Documentation - -- \[`pydocstyle`\] Improve discoverability of rules enabled for each convention ([#24973](https://github.com/astral-sh/ruff/pull/24973)) -- \[`ruff`\] Restore example code for Python versions before 3.15 (`RUF017`) ([#25439](https://github.com/astral-sh/ruff/pull/25439)) -- Fix typo `bin/active` → `bin/activate` in tutorial ([#25473](https://github.com/astral-sh/ruff/pull/25473)) - -### Other changes - -- Shrink additional parser AST collections ([#25465](https://github.com/astral-sh/ruff/pull/25465)) - -### Contributors - -- [@Redslayer112](https://github.com/Redslayer112) -- [@koriyoshi2041](https://github.com/koriyoshi2041) -- [@George-Ogden](https://github.com/George-Ogden) -- [@TejasAmle](https://github.com/TejasAmle) -- [@anishgirianish](https://github.com/anishgirianish) - [@ntBre](https://github.com/ntBre) -- [@MichaReiser](https://github.com/MichaReiser) -- [@loganrosen](https://github.com/loganrosen) -- [@RafaelJohn9](https://github.com/RafaelJohn9) -- [@adityasingh2400](https://github.com/adityasingh2400) - -## 0.15.15 - -Released on 2026-05-28. - -### Preview features - -- Fix Markdown closing fence handling ([#25310](https://github.com/astral-sh/ruff/pull/25310)) -- \[`pyflakes`\] Report duplicate imports in `typing.TYPE_CHECKING` block (`F811`) ([#22560](https://github.com/astral-sh/ruff/pull/22560)) - -### Bug fixes - -- \[`pyflakes`\] Treat function-scope bare annotations as locals per PEP 526 (`F821`) ([#21540](https://github.com/astral-sh/ruff/pull/21540)) - -### Performance - -- Avoid redundant `TokenValue` drops in the lexer ([#25300](https://github.com/astral-sh/ruff/pull/25300)) -- Reduce memory usage by dropping token-excess capacity and improve performance by approximating the initial tokens `Vec` size ([#25354](https://github.com/astral-sh/ruff/pull/25354)) -- Use `ThinVec` in AST to shrink `Stmt` ([#25361](https://github.com/astral-sh/ruff/pull/25361)) - -### Documentation - -- Fix `line-length` example for `--config` option ([#25389](https://github.com/astral-sh/ruff/pull/25389)) -- \[`flake8-comprehensions`\] Document `RecursionError` edge case in `__len__` (`C416`) ([#25286](https://github.com/astral-sh/ruff/pull/25286)) -- \[`mccabe`\] Improve example (`C901`) ([#25287](https://github.com/astral-sh/ruff/pull/25287)) -- \[`pyupgrade`\] Clarify fix safety docs (`UP007`, `UP045`) ([#25288](https://github.com/astral-sh/ruff/pull/25288)) -- \[`refurb`\] Document `FURB192` exception change for empty sequences ([#25317](https://github.com/astral-sh/ruff/pull/25317)) -- \[`ruff`\] Document false negative for user-defined types (`RUF013`) ([#25289](https://github.com/astral-sh/ruff/pull/25289)) - -### Formatter - -- Fix formatting of lambdas nested within f-strings ([#25398](https://github.com/astral-sh/ruff/pull/25398)) - -### Server - -- Return code action for `codeAction/resolve` requests that contain no or no valid URL ([#25365](https://github.com/astral-sh/ruff/pull/25365)) +- [@HarshalPatel1972](https://github.com/HarshalPatel1972) +- [@mjpieters](https://github.com/mjpieters) +- [@joshuavetos](https://github.com/joshuavetos) +- [@jesco-absolute](https://github.com/jesco-absolut) +- [@vidigoat](https://github.com/vidigoat) +- [@baltasarblanco](https://github.com/baltasarblanco) +- [@ribru17](https://github.com/ribru17) +- [@oh-summy](https://github.com/oh-summy) +- [@Jayashanker-Padishala](https://github.com/Jayashanker-Padishala) -### Other changes +## 0.16.0 -- Expand semantic syntax errors for invalid walruses ([#25415](https://github.com/astral-sh/ruff/pull/25415)) +Released on 2026-07-23. -### Contributors +Check out the [blog post](https://astral.sh/blog/ruff-v0.16.0) for a migration +guide and overview of the changes! -- [@chirizxc](https://github.com/chirizxc) -- [@ntBre](https://github.com/ntBre) -- [@adityasingh2400](https://github.com/adityasingh2400) -- [@charliermarsh](https://github.com/charliermarsh) -- [@fallintoplace](https://github.com/fallintoplace) -- [@martin-schlossarek](https://github.com/martin-schlossarek) -- [@MichaReiser](https://github.com/MichaReiser) -- [@Ruchir28](https://github.com/Ruchir28) +### Breaking changes -## 0.15.14 +- Ruff now enables a much larger set of rules by default (413, up from 59). See the blog post for + more details and the new [Default Rules](https://docs.astral.sh/ruff/default-rules/) page for a + full listing of the enabled rules. Note that this is primarily an expansion, but 18 of the more + opinionated pycodestyle (`E`) and pyflakes (`F`) rules have been removed from the default set: + `E401`, `E402`, `E701`, `E702`, `E703`, `E711`, `E712`, `E713`, `E714`, `E721`, `E731`, `E741`, + `E742`, `E743`, `F403`, `F405`, `F406`, and `F722`. -Released on 2026-05-21. +- Ruff can now format Python code blocks in Markdown files and will do this by default. See the + [documentation](https://docs.astral.sh/ruff/formatter/#markdown-code-formatting) for more details. -### Preview features +- Ruff now supports `ruff: ignore` comments at the ends of lines, like `noqa` comments, or on the line preceding a diagnostic. For example, these both suppress an [`unused-import`](https://docs.astral.sh/ruff/rules/unused-import/) (`F401`) diagnostic: -- \[`airflow`\] Implement `airflow-task-implicit-multiple-outputs` (`AIR202`) ([#25152](https://github.com/astral-sh/ruff/pull/25152)) -- \[`flake8-use-pathlib`\] Mark `PTH101` fix as unsafe when first argument is a class attribute annotated as `int` ([#25086](https://github.com/astral-sh/ruff/pull/25086)) -- \[`pylint`\] Implement `too-many-try-statements` (`W0717`) ([#23970](https://github.com/astral-sh/ruff/pull/23970)) -- \[`ruff`\] Add `incorrect-decorator-order` (`RUF074`) ([#23461](https://github.com/astral-sh/ruff/pull/23461)) -- \[`ruff`\] Add `fallible-context-manager` (`RUF075`) ([#22844](https://github.com/astral-sh/ruff/pull/22844)) + ```py + import math # ruff: ignore[F401] -### Bug fixes + # ruff: ignore[F401] + import os + ``` -- Fix lambda formatting in interpolated string expressions ([#25144](https://github.com/astral-sh/ruff/pull/25144)) -- Treat generic `frozenset` annotations as immutable ([#25251](https://github.com/astral-sh/ruff/pull/25251)) -- \[`flake8-type-checking`\] Avoid `strict` behavior when `future-annotations` are enabled (`TC001`, `TC002`, `TC003`) ([#25035](https://github.com/astral-sh/ruff/pull/25035)) -- \[`pylint`\] Avoid false positives in `else` clause (`PLR1733`) ([#25177](https://github.com/astral-sh/ruff/pull/25177)) +- Fixes are now shown in `check` and `format --check` output: -### Rule changes + ````console + ❯ ruff format --check . + unformatted: File would be reformatted + --> try.md:1:1 + | + 1 | ```python + - import math + 2 + import math + 3 | ``` + | -- \[`flake8-comprehensions`\] Skip `C417` for lambdas with positional-only parameters ([#25272](https://github.com/astral-sh/ruff/pull/25272)) -- \[`flake8-simplify`\] Preserve f-string source verbatim in `SIM101` fix ([#25061](https://github.com/astral-sh/ruff/pull/25061)) + 1 file would be reformatted + ```` -### Performance + This example also shows off the Markdown formatting. -- Avoid unnecessary parser lookahead for operators ([#25290](https://github.com/astral-sh/ruff/pull/25290)) +- `format --check` now supports the same output formats as the linter, including the `github` and + `gitlab` outputs for rendering annotations in CI: -### Documentation + ```console + ❯ ruff format --check --output-format github . + ::error title=ruff (unformatted),file=try.md,line=2,col=8,endLine=2,endColumn=10::try.md:2:8: unformatted: File would be reformatted + ``` -- Update code example setting Neovim LSP log level ([#25284](https://github.com/astral-sh/ruff/pull/25284)) + See the CLI help or [documentation](https://docs.astral.sh/ruff/settings/#output-format) for the + full list of supported formats. -### Other changes +- The `filename`, `location`, `end_location`, `fix.edits[].location`, and `fix.edits[].end_location` + fields in the JSON output format may now be `null` rather than defaulting to the empty string and + row 1, column 1, respectively. -- Add full PEP 798 support ([#25104](https://github.com/astral-sh/ruff/pull/25104)) -- Add a parser recursion limit ([#24810](https://github.com/astral-sh/ruff/pull/24810)) -- Update various `ruff_python_stdlib` APIs ([#25273](https://github.com/astral-sh/ruff/pull/25273)) +### Stabilization -### Contributors +The following rules have been stabilized and are no longer in preview: -- [@ocaballeror](https://github.com/ocaballeror) -- [@lerebear](https://github.com/lerebear) -- [@samuelcolvin](https://github.com/samuelcolvin) -- [@baltasarblanco](https://github.com/baltasarblanco) -- [@aconal-com](https://github.com/aconal-com) -- [@anishgirianish](https://github.com/anishgirianish) -- [@JelleZijlstra](https://github.com/JelleZijlstra) -- [@AlexWaygood](https://github.com/AlexWaygood) -- [@ntBre](https://github.com/ntBre) -- [@adityasingh2400](https://github.com/adityasingh2400) -- [@charliermarsh](https://github.com/charliermarsh) -- [@Dev-iL](https://github.com/Dev-iL) -- [@neutrinoceros](https://github.com/neutrinoceros) -- [@shivamtiwari3](https://github.com/shivamtiwari3) -- [@Dev-X25874](https://github.com/Dev-X25874) +- [`airflow3-incompatible-function-signature`](https://docs.astral.sh/ruff/rules/airflow3-incompatible-function-signature) + (`AIR303`) +- [`missing-copyright-notice`](https://docs.astral.sh/ruff/rules/missing-copyright-notice) + (`CPY001`) +- [`unnecessary-from-float`](https://docs.astral.sh/ruff/rules/unnecessary-from-float) (`FURB164`) +- [`sorted-min-max`](https://docs.astral.sh/ruff/rules/sorted-min-max) (`FURB192`) +- [`implicit-string-concatenation-in-collection-literal`](https://docs.astral.sh/ruff/rules/implicit-string-concatenation-in-collection-literal) + (`ISC004`) +- [`log-exception-outside-except-handler`](https://docs.astral.sh/ruff/rules/log-exception-outside-except-handler) + (`LOG004`) +- [`invalid-bool-return-type`](https://docs.astral.sh/ruff/rules/invalid-bool-return-type) + (`PLE0304`) +- [`too-many-positional-arguments`](https://docs.astral.sh/ruff/rules/too-many-positional-arguments) + (`PLR0917`) +- [`stop-iteration-return`](https://docs.astral.sh/ruff/rules/stop-iteration-return) (`PLR1708`) +- [`none-not-at-end-of-union`](https://docs.astral.sh/ruff/rules/none-not-at-end-of-union) + (`RUF036`) +- [`access-annotations-from-class-dict`](https://docs.astral.sh/ruff/rules/access-annotations-from-class-dict) + (`RUF063`) +- [`duplicate-entry-in-dunder-all`](https://docs.astral.sh/ruff/rules/duplicate-entry-in-dunder-all) + (`RUF068`) -## 0.15.13 +The following behaviors have been stabilized: -Released on 2026-05-14. +- [`blind-except`](https://docs.astral.sh/ruff/rules/blind-except) (`BLE001`) is now suppressed when + the exception is logged via `logging` methods other than `critical`, `error` and `exception`. +- [`future-required-type-annotation`](https://docs.astral.sh/ruff/rules/future-required-type-annotation) + (`FA102`) now checks for additional [PEP 585](https://peps.python.org/pep-0585/)-compatible + APIs, such as those from `collections.abc`. +- [`f-string-in-get-text-func-call`](https://docs.astral.sh/ruff/rules/f-string-in-get-text-func-call) + (`INT001`), + [`format-in-get-text-func-call`](https://docs.astral.sh/ruff/rules/format-in-get-text-func-call) + (`INT002`), and + [`printf-in-get-text-func-call`](https://docs.astral.sh/ruff/rules/printf-in-get-text-func-call) + (`INT003`) now check for additional common ways of using the `gettext` module, such as assigning + it to `builtins._`. +- [`suspicious-url-open-usage`](https://docs.astral.sh/ruff/rules/suspicious-url-open-usage) + (`S310`) now resolves local string literal bindings to avoid more false positives. +- [`snmp-insecure-version`](https://docs.astral.sh/ruff/rules/snmp-insecure-version) (`S508`) and + [`snmp-weak-cryptography`](https://docs.astral.sh/ruff/rules/snmp-weak-cryptography) (`S509`) now + support the recommended API from newer versions of PySNMP. +- [`typing-text-str-alias`](https://docs.astral.sh/ruff/rules/typing-text-str-alias) (`UP019`) now + recognizes `typing_extensions.Text` in addition to `typing.Text`. ### Preview features -- Add a rule to flag lazy imports that are eagerly evaluated ([#25016](https://github.com/astral-sh/ruff/pull/25016)) -- \[`pylint`\] Standardize diagnostic message (`PLR0914`, `PLR0917`) ([#24996](https://github.com/astral-sh/ruff/pull/24996)) +- \[`pyupgrade`\] Fix false positive with `TypeVar` default before Python 3.13 (`UP040`) ([#26888](https://github.com/astral-sh/ruff/pull/26888)) ### Bug fixes -- Fix `F811` false positive for class methods ([#24933](https://github.com/astral-sh/ruff/pull/24933)) -- Fix setting selection for multi-folder workspace ([#24819](https://github.com/astral-sh/ruff/pull/24819)) -- \[`eradicate`\] Fix false positive for lines with leading whitespace (`ERA001`) ([#25122](https://github.com/astral-sh/ruff/pull/25122)) -- \[`flake8-pyi`\] Fix false positive for f-string debug specifier (`PYI016`) ([#24098](https://github.com/astral-sh/ruff/pull/24098)) +- \[`ruff`\] Fix missing check on unrecognized early bound (`RUF016`) ([#26986](https://github.com/astral-sh/ruff/pull/26986)) ### Rule changes -- Always include panic payload in panic diagnostic message ([#24873](https://github.com/astral-sh/ruff/pull/24873)) -- Restrict `PYI034` for in-place operations to enclosing class ([#24511](https://github.com/astral-sh/ruff/pull/24511)) -- Improve error message for parameters that are declared `global` ([#24902](https://github.com/astral-sh/ruff/pull/24902)) -- Update known stdlib ([#25103](https://github.com/astral-sh/ruff/pull/25103)) +- Insert a space after the colon in Ruff suppression comments ([#27123](https://github.com/astral-sh/ruff/pull/27123)) ### Performance -- \[`isort`\] Avoid constructing `glob::Pattern`s for literal known modules ([#25123](https://github.com/astral-sh/ruff/pull/25123)) - -### CLI - -- Add TOML examples to `--config` help text ([#25013](https://github.com/astral-sh/ruff/pull/25013)) -- Colorize ruff check 'All checks passed' ([#25085](https://github.com/astral-sh/ruff/pull/25085)) - -### Configuration - -- Increase max allowed value of `line-length` setting ([#24962](https://github.com/astral-sh/ruff/pull/24962)) +- \[`pyupgrade`\] Speed up `unnecessary-future-import` (`UP010`) ([#27047](https://github.com/astral-sh/ruff/pull/27047)) ### Documentation -- Add `D203` to rules that conflict with the formatter ([#25044](https://github.com/astral-sh/ruff/pull/25044)) -- Clarify `COM819` and formatter interaction ([#25045](https://github.com/astral-sh/ruff/pull/25045)) -- Clarify that `NotImplemented` is a value, not an exception (`F901`) ([#25054](https://github.com/astral-sh/ruff/pull/25054)) -- Update number of lint rules supported ([#24942](https://github.com/astral-sh/ruff/pull/24942)) - -### Other changes - -- Simplify the playground's markdown template ([#24924](https://github.com/astral-sh/ruff/pull/24924)) +- \[`ruff`\] Add missing period in "Why is this bad?" section (`RUF200`) ([#26930](https://github.com/astral-sh/ruff/pull/26930)) +- \[`flake8-simplify`\] Clarify `os.environ` behavior on Windows (`SIM112`) ([#26972](https://github.com/astral-sh/ruff/pull/26972)) +- \[`pydocstyle`\] Document fix safety (`D400`) ([#26971](https://github.com/astral-sh/ruff/pull/26971)) ### Contributors +- [@jonathandung](https://github.com/jonathandung) +- [@Joosboy](https://github.com/Joosboy) - [@MichaReiser](https://github.com/MichaReiser) -- [@brian-c11](https://github.com/brian-c11) - [@Andrej730](https://github.com/Andrej730) -- [@denyszhak](https://github.com/denyszhak) -- [@darestack](https://github.com/darestack) -- [@sharkdp](https://github.com/sharkdp) -- [@charliermarsh](https://github.com/charliermarsh) -- [@EkriirkE](https://github.com/EkriirkE) -- [@eyupcanakman](https://github.com/eyupcanakman) -- [@Hrk84ya](https://github.com/Hrk84ya) -- [@thernstig](https://github.com/thernstig) - [@ntBre](https://github.com/ntBre) +- [@zaniebot](https://github.com/zaniebot) -## 0.15.12 - -Released on 2026-04-24. - -### Preview features - -- Implement `#ruff:file-ignore` file-level suppressions ([#23599](https://github.com/astral-sh/ruff/pull/23599)) -- Implement `#ruff:ignore` logical-line suppressions ([#23404](https://github.com/astral-sh/ruff/pull/23404)) -- Revert preview changes to displayed diagnostic severity in LSP ([#24789](https://github.com/astral-sh/ruff/pull/24789)) -- \[`airflow`\] Implement `task-branch-as-short-circuit` (`AIR004`) ([#23579](https://github.com/astral-sh/ruff/pull/23579)) -- \[`flake8-bugbear`\] Fix `break`/`continue` handling in `loop-iterator-mutation` (`B909`) ([#24440](https://github.com/astral-sh/ruff/pull/24440)) -- \[`pylint`\] Fix `PLC2701` for type parameter scopes ([#24576](https://github.com/astral-sh/ruff/pull/24576)) - -### Rule changes - -- \[`pandas-vet`\] Suggest `.array` as well in `PD011` ([#24805](https://github.com/astral-sh/ruff/pull/24805)) - -### CLI - -- Respect default Unix permissions for cache files ([#24794](https://github.com/astral-sh/ruff/pull/24794)) - -### Documentation - -- \[`pylint`\] Fix `PLR0124` description not to claim self-comparison always returns the same value ([#24749](https://github.com/astral-sh/ruff/pull/24749)) -- \[`pyupgrade`\] Expand docs on reusable `TypeVar`s and scoping (`UP046`) ([#24153](https://github.com/astral-sh/ruff/pull/24153)) -- Improve rules table accessibility ([#24711](https://github.com/astral-sh/ruff/pull/24711)) - -### Contributors - -- [@dylwil3](https://github.com/dylwil3) -- [@AlexWaygood](https://github.com/AlexWaygood) -- [@woodruffw](https://github.com/woodruffw) -- [@avasis-ai](https://github.com/avasis-ai) -- [@Dev-iL](https://github.com/Dev-iL) -- [@denyszhak](https://github.com/denyszhak) -- [@ShipItAndPray](https://github.com/ShipItAndPray) -- [@anishgirianish](https://github.com/anishgirianish) -- [@augustelalande](https://github.com/augustelalande) -- [@amyreese](https://github.com/amyreese) -- [@majiayu000](https://github.com/majiayu000) - -## 0.15.11 - -Released on 2026-04-16. - -### Preview features - -- \[`ruff`\] Ignore `RUF029` when function is decorated with `asynccontextmanager` ([#24642](https://github.com/astral-sh/ruff/pull/24642)) -- \[`airflow`\] Implement `airflow-xcom-pull-in-template-string` (`AIR201`) ([#23583](https://github.com/astral-sh/ruff/pull/23583)) -- \[`flake8-bandit`\] Fix `S103` false positives and negatives in mask analysis ([#24424](https://github.com/astral-sh/ruff/pull/24424)) - -### Bug fixes - -- \[`flake8-async`\] Omit overridden methods for `ASYNC109` ([#24648](https://github.com/astral-sh/ruff/pull/24648)) - -### Documentation - -- \[`flake8-async`\] Add override mention to `ASYNC109` docs ([#24666](https://github.com/astral-sh/ruff/pull/24666)) -- Update Neovim config examples to use `vim.lsp.config` ([#24577](https://github.com/astral-sh/ruff/pull/24577)) - -### Contributors - -- [@augustelalande](https://github.com/augustelalande) -- [@anishgirianish](https://github.com/anishgirianish) -- [@benberryallwood](https://github.com/benberryallwood) -- [@charliermarsh](https://github.com/charliermarsh) -- [@Dev-iL](https://github.com/Dev-iL) - -## 0.15.10 - -Released on 2026-04-09. - -### Preview features - -- \[`flake8-logging`\] Allow closures in except handlers (`LOG004`) ([#24464](https://github.com/astral-sh/ruff/pull/24464)) -- \[`flake8-self`\] Make `SLF` diagnostics robust to non-self-named variables ([#24281](https://github.com/astral-sh/ruff/pull/24281)) -- \[`flake8-simplify`\] Make the fix for `collapsible-if` safe in `preview` (`SIM102`) ([#24371](https://github.com/astral-sh/ruff/pull/24371)) - -### Bug fixes - -- Avoid emitting multi-line f-string elements before Python 3.12 ([#24377](https://github.com/astral-sh/ruff/pull/24377)) -- Avoid syntax error from `E502` fixes in f-strings and t-strings ([#24410](https://github.com/astral-sh/ruff/pull/24410)) -- Strip form feeds from indent passed to `dedent_to` ([#24381](https://github.com/astral-sh/ruff/pull/24381)) -- \[`pyupgrade`\] Fix panic caused by handling of octals (`UP012`) ([#24390](https://github.com/astral-sh/ruff/pull/24390)) -- Reject multi-line f-string elements before Python 3.12 ([#24355](https://github.com/astral-sh/ruff/pull/24355)) - -### Rule changes - -- \[`ruff`\] Treat f-string interpolation as potential side effect (`RUF019`) ([#24426](https://github.com/astral-sh/ruff/pull/24426)) - -### Server - -- Add support for custom file extensions ([#24463](https://github.com/astral-sh/ruff/pull/24463)) - -### Documentation - -- Document adding fixes in CONTRIBUTING.md ([#24393](https://github.com/astral-sh/ruff/pull/24393)) -- Fix JSON typo in settings example ([#24517](https://github.com/astral-sh/ruff/pull/24517)) - -### Contributors - -- [@charliermarsh](https://github.com/charliermarsh) -- [@dylwil3](https://github.com/dylwil3) -- [@silverstein](https://github.com/silverstein) -- [@anishgirianish](https://github.com/anishgirianish) -- [@shizukushq](https://github.com/shizukushq) -- [@zanieb](https://github.com/zanieb) -- [@AlexWaygood](https://github.com/AlexWaygood) - -## 0.15.9 - -Released on 2026-04-02. +## 0.15.x -### Preview features - -- \[`pyflakes`\] Flag annotated variable redeclarations as `F811` in preview mode ([#24244](https://github.com/astral-sh/ruff/pull/24244)) -- \[`ruff`\] Allow dunder-named assignments in non-strict mode for `RUF067` ([#24089](https://github.com/astral-sh/ruff/pull/24089)) - -### Bug fixes - -- \[`flake8-errmsg`\] Avoid shadowing existing `msg` in fix for `EM101` ([#24363](https://github.com/astral-sh/ruff/pull/24363)) -- \[`flake8-simplify`\] Ignore pre-initialization references in `SIM113` ([#24235](https://github.com/astral-sh/ruff/pull/24235)) -- \[`pycodestyle`\] Fix `W391` fixes for consecutive empty notebook cells ([#24236](https://github.com/astral-sh/ruff/pull/24236)) -- \[`pyupgrade`\] Fix `UP008` nested class matching ([#24273](https://github.com/astral-sh/ruff/pull/24273)) -- \[`pyupgrade`\] Ignore strings with string-only escapes (`UP012`) ([#16058](https://github.com/astral-sh/ruff/pull/16058)) -- \[`ruff`\] `RUF072`: skip formfeeds on dedent ([#24308](https://github.com/astral-sh/ruff/pull/24308)) -- \[`ruff`\] Avoid re-using symbol in `RUF024` fix ([#24316](https://github.com/astral-sh/ruff/pull/24316)) -- \[`ruff`\] Parenthesize expression in `RUF050` fix ([#24234](https://github.com/astral-sh/ruff/pull/24234)) -- Disallow starred expressions as values of starred expressions ([#24280](https://github.com/astral-sh/ruff/pull/24280)) - -### Rule changes - -- \[`flake8-simplify`\] Suppress `SIM105` for `except*` before Python 3.12 ([#23869](https://github.com/astral-sh/ruff/pull/23869)) -- \[`pyflakes`\] Extend `F507` to flag `%`-format strings with zero placeholders ([#24215](https://github.com/astral-sh/ruff/pull/24215)) -- \[`pyupgrade`\] `UP018` should detect more unnecessarily wrapped literals (UP018) ([#24093](https://github.com/astral-sh/ruff/pull/24093)) -- \[`pyupgrade`\] Fix `UP008` callable scope handling to support lambdas ([#24274](https://github.com/astral-sh/ruff/pull/24274)) -- \[`ruff`\] `RUF010`: Mark fix as unsafe when it deletes a comment ([#24270](https://github.com/astral-sh/ruff/pull/24270)) - -### Formatter - -- Add `nested-string-quote-style` formatting option ([#24312](https://github.com/astral-sh/ruff/pull/24312)) - -### Documentation - -- \[`flake8-bugbear`\] Clarify RUF071 fix safety for non-path string comparisons ([#24149](https://github.com/astral-sh/ruff/pull/24149)) -- \[`flake8-type-checking`\] Clarify import cycle wording for `TC001`/`TC002`/`TC003` ([#24322](https://github.com/astral-sh/ruff/pull/24322)) - -### Other changes - -- Avoid rendering fix lines with trailing whitespace after `|` ([#24343](https://github.com/astral-sh/ruff/pull/24343)) - -### Contributors - -- [@charliermarsh](https://github.com/charliermarsh) -- [@MichaReiser](https://github.com/MichaReiser) -- [@tranhoangtu-it](https://github.com/tranhoangtu-it) -- [@dylwil3](https://github.com/dylwil3) -- [@zsol](https://github.com/zsol) -- [@renovate](https://github.com/renovate) -- [@bitloi](https://github.com/bitloi) -- [@danparizher](https://github.com/danparizher) -- [@chinar-amrutkar](https://github.com/chinar-amrutkar) -- [@second-ed](https://github.com/second-ed) -- [@getehen](https://github.com/getehen) -- [@Redovo1](https://github.com/Redovo1) -- [@matthewlloyd](https://github.com/matthewlloyd) -- [@zanieb](https://github.com/zanieb) -- [@InSyncWithFoo](https://github.com/InSyncWithFoo) -- [@RenzoMXD](https://github.com/RenzoMXD) - -## 0.15.8 - -Released on 2026-03-26. - -### Preview features - -- \[`ruff`\] New rule `unnecessary-if` (`RUF050`) ([#24114](https://github.com/astral-sh/ruff/pull/24114)) -- \[`ruff`\] New rule `useless-finally` (`RUF072`) ([#24165](https://github.com/astral-sh/ruff/pull/24165)) -- \[`ruff`\] New rule `f-string-percent-format` (`RUF073`): warn when using `%` operator on an f-string ([#24162](https://github.com/astral-sh/ruff/pull/24162)) -- \[`pyflakes`\] Recognize `frozendict` as a builtin for Python 3.15+ ([#24100](https://github.com/astral-sh/ruff/pull/24100)) - -### Bug fixes - -- \[`flake8-async`\] Use fully-qualified `anyio.lowlevel` import in autofix (`ASYNC115`) ([#24166](https://github.com/astral-sh/ruff/pull/24166)) -- \[`flake8-bandit`\] Check tuple arguments for partial paths in `S607` ([#24080](https://github.com/astral-sh/ruff/pull/24080)) -- \[`pyflakes`\] Skip `undefined-name` (`F821`) for conditionally deleted variables ([#24088](https://github.com/astral-sh/ruff/pull/24088)) -- `E501`/`W505`/formatter: Exclude nested pragma comments from line width calculation ([#24071](https://github.com/astral-sh/ruff/pull/24071)) -- Fix `%foo?` parsing in IPython assignment expressions ([#24152](https://github.com/astral-sh/ruff/pull/24152)) -- `analyze graph`: resolve string imports that reference attributes, not just modules ([#24058](https://github.com/astral-sh/ruff/pull/24058)) - -### Rule changes - -- \[`eradicate`\] ignore `ty: ignore` comments in `ERA001` ([#24192](https://github.com/astral-sh/ruff/pull/24192)) -- \[`flake8-bandit`\] Treat `sys.executable` as trusted input in `S603` ([#24106](https://github.com/astral-sh/ruff/pull/24106)) -- \[`flake8-self`\] Recognize `Self` annotation and `self` assignment in `SLF001` ([#24144](https://github.com/astral-sh/ruff/pull/24144)) -- \[`pyflakes`\] `F507`: Fix false negative for non-tuple RHS in `%`-formatting ([#24142](https://github.com/astral-sh/ruff/pull/24142)) -- \[`refurb`\] Parenthesize generator arguments in `FURB142` fixer ([#24200](https://github.com/astral-sh/ruff/pull/24200)) - -### Performance - -- Speed up diagnostic rendering ([#24146](https://github.com/astral-sh/ruff/pull/24146)) - -### Server - -- Warn when Markdown files are skipped due to preview being disabled ([#24150](https://github.com/astral-sh/ruff/pull/24150)) - -### Documentation - -- Clarify `extend-ignore` and `extend-select` settings documentation ([#24064](https://github.com/astral-sh/ruff/pull/24064)) -- Mention AI policy in PR template ([#24198](https://github.com/astral-sh/ruff/pull/24198)) - -### Other changes - -- Use trusted publishing for NPM packages ([#24171](https://github.com/astral-sh/ruff/pull/24171)) - -### Contributors - -- [@bitloi](https://github.com/bitloi) -- [@Sim-hu](https://github.com/Sim-hu) -- [@mvanhorn](https://github.com/mvanhorn) -- [@chinar-amrutkar](https://github.com/chinar-amrutkar) -- [@markjm](https://github.com/markjm) -- [@RenzoMXD](https://github.com/RenzoMXD) -- [@vivekkhimani](https://github.com/vivekkhimani) -- [@seroperson](https://github.com/seroperson) -- [@moktamd](https://github.com/moktamd) -- [@charliermarsh](https://github.com/charliermarsh) -- [@ntBre](https://github.com/ntBre) -- [@zanieb](https://github.com/zanieb) -- [@dylwil3](https://github.com/dylwil3) -- [@MichaReiser](https://github.com/MichaReiser) - -## 0.15.7 - -Released on 2026-03-19. - -### Preview features - -- Display output severity in preview ([#23845](https://github.com/astral-sh/ruff/pull/23845)) -- Don't show `noqa` hover for non-Python documents ([#24040](https://github.com/astral-sh/ruff/pull/24040)) - -### Rule changes - -- \[`pycodestyle`\] Recognize `pyrefly:` as a pragma comment (`E501`) ([#24019](https://github.com/astral-sh/ruff/pull/24019)) - -### Server - -- Don't return code actions for non-Python documents ([#23905](https://github.com/astral-sh/ruff/pull/23905)) - -### Documentation - -- Add company AI policy to contributing guide ([#24021](https://github.com/astral-sh/ruff/pull/24021)) -- Document editor features for Markdown code formatting ([#23924](https://github.com/astral-sh/ruff/pull/23924)) -- \[`pylint`\] Improve phrasing (`PLC0208`) ([#24033](https://github.com/astral-sh/ruff/pull/24033)) - -### Other changes - -- Use PEP 639 license information ([#19661](https://github.com/astral-sh/ruff/pull/19661)) - -### Contributors - -- [@tmimmanuel](https://github.com/tmimmanuel) -- [@DimitriPapadopoulos](https://github.com/DimitriPapadopoulos) -- [@amyreese](https://github.com/amyreese) -- [@statxc](https://github.com/statxc) -- [@dylwil3](https://github.com/dylwil3) -- [@hunterhogan](https://github.com/hunterhogan) -- [@renovate](https://github.com/renovate) - -## 0.15.6 - -Released on 2026-03-12. - -### Preview features - -- Add support for `lazy` import parsing ([#23755](https://github.com/astral-sh/ruff/pull/23755)) -- Add support for star-unpacking of comprehensions (PEP 798) ([#23788](https://github.com/astral-sh/ruff/pull/23788)) -- Reject semantic syntax errors for lazy imports ([#23757](https://github.com/astral-sh/ruff/pull/23757)) -- Drop a few rules from the preview default set ([#23879](https://github.com/astral-sh/ruff/pull/23879)) -- \[`airflow`\] Flag `Variable.get()` calls outside of task execution context (`AIR003`) ([#23584](https://github.com/astral-sh/ruff/pull/23584)) -- \[`airflow`\] Flag runtime-varying values in DAG/task constructor arguments (`AIR304`) ([#23631](https://github.com/astral-sh/ruff/pull/23631)) -- \[`flake8-bugbear`\] Implement `delattr-with-constant` (`B043`) ([#23737](https://github.com/astral-sh/ruff/pull/23737)) -- \[`flake8-tidy-imports`\] Add `TID254` to enforce lazy imports ([#23777](https://github.com/astral-sh/ruff/pull/23777)) -- \[`flake8-tidy-imports`\] Allow users to ban lazy imports with `TID254` ([#23847](https://github.com/astral-sh/ruff/pull/23847)) -- \[`isort`\] Retain `lazy` keyword when sorting imports ([#23762](https://github.com/astral-sh/ruff/pull/23762)) -- \[`pyupgrade`\] Add `from __future__ import annotations` automatically (`UP006`) ([#23260](https://github.com/astral-sh/ruff/pull/23260)) -- \[`refurb`\] Support `newline` parameter in `FURB101` for Python 3.13+ ([#23754](https://github.com/astral-sh/ruff/pull/23754)) -- \[`ruff`\] Add `os-path-commonprefix` (`RUF071`) ([#23814](https://github.com/astral-sh/ruff/pull/23814)) -- \[`ruff`\] Add unsafe fix for os-path-commonprefix (`RUF071`) ([#23852](https://github.com/astral-sh/ruff/pull/23852)) -- \[`ruff`\] Limit `RUF036` to typing contexts; make it unsafe for non-typing-only ([#23765](https://github.com/astral-sh/ruff/pull/23765)) -- \[`ruff`\] Use starred unpacking for `RUF017` in Python 3.15+ ([#23789](https://github.com/astral-sh/ruff/pull/23789)) - -### Bug fixes - -- Fix `--add-noqa` creating unwanted leading whitespace ([#23773](https://github.com/astral-sh/ruff/pull/23773)) -- Fix `--add-noqa` breaking shebangs ([#23577](https://github.com/astral-sh/ruff/pull/23577)) -- [formatter] Fix lambda body formatting for multiline calls and subscripts ([#23866](https://github.com/astral-sh/ruff/pull/23866)) -- [formatter] Preserve required annotation parentheses in annotated assignments ([#23865](https://github.com/astral-sh/ruff/pull/23865)) -- [formatter] Preserve type-expression parentheses in the formatter ([#23867](https://github.com/astral-sh/ruff/pull/23867)) -- \[`flake8-annotations`\] Fix stack overflow in `ANN401` on quoted annotations with escape sequences ([#23912](https://github.com/astral-sh/ruff/pull/23912)) -- \[`pep8-naming`\] Check naming conventions in `match` pattern bindings (`N806`, `N815`, `N816`) ([#23899](https://github.com/astral-sh/ruff/pull/23899)) -- \[`perflint`\] Fix comment duplication in fixes (`PERF401`, `PERF403`) ([#23729](https://github.com/astral-sh/ruff/pull/23729)) -- \[`pyupgrade`\] Properly trigger `super` change in nested class (`UP008`) ([#22677](https://github.com/astral-sh/ruff/pull/22677)) -- \[`ruff`\] Avoid syntax errors in `RUF036` fixes ([#23764](https://github.com/astral-sh/ruff/pull/23764)) - -### Rule changes - -- \[`flake8-bandit`\] Flag `S501` with `requests.request` ([#23873](https://github.com/astral-sh/ruff/pull/23873)) -- \[`flake8-executable`\] Fix WSL detection in non-Docker containers ([#22879](https://github.com/astral-sh/ruff/pull/22879)) -- \[`flake8-print`\] Ignore `pprint` calls with `stream=` ([#23787](https://github.com/astral-sh/ruff/pull/23787)) - -### Documentation - -- Update docs for Markdown code block formatting ([#23871](https://github.com/astral-sh/ruff/pull/23871)) -- \[`flake8-bugbear`\] Fix misleading description for `B904` ([#23731](https://github.com/astral-sh/ruff/pull/23731)) - -### Contributors - -- [@zsol](https://github.com/zsol) -- [@carljm](https://github.com/carljm) -- [@ntBre](https://github.com/ntBre) -- [@Bortlesboat](https://github.com/Bortlesboat) -- [@sososonia-cyber](https://github.com/sososonia-cyber) -- [@chirizxc](https://github.com/chirizxc) -- [@leandrobbraga](https://github.com/leandrobbraga) -- [@11happy](https://github.com/11happy) -- [@Acelogic](https://github.com/Acelogic) -- [@anishgirianish](https://github.com/anishgirianish) -- [@amyreese](https://github.com/amyreese) -- [@xvchris](https://github.com/xvchris) -- [@charliermarsh](https://github.com/charliermarsh) -- [@getehen](https://github.com/getehen) -- [@Dev-iL](https://github.com/Dev-iL) - -## 0.15.5 - -Released on 2026-03-05. - -### Preview features - -- Discover Markdown files by default in preview mode ([#23434](https://github.com/astral-sh/ruff/pull/23434)) -- \[`perflint`\] Extend `PERF102` to comprehensions and generators ([#23473](https://github.com/astral-sh/ruff/pull/23473)) -- \[`refurb`\] Fix `FURB101` and `FURB103` false positives when I/O variable is used later ([#23542](https://github.com/astral-sh/ruff/pull/23542)) -- \[`ruff`\] Add fix for `none-not-at-end-of-union` (`RUF036`) ([#22829](https://github.com/astral-sh/ruff/pull/22829)) -- \[`ruff`\] Fix false positive for `re.split` with empty string pattern (`RUF055`) ([#23634](https://github.com/astral-sh/ruff/pull/23634)) - -### Bug fixes - -- \[`fastapi`\] Handle callable class dependencies with `__call__` method (`FAST003`) ([#23553](https://github.com/astral-sh/ruff/pull/23553)) -- \[`pydocstyle`\] Fix numpy section ordering (`D420`) ([#23685](https://github.com/astral-sh/ruff/pull/23685)) -- \[`pyflakes`\] Fix false positive for names shadowing re-exports (`F811`) ([#23356](https://github.com/astral-sh/ruff/pull/23356)) -- \[`pyupgrade`\] Avoid inserting redundant `None` elements in `UP045` ([#23459](https://github.com/astral-sh/ruff/pull/23459)) - -### Documentation - -- Document extension mapping for Markdown code formatting ([#23574](https://github.com/astral-sh/ruff/pull/23574)) -- Update default Python version examples ([#23605](https://github.com/astral-sh/ruff/pull/23605)) - -### Other changes - -- Publish releases to Astral mirror ([#23616](https://github.com/astral-sh/ruff/pull/23616)) - -### Contributors - -- [@amyreese](https://github.com/amyreese) -- [@stakeswky](https://github.com/stakeswky) -- [@chirizxc](https://github.com/chirizxc) -- [@anishgirianish](https://github.com/anishgirianish) -- [@bxff](https://github.com/bxff) -- [@zsol](https://github.com/zsol) -- [@charliermarsh](https://github.com/charliermarsh) -- [@ntBre](https://github.com/ntBre) -- [@kar-ganap](https://github.com/kar-ganap) - -## 0.15.4 - -Released on 2026-02-26. - -This is a follow-up release to 0.15.3 that resolves a panic when the new rule `PLR1712` was enabled with any rule that analyzes definitions, such as many of the `ANN` or `D` rules. - -### Bug fixes - -- Fix panic on access to definitions after analyzing definitions ([#23588](https://github.com/astral-sh/ruff/pull/23588)) -- \[`pyflakes`\] Suppress false positive in `F821` for names used before `del` in stub files ([#23550](https://github.com/astral-sh/ruff/pull/23550)) - -### Documentation - -- Clarify first-party import detection in Ruff ([#23591](https://github.com/astral-sh/ruff/pull/23591)) -- Fix incorrect `import-heading` example ([#23568](https://github.com/astral-sh/ruff/pull/23568)) - -### Contributors - -- [@stakeswky](https://github.com/stakeswky) -- [@ntBre](https://github.com/ntBre) -- [@thejcannon](https://github.com/thejcannon) -- [@GeObts](https://github.com/GeObts) - -## 0.15.3 - -Released on 2026-02-26. - -### Preview features - -- Drop explicit support for `.qmd` file extension ([#23572](https://github.com/astral-sh/ruff/pull/23572)) - - This can now be enabled instead by setting the [`extension`](https://docs.astral.sh/ruff/settings/#extension) option: - - ```toml - # ruff.toml - extension = { qmd = "markdown" } - - # pyproject.toml - [tool.ruff] - extension = { qmd = "markdown" } - ``` - -- Include configured extensions in file discovery ([#23400](https://github.com/astral-sh/ruff/pull/23400)) - -- \[`flake8-bandit`\] Allow suspicious imports in `TYPE_CHECKING` blocks (`S401`-`S415`) ([#23441](https://github.com/astral-sh/ruff/pull/23441)) - -- \[`flake8-bugbear`\] Allow `B901` in pytest hook wrappers ([#21931](https://github.com/astral-sh/ruff/pull/21931)) - -- \[`flake8-import-conventions`\] Add missing conventions from upstream (`ICN001`, `ICN002`) ([#21373](https://github.com/astral-sh/ruff/pull/21373)) - -- \[`pydocstyle`\] Add rule to enforce docstring section ordering (`D420`) ([#23537](https://github.com/astral-sh/ruff/pull/23537)) - -- \[`pylint`\] Implement `swap-with-temporary-variable` (`PLR1712`) ([#22205](https://github.com/astral-sh/ruff/pull/22205)) - -- \[`ruff`\] Add `unnecessary-assign-before-yield` (`RUF070`) ([#23300](https://github.com/astral-sh/ruff/pull/23300)) - -- \[`ruff`\] Support file-level noqa in `RUF102` ([#23535](https://github.com/astral-sh/ruff/pull/23535)) - -- \[`ruff`\] Suppress diagnostic for invalid f-strings before Python 3.12 (`RUF027`) ([#23480](https://github.com/astral-sh/ruff/pull/23480)) - -- \[`flake8-bandit`\] Don't flag `BaseLoader`/`CBaseLoader` as unsafe (`S506`) ([#23510](https://github.com/astral-sh/ruff/pull/23510)) - -### Bug fixes - -- Avoid infinite loop between `I002` and `PYI025` ([#23352](https://github.com/astral-sh/ruff/pull/23352)) -- \[`pyflakes`\] Fix false positive for `@overload` from `lint.typing-modules` (`F811`) ([#23357](https://github.com/astral-sh/ruff/pull/23357)) -- \[`pyupgrade`\] Fix false positive for `TypeVar` default before Python 3.12 (`UP046`) ([#23540](https://github.com/astral-sh/ruff/pull/23540)) -- \[`pyupgrade`\] Fix handling of `\N` in raw strings (`UP032`) ([#22149](https://github.com/astral-sh/ruff/pull/22149)) - -### Rule changes - -- Render sub-diagnostics in the GitHub output format ([#23455](https://github.com/astral-sh/ruff/pull/23455)) - -- \[`flake8-bugbear`\] Tag certain `B007` diagnostics as unnecessary ([#23453](https://github.com/astral-sh/ruff/pull/23453)) - -- \[`ruff`\] Ignore unknown rule codes in `RUF100` ([#23531](https://github.com/astral-sh/ruff/pull/23531)) - - These are now flagged by [`RUF102`](https://docs.astral.sh/ruff/rules/invalid-rule-code/) instead. - -### Documentation - -- Fix missing settings links for several linters ([#23519](https://github.com/astral-sh/ruff/pull/23519)) -- Update isort action comments heading ([#23515](https://github.com/astral-sh/ruff/pull/23515)) -- \[`pydocstyle`\] Fix double comma in description of `D404` ([#23440](https://github.com/astral-sh/ruff/pull/23440)) - -### Other changes - -- Update the Python module (notably `find_ruff_bin`) for parity with uv ([#23406](https://github.com/astral-sh/ruff/pull/23406)) - -### Contributors - -- [@zanieb](https://github.com/zanieb) -- [@o1x3](https://github.com/o1x3) -- [@assadyousuf](https://github.com/assadyousuf) -- [@kar-ganap](https://github.com/kar-ganap) -- [@denyszhak](https://github.com/denyszhak) -- [@amyreese](https://github.com/amyreese) -- [@carljm](https://github.com/carljm) -- [@anishgirianish](https://github.com/anishgirianish) -- [@Bnyro](https://github.com/Bnyro) -- [@danparizher](https://github.com/danparizher) -- [@ntBre](https://github.com/ntBre) -- [@gcomneno](https://github.com/gcomneno) -- [@jaap3](https://github.com/jaap3) -- [@stakeswky](https://github.com/stakeswky) - -## 0.15.2 - -Released on 2026-02-19. - -### Preview features - -- Expand the default rule set ([#23385](https://github.com/astral-sh/ruff/pull/23385)) - - In preview, Ruff now enables a significantly expanded default rule set of 412 - rules, up from the stable default set of 59 rules. The new rules are mostly a - superset of the stable defaults, with the exception of these rules, which are - removed from the preview defaults: - - - [`multiple-imports-on-one-line`](https://docs.astral.sh/ruff/rules/multiple-imports-on-one-line) (`E401`) - - [`module-import-not-at-top-of-file`](https://docs.astral.sh/ruff/rules/module-import-not-at-top-of-file) (`E402`) - - [`module-import-not-at-top-of-file`](https://docs.astral.sh/ruff/rules/module-import-not-at-top-of-file) (`E701`) - - [`multiple-statements-on-one-line-semicolon`](https://docs.astral.sh/ruff/rules/multiple-statements-on-one-line-semicolon) (`E702`) - - [`useless-semicolon`](https://docs.astral.sh/ruff/rules/useless-semicolon) (`E703`) - - [`none-comparison`](https://docs.astral.sh/ruff/rules/none-comparison) (`E711`) - - [`true-false-comparison`](https://docs.astral.sh/ruff/rules/true-false-comparison) (`E712`) - - [`not-in-test`](https://docs.astral.sh/ruff/rules/not-in-test) (`E713`) - - [`not-is-test`](https://docs.astral.sh/ruff/rules/not-is-test) (`E714`) - - [`type-comparison`](https://docs.astral.sh/ruff/rules/type-comparison) (`E721`) - - [`lambda-assignment`](https://docs.astral.sh/ruff/rules/lambda-assignment) (`E731`) - - [`ambiguous-variable-name`](https://docs.astral.sh/ruff/rules/ambiguous-variable-name) (`E741`) - - [`ambiguous-class-name`](https://docs.astral.sh/ruff/rules/ambiguous-class-name) (`E742`) - - [`ambiguous-function-name`](https://docs.astral.sh/ruff/rules/ambiguous-function-name) (`E743`) - - [`undefined-local-with-import-star`](https://docs.astral.sh/ruff/rules/undefined-local-with-import-star) (`F403`) - - [`undefined-local-with-import-star-usage`](https://docs.astral.sh/ruff/rules/undefined-local-with-import-star-usage) (`F405`) - - [`undefined-local-with-nested-import-star-usage`](https://docs.astral.sh/ruff/rules/undefined-local-with-nested-import-star-usage) (`F406`) - - [`forward-annotation-syntax-error`](https://docs.astral.sh/ruff/rules/forward-annotation-syntax-error) (`F722`) - - If you use preview and prefer the old defaults, you can restore them with - configuration like: - - ```toml - - # ruff.toml - - [lint] - select = ["E4", "E7", "E9", "F"] - - # pyproject.toml - - [tool.ruff.lint] - select = ["E4", "E7", "E9", "F"] - ``` - - If you do give them a try, feel free to share your feedback in the [GitHub - discussion](https://github.com/astral-sh/ruff/discussions/23203)! - -- \[`flake8-pyi`\] Also check string annotations (`PYI041`) ([#19023](https://github.com/astral-sh/ruff/pull/19023)) - -### Bug fixes - -- \[`flake8-async`\] Fix `in_async_context` logic ([#23426](https://github.com/astral-sh/ruff/pull/23426)) -- \[`ruff`\] Fix for `RUF102` should delete entire comment ([#23380](https://github.com/astral-sh/ruff/pull/23380)) -- \[`ruff`\] Suppress diagnostic for strings with backslashes in interpolations before Python 3.12 (`RUF027`) ([#21069](https://github.com/astral-sh/ruff/pull/21069)) -- \[`flake8-bugbear`\] Fix `B023` false positive for immediately-invoked lambdas ([#23294](https://github.com/astral-sh/ruff/pull/23294)) -- [parser] Fix false syntax error for match-like annotated assignments ([#23297](https://github.com/astral-sh/ruff/pull/23297)) -- [parser] Fix indentation tracking after line continuations ([#23417](https://github.com/astral-sh/ruff/pull/23417)) - -### Rule changes - -- \[`flake8-executable`\] Allow global flags in uv shebangs (`EXE003`) ([#22582](https://github.com/astral-sh/ruff/pull/22582)) -- \[`pyupgrade`\] Fix handling of `typing.{io,re}` (`UP035`) ([#23131](https://github.com/astral-sh/ruff/pull/23131)) -- \[`ruff`\] Detect `PLC0207` on chained `str.split()` calls ([#23275](https://github.com/astral-sh/ruff/pull/23275)) - -### CLI - -- Remove invalid inline `noqa` warning ([#23270](https://github.com/astral-sh/ruff/pull/23270)) - -### Configuration - -- Add extension mapping to configuration file options ([#23384](https://github.com/astral-sh/ruff/pull/23384)) - -### Documentation - -- Add `Q004` to the list of conflicting rules ([#23340](https://github.com/astral-sh/ruff/pull/23340)) -- \[`ruff`\] Expand `lint.external` docs and add sub-diagnostic (`RUF100`, `RUF102`) ([#23268](https://github.com/astral-sh/ruff/pull/23268)) - -### Contributors - -- [@dylwil3](https://github.com/dylwil3) -- [@Jkhall81](https://github.com/Jkhall81) -- [@danparizher](https://github.com/danparizher) -- [@dhruvmanila](https://github.com/dhruvmanila) -- [@harupy](https://github.com/harupy) -- [@ngnpope](https://github.com/ngnpope) -- [@amyreese](https://github.com/amyreese) -- [@kar-ganap](https://github.com/kar-ganap) -- [@robsdedude](https://github.com/robsdedude) -- [@shaanmajid](https://github.com/shaanmajid) -- [@ntBre](https://github.com/ntBre) -- [@toslunar](https://github.com/toslunar) - -## 0.15.1 - -Released on 2026-02-12. - -### Preview features - -- \[`airflow`\] Add ruff rules to catch deprecated Airflow imports for Airflow 3.1 (`AIR321`) ([#22376](https://github.com/astral-sh/ruff/pull/22376)) -- \[`airflow`\] Third positional parameter not named `ti_key` should be flagged for `BaseOperatorLink.get_link` (`AIR303`) ([#22828](https://github.com/astral-sh/ruff/pull/22828)) -- \[`flake8-gettext`\] Fix false negatives for plural argument of `ngettext` (`INT001`, `INT002`, `INT003`) ([#21078](https://github.com/astral-sh/ruff/pull/21078)) -- \[`pyflakes`\] Fix infinite loop in preview fix for `unused-import` (`F401`) ([#23038](https://github.com/astral-sh/ruff/pull/23038)) -- \[`pygrep-hooks`\] Detect non-existent mock methods in standalone expressions (`PGH005`) ([#22830](https://github.com/astral-sh/ruff/pull/22830)) -- \[`pylint`\] Allow dunder submodules and improve diagnostic range (`PLC2701`) ([#22804](https://github.com/astral-sh/ruff/pull/22804)) -- \[`pyupgrade`\] Improve diagnostic range for tuples (`UP024`) ([#23013](https://github.com/astral-sh/ruff/pull/23013)) -- \[`refurb`\] Check subscripts in tuple do not use lambda parameters in `reimplemented-operator` (`FURB118`) ([#23079](https://github.com/astral-sh/ruff/pull/23079)) -- \[`ruff`\] Detect mutable defaults in `field` calls (`RUF008`) ([#23046](https://github.com/astral-sh/ruff/pull/23046)) -- \[`ruff`\] Ignore std `cmath.inf` (`RUF069`) ([#23120](https://github.com/astral-sh/ruff/pull/23120)) -- \[`ruff`\] New rule `float-equality-comparison` (`RUF069`) ([#20585](https://github.com/astral-sh/ruff/pull/20585)) -- Don't format unlabeled Markdown code blocks ([#23106](https://github.com/astral-sh/ruff/pull/23106)) -- Markdown formatting support in LSP ([#23063](https://github.com/astral-sh/ruff/pull/23063)) -- Support Quarto Markdown language markers ([#22947](https://github.com/astral-sh/ruff/pull/22947)) -- Support formatting `pycon` Markdown code blocks ([#23112](https://github.com/astral-sh/ruff/pull/23112)) -- Use extension mapping to select Markdown code block language ([#22934](https://github.com/astral-sh/ruff/pull/22934)) - -### Bug fixes - -- Avoid false positive for undefined variables in `FAST001` ([#23224](https://github.com/astral-sh/ruff/pull/23224)) -- Avoid introducing syntax errors for `FAST003` autofix ([#23227](https://github.com/astral-sh/ruff/pull/23227)) -- Avoid suggesting `InitVar` for `__post_init__` that references PEP 695 type parameters ([#23226](https://github.com/astral-sh/ruff/pull/23226)) -- Deduplicate type variables in generic functions ([#23225](https://github.com/astral-sh/ruff/pull/23225)) -- Fix exception handler parenthesis removal for Python 3.14+ ([#23126](https://github.com/astral-sh/ruff/pull/23126)) -- Fix f-string middle panic when parsing t-strings ([#23232](https://github.com/astral-sh/ruff/pull/23232)) -- Wrap `RUF020` target for multiline fixes ([#23210](https://github.com/astral-sh/ruff/pull/23210)) -- Wrap `UP007` target for multiline fixes ([#23208](https://github.com/astral-sh/ruff/pull/23208)) -- Fix missing diagnostics for last range suppression in file ([#23242](https://github.com/astral-sh/ruff/pull/23242)) -- \[`pyupgrade`\] Fix syntax error on string with newline escape and comment (`UP037`) ([#22968](https://github.com/astral-sh/ruff/pull/22968)) - -### Rule changes - -- Use `ruff` instead of `Ruff` as the program name in GitHub output format ([#23240](https://github.com/astral-sh/ruff/pull/23240)) -- \[`PT006`\] Fix syntax error when unpacking nested tuples in `parametrize` fixes (#22441) ([#22464](https://github.com/astral-sh/ruff/pull/22464)) -- \[`airflow`\] Catch deprecated attribute access from context key for Airflow 3.0 (`AIR301`) ([#22850](https://github.com/astral-sh/ruff/pull/22850)) -- \[`airflow`\] Capture deprecated arguments and a decorator (`AIR301`) ([#23170](https://github.com/astral-sh/ruff/pull/23170)) -- \[`flake8-boolean-trap`\] Add `multiprocessing.Value` to excluded functions for `FBT003` ([#23010](https://github.com/astral-sh/ruff/pull/23010)) -- \[`flake8-bugbear`\] Add a secondary annotation showing the previous occurrence (`B033`) ([#22634](https://github.com/astral-sh/ruff/pull/22634)) -- \[`flake8-type-checking`\] Add sub-diagnostic showing the runtime use of an annotation (`TC004`) ([#23091](https://github.com/astral-sh/ruff/pull/23091)) -- \[`isort`\] Support configurable import section heading comments ([#23151](https://github.com/astral-sh/ruff/pull/23151)) -- \[`ruff`\] Improve the diagnostic for `RUF012` ([#23202](https://github.com/astral-sh/ruff/pull/23202)) - -### Formatter - -- Suppress diagnostic output for `format --check --silent` ([#17736](https://github.com/astral-sh/ruff/pull/17736)) - -### Documentation - -- Add tabbed shell completion documentation ([#23169](https://github.com/astral-sh/ruff/pull/23169)) -- Explain how to enable Markdown formatting for pre-commit hook ([#23077](https://github.com/astral-sh/ruff/pull/23077)) -- Fixed import in `runtime-evaluated-decorators` example ([#23187](https://github.com/astral-sh/ruff/pull/23187)) -- Update ruff server contributing guide ([#23060](https://github.com/astral-sh/ruff/pull/23060)) - -### Other changes - -- Exclude WASM artifacts from GitHub releases ([#23221](https://github.com/astral-sh/ruff/pull/23221)) - -### Contributors - -- [@mkniewallner](https://github.com/mkniewallner) -- [@bxff](https://github.com/bxff) -- [@dylwil3](https://github.com/dylwil3) -- [@Avasam](https://github.com/Avasam) -- [@amyreese](https://github.com/amyreese) -- [@charliermarsh](https://github.com/charliermarsh) -- [@Alex-ley-scrub](https://github.com/Alex-ley-scrub) -- [@Kalmaegi](https://github.com/Kalmaegi) -- [@danparizher](https://github.com/danparizher) -- [@AiyionPrime](https://github.com/AiyionPrime) -- [@eureka928](https://github.com/eureka928) -- [@11happy](https://github.com/11happy) -- [@Jkhall81](https://github.com/Jkhall81) -- [@chirizxc](https://github.com/chirizxc) -- [@leandrobbraga](https://github.com/leandrobbraga) -- [@tvatter](https://github.com/tvatter) -- [@anishgirianish](https://github.com/anishgirianish) -- [@shaanmajid](https://github.com/shaanmajid) -- [@ntBre](https://github.com/ntBre) -- [@sjyangkevin](https://github.com/sjyangkevin) - -## 0.15.0 - -Released on 2026-02-03. - -Check out the [blog post](https://astral.sh/blog/ruff-v0.15.0) for a migration -guide and overview of the changes! - -### Breaking changes - -- Ruff now formats your code according to the 2026 style guide. See the formatter section below or in the blog post for a detailed list of changes. - -- The linter now supports block suppression comments. For example, to suppress `N803` for all parameters in this function: - - ```python - # ruff: disable[N803] - def foo( - legacyArg1, - legacyArg2, - legacyArg3, - legacyArg4, - ): ... - # ruff: enable[N803] - ``` - - See the [documentation](https://docs.astral.sh/ruff/linter/#block-level) for more details. - -- The `ruff:alpine` Docker image is now based on Alpine 3.23 (up from 3.21). - -- The `ruff:debian` and `ruff:debian-slim` Docker images are now based on Debian 13 "Trixie" instead of Debian 12 "Bookworm." - -- Binaries for the `ppc64` (64-bit big-endian PowerPC) architecture are no longer included in our releases. It should still be possible to build Ruff manually for this platform, if needed. - -- Ruff now resolves all `extend`ed configuration files before falling back on a default Python version. - -### Stabilization - -The following rules have been stabilized and are no longer in preview: - -- [`blocking-http-call-httpx-in-async-function`](https://docs.astral.sh/ruff/rules/blocking-http-call-httpx-in-async-function) - (`ASYNC212`) -- [`blocking-path-method-in-async-function`](https://docs.astral.sh/ruff/rules/blocking-path-method-in-async-function) - (`ASYNC240`) -- [`blocking-input-in-async-function`](https://docs.astral.sh/ruff/rules/blocking-input-in-async-function) - (`ASYNC250`) -- [`map-without-explicit-strict`](https://docs.astral.sh/ruff/rules/map-without-explicit-strict) - (`B912`) -- [`if-exp-instead-of-or-operator`](https://docs.astral.sh/ruff/rules/if-exp-instead-of-or-operator) - (`FURB110`) -- [`single-item-membership-test`](https://docs.astral.sh/ruff/rules/single-item-membership-test) - (`FURB171`) -- [`missing-maxsplit-arg`](https://docs.astral.sh/ruff/rules/missing-maxsplit-arg) (`PLC0207`) -- [`unnecessary-lambda`](https://docs.astral.sh/ruff/rules/unnecessary-lambda) (`PLW0108`) -- [`unnecessary-empty-iterable-within-deque-call`](https://docs.astral.sh/ruff/rules/unnecessary-empty-iterable-within-deque-call) - (`RUF037`) -- [`in-empty-collection`](https://docs.astral.sh/ruff/rules/in-empty-collection) (`RUF060`) -- [`legacy-form-pytest-raises`](https://docs.astral.sh/ruff/rules/legacy-form-pytest-raises) - (`RUF061`) -- [`non-octal-permissions`](https://docs.astral.sh/ruff/rules/non-octal-permissions) (`RUF064`) -- [`invalid-rule-code`](https://docs.astral.sh/ruff/rules/invalid-rule-code) (`RUF102`) -- [`invalid-suppression-comment`](https://docs.astral.sh/ruff/rules/invalid-suppression-comment) - (`RUF103`) -- [`unmatched-suppression-comment`](https://docs.astral.sh/ruff/rules/unmatched-suppression-comment) - (`RUF104`) -- [`replace-str-enum`](https://docs.astral.sh/ruff/rules/replace-str-enum) (`UP042`) - -The following behaviors have been stabilized: - -- The `--output-format` flag is now respected when running Ruff in `--watch` mode, and the `full` output format is now used by default, matching the regular CLI output. -- [`builtin-attribute-shadowing`](https://docs.astral.sh/ruff/rules/builtin-attribute-shadowing/) (`A003`) now detects the use of shadowed built-in names in additional contexts like decorators, default arguments, and other attribute definitions. -- [`duplicate-union-member`](https://docs.astral.sh/ruff/rules/duplicate-union-member/) (`PYI016`) now considers `typing.Optional` when searching for duplicate union members. -- [`split-static-string`](https://docs.astral.sh/ruff/rules/split-static-string/) (`SIM905`) now offers an autofix when the `maxsplit` argument is provided, even without a `sep` argument. -- [`dict-get-with-none-default`](https://docs.astral.sh/ruff/rules/dict-get-with-none-default/) (`SIM910`) now applies to more types of key expressions. -- [`super-call-with-parameters`](https://docs.astral.sh/ruff/rules/super-call-with-parameters/) (`UP008`) now has a safe fix when it will not delete comments. -- [`unnecessary-default-type-args`](https://docs.astral.sh/ruff/rules/unnecessary-default-type-args/) (`UP043`) now applies to stub (`.pyi`) files on Python versions before 3.13. - -### Formatter - -This release introduces the new 2026 style guide, with the following changes: - -- Lambda parameters are now kept on the same line and lambda bodies will be parenthesized to let - them break across multiple lines ([#21385](https://github.com/astral-sh/ruff/pull/21385)) -- Parentheses around tuples of exceptions in `except` clauses will now be removed on Python 3.14 and - later ([#20768](https://github.com/astral-sh/ruff/pull/20768)) -- A single empty line is now permitted at the beginning of function bodies ([#21110](https://github.com/astral-sh/ruff/pull/21110)) -- Parentheses are avoided for long `as` captures in `match` statements ([#21176](https://github.com/astral-sh/ruff/pull/21176)) -- Extra spaces between escaped quotes and ending triple quotes can now be omitted ([#17216](https://github.com/astral-sh/ruff/pull/17216)) -- Blank lines are now enforced before classes with decorators in stub files ([#18888](https://github.com/astral-sh/ruff/pull/18888)) - -### Preview features - -- Apply formatting to Markdown code blocks ([#22470](https://github.com/astral-sh/ruff/pull/22470), [#22990](https://github.com/astral-sh/ruff/pull/22990), [#22996](https://github.com/astral-sh/ruff/pull/22996)) - - See the [documentation](https://docs.astral.sh/ruff/formatter/#markdown-code-formatting) for more details. - -### Bug fixes - -- Fix suppression indentation matching ([#22903](https://github.com/astral-sh/ruff/pull/22903)) - -### Rule changes - -- Customize where the `fix_title` sub-diagnostic appears ([#23044](https://github.com/astral-sh/ruff/pull/23044)) -- \[`FastAPI`\] Add sub-diagnostic explaining why a fix was unavailable (`FAST002`) ([#22565](https://github.com/astral-sh/ruff/pull/22565)) -- \[`flake8-annotations`\] Don't suggest `NoReturn` for functions raising `NotImplementedError` (`ANN201`, `ANN202`, `ANN205`, `ANN206`) ([#21311](https://github.com/astral-sh/ruff/pull/21311)) -- \[`pyupgrade`\] Make fix unsafe if it deletes comments (`UP017`) ([#22873](https://github.com/astral-sh/ruff/pull/22873)) -- \[`pyupgrade`\] Make fix unsafe if it deletes comments (`UP020`) ([#22872](https://github.com/astral-sh/ruff/pull/22872)) -- \[`pyupgrade`\] Make fix unsafe if it deletes comments (`UP033`) ([#22871](https://github.com/astral-sh/ruff/pull/22871)) -- \[`refurb`\] Do not add `abc.ABC` if already present (`FURB180`) ([#22234](https://github.com/astral-sh/ruff/pull/22234)) -- \[`refurb`\] Make fix unsafe if it deletes comments (`FURB110`) ([#22768](https://github.com/astral-sh/ruff/pull/22768)) -- \[`ruff`\] Add sub-diagnostics with permissions (`RUF064`) ([#22972](https://github.com/astral-sh/ruff/pull/22972)) - -### Server - -- Identify notebooks by LSP `didOpen` instead of `.ipynb` file extension ([#22810](https://github.com/astral-sh/ruff/pull/22810)) - -### CLI - -- Add `--color` CLI option to force colored output ([#22806](https://github.com/astral-sh/ruff/pull/22806)) - -### Documentation - -- Document `-` stdin convention in CLI help text ([#22817](https://github.com/astral-sh/ruff/pull/22817)) -- \[`refurb`\] Change example to `re.search` with `^` anchor (`FURB167`) ([#22984](https://github.com/astral-sh/ruff/pull/22984)) -- Fix link to Sphinx code block directives ([#23041](https://github.com/astral-sh/ruff/pull/23041)) -- \[`pydocstyle`\] Clarify which quote styles are allowed (`D300`) ([#22825](https://github.com/astral-sh/ruff/pull/22825)) -- \[`flake8-bugbear`\] Improve docs for `no-explicit-stacklevel` (`B028`) ([#22538](https://github.com/astral-sh/ruff/pull/22538)) - -### Other changes - -- Update MSRV to 1.91 ([#22874](https://github.com/astral-sh/ruff/pull/22874)) - -### Contributors - -- [@danparizher](https://github.com/danparizher) -- [@chirizxc](https://github.com/chirizxc) -- [@amyreese](https://github.com/amyreese) -- [@Jkhall81](https://github.com/Jkhall81) -- [@cwkang1998](https://github.com/cwkang1998) -- [@manzt](https://github.com/manzt) -- [@11happy](https://github.com/11happy) -- [@hugovk](https://github.com/hugovk) -- [@caiquejjx](https://github.com/caiquejjx) -- [@ntBre](https://github.com/ntBre) -- [@akawd](https://github.com/akawd) -- [@konstin](https://github.com/konstin) +See [changelogs/0.15.x](./changelogs/0.15.x.md) ## 0.14.x diff --git a/Cargo.lock b/Cargo.lock index 1d26333bb2..ba1fb6ac0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,9 +10,9 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] @@ -155,9 +155,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.103" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "approx" @@ -215,7 +215,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -229,7 +229,7 @@ dependencies = [ "manyhow", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -245,7 +245,7 @@ dependencies = [ "proc-macro2", "quote", "quote-use", - "syn", + "syn 2.0.119", ] [[package]] @@ -277,9 +277,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.13.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" dependencies = [ "serde_core", ] @@ -456,7 +456,7 @@ checksum = "89385e82b5d1821d2219e0b095efa2cc1f246cbf99080f3be46a1a85c0d392d9" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -476,9 +476,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.2.4" +version = "1.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f2d30e4173c4026932d51d31d6b0613b1fd3014bf3f9f8943d4ba139c437ba0" +checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d" dependencies = [ "serde_core", ] @@ -586,9 +586,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.1" +version = "4.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +checksum = "301b56658598e48f3648647ac6fc887be7e7108eddfa4e9b63fcf3ec58c0cadf" dependencies = [ "clap_builder", "clap_derive", @@ -596,9 +596,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.0" +version = "4.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +checksum = "94a65403d1a1bd28f7dc68eb8506e8874808ee5eecb59298de588e2e1407a078" dependencies = [ "anstream 1.0.0", "anstyle", @@ -639,14 +639,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.1" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] @@ -663,9 +663,9 @@ checksum = "d669bb552908e336ad5681789752033b45566b7e591aeaac7a614e58e5d6d8f2" dependencies = [ "nix", "terminfo", - "thiserror 2.0.18", + "thiserror 2.0.19", "which", - "windows-sys 0.61.0", + "windows-sys 0.59.0", ] [[package]] @@ -748,7 +748,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -784,7 +784,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] @@ -831,9 +831,9 @@ dependencies = [ [[package]] name = "console_log" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be8aed40e4edbf4d3b4431ab260b63fdc40f5780a4766824329ea0f1eefe3c0f" +checksum = "86919cef3e37b9356ccf54d4421208c17ecfda01beae61393e7ffd72916c0ef1" dependencies = [ "log", "web-sys", @@ -1047,7 +1047,7 @@ checksum = "a867d7322eb69cf3a68a5426387a25b45cb3b9c5ee41023ee6cea92e2afadd82" dependencies = [ "camino", "fancy-regex", - "libtest-mimic 0.8.1", + "libtest-mimic", "walkdir", ] @@ -1071,7 +1071,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1080,7 +1080,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" dependencies = [ - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -1091,7 +1091,7 @@ checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1134,7 +1134,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.0", + "windows-sys 0.59.0", ] [[package]] @@ -1143,7 +1143,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2", "libc", "objc2", @@ -1157,7 +1157,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1168,7 +1168,7 @@ checksum = "8dc51d98e636f5e3b0759a39257458b22619cac7e96d932da6eeb052891bb67c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1214,7 +1214,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] @@ -1374,9 +1374,9 @@ dependencies = [ [[package]] name = "gen-lsp-types" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd635c5206acd03ea024d6b5902539e5c903de3afa220fdb5c94b583af77f4f" +checksum = "b64887ac3a8083427ae935a7296db876871582cd57eac077564f8bc18fa49116" dependencies = [ "serde", "serde_json", @@ -1401,7 +1401,7 @@ checksum = "c736d226c32e496b8377813b52269e11ad3a48d8373b68862d0364f04fd1229d" dependencies = [ "attribute-derive", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1468,15 +1468,15 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" [[package]] name = "globset" -version = "0.4.18" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +checksum = "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988" dependencies = [ "aho-corasick", "bstr", @@ -1491,7 +1491,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "ignore", "walkdir", ] @@ -1697,9 +1697,9 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.30" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b009b6744c1445efd7244084e25e498636412effb6760b55067553baa925cc7" +checksum = "00b69833ed729dc5aa7d19541d96d6cf8e9137194207a04916d658e43168402f" dependencies = [ "crossbeam-deque", "globset", @@ -1772,7 +1772,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "inotify-sys", "libc", ] @@ -1848,7 +1848,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1912,11 +1912,12 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.32" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "961d16382652bfdd8c6f68b223b26a8c93e0d475c672f414411db31c6c5c900e" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" dependencies = [ "defmt", + "jiff-core", "jiff-static", "jiff-tzdb-platform", "log", @@ -1926,15 +1927,25 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + [[package]] name = "jiff-static" -version = "0.2.32" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0879bd39df99c4c5e2c6615ccc026391a423dde10532c573e6086eb94a802cc" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" dependencies = [ + "jiff-core", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2013,9 +2024,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.186" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libcst" @@ -2029,7 +2040,7 @@ dependencies = [ "paste", "peg", "regex", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -2039,7 +2050,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0903173ea316c34a44d0497161e04d9210af44f5f5e89bf2f55d9a254c9a0e8d" dependencies = [ "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2063,22 +2074,10 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "libc", ] -[[package]] -name = "libtest-mimic" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc0bda45ed5b3a2904262c1bb91e526127aa70e7ef3758aba2ef93cf896b9b58" -dependencies = [ - "clap", - "escape8259", - "termcolor", - "threadpool", -] - [[package]] name = "libtest-mimic" version = "0.8.1" @@ -2149,7 +2148,7 @@ dependencies = [ "manyhow-macros", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2210,10 +2209,10 @@ dependencies = [ "rustc-stable-hash", "salsa", "serde", - "similar 3.1.1", + "similar 3.1.2", "smallvec", - "thiserror 2.0.18", - "toml 1.1.3+spec-1.1.0", + "thiserror 2.0.19", + "toml 1.1.4+spec-1.1.0", "toml_parser", "tracing", ] @@ -2296,7 +2295,7 @@ checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2320,7 +2319,7 @@ version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "cfg-if", "cfg_aliases", "libc", @@ -2348,7 +2347,7 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "fsevent-sys", "inotify", "kqueue", @@ -2385,16 +2384,6 @@ dependencies = [ "libm", ] -[[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "objc2" version = "0.6.3" @@ -2605,7 +2594,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8" dependencies = [ "memchr", - "thiserror 2.0.18", + "thiserror 2.0.19", "ucd-trie", ] @@ -2629,7 +2618,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2788,7 +2777,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.119", ] [[package]] @@ -2819,7 +2808,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2835,9 +2824,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] @@ -2859,7 +2848,7 @@ checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2872,7 +2861,7 @@ dependencies = [ "pep440_rs", "pep508_rs", "serde", - "thiserror 2.0.18", + "thiserror 2.0.19", "toml 0.9.12+spec-1.1.0", ] @@ -2887,7 +2876,7 @@ dependencies = [ "newtype-uuid", "quick-xml", "strip-ansi-escapes", - "thiserror 2.0.18", + "thiserror 2.0.19", "uuid", ] @@ -2917,14 +2906,14 @@ checksum = "a9a28b8493dd664c8b171dd944da82d933f7d456b829bfb236738e1fe06c5ba4" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "quote" -version = "1.0.46" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -2948,7 +2937,7 @@ dependencies = [ "proc-macro-utils", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -3051,7 +3040,7 @@ version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", ] [[package]] @@ -3062,7 +3051,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -3082,14 +3071,14 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "regex" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -3099,9 +3088,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.15" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -3156,7 +3145,7 @@ checksum = "c0ed1a78a1b19d184b0daa629dd9a024573173ec7d485b287cb369fb3607cc1c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -3165,7 +3154,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "once_cell", "serde", "serde_derive", @@ -3175,12 +3164,12 @@ dependencies = [ [[package]] name = "ruff" -version = "0.15.22" +version = "0.16.2" dependencies = [ "anyhow", "argfile", "assert_fs", - "bitflags 2.13.0", + "bitflags 2.13.1", "cachedir", "clap", "clap_complete_command", @@ -3229,9 +3218,9 @@ dependencies = [ "strum", "tempfile", "test-case", - "thiserror 2.0.18", + "thiserror 2.0.19", "tikv-jemallocator", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "tracing", "walkdir", "wild", @@ -3239,16 +3228,13 @@ dependencies = [ [[package]] name = "ruff_annotate_snippets" -version = "0.0.5" +version = "0.0.8" dependencies = [ "anstream 1.0.0", "anstyle", "memchr", "ruff_annotate_snippets", - "serde", "snapbox", - "toml 1.1.3+spec-1.1.0", - "tryfn", "unicode-width", ] @@ -3280,7 +3266,7 @@ dependencies = [ [[package]] name = "ruff_cache" -version = "0.0.5" +version = "0.0.8" dependencies = [ "char_str", "filetime", @@ -3294,7 +3280,7 @@ dependencies = [ [[package]] name = "ruff_db" -version = "0.0.5" +version = "0.0.8" dependencies = [ "anstyle", "arc-swap", @@ -3327,10 +3313,10 @@ dependencies = [ "schemars", "serde", "serde_json", - "similar 3.1.1", + "similar 3.1.2", "supports-hyperlinks", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.19", "tracing", "tracing-subscriber", "ty_static", @@ -3369,10 +3355,10 @@ dependencies = [ "schemars", "serde", "serde_json", - "similar 3.1.1", + "similar 3.1.2", "strum", "tempfile", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "tracing", "tracing-indicatif", "tracing-subscriber", @@ -3385,7 +3371,7 @@ dependencies = [ [[package]] name = "ruff_diagnostics" -version = "0.0.5" +version = "0.0.8" dependencies = [ "get-size2", "is-macro", @@ -3395,7 +3381,7 @@ dependencies = [ [[package]] name = "ruff_formatter" -version = "0.0.5" +version = "0.0.8" dependencies = [ "drop_bomb", "ruff_cache", @@ -3411,7 +3397,7 @@ dependencies = [ [[package]] name = "ruff_graph" -version = "0.0.5" +version = "0.0.8" dependencies = [ "anyhow", "clap", @@ -3432,7 +3418,7 @@ dependencies = [ [[package]] name = "ruff_index" -version = "0.0.5" +version = "0.0.8" dependencies = [ "get-size2", "ruff_macros", @@ -3442,11 +3428,11 @@ dependencies = [ [[package]] name = "ruff_linter" -version = "0.15.22" +version = "0.16.2" dependencies = [ "aho-corasick", "anyhow", - "bitflags 2.13.0", + "bitflags 2.13.1", "clap", "colored", "compact_str", @@ -3488,14 +3474,14 @@ dependencies = [ "schemars", "serde", "serde_json", - "similar 3.1.1", + "similar 3.1.2", "smallvec", "strum", "strum_macros", "tempfile", "test-case", - "thiserror 2.0.18", - "toml 1.1.3+spec-1.1.0", + "thiserror 2.0.19", + "toml 1.1.4+spec-1.1.0", "typed-arena", "unicode-normalization", "unicode-width", @@ -3505,7 +3491,7 @@ dependencies = [ [[package]] name = "ruff_macros" -version = "0.0.5" +version = "0.0.8" dependencies = [ "heck", "itertools 0.15.0", @@ -3513,12 +3499,12 @@ dependencies = [ "quote", "regex", "ruff_python_trivia", - "syn", + "syn 3.0.3", ] [[package]] name = "ruff_markdown" -version = "0.0.5" +version = "0.0.8" dependencies = [ "insta", "regex", @@ -3549,14 +3535,14 @@ dependencies = [ [[package]] name = "ruff_memory_usage" -version = "0.0.5" +version = "0.0.8" dependencies = [ "get-size2", ] [[package]] name = "ruff_notebook" -version = "0.0.5" +version = "0.0.8" dependencies = [ "anyhow", "rand 0.10.2", @@ -3566,24 +3552,24 @@ dependencies = [ "serde", "serde_json", "test-case", - "thiserror 2.0.18", + "thiserror 2.0.19", "uuid", ] [[package]] name = "ruff_options_metadata" -version = "0.0.5" +version = "0.0.8" dependencies = [ "serde", ] [[package]] name = "ruff_python_ast" -version = "0.0.5" +version = "0.0.8" dependencies = [ "aho-corasick", "arrayvec", - "bitflags 2.13.0", + "bitflags 2.13.1", "char_str", "compact_str", "get-size2", @@ -3600,7 +3586,7 @@ dependencies = [ "serde", "serde_json", "thin-vec", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -3616,7 +3602,7 @@ dependencies = [ [[package]] name = "ruff_python_codegen" -version = "0.0.5" +version = "0.0.8" dependencies = [ "ruff_python_ast", "ruff_python_literal", @@ -3628,7 +3614,7 @@ dependencies = [ [[package]] name = "ruff_python_formatter" -version = "0.0.5" +version = "0.0.8" dependencies = [ "anyhow", "clap", @@ -3652,16 +3638,16 @@ dependencies = [ "schemars", "serde", "serde_json", - "similar 3.1.1", + "similar 3.1.2", "smallvec", "static_assertions", - "thiserror 2.0.18", + "thiserror 2.0.19", "tracing", ] [[package]] name = "ruff_python_importer" -version = "0.0.5" +version = "0.0.8" dependencies = [ "anyhow", "insta", @@ -3676,7 +3662,7 @@ dependencies = [ [[package]] name = "ruff_python_index" -version = "0.0.5" +version = "0.0.8" dependencies = [ "ruff_python_ast", "ruff_python_parser", @@ -3687,9 +3673,9 @@ dependencies = [ [[package]] name = "ruff_python_literal" -version = "0.0.5" +version = "0.0.8" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "icu_properties", "itertools 0.15.0", "ruff_python_ast", @@ -3697,10 +3683,10 @@ dependencies = [ [[package]] name = "ruff_python_parser" -version = "0.0.5" +version = "0.0.8" dependencies = [ "anyhow", - "bitflags 2.13.0", + "bitflags 2.13.1", "bstr", "datatest-stable", "drop_bomb", @@ -3726,9 +3712,9 @@ dependencies = [ [[package]] name = "ruff_python_semantic" -version = "0.0.5" +version = "0.0.8" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "insta", "is-macro", "ruff_cache", @@ -3747,15 +3733,15 @@ dependencies = [ [[package]] name = "ruff_python_stdlib" -version = "0.0.5" +version = "0.0.8" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "unicode-ident", ] [[package]] name = "ruff_python_trivia" -version = "0.0.5" +version = "0.0.8" dependencies = [ "itertools 0.15.0", "ruff_source_file", @@ -3776,19 +3762,19 @@ dependencies = [ [[package]] name = "ruff_ranged_value" -version = "0.0.5" +version = "0.0.8" dependencies = [ "get-size2", "ruff_db", "ruff_text_size", "schemars", "serde", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", ] [[package]] name = "ruff_server" -version = "0.0.5" +version = "0.0.8" dependencies = [ "anyhow", "crossbeam", @@ -3822,8 +3808,8 @@ dependencies = [ "shellexpand", "smallvec", "tempfile", - "thiserror 2.0.18", - "toml 1.1.3+spec-1.1.0", + "thiserror 2.0.19", + "toml 1.1.4+spec-1.1.0", "tracing", "tracing-log", "tracing-subscriber", @@ -3831,7 +3817,7 @@ dependencies = [ [[package]] name = "ruff_source_file" -version = "0.0.5" +version = "0.0.8" dependencies = [ "get-size2", "memchr", @@ -3841,7 +3827,7 @@ dependencies = [ [[package]] name = "ruff_text_size" -version = "0.0.5" +version = "0.0.8" dependencies = [ "get-size2", "schemars", @@ -3852,7 +3838,7 @@ dependencies = [ [[package]] name = "ruff_wasm" -version = "0.15.22" +version = "0.16.2" dependencies = [ "console_error_panic_hook", "console_log", @@ -3879,7 +3865,7 @@ dependencies = [ [[package]] name = "ruff_workspace" -version = "0.0.5" +version = "0.0.8" dependencies = [ "anyhow", "colored", @@ -3916,7 +3902,7 @@ dependencies = [ "shellexpand", "strum", "tempfile", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "unicode-normalization", ] @@ -3948,11 +3934,11 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] @@ -3969,9 +3955,9 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "salsa" -version = "0.28.1" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a14fdadbf856222e731756d7fdbdf193a7abf8fdab009bb45f48671a42719a84" +checksum = "cf0e374215cd2db2b5c75d7b3a99cb0cc052c0595335dfdefc03d4eb08f4aa81" dependencies = [ "boxcar", "compact_str", @@ -3996,19 +3982,19 @@ dependencies = [ [[package]] name = "salsa-macro-rules" -version = "0.28.1" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d7dc08ba69b9aedfa61dfc4d65548ae42c0d8b90bbd62cd121776920841bcf" +checksum = "85f4b7d4405540bbd6d4ffa52d4322d983f3781954d3073067ac1bdb028459b3" [[package]] name = "salsa-macros" -version = "0.28.1" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec5c48c5a4a53a6e2be9762f56566f1325d4394c011c00b3ea86ba2d13411e71" +checksum = "445be2bfbb2f67cb663225ecd7bc5a25370c0250fca30f9d8cbad9a913650370" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] @@ -4022,9 +4008,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" dependencies = [ "dyn-clone", "ref-cast", @@ -4035,14 +4021,14 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" +checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn", + "syn 3.0.3", ] [[package]] @@ -4065,9 +4051,9 @@ checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -4086,40 +4072,40 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] name = "serde_derive_internals" -version = "0.29.1" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -4195,9 +4181,9 @@ checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] name = "similar" -version = "3.1.1" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6505efef05804732ed8a3f2d4f279429eb485bd69d5b0cc6b19cc02005cda16" +checksum = "85ee016af5d736b69fc89e19254540fa4b5f5492853fb5503920f084011c78b6" dependencies = [ "bstr", ] @@ -4304,7 +4290,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4324,6 +4310,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -4332,7 +4329,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4351,26 +4348,17 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.0", -] - -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", + "windows-sys 0.52.0", ] [[package]] name = "terminal_size" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -4409,7 +4397,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4420,15 +4408,15 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "test-case-core", ] [[package]] name = "thin-vec" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0f7e269b48f0a7dd0146680fa24b50cc67fc0373f086a5b2f99bd084639b482" +checksum = "79def32ffcd477db1ff26f76dab9e3a91f0bd42a85ca96577089b24623056f9d" dependencies = [ "serde", ] @@ -4444,11 +4432,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl 2.0.19", ] [[package]] @@ -4459,18 +4447,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] @@ -4482,15 +4470,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "threadpool" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" -dependencies = [ - "num_cpus", -] - [[package]] name = "tikv-jemalloc-sys" version = "0.6.1+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7" @@ -4563,9 +4542,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.3+spec-1.1.0" +version = "1.1.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" dependencies = [ "indexmap", "serde_core", @@ -4609,9 +4588,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ "winnow 1.0.0", ] @@ -4642,7 +4621,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4708,17 +4687,6 @@ dependencies = [ "tracing-log", ] -[[package]] -name = "tryfn" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f68b00518dd6c69ee2289900b140e55dad068cb925678603bfa8d539f61ef6c1" -dependencies = [ - "ignore", - "libtest-mimic 0.7.3", - "snapbox", -] - [[package]] name = "ty" version = "0.0.0" @@ -4754,7 +4722,7 @@ dependencies = [ "serde_json", "tempfile", "tikv-jemallocator", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "tracing", "tracing-flame", "tracing-subscriber", @@ -4772,7 +4740,7 @@ dependencies = [ [[package]] name = "ty_combine" -version = "0.0.5" +version = "0.0.8" dependencies = [ "ordermap", "ruff_db", @@ -4806,7 +4774,7 @@ dependencies = [ "ruff_text_size", "serde", "tempfile", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "ty_ide", "ty_module_resolver", "ty_project", @@ -4817,7 +4785,7 @@ dependencies = [ name = "ty_ide" version = "0.0.0" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "camino", "compact_str", "get-size2", @@ -4857,13 +4825,14 @@ dependencies = [ [[package]] name = "ty_module_resolver" -version = "0.0.5" +version = "0.0.8" dependencies = [ "anyhow", "camino", "compact_str", "get-size2", "insta", + "ordermap", "regex", "regex-syntax", "ruff_db", @@ -4875,7 +4844,7 @@ dependencies = [ "strum", "strum_macros", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.19", "tracing", "ty_vendored", ] @@ -4893,7 +4862,6 @@ dependencies = [ "get-size2", "globset", "insta", - "memchr", "notify", "ordermap", "parking_lot", @@ -4919,8 +4887,8 @@ dependencies = [ "shellexpand", "strum", "strum_macros", - "thiserror 2.0.18", - "toml 1.1.3+spec-1.1.0", + "thiserror 2.0.19", + "toml 1.1.4+spec-1.1.0", "tracing", "ty_combine", "ty_module_resolver", @@ -4932,10 +4900,10 @@ dependencies = [ [[package]] name = "ty_python_core" -version = "0.0.5" +version = "0.0.8" dependencies = [ "anyhow", - "bitflags 2.13.0", + "bitflags 2.13.1", "bitvec", "char_str", "get-size2", @@ -4966,10 +4934,10 @@ dependencies = [ [[package]] name = "ty_python_semantic" -version = "0.0.5" +version = "0.0.8" dependencies = [ "anyhow", - "bitflags 2.13.0", + "bitflags 2.13.1", "camino", "char_str", "compact_str", @@ -5008,7 +4976,7 @@ dependencies = [ "strum", "strum_macros", "test-case", - "thiserror 2.0.18", + "thiserror 2.0.19", "tracing", "ty_module_resolver", "ty_python_core", @@ -5024,7 +4992,7 @@ name = "ty_server" version = "0.0.0" dependencies = [ "anyhow", - "bitflags 2.13.0", + "bitflags 2.13.1", "crossbeam", "dunce", "gen-lsp-types", @@ -5049,7 +5017,7 @@ dependencies = [ "smallvec", "strum", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.19", "tracing", "tracing-subscriber", "ty_combine", @@ -5057,11 +5025,12 @@ dependencies = [ "ty_module_resolver", "ty_project", "ty_python_core", + "ty_python_semantic", ] [[package]] name = "ty_site_packages" -version = "0.0.5" +version = "0.0.8" dependencies = [ "camino", "colored", @@ -5082,7 +5051,7 @@ dependencies = [ [[package]] name = "ty_static" -version = "0.0.5" +version = "0.0.8" dependencies = [ "ruff_macros", ] @@ -5104,7 +5073,7 @@ dependencies = [ "salsa", "serde", "tempfile", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "tracing", "ty_module_resolver", "ty_python_core", @@ -5114,7 +5083,7 @@ dependencies = [ [[package]] name = "ty_vendored" -version = "0.0.5" +version = "0.0.8" dependencies = [ "path-slash", "ruff_db", @@ -5143,6 +5112,7 @@ dependencies = [ "ty_ide", "ty_project", "ty_python_core", + "ty_python_semantic", "wasm-bindgen", "wasm-bindgen-test", ] @@ -5289,9 +5259,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.5" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea5fab0d6c3c01ae70085a09cb03d4c7a1d6314e2b3e075392783396d724ca0a" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ "js-sys", "wasm-bindgen", @@ -5433,7 +5403,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.119", "wasm-bindgen-shared", ] @@ -5476,7 +5446,7 @@ checksum = "ee997551ce1ad5adda03f7ce37ec34b4140fe9f547fd07b46d55901d1ba1a06b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -5513,7 +5483,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "hashbrown 0.15.5", "indexmap", "semver", @@ -5541,9 +5511,9 @@ dependencies = [ [[package]] name = "which" -version = "8.0.4" +version = "8.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d7cd18d4acb58fb3cdfe9ea54e6cd96a4e7d4cc45c56338b236e82dad47248" +checksum = "8f3ef584124b911bcc3875c2f1472e80f24361ceb789bd1c62b3e9a3df9ff43c" dependencies = [ "libc", ] @@ -5579,7 +5549,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] @@ -5609,7 +5579,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -5620,7 +5590,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -5869,7 +5839,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn", + "syn 2.0.119", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -5885,7 +5855,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.119", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -5897,7 +5867,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.13.0", + "bitflags 2.13.1", "indexmap", "log", "serde", @@ -5967,7 +5937,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] @@ -5988,7 +5958,7 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -6008,7 +5978,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] @@ -6042,7 +6012,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index a69df89e58..0b1c49f69c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,51 +25,51 @@ by_opt = { path = "crates/by_opt" } by_rt = { path = "crates/by_rt" } by_transforms = { path = "crates/by_transforms" } char_str = { version = "0.0.2" } -ruff = { version = "0.15.22", path = "crates/ruff" } -ruff_annotate_snippets = { version = "0.0.5", path = "crates/ruff_annotate_snippets" } -ruff_cache = { version = "0.0.5", path = "crates/ruff_cache" } -ruff_db = { version = "0.0.5", path = "crates/ruff_db", default-features = false } -ruff_diagnostics = { version = "0.0.5", path = "crates/ruff_diagnostics" } -ruff_formatter = { version = "0.0.5", path = "crates/ruff_formatter" } -ruff_graph = { version = "0.0.5", path = "crates/ruff_graph" } -ruff_index = { version = "0.0.5", path = "crates/ruff_index" } -ruff_linter = { version = "0.15.22", path = "crates/ruff_linter" } -ruff_macros = { version = "0.0.5", path = "crates/ruff_macros" } -ruff_markdown = { version = "0.0.5", path = "crates/ruff_markdown" } -ruff_memory_usage = { version = "0.0.5", path = "crates/ruff_memory_usage" } -ruff_notebook = { version = "0.0.5", path = "crates/ruff_notebook" } -ruff_options_metadata = { version = "0.0.5", path = "crates/ruff_options_metadata" } -ruff_python_ast = { version = "0.0.5", path = "crates/ruff_python_ast" } -ruff_python_codegen = { version = "0.0.5", path = "crates/ruff_python_codegen" } -ruff_python_formatter = { version = "0.0.5", path = "crates/ruff_python_formatter" } -ruff_python_importer = { version = "0.0.5", path = "crates/ruff_python_importer" } -ruff_python_index = { version = "0.0.5", path = "crates/ruff_python_index" } -ruff_python_literal = { version = "0.0.5", path = "crates/ruff_python_literal" } -ruff_python_parser = { version = "0.0.5", path = "crates/ruff_python_parser" } -ruff_python_semantic = { version = "0.0.5", path = "crates/ruff_python_semantic" } -ruff_python_stdlib = { version = "0.0.5", path = "crates/ruff_python_stdlib" } -ruff_python_trivia = { version = "0.0.5", path = "crates/ruff_python_trivia" } -ruff_server = { version = "0.0.5", path = "crates/ruff_server" } -ruff_source_file = { version = "0.0.5", path = "crates/ruff_source_file" } +ruff = { version = "0.16.2", path = "crates/ruff" } +ruff_annotate_snippets = { version = "0.0.8", path = "crates/ruff_annotate_snippets" } +ruff_cache = { version = "0.0.8", path = "crates/ruff_cache" } +ruff_db = { version = "0.0.8", path = "crates/ruff_db", default-features = false } +ruff_diagnostics = { version = "0.0.8", path = "crates/ruff_diagnostics" } +ruff_formatter = { version = "0.0.8", path = "crates/ruff_formatter" } +ruff_graph = { version = "0.0.8", path = "crates/ruff_graph" } +ruff_index = { version = "0.0.8", path = "crates/ruff_index" } +ruff_linter = { version = "0.16.2", path = "crates/ruff_linter" } +ruff_macros = { version = "0.0.8", path = "crates/ruff_macros" } +ruff_markdown = { version = "0.0.8", path = "crates/ruff_markdown" } +ruff_memory_usage = { version = "0.0.8", path = "crates/ruff_memory_usage" } +ruff_notebook = { version = "0.0.8", path = "crates/ruff_notebook" } +ruff_options_metadata = { version = "0.0.8", path = "crates/ruff_options_metadata" } +ruff_python_ast = { version = "0.0.8", path = "crates/ruff_python_ast" } +ruff_python_codegen = { version = "0.0.8", path = "crates/ruff_python_codegen" } +ruff_python_formatter = { version = "0.0.8", path = "crates/ruff_python_formatter" } +ruff_python_importer = { version = "0.0.8", path = "crates/ruff_python_importer" } +ruff_python_index = { version = "0.0.8", path = "crates/ruff_python_index" } +ruff_python_literal = { version = "0.0.8", path = "crates/ruff_python_literal" } +ruff_python_parser = { version = "0.0.8", path = "crates/ruff_python_parser" } +ruff_python_semantic = { version = "0.0.8", path = "crates/ruff_python_semantic" } +ruff_python_stdlib = { version = "0.0.8", path = "crates/ruff_python_stdlib" } +ruff_python_trivia = { version = "0.0.8", path = "crates/ruff_python_trivia" } +ruff_server = { version = "0.0.8", path = "crates/ruff_server" } +ruff_source_file = { version = "0.0.8", path = "crates/ruff_source_file" } ruff_mdtest = { path = "crates/ruff_mdtest" } -ruff_ranged_value = { version = "0.0.5", path = "crates/ruff_ranged_value" } -ruff_text_size = { version = "0.0.5", path = "crates/ruff_text_size" } -ruff_workspace = { version = "0.0.5", path = "crates/ruff_workspace" } +ruff_ranged_value = { version = "0.0.8", path = "crates/ruff_ranged_value" } +ruff_text_size = { version = "0.0.8", path = "crates/ruff_text_size" } +ruff_workspace = { version = "0.0.8", path = "crates/ruff_workspace" } ty = { path = "crates/ty" } -ty_combine = { version = "0.0.5", path = "crates/ty_combine" } +ty_combine = { version = "0.0.8", path = "crates/ty_combine" } ty_completion_bench = { path = "crates/ty_completion_bench" } ty_completion_eval = { path = "crates/ty_completion_eval" } ty_ide = { path = "crates/ty_ide" } -ty_module_resolver = { version = "0.0.5", path = "crates/ty_module_resolver" } +ty_module_resolver = { version = "0.0.8", path = "crates/ty_module_resolver" } ty_project = { path = "crates/ty_project", default-features = false } -ty_python_semantic = { version = "0.0.5", path = "crates/ty_python_semantic" } -ty_python_core = { version = "0.0.5", path = "crates/ty_python_core" } +ty_python_semantic = { version = "0.0.8", path = "crates/ty_python_semantic" } +ty_python_core = { version = "0.0.8", path = "crates/ty_python_core" } ty_server = { path = "crates/ty_server" } -ty_site_packages = { version = "0.0.5", path = "crates/ty_site_packages" } -ty_static = { version = "0.0.5", path = "crates/ty_static" } +ty_site_packages = { version = "0.0.8", path = "crates/ty_site_packages" } +ty_static = { version = "0.0.8", path = "crates/ty_static" } ty_test = { path = "crates/ty_test" } -ty_vendored = { version = "0.0.5", path = "crates/ty_vendored" } +ty_vendored = { version = "0.0.8", path = "crates/ty_vendored" } mdtest = { path = "crates/mdtest" } @@ -142,10 +142,10 @@ libc = { version = "0.2.153" } libcst = { version = "1.8.4", default-features = false } log = { version = "0.4.17" } lsp-server = { version = "0.10.0" } -lsp-types = { package = "gen-lsp-types", version = "0.10.0", features = ["url"] } +lsp-types = { package = "gen-lsp-types", version = "0.11.0", features = ["url"] } matchit = { version = "0.9.0" } memchr = { version = "2.7.1" } -mimalloc = { version = "0.1.49", features = ["v2"] } +mimalloc = { version = "0.1.52" } natord = { version = "1.0.9" } notify = { version = "8.0.0" } ordermap = { version = "1.0.0" } @@ -170,7 +170,7 @@ regex-syntax = { version = "0.8.8" } rustc-hash = { version = "2.0.0" } rustc-stable-hash = { version = "0.1.2" } # When updating salsa, make sure to also update the version in `fuzz/Cargo.toml` -salsa = { version = "0.28.1", default-features = false, features = [ +salsa = { version = "0.28.2", default-features = false, features = [ "compact_str", "macros", "salsa_unstable", @@ -196,7 +196,7 @@ static_assertions = "1.1.0" strum = { version = "0.28.0", features = ["strum_macros"] } strum_macros = { version = "0.28.0" } supports-hyperlinks = { version = "3.1.0" } -syn = { version = "2.0.55" } +syn = { version = "3.0.0" } tempfile = { version = "3.9.0" } test-case = { version = "3.3.1" } thiserror = { version = "2.0.0" } @@ -215,7 +215,6 @@ tracing-subscriber = { version = "0.3.18", default-features = false, features = "ansi", "smallvec", ] } -tryfn = { version = "1.0.0" } typed-arena = { version = "2.0.2" } unicode-ident = { version = "1.0.12" } unicode-normalization = { version = "0.1.23" } @@ -358,6 +357,9 @@ lto = false [profile.fast-test] inherits = "dev" opt-level = 1 +debug = "line-tables-only" +# Avoid the local ThinLTO that Cargo enables at nonzero optimization levels. +lto = "off" # The profile that 'cargo dist' will build with. [profile.dist] diff --git a/changelogs/0.15.x.md b/changelogs/0.15.x.md new file mode 100644 index 0000000000..4bb2e2060f --- /dev/null +++ b/changelogs/0.15.x.md @@ -0,0 +1,1329 @@ +## 0.15.0 + +Released on 2026-02-03. + +Check out the [blog post](https://astral.sh/blog/ruff-v0.15.0) for a migration +guide and overview of the changes! + +### Breaking changes + +- Ruff now formats your code according to the 2026 style guide. See the formatter section below or in the blog post for a detailed list of changes. + +- The linter now supports block suppression comments. For example, to suppress `N803` for all parameters in this function: + + ```python + # ruff: disable[N803] + def foo( + legacyArg1, + legacyArg2, + legacyArg3, + legacyArg4, + ): ... + # ruff: enable[N803] + ``` + + See the [documentation](https://docs.astral.sh/ruff/linter/#block-level) for more details. + +- The `ruff:alpine` Docker image is now based on Alpine 3.23 (up from 3.21). + +- The `ruff:debian` and `ruff:debian-slim` Docker images are now based on Debian 13 "Trixie" instead of Debian 12 "Bookworm." + +- Binaries for the `ppc64` (64-bit big-endian PowerPC) architecture are no longer included in our releases. It should still be possible to build Ruff manually for this platform, if needed. + +- Ruff now resolves all `extend`ed configuration files before falling back on a default Python version. + +### Stabilization + +The following rules have been stabilized and are no longer in preview: + +- [`blocking-http-call-httpx-in-async-function`](https://docs.astral.sh/ruff/rules/blocking-http-call-httpx-in-async-function) + (`ASYNC212`) +- [`blocking-path-method-in-async-function`](https://docs.astral.sh/ruff/rules/blocking-path-method-in-async-function) + (`ASYNC240`) +- [`blocking-input-in-async-function`](https://docs.astral.sh/ruff/rules/blocking-input-in-async-function) + (`ASYNC250`) +- [`map-without-explicit-strict`](https://docs.astral.sh/ruff/rules/map-without-explicit-strict) + (`B912`) +- [`if-exp-instead-of-or-operator`](https://docs.astral.sh/ruff/rules/if-exp-instead-of-or-operator) + (`FURB110`) +- [`single-item-membership-test`](https://docs.astral.sh/ruff/rules/single-item-membership-test) + (`FURB171`) +- [`missing-maxsplit-arg`](https://docs.astral.sh/ruff/rules/missing-maxsplit-arg) (`PLC0207`) +- [`unnecessary-lambda`](https://docs.astral.sh/ruff/rules/unnecessary-lambda) (`PLW0108`) +- [`unnecessary-empty-iterable-within-deque-call`](https://docs.astral.sh/ruff/rules/unnecessary-empty-iterable-within-deque-call) + (`RUF037`) +- [`in-empty-collection`](https://docs.astral.sh/ruff/rules/in-empty-collection) (`RUF060`) +- [`legacy-form-pytest-raises`](https://docs.astral.sh/ruff/rules/legacy-form-pytest-raises) + (`RUF061`) +- [`non-octal-permissions`](https://docs.astral.sh/ruff/rules/non-octal-permissions) (`RUF064`) +- [`invalid-rule-code`](https://docs.astral.sh/ruff/rules/invalid-rule-code) (`RUF102`) +- [`invalid-suppression-comment`](https://docs.astral.sh/ruff/rules/invalid-suppression-comment) + (`RUF103`) +- [`unmatched-suppression-comment`](https://docs.astral.sh/ruff/rules/unmatched-suppression-comment) + (`RUF104`) +- [`replace-str-enum`](https://docs.astral.sh/ruff/rules/replace-str-enum) (`UP042`) + +The following behaviors have been stabilized: + +- The `--output-format` flag is now respected when running Ruff in `--watch` mode, and the `full` output format is now used by default, matching the regular CLI output. +- [`builtin-attribute-shadowing`](https://docs.astral.sh/ruff/rules/builtin-attribute-shadowing/) (`A003`) now detects the use of shadowed built-in names in additional contexts like decorators, default arguments, and other attribute definitions. +- [`duplicate-union-member`](https://docs.astral.sh/ruff/rules/duplicate-union-member/) (`PYI016`) now considers `typing.Optional` when searching for duplicate union members. +- [`split-static-string`](https://docs.astral.sh/ruff/rules/split-static-string/) (`SIM905`) now offers an autofix when the `maxsplit` argument is provided, even without a `sep` argument. +- [`dict-get-with-none-default`](https://docs.astral.sh/ruff/rules/dict-get-with-none-default/) (`SIM910`) now applies to more types of key expressions. +- [`super-call-with-parameters`](https://docs.astral.sh/ruff/rules/super-call-with-parameters/) (`UP008`) now has a safe fix when it will not delete comments. +- [`unnecessary-default-type-args`](https://docs.astral.sh/ruff/rules/unnecessary-default-type-args/) (`UP043`) now applies to stub (`.pyi`) files on Python versions before 3.13. + +### Formatter + +This release introduces the new 2026 style guide, with the following changes: + +- Lambda parameters are now kept on the same line and lambda bodies will be parenthesized to let + them break across multiple lines ([#21385](https://github.com/astral-sh/ruff/pull/21385)) +- Parentheses around tuples of exceptions in `except` clauses will now be removed on Python 3.14 and + later ([#20768](https://github.com/astral-sh/ruff/pull/20768)) +- A single empty line is now permitted at the beginning of function bodies ([#21110](https://github.com/astral-sh/ruff/pull/21110)) +- Parentheses are avoided for long `as` captures in `match` statements ([#21176](https://github.com/astral-sh/ruff/pull/21176)) +- Extra spaces between escaped quotes and ending triple quotes can now be omitted ([#17216](https://github.com/astral-sh/ruff/pull/17216)) +- Blank lines are now enforced before classes with decorators in stub files ([#18888](https://github.com/astral-sh/ruff/pull/18888)) + +### Preview features + +- Apply formatting to Markdown code blocks ([#22470](https://github.com/astral-sh/ruff/pull/22470), [#22990](https://github.com/astral-sh/ruff/pull/22990), [#22996](https://github.com/astral-sh/ruff/pull/22996)) + + See the [documentation](https://docs.astral.sh/ruff/formatter/#markdown-code-formatting) for more details. + +### Bug fixes + +- Fix suppression indentation matching ([#22903](https://github.com/astral-sh/ruff/pull/22903)) + +### Rule changes + +- Customize where the `fix_title` sub-diagnostic appears ([#23044](https://github.com/astral-sh/ruff/pull/23044)) +- \[`FastAPI`\] Add sub-diagnostic explaining why a fix was unavailable (`FAST002`) ([#22565](https://github.com/astral-sh/ruff/pull/22565)) +- \[`flake8-annotations`\] Don't suggest `NoReturn` for functions raising `NotImplementedError` (`ANN201`, `ANN202`, `ANN205`, `ANN206`) ([#21311](https://github.com/astral-sh/ruff/pull/21311)) +- \[`pyupgrade`\] Make fix unsafe if it deletes comments (`UP017`) ([#22873](https://github.com/astral-sh/ruff/pull/22873)) +- \[`pyupgrade`\] Make fix unsafe if it deletes comments (`UP020`) ([#22872](https://github.com/astral-sh/ruff/pull/22872)) +- \[`pyupgrade`\] Make fix unsafe if it deletes comments (`UP033`) ([#22871](https://github.com/astral-sh/ruff/pull/22871)) +- \[`refurb`\] Do not add `abc.ABC` if already present (`FURB180`) ([#22234](https://github.com/astral-sh/ruff/pull/22234)) +- \[`refurb`\] Make fix unsafe if it deletes comments (`FURB110`) ([#22768](https://github.com/astral-sh/ruff/pull/22768)) +- \[`ruff`\] Add sub-diagnostics with permissions (`RUF064`) ([#22972](https://github.com/astral-sh/ruff/pull/22972)) + +### Server + +- Identify notebooks by LSP `didOpen` instead of `.ipynb` file extension ([#22810](https://github.com/astral-sh/ruff/pull/22810)) + +### CLI + +- Add `--color` CLI option to force colored output ([#22806](https://github.com/astral-sh/ruff/pull/22806)) + +### Documentation + +- Document `-` stdin convention in CLI help text ([#22817](https://github.com/astral-sh/ruff/pull/22817)) +- \[`refurb`\] Change example to `re.search` with `^` anchor (`FURB167`) ([#22984](https://github.com/astral-sh/ruff/pull/22984)) +- Fix link to Sphinx code block directives ([#23041](https://github.com/astral-sh/ruff/pull/23041)) +- \[`pydocstyle`\] Clarify which quote styles are allowed (`D300`) ([#22825](https://github.com/astral-sh/ruff/pull/22825)) +- \[`flake8-bugbear`\] Improve docs for `no-explicit-stacklevel` (`B028`) ([#22538](https://github.com/astral-sh/ruff/pull/22538)) + +### Other changes + +- Update MSRV to 1.91 ([#22874](https://github.com/astral-sh/ruff/pull/22874)) + +### Contributors + +- [@danparizher](https://github.com/danparizher) +- [@chirizxc](https://github.com/chirizxc) +- [@amyreese](https://github.com/amyreese) +- [@Jkhall81](https://github.com/Jkhall81) +- [@cwkang1998](https://github.com/cwkang1998) +- [@manzt](https://github.com/manzt) +- [@11happy](https://github.com/11happy) +- [@hugovk](https://github.com/hugovk) +- [@caiquejjx](https://github.com/caiquejjx) +- [@ntBre](https://github.com/ntBre) +- [@akawd](https://github.com/akawd) +- [@konstin](https://github.com/konstin) + +## 0.15.1 + +Released on 2026-02-12. + +### Preview features + +- \[`airflow`\] Add ruff rules to catch deprecated Airflow imports for Airflow 3.1 (`AIR321`) ([#22376](https://github.com/astral-sh/ruff/pull/22376)) +- \[`airflow`\] Third positional parameter not named `ti_key` should be flagged for `BaseOperatorLink.get_link` (`AIR303`) ([#22828](https://github.com/astral-sh/ruff/pull/22828)) +- \[`flake8-gettext`\] Fix false negatives for plural argument of `ngettext` (`INT001`, `INT002`, `INT003`) ([#21078](https://github.com/astral-sh/ruff/pull/21078)) +- \[`pyflakes`\] Fix infinite loop in preview fix for `unused-import` (`F401`) ([#23038](https://github.com/astral-sh/ruff/pull/23038)) +- \[`pygrep-hooks`\] Detect non-existent mock methods in standalone expressions (`PGH005`) ([#22830](https://github.com/astral-sh/ruff/pull/22830)) +- \[`pylint`\] Allow dunder submodules and improve diagnostic range (`PLC2701`) ([#22804](https://github.com/astral-sh/ruff/pull/22804)) +- \[`pyupgrade`\] Improve diagnostic range for tuples (`UP024`) ([#23013](https://github.com/astral-sh/ruff/pull/23013)) +- \[`refurb`\] Check subscripts in tuple do not use lambda parameters in `reimplemented-operator` (`FURB118`) ([#23079](https://github.com/astral-sh/ruff/pull/23079)) +- \[`ruff`\] Detect mutable defaults in `field` calls (`RUF008`) ([#23046](https://github.com/astral-sh/ruff/pull/23046)) +- \[`ruff`\] Ignore std `cmath.inf` (`RUF069`) ([#23120](https://github.com/astral-sh/ruff/pull/23120)) +- \[`ruff`\] New rule `float-equality-comparison` (`RUF069`) ([#20585](https://github.com/astral-sh/ruff/pull/20585)) +- Don't format unlabeled Markdown code blocks ([#23106](https://github.com/astral-sh/ruff/pull/23106)) +- Markdown formatting support in LSP ([#23063](https://github.com/astral-sh/ruff/pull/23063)) +- Support Quarto Markdown language markers ([#22947](https://github.com/astral-sh/ruff/pull/22947)) +- Support formatting `pycon` Markdown code blocks ([#23112](https://github.com/astral-sh/ruff/pull/23112)) +- Use extension mapping to select Markdown code block language ([#22934](https://github.com/astral-sh/ruff/pull/22934)) + +### Bug fixes + +- Avoid false positive for undefined variables in `FAST001` ([#23224](https://github.com/astral-sh/ruff/pull/23224)) +- Avoid introducing syntax errors for `FAST003` autofix ([#23227](https://github.com/astral-sh/ruff/pull/23227)) +- Avoid suggesting `InitVar` for `__post_init__` that references PEP 695 type parameters ([#23226](https://github.com/astral-sh/ruff/pull/23226)) +- Deduplicate type variables in generic functions ([#23225](https://github.com/astral-sh/ruff/pull/23225)) +- Fix exception handler parenthesis removal for Python 3.14+ ([#23126](https://github.com/astral-sh/ruff/pull/23126)) +- Fix f-string middle panic when parsing t-strings ([#23232](https://github.com/astral-sh/ruff/pull/23232)) +- Wrap `RUF020` target for multiline fixes ([#23210](https://github.com/astral-sh/ruff/pull/23210)) +- Wrap `UP007` target for multiline fixes ([#23208](https://github.com/astral-sh/ruff/pull/23208)) +- Fix missing diagnostics for last range suppression in file ([#23242](https://github.com/astral-sh/ruff/pull/23242)) +- \[`pyupgrade`\] Fix syntax error on string with newline escape and comment (`UP037`) ([#22968](https://github.com/astral-sh/ruff/pull/22968)) + +### Rule changes + +- Use `ruff` instead of `Ruff` as the program name in GitHub output format ([#23240](https://github.com/astral-sh/ruff/pull/23240)) +- \[`PT006`\] Fix syntax error when unpacking nested tuples in `parametrize` fixes (#22441) ([#22464](https://github.com/astral-sh/ruff/pull/22464)) +- \[`airflow`\] Catch deprecated attribute access from context key for Airflow 3.0 (`AIR301`) ([#22850](https://github.com/astral-sh/ruff/pull/22850)) +- \[`airflow`\] Capture deprecated arguments and a decorator (`AIR301`) ([#23170](https://github.com/astral-sh/ruff/pull/23170)) +- \[`flake8-boolean-trap`\] Add `multiprocessing.Value` to excluded functions for `FBT003` ([#23010](https://github.com/astral-sh/ruff/pull/23010)) +- \[`flake8-bugbear`\] Add a secondary annotation showing the previous occurrence (`B033`) ([#22634](https://github.com/astral-sh/ruff/pull/22634)) +- \[`flake8-type-checking`\] Add sub-diagnostic showing the runtime use of an annotation (`TC004`) ([#23091](https://github.com/astral-sh/ruff/pull/23091)) +- \[`isort`\] Support configurable import section heading comments ([#23151](https://github.com/astral-sh/ruff/pull/23151)) +- \[`ruff`\] Improve the diagnostic for `RUF012` ([#23202](https://github.com/astral-sh/ruff/pull/23202)) + +### Formatter + +- Suppress diagnostic output for `format --check --silent` ([#17736](https://github.com/astral-sh/ruff/pull/17736)) + +### Documentation + +- Add tabbed shell completion documentation ([#23169](https://github.com/astral-sh/ruff/pull/23169)) +- Explain how to enable Markdown formatting for pre-commit hook ([#23077](https://github.com/astral-sh/ruff/pull/23077)) +- Fixed import in `runtime-evaluated-decorators` example ([#23187](https://github.com/astral-sh/ruff/pull/23187)) +- Update ruff server contributing guide ([#23060](https://github.com/astral-sh/ruff/pull/23060)) + +### Other changes + +- Exclude WASM artifacts from GitHub releases ([#23221](https://github.com/astral-sh/ruff/pull/23221)) + +### Contributors + +- [@mkniewallner](https://github.com/mkniewallner) +- [@bxff](https://github.com/bxff) +- [@dylwil3](https://github.com/dylwil3) +- [@Avasam](https://github.com/Avasam) +- [@amyreese](https://github.com/amyreese) +- [@charliermarsh](https://github.com/charliermarsh) +- [@Alex-ley-scrub](https://github.com/Alex-ley-scrub) +- [@Kalmaegi](https://github.com/Kalmaegi) +- [@danparizher](https://github.com/danparizher) +- [@AiyionPrime](https://github.com/AiyionPrime) +- [@eureka928](https://github.com/eureka928) +- [@11happy](https://github.com/11happy) +- [@Jkhall81](https://github.com/Jkhall81) +- [@chirizxc](https://github.com/chirizxc) +- [@leandrobbraga](https://github.com/leandrobbraga) +- [@tvatter](https://github.com/tvatter) +- [@anishgirianish](https://github.com/anishgirianish) +- [@shaanmajid](https://github.com/shaanmajid) +- [@ntBre](https://github.com/ntBre) +- [@sjyangkevin](https://github.com/sjyangkevin) + +## 0.15.2 + +Released on 2026-02-19. + +### Preview features + +- Expand the default rule set ([#23385](https://github.com/astral-sh/ruff/pull/23385)) + + In preview, Ruff now enables a significantly expanded default rule set of 412 + rules, up from the stable default set of 59 rules. The new rules are mostly a + superset of the stable defaults, with the exception of these rules, which are + removed from the preview defaults: + + - [`multiple-imports-on-one-line`](https://docs.astral.sh/ruff/rules/multiple-imports-on-one-line) (`E401`) + - [`module-import-not-at-top-of-file`](https://docs.astral.sh/ruff/rules/module-import-not-at-top-of-file) (`E402`) + - [`module-import-not-at-top-of-file`](https://docs.astral.sh/ruff/rules/module-import-not-at-top-of-file) (`E701`) + - [`multiple-statements-on-one-line-semicolon`](https://docs.astral.sh/ruff/rules/multiple-statements-on-one-line-semicolon) (`E702`) + - [`useless-semicolon`](https://docs.astral.sh/ruff/rules/useless-semicolon) (`E703`) + - [`none-comparison`](https://docs.astral.sh/ruff/rules/none-comparison) (`E711`) + - [`true-false-comparison`](https://docs.astral.sh/ruff/rules/true-false-comparison) (`E712`) + - [`not-in-test`](https://docs.astral.sh/ruff/rules/not-in-test) (`E713`) + - [`not-is-test`](https://docs.astral.sh/ruff/rules/not-is-test) (`E714`) + - [`type-comparison`](https://docs.astral.sh/ruff/rules/type-comparison) (`E721`) + - [`lambda-assignment`](https://docs.astral.sh/ruff/rules/lambda-assignment) (`E731`) + - [`ambiguous-variable-name`](https://docs.astral.sh/ruff/rules/ambiguous-variable-name) (`E741`) + - [`ambiguous-class-name`](https://docs.astral.sh/ruff/rules/ambiguous-class-name) (`E742`) + - [`ambiguous-function-name`](https://docs.astral.sh/ruff/rules/ambiguous-function-name) (`E743`) + - [`undefined-local-with-import-star`](https://docs.astral.sh/ruff/rules/undefined-local-with-import-star) (`F403`) + - [`undefined-local-with-import-star-usage`](https://docs.astral.sh/ruff/rules/undefined-local-with-import-star-usage) (`F405`) + - [`undefined-local-with-nested-import-star-usage`](https://docs.astral.sh/ruff/rules/undefined-local-with-nested-import-star-usage) (`F406`) + - [`forward-annotation-syntax-error`](https://docs.astral.sh/ruff/rules/forward-annotation-syntax-error) (`F722`) + + If you use preview and prefer the old defaults, you can restore them with + configuration like: + + ```toml + + # ruff.toml + + [lint] + select = ["E4", "E7", "E9", "F"] + + # pyproject.toml + + [tool.ruff.lint] + select = ["E4", "E7", "E9", "F"] + ``` + + If you do give them a try, feel free to share your feedback in the [GitHub + discussion](https://github.com/astral-sh/ruff/discussions/23203)! + +- \[`flake8-pyi`\] Also check string annotations (`PYI041`) ([#19023](https://github.com/astral-sh/ruff/pull/19023)) + +### Bug fixes + +- \[`flake8-async`\] Fix `in_async_context` logic ([#23426](https://github.com/astral-sh/ruff/pull/23426)) +- \[`ruff`\] Fix for `RUF102` should delete entire comment ([#23380](https://github.com/astral-sh/ruff/pull/23380)) +- \[`ruff`\] Suppress diagnostic for strings with backslashes in interpolations before Python 3.12 (`RUF027`) ([#21069](https://github.com/astral-sh/ruff/pull/21069)) +- \[`flake8-bugbear`\] Fix `B023` false positive for immediately-invoked lambdas ([#23294](https://github.com/astral-sh/ruff/pull/23294)) +- [parser] Fix false syntax error for match-like annotated assignments ([#23297](https://github.com/astral-sh/ruff/pull/23297)) +- [parser] Fix indentation tracking after line continuations ([#23417](https://github.com/astral-sh/ruff/pull/23417)) + +### Rule changes + +- \[`flake8-executable`\] Allow global flags in uv shebangs (`EXE003`) ([#22582](https://github.com/astral-sh/ruff/pull/22582)) +- \[`pyupgrade`\] Fix handling of `typing.{io,re}` (`UP035`) ([#23131](https://github.com/astral-sh/ruff/pull/23131)) +- \[`ruff`\] Detect `PLC0207` on chained `str.split()` calls ([#23275](https://github.com/astral-sh/ruff/pull/23275)) + +### CLI + +- Remove invalid inline `noqa` warning ([#23270](https://github.com/astral-sh/ruff/pull/23270)) + +### Configuration + +- Add extension mapping to configuration file options ([#23384](https://github.com/astral-sh/ruff/pull/23384)) + +### Documentation + +- Add `Q004` to the list of conflicting rules ([#23340](https://github.com/astral-sh/ruff/pull/23340)) +- \[`ruff`\] Expand `lint.external` docs and add sub-diagnostic (`RUF100`, `RUF102`) ([#23268](https://github.com/astral-sh/ruff/pull/23268)) + +### Contributors + +- [@dylwil3](https://github.com/dylwil3) +- [@Jkhall81](https://github.com/Jkhall81) +- [@danparizher](https://github.com/danparizher) +- [@dhruvmanila](https://github.com/dhruvmanila) +- [@harupy](https://github.com/harupy) +- [@ngnpope](https://github.com/ngnpope) +- [@amyreese](https://github.com/amyreese) +- [@kar-ganap](https://github.com/kar-ganap) +- [@robsdedude](https://github.com/robsdedude) +- [@shaanmajid](https://github.com/shaanmajid) +- [@ntBre](https://github.com/ntBre) +- [@toslunar](https://github.com/toslunar) + +## 0.15.3 + +Released on 2026-02-26. + +### Preview features + +- Drop explicit support for `.qmd` file extension ([#23572](https://github.com/astral-sh/ruff/pull/23572)) + + This can now be enabled instead by setting the [`extension`](https://docs.astral.sh/ruff/settings/#extension) option: + + ```toml + # ruff.toml + extension = { qmd = "markdown" } + + # pyproject.toml + [tool.ruff] + extension = { qmd = "markdown" } + ``` + +- Include configured extensions in file discovery ([#23400](https://github.com/astral-sh/ruff/pull/23400)) + +- \[`flake8-bandit`\] Allow suspicious imports in `TYPE_CHECKING` blocks (`S401`-`S415`) ([#23441](https://github.com/astral-sh/ruff/pull/23441)) + +- \[`flake8-bugbear`\] Allow `B901` in pytest hook wrappers ([#21931](https://github.com/astral-sh/ruff/pull/21931)) + +- \[`flake8-import-conventions`\] Add missing conventions from upstream (`ICN001`, `ICN002`) ([#21373](https://github.com/astral-sh/ruff/pull/21373)) + +- \[`pydocstyle`\] Add rule to enforce docstring section ordering (`D420`) ([#23537](https://github.com/astral-sh/ruff/pull/23537)) + +- \[`pylint`\] Implement `swap-with-temporary-variable` (`PLR1712`) ([#22205](https://github.com/astral-sh/ruff/pull/22205)) + +- \[`ruff`\] Add `unnecessary-assign-before-yield` (`RUF070`) ([#23300](https://github.com/astral-sh/ruff/pull/23300)) + +- \[`ruff`\] Support file-level noqa in `RUF102` ([#23535](https://github.com/astral-sh/ruff/pull/23535)) + +- \[`ruff`\] Suppress diagnostic for invalid f-strings before Python 3.12 (`RUF027`) ([#23480](https://github.com/astral-sh/ruff/pull/23480)) + +- \[`flake8-bandit`\] Don't flag `BaseLoader`/`CBaseLoader` as unsafe (`S506`) ([#23510](https://github.com/astral-sh/ruff/pull/23510)) + +### Bug fixes + +- Avoid infinite loop between `I002` and `PYI025` ([#23352](https://github.com/astral-sh/ruff/pull/23352)) +- \[`pyflakes`\] Fix false positive for `@overload` from `lint.typing-modules` (`F811`) ([#23357](https://github.com/astral-sh/ruff/pull/23357)) +- \[`pyupgrade`\] Fix false positive for `TypeVar` default before Python 3.12 (`UP046`) ([#23540](https://github.com/astral-sh/ruff/pull/23540)) +- \[`pyupgrade`\] Fix handling of `\N` in raw strings (`UP032`) ([#22149](https://github.com/astral-sh/ruff/pull/22149)) + +### Rule changes + +- Render sub-diagnostics in the GitHub output format ([#23455](https://github.com/astral-sh/ruff/pull/23455)) + +- \[`flake8-bugbear`\] Tag certain `B007` diagnostics as unnecessary ([#23453](https://github.com/astral-sh/ruff/pull/23453)) + +- \[`ruff`\] Ignore unknown rule codes in `RUF100` ([#23531](https://github.com/astral-sh/ruff/pull/23531)) + + These are now flagged by [`RUF102`](https://docs.astral.sh/ruff/rules/invalid-rule-code/) instead. + +### Documentation + +- Fix missing settings links for several linters ([#23519](https://github.com/astral-sh/ruff/pull/23519)) +- Update isort action comments heading ([#23515](https://github.com/astral-sh/ruff/pull/23515)) +- \[`pydocstyle`\] Fix double comma in description of `D404` ([#23440](https://github.com/astral-sh/ruff/pull/23440)) + +### Other changes + +- Update the Python module (notably `find_ruff_bin`) for parity with uv ([#23406](https://github.com/astral-sh/ruff/pull/23406)) + +### Contributors + +- [@zanieb](https://github.com/zanieb) +- [@o1x3](https://github.com/o1x3) +- [@assadyousuf](https://github.com/assadyousuf) +- [@kar-ganap](https://github.com/kar-ganap) +- [@denyszhak](https://github.com/denyszhak) +- [@amyreese](https://github.com/amyreese) +- [@carljm](https://github.com/carljm) +- [@anishgirianish](https://github.com/anishgirianish) +- [@Bnyro](https://github.com/Bnyro) +- [@danparizher](https://github.com/danparizher) +- [@ntBre](https://github.com/ntBre) +- [@gcomneno](https://github.com/gcomneno) +- [@jaap3](https://github.com/jaap3) +- [@stakeswky](https://github.com/stakeswky) + +## 0.15.4 + +Released on 2026-02-26. + +This is a follow-up release to 0.15.3 that resolves a panic when the new rule `PLR1712` was enabled with any rule that analyzes definitions, such as many of the `ANN` or `D` rules. + +### Bug fixes + +- Fix panic on access to definitions after analyzing definitions ([#23588](https://github.com/astral-sh/ruff/pull/23588)) +- \[`pyflakes`\] Suppress false positive in `F821` for names used before `del` in stub files ([#23550](https://github.com/astral-sh/ruff/pull/23550)) + +### Documentation + +- Clarify first-party import detection in Ruff ([#23591](https://github.com/astral-sh/ruff/pull/23591)) +- Fix incorrect `import-heading` example ([#23568](https://github.com/astral-sh/ruff/pull/23568)) + +### Contributors + +- [@stakeswky](https://github.com/stakeswky) +- [@ntBre](https://github.com/ntBre) +- [@thejcannon](https://github.com/thejcannon) +- [@GeObts](https://github.com/GeObts) + +## 0.15.5 + +Released on 2026-03-05. + +### Preview features + +- Discover Markdown files by default in preview mode ([#23434](https://github.com/astral-sh/ruff/pull/23434)) +- \[`perflint`\] Extend `PERF102` to comprehensions and generators ([#23473](https://github.com/astral-sh/ruff/pull/23473)) +- \[`refurb`\] Fix `FURB101` and `FURB103` false positives when I/O variable is used later ([#23542](https://github.com/astral-sh/ruff/pull/23542)) +- \[`ruff`\] Add fix for `none-not-at-end-of-union` (`RUF036`) ([#22829](https://github.com/astral-sh/ruff/pull/22829)) +- \[`ruff`\] Fix false positive for `re.split` with empty string pattern (`RUF055`) ([#23634](https://github.com/astral-sh/ruff/pull/23634)) + +### Bug fixes + +- \[`fastapi`\] Handle callable class dependencies with `__call__` method (`FAST003`) ([#23553](https://github.com/astral-sh/ruff/pull/23553)) +- \[`pydocstyle`\] Fix numpy section ordering (`D420`) ([#23685](https://github.com/astral-sh/ruff/pull/23685)) +- \[`pyflakes`\] Fix false positive for names shadowing re-exports (`F811`) ([#23356](https://github.com/astral-sh/ruff/pull/23356)) +- \[`pyupgrade`\] Avoid inserting redundant `None` elements in `UP045` ([#23459](https://github.com/astral-sh/ruff/pull/23459)) + +### Documentation + +- Document extension mapping for Markdown code formatting ([#23574](https://github.com/astral-sh/ruff/pull/23574)) +- Update default Python version examples ([#23605](https://github.com/astral-sh/ruff/pull/23605)) + +### Other changes + +- Publish releases to Astral mirror ([#23616](https://github.com/astral-sh/ruff/pull/23616)) + +### Contributors + +- [@amyreese](https://github.com/amyreese) +- [@stakeswky](https://github.com/stakeswky) +- [@chirizxc](https://github.com/chirizxc) +- [@anishgirianish](https://github.com/anishgirianish) +- [@bxff](https://github.com/bxff) +- [@zsol](https://github.com/zsol) +- [@charliermarsh](https://github.com/charliermarsh) +- [@ntBre](https://github.com/ntBre) +- [@kar-ganap](https://github.com/kar-ganap) + +## 0.15.6 + +Released on 2026-03-12. + +### Preview features + +- Add support for `lazy` import parsing ([#23755](https://github.com/astral-sh/ruff/pull/23755)) +- Add support for star-unpacking of comprehensions (PEP 798) ([#23788](https://github.com/astral-sh/ruff/pull/23788)) +- Reject semantic syntax errors for lazy imports ([#23757](https://github.com/astral-sh/ruff/pull/23757)) +- Drop a few rules from the preview default set ([#23879](https://github.com/astral-sh/ruff/pull/23879)) +- \[`airflow`\] Flag `Variable.get()` calls outside of task execution context (`AIR003`) ([#23584](https://github.com/astral-sh/ruff/pull/23584)) +- \[`airflow`\] Flag runtime-varying values in DAG/task constructor arguments (`AIR304`) ([#23631](https://github.com/astral-sh/ruff/pull/23631)) +- \[`flake8-bugbear`\] Implement `delattr-with-constant` (`B043`) ([#23737](https://github.com/astral-sh/ruff/pull/23737)) +- \[`flake8-tidy-imports`\] Add `TID254` to enforce lazy imports ([#23777](https://github.com/astral-sh/ruff/pull/23777)) +- \[`flake8-tidy-imports`\] Allow users to ban lazy imports with `TID254` ([#23847](https://github.com/astral-sh/ruff/pull/23847)) +- \[`isort`\] Retain `lazy` keyword when sorting imports ([#23762](https://github.com/astral-sh/ruff/pull/23762)) +- \[`pyupgrade`\] Add `from __future__ import annotations` automatically (`UP006`) ([#23260](https://github.com/astral-sh/ruff/pull/23260)) +- \[`refurb`\] Support `newline` parameter in `FURB101` for Python 3.13+ ([#23754](https://github.com/astral-sh/ruff/pull/23754)) +- \[`ruff`\] Add `os-path-commonprefix` (`RUF071`) ([#23814](https://github.com/astral-sh/ruff/pull/23814)) +- \[`ruff`\] Add unsafe fix for os-path-commonprefix (`RUF071`) ([#23852](https://github.com/astral-sh/ruff/pull/23852)) +- \[`ruff`\] Limit `RUF036` to typing contexts; make it unsafe for non-typing-only ([#23765](https://github.com/astral-sh/ruff/pull/23765)) +- \[`ruff`\] Use starred unpacking for `RUF017` in Python 3.15+ ([#23789](https://github.com/astral-sh/ruff/pull/23789)) + +### Bug fixes + +- Fix `--add-noqa` creating unwanted leading whitespace ([#23773](https://github.com/astral-sh/ruff/pull/23773)) +- Fix `--add-noqa` breaking shebangs ([#23577](https://github.com/astral-sh/ruff/pull/23577)) +- [formatter] Fix lambda body formatting for multiline calls and subscripts ([#23866](https://github.com/astral-sh/ruff/pull/23866)) +- [formatter] Preserve required annotation parentheses in annotated assignments ([#23865](https://github.com/astral-sh/ruff/pull/23865)) +- [formatter] Preserve type-expression parentheses in the formatter ([#23867](https://github.com/astral-sh/ruff/pull/23867)) +- \[`flake8-annotations`\] Fix stack overflow in `ANN401` on quoted annotations with escape sequences ([#23912](https://github.com/astral-sh/ruff/pull/23912)) +- \[`pep8-naming`\] Check naming conventions in `match` pattern bindings (`N806`, `N815`, `N816`) ([#23899](https://github.com/astral-sh/ruff/pull/23899)) +- \[`perflint`\] Fix comment duplication in fixes (`PERF401`, `PERF403`) ([#23729](https://github.com/astral-sh/ruff/pull/23729)) +- \[`pyupgrade`\] Properly trigger `super` change in nested class (`UP008`) ([#22677](https://github.com/astral-sh/ruff/pull/22677)) +- \[`ruff`\] Avoid syntax errors in `RUF036` fixes ([#23764](https://github.com/astral-sh/ruff/pull/23764)) + +### Rule changes + +- \[`flake8-bandit`\] Flag `S501` with `requests.request` ([#23873](https://github.com/astral-sh/ruff/pull/23873)) +- \[`flake8-executable`\] Fix WSL detection in non-Docker containers ([#22879](https://github.com/astral-sh/ruff/pull/22879)) +- \[`flake8-print`\] Ignore `pprint` calls with `stream=` ([#23787](https://github.com/astral-sh/ruff/pull/23787)) + +### Documentation + +- Update docs for Markdown code block formatting ([#23871](https://github.com/astral-sh/ruff/pull/23871)) +- \[`flake8-bugbear`\] Fix misleading description for `B904` ([#23731](https://github.com/astral-sh/ruff/pull/23731)) + +### Contributors + +- [@zsol](https://github.com/zsol) +- [@carljm](https://github.com/carljm) +- [@ntBre](https://github.com/ntBre) +- [@Bortlesboat](https://github.com/Bortlesboat) +- [@sososonia-cyber](https://github.com/sososonia-cyber) +- [@chirizxc](https://github.com/chirizxc) +- [@leandrobbraga](https://github.com/leandrobbraga) +- [@11happy](https://github.com/11happy) +- [@Acelogic](https://github.com/Acelogic) +- [@anishgirianish](https://github.com/anishgirianish) +- [@amyreese](https://github.com/amyreese) +- [@xvchris](https://github.com/xvchris) +- [@charliermarsh](https://github.com/charliermarsh) +- [@getehen](https://github.com/getehen) +- [@Dev-iL](https://github.com/Dev-iL) + +## 0.15.7 + +Released on 2026-03-19. + +### Preview features + +- Display output severity in preview ([#23845](https://github.com/astral-sh/ruff/pull/23845)) +- Don't show `noqa` hover for non-Python documents ([#24040](https://github.com/astral-sh/ruff/pull/24040)) + +### Rule changes + +- \[`pycodestyle`\] Recognize `pyrefly:` as a pragma comment (`E501`) ([#24019](https://github.com/astral-sh/ruff/pull/24019)) + +### Server + +- Don't return code actions for non-Python documents ([#23905](https://github.com/astral-sh/ruff/pull/23905)) + +### Documentation + +- Add company AI policy to contributing guide ([#24021](https://github.com/astral-sh/ruff/pull/24021)) +- Document editor features for Markdown code formatting ([#23924](https://github.com/astral-sh/ruff/pull/23924)) +- \[`pylint`\] Improve phrasing (`PLC0208`) ([#24033](https://github.com/astral-sh/ruff/pull/24033)) + +### Other changes + +- Use PEP 639 license information ([#19661](https://github.com/astral-sh/ruff/pull/19661)) + +### Contributors + +- [@tmimmanuel](https://github.com/tmimmanuel) +- [@DimitriPapadopoulos](https://github.com/DimitriPapadopoulos) +- [@amyreese](https://github.com/amyreese) +- [@statxc](https://github.com/statxc) +- [@dylwil3](https://github.com/dylwil3) +- [@hunterhogan](https://github.com/hunterhogan) +- [@renovate](https://github.com/renovate) + +## 0.15.8 + +Released on 2026-03-26. + +### Preview features + +- \[`ruff`\] New rule `unnecessary-if` (`RUF050`) ([#24114](https://github.com/astral-sh/ruff/pull/24114)) +- \[`ruff`\] New rule `useless-finally` (`RUF072`) ([#24165](https://github.com/astral-sh/ruff/pull/24165)) +- \[`ruff`\] New rule `f-string-percent-format` (`RUF073`): warn when using `%` operator on an f-string ([#24162](https://github.com/astral-sh/ruff/pull/24162)) +- \[`pyflakes`\] Recognize `frozendict` as a builtin for Python 3.15+ ([#24100](https://github.com/astral-sh/ruff/pull/24100)) + +### Bug fixes + +- \[`flake8-async`\] Use fully-qualified `anyio.lowlevel` import in autofix (`ASYNC115`) ([#24166](https://github.com/astral-sh/ruff/pull/24166)) +- \[`flake8-bandit`\] Check tuple arguments for partial paths in `S607` ([#24080](https://github.com/astral-sh/ruff/pull/24080)) +- \[`pyflakes`\] Skip `undefined-name` (`F821`) for conditionally deleted variables ([#24088](https://github.com/astral-sh/ruff/pull/24088)) +- `E501`/`W505`/formatter: Exclude nested pragma comments from line width calculation ([#24071](https://github.com/astral-sh/ruff/pull/24071)) +- Fix `%foo?` parsing in IPython assignment expressions ([#24152](https://github.com/astral-sh/ruff/pull/24152)) +- `analyze graph`: resolve string imports that reference attributes, not just modules ([#24058](https://github.com/astral-sh/ruff/pull/24058)) + +### Rule changes + +- \[`eradicate`\] ignore `ty: ignore` comments in `ERA001` ([#24192](https://github.com/astral-sh/ruff/pull/24192)) +- \[`flake8-bandit`\] Treat `sys.executable` as trusted input in `S603` ([#24106](https://github.com/astral-sh/ruff/pull/24106)) +- \[`flake8-self`\] Recognize `Self` annotation and `self` assignment in `SLF001` ([#24144](https://github.com/astral-sh/ruff/pull/24144)) +- \[`pyflakes`\] `F507`: Fix false negative for non-tuple RHS in `%`-formatting ([#24142](https://github.com/astral-sh/ruff/pull/24142)) +- \[`refurb`\] Parenthesize generator arguments in `FURB142` fixer ([#24200](https://github.com/astral-sh/ruff/pull/24200)) + +### Performance + +- Speed up diagnostic rendering ([#24146](https://github.com/astral-sh/ruff/pull/24146)) + +### Server + +- Warn when Markdown files are skipped due to preview being disabled ([#24150](https://github.com/astral-sh/ruff/pull/24150)) + +### Documentation + +- Clarify `extend-ignore` and `extend-select` settings documentation ([#24064](https://github.com/astral-sh/ruff/pull/24064)) +- Mention AI policy in PR template ([#24198](https://github.com/astral-sh/ruff/pull/24198)) + +### Other changes + +- Use trusted publishing for NPM packages ([#24171](https://github.com/astral-sh/ruff/pull/24171)) + +### Contributors + +- [@bitloi](https://github.com/bitloi) +- [@Sim-hu](https://github.com/Sim-hu) +- [@mvanhorn](https://github.com/mvanhorn) +- [@chinar-amrutkar](https://github.com/chinar-amrutkar) +- [@markjm](https://github.com/markjm) +- [@RenzoMXD](https://github.com/RenzoMXD) +- [@vivekkhimani](https://github.com/vivekkhimani) +- [@seroperson](https://github.com/seroperson) +- [@moktamd](https://github.com/moktamd) +- [@charliermarsh](https://github.com/charliermarsh) +- [@ntBre](https://github.com/ntBre) +- [@zanieb](https://github.com/zanieb) +- [@dylwil3](https://github.com/dylwil3) +- [@MichaReiser](https://github.com/MichaReiser) + +## 0.15.9 + +Released on 2026-04-02. + +### Preview features + +- \[`pyflakes`\] Flag annotated variable redeclarations as `F811` in preview mode ([#24244](https://github.com/astral-sh/ruff/pull/24244)) +- \[`ruff`\] Allow dunder-named assignments in non-strict mode for `RUF067` ([#24089](https://github.com/astral-sh/ruff/pull/24089)) + +### Bug fixes + +- \[`flake8-errmsg`\] Avoid shadowing existing `msg` in fix for `EM101` ([#24363](https://github.com/astral-sh/ruff/pull/24363)) +- \[`flake8-simplify`\] Ignore pre-initialization references in `SIM113` ([#24235](https://github.com/astral-sh/ruff/pull/24235)) +- \[`pycodestyle`\] Fix `W391` fixes for consecutive empty notebook cells ([#24236](https://github.com/astral-sh/ruff/pull/24236)) +- \[`pyupgrade`\] Fix `UP008` nested class matching ([#24273](https://github.com/astral-sh/ruff/pull/24273)) +- \[`pyupgrade`\] Ignore strings with string-only escapes (`UP012`) ([#16058](https://github.com/astral-sh/ruff/pull/16058)) +- \[`ruff`\] `RUF072`: skip formfeeds on dedent ([#24308](https://github.com/astral-sh/ruff/pull/24308)) +- \[`ruff`\] Avoid re-using symbol in `RUF024` fix ([#24316](https://github.com/astral-sh/ruff/pull/24316)) +- \[`ruff`\] Parenthesize expression in `RUF050` fix ([#24234](https://github.com/astral-sh/ruff/pull/24234)) +- Disallow starred expressions as values of starred expressions ([#24280](https://github.com/astral-sh/ruff/pull/24280)) + +### Rule changes + +- \[`flake8-simplify`\] Suppress `SIM105` for `except*` before Python 3.12 ([#23869](https://github.com/astral-sh/ruff/pull/23869)) +- \[`pyflakes`\] Extend `F507` to flag `%`-format strings with zero placeholders ([#24215](https://github.com/astral-sh/ruff/pull/24215)) +- \[`pyupgrade`\] `UP018` should detect more unnecessarily wrapped literals (UP018) ([#24093](https://github.com/astral-sh/ruff/pull/24093)) +- \[`pyupgrade`\] Fix `UP008` callable scope handling to support lambdas ([#24274](https://github.com/astral-sh/ruff/pull/24274)) +- \[`ruff`\] `RUF010`: Mark fix as unsafe when it deletes a comment ([#24270](https://github.com/astral-sh/ruff/pull/24270)) + +### Formatter + +- Add `nested-string-quote-style` formatting option ([#24312](https://github.com/astral-sh/ruff/pull/24312)) + +### Documentation + +- \[`flake8-bugbear`\] Clarify RUF071 fix safety for non-path string comparisons ([#24149](https://github.com/astral-sh/ruff/pull/24149)) +- \[`flake8-type-checking`\] Clarify import cycle wording for `TC001`/`TC002`/`TC003` ([#24322](https://github.com/astral-sh/ruff/pull/24322)) + +### Other changes + +- Avoid rendering fix lines with trailing whitespace after `|` ([#24343](https://github.com/astral-sh/ruff/pull/24343)) + +### Contributors + +- [@charliermarsh](https://github.com/charliermarsh) +- [@MichaReiser](https://github.com/MichaReiser) +- [@tranhoangtu-it](https://github.com/tranhoangtu-it) +- [@dylwil3](https://github.com/dylwil3) +- [@zsol](https://github.com/zsol) +- [@renovate](https://github.com/renovate) +- [@bitloi](https://github.com/bitloi) +- [@danparizher](https://github.com/danparizher) +- [@chinar-amrutkar](https://github.com/chinar-amrutkar) +- [@second-ed](https://github.com/second-ed) +- [@getehen](https://github.com/getehen) +- [@Redovo1](https://github.com/Redovo1) +- [@matthewlloyd](https://github.com/matthewlloyd) +- [@zanieb](https://github.com/zanieb) +- [@InSyncWithFoo](https://github.com/InSyncWithFoo) +- [@RenzoMXD](https://github.com/RenzoMXD) + +## 0.15.10 + +Released on 2026-04-09. + +### Preview features + +- \[`flake8-logging`\] Allow closures in except handlers (`LOG004`) ([#24464](https://github.com/astral-sh/ruff/pull/24464)) +- \[`flake8-self`\] Make `SLF` diagnostics robust to non-self-named variables ([#24281](https://github.com/astral-sh/ruff/pull/24281)) +- \[`flake8-simplify`\] Make the fix for `collapsible-if` safe in `preview` (`SIM102`) ([#24371](https://github.com/astral-sh/ruff/pull/24371)) + +### Bug fixes + +- Avoid emitting multi-line f-string elements before Python 3.12 ([#24377](https://github.com/astral-sh/ruff/pull/24377)) +- Avoid syntax error from `E502` fixes in f-strings and t-strings ([#24410](https://github.com/astral-sh/ruff/pull/24410)) +- Strip form feeds from indent passed to `dedent_to` ([#24381](https://github.com/astral-sh/ruff/pull/24381)) +- \[`pyupgrade`\] Fix panic caused by handling of octals (`UP012`) ([#24390](https://github.com/astral-sh/ruff/pull/24390)) +- Reject multi-line f-string elements before Python 3.12 ([#24355](https://github.com/astral-sh/ruff/pull/24355)) + +### Rule changes + +- \[`ruff`\] Treat f-string interpolation as potential side effect (`RUF019`) ([#24426](https://github.com/astral-sh/ruff/pull/24426)) + +### Server + +- Add support for custom file extensions ([#24463](https://github.com/astral-sh/ruff/pull/24463)) + +### Documentation + +- Document adding fixes in CONTRIBUTING.md ([#24393](https://github.com/astral-sh/ruff/pull/24393)) +- Fix JSON typo in settings example ([#24517](https://github.com/astral-sh/ruff/pull/24517)) + +### Contributors + +- [@charliermarsh](https://github.com/charliermarsh) +- [@dylwil3](https://github.com/dylwil3) +- [@silverstein](https://github.com/silverstein) +- [@anishgirianish](https://github.com/anishgirianish) +- [@shizukushq](https://github.com/shizukushq) +- [@zanieb](https://github.com/zanieb) +- [@AlexWaygood](https://github.com/AlexWaygood) + +## 0.15.11 + +Released on 2026-04-16. + +### Preview features + +- \[`ruff`\] Ignore `RUF029` when function is decorated with `asynccontextmanager` ([#24642](https://github.com/astral-sh/ruff/pull/24642)) +- \[`airflow`\] Implement `airflow-xcom-pull-in-template-string` (`AIR201`) ([#23583](https://github.com/astral-sh/ruff/pull/23583)) +- \[`flake8-bandit`\] Fix `S103` false positives and negatives in mask analysis ([#24424](https://github.com/astral-sh/ruff/pull/24424)) + +### Bug fixes + +- \[`flake8-async`\] Omit overridden methods for `ASYNC109` ([#24648](https://github.com/astral-sh/ruff/pull/24648)) + +### Documentation + +- \[`flake8-async`\] Add override mention to `ASYNC109` docs ([#24666](https://github.com/astral-sh/ruff/pull/24666)) +- Update Neovim config examples to use `vim.lsp.config` ([#24577](https://github.com/astral-sh/ruff/pull/24577)) + +### Contributors + +- [@augustelalande](https://github.com/augustelalande) +- [@anishgirianish](https://github.com/anishgirianish) +- [@benberryallwood](https://github.com/benberryallwood) +- [@charliermarsh](https://github.com/charliermarsh) +- [@Dev-iL](https://github.com/Dev-iL) + +## 0.15.12 + +Released on 2026-04-24. + +### Preview features + +- Implement `#ruff:file-ignore` file-level suppressions ([#23599](https://github.com/astral-sh/ruff/pull/23599)) +- Implement `#ruff:ignore` logical-line suppressions ([#23404](https://github.com/astral-sh/ruff/pull/23404)) +- Revert preview changes to displayed diagnostic severity in LSP ([#24789](https://github.com/astral-sh/ruff/pull/24789)) +- \[`airflow`\] Implement `task-branch-as-short-circuit` (`AIR004`) ([#23579](https://github.com/astral-sh/ruff/pull/23579)) +- \[`flake8-bugbear`\] Fix `break`/`continue` handling in `loop-iterator-mutation` (`B909`) ([#24440](https://github.com/astral-sh/ruff/pull/24440)) +- \[`pylint`\] Fix `PLC2701` for type parameter scopes ([#24576](https://github.com/astral-sh/ruff/pull/24576)) + +### Rule changes + +- \[`pandas-vet`\] Suggest `.array` as well in `PD011` ([#24805](https://github.com/astral-sh/ruff/pull/24805)) + +### CLI + +- Respect default Unix permissions for cache files ([#24794](https://github.com/astral-sh/ruff/pull/24794)) + +### Documentation + +- \[`pylint`\] Fix `PLR0124` description not to claim self-comparison always returns the same value ([#24749](https://github.com/astral-sh/ruff/pull/24749)) +- \[`pyupgrade`\] Expand docs on reusable `TypeVar`s and scoping (`UP046`) ([#24153](https://github.com/astral-sh/ruff/pull/24153)) +- Improve rules table accessibility ([#24711](https://github.com/astral-sh/ruff/pull/24711)) + +### Contributors + +- [@dylwil3](https://github.com/dylwil3) +- [@AlexWaygood](https://github.com/AlexWaygood) +- [@woodruffw](https://github.com/woodruffw) +- [@avasis-ai](https://github.com/avasis-ai) +- [@Dev-iL](https://github.com/Dev-iL) +- [@denyszhak](https://github.com/denyszhak) +- [@ShipItAndPray](https://github.com/ShipItAndPray) +- [@anishgirianish](https://github.com/anishgirianish) +- [@augustelalande](https://github.com/augustelalande) +- [@amyreese](https://github.com/amyreese) +- [@majiayu000](https://github.com/majiayu000) + +## 0.15.13 + +Released on 2026-05-14. + +### Preview features + +- Add a rule to flag lazy imports that are eagerly evaluated ([#25016](https://github.com/astral-sh/ruff/pull/25016)) +- \[`pylint`\] Standardize diagnostic message (`PLR0914`, `PLR0917`) ([#24996](https://github.com/astral-sh/ruff/pull/24996)) + +### Bug fixes + +- Fix `F811` false positive for class methods ([#24933](https://github.com/astral-sh/ruff/pull/24933)) +- Fix setting selection for multi-folder workspace ([#24819](https://github.com/astral-sh/ruff/pull/24819)) +- \[`eradicate`\] Fix false positive for lines with leading whitespace (`ERA001`) ([#25122](https://github.com/astral-sh/ruff/pull/25122)) +- \[`flake8-pyi`\] Fix false positive for f-string debug specifier (`PYI016`) ([#24098](https://github.com/astral-sh/ruff/pull/24098)) + +### Rule changes + +- Always include panic payload in panic diagnostic message ([#24873](https://github.com/astral-sh/ruff/pull/24873)) +- Restrict `PYI034` for in-place operations to enclosing class ([#24511](https://github.com/astral-sh/ruff/pull/24511)) +- Improve error message for parameters that are declared `global` ([#24902](https://github.com/astral-sh/ruff/pull/24902)) +- Update known stdlib ([#25103](https://github.com/astral-sh/ruff/pull/25103)) + +### Performance + +- \[`isort`\] Avoid constructing `glob::Pattern`s for literal known modules ([#25123](https://github.com/astral-sh/ruff/pull/25123)) + +### CLI + +- Add TOML examples to `--config` help text ([#25013](https://github.com/astral-sh/ruff/pull/25013)) +- Colorize ruff check 'All checks passed' ([#25085](https://github.com/astral-sh/ruff/pull/25085)) + +### Configuration + +- Increase max allowed value of `line-length` setting ([#24962](https://github.com/astral-sh/ruff/pull/24962)) + +### Documentation + +- Add `D203` to rules that conflict with the formatter ([#25044](https://github.com/astral-sh/ruff/pull/25044)) +- Clarify `COM819` and formatter interaction ([#25045](https://github.com/astral-sh/ruff/pull/25045)) +- Clarify that `NotImplemented` is a value, not an exception (`F901`) ([#25054](https://github.com/astral-sh/ruff/pull/25054)) +- Update number of lint rules supported ([#24942](https://github.com/astral-sh/ruff/pull/24942)) + +### Other changes + +- Simplify the playground's markdown template ([#24924](https://github.com/astral-sh/ruff/pull/24924)) + +### Contributors + +- [@MichaReiser](https://github.com/MichaReiser) +- [@brian-c11](https://github.com/brian-c11) +- [@Andrej730](https://github.com/Andrej730) +- [@denyszhak](https://github.com/denyszhak) +- [@darestack](https://github.com/darestack) +- [@sharkdp](https://github.com/sharkdp) +- [@charliermarsh](https://github.com/charliermarsh) +- [@EkriirkE](https://github.com/EkriirkE) +- [@eyupcanakman](https://github.com/eyupcanakman) +- [@Hrk84ya](https://github.com/Hrk84ya) +- [@thernstig](https://github.com/thernstig) +- [@ntBre](https://github.com/ntBre) + +## 0.15.14 + +Released on 2026-05-21. + +### Preview features + +- \[`airflow`\] Implement `airflow-task-implicit-multiple-outputs` (`AIR202`) ([#25152](https://github.com/astral-sh/ruff/pull/25152)) +- \[`flake8-use-pathlib`\] Mark `PTH101` fix as unsafe when first argument is a class attribute annotated as `int` ([#25086](https://github.com/astral-sh/ruff/pull/25086)) +- \[`pylint`\] Implement `too-many-try-statements` (`W0717`) ([#23970](https://github.com/astral-sh/ruff/pull/23970)) +- \[`ruff`\] Add `incorrect-decorator-order` (`RUF074`) ([#23461](https://github.com/astral-sh/ruff/pull/23461)) +- \[`ruff`\] Add `fallible-context-manager` (`RUF075`) ([#22844](https://github.com/astral-sh/ruff/pull/22844)) + +### Bug fixes + +- Fix lambda formatting in interpolated string expressions ([#25144](https://github.com/astral-sh/ruff/pull/25144)) +- Treat generic `frozenset` annotations as immutable ([#25251](https://github.com/astral-sh/ruff/pull/25251)) +- \[`flake8-type-checking`\] Avoid `strict` behavior when `future-annotations` are enabled (`TC001`, `TC002`, `TC003`) ([#25035](https://github.com/astral-sh/ruff/pull/25035)) +- \[`pylint`\] Avoid false positives in `else` clause (`PLR1733`) ([#25177](https://github.com/astral-sh/ruff/pull/25177)) + +### Rule changes + +- \[`flake8-comprehensions`\] Skip `C417` for lambdas with positional-only parameters ([#25272](https://github.com/astral-sh/ruff/pull/25272)) +- \[`flake8-simplify`\] Preserve f-string source verbatim in `SIM101` fix ([#25061](https://github.com/astral-sh/ruff/pull/25061)) + +### Performance + +- Avoid unnecessary parser lookahead for operators ([#25290](https://github.com/astral-sh/ruff/pull/25290)) + +### Documentation + +- Update code example setting Neovim LSP log level ([#25284](https://github.com/astral-sh/ruff/pull/25284)) + +### Other changes + +- Add full PEP 798 support ([#25104](https://github.com/astral-sh/ruff/pull/25104)) +- Add a parser recursion limit ([#24810](https://github.com/astral-sh/ruff/pull/24810)) +- Update various `ruff_python_stdlib` APIs ([#25273](https://github.com/astral-sh/ruff/pull/25273)) + +### Contributors + +- [@ocaballeror](https://github.com/ocaballeror) +- [@lerebear](https://github.com/lerebear) +- [@samuelcolvin](https://github.com/samuelcolvin) +- [@baltasarblanco](https://github.com/baltasarblanco) +- [@aconal-com](https://github.com/aconal-com) +- [@anishgirianish](https://github.com/anishgirianish) +- [@JelleZijlstra](https://github.com/JelleZijlstra) +- [@AlexWaygood](https://github.com/AlexWaygood) +- [@ntBre](https://github.com/ntBre) +- [@adityasingh2400](https://github.com/adityasingh2400) +- [@charliermarsh](https://github.com/charliermarsh) +- [@Dev-iL](https://github.com/Dev-iL) +- [@neutrinoceros](https://github.com/neutrinoceros) +- [@shivamtiwari3](https://github.com/shivamtiwari3) +- [@Dev-X25874](https://github.com/Dev-X25874) + +## 0.15.15 + +Released on 2026-05-28. + +### Preview features + +- Fix Markdown closing fence handling ([#25310](https://github.com/astral-sh/ruff/pull/25310)) +- \[`pyflakes`\] Report duplicate imports in `typing.TYPE_CHECKING` block (`F811`) ([#22560](https://github.com/astral-sh/ruff/pull/22560)) + +### Bug fixes + +- \[`pyflakes`\] Treat function-scope bare annotations as locals per PEP 526 (`F821`) ([#21540](https://github.com/astral-sh/ruff/pull/21540)) + +### Performance + +- Avoid redundant `TokenValue` drops in the lexer ([#25300](https://github.com/astral-sh/ruff/pull/25300)) +- Reduce memory usage by dropping token-excess capacity and improve performance by approximating the initial tokens `Vec` size ([#25354](https://github.com/astral-sh/ruff/pull/25354)) +- Use `ThinVec` in AST to shrink `Stmt` ([#25361](https://github.com/astral-sh/ruff/pull/25361)) + +### Documentation + +- Fix `line-length` example for `--config` option ([#25389](https://github.com/astral-sh/ruff/pull/25389)) +- \[`flake8-comprehensions`\] Document `RecursionError` edge case in `__len__` (`C416`) ([#25286](https://github.com/astral-sh/ruff/pull/25286)) +- \[`mccabe`\] Improve example (`C901`) ([#25287](https://github.com/astral-sh/ruff/pull/25287)) +- \[`pyupgrade`\] Clarify fix safety docs (`UP007`, `UP045`) ([#25288](https://github.com/astral-sh/ruff/pull/25288)) +- \[`refurb`\] Document `FURB192` exception change for empty sequences ([#25317](https://github.com/astral-sh/ruff/pull/25317)) +- \[`ruff`\] Document false negative for user-defined types (`RUF013`) ([#25289](https://github.com/astral-sh/ruff/pull/25289)) + +### Formatter + +- Fix formatting of lambdas nested within f-strings ([#25398](https://github.com/astral-sh/ruff/pull/25398)) + +### Server + +- Return code action for `codeAction/resolve` requests that contain no or no valid URL ([#25365](https://github.com/astral-sh/ruff/pull/25365)) + +### Other changes + +- Expand semantic syntax errors for invalid walruses ([#25415](https://github.com/astral-sh/ruff/pull/25415)) + +### Contributors + +- [@chirizxc](https://github.com/chirizxc) +- [@ntBre](https://github.com/ntBre) +- [@adityasingh2400](https://github.com/adityasingh2400) +- [@charliermarsh](https://github.com/charliermarsh) +- [@fallintoplace](https://github.com/fallintoplace) +- [@martin-schlossarek](https://github.com/martin-schlossarek) +- [@MichaReiser](https://github.com/MichaReiser) +- [@Ruchir28](https://github.com/Ruchir28) + +## 0.15.16 + +Released on 2026-06-04. + +### Preview features + +- \[`flake8-async`\] Implement `yield-in-context-manager-in-async-generator` (`ASYNC119`) ([#24644](https://github.com/astral-sh/ruff/pull/24644)) +- \[`pylint`\] Narrow diagnostic range and exclude cases without exception handlers (`PLW0717`) ([#25440](https://github.com/astral-sh/ruff/pull/25440)) +- \[`ruff`\] Treat `yield` before `break` from a terminal loop as terminal (`RUF075`) ([#25447](https://github.com/astral-sh/ruff/pull/25447)) + +### Bug fixes + +- \[`eradicate`\] Avoid flagging `ruff:ignore` comments as code (`ERA001`) ([#25537](https://github.com/astral-sh/ruff/pull/25537)) +- \[`eradicate`\] Fix `ERA001`/`RUF100` conflict when `noqa` is on commented-out code ([#25414](https://github.com/astral-sh/ruff/pull/25414)) +- \[`pyflakes`\] Avoid removing the `format` call when it would change behavior (`F523`) ([#25320](https://github.com/astral-sh/ruff/pull/25320)) +- \[`pylint`\] Avoid syntax errors in invalid character replacements in f-strings before Python 3.12 (`PLE2510`, `PLE2512`, `PLE2513`, `PLE2514`, `PLE2515`) ([#25544](https://github.com/astral-sh/ruff/pull/25544)) +- \[`pyupgrade`\] Avoid converting `format` calls with more kinds of side effects (`UP032`) ([#25484](https://github.com/astral-sh/ruff/pull/25484)) + +### Rule changes + +- \[`flake8-pytest-style`\] Avoid fixes for ambiguous `argnames` and `argvalues` combinations (`PT006`) ([#24776](https://github.com/astral-sh/ruff/pull/24776)) + +### Performance + +- Drop excess capacity from statement suites during parsing ([#25368](https://github.com/astral-sh/ruff/pull/25368)) + +### Documentation + +- \[`pydocstyle`\] Improve discoverability of rules enabled for each convention ([#24973](https://github.com/astral-sh/ruff/pull/24973)) +- \[`ruff`\] Restore example code for Python versions before 3.15 (`RUF017`) ([#25439](https://github.com/astral-sh/ruff/pull/25439)) +- Fix typo `bin/active` → `bin/activate` in tutorial ([#25473](https://github.com/astral-sh/ruff/pull/25473)) + +### Other changes + +- Shrink additional parser AST collections ([#25465](https://github.com/astral-sh/ruff/pull/25465)) + +### Contributors + +- [@Redslayer112](https://github.com/Redslayer112) +- [@koriyoshi2041](https://github.com/koriyoshi2041) +- [@George-Ogden](https://github.com/George-Ogden) +- [@TejasAmle](https://github.com/TejasAmle) +- [@anishgirianish](https://github.com/anishgirianish) +- [@ntBre](https://github.com/ntBre) +- [@MichaReiser](https://github.com/MichaReiser) +- [@loganrosen](https://github.com/loganrosen) +- [@RafaelJohn9](https://github.com/RafaelJohn9) +- [@adityasingh2400](https://github.com/adityasingh2400) + +## 0.15.17 + +Released on 2026-06-11. + +### Preview features + +- Allow human-readable names in suppression comments ([#25614](https://github.com/astral-sh/ruff/pull/25614)) +- Fix handling of `ignore` comments within a `disable`/`enable` pair ([#25845](https://github.com/astral-sh/ruff/pull/25845)) +- Prioritize human-readable names in CLI output ([#25869](https://github.com/astral-sh/ruff/pull/25869)) +- Respect diagnostic start and parent ranges and trailing comments in `ruff:ignore` suppressions ([#25673](https://github.com/astral-sh/ruff/pull/25673)) +- \[`flake8-async`\] Add `trio.as_safe_channel` to safe decorators (`ASYNC119`) ([#25775](https://github.com/astral-sh/ruff/pull/25775)) +- \[`flake8-pytest-style`\] Also check `pytest_asyncio` fixtures ([#25375](https://github.com/astral-sh/ruff/pull/25375)) +- \[`ruff`\] Ban `pytest` autouse fixtures (`RUF076`) ([#25477](https://github.com/astral-sh/ruff/pull/25477)) +- \[`pyupgrade`\] Add `from __future__ import annotations` automatically (`UP007`, `UP045`) ([#23259](https://github.com/astral-sh/ruff/pull/23259)) + +### Bug fixes + +- Fix diagnostic when `ruff:enable` or `ruff:disable` appears where `ruff:ignore` is expected ([#25700](https://github.com/astral-sh/ruff/pull/25700)) +- \[`pyupgrade`\] Preserve leading empty literals to avoid syntax errors (`UP032`) ([#25491](https://github.com/astral-sh/ruff/pull/25491)) + +### Rule changes + +- \[`flake8-pytest-style`\] Clarify diagnostic message for single parameters (`PT007`) ([#25592](https://github.com/astral-sh/ruff/pull/25592)) +- \[`numpy`\] Drop autofix for `np.in1d` (`NPY201`) ([#25612](https://github.com/astral-sh/ruff/pull/25612)) +- \[`pylint`\] Exempt Python version comparisons (`PLR2004`) ([#25743](https://github.com/astral-sh/ruff/pull/25743)) + +### Performance + +- Reserve AST `Vec`s with correct capacity for common cases ([#25451](https://github.com/astral-sh/ruff/pull/25451)) + +### Formatter + +- Preserve whitespace for Quarto cell option comments ([#25641](https://github.com/astral-sh/ruff/pull/25641)) + +### CLI + +- Allow rule names in `ruff rule` ([#25640](https://github.com/astral-sh/ruff/pull/25640)) + +### Other changes + +- Fix playground diagnostics scrollbars ([#25642](https://github.com/astral-sh/ruff/pull/25642)) + +### Contributors + +- [@SuryanshSS1011](https://github.com/SuryanshSS1011) +- [@anishgirianish](https://github.com/anishgirianish) +- [@romero-deshaw](https://github.com/romero-deshaw) +- [@karlhillx](https://github.com/karlhillx) +- [@carljm](https://github.com/carljm) +- [@ntBre](https://github.com/ntBre) +- [@11happy](https://github.com/11happy) +- [@Kilo59](https://github.com/Kilo59) +- [@oconnor663](https://github.com/oconnor663) +- [@LeonidasZhak](https://github.com/LeonidasZhak) +- [@DavisVaughan](https://github.com/DavisVaughan) +- [@MeGaGiGaGon](https://github.com/MeGaGiGaGon) +- [@jonathandung](https://github.com/jonathandung) +- [@MichaReiser](https://github.com/MichaReiser) +- [@brianmego](https://github.com/brianmego) + +## 0.15.18 + +Released on 2026-06-18. + +### Preview features + +- Handle nested `ruff:ignore` comments ([#25791](https://github.com/astral-sh/ruff/pull/25791)) +- Stop displaying severity in output ([#26050](https://github.com/astral-sh/ruff/pull/26050)) +- Use human-readable names in CLI output ([#25937](https://github.com/astral-sh/ruff/pull/25937)) +- Use human-readable names in LSP and playground diagnostics ([#26058](https://github.com/astral-sh/ruff/pull/26058)) +- \[`pydocstyle`\] Prevent property docstrings starting with verbs (`D421`) ([#23775](https://github.com/astral-sh/ruff/pull/23775)) +- \[`flake8-pyi`\] Extend `PYI033` to Python files ([#26129](https://github.com/astral-sh/ruff/pull/26129)) + +### Bug fixes + +- Detect equivalent numeric mapping keys ([#26009](https://github.com/astral-sh/ruff/pull/26009)) +- Detect mapping keys equivalent to booleans ([#25982](https://github.com/astral-sh/ruff/pull/25982)) +- Detect repeated signed and complex dictionary keys ([#26007](https://github.com/astral-sh/ruff/pull/26007)) + +### Rule changes + +- \[`flake8-pyi`\] Rename `PYI033` to `legacy-type-comment` ([#26131](https://github.com/astral-sh/ruff/pull/26131)) + +### Performance + +- Use `ThinVec` for call keywords ([#25999](https://github.com/astral-sh/ruff/pull/25999)) +- Inline parser recovery context checks ([#26038](https://github.com/astral-sh/ruff/pull/26038)) +- Match parser keywords as bytes ([#26037](https://github.com/astral-sh/ruff/pull/26037)) +- Move value parsing out of lexing ([#25360](https://github.com/astral-sh/ruff/pull/25360)) + +### Server + +- Render subdiagnostics and secondary annotations as related information ([#26011](https://github.com/astral-sh/ruff/pull/26011)) + +### Documentation + +- Update fix availability for always-fixable rules ([#26091](https://github.com/astral-sh/ruff/pull/26091)) +- \[`flake8-tidy-imports`\] Add fix safety section (`TID252`) ([#17491](https://github.com/astral-sh/ruff/pull/17491)) + +### Parser + +- Reject `__debug__` lambda parameters ([#26022](https://github.com/astral-sh/ruff/pull/26022)) +- Reject `_` as a match-pattern target ([#25977](https://github.com/astral-sh/ruff/pull/25977)) +- Reject multiple starred names in sequence patterns ([#25976](https://github.com/astral-sh/ruff/pull/25976)) +- Reject parenthesized star imports ([#26021](https://github.com/astral-sh/ruff/pull/26021)) +- Reject starred comprehension targets ([#26023](https://github.com/astral-sh/ruff/pull/26023)) +- Reject unparenthesized generator expressions in class bases ([#25978](https://github.com/astral-sh/ruff/pull/25978)) +- Reject `yield` expressions after commas ([#26024](https://github.com/astral-sh/ruff/pull/26024)) +- Validate function type parameter default order ([#25981](https://github.com/astral-sh/ruff/pull/25981)) + +### Playground + +- Make diagnostic links clickable ([#26104](https://github.com/astral-sh/ruff/pull/26104)) +- Use diagnostic tags ([#26105](https://github.com/astral-sh/ruff/pull/26105)) + +### Contributors + +- [@AlexWaygood](https://github.com/AlexWaygood) +- [@ntBre](https://github.com/ntBre) +- [@gtkacz](https://github.com/gtkacz) +- [@MichaReiser](https://github.com/MichaReiser) +- [@charliermarsh](https://github.com/charliermarsh) +- [@Kalmaegi](https://github.com/Kalmaegi) + +## 0.15.19 + +Released on 2026-06-23. + +### Preview features + +- Support human-readable names when hovering suppression comments and in code actions ([#26114](https://github.com/astral-sh/ruff/pull/26114)) + +### Bug fixes + +- Fall back to default settings when editor-only settings are invalid ([#26244](https://github.com/astral-sh/ruff/pull/26244)) +- Fix panic when inserting text at a notebook cell boundary ([#26111](https://github.com/astral-sh/ruff/pull/26111)) + +### Rule changes + +- \[`pylint`\] Update fix suggestions for `__floor__`, `__trunc__`, `__length_hint__`, and `__matmul__` variants (`PLC2801`) ([#26239](https://github.com/astral-sh/ruff/pull/26239)) + +### Performance + +- Avoid allocating when parsing single string literals ([#26200](https://github.com/astral-sh/ruff/pull/26200)) +- Avoid reallocating singleton call arguments ([#26223](https://github.com/astral-sh/ruff/pull/26223)) +- Lazily create source files for lint diagnostics ([#26226](https://github.com/astral-sh/ruff/pull/26226)) +- Optimize formatter text width and indentation ([#26236](https://github.com/astral-sh/ruff/pull/26236)) +- Reserve capacity for builtin bindings ([#26229](https://github.com/astral-sh/ruff/pull/26229)) +- Skip repeated-key checks for singleton dictionaries ([#26228](https://github.com/astral-sh/ruff/pull/26228)) +- Use ArrayVec for qualified name segments ([#26224](https://github.com/astral-sh/ruff/pull/26224)) + +### Documentation + +- \[`flake8-pyi`\] Note that `PYI051` is an opinionated stylistic rule ([#26179](https://github.com/astral-sh/ruff/pull/26179)) +- \[`pyupgrade`\] Clarify `UP029` as a Python 2 compatibility rule ([#26243](https://github.com/astral-sh/ruff/pull/26243)) + +### Other changes + +- Publish Ruff crates to crates.io ([#26271](https://github.com/astral-sh/ruff/pull/26271)) + +### Contributors + +- [@MakenRosa](https://github.com/MakenRosa) +- [@MichaReiser](https://github.com/MichaReiser) +- [@trilamsr](https://github.com/trilamsr) +- [@ntBre](https://github.com/ntBre) +- [@sanjibani](https://github.com/sanjibani) +- [@charliermarsh](https://github.com/charliermarsh) + +## 0.15.20 + +Released on 2026-06-25. + +### Preview features + +- Allow human-readable names in rule selectors ([#25887](https://github.com/astral-sh/ruff/pull/25887)) +- Emit a warning instead of an error for unknown rule selectors ([#26113](https://github.com/astral-sh/ruff/pull/26113)) +- Match `noqa` shebang handling in `ruff:ignore` comments ([#26286](https://github.com/astral-sh/ruff/pull/26286)) +- \[`ruff`\] Remove `pytest-fixture-autouse` (`RUF076`) ([#26240](https://github.com/astral-sh/ruff/pull/26240), [#26371](https://github.com/astral-sh/ruff/pull/26371)) + +### Documentation + +- Add versioning sections to custom crate READMEs ([#26317](https://github.com/astral-sh/ruff/pull/26317)) +- Update `ruff_python_parser` README for crates.io ([#26315](https://github.com/astral-sh/ruff/pull/26315)) +- \[`perflint`\] Clarify that `PERF402` applies to any iterable ([#26242](https://github.com/astral-sh/ruff/pull/26242)) + +### Contributors + +- [@dhruvmanila](https://github.com/dhruvmanila) +- [@MichaReiser](https://github.com/MichaReiser) +- [@ntBre](https://github.com/ntBre) +- [@trilamsr](https://github.com/trilamsr) + +## 0.15.21 + +Released on 2026-07-09. + +### Preview features + +- Add `--add-ignore` for adding `ruff:ignore` comments ([#26346](https://github.com/astral-sh/ruff/pull/26346)) +- \[`flake8-comprehensions`\] Drop `C409` tuple comprehension preview behavior ([#25707](https://github.com/astral-sh/ruff/pull/25707)) +- Avoid whitespace normalization when formatting comments ([#26455](https://github.com/astral-sh/ruff/pull/26455)) +- \[`pyupgrade`\] Lint and fix use of deprecated `abc` decorators (`UP051`) ([#26417](https://github.com/astral-sh/ruff/pull/26417)) + +### Bug fixes + +- Refine non-empty f-string detection ([#26526](https://github.com/astral-sh/ruff/pull/26526)) +- Detect syntax errors in individual notebook cells ([#26419](https://github.com/astral-sh/ruff/pull/26419)) +- \[`flake8-implicit-str-concat`\] Fix `ISC003` autofix incorrectly stripping `+` from comments ([#26554](https://github.com/astral-sh/ruff/pull/26554)) + +### Rule changes + +- \[`flake8-executable`\] Mark `EXE004` fix as unsafe ([#26033](https://github.com/astral-sh/ruff/pull/26033)) +- \[`flake8-pyi`\] Mark `PYI061` fixes as unsafe in Python files ([#26533](https://github.com/astral-sh/ruff/pull/26533)) +- \[`pydocstyle`\] Skip `overload-with-docstring` in stub files (`D418`) ([#26318](https://github.com/astral-sh/ruff/pull/26318)) + +### Performance + +- Avoid per-token source index visitor calls ([#26506](https://github.com/astral-sh/ruff/pull/26506)) +- Cache parenthesized expression boundaries in the formatter ([#26344](https://github.com/astral-sh/ruff/pull/26344)) +- Improve performance of rendering edits in preview mode ([#26565](https://github.com/astral-sh/ruff/pull/26565)) +- Inline `fits_element` in formatter ([#26429](https://github.com/astral-sh/ruff/pull/26429)) +- Inline formatter printing hot paths ([#26504](https://github.com/astral-sh/ruff/pull/26504)) +- Lazily create builtin bindings ([#26510](https://github.com/astral-sh/ruff/pull/26510)) +- Skip empty trivia scans in the source indexer ([#26507](https://github.com/astral-sh/ruff/pull/26507)) +- Use ICF for macOS release builds ([#25780](https://github.com/astral-sh/ruff/pull/25780)) + +### Formatter + +- Add `--extend-exclude` to `ruff format` ([#26372](https://github.com/astral-sh/ruff/pull/26372)) + +### Documentation + +- Add "How does Ruff's import sorting compare to isort?" link to README ([#26530](https://github.com/astral-sh/ruff/pull/26530)) +- Fix Mozilla Firefox repository link in README ([#26537](https://github.com/astral-sh/ruff/pull/26537)) +- \[`flake8-bandit`\] Fix misleading docstring for `mako-templates` (`S702`) ([#26432](https://github.com/astral-sh/ruff/pull/26432)) +- \[`ruff`\] Fix non-triggering example for `if-key-in-dict-del` (`RUF051`) ([#26433](https://github.com/astral-sh/ruff/pull/26433)) + +### Contributors + +- [@EkriirkE](https://github.com/EkriirkE) +- [@tingerrr](https://github.com/tingerrr) +- [@s-rigaud](https://github.com/s-rigaud) +- [@nikolauspschuetz](https://github.com/nikolauspschuetz) +- [@Avasam](https://github.com/Avasam) +- [@ntBre](https://github.com/ntBre) +- [@omar-y-abdi](https://github.com/omar-y-abdi) +- [@AlexWaygood](https://github.com/AlexWaygood) +- [@sylvestre](https://github.com/sylvestre) +- [@shaanmajid](https://github.com/shaanmajid) +- [@lerebear](https://github.com/lerebear) +- [@baltasarblanco](https://github.com/baltasarblanco) +- [@Sanjays2402](https://github.com/Sanjays2402) +- [@ZedThree](https://github.com/ZedThree) +- [@servusdei2018](https://github.com/servusdei2018) +- [@charliermarsh](https://github.com/charliermarsh) +- [@jesco-absolute](https://github.com/jesco-absolut) +- [@velikodniy](https://github.com/velikodniy) +- [@zaniebot](https://github.com/zaniebot) +- [@epage](https://github.com/epage) + +## 0.15.22 + +Released on 2026-07-16. + +### Preview features + +- \[`pycodestyle`\] Add an autofix for `E402` ([#22212](https://github.com/astral-sh/ruff/pull/22212)) +- \[`refurb`\] Allow subclassing builtins in stub files (`FURB189`) ([#26812](https://github.com/astral-sh/ruff/pull/26812)) +- \[`ruff`\] Add rule to replace `noqa` comments with `ruff:ignore` (`RUF105`) ([#26423](https://github.com/astral-sh/ruff/pull/26423)) +- \[`ruff`\] Add rule to use human-readable names in `ruff:ignore` comments (`RUF106`) ([#26682](https://github.com/astral-sh/ruff/pull/26682)) +- \[`ruff`\] Add rule to use human-readable names in configuration selectors (`RUF201`) ([#26772](https://github.com/astral-sh/ruff/pull/26772)) + +### Bug fixes + +- \[`flake8-pyi`\] Fix false positive in `__all__` (`PYI053`) ([#26872](https://github.com/astral-sh/ruff/pull/26872)) + +### Rule changes + +- \[`pylint`\] Ignore mutable type updates in `redefined-loop-name` (`PLW2901`) ([#25733](https://github.com/astral-sh/ruff/pull/25733)) + +### Performance + +- Avoid redundant lexer token bookkeeping ([#26765](https://github.com/astral-sh/ruff/pull/26765)) +- Avoid redundant pending-indentation writes ([#26774](https://github.com/astral-sh/ruff/pull/26774)) +- Avoid unnecessary identifier lookahead ([#26525](https://github.com/astral-sh/ruff/pull/26525)) +- Reuse parser scratch buffers ([#26798](https://github.com/astral-sh/ruff/pull/26798)) + +### Documentation + +- Document argfile support ([#26803](https://github.com/astral-sh/ruff/pull/26803)) +- \[`flake8-datetimez`\] Clarify naming guidance for `datetime.today` (`DTZ002`) ([#26658](https://github.com/astral-sh/ruff/pull/26658)) +- \[`pycodestyle`\] Document `E731` fix safety ([#26847](https://github.com/astral-sh/ruff/pull/26847)) +- \[`ruff`\] Clarify intentional async contexts for `unused-async` (`RUF029`) ([#26641](https://github.com/astral-sh/ruff/pull/26641)) + +### Contributors + +- [@dwego](https://github.com/dwego) +- [@MichaReiser](https://github.com/MichaReiser) +- [@Joosboy](https://github.com/Joosboy) +- [@KaufmanDmitriy](https://github.com/KaufmanDmitriy) +- [@PeterJCLaw](https://github.com/PeterJCLaw) +- [@ntBre](https://github.com/ntBre) +- [@charliermarsh](https://github.com/charliermarsh) diff --git a/crates/basedpython/Cargo.lock b/crates/basedpython/Cargo.lock index b55c0b6ecf..8ff93b7e8a 100644 --- a/crates/basedpython/Cargo.lock +++ b/crates/basedpython/Cargo.lock @@ -128,7 +128,7 @@ dependencies = [ "manyhow", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -144,7 +144,7 @@ dependencies = [ "proc-macro2", "quote", "quote-use", - "syn", + "syn 2.0.117", ] [[package]] @@ -218,6 +218,57 @@ version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "by_build" +version = "0.0.0" +dependencies = [ + "anyhow", + "by_codegen_c", + "by_ir", + "by_irbuild", + "by_opt", + "by_rt", + "by_transforms", + "serde", + "serde_json", +] + +[[package]] +name = "by_codegen_c" +version = "0.0.0" +dependencies = [ + "by_ir", +] + +[[package]] +name = "by_ir" +version = "0.0.0" + +[[package]] +name = "by_irbuild" +version = "0.0.0" +dependencies = [ + "by_ir", + "ruff_db", + "ruff_python_ast", + "ruff_python_stdlib", + "ruff_text_size", + "thin-vec", + "ty_project", + "ty_python_semantic", +] + +[[package]] +name = "by_opt" +version = "0.0.0" +dependencies = [ + "by_ir", +] + +[[package]] +name = "by_rt" +version = "0.0.0" + [[package]] name = "by_transforms" version = "0.0.0" @@ -259,7 +310,7 @@ checksum = "89385e82b5d1821d2219e0b095efa2cc1f246cbf99080f3be46a1a85c0d392d9" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -416,7 +467,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -604,7 +655,7 @@ checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -648,7 +699,7 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -815,9 +866,9 @@ dependencies = [ [[package]] name = "gen-lsp-types" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd635c5206acd03ea024d6b5902539e5c903de3afa220fdb5c94b583af77f4f" +checksum = "b64887ac3a8083427ae935a7296db876871582cd57eac077564f8bc18fa49116" dependencies = [ "serde", "serde_json", @@ -832,7 +883,7 @@ checksum = "c736d226c32e496b8377813b52269e11ad3a48d8373b68862d0364f04fd1229d" dependencies = [ "attribute-derive", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1204,7 +1255,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1260,7 +1311,7 @@ checksum = "782d32378dddf207193ac91cefb848ad41abb58195c95168e1291227a0832b47" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1366,7 +1417,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0903173ea316c34a44d0497161e04d9210af44f5f5e89bf2f55d9a254c9a0e8d" dependencies = [ "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1436,7 +1487,7 @@ dependencies = [ "manyhow-macros", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1524,7 +1575,7 @@ checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1895,7 +1946,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.117", ] [[package]] @@ -1935,7 +1986,7 @@ checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2004,7 +2055,7 @@ dependencies = [ "proc-macro-utils", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2137,7 +2188,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2205,12 +2256,12 @@ checksum = "c0ed1a78a1b19d184b0daa629dd9a024573173ec7d485b287cb369fb3607cc1c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] name = "ruff" -version = "0.15.22" +version = "0.16.2" dependencies = [ "anyhow", "argfile", @@ -2267,7 +2318,7 @@ dependencies = [ [[package]] name = "ruff_annotate_snippets" -version = "0.0.5" +version = "0.0.8" dependencies = [ "anstyle", "memchr", @@ -2276,7 +2327,7 @@ dependencies = [ [[package]] name = "ruff_cache" -version = "0.0.5" +version = "0.0.8" dependencies = [ "char_str", "filetime", @@ -2289,7 +2340,7 @@ dependencies = [ [[package]] name = "ruff_db" -version = "0.0.5" +version = "0.0.8" dependencies = [ "anstyle", "arc-swap", @@ -2332,7 +2383,7 @@ dependencies = [ [[package]] name = "ruff_diagnostics" -version = "0.0.5" +version = "0.0.8" dependencies = [ "get-size2", "is-macro", @@ -2342,7 +2393,7 @@ dependencies = [ [[package]] name = "ruff_formatter" -version = "0.0.5" +version = "0.0.8" dependencies = [ "drop_bomb", "ruff_cache", @@ -2357,7 +2408,7 @@ dependencies = [ [[package]] name = "ruff_graph" -version = "0.0.5" +version = "0.0.8" dependencies = [ "anyhow", "clap", @@ -2378,7 +2429,7 @@ dependencies = [ [[package]] name = "ruff_index" -version = "0.0.5" +version = "0.0.8" dependencies = [ "get-size2", "ruff_macros", @@ -2387,7 +2438,7 @@ dependencies = [ [[package]] name = "ruff_linter" -version = "0.15.22" +version = "0.16.2" dependencies = [ "aho-corasick", "anyhow", @@ -2446,7 +2497,7 @@ dependencies = [ [[package]] name = "ruff_macros" -version = "0.0.5" +version = "0.0.8" dependencies = [ "heck", "itertools 0.15.0", @@ -2454,12 +2505,12 @@ dependencies = [ "quote", "regex", "ruff_python_trivia", - "syn", + "syn 3.0.3", ] [[package]] name = "ruff_markdown" -version = "0.0.5" +version = "0.0.8" dependencies = [ "regex", "ruff_python_ast", @@ -2472,14 +2523,14 @@ dependencies = [ [[package]] name = "ruff_memory_usage" -version = "0.0.5" +version = "0.0.8" dependencies = [ "get-size2", ] [[package]] name = "ruff_notebook" -version = "0.0.5" +version = "0.0.8" dependencies = [ "anyhow", "rand 0.10.1", @@ -2494,14 +2545,14 @@ dependencies = [ [[package]] name = "ruff_options_metadata" -version = "0.0.5" +version = "0.0.8" dependencies = [ "serde", ] [[package]] name = "ruff_python_ast" -version = "0.0.5" +version = "0.0.8" dependencies = [ "aho-corasick", "arrayvec", @@ -2525,7 +2576,7 @@ dependencies = [ [[package]] name = "ruff_python_codegen" -version = "0.0.5" +version = "0.0.8" dependencies = [ "ruff_python_ast", "ruff_python_literal", @@ -2536,7 +2587,7 @@ dependencies = [ [[package]] name = "ruff_python_formatter" -version = "0.0.5" +version = "0.0.8" dependencies = [ "anyhow", "clap", @@ -2564,7 +2615,7 @@ dependencies = [ [[package]] name = "ruff_python_importer" -version = "0.0.5" +version = "0.0.8" dependencies = [ "anyhow", "ruff_diagnostics", @@ -2577,7 +2628,7 @@ dependencies = [ [[package]] name = "ruff_python_index" -version = "0.0.5" +version = "0.0.8" dependencies = [ "ruff_python_ast", "ruff_python_trivia", @@ -2587,7 +2638,7 @@ dependencies = [ [[package]] name = "ruff_python_literal" -version = "0.0.5" +version = "0.0.8" dependencies = [ "bitflags", "icu_properties", @@ -2597,7 +2648,7 @@ dependencies = [ [[package]] name = "ruff_python_parser" -version = "0.0.5" +version = "0.0.8" dependencies = [ "bitflags", "bstr", @@ -2617,7 +2668,7 @@ dependencies = [ [[package]] name = "ruff_python_semantic" -version = "0.0.5" +version = "0.0.8" dependencies = [ "bitflags", "is-macro", @@ -2635,7 +2686,7 @@ dependencies = [ [[package]] name = "ruff_python_stdlib" -version = "0.0.5" +version = "0.0.8" dependencies = [ "bitflags", "unicode-ident", @@ -2643,7 +2694,7 @@ dependencies = [ [[package]] name = "ruff_python_trivia" -version = "0.0.5" +version = "0.0.8" dependencies = [ "itertools 0.15.0", "ruff_source_file", @@ -2654,7 +2705,7 @@ dependencies = [ [[package]] name = "ruff_ranged_value" -version = "0.0.5" +version = "0.0.8" dependencies = [ "get-size2", "ruff_db", @@ -2665,7 +2716,7 @@ dependencies = [ [[package]] name = "ruff_server" -version = "0.0.5" +version = "0.0.8" dependencies = [ "anyhow", "crossbeam", @@ -2703,7 +2754,7 @@ dependencies = [ [[package]] name = "ruff_source_file" -version = "0.0.5" +version = "0.0.8" dependencies = [ "get-size2", "memchr", @@ -2713,7 +2764,7 @@ dependencies = [ [[package]] name = "ruff_text_size" -version = "0.0.5" +version = "0.0.8" dependencies = [ "get-size2", "serde", @@ -2721,7 +2772,7 @@ dependencies = [ [[package]] name = "ruff_workspace" -version = "0.0.5" +version = "0.0.8" dependencies = [ "anyhow", "colored", @@ -2802,9 +2853,9 @@ checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "salsa" -version = "0.28.1" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a14fdadbf856222e731756d7fdbdf193a7abf8fdab009bb45f48671a42719a84" +checksum = "cf0e374215cd2db2b5c75d7b3a99cb0cc052c0595335dfdefc03d4eb08f4aa81" dependencies = [ "boxcar", "compact_str", @@ -2829,19 +2880,19 @@ dependencies = [ [[package]] name = "salsa-macro-rules" -version = "0.28.1" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d7dc08ba69b9aedfa61dfc4d65548ae42c0d8b90bbd62cd121776920841bcf" +checksum = "85f4b7d4405540bbd6d4ffa52d4322d983f3781954d3073067ac1bdb028459b3" [[package]] name = "salsa-macros" -version = "0.28.1" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec5c48c5a4a53a6e2be9762f56566f1325d4394c011c00b3ea86ba2d13411e71" +checksum = "445be2bfbb2f67cb663225ecd7bc5a25370c0250fca30f9d8cbad9a913650370" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] @@ -2875,7 +2926,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn", + "syn 2.0.117", ] [[package]] @@ -2923,7 +2974,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2934,7 +2985,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3061,7 +3112,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3081,6 +3132,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -3089,7 +3151,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3168,7 +3230,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3179,7 +3241,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3284,6 +3346,19 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.3", +] + [[package]] name = "toml_parser" version = "1.1.2+spec-1.1.0" @@ -3295,9 +3370,9 @@ dependencies = [ [[package]] name = "toml_writer" -version = "1.1.1+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "tracing" @@ -3319,7 +3394,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3378,6 +3453,9 @@ version = "0.0.0" dependencies = [ "anyhow", "argfile", + "by_build", + "by_ir", + "by_irbuild", "by_transforms", "clap", "clap_complete_command", @@ -3391,6 +3469,7 @@ dependencies = [ "ruff_db", "ruff_diagnostics", "ruff_ranged_value", + "ruff_text_size", "salsa", "serde", "serde_json", @@ -3412,7 +3491,7 @@ dependencies = [ [[package]] name = "ty_combine" -version = "0.0.5" +version = "0.0.8" dependencies = [ "ordermap", "ruff_db", @@ -3449,6 +3528,7 @@ dependencies = [ "smallvec", "strum", "strum_macros", + "toml_edit", "tracing", "ty_module_resolver", "ty_project", @@ -3459,12 +3539,13 @@ dependencies = [ [[package]] name = "ty_module_resolver" -version = "0.0.5" +version = "0.0.8" dependencies = [ "anyhow", "camino", "compact_str", "get-size2", + "ordermap", "regex", "regex-syntax", "ruff_db", @@ -3491,11 +3572,11 @@ dependencies = [ "crossbeam", "get-size2", "globset", - "memchr", "notify", "ordermap", "parking_lot", "pep440_rs", + "pep508_rs", "rayon", "regex", "regex-automata", @@ -3527,7 +3608,7 @@ dependencies = [ [[package]] name = "ty_python_core" -version = "0.0.5" +version = "0.0.8" dependencies = [ "bitflags", "bitvec", @@ -3557,7 +3638,7 @@ dependencies = [ [[package]] name = "ty_python_semantic" -version = "0.0.5" +version = "0.0.8" dependencies = [ "bitflags", "char_str", @@ -3630,11 +3711,12 @@ dependencies = [ "ty_module_resolver", "ty_project", "ty_python_core", + "ty_python_semantic", ] [[package]] name = "ty_site_packages" -version = "0.0.5" +version = "0.0.8" dependencies = [ "camino", "colored", @@ -3654,14 +3736,14 @@ dependencies = [ [[package]] name = "ty_static" -version = "0.0.5" +version = "0.0.8" dependencies = [ "ruff_macros", ] [[package]] name = "ty_vendored" -version = "0.0.5" +version = "0.0.8" dependencies = [ "path-slash", "ruff_db", @@ -3886,7 +3968,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -3991,7 +4073,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -4002,7 +4084,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -4123,6 +4205,9 @@ name = "winnow" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] [[package]] name = "wit-bindgen" @@ -4160,7 +4245,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -4176,7 +4261,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -4252,7 +4337,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -4273,7 +4358,7 @@ checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -4293,7 +4378,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -4327,7 +4412,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] diff --git a/crates/by_irbuild/src/lib.rs b/crates/by_irbuild/src/lib.rs index 26b29e3910..d727ddcb61 100644 --- a/crates/by_irbuild/src/lib.rs +++ b/crates/by_irbuild/src/lib.rs @@ -66,11 +66,13 @@ use ruff_python_ast::{ }; use ruff_python_stdlib::identifiers::is_identifier; use ruff_text_size::{Ranged, TextSize}; +use ty_python_semantic::ProgramEnvironment; use ty_python_semantic::{HasType, SemanticModel}; /// lower every module-level function ty can represent natively pub fn build_module( db: &dyn ty_python_semantic::Db, + env: &ProgramEnvironment<'_>, model: &SemanticModel<'_>, suite: &[Stmt], module_name: &str, @@ -119,7 +121,7 @@ pub fn build_module( if let Stmt::ClassDef(class) = stmt && layouts.contains_key(class.name.as_str()) { - match class_fields(db, model, suite, class, &layouts) { + match class_fields(db, env, model, suite, class, &layouts) { Ok(fields) => { if layouts.get(class.name.as_str()) != Some(&fields) { layouts.insert(class.name.to_string(), fields); @@ -163,7 +165,7 @@ pub fn build_module( if decorated { mutable.insert(class.name.as_str()); } - if let Some(base) = base_class(db, model, class, &layouts).ok().flatten() { + if let Some(base) = base_class(db, env, model, class, &layouts).ok().flatten() { mutable.insert(class.name.as_str()); // a base of ours is made mutable too, whether it stands alone or beside a // name from outside: this class may override a method of it, and a direct @@ -196,6 +198,7 @@ pub fn build_module( Stmt::FunctionDef(method) if method.decorator_list.is_empty() => { let mut signature = signature( db, + env, model, method, &layouts, @@ -220,7 +223,8 @@ pub fn build_module( .iter() .filter_map(|stmt| match stmt { Stmt::FunctionDef(function) => { - let mut signature = signature(db, model, function, &layouts, None, &[]).ok()?; + let mut signature = + signature(db, env, model, function, &layouts, None, &[]).ok()?; resumable_return(function, &mut signature); Some((function.name.to_string(), signature)) } @@ -234,14 +238,15 @@ pub fn build_module( // handing a name to a callee's edition keeps it in a buffer, which is itself an // eligibility this is computing — so it is iterated. eligibility only ever grows, // and the positions are bounded by the parameters, so it settles - let supplied = supplied_arrays(db, model, suite, &layouts); + let supplied = supplied_arrays(db, env, model, suite, &layouts); let mut arrays = ArrayEditions::new(); loop { let next: ArrayEditions = suite .iter() .filter_map(|stmt| match stmt { Stmt::FunctionDef(function) if !generators::is_generator(&function.body) => { - let found = array_editions(db, model, function, &layouts, &arrays, &supplied); + let found = + array_editions(db, env, model, function, &layouts, &arrays, &supplied); (!found.is_empty()).then(|| (function.name.to_string(), found)) } _ => None, @@ -299,6 +304,7 @@ pub fn build_module( }; let signature = signature( db, + env, model, init, &layouts, @@ -321,7 +327,7 @@ pub fn build_module( }) .filter_map(|class| { // the map is the *layout* chain, which only an in-module base extends - base_class(db, model, class, &layouts) + base_class(db, env, model, class, &layouts) .ok() .flatten() .and_then(|base| base.in_module().map(str::to_owned)) @@ -334,6 +340,7 @@ pub fn build_module( bases: &bases, unique_loop_bindings, db, + env, model, native_callees: &native_callees, suite, @@ -365,10 +372,10 @@ pub fn build_module( if let Stmt::FunctionDef(function) = stmt { module .gradual - .extend(gradual_signature_places(db, model, function)); + .extend(gradual_signature_places(db, env, model, function)); module .promoted - .extend(promoted_places(db, model, function, &layouts)); + .extend(promoted_places(db, env, model, function, &layouts)); match lower_function(unit, function).and_then(verified) { Ok((lowered, environments)) => { module.functions.push(lowered); @@ -542,7 +549,11 @@ fn lower_generator( captures: Option<&closures::Nested>, ) -> Lowered<(Function, Vec)> { let Unit { - db, model, layouts, .. + env, + db, + model, + layouts, + .. } = unit; generators::check(function)?; @@ -556,7 +567,8 @@ fn lower_generator( // a field is a *cell* — `object`, with an unset check on every read — unless the // name is definitely assigned, in which case it takes the local's own // representation and the read is an infallible `GetField` - let representations = local_representations(db, model, &function.body, layouts, unit.arrays); + let representations = + local_representations(db, env, model, &function.body, layouts, unit.arrays); let locals: Vec = representations .iter() .map(|(name, _)| name.clone()) @@ -567,7 +579,7 @@ fn lower_generator( // the constructor seeds every one of them, so they are as assigned as a parameter let mut assigned = generators::definitely_assigned(function); assigned.extend(captured.iter().cloned()); - let parameters = signature(db, model, function, layouts, receiver, &[])?.params; + let parameters = signature(db, env, model, function, layouts, receiver, &[])?.params; let representation = |name: &str| { parameters .iter() @@ -674,7 +686,11 @@ fn lower_generator_constructor( captured: &[String], ) -> Lowered { let Unit { - db, model, layouts, .. + env, + db, + model, + layouts, + .. } = unit; let Signature { params, @@ -686,7 +702,7 @@ fn lower_generator_constructor( deferring, computed_defaults, .. - } = signature(db, model, function, layouts, receiver, &[])?; + } = signature(db, env, model, function, layouts, receiver, &[])?; let mut builder = FunctionBuilder::new(function.name.to_string(), RType::OBJECT); builder.at(span(function.range)); @@ -1122,13 +1138,14 @@ fn prune_unbuildable( /// been chosen, and where the report can name something a user wrote fn promoted_places( db: &dyn ty_python_semantic::Db, + env: &ProgramEnvironment<'_>, model: &SemanticModel<'_>, function: &ast::StmtFunctionDef, layouts: &Layouts, ) -> Vec { let mut places: Vec = Vec::new(); let record = |places: &mut Vec<_>, place: String, ty| { - if let Some(missed) = mapper::missed_representation(db, ty, layouts) { + if let Some(missed) = mapper::missed_representation(db, env, ty, layouts) { places.push(by_ir::function::PromotedPlace { function: function.name.to_string(), place, @@ -1164,6 +1181,7 @@ fn promoted_places( fn gradual_signature_places( db: &dyn ty_python_semantic::Db, + env: &ProgramEnvironment<'_>, model: &SemanticModel<'_>, function: &ast::StmtFunctionDef, ) -> Vec { @@ -1171,7 +1189,7 @@ fn gradual_signature_places( // member answers for the whole type, and so does the gradual bound of the hole an // unannotated parameter opens let is_gradual = - |ty: ty_python_semantic::types::Type<'_>| ty.is_dynamic() || ty.has_gradual_member(db); + |ty: ty_python_semantic::types::Type<'_>| ty.is_dynamic() || ty.has_gradual_member(db, env); let mut places = Vec::new(); for parameter in &function.parameters.args { if parameter @@ -1215,6 +1233,7 @@ fn lower_class<'a>( class: &'a ast::StmtClassDef, ) -> Lowered<(by_ir::function::ClassIr, Vec)> { let Unit { + env, db, model, suite, @@ -1249,9 +1268,9 @@ fn lower_class<'a>( } } } - let base = base_class(db, model, class, layouts)?; + let base = base_class(db, env, model, class, layouts)?; - let fields = class_fields(db, model, suite, class, layouts)?; + let fields = class_fields(db, env, model, suite, class, layouts)?; let mut lowered = Vec::new(); let mut environments = Vec::new(); let mut constants = Vec::new(); @@ -1462,6 +1481,7 @@ fn slot_zero(parameters: &ast::Parameters) -> Option<&ast::ParameterWithDefault> /// there, and a struct field has no way to be absent fn init_fields( db: &dyn ty_python_semantic::Db, + env: &ProgramEnvironment<'_>, model: &SemanticModel<'_>, class: &ast::StmtClassDef, layouts: &Layouts, @@ -1504,7 +1524,7 @@ fn init_fields( let ty = target .inferred_type(model) .ok_or_else(|| Decline::new("an attribute assignment has no inferred type"))?; - let rtype = map_type_with(db, ty, layouts)?; + let rtype = map_type_with(db, env, ty, layouts)?; match widths.iter_mut().find(|(written, _)| *written == name) { Some((_, existing)) => { if *existing != rtype { @@ -1719,6 +1739,7 @@ fn attribute_of(target: &Expr, receiver: &str, owner: Option<&str>) -> Option, model: &SemanticModel<'_>, class: &ast::StmtClassDef, layouts: &Layouts, @@ -1738,7 +1759,7 @@ fn base_class( // and no members, so there is nothing to lay out and nothing to inherit. // resolved rather than matched by name, because a module may bind `object` // to something else entirely - [base] if !keyed && is_builtin_object(db, model, base, layouts) => Ok(None), + [base] if !keyed && is_builtin_object(db, env, model, base, layouts) => Ok(None), [Expr::Name(name)] if layouts.contains_key(name.id.as_str()) => { if keyed { // the layout would have to be ours, which only the type spec lays out, @@ -1841,6 +1862,7 @@ fn class_keywords(class: &ast::StmtClassDef) -> Lowered> { /// subclass the wrong base entirely fn is_builtin_object( db: &dyn ty_python_semantic::Db, + env: &ProgramEnvironment<'_>, model: &SemanticModel<'_>, base: &Expr, layouts: &Layouts, @@ -1854,7 +1876,7 @@ fn is_builtin_object( // and it has to *be* a class: an unresolved name is gradual, and a gradual base // says nothing about what it brings base.inferred_type(model) - .is_some_and(|ty| !ty.is_dynamic() && mapper::map_type(db, ty).is_ok()) + .is_some_and(|ty| !ty.is_dynamic() && mapper::map_type(db, env, ty).is_ok()) } /// a base written as a name, or as a chain of attributes on one, as a dotted path @@ -1956,6 +1978,7 @@ fn stores_through_setattr(class: &ast::StmtClassDef) -> bool { /// the declared fields of a class, or a decline explaining why it has no layout fn class_fields( db: &dyn ty_python_semantic::Db, + env: &ProgramEnvironment<'_>, model: &SemanticModel<'_>, suite: &[Stmt], class: &ast::StmtClassDef, @@ -1984,7 +2007,7 @@ fn class_fields( } } } - let base = base_class(db, model, class, layouts)?; + let base = base_class(db, env, model, class, layouts)?; // a subclass's struct *begins* with its base's fields, in the same order and // unchanged, so a pointer to one is a valid pointer to the other — which is what @@ -2000,17 +2023,21 @@ fn class_fields( .cloned() .collect(); let fields = if is_data { - data_fields(db, model, class, layouts, inherited)? + data_fields(db, env, model, class, layouts, inherited)? } else { // a plain class *is* its `__init__`: the fields are the attributes it gives // the instance, in the order it gives them, and a `__slots__` declares the // ones no assignment reached - slot_fields(class, init_fields(db, model, class, layouts, inherited)?)? + slot_fields( + class, + init_fields(db, env, model, class, layouts, inherited)?, + )? }; - let fields = spec_built_where_needed(db, model, suite, class, base.as_ref(), layouts, fields)?; + let fields = + spec_built_where_needed(db, env, model, suite, class, base.as_ref(), layouts, fields)?; metaclass_carries_the_body(class, base.as_ref(), layouts, is_data)?; Ok(presence_where_a_finalizer_reads( - db, model, suite, layouts, class, fields, + db, env, model, suite, layouts, class, fields, )) } @@ -2069,6 +2096,7 @@ fn metaclass_carries_the_body( /// the fields of a `data class`: the annotations its body writes, after its base's fn data_fields( db: &dyn ty_python_semantic::Db, + env: &ProgramEnvironment<'_>, model: &SemanticModel<'_>, class: &ast::StmtClassDef, layouts: &Layouts, @@ -2098,7 +2126,7 @@ fn data_fields( .ok_or_else(|| Decline::new("a field has no inferred type"))?; fields.push(by_ir::function::FieldDecl { name, - ty: map_type_with(db, ty, layouts)?, + ty: map_type_with(db, env, ty, layouts)?, default, optional: false, }); @@ -2121,8 +2149,10 @@ fn data_fields( /// spec is the only construction that can say where — so such a class needs `type` for /// every base's metaclass and no class keyword to place. one that adds no fields is /// built through its metaclass instead, and neither restriction reaches it +#[expect(clippy::too_many_arguments)] fn spec_built_where_needed( db: &dyn ty_python_semantic::Db, + env: &ProgramEnvironment<'_>, model: &SemanticModel<'_>, suite: &[Stmt], class: &ast::StmtClassDef, @@ -2147,7 +2177,9 @@ fn spec_built_where_needed( let appended = match base { None => false, Some(ClassBase::External(_)) => true, - Some(ClassBase::InModule(name)) => laid_out_from_outside(db, model, suite, layouts, name), + Some(ClassBase::InModule(name)) => { + laid_out_from_outside(db, env, model, suite, layouts, name) + } }; if appended && base.is_some_and(|base| { @@ -2202,13 +2234,14 @@ fn class_written<'a>(suite: &'a [Stmt], name: &str) -> Option<&'a ast::StmtClass /// the shared ones at two different offsets fn presence_where_a_finalizer_reads( db: &dyn ty_python_semantic::Db, + env: &ProgramEnvironment<'_>, model: &SemanticModel<'_>, suite: &[Stmt], layouts: &Layouts, class: &ast::StmtClassDef, mut fields: Vec, ) -> Vec { - if fields.is_empty() || !layout_tree_finalizes(db, model, suite, layouts, class) { + if fields.is_empty() || !layout_tree_finalizes(db, env, model, suite, layouts, class) { return fields; } for field in &mut fields { @@ -2223,6 +2256,7 @@ fn presence_where_a_finalizer_reads( /// working a layout out is a walk up the base chain through ty fn layout_tree_finalizes( db: &dyn ty_python_semantic::Db, + env: &ProgramEnvironment<'_>, model: &SemanticModel<'_>, suite: &[Stmt], layouts: &Layouts, @@ -2246,10 +2280,10 @@ fn layout_tree_finalizes( if finalizers.is_empty() { return false; } - let root = layout_root(db, model, suite, layouts, class); + let root = layout_root(db, env, model, suite, layouts, class); finalizers .into_iter() - .any(|candidate| layout_root(db, model, suite, layouts, candidate) == root) + .any(|candidate| layout_root(db, env, model, suite, layouts, candidate) == root) } /// the topmost class of this module's own that this one's layout extends @@ -2258,6 +2292,7 @@ fn layout_tree_finalizes( /// they share this fn layout_root( db: &dyn ty_python_semantic::Db, + env: &ProgramEnvironment<'_>, model: &SemanticModel<'_>, suite: &[Stmt], layouts: &Layouts, @@ -2266,7 +2301,7 @@ fn layout_root( let mut current = class; // bounded the way [`laid_out_from_outside`] is for _ in 0..=suite.len() { - let next = base_class(db, model, current, layouts) + let next = base_class(db, env, model, current, layouts) .ok() .flatten() .and_then(|base| base.in_module().map(str::to_owned)) @@ -2288,6 +2323,7 @@ fn layout_root( /// out here, and a subclass on it extends a struct rather than appending to one fn laid_out_from_outside( db: &dyn ty_python_semantic::Db, + env: &ProgramEnvironment<'_>, model: &SemanticModel<'_>, suite: &[Stmt], layouts: &Layouts, @@ -2300,7 +2336,7 @@ fn laid_out_from_outside( // bounded by the class count: a base chain cannot visit one twice without being a // cycle, and a cycle would otherwise spin here rather than settle for _ in 0..=suite.len() { - match base_class(db, model, current, layouts).ok().flatten() { + match base_class(db, env, model, current, layouts).ok().flatten() { None => return false, Some(ClassBase::External(_)) => return true, Some(ClassBase::InModule(base)) => match class_written(suite, &base) { @@ -2679,6 +2715,7 @@ fn lower_function_with_receiver( arrays: &[(usize, RType)], ) -> Lowered<(Function, Vec)> { let Unit { + env, db, model, native_callees, @@ -2725,12 +2762,12 @@ fn lower_function_with_receiver( ret, deferring, computed_defaults, - } = signature(db, model, function, layouts, receiver, arrays)?; + } = signature(db, env, model, function, layouts, receiver, arrays)?; // a nested function lives on a generated environment class, whose fields are // the captures. it has to exist before the body is lowered, because the `def` // statement allocates it - let locals_here = local_representations(db, model, &function.body, layouts, unit.arrays); + let locals_here = local_representations(db, env, model, &function.body, layouts, unit.arrays); let bound: HashSet = params .iter() .map(|(name, _)| name.clone()) @@ -2847,6 +2884,7 @@ fn lower_function_with_receiver( .filter_map(|entry| { let mut signature = signature( db, + env, model, &entry.def, &layouts_with_env, @@ -3193,6 +3231,8 @@ type Methods = HashMap>; /// environment's methods need to see the environment's own layout #[derive(Clone, Copy)] struct Unit<'a> { + /// the environment the module is being checked in, which every type query needs + env: &'a ProgramEnvironment<'a>, /// whether a closure made in a loop binds *that* iteration's values, which is /// the language's default and the transpiler's. it decides one thing here: /// whether a captured loop target is a shared cell or a per-closure copy @@ -3487,6 +3527,7 @@ fn edition_name(function: &str, signature: &[(usize, RType)]) -> String { /// answer depends only on this function, so it is settled before any body is lowered fn array_editions( db: &dyn ty_python_semantic::Db, + env: &ProgramEnvironment<'_>, model: &SemanticModel<'_>, function: &ast::StmtFunctionDef, layouts: &Layouts, @@ -3511,7 +3552,7 @@ fn array_editions( let rtype = parameter .parameter .inferred_type(model) - .and_then(|ty| mapper::map_local_type(db, ty, layouts).ok()) + .and_then(|ty| mapper::map_local_type(db, env, ty, layouts).ok()) .filter(|rtype| matches!(rtype, RType::Array(_)))?; Some((index, rtype)) }) @@ -3547,6 +3588,7 @@ fn array_editions( /// argument's representation, which is what this is computing fn supplied_arrays( db: &dyn ty_python_semantic::Db, + env: &ProgramEnvironment<'_>, model: &SemanticModel<'_>, suite: &[Stmt], layouts: &Layouts, @@ -3571,7 +3613,7 @@ fn supplied_arrays( .filter_map(|(index, argument)| { let rtype = argument .inferred_type(model) - .and_then(|ty| mapper::map_local_type(db, ty, layouts).ok()) + .and_then(|ty| mapper::map_local_type(db, env, ty, layouts).ok()) .filter(|rtype| matches!(rtype, RType::Array(_)))?; Some((index, rtype)) }) @@ -3590,6 +3632,7 @@ fn supplied_arrays( fn signature( db: &dyn ty_python_semantic::Db, + env: &ProgramEnvironment<'_>, model: &SemanticModel<'_>, function: &ast::StmtFunctionDef, layouts: &Layouts, @@ -3668,11 +3711,11 @@ fn signature( // interpreted twin, and a method's twin belongs to a *different* // class than the compiled receiver, while a nested function has no // python-visible name to have been defined under - if receiver.is_none() && mapper::is_promoted_float(db, ty) { + if receiver.is_none() && mapper::is_promoted_float(db, env, ty) { deferring.push(params.len()); RType::FLOAT } else { - map_type_with(db, ty, layouts)? + map_type_with(db, env, ty, layouts)? } } }; @@ -3706,7 +3749,7 @@ fn signature( posonly: parameters.posonlyargs.len() + usize::from(matches!(receiver, Some(Receiver::Implicit(_)))), kwonly: parameters.kwonlyargs.len(), - ret: return_type(db, model, function, layouts)?, + ret: return_type(db, env, model, function, layouts)?, deferring, computed_defaults, }) @@ -3856,6 +3899,7 @@ fn boxed_object(builder: &mut by_ir::builder::FunctionBuilder, id: RegisterId) - fn return_type( db: &dyn ty_python_semantic::Db, + env: &ProgramEnvironment<'_>, model: &SemanticModel<'_>, function: &ast::StmtFunctionDef, layouts: &Layouts, @@ -3870,7 +3914,7 @@ fn return_type( let ty = value .inferred_type(model) .ok_or_else(|| Decline::new("a returned expression has no inferred type"))?; - map_type_with(db, ty, layouts)? + map_type_with(db, env, ty, layouts)? } }; found = Some(match found { @@ -3908,7 +3952,7 @@ fn return_type( let ty = annotation .inferred_type(model) .ok_or_else(|| Decline::new("a return annotation has no inferred type"))?; - map_type_with(db, ty, layouts) + map_type_with(db, env, ty, layouts) } None => Ok(RType::NONE), } @@ -3920,6 +3964,7 @@ fn return_type( /// write to it has to fit fn local_representations( db: &dyn ty_python_semantic::Db, + env: &ProgramEnvironment<'_>, model: &SemanticModel<'_>, body: &[Stmt], layouts: &Layouts, @@ -3952,7 +3997,7 @@ fn local_representations( // `buffer_safe` first. a buffer handed out ungated is a list that escapes let peek = |expr: &Expr| -> RType { expr.inferred_type(model) - .and_then(|ty| map_type_with(db, ty, layouts).ok()) + .and_then(|ty| map_type_with(db, env, ty, layouts).ok()) .unwrap_or(RType::OBJECT) }; @@ -4006,7 +4051,7 @@ fn local_representations( if display.elts.is_empty() { return expr .inferred_type(model) - .and_then(|ty| mapper::map_local_type(db, ty, layouts).ok()) + .and_then(|ty| mapper::map_local_type(db, env, ty, layouts).ok()) .filter(|rtype| matches!(rtype, RType::Array(_))); } let mut element: Option = None; @@ -6808,6 +6853,7 @@ impl Lowering<'_, '_> { /// definition. calling the native entry directly would skip the test and unbox /// the argument, which raises where python does not fn defers_call(&self, name: &str, node: &ast::ExprCall) -> bool { + let env = &self.model.program_environment(); let Some(signature) = self.signatures.get(name) else { return false; }; @@ -6842,7 +6888,7 @@ impl Lowering<'_, '_> { supplied.is_none_or(|argument| { argument .inferred_type(self.model) - .and_then(|ty| map_type(self.db, ty).ok()) + .and_then(|ty| map_type(self.db, env, ty).ok()) != Some(RType::FLOAT) }) }) @@ -7415,10 +7461,11 @@ impl Lowering<'_, '_> { /// the representation an expression will produce, without lowering it fn peek_type(&self, expr: &Expr) -> Lowered { + let env = &self.model.program_environment(); let ty = expr .inferred_type(self.model) .ok_or_else(|| Decline::new("an expression has no inferred type"))?; - map_type_with(self.db, ty, self.layouts) + map_type_with(self.db, env, ty, self.layouts) } /// the representation that holds every one of `exprs` @@ -7854,6 +7901,7 @@ impl Lowering<'_, '_> { /// register holds a double — so the representation knows more than the /// annotation, and this is the one place that matters fn float_by_proof(&self, left: &Expr, lhs_ty: &RType, right: &Expr, rhs_ty: &RType) -> bool { + let env = &self.model.program_environment(); if *lhs_ty != RType::FLOAT && *rhs_ty != RType::FLOAT { return false; } @@ -7864,7 +7912,7 @@ impl Lowering<'_, '_> { *rtype == RType::OBJECT && expr .inferred_type(self.model) - .is_some_and(|ty| mapper::is_promoted_float(self.db, ty)) + .is_some_and(|ty| mapper::is_promoted_float(self.db, env, ty)) }; numeric(left, lhs_ty) && numeric(right, rhs_ty) } @@ -9043,6 +9091,7 @@ impl Lowering<'_, '_> { } fn call(&mut self, node: &ast::ExprCall) -> Lowered<(Value, RType)> { + let env = &self.model.program_environment(); // `super()` with no arguments is not an ordinary call: python's own compiler // fills the two arguments in from the frame. a compiled method has no frame, // but the compiler knows both — so it fills them in here instead. a shadowed @@ -9227,7 +9276,7 @@ impl Lowering<'_, '_> { // part company for a function that never returns at all let result_ty = match self.signatures.get(name) { Some(signature) => signature.ret.clone(), - None => map_type(self.db, ty)?, + None => map_type(self.db, env, ty)?, }; // the callee's parameter representations, so each argument is coerced rather diff --git a/crates/by_irbuild/src/mapper.rs b/crates/by_irbuild/src/mapper.rs index fc7e24787a..3048b10291 100644 --- a/crates/by_irbuild/src/mapper.rs +++ b/crates/by_irbuild/src/mapper.rs @@ -11,6 +11,7 @@ use by_ir::function::FieldDecl; use by_ir::rtype::RType; +use ty_python_semantic::ProgramEnvironment; use ty_python_semantic::types::{KnownClass, Type}; /// why a construct could not be lowered natively @@ -43,6 +44,7 @@ pub type Layouts = std::collections::HashMap>; /// turns an attribute read into a field read at a compile-time offset pub fn map_type_with( db: &dyn ty_python_semantic::Db, + env: &ProgramEnvironment<'_>, ty: Type<'_>, layouts: &Layouts, ) -> Lowered { @@ -52,7 +54,7 @@ pub fn map_type_with( // getting its own class's representation let bare = ty.erase_restriction(db); if !bare.is_dynamic() - && let Some(class) = bare.nominal_class_name(db) + && let Some(class) = bare.nominal_class_name(db, env) && layouts.contains_key(class) { return Ok(RType::Instance { @@ -60,10 +62,10 @@ pub fn map_type_with( // a `@final` or `sealed` class admits no subclass, so a value of it is // exactly it — which is what re-licenses the direct method call on a // class that is otherwise open - exact: ty.nominal_class_is_exact(db), + exact: ty.nominal_class_is_exact(db, env), }); } - map_type(db, ty) + map_type(db, env, ty) } /// the representation a *local* gets, which may be an unboxed array where an @@ -75,17 +77,18 @@ pub fn map_type_with( /// would lose the list's *identity*, not just time pub fn map_local_type( db: &dyn ty_python_semantic::Db, + env: &ProgramEnvironment<'_>, ty: Type<'_>, layouts: &Layouts, ) -> Lowered { - if let Some(element) = ty.list_element_type(db) - && let Ok(element) = map_type_with(db, element, layouts) + if let Some(element) = ty.list_element_type(db, env) + && let Ok(element) = map_type_with(db, env, element, layouts) && element.is_unboxed() && !element.is_refcounted() { return Ok(RType::Array(Box::new(element))); } - map_type_with(db, ty, layouts) + map_type_with(db, env, ty, layouts) } /// the representation `ty` would have had, had python's numeric promotion not @@ -96,23 +99,25 @@ pub fn map_local_type( /// what recovers it pub fn missed_representation( db: &dyn ty_python_semantic::Db, + env: &ProgramEnvironment<'_>, ty: Type<'_>, layouts: &Layouts, ) -> Option { - if is_promoted_float(db, ty) { + if is_promoted_float(db, env, ty) { return Some(RType::FLOAT); } - let element = ty.list_element_type(db)?; + let element = ty.list_element_type(db, env)?; // the element's *strict* representation, which is what a buffer needs - let strict = if is_promoted_float(db, element) { + let strict = if is_promoted_float(db, env, element) { RType::FLOAT } else { - map_type_with(db, element, layouts).ok()? + map_type_with(db, env, element, layouts).ok()? }; // a list whose element is already unboxed is a buffer, so nothing was missed let missed = strict.is_unboxed() && !strict.is_refcounted() - && map_local_type(db, ty, layouts).is_ok_and(|rtype| !matches!(rtype, RType::Array(_))); + && map_local_type(db, env, ty, layouts) + .is_ok_and(|rtype| !matches!(rtype, RType::Array(_))); missed.then(|| RType::Array(Box::new(strict))) } @@ -123,20 +128,25 @@ pub fn missed_representation( /// `float` is a union, and nothing about it proves a `double` representation — /// only the boundary can, one call at a time. `.by` opts out of the promotion, so /// this is never true there -pub fn is_promoted_float(db: &dyn ty_python_semantic::Db, ty: Type<'_>) -> bool { +pub fn is_promoted_float( + db: &dyn ty_python_semantic::Db, + env: &ProgramEnvironment<'_>, + ty: Type<'_>, +) -> bool { let Some(union) = ty.as_union() else { return false; }; - let int = KnownClass::Int.to_instance(db); - let float = KnownClass::Float.to_instance(db); + let int = KnownClass::Int.to_instance(db, env); + let float = KnownClass::Float.to_instance(db, env); if int.is_dynamic() || float.is_dynamic() { return false; } - let same = |a: Type<'_>, b: Type<'_>| a.is_assignable_to(db, b) && b.is_assignable_to(db, a); + let same = + |a: Type<'_>, b: Type<'_>| a.is_assignable_to(db, env, b) && b.is_assignable_to(db, env, a); // a *gradual* element is assignable both ways to everything, so one of them would // answer for both halves of this and any `Unknown | T` would read as the // promotion. gradual proves nothing, which is the rule the whole mapper rests on - if ty.has_gradual_member(db) { + if ty.has_gradual_member(db, env) { return false; } let elements = union.elements(db); @@ -150,7 +160,11 @@ pub fn is_promoted_float(db: &dyn ty_python_semantic::Db, ty: Type<'_>) -> bool /// the order of the checks matters: `bool` is a subclass of `int` in python, so /// it has to be recognized first or every `bool` would be given the tagged /// integer representation -pub fn map_type(db: &dyn ty_python_semantic::Db, ty: Type<'_>) -> Lowered { +pub fn map_type( + db: &dyn ty_python_semantic::Db, + env: &ProgramEnvironment<'_>, + ty: Type<'_>, +) -> Lowered { // a gradual type is not a proof of anything, so it lands on the widest // representation. `object` assumes nothing, which is exactly why it needs no // check: the representation invariant only bites when *narrowing* @@ -162,12 +176,12 @@ pub fn map_type(db: &dyn ty_python_semantic::Db, ty: Type<'_>) -> Lowered // `def f(x=None)` is the common way to meet the union half: its type is // `Unknown | None`, which read as `None` and made storing anything else into `x` // impossible. narrowing produces the intersection half, `Unknown & None` - if ty.has_gradual_member(db) { + if ty.has_gradual_member(db, env) { return Ok(RType::OBJECT); } - let none = Type::none(db); - if ty.is_assignable_to(db, none) { + let none = Type::none(db, env); + if ty.is_assignable_to(db, env, none) { return Ok(RType::NONE); } for (known, rtype) in [ @@ -176,8 +190,8 @@ pub fn map_type(db: &dyn ty_python_semantic::Db, ty: Type<'_>) -> Lowered (KnownClass::Float, RType::FLOAT), (KnownClass::Str, RType::STR), ] { - let instance = known.to_instance(db); - if !instance.is_dynamic() && ty.is_assignable_to(db, instance) { + let instance = known.to_instance(db, env); + if !instance.is_dynamic() && ty.is_assignable_to(db, env, instance) { return Ok(rtype); } } @@ -205,14 +219,14 @@ mod tests { &format!( "from typing import Any, Literal\ndef f(a: {annotation}) -> None:\n pass\n" ), - |db, model, suite| { + |db, env, model, suite| { let ruff_python_ast::Stmt::FunctionDef(function) = &suite[1] else { return Err("not a function".to_string()); }; let parameter = &function.parameters.args[0].parameter; let ty = ty_python_semantic::HasType::inferred_type(parameter, model) .ok_or_else(|| "no inferred type".to_string())?; - map_type(db, ty).map_err(|decline| decline.reason) + map_type(db, env, ty).map_err(|decline| decline.reason) }, ) } @@ -231,13 +245,13 @@ mod tests { crate::single_file::with_source_in( &format!("def f(a: {annotation}) -> None:\n pass\n"), crate::Language::Python, - |db, model, suite| { + |db, env, model, suite| { let ruff_python_ast::Stmt::FunctionDef(function) = &suite[0] else { return false; }; let parameter = &function.parameters.args[0].parameter; ty_python_semantic::HasType::inferred_type(parameter, model) - .is_some_and(|ty| is_promoted_float(db, ty)) + .is_some_and(|ty| is_promoted_float(db, env, ty)) }, ) } @@ -293,12 +307,12 @@ mod tests { /// over an *unannotated* `a` — the gradual value narrowing acts on fn about_returned( expr: &str, - ask: impl FnOnce(&dyn ty_python_semantic::Db, Type<'_>) -> R, + ask: impl FnOnce(&dyn ty_python_semantic::Db, &ProgramEnvironment<'_>, Type<'_>) -> R, ) -> Result { crate::single_file::with_source_in( &format!("def f(a):\n return {expr}\n"), crate::Language::Python, - |db, model, suite| { + |db, env, model, suite| { let ruff_python_ast::Stmt::FunctionDef(function) = &suite[0] else { return Err("not a function".to_string()); }; @@ -311,7 +325,7 @@ mod tests { .ok_or_else(|| "a bare return".to_string())?; let ty = ty_python_semantic::HasType::inferred_type(value, model) .ok_or_else(|| "no inferred type".to_string())?; - Ok(ask(db, ty)) + Ok(ask(db, env, ty)) }, ) } @@ -329,9 +343,7 @@ mod tests { // the promotion asks the same question of the same shape, so it answers no here // for the same reason: `Unknown & int` is assignable both ways to `int` assert_eq!( - about_returned("a if isinstance(a, int) else 1.0", |db, ty| { - is_promoted_float(db, ty) - }), + about_returned("a if isinstance(a, int) else 1.0", is_promoted_float), Ok(false) ); } diff --git a/crates/by_irbuild/src/single_file.rs b/crates/by_irbuild/src/single_file.rs index 0db564df87..3a9f705e50 100644 --- a/crates/by_irbuild/src/single_file.rs +++ b/crates/by_irbuild/src/single_file.rs @@ -11,14 +11,19 @@ use ruff_db::files::{File, system_path_to_file}; use ruff_db::system::{DbWithWritableSystem, SystemPathBuf}; use ruff_python_ast::Stmt; use ty_project::{ProjectMetadata, TestDb}; -use ty_python_semantic::SemanticModel; +use ty_python_semantic::{ProgramEnvironment, SemanticModel}; use crate::Language; /// build a one-file db from basedpython source and hand its model and suite to `f` pub fn with_source( source: &str, - f: impl FnOnce(&dyn ty_python_semantic::Db, &SemanticModel<'_>, &[Stmt]) -> T, + f: impl FnOnce( + &dyn ty_python_semantic::Db, + &ProgramEnvironment<'_>, + &SemanticModel<'_>, + &[Stmt], + ) -> T, ) -> T { with_source_in(source, Language::BasedPython, f) } @@ -28,12 +33,19 @@ pub fn with_source( pub fn with_source_in( source: &str, language: Language, - f: impl FnOnce(&dyn ty_python_semantic::Db, &SemanticModel<'_>, &[Stmt]) -> T, + f: impl FnOnce( + &dyn ty_python_semantic::Db, + &ProgramEnvironment<'_>, + &SemanticModel<'_>, + &[Stmt], + ) -> T, ) -> T { let (db, file) = make_db(source, language); - let parsed = ruff_db::parsed::parsed_module(&db, file).load(&db); - let model = SemanticModel::new(&db, file); - f(&db, &model, parsed.suite()) + let program_file = ty_python_semantic::Db::program_file(&db, file); + let parsed = ruff_db::parsed::parsed_module(&db, program_file.python_file(&db)).load(&db); + let model = SemanticModel::new(&db, program_file); + let env = ProgramEnvironment::from_file(program_file); + f(&db, &env, &model, parsed.suite()) } fn make_db(source: &str, language: Language) -> (TestDb, File) { @@ -55,9 +67,10 @@ pub fn module_from_source( module_name: &str, language: Language, ) -> by_ir::function::ModuleIr { - with_source_in(source, language, |db, model, suite| { + with_source_in(source, language, |db, env, model, suite| { crate::build_module( db, + env, model, suite, module_name, diff --git a/crates/by_irbuild/src/tests.rs b/crates/by_irbuild/src/tests.rs index c56cb0012e..a14b7fee5c 100644 --- a/crates/by_irbuild/src/tests.rs +++ b/crates/by_irbuild/src/tests.rs @@ -24,8 +24,8 @@ fn has_op(function: &by_ir::function::Function, predicate: impl Fn(&Op) -> bool) /// lower `source` and render the module's IR, failing if it does not verify fn ir(source: &str) -> String { - with_source(source, |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + with_source(source, |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); if let Err(errors) = verify_module(&module) { let detail = errors .iter() @@ -43,8 +43,8 @@ fn ir(source: &str) -> String { /// the reason the single function in `source` was declined fn decline(source: &str) -> String { - with_source(source, |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + with_source(source, |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!( module.functions.is_empty(), "expected the function to be declined, but it lowered:\n{}", @@ -319,8 +319,8 @@ def part(s: str, a: int, b: int) -> str: def keyed(d: dict[str, int], k: str) -> int: return d[k] ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let function = |name: &str| { module @@ -367,8 +367,8 @@ def same(a: str, b: str) -> bool: def before(a: str, b: object) -> bool: return a < b ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let function = |name: &str| { module @@ -459,8 +459,8 @@ fn a_starred_display_is_built_in_runs() { def f(xs: list[int]) -> object: return [1, 2, *xs, 3] ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let f = module .all_functions() @@ -491,8 +491,8 @@ fn a_dict_display_with_a_merge_updates_in_place() { def f(d: dict[str, int]) -> object: return {'a': 1, **d} ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let f = module .all_functions() @@ -519,8 +519,8 @@ def add(a: int, b: int) -> int: def f(xs: list[int]) -> int: return add(*xs) ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let f = module .all_functions() @@ -548,8 +548,8 @@ fn a_signature_records_which_parameters_are_reachable_how() { def f(a: int, /, b: int, *, c: int) -> int: return a + b + c ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let f = module .all_functions() @@ -576,8 +576,8 @@ fn a_comprehension_gives_each_for_its_own_header() { def f(rows: list[list[int]]) -> object: return [x for row in rows if len(row) > 1 for x in row] ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let f = module .all_functions() @@ -604,8 +604,8 @@ def f(xs: list[int]) -> int: a, b = xs return a + b ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let f = module .all_functions() @@ -635,8 +635,8 @@ def f(xs: list[int]) -> object: head, *tail = xs return tail ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let f = module .all_functions() @@ -668,8 +668,8 @@ def f() -> int: a = b = side() return a + b ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let f = module .all_functions() @@ -696,8 +696,8 @@ def f(xs: list[int]) -> int: a, b = xs return a + b ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); let f = module .all_functions() .find(|function| function.name == "f") @@ -725,8 +725,8 @@ def f(n: int) -> int: except (Custom, ValueError): return 0 ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); let f = module .all_functions() .find(|function| function.name == "f") @@ -755,8 +755,8 @@ fn a_shadowed_error_class_is_not_the_builtin() { def f(ValueError: object) -> None: raise ValueError ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); let f = module .all_functions() .find(|function| function.name == "f") @@ -787,8 +787,8 @@ def f(n: int) -> int: except ValueError: return 0 ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); let f = module .all_functions() .find(|function| function.name == "f") @@ -819,8 +819,8 @@ fn a_bare_raise_outside_a_handler_is_declined() { def f() -> None: raise ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); module .declined .iter() @@ -841,8 +841,8 @@ fn a_function_that_never_returns_takes_its_representation_from_the_annotation() def fail(reason: str) -> int: raise ValueError(reason) ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); let fail = module .all_functions() .find(|function| function.name == "fail") @@ -862,8 +862,8 @@ class Custom(Exception): ... def f() -> None: raise Custom "; - with_source(source, |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + with_source(source, |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!( !module.declined.iter().any(|declined| declined.name == "f"), "{:?}", @@ -892,8 +892,8 @@ data class Point: def total(self) -> int: return self.x + self.y ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let [class] = module.classes.as_slice() else { panic!("one class"); @@ -919,8 +919,8 @@ fn a_class_with_no_constructor_lays_out_nothing() { class Point: x: int ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); module .classes @@ -934,8 +934,8 @@ class Point: /// the class-level constants of one emitted class, in the order the body wrote them fn class_constants(source: &str, class: &str) -> Vec { - with_source(source, |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + with_source(source, |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); module .classes @@ -1006,8 +1006,8 @@ fn an_annotated_attribute_of_a_data_class_is_a_field_rather_than_a_constant() { data class Point: x: int = 1 ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let class = &module.classes[0]; let fields: Vec<&str> = class.fields.iter().map(|f| f.name.as_str()).collect(); @@ -1092,8 +1092,8 @@ class Stream: def take(self) -> str: return self.__read() ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let class = &module.classes[0]; let fields: Vec<&str> = class.fields.iter().map(|f| f.name.as_str()).collect(); @@ -1143,8 +1143,8 @@ class Stream: def each(self) -> object: return [self.__buffer for _ in range(1)] ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); module .classes @@ -1172,8 +1172,8 @@ class Point: def __repr__(self) -> str: return \"p\" ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let names: Vec<&str> = module.classes[0] .methods @@ -1215,8 +1215,8 @@ class Thing: self.a = a self.b = b ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let [class] = module.classes.as_slice() else { panic!("one class"); @@ -1407,8 +1407,8 @@ class Apart: def __init__(self, n: int) -> None: self.n = n ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); module .classes @@ -1476,8 +1476,8 @@ class Both: class Private: __slots__ = ('__hidden',) ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); module .classes @@ -1717,8 +1717,8 @@ class BelowConstant(Constant): ); // the base each subclass gets is the point: an `InModule` one would name a type // this module never emits - let bases = with_source(SOURCE, |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + let bases = with_source(SOURCE, |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); module .classes .iter() @@ -1763,8 +1763,8 @@ def unproven(xs: list[float], n: int) -> float: i = i + 1 return out ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); let edition = |name: &str| { module .all_functions() @@ -1805,8 +1805,8 @@ class Point: if x > 0: self.big = x ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); module .classes @@ -1844,8 +1844,8 @@ class Point: else: self.small = 1 ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); module .classes .iter() @@ -1872,8 +1872,8 @@ class Point: self.x = x self.y = y ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let class = &module.classes[0]; let fields: Vec<&str> = class.fields.iter().map(|f| f.name.as_str()).collect(); @@ -1895,8 +1895,8 @@ data class Shape: data class Circle(Shape): radius: float ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let circle = module .classes @@ -1937,8 +1937,8 @@ class Marker: Marker = Marker() ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); let named = |name: &str| module.classes.iter().any(|class| class.name == name); (named("Marker"), named("Ready")) }, @@ -1961,8 +1961,8 @@ fn a_base_out_of_the_unit_may_add_storage_of_its_own() { data class Timed(Exception): at: int ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); let declined = module .declined .iter() @@ -1990,8 +1990,8 @@ class Timed(Exception): def label(self) -> str: return \"timed\" ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); module .classes @@ -2033,8 +2033,8 @@ def through(s: Shape) -> str: def plain(p: Plain) -> int: return p.doubled() ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let ops = |name: &str| { print_function( @@ -2092,8 +2092,8 @@ def on_final_inherited(f: Fixed) -> int: def on_open(o: Open) -> int: return o.doubled() ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let ops = |name: &str| { print_function( @@ -2147,8 +2147,8 @@ data class Circle(Shape): def through(s: Shape) -> str: return s.describe() ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let through = print_function( module @@ -2189,8 +2189,8 @@ def loud(x: Loud) -> int: def quiet(x: Quiet) -> int: return x.doubled() ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let ops = |name: &str| { let function = module @@ -2233,8 +2233,8 @@ data class Point: def __init__(self, x: int) -> None: pass "; - let reason = with_source(source, |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + let reason = with_source(source, |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); module .declined .iter() @@ -2289,8 +2289,8 @@ def deco(f: object) -> object: def f() -> None: pass "; - with_source(source, |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + with_source(source, |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let decorated = module .functions @@ -2312,8 +2312,8 @@ def make(n: int) -> object: def f() -> None: pass "; - let reason = with_source(source, |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + let reason = with_source(source, |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); module .declined .iter() @@ -2339,8 +2339,8 @@ fn variadic_parameters_hold_a_tuple_and_a_dict() { def both(a: int, *rest: int, **named: object) -> int: return a + len(rest) + len(named) ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let function = &module.functions[0]; assert!(function.vararg); @@ -2565,8 +2565,8 @@ def bad(a: int) -> None: except* ValueError: pass "; - with_source(source, |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + with_source(source, |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert_eq!(module.functions.len(), 1); assert_eq!(module.functions[0].name, "good"); assert_eq!(module.declined.len(), 1); @@ -2603,8 +2603,8 @@ data class Point: def sum_of(p: Point) -> int: return p.x + p.y ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); // the method reaches `self` through the forced receiver, the free // function through its annotation — both are field reads @@ -2631,8 +2631,8 @@ data class Counter: self.n = self.n + by return self.n ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let method = &module.classes[0].methods[0]; let text = print_function(method); @@ -2665,8 +2665,8 @@ data class Point: x: int y: int ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let line = module .classes @@ -2704,8 +2704,8 @@ class Plain: def read(p: Plain) -> object: return p.x ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); let read = &module.functions[0]; assert!( has_op(read, |op| matches!(op, Op::GetAttr { .. })), @@ -2726,8 +2726,8 @@ frozen data class Point: data class Loose: y: int ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); let frozen = |name: &str| { module .classes @@ -2754,8 +2754,8 @@ data class Point: def diag(a: int) -> int: return Point(a, a).x ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let text = print_function(&module.functions[0]); assert!( @@ -2788,8 +2788,8 @@ def helper(a: int) -> int: def caller(a: int) -> int: return helper(a) ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); let reason = |name: &str| { module .declined @@ -2825,8 +2825,8 @@ def middle(a: int) -> int: def top(a: int) -> int: return middle(a) ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.functions.is_empty(), "{:?}", module.functions); assert_eq!(module.declined.len(), 3); }, @@ -2853,8 +2853,8 @@ data class Point: def read(p: Point) -> int: return p.x ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.classes.is_empty(), "{:?}", module.classes); assert!(module.functions.is_empty(), "{:?}", module.functions); let reason = |name: &str| { @@ -2893,8 +2893,8 @@ class Parser(Container): def __init__(self, tag: str) -> None: Container.__init__(self, tag) ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.classes.is_empty(), "{:?}", module.classes); let reason = |name: &str| { module @@ -2932,8 +2932,8 @@ def caller(a: int) -> int: def alone(a: int) -> int: return a + 1 ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); let names: Vec<&str> = module .functions .iter() @@ -2962,8 +2962,8 @@ data class Point: def use(p: Point) -> int: return p.scaled(3) + p.total() ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let scaled = module.classes[0] .methods @@ -2996,8 +2996,8 @@ data class Box: def use(b: Box) -> int: return b.add(2) ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); let text = print_function(&module.functions[0]); // no box on the way in and no unbox on the way out assert!( @@ -3018,8 +3018,8 @@ fn a_method_call_on_a_boxed_receiver_still_uses_the_protocol() { def use(p: object) -> object: return p.total() ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!( has_op(&module.functions[0], |op| matches!( op, @@ -3050,8 +3050,8 @@ data class Point: def raw(self) -> int: return self.x ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let class = module .classes @@ -3085,8 +3085,8 @@ def slow(a: int) -> None: except* ValueError: pass "; - let range = with_source(source, |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + let range = with_source(source, |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); module .declined .iter() @@ -3114,8 +3114,8 @@ def helper(a: int) -> int: def caller(a: int) -> int: return helper(a) "; - let range = with_source(source, |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + let range = with_source(source, |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); module .declined .iter() @@ -3136,8 +3136,8 @@ def f(a: int) -> int: return a return 0 "; - with_source(source, |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + with_source(source, |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); let function = &module.functions[0]; let (start, end) = function.range.expect("the function has a span"); assert!(source[start as usize..end as usize].starts_with("def f")); @@ -3164,8 +3164,8 @@ def f(a: int) -> int: c = b + 1 return c "; - with_source(source, |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + with_source(source, |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); let (start, _) = module.functions[0].blocks[0] .range .expect("the entry block has a span"); @@ -3185,8 +3185,8 @@ fn calling_a_callable_held_in_a_parameter_reads_the_register() { def apply(f: object, a: int) -> object: return f(a) ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let function = &module.functions[0]; assert!( @@ -3207,8 +3207,8 @@ def pick(flag: bool) -> object: fn = len return fn(\"abc\") ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); let function = &module.functions[0]; assert!( has_op(function, |op| matches!(op, Op::CallValue { .. })), @@ -3226,8 +3226,8 @@ fn calling_a_name_this_frame_does_not_bind_still_resolves_as_a_global() { def use(a: int) -> object: return print(a) ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); let function = &module.functions[0]; assert!( has_op(function, |op| matches!(op, Op::CallPython { .. })), @@ -3247,8 +3247,8 @@ fn a_shadowed_builtin_is_not_the_builtin() { def use(len: object, s: str) -> object: return len(s) ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); let function = &module.functions[0]; assert!( has_op(function, |op| matches!(op, Op::CallValue { .. })), @@ -3269,8 +3269,8 @@ LIMIT = 10 def limit() -> object: return LIMIT ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); let function = &module.functions[0]; assert!( has_op(function, |op| matches!(op, Op::LoadGlobal { .. })), @@ -3290,8 +3290,8 @@ def make_adder(n: int) -> object: return a + n return add ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let environment = module .classes @@ -3345,8 +3345,8 @@ def counter() -> (() -> int): n = 1 return get ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let environment = &module.classes[0]; // a cell is always `object`: it starts unset, and NULL has to be @@ -3383,8 +3383,8 @@ def bumper() -> (() -> int): return n return bump ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let bump = &module.classes[0].methods[0]; let text = print_function(bump); @@ -3415,8 +3415,8 @@ def prefix(n: int) -> float: out = out + x return out ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let prefix = module .all_functions() @@ -3453,8 +3453,8 @@ def each(xs: list[int]) -> list[object]: out.append(lambda: i) return out ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let each = module .all_functions() @@ -3504,8 +3504,8 @@ def each(xs: list[int]) -> list[object]: out.append(lambda: i) return out ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", false); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", false); assert!(module.declined.is_empty(), "{:?}", module.declined); let each = module .all_functions() @@ -3541,8 +3541,8 @@ def each(xs: list[int]) -> list[object]: total = 200 return out ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let field = |class: &str, name: &str| { module @@ -3581,8 +3581,8 @@ fn a_comprehension_target_is_a_local_a_closure_can_capture() { def each(xs: list[int]) -> list[object]: return [lambda: i for i in xs] ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let inner = module .all_functions() @@ -3613,8 +3613,8 @@ def loop_closures() -> list[object]: i = i + 1 return out ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let outer = print_function(&module.functions[0]); // one allocation, before the loop — not one per iteration @@ -3633,8 +3633,8 @@ def outer(n: int) -> object: return n return bump ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); module .declined .iter() @@ -3659,8 +3659,8 @@ def outer(a: int) -> object: return inner return middle ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let field = |class: &str, name: &str| { module @@ -3701,8 +3701,8 @@ def outer(a: int) -> object: return inner return middle ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let inner = module .all_functions() @@ -3749,8 +3749,8 @@ def outer() -> object: n = 1 return middle ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let holders: Vec<&str> = module .classes @@ -3776,8 +3776,8 @@ def helper(a: int) -> int: return x * 2 return double(a) ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let environment = module .classes @@ -3801,8 +3801,8 @@ def twice(f: object) -> object: return f(n) * 2 return wrapper ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let method = &module.classes[0].methods[0]; let text = print_function(method); @@ -3833,8 +3833,8 @@ def run(times: int, k: int) -> int: total = step(total) return total ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let outer = &module.functions[0]; let text = print_function(outer); @@ -3861,8 +3861,8 @@ def early(a: int) -> int: return x * 2 return helper(a) ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); module .declined .iter() @@ -3883,8 +3883,8 @@ def counted(n: int) -> object: yield i i = i + 1 ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let state = &module.classes[0]; assert_eq!(state.name, "counted$gen"); @@ -4041,8 +4041,8 @@ async def summed(n: int) -> int: i = i + 1 return total ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let state = module .classes @@ -4081,8 +4081,8 @@ def each(words: list[str]) -> object: for w in words: yield w ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let state = &module.classes[0]; // one reserved field per `for`, because the iterator has no source name @@ -4105,8 +4105,8 @@ def guarded(log: list[str], n: int) -> object: finally: log.append(\"closed\") ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let state = &module.classes[0]; // the exception rides in a field, checked at every resumption point @@ -4143,8 +4143,8 @@ def outer(n: int) -> object: got = yield from inner(n) yield got ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let outer = module .classes @@ -4177,8 +4177,8 @@ async def plain(n: int) -> int: async def chained(n: int) -> int: return await plain(n) ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let chained = module .classes @@ -4212,8 +4212,8 @@ fn an_async_generator_presents_the_async_iteration_surface() { async def both(n: int) -> object: yield n ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); module .classes .iter() @@ -4234,8 +4234,8 @@ def guarded(mgr: object) -> str: return \"body\" return \"after\" ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let function = &module.functions[0]; let text = print_function(function); @@ -4258,8 +4258,8 @@ def early(log: list[str]) -> str: finally: log.append(\"f\") ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let function = &module.functions[0]; let text = print_function(function); @@ -4290,8 +4290,8 @@ def looped(log: list[str], n: int) -> str: log.append(\"outer\") return \"done\" ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let text = print_function(&module.functions[0]); // the `break` unwinds to the loop's depth: the inner `finally` runs, the @@ -4311,8 +4311,8 @@ fn a_lambda_is_a_nested_function_with_a_generated_name() { def adder(n: int) -> ((int) -> int): return lambda x: x + n ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let environment = &module.classes[0]; assert_eq!(environment.name, "adder$env"); @@ -4337,8 +4337,8 @@ def counter() -> (() -> int): n = 1 return f ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let method = &module.classes[0].methods[0]; assert!( @@ -4363,8 +4363,8 @@ data class Scaler: def make(self) -> ((int) -> int): return lambda x: x * self.k ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let names: Vec<&str> = module .classes @@ -4405,8 +4405,8 @@ def counted(n: int) -> object: yield i i = i + 1 ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let state = &module.classes[0]; let ty = |name: &str| { @@ -4448,8 +4448,8 @@ fn a_generator_local_that_may_be_unset_stays_a_cell() { "e", ), ] { - with_source(source, |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + with_source(source, |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); let state = &module.classes[0]; let field = state .fields @@ -4477,8 +4477,8 @@ def both(a: int, *rest: int, **named: object) -> int: def caller(a: int) -> int: return both(a, 1, 2, k=3) ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); let caller = module .functions @@ -4503,8 +4503,8 @@ def picked(flag: bool, n: int) -> int: value = n return value ", - |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); module .functions @@ -4525,8 +4525,8 @@ def picked(flag: bool, n: int) -> int: /// the reason each declined entry in `source` gives, by name fn declines(source: &str) -> Vec<(String, String)> { - with_source(source, |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + with_source(source, |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); module .declined .iter() @@ -4541,8 +4541,8 @@ fn declines(source: &str) -> Vec<(String, String)> { /// is reached the intended way, which is what catches a lowering that regresses to a /// slower or differently-scoped shape while still computing the same thing fn method_ir(source: &str, class: &str, method: &str) -> String { - with_source(source, |db, model, suite| { - let module = crate::build_module(db, model, suite, "app", true); + with_source(source, |db, env, model, suite| { + let module = crate::build_module(db, env, model, suite, "app", true); assert!(module.declined.is_empty(), "{:?}", module.declined); module .classes diff --git a/crates/by_override_patch/src/main.rs b/crates/by_override_patch/src/main.rs index d5db3cfc36..b17174abd8 100644 --- a/crates/by_override_patch/src/main.rs +++ b/crates/by_override_patch/src/main.rs @@ -32,7 +32,7 @@ use ruff_db::source::source_text; use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf}; use ruff_ranged_value::RangedValue; use ty_project::metadata::options::{Options, Rules}; -use ty_project::{Db as _, ProjectDatabase, ProjectMetadata}; +use ty_project::{Db as _, ProjectDatabase, ProjectMetadata, SemanticDb as _}; use ty_python_semantic::lint::Level; use ty_python_semantic::types::check_types; @@ -145,7 +145,7 @@ fn mark_pass(stdlib_dir: &Path, typeshed_root: &Path) -> Result<(usize, usize)> let mut methods_marked = 0_usize; for (path, file) in files { // offsets of the overriding method names ty flagged as missing `override` - let mut name_offsets: Vec = check_types(&db, file) + let mut name_offsets: Vec = check_types(&db, db.program_file(file)) .into_iter() .filter(|diag| diag.id().is_lint_named(LINT)) .filter_map(|diag| Some(diag.primary_span_ref()?.range()?.start().to_usize())) diff --git a/crates/by_transforms/src/lib.rs b/crates/by_transforms/src/lib.rs index 70e8363aec..6e04540321 100644 --- a/crates/by_transforms/src/lib.rs +++ b/crates/by_transforms/src/lib.rs @@ -89,11 +89,11 @@ fn run_context_sensitive_phase<'a>( db: &dyn ty_python_semantic::Db, file: File, ) -> std::borrow::Cow<'a, str> { - let parsed = ruff_db::parsed::parsed_module(db, file).load(db); + let parsed = ruff_db::parsed::parsed_module(db, db.program_file(file).python_file(db)).load(db); if !parsed.errors().is_empty() { return std::borrow::Cow::Borrowed(source); } - let model = ty_python_semantic::SemanticModel::new(db, file); + let model = ty_python_semantic::SemanticModel::new(db, db.program_file(file)); transforms::context_sensitive::qualify(source, parsed.suite(), &model) } @@ -111,11 +111,11 @@ fn run_erased_union_phase<'a>( file: File, config: &Config, ) -> std::borrow::Cow<'a, str> { - let parsed = ruff_db::parsed::parsed_module(db, file).load(db); + let parsed = ruff_db::parsed::parsed_module(db, db.program_file(file).python_file(db)).load(db); if !parsed.errors().is_empty() { return std::borrow::Cow::Borrowed(source); } - let model = ty_python_semantic::SemanticModel::new(db, file); + let model = ty_python_semantic::SemanticModel::new(db, db.program_file(file)); transforms::erased_union::reify(source, parsed.suite(), &model, config.min_version) } @@ -179,8 +179,15 @@ pub fn transpile(source: &str, config: &Config) -> Result { let (db, file) = make_in_memory_db(source); let source_ref = ruff_db::source::source_text(&db, file); let src = source_ref.as_str(); - let module = ruff_db::parsed::parsed_module(&db, file).load(&db); - let model = ty_python_semantic::SemanticModel::new(&db, file); + let module = ruff_db::parsed::parsed_module( + &db, + ty_python_semantic::Db::program_file(&db, file).python_file(&db), + ) + .load(&db); + let model = ty_python_semantic::SemanticModel::new( + &db, + ty_python_semantic::Db::program_file(&db, file), + ); if let Some(err) = module.errors().iter().find(|e| e.is_basedpython_only()) { return Err(err.to_string()); } @@ -301,7 +308,8 @@ pub fn transpile_typed_with_map( }; // which imports must stay eager, computed against the *project* db: a // single-file db cannot resolve the modules that declare the conformances - let eager_imports = ty_python_semantic::SemanticModel::new(db, file).eagerly_imported_modules(); + let eager_imports = ty_python_semantic::SemanticModel::new(db, db.program_file(file)) + .eagerly_imported_modules(); let (spliced, ast_errors, phase0_map) = transforms::ast_driver::run_against_source(working_source, config, project); if let Some(first) = ast_errors.first() { @@ -312,8 +320,15 @@ pub fn transpile_typed_with_map( let (local_db, local_file) = make_in_memory_db(&modified); let local_source_ref = ruff_db::source::source_text(&local_db, local_file); let src = local_source_ref.as_str(); - let module = ruff_db::parsed::parsed_module(&local_db, local_file).load(&local_db); - let model = ty_python_semantic::SemanticModel::new(&local_db, local_file); + let module = ruff_db::parsed::parsed_module( + &local_db, + ty_python_semantic::Db::program_file(&local_db, local_file).python_file(&local_db), + ) + .load(&local_db); + let model = ty_python_semantic::SemanticModel::new( + &local_db, + ty_python_semantic::Db::program_file(&local_db, local_file), + ); let LoweringResult { output, errors } = run_lowering_phase(src, module.suite(), config, &model); (output, errors) @@ -323,14 +338,22 @@ pub fn transpile_typed_with_map( let (local_db, local_file) = make_in_memory_db(working_source); let local_source_ref = ruff_db::source::source_text(&local_db, local_file); let src = local_source_ref.as_str(); - let module = ruff_db::parsed::parsed_module(&local_db, local_file).load(&local_db); - let model = ty_python_semantic::SemanticModel::new(&local_db, local_file); + let module = ruff_db::parsed::parsed_module( + &local_db, + ty_python_semantic::Db::program_file(&local_db, local_file).python_file(&local_db), + ) + .load(&local_db); + let model = ty_python_semantic::SemanticModel::new( + &local_db, + ty_python_semantic::Db::program_file(&local_db, local_file), + ); let LoweringResult { output, errors } = run_lowering_phase(src, module.suite(), config, &model); (output, errors) } else { - let module = ruff_db::parsed::parsed_module(db, file).load(db); - let model = ty_python_semantic::SemanticModel::new(db, file); + let module = + ruff_db::parsed::parsed_module(db, db.program_file(file).python_file(db)).load(db); + let model = ty_python_semantic::SemanticModel::new(db, db.program_file(file)); let LoweringResult { output, errors } = run_lowering_phase(original_source, module.suite(), config, &model); (output, errors) @@ -399,8 +422,15 @@ fn run_anon_named_tuple_cleanup(mut source: String, config: &Config) -> Result String { let (db, file) = make_in_memory_db(&source); let source_ref = ruff_db::source::source_text(&db, file); let src = source_ref.as_str(); - let module = ruff_db::parsed::parsed_module(&db, file).load(&db); + let module = ruff_db::parsed::parsed_module( + &db, + ty_python_semantic::Db::program_file(&db, file).python_file(&db), + ) + .load(&db); let mut typing_redirect = transforms::typing_redirect::TypingRedirect::new(src, config.clone()); for stmt in module.suite() { @@ -503,7 +537,11 @@ fn run_lazy_import_phase(source: String, config: &Config, eager: &[String]) -> S let (db, file) = make_in_memory_db(&source); let source_ref = ruff_db::source::source_text(&db, file); let src = source_ref.as_str(); - let module = ruff_db::parsed::parsed_module(&db, file).load(&db); + let module = ruff_db::parsed::parsed_module( + &db, + ty_python_semantic::Db::program_file(&db, file).python_file(&db), + ) + .load(&db); let keyword_supported = config.min_version >= ruff_python_ast::PythonVersion::from((3, 15)); let mut lazy = transforms::lazy_import::LazyImport::new(src, keyword_supported, eager); @@ -755,8 +793,15 @@ pub fn reverse_transpile(source: &str, config: &Config) -> Result( SemDb::Project(db, f) => (*db, *f), SemDb::Local(db, f) => (db, *f), }; - let parsed_handle = ruff_db::parsed::parsed_module(sem_db, sem_file).load(sem_db); - let semantic_model = ty_python_semantic::SemanticModel::new(sem_db, sem_file); + let parsed_handle = + ruff_db::parsed::parsed_module(sem_db, sem_db.program_file(sem_file).python_file(sem_db)) + .load(sem_db); + let semantic_model = + ty_python_semantic::SemanticModel::new(sem_db, sem_db.program_file(sem_file)); // identity line table for the no-change early returns: stripping variance // is within-line, so every line still maps to itself diff --git a/crates/by_transforms/src/transforms/identity_swap.rs b/crates/by_transforms/src/transforms/identity_swap.rs index e906df8d61..827c036b3b 100644 --- a/crates/by_transforms/src/transforms/identity_swap.rs +++ b/crates/by_transforms/src/transforms/identity_swap.rs @@ -99,7 +99,7 @@ impl State<'_> { fn isinstance_call(lhs: Expr, rhs: Expr, negate: bool) -> Expr { let call = Expr::Call(ExprCall { node_index: AtomicNodeIndex::NONE, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), func: Box::new(Expr::Name(ExprName { node_index: AtomicNodeIndex::NONE, range: TextRange::default(), diff --git a/crates/by_transforms/src/transforms/sentinel.rs b/crates/by_transforms/src/transforms/sentinel.rs index 3ea778c239..e00ba1348b 100644 --- a/crates/by_transforms/src/transforms/sentinel.rs +++ b/crates/by_transforms/src/transforms/sentinel.rs @@ -58,7 +58,7 @@ impl Transformer for Sentinel { }); let call = Expr::Call(ExprCall { node_index: AtomicNodeIndex::NONE, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), func: Box::new(Expr::Name(ExprName { node_index: AtomicNodeIndex::NONE, range: TextRange::default(), diff --git a/crates/by_transforms/src/type_info.rs b/crates/by_transforms/src/type_info.rs index d526987c86..11d9a88d79 100644 --- a/crates/by_transforms/src/type_info.rs +++ b/crates/by_transforms/src/type_info.rs @@ -8,7 +8,9 @@ use ty_python_core::{global_scope, place_table, semantic_index}; use ty_python_semantic::types::{ DisplaySettings, DynamicType, KnownClass, KnownInstanceType, Type, UnpackedKwargs, character, }; -use ty_python_semantic::{Db, HasType, ImplicitReceiverReference, SemanticModel}; +use ty_python_semantic::{ + Db, HasType, ImplicitReceiverReference, ProgramEnvironment, SemanticModel, +}; /// How the postfix `^` / `!` operators test the "absent" arm of an operand's /// wrapped type. `T?` lowers to `T | None`, so its absent arm is `None`; a @@ -542,6 +544,7 @@ impl TypeInfo for SemanticModel<'_> { ) -> Option { ty_python_semantic::types::exceptions::declared_raises_runtime_target( self.db(), + &self.program_environment(), self.file(), function.inferred_type(self)?, ) @@ -583,7 +586,11 @@ impl TypeInfo for SemanticModel<'_> { fn is_keeps_identity(&self, expr: &Expr) -> bool { expr.inferred_type(self).is_some_and(|ty| { - ty_python_semantic::types::basedpython_is_keeps_identity(self.db(), ty) + ty_python_semantic::types::basedpython_is_keeps_identity( + self.db(), + &self.program_environment(), + ty, + ) }) } @@ -677,7 +684,7 @@ impl TypeInfo for SemanticModel<'_> { fn is_unbound_at(&self, name: &str, anchor: &Expr) -> bool { let db = self.db(); - let file = self.file(); + let file = self.program_file(); let index = semantic_index(db, file); let Some(scope_id) = index.try_expression_scope_id(anchor) else { return true; @@ -696,7 +703,7 @@ impl TypeInfo for SemanticModel<'_> { } fn is_bound_globally(&self, name: &str) -> bool { - let global = global_scope(self.db(), self.file()); + let global = global_scope(self.db(), self.program_file()); let table = place_table(self.db(), global); table .symbol_by_name(name) @@ -705,7 +712,7 @@ impl TypeInfo for SemanticModel<'_> { fn trailing_block_capture(&self, name: &str, anchor: &Expr) -> Option { let db = self.db(); - let file = self.file(); + let file = self.program_file(); let index = semantic_index(db, file); let block_scope = index.try_expression_scope_id(anchor)?; // walk outward from the block's own scope (skipped) to the nearest scope @@ -731,7 +738,7 @@ impl TypeInfo for SemanticModel<'_> { fn trailing_block_fresh_capture(&self, anchor: &Expr) -> Option { let db = self.db(); - let file = self.file(); + let file = self.program_file(); let index = semantic_index(db, file); let block_scope = index.try_expression_scope_id(anchor)?; // the nearest function / module ancestor — where a fresh binding becomes @@ -748,7 +755,7 @@ impl TypeInfo for SemanticModel<'_> { fn reads_binding_of(&self, reference: &ExprName, anchor: &Expr) -> Option { let db = self.db(); - let file = self.file(); + let file = self.program_file(); let index = semantic_index(db, file); let reference_scope = index.try_expression_scope_id(&ExprRef::from(reference))?; let target_scope = index.try_expression_scope_id(&ExprRef::from(anchor))?; @@ -785,7 +792,7 @@ impl TypeInfo for SemanticModel<'_> { fn shares_a_cell_scope(&self, reference: &ExprName, anchor: &Expr) -> bool { let db = self.db(); - let index = semantic_index(db, self.file()); + let index = semantic_index(db, self.program_file()); let Some(reference_scope) = index.try_expression_scope_id(&ExprRef::from(reference)) else { return false; }; @@ -805,8 +812,8 @@ impl TypeInfo for SemanticModel<'_> { fn promoted_type_display(&self, expr: &Expr) -> Option { let ty = expr.inferred_type(self)?; - let promoted = ty.promote(self.db()); - let rendered = display_for_python(self.db(), promoted); + let promoted = ty.promote(self.db(), &self.program_environment()); + let rendered = display_for_python(self.db(), &self.program_environment(), promoted); // ty's default display tags type variables with their binding scope // for disambiguation (e.g. `T@render`); that suffix is not valid in // emitted Python source. strip it before returning so the rendered @@ -827,6 +834,7 @@ impl TypeInfo for SemanticModel<'_> { // out as `Literal[..]` rather than bare — the transpiler emits python Some(strip_binding_context_suffix(&display_for_python( self.db(), + &self.program_environment(), ty, ))) } @@ -841,7 +849,7 @@ impl TypeInfo for SemanticModel<'_> { let name = tv.name(self.db()).to_string(); let default = tv .default_type(self.db()) - .map(|d| display_for_python(self.db(), d)); + .map(|d| display_for_python(self.db(), &self.program_environment(), d)); (name, default) }) .collect(), @@ -850,13 +858,14 @@ impl TypeInfo for SemanticModel<'_> { fn unpacked_kwargs(&self, expr: &Expr) -> Option { let db = self.db(); + let env = &self.program_environment(); let ty = expr.inferred_type(self)?; // an unresolved name (but not an explicit `Any`) tells us nothing, so keep the // `ParamSpec` reading rather than committing to a shape from a type we don't have if ty.is_dynamic() && !matches!(ty, Type::Dynamic(DynamicType::Any)) { return Some(UnpackedKwargsLowering::ParameterPack); } - Some(match ty.unpacked_kwargs(db)? { + Some(match ty.unpacked_kwargs(db, env)? { UnpackedKwargs::ParameterPack => UnpackedKwargsLowering::ParameterPack, UnpackedKwargs::TypedDict => UnpackedKwargsLowering::TypedDict, UnpackedKwargs::Protocol(members) => UnpackedKwargsLowering::Protocol( @@ -865,7 +874,7 @@ impl TypeInfo for SemanticModel<'_> { .map(|(name, ty)| { ( name.to_string(), - strip_binding_context_suffix(&display_for_python(db, ty)), + strip_binding_context_suffix(&display_for_python(db, env, ty)), ) }) .collect(), @@ -909,7 +918,7 @@ impl TypeInfo for SemanticModel<'_> { ) { return Some(AbsentTest::WrappedOptional); } - let base_exception = KnownClass::BaseException.to_instance(db); + let base_exception = KnownClass::BaseException.to_instance(db, &self.program_environment()); let elements: Vec = match ty { Type::Union(union) => union.elements(db).to_vec(), other => vec![other], @@ -917,10 +926,9 @@ impl TypeInfo for SemanticModel<'_> { // an exception arm wins over a `None` arm: a `T | E` (or a decomposed // `(T ? E)?` carrying both) propagates the error. `Any`/`Unknown` arms // are assignable to anything, so exclude them from the exception probe - if elements - .iter() - .any(|t| !t.is_dynamic() && t.is_assignable_to(db, base_exception)) - { + if elements.iter().any(|t| { + !t.is_dynamic() && t.is_assignable_to(db, &self.program_environment(), base_exception) + }) { Some(AbsentTest::Result) } else if elements.iter().any(|t| t.is_none(db)) { Some(AbsentTest::Optional) @@ -938,25 +946,39 @@ impl TypeInfo for SemanticModel<'_> { fn call_result_is_typevar_derived(&self, callee: &Expr) -> bool { callee.inferred_type(self).is_some_and(|ty| { - ty_python_semantic::types::soundness::call_result_is_typevar_derived(self.db(), ty) + ty_python_semantic::types::soundness::call_result_is_typevar_derived( + self.db(), + &self.program_environment(), + ty, + ) }) } fn is_specialized_generic_instance(&self, expr: &Expr) -> bool { expr.inferred_type(self).is_some_and(|ty| { - ty_python_semantic::types::soundness::is_specialized_generic_instance(self.db(), ty) + ty_python_semantic::types::soundness::is_specialized_generic_instance( + self.db(), + &self.program_environment(), + ty, + ) }) } fn soundness_check_plan(&self, expr: &Expr) -> Option { let ty = expr.inferred_type(self)?; - ty_python_semantic::types::soundness::runtime_check_plan(self.db(), self.file(), ty) + ty_python_semantic::types::soundness::runtime_check_plan( + self.db(), + &self.program_environment(), + self.file(), + ty, + ) } fn call_positional_param_plan(&self, callee: &Expr, index: usize) -> Option { let ty = callee.inferred_type(self)?; ty_python_semantic::types::soundness::parameter_check_plan( self.db(), + &self.program_environment(), self.file(), ty, ty_python_semantic::types::soundness::ArgSelector::Positional(index), @@ -967,6 +989,7 @@ impl TypeInfo for SemanticModel<'_> { let ty = callee.inferred_type(self)?; ty_python_semantic::types::soundness::parameter_check_plan( self.db(), + &self.program_environment(), self.file(), ty, ty_python_semantic::types::soundness::ArgSelector::Keyword(name), @@ -975,7 +998,12 @@ impl TypeInfo for SemanticModel<'_> { fn cast_check_plan(&self, type_expr: &Expr) -> Option { let ty = type_expr.inferred_type(self)?; - ty_python_semantic::types::soundness::cast_check_plan(self.db(), self.file(), ty) + ty_python_semantic::types::soundness::cast_check_plan( + self.db(), + &self.program_environment(), + self.file(), + ty, + ) } fn cast_is_redundant(&self, value: &Expr, target: &Expr) -> bool { @@ -984,7 +1012,12 @@ impl TypeInfo for SemanticModel<'_> { else { return false; }; - ty_python_semantic::types::soundness::cast_is_redundant(self.db(), value_ty, target_ty) + ty_python_semantic::types::soundness::cast_is_redundant( + self.db(), + &self.program_environment(), + value_ty, + target_ty, + ) } fn cast_target_is_unverifiable(&self, type_expr: &Expr) -> bool { @@ -993,6 +1026,7 @@ impl TypeInfo for SemanticModel<'_> { }; ty_python_semantic::types::soundness::cast_target_is_unverifiable_protocol( self.db(), + &self.program_environment(), self.file(), ty, ) @@ -1016,6 +1050,7 @@ impl TypeInfo for SemanticModel<'_> { }; ty_python_semantic::types::context_params::implicit_context_arguments( self.db(), + &self.program_environment(), self.file(), callee, call, @@ -1039,19 +1074,24 @@ impl TypeInfo for SemanticModel<'_> { // `Unknown & ~int` would answer yes here and have the rewrite applied to a value // that is not a string at all !ty.is_dynamic() - && !ty.has_gradual_member(db) - && ty.is_assignable_to(db, KnownClass::Str.to_instance(db)) + && !ty.has_gradual_member(db, &self.program_environment()) + && ty.is_assignable_to( + db, + &self.program_environment(), + KnownClass::Str.to_instance(db, &self.program_environment()), + ) } fn annotation_is_character(&self, annotation: &Expr) -> bool { - annotation - .inferred_type(self) - .is_some_and(|ty| character::denotes_character(self.db(), ty)) + annotation.inferred_type(self).is_some_and(|ty| { + character::denotes_character(self.db(), &self.program_environment(), ty) + }) } fn is_character_instance(&self, expr: &Expr) -> bool { - expr.inferred_type(self) - .is_some_and(|ty| character::is_character_instance(self.db(), ty)) + expr.inferred_type(self).is_some_and(|ty| { + character::is_character_instance(self.db(), &self.program_environment(), ty) + }) } fn framework_class_role(&self, class_def: &StmtClassDef) -> Option { @@ -1064,12 +1104,13 @@ impl TypeInfo for SemanticModel<'_> { let ty = expr.inferred_type(self)?; // a dynamic part (`Unknown` from an empty `[]`, an unresolved import, // `Any`) has no faithful annotation — leave the assignment bare - if ty.has_dynamic(self.db()) { + if ty.has_dynamic(self.db(), &self.program_environment()) { return None; } - let promoted = ty.promote(self.db()); + let promoted = ty.promote(self.db(), &self.program_environment()); Some(strip_binding_context_suffix(&display_for_python( self.db(), + &self.program_environment(), promoted, ))) } @@ -1095,9 +1136,14 @@ impl TypeInfo for SemanticModel<'_> { /// diagnostic, where a symbolic arithmetic operation is shown as the expression it stands /// for (`I + 1`); emitting that would evaluate `_I + 1` on a `TypeVar` object at import, so /// the transpiler asks for the type it reduces to instead -fn display_for_python<'db>(db: &'db dyn Db, ty: Type<'db>) -> String { +fn display_for_python<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> String { ty.display_with( db, + env, DisplaySettings::default().with_reduced_symbolic_operations(), ) .to_string() diff --git a/crates/by_typeshed_patch/src/patches/strip_typing_imports.rs b/crates/by_typeshed_patch/src/patches/strip_typing_imports.rs index f17525c2d0..7a353c52e4 100644 --- a/crates/by_typeshed_patch/src/patches/strip_typing_imports.rs +++ b/crates/by_typeshed_patch/src/patches/strip_typing_imports.rs @@ -8,7 +8,9 @@ //! (`TypeVar`, `ParamSpec`), and qualifiers (`ClassVar`) still need their import //! //! aliased imports (`Set as AbstractSet`) are left alone: the bound name may not -//! match the implicit name's meaning +//! match the implicit name's meaning, and so are re-exports (`from typing export +//! Never`) — the name being implicit *here* says nothing about the module that +//! imports it from this one use std::path::Path; @@ -49,6 +51,11 @@ fn walk(body: &[Stmt], source: &str, edits: &mut Vec) { { continue; } + // a re-export is what makes `from typing_extensions import Never` resolve + // at all, so it stays even though the name is implicit here + if import.is_export { + continue; + } // an implicit name imported without an alias is redundant let (drop, keep): (Vec<_>, Vec<_>) = import.names.iter().partition(|alias| { alias.asname.is_none() && is_implicit_typing_name(alias.name.as_str()) diff --git a/crates/mdtest/src/assertion.rs b/crates/mdtest/src/assertion.rs index 2c4dd279d4..a49bafbdcc 100644 --- a/crates/mdtest/src/assertion.rs +++ b/crates/mdtest/src/assertion.rs @@ -535,10 +535,12 @@ pub(crate) enum ErrorAssertionParseError<'a> { mod tests { use super::*; use crate::tests::TestDb; + use ruff_db::PythonFile; use ruff_db::files::system_path_to_file; use ruff_db::parsed::parsed_module; use ruff_db::source::line_index; use ruff_db::system::DbWithWritableSystem as _; + use ruff_python_ast::PythonVersion; use ruff_python_trivia::textwrap::dedent; use ruff_source_file::OneIndexed; @@ -546,7 +548,8 @@ mod tests { let mut db = TestDb::setup(); db.write_file("/src/test.py", source).unwrap(); let file = system_path_to_file(&db, "/src/test.py").unwrap(); - let parsed = parsed_module(&db, file).load(&db); + let parsed = + parsed_module(&db, PythonFile::new(&db, file, PythonVersion::latest_ty())).load(&db); InlineFileAssertions::from_file( source, AssertionSource::Python(&parsed), diff --git a/crates/mdtest/src/lib.rs b/crates/mdtest/src/lib.rs index a1d345dd39..d7d226bd36 100644 --- a/crates/mdtest/src/lib.rs +++ b/crates/mdtest/src/lib.rs @@ -190,7 +190,7 @@ impl OutputFormat { /// Actions can detect them as workflow commands. Workflow commands must /// appear at the beginning of a line in stdout to be parsed by GitHub. #[expect(clippy::print_stdout)] - pub fn write_error( + fn write_error( self, assertion_buf: &mut String, file: &str, @@ -220,7 +220,7 @@ impl OutputFormat { /// Write a module-resolution inconsistency in the appropriate format. /// - /// See [`write_error`](Self::write_error) for details on why GitHub-format + /// See `write_error` for details on why GitHub-format /// messages must be printed directly to stdout. #[expect(clippy::print_stdout)] pub fn write_inconsistency( @@ -314,10 +314,9 @@ impl TestFile<'_> { } } -pub(crate) fn diagnostic_display_config(tool_name: &'static str) -> DisplayDiagnosticConfig { +fn diagnostic_display_config(tool_name: &'static str) -> DisplayDiagnosticConfig { DisplayDiagnosticConfig::new(tool_name) .color(false) - .show_fix_diff(true) .with_fix_applicability(Applicability::DisplayOnly) // Surrounding context in source annotations can be confusing in mdtests, // since you may get to see context from the *subsequent* code block (all @@ -333,11 +332,7 @@ pub fn render_diagnostic(db: &dyn Db, tool_name: &'static str, diagnostic: &Diag .to_string() } -pub(crate) fn render_diagnostics( - db: &dyn Db, - tool_name: &'static str, - diagnostics: &[Diagnostic], -) -> String { +fn render_diagnostics(db: &dyn Db, tool_name: &'static str, diagnostics: &[Diagnostic]) -> String { let mut rendered = String::new(); for diag in diagnostics { writeln!(rendered, "{}", render_diagnostic(db, tool_name, diag)).unwrap(); @@ -346,14 +341,14 @@ pub(crate) fn render_diagnostics( rendered.trim_end_matches('\n').to_string() } -pub(crate) fn is_update_inline_snapshots_enabled() -> bool { +fn is_update_inline_snapshots_enabled() -> bool { let is_enabled: std::sync::LazyLock<_> = std::sync::LazyLock::new(|| { std::env::var_os(MDTEST_UPDATE_SNAPSHOTS).is_some_and(|v| v != "0") }); *is_enabled } -pub(crate) fn apply_snapshot_filters(rendered: &str) -> std::borrow::Cow<'_, str> { +fn apply_snapshot_filters(rendered: &str) -> std::borrow::Cow<'_, str> { static INLINE_SNAPSHOT_PATH_FILTER: std::sync::LazyLock = std::sync::LazyLock::new(|| regex::Regex::new(r#"\\(\w\w|\.|")"#).unwrap()); @@ -413,7 +408,9 @@ pub fn validate_inline_snapshot( failures.push( failure_line, vec![Failure::new( - "This code block has a `snapshot` code block but no `# snapshot` assertions. Remove the `snapshot` code block or add a `# snapshot:` assertion.", + "This code block has a `snapshot` code block but no `# snapshot` \ + assertions. Remove the `snapshot` code block or add a `# snapshot:` \ + assertion.", )], ); } @@ -437,7 +434,8 @@ pub fn validate_inline_snapshot( failures.push( line, vec![Failure::new(format!( - "Add a `snapshot` block for this `# snapshot` assertion, or set `{MDTEST_UPDATE_SNAPSHOTS}=1` to insert one automatically", + "Add a `snapshot` block for this `# snapshot` assertion, \ + or set `{MDTEST_UPDATE_SNAPSHOTS}=1` to insert one automatically", ))], ); } @@ -456,10 +454,14 @@ pub fn validate_inline_snapshot( } else { failures.push( failure_line, - vec![Failure::new(format_args!( - "inline diagnostics snapshot are out of date; set `{MDTEST_UPDATE_SNAPSHOTS}=1` to update the `snapshot` block", - )).with_diff(snapshot_code_block.expected.to_string(), actual)], - ); + vec![ + Failure::new(format_args!( + "inline diagnostics snapshot are out of date; \ + set `{MDTEST_UPDATE_SNAPSHOTS}=1` to update the `snapshot` block", + )) + .with_diff(snapshot_code_block.expected.to_string(), actual), + ], + ); } } @@ -523,7 +525,7 @@ fn try_apply_markdown_edits( } } -pub fn create_diagnostic_snapshot<'d, C>( +fn create_diagnostic_snapshot<'d, C>( db: &dyn Db, tool_name: &'static str, relative_fixture_path: &Utf8Path, @@ -582,8 +584,8 @@ pub fn create_diagnostic_snapshot<'d, C>( #[derive(Debug, Clone)] pub struct MarkdownEdit { - pub(crate) range: TextRange, - pub(crate) replacement: String, + range: TextRange, + replacement: String, } /// Run a function over an embedded test file, catching any panics that occur in the process. @@ -679,7 +681,8 @@ pub fn check_panic(test: &MarkdownTest<'_, '_, C>, panic_info: Option &Files { &self.files } - - fn python_version(&self) -> ruff_python_ast::PythonVersion { - ruff_python_ast::PythonVersion::latest_ty() - } } impl DbWithTestSystem for TestDb { diff --git a/crates/mdtest/src/matcher.rs b/crates/mdtest/src/matcher.rs index 8f5eb6a3d0..879e3c09e3 100644 --- a/crates/mdtest/src/matcher.rs +++ b/crates/mdtest/src/matcher.rs @@ -9,10 +9,12 @@ use std::sync::LazyLock; use colored::Colorize; use path_slash::PathExt; use ruff_db::Db; +use ruff_db::PythonFile; use ruff_db::diagnostic::{Diagnostic, DiagnosticId}; use ruff_db::files::File; use ruff_db::parsed::parsed_module; use ruff_db::source::{SourceText, line_index, source_text}; +use ruff_python_ast::PythonVersion; use ruff_source_file::{LineIndex, OneIndexed}; use smallvec::SmallVec; @@ -29,7 +31,7 @@ pub struct FailuresByLine { } impl FailuresByLine { - pub fn iter(&self) -> impl Iterator { + pub(crate) fn iter(&self) -> impl Iterator { self.lines.iter().map(|line_failures| { ( line_failures.line_number, @@ -93,6 +95,7 @@ struct LineFailures { pub fn match_file( db: &dyn Db, file: File, + python_version: PythonVersion, diagnostics: &[Diagnostic], options: RunOptions, ) -> Result, FailuresByLine> { @@ -108,7 +111,7 @@ pub fn match_file( }); (assertions, diagnostics) } else { - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, PythonFile::new(db, file, python_version)).load(db); let assertions = InlineFileAssertions::from_file( source.as_str(), AssertionSource::Python(&parsed), @@ -462,7 +465,7 @@ fn match_reveal_type_diagnostic( return false; } - let primary_message = diagnostic.primary_message(); + let headline_message = diagnostic.headline_message(); let Some(primary_annotation) = (diagnostic.primary_annotation()).and_then(|a| a.get_message()) else { @@ -473,7 +476,7 @@ fn match_reveal_type_diagnostic( // reveal_type, reveal_protocol_interface if matches!( - primary_message, + headline_message, "Revealed type" | "Revealed protocol interface" ) && expected_reveal_type_message.is_none_or(|expected_reveal_type_message| { primary_annotation == expected_reveal_type_message @@ -483,7 +486,7 @@ fn match_reveal_type_diagnostic( // reveal_when_assignable_to, reveal_when_subtype_of, reveal_mro if matches!( - primary_message, + headline_message, "Assignability holds" | "Subtyping holds" | "Revealed MRO" ) && expected_reveal_type .is_none_or(|expected_reveal_type| primary_annotation == expected_reveal_type) @@ -528,6 +531,7 @@ mod tests { use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticId, Severity, Span}; use ruff_db::files::{File, system_path_to_file}; use ruff_db::system::DbWithWritableSystem as _; + use ruff_python_ast::PythonVersion; use ruff_python_trivia::textwrap::dedent; use ruff_source_file::OneIndexed; use ruff_text_size::TextRange; @@ -588,7 +592,7 @@ mod tests { .into_iter() .map(|diagnostic| diagnostic.into_diagnostic(file)) .collect(); - super::match_file(&db, file, &diagnostics, options) + super::match_file(&db, file, PythonVersion::latest_ty(), &diagnostics, options) } fn assert_fail(result: Result, FailuresByLine>, messages: &[(usize, &[&str])]) { diff --git a/crates/mdtest/src/parser.rs b/crates/mdtest/src/parser.rs index f5b9241596..2d01f7bc3d 100644 --- a/crates/mdtest/src/parser.rs +++ b/crates/mdtest/src/parser.rs @@ -366,7 +366,7 @@ pub(crate) enum EmbeddedFilePath<'s> { } impl EmbeddedFilePath<'_> { - pub(crate) fn as_str(&self) -> &str { + fn as_str(&self) -> &str { match self { EmbeddedFilePath::Autogenerated(kind) => kind.filename(), EmbeddedFilePath::Explicit(path) => path, @@ -448,7 +448,7 @@ impl EmbeddedFile<'_> { } } - pub(crate) fn is_checkable(&self) -> bool { + fn is_checkable(&self) -> bool { matches!( self.lang, "py" | "python" | "pyi" | "ipynb" | "toml" | "by" | "byi" | "bython" | "basedpython" @@ -637,7 +637,8 @@ where SECTION_CONFIG_SNAPSHOT => { anyhow::ensure!( value.is_none(), - "The `{SECTION_CONFIG_SNAPSHOT}` directive does not take a value." + "The `{SECTION_CONFIG_SNAPSHOT}` directive \ + does not take a value." ); self.process_mdtest_directive( MdtestDirective::SnapshotDiagnostics, @@ -647,7 +648,8 @@ where SECTION_CONFIG_PULLTYPES => { anyhow::ensure!( value.is_none(), - "The `{SECTION_CONFIG_PULLTYPES}` directive does not take a value." + "The `{SECTION_CONFIG_PULLTYPES}` directive \ + does not take a value." ); self.process_mdtest_directive( MdtestDirective::PullTypesSkip, @@ -660,8 +662,9 @@ where _ => { if !HTML_COMMENT_ALLOWLIST.contains(&html_comment) { bail!( - "Unknown HTML comment `{html_comment}` -- possibly a typo? \ - (Add to `HTML_COMMENT_ALLOWLIST` if this is a false positive)" + "Unknown HTML comment `{html_comment}` -- \ + possibly a typo? (Add to `HTML_COMMENT_ALLOWLIST` \ + if this is a false positive)" ); } } @@ -700,7 +703,8 @@ where 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." + "Code blocks must start on a new line \ + and be preceded by at least one blank line." ); } @@ -717,7 +721,8 @@ where if !self.cursor.eat_char('\n') { bail!( - "Trailing code-block metadata is not supported. Only the code block language can be specified." + "Trailing code-block metadata is not supported. \ + Only the code block language can be specified." ); } @@ -732,7 +737,8 @@ where .any(|attribute| attribute == r#"data-mdtest="ignore""#) } else { bail!( - "Trailing code-block metadata must use the `{{...}}` attribute-list syntax." + "Trailing code-block metadata must use the `{{...}}` \ + attribute-list syntax." ); }; @@ -880,7 +886,9 @@ where { let backtick_start = self.line_number(backtick_offsets.start()); bail!( - "File extension of test file path `{explicit_path}` in test `{test_name}` does not match language specified `{lang}` of code block on line `{backtick_start}`" + "File extension of test file path `{explicit_path}` \ + in test `{test_name}` does not match language specified `{lang}` \ + of code block on line `{backtick_start}`" ); } } @@ -903,12 +911,14 @@ where "byi" => EmbeddedFilePath::Autogenerated(AutogenKind::Byi), "" => { bail!( - "Cannot auto-generate file name for code block with empty language specifier in test `{test_name}`" + "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 language `{lang}` in test `{test_name}`" + "Cannot auto-generate file name for code block \ + with language `{lang}` in test `{test_name}`" ); } }, @@ -921,7 +931,8 @@ where Entry::Vacant(entry) => { if has_merged_snippets { bail!( - "Merged snippets in test `{test_name}` are not allowed in the presence of other files." + "Merged snippets in test `{test_name}` are not allowed \ + in the presence of other files." ); } @@ -949,7 +960,8 @@ where if has_explicit_file_paths { bail!( - "Merged snippets in test `{test_name}` are not allowed in the presence of other files." + "Merged snippets in test `{test_name}` are not allowed \ + in the presence of other files." ); } @@ -999,7 +1011,8 @@ where let backtick_start = line_number(offsets.start(), self.source); bail!( - "`snapshot` code block on line {backtick_start} must follow a checkable code block, but section has no files." + "`snapshot` code block on line {backtick_start} \ + must follow a checkable code block, but section has no files." ); }; @@ -1009,7 +1022,9 @@ where let backtick_start = line_number(offsets.start(), self.source); bail!( - "`snapshot` code block on line {backtick_start} must follow a checkable code block in the same section but it follows a `{}` block.", + "`snapshot` code block on line {backtick_start} \ + must follow a checkable code block in the same section \ + but it follows a `{}` block.", file.lang ); } @@ -1022,7 +1037,9 @@ where let existing_start = line_number(existing_block.range.start(), self.source); bail!( - "Code block on line `{code_block_start}` has more than one `snapshot` block: first on line {existing_start} and another on line {backtick_start}.", + "Code block on line `{code_block_start}` \ + has more than one `snapshot` block: first on line {existing_start} \ + and another on line {backtick_start}.", ); } @@ -1661,7 +1678,8 @@ mod tests { let err = parse("file.md", &source).expect_err("Should fail to parse"); assert_eq!( err.to_string(), - "Cannot auto-generate file name for code block with empty language specifier in test `No language specifier`" + "Cannot auto-generate file name for code block \ + with empty language specifier in test `No language specifier`" ); } @@ -1679,7 +1697,8 @@ mod tests { let err = parse("file.md", &source).expect_err("Should fail to parse"); assert_eq!( err.to_string(), - "Cannot auto-generate file name for code block with language `json` in test `JSON test?`" + "Cannot auto-generate file name for code block with language `json` \ + in test `JSON test?`" ); } @@ -1754,7 +1773,8 @@ mod tests { let err = parse("file.md", &source).expect_err("Should fail to parse"); assert_eq!( err.to_string(), - "File extension of test file path `a.py` in test `Accidental stub` does not match language specified `pyi` of code block on line `6`" + "File extension of test file path `a.py` in test `Accidental stub` \ + does not match language specified `pyi` of code block on line `6`" ); } diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index 7f6d3eb74d..a721162f38 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff" -version = "0.15.22" +version = "0.16.2" description = "An extremely fast Python linter and code formatter" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff/README.md b/crates/ruff/README.md index f9aa90fe1b..2364cf7d26 100644 --- a/crates/ruff/README.md +++ b/crates/ruff/README.md @@ -10,7 +10,7 @@ See the [documentation](https://docs.astral.sh/ruff/) or This crate is the entry point to the Ruff command-line interface. The Rust API exposed here is not considered public interface. -This is version 0.15.22. The source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff). +This is version 0.16.2. The source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff). The following Ruff workspace members are also available: diff --git a/crates/ruff/src/args.rs b/crates/ruff/src/args.rs index 15159230c5..bbf6b5516c 100644 --- a/crates/ruff/src/args.rs +++ b/crates/ruff/src/args.rs @@ -57,7 +57,7 @@ pub struct GlobalConfigArgs { global = true, help_heading = "Global options", )] - pub config: Vec, + config: Vec, /// Ignore all configuration files. // // Note: We can't mark this as conflicting with `--config` here @@ -68,7 +68,7 @@ pub struct GlobalConfigArgs { // If a user specifies `ruff check --isolated --config=ruff.toml`, // we emit an error later on, after the initial parsing by clap. #[arg(long, help_heading = "Global options", global = true)] - pub isolated: bool, + isolated: bool, /// Control when colored output is used. #[arg( @@ -233,7 +233,7 @@ pub struct AnalyzeGraphCommand { pub struct CheckCommand { /// List of files or directories to check. #[clap(help = "List of files or directories to check, or `-` to read from stdin [default: .]")] - pub files: Vec, + files: Vec, /// Apply fixes to resolve lint violations. /// Use `--no-fix` to disable or `--unsafe-fixes` to include unsafe fixes. #[arg(long, overrides_with("no_fix"))] @@ -255,10 +255,10 @@ pub struct CheckCommand { /// Avoid writing any fixed files back; instead, output a diff for each changed file to stdout, and exit 0 if there are no diffs. /// Implies `--fix-only`. #[arg(long, conflicts_with = "show_fixes")] - pub diff: bool, + diff: bool, /// Run in watch mode by re-running whenever files change. #[arg(short, long)] - pub watch: bool, + watch: bool, /// Apply fixes to resolve lint violations, but don't report on, or exit non-zero for, leftover violations. Implies `--fix`. /// Use `--no-fix-only` to disable or `--unsafe-fixes` to include unsafe fixes. #[arg(long, overrides_with("no_fix_only"))] @@ -272,14 +272,14 @@ pub struct CheckCommand { /// Output serialization format for violations. /// The default serialization format is "full". #[arg(long, value_enum, env = "RUFF_OUTPUT_FORMAT")] - pub output_format: Option, + output_format: Option, /// Specify file to write the linter output to (default: stdout). #[arg(short, long, env = "RUFF_OUTPUT_FILE")] - pub output_file: Option, + output_file: Option, /// The minimum Python version that should be supported. #[arg(long, value_enum)] - pub target_version: Option, + target_version: Option, /// Enable preview mode; checks will include unstable rules and fixes. /// Use `--no-preview` to disable. #[arg(long, overrides_with("no_preview"))] @@ -295,7 +295,7 @@ pub struct CheckCommand { help_heading = "Rule selection", hide_possible_values = true )] - pub select: Option>, + select: Option>, /// Comma-separated list of rule codes to disable. #[arg( long, @@ -305,7 +305,7 @@ pub struct CheckCommand { help_heading = "Rule selection", hide_possible_values = true )] - pub ignore: Option>, + ignore: Option>, /// Like --select, but adds additional rule codes on top of those already specified. #[arg( long, @@ -315,7 +315,7 @@ pub struct CheckCommand { help_heading = "Rule selection", hide_possible_values = true )] - pub extend_select: Option>, + extend_select: Option>, /// Like --ignore. (Deprecated: You can just use --ignore instead.) #[arg( long, @@ -325,13 +325,13 @@ pub struct CheckCommand { help_heading = "Rule selection", hide = true )] - pub extend_ignore: Option>, + extend_ignore: Option>, /// List of mappings from file pattern to code to exclude. #[arg(long, value_delimiter = ',', help_heading = "Rule selection")] - pub per_file_ignores: Option>, + per_file_ignores: Option>, /// Like `--per-file-ignores`, but adds additional ignores on top of those already specified. #[arg(long, value_delimiter = ',', help_heading = "Rule selection")] - pub extend_per_file_ignores: Option>, + extend_per_file_ignores: Option>, /// List of paths, used to omit files and/or directories from analysis. #[arg( long, @@ -339,7 +339,7 @@ pub struct CheckCommand { value_name = "FILE_PATTERN", help_heading = "File selection" )] - pub exclude: Option>, + exclude: Option>, /// Like --exclude, but adds additional files and directories on top of those already excluded. #[arg( long, @@ -347,7 +347,7 @@ pub struct CheckCommand { value_name = "FILE_PATTERN", help_heading = "File selection" )] - pub extend_exclude: Option>, + extend_exclude: Option>, /// List of rule codes to treat as eligible for fix. Only applicable when fix itself is enabled (e.g., via `--fix`). #[arg( long, @@ -357,7 +357,7 @@ pub struct CheckCommand { help_heading = "Rule selection", hide_possible_values = true )] - pub fixable: Option>, + fixable: Option>, /// List of rule codes to treat as ineligible for fix. Only applicable when fix itself is enabled (e.g., via `--fix`). #[arg( long, @@ -367,7 +367,7 @@ pub struct CheckCommand { help_heading = "Rule selection", hide_possible_values = true )] - pub unfixable: Option>, + unfixable: Option>, /// Like --fixable, but adds additional rule codes on top of those already specified. #[arg( long, @@ -377,7 +377,7 @@ pub struct CheckCommand { help_heading = "Rule selection", hide_possible_values = true )] - pub extend_fixable: Option>, + extend_fixable: Option>, /// Like --unfixable. (Deprecated: You can just use --unfixable instead.) #[arg( long, @@ -387,7 +387,7 @@ pub struct CheckCommand { help_heading = "Rule selection", hide = true )] - pub extend_unfixable: Option>, + extend_unfixable: Option>, /// Respect file exclusions via `.gitignore` and other standard ignore files. /// Use `--no-respect-gitignore` to disable. #[arg( @@ -410,23 +410,23 @@ pub struct CheckCommand { no_force_exclude: bool, /// Set the line-length for length-associated rules and automatic formatting. #[arg(long, help_heading = "Rule configuration", hide = true)] - pub line_length: Option, + line_length: Option, /// Regular expression matching the name of dummy variables. #[arg(long, help_heading = "Rule configuration", hide = true)] - pub dummy_variable_rgx: Option, + dummy_variable_rgx: Option, /// Disable cache reads. #[arg(short, long, env = "RUFF_NO_CACHE", help_heading = "Miscellaneous")] - pub no_cache: bool, + no_cache: bool, /// Path to the cache directory. #[arg(long, env = "RUFF_CACHE_DIR", help_heading = "Miscellaneous")] - pub cache_dir: Option, + cache_dir: Option, /// The name of the file when passing it through stdin. #[arg(long, help_heading = "Miscellaneous")] - pub stdin_filename: Option, + stdin_filename: Option, /// List of mappings from file extension to language (one of `python`, `ipynb`, `pyi`). For /// example, to treat `.ipy` files as IPython notebooks, use `--extension ipy:ipynb`. #[arg(long, value_delimiter = ',')] - pub extension: Option>, + extension: Option>, /// Exit with status code "0", even upon detecting lint violations. #[arg( short, @@ -434,10 +434,10 @@ pub struct CheckCommand { help_heading = "Miscellaneous", conflicts_with = "exit_non_zero_on_fix" )] - pub exit_zero: bool, + exit_zero: bool, /// Exit with a non-zero status code if any files were modified via fix, even if no lint violations remain. #[arg(long, help_heading = "Miscellaneous", conflicts_with = "exit_zero")] - pub exit_non_zero_on_fix: bool, + exit_non_zero_on_fix: bool, /// Show counts for every rule with at least one violation. #[arg( long, @@ -445,7 +445,7 @@ pub struct CheckCommand { conflicts_with = "diff", conflicts_with = "watch", )] - pub statistics: bool, + statistics: bool, /// Enable automatic additions of `noqa` directives to failing lines. /// Optionally provide a reason to append after the codes. #[arg( @@ -466,10 +466,10 @@ pub struct CheckCommand { conflicts_with = "fix", conflicts_with = "diff", )] - pub add_noqa: Option, - /// Enable automatic additions of `ruff:ignore` comments to failing lines. - /// Optionally provide a reason to append after the rule names. - /// Requires preview mode. + add_noqa: Option, + /// Enable automatic additions of `ruff: ignore` comments to failing lines. + /// Optionally provide a reason to append after the codes. + /// In preview, add suppression comments with rule names instead. #[arg( long, value_name = "REASON", @@ -488,7 +488,7 @@ pub struct CheckCommand { conflicts_with = "fix", conflicts_with = "diff", )] - pub add_ignore: Option, + add_ignore: Option, /// See the files Ruff will be run against with the current settings. #[arg( long, @@ -502,7 +502,7 @@ pub struct CheckCommand { conflicts_with = "stdin_filename", conflicts_with = "watch", )] - pub show_files: bool, + show_files: bool, /// See the settings Ruff will use to lint a given Python file. #[arg( long, @@ -516,7 +516,7 @@ pub struct CheckCommand { conflicts_with = "stdin_filename", conflicts_with = "watch", )] - pub show_settings: bool, + show_settings: bool, } #[derive(Clone, Debug, clap::Parser)] @@ -526,22 +526,22 @@ pub struct FormatCommand { #[clap( help = "List of files or directories to format, or `-` to read from stdin [default: .]" )] - pub files: Vec, + files: Vec, /// Avoid writing any formatted files back; instead, exit with a non-zero status code if any /// files would have been modified, and zero otherwise. #[arg(long)] - pub check: bool, + check: bool, /// Avoid writing any formatted files back; instead, exit with a non-zero status code and the /// difference between the current file and how the formatted file would look like. #[arg(long)] - pub diff: bool, + diff: bool, /// Disable cache reads. #[arg(short, long, env = "RUFF_NO_CACHE", help_heading = "Miscellaneous")] - pub no_cache: bool, + no_cache: bool, /// Path to the cache directory. #[arg(long, env = "RUFF_CACHE_DIR", help_heading = "Miscellaneous")] - pub cache_dir: Option, + cache_dir: Option, /// Respect file exclusions via `.gitignore` and other standard ignore files. /// Use `--no-respect-gitignore` to disable. @@ -560,7 +560,7 @@ pub struct FormatCommand { value_name = "FILE_PATTERN", help_heading = "File selection" )] - pub exclude: Option>, + exclude: Option>, /// Like --exclude, but adds additional files and directories on top of those already excluded. #[arg( long, @@ -582,17 +582,17 @@ pub struct FormatCommand { no_force_exclude: bool, /// Set the line-length. #[arg(long, help_heading = "Format configuration")] - pub line_length: Option, + line_length: Option, /// The name of the file when passing it through stdin. #[arg(long, help_heading = "Miscellaneous")] - pub stdin_filename: Option, + stdin_filename: Option, /// List of mappings from file extension to language (one of `python`, `ipynb`, `pyi`). For /// example, to treat `.ipy` files as IPython notebooks, use `--extension ipy:ipynb`. #[arg(long, value_delimiter = ',')] - pub extension: Option>, + extension: Option>, /// The minimum Python version that should be supported. #[arg(long, value_enum)] - pub target_version: Option, + target_version: Option, /// Enable preview mode; enables unstable formatting. /// Use `--no-preview` to disable. #[arg(long, overrides_with("no_preview"))] @@ -613,19 +613,16 @@ pub struct FormatCommand { /// /// The option can only be used when formatting a single file. Range formatting of notebooks is unsupported. #[clap(long, help_heading = "Editor options", verbatim_doc_comment)] - pub range: Option, + range: Option, /// Exit with a non-zero status code if any files were modified via format, even if all files were formatted successfully. #[arg(long, help_heading = "Miscellaneous", alias = "exit-non-zero-on-fix")] - pub exit_non_zero_on_format: bool, + exit_non_zero_on_format: bool, /// Output serialization format for violations, when used with `--check`. /// The default serialization format is "full". - /// - /// Note that this option is currently only respected in preview mode. A warning will be emitted - /// if this flag is used on stable. #[arg(long, value_enum, env = "RUFF_OUTPUT_FORMAT")] - pub output_format: Option, + output_format: Option, } #[derive(Copy, Clone, Debug, clap::Parser)] @@ -663,7 +660,7 @@ pub struct LogLevelArgs { group = "verbosity", help_heading = "Log levels" )] - pub verbose: bool, + verbose: bool, /// Print diagnostics, but nothing else. #[arg( short, @@ -672,7 +669,7 @@ pub struct LogLevelArgs { group = "verbosity", help_heading = "Log levels" )] - pub quiet: bool, + quiet: bool, /// Disable all logging (but still exit with status code "1" upon detecting diagnostics). #[arg( short, @@ -681,7 +678,7 @@ pub struct LogLevelArgs { group = "verbosity", help_heading = "Log levels" )] - pub silent: bool, + silent: bool, } impl From<&LogLevelArgs> for LogLevel { @@ -720,7 +717,7 @@ pub struct ConfigArguments { } impl ConfigArguments { - pub fn config_file(&self) -> Option<&Path> { + pub(crate) fn config_file(&self) -> Option<&Path> { self.config_file.as_deref() } @@ -791,7 +788,7 @@ impl ConfigurationTransformer for ConfigArguments { impl CheckCommand { /// Partition the CLI into command-line arguments and configuration /// overrides. - pub fn partition( + pub(crate) fn partition( self, global_options: GlobalConfigArgs, ) -> anyhow::Result<(CheckArguments, ConfigArguments)> { @@ -887,7 +884,7 @@ impl FormatCommand { impl AnalyzeGraphCommand { /// Partition the CLI into command-line arguments and configuration /// overrides. - pub fn partition( + pub(crate) fn partition( self, global_options: GlobalConfigArgs, ) -> anyhow::Result<(AnalyzeGraphArgs, ConfigArguments)> { @@ -1134,21 +1131,21 @@ Possible choices: /// CLI settings that are distinct from configuration (commands, lists of files, /// etc.). #[expect(clippy::struct_excessive_bools)] -pub struct CheckArguments { - pub add_noqa: Option, - pub add_ignore: Option, - pub diff: bool, - pub exit_non_zero_on_fix: bool, - pub exit_zero: bool, - pub files: Vec, - pub ignore_noqa: bool, - pub no_cache: bool, - pub output_file: Option, - pub show_files: bool, - pub show_settings: bool, - pub statistics: bool, - pub stdin_filename: Option, - pub watch: bool, +pub(crate) struct CheckArguments { + pub(crate) add_noqa: Option, + pub(crate) add_ignore: Option, + pub(crate) diff: bool, + pub(crate) exit_non_zero_on_fix: bool, + pub(crate) exit_zero: bool, + pub(crate) files: Vec, + pub(crate) ignore_noqa: bool, + pub(crate) no_cache: bool, + pub(crate) output_file: Option, + pub(crate) show_files: bool, + pub(crate) show_settings: bool, + pub(crate) statistics: bool, + pub(crate) stdin_filename: Option, + pub(crate) watch: bool, } /// CLI settings that are distinct from configuration (commands, lists of files, @@ -1245,8 +1242,8 @@ impl std::error::Error for FormatRangeParseError {} #[derive(Copy, Clone, Debug)] pub struct LineColumn { - pub line: OneIndexed, - pub column: OneIndexed, + line: OneIndexed, + column: OneIndexed, } impl From for ruff_source_file::SourceLocation { @@ -1372,9 +1369,9 @@ impl LineColumnParseError { /// CLI settings that are distinct from configuration (commands, lists of files, etc.). #[derive(Clone, Debug)] pub struct AnalyzeGraphArgs { - pub files: Vec, - pub direction: Direction, - pub python: Option, + pub(crate) files: Vec, + pub(crate) direction: Direction, + pub(crate) python: Option, } /// Configuration overrides provided via dedicated CLI flags: @@ -1509,7 +1506,7 @@ impl ConfigurationTransformer for ExplicitConfigOverrides { } /// Convert a list of `PatternPrefixPair` structs to `PerFileIgnore`. -pub fn collect_per_file_ignores(pairs: Vec) -> Vec { +fn collect_per_file_ignores(pairs: Vec) -> Vec { let mut per_file_ignores: FxHashMap> = FxHashMap::default(); for pair in pairs { per_file_ignores diff --git a/crates/ruff/src/cache.rs b/crates/ruff/src/cache.rs index fc8d756b33..94ca467719 100644 --- a/crates/ruff/src/cache.rs +++ b/crates/ruff/src/cache.rs @@ -91,7 +91,7 @@ impl Cache { /// /// Finally `settings` is used to ensure we don't open a cache for different /// settings. It also defines the directory where to store the cache. - pub(crate) fn open(package_root: PathBuf, settings: &Settings) -> Self { + fn open(package_root: PathBuf, settings: &Settings) -> Self { debug_assert!(package_root.is_absolute(), "package root not canonicalized"); let key = format!("{}", cache_key(&package_root, settings)); @@ -154,7 +154,7 @@ impl Cache { } /// Applies the pending changes and persists the cache to disk, if it has been changed. - pub(crate) fn persist(mut self) -> Result<()> { + fn persist(mut self) -> Result<()> { if !self.save() { // No changes made, no need to write the same cache file back to // disk. @@ -199,7 +199,7 @@ impl Cache { /// Applies the pending changes without storing the cache to disk. #[expect(clippy::cast_possible_truncation)] - pub(crate) fn save(&mut self) -> bool { + 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_hours(720); // 30 days. @@ -371,7 +371,7 @@ fn cache_key(package_root: &Path, settings: &Settings) -> u64 { } /// Initialize the cache at the specified `Path`. -pub(crate) fn init(path: &Path) -> Result<()> { +fn init(path: &Path) -> Result<()> { // Create the cache directories. fs::create_dir_all(path.join(VERSION))?; @@ -514,10 +514,11 @@ mod tests { use ruff_cache::CACHE_DIR_NAME; use ruff_linter::package::PackageRoot; + use ruff_linter::registry::Rule; use ruff_linter::settings::LinterSettings; use ruff_linter::settings::flags; use ruff_linter::settings::types::UnsafeFixes; - use ruff_python_ast::{PySourceType, PythonVersion}; + use ruff_python_ast::PySourceType; use ruff_workspace::Settings; use crate::cache::{self, ChangeData, FileCache, FileCacheData, FileCacheKey}; @@ -535,10 +536,7 @@ mod tests { let settings = Settings { cache_dir, - linter: LinterSettings { - unresolved_target_version: PythonVersion::latest().into(), - ..Default::default() - }, + linter: LinterSettings::for_rule(Rule::UnusedVariable), ..Settings::default() }; @@ -1031,6 +1029,7 @@ mod tests { let settings = Settings { cache_dir, + linter: LinterSettings::for_rule(Rule::UndefinedExport), ..Settings::default() }; diff --git a/crates/ruff/src/commands/add_noqa.rs b/crates/ruff/src/commands/add_noqa.rs index a70be76a3d..17bd202a80 100644 --- a/crates/ruff/src/commands/add_noqa.rs +++ b/crates/ruff/src/commands/add_noqa.rs @@ -1,14 +1,13 @@ use std::path::PathBuf; use std::time::Instant; -use anyhow::{Result, bail}; +use anyhow::Result; use log::{debug, error}; #[cfg(not(target_family = "wasm"))] use rayon::prelude::*; use ruff_linter::SuppressionKind; use ruff_linter::linter::add_suppressions_to_path; -use ruff_linter::preview::is_human_readable_names_enabled; use ruff_linter::source_kind::SourceKind; use ruff_linter::warn_user_once; use ruff_python_ast::{PySourceType, SourceType}; @@ -85,14 +84,6 @@ pub(crate) fn add_noqa( { return Ok(0); } - if matches!(suppression_kind, SuppressionKind::Ignore) - && !is_human_readable_names_enabled(settings.linter.preview) - { - bail!( - "`--add-ignore` requires preview mode, but preview is disabled for `{}`", - path.display() - ); - } let source_kind = match SourceKind::from_path(path, source_type) { Ok(Some(source_kind)) => source_kind, Ok(None) => return Ok(0), diff --git a/crates/ruff/src/commands/analyze_graph.rs b/crates/ruff/src/commands/analyze_graph.rs index 48fdf14a7b..6b40086994 100644 --- a/crates/ruff/src/commands/analyze_graph.rs +++ b/crates/ruff/src/commands/analyze_graph.rs @@ -6,7 +6,9 @@ use indexmap::IndexSet; use log::{debug, warn}; use path_absolutize::CWD; use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf}; -use ruff_graph::{Direction, ImportMap, ModuleDb, ModuleImports}; +use ruff_graph::{ + Direction, ImportMap, ModuleDb, ModuleImports, ResolverEnvironment, resolve_search_paths, +}; use ruff_linter::package::PackageRoot; use ruff_linter::source_kind::SourceKind; use ruff_linter::{warn_user, warn_user_once}; @@ -96,18 +98,14 @@ pub(crate) fn analyze_graph( ); let system = OsSystem::default(); - let db = ModuleDb::from_src_roots( - system, + let search_paths = resolve_search_paths( + &system, src_roots.into_iter().collect(), - pyproject_config - .settings - .analyze - .target_version - .as_tuple() - .into(), args.python .and_then(|python| SystemPathBuf::from_path_buf(python).ok()), )?; + let db = ModuleDb::new(system); + search_paths.try_register_static_roots(&db); let imports = { // Create a cache for resolved globs. @@ -117,6 +115,7 @@ pub(crate) fn analyze_graph( let result = Arc::new(Mutex::new(Vec::new())); let inner_result = Arc::clone(&result); let db = db.clone(); + let search_paths = &search_paths; rayon::scope(move |scope| { for resolved_file in paths { @@ -135,6 +134,7 @@ pub(crate) fn analyze_graph( let string_imports = settings.analyze.string_imports; let include_dependencies = settings.analyze.include_dependencies.get(path).cloned(); let type_checking_imports = settings.analyze.type_checking_imports; + let python_version = settings.analyze.target_version; let source_type = settings.analyze.extension.get_source_type(path); // Skip excluded files. @@ -176,13 +176,13 @@ pub(crate) fn analyze_graph( } }; - let source_code = source_kind.source_code(); + let environment = ResolverEnvironment::new(&db, python_version, search_paths); // Identify any imports via static analysis. let mut imports = ModuleImports::detect( &db, - source_code, - source_type.expect_python(), + environment, + &source_kind, &path, package.as_deref(), string_imports, diff --git a/crates/ruff/src/commands/format.rs b/crates/ruff/src/commands/format.rs index ccf09eb843..66cd4ffa0b 100644 --- a/crates/ruff/src/commands/format.rs +++ b/crates/ruff/src/commands/format.rs @@ -36,7 +36,7 @@ use ruff_python_ast::{PySourceType, SourceType}; use ruff_python_formatter::{ AssignmentAlignment, FormatModuleError, QuoteStyle, format_module_source, format_range, }; -use ruff_source_file::{LineIndex, LineRanges, OneIndexed, SourceFileBuilder}; +use ruff_source_file::{LineIndex, LineRanges, OneIndexed, SourceFile, SourceFileBuilder}; use ruff_text_size::{TextLen, TextRange, TextSize}; use ruff_workspace::FormatterSettings; use ruff_workspace::resolver::{ @@ -80,7 +80,6 @@ pub(crate) fn format( let (paths, resolver) = project_files_in_path(&files, pyproject_config, config_arguments)?; let output_format = pyproject_config.settings.output_format; - let preview = pyproject_config.settings.formatter.preview; if paths.is_empty() { warn_user_once!("No Python files found under the given path(s)"); @@ -193,9 +192,9 @@ pub(crate) fn format( // Report on any errors. // - // We only convert errors to `Diagnostic`s in `Check` mode with preview enabled, otherwise we - // fall back on printing simple messages. - if !(preview.is_enabled() && mode.is_check()) { + // We only convert errors to `Diagnostic`s in `Check` mode, otherwise we fall back on printing + // simple messages. + if !mode.is_check() { errors.sort_unstable_by(|a, b| a.path().cmp(&b.path())); for error in &errors { @@ -208,11 +207,7 @@ pub(crate) fn format( match mode { FormatMode::Write => {} FormatMode::Check => { - if preview.is_enabled() { - results.write_changed_preview(&mut stdout().lock(), output_format, &errors)?; - } else { - results.write_changed(&mut stdout().lock())?; - } + results.write_changed(&mut stdout().lock(), output_format, &errors)?; } FormatMode::Diff => { results.write_diff(&mut stdout().lock())?; @@ -225,7 +220,7 @@ pub(crate) fn format( if mode.is_diff() { // Allow piping the diff to e.g. a file by writing the summary to stderr results.write_summary(&mut stderr().lock())?; - } else if !preview.is_enabled() || output_format.is_human_readable() { + } else if output_format.is_human_readable() { results.write_summary(&mut stdout().lock())?; } } @@ -389,12 +384,7 @@ pub(crate) fn format_source( let formatted = formatted.map_err(|err| { if let FormatModuleError::ParseError(err) = err { - DisplayParseError::from_source_kind( - err, - path.map(Path::to_path_buf), - source_kind, - ) - .into() + FormatCommandError::parse(err, path, source_kind) } else { FormatCommandError::Format(path.map(Path::to_path_buf), err) } @@ -438,15 +428,14 @@ pub(crate) fn format_source( format_module_source(unformatted, options.clone()).map_err(|err| { if let FormatModuleError::ParseError(err) = err { // Offset the error by the start of the cell - DisplayParseError::from_source_kind( + FormatCommandError::parse( ParseError { error: err.error, location: err.location.checked_add(*start).unwrap(), }, - path.map(Path::to_path_buf), + path, source_kind, ) - .into() } else { FormatCommandError::Format(path.map(Path::to_path_buf), err) } @@ -498,12 +487,6 @@ pub(crate) fn format_source( ))) } SourceKind::Markdown(unformatted_document) => { - if !settings.preview.is_enabled() { - return Err(FormatCommandError::MarkdownExperimental( - path.map(Path::to_path_buf), - )); - } - if range.is_some() { return Err(FormatCommandError::RangeFormatNotSupported( path.map(Path::to_path_buf), @@ -592,34 +575,28 @@ impl<'a> FormatResults<'a> { Ok(()) } - /// Write a list of the files that would be changed to the given writer. - fn write_changed(&self, f: &mut impl Write) -> io::Result<()> { - for path in self - .results - .iter() - .filter_map(|result| { - if result.result.is_diff() { - Some(result.path.as_path()) - } else { - None - } - }) - .sorted_unstable() - { - writeln!(f, "Would reformat: {}", fs::relativize_path(path).bold())?; - } - - Ok(()) - } - /// Write a list of the files that would be changed and any errors to the given writer. - fn write_changed_preview( + fn write_changed( &self, f: &mut impl Write, output_format: OutputFormat, errors: &[FormatCommandError], ) -> io::Result<()> { - let mut notebook_index = FxHashMap::default(); + let mut notebook_index = errors + .iter() + .filter_map(|error| { + if let FormatCommandError::Parse { + source_file, + notebook_index: Some(notebook_index), + .. + } = error + { + Some((source_file.name().to_string(), notebook_index.clone())) + } else { + None + } + }) + .collect(); let diagnostics: Vec<_> = errors .iter() .map(Diagnostic::from) @@ -630,7 +607,6 @@ impl<'a> FormatResults<'a> { let context = EmitterContext::new(¬ebook_index); let config = DisplayDiagnosticConfig::new("ruff") .hide_severity(true) - .show_fix_diff(true) .color(!cfg!(test) && colored::control::SHOULD_COLORIZE.should_colorize()); render_diagnostics(f, output_format, config, &context, &diagnostics) @@ -853,16 +829,37 @@ impl<'a> FormatResults<'a> { #[derive(Error, Debug)] pub(crate) enum FormatCommandError { Ignore(#[from] ignore::Error), - Parse(#[from] DisplayParseError), + Parse { + error: DisplayParseError, + source_file: SourceFile, + notebook_index: Option, + }, Panic(Option, Box), Read(Option, SourceError), Format(Option, FormatModuleError), Write(Option, SourceError), RangeFormatNotSupported(Option), - MarkdownExperimental(Option), } impl FormatCommandError { + fn parse(error: ParseError, path: Option<&Path>, source_kind: &SourceKind) -> Self { + let name = path.map_or_else(|| "-".into(), Path::to_string_lossy); + let source_file = SourceFileBuilder::new(name, source_kind.source_code()).finish(); + let notebook_index = source_kind + .as_ipy_notebook() + .map(|notebook| notebook.index().clone()); + + Self::Parse { + error: DisplayParseError::from_source_kind( + error, + path.map(Path::to_path_buf), + source_kind, + ), + source_file, + notebook_index, + } + } + fn path(&self) -> Option<&Path> { match self { Self::Ignore(err) => { @@ -872,13 +869,12 @@ impl FormatCommandError { None } } - Self::Parse(err) => err.path(), + Self::Parse { error, .. } => error.path(), Self::Panic(path, _) | Self::Read(path, _) | Self::Format(path, _) | Self::Write(path, _) - | Self::RangeFormatNotSupported(path) - | Self::MarkdownExperimental(path) => path.as_deref(), + | Self::RangeFormatNotSupported(path) => path.as_deref(), } } } @@ -897,11 +893,15 @@ impl From<&FormatCommandError> for Diagnostic { FormatCommandError::Ignore(error) => { Diagnostic::new(DiagnosticId::Io, Severity::Error, error) } - FormatCommandError::Parse(display_parse_error) => Diagnostic::new( - DiagnosticId::InvalidSyntax, - Severity::Error, - &display_parse_error.error().error, - ), + FormatCommandError::Parse { + error, source_file, .. + } => { + return Diagnostic::invalid_syntax( + source_file.clone(), + &error.error().error, + error.error(), + ); + } FormatCommandError::Panic(path, panic_error) => { return create_panic_diagnostic(panic_error, path.as_deref()); } @@ -915,11 +915,6 @@ impl From<&FormatCommandError> for Diagnostic { Severity::Error, "Range formatting is only supported for Python files.", ), - FormatCommandError::MarkdownExperimental(_) => Diagnostic::new( - DiagnosticId::PreviewFeature, - Severity::Warning, - "Markdown formatting is experimental, enable preview mode.", - ), }; if let Some(annotation) = annotation { @@ -955,8 +950,8 @@ impl Display for FormatCommandError { ) } } - Self::Parse(err) => { - write!(f, "{err}") + Self::Parse { error, .. } => { + write!(f, "{error}") } Self::Read(path, err) => { if let Some(path) = path { @@ -1014,23 +1009,6 @@ impl Display for FormatCommandError { ) } } - Self::MarkdownExperimental(path) => { - if let Some(path) = path { - write!( - f, - "{header}{path}{colon} Markdown formatting is experimental, enable preview mode.", - header = "Failed to format ".bold(), - path = fs::relativize_path(path).bold(), - colon = ":".bold() - ) - } else { - write!( - f, - "{header} Markdown formatting is experimental, enable preview mode", - header = "Failed to format:".bold() - ) - } - } Self::Panic(path, err) => { let message = r"This indicates a bug in Ruff. If you could open an issue at: @@ -1303,7 +1281,6 @@ mod tests { use insta::assert_snapshot; use ruff_db::panic::catch_unwind; - use ruff_linter::logging::DisplayParseError; use ruff_linter::source_kind::{SourceError, SourceKind}; use ruff_python_formatter::FormatModuleError; use ruff_python_parser::{ParseError, ParseErrorType}; @@ -1334,14 +1311,14 @@ mod tests { "Permission denied", ))), }), - FormatCommandError::Parse(DisplayParseError::from_source_kind( + FormatCommandError::parse( ParseError { error: ParseErrorType::UnexpectedIndentation, location: TextRange::default(), }, - Some(path.clone()), + Some(&path), &source_kind, - )), + ), FormatCommandError::Panic(Some(path.clone()), Box::new(panic_error)), FormatCommandError::Read( Some(path.clone()), @@ -1366,7 +1343,7 @@ mod tests { let results = FormatResults::new(&[], FormatMode::Check); let mut buf = Vec::new(); - results.write_changed_preview( + results.write_changed( &mut buf, ruff_linter::settings::types::OutputFormat::Full, &errors, @@ -1380,9 +1357,6 @@ mod tests { io: test.py: Permission denied --> test.py:1:1 - invalid-syntax: Unexpected indentation - --> test.py:1:1 - io: File not found --> test.py:1:1 @@ -1395,6 +1369,12 @@ mod tests { invalid-cli-option: Range formatting is only supported for Python files. --> test.py:1:1 + invalid-syntax: Unexpected indentation + --> test.py:1:1 + | + 1 | 1 + | ^ + panic: Panicked at when checking `test.py`: `Test panic for FormatCommandError` --> test.py:1:1 info: This indicates a bug in Ruff. diff --git a/crates/ruff/src/diagnostics.rs b/crates/ruff/src/diagnostics.rs index 634ad80b1c..f892eca98f 100644 --- a/crates/ruff/src/diagnostics.rs +++ b/crates/ruff/src/diagnostics.rs @@ -54,7 +54,7 @@ impl Diagnostics { } /// Generate [`Diagnostics`] based on a [`SourceError`]. - pub(crate) fn from_source_error( + fn from_source_error( err: &SourceError, path: Option<&Path>, settings: &LinterSettings, diff --git a/crates/ruff/src/lib.rs b/crates/ruff/src/lib.rs index 53c0c901f0..b27fd72116 100644 --- a/crates/ruff/src/lib.rs +++ b/crates/ruff/src/lib.rs @@ -212,14 +212,8 @@ pub fn run( } fn format(args: FormatCommand, global_options: GlobalConfigArgs) -> Result { - let cli_output_format_set = args.output_format.is_some(); let (cli, config_arguments) = args.partition(global_options)?; let pyproject_config = resolve::resolve(&config_arguments, cli.stdin_filename.as_deref())?; - if cli_output_format_set && !pyproject_config.settings.formatter.preview.is_enabled() { - warn_user_once!( - "The --output-format flag for the formatter is unstable and requires preview mode to use." - ); - } if is_stdin(&cli.files, cli.stdin_filename.as_deref()) { commands::format_stdin::format_stdin(&cli, &config_arguments, &pyproject_config) } else { @@ -381,6 +375,7 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result Result Result Result ExitCode { +fn main() -> ExitCode { // Enabled ANSI colors on Windows 10. #[cfg(windows)] assert!(colored::control::set_virtual_terminal(true).is_ok()); diff --git a/crates/ruff/src/printer.rs b/crates/ruff/src/printer.rs index aad9984176..84b86c2c3a 100644 --- a/crates/ruff/src/printer.rs +++ b/crates/ruff/src/printer.rs @@ -210,6 +210,7 @@ impl Printer { diagnostics: &Diagnostics, writer: &mut dyn Write, preview: PreviewMode, + prefer_rule_codes: bool, ) -> Result<()> { if matches!(self.log_level, LogLevel::Silent) { return Ok(()); @@ -223,7 +224,7 @@ impl Printer { if self.flags.intersects(Flags::SHOW_FIX_SUMMARY) { if !diagnostics.fixed.is_empty() { writeln!(writer)?; - print_fix_summary(writer, &diagnostics.fixed, preview)?; + print_fix_summary(writer, &diagnostics.fixed, preview, prefer_rule_codes)?; writeln!(writer)?; } } @@ -237,11 +238,11 @@ impl Printer { let config = DisplayDiagnosticConfig::new("ruff") .preview(preview.is_enabled()) + .prefer_rule_codes(prefer_rule_codes) .hide_severity(true) .color(!cfg!(test) && colored::control::SHOULD_COLORIZE.should_colorize()) .with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref())) - .with_fix_applicability(self.unsafe_fixes.required_applicability()) - .show_fix_diff(preview.is_enabled()); + .with_fix_applicability(self.unsafe_fixes.required_applicability()); render_diagnostics(writer, self.format, config, &context, &diagnostics.inner)?; @@ -252,7 +253,7 @@ impl Printer { if self.flags.intersects(Flags::SHOW_FIX_SUMMARY) { if !diagnostics.fixed.is_empty() { writeln!(writer)?; - print_fix_summary(writer, &diagnostics.fixed, preview)?; + print_fix_summary(writer, &diagnostics.fixed, preview, prefer_rule_codes)?; writeln!(writer)?; } } @@ -384,6 +385,7 @@ impl Printer { writer: &mut dyn Write, diagnostics: &Diagnostics, preview: PreviewMode, + prefer_rule_codes: bool, ) -> Result<()> { if matches!(self.log_level, LogLevel::Silent) { return Ok(()); @@ -411,11 +413,11 @@ impl Printer { let context = EmitterContext::new(&diagnostics.notebook_indexes); let config = DisplayDiagnosticConfig::new("ruff") .preview(preview.is_enabled()) + .prefer_rule_codes(prefer_rule_codes) .hide_severity(true) .color(!cfg!(test) && colored::control::SHOULD_COLORIZE.should_colorize()) .with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref())) - .with_fix_applicability(self.unsafe_fixes.required_applicability()) - .show_fix_diff(preview.is_enabled()); + .with_fix_applicability(self.unsafe_fixes.required_applicability()); render_diagnostics(writer, self.format, config, &context, &diagnostics.inner)?; } writer.flush()?; @@ -447,7 +449,12 @@ fn show_fix_status(fix_mode: flags::FixMode, fixables: Option<&FixableStatistics (!fix_mode.is_apply()) && fixables.is_some_and(FixableStatistics::any_applicable_fixes) } -fn print_fix_summary(writer: &mut dyn Write, fixed: &FixMap, preview: PreviewMode) -> Result<()> { +fn print_fix_summary( + writer: &mut dyn Write, + fixed: &FixMap, + preview: PreviewMode, + prefer_rule_codes: bool, +) -> Result<()> { let total = fixed .values() .map(|table| table.counts().sum::()) @@ -477,7 +484,7 @@ fn print_fix_summary(writer: &mut dyn Write, fixed: &FixMap, preview: PreviewMod ":".cyan() )?; for (code, name, count) in table.iter().sorted_by_key(|(.., count)| Reverse(*count)) { - if is_human_readable_names_enabled(preview) { + if is_human_readable_names_enabled(preview) && !prefer_rule_codes { writeln!( writer, " {count:>num_digits$} × {name} ({code})", diff --git a/crates/ruff/tests/cli/format.rs b/crates/ruff/tests/cli/format.rs index 8f6bc2c729..20260c089f 100644 --- a/crates/ruff/tests/cli/format.rs +++ b/crates/ruff/tests/cli/format.rs @@ -51,16 +51,28 @@ fn default_files() -> Result<()> { assert_cmd_snapshot!(test.format_command() .arg("--isolated") - .arg("--check"), @" + .arg("--check"), @r#" success: false exit_code: 1 ----- stdout ----- - Would reformat: bar.py - Would reformat: foo.py + unformatted: File would be reformatted + --> bar.py:1:7 + | + - bar = "needs formatting" + 1 + bar = "needs formatting" + | + + unformatted: File would be reformatted + --> foo.py:1:7 + | + - foo = "needs formatting" + 1 + foo = "needs formatting" + | + 2 files would be reformatted ----- stderr ----- - "); + "#); Ok(()) } @@ -250,6 +262,9 @@ fn format_options() -> Result<()> { indent-width = 8 line-length = 84 +[lint] +isort.split-on-trailing-comma = false + [format] indent-style = "tab" quote-style = "single" @@ -446,16 +461,30 @@ OTHER = "OTHER" // Explicitly pass test.py, should be formatted regardless of it being excluded by format.exclude .arg("test.py") // Format all other files in the directory, should respect the `exclude` and `format.exclude` options - .arg("."), @" + .arg("."), @r#" success: false exit_code: 1 ----- stdout ----- - Would reformat: main.py - Would reformat: test.py + unformatted: File would be reformatted + --> main.py:1:1 + | + - + 1 | from test import say_hy + | + + unformatted: File would be reformatted + --> test.py:1:1 + | + - + 1 | def say_hy(name: str): + - print(f"Hy {name}") + 2 + print(f"Hy {name}") + | + 2 files would be reformatted ----- stderr ----- - "); + "#); Ok(()) } @@ -490,7 +519,13 @@ exclude = ["format_excluded.py"] success: false exit_code: 1 ----- stdout ----- - Would reformat: main.py + unformatted: File would be reformatted + --> main.py:1:3 + | + - x = 1 + 1 + x = 1 + | + 1 file would be reformatted ----- stderr ----- @@ -512,7 +547,13 @@ fn deduplicate_directory_and_explicit_file() -> Result<()> { success: false exit_code: 1 ----- stdout ----- - Would reformat: main.py + unformatted: File would be reformatted + --> main.py:1:3 + | + - x = 1 + 1 + x = 1 + | + 1 file would be reformatted ----- stderr ----- @@ -538,9 +579,14 @@ from module import = success: false exit_code: 2 ----- stdout ----- + invalid-syntax: Expected an import name + --> main.py:2:20 + | + 2 | from module import = + | ^ + ----- stderr ----- - error: Failed to parse main.py:2:20: Expected an import name "); Ok(()) @@ -565,7 +611,13 @@ if __name__ == "__main__": success: false exit_code: 1 ----- stdout ----- - Would reformat: main.py + unformatted: File would be reformatted + --> main.py:1:1 + | + - + 1 | from test import say_hy + | + 1 file would be reformatted ----- stderr ----- @@ -628,13 +680,8 @@ if __name__ == "__main__": assert_cmd_snapshot!( snapshot, - test.format_command().args([ - "--output-format", - output_format, - "--preview", - "--check", - "input.py", - ]), + test.format_command() + .args(["--output-format", output_format, "--check", "input.py",]), ); Ok(()) @@ -646,13 +693,13 @@ fn output_format_notebook() -> Result<()> { let path = test.fixture_path("unformatted.ipynb"); assert_cmd_snapshot!( - test.format_command().args(["--isolated", "--preview", "--check"]).arg(path), + test.format_command().args(["--isolated", "--check"]).arg(path), @" success: false exit_code: 1 ----- stdout ----- unformatted: File would be reformatted - --> CRATE_ROOT/resources/test/fixtures/unformatted.ipynb:cell 1:1:1 + --> CRATE_ROOT/resources/test/fixtures/unformatted.ipynb:cell 1:2:1 ::: cell 1 | 1 | import numpy @@ -786,7 +833,15 @@ fn check_quiet_mode_shows_diagnostics_only() -> Result<()> { success: false exit_code: 1 ----- stdout ----- - Would reformat: main.py + unformatted: File would be reformatted + --> main.py:1:5 + | + - def foo(): + - pass + 1 + def foo(): + 2 + pass + | + ----- stderr ----- "); @@ -802,7 +857,15 @@ fn check_default_mode_shows_diagnostics_and_summary() -> Result<()> { success: false exit_code: 1 ----- stdout ----- - Would reformat: main.py + unformatted: File would be reformatted + --> main.py:1:5 + | + - def foo(): + - pass + 1 + def foo(): + 2 + pass + | + 1 file would be reformatted ----- stderr ----- @@ -859,7 +922,13 @@ OTHER = "OTHER" success: false exit_code: 1 ----- stdout ----- - Would reformat: main.py + unformatted: File would be reformatted + --> main.py:1:1 + | + - + 1 | from test import say_hy + | + 1 file would be reformatted ----- stderr ----- @@ -2003,9 +2072,8 @@ fn test_notebook_trailing_semicolon() -> Result<()> { Ok(()) } -#[test] -fn syntax_error_in_notebooks() -> Result<()> { - let test = CliTest::with_files([ +fn notebook_with_syntax_error() -> Result { + CliTest::with_files([ ( "ruff.toml", r#" @@ -2063,7 +2131,12 @@ include = ["*.ipy"] } "#, ), - ])?; + ]) +} + +#[test] +fn syntax_error_in_notebooks() -> Result<()> { + let test = notebook_with_syntax_error()?; assert_cmd_snapshot!(test.format_command() .args(["--config", "ruff.toml"]) @@ -2079,6 +2152,35 @@ include = ["*.ipy"] Ok(()) } +#[test] +fn syntax_error_in_notebooks_check() -> Result<()> { + let test = notebook_with_syntax_error()?; + + assert_cmd_snapshot!( + test.format_command() + .args(["--config", "ruff.toml"]) + .args(["--extension", "ipy:ipynb"]) + .arg("--check") + .arg("."), + @" + success: false + exit_code: 2 + ----- stdout ----- + invalid-syntax: Expected an expression + --> main.ipy:cell 2:3:24 + | + 1 | for i in range(iterations): + 2 | # выберите случайный индекс в диапазон от 0 до len(X)-1 включительно при помощи функции random.randint + 3 | j = # ваш код здесь + | ^ + + + ----- stderr ----- + " + ); + Ok(()) +} + #[test] fn extension() -> Result<()> { let test = CliTest::with_files([ @@ -2563,57 +2665,19 @@ fn cookiecutter_globbing() -> Result<()> { } #[test] -fn stable_output_format_warning() -> Result<()> { - let test = CliTest::new()?; - assert_cmd_snapshot!( - test.format_command() - .args(["--output-format=full", "-"]) - .pass_stdin(""), - @" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - warning: The --output-format flag for the formatter is unstable and requires preview mode to use. - ", - ); - Ok(()) -} - -#[test] -fn markdown_formatting_preview_disabled() -> Result<()> { - let test = CliTest::new()?; - let unformatted = test.fixture_path("unformatted.md"); - - assert_cmd_snapshot!(test.format_command() - .args(["--isolated", "--no-preview", "--diff"]) - .arg(unformatted), - @" - success: false - exit_code: 2 - ----- stdout ----- - - ----- stderr ----- - error: Failed to format CRATE_ROOT/resources/test/fixtures/unformatted.md: Markdown formatting is experimental, enable preview mode. - "); - Ok(()) -} - -#[test] -fn markdown_formatting_preview_enabled() -> Result<()> { +fn markdown_formatting() -> Result<()> { let test = CliTest::new()?; let unformatted = test.fixture_path("unformatted.md"); assert_cmd_snapshot!(test.format_command() - .args(["--isolated", "--preview", "--check"]) + .args(["--isolated", "--check"]) .arg(unformatted), @r#" success: false exit_code: 1 ----- stdout ----- unformatted: File would be reformatted - --> CRATE_ROOT/resources/test/fixtures/unformatted.md:1:1 + --> CRATE_ROOT/resources/test/fixtures/unformatted.md:4:7 | 3 | ```py - print( "hello" ) @@ -2648,7 +2712,7 @@ fn markdown_formatting_stdin() -> Result<()> { let unformatted = fs::read(test.fixture_path("unformatted.md")).unwrap(); assert_cmd_snapshot!(test.format_command() - .args(["--isolated", "--preview", "--stdin-filename", "unformatted.md"]) + .args(["--isolated", "--stdin-filename", "unformatted.md"]) .arg("-") .pass_stdin(unformatted), @r#" success: true @@ -2691,7 +2755,7 @@ print( 'hello' ) ])?; assert_cmd_snapshot!( - test.format_command().args(["--preview", "--diff", "test.qmd"]), + test.format_command().args(["--diff", "test.qmd"]), @r#" success: false exit_code: 1 @@ -2744,16 +2808,13 @@ print( 'hello' ) assert_cmd_snapshot!( test.format_command() - .args(["format", "--preview", "--check", "."]), + .args(["--check", "."]), @r#" success: false - exit_code: 2 + exit_code: 1 ----- stdout ----- - io: [TMP]/format: No such file or directory (os error 2) - --> format:1:1 - unformatted: File would be reformatted - --> test.bar:1:1 + --> test.bar:5:7 | 4 | ```py - print( 'hello' ) diff --git a/crates/ruff/tests/cli/lint.rs b/crates/ruff/tests/cli/lint.rs index c4f3e19c32..3abf3a6c66 100644 --- a/crates/ruff/tests/cli/lint.rs +++ b/crates/ruff/tests/cli/lint.rs @@ -46,7 +46,8 @@ inline-quotes = "single" test.py:1:5: Q000 [*] Double quotes found but single quotes preferred test.py:1:5: B005 Using `.strip()` with multi-character strings is misleading test.py:1:19: Q000 [*] Double quotes found but single quotes preferred - Found 3 errors. + test.py:1:19: PLE1310 String `strip` call contains duplicate characters + Found 4 errors. [*] 2 fixable with the `--fix` option. ----- stderr ----- @@ -83,7 +84,8 @@ inline-quotes = "single" -:1:5: Q000 [*] Double quotes found but single quotes preferred -:1:5: B005 Using `.strip()` with multi-character strings is misleading -:1:19: Q000 [*] Double quotes found but single quotes preferred - Found 3 errors. + -:1:19: PLE1310 String `strip` call contains duplicate characters + Found 4 errors. [*] 2 fixable with the `--fix` option. ----- stderr ----- @@ -117,7 +119,8 @@ inline-quotes = "single" -:1:5: Q000 [*] Double quotes found but single quotes preferred -:1:5: B005 Using `.strip()` with multi-character strings is misleading -:1:19: Q000 [*] Double quotes found but single quotes preferred - Found 3 errors. + -:1:19: PLE1310 String `strip` call contains duplicate characters + Found 4 errors. [*] 2 fixable with the `--fix` option. ----- stderr ----- @@ -157,7 +160,8 @@ inline-quotes = "single" -:1:5: Q000 [*] Double quotes found but single quotes preferred -:1:5: B005 Using `.strip()` with multi-character strings is misleading -:1:19: Q000 [*] Double quotes found but single quotes preferred - Found 3 errors. + -:1:19: PLE1310 String `strip` call contains duplicate characters + Found 4 errors. [*] 2 fixable with the `--fix` option. ----- stderr ----- @@ -2610,7 +2614,6 @@ fn add_ignore() -> Result<()> { fixture .check_command() .arg("--select=RUF015") - .arg("--preview") .arg("--add-ignore"), @" success: true @@ -2628,38 +2631,11 @@ fn add_ignore() -> Result<()> { test_code, @" - def first_square(): - return [x * x for x in range(20)][0] # ruff:ignore[unnecessary-iterable-allocation-for-first-element] - ", - ); - - Ok(()) -} - -#[test] -fn add_ignore_requires_preview() -> Result<()> { - let fixture = CliTest::new()?; - fixture.write_file("noqa.py", "import os\n")?; - - assert_cmd_snapshot!( - fixture - .check_command() - .arg("--select=F401") - .arg("--add-ignore"), - @" - success: false - exit_code: 2 - ----- stdout ----- - - ----- stderr ----- - ruff failed - Cause: `--add-ignore` requires preview mode, but preview is disabled for `[TMP]/noqa.py` - ", + def first_square(): + return [x * x for x in range(20)][0] # ruff: ignore[RUF015] + ", ); - let test_code = fixture.read_file("noqa.py")?; - insta::assert_snapshot!(test_code, @"import os"); - Ok(()) } @@ -2685,7 +2661,6 @@ fn add_noqa_existing_ignore() -> Result<()> { ----- stdout ----- ----- stderr ----- - warning: #ruff:ignore comment found but not active, enable preview mode Added 1 noqa directive. ", ); @@ -2696,7 +2671,7 @@ fn add_noqa_existing_ignore() -> Result<()> { test_code, @" - def unused(x): # ruff:ignore[ANN001, ARG001, D103] # noqa: ANN001, ANN201, D103 + def unused(x): # ruff:ignore[ANN001, ARG001, D103] # noqa: ANN201 pass ", ); @@ -3744,18 +3719,18 @@ def foo(): ]) .pass_stdin(source), @" - success: true - exit_code: 0 - ----- stdout ----- - # ruff:file-ignore[unused-import] - import os + success: true + exit_code: 0 + ----- stdout ----- + # ruff: file-ignore[unused-import] + import os - def foo(): - value = 1 # ruff:ignore[unused-variable] + def foo(): + value = 1 # ruff: ignore[unused-variable] - ----- stderr ----- - Found 4 errors (4 fixed, 0 remaining). - ", + ----- stderr ----- + Found 4 errors (4 fixed, 0 remaining). + ", ); Ok(()) @@ -3981,7 +3956,7 @@ fn walrus_before_py38() { .args(["--stdin-filename", "test.py"]) .arg("--target-version=py38") .arg("-") - .pass_stdin(r#"(x := 1)"#), + .pass_stdin(r#"if (x := 1): ..."#), @" success: true exit_code: 0 @@ -3998,12 +3973,12 @@ fn walrus_before_py38() { .args(["--stdin-filename", "test.py"]) .arg("--target-version=py37") .arg("-") - .pass_stdin(r#"(x := 1)"#), + .pass_stdin(r#"if (x := 1): ..."#), @" success: false exit_code: 1 ----- stdout ----- - test.py:1:2: invalid-syntax: Cannot use named assignment expression (`:=`) on Python 3.7 (syntax was added in Python 3.8) + test.py:1:5: invalid-syntax: Cannot use named assignment expression (`:=`) on Python 3.7 (syntax was added in Python 3.8) Found 1 error. ----- stderr ----- @@ -4298,6 +4273,31 @@ class Foo: ); } +#[test] +fn prefer_rule_codes_in_output() { + assert_cmd_snapshot!( + Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .args([ + "--preview", + "--config", + "output-prefer-rule-codes = true", + "--select=A001", + "-", + ]) + .pass_stdin("print = 1\n"), + @" + success: false + exit_code: 1 + ----- stdout ----- + -:1:1: A001 Variable `print` is shadowing a Python builtin + Found 1 error. + + ----- stderr ----- + " + ); +} + #[test_case::test_case("concise")] #[test_case::test_case("full")] #[test_case::test_case("json")] @@ -4500,7 +4500,6 @@ fn show_fixes_in_full_output_with_preview_enabled() { | 1 | import math | ^^^^ - | help: Remove unused import: `math` | - import math @@ -4767,7 +4766,7 @@ fn supported_file_extensions_preview_enabled() -> Result<()> { } #[test] -fn preview_default_rules() -> Result<()> { +fn default_rules() -> Result<()> { let test = CliTest::with_settings(|_path, mut settings| { settings.add_filter(r"(?s).*(linter\.rules\.enabled[^]]+]).*", "$1"); settings @@ -4776,7 +4775,7 @@ fn preview_default_rules() -> Result<()> { test.write_file("try.py", "1")?; assert_cmd_snapshot!( - test.check_command().args(["--preview", "--show-settings"]), + test.check_command().arg("--show-settings"), @" linter.rules.enabled = [ sys-version-slice3 (YTT101), @@ -4869,6 +4868,7 @@ fn preview_default_rules() -> Result<()> { f-string-in-get-text-func-call (INT001), format-in-get-text-func-call (INT002), printf-in-get-text-func-call (INT003), + implicit-string-concatenation-in-collection-literal (ISC004), direct-logger-instantiation (LOG001), invalid-get-logger-argument (LOG002), undocumented-warn (LOG009), @@ -5032,6 +5032,7 @@ fn preview_default_rules() -> Result<()> { nonlocal-without-binding (PLE0117), load-before-global-declaration (PLE0118), invalid-length-return-type (PLE0303), + invalid-bool-return-type (PLE0304), invalid-index-return-type (PLE0305), invalid-str-return-type (PLE0307), invalid-bytes-return-type (PLE0308), @@ -5062,6 +5063,7 @@ fn preview_default_rules() -> Result<()> { property-with-parameters (PLR0206), manual-from-import (PLR0402), redefined-argument-from-local (PLR1704), + stop-iteration-return (PLR1708), useless-return (PLR1711), boolean-chained-comparison (PLR1716), sys-exit-alias (PLR1722), @@ -5147,6 +5149,7 @@ fn preview_default_rules() -> Result<()> { implicit-cwd (FURB177), hashlib-digest-hex (FURB181), slice-to-remove-prefix-or-suffix (FURB188), + sorted-min-max (FURB192), zip-instead-of-pairwise (RUF007), mutable-dataclass-default (RUF008), function-call-in-dataclass-default-argument (RUF009), @@ -5178,6 +5181,8 @@ fn preview_default_rules() -> Result<()> { unnecessary-round (RUF057), starmap-zip (RUF058), unused-unpacked-variable (RUF059), + access-annotations-from-class-dict (RUF063), + duplicate-entry-in-dunder-all (RUF068), unused-noqa (RUF100), redirected-noqa (RUF101), invalid-pyproject-toml (RUF200), @@ -5214,7 +5219,6 @@ fn ruff_toml_is_linted() -> Result<()> { | 1 | lint.select = ["F401"] | ^^^^ - | help: Replace rule code with `unused-import` | - lint.select = ["F401"] diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__output_format_full.snap b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_full.snap index ce1e673e64..59e2fcbe6d 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__output_format_full.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_full.snap @@ -25,6 +25,10 @@ F401 [*] `os` imported but unused 3 | match 42: # invalid-syntax | help: Remove unused import: `os` + | + - import os # F401 +1 | x = y # F821 + | F821 Undefined name `y` --> input.py:2:5 diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap index 827994fc0d..b28cb65edc 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap @@ -21,6 +21,7 @@ cache_dir = "[TMP]/.ruff_cache" fix = false fix_only = false output_format = concise +output_prefer_rule_codes = false show_fixes = false unsafe_fixes = hint @@ -63,6 +64,7 @@ file_resolver.include = [ "**/pyproject.toml", "**/ruff.toml", "**/.ruff.toml", + "*.md", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap index b1022a0e08..82232902b0 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap @@ -23,6 +23,7 @@ cache_dir = "[TMP]/.ruff_cache" fix = false fix_only = false output_format = concise +output_prefer_rule_codes = false show_fixes = false unsafe_fixes = hint @@ -65,6 +66,7 @@ file_resolver.include = [ "**/pyproject.toml", "**/ruff.toml", "**/.ruff.toml", + "*.md", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap index 244ebe3bf6..670819540a 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap @@ -24,6 +24,7 @@ cache_dir = "[TMP]/.ruff_cache" fix = false fix_only = false output_format = concise +output_prefer_rule_codes = false show_fixes = false unsafe_fixes = hint diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap index f0faef79ce..8fb6ee050b 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap @@ -25,6 +25,7 @@ cache_dir = "[TMP]/.ruff_cache" fix = false fix_only = false output_format = concise +output_prefer_rule_codes = false show_fixes = false unsafe_fixes = hint @@ -67,6 +68,7 @@ file_resolver.include = [ "**/pyproject.toml", "**/ruff.toml", "**/.ruff.toml", + "*.md", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap index 44508e1394..a15346f9e7 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap @@ -22,6 +22,7 @@ cache_dir = "[TMP]/.ruff_cache" fix = false fix_only = false output_format = concise +output_prefer_rule_codes = false show_fixes = false unsafe_fixes = hint @@ -64,6 +65,7 @@ file_resolver.include = [ "**/pyproject.toml", "**/ruff.toml", "**/.ruff.toml", + "*.md", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap index 8a864404ac..143c8b287f 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap @@ -23,6 +23,7 @@ cache_dir = "[TMP]/foo/.ruff_cache" fix = false fix_only = false output_format = concise +output_prefer_rule_codes = false show_fixes = false unsafe_fixes = hint @@ -65,6 +66,7 @@ file_resolver.include = [ "**/pyproject.toml", "**/ruff.toml", "**/.ruff.toml", + "*.md", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap index b877a21d24..35cadb9e91 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap @@ -21,6 +21,7 @@ cache_dir = "[TMP]/.ruff_cache" fix = false fix_only = false output_format = concise +output_prefer_rule_codes = false show_fixes = false unsafe_fixes = hint @@ -63,6 +64,7 @@ file_resolver.include = [ "**/pyproject.toml", "**/ruff.toml", "**/.ruff.toml", + "*.md", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap index 0a993b29e8..12fb491c02 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap @@ -21,6 +21,7 @@ cache_dir = "[TMP]/.ruff_cache" fix = false fix_only = false output_format = concise +output_prefer_rule_codes = false show_fixes = false unsafe_fixes = hint @@ -63,6 +64,7 @@ file_resolver.include = [ "**/pyproject.toml", "**/ruff.toml", "**/.ruff.toml", + "*.md", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap index f185de3b8c..58f7d76c06 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap @@ -21,6 +21,7 @@ cache_dir = "[TMP]/.ruff_cache" fix = false fix_only = false output_format = concise +output_prefer_rule_codes = false show_fixes = false unsafe_fixes = hint @@ -63,6 +64,7 @@ file_resolver.include = [ "**/pyproject.toml", "**/ruff.toml", "**/.ruff.toml", + "*.md", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap b/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap index b890abda11..6c16578e54 100644 --- a/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap +++ b/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap @@ -18,6 +18,7 @@ cache_dir = "[TMP]/.ruff_cache" fix = false fix_only = false output_format = full +output_prefer_rule_codes = false show_fixes = false unsafe_fixes = hint @@ -60,6 +61,7 @@ file_resolver.include = [ "**/pyproject.toml", "**/ruff.toml", "**/.ruff.toml", + "*.md", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true @@ -69,28 +71,208 @@ file_resolver.project_root = "[TMP]/" linter.exclude = [] linter.project_root = "[TMP]/" linter.rules.enabled = [ - multiple-imports-on-one-line (E401), - module-import-not-at-top-of-file (E402), - multiple-statements-on-one-line-colon (E701), - multiple-statements-on-one-line-semicolon (E702), - useless-semicolon (E703), - none-comparison (E711), - true-false-comparison (E712), - not-in-test (E713), - not-is-test (E714), - type-comparison (E721), + sys-version-slice3 (YTT101), + sys-version2 (YTT102), + sys-version-cmp-str3 (YTT103), + sys-version-info0-eq3 (YTT201), + six-py3 (YTT202), + sys-version-info1-cmp-int (YTT203), + sys-version-info-minor-cmp-int (YTT204), + sys-version0 (YTT301), + sys-version-cmp-str10 (YTT302), + sys-version-slice1 (YTT303), + cancel-scope-no-checkpoint (ASYNC100), + trio-sync-call (ASYNC105), + async-zero-sleep (ASYNC115), + long-sleep-not-forever (ASYNC116), + blocking-http-call-in-async-function (ASYNC210), + create-subprocess-in-async-function (ASYNC220), + run-process-in-async-function (ASYNC221), + wait-for-process-in-async-function (ASYNC222), + blocking-open-call-in-async-function (ASYNC230), + blocking-sleep-in-async-function (ASYNC251), + exec-builtin (S102), + try-except-pass (S110), + try-except-continue (S112), + blind-except (BLE001), + unary-prefix-increment-decrement (B002), + assignment-to-os-environ (B003), + unreliable-callable-check (B004), + strip-with-multi-characters (B005), + mutable-argument-default (B006), + function-call-in-default-argument (B008), + get-attr-with-constant (B009), + set-attr-with-constant (B010), + jump-statement-in-finally (B012), + redundant-tuple-in-exception-handler (B013), + duplicate-handler-exception (B014), + useless-comparison (B015), + raise-literal (B016), + assert-raises-exception (B017), + useless-expression (B018), + cached-instance-method (B019), + loop-variable-overrides-iterator (B020), + f-string-docstring (B021), + useless-contextlib-suppress (B022), + function-uses-loop-variable (B023), + duplicate-try-block-exception (B025), + star-arg-unpacking-after-keyword-arg (B026), + except-with-empty-tuple (B029), + except-with-non-exception-classes (B030), + reuse-of-groupby-generator (B031), + unintentional-type-annotation (B032), + duplicate-value (B033), + static-key-dict-comprehension (B035), + mutable-contextvar-default (B039), + unnecessary-generator-list (C400), + unnecessary-generator-set (C401), + unnecessary-generator-dict (C402), + unnecessary-list-comprehension-set (C403), + unnecessary-list-comprehension-dict (C404), + unnecessary-literal-set (C405), + unnecessary-literal-dict (C406), + unnecessary-collection-call (C408), + unnecessary-literal-within-tuple-call (C409), + unnecessary-literal-within-list-call (C410), + unnecessary-list-call (C411), + unnecessary-call-around-sorted (C413), + unnecessary-double-cast-or-process (C414), + unnecessary-subscript-reversal (C415), + unnecessary-map (C417), + unnecessary-literal-within-dict-call (C418), + unnecessary-comprehension-in-call (C419), + call-datetime-without-tzinfo (DTZ001), + call-datetime-today (DTZ002), + call-datetime-utcnow (DTZ003), + call-datetime-utcfromtimestamp (DTZ004), + call-datetime-now-without-tzinfo (DTZ005), + call-datetime-fromtimestamp (DTZ006), + call-datetime-strptime-without-zone (DTZ007), + call-date-today (DTZ011), + call-date-fromtimestamp (DTZ012), + datetime-min-max (DTZ901), + debugger (T100), + shebang-not-executable (EXE001), + shebang-missing-executable-file (EXE002), + shebang-leading-whitespace (EXE004), + shebang-not-first-line (EXE005), + future-rewritable-type-annotation (FA100), + future-required-type-annotation (FA102), + f-string-in-get-text-func-call (INT001), + format-in-get-text-func-call (INT002), + printf-in-get-text-func-call (INT003), + implicit-string-concatenation-in-collection-literal (ISC004), + direct-logger-instantiation (LOG001), + invalid-get-logger-argument (LOG002), + undocumented-warn (LOG009), + exc-info-outside-except-handler (LOG014), + root-logger-call (LOG015), + logging-warn (G010), + logging-extra-attr-clash (G101), + logging-exc-info (G201), + logging-redundant-exc-info (G202), + unnecessary-placeholder (PIE790), + duplicate-class-field-definition (PIE794), + non-unique-enums (PIE796), + unnecessary-spread (PIE800), + unnecessary-dict-kwargs (PIE804), + reimplemented-container-builtin (PIE807), + unnecessary-range-start (PIE808), + multiple-starts-ends-with (PIE810), + unprefixed-type-param (PYI001), + complex-if-statement-in-stub (PYI002), + unrecognized-version-info-check (PYI003), + patch-version-comparison (PYI004), + wrong-tuple-length-version-comparison (PYI005), + bad-version-info-comparison (PYI006), + unrecognized-platform-check (PYI007), + unrecognized-platform-name (PYI008), + pass-statement-stub-body (PYI009), + non-empty-stub-body (PYI010), + pass-in-class-body (PYI012), + ellipsis-in-non-empty-class-body (PYI013), + assignment-default-in-stub (PYI015), + duplicate-union-member (PYI016), + complex-assignment-in-stub (PYI017), + unused-private-type-var (PYI018), + custom-type-var-for-self (PYI019), + quoted-annotation-in-stub (PYI020), + unaliased-collections-abc-set-import (PYI025), + type-alias-without-annotation (PYI026), + str-or-repr-defined-in-stub (PYI029), + unnecessary-literal-union (PYI030), + any-eq-ne-annotation (PYI032), + legacy-type-comment (PYI033), + non-self-return-type (PYI034), + unassigned-special-variable-in-stub (PYI035), + bad-exit-annotation (PYI036), + redundant-numeric-union (PYI041), + snake-case-type-alias (PYI042), + t-suffixed-type-alias (PYI043), + future-annotations-in-stub (PYI044), + iter-method-return-iterable (PYI045), + unused-private-protocol (PYI046), + unused-private-type-alias (PYI047), + stub-body-multiple-statements (PYI048), + unused-private-typed-dict (PYI049), + no-return-argument-annotation-in-stub (PYI050), + unannotated-assignment-in-stub (PYI052), + unnecessary-type-union (PYI055), + byte-string-usage (PYI057), + generator-return-from-iter-method (PYI058), + generic-not-last-base-class (PYI059), + redundant-none-literal (PYI061), + duplicate-literal-member (PYI062), + pep484-style-positional-only-parameter (PYI063), + redundant-final-literal (PYI064), + bad-version-info-order (PYI066), + pytest-raises-without-exception (PT010), + pytest-duplicate-parametrize-test-cases (PT014), + pytest-deprecated-yield-fixture (PT020), + pytest-erroneous-use-fixtures-on-fixture (PT025), + pytest-use-fixtures-without-parameters (PT026), + pytest-warns-with-multiple-statements (PT031), + unnecessary-return-none (RET501), + duplicate-isinstance-call (SIM101), + collapsible-if (SIM102), + needless-bool (SIM103), + return-in-try-except-finally (SIM107), + enumerate-for-loop (SIM113), + if-with-same-arms (SIM114), + open-file-with-context-handler (SIM115), + multiple-with-statements (SIM117), + in-dict-keys (SIM118), + negate-equal-op (SIM201), + negate-not-equal-op (SIM202), + double-negation (SIM208), + if-expr-with-true-false (SIM210), + if-expr-with-false-true (SIM211), + expr-and-not-expr (SIM220), + expr-or-not-expr (SIM221), + expr-or-true (SIM222), + expr-and-false (SIM223), + if-else-block-instead-of-dict-get (SIM401), + split-static-string (SIM905), + zip-dict-keys-and-values (SIM911), + runtime-import-in-type-checking-block (TC004), + empty-type-checking-block (TC005), + unquoted-type-alias (TC007), + runtime-string-union (TC010), + py-path (PTH124), + invalid-pathlib-with-suffix (PTH210), + static-join-to-f-string (FLY002), + unsorted-imports (I001), + invalid-module-name (N999), + unnecessary-list-cast (PERF101), + incorrect-dict-iterator (PERF102), + manual-list-copy (PERF402), bare-except (E722), - lambda-assignment (E731), - ambiguous-variable-name (E741), - ambiguous-class-name (E742), - ambiguous-function-name (E743), io-error (E902), + invalid-escape-sequence (W605), + empty-docstring (D419), unused-import (F401), import-shadowed-by-loop-var (F402), - undefined-local-with-import-star (F403), late-future-import (F404), - undefined-local-with-import-star-usage (F405), - undefined-local-with-nested-import-star-usage (F406), future-feature-not-defined (F407), percent-format-invalid-format (F501), percent-format-expected-mapping (F502), @@ -120,7 +302,6 @@ linter.rules.enabled = [ yield-outside-function (F704), return-outside-function (F706), default-except-not-last (F707), - forward-annotation-syntax-error (F722), redefined-while-unused (F811), undefined-name (F821), undefined-export (F822), @@ -128,30 +309,385 @@ linter.rules.enabled = [ unused-variable (F841), unused-annotation (F842), raise-not-implemented (F901), + invalid-mock-access (PGH005), + type-name-incorrect-variance (PLC0105), + type-bivariance (PLC0131), + type-param-name-mismatch (PLC0132), + single-string-slots (PLC0205), + dict-index-missing-items (PLC0206), + iteration-over-set (PLC0208), + useless-import-alias (PLC0414), + unnecessary-direct-lambda-call (PLC3002), + yield-in-init (PLE0100), + return-in-init (PLE0101), + nonlocal-and-global (PLE0115), + continue-in-finally (PLE0116), + nonlocal-without-binding (PLE0117), + load-before-global-declaration (PLE0118), + invalid-length-return-type (PLE0303), + invalid-bool-return-type (PLE0304), + invalid-index-return-type (PLE0305), + invalid-str-return-type (PLE0307), + invalid-bytes-return-type (PLE0308), + invalid-hash-return-type (PLE0309), + invalid-all-object (PLE0604), + invalid-all-format (PLE0605), + potential-index-error (PLE0643), + misplaced-bare-raise (PLE0704), + repeated-keyword-argument (PLE1132), + await-outside-async (PLE1142), + logging-too-many-args (PLE1205), + logging-too-few-args (PLE1206), + bad-string-format-character (PLE1300), + bad-string-format-type (PLE1307), + bad-str-strip-call (PLE1310), + invalid-envvar-value (PLE1507), + singledispatch-method (PLE1519), + singledispatchmethod-function (PLE1520), + yield-from-in-async-function (PLE1700), + bidirectional-unicode (PLE2502), + invalid-character-backspace (PLE2510), + invalid-character-sub (PLE2512), + invalid-character-esc (PLE2513), + invalid-character-nul (PLE2514), + invalid-character-zero-width-space (PLE2515), + comparison-with-itself (PLR0124), + comparison-of-constant (PLR0133), + property-with-parameters (PLR0206), + manual-from-import (PLR0402), + redefined-argument-from-local (PLR1704), + stop-iteration-return (PLR1708), + useless-return (PLR1711), + boolean-chained-comparison (PLR1716), + sys-exit-alias (PLR1722), + if-stmt-min-max (PLR1730), + unnecessary-dict-index-lookup (PLR1733), + unnecessary-list-index-lookup (PLR1736), + empty-comment (PLR2044), + useless-else-on-loop (PLW0120), + self-assigning-variable (PLW0127), + redeclared-assigned-name (PLW0128), + assert-on-string-literal (PLW0129), + named-expr-without-context (PLW0131), + useless-exception-statement (PLW0133), + nan-comparison (PLW0177), + bad-staticmethod-argument (PLW0211), + super-without-brackets (PLW0245), + import-self (PLW0406), + global-variable-not-assigned (PLW0602), + global-at-module-level (PLW0604), + self-or-cls-assignment (PLW0642), + binary-op-exception (PLW0711), + bad-open-mode (PLW1501), + shallow-copy-environ (PLW1507), + invalid-envvar-default (PLW1508), + subprocess-popen-preexec-fn (PLW1509), + subprocess-run-without-check (PLW1510), + useless-with-lock (PLW2101), + useless-metaclass-type (UP001), + type-of-primitive (UP003), + useless-object-inheritance (UP004), + deprecated-unittest-alias (UP005), + non-pep585-annotation (UP006), + non-pep604-annotation-union (UP007), + super-call-with-parameters (UP008), + utf8-encoding-declaration (UP009), + unnecessary-future-import (UP010), + lru-cache-without-parameters (UP011), + unnecessary-encode-utf8 (UP012), + convert-named-tuple-functional-to-class (UP014), + datetime-timezone-utc (UP017), + native-literals (UP018), + typing-text-str-alias (UP019), + open-alias (UP020), + replace-universal-newlines (UP021), + replace-stdout-stderr (UP022), + deprecated-c-element-tree (UP023), + os-error-alias (UP024), + unicode-kind-prefix (UP025), + deprecated-mock-import (UP026), + yield-in-for-loop (UP028), + unnecessary-builtin-import (UP029), + format-literals (UP030), + printf-string-formatting (UP031), + f-string (UP032), + lru-cache-with-maxsize-none (UP033), + extraneous-parentheses (UP034), + deprecated-import (UP035), + outdated-version-block (UP036), + quoted-annotation (UP037), + unnecessary-class-parentheses (UP039), + non-pep695-type-alias (UP040), + timeout-error-alias (UP041), + unnecessary-default-type-args (UP043), + non-pep646-unpack (UP044), + non-pep604-annotation-optional (UP045), + non-pep695-generic-class (UP046), + non-pep695-generic-function (UP047), + private-type-parameter (UP049), + useless-class-metaclass-type (UP050), + print-empty-string (FURB105), + for-loop-writes (FURB122), + readlines-in-for (FURB129), + check-and-remove-from-set (FURB132), + if-expr-min-max (FURB136), + verbose-decimal-constructor (FURB157), + bit-count (FURB161), + fromisoformat-replace-z (FURB162), + redundant-log-base (FURB163), + int-on-sliced-str (FURB166), + regex-flag-alias (FURB167), + isinstance-type-none (FURB168), + type-none-comparison (FURB169), + implicit-cwd (FURB177), + hashlib-digest-hex (FURB181), + slice-to-remove-prefix-or-suffix (FURB188), + sorted-min-max (FURB192), + zip-instead-of-pairwise (RUF007), + mutable-dataclass-default (RUF008), + function-call-in-dataclass-default-argument (RUF009), + explicit-f-string-type-conversion (RUF010), + mutable-class-default (RUF012), + implicit-optional (RUF013), + unnecessary-iterable-allocation-for-first-element (RUF015), + invalid-index-type (RUF016), + quadratic-list-summation (RUF017), + assignment-in-assert (RUF018), + unnecessary-key-check (RUF019), + never-union (RUF020), + unsorted-dunder-all (RUF022), + unsorted-dunder-slots (RUF023), + mutable-fromkeys-value (RUF024), + default-factory-kwarg (RUF026), + invalid-formatter-suppression-comment (RUF028), + assert-with-print-message (RUF030), + decimal-from-float-literal (RUF032), + post-init-default (RUF033), + useless-if-else (RUF034), + invalid-assert-message-literal-argument (RUF040), + unnecessary-nested-literal (RUF041), + unnecessary-cast-to-int (RUF046), + map-int-version-parsing (RUF048), + dataclass-enum (RUF049), + if-key-in-dict-del (RUF051), + class-with-mixed-type-vars (RUF053), + unnecessary-round (RUF057), + starmap-zip (RUF058), + unused-unpacked-variable (RUF059), + access-annotations-from-class-dict (RUF063), + duplicate-entry-in-dunder-all (RUF068), + unused-noqa (RUF100), + redirected-noqa (RUF101), + invalid-pyproject-toml (RUF200), + raise-vanilla-class (TRY002), + type-check-without-type-error (TRY004), + verbose-raise (TRY201), + useless-try-except (TRY203), + verbose-log-message (TRY401), ] linter.rules.should_fix = [ - multiple-imports-on-one-line (E401), - module-import-not-at-top-of-file (E402), - multiple-statements-on-one-line-colon (E701), - multiple-statements-on-one-line-semicolon (E702), - useless-semicolon (E703), - none-comparison (E711), - true-false-comparison (E712), - not-in-test (E713), - not-is-test (E714), - type-comparison (E721), + sys-version-slice3 (YTT101), + sys-version2 (YTT102), + sys-version-cmp-str3 (YTT103), + sys-version-info0-eq3 (YTT201), + six-py3 (YTT202), + sys-version-info1-cmp-int (YTT203), + sys-version-info-minor-cmp-int (YTT204), + sys-version0 (YTT301), + sys-version-cmp-str10 (YTT302), + sys-version-slice1 (YTT303), + cancel-scope-no-checkpoint (ASYNC100), + trio-sync-call (ASYNC105), + async-zero-sleep (ASYNC115), + long-sleep-not-forever (ASYNC116), + blocking-http-call-in-async-function (ASYNC210), + create-subprocess-in-async-function (ASYNC220), + run-process-in-async-function (ASYNC221), + wait-for-process-in-async-function (ASYNC222), + blocking-open-call-in-async-function (ASYNC230), + blocking-sleep-in-async-function (ASYNC251), + exec-builtin (S102), + try-except-pass (S110), + try-except-continue (S112), + blind-except (BLE001), + unary-prefix-increment-decrement (B002), + assignment-to-os-environ (B003), + unreliable-callable-check (B004), + strip-with-multi-characters (B005), + mutable-argument-default (B006), + function-call-in-default-argument (B008), + get-attr-with-constant (B009), + set-attr-with-constant (B010), + jump-statement-in-finally (B012), + redundant-tuple-in-exception-handler (B013), + duplicate-handler-exception (B014), + useless-comparison (B015), + raise-literal (B016), + assert-raises-exception (B017), + useless-expression (B018), + cached-instance-method (B019), + loop-variable-overrides-iterator (B020), + f-string-docstring (B021), + useless-contextlib-suppress (B022), + function-uses-loop-variable (B023), + duplicate-try-block-exception (B025), + star-arg-unpacking-after-keyword-arg (B026), + except-with-empty-tuple (B029), + except-with-non-exception-classes (B030), + reuse-of-groupby-generator (B031), + unintentional-type-annotation (B032), + duplicate-value (B033), + static-key-dict-comprehension (B035), + mutable-contextvar-default (B039), + unnecessary-generator-list (C400), + unnecessary-generator-set (C401), + unnecessary-generator-dict (C402), + unnecessary-list-comprehension-set (C403), + unnecessary-list-comprehension-dict (C404), + unnecessary-literal-set (C405), + unnecessary-literal-dict (C406), + unnecessary-collection-call (C408), + unnecessary-literal-within-tuple-call (C409), + unnecessary-literal-within-list-call (C410), + unnecessary-list-call (C411), + unnecessary-call-around-sorted (C413), + unnecessary-double-cast-or-process (C414), + unnecessary-subscript-reversal (C415), + unnecessary-map (C417), + unnecessary-literal-within-dict-call (C418), + unnecessary-comprehension-in-call (C419), + call-datetime-without-tzinfo (DTZ001), + call-datetime-today (DTZ002), + call-datetime-utcnow (DTZ003), + call-datetime-utcfromtimestamp (DTZ004), + call-datetime-now-without-tzinfo (DTZ005), + call-datetime-fromtimestamp (DTZ006), + call-datetime-strptime-without-zone (DTZ007), + call-date-today (DTZ011), + call-date-fromtimestamp (DTZ012), + datetime-min-max (DTZ901), + debugger (T100), + shebang-not-executable (EXE001), + shebang-missing-executable-file (EXE002), + shebang-leading-whitespace (EXE004), + shebang-not-first-line (EXE005), + future-rewritable-type-annotation (FA100), + future-required-type-annotation (FA102), + f-string-in-get-text-func-call (INT001), + format-in-get-text-func-call (INT002), + printf-in-get-text-func-call (INT003), + implicit-string-concatenation-in-collection-literal (ISC004), + direct-logger-instantiation (LOG001), + invalid-get-logger-argument (LOG002), + undocumented-warn (LOG009), + exc-info-outside-except-handler (LOG014), + root-logger-call (LOG015), + logging-warn (G010), + logging-extra-attr-clash (G101), + logging-exc-info (G201), + logging-redundant-exc-info (G202), + unnecessary-placeholder (PIE790), + duplicate-class-field-definition (PIE794), + non-unique-enums (PIE796), + unnecessary-spread (PIE800), + unnecessary-dict-kwargs (PIE804), + reimplemented-container-builtin (PIE807), + unnecessary-range-start (PIE808), + multiple-starts-ends-with (PIE810), + unprefixed-type-param (PYI001), + complex-if-statement-in-stub (PYI002), + unrecognized-version-info-check (PYI003), + patch-version-comparison (PYI004), + wrong-tuple-length-version-comparison (PYI005), + bad-version-info-comparison (PYI006), + unrecognized-platform-check (PYI007), + unrecognized-platform-name (PYI008), + pass-statement-stub-body (PYI009), + non-empty-stub-body (PYI010), + pass-in-class-body (PYI012), + ellipsis-in-non-empty-class-body (PYI013), + assignment-default-in-stub (PYI015), + duplicate-union-member (PYI016), + complex-assignment-in-stub (PYI017), + unused-private-type-var (PYI018), + custom-type-var-for-self (PYI019), + quoted-annotation-in-stub (PYI020), + unaliased-collections-abc-set-import (PYI025), + type-alias-without-annotation (PYI026), + str-or-repr-defined-in-stub (PYI029), + unnecessary-literal-union (PYI030), + any-eq-ne-annotation (PYI032), + legacy-type-comment (PYI033), + non-self-return-type (PYI034), + unassigned-special-variable-in-stub (PYI035), + bad-exit-annotation (PYI036), + redundant-numeric-union (PYI041), + snake-case-type-alias (PYI042), + t-suffixed-type-alias (PYI043), + future-annotations-in-stub (PYI044), + iter-method-return-iterable (PYI045), + unused-private-protocol (PYI046), + unused-private-type-alias (PYI047), + stub-body-multiple-statements (PYI048), + unused-private-typed-dict (PYI049), + no-return-argument-annotation-in-stub (PYI050), + unannotated-assignment-in-stub (PYI052), + unnecessary-type-union (PYI055), + byte-string-usage (PYI057), + generator-return-from-iter-method (PYI058), + generic-not-last-base-class (PYI059), + redundant-none-literal (PYI061), + duplicate-literal-member (PYI062), + pep484-style-positional-only-parameter (PYI063), + redundant-final-literal (PYI064), + bad-version-info-order (PYI066), + pytest-raises-without-exception (PT010), + pytest-duplicate-parametrize-test-cases (PT014), + pytest-deprecated-yield-fixture (PT020), + pytest-erroneous-use-fixtures-on-fixture (PT025), + pytest-use-fixtures-without-parameters (PT026), + pytest-warns-with-multiple-statements (PT031), + unnecessary-return-none (RET501), + duplicate-isinstance-call (SIM101), + collapsible-if (SIM102), + needless-bool (SIM103), + return-in-try-except-finally (SIM107), + enumerate-for-loop (SIM113), + if-with-same-arms (SIM114), + open-file-with-context-handler (SIM115), + multiple-with-statements (SIM117), + in-dict-keys (SIM118), + negate-equal-op (SIM201), + negate-not-equal-op (SIM202), + double-negation (SIM208), + if-expr-with-true-false (SIM210), + if-expr-with-false-true (SIM211), + expr-and-not-expr (SIM220), + expr-or-not-expr (SIM221), + expr-or-true (SIM222), + expr-and-false (SIM223), + if-else-block-instead-of-dict-get (SIM401), + split-static-string (SIM905), + zip-dict-keys-and-values (SIM911), + runtime-import-in-type-checking-block (TC004), + empty-type-checking-block (TC005), + unquoted-type-alias (TC007), + runtime-string-union (TC010), + py-path (PTH124), + invalid-pathlib-with-suffix (PTH210), + static-join-to-f-string (FLY002), + unsorted-imports (I001), + invalid-module-name (N999), + unnecessary-list-cast (PERF101), + incorrect-dict-iterator (PERF102), + manual-list-copy (PERF402), bare-except (E722), - lambda-assignment (E731), - ambiguous-variable-name (E741), - ambiguous-class-name (E742), - ambiguous-function-name (E743), io-error (E902), + invalid-escape-sequence (W605), + empty-docstring (D419), unused-import (F401), import-shadowed-by-loop-var (F402), - undefined-local-with-import-star (F403), late-future-import (F404), - undefined-local-with-import-star-usage (F405), - undefined-local-with-nested-import-star-usage (F406), future-feature-not-defined (F407), percent-format-invalid-format (F501), percent-format-expected-mapping (F502), @@ -181,7 +717,6 @@ linter.rules.should_fix = [ yield-outside-function (F704), return-outside-function (F706), default-except-not-last (F707), - forward-annotation-syntax-error (F722), redefined-while-unused (F811), undefined-name (F821), undefined-export (F822), @@ -189,6 +724,181 @@ linter.rules.should_fix = [ unused-variable (F841), unused-annotation (F842), raise-not-implemented (F901), + invalid-mock-access (PGH005), + type-name-incorrect-variance (PLC0105), + type-bivariance (PLC0131), + type-param-name-mismatch (PLC0132), + single-string-slots (PLC0205), + dict-index-missing-items (PLC0206), + iteration-over-set (PLC0208), + useless-import-alias (PLC0414), + unnecessary-direct-lambda-call (PLC3002), + yield-in-init (PLE0100), + return-in-init (PLE0101), + nonlocal-and-global (PLE0115), + continue-in-finally (PLE0116), + nonlocal-without-binding (PLE0117), + load-before-global-declaration (PLE0118), + invalid-length-return-type (PLE0303), + invalid-bool-return-type (PLE0304), + invalid-index-return-type (PLE0305), + invalid-str-return-type (PLE0307), + invalid-bytes-return-type (PLE0308), + invalid-hash-return-type (PLE0309), + invalid-all-object (PLE0604), + invalid-all-format (PLE0605), + potential-index-error (PLE0643), + misplaced-bare-raise (PLE0704), + repeated-keyword-argument (PLE1132), + await-outside-async (PLE1142), + logging-too-many-args (PLE1205), + logging-too-few-args (PLE1206), + bad-string-format-character (PLE1300), + bad-string-format-type (PLE1307), + bad-str-strip-call (PLE1310), + invalid-envvar-value (PLE1507), + singledispatch-method (PLE1519), + singledispatchmethod-function (PLE1520), + yield-from-in-async-function (PLE1700), + bidirectional-unicode (PLE2502), + invalid-character-backspace (PLE2510), + invalid-character-sub (PLE2512), + invalid-character-esc (PLE2513), + invalid-character-nul (PLE2514), + invalid-character-zero-width-space (PLE2515), + comparison-with-itself (PLR0124), + comparison-of-constant (PLR0133), + property-with-parameters (PLR0206), + manual-from-import (PLR0402), + redefined-argument-from-local (PLR1704), + stop-iteration-return (PLR1708), + useless-return (PLR1711), + boolean-chained-comparison (PLR1716), + sys-exit-alias (PLR1722), + if-stmt-min-max (PLR1730), + unnecessary-dict-index-lookup (PLR1733), + unnecessary-list-index-lookup (PLR1736), + empty-comment (PLR2044), + useless-else-on-loop (PLW0120), + self-assigning-variable (PLW0127), + redeclared-assigned-name (PLW0128), + assert-on-string-literal (PLW0129), + named-expr-without-context (PLW0131), + useless-exception-statement (PLW0133), + nan-comparison (PLW0177), + bad-staticmethod-argument (PLW0211), + super-without-brackets (PLW0245), + import-self (PLW0406), + global-variable-not-assigned (PLW0602), + global-at-module-level (PLW0604), + self-or-cls-assignment (PLW0642), + binary-op-exception (PLW0711), + bad-open-mode (PLW1501), + shallow-copy-environ (PLW1507), + invalid-envvar-default (PLW1508), + subprocess-popen-preexec-fn (PLW1509), + subprocess-run-without-check (PLW1510), + useless-with-lock (PLW2101), + useless-metaclass-type (UP001), + type-of-primitive (UP003), + useless-object-inheritance (UP004), + deprecated-unittest-alias (UP005), + non-pep585-annotation (UP006), + non-pep604-annotation-union (UP007), + super-call-with-parameters (UP008), + utf8-encoding-declaration (UP009), + unnecessary-future-import (UP010), + lru-cache-without-parameters (UP011), + unnecessary-encode-utf8 (UP012), + convert-named-tuple-functional-to-class (UP014), + datetime-timezone-utc (UP017), + native-literals (UP018), + typing-text-str-alias (UP019), + open-alias (UP020), + replace-universal-newlines (UP021), + replace-stdout-stderr (UP022), + deprecated-c-element-tree (UP023), + os-error-alias (UP024), + unicode-kind-prefix (UP025), + deprecated-mock-import (UP026), + yield-in-for-loop (UP028), + unnecessary-builtin-import (UP029), + format-literals (UP030), + printf-string-formatting (UP031), + f-string (UP032), + lru-cache-with-maxsize-none (UP033), + extraneous-parentheses (UP034), + deprecated-import (UP035), + outdated-version-block (UP036), + quoted-annotation (UP037), + unnecessary-class-parentheses (UP039), + non-pep695-type-alias (UP040), + timeout-error-alias (UP041), + unnecessary-default-type-args (UP043), + non-pep646-unpack (UP044), + non-pep604-annotation-optional (UP045), + non-pep695-generic-class (UP046), + non-pep695-generic-function (UP047), + private-type-parameter (UP049), + useless-class-metaclass-type (UP050), + print-empty-string (FURB105), + for-loop-writes (FURB122), + readlines-in-for (FURB129), + check-and-remove-from-set (FURB132), + if-expr-min-max (FURB136), + verbose-decimal-constructor (FURB157), + bit-count (FURB161), + fromisoformat-replace-z (FURB162), + redundant-log-base (FURB163), + int-on-sliced-str (FURB166), + regex-flag-alias (FURB167), + isinstance-type-none (FURB168), + type-none-comparison (FURB169), + implicit-cwd (FURB177), + hashlib-digest-hex (FURB181), + slice-to-remove-prefix-or-suffix (FURB188), + sorted-min-max (FURB192), + zip-instead-of-pairwise (RUF007), + mutable-dataclass-default (RUF008), + function-call-in-dataclass-default-argument (RUF009), + explicit-f-string-type-conversion (RUF010), + mutable-class-default (RUF012), + implicit-optional (RUF013), + unnecessary-iterable-allocation-for-first-element (RUF015), + invalid-index-type (RUF016), + quadratic-list-summation (RUF017), + assignment-in-assert (RUF018), + unnecessary-key-check (RUF019), + never-union (RUF020), + unsorted-dunder-all (RUF022), + unsorted-dunder-slots (RUF023), + mutable-fromkeys-value (RUF024), + default-factory-kwarg (RUF026), + invalid-formatter-suppression-comment (RUF028), + assert-with-print-message (RUF030), + decimal-from-float-literal (RUF032), + post-init-default (RUF033), + useless-if-else (RUF034), + invalid-assert-message-literal-argument (RUF040), + unnecessary-nested-literal (RUF041), + unnecessary-cast-to-int (RUF046), + map-int-version-parsing (RUF048), + dataclass-enum (RUF049), + if-key-in-dict-del (RUF051), + class-with-mixed-type-vars (RUF053), + unnecessary-round (RUF057), + starmap-zip (RUF058), + unused-unpacked-variable (RUF059), + access-annotations-from-class-dict (RUF063), + duplicate-entry-in-dunder-all (RUF068), + unused-noqa (RUF100), + redirected-noqa (RUF101), + invalid-pyproject-toml (RUF200), + raise-vanilla-class (TRY002), + type-check-without-type-error (TRY004), + verbose-raise (TRY201), + useless-try-except (TRY203), + verbose-log-message (TRY401), ] linter.per_file_ignores = {} linter.safety_table.forced_safe = [] diff --git a/crates/ruff/tests/cli/snapshots/cli__show_settings__display_settings_from_nested_directory.snap b/crates/ruff/tests/cli/snapshots/cli__show_settings__display_settings_from_nested_directory.snap index 7e2f8565b4..ad6d84c9e1 100644 --- a/crates/ruff/tests/cli/snapshots/cli__show_settings__display_settings_from_nested_directory.snap +++ b/crates/ruff/tests/cli/snapshots/cli__show_settings__display_settings_from_nested_directory.snap @@ -18,6 +18,7 @@ cache_dir = "[TMP]/subdir/.ruff_cache" fix = false fix_only = false output_format = full +output_prefer_rule_codes = false show_fixes = false unsafe_fixes = hint @@ -60,6 +61,7 @@ file_resolver.include = [ "**/pyproject.toml", "**/ruff.toml", "**/.ruff.toml", + "*.md", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/config.rs b/crates/ruff/tests/config.rs index 09cd474485..737aa9d23d 100644 --- a/crates/ruff/tests/config.rs +++ b/crates/ruff/tests/config.rs @@ -21,12 +21,12 @@ fn lint_select() { specific prefixes. `ignore` takes precedence over `select` if the same prefix appears in both. - Default value: ["E4", "E7", "E9", "F"] + Default value: See https://docs.astral.sh/ruff/default-rules/ or run `ruff check --show-settings --isolated` Type: list[RuleSelector] Example usage: ```toml - # On top of the defaults (`E4`, E7`, `E9`, and `F`), enable flake8-bugbear (`B`) and flake8-quotes (`Q`). - select = ["E4", "E7", "E9", "F", "B", "Q"] + # On top of the defaults, enable flake8-bugbear (`B`) and flake8-quotes (`Q`). + extend-select = ["B", "Q"] ``` ----- stderr ----- @@ -43,10 +43,10 @@ fn lint_select_json() { ----- stdout ----- { "doc": "A list of rule codes or prefixes to enable. Prefixes can specify exact\nrules (like `F841`), entire categories (like `F`), or anything in\nbetween.\n\nWhen breaking ties between enabled and disabled rules (via `select` and\n`ignore`, respectively), more specific prefixes override less\nspecific prefixes. `ignore` takes precedence over `select` if the\nsame prefix appears in both.", - "default": "[\"E4\", \"E7\", \"E9\", \"F\"]", + "default": "See https://docs.astral.sh/ruff/default-rules/ or run `ruff check --show-settings --isolated`", "value_type": "list[RuleSelector]", "scope": null, - "example": "# On top of the defaults (`E4`, E7`, `E9`, and `F`), enable flake8-bugbear (`B`) and flake8-quotes (`Q`).\nselect = [\"E4\", \"E7\", \"E9\", \"F\", \"B\", \"Q\"]", + "example": "# On top of the defaults, enable flake8-bugbear (`B`) and flake8-quotes (`Q`).\nextend-select = [\"B\", \"Q\"]", "deprecated": null } diff --git a/crates/ruff/tests/integration_test.rs b/crates/ruff/tests/integration_test.rs index 61568825af..88645b21ab 100644 --- a/crates/ruff/tests/integration_test.rs +++ b/crates/ruff/tests/integration_test.rs @@ -120,8 +120,10 @@ fn stdin_error() { | 1 | import os | ^^ - | help: Remove unused import: `os` + | + - import os + | Found 1 error. [*] 1 fixable with the `--fix` option. @@ -145,8 +147,10 @@ fn stdin_filename() { | 1 | import os | ^^ - | help: Remove unused import: `os` + | + - import os + | Found 1 error. [*] 1 fixable with the `--fix` option. @@ -181,16 +185,22 @@ import bar # unused import | 2 | import bar # unused import | ^^^ - | help: Remove unused import: `bar` + | + 1 | + - import bar # unused import + | F401 [*] `foo` imported but unused --> foo.py:2:8 | 2 | import foo # unused import | ^^^ - | help: Remove unused import: `foo` + | + 1 | + - import foo # unused import + | Found 2 errors. [*] 2 fixable with the `--fix` option. @@ -217,8 +227,10 @@ fn check_warn_stdin_filename_with_files() { | 1 | import os | ^^ - | help: Remove unused import: `os` + | + - import os + | Found 1 error. [*] 1 fixable with the `--fix` option. @@ -244,8 +256,10 @@ fn stdin_source_type_py() { | 1 | import os | ^^ - | help: Remove unused import: `os` + | + - import os + | Found 1 error. [*] 1 fixable with the `--fix` option. @@ -482,7 +496,6 @@ fn stdin_fix_jupyter() { | 1 | print(x) | ^ - | Found 3 errors (2 fixed, 1 remaining). "#); @@ -581,16 +594,22 @@ fn stdin_override_parser_ipynb() { | 1 | import os | ^^ - | help: Remove unused import: `os` + ::: cell 1 + | + - import os + | F401 [*] `sys` imported but unused --> Jupyter.py:cell 3:1:8 | 1 | import sys | ^^^ - | help: Remove unused import: `sys` + ::: cell 3 + | + - import sys + | Found 2 errors. [*] 2 fixable with the `--fix` option. @@ -619,8 +638,10 @@ fn stdin_override_parser_py() { | 1 | import os | ^^ - | help: Remove unused import: `os` + | + - import os + | Found 1 error. [*] 1 fixable with the `--fix` option. @@ -654,8 +675,10 @@ extension = {ipynb="python"} | 1 | import os | ^^ - | help: Remove unused import: `os` + | + - import os + | Found 1 error. [*] 1 fixable with the `--fix` option. @@ -850,7 +873,6 @@ fn stdin_parse_error() { | 1 | from foo import | ^ - | Found 1 error. @@ -880,7 +902,6 @@ fn stdin_multiple_parse_error() { 1 | from foo import 2 | bar = | ^ - | Found 2 errors. @@ -902,7 +923,6 @@ fn parse_error_not_included() { | 1 | foo = | ^ - | Found 1 error. @@ -925,7 +945,6 @@ fn full_output_preview() { | 1 | l = 1 | ^ - | Found 1 error. @@ -954,7 +973,6 @@ preview = true | 1 | l = 1 | ^ - | Found 1 error. @@ -965,7 +983,10 @@ preview = true #[test] fn full_output_format() { - let mut cmd = RuffCheck::default().output_format("full").build(); + let mut cmd = RuffCheck::default() + .output_format("full") + .args(["--select=E741"]) + .build(); assert_cmd_snapshot!(cmd .pass_stdin("l = 1"), @" success: false @@ -976,7 +997,6 @@ fn full_output_format() { | 1 | l = 1 | ^ - | Found 1 error. @@ -1833,8 +1853,10 @@ fn check_input_from_argfile() -> Result<()> { | 1 | import os | ^^ - | help: Remove unused import: `os` + | + - import os + | Found 1 error. [*] 1 fixable with the `--fix` option. @@ -1878,6 +1900,9 @@ fn check_hints_hidden_unsafe_fixes() { ----- stdout ----- RUF901 [*] Hey this is a stable test rule with a safe fix. --> -:1:1 + | + 1 + # fix from stable-test-rule-safe-fix + | RUF902 Hey this is a stable test rule with an unsafe fix. --> -:1:1 @@ -1920,6 +1945,9 @@ fn check_no_hint_for_hidden_unsafe_fixes_when_disabled() { ----- stdout ----- RUF901 [*] Hey this is a stable test rule with a safe fix. --> -:1:1 + | + 1 + # fix from stable-test-rule-safe-fix + | RUF902 Hey this is a stable test rule with an unsafe fix. --> -:1:1 @@ -1963,9 +1991,16 @@ fn check_shows_unsafe_fixes_with_opt_in() { ----- stdout ----- RUF901 [*] Hey this is a stable test rule with a safe fix. --> -:1:1 + | + 1 + # fix from stable-test-rule-safe-fix + | RUF902 [*] Hey this is a stable test rule with an unsafe fix. --> -:1:1 + | + 1 + # fix from stable-test-rule-unsafe-fix + | + note: This is an unsafe fix and may change runtime behavior Found 2 errors. [*] 2 fixable with the `--fix` option. @@ -2241,9 +2276,15 @@ extend-safe-fixes = ["RUF902"] ----- stdout ----- RUF901 [*] Hey this is a stable test rule with a safe fix. --> -:1:1 + | + 1 + # fix from stable-test-rule-safe-fix + | RUF902 [*] Hey this is a stable test rule with an unsafe fix. --> -:1:1 + | + 1 + # fix from stable-test-rule-unsafe-fix + | Found 2 errors. [*] 2 fixable with the `--fix` option. @@ -2279,6 +2320,9 @@ extend-safe-fixes = ["RUF902"] ----- stdout ----- RUF901 [*] Hey this is a stable test rule with a safe fix. --> -:1:1 + | + 1 + # fix from stable-test-rule-safe-fix + | RUF902 Hey this is a stable test rule with an unsafe fix. --> -:1:1 @@ -2325,6 +2369,10 @@ extend-safe-fixes = ["RUF9"] RUF902 [*] Hey this is a stable test rule with an unsafe fix. --> -:1:1 + | + 1 + # fix from stable-test-rule-unsafe-fix + 2 | x = {'a': 1, 'a': 1} + | RUF903 Hey this is a stable test rule with a display only fix. --> -:1:1 @@ -2436,7 +2484,6 @@ select = ["RUF017"] 2 | y = [4, 5, 6] 3 | sum([x, y], []) | ^^^^^^^^^^^^^^^ - | help: Replace with `functools.reduce` Found 1 error. @@ -2477,7 +2524,6 @@ unfixable = ["RUF"] 2 | y = [4, 5, 6] 3 | sum([x, y], []) | ^^^^^^^^^^^^^^^ - | help: Replace with `functools.reduce` Found 1 error. @@ -2505,7 +2551,6 @@ fn pyproject_toml_stdin_syntax_error() { | 1 | [project | ^ - | Found 1 error. @@ -2532,7 +2577,6 @@ fn pyproject_toml_stdin_schema_error() { 1 | [project] 2 | name = 1 | ^ - | Found 1 error. @@ -2544,7 +2588,7 @@ fn pyproject_toml_stdin_schema_error() { #[test] fn pyproject_toml_stdin_no_applicable_rules_selected() { let mut cmd = RuffCheck::default() - .args(["--stdin-filename", "pyproject.toml"]) + .args(["--stdin-filename", "pyproject.toml", "--ignore=RUF200"]) .build(); assert_cmd_snapshot!( @@ -2586,7 +2630,7 @@ fn pyproject_toml_stdin_no_errors() { .build(); assert_cmd_snapshot!( - cmd.pass_stdin(r#"[project]\nname = "ruff"\nversion = "0.0.0""#), + cmd.pass_stdin("[project]\nname = 'ruff'\nversion = '0.0.0'"), @" success: true exit_code: 0 @@ -2625,7 +2669,6 @@ fn pyproject_toml_stdin_schema_error_fix() { 1 | [project] 2 | name = 1 | ^ - | Found 1 error. " diff --git a/crates/ruff_annotate_snippets/Cargo.toml b/crates/ruff_annotate_snippets/Cargo.toml index d40beddeec..888ee9b3d9 100644 --- a/crates/ruff_annotate_snippets/Cargo.toml +++ b/crates/ruff_annotate_snippets/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_annotate_snippets" -version = "0.0.5" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } @@ -12,28 +12,91 @@ license = "MIT OR Apache-2.0" [dependencies] anstyle = { workspace = true } -memchr = { workspace = true } +memchr = { workspace = true, optional = true } unicode-width = { workspace = true } [dev-dependencies] ruff_annotate_snippets = { path = ".", features = ["testing-colors"] } - anstream = { workspace = true } -serde = { workspace = true, features = ["derive"] } -snapbox = { workspace = true, features = ["diff", "term-svg", "cmd", "examples"] } -toml = { workspace = true } -tryfn = { workspace = true } +snapbox = { workspace = true } [features] -default = [] +default = ["std", "simd"] +std = ["anstyle/std", "memchr?/std"] +simd = ["dep:memchr"] testing-colors = [] -[[test]] -name = "fixtures" -harness = false +# Using upstream lints +[lints.rust] +rust_2018_idioms = { level = "warn", priority = -1 } +unnameable_types = "warn" +unreachable_pub = "warn" +unsafe_op_in_unsafe_fn = "warn" +unused_lifetimes = "warn" +unused_macro_rules = "warn" +unused_qualifications = "warn" -[lints] -workspace = true +[lints.clippy] +disallowed_methods = "allow" # HACK: minimize changes from upstream +bool_assert_comparison = "allow" +branches_sharing_code = "allow" +checked_conversions = "warn" +collapsible_else_if = "allow" +create_dir = "warn" +dbg_macro = "warn" +debug_assert_with_mut_call = "warn" +doc_markdown = "warn" +empty_enums = "warn" +enum_glob_use = "warn" +expl_impl_clone_on_copy = "warn" +explicit_deref_methods = "warn" +explicit_into_iter_loop = "warn" +fallible_impl_from = "warn" +filter_map_next = "warn" +flat_map_option = "warn" +float_cmp_const = "warn" +fn_params_excessive_bools = "warn" +from_iter_instead_of_collect = "warn" +if_same_then_else = "allow" +implicit_clone = "warn" +imprecise_flops = "warn" +inconsistent_struct_constructor = "warn" +inefficient_to_string = "warn" +infinite_loop = "warn" +invalid_upcast_comparisons = "warn" +large_digit_groups = "warn" +large_stack_arrays = "warn" +large_types_passed_by_value = "warn" +let_and_return = "allow" # sometimes good to name what you are returning +linkedlist = "warn" +lossy_float_literal = "warn" +macro_use_imports = "warn" +mem_forget = "warn" +mutex_integer = "warn" +needless_continue = "allow" +needless_for_each = "warn" +negative_feature_names = "warn" +path_buf_push_overwrite = "warn" +ptr_as_ptr = "warn" +rc_mutex = "warn" +redundant_feature_names = "warn" +ref_option_ref = "warn" +rest_pat_in_fully_bound_structs = "warn" +result_large_err = "allow" +same_functions_in_if_condition = "warn" +self_named_module_files = "warn" +semicolon_if_nothing_returned = "warn" +str_to_string = "warn" +string_add = "warn" +string_add_assign = "warn" +string_lit_as_bytes = "warn" +todo = "warn" +trait_duplication_in_bounds = "warn" +uninlined_format_args = "warn" +verbose_file_reads = "warn" +wildcard_imports = "warn" +zero_sized_map_values = "warn" [lib] +name = "annotate_snippets" test = false diff --git a/crates/ruff_annotate_snippets/examples/custom_error.rs b/crates/ruff_annotate_snippets/examples/custom_error.rs new file mode 100644 index 0000000000..1618d3f0cf --- /dev/null +++ b/crates/ruff_annotate_snippets/examples/custom_error.rs @@ -0,0 +1,32 @@ +use annotate_snippets::renderer::DecorStyle; +use annotate_snippets::{AnnotationKind, Level, Renderer, Snippet}; + +fn main() { + let source = r#"//@ compile-flags: -Ztreat-err-as-bug +//@ failure-status: 101 +//@ error-pattern: aborting due to `-Z treat-err-as-bug=1` +//@ error-pattern: [eval_static_initializer] evaluating initializer of static `C` +//@ normalize-stderr: "note: .*\n\n" -> "" +//@ normalize-stderr: "thread 'rustc' panicked.*:\n.*\n" -> "" +//@ rustc-env:RUST_BACKTRACE=0 + +#![crate_type = "rlib"] + +pub static C: u32 = 0 - 1; +//~^ ERROR could not evaluate static initializer +"#; + let report = &[Level::ERROR + .with_name(Some("error: internal compiler error")) + .primary_title("could not evaluate static initializer") + .id("E0080") + .element( + Snippet::source(source).path("$DIR/err.rs").annotation( + AnnotationKind::Primary + .span(386..391) + .label("attempt to compute `0_u32 - 1_u32`, which would overflow"), + ), + )]; + + let renderer = Renderer::styled().decor_style(DecorStyle::Unicode); + anstream::println!("{}", renderer.render(report)); +} diff --git a/crates/ruff_annotate_snippets/tests/fixtures/color/strip_line.svg b/crates/ruff_annotate_snippets/examples/custom_error.svg similarity index 57% rename from crates/ruff_annotate_snippets/tests/fixtures/color/strip_line.svg rename to crates/ruff_annotate_snippets/examples/custom_error.svg index 75709d703a..8c05a6c47a 100644 --- a/crates/ruff_annotate_snippets/tests/fixtures/color/strip_line.svg +++ b/crates/ruff_annotate_snippets/examples/custom_error.svg @@ -1,4 +1,4 @@ - +
+ /// + /// Text passed to this function is considered "untrusted input", as such + /// all text is passed through a normalization function. Styled text is + /// not allowed to be passed to this function. + /// + ///
+ pub fn primary_title(self, text: impl Into>) -> Title<'a> { + Title { + level: self, + id: None, + text: text.into(), + allows_styling: false, + is_fixable: false, + } + } + + /// For any secondary, or context, [`Group`][crate::Group]s (subsequent) in a [`Report`][crate::Report] + /// + /// See [`Group::with_title`][crate::Group::with_title] + /// + ///
+ /// + /// Text passed to this function is allowed to be styled, as such all + /// text is considered "trusted input" and has no normalizations applied to + /// it. [`normalize_untrusted_str`](crate::normalize_untrusted_str) can be + /// used to normalize untrusted text before it is passed to this function. + /// + ///
+ pub fn secondary_title(self, text: impl Into>) -> Title<'a> { + Title { + level: self, + id: None, + text: text.into(), + allows_styling: true, + is_fixable: false, + } + } + + /// A text [`Element`][crate::Element] in a [`Group`][crate::Group] + /// + ///
+ /// + /// Text passed to this function is allowed to be styled, as such all + /// text is considered "trusted input" and has no normalizations applied to + /// it. [`normalize_untrusted_str`](crate::normalize_untrusted_str) can be + /// used to normalize untrusted text before it is passed to this function. + /// + ///
+ pub fn message(self, text: impl Into>) -> Message<'a> { + Message { + level: self, + text: text.into(), + } + } + + pub(crate) fn as_str(&'a self) -> &'a str { + match (&self.name, self.level) { + (Some(Some(name)), _) => name.as_ref(), + (Some(None), _) => "", + (None, LevelInner::Error) => ERROR_TXT, + (None, LevelInner::Warning) => WARNING_TXT, + (None, LevelInner::Info) => INFO_TXT, + (None, LevelInner::Note) => NOTE_TXT, + (None, LevelInner::Help) => HELP_TXT, + } + } + + pub(crate) fn style(&self, stylesheet: &Stylesheet) -> Style { + self.level.style(stylesheet) + } +} + +/// # Customize the `Level` +impl<'a> Level<'a> { + /// Replace the name describing this [`Level`] + /// + ///
+ /// + /// Text passed to this function is considered "untrusted input", as such + /// all text is passed through a normalization function. Pre-styled text is + /// not allowed to be passed to this function. + /// + ///
+ /// + /// # Example + /// + /// ```rust + /// # #[allow(clippy::needless_doctest_main)] + #[doc = include_str!("../examples/custom_level.rs")] + /// ``` + #[doc = include_str!("../examples/custom_level.svg")] + pub fn with_name(self, name: impl Into>) -> Level<'a> { + Level { + name: Some(name.into().0), + level: self.level, + } + } + + /// Do not show the [`Level`]s name + /// + /// Useful for: + /// - Another layer of the application will include the level (e.g. when rendering errors) + /// - [`Message`]s that are part of a previous [`Group`][crate::Group] [`Element`][crate::Element]s + /// + /// # Example + /// + /// ```rust + /// # use annotate_snippets::{Group, Snippet, AnnotationKind, Level}; + ///let source = r#"fn main() { + /// let b: &[u8] = include_str!("file.txt"); //~ ERROR mismatched types + /// let s: &str = include_bytes!("file.txt"); //~ ERROR mismatched types + /// }"#; + /// let report = &[ + /// Level::ERROR.primary_title("mismatched types").id("E0308") + /// .element( + /// Snippet::source(source) + /// .path("$DIR/mismatched-types.rs") + /// .annotation( + /// AnnotationKind::Primary + /// .span(105..131) + /// .label("expected `&str`, found `&[u8; 0]`"), + /// ) + /// .annotation( + /// AnnotationKind::Context + /// .span(98..102) + /// .label("expected due to this"), + /// ), + /// ) + /// .element( + /// Level::NOTE + /// .no_name() + /// .message("expected reference `&str`\nfound reference `&'static [u8; 0]`"), + /// ), + /// ]; + /// ``` + pub fn no_name(self) -> Level<'a> { + self.with_name(None::<&str>) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub(crate) enum LevelInner { + Error, + Warning, + Info, + Note, + Help, +} + +impl LevelInner { + pub(crate) fn style(self, stylesheet: &Stylesheet) -> Style { + match self { + LevelInner::Error => stylesheet.error, + LevelInner::Warning => stylesheet.warning, + LevelInner::Info => stylesheet.info, + LevelInner::Note => stylesheet.note, + LevelInner::Help => stylesheet.help, + } + } +} diff --git a/crates/ruff_annotate_snippets/src/lib.rs b/crates/ruff_annotate_snippets/src/lib.rs index f8d0e1e5c6..e47083464a 100644 --- a/crates/ruff_annotate_snippets/src/lib.rs +++ b/crates/ruff_annotate_snippets/src/lib.rs @@ -1,35 +1,93 @@ -//! A library for formatting of text or programming code snippets. -//! -//! It's primary purpose is to build an ASCII-graphical representation of the snippet -//! with annotations. +//! Format [diagnostic reports][Report], including highlighting snippets of text //! //! # Example //! //! ```rust +//! # #[allow(clippy::needless_doctest_main)] #![doc = include_str!("../examples/expected_type.rs")] //! ``` //! #![doc = include_str!("../examples/expected_type.svg")] //! -//! The crate uses a three stage process with two conversions between states: +//! # Visual overview +//! +//! [`Report`] +//! +#![doc = include_str!("../examples/multi_suggestion.svg")] +//! +//! ### Primary group +//! +//! [`Title`] +//! ```text +//! error: cannot construct `Box<_, _>` with struct literal syntax due to private fields +//! ``` +//! +//! +//! [`Annotation`] on a [`Snippet`] +//! ```text +//! ╭▸ $DIR/multi-suggestion.rs:17:13 +//! │ +//! 17 │ let _ = Box {}; +//! │ ━━━ +//! │ +//! ``` +//! +//! [`Message`] +//! ```text +//! ╰ note: private fields `0` and `1` that were not provided +//! ``` +//! +//! +//! +//! ### Secondary group: suggested fix +//! +//! [`Title`] (proposed solution) +//! ```text +//! help: you might have meant to use an associated function to build this type +//! ``` +//! +//! [`Patch`] Option 1 on a [`Snippet`] +//! ```text +//! ╭╴ +//! 21 - let _ = Box {}; +//! 21 + let _ = Box::new(_); +//! ├╴ +//! ``` //! +//! [`Patch`] Option 2 on a [`Snippet`] //! ```text -//! Message --> Renderer --> impl Display +//! ├╴ +//! 17 - let _ = Box {}; +//! 17 + let _ = Box::new_uninit(); +//! ├╴ //! ``` //! -//! The input type - [Message] is a structure designed -//! to align with likely output from any parser whose code snippet is to be -//! annotated. +//! *etc for Options 3 and 4* +//! +//! [`Message`] +//! ```text +//! ╰ and 12 other candidates +//! ``` //! -//! The middle structure - [Renderer] is a structure designed -//! to convert a snippet into an internal structure that is designed to store -//! the snippet data in a way that is easy to format. -//! [Renderer] also handles the user-configurable formatting -//! options, such as color, or margins. +//! ### Secondary group: alternative suggested fix //! -//! Finally, `impl Display` into a final `String` output. +//! [`Title`] (proposed solution) +//! ```text +//! help: consider using the `Default` trait +//! ``` +//! +//! Only [`Patch`] on a [`Snippet`] +//! ```text +//! ╭╴ +//! 17 - let _ = Box {}; +//! 17 + let _ = ::default(); +//! ╰╴ +//! ``` +//! +//! # Cargo `features` +//! +//! - `simd` - Speeds up folding //! -//! # features //! - `testing-colors` - Makes [Renderer::styled] colors OS independent, which //! allows for easier testing when testing colored output. It should be added as //! a feature in `[dev-dependencies]`, which can be done with the following command: @@ -37,33 +95,36 @@ //! cargo add annotate-snippets --dev --feature testing-colors //! ``` -#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![cfg_attr(all(not(feature = "std"), not(test)), no_std)] +#![cfg_attr(docsrs, feature(doc_cfg))] #![warn(clippy::print_stderr)] #![warn(clippy::print_stdout)] +#![warn(clippy::std_instead_of_alloc)] +#![warn(clippy::std_instead_of_core)] #![warn(missing_debug_implementations)] -// Since this is a vendored copy of `annotate-snippets`, we squash Clippy -// warnings from upstream in order to the reduce the diff. If our copy drifts -// far from upstream such that patches become impractical to apply in both -// places, then we can get rid of these suppressions and fix the lints. -#![allow( - clippy::return_self_not_must_use, - clippy::cast_possible_truncation, - clippy::cast_precision_loss, - clippy::explicit_iter_loop, - clippy::unused_self, - clippy::unnecessary_wraps, - clippy::range_plus_one, - clippy::redundant_closure_for_method_calls, - clippy::struct_field_names, - clippy::cloned_instead_of_copied, - clippy::cast_sign_loss, - clippy::needless_as_bytes, - clippy::unnecessary_map_or -)] +extern crate alloc; + +use alloc::string::String; + +pub mod level; pub mod renderer; mod snippet; +/// Normalize the string to avoid any unicode control characters. +/// +/// This is important for untrusted input, as it can contain +/// invalid unicode sequences. +pub fn normalize_untrusted_str(s: &str) -> String { + renderer::normalize_whitespace(s).into_owned() +} + +#[doc(inline)] +pub use level::Level; #[doc(inline)] pub use renderer::Renderer; pub use snippet::*; + +#[doc = include_str!("../README.md")] +#[cfg(doctest)] +pub struct ReadmeDoctests; diff --git a/crates/ruff_annotate_snippets/src/renderer/display_list.rs b/crates/ruff_annotate_snippets/src/renderer/display_list.rs deleted file mode 100644 index 85396c0a30..0000000000 --- a/crates/ruff_annotate_snippets/src/renderer/display_list.rs +++ /dev/null @@ -1,1946 +0,0 @@ -//! `display_list` module stores the output model for the snippet. -//! -//! `DisplayList` is a central structure in the crate, which contains -//! the structured list of lines to be displayed. -//! -//! It is made of two types of lines: `Source` and `Raw`. All `Source` lines -//! are structured using four columns: -//! -//! ```text -//! /------------ (1) Line number column. -//! | /--------- (2) Line number column delimiter. -//! | | /------- (3) Inline marks column. -//! | | | /--- (4) Content column with the source and annotations for slices. -//! | | | | -//! ============================================================================= -//! error[E0308]: mismatched types -//! --> src/format.rs:51:5 -//! | -//! 151 | / fn test() -> String { -//! 152 | | return "test"; -//! 153 | | } -//! | |___^ error: expected `String`, for `&str`. -//! | -//! ``` -//! -//! The first two lines of the example above are `Raw` lines, while the rest -//! are `Source` lines. -//! -//! `DisplayList` does not store column alignment information, and those are -//! only calculated by the implementation of `std::fmt::Display` using information such as -//! styling. -//! -//! The above snippet has been built out of the following structure: -use crate::{Id, snippet}; -use std::borrow::Cow; -use std::cmp::{Reverse, max, min}; -use std::collections::HashMap; -use std::fmt::Display; -use std::ops::Range; -use std::{cmp, fmt}; - -use unicode_width::UnicodeWidthStr; - -use crate::renderer::styled_buffer::StyledBuffer; -use crate::renderer::{DEFAULT_TERM_WIDTH, Margin, Style, stylesheet::Stylesheet}; - -const ANONYMIZED_LINE_NUM: &str = "LL"; -const ERROR_TXT: &str = "error"; -const HELP_TXT: &str = "help"; -const INFO_TXT: &str = "info"; -const NOTE_TXT: &str = "note"; -const WARNING_TXT: &str = "warning"; - -/// List of lines to be displayed. -pub(crate) struct DisplayList<'a> { - pub(crate) body: Vec>, - pub(crate) stylesheet: &'a Stylesheet, - pub(crate) anonymized_line_numbers: bool, - pub(crate) cut_indicator: &'static str, - pub(crate) lineno_offset: usize, -} - -impl PartialEq for DisplayList<'_> { - fn eq(&self, other: &Self) -> bool { - self.body == other.body && self.anonymized_line_numbers == other.anonymized_line_numbers - } -} - -impl fmt::Debug for DisplayList<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("DisplayList") - .field("body", &self.body) - .field("anonymized_line_numbers", &self.anonymized_line_numbers) - .finish() - } -} - -impl Display for DisplayList<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let lineno_width = self.body.iter().fold(0, |max, set| { - set.display_lines.iter().fold(max, |max, line| match line { - DisplayLine::Source { lineno, .. } => cmp::max(lineno.unwrap_or(0), max), - _ => max, - }) - }); - let lineno_width = self.lineno_offset - + if lineno_width == 0 { - lineno_width - } else if self.anonymized_line_numbers { - ANONYMIZED_LINE_NUM.len() - } else { - ((lineno_width as f64).log10().floor() as usize) + 1 - }; - - let multiline_depth = self.body.iter().fold(0, |max, set| { - set.display_lines.iter().fold(max, |max2, line| match line { - DisplayLine::Source { annotations, .. } => cmp::max( - annotations.iter().fold(max2, |max3, line| { - cmp::max( - match line.annotation_part { - DisplayAnnotationPart::Standalone => 0, - DisplayAnnotationPart::LabelContinuation => 0, - DisplayAnnotationPart::MultilineStart(depth) => depth + 1, - DisplayAnnotationPart::MultilineEnd(depth) => depth + 1, - }, - max3, - ) - }), - max, - ), - _ => max2, - }) - }); - let mut buffer = StyledBuffer::new(); - for set in self.body.iter() { - self.format_set(set, lineno_width, multiline_depth, &mut buffer)?; - } - write!(f, "{}", buffer.render(self.stylesheet)?) - } -} - -impl<'a> DisplayList<'a> { - pub(crate) fn new( - message: snippet::Message<'a>, - stylesheet: &'a Stylesheet, - anonymized_line_numbers: bool, - term_width: usize, - cut_indicator: &'static str, - ) -> DisplayList<'a> { - let lineno_offset = message.lineno_offset; - let body = format_message( - message, - term_width, - anonymized_line_numbers, - cut_indicator, - true, - ); - - Self { - body, - stylesheet, - anonymized_line_numbers, - cut_indicator, - lineno_offset, - } - } - - fn format_set( - &self, - set: &DisplaySet<'_>, - lineno_width: usize, - multiline_depth: usize, - buffer: &mut StyledBuffer, - ) -> fmt::Result { - for line in &set.display_lines { - set.format_line( - line, - lineno_width, - multiline_depth, - self.stylesheet, - self.anonymized_line_numbers, - self.cut_indicator, - buffer, - )?; - } - Ok(()) - } -} - -#[derive(Debug, PartialEq)] -pub(crate) struct DisplaySet<'a> { - pub(crate) display_lines: Vec>, - pub(crate) margin: Margin, -} - -impl DisplaySet<'_> { - fn format_label( - &self, - line_offset: usize, - label: &[DisplayTextFragment<'_>], - stylesheet: &Stylesheet, - buffer: &mut StyledBuffer, - ) -> fmt::Result { - for fragment in label { - let style = match fragment.style { - DisplayTextStyle::Regular => stylesheet.none(), - DisplayTextStyle::Emphasis => stylesheet.emphasis(), - }; - buffer.append(line_offset, fragment.content, *style); - } - Ok(()) - } - - fn format_annotation( - &self, - line_offset: usize, - annotation: &Annotation<'_>, - continuation: bool, - stylesheet: &Stylesheet, - buffer: &mut StyledBuffer, - ) -> fmt::Result { - let hide_severity = annotation.annotation_type.is_none(); - let color = get_annotation_style(&annotation.annotation_type, stylesheet); - - let formatted_len = if let Some(id) = &annotation.id { - let id_len = id.id.len(); - if hide_severity { - id_len - } else { - 2 + id_len + annotation_type_len(&annotation.annotation_type) - } - } else { - annotation_type_len(&annotation.annotation_type) - }; - - if continuation { - for _ in 0..formatted_len + 2 { - buffer.append(line_offset, " ", Style::new()); - } - return self.format_label(line_offset, &annotation.label, stylesheet, buffer); - } - if formatted_len == 0 { - self.format_label(line_offset, &annotation.label, stylesheet, buffer) - } else { - // TODO(brent) All of this complicated checking of `hide_severity` should be reverted - // once we have real severities in Ruff. This code is trying to account for two - // different cases: - // - // - main diagnostic message - // - subdiagnostic message - // - // In the first case, signaled by `hide_severity = true`, we want to print the ID (the - // noqa code for a ruff lint diagnostic, e.g. `F401`, or `invalid-syntax` for a syntax - // error) without brackets. Instead, for subdiagnostics, we actually want to print the - // severity (usually `help`) regardless of the `hide_severity` setting. This is signaled - // by an ID of `None`. - // - // With real severities these should be reported more like in ty: - // - // ``` - // error[F401]: `math` imported but unused - // error[invalid-syntax]: Cannot use `match` statement on Python 3.9... - // ``` - // - // instead of the current versions intended to mimic the old Ruff output format: - // - // ``` - // F401 `math` imported but unused - // invalid-syntax: Cannot use `match` statement on Python 3.9... - // ``` - // - // Note that the `invalid-syntax` colon is added manually in `ruff_db`, not here. We - // could eventually add a colon to Ruff lint diagnostics (`F401:`) and then make the - // colon below unconditional again. - // - // This also applies to the hard-coded `stylesheet.error()` styling of the - // hidden-severity `id`. This should just be `*color` again later, but for now we don't - // want an unformatted `id`, which is what `get_annotation_style` returns for - // `DisplayAnnotationType::None`. - let annotation_type = annotation_type_str(&annotation.annotation_type); - if let Some(id) = annotation.id { - if hide_severity { - buffer.append( - line_offset, - &format!("{id} ", id = fmt_with_hyperlink(id.id, id.url, stylesheet)), - *stylesheet.error(), - ); - } else { - buffer.append( - line_offset, - &format!( - "{annotation_type}[{id}]", - id = fmt_with_hyperlink(id.id, id.url, stylesheet) - ), - *color, - ); - } - } else { - buffer.append(line_offset, annotation_type, *color); - } - - if annotation.is_fixable { - buffer.append(line_offset, "[", stylesheet.none); - buffer.append(line_offset, "*", stylesheet.help); - buffer.append(line_offset, "]", stylesheet.none); - // In the hide-severity case, we need a space instead of the colon and space below. - if hide_severity { - buffer.append(line_offset, " ", stylesheet.none); - } - } - - if !is_annotation_empty(annotation) { - if annotation.id.is_none() || !hide_severity { - buffer.append(line_offset, ": ", stylesheet.none); - } - self.format_label(line_offset, &annotation.label, stylesheet, buffer)?; - } - Ok(()) - } - } - - #[inline] - fn format_raw_line( - &self, - line_offset: usize, - line: &DisplayRawLine<'_>, - lineno_width: usize, - stylesheet: &Stylesheet, - anonymized_line_numbers: bool, - buffer: &mut StyledBuffer, - ) -> fmt::Result { - match line { - DisplayRawLine::Origin { - path, - pos, - header_type, - } => { - let header_sigil = match header_type { - DisplayHeaderType::Initial => "-->", - DisplayHeaderType::Continuation => ":::", - }; - let lineno_color = stylesheet.line_no(); - buffer.puts(line_offset, lineno_width, header_sigil, *lineno_color); - buffer.puts(line_offset, lineno_width + 4, path, stylesheet.none); - if let Some(Position { row, col, cell }) = pos { - if let Some(cell) = cell { - buffer.append(line_offset, ":", stylesheet.none); - buffer.append(line_offset, &format!("cell {cell}"), stylesheet.none); - } - buffer.append(line_offset, ":", stylesheet.none); - if anonymized_line_numbers { - buffer.append(line_offset, ANONYMIZED_LINE_NUM, stylesheet.none); - } else { - buffer.append(line_offset, row.to_string().as_str(), stylesheet.none); - } - buffer.append(line_offset, ":", stylesheet.none); - buffer.append(line_offset, col.to_string().as_str(), stylesheet.none); - } - Ok(()) - } - DisplayRawLine::Annotation { - annotation, - source_aligned, - continuation, - } => { - if *source_aligned { - if *continuation { - for _ in 0..lineno_width + 3 { - buffer.append(line_offset, " ", stylesheet.none); - } - } else { - let lineno_color = stylesheet.line_no(); - for _ in 0..lineno_width + 1 { - buffer.append(line_offset, " ", stylesheet.none); - } - buffer.append(line_offset, "=", *lineno_color); - buffer.append(line_offset, " ", *lineno_color); - } - } - self.format_annotation(line_offset, annotation, *continuation, stylesheet, buffer) - } - } - } - - // Adapted from https://github.com/rust-lang/rust/blob/d371d17496f2ce3a56da76aa083f4ef157572c20/compiler/rustc_errors/src/emitter.rs#L706-L1211 - #[expect(clippy::too_many_arguments)] - #[inline] - fn format_line( - &self, - dl: &DisplayLine<'_>, - lineno_width: usize, - multiline_depth: usize, - stylesheet: &Stylesheet, - anonymized_line_numbers: bool, - cut_indicator: &'static str, - buffer: &mut StyledBuffer, - ) -> fmt::Result { - let line_offset = buffer.num_lines(); - match dl { - DisplayLine::Source { - lineno, - inline_marks, - line, - annotations, - } => { - let lineno_color = stylesheet.line_no(); - if anonymized_line_numbers && lineno.is_some() { - let num = format!("{ANONYMIZED_LINE_NUM:>lineno_width$} |"); - buffer.puts(line_offset, 0, &num, *lineno_color); - } else { - match lineno { - Some(n) => { - let num = format!("{n:>lineno_width$} |"); - buffer.puts(line_offset, 0, &num, *lineno_color); - } - None => { - buffer.putc(line_offset, lineno_width + 1, '|', *lineno_color); - } - } - } - if let DisplaySourceLine::Content { text, .. } = line { - // The width of the line number, a space, pipe, and a space - // `123 | ` is `lineno_width + 3`. - let width_offset = lineno_width + 3; - let code_offset = if multiline_depth == 0 { - width_offset - } else { - width_offset + multiline_depth + 1 - }; - - // Add any inline marks to the code line - if !inline_marks.is_empty() || 0 < multiline_depth { - format_inline_marks( - line_offset, - inline_marks, - lineno_width, - stylesheet, - buffer, - )?; - } - - let text = normalize_whitespace(text); - let line_len = text.as_bytes().len(); - let left = self.margin.left(line_len); - let right = self.margin.right(line_len); - - // On long lines, we strip the source line, accounting for unicode. - let mut taken = 0; - let mut was_cut_right = false; - let mut code = String::new(); - for ch in text.chars().skip(left) { - // Make sure that the trimming on the right will fall within the terminal width. - // 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 = char_width(ch).unwrap_or(1); - if taken + next > right - left { - was_cut_right = true; - break; - } - taken += next; - code.push(ch); - } - buffer.puts(line_offset, code_offset, &code, Style::new()); - if self.margin.was_cut_left() { - // We have stripped some code/whitespace from the beginning, make it clear. - buffer.puts(line_offset, code_offset, cut_indicator, *lineno_color); - } - if was_cut_right { - buffer.puts( - line_offset, - code_offset + taken - cut_indicator.width(), - cut_indicator, - *lineno_color, - ); - } - - let left: usize = text - .chars() - .take(left) - .map(|ch| char_width(ch).unwrap_or(1)) - .sum(); - - let mut annotations = annotations.clone(); - annotations.sort_by_key(|a| Reverse(a.range.0)); - - let mut annotations_positions = vec![]; - let mut line_len: usize = 0; - let mut p = 0; - for (i, annotation) in annotations.iter().enumerate() { - for (j, next) in annotations.iter().enumerate() { - // This label overlaps with another one and both take space ( - // they have text and are not multiline lines). - if overlaps(next, annotation, 0) - && annotation.has_label() - && j > i - && p == 0 - // We're currently on the first line, move the label one line down - { - // If we're overlapping with an un-labelled annotation with the same span - // we can just merge them in the output - if next.range.0 == annotation.range.0 - && next.range.1 == annotation.range.1 - && !next.has_label() - { - continue; - } - - // This annotation needs a new line in the output. - p += 1; - break; - } - } - annotations_positions.push((p, annotation)); - for (j, next) in annotations.iter().enumerate() { - if j > i { - let l = next - .annotation - .label - .iter() - .map(|label| label.content) - .collect::>() - .join("") - .len() - + 2; - // Do not allow two labels to be in the same line if they - // overlap including padding, to avoid situations like: - // - // fn foo(x: u32) { - // -------^------ - // | | - // fn_spanx_span - // - // Both labels must have some text, otherwise they are not - // overlapping. Do not add a new line if this annotation or - // the next are vertical line placeholders. If either this - // or the next annotation is multiline start/end, move it - // to a new line so as not to overlap the horizontal lines. - if (overlaps(next, annotation, l) - && annotation.has_label() - && next.has_label()) - || (annotation.takes_space() && next.has_label()) - || (annotation.has_label() && next.takes_space()) - || (annotation.takes_space() && next.takes_space()) - || (overlaps(next, annotation, l) - && next.range.1 <= annotation.range.1 - && next.has_label() - && p == 0) - // Avoid #42595. - { - // This annotation needs a new line in the output. - p += 1; - break; - } - } - } - line_len = max(line_len, p); - } - - if line_len != 0 { - line_len += 1; - } - - if annotations_positions.iter().all(|(_, ann)| { - matches!( - ann.annotation_part, - DisplayAnnotationPart::MultilineStart(_) - ) - }) { - if let Some(max_pos) = - annotations_positions.iter().map(|(pos, _)| *pos).max() - { - // Special case the following, so that we minimize overlapping multiline spans. - // - // 3 │ X0 Y0 Z0 - // │ ┏━━━━━┛ │ │ < We are writing these lines - // │ ┃┌───────┘ │ < by reverting the "depth" of - // │ ┃│┌─────────┘ < their multiline spans. - // 4 │ ┃││ X1 Y1 Z1 - // 5 │ ┃││ X2 Y2 Z2 - // │ ┃│└────╿──│──┘ `Z` label - // │ ┃└─────│──┤ - // │ ┗━━━━━━┥ `Y` is a good letter too - // ╰╴ `X` is a good letter - for (pos, _) in &mut annotations_positions { - *pos = max_pos - *pos; - } - // We know then that we don't need an additional line for the span label, saving us - // one line of vertical space. - line_len = line_len.saturating_sub(1); - } - } - - // This is a special case where we have a multiline - // annotation that is at the start of the line disregarding - // any leading whitespace, and no other multiline - // annotations overlap it. In this case, we want to draw - // - // 2 | fn foo() { - // | _^ - // 3 | | - // 4 | | } - // | |_^ test - // - // we simplify the output to: - // - // 2 | / fn foo() { - // 3 | | - // 4 | | } - // | |_^ test - if multiline_depth == 1 - && annotations_positions.len() == 1 - && annotations_positions - .first() - .map_or(false, |(_, annotation)| { - matches!( - annotation.annotation_part, - DisplayAnnotationPart::MultilineStart(_) - ) && text - .chars() - .take(annotation.range.0) - .all(|c| c.is_whitespace()) - }) - { - let (_, ann) = annotations_positions.remove(0); - let style = get_annotation_style(&ann.annotation_type, stylesheet); - buffer.putc(line_offset, 3 + lineno_width, '/', *style); - } - - // Draw the column separator for any extra lines that were - // created - // - // After this we will have: - // - // 2 | fn foo() { - // | - // | - // | - // 3 | - // 4 | } - // | - if !annotations_positions.is_empty() { - for pos in 0..=line_len { - buffer.putc( - line_offset + pos + 1, - lineno_width + 1, - '|', - stylesheet.line_no, - ); - } - } - - // Write the horizontal lines for multiline annotations - // (only the first and last lines need this). - // - // After this we will have: - // - // 2 | fn foo() { - // | __________ - // | - // | - // 3 | - // 4 | } - // | _ - for &(pos, annotation) in &annotations_positions { - let style = get_annotation_style(&annotation.annotation_type, stylesheet); - let pos = pos + 1; - match annotation.annotation_part { - DisplayAnnotationPart::MultilineStart(depth) - | DisplayAnnotationPart::MultilineEnd(depth) => { - for col in width_offset + depth - ..(code_offset + annotation.range.0).saturating_sub(left) - { - buffer.putc(line_offset + pos, col + 1, '_', *style); - } - } - _ => {} - } - } - - // Write the vertical lines for labels that are on a different line as the underline. - // - // After this we will have: - // - // 2 | fn foo() { - // | __________ - // | | | - // | | - // 3 | | - // 4 | | } - // | |_ - for &(pos, annotation) in &annotations_positions { - let style = get_annotation_style(&annotation.annotation_type, stylesheet); - let pos = pos + 1; - if pos > 1 && (annotation.has_label() || annotation.takes_space()) { - for p in line_offset + 2..=line_offset + pos { - buffer.putc( - p, - (code_offset + annotation.range.0).saturating_sub(left), - '|', - *style, - ); - } - } - match annotation.annotation_part { - DisplayAnnotationPart::MultilineStart(depth) => { - for p in line_offset + pos + 1..line_offset + line_len + 2 { - buffer.putc(p, width_offset + depth, '|', *style); - } - } - DisplayAnnotationPart::MultilineEnd(depth) => { - for p in line_offset..=line_offset + pos { - buffer.putc(p, width_offset + depth, '|', *style); - } - } - _ => {} - } - } - - // Add in any inline marks for any extra lines that have - // been created. Output should look like above. - for inline_mark in inline_marks { - let DisplayMarkType::AnnotationThrough(depth) = inline_mark.mark_type; - let style = get_annotation_style(&inline_mark.annotation_type, stylesheet); - if annotations_positions.is_empty() { - buffer.putc(line_offset, width_offset + depth, '|', *style); - } else { - for p in line_offset..=line_offset + line_len + 1 { - buffer.putc(p, width_offset + depth, '|', *style); - } - } - } - - // Write the labels on the annotations that actually have a label. - // - // After this we will have: - // - // 2 | fn foo() { - // | __________ - // | | - // | something about `foo` - // 3 | - // 4 | } - // | _ test - for &(pos, annotation) in &annotations_positions { - if !is_annotation_empty(&annotation.annotation) { - let style = - get_annotation_style(&annotation.annotation_type, stylesheet); - let mut formatted_len = if let Some(id) = &annotation.annotation.id { - 2 + id.id.len() - + annotation_type_len(&annotation.annotation.annotation_type) - } else { - annotation_type_len(&annotation.annotation.annotation_type) - }; - let (pos, col) = if pos == 0 { - (pos + 1, (annotation.range.1 + 1).saturating_sub(left)) - } else { - (pos + 2, annotation.range.0.saturating_sub(left)) - }; - if annotation.annotation_part - == DisplayAnnotationPart::LabelContinuation - { - formatted_len = 0; - } else if formatted_len != 0 { - formatted_len += 2; - let id = match &annotation.annotation.id { - Some(id) => format!( - "[{id}]", - id = fmt_with_hyperlink(&id.id, id.url, stylesheet) - ), - None => String::new(), - }; - buffer.puts( - line_offset + pos, - col + code_offset, - &format!( - "{}{}: ", - annotation_type_str(&annotation.annotation_type), - id - ), - *style, - ); - } else { - formatted_len = 0; - } - let mut before = 0; - for fragment in &annotation.annotation.label { - let inner_col = before + formatted_len + col + code_offset; - buffer.puts(line_offset + pos, inner_col, fragment.content, *style); - before += fragment.content.len(); - } - } - } - - // Sort from biggest span to smallest span so that smaller spans are - // represented in the output: - // - // x | fn foo() - // | ^^^---^^ - // | | | - // | | something about `foo` - // | something about `fn foo()` - annotations_positions.sort_by_key(|(_, ann)| { - // Decreasing order. When annotations share the same length, prefer `Primary`. - Reverse(ann.len()) - }); - - // Write the underlines. - // - // After this we will have: - // - // 2 | fn foo() { - // | ____-_____^ - // | | - // | something about `foo` - // 3 | - // 4 | } - // | _^ test - for &(_, annotation) in &annotations_positions { - let mark = match annotation.annotation_type { - DisplayAnnotationType::Error => '^', - DisplayAnnotationType::Warning => '-', - DisplayAnnotationType::Info => '-', - DisplayAnnotationType::Note => '-', - DisplayAnnotationType::Help => '-', - DisplayAnnotationType::None => ' ', - }; - let style = get_annotation_style(&annotation.annotation_type, stylesheet); - for p in annotation.range.0..annotation.range.1 { - buffer.putc( - line_offset + 1, - (code_offset + p).saturating_sub(left), - mark, - *style, - ); - } - } - } else if !inline_marks.is_empty() { - format_inline_marks( - line_offset, - inline_marks, - lineno_width, - stylesheet, - buffer, - )?; - } - Ok(()) - } - DisplayLine::Fold { inline_marks } => { - buffer.puts(line_offset, 0, cut_indicator, *stylesheet.line_no()); - if !inline_marks.is_empty() || 0 < multiline_depth { - format_inline_marks( - line_offset, - inline_marks, - lineno_width, - stylesheet, - buffer, - )?; - } - Ok(()) - } - DisplayLine::Raw(line) => self.format_raw_line( - line_offset, - line, - lineno_width, - stylesheet, - anonymized_line_numbers, - buffer, - ), - } - } -} - -/// Inline annotation which can be used in either Raw or Source line. -#[derive(Clone, Debug, PartialEq)] -pub(crate) struct Annotation<'a> { - pub(crate) annotation_type: DisplayAnnotationType, - pub(crate) id: Option>, - pub(crate) label: Vec>, - pub(crate) is_fixable: bool, -} - -/// A single line used in `DisplayList`. -#[derive(Debug, PartialEq)] -pub(crate) enum DisplayLine<'a> { - /// A line with `lineno` portion of the slice. - Source { - lineno: Option, - inline_marks: Vec, - line: DisplaySourceLine<'a>, - annotations: Vec>, - }, - - /// A line indicating a folded part of the slice. - Fold { inline_marks: Vec }, - - /// A line which is displayed outside of slices. - Raw(DisplayRawLine<'a>), -} - -/// A source line. -#[derive(Debug, PartialEq)] -pub(crate) enum DisplaySourceLine<'a> { - /// A line with the content of the Snippet. - Content { - text: &'a str, - range: (usize, usize), // meta information for annotation placement. - end_line: EndLine, - }, - /// An empty source line. - Empty, -} - -#[derive(Clone, Debug, PartialEq)] -pub(crate) struct DisplaySourceAnnotation<'a> { - pub(crate) annotation: Annotation<'a>, - pub(crate) range: (usize, usize), - pub(crate) annotation_type: DisplayAnnotationType, - pub(crate) annotation_part: DisplayAnnotationPart, -} - -impl DisplaySourceAnnotation<'_> { - fn has_label(&self) -> bool { - !self - .annotation - .label - .iter() - .all(|label| label.content.is_empty()) - } - - // Length of this annotation as displayed in the stderr output - fn len(&self) -> usize { - // Account for usize underflows - self.range.1.abs_diff(self.range.0) - } - - fn takes_space(&self) -> bool { - // Multiline annotations always have to keep vertical space. - matches!( - self.annotation_part, - DisplayAnnotationPart::MultilineStart(_) | DisplayAnnotationPart::MultilineEnd(_) - ) - } -} - -#[derive(Debug, PartialEq)] -pub(crate) struct Position { - row: usize, - col: usize, - cell: Option, -} - -/// Raw line - a line which does not have the `lineno` part and is not considered -/// a part of the snippet. -#[derive(Debug, PartialEq)] -pub(crate) enum DisplayRawLine<'a> { - /// A line which provides information about the location of the given - /// slice in the project structure. - Origin { - path: &'a str, - pos: Option, - header_type: DisplayHeaderType, - }, - - /// An annotation line which is not part of any snippet. - Annotation { - annotation: Annotation<'a>, - - /// If set to `true`, the annotation will be aligned to the - /// lineno delimiter of the snippet. - source_aligned: bool, - /// If set to `true`, only the label of the `Annotation` will be - /// displayed. It allows for a multiline annotation to be aligned - /// without displaying the meta information (`type` and `id`) to be - /// displayed on each line. - continuation: bool, - }, -} - -/// An inline text fragment which any label is composed of. -#[derive(Clone, Debug, PartialEq)] -pub(crate) struct DisplayTextFragment<'a> { - pub(crate) content: &'a str, - pub(crate) style: DisplayTextStyle, -} - -/// A style for the `DisplayTextFragment` which can be visually formatted. -/// -/// This information may be used to emphasis parts of the label. -#[derive(Debug, Clone, Copy, PartialEq)] -pub(crate) enum DisplayTextStyle { - Regular, - Emphasis, -} - -/// An indicator of what part of the annotation a given `Annotation` is. -#[derive(Debug, Clone, PartialEq)] -pub(crate) enum DisplayAnnotationPart { - /// A standalone, single-line annotation. - Standalone, - /// A continuation of a multi-line label of an annotation. - LabelContinuation, - /// A line starting a multiline annotation. - MultilineStart(usize), - /// A line ending a multiline annotation. - MultilineEnd(usize), -} - -/// A visual mark used in `inline_marks` field of the `DisplaySourceLine`. -#[derive(Debug, Clone, PartialEq)] -pub(crate) struct DisplayMark { - pub(crate) mark_type: DisplayMarkType, - pub(crate) annotation_type: DisplayAnnotationType, -} - -/// A type of the `DisplayMark`. -#[derive(Debug, Clone, PartialEq)] -pub(crate) enum DisplayMarkType { - /// A mark indicating a multiline annotation going through the current line. - AnnotationThrough(usize), -} - -/// A type of the `Annotation` which may impact the sigils, style or text displayed. -/// -/// There are several ways to uses this information when formatting the `DisplayList`: -/// -/// * An annotation may display the name of the type like `error` or `info`. -/// * An underline for `Error` may be `^^^` while for `Warning` it could be `---`. -/// * `ColorStylesheet` may use different colors for different annotations. -#[derive(Debug, Clone, PartialEq)] -pub(crate) enum DisplayAnnotationType { - None, - Error, - Warning, - Info, - Note, - Help, -} - -impl DisplayAnnotationType { - #[inline] - const fn is_none(&self) -> bool { - matches!(self, Self::None) - } -} - -impl From for DisplayAnnotationType { - fn from(at: snippet::Level) -> Self { - match at { - snippet::Level::None => DisplayAnnotationType::None, - snippet::Level::Error => DisplayAnnotationType::Error, - snippet::Level::Warning => DisplayAnnotationType::Warning, - snippet::Level::Info => DisplayAnnotationType::Info, - snippet::Level::Note => DisplayAnnotationType::Note, - snippet::Level::Help => DisplayAnnotationType::Help, - } - } -} - -/// Information whether the header is the initial one or a consecutive one -/// for multi-slice cases. -// TODO: private -#[derive(Debug, Clone, PartialEq)] -pub(crate) enum DisplayHeaderType { - /// Initial header is the first header in the snippet. - Initial, - - /// Continuation marks all headers of following slices in the snippet. - Continuation, -} - -struct CursorLines<'a>(&'a str); - -impl CursorLines<'_> { - fn new(src: &str) -> CursorLines<'_> { - CursorLines(src) - } -} - -#[derive(Copy, Clone, Debug, PartialEq)] -pub(crate) enum EndLine { - Eof, - Lf, - Crlf, -} - -impl EndLine { - /// The number of characters this line ending occupies in bytes. - pub(crate) fn len(self) -> usize { - match self { - EndLine::Eof => 0, - EndLine::Lf => 1, - EndLine::Crlf => 2, - } - } -} - -impl<'a> Iterator for CursorLines<'a> { - type Item = (&'a str, EndLine); - - fn next(&mut self) -> Option { - if self.0.is_empty() { - None - } else { - self.0 - .find('\n') - .map(|x| { - let ret = if 0 < x { - if self.0.as_bytes()[x - 1] == b'\r' { - (&self.0[..x - 1], EndLine::Crlf) - } else { - (&self.0[..x], EndLine::Lf) - } - } else { - ("", EndLine::Lf) - }; - self.0 = &self.0[x + 1..]; - ret - }) - .or_else(|| { - let ret = Some((self.0, EndLine::Eof)); - self.0 = ""; - ret - }) - } - } -} - -fn format_message<'m>( - message: snippet::Message<'m>, - term_width: usize, - anonymized_line_numbers: bool, - cut_indicator: &'static str, - primary: bool, -) -> Vec> { - let snippet::Message { - level, - id, - title, - footer, - snippets, - is_fixable, - lineno_offset: _, - } = message; - - let mut sets = vec![]; - let body = if !snippets.is_empty() || primary { - vec![format_title(level, id, title, is_fixable)] - } else { - format_footer(level, id, title) - }; - - for (idx, snippet) in snippets.into_iter().enumerate() { - let snippet = fold_prefix_suffix(snippet); - sets.push(format_snippet( - snippet, - idx == 0, - !footer.is_empty(), - term_width, - anonymized_line_numbers, - cut_indicator, - )); - } - - if let Some(first) = sets.first_mut() { - for line in body { - first.display_lines.insert(0, line); - } - } else { - sets.push(DisplaySet { - display_lines: body, - margin: Margin::new(0, 0, 0, 0, DEFAULT_TERM_WIDTH, 0), - }); - } - - for annotation in footer { - sets.extend(format_message( - annotation, - term_width, - anonymized_line_numbers, - cut_indicator, - false, - )); - } - - sets -} - -fn format_title<'a>( - level: crate::Level, - id: Option>, - label: &'a str, - is_fixable: bool, -) -> DisplayLine<'a> { - DisplayLine::Raw(DisplayRawLine::Annotation { - annotation: Annotation { - annotation_type: DisplayAnnotationType::from(level), - id, - label: format_label(Some(label), Some(DisplayTextStyle::Emphasis)), - is_fixable, - }, - source_aligned: false, - continuation: false, - }) -} - -fn format_footer<'a>( - level: crate::Level, - id: Option>, - label: &'a str, -) -> Vec> { - let mut result = vec![]; - for (i, line) in label.lines().enumerate() { - result.push(DisplayLine::Raw(DisplayRawLine::Annotation { - annotation: Annotation { - annotation_type: DisplayAnnotationType::from(level), - id, - label: format_label(Some(line), None), - is_fixable: false, - }, - source_aligned: true, - continuation: i != 0, - })); - } - result -} - -fn format_label( - label: Option<&str>, - style: Option, -) -> Vec> { - let mut result = vec![]; - if let Some(label) = label { - let element_style = style.unwrap_or(DisplayTextStyle::Regular); - result.push(DisplayTextFragment { - content: label, - style: element_style, - }); - } - result -} - -fn format_snippet<'m>( - snippet: snippet::Snippet<'m>, - is_first: bool, - has_footer: bool, - term_width: usize, - anonymized_line_numbers: bool, - cut_indicator: &'static str, -) -> DisplaySet<'m> { - let main_range = snippet.annotations.first().map(|x| x.range.start); - let origin = snippet.origin; - let need_empty_header = origin.is_some() || is_first; - - let is_file_level = snippet.annotations.iter().any(|ann| ann.is_file_level); - if is_file_level { - // TODO(brent) enable this assertion again once we set `is_file_level` for individual rules. - // It's causing too many false positives currently when the default is to make any - // annotation with a default range file-level. See - // https://github.com/astral-sh/ruff/issues/19688. - // - // assert!( - // snippet.source.is_empty(), - // "Non-empty file-level snippet that won't be rendered: {:?}", - // snippet.source - // ); - let header = format_header(origin, main_range, &[], is_first, snippet.cell_index); - return DisplaySet { - display_lines: header.map_or_else(Vec::new, |header| vec![header]), - margin: Margin::new(0, 0, 0, 0, term_width, 0), - }; - } - - let cell_index = snippet.cell_index; - - let mut body = format_body( - snippet, - need_empty_header, - has_footer, - term_width, - anonymized_line_numbers, - cut_indicator, - ); - let header = format_header( - origin, - main_range, - &body.display_lines, - is_first, - cell_index, - ); - - if let Some(header) = header { - body.display_lines.insert(0, header); - } - - body -} - -fn format_header<'a>( - origin: Option<&'a str>, - main_range: Option, - body: &[DisplayLine<'_>], - is_first: bool, - cell_index: Option, -) -> Option> { - let display_header = if is_first { - DisplayHeaderType::Initial - } else { - DisplayHeaderType::Continuation - }; - - if let Some((main_range, path)) = main_range.zip(origin) { - let mut col = 1; - let mut line_offset = 1; - - for item in body { - if let DisplayLine::Source { - line: - DisplaySourceLine::Content { - text, - range, - end_line, - }, - lineno, - .. - } = item - { - // At the very end of the `main_range`, report the location as the first character - // in the next line instead of falling back to the default location of `1:1`. This - // is another divergence from upstream. - let end_of_range = range.1 + max(*end_line as usize, 1); - if main_range >= range.0 && main_range < end_of_range { - let char_column = text[0..(main_range - range.0).min(text.len())] - .chars() - .count(); - col = char_column + 1; - line_offset = lineno.unwrap_or(1); - break; - } else if main_range == end_of_range { - line_offset = lineno.map_or(1, |line| line + 1); - break; - } - } - } - - return Some(DisplayLine::Raw(DisplayRawLine::Origin { - path, - pos: Some(Position { - row: line_offset, - col, - cell: cell_index, - }), - header_type: display_header, - })); - } - - if let Some(path) = origin { - return Some(DisplayLine::Raw(DisplayRawLine::Origin { - path, - pos: None, - header_type: display_header, - })); - } - - None -} - -fn fold_prefix_suffix(mut snippet: snippet::Snippet<'_>) -> snippet::Snippet<'_> { - if !snippet.fold { - return snippet; - } - - let ann_start = snippet - .annotations - .iter() - .map(|ann| ann.range.start) - .min() - .unwrap_or(0); - if let Some(before_new_start) = snippet.source[0..ann_start].rfind('\n') { - let new_start = before_new_start + 1; - - let line_offset = newline_count(&snippet.source[..new_start]); - snippet.line_start += line_offset; - - snippet.source = &snippet.source[new_start..]; - - for ann in &mut snippet.annotations { - let range_start = ann.range.start - new_start; - let range_end = ann.range.end - new_start; - ann.range = range_start..range_end; - } - } - - let ann_end = snippet - .annotations - .iter() - .map(|ann| ann.range.end) - .max() - .unwrap_or(snippet.source.len()); - if let Some(end_offset) = snippet.source[ann_end..].find('\n') { - let new_end = ann_end + end_offset; - snippet.source = &snippet.source[..new_end]; - } - - snippet -} - -fn newline_count(body: &str) -> usize { - memchr::memchr_iter(b'\n', body.as_bytes()).count() -} - -fn fold_body(body: Vec>) -> Vec> { - const INNER_CONTEXT: usize = 1; - const INNER_UNFOLD_SIZE: usize = INNER_CONTEXT * 2 + 1; - - let mut lines = vec![]; - let mut unhighlighted_lines = vec![]; - for line in body { - match &line { - DisplayLine::Source { annotations, .. } => { - if annotations.is_empty() { - unhighlighted_lines.push(line); - } else { - if lines.is_empty() { - // Ignore leading unhighlighted lines - unhighlighted_lines.clear(); - } - match unhighlighted_lines.len() { - 0 => {} - n if n <= INNER_UNFOLD_SIZE => { - // Rather than render our cut indicator, don't fold - lines.append(&mut unhighlighted_lines); - } - _ => { - lines.extend(unhighlighted_lines.drain(..INNER_CONTEXT)); - let inline_marks = lines - .last() - .and_then(|line| { - if let DisplayLine::Source { inline_marks, .. } = line { - let inline_marks = inline_marks.clone(); - Some(inline_marks) - } else { - None - } - }) - .unwrap_or_default(); - lines.push(DisplayLine::Fold { - inline_marks: inline_marks.clone(), - }); - unhighlighted_lines - .drain(..unhighlighted_lines.len().saturating_sub(INNER_CONTEXT)); - lines.append(&mut unhighlighted_lines); - } - } - lines.push(line); - } - } - _ => { - unhighlighted_lines.push(line); - } - } - } - - lines -} - -fn format_body<'m>( - snippet: snippet::Snippet<'m>, - need_empty_header: bool, - has_footer: bool, - term_width: usize, - anonymized_line_numbers: bool, - cut_indicator: &'static str, -) -> DisplaySet<'m> { - let source_len = snippet.source.len(); - if let Some(bigger) = snippet.annotations.iter().find_map(|x| { - // Allow highlighting one past the last character in the source. - if source_len + 1 < x.range.end { - Some(&x.range) - } else { - None - } - }) { - panic!("SourceAnnotation range `{bigger:?}` is beyond the end of buffer `{source_len}`") - } - - let mut body = vec![]; - let mut current_line = snippet.line_start; - let mut current_index = 0; - - let mut whitespace_margin = usize::MAX; - let mut span_left_margin = usize::MAX; - let mut span_right_margin = 0; - let mut label_right_margin = 0; - let mut max_line_len = 0; - - let mut depth_map: HashMap = HashMap::new(); - let mut current_depth = 0; - let mut annotations = snippet.annotations; - let ranges = annotations - .iter() - .map(|a| a.range.clone()) - .collect::>(); - // We want to merge multiline annotations that have the same range into one - // multiline annotation to save space. This is done by making any duplicate - // multiline annotations into a single-line annotation pointing at the end - // of the range. - // - // 3 | X0 Y0 Z0 - // | _____^ - // | | ____| - // | || ___| - // | ||| - // 4 | ||| X1 Y1 Z1 - // 5 | ||| X2 Y2 Z2 - // | ||| ^ - // | |||____| - // | ||____`X` is a good letter - // | |____`Y` is a good letter too - // | `Z` label - // Should be - // error: foo - // --> test.rs:3:3 - // | - // 3 | / X0 Y0 Z0 - // 4 | | X1 Y1 Z1 - // 5 | | X2 Y2 Z2 - // | | ^ - // | |____| - // | `X` is a good letter - // | `Y` is a good letter too - // | `Z` label - // | - ranges.iter().enumerate().for_each(|(r_idx, range)| { - annotations - .iter_mut() - .enumerate() - .skip(r_idx + 1) - .for_each(|(ann_idx, ann)| { - // Skip if the annotation's index matches the range index - if ann_idx != r_idx - // We only want to merge multiline annotations - && snippet.source[ann.range.clone()].lines().count() > 1 - // We only want to merge annotations that have the same range - && ann.range.start == range.start - && ann.range.end == range.end - { - ann.range.start = ann.range.end.saturating_sub(1); - } - }); - }); - annotations.sort_by_key(|a| a.range.start); - let mut annotations = annotations.into_iter().enumerate().collect::>(); - - for (idx, (line, end_line)) in CursorLines::new(snippet.source).enumerate() { - 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![], - line: DisplaySourceLine::Content { - text: line, - range: line_range, - end_line, - }, - annotations: vec![], - }); - - let leading_whitespace = line - .chars() - .take_while(|c| c.is_whitespace()) - .map(|c| { - match c { - // Tabs are displayed as 4 spaces - '\t' => 4, - _ => 1, - } - }) - .sum(); - whitespace_margin = min(whitespace_margin, leading_whitespace); - max_line_len = max(max_line_len, line_length); - - let line_start_index = line_range.0; - let line_end_index = line_range.1; - current_line += 1; - current_index += line_length + end_line_size; - - // It would be nice to use filter_drain here once it's stable. - annotations.retain(|(key, annotation)| { - let body_idx = idx; - let annotation_type = match annotation.level { - snippet::Level::Error => DisplayAnnotationType::None, - snippet::Level::Warning => DisplayAnnotationType::None, - _ => DisplayAnnotationType::from(annotation.level), - }; - let label_right = annotation.label.map_or(0, |label| label.len() + 1); - match annotation.range { - // This handles if the annotation is on the next line. We add - // the `end_line_size` to account for annotating the line end. - Range { start, .. } if start > line_end_index + end_line_size => true, - // This handles the case where an annotation is contained - // within the current line including any line-end characters. - Range { start, end } - if start >= line_start_index - // We add at least one to `line_end_index` to allow - // highlighting the end of a file - && end <= line_end_index + max(end_line_size, 1) => - { - if let DisplayLine::Source { - ref mut annotations, - .. - } = body[body_idx] - { - let annotation_start_col = line - [0..(start - line_start_index).min(line_length)] - .chars() - .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| char_width(c).unwrap_or(0)) - .sum::(); - if annotation_start_col == annotation_end_col { - // At least highlight something - annotation_end_col += 1; - } - - span_left_margin = min(span_left_margin, annotation_start_col); - span_right_margin = max(span_right_margin, annotation_end_col); - label_right_margin = - max(label_right_margin, annotation_end_col + label_right); - - let range = (annotation_start_col, annotation_end_col); - annotations.push(DisplaySourceAnnotation { - annotation: Annotation { - annotation_type, - id: None, - label: format_label(annotation.label, None), - is_fixable: false, - }, - range, - annotation_type: DisplayAnnotationType::from(annotation.level), - annotation_part: DisplayAnnotationPart::Standalone, - }); - } - false - } - // This handles the case where a multiline annotation starts - // somewhere on the current line, including any line-end chars - Range { start, end } - if start >= line_start_index - // The annotation can start on a line ending - && start <= line_end_index + end_line_size.saturating_sub(1) - && end > line_end_index => - { - if let DisplayLine::Source { - ref mut annotations, - .. - } = body[body_idx] - { - let annotation_start_col = line - [0..(start - line_start_index).min(line_length)] - .chars() - .map(|c| char_width(c).unwrap_or(0)) - .sum::(); - let annotation_end_col = annotation_start_col + 1; - - span_left_margin = min(span_left_margin, annotation_start_col); - span_right_margin = max(span_right_margin, annotation_end_col); - label_right_margin = - max(label_right_margin, annotation_end_col + label_right); - - let range = (annotation_start_col, annotation_end_col); - annotations.push(DisplaySourceAnnotation { - annotation: Annotation { - annotation_type, - id: None, - label: vec![], - is_fixable: false, - }, - range, - annotation_type: DisplayAnnotationType::from(annotation.level), - annotation_part: DisplayAnnotationPart::MultilineStart(current_depth), - }); - depth_map.insert(*key, current_depth); - current_depth += 1; - } - true - } - // This handles the case where a multiline annotation starts - // somewhere before this line and ends after it as well - Range { start, end } - if start < line_start_index && end > line_end_index + max(end_line_size, 1) => - { - if let DisplayLine::Source { - ref mut inline_marks, - .. - } = body[body_idx] - { - let depth = depth_map.get(key).cloned().unwrap_or_default(); - inline_marks.push(DisplayMark { - mark_type: DisplayMarkType::AnnotationThrough(depth), - annotation_type: DisplayAnnotationType::from(annotation.level), - }); - } - true - } - // This handles the case where a multiline annotation ends - // somewhere on the current line, including any line-end chars - Range { start, end } - if start < line_start_index - && end >= line_start_index - // We add at least one to `line_end_index` to allow - // highlighting the end of a file - && end <= line_end_index + max(end_line_size, 1) => - { - if let DisplayLine::Source { - ref mut annotations, - .. - } = body[body_idx] - { - let end_mark = line[0..(end - line_start_index).min(line_length)] - .chars() - .map(|c| char_width(c).unwrap_or(0)) - .sum::() - .saturating_sub(1); - // If the annotation ends on a line-end character, we - // need to annotate one past the end of the line - let (end_mark, end_plus_one) = if end > line_end_index - // Special case for highlighting the end of a file - || (end == line_end_index + 1 && end_line_size == 0) - { - (end_mark + 1, end_mark + 2) - } else { - (end_mark, end_mark + 1) - }; - - span_left_margin = min(span_left_margin, end_mark); - span_right_margin = max(span_right_margin, end_plus_one); - label_right_margin = max(label_right_margin, end_plus_one + label_right); - - let range = (end_mark, end_plus_one); - let depth = depth_map.remove(key).unwrap_or(0); - annotations.push(DisplaySourceAnnotation { - annotation: Annotation { - annotation_type, - id: None, - - label: format_label(annotation.label, None), - is_fixable: false, - }, - range, - annotation_type: DisplayAnnotationType::from(annotation.level), - annotation_part: DisplayAnnotationPart::MultilineEnd(depth), - }); - } - false - } - _ => true, - } - }); - // Reset the depth counter, but only after we've processed all - // annotations for a given line. - let max = depth_map.len(); - if current_depth > max { - current_depth = max; - } - } - - if snippet.fold { - body = fold_body(body); - } - - if need_empty_header { - body.insert( - 0, - DisplayLine::Source { - lineno: None, - inline_marks: vec![], - line: DisplaySourceLine::Empty, - annotations: vec![], - }, - ); - } - - if has_footer { - body.push(DisplayLine::Source { - lineno: None, - inline_marks: vec![], - line: DisplaySourceLine::Empty, - annotations: vec![], - }); - } else if let Some(DisplayLine::Source { .. }) = body.last() { - body.push(DisplayLine::Source { - lineno: None, - inline_marks: vec![], - line: DisplaySourceLine::Empty, - annotations: vec![], - }); - } - let max_line_num_len = if anonymized_line_numbers { - ANONYMIZED_LINE_NUM.len() - } else { - current_line.to_string().len() - }; - - let width_offset = cut_indicator.len() + max_line_num_len; - - if span_left_margin == usize::MAX { - span_left_margin = 0; - } - - let margin = Margin::new( - whitespace_margin, - span_left_margin, - span_right_margin, - label_right_margin, - term_width.saturating_sub(width_offset), - max_line_len, - ); - - DisplaySet { - display_lines: body, - margin, - } -} - -#[inline] -fn annotation_type_str(annotation_type: &DisplayAnnotationType) -> &'static str { - match annotation_type { - DisplayAnnotationType::Error => ERROR_TXT, - DisplayAnnotationType::Help => HELP_TXT, - DisplayAnnotationType::Info => INFO_TXT, - DisplayAnnotationType::Note => NOTE_TXT, - DisplayAnnotationType::Warning => WARNING_TXT, - DisplayAnnotationType::None => "", - } -} - -fn annotation_type_len(annotation_type: &DisplayAnnotationType) -> usize { - match annotation_type { - DisplayAnnotationType::Error => ERROR_TXT.len(), - DisplayAnnotationType::Help => HELP_TXT.len(), - DisplayAnnotationType::Info => INFO_TXT.len(), - DisplayAnnotationType::Note => NOTE_TXT.len(), - DisplayAnnotationType::Warning => WARNING_TXT.len(), - DisplayAnnotationType::None => 0, - } -} - -fn get_annotation_style<'a>( - annotation_type: &DisplayAnnotationType, - stylesheet: &'a Stylesheet, -) -> &'a Style { - match annotation_type { - DisplayAnnotationType::Error => stylesheet.error(), - DisplayAnnotationType::Warning => stylesheet.warning(), - DisplayAnnotationType::Info => stylesheet.info(), - DisplayAnnotationType::Note => stylesheet.note(), - DisplayAnnotationType::Help => stylesheet.help(), - DisplayAnnotationType::None => stylesheet.none(), - } -} - -#[inline] -fn is_annotation_empty(annotation: &Annotation<'_>) -> bool { - annotation - .label - .iter() - .all(|fragment| fragment.content.is_empty()) -} - -// We replace some characters so the CLI output is always consistent and underlines aligned. -const OUTPUT_REPLACEMENTS: &[(char, &str)] = &[ - ('\t', " "), // We do our own tab replacement - ('\u{200D}', ""), // Replace ZWJ with nothing for consistent terminal output of grapheme clusters. - ('\u{202A}', ""), // The following unicode text flow control characters are inconsistently - ('\u{202B}', ""), // supported across CLIs and can cause confusion due to the bytes on disk - ('\u{202D}', ""), // not corresponding to the visible source code, so we replace them always. - ('\u{202E}', ""), - ('\u{2066}', ""), - ('\u{2067}', ""), - ('\u{2068}', ""), - ('\u{202C}', ""), - ('\u{2069}', ""), -]; - -fn normalize_whitespace(str: &str) -> Cow<'_, str> { - // This is an optimization to avoid repeated `str::replace` calls in the typical case of no - // valid replacements. Note that this list needs to be kept in sync with `OUTPUT_REPLACEMENTS`. - if !str.contains([ - '\t', '\u{200d}', '\u{202a}', '\u{202b}', '\u{202d}', '\u{202e}', '\u{2066}', '\u{2067}', - '\u{2068}', '\u{202c}', '\u{2069}', - ]) { - return Cow::Borrowed(str); - } - - let mut s = str.to_owned(); - for (c, replacement) in OUTPUT_REPLACEMENTS { - s = s.replace(*c, replacement); - } - Cow::Owned(s) -} - -fn overlaps( - a1: &DisplaySourceAnnotation<'_>, - a2: &DisplaySourceAnnotation<'_>, - padding: usize, -) -> bool { - (a2.range.0..a2.range.1).contains(&a1.range.0) - || (a1.range.0..a1.range.1 + padding).contains(&a2.range.0) -} - -fn format_inline_marks( - line: usize, - inline_marks: &[DisplayMark], - lineno_width: usize, - stylesheet: &Stylesheet, - buf: &mut StyledBuffer, -) -> fmt::Result { - for mark in inline_marks.iter() { - let annotation_style = get_annotation_style(&mark.annotation_type, stylesheet); - match mark.mark_type { - DisplayMarkType::AnnotationThrough(depth) => { - buf.putc(line, 3 + lineno_width + depth, '|', *annotation_style); - } - } - } - Ok(()) -} - -fn char_width(c: char) -> Option { - if c == '\t' { - Some(4) - } else { - unicode_width::UnicodeWidthChar::width(c) - } -} - -pub(super) fn fmt_with_hyperlink<'a, T>( - content: T, - url: Option<&'a str>, - stylesheet: &Stylesheet, -) -> impl std::fmt::Display + 'a -where - T: std::fmt::Display + 'a, -{ - let url = if stylesheet.hyperlink { url } else { None }; - - fmt::from_fn(move |f| { - if let Some(url) = url { - write!(f, "\x1B]8;;{url}\x1B\\")?; - } - - content.fmt(f)?; - - if url.is_some() { - f.write_str("\x1B]8;;\x1B\\")?; - } - - Ok(()) - }) -} diff --git a/crates/ruff_annotate_snippets/src/renderer/margin.rs b/crates/ruff_annotate_snippets/src/renderer/margin.rs index 40e94e5048..32503f99e5 100644 --- a/crates/ruff_annotate_snippets/src/renderer/margin.rs +++ b/crates/ruff_annotate_snippets/src/renderer/margin.rs @@ -1,4 +1,4 @@ -use std::cmp::{max, min}; +use core::cmp::{max, min}; const ELLIPSIS_PASSING: usize = 6; const LONG_WHITESPACE: usize = 20; @@ -17,7 +17,7 @@ pub(crate) struct Margin { /// The end of the line to be displayed. computed_right: usize, /// The current width of the terminal. 140 by default and in tests. - term_width: usize, + pub(crate) term_width: usize, /// The end column of a span label, including the span. Doesn't account for labels not in the /// same line as the span. label_right: usize, @@ -41,15 +41,9 @@ impl Margin { // | ^^^^^^^^^ // ``` - let whitespace_left = whitespace_left.saturating_sub(ELLIPSIS_PASSING); - let span_left = span_left.saturating_sub(ELLIPSIS_PASSING); - let mut m = Margin { - // When an annotation points at leading whitespace (e.g. an indentation error), - // `whitespace_left` can exceed `span_left`. Clamp it so that trimming whitespace - // never hides the leftmost annotation. - whitespace_left: min(whitespace_left, span_left), - span_left, + whitespace_left: whitespace_left.saturating_sub(ELLIPSIS_PASSING), + span_left: span_left.saturating_sub(ELLIPSIS_PASSING), span_right: span_right + ELLIPSIS_PASSING, computed_left: 0, computed_right: 0, @@ -77,7 +71,12 @@ impl Margin { if self.computed_right - self.computed_left > self.term_width { // Trimming only whitespace isn't enough, let's get craftier. - if self.label_right - self.whitespace_left <= self.term_width { + if self.label_right.saturating_sub(self.whitespace_left) <= self.term_width + // Trimming whitespace when the right-most label is somewhrere + // within it would result in the label pointing to the wrong + // place + && self.label_right >= self.whitespace_left + { // Attempt to fit the code window only trimming whitespace. self.computed_left = self.whitespace_left; self.computed_right = self.computed_left + self.term_width; diff --git a/crates/ruff_annotate_snippets/src/renderer/mod.rs b/crates/ruff_annotate_snippets/src/renderer/mod.rs index a48af545b6..78db9b993c 100644 --- a/crates/ruff_annotate_snippets/src/renderer/mod.rs +++ b/crates/ruff_annotate_snippets/src/renderer/mod.rs @@ -1,37 +1,118 @@ -//! The renderer for [`Message`]s +//! The [Renderer] and its settings //! //! # Example +//! //! ``` -//! use ruff_annotate_snippets::{Renderer, Snippet, Level}; -//! let snippet = Level::Error.title("mismatched types") -//! .snippet(Snippet::source("Foo").line_start(51).origin("src/format.rs")) -//! .snippet(Snippet::source("Faa").line_start(129).origin("src/display.rs")); +//! # use annotate_snippets::*; +//! # use annotate_snippets::renderer::*; +//! # use annotate_snippets::Level; +//! let report = // ... +//! # &[Group::with_title( +//! # Level::ERROR +//! # .primary_title("unresolved import `baz::zed`") +//! # .id("E0432") +//! # )]; //! -//! let renderer = Renderer::styled(); -//! println!("{}", renderer.render(snippet)); +//! let renderer = Renderer::styled().decor_style(DecorStyle::Unicode); +//! let output = renderer.render(report); +//! anstream::println!("{output}"); //! ``` -mod display_list; +pub(crate) mod render; +pub(crate) mod source_map; +pub(crate) mod stylesheet; + mod margin; mod styled_buffer; -pub(crate) mod stylesheet; -use crate::snippet::Message; +use alloc::string::String; + +use crate::Report; + +pub(crate) use render::ElementStyle; +pub(crate) use render::UnderlineParts; +pub(crate) use render::normalize_whitespace; +pub(crate) use render::{LineAnnotation, LineAnnotationType, char_width, num_overlap}; +pub(crate) use stylesheet::Stylesheet; + pub use anstyle::*; -use display_list::DisplayList; -use margin::Margin; -use std::fmt::Display; -use stylesheet::Stylesheet; +/// See [`Renderer::term_width`] pub const DEFAULT_TERM_WIDTH: usize = 140; -/// A renderer for [`Message`]s +const USE_WINDOWS_COLORS: bool = cfg!(windows) && !cfg!(feature = "testing-colors"); +const BRIGHT_BLUE: Style = if USE_WINDOWS_COLORS { + AnsiColor::BrightCyan.on_default() +} else { + AnsiColor::BrightBlue.on_default() +}; +/// [`Renderer::error`] applied by [`Renderer::styled`] +pub const DEFAULT_ERROR_STYLE: Style = AnsiColor::BrightRed.on_default().effects(Effects::BOLD); +/// [`Renderer::warning`] applied by [`Renderer::styled`] +pub const DEFAULT_WARNING_STYLE: Style = if USE_WINDOWS_COLORS { + AnsiColor::BrightYellow.on_default() +} else { + AnsiColor::Yellow.on_default() +} +.effects(Effects::BOLD); +/// [`Renderer::info`] applied by [`Renderer::styled`] +pub const DEFAULT_INFO_STYLE: Style = BRIGHT_BLUE.effects(Effects::BOLD); +/// [`Renderer::note`] applied by [`Renderer::styled`] +pub const DEFAULT_NOTE_STYLE: Style = AnsiColor::BrightGreen.on_default().effects(Effects::BOLD); +/// [`Renderer::help`] applied by [`Renderer::styled`] +pub const DEFAULT_HELP_STYLE: Style = AnsiColor::BrightCyan.on_default().effects(Effects::BOLD); +/// [`Renderer::line_num`] applied by [`Renderer::styled`] +pub const DEFAULT_LINE_NUM_STYLE: Style = BRIGHT_BLUE.effects(Effects::BOLD); +/// [`Renderer::emphasis`] applied by [`Renderer::styled`] +pub const DEFAULT_EMPHASIS_STYLE: Style = if USE_WINDOWS_COLORS { + AnsiColor::BrightWhite.on_default() +} else { + Style::new() +} +.effects(Effects::BOLD); +/// [`Renderer::none`] applied by [`Renderer::styled`] +pub const DEFAULT_NONE_STYLE: Style = Style::new(); +/// [`Renderer::context`] applied by [`Renderer::styled`] +pub const DEFAULT_CONTEXT_STYLE: Style = BRIGHT_BLUE.effects(Effects::BOLD); +/// [`Renderer::addition`] applied by [`Renderer::styled`] +pub const DEFAULT_ADDITION_STYLE: Style = AnsiColor::BrightGreen.on_default(); +/// [`Renderer::removal`] applied by [`Renderer::styled`] +pub const DEFAULT_REMOVAL_STYLE: Style = AnsiColor::BrightRed.on_default(); + +/// The [Renderer] for a [`Report`] +/// +/// The caller is expected to detect any relevant terminal features and configure the renderer, +/// including +/// - ANSI Escape code support (always outputted with [`Renderer::styled`]) +/// - Terminal width ([`Renderer::term_width`]) +/// - Unicode support ([`Renderer::decor_style`]) +/// +/// # Example +/// +/// ``` +/// # use annotate_snippets::*; +/// # use annotate_snippets::renderer::*; +/// # use annotate_snippets::Level; +/// let report = // ... +/// # &[Group::with_title( +/// # Level::ERROR +/// # .primary_title("unresolved import `baz::zed`") +/// # .id("E0432") +/// # )]; +/// +/// let renderer = Renderer::styled(); +/// let output = renderer.render(report); +/// anstream::println!("{output}"); +/// ``` #[derive(Clone, Debug)] pub struct Renderer { anonymized_line_numbers: bool, term_width: usize, + decor_style: DecorStyle, stylesheet: Stylesheet, - cut_indicator: &'static str, + hyperlink: bool, + short_message: bool, + cut_indicator: Option<&'static str>, } impl Renderer { @@ -40,57 +121,71 @@ impl Renderer { Self { anonymized_line_numbers: false, term_width: DEFAULT_TERM_WIDTH, + decor_style: DecorStyle::Ascii, stylesheet: Stylesheet::plain(), - cut_indicator: "...", + hyperlink: false, + short_message: false, + cut_indicator: None, } } /// Default terminal styling /// + /// If ANSI escape codes are not supported, either + /// - Call [`Renderer::plain`] instead + /// - Strip them after the fact, like with [`anstream`](https://docs.rs/anstream/latest/anstream/) + /// /// # Note + /// /// When testing styled terminal output, see the [`testing-colors` feature](crate#features) pub const fn styled() -> Self { - const USE_WINDOWS_COLORS: bool = cfg!(windows) && !cfg!(feature = "testing-colors"); - const BRIGHT_BLUE: Style = if USE_WINDOWS_COLORS { - AnsiColor::BrightCyan.on_default() - } else { - AnsiColor::BrightBlue.on_default() - }; Self { stylesheet: Stylesheet { - error: AnsiColor::BrightRed.on_default().effects(Effects::BOLD), - warning: if USE_WINDOWS_COLORS { - AnsiColor::BrightYellow.on_default() - } else { - 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: if USE_WINDOWS_COLORS { - AnsiColor::BrightWhite.on_default() - } else { - Style::new() - } - .effects(Effects::BOLD), - none: Style::new(), - hyperlink: true, + error: DEFAULT_ERROR_STYLE, + warning: DEFAULT_WARNING_STYLE, + info: DEFAULT_INFO_STYLE, + note: DEFAULT_NOTE_STYLE, + help: DEFAULT_HELP_STYLE, + line_num: DEFAULT_LINE_NUM_STYLE, + emphasis: DEFAULT_EMPHASIS_STYLE, + none: DEFAULT_NONE_STYLE, + context: DEFAULT_CONTEXT_STYLE, + addition: DEFAULT_ADDITION_STYLE, + removal: DEFAULT_REMOVAL_STYLE, }, + hyperlink: true, ..Self::plain() } } + /// Abbreviate the message + pub const fn short_message(mut self, short_message: bool) -> Self { + self.short_message = short_message; + self + } + + /// Set the width to render within + /// + /// Affects the rendering of [`Snippet`][crate::Snippet]s + pub const fn term_width(mut self, term_width: usize) -> Self { + self.term_width = term_width; + self + } + + /// Set the character set used for rendering decor + pub const fn decor_style(mut self, decor_style: DecorStyle) -> Self { + self.decor_style = decor_style; + self + } + /// Anonymize line numbers /// - /// This enables (or disables) line number anonymization. When enabled, line numbers are replaced - /// with `LL`. + /// When enabled, line numbers are replaced with `LL` which is useful for tests. /// /// # Example /// /// ```text - /// --> $DIR/whitespace-trimming.rs:LL:193 + /// --> $DIR/whitespace-trimming.rs:4:193 /// | /// LL | ... let _: () = 42; /// | ^^ expected (), found integer @@ -100,82 +195,252 @@ impl Renderer { self.anonymized_line_numbers = anonymized_line_numbers; self } +} - /// Set the terminal width - pub const fn term_width(mut self, term_width: usize) -> Self { - self.term_width = term_width; - self +impl Renderer { + /// Render a diagnostic [`Report`] + pub fn render(&self, groups: Report<'_>) -> String { + render::render(self, groups) } +} - /// Set the output style for `error` +/// Customize [`Renderer::styled`] +impl Renderer { + /// Override the output style for [error][crate::Level::ERROR] pub const fn error(mut self, style: Style) -> Self { self.stylesheet.error = style; self } - /// Set the output style for `warning` + /// Override the output style for [warnings][crate::Level::WARNING] pub const fn warning(mut self, style: Style) -> Self { self.stylesheet.warning = style; self } - /// Set the output style for `info` + /// Override the output style for [info][crate::Level::INFO] pub const fn info(mut self, style: Style) -> Self { self.stylesheet.info = style; self } - /// Set the output style for `note` + /// Override the output style for [notes][crate::Level::NOTE] pub const fn note(mut self, style: Style) -> Self { self.stylesheet.note = style; self } - /// Set the output style for `help` + /// Override the output style for [help][crate::Level::HELP] pub const fn help(mut self, style: Style) -> Self { self.stylesheet.help = style; self } - /// Set the output style for line numbers - pub const fn line_no(mut self, style: Style) -> Self { - self.stylesheet.line_no = style; + /// Override the output style for line numbers in the [`Snippet`][crate::Snippet] gutter + pub const fn line_num(mut self, style: Style) -> Self { + self.stylesheet.line_num = style; self } - /// Set the output style for emphasis + /// Override the output style for emphasis for the + /// [`primary_title`][crate::Level::primary_title] pub const fn emphasis(mut self, style: Style) -> Self { self.stylesheet.emphasis = style; self } - /// Set the output style for none + /// Override the output style for [`AnnotationKind::Context`][crate::AnnotationKind::Context] + pub const fn context(mut self, style: Style) -> Self { + self.stylesheet.context = style; + self + } + + /// Override the output style for [`Patch`][crate::Patch] additions + pub const fn addition(mut self, style: Style) -> Self { + self.stylesheet.addition = style; + self + } + + /// Override the output style for [`Patch`][crate::Patch] removals + pub const fn removal(mut self, style: Style) -> Self { + self.stylesheet.removal = style; + self + } + + /// Override the output style for all other text pub const fn none(mut self, style: Style) -> Self { self.stylesheet.none = style; self } pub const fn hyperlink(mut self, hyperlink: bool) -> Self { - self.stylesheet.hyperlink = hyperlink; + self.hyperlink = hyperlink; self } /// Set the string used for when a long line is cut. /// - /// The default is `...` (three `U+002E` characters). - pub const fn cut_indicator(mut self, string: &'static str) -> Self { - self.cut_indicator = string; + /// The default for [`DecorStyle::Ascii`] is `...` (three `U+002E` characters). + pub const fn cut_indicator(mut self, cut: &'static str) -> Self { + self.cut_indicator = Some(cut); self } +} + +/// The character set for rendering for decor +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DecorStyle { + Ascii, + Unicode, +} + +impl DecorStyle { + fn col_separator(&self) -> char { + match self { + DecorStyle::Ascii => '|', + DecorStyle::Unicode => '│', + } + } + + fn note_separator(&self, is_cont: bool) -> &str { + match self { + DecorStyle::Ascii => "= ", + DecorStyle::Unicode if is_cont => "├ ", + DecorStyle::Unicode => "╰ ", + } + } + + fn multi_suggestion_separator(&self) -> &'static str { + match self { + DecorStyle::Ascii => "|", + DecorStyle::Unicode => "├╴", + } + } + + fn file_start(&self, is_first: bool, alone: bool) -> &'static str { + match self { + DecorStyle::Ascii => "--> ", + DecorStyle::Unicode if is_first && alone => " ─▸ ", + DecorStyle::Unicode if is_first => " ╭▸ ", + DecorStyle::Unicode => " ├▸ ", + } + } - /// Render a snippet into a `Display`able object - pub fn render<'a>(&'a self, msg: Message<'a>) -> impl Display + 'a { - DisplayList::new( - msg, - &self.stylesheet, - self.anonymized_line_numbers, - self.term_width, - self.cut_indicator, - ) + fn secondary_file_start(&self) -> &'static str { + match self { + DecorStyle::Ascii => "::: ", + DecorStyle::Unicode => " ⸬ ", + } + } + + fn diff(&self) -> char { + match self { + DecorStyle::Ascii => '~', + DecorStyle::Unicode => '±', + } + } + + fn margin(&self) -> &'static str { + match self { + DecorStyle::Ascii => "...", + DecorStyle::Unicode => "…", + } + } + + fn underline(&self, is_primary: bool) -> UnderlineParts { + // X0 Y0 + // label_start > ┯━━━━ < underline + // │ < vertical_text_line + // text + + // multiline_start_down ⤷ X0 Y0 + // top_left > ┌───╿──┘ < top_right_flat + // top_left > ┏│━━━┙ < top_right + // multiline_vertical > ┃│ + // ┃│ X1 Y1 + // ┃│ X2 Y2 + // ┃└────╿──┘ < multiline_end_same_line + // bottom_left > ┗━━━━━┥ < bottom_right_with_text + // multiline_horizontal ^ `X` is a good letter + + // multiline_whole_line > ┏ X0 Y0 + // ┃ X1 Y1 + // ┗━━━━┛ < multiline_end_same_line + + // multiline_whole_line > ┏ X0 Y0 + // ┃ X1 Y1 + // ┃ ╿ < multiline_end_up + // ┗━━┛ < bottom_right + + match (self, is_primary) { + (DecorStyle::Ascii, true) => UnderlineParts { + style: ElementStyle::UnderlinePrimary, + underline: '^', + label_start: '^', + vertical_text_line: '|', + multiline_vertical: '|', + multiline_horizontal: '_', + multiline_whole_line: '/', + multiline_start_down: '^', + bottom_right: '|', + top_left: ' ', + top_right_flat: '^', + bottom_left: '|', + multiline_end_up: '^', + multiline_end_same_line: '^', + multiline_bottom_right_with_text: '|', + }, + (DecorStyle::Ascii, false) => UnderlineParts { + style: ElementStyle::UnderlineSecondary, + underline: '-', + label_start: '-', + vertical_text_line: '|', + multiline_vertical: '|', + multiline_horizontal: '_', + multiline_whole_line: '/', + multiline_start_down: '-', + bottom_right: '|', + top_left: ' ', + top_right_flat: '-', + bottom_left: '|', + multiline_end_up: '-', + multiline_end_same_line: '-', + multiline_bottom_right_with_text: '|', + }, + (DecorStyle::Unicode, true) => UnderlineParts { + style: ElementStyle::UnderlinePrimary, + underline: '━', + label_start: '┯', + vertical_text_line: '│', + multiline_vertical: '┃', + multiline_horizontal: '━', + multiline_whole_line: '┏', + multiline_start_down: '╿', + bottom_right: '┙', + top_left: '┏', + top_right_flat: '┛', + bottom_left: '┗', + multiline_end_up: '╿', + multiline_end_same_line: '┛', + multiline_bottom_right_with_text: '┥', + }, + (DecorStyle::Unicode, false) => UnderlineParts { + style: ElementStyle::UnderlineSecondary, + underline: '─', + label_start: '┬', + vertical_text_line: '│', + multiline_vertical: '│', + multiline_horizontal: '─', + multiline_whole_line: '┌', + multiline_start_down: '│', + bottom_right: '┘', + top_left: '┌', + top_right_flat: '┘', + bottom_left: '└', + multiline_end_up: '│', + multiline_end_same_line: '┘', + multiline_bottom_right_with_text: '┤', + }, + } } } diff --git a/crates/ruff_annotate_snippets/src/renderer/render.rs b/crates/ruff_annotate_snippets/src/renderer/render.rs new file mode 100644 index 0000000000..877e94185d --- /dev/null +++ b/crates/ruff_annotate_snippets/src/renderer/render.rs @@ -0,0 +1,2915 @@ +// Most of this file is adapted from https://github.com/rust-lang/rust/blob/160905b6253f42967ed4aef4b98002944c7df24c/compiler/rustc_errors/src/emitter.rs + +use alloc::borrow::Cow; +use alloc::collections::BTreeMap; +use alloc::string::{String, ToString}; +use alloc::{format, vec, vec::Vec}; +use core::cmp::{Ordering, Reverse, max, min}; +use core::fmt; + +use anstyle::Style; + +use super::DecorStyle; +use super::Renderer; +use super::margin::Margin; +use super::stylesheet::Stylesheet; +use crate::level::{Level, LevelInner}; +use crate::renderer::source_map::{ + AnnotatedLineInfo, LineInfo, Loc, SourceMap, SplicedLines, SubstitutionHighlight, TrimmedPatch, +}; +use crate::renderer::styled_buffer::StyledBuffer; +use crate::snippet::Id; +use crate::{ + Annotation, AnnotationKind, Element, Group, Message, Origin, Padding, Patch, Report, Snippet, + Title, +}; + +const ANONYMIZED_LINE_NUM: &str = "LL"; + +pub(crate) fn render(renderer: &Renderer, groups: Report<'_>) -> String { + if renderer.short_message { + render_short_message(renderer, groups).unwrap() + } else { + let lineno_offset = groups.iter().map(|g| g.lineno_offset).max().unwrap_or(0); + let (max_line_num, og_primary_path, groups) = pre_process(groups); + let max_line_num_len = lineno_offset + + if renderer.anonymized_line_numbers { + ANONYMIZED_LINE_NUM.len() + } else { + num_decimal_digits(max_line_num) + }; + let mut out_string = String::new(); + let group_len = groups.len(); + for ( + g, + PreProcessedGroup { + group, + elements, + primary_path, + max_depth, + }, + ) in groups.into_iter().enumerate() + { + let mut buffer = StyledBuffer::new(); + let level = group.primary_level.clone(); + let mut message_iter = elements.into_iter().enumerate().peekable(); + if let Some(title) = &group.title { + let peek = message_iter.peek().map(|(_, s)| s); + let title_style = if title.allows_styling { + TitleStyle::Header + } else { + TitleStyle::MainHeader + }; + let buffer_msg_line_offset = buffer.num_lines(); + render_title( + renderer, + &mut buffer, + title, + max_line_num_len, + title_style, + matches!(peek, Some(PreProcessedElement::Message(_))), + buffer_msg_line_offset, + ); + let buffer_msg_line_offset = buffer.num_lines(); + + if matches!(peek, Some(PreProcessedElement::Message(_))) { + draw_col_separator_no_space( + renderer, + &mut buffer, + buffer_msg_line_offset, + max_line_num_len + 1, + ); + } + if peek.is_none() + && title_style == TitleStyle::MainHeader + && g == 0 + && group_len > 1 + { + draw_col_separator_end( + renderer, + &mut buffer, + buffer_msg_line_offset, + max_line_num_len + 1, + ); + } + } + let mut seen_primary = false; + let mut last_suggestion_path = None; + while let Some((i, section)) = message_iter.next() { + let peek = message_iter.peek().map(|(_, s)| s); + let is_first = i == 0; + match section { + PreProcessedElement::Message(title) => { + let title_style = TitleStyle::Secondary; + let buffer_msg_line_offset = buffer.num_lines(); + render_title( + renderer, + &mut buffer, + title, + max_line_num_len, + title_style, + peek.is_some(), + buffer_msg_line_offset, + ); + } + PreProcessedElement::Cause((cause, source_map, annotated_lines)) => { + let is_primary = primary_path == cause.path.as_ref() && !seen_primary; + seen_primary |= is_primary; + render_snippet_annotations( + renderer, + &mut buffer, + max_line_num_len, + cause, + is_primary, + &source_map, + &annotated_lines, + max_depth, + peek.is_some() || (g == 0 && group_len > 1), + is_first, + ); + + if g == 0 { + let current_line = buffer.num_lines(); + match peek { + Some(PreProcessedElement::Message(_)) => { + draw_col_separator_no_space( + renderer, + &mut buffer, + current_line, + max_line_num_len + 1, + ); + } + None if group_len > 1 => draw_col_separator_end( + renderer, + &mut buffer, + current_line, + max_line_num_len + 1, + ), + _ => {} + } + } + } + PreProcessedElement::Suggestion(( + suggestion, + source_map, + spliced_lines, + display_suggestion, + )) => { + let matches_previous_suggestion = last_suggestion_path + == Some((Some(suggestion.path.as_ref()), suggestion.cell_index)); + emit_suggestion_default( + renderer, + &mut buffer, + suggestion, + spliced_lines, + display_suggestion, + max_line_num_len, + &source_map, + primary_path.or(og_primary_path), + matches_previous_suggestion, + is_first, + //matches!(peek, Some(Element::Message(_) | Element::Padding(_))), + peek.is_some(), + ); + + if matches!(peek, Some(PreProcessedElement::Suggestion(_))) { + last_suggestion_path = + Some((Some(suggestion.path.as_ref()), suggestion.cell_index)); + } else { + last_suggestion_path = None; + } + } + + PreProcessedElement::Origin(origin) => { + let buffer_msg_line_offset = buffer.num_lines(); + let is_primary = primary_path == origin.path.as_ref() && !seen_primary; + seen_primary |= is_primary; + render_origin( + renderer, + &mut buffer, + max_line_num_len, + origin, + is_primary, + is_first, + peek.is_none(), + buffer_msg_line_offset, + ); + let current_line = buffer.num_lines(); + if g == 0 && peek.is_none() && group_len > 1 { + draw_col_separator_end( + renderer, + &mut buffer, + current_line, + max_line_num_len + 1, + ); + } + } + PreProcessedElement::Padding(_) => { + let current_line = buffer.num_lines(); + if peek.is_none() { + draw_col_separator_end( + renderer, + &mut buffer, + current_line, + max_line_num_len + 1, + ); + } else { + draw_col_separator_no_space( + renderer, + &mut buffer, + current_line, + max_line_num_len + 1, + ); + } + } + } + } + buffer + .render(&level, &renderer.stylesheet, &mut out_string) + .unwrap(); + if g != group_len - 1 { + out_string.push('\n'); + } + } + out_string + } +} + +fn render_short_message(renderer: &Renderer, groups: &[Group<'_>]) -> Result { + let mut buffer = StyledBuffer::new(); + let mut labels = None; + let group = groups.first().expect("Expected at least one group"); + + let Some(title) = &group.title else { + panic!("Expected a Title"); + }; + + if let Some(Element::Cause(cause)) = group + .elements + .iter() + .find(|e| matches!(e, Element::Cause(_))) + { + let labels_inner = cause + .markers + .iter() + .filter_map(|ann| match &ann.label { + Some(msg) if ann.kind.is_primary() => { + if !msg.trim().is_empty() { + Some(msg.to_string()) + } else { + None + } + } + _ => None, + }) + .collect::>() + .join(", "); + if !labels_inner.is_empty() { + labels = Some(labels_inner); + } + + if let Some(path) = &cause.path { + let mut origin = Origin::path(path.as_ref()).cell_index(cause.cell_index); + + let source_map = SourceMap::new(&cause.source, cause.line_start); + let (_depth, annotated_lines) = + source_map.annotated_lines(cause.markers.clone(), cause.fold); + + if let Some(primary_line) = annotated_lines + .iter() + .find(|l| l.annotations.iter().any(LineAnnotation::is_primary)) + .or(annotated_lines.iter().find(|l| !l.annotations.is_empty())) + { + origin.line = Some(primary_line.line_index); + if let Some(first_annotation) = primary_line + .annotations + .iter() + .min_by_key(|a| (Reverse(a.is_primary()), a.start.char)) + { + origin.char_column = Some(first_annotation.start.char + 1); + } + } + + render_origin(renderer, &mut buffer, 0, &origin, true, true, true, 0); + buffer.append(0, ": ", ElementStyle::LineAndColumn); + } + } + + render_title( + renderer, + &mut buffer, + title, + 0, // No line numbers in short messages + TitleStyle::MainHeader, + false, + 0, + ); + + if let Some(labels) = labels { + buffer.append(0, &format!(": {labels}"), ElementStyle::NoStyle); + } + + let mut out_string = String::new(); + buffer.render(&title.level, &renderer.stylesheet, &mut out_string)?; + + Ok(out_string) +} + +#[allow(clippy::too_many_arguments)] +fn render_title( + renderer: &Renderer, + buffer: &mut StyledBuffer, + title: &dyn MessageOrTitle, + max_line_num_len: usize, + title_style: TitleStyle, + is_cont: bool, + buffer_msg_line_offset: usize, +) { + let (label_style, title_element_style) = match title_style { + TitleStyle::MainHeader => ( + ElementStyle::Level(title.level().level), + if renderer.short_message { + ElementStyle::NoStyle + } else { + ElementStyle::MainHeaderMsg + }, + ), + TitleStyle::Header => ( + ElementStyle::Level(title.level().level), + ElementStyle::HeaderMsg, + ), + TitleStyle::Secondary => { + for _ in 0..max_line_num_len { + buffer.append(buffer_msg_line_offset, " ", ElementStyle::NoStyle); + } + + draw_note_separator( + renderer, + buffer, + buffer_msg_line_offset, + max_line_num_len + 1, + is_cont, + ); + (ElementStyle::MainHeaderMsg, ElementStyle::NoStyle) + } + }; + let mut label_width = 0; + + if title.level().name != Some(None) { + buffer.append(buffer_msg_line_offset, title.level().as_str(), label_style); + label_width += title.level().as_str().len(); + if let Some(Id { id: Some(id), url }) = &title.id() { + buffer.append(buffer_msg_line_offset, "[", label_style); + if renderer.hyperlink + && let Some(url) = url.as_ref() + { + buffer.append( + buffer_msg_line_offset, + &format!("\x1B]8;;{url}\x1B\\"), + label_style, + ); + } + buffer.append(buffer_msg_line_offset, id, label_style); + if renderer.hyperlink && url.is_some() { + buffer.append(buffer_msg_line_offset, "\x1B]8;;\x1B\\", label_style); + } + buffer.append(buffer_msg_line_offset, "]", label_style); + label_width += 2 + id.len(); + } + if title.is_fixable() { + buffer.append(buffer_msg_line_offset, "[", ElementStyle::NoStyle); + buffer.append( + buffer_msg_line_offset, + "*", + ElementStyle::Level(LevelInner::Help), + ); + buffer.append(buffer_msg_line_offset, "]", ElementStyle::NoStyle); + label_width += 3; + } + buffer.append(buffer_msg_line_offset, ": ", title_element_style); + label_width += 2; + } else { + if let Some(Id { id: Some(id), url }) = &title.id() { + if renderer.hyperlink + && let Some(url) = url.as_ref() + { + buffer.append( + buffer_msg_line_offset, + &format!("\x1B]8;;{url}\x1B\\"), + label_style, + ); + } + buffer.append(buffer_msg_line_offset, id, label_style); + if renderer.hyperlink && url.is_some() { + buffer.append(buffer_msg_line_offset, "\x1B]8;;\x1B\\", label_style); + } + label_width += id.len(); + if title.is_fixable() { + buffer.append(buffer_msg_line_offset, " [", ElementStyle::NoStyle); + buffer.append( + buffer_msg_line_offset, + "*", + ElementStyle::Level(LevelInner::Help), + ); + buffer.append(buffer_msg_line_offset, "]", ElementStyle::NoStyle); + label_width += 4; + } + buffer.append(buffer_msg_line_offset, " ", title_element_style); + label_width += 1; + } + } + + let padding = " ".repeat(if title_style == TitleStyle::Secondary { + // The extra 3 ` ` is padding that's always needed to align to the + // label i.e. `note: `: + // + // error: message + // --> file.rs:13:20 + // | + // 13 | + // | ^^^^ + // | + // = note: multiline + // message + // ++^^^------ + // | | | + // | | | + // | | width of label + // | magic `3` + // `max_line_num_len` + max_line_num_len + 3 + label_width + } else { + label_width + }); + + let (title_str, style) = if title.allows_styling() { + (Cow::Borrowed(title.text()), ElementStyle::NoStyle) + } else { + (normalize_whitespace(title.text()), title_element_style) + }; + for (i, text) in title_str.split('\n').enumerate() { + #[allow(clippy::collapsible_if, reason = "reduce upstream divergence")] + if i != 0 { + if title_style == TitleStyle::Secondary + && is_cont + && matches!(renderer.decor_style, DecorStyle::Unicode) + { + buffer.append(buffer_msg_line_offset + i, &padding, ElementStyle::NoStyle); + // There's another note after this one, associated to the subwindow above. + // We write additional vertical lines to join them: + // ╭▸ test.rs:3:3 + // │ + // 3 │ code + // │ ━━━━ + // │ + // ├ note: foo + // │ bar + // ╰ note: foo + // bar + draw_col_separator_no_space( + renderer, + buffer, + buffer_msg_line_offset + i, + max_line_num_len + 1, + ); + } + } + buffer.append(buffer_msg_line_offset + i, text, style); + } +} + +#[allow(clippy::too_many_arguments)] +fn render_origin( + renderer: &Renderer, + buffer: &mut StyledBuffer, + max_line_num_len: usize, + origin: &Origin<'_>, + is_primary: bool, + is_first: bool, + alone: bool, + buffer_msg_line_offset: usize, +) { + if !renderer.short_message { + for _ in 0..max_line_num_len { + buffer.append(buffer_msg_line_offset, " ", ElementStyle::NoStyle); + } + } + + if is_primary && !renderer.short_message { + buffer.append( + buffer_msg_line_offset, + renderer.decor_style.file_start(is_first, alone), + ElementStyle::LineNumber, + ); + } else if !renderer.short_message { + // if !origin.standalone { + // // Add spacing line, as shown: + // // --> $DIR/file:54:15 + // // | + // // LL | code + // // | ^^^^ + // // | (<- It prints *this* line) + // // ::: $DIR/other_file.rs:15:5 + // // | + // // LL | code + // // | ---- + // draw_col_separator_no_space(renderer, + // buffer, + // buffer_msg_line_offset, + // max_line_num_len + 1, + // ); + // + // buffer_msg_line_offset += 1; + // } + // Then, the secondary file indicator + buffer.append( + buffer_msg_line_offset, + renderer.decor_style.secondary_file_start(), + ElementStyle::LineNumber, + ); + } + + let str = format_origin(origin, renderer.anonymized_line_numbers); + buffer.append(buffer_msg_line_offset, &str, ElementStyle::LineAndColumn); +} + +fn format_origin(origin: &Origin<'_>, anonymized_line_numbers: bool) -> String { + use core::fmt::Write as _; + + let mut buffer = String::new(); + if let Some(path) = &origin.path { + write!(&mut buffer, "{path}").unwrap(); + } + if let Some(cell_index) = origin.cell_index { + if !buffer.is_empty() { + write!(&mut buffer, ":").unwrap(); + } + write!(&mut buffer, "cell {cell_index}").unwrap(); + } + if let Some(line) = origin.line { + if anonymized_line_numbers { + write!(&mut buffer, ":{ANONYMIZED_LINE_NUM}").unwrap(); + } else { + write!(&mut buffer, ":{line}").unwrap(); + } + if let Some(col) = origin.char_column { + write!(&mut buffer, ":{col}").unwrap(); + } + } + buffer +} + +#[allow(clippy::too_many_arguments)] +fn render_snippet_annotations( + renderer: &Renderer, + buffer: &mut StyledBuffer, + max_line_num_len: usize, + snippet: &Snippet<'_, Annotation<'_>>, + is_primary: bool, + sm: &SourceMap<'_>, + annotated_lines: &[AnnotatedLineInfo<'_>], + multiline_depth: usize, + is_cont: bool, + is_first: bool, +) { + let show_snippet = !snippet.markers.iter().any(|s| s.is_file_level); + + if let Some(path) = &snippet.path { + let mut origin = Origin::path(path.as_ref()).cell_index(snippet.cell_index); + // print out the span location and spacer before we print the annotated source + // to do this, we need to know if this span will be primary + //let is_primary = primary_path == Some(&origin.path); + + if is_primary { + if let Some(primary_line) = annotated_lines + .iter() + .find(|l| l.annotations.iter().any(LineAnnotation::is_primary)) + .or(annotated_lines.iter().find(|l| !l.annotations.is_empty())) + { + origin.line = Some(primary_line.line_index); + if let Some(first_annotation) = primary_line + .annotations + .iter() + .min_by_key(|a| (Reverse(a.is_primary()), a.start.char)) + { + origin.char_column = Some(first_annotation.start.char + 1); + } + } + } else { + let buffer_msg_line_offset = buffer.num_lines(); + // Add spacing line, as shown: + // --> $DIR/file:54:15 + // | + // LL | code + // | ^^^^ + // | (<- It prints *this* line) + // ::: $DIR/other_file.rs:15:5 + // | + // LL | code + // | ---- + draw_col_separator_no_space( + renderer, + buffer, + buffer_msg_line_offset, + max_line_num_len + 1, + ); + if let Some(first_line) = annotated_lines + .iter() + .find(|l| !l.annotations.is_empty()) + .or(annotated_lines.first()) + { + origin.line = Some(first_line.line_index); + if let Some(first_annotation) = first_line.annotations.first() { + origin.char_column = Some(first_annotation.start.char + 1); + } + } + } + let buffer_msg_line_offset = buffer.num_lines(); + render_origin( + renderer, + buffer, + max_line_num_len, + &origin, + is_primary, + is_first, + !(show_snippet || is_cont), + buffer_msg_line_offset, + ); + // Put in the spacer between the location and annotated source + if show_snippet { + draw_col_separator_no_space( + renderer, + buffer, + buffer_msg_line_offset + 1, + max_line_num_len + 1, + ); + } + } else { + let buffer_msg_line_offset = buffer.num_lines(); + if is_primary { + if renderer.decor_style == DecorStyle::Unicode { + buffer.puts( + buffer_msg_line_offset, + max_line_num_len, + renderer.decor_style.file_start(is_first, false), + ElementStyle::LineNumber, + ); + } else { + draw_col_separator_no_space( + renderer, + buffer, + buffer_msg_line_offset, + max_line_num_len + 1, + ); + } + } else { + // Add spacing line, as shown: + // --> $DIR/file:54:15 + // | + // LL | code + // | ^^^^ + // | (<- It prints *this* line) + // ::: $DIR/other_file.rs:15:5 + // | + // LL | code + // | ---- + draw_col_separator_no_space( + renderer, + buffer, + buffer_msg_line_offset, + max_line_num_len + 1, + ); + + buffer.puts( + buffer_msg_line_offset + 1, + max_line_num_len, + renderer.decor_style.secondary_file_start(), + ElementStyle::LineNumber, + ); + } + } + + if !show_snippet { + return; + } + + // Contains the vertical lines' positions for active multiline annotations + let mut multilines = Vec::new(); + + // Get the left-side margin to remove it + let mut whitespace_margin = usize::MAX; + for line_info in annotated_lines { + let leading_whitespace = line_info + .line + .chars() + .take_while(|c| c.is_whitespace()) + .map(|c| { + match c { + // Tabs are displayed as 4 spaces + '\t' => 4, + _ => 1, + } + }) + .sum(); + if line_info.line.chars().any(|c| !c.is_whitespace()) { + whitespace_margin = min(whitespace_margin, leading_whitespace); + } + } + if whitespace_margin == usize::MAX { + whitespace_margin = 0; + } + + // Left-most column any visible span points at. + let mut span_left_margin = usize::MAX; + for line_info in annotated_lines { + for ann in &line_info.annotations { + span_left_margin = min(span_left_margin, ann.start.display); + span_left_margin = min(span_left_margin, ann.end.display); + } + } + if span_left_margin == usize::MAX { + span_left_margin = 0; + } + + // Right-most column any visible span points at. + let mut span_right_margin = 0; + let mut label_right_margin = 0; + let mut max_line_len = 0; + for line_info in annotated_lines { + max_line_len = max(max_line_len, str_width(line_info.line)); + for ann in &line_info.annotations { + span_right_margin = max(span_right_margin, ann.start.display); + span_right_margin = max(span_right_margin, ann.end.display); + // FIXME: account for labels not in the same line + let label_right = ann.label.as_ref().map_or(0, |l| str_width(l) + 1); + label_right_margin = max(label_right_margin, ann.end.display + label_right); + } + } + let width_offset = 3 + max_line_num_len; + let code_offset = if multiline_depth == 0 { + width_offset + } else { + width_offset + multiline_depth + 1 + }; + + let column_width = renderer.term_width.saturating_sub(code_offset); + + let margin = Margin::new( + whitespace_margin, + span_left_margin, + span_right_margin, + label_right_margin, + column_width, + max_line_len, + ); + + // Next, output the annotate source for this file + for annotated_line_idx in 0..annotated_lines.len() { + let previous_buffer_line = buffer.num_lines(); + + let depths = render_source_line( + renderer, + &annotated_lines[annotated_line_idx], + buffer, + width_offset, + code_offset, + max_line_num_len, + margin, + !is_cont && annotated_line_idx + 1 == annotated_lines.len(), + ); + + let mut to_add = BTreeMap::new(); + + for (depth, style) in depths { + if let Some(index) = multilines.iter().position(|(d, _)| d == &depth) { + multilines.swap_remove(index); + } else { + to_add.insert(depth, style); + } + } + + // Set the multiline annotation vertical lines to the left of + // the code in this line. + for (depth, style) in &multilines { + for line in previous_buffer_line..buffer.num_lines() { + draw_multiline_line(renderer, buffer, line, width_offset, *depth, *style, false); + } + } + // check to see if we need to print out or elide lines that come between + // this annotated line and the next one. + if annotated_line_idx < (annotated_lines.len() - 1) { + let line_idx_delta = annotated_lines[annotated_line_idx + 1].line_index + - annotated_lines[annotated_line_idx].line_index; + match line_idx_delta.cmp(&2) { + Ordering::Greater => { + let last_buffer_line_num = buffer.num_lines(); + + draw_line_separator(renderer, buffer, last_buffer_line_num, width_offset); + + // Set the multiline annotation vertical lines on `...` bridging line. + for (depth, style) in &multilines { + draw_multiline_line( + renderer, + buffer, + last_buffer_line_num, + width_offset, + *depth, + *style, + true, + ); + } + if let Some(line) = annotated_lines.get(annotated_line_idx) { + for ann in &line.annotations { + if let LineAnnotationType::MultilineStart(pos) = ann.annotation_type { + // In the case where we have elided the entire start of the + // multispan because those lines were empty, we still need + // to draw the `|`s across the `...`. + draw_multiline_line( + renderer, + buffer, + last_buffer_line_num, + width_offset, + pos, + if ann.is_primary() { + ElementStyle::UnderlinePrimary + } else { + ElementStyle::UnderlineSecondary + }, + true, + ); + } + } + } + } + + Ordering::Equal => { + let unannotated_line = sm + .get_line(annotated_lines[annotated_line_idx].line_index + 1) + .unwrap_or(""); + + let last_buffer_line_num = buffer.num_lines(); + + draw_line( + renderer, + buffer, + &normalize_whitespace(unannotated_line), + annotated_lines[annotated_line_idx + 1].line_index - 1, + last_buffer_line_num, + width_offset, + code_offset, + max_line_num_len, + margin, + ); + + for (depth, style) in &multilines { + draw_multiline_line( + renderer, + buffer, + last_buffer_line_num, + width_offset, + *depth, + *style, + false, + ); + } + if let Some(line) = annotated_lines.get(annotated_line_idx) { + for ann in &line.annotations { + if let LineAnnotationType::MultilineStart(pos) = ann.annotation_type { + draw_multiline_line( + renderer, + buffer, + last_buffer_line_num, + width_offset, + pos, + if ann.is_primary() { + ElementStyle::UnderlinePrimary + } else { + ElementStyle::UnderlineSecondary + }, + false, + ); + } + } + } + } + Ordering::Less => {} + } + } + + multilines.extend(to_add); + } +} + +#[allow(clippy::too_many_arguments)] +fn render_source_line( + renderer: &Renderer, + line_info: &AnnotatedLineInfo<'_>, + buffer: &mut StyledBuffer, + width_offset: usize, + code_offset: usize, + max_line_num_len: usize, + margin: Margin, + close_window: bool, +) -> Vec<(usize, ElementStyle)> { + // Draw: + // + // LL | ... code ... + // | ^^-^ span label + // | | + // | secondary span label + // + // ^^ ^ ^^^ ^^^^ ^^^ we don't care about code too far to the right of a span, we trim it + // | | | | + // | | | actual code found in your source code and the spans we use to mark it + // | | when there's too much wasted space to the left, trim it + // | vertical divider between the column number and the code + // column number + + let source_string = normalize_whitespace(line_info.line); + + let line_offset = buffer.num_lines(); + + let left = draw_line( + renderer, + buffer, + &source_string, + line_info.line_index, + line_offset, + width_offset, + code_offset, + max_line_num_len, + margin, + ); + + // If there are no annotations, we are done + if line_info.annotations.is_empty() { + // `close_window` normally gets handled later, but we are early + // returning, so it needs to be handled here + if close_window { + draw_col_separator_end(renderer, buffer, line_offset + 1, width_offset - 2); + } + return vec![]; + } + + // Special case when there's only one annotation involved, it is the start of a multiline + // span and there's no text at the beginning of the code line. Instead of doing the whole + // graph: + // + // 2 | fn foo() { + // | _^ + // 3 | | + // 4 | | } + // | |_^ test + // + // we simplify the output to: + // + // 2 | / fn foo() { + // 3 | | + // 4 | | } + // | |_^ test + let mut buffer_ops = vec![]; + let mut annotations = vec![]; + let mut short_start = true; + for ann in &line_info.annotations { + if let LineAnnotationType::MultilineStart(depth) = ann.annotation_type { + if source_string + .chars() + .take(ann.start.display) + .all(char::is_whitespace) + { + let uline = renderer.decor_style.underline(ann.is_primary()); + let chr = uline.multiline_whole_line; + annotations.push((depth, uline.style)); + buffer_ops.push((line_offset, width_offset + depth - 1, chr, uline.style)); + } else { + short_start = false; + break; + } + } else if let LineAnnotationType::MultilineLine(_) = ann.annotation_type { + } else { + short_start = false; + break; + } + } + if short_start { + for (y, x, c, s) in buffer_ops { + buffer.putc(y, x, c, s); + } + return annotations; + } + + // We want to display like this: + // + // vec.push(vec.pop().unwrap()); + // --- ^^^ - previous borrow ends here + // | | + // | error occurs here + // previous borrow of `vec` occurs here + // + // But there are some weird edge cases to be aware of: + // + // vec.push(vec.pop().unwrap()); + // -------- - previous borrow ends here + // || + // |this makes no sense + // previous borrow of `vec` occurs here + // + // For this reason, we group the lines into "highlight lines" + // and "annotations lines", where the highlight lines have the `^`. + + // Sort the annotations by (start, end col) + // The labels are reversed, sort and then reversed again. + // Consider a list of annotations (A1, A2, C1, C2, B1, B2) where + // the letter signifies the span. Here we are only sorting by the + // span and hence, the order of the elements with the same span will + // not change. On reversing the ordering (|a, b| but b.cmp(a)), you get + // (C1, C2, B1, B2, A1, A2). All the elements with the same span are + // still ordered first to last, but all the elements with different + // spans are ordered by their spans in last to first order. Last to + // first order is important, because the jiggly lines and | are on + // the left, so the rightmost span needs to be rendered first, + // otherwise the lines would end up needing to go over a message. + + let mut annotations = line_info.annotations.clone(); + annotations.sort_by_key(|a| Reverse((a.start.display, a.start.char))); + + // First, figure out where each label will be positioned. + // + // In the case where you have the following annotations: + // + // vec.push(vec.pop().unwrap()); + // -------- - previous borrow ends here [C] + // || + // |this makes no sense [B] + // previous borrow of `vec` occurs here [A] + // + // `annotations_position` will hold [(2, A), (1, B), (0, C)]. + // + // We try, when possible, to stick the rightmost annotation at the end + // of the highlight line: + // + // vec.push(vec.pop().unwrap()); + // --- --- - previous borrow ends here + // + // But sometimes that's not possible because one of the other + // annotations overlaps it. For example, from the test + // `span_overlap_label`, we have the following annotations + // (written on distinct lines for clarity): + // + // fn foo(x: u32) { + // -------------- + // - + // + // In this case, we can't stick the rightmost-most label on + // the highlight line, or we would get: + // + // fn foo(x: u32) { + // -------- x_span + // | + // fn_span + // + // which is totally weird. Instead we want: + // + // fn foo(x: u32) { + // -------------- + // | | + // | x_span + // fn_span + // + // which is...less weird, at least. In fact, in general, if + // the rightmost span overlaps with any other span, we should + // use the "hang below" version, so we can at least make it + // clear where the span *starts*. There's an exception for this + // logic, when the labels do not have a message: + // + // fn foo(x: u32) { + // -------------- + // | + // x_span + // + // instead of: + // + // fn foo(x: u32) { + // -------------- + // | | + // | x_span + // + // + let mut overlap = vec![false; annotations.len()]; + let mut annotations_position = vec![]; + let mut line_len: usize = 0; + let mut p = 0; + for (i, annotation) in annotations.iter().enumerate() { + for (j, next) in annotations.iter().enumerate() { + if overlaps(next, annotation, 0) && j > 1 { + overlap[i] = true; + overlap[j] = true; + } + if overlaps(next, annotation, 0) // This label overlaps with another one and both + && annotation.has_label() // take space (they have text and are not + && j > i // multiline lines). + && p == 0 + // We're currently on the first line, move the label one line down + { + // If we're overlapping with an un-labelled annotation with the same span + // we can just merge them in the output + if next.start.display == annotation.start.display + && next.start.char == annotation.start.char + && next.end.display == annotation.end.display + && next.end.char == annotation.end.char + && !next.has_label() + { + continue; + } + + // This annotation needs a new line in the output. + p += 1; + break; + } + } + annotations_position.push((p, annotation)); + for (j, next) in annotations.iter().enumerate() { + if j > i { + let l = next.label.as_ref().map_or(0, |label| label.len() + 2); + if (overlaps(next, annotation, l) // Do not allow two labels to be in the same + // line if they overlap including padding, to + // avoid situations like: + // + // fn foo(x: u32) { + // -------^------ + // | | + // fn_spanx_span + // + && annotation.has_label() // Both labels must have some text, otherwise + && next.has_label()) // they are not overlapping. + // Do not add a new line if this annotation + // or the next are vertical line placeholders. + || (annotation.takes_space() // If either this or the next annotation is + && next.has_label()) // multiline start/end, move it to a new line + || (annotation.has_label() // so as not to overlap the horizontal lines. + && next.takes_space()) + || (annotation.takes_space() && next.takes_space()) + || (overlaps(next, annotation, l) + && (next.end.display, next.end.char) <= (annotation.end.display, annotation.end.char) + && next.has_label() + && p == 0) + // Avoid #42595. + { + // This annotation needs a new line in the output. + p += 1; + break; + } + } + } + line_len = max(line_len, p); + } + + if line_len != 0 { + line_len += 1; + } + + // If there are no annotations or the only annotations on this line are + // MultilineLine, then there's only code being shown, stop processing. + if line_info.annotations.iter().all(LineAnnotation::is_line) { + return vec![]; + } + + if annotations_position + .iter() + .all(|(_, ann)| matches!(ann.annotation_type, LineAnnotationType::MultilineStart(_))) + && let Some(max_pos) = annotations_position.iter().map(|(pos, _)| *pos).max() + { + // Special case the following, so that we minimize overlapping multiline spans. + // + // 3 │ X0 Y0 Z0 + // │ ┏━━━━━┛ │ │ < We are writing these lines + // │ ┃┌───────┘ │ < by reverting the "depth" of + // │ ┃│┌─────────┘ < their multiline spans. + // 4 │ ┃││ X1 Y1 Z1 + // 5 │ ┃││ X2 Y2 Z2 + // │ ┃│└────╿──│──┘ `Z` label + // │ ┃└─────│──┤ + // │ ┗━━━━━━┥ `Y` is a good letter too + // ╰╴ `X` is a good letter + for (pos, _) in &mut annotations_position { + *pos = max_pos - *pos; + } + // We know then that we don't need an additional line for the span label, saving us + // one line of vertical space. + line_len = line_len.saturating_sub(1); + } + + // Write the column separator. + // + // After this we will have: + // + // 2 | fn foo() { + // | + // | + // | + // 3 | + // 4 | } + // | + for pos in 0..=line_len { + draw_col_separator_no_space(renderer, buffer, line_offset + pos + 1, width_offset - 2); + } + if close_window { + draw_col_separator_end( + renderer, + buffer, + line_offset + line_len + 1, + width_offset - 2, + ); + } + // Write the horizontal lines for multiline annotations + // (only the first and last lines need this). + // + // After this we will have: + // + // 2 | fn foo() { + // | __________ + // | + // | + // 3 | + // 4 | } + // | _ + for &(pos, annotation) in &annotations_position { + let underline = renderer.decor_style.underline(annotation.is_primary()); + let pos = pos + 1; + match annotation.annotation_type { + LineAnnotationType::MultilineStart(depth) | LineAnnotationType::MultilineEnd(depth) => { + draw_range( + buffer, + underline.multiline_horizontal, + line_offset + pos, + width_offset + depth, + (code_offset + annotation.start.display).saturating_sub(left), + underline.style, + ); + } + _ if annotation.highlight_source => { + buffer.set_style_range( + line_offset, + (code_offset + annotation.start.char).saturating_sub(left), + (code_offset + annotation.end.char).saturating_sub(left), + underline.style, + annotation.is_primary(), + ); + } + _ => {} + } + } + + // Write the vertical lines for labels that are on a different line as the underline. + // + // After this we will have: + // + // 2 | fn foo() { + // | __________ + // | | | + // | | + // 3 | | + // 4 | | } + // | |_ + for &(pos, annotation) in &annotations_position { + let underline = renderer.decor_style.underline(annotation.is_primary()); + let pos = pos + 1; + + if pos > 1 && (annotation.has_label() || annotation.takes_space()) { + for p in line_offset + 1..=line_offset + pos { + buffer.putc( + p, + (code_offset + annotation.start.display).saturating_sub(left), + match annotation.annotation_type { + LineAnnotationType::MultilineLine(_) => underline.multiline_vertical, + _ => underline.vertical_text_line, + }, + underline.style, + ); + } + if let LineAnnotationType::MultilineStart(_) = annotation.annotation_type { + buffer.putc( + line_offset + pos, + (code_offset + annotation.start.display).saturating_sub(left), + underline.bottom_right, + underline.style, + ); + } + if matches!( + annotation.annotation_type, + LineAnnotationType::MultilineEnd(_) + ) && annotation.has_label() + { + buffer.putc( + line_offset + pos, + (code_offset + annotation.start.display).saturating_sub(left), + underline.multiline_bottom_right_with_text, + underline.style, + ); + } + } + match annotation.annotation_type { + LineAnnotationType::MultilineStart(depth) => { + buffer.putc( + line_offset + pos, + width_offset + depth - 1, + underline.top_left, + underline.style, + ); + for p in line_offset + pos + 1..line_offset + line_len + 2 { + buffer.putc( + p, + width_offset + depth - 1, + underline.multiline_vertical, + underline.style, + ); + } + } + LineAnnotationType::MultilineEnd(depth) => { + for p in line_offset..line_offset + pos { + buffer.putc( + p, + width_offset + depth - 1, + underline.multiline_vertical, + underline.style, + ); + } + buffer.putc( + line_offset + pos, + width_offset + depth - 1, + underline.bottom_left, + underline.style, + ); + } + _ => (), + } + } + + // Write the labels on the annotations that actually have a label. + // + // After this we will have: + // + // 2 | fn foo() { + // | __________ + // | | + // | something about `foo` + // 3 | + // 4 | } + // | _ test + for &(pos, annotation) in &annotations_position { + let style = if annotation.is_primary() { + ElementStyle::LabelPrimary + } else { + ElementStyle::LabelSecondary + }; + let (pos, col) = if pos == 0 { + if annotation.end.display == 0 { + (pos + 1, (annotation.end.display + 2).saturating_sub(left)) + } else { + (pos + 1, (annotation.end.display + 1).saturating_sub(left)) + } + } else { + (pos + 2, annotation.start.display.saturating_sub(left)) + }; + if let Some(label) = &annotation.label { + buffer.puts(line_offset + pos, code_offset + col, label, style); + } + } + + // Sort from biggest span to smallest span so that smaller spans are + // represented in the output: + // + // x | fn foo() + // | ^^^---^^ + // | | | + // | | something about `foo` + // | something about `fn foo()` + annotations_position.sort_by_key(|(_, ann)| { + // Decreasing order. When annotations share the same length, prefer `Primary`. + (Reverse(ann.len()), ann.is_primary()) + }); + + // Write the underlines. + // + // After this we will have: + // + // 2 | fn foo() { + // | ____-_____^ + // | | + // | something about `foo` + // 3 | + // 4 | } + // | _^ test + for &(pos, annotation) in &annotations_position { + let uline = renderer.decor_style.underline(annotation.is_primary()); + for p in annotation.start.display..annotation.end.display { + // The default span label underline. + buffer.putc( + line_offset + 1, + (code_offset + p).saturating_sub(left), + uline.underline, + uline.style, + ); + } + + if pos == 0 + && matches!( + annotation.annotation_type, + LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_) + ) + { + // The beginning of a multiline span with its leftward moving line on the same line. + buffer.putc( + line_offset + 1, + (code_offset + annotation.start.display).saturating_sub(left), + match annotation.annotation_type { + LineAnnotationType::MultilineStart(_) => uline.top_right_flat, + LineAnnotationType::MultilineEnd(_) => uline.multiline_end_same_line, + _ => panic!("unexpected annotation type: {annotation:?}"), + }, + uline.style, + ); + } else if pos != 0 + && matches!( + annotation.annotation_type, + LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_) + ) + { + // The beginning of a multiline span with its leftward moving line on another line, + // so we start going down first. + buffer.putc( + line_offset + 1, + (code_offset + annotation.start.display).saturating_sub(left), + match annotation.annotation_type { + LineAnnotationType::MultilineStart(_) => uline.multiline_start_down, + LineAnnotationType::MultilineEnd(_) => uline.multiline_end_up, + _ => panic!("unexpected annotation type: {annotation:?}"), + }, + uline.style, + ); + } else if pos != 0 && annotation.has_label() { + // The beginning of a span label with an actual label, we'll point down. + buffer.putc( + line_offset + 1, + (code_offset + annotation.start.display).saturating_sub(left), + uline.label_start, + uline.style, + ); + } + } + + // We look for individual *long* spans, and we trim the *middle*, so that we render + // LL | ...= [0, 0, 0, ..., 0, 0]; + // | ^^^^^^^^^^...^^^^^^^ expected `&[u8]`, found `[{integer}; 1680]` + for (i, (_pos, annotation)) in annotations_position.iter().enumerate() { + // Skip cases where multiple spans overlap eachother. + if overlap[i] { + continue; + }; + let LineAnnotationType::Singleline = annotation.annotation_type else { + continue; + }; + let width = annotation.end.display - annotation.start.display; + + static MIN_PAD: usize = 5; + let cut_indicator = renderer + .cut_indicator + .unwrap_or(renderer.decor_style.margin()); + let margin_width = str_width(cut_indicator); + if width > margin.term_width * 2 && width > (MIN_PAD * 2 + margin_width) { + // If the terminal is *too* small, we keep at least a tiny bit of the span for + // display. + let pad = max(margin.term_width / 3, MIN_PAD); + // Code line + buffer.replace( + line_offset, + code_offset + (annotation.start.display + pad).saturating_sub(left), + code_offset + (annotation.end.display - pad).saturating_sub(left), + cut_indicator, + ); + // Underline line + buffer.replace( + line_offset + 1, + code_offset + (annotation.start.display + pad).saturating_sub(left), + code_offset + (annotation.end.display - pad).saturating_sub(left), + cut_indicator, + ); + } + } + annotations_position + .iter() + .filter_map(|&(_, annotation)| match annotation.annotation_type { + LineAnnotationType::MultilineStart(p) | LineAnnotationType::MultilineEnd(p) => { + let style = if annotation.is_primary() { + ElementStyle::LabelPrimary + } else { + ElementStyle::LabelSecondary + }; + Some((p, style)) + } + _ => None, + }) + .collect::>() +} + +#[allow(clippy::too_many_arguments)] +fn emit_suggestion_default( + renderer: &Renderer, + buffer: &mut StyledBuffer, + suggestion: &Snippet<'_, Patch<'_>>, + spliced_lines: SplicedLines<'_>, + show_code_change: DisplaySuggestion, + max_line_num_len: usize, + sm: &SourceMap<'_>, + primary_path: Option<&Cow<'_, str>>, + matches_previous_suggestion: bool, + _is_first: bool, + is_cont: bool, +) { + let buffer_offset = buffer.num_lines(); + let mut row_num = buffer_offset + usize::from(!matches_previous_suggestion); + let (complete, parts, highlights, replaced_highlights) = spliced_lines; + let is_multiline = complete.lines().count() > 1; + + let secondary_path = suggestion.path.as_ref() != primary_path; + if ((secondary_path && suggestion.path.as_ref().is_some()) || suggestion.cell_index.is_some()) + && !matches_previous_suggestion + { + let (loc, _) = sm.span_to_locations(parts[0].span.clone()); + // --> file.rs:line:col + // | + for _ in 0..max_line_num_len { + buffer.append(row_num - 1, " ", ElementStyle::NoStyle); + } + let arrow = renderer.decor_style.secondary_file_start(); + buffer.append(row_num - 1, arrow, ElementStyle::LineNumber); + let origin = Origin { + path: suggestion + .path + .as_ref() + .filter(|_| secondary_path) + .map(|p| Cow::Borrowed(p.as_ref())), + cell_index: suggestion.cell_index, + line: Some(loc.line), + char_column: Some(loc.char + 1), + }; + let message = format_origin(&origin, renderer.anonymized_line_numbers); + buffer.append(row_num - 1, &message, ElementStyle::LineAndColumn); + + draw_col_separator_no_space(renderer, buffer, row_num, max_line_num_len + 1); + row_num += 1; + } else if matches_previous_suggestion { + buffer.puts( + row_num - 1, + max_line_num_len + 1, + renderer.decor_style.multi_suggestion_separator(), + ElementStyle::LineNumber, + ); + } else { + draw_col_separator_start(renderer, buffer, row_num - 1, max_line_num_len + 1); + } + + if let DisplaySuggestion::Diff = show_code_change { + row_num += 1; + } + + let lo = parts.iter().map(|p| p.span.start).min().unwrap(); + let hi = parts.iter().map(|p| p.span.end).max().unwrap(); + + let file_lines = sm.span_to_lines(lo..hi); + let (line_start, line_end) = if suggestion.fold { + // We use the original span to get original line_start + sm.span_to_locations(parts[0].original_span.clone()) + } else { + sm.span_to_locations(0..sm.source.len()) + }; + let mut lines = complete.lines(); + if lines.clone().next().is_none() { + // Account for a suggestion to completely remove a line(s) with whitespace (#94192). + for line in line_start.line..=line_end.line { + buffer.puts( + row_num - 1 + line - line_start.line, + 0, + &maybe_anonymized(renderer, line, max_line_num_len), + ElementStyle::LineNumber, + ); + buffer.puts( + row_num - 1 + line - line_start.line, + max_line_num_len + 1, + "- ", + ElementStyle::Removal, + ); + buffer.puts( + row_num - 1 + line - line_start.line, + max_line_num_len + 3, + &normalize_whitespace(sm.get_line(line).unwrap()), + ElementStyle::Removal, + ); + } + row_num += line_end.line - line_start.line; + } + let mut unhighlighted_lines = Vec::new(); + for (line_pos, (line, highlight_parts)) in lines.by_ref().zip(highlights).enumerate() { + // Remember lines that are not highlighted to hide them if needed + if highlight_parts.is_empty() && suggestion.fold { + unhighlighted_lines.push((line_pos, line)); + continue; + } + + match unhighlighted_lines.len() { + 0 => (), + // Since we show first line, "..." line and last line, + // There is no reason to hide if there are 3 or less lines + // (because then we just replace a line with ... which is + // not helpful) + n if n <= 3 => unhighlighted_lines.drain(..).for_each(|(p, l)| { + draw_code_line( + renderer, + buffer, + &mut row_num, + &[], + &[], + p + line_start.line, + l, + show_code_change, + max_line_num_len, + &file_lines, + is_multiline, + ); + }), + // Print first unhighlighted line, "..." and last unhighlighted line, like so: + // + // LL | this line was highlighted + // LL | this line is just for context + // ... + // LL | this line is just for context + // LL | this line was highlighted + _ => { + let last_line = unhighlighted_lines.pop(); + let first_line = unhighlighted_lines.drain(..).next(); + + if let Some((p, l)) = first_line { + draw_code_line( + renderer, + buffer, + &mut row_num, + &[], + &[], + p + line_start.line, + l, + show_code_change, + max_line_num_len, + &file_lines, + is_multiline, + ); + } + + let cut_indicator = renderer + .cut_indicator + .unwrap_or(renderer.decor_style.margin()); + let padding = str_width(cut_indicator); + buffer.puts( + row_num, + max_line_num_len.saturating_sub(padding), + cut_indicator, + ElementStyle::LineNumber, + ); + row_num += 1; + + if let Some((p, l)) = last_line { + draw_code_line( + renderer, + buffer, + &mut row_num, + &[], + &[], + p + line_start.line, + l, + show_code_change, + max_line_num_len, + &file_lines, + is_multiline, + ); + } + } + } + draw_code_line( + renderer, + buffer, + &mut row_num, + &highlight_parts, + &replaced_highlights, + line_pos + line_start.line, + line, + show_code_change, + max_line_num_len, + &file_lines, + is_multiline, + ); + } + + // This offset and the ones below need to be signed to account for replacement code + // that is shorter than the original code. + let mut offsets: Vec<(usize, isize)> = Vec::new(); + // Only show an underline in the suggestions if the suggestion is not the + // entirety of the code being shown and the displayed code is not multiline. + if let DisplaySuggestion::Diff | DisplaySuggestion::Underline | DisplaySuggestion::Add = + show_code_change + { + for part in parts { + let (span_start, span_end) = sm.span_to_locations(part.span.clone()); + let span_start_pos = span_start.display; + let span_end_pos = span_end.display; + + // If this addition is _only_ whitespace, then don't trim it, + // or else we're just not rendering anything. + let is_whitespace_addition = part.replacement.trim().is_empty(); + + // Do not underline the leading... + let start = if is_whitespace_addition { + 0 + } else { + part.replacement + .len() + .saturating_sub(part.replacement.trim_start().len()) + }; + // ...or trailing spaces. Account for substitutions containing unicode + // characters. + let sub_len: usize = str_width(if is_whitespace_addition { + &part.replacement + } else { + part.replacement.trim() + }); + + let offset: isize = offsets + .iter() + .filter_map(|(start, v)| { + if span_start_pos < *start { + None + } else { + Some(v) + } + }) + .sum(); + let underline_start = (span_start_pos + start) as isize + offset; + let underline_end = (span_start_pos + start + sub_len) as isize + offset; + assert!(underline_start >= 0 && underline_end >= 0); + let padding: usize = max_line_num_len + 3; + for p in underline_start..underline_end { + if matches!(show_code_change, DisplaySuggestion::Underline) { + // If this is a replacement, underline with `~`, if this is an addition + // underline with `+`. + buffer.putc( + row_num, + (padding as isize + p) as usize, + if part.is_addition(sm) { + '+' + } else { + renderer.decor_style.diff() + }, + ElementStyle::Addition, + ); + } + } + + // length of the code after substitution + let full_sub_len = str_width(&part.replacement) as isize; + + // length of the code to be substituted + let snippet_len = span_end_pos as isize - span_start_pos as isize; + // For multiple substitutions, use the position *after* the previous + // substitutions have happened, only when further substitutions are + // located strictly after. + offsets.push((span_end_pos, full_sub_len - snippet_len)); + } + row_num += 1; + } + + // if we elided some lines, add an ellipsis + if lines.next().is_some() { + let cut_indicator = renderer + .cut_indicator + .unwrap_or(renderer.decor_style.margin()); + let padding = str_width(cut_indicator); + buffer.puts( + row_num, + max_line_num_len.saturating_sub(padding), + cut_indicator, + ElementStyle::LineNumber, + ); + } else { + let row = match show_code_change { + DisplaySuggestion::Diff | DisplaySuggestion::Add | DisplaySuggestion::Underline => { + row_num - 1 + } + DisplaySuggestion::None => row_num, + }; + if is_cont { + draw_col_separator_no_space(renderer, buffer, row, max_line_num_len + 1); + } else { + draw_col_separator_end(renderer, buffer, row, max_line_num_len + 1); + } + } +} + +#[allow(clippy::too_many_arguments)] +fn draw_code_line( + renderer: &Renderer, + buffer: &mut StyledBuffer, + row_num: &mut usize, + highlight_parts: &[SubstitutionHighlight], + replaced_parts: &[Vec], + line_num: usize, + line_to_add: &str, + show_code_change: DisplaySuggestion, + max_line_num_len: usize, + file_lines: &[&LineInfo<'_>], + is_multiline: bool, +) { + if let DisplaySuggestion::Diff = show_code_change { + // We need to print more than one line if the span we need to remove is multiline. + // For more info: https://github.com/rust-lang/rust/issues/92741 + let lines_to_remove = file_lines.iter().take(file_lines.len() - 1); + for (index, (line_to_remove, parts)) in lines_to_remove.zip(replaced_parts).enumerate() { + buffer.puts( + *row_num - 1, + 0, + &maybe_anonymized(renderer, line_num + index, max_line_num_len), + ElementStyle::LineNumber, + ); + buffer.puts( + *row_num - 1, + max_line_num_len + 1, + "- ", + ElementStyle::Removal, + ); + let line = normalize_whitespace(line_to_remove.line); + buffer.puts( + *row_num - 1, + max_line_num_len + 3, + &line, + ElementStyle::NoStyle, + ); + style_substitution_highlights( + parts, + ElementStyle::Removal, + *row_num - 1, + line_to_remove.line, + max_line_num_len, + buffer, + ); + *row_num += 1; + } + // If the last line is exactly equal to the line we need to add, we can skip both of + // them. This allows us to avoid output like the following: + // 2 - & + // 2 + if true { true } else { false } + // 3 - if true { true } else { false } + // If those lines aren't equal, we print their diff + let last_line = &file_lines.last().unwrap(); + if last_line.line == line_to_add { + *row_num -= 2; + // The last original line collapses into the previous drawn row, so + // fold its replaced-code highlights onto that row too. + style_substitution_highlights( + replaced_parts.last().unwrap(), + ElementStyle::Removal, + *row_num, + last_line.line, + max_line_num_len, + buffer, + ); + } else { + buffer.puts( + *row_num - 1, + 0, + &maybe_anonymized(renderer, line_num + file_lines.len() - 1, max_line_num_len), + ElementStyle::LineNumber, + ); + buffer.puts( + *row_num - 1, + max_line_num_len + 1, + "- ", + ElementStyle::Removal, + ); + buffer.puts( + *row_num - 1, + max_line_num_len + 3, + &normalize_whitespace(last_line.line), + ElementStyle::NoStyle, + ); + style_substitution_highlights( + replaced_parts.last().unwrap(), + ElementStyle::Removal, + *row_num - 1, + last_line.line, + max_line_num_len, + buffer, + ); + + if line_to_add.trim().is_empty() { + *row_num -= 1; + } else { + // Check if after the removal, the line is left with only whitespace. If so, we + // will not show an "addition" line, as removing the whole line is what the user + // would really want. + // For example, for the following: + // | + // 2 - .await + // 2 + (note the left over whitespace) + // | + // We really want + // | + // 2 - .await + // | + // *row_num -= 1; + buffer.puts( + *row_num, + 0, + &maybe_anonymized(renderer, line_num, max_line_num_len), + ElementStyle::LineNumber, + ); + buffer.puts(*row_num, max_line_num_len + 1, "+ ", ElementStyle::Addition); + buffer.append( + *row_num, + &normalize_whitespace(line_to_add), + ElementStyle::NoStyle, + ); + } + } + } else if is_multiline { + buffer.puts( + *row_num, + 0, + &maybe_anonymized(renderer, line_num, max_line_num_len), + ElementStyle::LineNumber, + ); + match &highlight_parts { + [SubstitutionHighlight { start: 0, end }] if *end == line_to_add.len() => { + buffer.puts(*row_num, max_line_num_len + 1, "+ ", ElementStyle::Addition); + } + [] | [SubstitutionHighlight { start: 0, end: 0 }] => { + // FIXME: needed? Doesn't get exercised in any test. + draw_col_separator_no_space(renderer, buffer, *row_num, max_line_num_len + 1); + } + _ => { + let diff = renderer.decor_style.diff(); + buffer.puts( + *row_num, + max_line_num_len + 1, + &format!("{diff} "), + ElementStyle::Addition, + ); + } + } + // LL | line_to_add + // ++^^^ + // | | + // | magic `3` + // `max_line_num_len` + buffer.puts( + *row_num, + max_line_num_len + 3, + &normalize_whitespace(line_to_add), + ElementStyle::NoStyle, + ); + } else if let DisplaySuggestion::Add = show_code_change { + buffer.puts( + *row_num, + 0, + &maybe_anonymized(renderer, line_num, max_line_num_len), + ElementStyle::LineNumber, + ); + buffer.puts(*row_num, max_line_num_len + 1, "+ ", ElementStyle::Addition); + buffer.append( + *row_num, + &normalize_whitespace(line_to_add), + ElementStyle::NoStyle, + ); + } else { + buffer.puts( + *row_num, + 0, + &maybe_anonymized(renderer, line_num, max_line_num_len), + ElementStyle::LineNumber, + ); + draw_col_separator(renderer, buffer, *row_num, max_line_num_len + 1); + buffer.append( + *row_num, + &normalize_whitespace(line_to_add), + ElementStyle::NoStyle, + ); + } + + style_substitution_highlights( + highlight_parts, + ElementStyle::Addition, + *row_num, + line_to_add, + max_line_num_len, + buffer, + ); + + *row_num += 1; +} + +fn style_substitution_highlights( + highlight_parts: &[SubstitutionHighlight], + style: ElementStyle, + row_num: usize, + unnormalized_line: &str, + max_line_num_len: usize, + buffer: &mut StyledBuffer, +) { + for &SubstitutionHighlight { start, end } in highlight_parts { + // This is a no-op for empty ranges + if start != end { + // We calculate the extra width from tabs for both the start and end + // of the span, as tabs could be present in the middle of the span + let extra_width_start: usize = extra_width_from_tabs(unnormalized_line, start); + let extra_width_end: usize = extra_width_from_tabs(unnormalized_line, end); + buffer.set_style_range( + row_num, + max_line_num_len + 3 + start + extra_width_start, + max_line_num_len + 3 + end + extra_width_end, + style, + true, + ); + } + } +} + +#[allow(clippy::too_many_arguments)] +fn draw_line( + renderer: &Renderer, + buffer: &mut StyledBuffer, + source_string: &str, + line_index: usize, + line_offset: usize, + width_offset: usize, + code_offset: usize, + max_line_num_len: usize, + margin: Margin, +) -> usize { + // Tabs are assumed to have been replaced by spaces in calling code. + debug_assert!(!source_string.contains('\t')); + let line_len = str_width(source_string); + // Create the source line we will highlight. + let mut left = margin.left(line_len); + let right = margin.right(line_len); + + let mut taken = 0; + let mut skipped = 0; + let code: String = source_string + .chars() + .skip_while(|ch| { + let w = char_width(*ch); + // If `skipped` is less than `left`, always skip the next `ch`, + // even if `ch` is a multi-width char that would make `skipped` + // exceed `left`. This ensures that we do not exceed term width on + // source lines. + if skipped < left { + skipped += w; + true + } else { + false + } + }) + .take_while(|ch| { + // Make sure that the trimming on the right will fall within the terminal width. + taken += char_width(*ch); + taken <= (right - left) + }) + .collect(); + // If we skipped more than `left`, adjust `left` to account for it. + if skipped > left { + left += skipped - left; + } + let cut_indicator = renderer + .cut_indicator + .unwrap_or(renderer.decor_style.margin()); + let padding = str_width(cut_indicator); + let (width_taken, bytes_taken) = if margin.was_cut_left() { + // We have stripped some code/whitespace from the beginning, make it clear. + let mut bytes_taken = 0; + let mut width_taken = 0; + for ch in code.chars() { + width_taken += char_width(ch); + bytes_taken += ch.len_utf8(); + + if width_taken >= padding { + break; + } + } + + buffer.puts( + line_offset, + code_offset, + cut_indicator, + ElementStyle::LineNumber, + ); + (width_taken, bytes_taken) + } else { + (0, 0) + }; + + buffer.puts( + line_offset, + code_offset + width_taken, + &code[bytes_taken..], + ElementStyle::Quotation, + ); + + if line_len > right { + // We have stripped some code/whitespace from the beginning, make it clear. + let mut char_taken = 0; + let mut width_taken_inner = 0; + for ch in code.chars().rev() { + width_taken_inner += char_width(ch); + char_taken += 1; + + if width_taken_inner >= padding { + break; + } + } + + buffer.puts( + line_offset, + code_offset + width_taken + code[bytes_taken..].chars().count() - char_taken, + cut_indicator, + ElementStyle::LineNumber, + ); + } + + buffer.puts( + line_offset, + 0, + &maybe_anonymized(renderer, line_index, max_line_num_len), + ElementStyle::LineNumber, + ); + + draw_col_separator_no_space(renderer, buffer, line_offset, width_offset - 2); + + left +} + +fn draw_range( + buffer: &mut StyledBuffer, + symbol: char, + line: usize, + col_from: usize, + col_to: usize, + style: ElementStyle, +) { + for col in col_from..col_to { + buffer.putc(line, col, symbol, style); + } +} + +fn draw_multiline_line( + renderer: &Renderer, + buffer: &mut StyledBuffer, + line: usize, + offset: usize, + depth: usize, + style: ElementStyle, + elided: bool, +) { + let chr = match (style, renderer.decor_style) { + (ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary, DecorStyle::Ascii) => '|', + (_, DecorStyle::Ascii) => '|', + (ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary, DecorStyle::Unicode) => { + if elided { + '┇' + } else { + '┃' + } + } + (_, DecorStyle::Unicode) => { + if elided { + '┆' + } else { + '│' + } + } + }; + buffer.putc(line, offset + depth - 1, chr, style); +} + +fn draw_col_separator(renderer: &Renderer, buffer: &mut StyledBuffer, line: usize, col: usize) { + let chr = renderer.decor_style.col_separator(); + buffer.puts(line, col, &format!("{chr} "), ElementStyle::LineNumber); +} + +fn draw_col_separator_no_space( + renderer: &Renderer, + buffer: &mut StyledBuffer, + line: usize, + col: usize, +) { + let chr = renderer.decor_style.col_separator(); + draw_col_separator_no_space_with_style(buffer, chr, line, col, ElementStyle::LineNumber); +} + +fn draw_col_separator_start( + renderer: &Renderer, + buffer: &mut StyledBuffer, + line: usize, + col: usize, +) { + match renderer.decor_style { + DecorStyle::Ascii => { + draw_col_separator_no_space_with_style( + buffer, + '|', + line, + col, + ElementStyle::LineNumber, + ); + } + DecorStyle::Unicode => { + draw_col_separator_no_space_with_style( + buffer, + '╭', + line, + col, + ElementStyle::LineNumber, + ); + draw_col_separator_no_space_with_style( + buffer, + '╴', + line, + col + 1, + ElementStyle::LineNumber, + ); + } + } +} + +fn draw_col_separator_end(renderer: &Renderer, buffer: &mut StyledBuffer, line: usize, col: usize) { + match renderer.decor_style { + DecorStyle::Ascii => { + draw_col_separator_no_space_with_style( + buffer, + '|', + line, + col, + ElementStyle::LineNumber, + ); + } + DecorStyle::Unicode => { + draw_col_separator_no_space_with_style( + buffer, + '╰', + line, + col, + ElementStyle::LineNumber, + ); + draw_col_separator_no_space_with_style( + buffer, + '╴', + line, + col + 1, + ElementStyle::LineNumber, + ); + } + } +} + +fn draw_col_separator_no_space_with_style( + buffer: &mut StyledBuffer, + chr: char, + line: usize, + col: usize, + style: ElementStyle, +) { + buffer.putc(line, col, chr, style); +} + +fn maybe_anonymized(renderer: &Renderer, line_num: usize, max_line_num_len: usize) -> String { + format!( + "{:>max_line_num_len$}", + if renderer.anonymized_line_numbers { + Cow::Borrowed(ANONYMIZED_LINE_NUM) + } else { + Cow::Owned(line_num.to_string()) + } + ) +} + +fn draw_note_separator( + renderer: &Renderer, + buffer: &mut StyledBuffer, + line: usize, + col: usize, + is_cont: bool, +) { + let chr = renderer.decor_style.note_separator(is_cont); + buffer.puts(line, col, chr, ElementStyle::LineNumber); +} + +fn draw_line_separator(renderer: &Renderer, buffer: &mut StyledBuffer, line: usize, col: usize) { + let (column, dots) = match renderer.decor_style { + DecorStyle::Ascii => (0, "..."), + DecorStyle::Unicode => (col - 2, "┆"), + }; + buffer.puts(line, column, dots, ElementStyle::LineNumber); +} + +trait MessageOrTitle { + fn level(&self) -> &Level<'_>; + fn id(&self) -> Option<&Id<'_>>; + fn text(&self) -> &str; + fn allows_styling(&self) -> bool; + fn is_fixable(&self) -> bool; +} + +impl MessageOrTitle for Title<'_> { + fn level(&self) -> &Level<'_> { + &self.level + } + fn id(&self) -> Option<&Id<'_>> { + self.id.as_ref() + } + fn text(&self) -> &str { + self.text.as_ref() + } + fn allows_styling(&self) -> bool { + self.allows_styling + } + fn is_fixable(&self) -> bool { + self.is_fixable + } +} + +impl MessageOrTitle for Message<'_> { + fn level(&self) -> &Level<'_> { + &self.level + } + fn id(&self) -> Option<&Id<'_>> { + None + } + fn text(&self) -> &str { + self.text.as_ref() + } + fn allows_styling(&self) -> bool { + true + } + fn is_fixable(&self) -> bool { + false + } +} + +/// Count extra display columns from tabs in the first `n` chars of `s`. +/// Each tab is displayed as 4 spaces, so the extra width per tab is 3. +fn extra_width_from_tabs(s: &str, n: usize) -> usize { + s.chars().take(n).filter(|&ch| ch == '\t').count() * 3 +} + +// instead of taking the String length or dividing by 10 while > 0, we multiply a limit by 10 until +// we're higher. If the loop isn't exited by the `return`, the last multiplication will wrap, which +// is OK, because while we cannot fit a higher power of 10 in a usize, the loop will end anyway. +// This is also why we need the max number of decimal digits within a `usize`. +fn num_decimal_digits(num: Option) -> usize { + #[cfg(target_pointer_width = "64")] + const MAX_DIGITS: usize = 20; + + #[cfg(target_pointer_width = "32")] + const MAX_DIGITS: usize = 10; + + #[cfg(target_pointer_width = "16")] + const MAX_DIGITS: usize = 5; + + let Some(num) = num else { + return 0; + }; + + let mut lim = 10; + for num_digits in 1..MAX_DIGITS { + if num < lim { + return num_digits; + } + lim = lim.wrapping_mul(10); + } + MAX_DIGITS +} + +fn str_width(s: &str) -> usize { + s.chars().map(char_width).sum() +} + +pub(crate) fn char_width(ch: char) -> usize { + // 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. + match ch { + '\t' => 4, + // Keep the following list in sync with `rustc_errors::emitter::OUTPUT_REPLACEMENTS`. These + // are control points that we replace before printing with a visible codepoint for the sake + // of being able to point at them with underlines. + '\u{0000}' | '\u{0001}' | '\u{0002}' | '\u{0003}' | '\u{0004}' | '\u{0005}' + | '\u{0006}' | '\u{0007}' | '\u{0008}' | '\u{000B}' | '\u{000C}' | '\u{000D}' + | '\u{000E}' | '\u{000F}' | '\u{0010}' | '\u{0011}' | '\u{0012}' | '\u{0013}' + | '\u{0014}' | '\u{0015}' | '\u{0016}' | '\u{0017}' | '\u{0018}' | '\u{0019}' + | '\u{001A}' | '\u{001B}' | '\u{001C}' | '\u{001D}' | '\u{001E}' | '\u{001F}' + | '\u{007F}' | '\u{202A}' | '\u{202B}' | '\u{202D}' | '\u{202E}' | '\u{2066}' + | '\u{2067}' | '\u{2068}' | '\u{202C}' | '\u{2069}' => 1, + _ => unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1), + } +} + +pub(crate) fn num_overlap( + a_start: usize, + a_end: usize, + b_start: usize, + b_end: usize, + inclusive: bool, +) -> bool { + let extra = usize::from(inclusive); + (b_start..b_end + extra).contains(&a_start) || (a_start..a_end + extra).contains(&b_start) +} + +fn overlaps(a1: &LineAnnotation<'_>, a2: &LineAnnotation<'_>, padding: usize) -> bool { + num_overlap( + a1.start.display, + a1.end.display + padding, + a2.start.display, + a2.end.display, + false, + ) +} + +#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)] +pub(crate) enum LineAnnotationType { + /// Annotation under a single line of code + Singleline, + + // The Multiline type above is replaced with the following three in order + // to reuse the current label drawing code. + // + // Each of these corresponds to one part of the following diagram: + // + // x | foo(1 + bar(x, + // | _________^ < MultilineStart + // x | | y), < MultilineLine + // | |______________^ label < MultilineEnd + // x | z); + /// Annotation marking the first character of a fully shown multiline span + MultilineStart(usize), + /// Annotation marking the last character of a fully shown multiline span + MultilineEnd(usize), + /// Line at the left enclosing the lines of a fully shown multiline span + // Just a placeholder for the drawing algorithm, to know that it shouldn't skip the first 4 + // and last 2 lines of code. The actual line is drawn in `emit_message_default` and not in + // `draw_multiline_line`. + MultilineLine(usize), +} + +#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)] +pub(crate) struct LineAnnotation<'a> { + /// Start column. + /// Note that it is important that this field goes + /// first, so that when we sort, we sort orderings by start + /// column. + pub start: Loc, + + /// End column within the line (exclusive) + pub end: Loc, + + /// level + pub kind: AnnotationKind, + + /// Optional label to display adjacent to the annotation. + pub label: Option>, + + /// Is this a single line, multiline or multiline span minimized down to a + /// smaller span. + pub annotation_type: LineAnnotationType, + + /// Whether the source code should be highlighted + pub highlight_source: bool, +} + +impl LineAnnotation<'_> { + pub(crate) fn is_primary(&self) -> bool { + self.kind == AnnotationKind::Primary + } + + /// Whether this annotation is a vertical line placeholder. + pub(crate) fn is_line(&self) -> bool { + matches!(self.annotation_type, LineAnnotationType::MultilineLine(_)) + } + + /// Length of this annotation as displayed in the stderr output + pub(crate) fn len(&self) -> usize { + // Account for usize underflows + self.end.display.abs_diff(self.start.display) + } + + pub(crate) fn has_label(&self) -> bool { + if let Some(label) = &self.label { + // Consider labels with no text as effectively not being there + // to avoid weird output with unnecessary vertical lines, like: + // + // X | fn foo(x: u32) { + // | -------^------ + // | | | + // | | + // | + // + // Note that this would be the complete output users would see. + !label.is_empty() + } else { + false + } + } + + pub(crate) fn takes_space(&self) -> bool { + // Multiline annotations always have to keep vertical space. + matches!( + self.annotation_type, + LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_) + ) + } +} + +#[derive(Clone, Copy, Debug)] +pub(crate) enum DisplaySuggestion { + Underline, + Diff, + None, + Add, +} + +impl DisplaySuggestion { + fn new(complete: &str, patches: &[TrimmedPatch<'_>], sm: &SourceMap<'_>) -> Self { + let has_deletion = patches + .iter() + .any(|p| p.is_deletion(sm) || p.is_destructive_replacement(sm)); + let is_multiline = complete.lines().count() > 1; + if has_deletion && !is_multiline { + DisplaySuggestion::Diff + } else if patches.len() == 1 + && patches.first().is_some_and(|p| { + p.replacement.ends_with('\n') && p.replacement.trim() == complete.trim() + }) + { + // We are adding a line(s) of code before code that was already there. + DisplaySuggestion::Add + } else if (patches.len() != 1 || patches[0].replacement.trim() != complete.trim()) + && !is_multiline + { + DisplaySuggestion::Underline + } else { + DisplaySuggestion::None + } + } +} + +// We replace some characters so the CLI output is always consistent and underlines aligned. +// Keep the following list in sync with `rustc_span::char_width`. +const OUTPUT_REPLACEMENTS: &[(char, &str)] = &[ + // In terminals without Unicode support the following will be garbled, but in *all* terminals + // the underlying codepoint will be as well. We could gate this replacement behind a "unicode + // support" gate. + ('\0', "␀"), + ('\u{0001}', "␁"), + ('\u{0002}', "␂"), + ('\u{0003}', "␃"), + ('\u{0004}', "␄"), + ('\u{0005}', "␅"), + ('\u{0006}', "␆"), + ('\u{0007}', "␇"), + ('\u{0008}', "␈"), + ('\t', " "), // We do our own tab replacement + ('\u{000b}', "␋"), + ('\u{000c}', "␌"), + ('\u{000d}', "␍"), + ('\u{000e}', "␎"), + ('\u{000f}', "␏"), + ('\u{0010}', "␐"), + ('\u{0011}', "␑"), + ('\u{0012}', "␒"), + ('\u{0013}', "␓"), + ('\u{0014}', "␔"), + ('\u{0015}', "␕"), + ('\u{0016}', "␖"), + ('\u{0017}', "␗"), + ('\u{0018}', "␘"), + ('\u{0019}', "␙"), + ('\u{001a}', "␚"), + ('\u{001b}', "␛"), + ('\u{001c}', "␜"), + ('\u{001d}', "␝"), + ('\u{001e}', "␞"), + ('\u{001f}', "␟"), + ('\u{007f}', "␡"), + ('\u{200d}', ""), // Replace ZWJ for consistent terminal output of grapheme clusters. + ('\u{202a}', "�"), // The following unicode text flow control characters are inconsistently + ('\u{202b}', "�"), // supported across CLIs and can cause confusion due to the bytes on disk + ('\u{202c}', "�"), // not corresponding to the visible source code, so we replace them always. + ('\u{202d}', "�"), + ('\u{202e}', "�"), + ('\u{2066}', "�"), + ('\u{2067}', "�"), + ('\u{2068}', "�"), + ('\u{2069}', "�"), +]; + +pub(crate) fn normalize_whitespace(s: &str) -> Cow<'_, str> { + if !s + .chars() + .any(|user| OUTPUT_REPLACEMENTS.iter().any(|(bad, _)| user == *bad)) + { + return Cow::Borrowed(s); + } + + // Scan the input string for a character in the ordered table above. + // If it's present, replace it with its alternative string (it can be more than 1 char!). + // Otherwise, retain the input char. + let normalized = s.chars().fold(String::with_capacity(s.len()), |mut s, c| { + match OUTPUT_REPLACEMENTS.binary_search_by_key(&c, |(k, _)| *k) { + Ok(i) => s.push_str(OUTPUT_REPLACEMENTS[i].1), + _ => s.push(c), + } + s + }); + Cow::Owned(normalized) +} + +#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq)] +pub(crate) enum ElementStyle { + MainHeaderMsg, + HeaderMsg, + LineAndColumn, + LineNumber, + Quotation, + UnderlinePrimary, + UnderlineSecondary, + LabelPrimary, + LabelSecondary, + NoStyle, + Level(LevelInner), + Addition, + Removal, +} + +impl ElementStyle { + pub(crate) fn color_spec(&self, level: &Level<'_>, stylesheet: &Stylesheet) -> Style { + match self { + ElementStyle::Addition => stylesheet.addition, + ElementStyle::Removal => stylesheet.removal, + ElementStyle::LineAndColumn => stylesheet.none, + ElementStyle::LineNumber => stylesheet.line_num, + ElementStyle::Quotation => stylesheet.none, + ElementStyle::MainHeaderMsg => stylesheet.emphasis, + ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary => level.style(stylesheet), + ElementStyle::UnderlineSecondary | ElementStyle::LabelSecondary => stylesheet.context, + ElementStyle::HeaderMsg | ElementStyle::NoStyle => stylesheet.none, + ElementStyle::Level(lvl) => lvl.style(stylesheet), + } + } +} + +#[derive(Debug, Clone, Copy)] +pub(crate) struct UnderlineParts { + pub(crate) style: ElementStyle, + pub(crate) underline: char, + pub(crate) label_start: char, + pub(crate) vertical_text_line: char, + pub(crate) multiline_vertical: char, + pub(crate) multiline_horizontal: char, + pub(crate) multiline_whole_line: char, + pub(crate) multiline_start_down: char, + pub(crate) bottom_right: char, + pub(crate) top_left: char, + pub(crate) top_right_flat: char, + pub(crate) bottom_left: char, + pub(crate) multiline_end_up: char, + pub(crate) multiline_end_same_line: char, + pub(crate) multiline_bottom_right_with_text: char, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum TitleStyle { + MainHeader, + Header, + Secondary, +} + +struct PreProcessedGroup<'a> { + group: &'a Group<'a>, + elements: Vec>, + primary_path: Option<&'a Cow<'a, str>>, + max_depth: usize, +} + +enum PreProcessedElement<'a> { + Message(&'a Message<'a>), + Cause( + ( + &'a Snippet<'a, Annotation<'a>>, + SourceMap<'a>, + Vec>, + ), + ), + Suggestion( + ( + &'a Snippet<'a, Patch<'a>>, + SourceMap<'a>, + SplicedLines<'a>, + DisplaySuggestion, + ), + ), + Origin(&'a Origin<'a>), + Padding(Padding), +} + +fn pre_process<'a>( + groups: &'a [Group<'a>], +) -> ( + Option, + Option<&'a Cow<'a, str>>, + Vec>, +) { + let mut max_line_num = None; + let mut og_primary_path = None; + let mut out = Vec::with_capacity(groups.len()); + for group in groups { + let mut elements = Vec::with_capacity(group.elements.len()); + let mut primary_path = None; + let mut max_depth = 0; + for element in &group.elements { + match element { + Element::Message(message) => { + elements.push(PreProcessedElement::Message(message)); + } + Element::Cause(cause) => { + let sm = SourceMap::new(&cause.source, cause.line_start); + let (depth, annotated_lines) = + sm.annotated_lines(cause.markers.clone(), cause.fold); + + let show_snippet = !cause.markers.iter().any(|s| s.is_file_level); + if show_snippet { + if cause.fold { + let end = cause + .markers + .iter() + .map(|a| a.span.end) + .max() + .unwrap_or(cause.source.len()) + .min(cause.source.len()); + + max_line_num = Some(max( + cause.line_start + newline_count(&cause.source[..end]), + max_line_num.unwrap_or(0), + )); + } else { + max_line_num = Some(max( + cause.line_start + newline_count(&cause.source), + max_line_num.unwrap_or(0), + )); + } + max_depth = max(depth, max_depth); + } + + if primary_path.is_none() { + primary_path = Some(cause.path.as_ref()); + } + elements.push(PreProcessedElement::Cause((cause, sm, annotated_lines))); + } + Element::Suggestion(suggestion) => { + let sm = SourceMap::new(&suggestion.source, suggestion.line_start); + if let Some((complete, patches, highlights, replaced_highlights)) = + sm.splice_lines(suggestion.markers.clone(), suggestion.fold) + { + let display_suggestion = DisplaySuggestion::new(&complete, &patches, &sm); + + if suggestion.fold { + if let Some(first) = patches.first() { + let (l_start, _) = + sm.span_to_locations(first.original_span.clone()); + let nc = newline_count(&complete); + let sugg_max_line_num = match display_suggestion { + DisplaySuggestion::Underline => l_start.line, + DisplaySuggestion::Diff => { + let file_lines = sm.span_to_lines(first.span.clone()); + file_lines + .last() + .map_or(l_start.line + nc, |line| line.line_index) + } + DisplaySuggestion::None => l_start.line + nc, + DisplaySuggestion::Add => l_start.line + nc, + }; + max_line_num = + Some(max(sugg_max_line_num, max_line_num.unwrap_or(0))); + } + } else { + max_line_num = Some(max( + suggestion.line_start + newline_count(&complete), + max_line_num.unwrap_or(0), + )); + } + + elements.push(PreProcessedElement::Suggestion(( + suggestion, + sm, + (complete, patches, highlights, replaced_highlights), + display_suggestion, + ))); + } + } + Element::Origin(origin) => { + if primary_path.is_none() { + primary_path = Some(origin.path.as_ref()); + } + elements.push(PreProcessedElement::Origin(origin)); + } + Element::Padding(padding) => { + elements.push(PreProcessedElement::Padding(padding.clone())); + } + } + } + let group = PreProcessedGroup { + group, + elements, + primary_path: primary_path.unwrap_or_default(), + max_depth, + }; + if og_primary_path.is_none() && group.primary_path.is_some() { + og_primary_path = group.primary_path; + } + out.push(group); + } + + (max_line_num, og_primary_path, out) +} + +fn newline_count(body: &str) -> usize { + #[cfg(feature = "simd")] + { + // Trailing newlines do not count towards the number of lines + // (this is based into `str::lines`) + let trailing_newline = body.ends_with('\n'); + memchr::memchr_iter(b'\n', body.as_bytes()).count() - usize::from(trailing_newline) + } + #[cfg(not(feature = "simd"))] + { + body.lines().count().saturating_sub(1) + } +} + +#[cfg(test)] +mod test { + use super::{OUTPUT_REPLACEMENTS, newline_count}; + use snapbox::IntoData; + + fn format_replacements(replacements: Vec<(char, &str)>) -> String { + replacements + .into_iter() + .map(|r| format!(" {r:?}")) + .collect::>() + .join("\n") + } + + #[test] + /// The [`OUTPUT_REPLACEMENTS`] array must be sorted (for binary search to + /// work) and must contain no duplicate entries + fn ensure_output_replacements_is_sorted() { + let mut expected = OUTPUT_REPLACEMENTS.to_owned(); + expected.sort_by_key(|r| r.0); + expected.dedup_by_key(|r| r.0); + let expected = format_replacements(expected); + let actual = format_replacements(OUTPUT_REPLACEMENTS.to_owned()); + snapbox::assert_data_eq!(actual, expected.into_data().raw()); + } + + #[test] + fn ensure_newline_count_correct() { + let source = r#" + cargo-features = ["path-bases"] + + [package] + name = "foo" + version = "0.5.0" + authors = ["wycats@example.com"] + + [dependencies] + bar = { base = '^^not-valid^^', path = 'bar' } + "#; + assert_eq!(newline_count(source), 10); + + assert_eq!(newline_count(""), 0); + + assert_eq!(newline_count("one"), 0); + + assert_eq!(newline_count("one\n"), 0); + + assert_eq!(newline_count("one\ntwo"), 1); + + assert_eq!(newline_count("one\ntwo\n"), 1); + + assert_eq!(newline_count("one\n\n"), 1); + + assert_eq!(newline_count("one\r\ntwo\r\n"), 1); + } +} diff --git a/crates/ruff_annotate_snippets/src/renderer/source_map.rs b/crates/ruff_annotate_snippets/src/renderer/source_map.rs new file mode 100644 index 0000000000..2c40ee1a92 --- /dev/null +++ b/crates/ruff_annotate_snippets/src/renderer/source_map.rs @@ -0,0 +1,837 @@ +use alloc::borrow::Cow; +use alloc::string::String; +use alloc::{vec, vec::Vec}; +use core::cmp::{max, min}; +use core::ops::Range; + +use crate::renderer::{LineAnnotation, LineAnnotationType, char_width, num_overlap}; +use crate::{Annotation, AnnotationKind, Patch}; + +#[derive(Debug)] +pub(crate) struct SourceMap<'a> { + lines: Vec>, + pub(crate) source: &'a str, +} + +impl<'a> SourceMap<'a> { + pub(crate) fn new(source: &'a str, line_start: usize) -> Self { + // Empty sources do have a "line", but it is empty, so we need to add + // a line with an empty string to the source map. + if source.is_empty() { + return Self { + lines: vec![LineInfo { + line: "", + line_index: line_start, + start_byte: 0, + end_byte: 0, + end_line_size: 0, + }], + source, + }; + } + + let mut current_index = 0; + + let mut mapping = vec![]; + for (idx, (line, end_line)) in CursorLines::new(source).enumerate() { + let line_length = line.len(); + let line_range = current_index..current_index + line_length; + let end_line_size = end_line.len(); + + mapping.push(LineInfo { + line, + line_index: line_start + idx, + start_byte: line_range.start, + end_byte: line_range.end + end_line_size, + end_line_size, + }); + + current_index += line_length + end_line_size; + } + Self { + lines: mapping, + source, + } + } + + pub(crate) fn get_line(&self, idx: usize) -> Option<&'a str> { + self.lines + .iter() + .find(|l| l.line_index == idx) + .map(|info| info.line) + } + + pub(crate) fn span_to_locations(&self, span: Range) -> (Loc, Loc) { + let start_info = self + .lines + .iter() + .find(|info| span.start >= info.start_byte && span.start < info.end_byte) + .unwrap_or(self.lines.last().unwrap()); + let (mut start_char_pos, start_display_pos) = start_info.line + [0..(span.start - start_info.start_byte).min(start_info.line.len())] + .chars() + .fold((0, 0), |(char_pos, byte_pos), c| { + let display = char_width(c); + (char_pos + 1, byte_pos + display) + }); + // correct the char pos if we are highlighting the end of a line + if (span.start - start_info.start_byte).saturating_sub(start_info.line.len()) > 0 { + start_char_pos += 1; + } + let start = Loc { + line: start_info.line_index, + char: start_char_pos, + display: start_display_pos, + byte: span.start, + }; + + if span.start == span.end { + return (start, start); + } + + let (end_idx, end_info, eof) = self + .lines + .iter() + .enumerate() + .find(|(_, info)| span.end >= info.start_byte && span.end < info.end_byte) + .map(|(idx, info)| (idx, info, false)) + .unwrap_or((self.lines.len() - 1, self.lines.last().unwrap(), true)); + let (end_char_pos, end_display_pos) = end_info.line + [0..(span.end - end_info.start_byte).min(end_info.line.len())] + .chars() + .fold((0, 0), |(char_pos, byte_pos), c| { + let display = char_width(c); + (char_pos + 1, byte_pos + display) + }); + + let mut end = Loc { + line: end_info.line_index, + char: end_char_pos, + display: end_display_pos, + byte: span.end, + }; + if start.line < end.line && end.char == 0 && !eof { + let prev_line_info = &self.lines[end_idx - 1]; + let (end_char_pos, end_display_pos) = prev_line_info.line + [0..(span.end - prev_line_info.start_byte).min(prev_line_info.line.len())] + .chars() + .fold((0, 0), |(char_pos, byte_pos), c| { + let display = char_width(c); + (char_pos + 1, byte_pos + display) + }); + if prev_line_info.end_byte == start.byte { + end = Loc { + line: prev_line_info.line_index, + char: end_char_pos + 1, + display: end_display_pos + 1, + byte: span.end, + }; + } else { + end = Loc { + line: prev_line_info.line_index, + char: end_char_pos, + display: end_display_pos, + byte: span.end, + }; + } + } + if start.line != end.line && end.byte > end_info.end_byte - end_info.end_line_size { + end.char += 1; + end.display += 1; + } + + (start, end) + } + + pub(crate) fn span_to_snippet(&self, span: Range) -> Option<&str> { + self.source.get(span) + } + + pub(crate) fn span_to_lines(&self, span: Range) -> Vec<&LineInfo<'a>> { + let mut lines = vec![]; + let start = span.start; + let end = span.end; + for line_info in &self.lines { + if start >= line_info.end_byte { + continue; + } + if end < line_info.start_byte { + break; + } + lines.push(line_info); + } + + if lines.is_empty() && !self.lines.is_empty() { + lines.push(self.lines.last().unwrap()); + } + + lines + } + + pub(crate) fn annotated_lines( + &self, + annotations: Vec>, + fold: bool, + ) -> (usize, Vec>) { + let source_len = self.source.len(); + if let Some(bigger) = annotations.iter().find_map(|x| { + // Allow highlighting one past the last character in the source. + if source_len + 1 < x.span.end { + Some(&x.span) + } else { + None + } + }) { + panic!("Annotation range `{bigger:?}` is beyond the end of buffer `{source_len}`") + } + + let mut annotated_line_infos = self + .lines + .iter() + .map(|info| AnnotatedLineInfo { + line: info.line, + line_index: info.line_index, + annotations: vec![], + keep: false, + }) + .collect::>(); + let mut multiline_annotations = vec![]; + + for Annotation { + span, + label, + kind, + highlight_source, + is_file_level: _, + } in annotations + { + let (lo, mut hi) = self.span_to_locations(span.clone()); + if kind == AnnotationKind::Visible { + for line_idx in lo.line..=hi.line { + self.keep_line(&mut annotated_line_infos, line_idx); + } + continue; + } + // Watch out for "empty spans". If we get a span like 6..6, we + // want to just display a `^` at 6, so convert that to + // 6..7. This is degenerate input, but it's best to degrade + // gracefully -- and the parser likes to supply a span like + // that for EOF, in particular. + + if lo.display == hi.display && lo.line == hi.line { + hi.display += 1; + } + + if lo.line == hi.line { + let line_ann = LineAnnotation { + start: lo, + end: hi, + kind, + label, + annotation_type: LineAnnotationType::Singleline, + highlight_source, + }; + self.add_annotation_to_file(&mut annotated_line_infos, lo.line, line_ann); + } else { + multiline_annotations.push(MultilineAnnotation { + depth: 1, + start: lo, + end: hi, + kind, + label, + overlaps_exactly: false, + highlight_source, + }); + } + } + + let mut primary_spans = vec![]; + + // Find overlapping multiline annotations, put them at different depths + multiline_annotations.sort_by_key(|ml| (ml.start.line, usize::MAX - ml.end.line)); + for (outer_i, ann) in multiline_annotations.clone().into_iter().enumerate() { + if ann.kind.is_primary() { + primary_spans.push((ann.start, ann.end)); + } + for (inner_i, a) in &mut multiline_annotations.iter_mut().enumerate() { + // Move all other multiline annotations overlapping with this one + // one level to the right. + if !ann.same_span(a) + && num_overlap(ann.start.line, ann.end.line, a.start.line, a.end.line, true) + { + a.increase_depth(); + } else if ann.same_span(a) && outer_i != inner_i { + a.overlaps_exactly = true; + } else { + if primary_spans + .iter() + .any(|(s, e)| a.start == *s && a.end == *e) + { + a.kind = AnnotationKind::Primary; + } + break; + } + } + } + + let mut max_depth = 0; // max overlapping multiline spans + for ann in &multiline_annotations { + max_depth = max(max_depth, ann.depth); + } + // Change order of multispan depth to minimize the number of overlaps in the ASCII art. + for a in &mut multiline_annotations { + a.depth = max_depth - a.depth + 1; + } + for ann in multiline_annotations { + let mut end_ann = ann.as_end(); + if ann.overlaps_exactly { + end_ann.annotation_type = LineAnnotationType::Singleline; + } else { + // avoid output like + // + // | foo( + // | _____^ + // | |_____| + // | || bar, + // | || ); + // | || ^ + // | ||______| + // | |______foo + // | baz + // + // and instead get + // + // | foo( + // | _____^ + // | | bar, + // | | ); + // | | ^ + // | | | + // | |______foo + // | baz + self.add_annotation_to_file( + &mut annotated_line_infos, + ann.start.line, + ann.as_start(), + ); + // 4 is the minimum vertical length of a multiline span when presented: two lines + // of code and two lines of underline. This is not true for the special case where + // the beginning doesn't have an underline, but the current logic seems to be + // working correctly. + let middle = min(ann.start.line + 4, ann.end.line); + // We'll show up to 4 lines past the beginning of the multispan start. + // We will *not* include the tail of lines that are only whitespace, a comment or + // a bare delimiter. + let filter = |s: &str| { + let s = s.trim(); + // Consider comments as empty, but don't consider docstrings to be empty. + !(s.starts_with("//") && !(s.starts_with("///") || s.starts_with("//!"))) + // Consider lines with nothing but whitespace, a single delimiter as empty. + && !["", "{", "}", "(", ")", "[", "]"].contains(&s) + }; + let until = (ann.start.line..middle) + .rev() + .filter_map(|line| self.get_line(line).map(|s| (line + 1, s))) + .find(|(_, s)| filter(s)) + .map_or(ann.start.line, |(line, _)| line); + for line in ann.start.line + 1..until { + // Every `|` that joins the beginning of the span (`___^`) to the end (`|__^`). + self.add_annotation_to_file(&mut annotated_line_infos, line, ann.as_line()); + } + let line_end = ann.end.line - 1; + let end_is_empty = self.get_line(line_end).is_some_and(|s| !filter(s)); + if middle < line_end && !end_is_empty { + self.add_annotation_to_file(&mut annotated_line_infos, line_end, ann.as_line()); + } + } + self.add_annotation_to_file(&mut annotated_line_infos, end_ann.end.line, end_ann); + } + + if fold { + annotated_line_infos.retain(|l| !l.annotations.is_empty() || l.keep); + } + + (max_depth, annotated_line_infos) + } + + fn add_annotation_to_file( + &self, + annotated_line_infos: &mut Vec>, + line_index: usize, + line_ann: LineAnnotation<'a>, + ) { + if let Some(line_info) = annotated_line_infos + .iter_mut() + .find(|line_info| line_info.line_index == line_index) + { + line_info.annotations.push(line_ann); + } else { + let info = self + .lines + .iter() + .find(|l| l.line_index == line_index) + .unwrap(); + annotated_line_infos.push(AnnotatedLineInfo { + line: info.line, + line_index, + annotations: vec![line_ann], + keep: false, + }); + annotated_line_infos.sort_by_key(|l| l.line_index); + } + } + + fn keep_line(&self, annotated_line_infos: &mut Vec>, line_index: usize) { + if let Some(line_info) = annotated_line_infos + .iter_mut() + .find(|line_info| line_info.line_index == line_index) + { + line_info.keep = true; + } else { + let info = self + .lines + .iter() + .find(|l| l.line_index == line_index) + .unwrap(); + annotated_line_infos.push(AnnotatedLineInfo { + line: info.line, + line_index, + annotations: vec![], + keep: true, + }); + annotated_line_infos.sort_by_key(|l| l.line_index); + } + } + + pub(crate) fn splice_lines<'b>( + &'a self, + mut patches: Vec>, + fold: bool, + ) -> Option> { + fn push_trailing(buf: &mut String, line_opt: Option<&str>, lo: &Loc, hi_opt: Option<&Loc>) { + // Convert CharPos to Usize, as CharPose is character offset + // Extract low index and high index + let (lo, hi_opt) = (lo.char, hi_opt.map(|hi| hi.char)); + if let Some(line) = line_opt { + if let Some(lo) = line.char_indices().map(|(i, _)| i).nth(lo) { + // Get high index while account for rare unicode and emoji with char_indices + let hi_opt = hi_opt.and_then(|hi| line.char_indices().map(|(i, _)| i).nth(hi)); + match hi_opt { + // If high index exist, take string from low to high index + Some(hi) if hi > lo => buf.push_str(&line[lo..hi]), + Some(_) => (), + // If high index absence, take string from low index till end string.len + None => buf.push_str(&line[lo..]), + } + } + // If high index is None + if hi_opt.is_none() { + buf.push('\n'); + } + } + } + + let source_len = self.source.len(); + if let Some(bigger) = patches.iter().find_map(|x| { + // Allow patching one past the last character in the source. + if source_len + 1 < x.span.end { + Some(&x.span) + } else { + None + } + }) { + panic!("Patch span `{bigger:?}` is beyond the end of buffer `{source_len}`") + } + + // Assumption: all spans are in the same file, and all spans + // are disjoint. Sort in ascending order. + patches.sort_by_key(|p| p.span.start); + + // Find the bounding span. + let (lo, hi) = if fold { + let lo = patches + .iter() + .map(|p| p.span.clone()) + .min_by_key(|s| s.start)?; + let hi = patches + .iter() + .map(|p| p.span.clone()) + .max_by_key(|s| s.end)?; + (lo, hi) + } else { + let lo = 0..source_len; + let hi = 0..source_len; + (lo, hi) + }; + + let lines = self.span_to_lines(lo.start..hi.end); + let (bounding_lo, _) = self.span_to_locations(lo); + let (_, bounding_hi) = self.span_to_locations(hi); + + let mut highlights = vec![]; + // To build up the result, we do this for each span: + // - push the line segment trailing the previous span + // (at the beginning a "phantom" span pointing at the start of the line) + // - push lines between the previous and current span (if any) + // - if the previous and current span are not on the same line + // push the line segment leading up to the current span + // - splice in the span substitution + // + // Finally push the trailing line segment of the last span + let mut prev_hi = bounding_lo; + prev_hi.char = 0; + let mut prev_line = lines.first().map(|line| line.line); + let mut buf = String::new(); + + let trimmed_patches = patches + .into_iter() + // If this is a replacement of, e.g. `"a"` into `"ab"`, adjust the + // suggestion and snippet to look as if we just suggested to add + // `"b"`, which is typically much easier for the user to understand. + .map(|part| part.trim_trivial_replacements(self.source)) + .collect::>(); + let mut line_highlight = vec![]; + // We need to keep track of the difference between the existing code and the added + // or deleted code in order to point at the correct column *after* substitution. + let mut acc = 0; + for part in &trimmed_patches { + let (cur_lo, cur_hi) = self.span_to_locations(part.span.clone()); + if prev_hi.line == cur_lo.line { + push_trailing(&mut buf, prev_line, &prev_hi, Some(&cur_lo)); + } else { + acc = 0; + highlights.push(core::mem::take(&mut line_highlight)); + push_trailing(&mut buf, prev_line, &prev_hi, None); + // push lines between the previous and current span (if any) + for idx in prev_hi.line + 1..(cur_lo.line) { + if let Some(line) = self.get_line(idx) { + buf.push_str(line.as_ref()); + buf.push('\n'); + highlights.push(core::mem::take(&mut line_highlight)); + } + } + if let Some(cur_line) = self.get_line(cur_lo.line) { + let end = match cur_line.char_indices().nth(cur_lo.char) { + Some((i, _)) => i, + None => cur_line.len(), + }; + buf.push_str(&cur_line[..end]); + } + } + // Add a whole line highlight per line in the snippet. + let len: isize = part + .replacement + .split('\n') + .next() + .unwrap_or(&part.replacement) + .chars() + .map(|c| match c { + '\t' => 4, + _ => 1, + }) + .sum(); + line_highlight.push(SubstitutionHighlight { + start: (cur_lo.char as isize + acc) as usize, + end: (cur_lo.char as isize + acc + len) as usize, + }); + buf.push_str(&part.replacement); + // Account for the difference between the width of the current code and the + // snippet being suggested, so that the *later* suggestions are correctly + // aligned on the screen. Note that cur_hi and cur_lo can be on different + // lines, so cur_hi.col can be smaller than cur_lo.col + acc += len - (cur_hi.char as isize - cur_lo.char as isize); + prev_hi = cur_hi; + prev_line = self.get_line(prev_hi.line); + for line in part.replacement.split('\n').skip(1) { + acc = 0; + highlights.push(core::mem::take(&mut line_highlight)); + let end: usize = line + .chars() + .map(|c| match c { + '\t' => 4, + _ => 1, + }) + .sum(); + line_highlight.push(SubstitutionHighlight { start: 0, end }); + } + } + highlights.push(core::mem::take(&mut line_highlight)); + if fold { + // if the replacement already ends with a newline, don't print the next line + if !buf.ends_with('\n') { + push_trailing(&mut buf, prev_line, &prev_hi, None); + } + } else { + // Add the trailing part of the source after the last patch + if let Some(snippet) = self.span_to_snippet(prev_hi.byte..source_len) { + buf.push_str(snippet); + for _ in snippet.matches('\n') { + highlights.push(core::mem::take(&mut line_highlight)); + } + } + } + // remove trailing newlines + while buf.ends_with('\n') { + buf.pop(); + } + + let line_count = bounding_hi.line.saturating_sub(bounding_lo.line) + 1; + let mut replaced_highlights: Vec> = vec![Vec::new(); line_count]; + for part in &trimmed_patches { + let (cur_lo, cur_hi) = self.span_to_locations(part.span.clone()); + for line in cur_lo.line..=cur_hi.line { + let start = if line == cur_lo.line { cur_lo.char } else { 0 }; + let end = if line == cur_hi.line { + cur_hi.char + } else { + self.get_line(line).unwrap_or_default().chars().count() + }; + replaced_highlights[line - bounding_lo.line] + .push(SubstitutionHighlight { start, end }); + } + } + + if highlights.iter().all(|parts| parts.is_empty()) { + None + } else { + Some((buf, trimmed_patches, highlights, replaced_highlights)) + } + } +} + +#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)] +pub(crate) struct MultilineAnnotation<'a> { + pub depth: usize, + pub start: Loc, + pub end: Loc, + pub kind: AnnotationKind, + pub label: Option>, + pub overlaps_exactly: bool, + pub highlight_source: bool, +} + +impl<'a> MultilineAnnotation<'a> { + pub(crate) fn increase_depth(&mut self) { + self.depth += 1; + } + + /// Compare two `MultilineAnnotation`s considering only the `Span` they cover. + pub(crate) fn same_span(&self, other: &MultilineAnnotation<'_>) -> bool { + self.start == other.start && self.end == other.end + } + + pub(crate) fn as_start(&self) -> LineAnnotation<'a> { + LineAnnotation { + start: self.start, + end: Loc { + line: self.start.line, + char: self.start.char + 1, + display: self.start.display + 1, + byte: self.start.byte + 1, + }, + kind: self.kind, + label: None, + annotation_type: LineAnnotationType::MultilineStart(self.depth), + highlight_source: self.highlight_source, + } + } + + pub(crate) fn as_end(&self) -> LineAnnotation<'a> { + LineAnnotation { + start: Loc { + line: self.end.line, + char: self.end.char.saturating_sub(1), + display: self.end.display.saturating_sub(1), + byte: self.end.byte.saturating_sub(1), + }, + end: self.end, + kind: self.kind, + label: self.label.clone(), + annotation_type: LineAnnotationType::MultilineEnd(self.depth), + highlight_source: self.highlight_source, + } + } + + pub(crate) fn as_line(&self) -> LineAnnotation<'a> { + LineAnnotation { + start: Loc::default(), + end: Loc::default(), + kind: self.kind, + label: None, + annotation_type: LineAnnotationType::MultilineLine(self.depth), + highlight_source: self.highlight_source, + } + } +} + +#[derive(Debug)] +pub(crate) struct LineInfo<'a> { + pub(crate) line: &'a str, + pub(crate) line_index: usize, + pub(crate) start_byte: usize, + pub(crate) end_byte: usize, + end_line_size: usize, +} + +#[derive(Debug)] +pub(crate) struct AnnotatedLineInfo<'a> { + pub(crate) line: &'a str, + pub(crate) line_index: usize, + pub(crate) annotations: Vec>, + pub(crate) keep: bool, +} + +/// A source code location used for error reporting. +#[derive(Clone, Copy, Debug, Default, PartialOrd, Ord, PartialEq, Eq)] +pub(crate) struct Loc { + /// The (1-based) line number. + pub(crate) line: usize, + /// The (0-based) column offset. + pub(crate) char: usize, + /// The (0-based) column offset when displayed. + pub(crate) display: usize, + /// The (0-based) byte offset. + pub(crate) byte: usize, +} + +struct CursorLines<'a>(&'a str); + +impl CursorLines<'_> { + fn new(src: &str) -> CursorLines<'_> { + CursorLines(src) + } +} + +#[derive(Copy, Clone, Debug, PartialEq)] +enum EndLine { + Eof, + Lf, + Crlf, +} + +impl EndLine { + /// The number of characters this line ending occupies in bytes. + pub(crate) fn len(self) -> usize { + match self { + EndLine::Eof => 0, + EndLine::Lf => 1, + EndLine::Crlf => 2, + } + } +} + +impl<'a> Iterator for CursorLines<'a> { + type Item = (&'a str, EndLine); + + fn next(&mut self) -> Option { + if self.0.is_empty() { + None + } else { + self.0 + .find('\n') + .map(|x| { + let ret = if 0 < x { + if self.0.as_bytes()[x - 1] == b'\r' { + (&self.0[..x - 1], EndLine::Crlf) + } else { + (&self.0[..x], EndLine::Lf) + } + } else { + ("", EndLine::Lf) + }; + self.0 = &self.0[x + 1..]; + ret + }) + .or_else(|| { + let ret = Some((self.0, EndLine::Eof)); + self.0 = ""; + ret + }) + } + } +} + +pub(crate) type SplicedLines<'a> = ( + String, + Vec>, + // Char spans to highlight per line of the post-substitution output. + Vec>, + // Char spans of the replaced (original) code, per original line in the + // bounding range covered by the splice. + Vec>, +); + +/// Used to translate between `Span`s and byte positions within a single output line in highlighted +/// code of structured suggestions. +#[derive(Debug, Clone, Copy)] +pub(crate) struct SubstitutionHighlight { + pub(crate) start: usize, + pub(crate) end: usize, +} + +#[derive(Clone, Debug)] +pub(crate) struct TrimmedPatch<'a> { + pub(crate) original_span: Range, + pub(crate) span: Range, + pub(crate) replacement: Cow<'a, str>, +} + +impl<'a> TrimmedPatch<'a> { + pub(crate) fn is_addition(&self, sm: &SourceMap<'_>) -> bool { + !self.replacement.is_empty() && !self.replaces_meaningful_content(sm) + } + + pub(crate) fn is_deletion(&self, sm: &SourceMap<'_>) -> bool { + self.replacement.trim().is_empty() && self.replaces_meaningful_content(sm) + } + + pub(crate) fn is_replacement(&self, sm: &SourceMap<'_>) -> bool { + !self.replacement.is_empty() && self.replaces_meaningful_content(sm) + } + + /// Whether this is a replacement that overwrites source with a snippet + /// in a way that isn't a superset of the original string. For example, + /// replacing "abc" with "abcde" is not destructive, but replacing it + /// it with "abx" is, since the "c" character is lost. + pub(crate) fn is_destructive_replacement(&self, sm: &SourceMap<'_>) -> bool { + self.is_replacement(sm) + && sm + .span_to_snippet(self.span.clone()) + .is_none_or(|s| as_substr(s.trim(), self.replacement.trim()).is_none()) + } + + fn replaces_meaningful_content(&self, sm: &SourceMap<'_>) -> bool { + sm.span_to_snippet(self.span.clone()) + .map_or(!self.span.is_empty(), |snippet| !snippet.trim().is_empty()) + } +} + +/// Given an original string like `AACC`, and a suggestion like `AABBCC`, try to detect +/// the case where a substring of the suggestion is "sandwiched" in the original, like +/// `BB` is. Return the length of the prefix, the "trimmed" suggestion, and the length +/// of the suffix. +pub(crate) fn as_substr<'a>( + original: &'a str, + suggestion: &'a str, +) -> Option<(usize, &'a str, usize)> { + if let Some(stripped) = suggestion.strip_prefix(original) { + Some((original.len(), stripped, 0)) + } else if let Some(stripped) = suggestion.strip_suffix(original) { + Some((0, stripped, original.len())) + } else { + let common_prefix = original + .chars() + .zip(suggestion.chars()) + .take_while(|(c1, c2)| c1 == c2) + .map(|(c, _)| c.len_utf8()) + .sum(); + let original = &original[common_prefix..]; + let suggestion = &suggestion[common_prefix..]; + if let Some(stripped) = suggestion.strip_suffix(original) { + let common_suffix = original.len(); + Some((common_prefix, stripped, common_suffix)) + } else { + None + } + } +} diff --git a/crates/ruff_annotate_snippets/src/renderer/styled_buffer.rs b/crates/ruff_annotate_snippets/src/renderer/styled_buffer.rs index 73b30cefca..ff51aa41ff 100644 --- a/crates/ruff_annotate_snippets/src/renderer/styled_buffer.rs +++ b/crates/ruff_annotate_snippets/src/renderer/styled_buffer.rs @@ -2,10 +2,13 @@ //! //! [styled_buffer]: https://github.com/rust-lang/rust/blob/894f7a4ba6554d3797404bbf550d9919df060b97/compiler/rustc_errors/src/styled_buffer.rs +use alloc::string::String; +use alloc::{vec, vec::Vec}; +use core::fmt::{self, Write}; + +use crate::Level; +use crate::renderer::ElementStyle; use crate::renderer::stylesheet::Stylesheet; -use anstyle::Style; -use std::fmt; -use std::fmt::Write; #[derive(Debug)] pub(crate) struct StyledBuffer { @@ -15,13 +18,13 @@ pub(crate) struct StyledBuffer { #[derive(Clone, Copy, Debug, PartialEq)] pub(crate) struct StyledChar { ch: char, - style: Style, + style: ElementStyle, } impl StyledChar { - pub(crate) const SPACE: Self = StyledChar::new(' ', Style::new()); + pub(crate) const SPACE: Self = StyledChar::new(' ', ElementStyle::NoStyle); - pub(crate) const fn new(ch: char, style: Style) -> StyledChar { + pub(crate) const fn new(ch: char, style: ElementStyle) -> StyledChar { StyledChar { ch, style } } } @@ -37,33 +40,40 @@ impl StyledBuffer { } } - pub(crate) fn render(&self, stylesheet: &Stylesheet) -> Result { + pub(crate) fn render( + &self, + level: &Level<'_>, + stylesheet: &Stylesheet, + str: &mut String, + ) -> Result<(), fmt::Error> { let capacity = self.lines.iter().map(|line| line.len()).sum(); - let mut str = String::with_capacity(capacity); + str.reserve(capacity); + for (i, line) in self.lines.iter().enumerate() { let mut current_style = stylesheet.none; - for ch in line { - if ch.style != current_style { + for StyledChar { ch, style } in line { + let ch_style = style.color_spec(level, stylesheet); + if ch_style != current_style { if !line.is_empty() { - write!(str, "{}", current_style.render_reset())?; + write!(str, "{current_style:#}")?; } - current_style = ch.style; - write!(str, "{}", current_style.render())?; + current_style = ch_style; + write!(str, "{current_style}")?; } - str.push(ch.ch); + str.push(*ch); } - write!(str, "{}", current_style.render_reset())?; + write!(str, "{current_style:#}")?; if i != self.lines.len() - 1 { str.push('\n'); } } - Ok(str) + Ok(()) } /// Sets `chr` with `style` for given `line`, `col`. /// If `line` does not exist in our buffer, adds empty lines up to the given /// and fills the last line with unstyled whitespace. - pub(crate) fn putc(&mut self, line: usize, col: usize, chr: char, style: Style) { + pub(crate) fn putc(&mut self, line: usize, col: usize, chr: char, style: ElementStyle) { self.ensure_lines(line); if col >= self.lines[line].len() { self.lines[line].resize(col + 1, StyledChar::SPACE); @@ -74,24 +84,29 @@ impl StyledBuffer { /// Sets `string` with `style` for given `line`, starting from `col`. /// If `line` does not exist in our buffer, adds empty lines up to the given /// and fills the last line with unstyled whitespace. - pub(crate) fn puts(&mut self, line: usize, col: usize, string: &str, style: Style) { + pub(crate) fn puts(&mut self, line: usize, col: usize, string: &str, style: ElementStyle) { if string.is_empty() { + // don't add trailing whitespace (from column offset) for blank strings return; } + self.ensure_lines(line); - let char_count = string.chars().count(); - let needed = col + char_count; - if needed > self.lines[line].len() { - self.lines[line].resize(needed, StyledChar::SPACE); - } let line = &mut self.lines[line]; - for (i, c) in string.chars().enumerate() { - line[col + i] = StyledChar::new(c, style); + + let new_len = col + string.chars().count(); + if new_len > line.len() { + line.resize(new_len, StyledChar::SPACE); + } + + for (offset, chr) in string.chars().enumerate() { + let col = col + offset; + line[col] = StyledChar::new(chr, style); } } + /// For given `line` inserts `string` with `style` after old content of that line, /// adding lines if needed - pub(crate) fn append(&mut self, line: usize, string: &str, style: Style) { + pub(crate) fn append(&mut self, line: usize, string: &str, style: ElementStyle) { if line >= self.lines.len() { self.puts(line, 0, string, style); } else { @@ -100,7 +115,58 @@ impl StyledBuffer { } } + pub(crate) fn replace(&mut self, line: usize, start: usize, end: usize, string: &str) { + if start == end { + return; + } + // If the replacement range would be out of bounds, do nothing, as we + // can't replace things that don't exist. + if start > self.lines[line].len() || end > self.lines[line].len() { + return; + }; + self.lines[line].splice( + start..end, + string + .chars() + .map(|c| StyledChar::new(c, ElementStyle::LineNumber)), + ); + } + pub(crate) fn num_lines(&self) -> usize { self.lines.len() } + + /// Set `style` for `line`, `col_start..col_end` range if: + /// 1. That line and column range exist in `StyledBuffer` + /// 2. `overwrite` is `true` or existing style is `Style::NoStyle` or `Style::Quotation` + pub(crate) fn set_style_range( + &mut self, + line: usize, + col_start: usize, + col_end: usize, + style: ElementStyle, + overwrite: bool, + ) { + for col in col_start..col_end { + self.set_style(line, col, style, overwrite); + } + } + + /// Set `style` for `line`, `col` if: + /// 1. That line and column exist in `StyledBuffer` + /// 2. `overwrite` is `true` or existing style is `Style::NoStyle` or `Style::Quotation` + pub(crate) fn set_style( + &mut self, + line: usize, + col: usize, + style: ElementStyle, + overwrite: bool, + ) { + if let Some(ref mut line) = self.lines.get_mut(line) + && let Some(StyledChar { style: s, .. }) = line.get_mut(col) + && (overwrite || matches!(s, ElementStyle::NoStyle | ElementStyle::Quotation)) + { + *s = style; + } + } } diff --git a/crates/ruff_annotate_snippets/src/renderer/stylesheet.rs b/crates/ruff_annotate_snippets/src/renderer/stylesheet.rs index d9ec70d6d0..075cad42a9 100644 --- a/crates/ruff_annotate_snippets/src/renderer/stylesheet.rs +++ b/crates/ruff_annotate_snippets/src/renderer/stylesheet.rs @@ -7,10 +7,12 @@ pub(crate) struct Stylesheet { pub(crate) info: Style, pub(crate) note: Style, pub(crate) help: Style, - pub(crate) line_no: Style, + pub(crate) line_num: Style, pub(crate) emphasis: Style, pub(crate) none: Style, - pub(crate) hyperlink: bool, + pub(crate) context: Style, + pub(crate) addition: Style, + pub(crate) removal: Style, } impl Default for Stylesheet { @@ -27,44 +29,12 @@ impl Stylesheet { info: Style::new(), note: Style::new(), help: Style::new(), - line_no: Style::new(), + line_num: Style::new(), emphasis: Style::new(), none: Style::new(), - hyperlink: false, + context: Style::new(), + addition: Style::new(), + removal: Style::new(), } } } - -impl Stylesheet { - pub(crate) fn error(&self) -> &Style { - &self.error - } - - pub(crate) fn warning(&self) -> &Style { - &self.warning - } - - pub(crate) fn info(&self) -> &Style { - &self.info - } - - pub(crate) fn note(&self) -> &Style { - &self.note - } - - pub(crate) fn help(&self) -> &Style { - &self.help - } - - pub(crate) fn line_no(&self) -> &Style { - &self.line_no - } - - pub(crate) fn emphasis(&self) -> &Style { - &self.emphasis - } - - pub(crate) fn none(&self) -> &Style { - &self.none - } -} diff --git a/crates/ruff_annotate_snippets/src/snippet.rs b/crates/ruff_annotate_snippets/src/snippet.rs index 363f8b2558..d10553453c 100644 --- a/crates/ruff_annotate_snippets/src/snippet.rs +++ b/crates/ruff_annotate_snippets/src/snippet.rs @@ -1,68 +1,222 @@ //! Structures used as an input for the library. -//! -//! Example: -//! -//! ``` -//! use ruff_annotate_snippets::*; -//! -//! Level::Error.title("mismatched types") -//! .snippet(Snippet::source("Foo").line_start(51).origin("src/format.rs")) -//! .snippet(Snippet::source("Faa").line_start(129).origin("src/display.rs")); -//! ``` - -use std::ops::Range; - -#[derive(Copy, Clone, Debug, Default, PartialEq)] + +use alloc::borrow::{Cow, ToOwned}; +use alloc::string::String; +use alloc::{vec, vec::Vec}; +use core::ops::Range; + +use crate::Level; +use crate::renderer::source_map::{TrimmedPatch, as_substr}; + +pub(crate) const ERROR_TXT: &str = "error"; +pub(crate) const HELP_TXT: &str = "help"; +pub(crate) const INFO_TXT: &str = "info"; +pub(crate) const NOTE_TXT: &str = "note"; +pub(crate) const WARNING_TXT: &str = "warning"; + +/// A [diagnostic message][Title] and any associated [context][Element] to help users +/// understand it +/// +/// The first [`Group`] is the ["primary" group][Level::primary_title], ie it contains the diagnostic +/// message. +/// +/// All subsequent [`Group`]s are for distinct pieces of [context][Level::secondary_title]. +/// The primary group will be visually distinguished to help tell them apart. +pub type Report<'a> = &'a [Group<'a>]; + +#[derive(Clone, Debug, Default)] pub(crate) struct Id<'a> { - pub(crate) id: &'a str, - pub(crate) url: Option<&'a str>, + pub(crate) id: Option>, + pub(crate) url: Option>, } -/// Primary structure provided for formatting +/// A [`Title`] with supporting [context][Element] within a [`Report`] /// -/// See [`Level::title`] to create a [`Message`] -#[derive(Debug)] -pub struct Message<'a> { - pub(crate) level: Level, - pub(crate) id: Option>, - pub(crate) title: &'a str, - pub(crate) snippets: Vec>, - pub(crate) footer: Vec>, - pub(crate) is_fixable: bool, +/// [Decor][crate::renderer::DecorStyle] is used to visually connect [`Element`]s of a `Group`. +/// +/// Generally, you will create separate group's for: +/// - New [`Snippet`]s, especially if they need their own [`AnnotationKind::Primary`] +/// - Each logically distinct set of [suggestions][Patch`] +/// +/// # Example +/// +/// ```rust +/// # #[allow(clippy::needless_doctest_main)] +#[doc = include_str!("../examples/highlight_message.rs")] +/// ``` +#[doc = include_str!("../examples/highlight_message.svg")] +#[derive(Clone, Debug)] +pub struct Group<'a> { + pub(crate) primary_level: Level<'a>, + pub(crate) title: Option>, + pub(crate) elements: Vec>, pub(crate) lineno_offset: usize, } -impl<'a> Message<'a> { - pub fn id(mut self, id: &'a str) -> Self { - self.id = Some(Id { id, url: None }); - self +impl<'a> Group<'a> { + /// Create group with a [`Title`], deriving [`AnnotationKind::Primary`] from its [`Level`] + pub fn with_title(title: Title<'a>) -> Self { + let level = title.level.clone(); + let mut x = Self::with_level(level); + x.title = Some(title); + x } - pub fn id_with_url(mut self, id: &'a str, url: Option<&'a str>) -> Self { - self.id = Some(Id { id, url }); + /// Create a title-less group with a primary [`Level`] for [`AnnotationKind::Primary`] + /// + /// # Example + /// + /// ```rust + /// # #[allow(clippy::needless_doctest_main)] + #[doc = include_str!("../examples/elide_header.rs")] + /// ``` + #[doc = include_str!("../examples/elide_header.svg")] + pub fn with_level(level: Level<'a>) -> Self { + Self { + primary_level: level, + title: None, + elements: vec![], + lineno_offset: 0, + } + } + + /// Append an [`Element`] that adds context to the [`Title`] + pub fn element(mut self, section: impl Into>) -> Self { + self.elements.push(section.into()); self } - pub fn snippet(mut self, slice: Snippet<'a>) -> Self { - self.snippets.push(slice); + /// Append [`Element`]s that adds context to the [`Title`] + pub fn elements(mut self, sections: impl IntoIterator>>) -> Self { + self.elements.extend(sections.into_iter().map(Into::into)); self } - pub fn snippets(mut self, slice: impl IntoIterator>) -> Self { - self.snippets.extend(slice); + pub fn is_empty(&self) -> bool { + self.elements.is_empty() && self.title.is_none() + } + + /// Add an offset used for aligning the header sigil (`-->`) with the line number separators. + /// + /// For normal diagnostics this is computed automatically based on the lines to be rendered. + /// This is intended only for use in the formatter, where we don't render a snippet directly but + /// still want the header to align with the diff. + pub fn lineno_offset(mut self, offset: usize) -> Self { + self.lineno_offset = offset; self } +} + +/// A section of content within a [`Group`] +#[derive(Clone, Debug)] +#[non_exhaustive] +pub enum Element<'a> { + Message(Message<'a>), + Cause(Snippet<'a, Annotation<'a>>), + Suggestion(Snippet<'a, Patch<'a>>), + Origin(Origin<'a>), + Padding(Padding), +} + +impl<'a> From> for Element<'a> { + fn from(value: Message<'a>) -> Self { + Element::Message(value) + } +} - pub fn footer(mut self, footer: Message<'a>) -> Self { - self.footer.push(footer); +impl<'a> From>> for Element<'a> { + fn from(value: Snippet<'a, Annotation<'a>>) -> Self { + Element::Cause(value) + } +} + +impl<'a> From>> for Element<'a> { + fn from(value: Snippet<'a, Patch<'a>>) -> Self { + Element::Suggestion(value) + } +} + +impl<'a> From> for Element<'a> { + fn from(value: Origin<'a>) -> Self { + Element::Origin(value) + } +} + +impl From for Element<'_> { + fn from(value: Padding) -> Self { + Self::Padding(value) + } +} + +/// A whitespace [`Element`] in a [`Group`] +#[derive(Clone, Debug)] +pub struct Padding; + +/// A title that introduces a [`Group`], describing the main point +/// +/// To create a `Title`, see [`Level::primary_title`] or [`Level::secondary_title`]. +/// +/// # Example +/// +/// ```rust +/// # use annotate_snippets::*; +/// let report = &[ +/// Group::with_title( +/// Level::ERROR.primary_title("mismatched types").id("E0308") +/// ), +/// Group::with_title( +/// Level::HELP.secondary_title("function defined here") +/// ), +/// ]; +/// ``` +#[derive(Clone, Debug)] +pub struct Title<'a> { + pub(crate) level: Level<'a>, + pub(crate) id: Option>, + pub(crate) text: Cow<'a, str>, + pub(crate) allows_styling: bool, + pub(crate) is_fixable: bool, +} + +impl<'a> Title<'a> { + /// The category for this [`Report`] + /// + /// Useful for looking searching for more information to resolve the diagnostic. + /// + ///
+ /// + /// Text passed to this function is considered "untrusted input", as such + /// all text is passed through a normalization function. Styled text is + /// not allowed to be passed to this function. + /// + ///
+ pub fn id(mut self, id: impl Into>) -> Self { + self.id.get_or_insert(Id::default()).id = Some(id.into()); self } - pub fn footers(mut self, footer: impl IntoIterator>) -> Self { - self.footer.extend(footer); + /// Provide a URL for [`Title::id`] for more information on this diagnostic + /// + ///
+ /// + /// This is only relevant if `id` is present + /// + ///
+ pub fn id_url(mut self, url: impl Into>) -> Self { + self.id.get_or_insert(Id::default()).url = Some(url.into()); self } + /// Append an [`Element`] that adds context to the [`Title`] + pub fn element(self, section: impl Into>) -> Group<'a> { + Group::with_title(self).element(section) + } + + /// Append [`Element`]s that adds context to the [`Title`] + pub fn elements(self, sections: impl IntoIterator>>) -> Group<'a> { + Group::with_title(self).elements(sections) + } + /// Whether or not the diagnostic for this message is fixable. /// /// This is rendered as a `[*]` indicator after the `id` in an annotation header, if the @@ -71,98 +225,166 @@ impl<'a> Message<'a> { self.is_fixable = yes; self } - - /// Add an offset used for aligning the header sigil (`-->`) with the line number separators. - /// - /// For normal diagnostics this is computed automatically based on the lines to be rendered. - /// This is intended only for use in the formatter, where we don't render a snippet directly but - /// still want the header to align with the diff. - pub fn lineno_offset(mut self, offset: usize) -> Self { - self.lineno_offset = offset; - self - } } -/// Structure containing the slice of text to be annotated and -/// basic information about the location of the slice. +/// A text [`Element`] in a [`Group`] /// -/// One `Snippet` is meant to represent a single, continuous, -/// slice of source code that you want to annotate. -#[derive(Debug)] -pub struct Snippet<'a> { - pub(crate) origin: Option<&'a str>, - pub(crate) line_start: usize, - - pub(crate) source: &'a str, - pub(crate) annotations: Vec>, - - pub(crate) fold: bool, +/// See [`Level::message`] to create this. +#[derive(Clone, Debug)] +pub struct Message<'a> { + pub(crate) level: Level<'a>, + pub(crate) text: Cow<'a, str>, +} +/// A source view [`Element`] in a [`Group`] +/// +/// If you do not have [source][Snippet::source] available, see instead [`Origin`] +/// +/// `Snippet`s come in the following styles (`T`): +/// - With [`Annotation`]s, see [`Snippet::annotation`] +/// - With [`Patch`]s, see [`Snippet::patch`] +#[derive(Clone, Debug)] +pub struct Snippet<'a, T> { + pub(crate) path: Option>, /// The optional cell index in a Jupyter notebook, used for reporting source locations along /// with the ranges on `annotations`. pub(crate) cell_index: Option, + pub(crate) line_start: usize, + pub(crate) source: Cow<'a, str>, + pub(crate) markers: Vec, + pub(crate) fold: bool, } -impl<'a> Snippet<'a> { - pub fn source(source: &'a str) -> Self { +impl<'a, T: Clone> Snippet<'a, T> { + /// The source code to be rendered + /// + ///
+ /// + /// Text passed to this function is considered "untrusted input", as such + /// all text is passed through a normalization function. Pre-styled text is + /// not allowed to be passed to this function. + /// + ///
+ pub fn source(source: impl Into>) -> Self { Self { - origin: None, + path: None, line_start: 1, - source, - annotations: vec![], - fold: false, cell_index: None, + source: source.into(), + markers: vec![], + fold: true, } } + /// When manually [`fold`][Self::fold]ing, + /// the [`source`][Self::source]s line offset from the original start pub fn line_start(mut self, line_start: usize) -> Self { self.line_start = line_start; self } - pub fn origin(mut self, origin: &'a str) -> Self { - self.origin = Some(origin); + /// The location of the [`source`][Self::source] (e.g. a path) + /// + ///
+ /// + /// Text passed to this function is considered "untrusted input", as such + /// all text is passed through a normalization function. Pre-styled text is + /// not allowed to be passed to this function. + /// + ///
+ pub fn path(mut self, path: impl Into>) -> Self { + self.path = path.into().0; + self + } + + /// Attach a Jupyter notebook cell index. + pub fn cell_index(mut self, index: Option) -> Self { + self.cell_index = index; + self + } + + /// Control whether lines without [`Annotation`]s are shown + /// + /// The default is `fold(true)`, collapsing uninteresting lines. + /// + /// See [`AnnotationKind::Visible`] to force specific spans to be shown. + pub fn fold(mut self, fold: bool) -> Self { + self.fold = fold; self } +} - pub fn annotation(mut self, annotation: Annotation<'a>) -> Self { - self.annotations.push(annotation); +impl<'a> Snippet<'a, Annotation<'a>> { + /// Highlight and describe a span of text within the [`source`][Self::source] + pub fn annotation(mut self, annotation: Annotation<'a>) -> Snippet<'a, Annotation<'a>> { + self.markers.push(annotation); self } + /// Highlight and describe spans of text within the [`source`][Self::source] pub fn annotations(mut self, annotation: impl IntoIterator>) -> Self { - self.annotations.extend(annotation); + self.markers.extend(annotation); self } +} - /// Hide lines without [`Annotation`]s - pub fn fold(mut self, fold: bool) -> Self { - self.fold = fold; +impl<'a> Snippet<'a, Patch<'a>> { + /// Suggest to the user an edit to the [`source`][Self::source] + pub fn patch(mut self, patch: Patch<'a>) -> Snippet<'a, Patch<'a>> { + self.markers.push(patch); self } - /// Attach a Jupyter notebook cell index. - pub fn cell_index(mut self, index: Option) -> Self { - self.cell_index = index; + /// Suggest to the user edits to the [`source`][Self::source] + pub fn patches(mut self, patches: impl IntoIterator>) -> Self { + self.markers.extend(patches); self } } -/// An annotation for a [`Snippet`]. +/// Highlight and describe a span of text within a [`Snippet`] /// -/// See [`Level::span`] to create a [`Annotation`] -#[derive(Debug)] +/// See [`AnnotationKind`] to create an annotation. +/// +/// # Example +/// +/// ```rust +/// # #[allow(clippy::needless_doctest_main)] +#[doc = include_str!("../examples/expected_type.rs")] +/// ``` +/// +#[doc = include_str!("../examples/expected_type.svg")] +#[derive(Clone, Debug)] pub struct Annotation<'a> { - /// The byte range of the annotation in the `source` string - pub(crate) range: Range, - pub(crate) label: Option<&'a str>, - pub(crate) level: Level, + pub(crate) span: Range, + pub(crate) label: Option>, + pub(crate) kind: AnnotationKind, + pub(crate) highlight_source: bool, pub(crate) is_file_level: bool, } impl<'a> Annotation<'a> { - pub fn label(mut self, label: &'a str) -> Self { - self.label = Some(label); + /// Describe the reason the span is highlighted + /// + /// This will be styled according to the [`AnnotationKind`] + /// + ///
+ /// + /// Text passed to this function is considered "untrusted input", as such + /// all text is passed through a normalization function. Pre-styled text is + /// not allowed to be passed to this function. + /// + ///
+ pub fn label(mut self, label: impl Into>) -> Self { + self.label = label.into().0; + self + } + + /// Style the source according to the [`AnnotationKind`] + /// + /// This gives extra emphasis to this annotation + pub fn highlight_source(mut self, highlight_source: bool) -> Self { + self.highlight_source = highlight_source; self } @@ -172,40 +394,225 @@ impl<'a> Annotation<'a> { } } -/// Types of annotations. -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Level { - /// Do not attach any annotation. - None, - /// Error annotations are displayed using red color and "^" character. - Error, - /// Warning annotations are displayed using blue color and "-" character. - Warning, - Info, - Note, - Help, +/// The type of [`Annotation`] being applied to a [`Snippet`] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[non_exhaustive] +pub enum AnnotationKind { + /// For showing the source that the [Group's Title][Group::with_title] references + /// + /// For [`Title`]-less groups, see [`Group::with_level`] + Primary, + /// Additional context to better understand the [`Primary`][Self::Primary] + /// [`Annotation`] + /// + /// See also [`Renderer::context`]. + /// + /// [`Renderer::context`]: crate::renderer::Renderer + Context, + /// Prevents the annotated text from getting [folded][Snippet::fold] + /// + /// By default, [`Snippet`]s will [fold][`Snippet::fold`] (remove) lines + /// that do not contain any annotations. [`Visible`][Self::Visible] makes + /// it possible to selectively prevent this behavior for specific text, + /// allowing context to be preserved without adding any annotation + /// characters. + /// + /// # Example + /// + /// ```rust + /// # #[allow(clippy::needless_doctest_main)] + #[doc = include_str!("../examples/struct_name_as_context.rs")] + /// ``` + /// + #[doc = include_str!("../examples/struct_name_as_context.svg")] + /// + Visible, } -impl Level { - pub fn title(self, title: &str) -> Message<'_> { - Message { - level: self, - id: None, - title, - snippets: vec![], - footer: vec![], - is_fixable: false, - lineno_offset: 0, - } - } - - /// Create a [`Annotation`] with the given span for a [`Snippet`] +impl AnnotationKind { + /// Annotate a byte span within [`Snippet`] pub fn span<'a>(self, span: Range) -> Annotation<'a> { Annotation { - range: span, + span, label: None, - level: self, + kind: self, + highlight_source: false, is_file_level: false, } } + + pub(crate) fn is_primary(&self) -> bool { + matches!(self, AnnotationKind::Primary) + } +} + +/// Suggested edit to the [`Snippet`] +/// +/// See [`Snippet::patch`] +/// +/// # Example +/// +/// ```rust +/// # #[allow(clippy::needless_doctest_main)] +#[doc = include_str!("../examples/multi_suggestion.rs")] +/// ``` +/// +#[doc = include_str!("../examples/multi_suggestion.svg")] +#[derive(Clone, Debug)] +pub struct Patch<'a> { + pub(crate) span: Range, + pub(crate) replacement: Cow<'a, str>, +} + +impl<'a> Patch<'a> { + /// Splice `replacement` into the [`Snippet`] at the specified byte span + /// + ///
+ /// + /// Text passed to this function is considered "untrusted input", as such + /// all text is passed through a normalization function. Pre-styled text is + /// not allowed to be passed to this function. + /// + ///
+ pub fn new(span: Range, replacement: impl Into>) -> Self { + Self { + span, + replacement: replacement.into(), + } + } + + /// Try to turn a replacement into an addition when the span that is being + /// overwritten matches either the prefix or suffix of the replacement. + pub(crate) fn trim_trivial_replacements(self, source: &str) -> TrimmedPatch<'a> { + let mut trimmed = TrimmedPatch { + original_span: self.span.clone(), + span: self.span, + replacement: self.replacement, + }; + + if trimmed.replacement.is_empty() { + return trimmed; + } + let Some(snippet) = source.get(trimmed.original_span.clone()) else { + return trimmed; + }; + + if let Some((prefix, substr, suffix)) = as_substr(snippet, &trimmed.replacement) { + trimmed.span = trimmed.original_span.start + prefix + ..trimmed.original_span.end.saturating_sub(suffix); + trimmed.replacement = Cow::Owned(substr.to_owned()); + } + trimmed + } +} + +/// A source location [`Element`] in a [`Group`] +/// +/// If you have source available, see instead [`Snippet`] +/// +/// # Example +/// +/// ```rust +/// # use annotate_snippets::{Group, Snippet, AnnotationKind, Level, Origin}; +/// let report = &[ +/// Level::ERROR.primary_title("mismatched types").id("E0308") +/// .element( +/// Origin::path("$DIR/mismatched-types.rs") +/// ) +/// ]; +/// ``` +#[derive(Clone, Debug)] +pub struct Origin<'a> { + pub(crate) path: Option>, + /// The optional cell index in a Jupyter notebook, used for reporting source locations along + /// with the ranges on `annotations`. + pub(crate) cell_index: Option, + pub(crate) line: Option, + pub(crate) char_column: Option, +} + +impl<'a> Origin<'a> { + ///
+ /// + /// Text passed to this function is considered "untrusted input", as such + /// all text is passed through a normalization function. Pre-styled text is + /// not allowed to be passed to this function. + /// + ///
+ pub fn path(path: impl Into>) -> Self { + Self { + path: Some(path.into()), + cell_index: None, + line: None, + char_column: None, + } + } + + /// Attach a Jupyter notebook cell index. + pub fn cell_index(mut self, index: Option) -> Self { + self.cell_index = index; + self + } + + /// Set the default line number to display + pub fn line(mut self, line: usize) -> Self { + self.line = Some(line); + self + } + + /// Set the default column to display + /// + ///
+ /// + /// `char_column` is only be respected if [`Origin::line`] is also set. + /// + ///
+ pub fn char_column(mut self, char_column: usize) -> Self { + self.char_column = Some(char_column); + self + } +} + +impl<'a> From> for Origin<'a> { + fn from(origin: Cow<'a, str>) -> Self { + Self::path(origin) + } +} + +#[derive(Debug)] +pub struct OptionCow<'a>(pub(crate) Option>); + +impl<'a, T: Into>> From> for OptionCow<'a> { + fn from(value: Option) -> Self { + Self(value.map(Into::into)) + } +} + +impl<'a> From<&'a Cow<'a, str>> for OptionCow<'a> { + fn from(value: &'a Cow<'a, str>) -> Self { + Self(Some(Cow::Borrowed(value))) + } +} + +impl<'a> From> for OptionCow<'a> { + fn from(value: Cow<'a, str>) -> Self { + Self(Some(value)) + } +} + +impl<'a> From<&'a str> for OptionCow<'a> { + fn from(value: &'a str) -> Self { + Self(Some(Cow::Borrowed(value))) + } +} +impl<'a> From for OptionCow<'a> { + fn from(value: String) -> Self { + Self(Some(Cow::Owned(value))) + } +} + +impl<'a> From<&'a String> for OptionCow<'a> { + fn from(value: &'a String) -> Self { + Self(Some(Cow::Borrowed(value.as_str()))) + } } diff --git a/crates/ruff_annotate_snippets/tests/fixtures/color/ann_removed_nl.svg b/crates/ruff_annotate_snippets/tests/color/ann_eof.ascii.term.svg similarity index 75% rename from crates/ruff_annotate_snippets/tests/fixtures/color/ann_removed_nl.svg rename to crates/ruff_annotate_snippets/tests/color/ann_eof.ascii.term.svg index 045b0ef413..dfc0c58739 100644 --- a/crates/ruff_annotate_snippets/tests/fixtures/color/ann_removed_nl.svg +++ b/crates/ruff_annotate_snippets/tests/color/ann_eof.ascii.term.svg @@ -1,4 +1,4 @@ - + ( - system: S, - src_roots: Vec, - python_version: PythonVersion, - venv_path: Option, - ) -> Result + /// Initialize a [`ModuleDb`] for the given system. + pub fn new(system: S) -> Self where S: System + 'static + Send + Sync + RefUnwindSafe, { - let mut search_path_settings = 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, &system)?; - search_path_settings.site_packages_paths = environment - .site_packages_paths(&system) - .context("Failed to discover the site-packages directory")? - .into_vec(); - } - let search_paths = search_path_settings - .to_search_paths(&system, &EMPTY_VENDORED, &FallibleStrategy) - .context("Invalid search path settings")?; - - let db = Self { + Self { storage: salsa::Storage::new(None), files: Files::default(), system: Arc::new(system), - search_paths: Arc::new(search_paths), - python_version, - }; - - // Register the static roots for salsa durability - db.search_paths.try_register_static_roots(&db); + } + } +} - Ok(db) +/// Resolve module search paths for the given source roots and Python environment. +pub fn resolve_search_paths( + system: &dyn System, + src_roots: Vec, + venv_path: Option, +) -> Result { + let mut search_path_settings = 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, system)?; + search_path_settings.site_packages_paths = environment + .site_packages_paths(system) + .context("Failed to discover the site-packages directory")? + .into_vec(); } + + search_path_settings + .to_search_paths(system, &EMPTY_VENDORED, &FallibleStrategy) + .context("Invalid search path settings") } #[salsa::db] @@ -80,18 +73,10 @@ impl SourceDb for ModuleDb { fn files(&self) -> &Files { &self.files } - - fn python_version(&self) -> PythonVersion { - self.python_version - } } #[salsa::db] -impl ty_module_resolver::Db for ModuleDb { - fn search_paths(&self) -> &SearchPaths { - &self.search_paths - } -} +impl ty_module_resolver::Db for ModuleDb {} #[salsa::db] impl salsa::Database for ModuleDb {} diff --git a/crates/ruff_graph/src/lib.rs b/crates/ruff_graph/src/lib.rs index 0ada26454f..4e2886ec69 100644 --- a/crates/ruff_graph/src/lib.rs +++ b/crates/ruff_graph/src/lib.rs @@ -3,12 +3,13 @@ use std::collections::{BTreeMap, BTreeSet}; use anyhow::Result; use ruff_db::system::{SystemPath, SystemPathBuf}; -use ruff_python_ast::PySourceType; +use ruff_linter::source_kind::SourceKind; use ruff_python_ast::helpers::to_module_path; use ruff_python_parser::{ParseOptions, parse}; +pub use ty_module_resolver::ResolverEnvironment; use crate::collector::Collector; -pub use crate::db::ModuleDb; +pub use crate::db::{ModuleDb, resolve_search_paths}; use crate::resolver::Resolver; pub use crate::settings::{AnalyzeSettings, Direction, StringImports}; @@ -23,17 +24,19 @@ pub struct ModuleImports(BTreeSet); impl ModuleImports { /// Detect the [`ModuleImports`] for a given Python file. - pub fn detect( - db: &ModuleDb, - source: &str, - source_type: PySourceType, + pub fn detect<'db>( + db: &'db ModuleDb, + environment: ResolverEnvironment<'db>, + source: &SourceKind, path: &SystemPath, package: Option<&SystemPath>, string_imports: StringImports, type_checking_imports: bool, ) -> Result { // Parse the source code. - let parsed = parse(source, ParseOptions::from(source_type))?; + let parse_options = ParseOptions::from(source.py_source_type()) + .with_target_version(environment.python_version(db)); + let parsed = parse(source.source_code(), parse_options)?; let module_path = package.and_then(|package| to_module_path(package.as_std_path(), path.as_std_path())); @@ -48,8 +51,9 @@ impl ModuleImports { // Resolve the imports. let mut resolved_imports = ModuleImports::default(); + let resolver = Resolver::new(db, path, environment); for import in imports { - for resolved in Resolver::new(db, path).resolve(import) { + for resolved in resolver.resolve(import) { if let Some(path) = resolved.as_system_path() { resolved_imports.insert(path.to_path_buf()); } @@ -60,7 +64,7 @@ impl ModuleImports { } /// Insert a file path into the module imports. - pub fn insert(&mut self, path: SystemPathBuf) { + fn insert(&mut self, path: SystemPathBuf) { self.0.insert(path); } diff --git a/crates/ruff_graph/src/resolver.rs b/crates/ruff_graph/src/resolver.rs index 01d6ef848a..6d65455573 100644 --- a/crates/ruff_graph/src/resolver.rs +++ b/crates/ruff_graph/src/resolver.rs @@ -1,8 +1,8 @@ use ruff_db::files::{File, FilePath, system_path_to_file}; use ruff_db::system::SystemPath; use ty_module_resolver::{ - ModuleName, resolve_module, resolve_module_confident, resolve_real_module, - resolve_real_module_confident, + ImportingFile, ModuleName, ResolverEnvironment, resolve_module, resolve_module_confident, + resolve_real_module, resolve_real_module_confident, }; use crate::ModuleDb; @@ -12,14 +12,23 @@ use crate::collector::CollectedImport; pub(crate) struct Resolver<'a> { db: &'a ModuleDb, file: Option, + environment: ResolverEnvironment<'a>, } impl<'a> Resolver<'a> { /// Initialize a [`Resolver`] with a given [`ModuleDb`]. - pub(crate) fn new(db: &'a ModuleDb, path: &SystemPath) -> Self { + pub(crate) fn new( + db: &'a ModuleDb, + path: &SystemPath, + environment: ResolverEnvironment<'a>, + ) -> Self { // If we know the importing file we can potentially resolve more imports let file = system_path_to_file(db, path).ok(); - Self { db, file } + Self { + db, + file, + environment, + } } /// Resolve the [`CollectedImport`] into a [`FilePath`]. @@ -99,11 +108,15 @@ impl<'a> Resolver<'a> { } /// Resolves a module name to a module. - pub(crate) fn resolve_module(&self, module_name: &ModuleName) -> Option<&'a FilePath> { + fn resolve_module(&self, module_name: &ModuleName) -> Option<&'a FilePath> { let module = if let Some(file) = self.file { - resolve_module(self.db, file, module_name)? + resolve_module( + self.db, + ImportingFile::File(file, self.environment), + module_name, + )? } else { - resolve_module_confident(self.db, module_name)? + resolve_module_confident(self.db, self.environment, module_name)? }; Some(module.file(self.db)?.path(self.db)) } @@ -111,9 +124,13 @@ impl<'a> Resolver<'a> { /// Resolves a module name to a module (stubs not allowed). fn resolve_real_module(&self, module_name: &ModuleName) -> Option<&'a FilePath> { let module = if let Some(file) = self.file { - resolve_real_module(self.db, file, module_name)? + resolve_real_module( + self.db, + ImportingFile::File(file, self.environment), + module_name, + )? } else { - resolve_real_module_confident(self.db, module_name)? + resolve_real_module_confident(self.db, self.environment, module_name)? }; Some(module.file(self.db)?.path(self.db)) } diff --git a/crates/ruff_index/Cargo.toml b/crates/ruff_index/Cargo.toml index b9817e75b5..1f62f61968 100644 --- a/crates/ruff_index/Cargo.toml +++ b/crates/ruff_index/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_index" -version = "0.0.5" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_index/README.md b/crates/ruff_index/README.md index d99bf84ffb..faa6312359 100644 --- a/crates/ruff_index/README.md +++ b/crates/ruff_index/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_index). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_index). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_index/src/frozen.rs b/crates/ruff_index/src/frozen.rs index 1a88b472a6..7fb455820d 100644 --- a/crates/ruff_index/src/frozen.rs +++ b/crates/ruff_index/src/frozen.rs @@ -12,7 +12,7 @@ pub struct FrozenIndexVec { impl FrozenIndexVec { #[inline] - pub fn from_raw(raw: Box<[T]>) -> Self { + fn from_raw(raw: Box<[T]>) -> Self { Self { raw, index: PhantomData, @@ -20,12 +20,12 @@ impl FrozenIndexVec { } #[inline] - pub fn as_slice(&self) -> &IndexSlice { + fn as_slice(&self) -> &IndexSlice { IndexSlice::from_raw(&self.raw) } #[inline] - pub fn as_mut_slice(&mut self) -> &mut IndexSlice { + fn as_mut_slice(&mut self) -> &mut IndexSlice { IndexSlice::from_raw_mut(&mut self.raw) } } diff --git a/crates/ruff_linter/Cargo.toml b/crates/ruff_linter/Cargo.toml index c2decabbbc..94aebb3b7b 100644 --- a/crates/ruff_linter/Cargo.toml +++ b/crates/ruff_linter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_linter" -version = "0.15.22" +version = "0.16.2" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_linter/README.md b/crates/ruff_linter/README.md index ba776d0d57..124078abe7 100644 --- a/crates/ruff_linter/README.md +++ b/crates/ruff_linter/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.15.22) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_linter). +This version (0.16.2) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_linter). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_linter/resources/mdtest/flake8-bandit/unsafe-markup-use.md b/crates/ruff_linter/resources/mdtest/flake8-bandit/unsafe-markup-use.md index ea1400b6b9..f395224b7e 100644 --- a/crates/ruff_linter/resources/mdtest/flake8-bandit/unsafe-markup-use.md +++ b/crates/ruff_linter/resources/mdtest/flake8-bandit/unsafe-markup-use.md @@ -22,7 +22,6 @@ error[S704]: Unsafe use of `markupsafe.Markup` detected | 5 | Markup(f"unsafe {content}") # snapshot: unsafe-markup-use | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | ``` ```py diff --git a/crates/ruff_linter/resources/mdtest/flake8-pyi/redundant-numeric-union.md b/crates/ruff_linter/resources/mdtest/flake8-pyi/redundant-numeric-union.md new file mode 100644 index 0000000000..e52d8921de --- /dev/null +++ b/crates/ruff_linter/resources/mdtest/flake8-pyi/redundant-numeric-union.md @@ -0,0 +1,117 @@ +# `redundant-numeric-union` (`PYI041`) + +```toml +target-version = "py311" + +[lint] +select = ["PYI041"] +``` + +## Ordinary parameter annotations + +Numeric unions are redundant when they are used only for static typing. + +```py +def function(value: int | float) -> None: ... # error: [redundant-numeric-union] +``` + +## Single-dispatch registrations + +The first annotated parameter determines the concrete types registered at runtime, so its numeric +union is not redundant. + +```py +import functools + +@functools.singledispatch +def dispatch(value: object) -> None: ... + +@dispatch.register +def _(value: int | float) -> None: ... +``` + +## Generic single-dispatch functions + +The generic function's annotation does not register concrete types, so its numeric union remains +redundant even when a registered implementation needs the same union. + +```py +import functools + +@functools.singledispatch +def dispatch(value: int | float) -> None: ... # error: [redundant-numeric-union] + +@dispatch.register +def _(value: int | float) -> None: ... +``` + +## Other parameters of registered functions + +Numeric unions remain redundant for parameters that do not determine dispatch registration. + +```py +import functools + +@functools.singledispatch +def dispatch(value: object) -> None: ... + +@dispatch.register +def _(value: float | complex, other: int | float) -> None: ... # snapshot: redundant-numeric-union +``` + +```snapshot +error[PYI041]: Use `float` instead of `int | float` + --> src/mdtest_snippet.py:7:38 + | +7 | def _(value: float | complex, other: int | float) -> None: ... # snapshot: redundant-numeric-union + | ^^^^^^^^^^^ +help: Remove redundant type + | +6 | @dispatch.register + - def _(value: float | complex, other: int | float) -> None: ... # snapshot: redundant-numeric-union +7 + def _(value: float | complex, other: float) -> None: ... # snapshot: redundant-numeric-union + | +``` + +## Single-dispatch method registrations + +The dispatch parameter comes after the unannotated instance parameter. + +```py +import functools + +class Dispatch: + @functools.singledispatchmethod + def dispatch(self, value: object) -> None: ... + + @dispatch.register + def _(self, value: int | float) -> None: ... +``` + +## Explicit single-dispatch registrations + +An explicit registration does not inspect the implementation's parameter annotation. + +```py +import functools + +@functools.singledispatch +def dispatch(value: object) -> None: ... + +@dispatch.register(int | float) +def _(value: int | float) -> None: ... # error: [redundant-numeric-union] +``` + +## Stub annotations + +Registered dispatch implementations retain their numeric unions in stub files as well. + +```pyi +import functools + +@functools.singledispatch +def dispatch(value: object) -> None: ... + +@dispatch.register +def _(value: int | float) -> None: ... +``` diff --git a/crates/ruff_linter/resources/mdtest/flake8-pytest-style/pytest-parametrize-names-wrong-type.md b/crates/ruff_linter/resources/mdtest/flake8-pytest-style/pytest-parametrize-names-wrong-type.md index 49b7c6f1be..01aa0f0b35 100644 --- a/crates/ruff_linter/resources/mdtest/flake8-pytest-style/pytest-parametrize-names-wrong-type.md +++ b/crates/ruff_linter/resources/mdtest/flake8-pytest-style/pytest-parametrize-names-wrong-type.md @@ -27,7 +27,6 @@ error[PT006]: Wrong type passed to first argument of `pytest.mark.parametrize`; | 5 | @pytest.mark.parametrize(("param",), [(1,), variable]) # snapshot: pytest-parametrize-names-wrong-type | ^^^^^^^^^^ - | help: Use a string for the first argument ``` diff --git a/crates/ruff_linter/resources/mdtest/flake8-return/unnecessary-assign.md b/crates/ruff_linter/resources/mdtest/flake8-return/unnecessary-assign.md new file mode 100644 index 0000000000..27b9d05511 --- /dev/null +++ b/crates/ruff_linter/resources/mdtest/flake8-return/unnecessary-assign.md @@ -0,0 +1,282 @@ +# `unnecessary-assign` (`RET504`) + +```toml +lint.select = ["RET504"] +``` + +RET504 is suppressed only when the assigned name is read in an enclosing `finally` suite, which +runs after the `return`. Reads elsewhere (sibling branches, `except` handlers) don't run after the +`return`, so they don't keep the assignment alive. + +## Variable read in the enclosing `finally` + +```py +def f(): + out = "" + try: + out = foo() + return out + except Exception as e: + out = str(e) + finally: + log(out) +``` + +A closure captured in `finally` reads the name from another scope: + +```py +def f(): + try: + x = foo() + return x + finally: + def _cleanup(): + log(x) + _cleanup() +``` + +Outer `finally` reads the name across a nested `try`: + +```py +def f(): + x = "" + try: + try: + x = foo() + return x + except: + pass + finally: + log(x) +``` + +The outer `finally` runs after a `return` in an inner `finally`: + +```py +def f(): + x = "" + try: + try: + pass + finally: + x = foo() + return x + finally: + log(x) +``` + +The `finally` also runs after a `return` in the `else` clause: + +```py +def f(): + try: + pass + except Exception: + pass + else: + x = compute() + return x + finally: + log(x) +``` + +And after a `return` in an `except` handler: + +```py +def f(): + try: + pass + except Exception: + x = recover() + return x + finally: + log(x) +``` + +The assignment may also come from a `with` body inside the `try`: + +```py +def f(): + try: + with open("f") as fh: + x = fh.read() + return x + finally: + log(x) +``` + +## Augmented assignment in `finally` reads the name + +```py +def f(): + try: + x = foo() + return x + finally: + x += 1 +``` + +```py +def f(): + try: + x = foo() + return x + finally: + if cond(): + x += 1 +``` + +## `del` of the name in `finally` + +Removing the assignment would leave the name unbound, so `del x` would raise `UnboundLocalError`: + +```py +def f(): + try: + x = foo() + return x + finally: + del x +``` + +```py +def f(): + try: + x = foo() + return x + finally: + if cond(): + del x +``` + +## A read after a `finally` rebind still suppresses + +A rebind in `finally` makes the assignment redundant, but distinguishing a rebind that kills the +value from a plain read needs control-flow analysis we don't do here. We conservatively treat the +later read as observing the assignment. + +```py +def f(): + try: + x = foo() + return x + finally: + x = "done" + log(x) +``` + +```py +def f(): + try: + x = foo() + return x + finally: + x: str = "done" + log(x) +``` + +```py +def f(): + try: + x = foo() + return x + finally: + x, _ = ("done", 0) + log(x) +``` + +```py +def f(): + try: + x = foo() + return x + finally: + if cond(): + x = "done" + log(x) +``` + +## A read in an `except` handler fires + +An `except` handler is an alternative path: if it runs, the `try` assignment never completed, so +removing the assignment doesn't change what the handler reads. + +```py +def f(): + result = None + try: + result = compute() + return result # error: [unnecessary-assign] + except Exception as e: + log(result) +``` + +## `finally` doesn't read the name + +```py +def f(): + try: + x = foo() + return x # error: [unnecessary-assign] + finally: + log("done") +``` + +```py +def f(): + try: + x = foo() + return x # error: [unnecessary-assign] + finally: + x = "done" +``` + +## Assignment and return both inside `finally` + +```py +def f(): + try: + pass + finally: + x = foo() + return x # error: [unnecessary-assign] +``` + +## `return` in an `except` handler with no later read + +```py +def f(): + try: + entry = fetch() + except AlreadyExists: + entry = lookup() + result = to_dict(entry) + return result # error: [unnecessary-assign] +``` + +## Same name assigned and returned in sibling branches + +Each branch's assignment is independently redundant. A later branch reusing the name doesn't +observe an earlier branch's value, so both fire. + +```py +def f(cond): + if cond: + x = compute() + return x # error: [unnecessary-assign] + else: + x = other() + return x # error: [unnecessary-assign] +``` + +The same holds when the branches are `try` arms without a `finally`: + +```py +def f(): + try: + x = compute() + return x # error: [unnecessary-assign] + except Exception: + x = fallback() + return x # error: [unnecessary-assign] +``` diff --git a/crates/ruff_linter/resources/mdtest/notebook/cell-boundaries.md b/crates/ruff_linter/resources/mdtest/notebook/cell-boundaries.md index c504a4b86f..1f7726f2d2 100644 --- a/crates/ruff_linter/resources/mdtest/notebook/cell-boundaries.md +++ b/crates/ruff_linter/resources/mdtest/notebook/cell-boundaries.md @@ -41,7 +41,6 @@ error[invalid-syntax]: Expected class, function definition or async function def | 2 | @deco | ^ - | ``` ## Notebooks without Python cells diff --git a/crates/ruff_linter/resources/mdtest/pyflakes/string-dot-format-extra-positional-arguments.md b/crates/ruff_linter/resources/mdtest/pyflakes/string-dot-format-extra-positional-arguments.md index a0c233d692..6ade98d90f 100644 --- a/crates/ruff_linter/resources/mdtest/pyflakes/string-dot-format-extra-positional-arguments.md +++ b/crates/ruff_linter/resources/mdtest/pyflakes/string-dot-format-extra-positional-arguments.md @@ -22,7 +22,6 @@ error[F523]: `.format` call has unused arguments at position(s): 0 | 1 | print("{{".format("!")) # snapshot: string-dot-format-extra-positional-arguments | ^^^^^^^^^^^^^^^^ - | help: Remove extra positional arguments at position(s): 0 | - print("{{".format("!")) # snapshot: string-dot-format-extra-positional-arguments @@ -36,7 +35,6 @@ error[F523]: `.format` call has unused arguments at position(s): 0 | 2 | print("{x}".format("!")) # snapshot: string-dot-format-extra-positional-arguments | ^^^^^^^^^^^^^^^^^ - | help: Remove extra positional arguments at position(s): 0 | 1 | print("{{".format("!")) # snapshot: string-dot-format-extra-positional-arguments diff --git a/crates/ruff_linter/resources/mdtest/pylint/invalid-character-backspace.md b/crates/ruff_linter/resources/mdtest/pylint/invalid-character-backspace.md index c0df57a385..fdf7b66cd9 100644 --- a/crates/ruff_linter/resources/mdtest/pylint/invalid-character-backspace.md +++ b/crates/ruff_linter/resources/mdtest/pylint/invalid-character-backspace.md @@ -26,7 +26,6 @@ error[PLE2510]: Invalid unescaped character backspace, use "\b" instead | 1 | replacement_field = f"{'␈'}" # snapshot: invalid-character-backspace | ^ - | help: Replace with escape sequence @@ -35,7 +34,6 @@ error[PLE2510]: Invalid unescaped character backspace, use "\b" instead | 2 | nested_f_string = f"{f'hello␈'}" # snapshot: invalid-character-backspace | ^ - | help: Replace with escape sequence ``` @@ -54,7 +52,6 @@ error[PLE2510]: Invalid unescaped character backspace, use "\b" instead | 1 | format_spec = f"{value:␈}" # snapshot: invalid-character-backspace | ^ - | help: Replace with escape sequence | - format_spec = f"{value:␈}" # snapshot: invalid-character-backspace @@ -68,7 +65,6 @@ error[PLE2510]: Invalid unescaped character backspace, use "\b" instead | 2 | f_string_literal = f"hello␈" # snapshot: invalid-character-backspace | ^ - | help: Replace with escape sequence | 1 | format_spec = f"{value:␈}" # snapshot: invalid-character-backspace @@ -101,7 +97,6 @@ error[PLE2510]: Invalid unescaped character backspace, use "\b" instead | 1 | replacement_field = f"{'␈'}" # snapshot: invalid-character-backspace | ^ - | help: Replace with escape sequence | - replacement_field = f"{'␈'}" # snapshot: invalid-character-backspace @@ -115,7 +110,6 @@ error[PLE2510]: Invalid unescaped character backspace, use "\b" instead | 2 | format_spec = f"{value:␈}" # snapshot: invalid-character-backspace | ^ - | help: Replace with escape sequence | 1 | replacement_field = f"{'␈'}" # snapshot: invalid-character-backspace @@ -130,7 +124,6 @@ error[PLE2510]: Invalid unescaped character backspace, use "\b" instead | 3 | f_string_literal = f"hello␈" # snapshot: invalid-character-backspace | ^ - | help: Replace with escape sequence | 2 | format_spec = f"{value:␈}" # snapshot: invalid-character-backspace @@ -145,7 +138,6 @@ error[PLE2510]: Invalid unescaped character backspace, use "\b" instead | 4 | nested_f_string = f"{f'hello␈'}" # snapshot: invalid-character-backspace | ^ - | help: Replace with escape sequence | 3 | f_string_literal = f"hello␈" # snapshot: invalid-character-backspace diff --git a/crates/ruff_linter/resources/mdtest/pylint/non-augmented-assignment.md b/crates/ruff_linter/resources/mdtest/pylint/non-augmented-assignment.md new file mode 100644 index 0000000000..63ec1db2e5 --- /dev/null +++ b/crates/ruff_linter/resources/mdtest/pylint/non-augmented-assignment.md @@ -0,0 +1,114 @@ +# `non-augmented-assignment` (`PLR6104`) + +```toml +[lint] +preview = true +select = ["PLR6104"] +``` + +## Unary operators on literals + +When the assignment target is the right-hand operand, the rule only rewrites the assignment if the +other operand is a number or a boolean literal, because the operator has to commute for the rewrite +to preserve behavior. + +The parser does not fold constants, so `-1` is a unary `-` applied to `1` rather than a literal. Any +stack of `+`, `-`, `~` or `not` over a number or boolean literal still evaluates to a number or a +boolean, so the operand is peeled before the literal check. + +```py +to_multiply = -1 + to_multiply # snapshot: non-augmented-assignment +to_multiply = +1 * to_multiply # error: [non-augmented-assignment] +to_multiply = --1 + to_multiply # error: [non-augmented-assignment] +to_multiply = -1.5 + to_multiply # error: [non-augmented-assignment] +to_multiply = -1j + to_multiply # error: [non-augmented-assignment] +flags = ~0x1 & flags # error: [non-augmented-assignment] +flags = -True | flags # error: [non-augmented-assignment] +``` + +```snapshot +error[PLR6104]: Use `+=` to perform an augmented assignment directly + --> src/mdtest_snippet.py:1:1 + | +1 | to_multiply = -1 + to_multiply # snapshot: non-augmented-assignment + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +help: Replace with augmented assignment + | + - to_multiply = -1 + to_multiply # snapshot: non-augmented-assignment +1 + to_multiply += -1 # snapshot: non-augmented-assignment +2 | to_multiply = +1 * to_multiply # error: [non-augmented-assignment] + | +note: This is an unsafe fix and may change runtime behavior +``` + +Parentheses around the moved operand are preserved: + +```py +to_multiply = (not True) + to_multiply # snapshot: non-augmented-assignment +``` + +```snapshot +error[PLR6104]: Use `+=` to perform an augmented assignment directly + --> src/mdtest_snippet.py:8:1 + | +8 | to_multiply = (not True) + to_multiply # snapshot: non-augmented-assignment + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +help: Replace with augmented assignment + | +7 | flags = -True | flags # error: [non-augmented-assignment] + - to_multiply = (not True) + to_multiply # snapshot: non-augmented-assignment +8 + to_multiply += (not True) # snapshot: non-augmented-assignment + | +note: This is an unsafe fix and may change runtime behavior +``` + +## Target already on the left + +Commutativity is irrelevant when the target is the left-hand operand, so a unary operand needs no +literal check at all. The right-hand side of an augmented assignment accepts any expression, so the +moved operand never needs new parentheses either. + +```py +to_multiply = to_multiply**-1 # snapshot: non-augmented-assignment +to_multiply = to_multiply - -1 # error: [non-augmented-assignment] +``` + +```snapshot +error[PLR6104]: Use `**=` to perform an augmented assignment directly + --> src/mdtest_snippet.py:1:1 + | +1 | to_multiply = to_multiply**-1 # snapshot: non-augmented-assignment + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +help: Replace with augmented assignment + | + - to_multiply = to_multiply**-1 # snapshot: non-augmented-assignment +1 + to_multiply **= -1 # snapshot: non-augmented-assignment +2 | to_multiply = to_multiply - -1 # error: [non-augmented-assignment] + | +note: This is an unsafe fix and may change runtime behavior +``` + +## Unary operators on non-literals + +The unary operand is not a literal, so its type is unknown and the operator may not commute. + +```py +to_multiply = -a_number + to_multiply +to_multiply = -to_multiply + 1 +``` + +`not` evaluates to a boolean whatever it is applied to, so rewriting the case below would in fact be +safe. The check deliberately stays narrow and only looks for number and boolean literals underneath +the unary operators. + +```py +to_multiply = (not "") + to_multiply +``` + +## Non-commutative operators + +`-` does not commute, regardless of the operand's type. + +```py +to_multiply = -1 - to_multiply +``` diff --git a/crates/ruff_linter/resources/mdtest/pylint/redefined-loop-name.md b/crates/ruff_linter/resources/mdtest/pylint/redefined-loop-name.md index 6b360389ce..b2d80b0f66 100644 --- a/crates/ruff_linter/resources/mdtest/pylint/redefined-loop-name.md +++ b/crates/ruff_linter/resources/mdtest/pylint/redefined-loop-name.md @@ -50,7 +50,6 @@ error[PLW2901]: `for` loop variable `i` overwritten by assignment target | 5 | i = [1] # snapshot: redefined-loop-name | ^ - | error[PLW2901]: `for` loop variable `i` overwritten by assignment target @@ -58,7 +57,6 @@ error[PLW2901]: `for` loop variable `i` overwritten by assignment target | 11 | i = {"b": 2} # snapshot: redefined-loop-name | ^ - | error[PLW2901]: `for` loop variable `i` overwritten by assignment target @@ -66,7 +64,6 @@ error[PLW2901]: `for` loop variable `i` overwritten by assignment target | 26 | i = {1} # snapshot: redefined-loop-name | ^ - | error[PLW2901]: `for` loop variable `i` overwritten by assignment target @@ -74,7 +71,6 @@ error[PLW2901]: `for` loop variable `i` overwritten by assignment target | 29 | i += (1,) # snapshot: redefined-loop-name | ^ - | error[PLW2901]: `for` loop variable `i` overwritten by assignment target @@ -82,5 +78,4 @@ error[PLW2901]: `for` loop variable `i` overwritten by assignment target | 32 | i += "a" # snapshot: redefined-loop-name | ^ - | ``` diff --git a/crates/ruff_linter/resources/mdtest/pylint/too-many-statements-in-try-clause.md b/crates/ruff_linter/resources/mdtest/pylint/too-many-statements-in-try-clause.md index bca4bb944d..1aa80ab9a1 100644 --- a/crates/ruff_linter/resources/mdtest/pylint/too-many-statements-in-try-clause.md +++ b/crates/ruff_linter/resources/mdtest/pylint/too-many-statements-in-try-clause.md @@ -29,7 +29,6 @@ error[PLW0717]: Try clause contains too many statements (6 > 5) | 2 | try: | ^^^ - | ``` ## Context managers diff --git a/crates/ruff_linter/resources/mdtest/pyupgrade/deprecated-abc-decorator.md b/crates/ruff_linter/resources/mdtest/pyupgrade/deprecated-abc-decorator.md index a63affba19..328b7ce563 100644 --- a/crates/ruff_linter/resources/mdtest/pyupgrade/deprecated-abc-decorator.md +++ b/crates/ruff_linter/resources/mdtest/pyupgrade/deprecated-abc-decorator.md @@ -31,7 +31,6 @@ error[UP051]: Use `@classmethod` and `@abstractmethod` instead of `abstractclass | 4 | @abc.abstractclassmethod # snapshot: deprecated-abc-decorator | ^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `@classmethod` and `abstractmethod` | 3 | class Foo(abc.ABC): @@ -47,7 +46,6 @@ error[UP051]: Use `@staticmethod` and `@abstractmethod` instead of `abstractstat | 7 | @abc.abstractstaticmethod # snapshot: deprecated-abc-decorator | ^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `@staticmethod` and `abstractmethod` | 6 | @@ -63,7 +61,6 @@ error[UP051]: Use `@property` and `@abstractmethod` instead of `abstractproperty | 10 | @abc.abstractproperty # snapshot: deprecated-abc-decorator | ^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `@property` and `abstractmethod` | 9 | diff --git a/crates/ruff_linter/resources/mdtest/pyupgrade/f-string.md b/crates/ruff_linter/resources/mdtest/pyupgrade/f-string.md index eb2e8a6b48..da0347d60e 100644 --- a/crates/ruff_linter/resources/mdtest/pyupgrade/f-string.md +++ b/crates/ruff_linter/resources/mdtest/pyupgrade/f-string.md @@ -41,7 +41,6 @@ error[UP032]: Use f-string instead of `format` call | 4 | "{x}".format(x=foo()) # snapshot: f-string | ^^^^^^^^^^^^^^^^^^^^^ - | help: Convert to f-string | 3 | @@ -66,7 +65,6 @@ error[UP032]: Use f-string instead of `format` call | 1 | "" "{}".format(x) # snapshot: f-string | ^^^^^^^^^^^^^^^^^ - | help: Convert to f-string | - "" "{}".format(x) # snapshot: f-string @@ -85,7 +83,6 @@ error[UP032]: Use f-string instead of `format` call | 2 | "a" "" "{}".format(x) # snapshot: f-string | ^^^^^^^^^^^^^^^^^^^^^ - | help: Convert to f-string | 1 | "" "{}".format(x) # snapshot: f-string @@ -108,7 +105,6 @@ error[UP032]: Use f-string instead of `format` call | 3 | x = ("" "{}").format(value) # snapshot: f-string | ^^^^^^^^^^^^^^^^^^^^^^^ - | help: Convert to f-string | 2 | "a" "" "{}".format(x) # snapshot: f-string @@ -136,7 +132,6 @@ error[UP032]: Use f-string instead of `format` call 6 | | # comment 7 | | "{}".format(value) | |______________________^ - | help: Convert to f-string | 6 | # comment @@ -159,7 +154,6 @@ error[UP032]: Use f-string instead of `format` call | 9 | y = ("" "").format(value) # snapshot: f-string | ^^^^^^^^^^^^^^^^^^^^^ - | help: Convert to f-string | 8 | ) diff --git a/crates/ruff_linter/resources/mdtest/pyupgrade/pep-695.md b/crates/ruff_linter/resources/mdtest/pyupgrade/pep-695.md new file mode 100644 index 0000000000..6eece28c0e --- /dev/null +++ b/crates/ruff_linter/resources/mdtest/pyupgrade/pep-695.md @@ -0,0 +1,167 @@ +# PEP 695 rules (`UP040`, `UP046`, `UP047`) + +```toml +target-version = "py313" + +[lint] +preview = true +select = [ + "non-pep695-type-alias", + "non-pep695-generic-class", + "non-pep695-generic-function", +] +``` + +## Defaulted `TypeVar` before a non-defaulted one + +In a PEP 695 type parameter list, a non-defaulted type parameter cannot follow a defaulted one, and +reordering the parameters would change how positional arguments bind when subscripting the alias, +class, or function. There is no valid, equivalent type parameter list in this case, so no diagnostic +is emitted (see [#27021](https://github.com/astral-sh/ruff/issues/27021)). + +### `non-pep695-type-alias` (`UP040`) + +#### No diagnostic for a `TypeAlias` annotation + +```py +from typing import TypeAlias, TypeVar + +T = TypeVar("T", default=int) +S = TypeVar("S") + +Pair: TypeAlias = tuple[T, S] +``` + +#### No diagnostic for a `TypeAliasType` call + +```py +from typing import TypeAliasType, TypeVar + +T = TypeVar("T", default=int) +S = TypeVar("S") + +Pair = TypeAliasType("Pair", tuple[T, S], type_params=(T, S)) +``` + +#### The fix is still offered when the non-defaulted `TypeVar` comes first + +```py +from typing import TypeAlias, TypeVar + +T = TypeVar("T", default=int) +S = TypeVar("S") + +Pair: TypeAlias = tuple[S, T] # snapshot: non-pep695-type-alias +``` + +```snapshot +error[UP040]: Type alias `Pair` uses `TypeAlias` annotation instead of the `type` keyword + --> src/mdtest_snippet.py:6:1 + | +6 | Pair: TypeAlias = tuple[S, T] # snapshot: non-pep695-type-alias + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +help: Use the `type` keyword + | +5 | + - Pair: TypeAlias = tuple[S, T] # snapshot: non-pep695-type-alias +6 + type Pair[S, T = int] = tuple[S, T] # snapshot: non-pep695-type-alias + | +note: This is an unsafe fix and may change runtime behavior +``` + +#### The fix is still offered when all of the type variables have defaults + +```py +from typing import TypeAlias, TypeVar + +T = TypeVar("T", default=int) +S = TypeVar("S", default=str) + +Pair: TypeAlias = tuple[T, S] # error: [non-pep695-type-alias] +``` + +### `non-pep695-generic-class` (`UP046`) + +#### No diagnostic when a defaulted `TypeVar` precedes a non-defaulted one + +```py +from typing import Generic, TypeVar + +T = TypeVar("T", default=int) +S = TypeVar("S") + +class Pair(Generic[T, S]): + t: T + s: S +``` + +#### The fix is still offered when the non-defaulted `TypeVar` comes first + +```py +from typing import Generic, TypeVar + +T = TypeVar("T", default=int) +S = TypeVar("S") + +class Pair(Generic[S, T]): # snapshot: non-pep695-generic-class + t: T + s: S +``` + +```snapshot +error[UP046]: Generic class `Pair` uses `Generic` subclass instead of type parameters + --> src/mdtest_snippet.py:6:12 + | +6 | class Pair(Generic[S, T]): # snapshot: non-pep695-generic-class + | ^^^^^^^^^^^^^ +help: Use type parameters + | +5 | + - class Pair(Generic[S, T]): # snapshot: non-pep695-generic-class +6 + class Pair[S, T = int]: # snapshot: non-pep695-generic-class +7 | t: T + | +note: This is an unsafe fix and may change runtime behavior +``` + +### `non-pep695-generic-function` (`UP047`) + +#### No diagnostic when a defaulted `TypeVar` precedes a non-defaulted one + +```py +from typing import TypeVar + +T = TypeVar("T", default=int) +S = TypeVar("S") + +def pair(t: T, s: S) -> tuple[T, S]: + return (t, s) +``` + +#### The fix is still offered when the non-defaulted `TypeVar` comes first + +```py +from typing import TypeVar + +T = TypeVar("T", default=int) +S = TypeVar("S") + +def pair(s: S, t: T) -> tuple[S, T]: # snapshot: non-pep695-generic-function + return (s, t) +``` + +```snapshot +error[UP047]: Generic function `pair` should use type parameters + --> src/mdtest_snippet.py:6:5 + | +6 | def pair(s: S, t: T) -> tuple[S, T]: # snapshot: non-pep695-generic-function + | ^^^^^^^^^^^^^^^^ +help: Use type parameters + | +5 | + - def pair(s: S, t: T) -> tuple[S, T]: # snapshot: non-pep695-generic-function +6 + def pair[S, T = int](s: S, t: T) -> tuple[S, T]: # snapshot: non-pep695-generic-function +7 | return (s, t) + | +note: This is an unsafe fix and may change runtime behavior +``` diff --git a/crates/ruff_linter/resources/mdtest/ruff/fallible-context-manager.md b/crates/ruff_linter/resources/mdtest/ruff/fallible-context-manager.md index b7a4f2043a..72dd2d3a46 100644 --- a/crates/ruff_linter/resources/mdtest/ruff/fallible-context-manager.md +++ b/crates/ruff_linter/resources/mdtest/ruff/fallible-context-manager.md @@ -27,7 +27,6 @@ error[RUF075]: Context manager does not catch exceptions | 7 | yield # snapshot: fallible-context-manager | ^^^^^ - | ``` ## Yield inside a nested `with`, not last diff --git a/crates/ruff_linter/resources/mdtest/ruff/invalid-pyproject-toml.md b/crates/ruff_linter/resources/mdtest/ruff/invalid-pyproject-toml.md index 033ca832e9..b4d1bc421a 100644 --- a/crates/ruff_linter/resources/mdtest/ruff/invalid-pyproject-toml.md +++ b/crates/ruff_linter/resources/mdtest/ruff/invalid-pyproject-toml.md @@ -20,7 +20,6 @@ error[RUF200]: Failed to parse pyproject.toml: invalid type: integer `1`, expect | 2 | name = 1 # snapshot: invalid-pyproject-toml | ^ - | ``` ## Respects per-file ignores diff --git a/crates/ruff_linter/resources/mdtest/ruff/logging-eager-conversion.md b/crates/ruff_linter/resources/mdtest/ruff/logging-eager-conversion.md new file mode 100644 index 0000000000..898c6e82e2 --- /dev/null +++ b/crates/ruff_linter/resources/mdtest/ruff/logging-eager-conversion.md @@ -0,0 +1,41 @@ +# `logging-eager-conversion` (`RUF065`) + +```toml +lint.preview = true +lint.select = ["RUF065"] +``` + +## Unpacked arguments + +The presence of a starred expression (`*args`) breaks the positional mapping between format string specifiers and variadic logging arguments. Ensure eager conversions *before* the starred argument are still flagged, but bail out on ambiguous cases *after* it. + +```py +import logging + +# 1. Starred before eager conversion (should not trigger for repr("5") because the mapping is broken) +logging.warning("%s%s%s%s %s", *"1234", repr("5")) + +# 2. Eager conversion before starred (should trigger for repr("1") because it maps reliably) +logging.warning("%s %s", repr("1"), *["1234"]) # snapshot: logging-eager-conversion + +# 3. Multiple starred arguments (should not trigger anywhere) +logging.warning("%s %s %s", *["1"], *["2"], repr("3")) + +# 4. Mixed specifiers and eager conversion before starred (should trigger for repr("1")) +logging.warning("%s %s %s", repr("1"), *["2", "3"]) # snapshot: logging-eager-conversion +``` + +```snapshot +error[RUF065]: Unnecessary `repr()` conversion when formatting with `%s`. Use `%r` instead of `%s` + --> src/mdtest_snippet.py:7:26 + | +7 | logging.warning("%s %s", repr("1"), *["1234"]) # snapshot: logging-eager-conversion + | ^^^^^^^^^ + + +error[RUF065]: Unnecessary `repr()` conversion when formatting with `%s`. Use `%r` instead of `%s` + --> src/mdtest_snippet.py:13:29 + | +13 | logging.warning("%s %s %s", repr("1"), *["2", "3"]) # snapshot: logging-eager-conversion + | ^^^^^^^^^ +``` diff --git a/crates/ruff_linter/resources/mdtest/ruff/noqa-comments.md b/crates/ruff_linter/resources/mdtest/ruff/noqa-comments.md index c84e2a8568..5c339fc7ad 100644 --- a/crates/ruff_linter/resources/mdtest/ruff/noqa-comments.md +++ b/crates/ruff_linter/resources/mdtest/ruff/noqa-comments.md @@ -17,17 +17,16 @@ import math ``` ```snapshot -error[RUF105]: `ruff: noqa` comment used instead of `ruff:file-ignore` +error[RUF105]: `ruff: noqa` comment used instead of `ruff: file-ignore` --> src/mdtest_snippet.py:2:1 | 2 | # ruff: noqa: F401 | ^^^^^^^^^^^^^^^^^^ - | -help: Use `ruff:file-ignore` instead +help: Use `ruff: file-ignore` instead | 1 | # snapshot: noqa-comments - # ruff: noqa: F401 -2 + # ruff:file-ignore[F401] +2 + # ruff: file-ignore[F401] 3 | import math | ``` @@ -45,17 +44,16 @@ for os in []: ``` ```snapshot -error[RUF105]: `ruff: noqa` comment used instead of `ruff:file-ignore` +error[RUF105]: `ruff: noqa` comment used instead of `ruff: file-ignore` --> src/mdtest_snippet.py:2:1 | 2 | # ruff: noqa: F401, F402, F403 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | -help: Use `ruff:file-ignore` instead +help: Use `ruff: file-ignore` instead | 1 | # snapshot: noqa-comments - # ruff: noqa: F401, F402, F403 -2 + # ruff:file-ignore[F401, F402, F403] +2 + # ruff: file-ignore[F401, F402, F403] 3 | import math | ``` @@ -73,17 +71,16 @@ for os in []: ``` ```snapshot -error[RUF105]: `ruff: noqa` comment used instead of `ruff:file-ignore` +error[RUF105]: `ruff: noqa` comment used instead of `ruff: file-ignore` --> src/mdtest_snippet.py:2:1 | 2 | # ruff: noqa: F401, F402, F403 for some reason | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | -help: Use `ruff:file-ignore` instead +help: Use `ruff: file-ignore` instead | 1 | # snapshot: noqa-comments - # ruff: noqa: F401, F402, F403 for some reason -2 + # ruff:file-ignore[F401, F402, F403] for some reason +2 + # ruff: file-ignore[F401, F402, F403] for some reason 3 | import math | ``` @@ -101,17 +98,16 @@ for os in []: ``` ```snapshot -error[RUF105]: `ruff: noqa` comment used instead of `ruff:file-ignore` +error[RUF105]: `ruff: noqa` comment used instead of `ruff: file-ignore` --> src/mdtest_snippet.py:2:1 | 2 | # ruff: noqa: F401, F402, F403 # fmt:skip | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | -help: Use `ruff:file-ignore` instead +help: Use `ruff: file-ignore` instead | 1 | # snapshot: noqa-comments - # ruff: noqa: F401, F402, F403 # fmt:skip -2 + # ruff:file-ignore[F401, F402, F403] # fmt:skip +2 + # ruff: file-ignore[F401, F402, F403] # fmt:skip 3 | import math | ``` @@ -134,17 +130,16 @@ import math # noqa: F401, UNK001 ``` ```snapshot -error[RUF105]: `noqa` comment used instead of `ruff:ignore` +error[RUF105]: `noqa` comment used instead of `ruff: ignore` --> src/mdtest_snippet.py:3:14 | 3 | import math # noqa: F401, UNK001 | ^^^^^^^^^^^^^^^^^^^^ - | -help: Use `ruff:ignore` instead +help: Use `ruff: ignore` instead | 2 | # snapshot: noqa-comments - import math # noqa: F401, UNK001 -3 + import math # ruff:ignore[F401, UNK001] +3 + import math # ruff: ignore[F401, UNK001] | ``` @@ -166,7 +161,7 @@ import math # noqa: EXT001, EXT002 However, if only some of the codes are `external`, a diagnostic is emitted without an autofix. In this case, the external codes likely need to remain in a `noqa` comment, while the codes known by -Ruff could potentially move into a `ruff:ignore` comment. +Ruff could potentially move into a `ruff: ignore` comment. ```py # snapshot: noqa-comments @@ -174,20 +169,19 @@ import math # noqa: F401, EXT001 ``` ```snapshot -error[RUF105]: `noqa` comment used instead of `ruff:ignore` +error[RUF105]: `noqa` comment used instead of `ruff: ignore` --> src/mdtest_snippet.py:4:14 | 4 | import math # noqa: F401, EXT001 | ^^^^^^^^^^^^^^^^^^^^ - | -help: Use `ruff:ignore` instead +help: Use `ruff: ignore` instead ``` ### Any unmatched code disables the fix This leaves an unused `noqa` comment to be cleaned up by `RUF100` instead, which can be especially important in the case of a standalone `noqa` comment, which has no effect (in almost all cases), but -could become an effectful own-line `ruff:ignore` comment if `RUF105` applied. +could become an effectful own-line `ruff: ignore` comment if `RUF105` applied. ```py # snapshot: noqa-comments @@ -196,13 +190,12 @@ import math ``` ```snapshot -error[RUF105]: `ruff: noqa` comment used instead of `ruff:file-ignore` +error[RUF105]: `ruff: noqa` comment used instead of `ruff: file-ignore` --> src/mdtest_snippet.py:2:1 | 2 | # ruff: noqa: F401, F402 | ^^^^^^^^^^^^^^^^^^^^^^^^ - | -help: Use `ruff:file-ignore` instead +help: Use `ruff: file-ignore` instead ``` ### Flake8 comments are ignored @@ -222,17 +215,16 @@ import math # noqa: F401 ``` ```snapshot -error[RUF105]: `noqa` comment used instead of `ruff:ignore` +error[RUF105]: `noqa` comment used instead of `ruff: ignore` --> src/mdtest_snippet.py:2:14 | 2 | import math # noqa: F401 | ^^^^^^^^^^^^ - | -help: Use `ruff:ignore` instead +help: Use `ruff: ignore` instead | 1 | # snapshot: noqa-comments - import math # noqa: F401 -2 + import math # ruff:ignore[F401] +2 + import math # ruff: ignore[F401] | ``` @@ -246,13 +238,12 @@ import os # noqa: F401, F402 ``` ```snapshot -error[RUF105]: `noqa` comment used instead of `ruff:ignore` +error[RUF105]: `noqa` comment used instead of `ruff: ignore` --> src/mdtest_snippet.py:2:12 | 2 | import os # noqa: F401, F402 | ^^^^^^^^^^^^^^^^^^ - | -help: Use `ruff:ignore` instead +help: Use `ruff: ignore` instead ``` ### Nested pragma comment before the directive @@ -263,17 +254,16 @@ import math # fmt:skip # noqa: F401 ``` ```snapshot -error[RUF105]: `noqa` comment used instead of `ruff:ignore` +error[RUF105]: `noqa` comment used instead of `ruff: ignore` --> src/mdtest_snippet.py:2:25 | 2 | import math # fmt:skip # noqa: F401 | ^^^^^^^^^^^^ - | -help: Use `ruff:ignore` instead +help: Use `ruff: ignore` instead | 1 | # snapshot: noqa-comments - import math # fmt:skip # noqa: F401 -2 + import math # fmt:skip # ruff:ignore[F401] +2 + import math # fmt:skip # ruff: ignore[F401] | ``` @@ -290,17 +280,16 @@ import math # noqa ``` ```snapshot -error[RUF105]: `noqa` comment used instead of `ruff:ignore` +error[RUF105]: `noqa` comment used instead of `ruff: ignore` --> src/mdtest_snippet.py:2:14 | 2 | import math # noqa | ^^^^^^ - | -help: Use `ruff:ignore` instead +help: Use `ruff: ignore` instead | 1 | # snapshot: noqa-comments - import math # noqa -2 + import math # ruff:ignore[F401] +2 + import math # ruff: ignore[F401] 3 | # snapshot: noqa-comments | ``` @@ -313,17 +302,16 @@ import foo, bar # noqa ``` ```snapshot -error[RUF105]: `noqa` comment used instead of `ruff:ignore` +error[RUF105]: `noqa` comment used instead of `ruff: ignore` --> src/mdtest_snippet.py:4:18 | 4 | import foo, bar # noqa | ^^^^^^ - | -help: Use `ruff:ignore` instead +help: Use `ruff: ignore` instead | 3 | # snapshot: noqa-comments - import foo, bar # noqa -4 + import foo, bar # ruff:ignore[F401] +4 + import foo, bar # ruff: ignore[F401] | ``` @@ -338,13 +326,12 @@ import math ``` ```snapshot -error[RUF105]: `ruff: noqa` comment used instead of `ruff:file-ignore` +error[RUF105]: `ruff: noqa` comment used instead of `ruff: file-ignore` --> src/mdtest_snippet.py:2:1 | 2 | # ruff: noqa | ^^^^^^^^^^^^ - | -help: Use `ruff:file-ignore` instead +help: Use `ruff: file-ignore` instead ``` ## Inline self-suppression @@ -368,7 +355,7 @@ But a suppression for `RUF100` should not prevent the rule from firing: import math # noqa: RUF100, F401 ``` -## Suppression with `ruff:ignore` +## Suppression with `ruff: ignore` ```toml [lint] diff --git a/crates/ruff_linter/resources/mdtest/ruff/rule-codes-in-selectors.md b/crates/ruff_linter/resources/mdtest/ruff/rule-codes-in-selectors.md index cd9bada104..8c9729f43c 100644 --- a/crates/ruff_linter/resources/mdtest/ruff/rule-codes-in-selectors.md +++ b/crates/ruff_linter/resources/mdtest/ruff/rule-codes-in-selectors.md @@ -26,7 +26,6 @@ error[RUF201]: Rule code used instead of name in `lint.select` | 3 | "F401", # snapshot: rule-codes-in-selectors | ^^^^ - | help: Replace rule code with `unused-import` | 2 | select = [ @@ -41,7 +40,6 @@ error[RUF201]: Rule code used instead of name in `lint.select` | 4 | 'F402', # snapshot: rule-codes-in-selectors | ^^^^ - | help: Replace rule code with `import-shadowed-by-loop-var` | 3 | "F401", # snapshot: rule-codes-in-selectors @@ -56,7 +54,6 @@ error[RUF201]: Rule code used instead of name in `lint.select` | 5 | """F403""", # snapshot: rule-codes-in-selectors | ^^^^ - | help: Replace rule code with `undefined-local-with-import-star` | 4 | 'F402', # snapshot: rule-codes-in-selectors @@ -71,7 +68,6 @@ error[RUF201]: Rule code used instead of name in `lint.select` | 6 | '''F404''', # snapshot: rule-codes-in-selectors | ^^^^ - | help: Replace rule code with `late-future-import` | 5 | """F403""", # snapshot: rule-codes-in-selectors @@ -100,7 +96,6 @@ error[RUF201]: Rule code used instead of name in `lint.select` | 3 | select = ["'F401'", "F402"] | ^^^^ - | help: Replace rule code with `import-shadowed-by-loop-var` | 2 | # snapshot: rule-codes-in-selectors @@ -205,7 +200,6 @@ error[RUF201]: Rule code used instead of name in `lint.select` | 2 | lint.select = ["F401"] | ^^^^ - | help: Replace rule code with `unused-import` ``` diff --git a/crates/ruff_linter/resources/mdtest/ruff/rule-codes-in-suppression-comments.md b/crates/ruff_linter/resources/mdtest/ruff/rule-codes-in-suppression-comments.md index e202300431..8f5d43dbc2 100644 --- a/crates/ruff_linter/resources/mdtest/ruff/rule-codes-in-suppression-comments.md +++ b/crates/ruff_linter/resources/mdtest/ruff/rule-codes-in-suppression-comments.md @@ -25,7 +25,6 @@ error[RUF106]: Rule code used instead of name in suppression comment | 3 | # ruff:ignore[F401, undefined-name, EXT001, UNKNOWN, F841] | ^^^^ - | help: Replace rule code with name | 2 | # snapshot: rule-codes-in-suppression-comments @@ -40,7 +39,6 @@ error[RUF106]: Rule code used instead of name in suppression comment | 3 | # ruff:ignore[F401, undefined-name, EXT001, UNKNOWN, F841] | ^^^^ - | help: Replace rule code with name | 2 | # snapshot: rule-codes-in-suppression-comments @@ -65,7 +63,6 @@ error[RUF106]: Rule code used instead of name in suppression comment | 7 | # ruff:ignore[F401, undefined-name, F841] | ^^^^ - | help: Replace rule code with name | 6 | # snapshot: rule-codes-in-suppression-comments @@ -80,7 +77,6 @@ error[RUF106]: Rule code used instead of name in suppression comment | 7 | # ruff:ignore[F401, undefined-name, F841] | ^^^^ - | help: Replace rule code with name | 6 | # snapshot: rule-codes-in-suppression-comments @@ -104,7 +100,6 @@ error[RUF106]: Rule code used instead of name in suppression comment | 3 | # ruff:file-ignore[F401, F841] | ^^^^ - | help: Replace rule code with name | 2 | # snapshot: rule-codes-in-suppression-comments @@ -118,7 +113,6 @@ error[RUF106]: Rule code used instead of name in suppression comment | 3 | # ruff:file-ignore[F401, F841] | ^^^^ - | help: Replace rule code with name | 2 | # snapshot: rule-codes-in-suppression-comments @@ -148,7 +142,6 @@ error[RUF106]: Rule code used instead of name in suppression comment 4 | value = 1 5 | # ruff:enable[F401, undefined-name, F841] | ---- - | help: Replace rule code with name | 2 | # snapshot: rule-codes-in-suppression-comments @@ -168,7 +161,6 @@ error[RUF106]: Rule code used instead of name in suppression comment 4 | value = 1 5 | # ruff:enable[F401, undefined-name, F841] | ---- - | help: Replace rule code with name | 2 | # snapshot: rule-codes-in-suppression-comments @@ -196,7 +188,6 @@ error[RUF106]: Rule code used instead of name in suppression comment | 2 | # ruff:disable[F401] | ^^^^ - | help: Replace rule code with name | 1 | # snapshot: rule-codes-in-suppression-comments @@ -229,7 +220,6 @@ error[RUF106]: Rule code used instead of name in suppression comment | 2 | # ruff:ignore[PGH001] | ^^^^^^ - | help: Replace rule code with name | 1 | # snapshot: rule-codes-in-suppression-comments @@ -255,7 +245,6 @@ error[RUF106]: Rule code used instead of name in suppression comment | 3 | value = 1 # explanation # ruff:ignore[F401, F841] reason # another | ^^^^ - | help: Replace rule code with name | 2 | # snapshot: rule-codes-in-suppression-comments @@ -269,7 +258,6 @@ error[RUF106]: Rule code used instead of name in suppression comment | 3 | value = 1 # explanation # ruff:ignore[F401, F841] reason # another | ^^^^ - | help: Replace rule code with name | 2 | # snapshot: rule-codes-in-suppression-comments diff --git a/crates/ruff_linter/resources/mdtest/suppression/ignore.md b/crates/ruff_linter/resources/mdtest/suppression/ignore.md index 752f1dd757..dca3a68ddf 100644 --- a/crates/ruff_linter/resources/mdtest/suppression/ignore.md +++ b/crates/ruff_linter/resources/mdtest/suppression/ignore.md @@ -8,7 +8,6 @@ diagnostic range. ```toml [lint] -preview = true select = ["RUF015"] ``` @@ -29,7 +28,6 @@ entire class definition. ```toml [lint] -preview = true select = ["B903"] ``` @@ -46,7 +44,6 @@ Diagnostics with empty ranges should also be suppressible, as with `noqa`. ```toml [lint] -preview = true select = ["W292"] ``` @@ -61,7 +58,6 @@ after the matching `ruff:enable` comment: ```toml [lint] -preview = true select = ["RUF015"] ``` @@ -91,7 +87,6 @@ not_suppressed = [ ```toml [lint] -preview = true select = ["RUF015"] ``` @@ -153,7 +148,6 @@ x = ( ```toml [lint] -preview = true select = [ "F" ] ``` @@ -174,7 +168,6 @@ from foo import ( # ruff:ignore[F401] ```toml [lint] -preview = true select = [ "F401", "RUF100" ] ``` @@ -201,7 +194,6 @@ from sys import ( # ruff:ignore[F401] ```toml [lint] -preview = true select = [ "W291" ] ``` @@ -269,7 +261,6 @@ error[RUF100]: Unused suppression (unused: `F401`) | 21 | import sys # start # ruff:ignore[F401] # end | ^^^^^^^^^^^^^^^^^^^^ - | help: Remove unused suppression | 20 | # snapshot: unused-noqa @@ -303,7 +294,6 @@ def f(): ```toml [lint] -preview = true select = ["F401", "RUF100", "RUF104"] ``` @@ -346,7 +336,6 @@ error[RUF102]: Invalid rule code in suppression: unused-import 4 | import math 5 | # ruff:enable[unused-import] | ------------- - | help: Enable `lint.preview` to use rule names help: Remove the suppression comment | @@ -379,7 +368,6 @@ error[RUF102]: Invalid rule code in suppression: unknown-rule, unused-import 9 | import sys 10 | # ruff:enable[unused-import, unknown-rule] | ------------------------------------------ - | help: Add non-Ruff rule codes to the `lint.external` configuration option help: Enable `lint.preview` to use rule names help: Remove the suppression comment @@ -454,7 +442,6 @@ error[RUF103]: Invalid suppression comment: no matching 'disable' comment | 12 | # ruff:enable[F401] | ^^^^^^^^^^^^^^^^^^^ - | help: Remove suppression comment | 11 | # snapshot: invalid-suppression-comment @@ -494,7 +481,6 @@ error[RUF102]: Invalid rule code in suppression: not-a-rule | 2 | # ruff:ignore[unused-import, not-a-rule] | ^^^^^^^^^^ - | help: Add non-Ruff rule codes to the `lint.external` configuration option help: Remove the rule code `not-a-rule` | @@ -528,7 +514,6 @@ error[RUF100]: Unused suppression (unused: `unused-import`) | 8 | import math # ruff:ignore[unused-import] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove unused suppression | 7 | # snapshot: unused-noqa @@ -553,7 +538,6 @@ error[RUF100]: Unused suppression (unused: `unused-import`) | 12 | # ruff:ignore[F401, unused-import] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove unused suppression | 11 | # snapshot: unused-noqa @@ -567,7 +551,6 @@ help: Remove unused suppression ```toml [lint] -preview = true select = ["F401", "RUF103", "RUF104"] ``` @@ -591,7 +574,6 @@ error[RUF103]: Invalid suppression comment: missing suppression codes like `[E50 | 4 | import sys # explanation # ruff:ignore # another | ^^^^^^^^^^^^^^ - | help: Remove suppression comment | 3 | # error: [unused-import] @@ -617,7 +599,6 @@ import foo ```toml [lint] -preview = true select = ["F401", "RUF103", "RUF104"] ``` @@ -642,7 +623,6 @@ error[RUF103]: Invalid suppression comment: trailing comments are only supported | 2 | # explanation # ruff:disable[F401] | ^^^^^^^^^^^^^^^^^^^^ - | help: Remove suppression comment | 1 | # snapshot: invalid-suppression-comment @@ -667,7 +647,6 @@ import foo ```toml [lint] -preview = true select = ["F401", "RUF100", "FIX002"] ``` @@ -699,7 +678,6 @@ a = 10 ```toml [lint] -preview = true select = ["E501", "F821", "RUF100", "RUF103"] ``` @@ -721,7 +699,6 @@ error[RUF100]: Unused suppression (unused: `E501`) | 3 | # ruff:ignore[E501] # ruff:file-ignore[F821] | ^^^^^^^^^^^^^^^^^^^^ - | help: Remove unused suppression | 2 | # error: [invalid-suppression-comment] @@ -756,7 +733,6 @@ error[RUF100]: Unused suppression (unused: `E501`) 10 | undefined_name 11 | # ruff:enable[E501] | ------------------- - | help: Remove unused suppression | 7 | # error: [unused-noqa] "F821" @@ -773,7 +749,6 @@ note: This is an unsafe fix and may change runtime behavior ```toml [lint] -preview = true select = ["E501", "RUF100", "FIX002"] ``` @@ -795,7 +770,6 @@ error[RUF100]: Unused suppression (unused: `E501`) 3 | value = 1 4 | # ruff:enable[E501] # TODO # ruff:ignore[FIX002] | -------------------- - | help: Remove unused suppression | 1 | # snapshot: unused-noqa @@ -811,7 +785,6 @@ note: This is an unsafe fix and may change runtime behavior ```toml [lint] -preview = true select = ["E501", "F401", "F821", "RUF100", "RUF103"] ``` @@ -831,7 +804,6 @@ error[RUF100]: Unused suppression (unused: `E501`) | 3 | # ruff:ignore[E501, F821] # ruff:file-ignore[F401] | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove unused suppression | 2 | # error: [invalid-suppression-comment] @@ -845,7 +817,6 @@ help: Remove unused suppression ```toml [lint] -preview = true select = ["F821", "RUF102", "RUF103"] ``` @@ -865,7 +836,6 @@ error[RUF102]: Invalid rule code in suppression: XYZ | 3 | # ruff:ignore[XYZ] # ruff:file-ignore[F821] | ^^^ - | help: Add non-Ruff rule codes to the `lint.external` configuration option help: Remove the suppression comment | @@ -881,7 +851,6 @@ note: This is an unsafe fix and may change runtime behavior ```toml [lint] -preview = true select = ["F401", "F821", "RUF100", "RUF103"] ``` @@ -902,7 +871,6 @@ error[RUF103]: Invalid suppression comment: trailing comments are only supported | 4 | # explanation # ruff:file-ignore[F401] # ruff:ignore[F401] | ^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove suppression comment | 3 | # error: [unused-noqa] @@ -929,7 +897,6 @@ error[RUF103]: Invalid suppression comment: missing suppression codes like `[E50 | 9 | # explanation # ruff:ignore # ruff:ignore[F821] | ^^^^^^^^^^^^^^ - | help: Remove suppression comment | 8 | # error: [unused-noqa] @@ -944,7 +911,6 @@ note: This is an unsafe fix and may change runtime behavior ```toml [lint] -preview = true select = ["RUF103"] ``` @@ -963,7 +929,6 @@ error[RUF103]: Invalid suppression comment: unknown ruff directive | 2 | import os # explanation # ruff:unknown[F401] # another | ^^^^^^^^^^^^^^^^^^^^^ - | help: Remove suppression comment | 1 | # snapshot: invalid-suppression-comment @@ -979,7 +944,6 @@ error[RUF103]: Invalid suppression comment: missing comma between codes | 4 | import sys # explanation # ruff:ignore[F401 F841] # another | ^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove suppression comment | 3 | # snapshot: invalid-suppression-comment @@ -993,7 +957,6 @@ note: This is an unsafe fix and may change runtime behavior ```toml [lint] -preview = true select = ["F401", "RUF103"] ``` @@ -1008,7 +971,6 @@ import os # before # ruff:ignore # ruff:ignore[F401] # after ```toml [lint] -preview = true select = ["RUF100"] ``` @@ -1028,7 +990,6 @@ error[RUF100]: Unused suppression (non-enabled: `F401`) | 2 | value = 1 # before # ruff:ignore[F401] # after | ^^^^^^^^^^^^^^^^^^^^ - | help: Remove unused suppression | 1 | # snapshot: unused-noqa @@ -1044,7 +1005,6 @@ error[RUF100]: Unused suppression (non-enabled: `F401`) | 5 | value = 1 # before # ruff:ignore[F401] | ^^^^^^^^^^^^^^^^^^^ - | help: Remove unused suppression | 4 | # snapshot: unused-noqa @@ -1061,7 +1021,6 @@ at offset zero, as with `noqa`. ```toml [lint] -preview = true select = ["D100"] ``` diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B006_B008.py b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B006_B008.py index 8e55d5b340..8f461b4dbb 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B006_B008.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B006_B008.py @@ -98,7 +98,7 @@ def dont_forget_me(value=collections.deque()): ... -# N.B. we're also flagging the function call in the comprehension +# B006 still flags mutable comprehension defaults. def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]): pass 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 c1ac839e27..d2ffaa7352 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C408.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C408.py @@ -37,3 +37,12 @@ def list(): 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" + +# https://github.com/astral-sh/ruff/issues/16234 +# Python normalizes identifiers to NFKC, but does not normalize string literals, so the fix has to +# normalize the keyword name to preserve the dictionary key at runtime. The character "ℼ" normalizes +# to "π", and "ſ" normalizes to "s". +dict(ℼ=3.14) +dict(ſ=1) +dict(𝕒=1, b=2) +dict(a=1, b=2) # already NFKC-normalized: unchanged diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT018.py b/crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT018.py index 0ccec6f759..77efd8a097 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT018.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT018.py @@ -70,3 +70,10 @@ def test_parenthesized_not(): assert (not self.find_graph_output(node.output[0]) or self.find_graph_input(node.input[0])) + + +def test_comments(): + assert ( + # comment + something and something_else + ) diff --git a/crates/ruff_linter/resources/test/fixtures/pydocstyle/sphinx_directive.py b/crates/ruff_linter/resources/test/fixtures/pydocstyle/sphinx_directive.py new file mode 100644 index 0000000000..4a79396323 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pydocstyle/sphinx_directive.py @@ -0,0 +1,78 @@ +"""A module-level docstring with a Sphinx directive containing section-like content. + +.. code-block:: yaml + + references: + - ref: Bibliographic citation in your favorite format. + refType: open literature + +This is more text after the directive. +""" + + +def func(): + """A function-level docstring with a Sphinx directive. + + Examples: + This is an example. + + .. code-block:: python + + returns = "not a section" + notes = "also not a section" + + Returns: + None + """ + + +def func2(): + """A function-level docstring with single-colon directive (invalid RST but still common). + + .. code-block: yaml + + references: + - ref: Some reference. + + More text. + """ + + +def func3(): + """A function-level docstring with nested directives. + + .. note:: + + .. code-block:: python + + warnings = "not a section" + + Returns: + None + """ + + +def func4(): + """A function-level docstring where a real section follows a directive. + + .. code-block:: python + + example = "code" + + Notes: + This IS a real section and should still be detected. + """ +# Regression test for nested directive state. +def func5(): + """A nested directive whose outer body contains section-like content. + + .. note:: + + .. code-block:: yaml + + references: + - ref: Some reference. + + references: + Still part of the note body, not a section. + """ diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/bad_string_format_character.py b/crates/ruff_linter/resources/test/fixtures/pylint/bad_string_format_character.py index cffe53d723..eb1d9b2a55 100644 --- a/crates/ruff_linter/resources/test/fixtures/pylint/bad_string_format_character.py +++ b/crates/ruff_linter/resources/test/fixtures/pylint/bad_string_format_character.py @@ -29,6 +29,10 @@ f"{1:z}" # [bad-format-character] -## False negatives +## Supporting concatenated strings print(("%" "z") % 1) + +## `%b` is only valid for bytes formatting. +"%b" % b"25" +b"%b" % b"25" diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/bad_string_format_type.py b/crates/ruff_linter/resources/test/fixtures/pylint/bad_string_format_type.py index e95b8ed9a6..61dfe779f0 100644 --- a/crates/ruff_linter/resources/test/fixtures/pylint/bad_string_format_type.py +++ b/crates/ruff_linter/resources/test/fixtures/pylint/bad_string_format_type.py @@ -63,3 +63,9 @@ "%c" % ("x",) "%c" % "x" "%c" % "œ" + +# No errors here, will be reported separately by bad-string-format-character. +"%b" % b"xx" + +# bool is acceptable as int/float/character. +"%d %c %f" % (True, True, True) diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB105.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB105.py index 007d5822de..51eae16737 100644 --- a/crates/ruff_linter/resources/test/fixtures/refurb/FURB105.py +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB105.py @@ -22,6 +22,10 @@ print(f"") print(f"", sep=",") print(f"", end="bar") +print(1, sep=None) + +def p(sep): + print(1, sep=sep) # OK. diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB164.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB164.py index 1ac29fbf70..992427c376 100644 --- a/crates/ruff_linter/resources/test/fixtures/refurb/FURB164.py +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB164.py @@ -75,3 +75,8 @@ # text .from_float(4.2) ) + +_ = Decimal.from_float( + # keep this comment + float("inf") +) diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB192_1.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB192_1.py new file mode 100644 index 0000000000..70b90d271b --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB192_1.py @@ -0,0 +1,17 @@ +# A `yield` expression is only valid as a call argument when it is +# parenthesized, so the parentheses have to be preserved by the fix. +# +# These live in their own fixture because `FURB192.py` shadows `sorted` with a +# module-level function, which suppresses the rule inside function bodies. + + +def f(l, key_fn): + sorted((yield))[0] + + sorted((yield l))[-1] + + sorted((yield), key=key_fn)[0] + + sorted((yield from l))[0] + + sorted((yield), reverse=True)[-1] diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index 8fce8c5550..df7b8b6aad 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -8,8 +8,7 @@ use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::preview::{ - is_future_required_preview_generics_enabled, is_pep604_future_annotations_fix_enabled, - is_up006_future_annotations_fix_enabled, + is_pep604_future_annotations_fix_enabled, is_up006_future_annotations_fix_enabled, }; use crate::registry::Rule; use crate::rules::{ @@ -85,11 +84,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { && checker.semantic.in_annotation() && checker.semantic.in_runtime_evaluated_annotation() && !checker.semantic.in_string_type_definition() - && typing::is_pep585_generic( - value, - &checker.semantic, - is_future_required_preview_generics_enabled(checker.settings()), - ) + && typing::is_pep585_generic(value, &checker.semantic) { flake8_future_annotations::rules::future_required_type_annotation( checker, @@ -113,8 +108,8 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { if checker.is_rule_enabled(Rule::UnnecessaryLiteralUnion) { flake8_pyi::rules::unnecessary_literal_union(checker, expr); } + // Avoid duplicate checks inside `Optional`. if checker.is_rule_enabled(Rule::DuplicateUnionMember) - // Avoid duplicate checks inside `Optional` && !checker.semantic.inside_optional() { flake8_pyi::rules::duplicate_union_member(checker, expr); @@ -561,7 +556,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { range: _, node_index: _, }, - range: _, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, @@ -1510,6 +1505,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { Rule::PercentFormatPositionalCountMismatch, Rule::PercentFormatStarRequiresSequence, Rule::PercentFormatUnsupportedFormatCharacter, + Rule::BadStringFormatCharacter, ]) { let location = expr.range(); match pyflakes::cformat::CFormatSummary::try_from(value.to_str()) { @@ -1524,6 +1520,11 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { }, location, ); + // PLE1300 + checker.report_diagnostic_if_enabled( + pylint::rules::BadStringFormatCharacter { format_char: c }, + location, + ); } Err(e) => { // F501 @@ -1576,13 +1577,6 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { if checker.is_rule_enabled(Rule::PrintfStringFormatting) { pyupgrade::rules::printf_string_formatting(checker, bin_op, format_string); } - if checker.is_rule_enabled(Rule::BadStringFormatCharacter) { - pylint::rules::bad_string_format_character::percent( - checker, - expr, - format_string, - ); - } if checker.is_rule_enabled(Rule::BadStringFormatType) { pylint::rules::bad_string_format_type(checker, bin_op, format_string); } @@ -1634,9 +1628,9 @@ 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() { + // Avoid duplicate checks inside `Optional`. if checker.is_rule_enabled(Rule::DuplicateUnionMember) && checker.semantic.in_type_definition() - // Avoid duplicate checks inside `Optional` && !checker.semantic.inside_optional() { flake8_pyi::rules::duplicate_union_member(checker, expr); diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs index 3032e08a02..22f5f16fe9 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs @@ -164,7 +164,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { flake8_pyi::rules::bad_exit_annotation(checker, function_def); } if checker.is_rule_enabled(Rule::RedundantNumericUnion) { - flake8_pyi::rules::redundant_numeric_union(checker, parameters); + flake8_pyi::rules::redundant_numeric_union(checker, function_def); } if checker.is_rule_enabled(Rule::Pep484StylePositionalOnlyParameter) { flake8_pyi::rules::pep_484_positional_parameter(checker, function_def); @@ -1383,7 +1383,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { flake8_bugbear::rules::jump_statement_in_finally(checker, finalbody); } if checker.is_rule_enabled(Rule::ContinueInFinally) { - if checker.target_version() <= PythonVersion::PY38 { + if checker.target_version() < PythonVersion::PY38 { pylint::rules::continue_in_finally(checker, finalbody); } } diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 18b5e322da..3bb52efe88 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -255,7 +255,7 @@ pub(crate) struct Checker<'a> { impl<'a> Checker<'a> { #[expect(clippy::too_many_arguments)] - pub(crate) fn new( + fn new( parsed: &'a Parsed, parsed_annotations_arena: &'a typed_arena::Arena>, settings: &'a LinterSettings, @@ -617,7 +617,7 @@ impl<'a> Checker<'a> { } /// Push `diagnostic` if the checker is not in a `@no_type_check` context. - pub(crate) fn report_type_diagnostic(&self, kind: T, range: TextRange) { + fn report_type_diagnostic(&self, kind: T, range: TextRange) { if !self.semantic.in_no_type_check() { self.report_diagnostic(kind, range); } @@ -1759,13 +1759,14 @@ impl<'a> Visitor<'a> for Checker<'a> { return; } + // `in_deferred_type_definition()` will only be `true` if we're now visiting the deferred nodes + // after having already traversed the source tree once. If we're now visiting the deferred nodes, + // we can't defer again, or we'll infinitely recurse! if !self.semantic.in_typing_literal() - // `in_deferred_type_definition()` will only be `true` if we're now visiting the deferred nodes - // after having already traversed the source tree once. If we're now visiting the deferred nodes, - // 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.target_version().defers_annotations()) + && (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 { @@ -1804,7 +1805,7 @@ impl<'a> Visitor<'a> for Checker<'a> { Expr::Call(ast::ExprCall { func, arguments: _, - range: _, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, @@ -1928,7 +1929,7 @@ impl<'a> Visitor<'a> for Checker<'a> { Expr::Call(ast::ExprCall { func, arguments, - range: _, + range_start: _, node_index: _, is_cast, is_checked_cast, @@ -2928,15 +2929,14 @@ impl<'a> Checker<'a> { match parent { Stmt::TypeAlias(_) => flags.insert(BindingFlags::DEFERRED_TYPE_ALIAS), + // TODO: It is a bit unfortunate that we do this check twice. Maybe we should change how + // we visit this statement so the semantic flag for the type alias sticks around until + // after we've handled this store, so we can check the flag instead of duplicating this check. Stmt::AnnAssign(ast::StmtAnnAssign { annotation, .. }) - // TODO: It is a bit unfortunate that we do this check twice - // maybe we should change how we visit this statement - // so the semantic flag for the type alias sticks around - // until after we've handled this store, so we can check - // the flag instead of duplicating this check - if self.semantic.match_typing_expr(annotation, "TypeAlias") => { - flags.insert(BindingFlags::ANNOTATED_TYPE_ALIAS); - } + if self.semantic.match_typing_expr(annotation, "TypeAlias") => + { + flags.insert(BindingFlags::ANNOTATED_TYPE_ALIAS); + } _ => {} } @@ -3676,7 +3676,7 @@ impl<'a> LintContext<'a> { /// Prefer [`LintContext::report_diagnostic_if_enabled`] unless you need to attach /// sub-diagnostics before the fix title. See its documentation for more details. #[expect(unused)] - pub(crate) fn report_custom_diagnostic_if_enabled<'chk, T: Violation>( + fn report_custom_diagnostic_if_enabled<'chk, T: Violation>( &'chk self, kind: T, range: TextRange, @@ -3794,7 +3794,7 @@ impl DiagnosticGuard<'_, '_> { /// /// Callers can add additional primary or secondary annotations via the /// `DerefMut` trait implementation to a `Diagnostic`. - pub(crate) fn set_primary_message(&mut self, message: impl IntoDiagnosticMessage) { + pub(crate) fn set_primary_annotation_message(&mut self, message: impl IntoDiagnosticMessage) { // N.B. It is normally bad juju to define `self` methods // on types that implement `Deref`. Instead, it's idiomatic // to do `fn foo(this: &mut LintDiagnosticGuard)`, which in @@ -3846,7 +3846,7 @@ impl DiagnosticGuard<'_, '_> { /// diagnostic.info("This will appear first"); /// diagnostic.before_drop(|diag| diag.info("This will appear last, after the fix title")); /// ``` - pub(crate) fn before_drop(&mut self, f: F) + fn before_drop(&mut self, f: F) where F: Fn(&mut Diagnostic) + 'static, { diff --git a/crates/ruff_linter/src/directives.rs b/crates/ruff_linter/src/directives.rs index 25a8ddcabf..ad9dbbed66 100644 --- a/crates/ruff_linter/src/directives.rs +++ b/crates/ruff_linter/src/directives.rs @@ -41,10 +41,10 @@ impl Flags { #[derive(Default, Debug)] pub struct IsortDirectives { /// Ranges for which sorting is disabled - pub exclusions: Vec, + pub(crate) exclusions: Vec, /// Text positions at which splits should be inserted - pub splits: Vec, - pub skip_file: bool, + pub(crate) splits: Vec, + pub(crate) skip_file: bool, } pub struct Directives { diff --git a/crates/ruff_linter/src/docstrings/sections.rs b/crates/ruff_linter/src/docstrings/sections.rs index a4151c67aa..fb73383a78 100644 --- a/crates/ruff_linter/src/docstrings/sections.rs +++ b/crates/ruff_linter/src/docstrings/sections.rs @@ -52,7 +52,7 @@ pub(crate) enum SectionKind { } impl SectionKind { - pub(crate) fn from_str(s: &str) -> Option { + fn from_str(s: &str) -> Option { match s.to_ascii_lowercase().as_str() { "args" => Some(Self::Args), "arguments" => Some(Self::Arguments), @@ -155,11 +155,31 @@ impl<'a> SectionContexts<'a> { // Skip the first line, which is the summary. let mut previous_line = lines.next(); + // Track only the outermost RST directive. Nested directives remain in its body + // until a non-blank line dedents to the outermost indentation. + // See: https://github.com/astral-sh/ruff/issues/23562 + let mut directive_indent = None; + while let Some(line) = lines.next() { - if let Some(section_kind) = suspected_as_section(&line, style) { - let indent = leading_space(&line); - let indent_size = indent.text_len(); + let indent = leading_space(&line); + let indent_size = indent.text_len(); + + if let Some(active_indent) = directive_indent + && (line.trim().is_empty() || indent_size > active_indent) + { + previous_line = Some(line); + continue; + } + + directive_indent = None; + if line.trim_start().starts_with(".. ") { + directive_indent = Some(indent_size); + previous_line = Some(line); + continue; + } + + if let Some(section_kind) = suspected_as_section(&line, style) { let section_name = leading_words(&line); let section_name_size = section_name.text_len(); diff --git a/crates/ruff_linter/src/fs.rs b/crates/ruff_linter/src/fs.rs index a16ad5dcfd..917f052299 100644 --- a/crates/ruff_linter/src/fs.rs +++ b/crates/ruff_linter/src/fs.rs @@ -8,7 +8,7 @@ use crate::settings::types::CompiledPerFileIgnoreList; /// Return the current working directory. /// /// On WASM this just returns `.`. Otherwise, defer to [`path_absolutize::path_dedot::CWD`]. -pub fn get_cwd() -> &'static Path { +pub(crate) fn get_cwd() -> &'static Path { cfg_select! { target_arch = "wasm32" => Path::new("."), _ => path_absolutize::path_dedot::CWD.as_path(), diff --git a/crates/ruff_linter/src/line_width.rs b/crates/ruff_linter/src/line_width.rs index 0c6eb95f70..f01390036c 100644 --- a/crates/ruff_linter/src/line_width.rs +++ b/crates/ruff_linter/src/line_width.rs @@ -10,7 +10,6 @@ use unicode_width::UnicodeWidthChar; use ruff_cache::{CacheKey, CacheKeyHasher}; use ruff_macros::CacheKey; use ruff_python_trivia::tab_offset; -use ruff_text_size::TextSize; /// The length of a line of text that is considered too long. /// @@ -21,16 +20,12 @@ pub struct LineLength(NonZeroU16); impl LineLength { /// Maximum allowed value for a valid [`LineLength`] - pub const MAX: u16 = u16::MAX; + const MAX: u16 = u16::MAX; /// Return the numeric value for this [`LineLength`] pub fn value(&self) -> u16 { self.0.get() } - - pub fn text_len(&self) -> TextSize { - TextSize::from(u32::from(self.value())) - } } impl Default for LineLength { @@ -184,12 +179,12 @@ impl Ord for LineWidthBuilder { } impl LineWidthBuilder { - pub fn get(&self) -> usize { + pub(crate) fn get(&self) -> usize { self.width } /// Creates a new `LineWidth` with the given tab size. - pub fn new(tab_size: IndentWidth) -> Self { + pub(crate) fn new(tab_size: IndentWidth) -> Self { LineWidthBuilder { width: 0, column: 0, @@ -221,13 +216,13 @@ impl LineWidthBuilder { /// Adds the given text to the line width. #[must_use] - pub fn add_str(self, text: &str) -> Self { + pub(crate) fn add_str(self, text: &str) -> Self { self.update(text.chars()) } /// Adds the given character to the line width. #[must_use] - pub fn add_char(self, c: char) -> Self { + pub(crate) fn add_char(self, c: char) -> Self { self.update(std::iter::once(c)) } @@ -237,7 +232,7 @@ impl LineWidthBuilder { /// The width and column should be the same for the corresponding text. /// Currently, this is only used to add spaces. #[must_use] - pub fn add_width(mut self, width: usize) -> Self { + pub(crate) fn add_width(mut self, width: usize) -> Self { self.width += width; self.column += width; self diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index dd324a6bf5..37d9ff0cf5 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -80,7 +80,7 @@ impl FixTable { .map(|(code, FixCount { rule_name, count })| (code, *rule_name, *count)) } - pub fn keys(&self) -> impl Iterator { + fn keys(&self) -> impl Iterator { self.0.keys() } @@ -436,6 +436,7 @@ pub fn add_suppressions_to_path( reason, &suppressions, suppression_kind, + settings.preview, ) } diff --git a/crates/ruff_linter/src/locator.rs b/crates/ruff_linter/src/locator.rs index 87afaae8bf..1110594f78 100644 --- a/crates/ruff_linter/src/locator.rs +++ b/crates/ruff_linter/src/locator.rs @@ -19,24 +19,17 @@ impl<'a> Locator<'a> { } } - pub fn with_index(contents: &'a str, index: LineIndex) -> Self { - Self { - contents, - index: OnceCell::from(index), - } - } - #[deprecated( note = "This is expensive, avoid using outside of the diagnostic phase. Prefer the other `Locator` methods instead." )] - pub fn compute_line_index(&self, offset: TextSize) -> OneIndexed { + pub(crate) fn compute_line_index(&self, offset: TextSize) -> OneIndexed { self.to_index().line_index(offset) } #[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) -> LineColumn { + pub(crate) fn compute_source_location(&self, offset: TextSize) -> LineColumn { self.to_source_code().line_column(offset) } @@ -45,23 +38,19 @@ impl<'a> Locator<'a> { .get_or_init(|| LineIndex::from_source_text(self.contents)) } - pub fn line_index(&self) -> Option<&LineIndex> { - self.index.get() - } - pub fn to_source_code(&self) -> SourceCode<'_, '_> { SourceCode::new(self.contents, self.to_index()) } /// Take the source code up to the given [`TextSize`]. #[inline] - pub fn up_to(&self, offset: TextSize) -> &'a str { + pub(crate) fn up_to(&self, offset: TextSize) -> &'a str { &self.contents[TextRange::up_to(offset)] } /// Take the source code after the given [`TextSize`]. #[inline] - pub fn after(&self, offset: TextSize) -> &'a str { + pub(crate) fn after(&self, offset: TextSize) -> &'a str { &self.contents[usize::from(offset)..] } @@ -109,7 +98,7 @@ impl<'a> Locator<'a> { /// Take the source code between the given [`TextRange`]. #[inline] - pub fn slice(&self, ranged: T) -> &'a str { + pub(crate) fn slice(&self, ranged: T) -> &'a str { &self.contents[ranged.range()] } @@ -119,18 +108,13 @@ impl<'a> Locator<'a> { } /// Return the number of bytes in the source code. - pub const fn len(&self) -> usize { + pub(crate) const fn len(&self) -> usize { self.contents.len() } - pub fn text_len(&self) -> TextSize { + pub(crate) fn text_len(&self) -> TextSize { self.contents.text_len() } - - /// Return `true` if the source code is empty. - pub const fn is_empty(&self) -> bool { - self.contents.is_empty() - } } // Override the `_str` methods from [`LineRanges`] to extend the lifetime to `'a`. @@ -138,30 +122,23 @@ impl<'a> Locator<'a> { /// Returns the text of the `offset`'s line. /// /// See [`LineRanges::full_lines_str`]. - pub fn full_line_str(&self, offset: TextSize) -> &'a str { + pub(crate) fn full_line_str(&self, offset: TextSize) -> &'a str { self.contents.full_line_str(offset) } /// Returns the text of the `offset`'s line. /// /// See [`LineRanges::line_str`]. - pub fn line_str(&self, offset: TextSize) -> &'a str { + pub(crate) fn line_str(&self, offset: TextSize) -> &'a str { self.contents.line_str(offset) } /// Returns the text of all lines that include `range`. /// /// See [`LineRanges::lines_str`]. - pub fn lines_str(&self, range: TextRange) -> &'a str { + pub(crate) fn lines_str(&self, range: TextRange) -> &'a str { self.contents.lines_str(range) } - - /// Returns the text of all lines that include `range`. - /// - /// See [`LineRanges::full_lines_str`]. - pub fn full_lines_str(&self, range: TextRange) -> &'a str { - self.contents.full_lines_str(range) - } } // Allow calling [`LineRanges`] methods on [`Locator`] directly. diff --git a/crates/ruff_linter/src/logging.rs b/crates/ruff_linter/src/logging.rs index 9dba2228b0..0b8bc4a341 100644 --- a/crates/ruff_linter/src/logging.rs +++ b/crates/ruff_linter/src/logging.rs @@ -186,7 +186,7 @@ impl DisplayParseError { } /// Create a [`DisplayParseError`] from a [`ParseError`] and a [`SourceCode`]. - pub fn from_source_code( + fn from_source_code( error: ParseError, path: Option, source_code: &SourceCode, diff --git a/crates/ruff_linter/src/message/grouped.rs b/crates/ruff_linter/src/message/grouped.rs index 6c7752b939..7f06cadd5a 100644 --- a/crates/ruff_linter/src/message/grouped.rs +++ b/crates/ruff_linter/src/message/grouped.rs @@ -13,10 +13,11 @@ use ruff_source_file::{LineColumn, OneIndexed}; use crate::fs::relativize_path; use crate::message::{Emitter, EmitterContext}; -pub struct GroupedEmitter { +pub(crate) struct GroupedEmitter { show_fix_status: bool, applicability: Applicability, preview: bool, + prefer_rule_codes: bool, } impl Default for GroupedEmitter { @@ -25,28 +26,35 @@ impl Default for GroupedEmitter { show_fix_status: false, applicability: Applicability::Safe, preview: false, + prefer_rule_codes: false, } } } impl GroupedEmitter { #[must_use] - pub fn with_show_fix_status(mut self, show_fix_status: bool) -> Self { + pub(crate) fn with_show_fix_status(mut self, show_fix_status: bool) -> Self { self.show_fix_status = show_fix_status; self } #[must_use] - pub fn with_applicability(mut self, applicability: Applicability) -> Self { + pub(crate) fn with_applicability(mut self, applicability: Applicability) -> Self { self.applicability = applicability; self } #[must_use] - pub fn with_preview(mut self, preview: bool) -> Self { + pub(crate) fn with_preview(mut self, preview: bool) -> Self { self.preview = preview; self } + + #[must_use] + pub(crate) fn with_prefer_rule_codes(mut self, prefer_rule_codes: bool) -> Self { + self.prefer_rule_codes = prefer_rule_codes; + self + } } impl Emitter for GroupedEmitter { @@ -87,6 +95,7 @@ impl Emitter for GroupedEmitter { row_length, column_length, preview: self.preview, + prefer_rule_codes: self.prefer_rule_codes, } )?; } @@ -136,6 +145,7 @@ struct DisplayGroupedMessage<'a> { column_length: NonZeroUsize, notebook_index: Option<&'a NotebookIndex>, preview: bool, + prefer_rule_codes: bool, } impl Display for DisplayGroupedMessage<'_> { @@ -182,6 +192,7 @@ impl Display for DisplayGroupedMessage<'_> { show_fix_status: self.show_fix_status, applicability: self.applicability, preview: self.preview, + prefer_rule_codes: self.prefer_rule_codes, }, )?; @@ -190,19 +201,21 @@ impl Display for DisplayGroupedMessage<'_> { } pub(super) struct RuleCodeAndBody<'a> { - pub(crate) message: &'a Diagnostic, - pub(crate) show_fix_status: bool, - pub(crate) applicability: Applicability, - pub(crate) preview: bool, + message: &'a Diagnostic, + show_fix_status: bool, + applicability: Applicability, + preview: bool, + prefer_rule_codes: bool, } impl Display for RuleCodeAndBody<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let use_name = self.preview && !self.prefer_rule_codes; if self.show_fix_status { if let Some(fix) = self.message.fix() { // Do not display an indicator for inapplicable fixes if fix.applies(self.applicability) { - let code = if self.preview { + let code = if use_name { self.message.id().as_str() } else { self.message.secondary_code_or_id() @@ -218,9 +231,7 @@ impl Display for RuleCodeAndBody<'_> { } } - if !self.preview - && let Some(code) = self.message.secondary_code() - { + if !use_name && let Some(code) = self.message.secondary_code() { write!( f, "{code} {body}", diff --git a/crates/ruff_linter/src/message/mod.rs b/crates/ruff_linter/src/message/mod.rs index 9959c42f75..72e6a50ab2 100644 --- a/crates/ruff_linter/src/message/mod.rs +++ b/crates/ruff_linter/src/message/mod.rs @@ -13,11 +13,11 @@ use ruff_db::diagnostic::{ }; use ruff_db::files::File; -pub use grouped::GroupedEmitter; +pub(crate) use grouped::GroupedEmitter; use ruff_notebook::NotebookIndex; use ruff_source_file::{SourceFile, SourceFileBuilder}; use ruff_text_size::{TextRange, TextSize}; -pub use sarif::SarifEmitter; +pub(crate) use sarif::SarifEmitter; use crate::Fix; use crate::registry::Rule; @@ -50,9 +50,10 @@ pub fn create_panic_diagnostic(error: &PanicError, path: Option<&Path>) -> Diagn match backtrace.status() { BacktraceStatus::Disabled => { diagnostic.sub(SubDiagnostic::new( - SubDiagnosticSeverity::Info, - "run with `RUST_BACKTRACE=1` environment variable to show the full backtrace information", - )); + SubDiagnosticSeverity::Info, + "run with `RUST_BACKTRACE=1` environment variable \ + to show the full backtrace information", + )); } BacktraceStatus::Captured => { diagnostic.sub(SubDiagnostic::new( @@ -76,7 +77,7 @@ pub fn create_panic_diagnostic(error: &PanicError, path: Option<&Path>) -> Diagn } #[expect(clippy::too_many_arguments)] -pub fn create_lint_diagnostic( +pub(crate) fn create_lint_diagnostic( body: B, suggestion: Option, range: TextRange, @@ -165,7 +166,7 @@ impl FileResolver for EmitterContext<'_> { /// Display format for [`Diagnostic`]s. /// /// The emitter serializes a slice of [`Diagnostic`]s and writes them to a [`Write`]. -pub trait Emitter { +pub(crate) trait Emitter { /// Serializes the `diagnostics` and writes the output to `writer`. fn emit( &mut self, @@ -175,7 +176,7 @@ pub trait Emitter { ) -> anyhow::Result<()>; } -/// Context passed to [`Emitter`]. +/// Context used while rendering diagnostics. pub struct EmitterContext<'a> { notebook_indexes: &'a FxHashMap, } @@ -185,12 +186,7 @@ impl<'a> EmitterContext<'a> { Self { notebook_indexes } } - /// Tests if the file with `name` is a jupyter notebook. - pub fn is_notebook(&self, name: &str) -> bool { - self.notebook_indexes.contains_key(name) - } - - pub fn notebook_index(&self, name: &str) -> Option<&NotebookIndex> { + fn notebook_index(&self, name: &str) -> Option<&NotebookIndex> { self.notebook_indexes.get(name) } } @@ -213,6 +209,7 @@ pub fn render_diagnostics( .with_show_fix_status(config.show_fix_status()) .with_applicability(config.fix_applicability()) .with_preview(config.preview_enabled()) + .with_prefer_rule_codes(config.is_prefer_rule_codes_enabled()) .emit(writer, diagnostics, context) .map_err(std::io::Error::other)?; } diff --git a/crates/ruff_linter/src/message/sarif.rs b/crates/ruff_linter/src/message/sarif.rs index 1d6a6edf86..0af6e30548 100644 --- a/crates/ruff_linter/src/message/sarif.rs +++ b/crates/ruff_linter/src/message/sarif.rs @@ -21,12 +21,12 @@ use crate::registry::{Linter, RuleNamespace}; /// Static Analysis Results Interchange Format (SARIF) is a standard format /// for static analysis results. For full specification, see: /// [SARIF 2.1.0](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html) -pub struct SarifEmitter<'a> { +pub(crate) struct SarifEmitter<'a> { config: &'a DisplayDiagnosticConfig, } impl<'a> SarifEmitter<'a> { - pub fn new(config: &'a DisplayDiagnosticConfig) -> Self { + pub(crate) fn new(config: &'a DisplayDiagnosticConfig) -> Self { Self { config } } } @@ -194,7 +194,11 @@ impl Serialize for RuleCode<'_> { impl<'a> RuleCode<'a> { fn from_diagnostic(code: &'a Diagnostic, config: &'a DisplayDiagnosticConfig) -> Self { match code.secondary_code() { - Some(diagnostic) if !config.preview_enabled() => Self::SecondaryCode(diagnostic), + Some(diagnostic) + if !config.preview_enabled() || config.is_prefer_rule_codes_enabled() => + { + Self::SecondaryCode(diagnostic) + } _ => Self::LintId(code.id().as_str()), } } diff --git a/crates/ruff_linter/src/noqa.rs b/crates/ruff_linter/src/noqa.rs index f7fe29b14b..9c8b973e4c 100644 --- a/crates/ruff_linter/src/noqa.rs +++ b/crates/ruff_linter/src/noqa.rs @@ -19,8 +19,10 @@ use rustc_hash::FxHashSet; use crate::Edit; use crate::Locator; use crate::fs::relativize_path; +use crate::preview::is_human_readable_names_enabled; use crate::registry::Rule; use crate::rule_redirects::get_redirect_target; +use crate::settings::types::PreviewMode; use crate::suppression::{self, Suppressions}; /// Generates an array of edits that matches the length of `diagnostics`. @@ -39,6 +41,7 @@ pub fn generate_suppression_edits( line_ending: LineEnding, suppressions: &Suppressions, suppression_kind: SuppressionKind, + preview: PreviewMode, ) -> Vec> { let file_directives = FileNoqaDirectives::extract(locator, comment_ranges, external, path); let exemption = FileExemption::from(&file_directives); @@ -51,6 +54,7 @@ pub fn generate_suppression_edits( noqa_line_for, suppressions, suppression_kind, + preview, ); build_suppression_edits_by_diagnostic(comments, locator, line_ending, None, suppression_kind) } @@ -276,13 +280,16 @@ impl<'a> FileNoqaDirectives<'a> { for warning in warnings { warn!( - "Missing or joined rule code(s) at {path_display}:{line}: {warning}" + "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." + "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; } @@ -744,14 +751,18 @@ pub(crate) enum LexicalError { impl Display for LexicalError { fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - LexicalError::MissingCodes => fmt.write_str("expected a comma-separated list of codes (e.g., `# noqa: F401, F841`)."), - LexicalError::InvalidSuffix => { - fmt.write_str("expected `:` followed by a comma-separated list of codes (e.g., `# noqa: F401, F841`).") - } - LexicalError::InvalidCodeSuffix => { - fmt.write_str("expected code to consist of uppercase letters followed by digits only (e.g. `F401`)") - } - + LexicalError::MissingCodes => fmt.write_str( + "expected a comma-separated list of codes \ + (e.g., `# noqa: F401, F841`).", + ), + LexicalError::InvalidSuffix => fmt.write_str( + "expected `:` followed by a comma-separated list of codes \ + (e.g., `# noqa: F401, F841`).", + ), + LexicalError::InvalidCodeSuffix => fmt.write_str( + "expected code to consist of uppercase letters followed by digits only \ + (e.g. `F401`)", + ), } } } @@ -763,7 +774,7 @@ impl Error for LexicalError {} pub enum SuppressionKind { /// A `noqa` comment Noqa, - /// A `ruff:ignore` comment + /// A `ruff: ignore` comment Ignore, } @@ -780,6 +791,7 @@ pub(crate) fn add_suppression( reason: Option<&str>, suppressions: &Suppressions, suppression_kind: SuppressionKind, + preview: PreviewMode, ) -> Result { let (count, output) = add_suppression_inner( path, @@ -792,6 +804,7 @@ pub(crate) fn add_suppression( reason, suppressions, suppression_kind, + preview, ); fs::write(path, output)?; @@ -810,6 +823,7 @@ fn add_suppression_inner( reason: Option<&str>, suppressions: &Suppressions, suppression_kind: SuppressionKind, + preview: PreviewMode, ) -> (usize, String) { let mut count = 0; @@ -827,6 +841,7 @@ fn add_suppression_inner( noqa_line_for, suppressions, suppression_kind, + preview, ); let edits = @@ -938,6 +953,7 @@ impl Ranged for ExistingDirective<'_> { } } +#[expect(clippy::too_many_arguments)] fn find_suppression_comments<'a>( diagnostics: &'a [Diagnostic], locator: &'a Locator, @@ -946,6 +962,7 @@ fn find_suppression_comments<'a>( noqa_line_for: &NoqaMapping, suppressions: &'a Suppressions, suppression_kind: SuppressionKind, + preview: PreviewMode, ) -> Vec>> { // List of suppression comments, ordered to match up with `messages` let mut comments_by_line: Vec>> = vec![]; @@ -1023,7 +1040,8 @@ fn find_suppression_comments<'a>( let identifier = match suppression_kind { SuppressionKind::Noqa => code.as_str(), - SuppressionKind::Ignore => message.name(), + SuppressionKind::Ignore if is_human_readable_names_enabled(preview) => message.name(), + SuppressionKind::Ignore => code.as_str(), }; comments_by_line.push(Some(SuppressionComment { @@ -1060,7 +1078,7 @@ impl SuppressionEdit<'_> { } match self.suppression_kind { SuppressionKind::Noqa => write!(writer, "# noqa: ").unwrap(), - SuppressionKind::Ignore => write!(writer, "# ruff:ignore[").unwrap(), + SuppressionKind::Ignore => write!(writer, "# ruff: ignore[").unwrap(), } push_codes( writer, @@ -1107,7 +1125,7 @@ fn generate_suppression_edit<'a>( (edit_range, blank_line) = suppression_edit_range(locator, line_range, codes.start()); existing_codes.extend(codes.iter().map(Code::as_str)); } - // Add additional rule names to an existing `ruff:ignore` comment. + // Add additional rule names to an existing `ruff: ignore` comment. (Some(ExistingDirective::Ignore(comment)), SuppressionKind::Ignore) => { (edit_range, blank_line) = suppression_edit_range(locator, line_range, comment.start()); existing_codes.extend(comment.codes_as_str(locator.contents())); @@ -1205,7 +1223,8 @@ impl<'a> NoqaDirectives<'a> { let path_display = relativize_path(path); for warning in warnings { warn!( - "Missing or joined rule code(s) at {path_display}:{line}: {warning}" + "Missing or joined rule code(s) \ + at {path_display}:{line}: {warning}" ); } } @@ -1231,10 +1250,7 @@ impl<'a> NoqaDirectives<'a> { Self { inner: directives } } - pub(crate) fn find_line_with_directive( - &self, - offset: TextSize, - ) -> Option<&NoqaDirectiveLine<'_>> { + fn find_line_with_directive(&self, offset: TextSize) -> Option<&NoqaDirectiveLine<'_>> { self.find_line_index(offset).map(|index| &self.inner[index]) } @@ -1368,6 +1384,7 @@ mod tests { use crate::rules::pycodestyle::rules::{AmbiguousVariableName, UselessSemicolon}; use crate::rules::pyflakes::rules::UnusedVariable; use crate::rules::pyupgrade::rules::PrintfStringFormatting; + use crate::settings::types::PreviewMode; use crate::settings::{LinterSettings, flags}; use crate::source_kind::SourceKind; use crate::suppression::Suppressions; @@ -1423,6 +1440,7 @@ mod tests { None, &suppressions, suppression_kind, + settings.preview, ) } @@ -1454,7 +1472,8 @@ mod tests { if second_count > 0 { writeln!( output, - "## Additional suppressions added on a second pass: {second_count}\n\n```py\n{fixed}\n```\n" + "## Additional suppressions added on a second pass: \ + {second_count}\n\n```py\n{fixed}\n```\n" )?; } @@ -3070,7 +3089,7 @@ mod tests { ## Fixed source ```py - def unused(x): # ruff:ignore[ANN001, ARG001, D103] # noqa: ANN001, ANN201, D103 + def unused(x): # ruff:ignore[ANN001, ARG001, D103] # noqa: ANN201 pass ``` " @@ -3132,7 +3151,7 @@ mod tests { ## Fixed source ```py - def unused(x): # noqa: ANN001, ARG001, D103 # ruff:ignore[missing-return-type-undocumented-public-function] + def unused(x): # noqa: ANN001, ARG001, D103 # ruff: ignore[missing-return-type-undocumented-public-function] pass ``` " @@ -3182,7 +3201,7 @@ mod tests { ## Fixed source ```py - import math # noqa: F401 # ruff:ignore[noqa-comments] + import math # noqa: F401 # ruff: ignore[noqa-comments] ``` " @@ -3213,7 +3232,7 @@ mod tests { ## Fixed source ```py - def unused(x): # ruff:ignore[ANN001, ARG001, D103, missing-return-type-undocumented-public-function] + def unused(x): # ruff: ignore[ANN001, ARG001, D103, missing-return-type-undocumented-public-function] pass ``` " @@ -3243,7 +3262,7 @@ mod tests { ## Fixed source ```py - def unused(x): # ruff:ignore[missing-return-type-undocumented-public-function, missing-type-function-argument, undocumented-public-function] + def unused(x): # ruff: ignore[missing-return-type-undocumented-public-function, missing-type-function-argument, undocumented-public-function] pass ``` " @@ -3269,7 +3288,7 @@ mod tests { ## Fixed source ```py - import z # ruff:ignore[unsorted-imports] + import z # ruff: ignore[unsorted-imports] import c import a ``` @@ -3301,7 +3320,7 @@ mod tests { ## Fixed source ```py - # ruff:ignore[ANN001, missing-return-type-undocumented-public-function] + # ruff: ignore[ANN001, missing-return-type-undocumented-public-function] def public(x): """Return x.""" return x @@ -3328,6 +3347,7 @@ mod tests { None, &Suppressions::default(), SuppressionKind::Noqa, + PreviewMode::Disabled, ); assert_eq!(count, 0); assert_eq!(output, format!("{contents}")); @@ -3354,6 +3374,7 @@ mod tests { None, &Suppressions::default(), SuppressionKind::Noqa, + PreviewMode::Disabled, ); assert_eq!(count, 1); assert_eq!(output, "x = 1 # noqa: F841\n"); @@ -3387,6 +3408,7 @@ mod tests { None, &Suppressions::default(), SuppressionKind::Noqa, + PreviewMode::Disabled, ); assert_eq!(count, 1); assert_eq!(output, "x = 1 # noqa: E741, F841\n"); @@ -3420,6 +3442,7 @@ mod tests { None, &Suppressions::default(), SuppressionKind::Noqa, + PreviewMode::Disabled, ); assert_eq!(count, 0); assert_eq!(output, "x = 1 # noqa"); @@ -3453,6 +3476,7 @@ print( LineEnding::Lf, &suppressions, SuppressionKind::Noqa, + PreviewMode::Disabled, ); assert_eq!( edits, @@ -3487,6 +3511,7 @@ bar = LineEnding::Lf, &suppressions, SuppressionKind::Noqa, + PreviewMode::Disabled, ); assert_eq!( edits, diff --git a/crates/ruff_linter/src/preview.rs b/crates/ruff_linter/src/preview.rs index 7de6ef4411..853063ec23 100644 --- a/crates/ruff_linter/src/preview.rs +++ b/crates/ruff_linter/src/preview.rs @@ -56,6 +56,11 @@ pub(crate) const fn is_fix_f_string_logging_enabled(settings: &LinterSettings) - settings.preview.is_enabled() } +// https://github.com/astral-sh/ruff/pull/27201 +pub(crate) const fn is_fix_pytest_composite_assertion_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() @@ -211,11 +216,6 @@ pub(crate) const fn is_allow_nested_roots_enabled(settings: &LinterSettings) -> settings.preview.is_enabled() } -// https://github.com/astral-sh/ruff/pull/20659 -pub(crate) const fn is_future_required_preview_generics_enabled(settings: &LinterSettings) -> bool { - settings.preview.is_enabled() -} - // https://github.com/astral-sh/ruff/pull/20169 pub(crate) const fn is_fix_builtin_open_enabled(settings: &LinterSettings) -> bool { settings.preview.is_enabled() @@ -245,37 +245,11 @@ pub(crate) const fn is_b006_unsafe_fix_preserve_assignment_expr_enabled( settings.preview.is_enabled() } -pub(crate) const fn is_typing_extensions_str_alias_enabled(settings: &LinterSettings) -> bool { - settings.preview.is_enabled() -} - -// https://github.com/astral-sh/ruff/pull/19045 -pub(crate) const fn is_extended_i18n_function_matching_enabled(settings: &LinterSettings) -> bool { - settings.preview.is_enabled() -} - -// https://github.com/astral-sh/ruff/pull/21374 -pub(crate) const fn is_extended_snmp_api_path_detection_enabled(settings: &LinterSettings) -> bool { - settings.preview.is_enabled() -} - // https://github.com/astral-sh/ruff/pull/21395 pub(crate) const fn is_enumerate_for_loop_int_index_enabled(settings: &LinterSettings) -> bool { settings.preview.is_enabled() } -// https://github.com/astral-sh/ruff/pull/21469 -pub(crate) const fn is_s310_resolve_string_literal_bindings_enabled( - settings: &LinterSettings, -) -> bool { - settings.preview.is_enabled() -} - -// https://github.com/astral-sh/ruff/pull/22057 -pub(crate) const fn is_ble001_exc_info_suppression_enabled(settings: &LinterSettings) -> bool { - settings.preview.is_enabled() -} - // https://github.com/astral-sh/ruff/pull/22419 pub(crate) const fn is_py315_support_enabled(settings: &LinterSettings) -> bool { settings.preview.is_enabled() @@ -356,17 +330,14 @@ pub(crate) const fn is_collapsible_if_fix_safe_enabled(settings: &LinterSettings settings.preview.is_enabled() } -// https://github.com/astral-sh/ruff/pull/23404 -pub(crate) const fn is_ruff_ignore_enabled(settings: &LinterSettings) -> bool { - settings.preview.is_enabled() -} - // https://github.com/astral-sh/ruff/pull/23259 pub(crate) const fn is_pep604_future_annotations_fix_enabled(settings: &LinterSettings) -> bool { settings.preview.is_enabled() } // https://github.com/astral-sh/ruff/pull/25614 +// TODO(brent) Remove ecosystem selector normalization when stabilizing human-readable rule names: +// https://github.com/astral-sh/ruff/pull/27158 pub const fn is_human_readable_names_enabled(preview: PreviewMode) -> bool { preview.is_enabled() } diff --git a/crates/ruff_linter/src/registry.rs b/crates/ruff_linter/src/registry.rs index 6d8a20fe31..dec68b2e80 100644 --- a/crates/ruff_linter/src/registry.rs +++ b/crates/ruff_linter/src/registry.rs @@ -12,10 +12,6 @@ use crate::codes::{self}; mod rule_set; -pub trait AsRule { - fn rule(&self) -> Rule; -} - impl Rule { pub fn from_code(code: &str) -> Result { let (linter, code) = Linter::parse_code(code).ok_or(FromCodeError::Unknown)?; diff --git a/crates/ruff_linter/src/registry/rule_set.rs b/crates/ruff_linter/src/registry/rule_set.rs index de625379c8..bd785d463e 100644 --- a/crates/ruff_linter/src/registry/rule_set.rs +++ b/crates/ruff_linter/src/registry/rule_set.rs @@ -24,10 +24,6 @@ impl RuleSet { Self(Self::EMPTY) } - pub fn clear(&mut self) { - self.0 = Self::EMPTY; - } - #[inline] pub const fn from_rule(rule: Rule) -> Self { let rule = rule as u16; @@ -257,7 +253,7 @@ impl RuleSet { /// Returns `true` if any of the rules in `rules` are in this set. #[inline] - pub const fn any(&self, rules: &[Rule]) -> bool { + pub(crate) const fn any(&self, rules: &[Rule]) -> bool { let mut any = false; let mut i = 0; diff --git a/crates/ruff_linter/src/rule_selector.rs b/crates/ruff_linter/src/rule_selector.rs index 70b6303c8d..1205af1a6b 100644 --- a/crates/ruff_linter/src/rule_selector.rs +++ b/crates/ruff_linter/src/rule_selector.rs @@ -106,6 +106,7 @@ impl std::fmt::Display for RuleResolutionError { ValueSource::File(path) => format_args!("`{}`", path.as_path()), ValueSource::Cli => format_args!("the CLI"), ValueSource::Editor => format_args!("the editor configuration"), + ValueSource::UvWorkspace => format_args!("uv workspace metadata"), }; match kind { RuleResolutionErrorKind::Removed => { @@ -437,7 +438,8 @@ impl RuleSelector { } /// Parse [`RuleSelector`] from a string; but do not follow redirects. - pub fn parse_no_redirect(s: &str) -> Result { + #[cfg(feature = "schemars")] + fn parse_no_redirect(s: &str) -> Result { // **Changes should be reflected in `from_str` as well** match s { "ALL" => Ok(Self::All), diff --git a/crates/ruff_linter/src/rules/airflow/helpers.rs b/crates/ruff_linter/src/rules/airflow/helpers.rs index d16587c374..b2b0d554bb 100644 --- a/crates/ruff_linter/src/rules/airflow/helpers.rs +++ b/crates/ruff_linter/src/rules/airflow/helpers.rs @@ -204,7 +204,7 @@ pub(crate) fn is_airflow_builtin_or_provider( } /// Return the [`ast::ExprName`] at the head of the expression, if any. -pub(crate) fn match_head(value: &Expr) -> Option<&ExprName> { +fn match_head(value: &Expr) -> Option<&ExprName> { match value { Expr::Attribute(ExprAttribute { value, .. }) => value.as_name_expr(), Expr::Name(name) => Some(name), diff --git a/crates/ruff_linter/src/rules/airflow/rules/function_signature_change_in_3.rs b/crates/ruff_linter/src/rules/airflow/rules/function_signature_change_in_3.rs index 7551133336..967ebbb101 100644 --- a/crates/ruff_linter/src/rules/airflow/rules/function_signature_change_in_3.rs +++ b/crates/ruff_linter/src/rules/airflow/rules/function_signature_change_in_3.rs @@ -36,7 +36,7 @@ use ruff_text_size::Ranged; /// collector.create_asset(uri="s3://bucket/key") /// ``` #[derive(ViolationMetadata)] -#[violation_metadata(preview_since = "0.14.11")] +#[violation_metadata(stable_since = "0.16.0")] pub(crate) struct Airflow3IncompatibleFunctionSignature { function_name: String, change: FunctionSignatureChange, diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR003_AIR003.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR003_AIR003.py.snap index 20e25fad0f..b8321a380a 100644 --- a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR003_AIR003.py.snap +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR003_AIR003.py.snap @@ -52,5 +52,4 @@ AIR003 `Variable.get()` outside of a task 28 | def helper(): 29 | return Variable.get("foo") # AIR003 | ^^^^^^^^^^^^^^^^^^^ - | help: Move into a `@task`-decorated function diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR003_AIR003_dag_decorator.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR003_AIR003_dag_decorator.py.snap index a58f3e2330..4c0280bddd 100644 --- a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR003_AIR003_dag_decorator.py.snap +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR003_AIR003_dag_decorator.py.snap @@ -8,5 +8,4 @@ AIR003 `Variable.get()` outside of a task 3 | 4 | var = Variable.get("foo") # AIR003 | ^^^^^^^^^^^^^^^^^^^ - | help: Use Jinja templates instead diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR004_AIR004.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR004_AIR004.py.snap index 0fe49748fc..1523e673a5 100644 --- a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR004_AIR004.py.snap +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR004_AIR004.py.snap @@ -12,7 +12,6 @@ AIR004 `@task.branch` can be replaced with `@task.short_circuit` 17 | | return ["my_downstream_task"] 18 | | return [] | |_____________^ - | AIR004 `@task.branch` can be replaced with `@task.short_circuit` --> AIR004.py:21:1 @@ -25,7 +24,6 @@ AIR004 `@task.branch` can be replaced with `@task.short_circuit` 26 | | return ["another_downstream_task"] 27 | | return [] | |_____________^ - | AIR004 `@task.branch` can be replaced with `@task.short_circuit` --> AIR004.py:30:1 @@ -38,7 +36,6 @@ AIR004 `@task.branch` can be replaced with `@task.short_circuit` 35 | | return ["downstream_task"] 36 | | return [] | |_____________^ - | AIR004 `@task.branch` can be replaced with `@task.short_circuit` --> AIR004.py:39:1 @@ -49,7 +46,6 @@ AIR004 `@task.branch` can be replaced with `@task.short_circuit` 42 | | return ["downstream_task"] 43 | | return [] | |_____________^ - | AIR004 `@task.branch` can be replaced with `@task.short_circuit` --> AIR004.py:46:1 @@ -60,7 +56,6 @@ AIR004 `@task.branch` can be replaced with `@task.short_circuit` 49 | | return ["downstream_task"] 50 | | return | |__________^ - | AIR004 `@task.branch` can be replaced with `@task.short_circuit` --> AIR004.py:53:1 @@ -71,14 +66,12 @@ AIR004 `@task.branch` can be replaced with `@task.short_circuit` 56 | | return ["downstream_task"] 57 | | return None | |_______________^ - | AIR004 `BranchPythonOperator` can be replaced with `ShortCircuitOperator` --> AIR004.py:107:1 | 107 | BranchPythonOperator(task_id="task", python_callable=operator_short_circuit_candidate) # AIR004 | ^^^^^^^^^^^^^^^^^^^^ - | AIR004 `BranchPythonOperator` can be replaced with `ShortCircuitOperator` --> AIR004.py:116:1 diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR004_AIR004_sdk.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR004_AIR004_sdk.py.snap index c70514b667..f22558042d 100644 --- a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR004_AIR004_sdk.py.snap +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR004_AIR004_sdk.py.snap @@ -12,4 +12,3 @@ AIR004 `@task.branch` can be replaced with `@task.short_circuit` 12 | | return ["my_downstream_task"] 13 | | return [] | |_____________^ - | 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 81fc67e2b0..ec4d7fd6f7 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 @@ -280,7 +280,6 @@ AIR301 `appbuilder` is removed in Airflow 3.0 107 | 108 | FabAuthManager(None) | ^^^^^^ - | help: The constructor takes no parameter now AIR301 [*] `default_var` is removed in Airflow 3.0 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 c7a4542400..512903692d 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 @@ -599,4 +599,3 @@ AIR301 `create_dagrun` is removed in Airflow 3.0 115 | test_dag = DAG(dag_id="test_dag") 116 | test_dag.create_dagrun() | ^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_context.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_context.py.snap index ffa9195f21..ade9431e79 100644 --- a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_context.py.snap +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_context.py.snap @@ -18,7 +18,6 @@ AIR301 `conf` is removed in Airflow 3.0 22 | print("access invalid key", context["conf"]) 23 | print("access invalid key", context.get("conf")) | ^^^^^^ - | AIR301 `execution_date` is removed in Airflow 3.0 --> AIR301_context.py:28:5 @@ -49,7 +48,6 @@ AIR301 `conf` is removed in Airflow 3.0 30 | print("execution date", execution_date) 31 | print("access invalid key", context.get("conf")) | ^^^^^^ - | AIR301 `execution_date` is removed in Airflow 3.0 --> AIR301_context.py:40:30 @@ -177,7 +175,6 @@ AIR301 [*] `triggering_dataset_events` is removed in Airflow 3.0 50 | yesterday_ds_nodash = context["yesterday_ds_nodash"] 51 | events = context["triggering_dataset_events"] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `triggering_asset_events` instead | 50 | yesterday_ds_nodash = context["yesterday_ds_nodash"] @@ -313,7 +310,6 @@ AIR301 [*] `triggering_dataset_events` is removed in Airflow 3.0 67 | yesterday_ds_nodash = context["yesterday_ds_nodash"] 68 | events = context["triggering_dataset_events"] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `triggering_asset_events` instead | 67 | yesterday_ds_nodash = context["yesterday_ds_nodash"] @@ -340,7 +336,6 @@ AIR301 `execution_date` is removed in Airflow 3.0 76 | c = get_current_context() 77 | c.get("execution_date") | ^^^^^^^^^^^^^^^^ - | AIR301 `conf` is removed in Airflow 3.0 --> AIR301_context.py:89:49 @@ -492,7 +487,6 @@ AIR301 `inlet_events[""]` is removed in Airflow 3.0 190 | print(context["inlet_events"].get("this://is-url")) 191 | print(context.get("inlet_events").get("this://is-url")) | ^^^^^^^^^^^^^^^ - | help: Accessing `inlet_events` via a string key is deprecated; use `context["inlet_events"][Asset(uri="this://is-url")]` instead of `context["inlet_events"]["this://is-url"]`. AIR301 `inlet_events[""]` is removed in Airflow 3.0 @@ -513,7 +507,6 @@ AIR301 `inlet_events[""]` is removed in Airflow 3.0 197 | print(inlet_events["this://is-url"]) 198 | print(inlet_events.get("this://is-url")) | ^^^^^^^^^^^^^^^ - | help: Accessing `inlet_events` via a string key is deprecated; use `context["inlet_events"][Asset(uri="this://is-url")]` instead of `context["inlet_events"]["this://is-url"]`. AIR301 `execution_date` is removed in Airflow 3.0 @@ -533,7 +526,6 @@ AIR301 `next_ds` is removed in Airflow 3.0 216 | execution_date = context["execution_date"] 217 | next_ds = context["next_ds"] | ^^^^^^^^^ - | AIR301 `execution_date` is removed in Airflow 3.0 --> AIR301_context.py:228:30 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 9f0bbd9a01..98efab280f 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 @@ -95,7 +95,6 @@ AIR301 `airflow.contrib.aws_athena_hook.AWSAthenaHook` is removed in Airflow 3.0 43 | # airflow.contrib.* 44 | AWSAthenaHook() | ^^^^^^^^^^^^^ - | help: The whole `airflow.contrib` module has been removed. AIR301 `airflow.datasets.DatasetAliasEvent` is removed in Airflow 3.0 @@ -114,7 +113,6 @@ AIR301 `airflow.datasets.DatasetEvent` is removed in Airflow 3.0 48 | DatasetAliasEvent() 49 | DatasetEvent() | ^^^^^^^^^^^^ - | help: `DatasetEvent` has been made private in Airflow 3. Use `dict[str, Any]` for the time being. An `AssetEvent` type will be added to the apache-airflow-task-sdk in a future version. AIR301 `airflow.operators.subdag.SubDagOperator` is removed in Airflow 3.0 @@ -240,7 +238,6 @@ AIR301 `airflow.utils.dag_cycle_tester.test_cycle` is removed in Airflow 3.0 81 | # airflow.utils.dag_cycle_tester 82 | test_cycle | ^^^^^^^^^^ - | AIR301 `airflow.utils.db.create_session` is removed in Airflow 3.0 --> AIR301_names.py:86:1 @@ -248,7 +245,6 @@ AIR301 `airflow.utils.db.create_session` is removed in Airflow 3.0 85 | # airflow.utils.db 86 | create_session | ^^^^^^^^^^^^^^ - | AIR301 `airflow.utils.file.mkdirs` is removed in Airflow 3.0 --> AIR301_names.py:90:1 @@ -256,7 +252,6 @@ AIR301 `airflow.utils.file.mkdirs` is removed in Airflow 3.0 89 | # airflow.utils.file 90 | mkdirs | ^^^^^^ - | help: Use `pathlib.Path({path}).mkdir` instead AIR301 `airflow.utils.state.SHUTDOWN` is removed in Airflow 3.0 @@ -285,7 +280,6 @@ AIR301 `airflow.utils.trigger_rule.TriggerRule.DUMMY` is removed in Airflow 3.0 97 | # airflow.utils.trigger_rule 98 | TriggerRule.DUMMY | ^^^^^^^^^^^^^^^^^ - | AIR301 `airflow.www.auth.has_access` is removed in Airflow 3.0 --> AIR301_names.py:102:1 @@ -323,4 +317,3 @@ AIR301 `airflow.www.utils.should_hide_value_for_key` is removed in Airflow 3.0 106 | get_sensitive_variables_fields 107 | should_hide_value_for_key | ^^^^^^^^^^^^^^^^^^^^^^^^^ - | 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 index d30128fa17..213b8d7830 100644 --- 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 @@ -204,7 +204,6 @@ AIR301 [*] `airflow.security.permissions.RESOURCE_DATASET` is removed in Airflow 31 | 32 | RESOURCE_DATASET | ^^^^^^^^^^^^^^^^ - | help: Use `RESOURCE_ASSET` from `airflow.security.permissions` instead. | 14 | from airflow.secrets.local_filesystem import load_connections @@ -243,7 +242,6 @@ AIR301 [*] `airflow.listeners.spec.dataset.on_dataset_changed` is removed in Air 40 | on_dataset_created() 41 | on_dataset_changed() | ^^^^^^^^^^^^^^^^^^ - | help: Use `on_asset_changed` from `airflow.listeners.spec.asset` instead. | 38 | ) @@ -357,7 +355,6 @@ AIR301 [*] `airflow.auth.managers.base_auth_manager.BaseAuthManager` is removed 71 | 72 | BaseAuthManager() | ^^^^^^^^^^^^^^^ - | help: Use `BaseAuthManager` from `airflow.api_fastapi.auth.managers.base_auth_manager` instead. | 69 | # airflow.auth.manager @@ -650,7 +647,6 @@ AIR301 [*] `airflow.utils.log.secrets_masker` is removed in Airflow 3.0 113 | # airflow.utils.log 114 | secrets_masker | ^^^^^^^^^^^^^^ - | help: `secrets_masker` has been moved to `airflow.sdk.execution_time` since Airflow 3.0 (with apache-airflow-task-sdk>=1.0.0). | 110 | 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 index f671d0ce6e..48d2bd2658 100644 --- 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 @@ -189,7 +189,6 @@ AIR301 [*] `airflow.providers.google.datasets.gcs.convert_dataset_to_openlineage 53 | gcs_create_dataset() 54 | gcs_convert_dataset_to_openlineage() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `convert_asset_to_openlineage` from `airflow.providers.google.assets.gcs` instead. | 51 | from airflow.providers.google.datasets.gcs import create_dataset as gcs_create_dataset 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 index b9245f8206..b270328a92 100644 --- 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 @@ -211,7 +211,6 @@ AIR302 [*] `airflow.operators.s3_to_redshift_operator.S3ToRedshiftTransfer` is m 33 | 34 | S3ToRedshiftTransfer() | ^^^^^^^^^^^^^^^^^^^^ - | help: Install `apache-airflow-providers-amazon>=1.0.0` and use `S3ToRedshiftOperator` from `airflow.providers.amazon.aws.transfers.s3_to_redshift` instead. | 32 | from airflow.operators.s3_to_redshift_operator import 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 index 9e375bcf51..5fa835ca2e 100644 --- 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 @@ -49,7 +49,6 @@ AIR302 [*] `airflow.executors.celery_executor.CeleryExecutor` is moved into `cel 11 | app 12 | CeleryExecutor() | ^^^^^^^^^^^^^^ - | help: Install `apache-airflow-providers-celery>=3.3.0` and use `CeleryExecutor` from `airflow.providers.celery.executors.celery_executor` instead. | 4 | from airflow.executors.celery_executor import ( 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 index 64d24f05a3..1eb3378472 100644 --- 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 @@ -65,7 +65,6 @@ AIR302 [*] `airflow.operators.check_operator.SQLCheckOperator` is moved into `co 14 | DbApiHook() 15 | SQLCheckOperator() | ^^^^^^^^^^^^^^^^ - | help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `SQLCheckOperator` from `airflow.providers.common.sql.operators.sql` instead. | 11 | from airflow.hooks.dbapi_hook import DbApiHook @@ -99,7 +98,6 @@ AIR302 [*] `airflow.operators.check_operator.CheckOperator` is moved into `commo 21 | SQLCheckOperator() 22 | CheckOperator() | ^^^^^^^^^^^^^ - | help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `SQLCheckOperator` from `airflow.providers.common.sql.operators.sql` instead. | 19 | from airflow.operators.sql import SQLCheckOperator @@ -115,7 +113,6 @@ AIR302 [*] `airflow.operators.druid_check_operator.CheckOperator` is moved into 26 | 27 | CheckOperator() | ^^^^^^^^^^^^^ - | help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `SQLCheckOperator` from `airflow.providers.common.sql.operators.sql` instead. | 25 | from airflow.operators.druid_check_operator import CheckOperator @@ -131,7 +128,6 @@ AIR302 [*] `airflow.operators.presto_check_operator.CheckOperator` is moved into 31 | 32 | CheckOperator() | ^^^^^^^^^^^^^ - | help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `SQLCheckOperator` from `airflow.providers.common.sql.operators.sql` instead. | 30 | from airflow.operators.presto_check_operator import CheckOperator @@ -203,7 +199,6 @@ AIR302 [*] `airflow.operators.check_operator.SQLIntervalCheckOperator` is moved 44 | IntervalCheckOperator() 45 | SQLIntervalCheckOperator() | ^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `SQLIntervalCheckOperator` from `airflow.providers.common.sql.operators.sql` instead. | 36 | IntervalCheckOperator, @@ -258,7 +253,6 @@ AIR302 [*] `airflow.operators.presto_check_operator.PrestoIntervalCheckOperator` 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. | 52 | from airflow.operators.sql import SQLIntervalCheckOperator @@ -293,7 +287,6 @@ AIR302 [*] `airflow.operators.check_operator.ThresholdCheckOperator` is moved in 64 | SQLThresholdCheckOperator() 65 | ThresholdCheckOperator() | ^^^^^^^^^^^^^^^^^^^^^^ - | help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `SQLThresholdCheckOperator` from `airflow.providers.common.sql.operators.sql` instead. | 59 | from airflow.operators.check_operator import ( @@ -312,7 +305,6 @@ AIR302 [*] `airflow.operators.sql.SQLThresholdCheckOperator` is moved into `comm 69 | 70 | SQLThresholdCheckOperator() | ^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `SQLThresholdCheckOperator` from `airflow.providers.common.sql.operators.sql` instead. | 67 | @@ -348,7 +340,6 @@ AIR302 [*] `airflow.operators.check_operator.ValueCheckOperator` is moved into ` 78 | SQLValueCheckOperator() 79 | ValueCheckOperator() | ^^^^^^^^^^^^^^^^^^ - | help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `SQLValueCheckOperator` from `airflow.providers.common.sql.operators.sql` instead. | 73 | from airflow.operators.check_operator import ( @@ -402,7 +393,6 @@ AIR302 [*] `airflow.operators.presto_check_operator.PrestoValueCheckOperator` is 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. | 86 | from airflow.operators.sql import SQLValueCheckOperator @@ -526,7 +516,6 @@ AIR302 [*] `airflow.operators.sql.parse_boolean` is moved into `common-sql` prov 106 | _convert_to_float_if_possible() 107 | parse_boolean() | ^^^^^^^^^^^^^ - | help: Install `apache-airflow-providers-common-sql>=1.0.0` and use `parse_boolean` from `airflow.providers.common.sql.operators.sql` instead. | 98 | _convert_to_float_if_possible, @@ -544,7 +533,6 @@ AIR302 [*] `airflow.sensors.sql.SqlSensor` is moved into `common-sql` provider i 111 | 112 | SqlSensor() | ^^^^^^^^^ - | help: Install `apache-airflow-providers-common-sql>=1.0.0` and use `SqlSensor` from `airflow.providers.common.sql.sensors.sql` instead. | 109 | @@ -561,7 +549,6 @@ AIR302 [*] `airflow.sensors.sql_sensor.SqlSensor` is moved into `common-sql` pro 116 | 117 | SqlSensor() | ^^^^^^^^^ - | help: Install `apache-airflow-providers-common-sql>=1.0.0` and use `SqlSensor` from `airflow.providers.common.sql.sensors.sql` instead. | 114 | 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 index 144db5f91f..5c0ee422ed 100644 --- 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 @@ -8,7 +8,6 @@ AIR302 [*] `airflow.executors.dask_executor.DaskExecutor` is moved into `daskexe 4 | 5 | DaskExecutor() | ^^^^^^^^^^^^ - | help: Install `apache-airflow-providers-daskexecutor>=1.0.0` and use `DaskExecutor` from `airflow.providers.daskexecutor.executors.dask_executor` instead. | 2 | 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 index d1690eaf83..9558e07336 100644 --- 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 @@ -69,7 +69,6 @@ AIR302 [*] `airflow.operators.hive_to_druid.HiveToDruidTransfer` is moved into ` 15 | HiveToDruidOperator() 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. | 7 | from airflow.operators.hive_to_druid import ( 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 index 6cbb31d782..77b7ae496e 100644 --- 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 @@ -366,7 +366,6 @@ AIR302 [*] `airflow.www.security.FabAirflowSecurityManagerOverride` is moved int 54 | 55 | FabAirflowSecurityManagerOverride() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Install `apache-airflow-providers-fab>=1.0.0` and use `FabAirflowSecurityManagerOverride` from `airflow.providers.fab.auth_manager.security_manager.override` instead. | 52 | 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 index 6c429ec90e..d0b17d61c4 100644 --- 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 @@ -26,7 +26,6 @@ AIR302 [*] `airflow.sensors.web_hdfs_sensor.WebHdfsSensor` is moved into `apache 6 | WebHDFSHook() 7 | WebHdfsSensor() | ^^^^^^^^^^^^^ - | help: Install `apache-airflow-providers-apache-hdfs>=1.0.0` and use `WebHdfsSensor` from `airflow.providers.apache.hdfs.sensors.web_hdfs` instead. | 3 | from airflow.hooks.webhdfs_hook import WebHDFSHook 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 index 9410bd64bc..9ea949fabf 100644 --- 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 @@ -197,7 +197,6 @@ AIR302 [*] `airflow.operators.hive_to_samba_operator.HiveToSambaOperator` is mov 28 | HiveToMySqlOperator() 29 | HiveToSambaOperator() | ^^^^^^^^^^^^^^^^^^^ - | help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `HiveToSambaOperator` from `airflow.providers.apache.hive.transfers.hive_to_samba` instead. | 15 | from airflow.operators.hive_to_mysql import HiveToMySqlOperator @@ -381,7 +380,6 @@ AIR302 [*] `airflow.sensors.named_hive_partition_sensor.NamedHivePartitionSensor 69 | 70 | NamedHivePartitionSensor() | ^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `NamedHivePartitionSensor` from `airflow.providers.apache.hive.sensors.named_hive_partition` instead. | 67 | 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 index 9498a928da..e14dae880c 100644 --- 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 @@ -48,7 +48,6 @@ AIR302 [*] `airflow.sensors.http_sensor.HttpSensor` is moved into `http` provide 8 | SimpleHttpOperator() 9 | HttpSensor() | ^^^^^^^^^^ - | help: Install `apache-airflow-providers-http>=1.0.0` and use `HttpSensor` from `airflow.providers.http.sensors.http` instead. | 4 | from airflow.operators.http_operator import 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 index 08e36e02b7..d488b793b5 100644 --- 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 @@ -27,7 +27,6 @@ AIR302 [*] `airflow.hooks.jdbc_hook.jaydebeapi` is moved into `jdbc` provider in 8 | JdbcHook() 9 | jaydebeapi() | ^^^^^^^^^^ - | help: Install `apache-airflow-providers-jdbc>=1.0.0` and use `jaydebeapi` from `airflow.providers.jdbc.hooks.jdbc` instead. | 4 | JdbcHook, 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 index d2d0d8696d..749fc444ff 100644 --- 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 @@ -197,7 +197,6 @@ AIR302 [*] `airflow.kubernetes.kubernetes_helper_functions.create_pod_id` is mov 33 | annotations_for_logging_task_metadata() 34 | create_pod_id() | ^^^^^^^^^^^^^ - | help: Install `apache-airflow-providers-cncf-kubernetes>=10.0.0` and use `create_unique_id` from `airflow.providers.cncf.kubernetes.kubernetes_helper_functions` instead. | 20 | ) @@ -772,7 +771,6 @@ AIR302 [*] `airflow.kubernetes.pod_generator.PodGeneratorDeprecated` is moved in 112 | 113 | PodGeneratorDeprecated() | ^^^^^^^^^^^^^^^^^^^^^^ - | help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `PodGenerator` from `airflow.providers.cncf.kubernetes.pod_generator` instead. | 111 | from airflow.kubernetes.pod_generator import 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 index 1d5624472f..0e41600906 100644 --- 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 @@ -50,7 +50,6 @@ AIR302 [*] `airflow.operators.presto_to_mysql.PrestoToMySqlTransfer` is moved in 10 | PrestoToMySqlOperator() 11 | PrestoToMySqlTransfer() | ^^^^^^^^^^^^^^^^^^^^^ - | help: Install `apache-airflow-providers-mysql>=1.0.0` and use `PrestoToMySqlOperator` from `airflow.providers.mysql.transfers.presto_to_mysql` instead. | 4 | from airflow.operators.presto_to_mysql import ( 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 index 40cfc36f94..27249b48d9 100644 --- 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 @@ -8,7 +8,6 @@ AIR302 [*] `airflow.hooks.oracle_hook.OracleHook` is moved into `oracle` provide 4 | 5 | OracleHook() | ^^^^^^^^^^ - | help: Install `apache-airflow-providers-oracle>=1.0.0` and use `OracleHook` from `airflow.providers.oracle.hooks.oracle` instead. | 2 | 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 index 3cadf1edd5..6afd5f958b 100644 --- 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 @@ -8,7 +8,6 @@ AIR302 [*] `airflow.operators.papermill_operator.PapermillOperator` is moved int 4 | 5 | PapermillOperator() | ^^^^^^^^^^^^^^^^^ - | help: Install `apache-airflow-providers-papermill>=1.0.0` and use `PapermillOperator` from `airflow.providers.papermill.operators.papermill` instead. | 2 | 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 index 9e0c48a9ed..d69d3f9223 100644 --- 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 @@ -26,7 +26,6 @@ AIR302 [*] `airflow.operators.pig_operator.PigOperator` is moved into `apache-pi 6 | PigCliHook() 7 | PigOperator() | ^^^^^^^^^^^ - | help: Install `apache-airflow-providers-apache-pig>=1.0.0` and use `PigOperator` from `airflow.providers.apache.pig.operators.pig` instead. | 3 | from airflow.hooks.pig_hook import PigCliHook 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 index 8032b0cec6..15790d11dd 100644 --- 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 @@ -8,7 +8,6 @@ AIR302 [*] `airflow.hooks.presto_hook.PrestoHook` is moved into `presto` provide 4 | 5 | PrestoHook() | ^^^^^^^^^^ - | help: Install `apache-airflow-providers-presto>=1.0.0` and use `PrestoHook` from `airflow.providers.presto.hooks.presto` instead. | 2 | 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 index 968db8e7f6..57326616fc 100644 --- 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 @@ -8,7 +8,6 @@ AIR302 [*] `airflow.hooks.samba_hook.SambaHook` is moved into `samba` provider i 4 | 5 | SambaHook() | ^^^^^^^^^ - | help: Install `apache-airflow-providers-samba>=1.0.0` and use `SambaHook` from `airflow.providers.samba.hooks.samba` instead. | 2 | 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 index 84da9319a1..8ffef8eb4b 100644 --- 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 @@ -46,7 +46,6 @@ AIR302 [*] `airflow.operators.slack_operator.SlackAPIPostOperator` is moved into 7 | SlackAPIOperator() 8 | SlackAPIPostOperator() | ^^^^^^^^^^^^^^^^^^^^ - | help: Install `apache-airflow-providers-slack>=1.0.0` and use `SlackAPIPostOperator` from `airflow.providers.slack.operators.slack` instead. | 3 | from airflow.hooks.slack_hook import SlackHook 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 index 8ee9e7dddb..dbb6eb29f6 100644 --- 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 @@ -27,7 +27,6 @@ AIR302 [*] `airflow.operators.email.EmailOperator` is moved into `smtp` provider 8 | 9 | EmailOperator() | ^^^^^^^^^^^^^ - | help: Install `apache-airflow-providers-smtp>=1.0.0` and use `EmailOperator` from `airflow.providers.smtp.operators.smtp` instead. | 6 | 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 index 26af38354a..9d054905dc 100644 --- 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 @@ -8,7 +8,6 @@ AIR302 [*] `airflow.hooks.sqlite_hook.SqliteHook` is moved into `sqlite` provide 4 | 5 | SqliteHook() | ^^^^^^^^^^ - | help: Install `apache-airflow-providers-sqlite>=1.0.0` and use `SqliteHook` from `airflow.providers.sqlite.hooks.sqlite` instead. | 2 | 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 index 0dc11bac45..6490009535 100644 --- 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 @@ -199,7 +199,6 @@ AIR302 [*] `airflow.sensors.external_task_sensor.ExternalTaskSensor` is moved in 32 | ExternalTaskMarker() 33 | ExternalTaskSensor() | ^^^^^^^^^^^^^^^^^^ - | help: Install `apache-airflow-providers-standard>=0.0.3` and use `ExternalTaskSensor` from `airflow.providers.standard.sensors.external_task` instead. | 16 | ExternalTaskMarker, @@ -408,7 +407,6 @@ AIR302 [*] `airflow.sensors.external_task_sensor.ExternalTaskSensorLink` is move 77 | 78 | ExternalTaskSensorLink() | ^^^^^^^^^^^^^^^^^^^^^^ - | help: Install `apache-airflow-providers-standard>=0.0.3` and use `ExternalDagLink` from `airflow.providers.standard.sensors.external_task` instead. | 76 | from airflow.sensors.external_task_sensor import ExternalTaskSensorLink 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 index 0000a16ccd..ebabfa0efb 100644 --- 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 @@ -8,7 +8,6 @@ AIR302 [*] `airflow.hooks.zendesk_hook.ZendeskHook` is moved into `zendesk` prov 4 | 5 | ZendeskHook() | ^^^^^^^^^^^ - | help: Install `apache-airflow-providers-zendesk>=1.0.0` and use `ZendeskHook` from `airflow.providers.zendesk.hooks.zendesk` instead. | 2 | diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR304_AIR304.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR304_AIR304.py.snap index df8750b04d..9385b6ab8e 100644 --- a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR304_AIR304.py.snap +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR304_AIR304.py.snap @@ -198,7 +198,6 @@ AIR304 `datetime.now()` produces a value that changes at runtime; using it in a 42 | # Walrus operator 43 | DAG(dag_id="s", start_date=(x := datetime.now())) | ^^^^^^^^^^^^^^ - | AIR304 `pendulum.now()` produces a value that changes at runtime; using it in a Dag or task argument causes infinite Dag version creation --> AIR304.py:46:17 @@ -225,7 +224,6 @@ AIR304 `datetime.now()` produces a value that changes at runtime; using it in a 52 | 53 | PythonSensor(task_id="s", start_date=datetime.now()) | ^^^^^^^^^^^^^^ - | AIR304 `datetime.utcnow()` produces a value that changes at runtime; using it in a Dag or task argument causes infinite Dag version creation --> AIR304.py:56:18 diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR311_AIR311_args.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR311_AIR311_args.py.snap index 6d916e1516..970495b1c0 100644 --- a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR311_AIR311_args.py.snap +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR311_AIR311_args.py.snap @@ -6,7 +6,6 @@ AIR311 [*] `airflow.DAG` is removed in Airflow 3.0; It still works in Airflow 3. | 13 | DAG(dag_id="class_sla_callback", sla_miss_callback=sla_callback) | ^^^ - | help: `DAG` has been moved to `airflow.sdk` since Airflow 3.0 (with apache-airflow-task-sdk>=1.0.0). | 4 | @@ -23,7 +22,6 @@ AIR311 `sla_miss_callback` is removed in Airflow 3.0; It still works in Airflow | 13 | DAG(dag_id="class_sla_callback", sla_miss_callback=sla_callback) | ^^^^^^^^^^^^^^^^^ - | AIR311 `sla_miss_callback` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. --> AIR311_args.py:16:6 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 54ebd0c1c8..fc73e40a2b 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 @@ -616,7 +616,6 @@ AIR311 [*] `airflow.decorators.base.task_decorator_factory` is removed in Airflo 90 | get_unique_task_id() 91 | task_decorator_factory() | ^^^^^^^^^^^^^^^^^^^^^^ - | help: `task_decorator_factory` has been moved to `airflow.sdk.bases.decorator` since Airflow 3.0 (with apache-airflow-task-sdk>=1.0.0). | 82 | get_unique_task_id, @@ -672,7 +671,6 @@ AIR311 [*] `airflow.models.ParamsDict` is removed in Airflow 3.0; It still works 98 | DagParam() 99 | ParamsDict() | ^^^^^^^^^^ - | help: `ParamsDict` has been moved to `airflow.sdk.definitions.param` since Airflow 3.0 (with apache-airflow-task-sdk>=1.0.0). | 93 | @@ -728,7 +726,6 @@ AIR311 [*] `airflow.models.param.ParamsDict` is removed in Airflow 3.0; It still 106 | DagParam() 107 | ParamsDict() | ^^^^^^^^^^ - | help: `ParamsDict` has been moved to `airflow.sdk.definitions.param` since Airflow 3.0 (with apache-airflow-task-sdk>=1.0.0). | 101 | @@ -787,7 +784,6 @@ AIR311 [*] `airflow.sensors.base.poke_mode_only` is removed in Airflow 3.0; It s 118 | PokeReturnValue() 119 | poke_mode_only() | ^^^^^^^^^^^^^^ - | help: `poke_mode_only` has been moved to `airflow.sdk.bases.sensor` since Airflow 3.0 (with apache-airflow-task-sdk>=1.0.0). | 112 | PokeReturnValue, 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 c76077d3da..2fefab2699 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 @@ -703,7 +703,6 @@ AIR312 [*] `airflow.triggers.temporal.TimeDeltaTrigger` is deprecated and moved 88 | DateTimeTrigger() 89 | TimeDeltaTrigger() | ^^^^^^^^^^^^^^^^ - | help: Install `apache-airflow-providers-standard>=0.0.3` and use `TimeDeltaTrigger` from `airflow.providers.standard.triggers.temporal` instead. | 78 | DateTimeTrigger, diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR321_AIR321_names.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR321_AIR321_names.py.snap index 4b871cd6f2..8f824539cb 100644 --- a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR321_AIR321_names.py.snap +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR321_AIR321_names.py.snap @@ -451,7 +451,6 @@ AIR321 [*] `airflow.hooks.base.BaseHook` is moved in Airflow 3.1 101 | from airflow.hooks.base import BaseHook 102 | BaseHook() | ^^^^^^^^ - | help: `BaseHook` has been moved to `airflow.sdk` since Airflow 3.1 (with apache-airflow-task-sdk>=1.1.0). | 100 | # airflow.hooks diff --git a/crates/ruff_linter/src/rules/basedpython/snapshots/ruff_linter__rules__basedpython__tests__BY003_BY003.by.snap b/crates/ruff_linter/src/rules/basedpython/snapshots/ruff_linter__rules__basedpython__tests__BY003_BY003.by.snap index b665e27075..48849a6801 100644 --- a/crates/ruff_linter/src/rules/basedpython/snapshots/ruff_linter__rules__basedpython__tests__BY003_BY003.by.snap +++ b/crates/ruff_linter/src/rules/basedpython/snapshots/ruff_linter__rules__basedpython__tests__BY003_BY003.by.snap @@ -180,7 +180,6 @@ BY003 [*] `isinstance` call can be written as `is` 22 | _ = isinstance(x, int).__class__ 23 | _ = -isinstance(x, int) | ^^^^^^^^^^^^^^^^^^ - | help: Replace with `is` | 22 | _ = isinstance(x, int).__class__ diff --git a/crates/ruff_linter/src/rules/basedpython/snapshots/ruff_linter__rules__basedpython__tests__BY004_BY004.by.snap b/crates/ruff_linter/src/rules/basedpython/snapshots/ruff_linter__rules__basedpython__tests__BY004_BY004.by.snap index b11700bfea..2259efb27b 100644 --- a/crates/ruff_linter/src/rules/basedpython/snapshots/ruff_linter__rules__basedpython__tests__BY004_BY004.by.snap +++ b/crates/ruff_linter/src/rules/basedpython/snapshots/ruff_linter__rules__basedpython__tests__BY004_BY004.by.snap @@ -42,7 +42,6 @@ BY004 [*] `super()` call can be written as `super` 13 | def attribute(self) -> object: 14 | return super().__class__ | ^^^^^^^ - | help: Remove the parentheses | 13 | def attribute(self) -> object: diff --git a/crates/ruff_linter/src/rules/basedpython/snapshots/ruff_linter__rules__basedpython__tests__BY007_BY007.by.snap b/crates/ruff_linter/src/rules/basedpython/snapshots/ruff_linter__rules__basedpython__tests__BY007_BY007.by.snap index 12d42e3e67..2e3dc32dd2 100644 --- a/crates/ruff_linter/src/rules/basedpython/snapshots/ruff_linter__rules__basedpython__tests__BY007_BY007.by.snap +++ b/crates/ruff_linter/src/rules/basedpython/snapshots/ruff_linter__rules__basedpython__tests__BY007_BY007.by.snap @@ -6,7 +6,6 @@ BY007 [*] `Any` can be written as `dynamic` | 4 | def annotated(payload: Any) -> Any: ... | ^^^ - | help: Replace with `dynamic` | 3 | @@ -20,7 +19,6 @@ BY007 [*] `Any` can be written as `dynamic` | 4 | def annotated(payload: Any) -> Any: ... | ^^^ - | help: Replace with `dynamic` | 3 | diff --git a/crates/ruff_linter/src/rules/basedpython/snapshots/ruff_linter__rules__basedpython__tests__BY009_BY009.by.snap b/crates/ruff_linter/src/rules/basedpython/snapshots/ruff_linter__rules__basedpython__tests__BY009_BY009.by.snap index cd98de790f..3f1f7a8f29 100644 --- a/crates/ruff_linter/src/rules/basedpython/snapshots/ruff_linter__rules__basedpython__tests__BY009_BY009.by.snap +++ b/crates/ruff_linter/src/rules/basedpython/snapshots/ruff_linter__rules__basedpython__tests__BY009_BY009.by.snap @@ -6,7 +6,6 @@ BY009 [*] `Unpack[…]` can be written as `*` | 4 | def f(*args: Unpack[tuple[int, ...]]): ... | ^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `*` | 3 | @@ -20,7 +19,6 @@ BY009 [*] `Unpack[…]` can be written as `*` | 10 | coords: tuple[Unpack[tuple[int, str]]] | ^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `*` | 9 | diff --git a/crates/ruff_linter/src/rules/basedpython/snapshots/ruff_linter__rules__basedpython__tests__BY010_BY010.by.snap b/crates/ruff_linter/src/rules/basedpython/snapshots/ruff_linter__rules__basedpython__tests__BY010_BY010.by.snap index df0c386183..4edf18fe95 100644 --- a/crates/ruff_linter/src/rules/basedpython/snapshots/ruff_linter__rules__basedpython__tests__BY010_BY010.by.snap +++ b/crates/ruff_linter/src/rules/basedpython/snapshots/ruff_linter__rules__basedpython__tests__BY010_BY010.by.snap @@ -24,7 +24,6 @@ BY010 [*] `TypeOf[…]` can be written as `typeof` 5 | a: TypeOf[b] = 1 6 | maybe: TypeOf[b] | None = None | ^^^^^^^^^ - | help: Replace with `typeof` | 5 | a: TypeOf[b] = 1 @@ -38,7 +37,6 @@ BY010 [*] `TypeOf[…]` can be written as `typeof` | 9 | def f(x: TypeOf[b]) -> TypeOf[b]: ... | ^^^^^^^^^ - | help: Replace with `typeof` | 8 | @@ -52,7 +50,6 @@ BY010 [*] `TypeOf[…]` can be written as `typeof` | 9 | def f(x: TypeOf[b]) -> TypeOf[b]: ... | ^^^^^^^^^ - | help: Replace with `typeof` | 8 | diff --git a/crates/ruff_linter/src/rules/basedpython/snapshots/ruff_linter__rules__basedpython__tests__BY011_BY011.by.snap b/crates/ruff_linter/src/rules/basedpython/snapshots/ruff_linter__rules__basedpython__tests__BY011_BY011.by.snap index a60f7378a4..abe1e86d4a 100644 --- a/crates/ruff_linter/src/rules/basedpython/snapshots/ruff_linter__rules__basedpython__tests__BY011_BY011.by.snap +++ b/crates/ruff_linter/src/rules/basedpython/snapshots/ruff_linter__rules__basedpython__tests__BY011_BY011.by.snap @@ -57,7 +57,6 @@ BY011 [*] `import name as name` can be written as `export` 3 | from collections.abc import Sequence as Sequence, Mapping as Mapping 4 | from ..pkg.deep import Thing as Thing | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `export` | 3 | from collections.abc import Sequence as Sequence, Mapping as Mapping diff --git a/crates/ruff_linter/src/rules/basedpython/snapshots/ruff_linter__rules__basedpython__tests__BY012_BY012.by.snap b/crates/ruff_linter/src/rules/basedpython/snapshots/ruff_linter__rules__basedpython__tests__BY012_BY012.by.snap index a12fc616c8..470bdbc009 100644 --- a/crates/ruff_linter/src/rules/basedpython/snapshots/ruff_linter__rules__basedpython__tests__BY012_BY012.by.snap +++ b/crates/ruff_linter/src/rules/basedpython/snapshots/ruff_linter__rules__basedpython__tests__BY012_BY012.by.snap @@ -20,7 +20,6 @@ BY012 [*] `typing` members are implicitly available 1 | from typing import Sequence 2 | from typing import Mapping, Iterator | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove the import | 1 | from typing import Sequence diff --git a/crates/ruff_linter/src/rules/basedpython/snapshots/ruff_linter__rules__basedpython__tests__BY017_BY017.by.snap b/crates/ruff_linter/src/rules/basedpython/snapshots/ruff_linter__rules__basedpython__tests__BY017_BY017.by.snap index f14ef5dff9..3037980d03 100644 --- a/crates/ruff_linter/src/rules/basedpython/snapshots/ruff_linter__rules__basedpython__tests__BY017_BY017.by.snap +++ b/crates/ruff_linter/src/rules/basedpython/snapshots/ruff_linter__rules__basedpython__tests__BY017_BY017.by.snap @@ -6,7 +6,6 @@ BY017 [*] Declaration body of `...` is unnecessary | 1 | class Empty: ... | ^^^^^ - | help: Remove the body | - class Empty: ... @@ -19,7 +18,6 @@ BY017 [*] Declaration body of `...` is unnecessary | 4 | class Stub(Empty): ... | ^^^^^ - | help: Remove the body | 3 | @@ -33,7 +31,6 @@ BY017 [*] Declaration body of `...` is unnecessary | 7 | def stub(x: int) -> int: ... | ^^^^^ - | help: Remove the body | 6 | @@ -47,7 +44,6 @@ BY017 [*] Declaration body of `...` is unnecessary | 10 | async def async_stub() -> None: ... | ^^^^^ - | help: Remove the body | 9 | @@ -80,7 +76,6 @@ BY017 [*] Declaration body of `...` is unnecessary 15 | 16 | class Nested: ... | ^^^^^ - | help: Remove the body | 15 | diff --git a/crates/ruff_linter/src/rules/basedpython/snapshots/ruff_linter__rules__basedpython__tests__BY020_BY020.by.snap b/crates/ruff_linter/src/rules/basedpython/snapshots/ruff_linter__rules__basedpython__tests__BY020_BY020.by.snap index b6c328bae0..49cb5f7113 100644 --- a/crates/ruff_linter/src/rules/basedpython/snapshots/ruff_linter__rules__basedpython__tests__BY020_BY020.by.snap +++ b/crates/ruff_linter/src/rules/basedpython/snapshots/ruff_linter__rules__basedpython__tests__BY020_BY020.by.snap @@ -63,7 +63,6 @@ BY020 [*] `cast` call can be written as the `cast` keyword 13 | 14 | return typing.cast(object, value) | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `cast` | 13 | diff --git a/crates/ruff_linter/src/rules/eradicate/snapshots/ruff_linter__rules__eradicate__tests__ERA001_ERA001.py.snap b/crates/ruff_linter/src/rules/eradicate/snapshots/ruff_linter__rules__eradicate__tests__ERA001_ERA001.py.snap index be3f2e5456..0bc73f1f9a 100644 --- a/crates/ruff_linter/src/rules/eradicate/snapshots/ruff_linter__rules__eradicate__tests__ERA001_ERA001.py.snap +++ b/crates/ruff_linter/src/rules/eradicate/snapshots/ruff_linter__rules__eradicate__tests__ERA001_ERA001.py.snap @@ -93,7 +93,6 @@ ERA001 [*] Found commented-out code 20 | pass 21 | # b = c | ^^^^^^^ - | help: Remove commented-out code | 20 | pass @@ -250,7 +249,6 @@ ERA001 [*] Found commented-out code 37 | # except Foo: 38 | # except Exception as e: print(e) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove commented-out code | 37 | # except Foo: diff --git a/crates/ruff_linter/src/rules/fastapi/mod.rs b/crates/ruff_linter/src/rules/fastapi/mod.rs index 51ff84c5bc..b8314377e7 100644 --- a/crates/ruff_linter/src/rules/fastapi/mod.rs +++ b/crates/ruff_linter/src/rules/fastapi/mod.rs @@ -41,14 +41,8 @@ mod tests { assert_diagnostics_diff!( snapshot, Path::new("fastapi").join(path).as_path(), - &LinterSettings { - unresolved_target_version: PythonVersion::PY313.into(), - ..LinterSettings::for_rule(rule_code) - }, - &LinterSettings { - unresolved_target_version: PythonVersion::PY314.into(), - ..LinterSettings::for_rule(rule_code) - }, + &LinterSettings::for_rule(rule_code).with_target_version(PythonVersion::PY313), + &LinterSettings::for_rule(rule_code).with_target_version(PythonVersion::PY314), ); Ok(()) } @@ -62,10 +56,7 @@ mod tests { let snapshot = format!("{}_{}_py38", rule_code.name(), path.to_string_lossy()); let diagnostics = test_path( Path::new("fastapi").join(path).as_path(), - &LinterSettings { - unresolved_target_version: PythonVersion::PY38.into(), - ..LinterSettings::for_rule(rule_code) - }, + &LinterSettings::for_rule(rule_code).with_target_version(PythonVersion::PY38), )?; 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 360299d488..4515962880 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 @@ -284,7 +284,11 @@ fn create_diagnostic( if is_default_argument_ellipsis && seen_default { // For ellipsis after a parameter with default, can't remove the default - diagnostic.info("Automatic fix is unavailable because a required parameter would follow an optional parameter. Consider reordering arguments to enable the fix."); + diagnostic.info( + "Automatic fix is unavailable because a required parameter \ + would follow an optional parameter. \ + Consider reordering arguments to enable the fix.", + ); return Ok(None); } @@ -316,7 +320,11 @@ fn create_diagnostic( } _ => { if seen_default { - diagnostic.info("Automatic fix is unavailable because a required parameter would follow an optional parameter. Consider reordering arguments to enable the fix."); + diagnostic.info( + "Automatic fix is unavailable because a required parameter \ + would follow an optional parameter. \ + Consider reordering arguments to enable the fix.", + ); return Ok(None); } format!( diff --git a/crates/ruff_linter/src/rules/flake8_2020/snapshots/ruff_linter__rules__flake8_2020__tests__YTT102_YTT102.py.snap b/crates/ruff_linter/src/rules/flake8_2020/snapshots/ruff_linter__rules__flake8_2020__tests__YTT102_YTT102.py.snap index 24afa44e35..89b34e0745 100644 --- a/crates/ruff_linter/src/rules/flake8_2020/snapshots/ruff_linter__rules__flake8_2020__tests__YTT102_YTT102.py.snap +++ b/crates/ruff_linter/src/rules/flake8_2020/snapshots/ruff_linter__rules__flake8_2020__tests__YTT102_YTT102.py.snap @@ -17,4 +17,3 @@ YTT102 `sys.version[2]` referenced (python3.10), use `sys.version_info` 4 | py_minor = sys.version[2] 5 | py_minor = version[2] | ^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_2020/snapshots/ruff_linter__rules__flake8_2020__tests__YTT103_YTT103.py.snap b/crates/ruff_linter/src/rules/flake8_2020/snapshots/ruff_linter__rules__flake8_2020__tests__YTT103_YTT103.py.snap index 356e19d247..5af38635a2 100644 --- a/crates/ruff_linter/src/rules/flake8_2020/snapshots/ruff_linter__rules__flake8_2020__tests__YTT103_YTT103.py.snap +++ b/crates/ruff_linter/src/rules/flake8_2020/snapshots/ruff_linter__rules__flake8_2020__tests__YTT103_YTT103.py.snap @@ -50,4 +50,3 @@ YTT103 `sys.version` compared to string (python3.10), use `sys.version_info` 7 | sys.version > "3.5" 8 | sys.version >= "3.5" | ^^^^^^^^^^^ - | 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 1bf91d93d8..6a3ecc6d8a 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 @@ -39,4 +39,3 @@ YTT201 `sys.version_info[0] != 3` referenced (python4), use `<` 9 | PY2 = sys.version_info[0] != 3 10 | PY2 = version_info[0] != 3 | ^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_2020/snapshots/ruff_linter__rules__flake8_2020__tests__YTT203_YTT203.py.snap b/crates/ruff_linter/src/rules/flake8_2020/snapshots/ruff_linter__rules__flake8_2020__tests__YTT203_YTT203.py.snap index 564a40092f..606aefb539 100644 --- a/crates/ruff_linter/src/rules/flake8_2020/snapshots/ruff_linter__rules__flake8_2020__tests__YTT203_YTT203.py.snap +++ b/crates/ruff_linter/src/rules/flake8_2020/snapshots/ruff_linter__rules__flake8_2020__tests__YTT203_YTT203.py.snap @@ -17,4 +17,3 @@ YTT203 `sys.version_info[1]` compared to integer (python4), compare `sys.version 4 | sys.version_info[1] >= 5 5 | version_info[1] < 6 | ^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_2020/snapshots/ruff_linter__rules__flake8_2020__tests__YTT204_YTT204.py.snap b/crates/ruff_linter/src/rules/flake8_2020/snapshots/ruff_linter__rules__flake8_2020__tests__YTT204_YTT204.py.snap index 3d8deee9a6..cfba92d378 100644 --- a/crates/ruff_linter/src/rules/flake8_2020/snapshots/ruff_linter__rules__flake8_2020__tests__YTT204_YTT204.py.snap +++ b/crates/ruff_linter/src/rules/flake8_2020/snapshots/ruff_linter__rules__flake8_2020__tests__YTT204_YTT204.py.snap @@ -17,4 +17,3 @@ YTT204 `sys.version_info.minor` compared to integer (python4), compare `sys.vers 4 | sys.version_info.minor <= 7 5 | version_info.minor > 8 | ^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_2020/snapshots/ruff_linter__rules__flake8_2020__tests__YTT301_YTT301.py.snap b/crates/ruff_linter/src/rules/flake8_2020/snapshots/ruff_linter__rules__flake8_2020__tests__YTT301_YTT301.py.snap index d22cefefaa..1f3681415e 100644 --- a/crates/ruff_linter/src/rules/flake8_2020/snapshots/ruff_linter__rules__flake8_2020__tests__YTT301_YTT301.py.snap +++ b/crates/ruff_linter/src/rules/flake8_2020/snapshots/ruff_linter__rules__flake8_2020__tests__YTT301_YTT301.py.snap @@ -17,4 +17,3 @@ YTT301 `sys.version[0]` referenced (python10), use `sys.version_info` 4 | py_major = sys.version[0] 5 | py_major = version[0] | ^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_2020/snapshots/ruff_linter__rules__flake8_2020__tests__YTT302_YTT302.py.snap b/crates/ruff_linter/src/rules/flake8_2020/snapshots/ruff_linter__rules__flake8_2020__tests__YTT302_YTT302.py.snap index a6d0a05293..17e39fdd93 100644 --- a/crates/ruff_linter/src/rules/flake8_2020/snapshots/ruff_linter__rules__flake8_2020__tests__YTT302_YTT302.py.snap +++ b/crates/ruff_linter/src/rules/flake8_2020/snapshots/ruff_linter__rules__flake8_2020__tests__YTT302_YTT302.py.snap @@ -50,4 +50,3 @@ YTT302 `sys.version` compared to string (python10), use `sys.version_info` 7 | sys.version > "3" 8 | sys.version >= "3" | ^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_2020/snapshots/ruff_linter__rules__flake8_2020__tests__YTT303_YTT303.py.snap b/crates/ruff_linter/src/rules/flake8_2020/snapshots/ruff_linter__rules__flake8_2020__tests__YTT303_YTT303.py.snap index 187ac8f9fe..c2a58a0035 100644 --- a/crates/ruff_linter/src/rules/flake8_2020/snapshots/ruff_linter__rules__flake8_2020__tests__YTT303_YTT303.py.snap +++ b/crates/ruff_linter/src/rules/flake8_2020/snapshots/ruff_linter__rules__flake8_2020__tests__YTT303_YTT303.py.snap @@ -17,4 +17,3 @@ YTT303 `sys.version[:1]` referenced (python10), use `sys.version_info` 4 | print(sys.version[:1]) 5 | print(version[:1]) | ^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_annotations/snapshots/ruff_linter__rules__flake8_annotations__tests__defaults.snap b/crates/ruff_linter/src/rules/flake8_annotations/snapshots/ruff_linter__rules__flake8_annotations__tests__defaults.snap index 58fce6fc82..d4d3b4e9bb 100644 --- a/crates/ruff_linter/src/rules/flake8_annotations/snapshots/ruff_linter__rules__flake8_annotations__tests__defaults.snap +++ b/crates/ruff_linter/src/rules/flake8_annotations/snapshots/ruff_linter__rules__flake8_annotations__tests__defaults.snap @@ -273,7 +273,6 @@ ANN401 Dynamically typed expressions (typing.Any) are disallowed in `a` 153 | def f(a: Annotated[Any, ...]) -> None: ... 154 | def f(a: "Union[str, bytes, Any]") -> None: ... | ^^^^^^^^^^^^^^^^^^^^^^^^ - | ANN204 [*] Missing return type annotation for special method `__init__` --> annotation_presence.py:159:9 @@ -318,7 +317,6 @@ ANN201 [*] Missing return type annotation for public function `quoted_escape` 171 | # should not cause a stack overflow. 172 | def quoted_escape(x: "'in\x74'"): pass | ^^^^^^^^^^^^^ - | help: Add return type annotation: `None` | 171 | # should not cause a stack overflow. @@ -334,4 +332,3 @@ ANN401 Dynamically typed expressions (typing.Any) are disallowed in `x` 171 | # should not cause a stack overflow. 172 | def quoted_escape(x: "'in\x74'"): pass | ^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_async/mod.rs b/crates/ruff_linter/src/rules/flake8_async/mod.rs index 0707cba7ba..4d5c602928 100644 --- a/crates/ruff_linter/src/rules/flake8_async/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_async/mod.rs @@ -47,10 +47,8 @@ mod tests { fn async109_python_310_or_older(path: &Path) -> Result<()> { let diagnostics = test_path( Path::new("flake8_async").join(path), - &LinterSettings { - unresolved_target_version: PythonVersion::PY310.into(), - ..LinterSettings::for_rule(Rule::AsyncFunctionWithTimeout) - }, + &LinterSettings::for_rule(Rule::AsyncFunctionWithTimeout) + .with_target_version(PythonVersion::PY310), )?; assert_diagnostics!(path.file_name().unwrap().to_str().unwrap(), diagnostics); Ok(()) 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 48a8f573a2..2887a5560c 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 @@ -87,7 +87,7 @@ pub(crate) fn sync_call(checker: &Checker, call: &ExprCall) { return; } - let mut diagnostic = checker.report_diagnostic(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( diff --git a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC100_ASYNC100.py.snap b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC100_ASYNC100.py.snap index 8f5fac5cbc..0a756ec575 100644 --- a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC100_ASYNC100.py.snap +++ b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC100_ASYNC100.py.snap @@ -8,7 +8,6 @@ ASYNC100 A `with trio.fail_after(...):` context does not contain any `await` sta 8 | / with trio.fail_after(): 9 | | ... | |___________^ - | ASYNC100 A `with trio.move_on_after(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint. --> ASYNC100.py:18:5 @@ -17,7 +16,6 @@ ASYNC100 A `with trio.move_on_after(...):` context does not contain any `await` 18 | / with trio.move_on_after(): 19 | | ... | |___________^ - | ASYNC100 A `with anyio.move_on_after(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint. --> ASYNC100.py:45:5 @@ -26,7 +24,6 @@ ASYNC100 A `with anyio.move_on_after(...):` context does not contain any `await` 45 | / with anyio.move_on_after(delay=0.2): 46 | | ... | |___________^ - | ASYNC100 A `with anyio.fail_after(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint. --> ASYNC100.py:50:5 @@ -35,7 +32,6 @@ ASYNC100 A `with anyio.fail_after(...):` context does not contain any `await` st 50 | / with anyio.fail_after(): 51 | | ... | |___________^ - | ASYNC100 A `with anyio.CancelScope(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint. --> ASYNC100.py:55:5 @@ -44,7 +40,6 @@ ASYNC100 A `with anyio.CancelScope(...):` context does not contain any `await` s 55 | / with anyio.CancelScope(): 56 | | ... | |___________^ - | ASYNC100 A `with anyio.CancelScope(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint. --> ASYNC100.py:60:5 @@ -53,7 +48,6 @@ ASYNC100 A `with anyio.CancelScope(...):` context does not contain any `await` s 60 | / with anyio.CancelScope(), nullcontext(): 61 | | ... | |___________^ - | ASYNC100 A `with anyio.CancelScope(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint. --> ASYNC100.py:65:5 @@ -62,7 +56,6 @@ ASYNC100 A `with anyio.CancelScope(...):` context does not contain any `await` s 65 | / with nullcontext(), anyio.CancelScope(): 66 | | ... | |___________^ - | ASYNC100 A `with asyncio.timeout(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint. --> ASYNC100.py:70:5 @@ -71,7 +64,6 @@ ASYNC100 A `with asyncio.timeout(...):` context does not contain any `await` sta 70 | / async with asyncio.timeout(delay=0.2): 71 | | ... | |___________^ - | ASYNC100 A `with asyncio.timeout_at(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint. --> ASYNC100.py:75:5 @@ -80,7 +72,6 @@ ASYNC100 A `with asyncio.timeout_at(...):` context does not contain any `await` 75 | / async with asyncio.timeout_at(when=0.2): 76 | | ... | |___________^ - | ASYNC100 A `with asyncio.timeout(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint. --> ASYNC100.py:85:5 @@ -89,7 +80,6 @@ ASYNC100 A `with asyncio.timeout(...):` context does not contain any `await` sta 85 | / async with asyncio.timeout(delay=0.2), asyncio.TaskGroup(), asyncio.timeout(delay=0.2): 86 | | ... | |___________^ - | ASYNC100 A `with asyncio.timeout(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint. --> ASYNC100.py:95:5 @@ -98,4 +88,3 @@ ASYNC100 A `with asyncio.timeout(...):` context does not contain any `await` sta 95 | / async with asyncio.timeout(delay=0.2), asyncio.timeout(delay=0.2): 96 | | ... | |___________^ - | diff --git a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC105_ASYNC105.py.snap b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC105_ASYNC105.py.snap index a0a9a20655..4e28a6e8bf 100644 --- a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC105_ASYNC105.py.snap +++ b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC105_ASYNC105.py.snap @@ -461,5 +461,4 @@ ASYNC105 Call to `trio.open_file` is not immediately awaited 63 | # ASYNC105 (without fix) 64 | trio.open_file(foo) | ^^^^^^^^^^^^^^^^^^^ - | help: Add `await` diff --git a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC109_0.py.snap b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC109_0.py.snap index 8b29154e50..1c94fe23c0 100644 --- a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC109_0.py.snap +++ b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC109_0.py.snap @@ -26,5 +26,4 @@ ASYNC109 Async function definition with a `timeout` parameter 19 | @abstractmethod 20 | async def foo(self, timeout: float): ... | ^^^^^^^^^^^^^^ - | help: Use `trio.fail_after` instead diff --git a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC109_ASYNC109_0.py.snap b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC109_ASYNC109_0.py.snap index 8b29154e50..1c94fe23c0 100644 --- a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC109_ASYNC109_0.py.snap +++ b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC109_ASYNC109_0.py.snap @@ -26,5 +26,4 @@ ASYNC109 Async function definition with a `timeout` parameter 19 | @abstractmethod 20 | async def foo(self, timeout: float): ... | ^^^^^^^^^^^^^^ - | help: Use `trio.fail_after` instead 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 97f6df299d..061bbe300b 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 @@ -8,7 +8,6 @@ ASYNC110 Use `trio.Event` instead of awaiting `trio.sleep` in a `while` loop 7 | / while True: 8 | | await trio.sleep(10) | |____________________________^ - | ASYNC110 Use `trio.Event` instead of awaiting `trio.sleep` in a `while` loop --> ASYNC110.py:12:5 @@ -17,7 +16,6 @@ ASYNC110 Use `trio.Event` instead of awaiting `trio.sleep` in a `while` loop 12 | / while True: 13 | | await trio.sleep_until(10) | |__________________________________^ - | ASYNC110 Use `anyio.Event` instead of awaiting `anyio.sleep` in a `while` loop --> ASYNC110.py:22:5 @@ -26,7 +24,6 @@ ASYNC110 Use `anyio.Event` instead of awaiting `anyio.sleep` in a `while` loop 22 | / while True: 23 | | await anyio.sleep(10) | |_____________________________^ - | ASYNC110 Use `anyio.Event` instead of awaiting `anyio.sleep` in a `while` loop --> ASYNC110.py:27:5 @@ -35,7 +32,6 @@ ASYNC110 Use `anyio.Event` instead of awaiting `anyio.sleep` in a `while` loop 27 | / while True: 28 | | await anyio.sleep_until(10) | |___________________________________^ - | ASYNC110 Use `asyncio.Event` instead of awaiting `asyncio.sleep` in a `while` loop --> ASYNC110.py:37:5 @@ -44,4 +40,3 @@ ASYNC110 Use `asyncio.Event` instead of awaiting `asyncio.sleep` in a `while` lo 37 | / while True: 38 | | await asyncio.sleep(10) | |_______________________________^ - | 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 0ff5848786..539627b95d 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 @@ -72,7 +72,6 @@ ASYNC115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)` 47 | 48 | trio.run(trio.sleep(0)) # ASYNC115 | ^^^^^^^^^^^^^ - | help: Replace with `trio.lowlevel.checkpoint()` | 1 + import trio.lowlevel @@ -90,7 +89,6 @@ ASYNC115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)` 54 | def func(): 55 | sleep(0) # ASYNC115 | ^^^^^^^^ - | help: Replace with `trio.lowlevel.checkpoint()` | 51 | from trio import Event, sleep @@ -109,7 +107,6 @@ ASYNC115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)` 58 | async def func(): 59 | await sleep(seconds=0) # ASYNC115 | ^^^^^^^^^^^^^^^^ - | help: Replace with `trio.lowlevel.checkpoint()` | 51 | from trio import Event, sleep @@ -195,7 +192,6 @@ ASYNC115 [*] Use `anyio.lowlevel.checkpoint()` instead of `anyio.sleep(0)` 127 | 128 | anyio.run(anyio.sleep(0)) # ASYNC115 | ^^^^^^^^^^^^^^ - | help: Replace with `anyio.lowlevel.checkpoint()` | 51 | from trio import Event, sleep @@ -284,7 +280,6 @@ ASYNC115 [*] Use `anyio.lowlevel.checkpoint()` instead of `anyio.sleep(0)` 187 | 188 | await anyio_sleep(0) # ASYNC115 | ^^^^^^^^^^^^^^ - | help: Replace with `anyio.lowlevel.checkpoint()` | 51 | from trio import Event, sleep 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 a0db966c62..12ebd5ca0e 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 @@ -99,7 +99,6 @@ ASYNC116 [*] `trio.sleep()` with >24 hour interval should usually be `trio.sleep 49 | # also checks that we don't break visit_Call 50 | trio.run(trio.sleep(86401)) # error: 116, "async" | ^^^^^^^^^^^^^^^^^ - | help: Replace with `trio.sleep_forever()` | 49 | # also checks that we don't break visit_Call @@ -115,7 +114,6 @@ ASYNC116 [*] `trio.sleep()` with >24 hour interval should usually be `trio.sleep 56 | # catch from import 57 | await sleep(86401) # error: 116, "async" | ^^^^^^^^^^^^ - | help: Replace with `trio.sleep_forever()` | 4 | from math import inf @@ -227,7 +225,6 @@ ASYNC116 [*] `anyio.sleep()` with >24 hour interval should usually be `anyio.sle 102 | # also checks that we don't break visit_Call 103 | anyio.run(anyio.sleep(86401)) # error: 116, "async" | ^^^^^^^^^^^^^^^^^^ - | help: Replace with `anyio.sleep_forever()` | 102 | # also checks that we don't break visit_Call @@ -243,7 +240,6 @@ ASYNC116 [*] `anyio.sleep()` with >24 hour interval should usually be `anyio.sle 109 | # catch from import 110 | await sleep(86401) # error: 116, "async" | ^^^^^^^^^^^^ - | help: Replace with `anyio.sleep_forever()` | 4 | from math import inf @@ -317,7 +313,6 @@ ASYNC116 [*] `trio.sleep()` with >24 hour interval should usually be `trio.sleep 137 | await sleep(18446744073709551616) 138 | await trio.sleep(99999999999999999999) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `trio.sleep_forever()` | 137 | await sleep(18446744073709551616) diff --git a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC119_ASYNC119.py.snap b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC119_ASYNC119.py.snap index 46652b36e7..b1702f8554 100644 --- a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC119_ASYNC119.py.snap +++ b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC119_ASYNC119.py.snap @@ -8,7 +8,6 @@ ASYNC119 Yield in context manager in async generator may not trigger cleanup 13 | with open(""): 14 | yield # ASYNC119 | ^^^^^ - | help: Use `@asynccontextmanager` if appropriate, or refactor ASYNC119 Yield in context manager in async generator may not trigger cleanup @@ -18,7 +17,6 @@ ASYNC119 Yield in context manager in async generator may not trigger cleanup 18 | async with open(""): 19 | yield # ASYNC119 | ^^^^^ - | help: Use `@asynccontextmanager` if appropriate, or refactor ASYNC119 Yield in context manager in async generator may not trigger cleanup @@ -63,7 +61,6 @@ ASYNC119 Yield in context manager in async generator may not trigger cleanup 27 | yield # ASYNC119 28 | yield # ASYNC119 | ^^^^^ - | help: Use `@asynccontextmanager` if appropriate, or refactor ASYNC119 Yield in context manager in async generator may not trigger cleanup @@ -73,7 +70,6 @@ ASYNC119 Yield in context manager in async generator may not trigger cleanup 33 | with open(""): 34 | yield # ASYNC119 | ^^^^^ - | help: Use `@asynccontextmanager` if appropriate, or refactor ASYNC119 Yield in context manager in async generator may not trigger cleanup @@ -83,5 +79,4 @@ ASYNC119 Yield in context manager in async generator may not trigger cleanup 61 | yield 62 | yield # ASYNC119 | ^^^^^ - | help: Use `@asynccontextmanager` if appropriate, or refactor diff --git a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC210_ASYNC210.py.snap b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC210_ASYNC210.py.snap index 6082f3daca..1ec055a0f4 100644 --- a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC210_ASYNC210.py.snap +++ b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC210_ASYNC210.py.snap @@ -7,7 +7,6 @@ ASYNC210 Async functions should not call blocking HTTP methods 7 | async def foo(): 8 | urllib.request.urlopen("http://example.com/foo/bar").read() # ASYNC210 | ^^^^^^^^^^^^^^^^^^^^^^ - | ASYNC210 Async functions should not call blocking HTTP methods --> ASYNC210.py:12:5 @@ -15,7 +14,6 @@ ASYNC210 Async functions should not call blocking HTTP methods 11 | async def foo(): 12 | requests.get() # ASYNC210 | ^^^^^^^^^^^^ - | ASYNC210 Async functions should not call blocking HTTP methods --> ASYNC210.py:16:5 @@ -23,7 +21,6 @@ ASYNC210 Async functions should not call blocking HTTP methods 15 | async def foo(): 16 | httpx.get() # ASYNC210 | ^^^^^^^^^ - | ASYNC210 Async functions should not call blocking HTTP methods --> ASYNC210.py:20:5 @@ -31,7 +28,6 @@ ASYNC210 Async functions should not call blocking HTTP methods 19 | async def foo(): 20 | requests.post() # ASYNC210 | ^^^^^^^^^^^^^ - | ASYNC210 Async functions should not call blocking HTTP methods --> ASYNC210.py:24:5 @@ -39,7 +35,6 @@ ASYNC210 Async functions should not call blocking HTTP methods 23 | async def foo(): 24 | httpx.post() # ASYNC210 | ^^^^^^^^^^ - | ASYNC210 Async functions should not call blocking HTTP methods --> ASYNC210.py:28:5 diff --git a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC221_ASYNC22x.py.snap b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC221_ASYNC22x.py.snap index 1a3756bca5..8cd50a77e4 100644 --- a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC221_ASYNC22x.py.snap +++ b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC221_ASYNC22x.py.snap @@ -7,7 +7,6 @@ ASYNC221 Async functions should not run processes with blocking methods 7 | async def func(): 8 | subprocess.run("foo") # ASYNC221 | ^^^^^^^^^^^^^^ - | ASYNC221 Async functions should not run processes with blocking methods --> ASYNC22x.py:12:5 @@ -15,7 +14,6 @@ ASYNC221 Async functions should not run processes with blocking methods 11 | async def func(): 12 | subprocess.call("foo") # ASYNC221 | ^^^^^^^^^^^^^^^ - | ASYNC221 Async functions should not run processes with blocking methods --> ASYNC22x.py:29:9 diff --git a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC222_ASYNC22x.py.snap b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC222_ASYNC22x.py.snap index 13e4a52a62..235ff6f93c 100644 --- a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC222_ASYNC22x.py.snap +++ b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC222_ASYNC22x.py.snap @@ -7,7 +7,6 @@ ASYNC222 Async functions should not wait on processes with blocking methods 19 | async def func(): 20 | os.wait4(10) # ASYNC222 | ^^^^^^^^ - | ASYNC222 Async functions should not wait on processes with blocking methods --> ASYNC22x.py:24:5 @@ -15,7 +14,6 @@ ASYNC222 Async functions should not wait on processes with blocking methods 23 | async def func(): 24 | os.wait(12) # ASYNC222 | ^^^^^^^ - | ASYNC222 Async functions should not wait on processes with blocking methods --> ASYNC22x.py:91:5 diff --git a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC230_ASYNC230.py.snap b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC230_ASYNC230.py.snap index 912a451a2b..90fc269994 100644 --- a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC230_ASYNC230.py.snap +++ b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC230_ASYNC230.py.snap @@ -67,7 +67,6 @@ ASYNC230 Async functions should not open files with blocking methods like `open` 28 | async def func(): 29 | open("foo") # ASYNC230 | ^^^^ - | ASYNC230 Async functions should not open files with blocking methods like `open` --> ASYNC230.py:36:5 @@ -75,7 +74,6 @@ ASYNC230 Async functions should not open files with blocking methods like `open` 35 | async def func(): 36 | Path("foo").open() # ASYNC230 | ^^^^^^^^^^^^^^^^ - | ASYNC230 Async functions should not open files with blocking methods like `open` --> ASYNC230.py:41:5 @@ -84,7 +82,6 @@ ASYNC230 Async functions should not open files with blocking methods like `open` 40 | p = Path("foo") 41 | p.open() # ASYNC230 | ^^^^^^ - | ASYNC230 Async functions should not open files with blocking methods like `open` --> ASYNC230.py:45:10 @@ -101,7 +98,6 @@ ASYNC230 Async functions should not open files with blocking methods like `open` 52 | async def bar(): 53 | p.open() # ASYNC230 | ^^^^^^ - | ASYNC230 Async functions should not open files with blocking methods like `open` --> ASYNC230.py:59:5 @@ -110,4 +106,3 @@ ASYNC230 Async functions should not open files with blocking methods like `open` 58 | 59 | p1.open() # ASYNC230 | ^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC240_ASYNC240.py.snap b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC240_ASYNC240.py.snap index 888905aed7..18bcfd35b2 100644 --- a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC240_ASYNC240.py.snap +++ b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC240_ASYNC240.py.snap @@ -108,4 +108,3 @@ ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or a 101 | async def path_as_optional_parameter_type(path: Optional[Path]): 102 | path.exists() # ASYNC240 | ^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC250_ASYNC250.py.snap b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC250_ASYNC250.py.snap index 22deadaa6c..1056f3f994 100644 --- a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC250_ASYNC250.py.snap +++ b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC250_ASYNC250.py.snap @@ -17,7 +17,6 @@ ASYNC250 Blocking call to `input()` in async context 7 | k = input() # ASYNC250 8 | input("hello world") # ASYNC250 | ^^^^^ - | ASYNC250 Blocking call to `input()` in async context --> ASYNC250.py:21:5 diff --git a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC251_ASYNC251.py.snap b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC251_ASYNC251.py.snap index bc6ec0e246..e76297dece 100644 --- a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC251_ASYNC251.py.snap +++ b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC251_ASYNC251.py.snap @@ -7,4 +7,3 @@ ASYNC251 Async functions should not call `time.sleep` 5 | async def func(): 6 | time.sleep(1) # ASYNC251 | ^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/mod.rs b/crates/ruff_linter/src/rules/flake8_bandit/mod.rs index 69b93f868f..f978ecc22d 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/mod.rs @@ -12,7 +12,6 @@ mod tests { use crate::registry::Rule; use crate::settings::LinterSettings; - use crate::settings::types::PreviewMode; use crate::test::test_path; use crate::{assert_diagnostics, assert_diagnostics_diff}; @@ -105,8 +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::SnmpInsecureVersion, Path::new("S508.py"))] - #[test_case(Rule::SnmpWeakCryptography, Path::new("S509.py"))] #[test_case(Rule::UnsafeYAMLLoad, Path::new("S506.py"))] fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!( @@ -118,14 +115,8 @@ mod tests { assert_diagnostics_diff!( snapshot, Path::new("flake8_bandit").join(path).as_path(), - &LinterSettings { - preview: PreviewMode::Disabled, - ..LinterSettings::for_rule(rule_code) - }, - &LinterSettings { - preview: PreviewMode::Enabled, - ..LinterSettings::for_rule(rule_code) - } + &LinterSettings::for_rule(rule_code), + &LinterSettings::for_rule(rule_code).with_preview_mode() ); 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 606630bccb..f208658e29 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 @@ -18,6 +18,9 @@ use crate::checkers::ast::Checker; /// /// Consider raising a meaningful error instead of using `assert`. /// +/// The rule exempts assertions within a `TYPE_CHECKING` block, assuming these are needed to satisfy +/// a type checker. +/// /// ## Example /// ```python /// assert x > 0, "Expected positive value." 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 8d963335ec..d0b9b3afc1 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 @@ -39,7 +39,9 @@ pub(crate) struct MakoTemplates; impl Violation for MakoTemplates { #[derive_message_formats] fn message(&self) -> String { - "Mako templates allow HTML and JavaScript rendering by default and are inherently open to XSS attacks".to_string() + "Mako templates allow HTML and JavaScript rendering by default \ + and are inherently open to XSS attacks" + .to_string() } } 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 d4d741d328..9569d72fa9 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 @@ -47,10 +47,18 @@ impl Violation for SubprocessPopenWithShellEqualsTrue { #[derive_message_formats] fn message(&self) -> String { match (self.safety, self.is_exact) { - (Safety::SeemsSafe, true) => "`subprocess` call with `shell=True` seems safe, but may be changed in the future; consider rewriting without `shell`".to_string(), - (Safety::Unknown, true) => "`subprocess` call with `shell=True` identified, security issue".to_string(), - (Safety::SeemsSafe, false) => "`subprocess` call with truthy `shell` seems safe, but may be changed in the future; consider rewriting without `shell`".to_string(), - (Safety::Unknown, false) => "`subprocess` call with truthy `shell` identified, security issue".to_string(), + (Safety::SeemsSafe, true) => "`subprocess` call with `shell=True` seems safe, \ + but may be changed in the future; consider rewriting without `shell`" + .to_string(), + (Safety::Unknown, true) => { + "`subprocess` call with `shell=True` identified, security issue".to_string() + } + (Safety::SeemsSafe, false) => "`subprocess` call with truthy `shell` seems safe, \ + but may be changed in the future; consider rewriting without `shell`" + .to_string(), + (Safety::Unknown, false) => { + "`subprocess` call with truthy `shell` identified, security issue".to_string() + } } } } @@ -181,8 +189,12 @@ impl Violation for StartProcessWithAShell { #[derive_message_formats] fn message(&self) -> String { match self.safety { - Safety::SeemsSafe => "Starting a process with a shell: seems safe, but may be changed in the future; consider rewriting without `shell`".to_string(), - Safety::Unknown => "Starting a process with a shell, possible injection detected".to_string(), + Safety::SeemsSafe => "Starting a process with a shell: seems safe, \ + but may be changed in the future; consider rewriting without `shell`" + .to_string(), + Safety::Unknown => { + "Starting a process with a shell, possible injection detected".to_string() + } } } } 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 8bf5830e31..44b90786e9 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 @@ -4,7 +4,6 @@ use ruff_text_size::Ranged; use crate::Violation; use crate::checkers::ast::Checker; -use crate::preview::is_extended_snmp_api_path_detection_enabled; /// ## What it does /// Checks for uses of SNMPv1 or SNMPv2. @@ -48,17 +47,10 @@ pub(crate) fn snmp_insecure_version(checker: &Checker, call: &ast::ExprCall) { .semantic() .resolve_qualified_name(&call.func) .is_some_and(|qualified_name| { - if is_extended_snmp_api_path_detection_enabled(checker.settings()) { - matches!( - qualified_name.segments(), - ["pysnmp", "hlapi", .., "CommunityData"] - ) - } else { - matches!( - qualified_name.segments(), - ["pysnmp", "hlapi", "CommunityData"] - ) - } + matches!( + qualified_name.segments(), + ["pysnmp", "hlapi", .., "CommunityData"] + ) }) { if let Some(keyword) = call.arguments.find_keyword("mpModel") { 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 390f1bf13a..4e9297fe4a 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 @@ -4,7 +4,6 @@ use ruff_text_size::Ranged; use crate::Violation; use crate::checkers::ast::Checker; -use crate::preview::is_extended_snmp_api_path_detection_enabled; /// ## What it does /// Checks for uses of the SNMPv3 protocol without encryption. @@ -48,17 +47,10 @@ pub(crate) fn snmp_weak_cryptography(checker: &Checker, call: &ast::ExprCall) { .semantic() .resolve_qualified_name(&call.func) .is_some_and(|qualified_name| { - if is_extended_snmp_api_path_detection_enabled(checker.settings()) { - matches!( - qualified_name.segments(), - ["pysnmp", "hlapi", .., "UsmUserData"] - ) - } else { - matches!( - qualified_name.segments(), - ["pysnmp", "hlapi", "UsmUserData"] - ) - } + matches!( + qualified_name.segments(), + ["pysnmp", "hlapi", .., "UsmUserData"] + ) }) { checker.report_diagnostic(SnmpWeakCryptography, call.func.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 06e2e7e348..41c09b6f70 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 @@ -10,10 +10,7 @@ use ruff_text_size::{Ranged, TextRange}; use crate::Violation; use crate::checkers::ast::Checker; -use crate::preview::{ - is_s310_resolve_string_literal_bindings_enabled, is_suspicious_function_reference_enabled, -}; -use crate::settings::LinterSettings; +use crate::preview::is_suspicious_function_reference_enabled; /// ## What it does /// Checks for calls to `pickle` functions or modules that wrap them. @@ -62,7 +59,9 @@ pub(crate) struct SuspiciousPickleUsage; impl Violation for SuspiciousPickleUsage { #[derive_message_formats] fn message(&self) -> String { - "`pickle` and modules that wrap it can be unsafe when used to deserialize untrusted data, possible security issue".to_string() + "`pickle` and modules that wrap it can be unsafe \ + when used to deserialize untrusted data, possible security issue" + .to_string() } } @@ -449,7 +448,9 @@ pub(crate) struct SuspiciousURLOpenUsage; impl Violation for SuspiciousURLOpenUsage { #[derive_message_formats] fn message(&self) -> String { - "Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.".to_string() + "Audit URL open for permitted schemes. \ + Allowing use of `file:` or custom schemes is often unexpected." + .to_string() } } @@ -537,7 +538,9 @@ pub(crate) struct SuspiciousXMLCElementTreeUsage; impl Violation for SuspiciousXMLCElementTreeUsage { #[derive_message_formats] fn message(&self) -> String { - "Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents".to_string() + "Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; \ + use `defusedxml` equivalents" + .to_string() } } @@ -582,7 +585,9 @@ pub(crate) struct SuspiciousXMLElementTreeUsage; impl Violation for SuspiciousXMLElementTreeUsage { #[derive_message_formats] fn message(&self) -> String { - "Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents".to_string() + "Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; \ + use `defusedxml` equivalents" + .to_string() } } @@ -627,7 +632,9 @@ pub(crate) struct SuspiciousXMLExpatReaderUsage; impl Violation for SuspiciousXMLExpatReaderUsage { #[derive_message_formats] fn message(&self) -> String { - "Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents".to_string() + "Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; \ + use `defusedxml` equivalents" + .to_string() } } @@ -672,7 +679,9 @@ pub(crate) struct SuspiciousXMLExpatBuilderUsage; impl Violation for SuspiciousXMLExpatBuilderUsage { #[derive_message_formats] fn message(&self) -> String { - "Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents".to_string() + "Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; \ + use `defusedxml` equivalents" + .to_string() } } @@ -717,7 +726,9 @@ pub(crate) struct SuspiciousXMLSaxUsage; impl Violation for SuspiciousXMLSaxUsage { #[derive_message_formats] fn message(&self) -> String { - "Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents".to_string() + "Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; \ + use `defusedxml` equivalents" + .to_string() } } @@ -762,7 +773,9 @@ pub(crate) struct SuspiciousXMLMiniDOMUsage; impl Violation for SuspiciousXMLMiniDOMUsage { #[derive_message_formats] fn message(&self) -> String { - "Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents".to_string() + "Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; \ + use `defusedxml` equivalents" + .to_string() } } @@ -807,7 +820,9 @@ pub(crate) struct SuspiciousXMLPullDOMUsage; impl Violation for SuspiciousXMLPullDOMUsage { #[derive_message_formats] fn message(&self) -> String { - "Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents".to_string() + "Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; \ + use `defusedxml` equivalents" + .to_string() } } @@ -896,7 +911,10 @@ pub(crate) struct SuspiciousUnverifiedContextUsage; impl Violation for SuspiciousUnverifiedContextUsage { #[derive_message_formats] fn message(&self) -> String { - "Python allows using an insecure context via the `_create_unverified_context` that reverts to the previous behavior that does not validate certificates or perform hostname checks.".to_string() + "Python allows using an insecure context via the `_create_unverified_context` \ + that reverts to the previous behavior that does not validate certificates \ + or perform hostname checks." + .to_string() } } @@ -948,7 +966,9 @@ pub(crate) struct SuspiciousFTPLibUsage; impl Violation for SuspiciousFTPLibUsage { #[derive_message_formats] fn message(&self) -> String { - "FTP-related functions are being called. FTP is considered insecure. Use SSH/SFTP/SCP or some other encrypted protocol.".to_string() + "FTP-related functions are being called. FTP is considered insecure. \ + Use SSH/SFTP/SCP or some other encrypted protocol." + .to_string() } } @@ -957,7 +977,7 @@ pub(crate) fn suspicious_function_call(checker: &Checker, call: &ExprCall) { checker, call.func.as_ref(), Some(&call.arguments), - call.range, + call.range(), ); } @@ -967,17 +987,16 @@ pub(crate) fn suspicious_function_reference(checker: &Checker, func: &Expr) { } match checker.semantic().current_expression_parent() { - Some(Expr::Call(parent)) - // Avoid duplicate diagnostics. For example: - // - // ```python - // # vvvvvvvvvvvvvvvvvvvvvvvvv Already reported as a call expression - // shelve.open(lorem, ipsum) - // # ^^^^^^ Should not be reported as a reference - // ``` - if parent.func.range().contains_range(func.range()) => { - return; - } + // Avoid duplicate diagnostics. For example: + // + // ```python + // # vvvvvvvvvvvvvvvvvvvvvvvvv Already reported as a call expression + // shelve.open(lorem, ipsum) + // # ^^^^^^ Should not be reported as a reference + // ``` + Some(Expr::Call(parent)) if parent.func.range().contains_range(func.range()) => { + return; + } Some(Expr::Attribute(_)) => { // Avoid duplicate diagnostics. For example: // @@ -1021,13 +1040,8 @@ fn suspicious_function( } /// Resolves `expr` to its binding and checks if the resolved expression starts with an HTTP or HTTPS prefix. - fn expression_starts_with_http_prefix( - expr: &Expr, - semantic: &SemanticModel, - settings: &LinterSettings, - ) -> bool { - let resolved_expression = if is_s310_resolve_string_literal_bindings_enabled(settings) - && let Some(name_expr) = expr.as_name_expr() + fn expression_starts_with_http_prefix(expr: &Expr, semantic: &SemanticModel) -> bool { + let resolved_expression = if let Some(name_expr) = expr.as_name_expr() && let Some(binding_id) = semantic.only_binding(name_expr) && let Some(value) = find_binding_value(semantic.binding(binding_id), semantic) { @@ -1170,11 +1184,7 @@ fn suspicious_function( .all(|keyword| keyword.arg.is_some()) { if let Some(url_expr) = arguments.find_argument_value("url", 0) - && expression_starts_with_http_prefix( - url_expr, - checker.semantic(), - checker.settings(), - ) + && expression_starts_with_http_prefix(url_expr, checker.semantic()) { return; } @@ -1211,11 +1221,7 @@ fn suspicious_function( }) => { if let Some(url_expr) = arguments.find_argument_value("url", 0) - && expression_starts_with_http_prefix( - url_expr, - checker.semantic(), - checker.settings(), - ) + && expression_starts_with_http_prefix(url_expr, checker.semantic()) { return; } @@ -1223,11 +1229,7 @@ fn suspicious_function( // If the `url` argument is a string literal (including resolved bindings), allow `http` and `https` schemes. Some(expr) - if expression_starts_with_http_prefix( - expr, - checker.semantic(), - checker.settings(), - ) => + if expression_starts_with_http_prefix(expr, checker.semantic()) => { return; } 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 d099b7fe42..09ee508ed0 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 @@ -31,7 +31,9 @@ pub(crate) struct SuspiciousTelnetlibImport; impl Violation for SuspiciousTelnetlibImport { #[derive_message_formats] fn message(&self) -> String { - "`telnetlib` and related modules are considered insecure. Use SSH or another encrypted protocol.".to_string() + "`telnetlib` and related modules are considered insecure. \ + Use SSH or another encrypted protocol." + .to_string() } } @@ -56,7 +58,9 @@ pub(crate) struct SuspiciousFtplibImport; impl Violation for SuspiciousFtplibImport { #[derive_message_formats] fn message(&self) -> String { - "`ftplib` and related modules are considered insecure. Use SSH, SFTP, SCP, or another encrypted protocol.".to_string() + "`ftplib` and related modules are considered insecure. \ + Use SSH, SFTP, SCP, or another encrypted protocol." + .to_string() } } @@ -306,7 +310,10 @@ pub(crate) struct SuspiciousHttpoxyImport; impl Violation for SuspiciousHttpoxyImport { #[derive_message_formats] fn message(&self) -> String { - "`httpoxy` is a set of vulnerabilities that affect application code running inCGI, or CGI-like environments. The use of CGI for web applications should be avoided".to_string() + "`httpoxy` is a set of vulnerabilities that affect application code \ + running inCGI, or CGI-like environments. \ + The use of CGI for web applications should be avoided" + .to_string() } } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S101_S101.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S101_S101.py.snap index 8a600103fd..0884d95a1f 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S101_S101.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S101_S101.py.snap @@ -6,7 +6,6 @@ S101 Use of `assert` detected | 1 | assert True # S101 | ^^^^^^ - | S101 Use of `assert` detected --> S101.py:6:5 @@ -25,4 +24,3 @@ S101 Use of `assert` detected 6 | assert x == 1 # S101 7 | assert x == 2 # S101 | ^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S102_S102.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S102_S102.py.snap index d424ab9357..9f4a931efc 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S102_S102.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S102_S102.py.snap @@ -19,7 +19,6 @@ S102 Use of `exec` detected 4 | 5 | exec('y = 3') | ^^^^ - | S102 Use of `exec` detected --> S102.py:11:5 diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S103_S103.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S103_S103.py.snap index a217da9e19..731df200a5 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S103_S103.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S103_S103.py.snap @@ -160,7 +160,6 @@ S103 `os.chmod` setting a permissive mask `0o11` on file or directory 26 | os.chmod("/etc/secrets.txt", 0o21) # OK (stable); Error (preview, S_IWGRP) 27 | os.chmod("/etc/secrets.txt", 0o11) # Error (S_IXGRP) | ^^^^ - | S103 `os.chmod` setting a permissive mask `0o777` on file or directory --> S103.py:31:20 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 a34d204422..cd1fd2a77f 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 @@ -28,7 +28,6 @@ S104 Possible binding to all interfaces 10 | '0.0.0.0' 11 | f"0.0.0.0" | ^^^^^^^ - | S104 Possible binding to all interfaces --> S104.py:15:6 @@ -36,7 +35,6 @@ S104 Possible binding to all interfaces 14 | # Error 15 | func("0.0.0.0") | ^^^^^^^^^ - | S104 Possible binding to all interfaces --> S104.py:19:9 diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S105_S105.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S105_S105.py.snap index fe9c896f66..d6544fc308 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S105_S105.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S105_S105.py.snap @@ -235,7 +235,6 @@ S105 Possible hardcoded password assigned to: "password" 34 | safe = d["password"] = "s3cr3t" 35 | d["password"] = safe = "s3cr3t" | ^^^^^^^^ - | S105 Possible hardcoded password assigned to: "password" --> S105.py:39:16 diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S106_S106.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S106_S106.py.snap index 3550dbb83a..27cee818ca 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S106_S106.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S106_S106.py.snap @@ -7,4 +7,3 @@ S106 Possible hardcoded password assigned to argument: "password" 13 | # Error 14 | func(1, password="s3cr3t") | ^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S110_typed.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S110_typed.snap index 948db1015b..3182e577b9 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S110_typed.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S110_typed.snap @@ -33,4 +33,3 @@ S110 `try`-`except`-`pass` detected, consider logging the exception 13 | / except ValueError: 14 | | pass | |________^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S301_S301.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S301_S301.py.snap index c15dd78222..dd86139e39 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S301_S301.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S301_S301.py.snap @@ -8,4 +8,3 @@ S301 `pickle` and modules that wrap it can be unsafe when used to deserialize un 2 | 3 | pickle.loads() | ^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S307_S307.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S307_S307.py.snap index 2c79297a12..068a011b32 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S307_S307.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S307_S307.py.snap @@ -17,4 +17,3 @@ S307 Use of possibly insecure function; consider using `ast.literal_eval` 3 | print(eval("1+1")) # S307 4 | print(eval("os.getcwd()")) # S307 | ^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S310_S310.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S310_S310.py.snap index 7c73532b23..7f379b537e 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S310_S310.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S310_S310.py.snap @@ -244,7 +244,6 @@ S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom sch 41 | urllib.request.urlopen(urllib.request.Request('file:///foo/bar/baz')) 42 | urllib.request.urlopen(urllib.request.Request(url)) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. --> S310.py:42:24 @@ -253,85 +252,3 @@ S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom sch 41 | urllib.request.urlopen(urllib.request.Request('file:///foo/bar/baz')) 42 | urllib.request.urlopen(urllib.request.Request(url)) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - -S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. - --> S310.py:51:1 - | -49 | # https://github.com/astral-sh/ruff/issues/21462 -50 | path = "https://example.com/data.csv" -51 | urllib.request.urlretrieve(path, "data.csv") - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -52 | url = "https://example.com/api" -53 | urllib.request.Request(url) - | - -S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. - --> S310.py:53:1 - | -51 | urllib.request.urlretrieve(path, "data.csv") -52 | url = "https://example.com/api" -53 | urllib.request.Request(url) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ -54 | -55 | # Test resolved f-strings and concatenated string literals - | - -S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. - --> S310.py:57:1 - | -55 | # Test resolved f-strings and concatenated string literals -56 | fstring_url = f"https://example.com/data.csv" -57 | urllib.request.urlopen(fstring_url) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -58 | urllib.request.Request(fstring_url) - | - -S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. - --> S310.py:58:1 - | -56 | fstring_url = f"https://example.com/data.csv" -57 | urllib.request.urlopen(fstring_url) -58 | urllib.request.Request(fstring_url) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -59 | -60 | concatenated_url = "https://" + "example.com/data.csv" - | - -S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. - --> S310.py:61:1 - | -60 | concatenated_url = "https://" + "example.com/data.csv" -61 | urllib.request.urlopen(concatenated_url) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -62 | urllib.request.Request(concatenated_url) - | - -S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. - --> S310.py:62:1 - | -60 | concatenated_url = "https://" + "example.com/data.csv" -61 | urllib.request.urlopen(concatenated_url) -62 | urllib.request.Request(concatenated_url) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -63 | -64 | nested_concatenated = "http://" + "example.com" + "/data.csv" - | - -S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. - --> S310.py:65:1 - | -64 | nested_concatenated = "http://" + "example.com" + "/data.csv" -65 | urllib.request.urlopen(nested_concatenated) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -66 | urllib.request.Request(nested_concatenated) - | - -S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. - --> S310.py:66:1 - | -64 | nested_concatenated = "http://" + "example.com" + "/data.csv" -65 | urllib.request.urlopen(nested_concatenated) -66 | urllib.request.Request(nested_concatenated) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S312_S312.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S312_S312.py.snap index f585796d40..634487e361 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S312_S312.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S312_S312.py.snap @@ -8,7 +8,6 @@ S312 Telnet is considered insecure. Use SSH or some other encrypted protocol. 2 | 3 | Telnet("localhost", 23) | ^^^^^^^^^^^^^^^^^^^^^^^ - | S312 Telnet is considered insecure. Use SSH or some other encrypted protocol. --> S312.py:14:24 diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S401_S401.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S401_S401.py.snap index ab44b4f761..d354574b27 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S401_S401.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S401_S401.py.snap @@ -15,4 +15,3 @@ S401 `telnetlib` and related modules are considered insecure. Use SSH or another 1 | import telnetlib # S401 2 | from telnetlib import Telnet # S401 | ^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S402_S402.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S402_S402.py.snap index a264f60e5f..713d42c33d 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S402_S402.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S402_S402.py.snap @@ -15,4 +15,3 @@ S402 `ftplib` and related modules are considered insecure. Use SSH, SFTP, SCP, o 1 | import ftplib # S402 2 | from ftplib import FTP # S402 | ^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S403_S403.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S403_S403.py.snap index bb3e712c16..d4c3c18c09 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S403_S403.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S403_S403.py.snap @@ -81,4 +81,3 @@ S403 `pickle`, `cPickle`, `dill`, and `shelve` modules are possibly insecure 7 | import pickle 8 | from pickle import load | ^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S404_S404.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S404_S404.py.snap index 26db33929d..7e82800657 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S404_S404.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S404_S404.py.snap @@ -26,4 +26,3 @@ S404 `subprocess` module is possibly insecure 2 | from subprocess import Popen # S404 3 | from subprocess import Popen as pop # S404 | ^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S405_S405.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S405_S405.py.snap index 8589ad87fe..eb2aba7dfa 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S405_S405.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S405_S405.py.snap @@ -37,4 +37,3 @@ S405 `xml.etree` methods are vulnerable to XML attacks 3 | import xml.etree.ElementTree # S405 4 | from xml.etree import ElementTree # S405 | ^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S406_S406.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S406_S406.py.snap index 4935a4a321..d14ac50f3d 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S406_S406.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S406_S406.py.snap @@ -26,4 +26,3 @@ S406 `xml.sax` methods are vulnerable to XML attacks 2 | import xml.sax as xmls # S406 3 | import xml.sax # S406 | ^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S407_S407.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S407_S407.py.snap index f9d67dfe4a..a260225e41 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S407_S407.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S407_S407.py.snap @@ -15,4 +15,3 @@ S407 `xml.dom.expatbuilder` is vulnerable to XML attacks 1 | from xml.dom import expatbuilder # S407 2 | import xml.dom.expatbuilder # S407 | ^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S408_S408.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S408_S408.py.snap index d88a35b987..9fcdc20834 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S408_S408.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S408_S408.py.snap @@ -15,4 +15,3 @@ S408 `xml.dom.minidom` is vulnerable to XML attacks 1 | from xml.dom.minidom import parseString # S408 2 | import xml.dom.minidom # S408 | ^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S409_S409.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S409_S409.py.snap index f6333d760f..efac83bfe7 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S409_S409.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S409_S409.py.snap @@ -15,4 +15,3 @@ S409 `xml.dom.pulldom` is vulnerable to XML attacks 1 | from xml.dom.pulldom import parseString # S409 2 | import xml.dom.pulldom # S409 | ^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S410_S410.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S410_S410.py.snap index f2bbac41cf..1f7d208746 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S410_S410.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S410_S410.py.snap @@ -15,4 +15,3 @@ S410 `lxml` is vulnerable to XML attacks 1 | import lxml # S410 2 | from lxml import etree # S410 | ^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S411_S411.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S411_S411.py.snap index 117d693b21..58bc452dea 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S411_S411.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S411_S411.py.snap @@ -15,4 +15,3 @@ S411 XMLRPC is vulnerable to remote XML attacks 1 | import xmlrpc # S411 2 | from xmlrpc import server # S411 | ^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S412_S412.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S412_S412.py.snap index 8fc8360557..82b2d894bc 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S412_S412.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S412_S412.py.snap @@ -6,4 +6,3 @@ S412 `httpoxy` is a set of vulnerabilities that affect application code running | 1 | from twisted.web.twcgi import CGIScript # S412 | ^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S413_S413.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S413_S413.py.snap index ace5f2b1b2..9c7b5e6961 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S413_S413.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S413_S413.py.snap @@ -37,4 +37,3 @@ S413 `pycrypto` library is known to have publicly disclosed buffer overflow vuln 3 | import Crypto.PublicKey # S413 4 | from Crypto.PublicKey import RSA # S413 | ^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S415_S415.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S415_S415.py.snap index dd5ea30bb3..6f2c905efe 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S415_S415.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S415_S415.py.snap @@ -15,4 +15,3 @@ S415 An IPMI-related module is being imported. Prefer an encrypted protocol over 1 | import pyghmi # S415 2 | from pyghmi import foo # S415 | ^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S501_S501.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S501_S501.py.snap index 52cd11ea34..187c548702 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S501_S501.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S501_S501.py.snap @@ -204,4 +204,3 @@ S501 Probable use of `httpx` call with `verify=False` disabling SSL certificate 41 | httpx.AsyncClient() 42 | httpx.AsyncClient(verify=False) | ^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S506_S506.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S506_S506.py.snap index 302026e370..7259e157c4 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S506_S506.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S506_S506.py.snap @@ -93,4 +93,3 @@ S506 Probable use of unsafe loader `CBaseLoader` with `yaml.load`. Allows instan 49 | from yaml.cyaml import CBaseLoader 50 | yaml.load("{}", Loader=CBaseLoader) | ^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S508_S508.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S508_S508.py.snap index acbb09666a..b47cdd7f77 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S508_S508.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S508_S508.py.snap @@ -20,3 +20,89 @@ S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. 5 | 6 | CommunityData("public", mpModel=2) # OK | + +S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. + --> S508.py:18:46 + | +16 | import pysnmp.hlapi.auth +17 | +18 | pysnmp.hlapi.asyncio.CommunityData("public", mpModel=0) # S508 + | ^^^^^^^^^ +19 | pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 +20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508 + | + +S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. + --> S508.py:19:58 + | +18 | pysnmp.hlapi.asyncio.CommunityData("public", mpModel=0) # S508 +19 | pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 + | ^^^^^^^^^ +20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508 +21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508 + | + +S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. + --> S508.py:20:53 + | +18 | pysnmp.hlapi.asyncio.CommunityData("public", mpModel=0) # S508 +19 | pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 +20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508 + | ^^^^^^^^^ +21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508 +22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 + | + +S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. + --> S508.py:21:45 + | +19 | pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 +20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508 +21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508 + | ^^^^^^^^^ +22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 +23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508 + | + +S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. + --> S508.py:22:58 + | +20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508 +21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508 +22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 + | ^^^^^^^^^ +23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508 +24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508 + | + +S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. + --> S508.py:23:53 + | +21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508 +22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 +23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508 + | ^^^^^^^^^ +24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508 +25 | pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508 + | + +S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. + --> S508.py:24:45 + | +22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 +23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508 +24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508 + | ^^^^^^^^^ +25 | pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508 + | + +S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. + --> S508.py:25:43 + | +23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508 +24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508 +25 | pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508 + | ^^^^^^^^^ +26 | +27 | pysnmp.hlapi.asyncio.CommunityData("public", mpModel=2) # OK + | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S509_S509.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S509_S509.py.snap index c52b437891..da81c2a630 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S509_S509.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S509_S509.py.snap @@ -18,3 +18,45 @@ S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` 6 | 7 | less_insecure = UsmUserData("securityName", "authName", "privName") # OK | + +S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure. + --> S509.py:15:1 + | +13 | import pysnmp.hlapi.auth +14 | +15 | pysnmp.hlapi.asyncio.UsmUserData("user") # S509 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +16 | pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509 +17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509 + | + +S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure. + --> S509.py:16:1 + | +15 | pysnmp.hlapi.asyncio.UsmUserData("user") # S509 +16 | pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509 +18 | pysnmp.hlapi.auth.UsmUserData("user") # S509 + | + +S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure. + --> S509.py:17:1 + | +15 | pysnmp.hlapi.asyncio.UsmUserData("user") # S509 +16 | pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509 +17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +18 | pysnmp.hlapi.auth.UsmUserData("user") # S509 + | + +S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure. + --> S509.py:18:1 + | +16 | pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509 +17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509 +18 | pysnmp.hlapi.auth.UsmUserData("user") # S509 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +19 | +20 | pysnmp.hlapi.asyncio.UsmUserData("user", "authkey", "privkey") # OK + | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S601_S601.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S601_S601.py.snap index bd0d6b4e13..1da4d5f16a 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S601_S601.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S601_S601.py.snap @@ -8,4 +8,3 @@ S601 Possible shell injection via Paramiko call; check inputs are properly sanit 2 | 3 | paramiko.exec_command('something; really; unsafe') | ^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S602_S602.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S602_S602.py.snap index 4115627c2d..0e362b2544 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S602_S602.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S602_S602.py.snap @@ -167,4 +167,3 @@ S602 `subprocess` call with truthy `shell` seems safe, but may be changed in the 41 | x = 1 42 | Popen("true", shell=f"{x=}") | ^^^^^ - | 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 586ccb21b9..3a18d7ed5c 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 @@ -135,4 +135,3 @@ S603 `subprocess` call: check for execution of untrusted input 40 | (e := "echo") 41 | run(e) | ^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S604_S604.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S604_S604.py.snap index 798e916ba6..e31ad1885b 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S604_S604.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S604_S604.py.snap @@ -90,4 +90,3 @@ S604 Function call with truthy `shell` parameter identified, security issue 23 | x = 1 24 | foo(shell=f"{x=}") | ^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S605_S605.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S605_S605.py.snap index 61d315bcba..86caca420b 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S605_S605.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S605_S605.py.snap @@ -149,7 +149,6 @@ S605 Starting a process with a shell: seems safe, but may be changed in the futu 20 | subprocess.getoutput("true") 21 | subprocess.getstatusoutput("true") | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | S605 Starting a process with a shell, possible injection detected --> S605.py:26:1 @@ -179,4 +178,3 @@ S605 Starting a process with a shell, possible injection detected 27 | os.system([var_string]) 28 | os.system([var_string, ""]) | ^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S606_S606.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S606_S606.py.snap index 17361ece3f..37d7204219 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S606_S606.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S606_S606.py.snap @@ -182,4 +182,3 @@ S606 Starting a process without a shell 19 | os.spawnvpe("true") 20 | os.startfile("true") | ^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S607_S607.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S607_S607.py.snap index 07984c590e..81e9d49569 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S607_S607.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S607_S607.py.snap @@ -249,4 +249,3 @@ S607 Starting a process with a partial executable path 48 | import subprocess 49 | subprocess.run(("echo", "foo")) | ^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S609_S609.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S609_S609.py.snap index 07c3f3b970..d93f427178 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S609_S609.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S609_S609.py.snap @@ -83,4 +83,3 @@ S609 Possible wildcard injection in call due to `*` usage 17 | x = 1 18 | subprocess.Popen("chmod +w foo*", shell=f"{x=}") | ^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S611_S611.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S611_S611.py.snap index 8ef3a7c3e5..a367ef7980 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S611_S611.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S611_S611.py.snap @@ -61,4 +61,3 @@ S611 Use of `RawSQL` can lead to SQL injection vulnerabilities 12 | User.objects.annotate(val=RawSQL(sql='{}secure'.format('no'), params=[])) 13 | User.objects.annotate(val=RawSQL(params=[], sql='{}secure'.format('no'))) | ^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S701_S701.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S701_S701.py.snap index f441488f52..9b399e0259 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S701_S701.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S701_S701.py.snap @@ -51,4 +51,3 @@ S701 Using jinja2 templates with `autoescape=False` is dangerous and can lead to 28 | return 'foobar' 29 | Environment(loader=templateLoader, autoescape=fake_func()) # S701 | ^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S702_S702.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S702_S702.py.snap index ac840f237c..c93ec1982e 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S702_S702.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S702_S702.py.snap @@ -26,4 +26,3 @@ S702 Mako templates allow HTML and JavaScript rendering by default and are inher 8 | mako.template.Template("hern") 9 | template.Template("hern") | ^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S301_S301.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S301_S301.py.snap index 6718083b32..705fcbce4a 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S301_S301.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S301_S301.py.snap @@ -27,4 +27,3 @@ S301 `pickle` and modules that wrap it can be unsafe when used to deserialize un 7 | map(pickle.load, []) 8 | foo = pickle.load | ^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S307_S307.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S307_S307.py.snap index 3a040f9d23..ccce35d6f4 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S307_S307.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S307_S307.py.snap @@ -27,4 +27,3 @@ S307 Use of possibly insecure function; consider using `ast.literal_eval` 16 | map(eval, []) 17 | foo = eval | ^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S308_S308.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S308_S308.py.snap index b224ea3431..be65c82c1e 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S308_S308.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S308_S308.py.snap @@ -69,4 +69,3 @@ S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities 42 | map(mark_safe, []) 43 | foo = mark_safe | ^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S310_S310.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S310_S310.py.snap index 081c3afb49..e92475a2b2 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S310_S310.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S310_S310.py.snap @@ -6,100 +6,9 @@ source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs +linter.preview = enabled --- Summary --- -Removed: 8 +Removed: 0 Added: 2 ---- Removed --- -S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. - --> S310.py:51:1 - | -49 | # https://github.com/astral-sh/ruff/issues/21462 -50 | path = "https://example.com/data.csv" -51 | urllib.request.urlretrieve(path, "data.csv") - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -52 | url = "https://example.com/api" -53 | urllib.request.Request(url) - | - - -S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. - --> S310.py:53:1 - | -51 | urllib.request.urlretrieve(path, "data.csv") -52 | url = "https://example.com/api" -53 | urllib.request.Request(url) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ -54 | -55 | # Test resolved f-strings and concatenated string literals - | - - -S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. - --> S310.py:57:1 - | -55 | # Test resolved f-strings and concatenated string literals -56 | fstring_url = f"https://example.com/data.csv" -57 | urllib.request.urlopen(fstring_url) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -58 | urllib.request.Request(fstring_url) - | - - -S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. - --> S310.py:58:1 - | -56 | fstring_url = f"https://example.com/data.csv" -57 | urllib.request.urlopen(fstring_url) -58 | urllib.request.Request(fstring_url) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -59 | -60 | concatenated_url = "https://" + "example.com/data.csv" - | - - -S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. - --> S310.py:61:1 - | -60 | concatenated_url = "https://" + "example.com/data.csv" -61 | urllib.request.urlopen(concatenated_url) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -62 | urllib.request.Request(concatenated_url) - | - - -S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. - --> S310.py:62:1 - | -60 | concatenated_url = "https://" + "example.com/data.csv" -61 | urllib.request.urlopen(concatenated_url) -62 | urllib.request.Request(concatenated_url) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -63 | -64 | nested_concatenated = "http://" + "example.com" + "/data.csv" - | - - -S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. - --> S310.py:65:1 - | -64 | nested_concatenated = "http://" + "example.com" + "/data.csv" -65 | urllib.request.urlopen(nested_concatenated) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -66 | urllib.request.Request(nested_concatenated) - | - - -S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. - --> S310.py:66:1 - | -64 | nested_concatenated = "http://" + "example.com" + "/data.csv" -65 | urllib.request.urlopen(nested_concatenated) -66 | urllib.request.Request(nested_concatenated) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - - - --- Added --- S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. --> S310.py:46:5 diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S311_S311.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S311_S311.py.snap index 8904a2523a..34952d3c03 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S311_S311.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S311_S311.py.snap @@ -27,4 +27,3 @@ S311 Standard pseudo-random generators are not suitable for cryptographic purpos 26 | map(random.randrange, []) 27 | foo = random.randrange | ^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S506_S506.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S506_S506.py.snap index 1437a7b6e3..ef68baafe8 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S506_S506.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S506_S506.py.snap @@ -88,4 +88,3 @@ S506 Probable use of unsafe loader `CBaseLoader` with `yaml.load`. Allows instan 49 | from yaml.cyaml import CBaseLoader 50 | yaml.load("{}", Loader=CBaseLoader) | ^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S508_S508.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S508_S508.py.snap deleted file mode 100644 index f763850e17..0000000000 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S508_S508.py.snap +++ /dev/null @@ -1,104 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs ---- ---- Linter settings --- --linter.preview = disabled -+linter.preview = enabled - ---- Summary --- -Removed: 0 -Added: 8 - ---- Added --- -S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. - --> S508.py:18:46 - | -16 | import pysnmp.hlapi.auth -17 | -18 | pysnmp.hlapi.asyncio.CommunityData("public", mpModel=0) # S508 - | ^^^^^^^^^ -19 | pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 -20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508 - | - - -S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. - --> S508.py:19:58 - | -18 | pysnmp.hlapi.asyncio.CommunityData("public", mpModel=0) # S508 -19 | pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 - | ^^^^^^^^^ -20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508 -21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508 - | - - -S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. - --> S508.py:20:53 - | -18 | pysnmp.hlapi.asyncio.CommunityData("public", mpModel=0) # S508 -19 | pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 -20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508 - | ^^^^^^^^^ -21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508 -22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 - | - - -S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. - --> S508.py:21:45 - | -19 | pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 -20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508 -21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508 - | ^^^^^^^^^ -22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 -23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508 - | - - -S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. - --> S508.py:22:58 - | -20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508 -21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508 -22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 - | ^^^^^^^^^ -23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508 -24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508 - | - - -S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. - --> S508.py:23:53 - | -21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508 -22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 -23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508 - | ^^^^^^^^^ -24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508 -25 | pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508 - | - - -S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. - --> S508.py:24:45 - | -22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 -23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508 -24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508 - | ^^^^^^^^^ -25 | pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508 - | - - -S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. - --> S508.py:25:43 - | -23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508 -24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508 -25 | pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508 - | ^^^^^^^^^ -26 | -27 | pysnmp.hlapi.asyncio.CommunityData("public", mpModel=2) # OK - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S509_S509.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S509_S509.py.snap deleted file mode 100644 index 026e848351..0000000000 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S509_S509.py.snap +++ /dev/null @@ -1,56 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs ---- ---- Linter settings --- --linter.preview = disabled -+linter.preview = enabled - ---- Summary --- -Removed: 0 -Added: 4 - ---- Added --- -S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure. - --> S509.py:15:1 - | -13 | import pysnmp.hlapi.auth -14 | -15 | pysnmp.hlapi.asyncio.UsmUserData("user") # S509 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -16 | pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509 -17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509 - | - - -S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure. - --> S509.py:16:1 - | -15 | pysnmp.hlapi.asyncio.UsmUserData("user") # S509 -16 | pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509 -18 | pysnmp.hlapi.auth.UsmUserData("user") # S509 - | - - -S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure. - --> S509.py:17:1 - | -15 | pysnmp.hlapi.asyncio.UsmUserData("user") # S509 -16 | pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509 -17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -18 | pysnmp.hlapi.auth.UsmUserData("user") # S509 - | - - -S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure. - --> S509.py:18:1 - | -16 | pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509 -17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509 -18 | pysnmp.hlapi.auth.UsmUserData("user") # S509 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -19 | -20 | pysnmp.hlapi.asyncio.UsmUserData("user", "authkey", "privkey") # OK - | 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 88f1c9099c..658908efb6 100644 --- a/crates/ruff_linter/src/rules/flake8_blind_except/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_blind_except/mod.rs @@ -9,9 +9,8 @@ mod tests { use test_case::test_case; use crate::registry::Rule; - use crate::settings::types::PreviewMode; use crate::test::test_path; - use crate::{assert_diagnostics, assert_diagnostics_diff, settings}; + use crate::{assert_diagnostics, settings}; #[test_case(Rule::BlindExcept, Path::new("BLE.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { @@ -23,26 +22,4 @@ mod tests { assert_diagnostics!(snapshot, diagnostics); Ok(()) } - - #[test_case(Rule::BlindExcept, Path::new("BLE.py"))] - fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { - let snapshot = format!( - "preview_{}_{}", - rule_code.noqa_code(), - path.to_string_lossy() - ); - assert_diagnostics_diff!( - snapshot, - Path::new("flake8_blind_except").join(path).as_path(), - &settings::LinterSettings { - preview: PreviewMode::Disabled, - ..settings::LinterSettings::for_rule(rule_code) - }, - &settings::LinterSettings { - preview: PreviewMode::Enabled, - ..settings::LinterSettings::for_rule(rule_code) - }, - ); - 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 1e85224bdf..f62788d800 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 @@ -8,9 +8,7 @@ use ruff_text_size::Ranged; use crate::Violation; use crate::checkers::ast::Checker; -use crate::preview::is_ble001_exc_info_suppression_enabled; use crate::rules::flake8_logging::helpers::is_logger_method_name; -use crate::settings::LinterSettings; /// ## What it does /// Checks for `except` clauses that catch all exceptions. This includes @@ -122,11 +120,7 @@ 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, - checker.settings(), - ); + let mut visitor = LogExceptionVisitor::new(semantic, &checker.settings().logger_objects); visitor.visit_body(body); if visitor.seen() { return; @@ -191,43 +185,26 @@ impl<'a> StatementVisitor<'a> for ReraiseVisitor<'a> { } /// Returns `true` if the `exc_info` keyword argument is truthy. -fn is_exc_info_enabled( - method_name: &str, - arguments: &ast::Arguments, - semantic: &SemanticModel, - settings: &LinterSettings, -) -> bool { - if is_ble001_exc_info_suppression_enabled(settings) - || matches!(method_name, "error" | "critical") - { - arguments.find_keyword("exc_info").is_some_and(|keyword| { - Truthiness::from_expr(&keyword.value, |id| semantic.has_builtin_binding(id)).into_bool() - != Some(false) - }) - } else { - false - } +fn is_exc_info_enabled(arguments: &ast::Arguments, semantic: &SemanticModel) -> bool { + arguments.find_keyword("exc_info").is_some_and(|keyword| { + Truthiness::from_expr(&keyword.value, |id| semantic.has_builtin_binding(id)).into_bool() + != Some(false) + }) } /// A visitor to detect whether the exception was logged. struct LogExceptionVisitor<'a> { semantic: &'a SemanticModel<'a>, logger_objects: &'a [String], - settings: &'a LinterSettings, seen: bool, } impl<'a> LogExceptionVisitor<'a> { /// Create a new [`LogExceptionVisitor`] with the given exception name. - fn new( - semantic: &'a SemanticModel<'a>, - logger_objects: &'a [String], - settings: &'a LinterSettings, - ) -> Self { + fn new(semantic: &'a SemanticModel<'a>, logger_objects: &'a [String]) -> Self { Self { semantic, logger_objects, - settings, seen: false, } } @@ -257,12 +234,9 @@ impl<'a> StatementVisitor<'a> for LogExceptionVisitor<'a> { self.logger_objects, ) && match attr.as_str() { "exception" => true, - _ if is_logger_method_name(attr) => is_exc_info_enabled( - attr, - arguments, - self.semantic, - self.settings, - ), + _ if is_logger_method_name(attr) => { + is_exc_info_enabled(arguments, self.semantic) + } _ => false, } => { @@ -273,12 +247,7 @@ impl<'a> StatementVisitor<'a> for LogExceptionVisitor<'a> { |qualified_name| match qualified_name.segments() { ["logging", "exception"] => true, ["logging", method] if is_logger_method_name(method) => { - is_exc_info_enabled( - method, - arguments, - self.semantic, - self.settings, - ) + is_exc_info_enabled(arguments, self.semantic) } _ => false, }, diff --git a/crates/ruff_linter/src/rules/flake8_blind_except/snapshots/ruff_linter__rules__flake8_blind_except__tests__BLE001_BLE.py.snap b/crates/ruff_linter/src/rules/flake8_blind_except/snapshots/ruff_linter__rules__flake8_blind_except__tests__BLE001_BLE.py.snap index 5011575277..b8a84ab19b 100644 --- a/crates/ruff_linter/src/rules/flake8_blind_except/snapshots/ruff_linter__rules__flake8_blind_except__tests__BLE001_BLE.py.snap +++ b/crates/ruff_linter/src/rules/flake8_blind_except/snapshots/ruff_linter__rules__flake8_blind_except__tests__BLE001_BLE.py.snap @@ -263,53 +263,3 @@ BLE001 Do not catch blind exception: `BaseException` | ^^^^^^^^^^^^^ 221 | pass | - -BLE001 Do not catch blind exception: `Exception` - --> BLE.py:269:8 - | -267 | try: -268 | pass -269 | except Exception as e: - | ^^^^^^^^^ -270 | logging.debug("...", exc_info=e) # ok - | - -BLE001 Do not catch blind exception: `Exception` - --> BLE.py:275:8 - | -273 | try: -274 | pass -275 | except Exception: - | ^^^^^^^^^ -276 | logging.info("...", exc_info=True) # ok - | - -BLE001 Do not catch blind exception: `Exception` - --> BLE.py:280:8 - | -278 | try: -279 | pass -280 | except Exception as e: - | ^^^^^^^^^ -281 | logging.warn("...", exc_info=e) # ok - | - -BLE001 Do not catch blind exception: `Exception` - --> BLE.py:286:8 - | -284 | try: -285 | pass -286 | except Exception: - | ^^^^^^^^^ -287 | logging.warning("...", exc_info=True) # ok - | - -BLE001 Do not catch blind exception: `Exception` - --> BLE.py:292:8 - | -290 | try: -291 | pass -292 | except Exception as e: - | ^^^^^^^^^ -293 | logging.log(logging.INFO, "...", exc_info=e) # ok - | diff --git a/crates/ruff_linter/src/rules/flake8_blind_except/snapshots/ruff_linter__rules__flake8_blind_except__tests__preview_BLE001_BLE.py.snap b/crates/ruff_linter/src/rules/flake8_blind_except/snapshots/ruff_linter__rules__flake8_blind_except__tests__preview_BLE001_BLE.py.snap deleted file mode 100644 index 7fc7dd0407..0000000000 --- a/crates/ruff_linter/src/rules/flake8_blind_except/snapshots/ruff_linter__rules__flake8_blind_except__tests__preview_BLE001_BLE.py.snap +++ /dev/null @@ -1,65 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/flake8_blind_except/mod.rs ---- ---- Linter settings --- --linter.preview = disabled -+linter.preview = enabled - ---- Summary --- -Removed: 5 -Added: 0 - ---- Removed --- -BLE001 Do not catch blind exception: `Exception` - --> BLE.py:269:8 - | -267 | try: -268 | pass -269 | except Exception as e: - | ^^^^^^^^^ -270 | logging.debug("...", exc_info=e) # ok - | - - -BLE001 Do not catch blind exception: `Exception` - --> BLE.py:275:8 - | -273 | try: -274 | pass -275 | except Exception: - | ^^^^^^^^^ -276 | logging.info("...", exc_info=True) # ok - | - - -BLE001 Do not catch blind exception: `Exception` - --> BLE.py:280:8 - | -278 | try: -279 | pass -280 | except Exception as e: - | ^^^^^^^^^ -281 | logging.warn("...", exc_info=e) # ok - | - - -BLE001 Do not catch blind exception: `Exception` - --> BLE.py:286:8 - | -284 | try: -285 | pass -286 | except Exception: - | ^^^^^^^^^ -287 | logging.warning("...", exc_info=True) # ok - | - - -BLE001 Do not catch blind exception: `Exception` - --> BLE.py:292:8 - | -290 | try: -291 | pass -292 | except Exception as e: - | ^^^^^^^^^ -293 | logging.log(logging.INFO, "...", exc_info=e) # ok - | 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 d851d0b0fe..33ded4bb21 100644 --- a/crates/ruff_linter/src/rules/flake8_boolean_trap/helpers.rs +++ b/crates/ruff_linter/src/rules/flake8_boolean_trap/helpers.rs @@ -8,7 +8,7 @@ use crate::checkers::ast::Checker; use crate::settings::LinterSettings; /// Returns `true` if a function call is allowed to use a boolean trap. -pub(super) fn is_allowed_func_call(name: &str) -> bool { +fn is_allowed_func_call(name: &str) -> bool { matches!( name, "__setattr__" @@ -51,10 +51,7 @@ pub(super) fn is_allowed_func_call(name: &str) -> bool { } /// Returns `true` if a call is semantically allowed to use a boolean trap. -pub(super) fn is_semantically_allowed_func_call( - call: &ast::ExprCall, - semantic: &SemanticModel, -) -> bool { +fn is_semantically_allowed_func_call(call: &ast::ExprCall, semantic: &SemanticModel) -> bool { semantic .resolve_qualified_name(call.func.as_ref()) .is_some_and(|qualified_name| { @@ -66,7 +63,7 @@ pub(super) fn is_semantically_allowed_func_call( } /// Returns `true` if a call is allowed by the user to use a boolean trap. -pub(super) fn is_user_allowed_func_call( +fn is_user_allowed_func_call( call: &ast::ExprCall, semantic: &SemanticModel, settings: &LinterSettings, @@ -88,78 +85,44 @@ pub(super) fn is_user_allowed_func_call( /// This only includes operators, i.e., functions that are usually not called directly. /// /// See: -pub(super) fn is_operator_method(name: &str) -> bool { - matches!( - name, - "__contains__" // in - // item access ([]) - | "__getitem__" // [] - | "__setitem__" // []= - | "__delitem__" // del [] - // addition (+) - | "__add__" // + - | "__radd__" // + - | "__iadd__" // += - // subtraction (-) - | "__sub__" // - - | "__rsub__" // - - | "__isub__" // -= - // multiplication (*) - | "__mul__" // * - | "__rmul__" // * - | "__imul__" // *= - // division (/) - | "__truediv__" // / - | "__rtruediv__" // / - | "__itruediv__" // /= - // floor division (//) - | "__floordiv__" // // - | "__rfloordiv__" // // - | "__ifloordiv__" // //= - // remainder (%) - | "__mod__" // % - | "__rmod__" // % - | "__imod__" // %= - // exponentiation (**) - | "__pow__" // ** - | "__rpow__" // ** - | "__ipow__" // **= - // left shift (<<) - | "__lshift__" // << - | "__rlshift__" // << - | "__ilshift__" // <<= - // right shift (>>) - | "__rshift__" // >> - | "__rrshift__" // >> - | "__irshift__" // >>= - // matrix multiplication (@) - | "__matmul__" // @ - | "__rmatmul__" // @ - | "__imatmul__" // @= - // meet (&) - | "__and__" // & - | "__rand__" // & - | "__iand__" // &= - // join (|) - | "__or__" // | - | "__ror__" // | - | "__ior__" // |= - // xor (^) - | "__xor__" // ^ - | "__rxor__" // ^ - | "__ixor__" // ^= - // comparison (>, <, >=, <=, ==, !=) - | "__gt__" // > - | "__lt__" // < - | "__ge__" // >= - | "__le__" // <= - | "__eq__" // == - | "__ne__" // != - // unary operators (included for completeness) - | "__pos__" // + - | "__neg__" // - - | "__invert__" // ~ - ) +fn is_operator_method(name: &str) -> bool { + match name { + // Membership (`in`). + "__contains__" => true, + // Item access (`[]`, `[]=`, and `del []`). + "__getitem__" | "__setitem__" | "__delitem__" => true, + // Addition (`+` and `+=`). + "__add__" | "__radd__" | "__iadd__" => true, + // Subtraction (`-` and `-=`). + "__sub__" | "__rsub__" | "__isub__" => true, + // Multiplication (`*` and `*=`). + "__mul__" | "__rmul__" | "__imul__" => true, + // Division (`/` and `/=`). + "__truediv__" | "__rtruediv__" | "__itruediv__" => true, + // Floor division (`//` and `//=`). + "__floordiv__" | "__rfloordiv__" | "__ifloordiv__" => true, + // Remainder (`%` and `%=`). + "__mod__" | "__rmod__" | "__imod__" => true, + // Exponentiation (`**` and `**=`). + "__pow__" | "__rpow__" | "__ipow__" => true, + // Left shift (`<<` and `<<=`). + "__lshift__" | "__rlshift__" | "__ilshift__" => true, + // Right shift (`>>` and `>>=`). + "__rshift__" | "__rrshift__" | "__irshift__" => true, + // Matrix multiplication (`@` and `@=`). + "__matmul__" | "__rmatmul__" | "__imatmul__" => true, + // Meet (`&` and `&=`). + "__and__" | "__rand__" | "__iand__" => true, + // Join (`|` and `|=`). + "__or__" | "__ror__" | "__ior__" => true, + // Exclusive-or (`^` and `^=`). + "__xor__" | "__rxor__" | "__ixor__" => true, + // Comparison (`>`, `<`, `>=`, `<=`, `==`, and `!=`). + "__gt__" | "__lt__" | "__ge__" | "__le__" | "__eq__" | "__ne__" => true, + // Unary operators (`+`, `-`, and `~`), included for completeness. + "__pos__" | "__neg__" | "__invert__" => true, + _ => false, + } } /// Returns `true` if a function definition is allowed to use a boolean trap. diff --git a/crates/ruff_linter/src/rules/flake8_boolean_trap/snapshots/ruff_linter__rules__flake8_boolean_trap__tests__FBT003_FBT.py.snap b/crates/ruff_linter/src/rules/flake8_boolean_trap/snapshots/ruff_linter__rules__flake8_boolean_trap__tests__FBT003_FBT.py.snap index 667df5dbbb..cb13d4d41b 100644 --- a/crates/ruff_linter/src/rules/flake8_boolean_trap/snapshots/ruff_linter__rules__flake8_boolean_trap__tests__FBT003_FBT.py.snap +++ b/crates/ruff_linter/src/rules/flake8_boolean_trap/snapshots/ruff_linter__rules__flake8_boolean_trap__tests__FBT003_FBT.py.snap @@ -36,7 +36,6 @@ FBT003 Boolean positional value in function call | 123 | settings(True) | ^^^^ - | FBT003 Boolean positional value in function call --> FBT.py:147:20 @@ -65,4 +64,3 @@ FBT003 Boolean positional value in function call 158 | class Settings(BaseSettings): 159 | foo: bool = Field(True, exclude=True) | ^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_boolean_trap/snapshots/ruff_linter__rules__flake8_boolean_trap__tests__extend_allowed_callable.snap b/crates/ruff_linter/src/rules/flake8_boolean_trap/snapshots/ruff_linter__rules__flake8_boolean_trap__tests__extend_allowed_callable.snap index 135c26367d..8d7b271df0 100644 --- a/crates/ruff_linter/src/rules/flake8_boolean_trap/snapshots/ruff_linter__rules__flake8_boolean_trap__tests__extend_allowed_callable.snap +++ b/crates/ruff_linter/src/rules/flake8_boolean_trap/snapshots/ruff_linter__rules__flake8_boolean_trap__tests__extend_allowed_callable.snap @@ -36,4 +36,3 @@ FBT003 Boolean positional value in function call | 123 | settings(True) | ^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/mod.rs b/crates/ruff_linter/src/rules/flake8_bugbear/mod.rs index c99e89b4c9..3a4fe913b3 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/mod.rs @@ -17,7 +17,6 @@ mod tests { use crate::settings::LinterSettings; use crate::test::{test_path, test_snippet}; - use crate::settings::types::PreviewMode; use ruff_python_ast::PythonVersion; #[test_case(Rule::AbstractBaseClassWithoutAbstractMethod, Path::new("B024.py"))] @@ -116,11 +115,9 @@ mod tests { ); let diagnostics = test_path( Path::new("flake8_bugbear").join(path).as_path(), - &LinterSettings { - preview: PreviewMode::Enabled, - unresolved_target_version: PythonVersion::PY314.into(), - ..LinterSettings::for_rule(rule_code) - }, + &LinterSettings::for_rule(rule_code) + .with_preview_mode() + .with_target_version(PythonVersion::PY314), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) @@ -155,10 +152,7 @@ mod tests { ); let diagnostics = test_path( Path::new("flake8_bugbear").join(path).as_path(), - &LinterSettings { - unresolved_target_version: target_version.into(), - ..LinterSettings::for_rule(rule_code) - }, + &LinterSettings::for_rule(rule_code).with_target_version(target_version), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) 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 789c3065bf..5c0f5dc5e2 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 @@ -71,7 +71,7 @@ fn assertion_error(msg: Option<&Expr>) -> Stmt { range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, is_cast: false, is_checked_cast: false, 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 cc2c9a3750..3c54c46abe 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 @@ -110,7 +110,7 @@ pub(crate) fn assert_raises_exception(checker: &Checker, items: &[WithItem]) { let Expr::Call(ast::ExprCall { func, arguments, - range: _, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, @@ -133,18 +133,16 @@ pub(crate) fn assert_raises_exception(checker: &Checker, items: &[WithItem]) { } /// B017 (call form) -pub(crate) fn assert_raises_exception_call( - checker: &Checker, - ast::ExprCall { +pub(crate) fn assert_raises_exception_call(checker: &Checker, call: &ast::ExprCall) { + let ast::ExprCall { func, arguments, - range, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, is_string_tag: _, - }: &ast::ExprCall, -) { + } = call; let semantic = checker.semantic(); if arguments.args.len() < 2 && arguments.find_argument("func", 1).is_none() { @@ -152,6 +150,6 @@ pub(crate) fn assert_raises_exception_call( } if let Some(exception) = detect_blind_exception(semantic, func.as_ref(), arguments) { - checker.report_diagnostic(AssertRaisesException { exception }, *range); + checker.report_diagnostic(AssertRaisesException { exception }, call.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 4091b13c80..8b7a75b39b 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,6 +1,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::ExprCall; use ruff_python_ast::PythonVersion; +use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::rules::flake8_bugbear::helpers::is_infinite_iterable; @@ -94,5 +95,5 @@ pub(crate) fn batched_without_explicit_strict(checker: &Checker, call: &ExprCall return; } - checker.report_diagnostic(BatchedWithoutExplicitStrict, call.range); + checker.report_diagnostic(BatchedWithoutExplicitStrict, call.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 5a0e2025b4..1652670db7 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 @@ -79,13 +79,13 @@ pub(crate) fn class_as_data_structure(checker: &Checker, class_def: &ast::StmtCl // skip `self` .skip(1) .all(|param| param.annotation().is_some() && !param.is_variadic()) - && (func_def.parameters.kwonlyargs.is_empty() || checker.target_version() >= PythonVersion::PY310) - // `__init__` should not have complicated logic in it - // only assignments - && func_def - .body - .iter() - .all(is_simple_assignment_to_attribute) + && (func_def.parameters.kwonlyargs.is_empty() + || checker.target_version() >= PythonVersion::PY310) + && ( + // `__init__` should not have complicated logic in it + // only assignments + func_def.body.iter().all(is_simple_assignment_to_attribute) + ) { has_dunder_init = true; } @@ -94,8 +94,8 @@ pub(crate) fn class_as_data_structure(checker: &Checker, class_def: &ast::StmtCl } } // Ignore class variables - ast::Stmt::Assign(_) | ast::Stmt::AnnAssign(_) | - // and expressions (e.g. string literals) + ast::Stmt::Assign(_) | ast::Stmt::AnnAssign(_) => {} + // Ignore expressions (e.g. string literals) ast::Stmt::Expr(_) => {} _ => { // Bail for anything else - e.g. nested classes 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 0609669a68..2b4a6ab243 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 @@ -43,9 +43,13 @@ impl Violation for ExceptWithEmptyTuple { #[derive_message_formats] fn message(&self) -> String { if self.is_star { - "Using `except* ():` with an empty tuple does not catch anything; add exceptions to handle".to_string() + "Using `except* ():` with an empty tuple does not catch anything; \ + add exceptions to handle" + .to_string() } else { - "Using `except ():` with an empty tuple does not catch anything; add exceptions to handle".to_string() + "Using `except ():` with an empty tuple does not catch anything; \ + add exceptions to handle" + .to_string() } } } 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 bfccc9f0d6..305249b7bc 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 @@ -37,7 +37,10 @@ pub(crate) struct FStringDocstring; impl Violation for FStringDocstring { #[derive_message_formats] fn message(&self) -> String { - "f-string used as docstring. Python will interpret this as a joined string, rather than a docstring.".to_string() + "f-string used as docstring. \ + Python will interpret this as a joined string, \ + rather than a docstring." + .to_string() } } 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 47b57d56fd..a0c1f3473b 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 @@ -72,10 +72,15 @@ impl Violation for FunctionCallInDefaultArgument { 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" + "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() + "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() } } } 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 1cc2069ebb..d3a36c37a6 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 @@ -188,15 +188,14 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> { return; } + // Mark `return lambda: x` as safe. Stmt::Return(ast::StmtReturn { value: Some(value), range: _, node_index: _, - }) - // Mark `return lambda: x` as safe. - if value.is_lambda_expr() => { - self.safe_functions.push(value); - } + }) if value.is_lambda_expr() => { + self.safe_functions.push(value); + } _ => {} } visitor::walk_stmt(self, stmt); @@ -207,7 +206,7 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> { Expr::Call(ast::ExprCall { func, arguments, - range: _, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, 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 269e7b0598..f7826e1bfe 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 @@ -73,28 +73,27 @@ pub(crate) fn loop_iterator_mutation(checker: &Checker, stmt_for: &StmtFor) { // Ex) Given, `for item in items:`, `item` is the index and `items` is the iterable. (&**target, &**target, &**iter) } + // Ex) Given `for i, item in enumerate(items):`, `i` is the index and `items` is the + // iterable. Expr::Call(ExprCall { func, arguments, .. - }) - // Ex) Given `for i, item in enumerate(items):`, `i` is the index and `items` is the - // iterable. - if checker.semantic().match_builtin_expr(func, "enumerate") => { - // Ex) `items` - let Some(iter) = arguments.args.first() else { - return; - }; - - let Expr::Tuple(ExprTuple { elts, .. }) = &**target else { - return; - }; - - let [index, target] = elts.as_slice() else { - return; - }; - - // Ex) `i` - (index, target, iter) - } + }) if checker.semantic().match_builtin_expr(func, "enumerate") => { + // Ex) `items` + let Some(iter) = arguments.args.first() else { + return; + }; + + let Expr::Tuple(ExprTuple { elts, .. }) = &**target else { + return; + }; + + let [index, target] = elts.as_slice() else { + return; + }; + + // Ex) `i` + (index, target, iter) + } _ => { return; } @@ -442,10 +441,7 @@ impl<'a> Visitor<'a> for LoopMutationsVisitor<'a> { // Handle the `elif` and `else` branches. for clause in elif_else_clauses { self.enter_new_branch(); - if let Some(test) = &clause.test { - self.visit_expr(test); - } - self.visit_body(&clause.body); + self.visit_elif_else_clause(clause); self.merge_branch_into(saved_branch); } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/map_without_explicit_strict.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/map_without_explicit_strict.rs index a8b2ad601c..f9bc46860c 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/map_without_explicit_strict.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/map_without_explicit_strict.rs @@ -67,10 +67,9 @@ pub(crate) fn map_without_explicit_strict(checker: &Checker, call: &ast::ExprCal if semantic.match_builtin_expr(&call.func, "map") && call.arguments.find_keyword("strict").is_none() && ( - // at least 2 iterables (+ 1 function) + // at least 2 iterables (+ 1 function), or a starred argument. call.arguments.args.len() >= 3 - // or a starred argument - || call.arguments.args.iter().any(ast::Expr::is_starred_expr) + || call.arguments.args.iter().any(ast::Expr::is_starred_expr) ) && !any_infinite_iterables(call.arguments.args.iter().skip(1), semantic) { 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 0cf1b3e1ed..ff8a6e329e 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 @@ -40,7 +40,9 @@ pub(crate) struct ReuseOfGroupbyGenerator; impl Violation for ReuseOfGroupbyGenerator { #[derive_message_formats] fn message(&self) -> String { - "Using the generator returned from `itertools.groupby()` more than once will do nothing on the second usage".to_string() + "Using the generator returned from `itertools.groupby()` more than once \ + will do nothing on the second usage" + .to_string() } } 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 db71c7b2fb..475a920b2c 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 @@ -57,14 +57,11 @@ impl AlwaysFixableViolation for ZipWithoutExplicitStrict { pub(crate) fn zip_without_explicit_strict(checker: &Checker, call: &ast::ExprCall) { let semantic = checker.semantic(); + // any call to `zip()` with at least 2 iterables, or a starred argument. if semantic.match_builtin_expr(&call.func, "zip") && call.arguments.find_keyword("strict").is_none() - && ( - // at least 2 iterables - call.arguments.args.len() >= 2 - // or a starred argument - || call.arguments.args.iter().any(ast::Expr::is_starred_expr) - ) + && (call.arguments.args.len() >= 2 + || call.arguments.args.iter().any(ast::Expr::is_starred_expr)) && !any_infinite_iterables(call.arguments.args.iter(), semantic) { checker diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B002_B002.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B002_B002.py.snap index 891ffb6e4f..d60a5fb596 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B002_B002.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B002_B002.py.snap @@ -27,7 +27,6 @@ B002 Python does not support the unary prefix increment operator (`++`) 23 | def this_is_buggy_too(n): 24 | return ++n, --n | ^^^ - | B002 Python does not support the unary prefix decrement operator (`--`) --> B002.py:24:17 @@ -35,4 +34,3 @@ B002 Python does not support the unary prefix decrement operator (`--`) 23 | def this_is_buggy_too(n): 24 | return ++n, --n | ^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B006_B006_4.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B006_B006_4.py.snap index 987740d098..33bd877748 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B006_B006_4.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B006_B006_4.py.snap @@ -5,8 +5,8 @@ B006 [*] Do not use mutable data structures for argument defaults --> B006_4.py:7:26 | 6 | class FormFeedIndent: -7 | def __init__(self, a=[]): - | ^^ +7 | ␌ def __init__(self, a=[]): + | ^^ 8 | print(a) | help: Replace with `None`; initialize within function diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B006_B006_5.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B006_B006_5.py.snap index 1af32ce6af..018c6e39f4 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B006_B006_5.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B006_B006_5.py.snap @@ -267,7 +267,6 @@ B006 Do not use mutable data structures for argument defaults | 67 | def import_module_wrong(value: dict[str, str] = {}): import os | ^^ - | help: Replace with `None`; initialize within function B006 Do not use mutable data structures for argument defaults @@ -275,7 +274,6 @@ B006 Do not use mutable data structures for argument defaults | 70 | def import_module_wrong(value: dict[str, str] = {}): import os; import sys | ^^ - | help: Replace with `None`; initialize within function B006 Do not use mutable data structures for argument defaults diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B006_B006_B008.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B006_B006_B008.py.snap index 9ee2465a7f..d4e71b5232 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B006_B006_B008.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B006_B006_B008.py.snap @@ -79,7 +79,6 @@ B006 Do not use mutable data structures for argument defaults 81 | 82 | def single_line_func_wrong(value = {}): ... | ^^ - | help: Replace with `None`; initialize within function B006 [*] Do not use mutable data structures for argument defaults @@ -149,14 +148,14 @@ note: This is an unsafe fix and may change runtime behavior B006 [*] Do not use mutable data structures for argument defaults --> B006_B008.py:102:46 | -101 | # N.B. we're also flagging the function call in the comprehension +101 | # B006 still flags mutable comprehension defaults. 102 | def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]): | ^^^^^^^^^^^^^^^^^^^^^^^^ 103 | pass | help: Replace with `None`; initialize within function | -101 | # N.B. we're also flagging the function call in the comprehension +101 | # B006 still flags mutable comprehension defaults. - def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]): 102 + def list_comprehension_also_not_okay(default=None): 103 | pass diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B008_B006_B008.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B008_B006_B008.py.snap index edaaadb944..3fbd47e194 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B008_B006_B008.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B008_B006_B008.py.snap @@ -1,31 +1,6 @@ --- source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs --- -B008 Do not perform function call `range` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable - --> B006_B008.py:102:61 - | -101 | # N.B. we're also flagging the function call in the comprehension -102 | def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]): - | ^^^^^^^^ -103 | pass - | - -B008 Do not perform function call `range` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable - --> B006_B008.py:106:64 - | -106 | def dict_comprehension_also_not_okay(default={i: i**2 for i in range(3)}): - | ^^^^^^^^ -107 | pass - | - -B008 Do not perform function call `range` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable - --> B006_B008.py:110:60 - | -110 | def set_comprehension_also_not_okay(default={i**2 for i in range(3)}): - | ^^^^^^^^ -111 | pass - | - B008 Do not perform function call `time.time` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable --> B006_B008.py:126:39 | 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 f80ae80ec2..9ae70121a8 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 @@ -260,7 +260,6 @@ B009 [*] Do not call `getattr` with a constant attribute value. It is not any sa 34 | / getattr("foo" 35 | | "bar", "real") | |______________________^ - | help: Replace `getattr` with attribute access | 33 | getattr(x + y, "real") @@ -352,7 +351,6 @@ B009 [*] Do not call `getattr` with a constant attribute value. It is not any sa 87 | | "foo", 88 | | ) | |_^ - | help: Replace `getattr` with attribute access | 83 | diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B010_B009_B010.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B010_B009_B010.py.snap index 0a1bac254f..b9036308d2 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B010_B009_B010.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B010_B009_B010.py.snap @@ -114,7 +114,6 @@ B010 [*] Do not call `setattr` with a constant attribute value. It is not any sa 80 | getattr(foo, "ſ") 81 | setattr(foo, "ſ", 1) | ^^^^^^^^^^^^^^^^^^^^ - | help: Replace `setattr` with assignment | 80 | getattr(foo, "ſ") diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B011_B011.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B011_B011.py.snap index 0eed584e82..60c7697ffe 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B011_B011.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B011_B011.py.snap @@ -26,7 +26,6 @@ B011 [*] Do not `assert False` (`python -O` removes these calls), raise `Asserti 9 | assert 1 != 2, "message" 10 | assert False, "message" | ^^^^^ - | help: Replace `assert False` | 9 | assert 1 != 2, "message" diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B012_B012.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B012_B012.py.snap index 91083d5070..b85072993e 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B012_B012.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B012_B012.py.snap @@ -8,7 +8,6 @@ B012 `return` inside `finally` blocks cause exceptions to be silenced 4 | finally: 5 | return # warning | ^^^^^^ - | B012 `return` inside `finally` blocks cause exceptions to be silenced --> B012.py:13:13 @@ -17,7 +16,6 @@ B012 `return` inside `finally` blocks cause exceptions to be silenced 12 | if 1 + 0 == 2 - 1: 13 | return # warning | ^^^^^^ - | B012 `return` inside `finally` blocks cause exceptions to be silenced --> B012.py:21:13 @@ -81,7 +79,6 @@ B012 `return` inside `finally` blocks cause exceptions to be silenced 93 | while True: 94 | return # warning | ^^^^^^ - | B012 `continue` inside `finally` blocks cause exceptions to be silenced --> B012.py:101:9 @@ -101,7 +98,6 @@ B012 `break` inside `finally` blocks cause exceptions to be silenced 106 | finally: 107 | break # warning | ^^^^^ - | B012 `break` inside `finally` blocks cause exceptions to be silenced --> B012.py:118:17 @@ -110,4 +106,3 @@ B012 `break` inside `finally` blocks cause exceptions to be silenced 117 | case 0, *x: 118 | break # warning | ^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B015_B015.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B015_B015.py.snap index 3c63b71912..a0dbf0f3cc 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B015_B015.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B015_B015.py.snap @@ -19,7 +19,6 @@ B015 Pointless comparison. Did you mean to assign a value? Otherwise, prepend `a 6 | 7 | 1 in (1, 2) | ^^^^^^^^^^^ - | B015 Pointless comparison at end of function scope. Did you mean to return the expression result? --> B015.py:17:5 @@ -28,7 +27,6 @@ B015 Pointless comparison at end of function scope. Did you mean to return the e 16 | 17 | 1 in (1, 2) | ^^^^^^^^^^^ - | B015 Pointless comparison. Did you mean to assign a value? Otherwise, prepend `assert` or remove it. --> B015.py:21:5 @@ -45,4 +43,3 @@ B015 Pointless comparison. Did you mean to assign a value? Otherwise, prepend `a 28 | class TestClass: 29 | 1 == 1 | ^^^^^^ - | 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 index 5b0767019c..3b2288b445 100644 --- 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 @@ -18,7 +18,6 @@ B017 Do not assert blind exception: `BaseException` 20 | self.assertRaises(Exception, something_else) 21 | self.assertRaises(BaseException, something_else) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | B017 Do not assert blind exception: `Exception` --> B017_1.py:25:5 diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B018_B018.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B018_B018.py.snap index 6013b01e3f..25b0c14ae2 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B018_B018.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B018_B018.py.snap @@ -106,7 +106,6 @@ B018 Found useless expression. Either assign it to a variable or remove it. 19 | {1, 2} # set 20 | {"foo": "bar"} # dict | ^^^^^^^^^^^^^^ - | B018 Found useless expression. Either assign it to a variable or remove it. --> B018.py:24:5 @@ -125,7 +124,6 @@ B018 Found useless expression. Either assign it to a variable or remove it. 26 | "str" 27 | 1 | ^ - | B018 Found useless expression. Either assign it to a variable or remove it. --> B018.py:39:5 @@ -232,7 +230,6 @@ B018 Found useless expression. Either assign it to a variable or remove it. 47 | {1, 2} # set 48 | {"foo": "bar"} # dict | ^^^^^^^^^^^^^^ - | B018 Found useless expression. Either assign it to a variable or remove it. --> B018.py:52:5 @@ -251,7 +248,6 @@ B018 Found useless expression. Either assign it to a variable or remove it. 54 | "str" 55 | 3 | ^ - | B018 Found useless expression. Either assign it to a variable or remove it. --> B018.py:63:5 @@ -280,4 +276,3 @@ B018 Found useless expression. Either assign it to a variable or remove it. 64 | object().__class__ # Attribute (raise) 65 | "foo" + "bar" # BinOp (raise) | ^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B018_B018_basedpython.by.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B018_B018_basedpython.by.snap index 513f080c80..e3fcccbc8c 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B018_B018_basedpython.by.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B018_B018_basedpython.by.snap @@ -28,4 +28,3 @@ B018 Found useless expression. Either assign it to a variable or remove it. 28 | 1 # B018 29 | x.attr # B018 | ^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B021_B021.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B021_B021.py.snap index fa9f286493..4de66c82bc 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B021_B021.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B021_B021.py.snap @@ -19,7 +19,6 @@ B021 f-string used as docstring. Python will interpret this as a joined string, 13 | def foo2(): 14 | f"""hello {VARIABLE}!""" | ^^^^^^^^^^^^^^^^^^^^^^^^ - | B021 f-string used as docstring. Python will interpret this as a joined string, rather than a docstring. --> B021.py:22:5 @@ -27,7 +26,6 @@ B021 f-string used as docstring. Python will interpret this as a joined string, 21 | class bar2: 22 | f"""hello {VARIABLE}!""" | ^^^^^^^^^^^^^^^^^^^^^^^^ - | B021 f-string used as docstring. Python will interpret this as a joined string, rather than a docstring. --> B021.py:30:5 @@ -35,7 +33,6 @@ B021 f-string used as docstring. Python will interpret this as a joined string, 29 | def foo2(): 30 | f"""hello {VARIABLE}!""" | ^^^^^^^^^^^^^^^^^^^^^^^^ - | B021 f-string used as docstring. Python will interpret this as a joined string, rather than a docstring. --> B021.py:38:5 @@ -43,7 +40,6 @@ B021 f-string used as docstring. Python will interpret this as a joined string, 37 | class bar2: 38 | f"""hello {VARIABLE}!""" | ^^^^^^^^^^^^^^^^^^^^^^^^ - | B021 f-string used as docstring. Python will interpret this as a joined string, rather than a docstring. --> B021.py:46:5 @@ -51,7 +47,6 @@ B021 f-string used as docstring. Python will interpret this as a joined string, 45 | def foo2(): 46 | f"hello {VARIABLE}!" | ^^^^^^^^^^^^^^^^^^^^ - | B021 f-string used as docstring. Python will interpret this as a joined string, rather than a docstring. --> B021.py:54:5 @@ -59,7 +54,6 @@ B021 f-string used as docstring. Python will interpret this as a joined string, 53 | class bar2: 54 | f"hello {VARIABLE}!" | ^^^^^^^^^^^^^^^^^^^^ - | B021 f-string used as docstring. Python will interpret this as a joined string, rather than a docstring. --> B021.py:62:5 @@ -67,7 +61,6 @@ B021 f-string used as docstring. Python will interpret this as a joined string, 61 | def foo2(): 62 | f"hello {VARIABLE}!" | ^^^^^^^^^^^^^^^^^^^^ - | B021 f-string used as docstring. Python will interpret this as a joined string, rather than a docstring. --> B021.py:70:5 @@ -75,7 +68,6 @@ B021 f-string used as docstring. Python will interpret this as a joined string, 69 | class bar2: 70 | f"hello {VARIABLE}!" | ^^^^^^^^^^^^^^^^^^^^ - | B021 f-string used as docstring. Python will interpret this as a joined string, rather than a docstring. --> B021.py:74:5 diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B023_B023.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B023_B023.py.snap index 034c5f4f03..561c4bbf2b 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B023_B023.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B023_B023.py.snap @@ -70,7 +70,6 @@ B023 Function definition does not bind loop variable `x` 30 | gn = (lambda: x for x in range(2)) # error 31 | dt = {x: lambda: x for x in range(2)} # error | ^ - | B023 Function definition does not bind loop variable `x` --> B023.py:40:34 @@ -90,7 +89,6 @@ B023 Function definition does not bind loop variable `x` 41 | 42 | [lambda: x async for x in pointless_async_iterable()] # error | ^ - | B023 Function definition does not bind loop variable `a` --> B023.py:50:30 @@ -143,7 +141,6 @@ B023 Function definition does not bind loop variable `j` 60 | for k in range(3): 61 | lambda: j * k # error | ^ - | B023 Function definition does not bind loop variable `k` --> B023.py:61:21 @@ -152,7 +149,6 @@ B023 Function definition does not bind loop variable `k` 60 | for k in range(3): 61 | lambda: j * k # error | ^ - | B023 Function definition does not bind loop variable `l` --> B023.py:68:10 @@ -171,7 +167,6 @@ B023 Function definition does not bind loop variable `i` 81 | for i in range(3): 82 | lambda: f"{i}" | ^ - | B023 Function definition does not bind loop variable `x` --> B023.py:117:24 @@ -243,4 +238,3 @@ B023 Function definition does not bind loop variable `i` 173 | if False: 174 | return [lambda: i for i in range(3)] # error | ^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B026_B026.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B026_B026.py.snap index 3acc613e93..98e03ccfef 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B026_B026.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B026_B026.py.snap @@ -72,4 +72,3 @@ B026 Star-arg unpacking after a keyword argument is strongly discouraged 20 | foo(bam="bam", *["bar"], *["baz"]) 21 | foo(*["bar"], bam="bam", *["baz"]) | ^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B027_B027_basedpython.by.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B027_B027_basedpython.by.snap index 11888ac2c6..e44d1a0057 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B027_B027_basedpython.by.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B027_B027_basedpython.by.snap @@ -8,4 +8,3 @@ B027 `Animal.speak` is an empty method in an abstract base class, but has no abs 12 | class Animal(ABC): 13 | def speak(self) -> str: ... | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B029_B029.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B029_B029.py.snap index 2feb56e38b..74eea51d40 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B029_B029.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B029_B029.py.snap @@ -45,4 +45,3 @@ B029 Using `except* ():` with an empty tuple does not catch anything; add except 23 | / except* () as e: 24 | | pass | |________^ - | 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 50698965e3..b0854c5b61 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 @@ -30,7 +30,6 @@ B031 Using the generator returned from `itertools.groupby()` more than once will 32 | collect_shop_items("Jane", section_items) 33 | collect_shop_items("Joe", section_items) # B031 | ^^^^^^^^^^^^^ - | B031 Using the generator returned from `itertools.groupby()` more than once will do nothing on the second usage --> B031.py:40:37 diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B033_B033.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B033_B033.py.snap index d3a538bc42..e0c5f128b3 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B033_B033.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B033_B033.py.snap @@ -2,7 +2,7 @@ source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs --- B033 [*] Sets should not contain duplicate item `"value1"` - --> B033.py:4:18 + --> B033.py:4:35 | 2 | # Errors. 3 | ### @@ -22,7 +22,7 @@ help: Remove duplicate item | B033 [*] Sets should not contain duplicate item `1` - --> B033.py:5:18 + --> B033.py:5:21 | 3 | ### 4 | incorrect_set = {"value1", 23, 5, "value1"} @@ -42,7 +42,7 @@ help: Remove duplicate item | B033 [*] Sets should not contain duplicate item `"value1"` - --> B033.py:7:5 + --> B033.py:10:5 | 5 | incorrect_set = {1, 1, 2} 6 | incorrect_set_multiline = { @@ -63,7 +63,7 @@ help: Remove duplicate item | B033 [*] Sets should not contain duplicate item `1` - --> B033.py:13:18 + --> B033.py:13:21 | 11 | # B033 12 | } @@ -83,7 +83,7 @@ help: Remove duplicate item | B033 [*] Sets should not contain duplicate item `1` - --> B033.py:14:18 + --> B033.py:14:21 | 12 | } 13 | incorrect_set = {1, 1} @@ -103,7 +103,7 @@ help: Remove duplicate item | B033 [*] Sets should not contain duplicate item `1` - --> B033.py:15:21 + --> B033.py:15:24 | 13 | incorrect_set = {1, 1} 14 | incorrect_set = {1, 1,} @@ -123,7 +123,7 @@ help: Remove duplicate item | B033 [*] Sets should not contain duplicate item `1` - --> B033.py:16:21 + --> B033.py:16:24 | 14 | incorrect_set = {1, 1,} 15 | incorrect_set = {0, 1, 1,} @@ -143,7 +143,7 @@ help: Remove duplicate item | B033 [*] Sets should not contain duplicate item `1` - --> B033.py:19:5 + --> B033.py:20:5 | 17 | incorrect_set = { 18 | 0, @@ -162,7 +162,7 @@ help: Remove duplicate item | B033 [*] Sets should not contain duplicate items, but `False` and `0` has the same value - --> B033.py:22:18 + --> B033.py:22:28 | 20 | 1, 21 | } @@ -182,7 +182,7 @@ help: Remove duplicate item | B033 [*] Sets should not contain duplicate item `"value1"` - --> B033.py:24:5 + --> B033.py:27:5 | 22 | incorrect_set = {False, 1, 0} 23 | incorrect_set_multiline_with_comment = { diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B035_B035.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B035_B035.py.snap index 9d25a4c1b6..87e78384e6 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B035_B035.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B035_B035.py.snap @@ -94,4 +94,3 @@ B035 Dictionary comprehension uses static key: `tokens` 24 | {constant[0]: value.upper() for value in data} 25 | {tokens: token for token in tokens} | ^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B039_B039.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B039_B039.py.snap index 07fdb4b2a7..1656b5c04d 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B039_B039.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B039_B039.py.snap @@ -159,5 +159,4 @@ B039 Do not use mutable data structures for `ContextVar` defaults 39 | def baz(): ... 40 | ContextVar("cv", default=baz()) | ^^^^^ - | help: Replace with `None`; initialize with `.set()`` diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B043_B043.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B043_B043.py.snap index 4748d849bb..a188337ef7 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B043_B043.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B043_B043.py.snap @@ -139,7 +139,6 @@ B043 [*] Do not call `delattr` with a constant attribute value. It is not any sa 37 | import builtins 38 | builtins.delattr(foo, "bar") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace `delattr` with `del` statement | 37 | import builtins diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B901_B901.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B901_B901.py.snap index 666db24c08..c8ded0415e 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B901_B901.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B901_B901.py.snap @@ -28,7 +28,6 @@ B901 Using `yield` and `return {value}` in a generator function can lead to conf 55 | def broken3(): 56 | return (yield from []) | ^^^^^^^^^^^^^^^^^^^^^^ - | B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior --> B901.py:61:5 @@ -37,7 +36,6 @@ B901 Using `yield` and `return {value}` in a generator function can lead to conf 60 | x = yield from [] 61 | return x | ^^^^^^^^ - | B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior --> B901.py:72:5 @@ -45,7 +43,6 @@ B901 Using `yield` and `return {value}` in a generator function can lead to conf 71 | inner((yield from [])) 72 | return x | ^^^^^^^^ - | B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior --> B901.py:83:5 @@ -54,7 +51,6 @@ B901 Using `yield` and `return {value}` in a generator function can lead to conf 82 | yield 1 83 | return foo() | ^^^^^^^^^^^^ - | B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior --> B901.py:88:5 @@ -63,7 +59,6 @@ B901 Using `yield` and `return {value}` in a generator function can lead to conf 87 | yield 1 88 | return [1, 2, 3] | ^^^^^^^^^^^^^^^^ - | B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior --> B901.py:116:5 @@ -72,7 +67,6 @@ B901 Using `yield` and `return {value}` in a generator function can lead to conf 115 | yield 116 | return "should error" | ^^^^^^^^^^^^^^^^^^^^^ - | B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior --> B901.py:122:5 @@ -81,7 +75,6 @@ B901 Using `yield` and `return {value}` in a generator function can lead to conf 121 | yield 122 | return "should error" | ^^^^^^^^^^^^^^^^^^^^^ - | B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior --> B901.py:128:5 @@ -90,4 +83,3 @@ B901 Using `yield` and `return {value}` in a generator function can lead to conf 127 | yield 128 | return "should error" | ^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B903_class_as_data_structure.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B903_class_as_data_structure.py.snap index 6c5e006f18..5dec517fdc 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B903_class_as_data_structure.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B903_class_as_data_structure.py.snap @@ -9,7 +9,6 @@ B903 Class could be dataclass or namedtuple 8 | | self.x = x 9 | | self.y = y | |__________________^ - | B903 Class could be dataclass or namedtuple --> class_as_data_structure.py:40:1 @@ -50,7 +49,6 @@ B903 Class could be dataclass or namedtuple 67 | | self.foo = foo 68 | | self.bar = bar | |______________________^ - | B903 Class could be dataclass or namedtuple --> class_as_data_structure.py:85:1 @@ -60,7 +58,6 @@ B903 Class could be dataclass or namedtuple 87 | | self.foo = foo 88 | | self.bar = bar | |______________________^ - | B903 Class could be dataclass or namedtuple --> class_as_data_structure.py:91:1 @@ -72,7 +69,6 @@ B903 Class could be dataclass or namedtuple 95 | | self.foo = foo 96 | | self.bar = bar | |______________________^ - | B903 Class could be dataclass or namedtuple --> class_as_data_structure.py:99:1 diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B903_py39_class_as_data_structure.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B903_py39_class_as_data_structure.py.snap index 57347eeb2f..598fb5b007 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B903_py39_class_as_data_structure.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B903_py39_class_as_data_structure.py.snap @@ -9,7 +9,6 @@ B903 Class could be dataclass or namedtuple 8 | | self.x = x 9 | | self.y = y | |__________________^ - | B903 Class could be dataclass or namedtuple --> class_as_data_structure.py:40:1 @@ -50,7 +49,6 @@ B903 Class could be dataclass or namedtuple 67 | | self.foo = foo 68 | | self.bar = bar | |______________________^ - | B903 Class could be dataclass or namedtuple --> class_as_data_structure.py:85:1 @@ -60,7 +58,6 @@ B903 Class could be dataclass or namedtuple 87 | | self.foo = foo 88 | | self.bar = bar | |______________________^ - | B903 Class could be dataclass or namedtuple --> class_as_data_structure.py:91:1 @@ -72,4 +69,3 @@ B903 Class could be dataclass or namedtuple 95 | | self.foo = foo 96 | | self.bar = bar | |______________________^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B904_B904.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B904_B904.py.snap index 19d7b0c5bb..133533f5c9 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B904_B904.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B904_B904.py.snap @@ -63,7 +63,6 @@ B904 Within an `except` clause, raise exceptions with `raise ... from err` or `r 64 | else: 65 | raise RuntimeError("bang!") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | B904 Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling --> B904.py:73:13 diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B905.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B905.py.snap index a4c059bcfc..201710c1ec 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B905.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B905.py.snap @@ -118,7 +118,6 @@ B905 [*] `zip()` without an explicit `strict=` parameter 33 | # Error 34 | zip(*lot_of_iterators) | ^^^^^^^^^^^^^^^^^^^^^^ - | help: Add explicit value for parameter `strict=` | 33 | # Error diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B909_B909.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B909_B909.py.snap index f55c3c4f5d..d461a55e89 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B909_B909.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B909_B909.py.snap @@ -235,7 +235,6 @@ B909 Mutation to loop iterable `a.some_list` during iteration 84 | a.some_list.remove(0) 85 | del a.some_list[2] | ^^^^^^^^^^^^^^^^^^ - | B909 Mutation to loop iterable `foo` during iteration --> B909.py:93:5 @@ -330,7 +329,6 @@ B909 Mutation to loop iterable `foo` during iteration 104 | foo -= bar 105 | foo ^= bar | ^^^^^^^^^^ - | B909 Mutation to loop iterable `foo` during iteration --> B909.py:136:9 diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B912_B912.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B912_B912.py.snap index 1f3d8d27ad..2adef201a2 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B912_B912.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B912_B912.py.snap @@ -139,7 +139,6 @@ B912 [*] `map()` without an explicit `strict=` parameter 35 | # Regression https://github.com/astral-sh/ruff/issues/20997 36 | map(f, *lots_of_iterators) | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add explicit value for parameter `strict=` | 35 | # Regression https://github.com/astral-sh/ruff/issues/20997 diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__extend_immutable_calls_arg_default.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__extend_immutable_calls_arg_default.snap index 5d5317e0a7..6d46c3943e 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__extend_immutable_calls_arg_default.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__extend_immutable_calls_arg_default.snap @@ -23,4 +23,3 @@ B008 Do not perform function call `L` in argument defaults; instead, perform the 47 | def okay(obj = N()): ... 48 | def error(obj = L()): ... | ^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__extend_mutable_contextvar_default.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__extend_mutable_contextvar_default.snap index 9988069bd2..b0ca802b57 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__extend_mutable_contextvar_default.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__extend_mutable_contextvar_default.snap @@ -7,5 +7,4 @@ B039 Do not use mutable data structures for `ContextVar` defaults 6 | from something_else import Depends 7 | ContextVar("cv", default=Depends()) | ^^^^^^^^^ - | help: Replace with `None`; initialize with `.set()`` diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__preview__B006_B006_4.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__preview__B006_B006_4.py.snap index 987740d098..33bd877748 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__preview__B006_B006_4.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__preview__B006_B006_4.py.snap @@ -5,8 +5,8 @@ B006 [*] Do not use mutable data structures for argument defaults --> B006_4.py:7:26 | 6 | class FormFeedIndent: -7 | def __init__(self, a=[]): - | ^^ +7 | ␌ def __init__(self, a=[]): + | ^^ 8 | print(a) | help: Replace with `None`; initialize within function diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__preview__B006_B006_5.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__preview__B006_B006_5.py.snap index 1af32ce6af..018c6e39f4 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__preview__B006_B006_5.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__preview__B006_B006_5.py.snap @@ -267,7 +267,6 @@ B006 Do not use mutable data structures for argument defaults | 67 | def import_module_wrong(value: dict[str, str] = {}): import os | ^^ - | help: Replace with `None`; initialize within function B006 Do not use mutable data structures for argument defaults @@ -275,7 +274,6 @@ B006 Do not use mutable data structures for argument defaults | 70 | def import_module_wrong(value: dict[str, str] = {}): import os; import sys | ^^ - | help: Replace with `None`; initialize within function B006 Do not use mutable data structures for argument defaults diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__preview__B006_B006_B008.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__preview__B006_B006_B008.py.snap index 9ee2465a7f..d4e71b5232 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__preview__B006_B006_B008.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__preview__B006_B006_B008.py.snap @@ -79,7 +79,6 @@ B006 Do not use mutable data structures for argument defaults 81 | 82 | def single_line_func_wrong(value = {}): ... | ^^ - | help: Replace with `None`; initialize within function B006 [*] Do not use mutable data structures for argument defaults @@ -149,14 +148,14 @@ note: This is an unsafe fix and may change runtime behavior B006 [*] Do not use mutable data structures for argument defaults --> B006_B008.py:102:46 | -101 | # N.B. we're also flagging the function call in the comprehension +101 | # B006 still flags mutable comprehension defaults. 102 | def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]): | ^^^^^^^^^^^^^^^^^^^^^^^^ 103 | pass | help: Replace with `None`; initialize within function | -101 | # N.B. we're also flagging the function call in the comprehension +101 | # B006 still flags mutable comprehension defaults. - def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]): 102 + def list_comprehension_also_not_okay(default=None): 103 | pass diff --git a/crates/ruff_linter/src/rules/flake8_builtins/mod.rs b/crates/ruff_linter/src/rules/flake8_builtins/mod.rs index 010283cb5b..6f289a219d 100644 --- a/crates/ruff_linter/src/rules/flake8_builtins/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_builtins/mod.rs @@ -74,14 +74,8 @@ mod tests { assert_diagnostics_diff!( snapshot, Path::new("flake8_builtins").join(path).as_path(), - &LinterSettings { - unresolved_target_version: PythonVersion::PY313.into(), - ..LinterSettings::for_rule(rule_code) - }, - &LinterSettings { - unresolved_target_version: PythonVersion::PY314.into(), - ..LinterSettings::for_rule(rule_code) - }, + &LinterSettings::for_rule(rule_code).with_target_version(PythonVersion::PY313), + &LinterSettings::for_rule(rule_code).with_target_version(PythonVersion::PY314), ); Ok(()) } @@ -239,10 +233,7 @@ mod tests { let snapshot = format!("{}_{}_py38", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( Path::new("flake8_builtins").join(path).as_path(), - &LinterSettings { - unresolved_target_version: PythonVersion::PY38.into(), - ..LinterSettings::for_rule(rule_code) - }, + &LinterSettings::for_rule(rule_code).with_target_version(PythonVersion::PY38), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) diff --git a/crates/ruff_linter/src/rules/flake8_builtins/snapshots/ruff_linter__rules__flake8_builtins__tests__A001_A001.py.snap b/crates/ruff_linter/src/rules/flake8_builtins/snapshots/ruff_linter__rules__flake8_builtins__tests__A001_A001.py.snap index e220c41130..bb1b7a8b39 100644 --- a/crates/ruff_linter/src/rules/flake8_builtins/snapshots/ruff_linter__rules__flake8_builtins__tests__A001_A001.py.snap +++ b/crates/ruff_linter/src/rules/flake8_builtins/snapshots/ruff_linter__rules__flake8_builtins__tests__A001_A001.py.snap @@ -173,4 +173,3 @@ A001 Variable `sum` is shadowing a Python builtin 29 | 30 | [0 for sum in ()] | ^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_builtins/snapshots/ruff_linter__rules__flake8_builtins__tests__A001_A001.py_builtins_ignorelist.snap b/crates/ruff_linter/src/rules/flake8_builtins/snapshots/ruff_linter__rules__flake8_builtins__tests__A001_A001.py_builtins_ignorelist.snap index c14d834004..d78d048fdd 100644 --- a/crates/ruff_linter/src/rules/flake8_builtins/snapshots/ruff_linter__rules__flake8_builtins__tests__A001_A001.py_builtins_ignorelist.snap +++ b/crates/ruff_linter/src/rules/flake8_builtins/snapshots/ruff_linter__rules__flake8_builtins__tests__A001_A001.py_builtins_ignorelist.snap @@ -162,4 +162,3 @@ A001 Variable `sum` is shadowing a Python builtin 29 | 30 | [0 for sum in ()] | ^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_builtins/snapshots/ruff_linter__rules__flake8_builtins__tests__A003_A003.py.snap b/crates/ruff_linter/src/rules/flake8_builtins/snapshots/ruff_linter__rules__flake8_builtins__tests__A003_A003.py.snap index 537a64433c..b13c2a3b53 100644 --- a/crates/ruff_linter/src/rules/flake8_builtins/snapshots/ruff_linter__rules__flake8_builtins__tests__A003_A003.py.snap +++ b/crates/ruff_linter/src/rules/flake8_builtins/snapshots/ruff_linter__rules__flake8_builtins__tests__A003_A003.py.snap @@ -27,4 +27,3 @@ A003 Python builtin is shadowed by class attribute `bin` from line 35 35 | bin = 2 36 | foo = [bin] | ^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_builtins/snapshots/ruff_linter__rules__flake8_builtins__tests__A003_A003.py_builtins_ignorelist.snap b/crates/ruff_linter/src/rules/flake8_builtins/snapshots/ruff_linter__rules__flake8_builtins__tests__A003_A003.py_builtins_ignorelist.snap index c335dca6bd..674e4a6f18 100644 --- a/crates/ruff_linter/src/rules/flake8_builtins/snapshots/ruff_linter__rules__flake8_builtins__tests__A003_A003.py_builtins_ignorelist.snap +++ b/crates/ruff_linter/src/rules/flake8_builtins/snapshots/ruff_linter__rules__flake8_builtins__tests__A003_A003.py_builtins_ignorelist.snap @@ -18,4 +18,3 @@ A003 Python builtin is shadowed by class attribute `bin` from line 35 35 | bin = 2 36 | foo = [bin] | ^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_builtins/snapshots/ruff_linter__rules__flake8_builtins__tests__A004_A004.py.snap b/crates/ruff_linter/src/rules/flake8_builtins/snapshots/ruff_linter__rules__flake8_builtins__tests__A004_A004.py.snap index 9df8672c5e..9f81225ab3 100644 --- a/crates/ruff_linter/src/rules/flake8_builtins/snapshots/ruff_linter__rules__flake8_builtins__tests__A004_A004.py.snap +++ b/crates/ruff_linter/src/rules/flake8_builtins/snapshots/ruff_linter__rules__flake8_builtins__tests__A004_A004.py.snap @@ -68,7 +68,6 @@ A004 Import `BaseExceptionGroup` is shadowing a Python builtin 10 | if sys.version_info < (3, 11): 11 | from exceptiongroup import BaseExceptionGroup, ExceptionGroup | ^^^^^^^^^^^^^^^^^^ - | A004 Import `ExceptionGroup` is shadowing a Python builtin --> A004.py:11:52 @@ -76,4 +75,3 @@ A004 Import `ExceptionGroup` is shadowing a Python builtin 10 | if sys.version_info < (3, 11): 11 | from exceptiongroup import BaseExceptionGroup, ExceptionGroup | ^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_builtins/snapshots/ruff_linter__rules__flake8_builtins__tests__A004_A004.py_builtins_ignorelist.snap b/crates/ruff_linter/src/rules/flake8_builtins/snapshots/ruff_linter__rules__flake8_builtins__tests__A004_A004.py_builtins_ignorelist.snap index b5381b7f41..2dc1f24e4c 100644 --- a/crates/ruff_linter/src/rules/flake8_builtins/snapshots/ruff_linter__rules__flake8_builtins__tests__A004_A004.py_builtins_ignorelist.snap +++ b/crates/ruff_linter/src/rules/flake8_builtins/snapshots/ruff_linter__rules__flake8_builtins__tests__A004_A004.py_builtins_ignorelist.snap @@ -57,7 +57,6 @@ A004 Import `BaseExceptionGroup` is shadowing a Python builtin 10 | if sys.version_info < (3, 11): 11 | from exceptiongroup import BaseExceptionGroup, ExceptionGroup | ^^^^^^^^^^^^^^^^^^ - | A004 Import `ExceptionGroup` is shadowing a Python builtin --> A004.py:11:52 @@ -65,4 +64,3 @@ A004 Import `ExceptionGroup` is shadowing a Python builtin 10 | if sys.version_info < (3, 11): 11 | from exceptiongroup import BaseExceptionGroup, ExceptionGroup | ^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_commas/snapshots/ruff_linter__rules__flake8_commas__tests__COM81.py.snap b/crates/ruff_linter/src/rules/flake8_commas/snapshots/ruff_linter__rules__flake8_commas__tests__COM81.py.snap index 2e5644a58e..dab988936c 100644 --- a/crates/ruff_linter/src/rules/flake8_commas/snapshots/ruff_linter__rules__flake8_commas__tests__COM81.py.snap +++ b/crates/ruff_linter/src/rules/flake8_commas/snapshots/ruff_linter__rules__flake8_commas__tests__COM81.py.snap @@ -61,7 +61,6 @@ COM812 [*] Trailing comma missing 22 | 2, 23 | 3 | ^ - | help: Add trailing comma | 22 | 2, diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/fixes.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/fixes.rs index 32d850820b..eb0b0acdc9 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/fixes.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/fixes.rs @@ -14,6 +14,7 @@ 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 unicode_normalization::UnicodeNormalization; use crate::Locator; use crate::cst::helpers::{negate, space}; @@ -241,6 +242,10 @@ pub(crate) fn fix_unnecessary_collection_call( .unwrap_or(stylist.quote()); // Quote each argument. + // + // Python normalizes identifiers to NFKC, but string literals are not normalized. Emitting the + // raw source text of a keyword argument would change the dictionary key at runtime, so the + // name has to be normalized. See https://github.com/astral-sh/ruff/issues/16234. for arg in &call.args { let quoted = format!( "{}{}{}", @@ -248,7 +253,8 @@ pub(crate) fn fix_unnecessary_collection_call( arg.keyword .as_ref() .expect("Expected dictionary argument to be kwarg") - .value, + .value + .nfkc(), quote, ); arena.push(quoted); @@ -313,7 +319,7 @@ pub(crate) fn fix_unnecessary_collection_call( /// However, this is a syntax error under the f-string grammar. As such, /// this method will pad the start and end of an expression as needed to /// avoid producing invalid syntax. -pub(crate) fn pad_expression( +fn pad_expression( content: String, range: TextRange, locator: &Locator, diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs index 60e612f057..c7d99586b8 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs @@ -15,7 +15,6 @@ mod tests { use crate::assert_diagnostics; use crate::registry::Rule; use crate::settings::LinterSettings; - use crate::settings::types::PreviewMode; use crate::test::test_path; #[test_case(Rule::UnnecessaryCallAroundSorted, Path::new("C413.py"))] @@ -78,10 +77,7 @@ mod tests { ); let diagnostics = test_path( Path::new("flake8_comprehensions").join(path).as_path(), - &LinterSettings { - preview: PreviewMode::Enabled, - ..LinterSettings::for_rule(rule_code) - }, + &LinterSettings::for_rule(rule_code).with_preview_mode(), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) 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 3bf43db4c1..dbea6c7b28 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 @@ -224,7 +224,7 @@ fn fix_unnecessary_dict_comprehension(value: &Expr, generator: &Comprehension) - node_index: ruff_python_ast::AtomicNodeIndex::NONE, })), arguments: args, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, is_cast: false, is_checked_cast: false, 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 8b906a94b7..02983f4e33 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 @@ -50,7 +50,7 @@ pub(crate) fn unnecessary_list_call(checker: &Checker, expr: &Expr, call: &ExprC let ExprCall { func, arguments, - range: _, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, 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 5441bde04e..7850d9f60c 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 @@ -6,6 +6,7 @@ use ruff_python_ast::visitor::Visitor; use ruff_python_ast::{self as ast, Expr, ExprContext, Parameters, Stmt}; use ruff_python_ast::{ExprLambda, visitor}; use ruff_python_semantic::SemanticModel; +use ruff_text_size::Ranged; use crate::Fix; use crate::checkers::ast::Checker; @@ -146,7 +147,7 @@ pub(crate) fn unnecessary_map(checker: &Checker, call: &ast::ExprCall) { return; } - let mut diagnostic = checker.report_diagnostic(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, diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C400_C400.py.snap b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C400_C400.py.snap index 1966f8c80f..702b707c45 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C400_C400.py.snap +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C400_C400.py.snap @@ -29,7 +29,6 @@ C400 [*] Unnecessary generator (rewrite as a list comprehension) 4 | | 2 * x + 1 for x in range(3) 5 | | ) | |_^ - | help: Rewrite as a list comprehension | 2 | even_nums = list(2 * x for x in range(3)) @@ -170,7 +169,6 @@ C400 [*] Unnecessary generator (rewrite as a list comprehension) 26 | | # some more 27 | | ) | |__^ - | help: Rewrite as a list comprehension | 21 | list((0 for _ in []),) diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C400_C400_py315.py.snap b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C400_C400_py315.py.snap index 572fda0216..b3f136505c 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C400_C400_py315.py.snap +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C400_C400_py315.py.snap @@ -8,7 +8,6 @@ C400 [*] Unnecessary generator (rewrite as a list comprehension) 2 | 3 | list(*x for x in xs) | ^^^^^^^^^^^^^^^^^^^^ - | help: Rewrite as a list comprehension | 2 | 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 0e7e68b74b..28dffa3bc5 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 @@ -86,7 +86,6 @@ C401 [*] Unnecessary generator (rewrite as a set comprehension) 11 | print(f"Hello {set(f(a) for a in 'abc')} World") 12 | print(f"Hello { set(f(a) for a in 'abc') } World") | ^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Rewrite as a set comprehension | 11 | print(f"Hello {set(f(a) for a in 'abc')} World") @@ -454,7 +453,6 @@ C401 [*] Unnecessary generator (rewrite using `set()`) 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()` | 44 | print(t"{set(a for a in 'abc') - set(a for a in 'ab')}") @@ -471,7 +469,6 @@ C401 [*] Unnecessary generator (rewrite using `set()`) 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()` | 44 | print(t"{set(a for a in 'abc') - set(a for a in 'ab')}") diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C401_C401_py315.py.snap b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C401_C401_py315.py.snap index f335e0b44e..e7972d0580 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C401_C401_py315.py.snap +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C401_C401_py315.py.snap @@ -8,7 +8,6 @@ C401 [*] Unnecessary generator (rewrite as a set comprehension) 2 | 3 | set(*x for x in xs) | ^^^^^^^^^^^^^^^^^^^ - | help: Rewrite as a set comprehension | 2 | 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 b245ea00ae..e6dcb05b14 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 @@ -405,7 +405,6 @@ 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'])}" | ^^^^^^^^^^^^^^^^^^^^^^ - | help: Rewrite as a set comprehension | 47 | s = t"{ set([x for x in 'ab']) | set([x for x in 'ab']) }" @@ -421,7 +420,6 @@ 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'])}" | ^^^^^^^^^^^^^^^^^^^^^^ - | help: Rewrite as a set comprehension | 47 | s = t"{ set([x for x in 'ab']) | set([x for x in 'ab']) }" diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C403_C403_py315.py.snap b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C403_C403_py315.py.snap index 7ed3ba81d2..2231cf5586 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C403_C403_py315.py.snap +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C403_C403_py315.py.snap @@ -8,7 +8,6 @@ C403 [*] Unnecessary list comprehension (rewrite as a set comprehension) 2 | 3 | set([*x for x in xs]) | ^^^^^^^^^^^^^^^^^^^^^ - | help: Rewrite as a set comprehension | 2 | diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C404_C404.py.snap b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C404_C404.py.snap index bbbc23c396..8f46f7a7f0 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C404_C404.py.snap +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C404_C404.py.snap @@ -168,7 +168,6 @@ C404 [*] Unnecessary list comprehension (rewrite as a dict comprehension) 15 | # Regression test for: https://github.com/astral-sh/ruff/issues/7087 16 | saved.append(dict([(k, v)for k,v in list(unique_instance.__dict__.items()) if k in [f.name for f in unique_instance._meta.fields]])) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Rewrite as a dict comprehension | 15 | # Regression test for: https://github.com/astral-sh/ruff/issues/7087 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 59eee7baf5..d8e9ff2ba1 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 @@ -563,7 +563,6 @@ C405 [*] Unnecessary list literal (rewrite as a set literal) 34 | t"a {set(['a', 'b']) - set(['a'])} b" 35 | t"a { set(['a', 'b']) - set(['a']) } b" | ^^^^^^^^^^^^^^^ - | help: Rewrite as a set literal | 34 | t"a {set(['a', 'b']) - set(['a'])} b" @@ -579,7 +578,6 @@ C405 [*] Unnecessary list literal (rewrite as a set literal) 34 | t"a {set(['a', 'b']) - set(['a'])} b" 35 | t"a { set(['a', 'b']) - set(['a']) } b" | ^^^^^^^^^^ - | help: Rewrite as a set literal | 34 | t"a {set(['a', 'b']) - set(['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 d3fa03663c..0fea149499 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 @@ -529,12 +529,15 @@ C408 [*] Unnecessary `dict()` call (rewrite as a literal) 38 | t"a {dict(x='y') | dict(y='z')} b" 39 | t"a { dict(x='y') | dict(y='z') } b" | ^^^^^^^^^^^ +40 | +41 | # https://github.com/astral-sh/ruff/issues/16234 | help: Rewrite as a literal | 38 | t"a {dict(x='y') | dict(y='z')} b" - t"a { dict(x='y') | dict(y='z') } b" 39 + t"a { {'x': 'y'} | dict(y='z') } b" +40 | | note: This is an unsafe fix and may change runtime behavior @@ -545,11 +548,85 @@ C408 [*] Unnecessary `dict()` call (rewrite as a literal) 38 | t"a {dict(x='y') | dict(y='z')} b" 39 | t"a { dict(x='y') | dict(y='z') } b" | ^^^^^^^^^^^ +40 | +41 | # https://github.com/astral-sh/ruff/issues/16234 | help: Rewrite as a literal | 38 | t"a {dict(x='y') | dict(y='z')} b" - t"a { dict(x='y') | dict(y='z') } b" 39 + t"a { dict(x='y') | {'y': 'z'} } b" +40 | + | +note: This is an unsafe fix and may change runtime behavior + +C408 [*] Unnecessary `dict()` call (rewrite as a literal) + --> C408.py:45:1 + | +43 | # normalize the keyword name to preserve the dictionary key at runtime. The character "ℼ" normalizes +44 | # to "π", and "ſ" normalizes to "s". +45 | dict(ℼ=3.14) + | ^^^^^^^^^^^^ +46 | dict(ſ=1) +47 | dict(𝕒=1, b=2) + | +help: Rewrite as a literal + | +44 | # to "π", and "ſ" normalizes to "s". + - dict(ℼ=3.14) +45 + {"π": 3.14} +46 | dict(ſ=1) + | +note: This is an unsafe fix and may change runtime behavior + +C408 [*] Unnecessary `dict()` call (rewrite as a literal) + --> C408.py:46:1 + | +44 | # to "π", and "ſ" normalizes to "s". +45 | dict(ℼ=3.14) +46 | dict(ſ=1) + | ^^^^^^^^^ +47 | dict(𝕒=1, b=2) +48 | dict(a=1, b=2) # already NFKC-normalized: unchanged + | +help: Rewrite as a literal + | +45 | dict(ℼ=3.14) + - dict(ſ=1) +46 + {"s": 1} +47 | dict(𝕒=1, b=2) + | +note: This is an unsafe fix and may change runtime behavior + +C408 [*] Unnecessary `dict()` call (rewrite as a literal) + --> C408.py:47:1 + | +45 | dict(ℼ=3.14) +46 | dict(ſ=1) +47 | dict(𝕒=1, b=2) + | ^^^^^^^^^^^^^^ +48 | dict(a=1, b=2) # already NFKC-normalized: unchanged + | +help: Rewrite as a literal + | +46 | dict(ſ=1) + - dict(𝕒=1, b=2) +47 + {"a": 1, "b": 2} +48 | dict(a=1, b=2) # already NFKC-normalized: unchanged + | +note: This is an unsafe fix and may change runtime behavior + +C408 [*] Unnecessary `dict()` call (rewrite as a literal) + --> C408.py:48:1 + | +46 | dict(ſ=1) +47 | dict(𝕒=1, b=2) +48 | dict(a=1, b=2) # already NFKC-normalized: unchanged + | ^^^^^^^^^^^^^^ +help: Rewrite as a literal + | +47 | dict(𝕒=1, b=2) + - dict(a=1, b=2) # already NFKC-normalized: unchanged +48 + {"a": 1, "b": 2} # already NFKC-normalized: unchanged | note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C409_C409.py.snap b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C409_C409.py.snap index dccb52bf72..96fb283588 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C409_C409.py.snap +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C409_C409.py.snap @@ -255,7 +255,6 @@ C409 [*] Unnecessary list literal passed to `tuple()` (rewrite as a tuple litera 46 | t9 = tuple([1],) 47 | t10 = tuple([1, 2],) | ^^^^^^^^^^^^^^ - | help: Rewrite as a tuple literal | 46 | t9 = tuple([1],) diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C410_C410.py.snap b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C410_C410.py.snap index ea6b3060f9..9e3043db41 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C410_C410.py.snap +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C410_C410.py.snap @@ -60,7 +60,6 @@ C410 [*] Unnecessary tuple literal passed to `list()` (rewrite as a single list 3 | l3 = list([]) 4 | l4 = list(()) | ^^^^^^^^ - | help: Rewrite as a single list literal | 3 | l3 = list([]) diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C411_C411_py315.py.snap b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C411_C411_py315.py.snap index 375114392b..91188754db 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C411_C411_py315.py.snap +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C411_C411_py315.py.snap @@ -8,5 +8,4 @@ C411 Unnecessary `list()` call (remove the outer call to `list()`) 2 | 3 | list([*x for x in xs]) | ^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove outer `list()` call diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C413_C413.py.snap b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C413_C413.py.snap index 4a5feb7d4d..4acd1d7bae 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C413_C413.py.snap +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C413_C413.py.snap @@ -291,7 +291,6 @@ C413 [*] Unnecessary `list()` call around `sorted()` 28 | / list(sorted 29 | | ("xy")) | |_______^ - | help: Remove unnecessary `list()` call | 27 | ("")) diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C414_C414.py.snap b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C414_C414.py.snap index 55b028356f..c2cf4caba8 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C414_C414.py.snap +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C414_C414.py.snap @@ -460,7 +460,6 @@ C414 [*] Unnecessary `list()` call within `sorted()` 46 | | key=lambda xxxxx: xxxxx or "", 47 | | ) | |_^ - | help: Remove the inner `list()` call | 44 | xxxxxxxxxxx_xxxxx_xxxxx = sorted( 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 bd1f3e7f08..26214dbd82 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 @@ -176,7 +176,6 @@ C417 [*] Unnecessary `map()` usage (rewrite using a generator expression) 12 | all(map(lambda v: isinstance(v, dict), nums)) 13 | filter(func, map(lambda v: v, nums)) | ^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace `map()` with a generator expression | 12 | all(map(lambda v: isinstance(v, dict), nums)) @@ -319,7 +318,6 @@ C417 [*] Unnecessary `map()` usage (rewrite using a dict comprehension) 75 | _ = t"{set(map(lambda x: x % 2 == 0, nums))}" 76 | _ = t"{dict(map(lambda v: (v, v**2), nums))}" | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace `map()` with a dict comprehension | 75 | _ = t"{set(map(lambda x: x % 2 == 0, nums))}" diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C417_C417_1.py.snap b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C417_C417_1.py.snap index 7ed6953d33..bf25050df0 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C417_C417_1.py.snap +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C417_C417_1.py.snap @@ -8,7 +8,6 @@ C417 [*] Unnecessary `map()` usage (rewrite using a generator expression) 6 | list = ... 7 | list(map(lambda x: x, [])) | ^^^^^^^^^^^^^^^^^^^^ - | help: Replace `map()` with a generator expression | 6 | list = ... diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C418_C418_py315.py.snap b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C418_C418_py315.py.snap index 1d63a5a8cd..3dc227f900 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C418_C418_py315.py.snap +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C418_C418_py315.py.snap @@ -8,5 +8,4 @@ C418 Unnecessary dict comprehension passed to `dict()` (remove the outer call to 2 | 3 | dict({**d for d in dicts}) | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove outer `dict()` call diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C419_C419_py315.py.snap b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C419_C419_py315.py.snap index 0af68931a9..438cf0716e 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C419_C419_py315.py.snap +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C419_C419_py315.py.snap @@ -18,5 +18,4 @@ C419 Unnecessary set comprehension 3 | all([*x for x in xs]) 4 | any({*x for x in xs}) | ^^^^^^^^^^^^^^^^ - | help: Remove unnecessary comprehension 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 203d399eae..a851b26c1f 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 @@ -8,7 +8,6 @@ C420 [*] Unnecessary dict comprehension for iterable; use `dict.fromkeys` instea 5 | numbers = [1, 2, 3] 6 | {n: None for n in numbers} # RUF025 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `dict.fromkeys(iterable, value)`) | 5 | numbers = [1, 2, 3] @@ -39,7 +38,6 @@ C420 [*] Unnecessary dict comprehension for iterable; use `dict.fromkeys` instea 14 | def func(): 15 | {n: 1.1 for n in [1, 2, 3]} # RUF025 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `dict.fromkeys(iterable)`) | 14 | def func(): @@ -55,7 +53,6 @@ C420 [*] Unnecessary dict comprehension for iterable; use `dict.fromkeys` instea 25 | 26 | f({c: "a" for c in "12345"}) # RUF025 | ^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `dict.fromkeys(iterable)`) | 25 | @@ -70,7 +67,6 @@ C420 [*] Unnecessary dict comprehension for iterable; use `dict.fromkeys` instea 29 | def func(): 30 | {n: True for n in [1, 2, 2]} # RUF025 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `dict.fromkeys(iterable)`) | 29 | def func(): @@ -85,7 +81,6 @@ C420 [*] Unnecessary dict comprehension for iterable; use `dict.fromkeys` instea 33 | def func(): 34 | {n: b"hello" for n in (1, 2, 2)} # RUF025 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `dict.fromkeys(iterable)`) | 33 | def func(): @@ -100,7 +95,6 @@ C420 [*] Unnecessary dict comprehension for iterable; use `dict.fromkeys` instea 37 | def func(): 38 | {n: ... for n in [1, 2, 3]} # RUF025 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `dict.fromkeys(iterable)`) | 37 | def func(): @@ -115,7 +109,6 @@ C420 [*] Unnecessary dict comprehension for iterable; use `dict.fromkeys` instea 41 | def func(): 42 | {n: False for n in {1: "a", 2: "b"}} # RUF025 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `dict.fromkeys(iterable)`) | 41 | def func(): @@ -130,7 +123,6 @@ C420 [*] Unnecessary dict comprehension for iterable; use `dict.fromkeys` instea 45 | def func(): 46 | {(a, b): 1 for (a, b) in [(1, 2), (3, 4)]} # RUF025 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `dict.fromkeys(iterable)`) | 45 | def func(): @@ -145,7 +137,6 @@ C420 [*] Unnecessary dict comprehension for iterable; use `dict.fromkeys` instea 53 | a = f() 54 | {n: a for n in [1, 2, 3]} # RUF025 | ^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `dict.fromkeys(iterable)`) | 53 | a = f() @@ -161,7 +152,6 @@ C420 [*] Unnecessary dict comprehension for iterable; use `dict.fromkeys` instea 58 | values = ["a", "b", "c"] 59 | [{n: values for n in [1, 2, 3]}] # RUF025 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `dict.fromkeys(iterable)`) | 58 | values = ["a", "b", "c"] @@ -184,7 +174,6 @@ C420 [*] Unnecessary dict comprehension for iterable; use `dict.fromkeys` instea 102 | | iterable # 8 103 | | } # 9 | |_^ - | help: Replace with `dict.fromkeys(iterable, value)`) | 94 | # https://github.com/astral-sh/ruff/issues/18764 diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C420_C420_1.py.snap b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C420_C420_1.py.snap index 41a17dc8a7..e979590b6e 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C420_C420_1.py.snap +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C420_C420_1.py.snap @@ -6,7 +6,6 @@ C420 [*] Unnecessary dict comprehension for iterable; use `dict.fromkeys` instea | 1 | {x: NotImplemented for x in "XY"} | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `dict.fromkeys(iterable)`) | - {x: NotImplemented for x in "XY"} 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 index 75b6af4ebb..76e3b9d9f5 100644 --- 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 @@ -6,7 +6,6 @@ C420 [*] Unnecessary dict comprehension for iterable; use `dict.fromkeys` instea | 1 | foo or{x: None for x in bar} | ^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `dict.fromkeys(iterable, value)`) | - foo or{x: None for x in bar} 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 b3f70a7f88..0b182c288d 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 @@ -20,7 +20,7 @@ use crate::settings::LinterSettings; /// - `lint.flake8-copyright.min-file-size` /// - `lint.flake8-copyright.notice-rgx` #[derive(ViolationMetadata)] -#[violation_metadata(preview_since = "v0.0.273")] +#[violation_metadata(stable_since = "0.16.0")] pub(crate) struct MissingCopyrightNotice; impl Violation for MissingCopyrightNotice { 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 458dd6b90e..484b88fbd1 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 @@ -2,6 +2,7 @@ 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; @@ -99,5 +100,5 @@ pub(crate) fn call_datetime_fromtimestamp(checker: &Checker, call: &ast::ExprCal None => DatetimeModuleAntipattern::NoTzArgumentPassed, }; - checker.report_diagnostic(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 5827cad81c..be0f86d395 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 @@ -2,6 +2,7 @@ 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; @@ -94,5 +95,5 @@ pub(crate) fn call_datetime_now_without_tzinfo(checker: &Checker, call: &ast::Ex None => DatetimeModuleAntipattern::NoTzArgumentPassed, }; - checker.report_diagnostic(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 d01bfb628f..384e3ea58d 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,6 +1,7 @@ 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; @@ -140,7 +141,7 @@ pub(crate) fn call_datetime_strptime_without_zone(checker: &Checker, call: &ast: semantic.current_expression_grandparent(), semantic.current_expression_parent(), ) { - checker.report_diagnostic(CallDatetimeStrptimeWithoutZone(antipattern), call.range); + checker.report_diagnostic(CallDatetimeStrptimeWithoutZone(antipattern), call.range()); } } 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 a621ad202b..c937149c5c 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 @@ -2,6 +2,7 @@ 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; @@ -91,5 +92,5 @@ pub(crate) fn call_datetime_without_tzinfo(checker: &Checker, call: &ast::ExprCa None => DatetimeModuleAntipattern::NoTzArgumentPassed, }; - checker.report_diagnostic(CallDatetimeWithoutTzinfo(antipattern), call.range); + checker.report_diagnostic(CallDatetimeWithoutTzinfo(antipattern), call.range()); } diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/snapshots/ruff_linter__rules__flake8_datetimez__tests__DTZ011_DTZ011.py.snap b/crates/ruff_linter/src/rules/flake8_datetimez/snapshots/ruff_linter__rules__flake8_datetimez__tests__DTZ011_DTZ011.py.snap index 3168567485..398da9f326 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/snapshots/ruff_linter__rules__flake8_datetimez__tests__DTZ011_DTZ011.py.snap +++ b/crates/ruff_linter/src/rules/flake8_datetimez/snapshots/ruff_linter__rules__flake8_datetimez__tests__DTZ011_DTZ011.py.snap @@ -18,5 +18,4 @@ DTZ011 `datetime.date.today()` used 8 | # unqualified 9 | date.today() | ^^^^^^^^^^^^ - | help: Use `datetime.datetime.now(tz=...).date()` instead diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/snapshots/ruff_linter__rules__flake8_datetimez__tests__DTZ012_DTZ012.py.snap b/crates/ruff_linter/src/rules/flake8_datetimez/snapshots/ruff_linter__rules__flake8_datetimez__tests__DTZ012_DTZ012.py.snap index 9160496ea6..c1efa6f9c8 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/snapshots/ruff_linter__rules__flake8_datetimez__tests__DTZ012_DTZ012.py.snap +++ b/crates/ruff_linter/src/rules/flake8_datetimez/snapshots/ruff_linter__rules__flake8_datetimez__tests__DTZ012_DTZ012.py.snap @@ -18,5 +18,4 @@ DTZ012 `datetime.date.fromtimestamp()` used 8 | # unqualified 9 | date.fromtimestamp(1234) | ^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `datetime.datetime.fromtimestamp(ts, tz=...).date()` instead diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/snapshots/ruff_linter__rules__flake8_datetimez__tests__DTZ901_DTZ901.py.snap b/crates/ruff_linter/src/rules/flake8_datetimez/snapshots/ruff_linter__rules__flake8_datetimez__tests__DTZ901_DTZ901.py.snap index 193014c717..b09afcea03 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/snapshots/ruff_linter__rules__flake8_datetimez__tests__DTZ901_DTZ901.py.snap +++ b/crates/ruff_linter/src/rules/flake8_datetimez/snapshots/ruff_linter__rules__flake8_datetimez__tests__DTZ901_DTZ901.py.snap @@ -40,7 +40,6 @@ DTZ901 Use of `datetime.datetime.min` without timezone information 8 | datetime.datetime.max.replace(year=...) 9 | datetime.datetime.min.replace(hour=...) | ^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `datetime.datetime.min.replace(tzinfo=...)` DTZ901 Use of `datetime.datetime.max` without timezone information @@ -82,5 +81,4 @@ DTZ901 Use of `datetime.datetime.min` without timezone information 30 | datetime.max.replace(year=...) 31 | datetime.min.replace(hour=...) | ^^^^^^^^^^^^ - | help: Replace with `datetime.datetime.min.replace(tzinfo=...)` diff --git a/crates/ruff_linter/src/rules/flake8_debugger/snapshots/ruff_linter__rules__flake8_debugger__tests__T100_T100.py.snap b/crates/ruff_linter/src/rules/flake8_debugger/snapshots/ruff_linter__rules__flake8_debugger__tests__T100_T100.py.snap index e8b2f2a1fa..2cf50c35de 100644 --- a/crates/ruff_linter/src/rules/flake8_debugger/snapshots/ruff_linter__rules__flake8_debugger__tests__T100_T100.py.snap +++ b/crates/ruff_linter/src/rules/flake8_debugger/snapshots/ruff_linter__rules__flake8_debugger__tests__T100_T100.py.snap @@ -201,7 +201,6 @@ T100 Trace found: `ptvsd.wait_for_attach` used 24 | break_into_debugger() 25 | wait_for_attach() | ^^^^^^^^^^^^^^^^^ - | T100 Import for `sys.breakpointhook` found --> T100.py:33:5 @@ -261,4 +260,3 @@ T100 Trace found: `sys.__breakpointhook__` used 42 | sys.breakpointhook() # error 43 | sys.__breakpointhook__() # error | ^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ001_DJ001.py.snap b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ001_DJ001.py.snap index 56a8c683fb..80a89942f9 100644 --- a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ001_DJ001.py.snap +++ b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ001_DJ001.py.snap @@ -61,7 +61,6 @@ DJ001 Avoid using `null=True` on string-based fields such as `URLField` 11 | filepathfield = models.FilePathField(max_length=255, null=True) 12 | urlfield = models.URLField(max_length=255, null=True) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | DJ001 Avoid using `null=True` on string-based fields such as `CharField` --> DJ001.py:16:17 @@ -123,7 +122,6 @@ DJ001 Avoid using `null=True` on string-based fields such as `URLField` 20 | filepathfield = models.FilePathField(max_length=255, null=True) 21 | urlfield = models.URLField(max_length=255, null=True) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | DJ001 Avoid using `null=True` on string-based fields such as `CharField` --> DJ001.py:25:17 @@ -185,7 +183,6 @@ DJ001 Avoid using `null=True` on string-based fields such as `URLField` 29 | filepathfield = models.FilePathField(max_length=255, null=True) 30 | urlfield = models.URLField(max_length=255, null=True) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | DJ001 Avoid using `null=True` on string-based fields such as `CharField` --> DJ001.py:52:35 @@ -214,4 +211,3 @@ DJ001 Avoid using `null=True` on string-based fields such as `SlugField` 53 | textfield: models.TextField = models.TextField(max_length=255, null=True) 54 | slugfield: models.SlugField = models.SlugField(max_length=255, null=True) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ003_DJ003.py.snap b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ003_DJ003.py.snap index e07b14604c..8ba9bbd1b9 100644 --- a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ003_DJ003.py.snap +++ b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ003_DJ003.py.snap @@ -7,7 +7,6 @@ DJ003 Avoid passing `locals()` as context to a `render` function 4 | def test_view1(request): 5 | return render(request, "index.html", locals()) | ^^^^^^^^ - | DJ003 Avoid passing `locals()` as context to a `render` function --> DJ003.py:9:50 @@ -15,4 +14,3 @@ DJ003 Avoid passing `locals()` as context to a `render` function 8 | def test_view2(request): 9 | return render(request, "index.html", context=locals()) | ^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ006_DJ006.py.snap b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ006_DJ006.py.snap index a78edbc248..055c648e8e 100644 --- a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ006_DJ006.py.snap +++ b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ006_DJ006.py.snap @@ -8,4 +8,3 @@ DJ006 Do not use `exclude` with `ModelForm`, use `fields` instead 5 | class Meta: 6 | exclude = ["bar"] | ^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ007_DJ007.py.snap b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ007_DJ007.py.snap index 6220696a2d..debfc9a45d 100644 --- a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ007_DJ007.py.snap +++ b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ007_DJ007.py.snap @@ -8,7 +8,6 @@ DJ007 Do not use `__all__` with `ModelForm`, use `fields` instead 5 | class Meta: 6 | fields = "__all__" | ^^^^^^^^^^^^^^^^^^ - | DJ007 Do not use `__all__` with `ModelForm`, use `fields` instead --> DJ007.py:11:9 @@ -17,4 +16,3 @@ DJ007 Do not use `__all__` with `ModelForm`, use `fields` instead 10 | class Meta: 11 | fields = b"__all__" | ^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ012_DJ012.py.snap b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ012_DJ012.py.snap index 14cd967f3c..256b1bc795 100644 --- a/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ012_DJ012.py.snap +++ b/crates/ruff_linter/src/rules/flake8_django/snapshots/ruff_linter__rules__flake8_django__tests__DJ012_DJ012.py.snap @@ -8,7 +8,6 @@ DJ012 Order of model's inner classes, methods, and fields does not follow the Dj 27 | 28 | first_name = models.CharField(max_length=32) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | DJ012 Order of model's inner classes, methods, and fields does not follow the Django Style Guide: field declaration should come before manager declaration --> DJ012.py:43:5 @@ -17,7 +16,6 @@ DJ012 Order of model's inner classes, methods, and fields does not follow the Dj 42 | 43 | first_name = models.CharField(max_length=32) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | DJ012 Order of model's inner classes, methods, and fields does not follow the Django Style Guide: Magic method should come before custom method --> DJ012.py:56:5 @@ -27,7 +25,6 @@ DJ012 Order of model's inner classes, methods, and fields does not follow the Dj 56 | / def __str__(self): 57 | | return "foobar" | |_______________________^ - | DJ012 Order of model's inner classes, methods, and fields does not follow the Django Style Guide: `save` method should come before `get_absolute_url` method --> DJ012.py:69:5 @@ -37,7 +34,6 @@ DJ012 Order of model's inner classes, methods, and fields does not follow the Dj 69 | / def save(self): 70 | | pass | |____________^ - | DJ012 Order of model's inner classes, methods, and fields does not follow the Django Style Guide: field declaration should come before `Meta` class --> DJ012.py:123:5 @@ -56,7 +52,6 @@ DJ012 Order of model's inner classes, methods, and fields does not follow the Dj 128 | 129 | middle_name = models.CharField(max_length=32) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | DJ012 Order of model's inner classes, methods, and fields does not follow the Django Style Guide: field declaration should come before `Meta` class --> DJ012.py:146:5 @@ -65,4 +60,3 @@ DJ012 Order of model's inner classes, methods, and fields does not follow the Dj 145 | 146 | first_name = models.CharField(max_length=32) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_errmsg/snapshots/ruff_linter__rules__flake8_errmsg__tests__custom.snap b/crates/ruff_linter/src/rules/flake8_errmsg/snapshots/ruff_linter__rules__flake8_errmsg__tests__custom.snap index 2d045fe812..845ebe42ea 100644 --- a/crates/ruff_linter/src/rules/flake8_errmsg/snapshots/ruff_linter__rules__flake8_errmsg__tests__custom.snap +++ b/crates/ruff_linter/src/rules/flake8_errmsg/snapshots/ruff_linter__rules__flake8_errmsg__tests__custom.snap @@ -7,7 +7,6 @@ EM101 [*] Exception must not use a string literal, assign to variable first 4 | def f_a(): 5 | raise RuntimeError("This is an example exception") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Assign to variable; remove string literal | 4 | def f_a(): @@ -25,7 +24,6 @@ EM102 [*] Exception must not use an f-string literal, assign to variable first 17 | example = "example" 18 | raise RuntimeError(f"This is an {example} exception") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Assign to variable; remove f-string literal | 17 | example = "example" @@ -42,7 +40,6 @@ EM103 [*] Exception must not use a `.format()` string directly, assign to variab 21 | def f_c(): 22 | raise RuntimeError("This is an {example} exception".format(example="example")) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Assign to variable; remove `.format()` string | 21 | def f_c(): @@ -60,7 +57,6 @@ EM101 [*] Exception must not use a string literal, assign to variable first 31 | msg = "hello" 32 | raise RuntimeError("This is an example exception") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Assign to variable; remove string literal | 31 | msg = "hello" @@ -78,7 +74,6 @@ EM101 [*] Exception must not use a string literal, assign to variable first 38 | 39 | raise RuntimeError("This is an example exception") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Assign to variable; remove string literal | 38 | @@ -95,7 +90,6 @@ EM101 [*] Exception must not use a string literal, assign to variable first 45 | def nested(): 46 | raise RuntimeError("This is an example exception") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Assign to variable; remove string literal | 45 | def nested(): @@ -152,7 +146,6 @@ EM103 [*] Exception must not use a `.format()` string directly, assign to variab 54 | raise RuntimeError(f"This is an exception: {foo}") 55 | raise RuntimeError("This is an exception: {}".format(foo)) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Assign to variable; remove `.format()` string | 54 | raise RuntimeError(f"This is an exception: {foo}") @@ -180,7 +173,6 @@ EM101 Exception must not use a string literal, assign to variable first 59 | if foo: raise RuntimeError("This is an example exception") 60 | if foo: x = 1; raise RuntimeError("This is an example exception") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Assign to variable; remove string literal EM102 [*] Exception must not use an f-string literal, assign to variable first @@ -189,7 +181,6 @@ EM102 [*] Exception must not use an f-string literal, assign to variable first 63 | def f_triple_quoted_string(): 64 | raise RuntimeError(f"""This is an {"example"} exception""") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Assign to variable; remove f-string literal | 63 | def f_triple_quoted_string(): diff --git a/crates/ruff_linter/src/rules/flake8_errmsg/snapshots/ruff_linter__rules__flake8_errmsg__tests__defaults.snap b/crates/ruff_linter/src/rules/flake8_errmsg/snapshots/ruff_linter__rules__flake8_errmsg__tests__defaults.snap index d9332caa29..a4d86c3aa7 100644 --- a/crates/ruff_linter/src/rules/flake8_errmsg/snapshots/ruff_linter__rules__flake8_errmsg__tests__defaults.snap +++ b/crates/ruff_linter/src/rules/flake8_errmsg/snapshots/ruff_linter__rules__flake8_errmsg__tests__defaults.snap @@ -7,7 +7,6 @@ EM101 [*] Exception must not use a string literal, assign to variable first 4 | def f_a(): 5 | raise RuntimeError("This is an example exception") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Assign to variable; remove string literal | 4 | def f_a(): @@ -24,7 +23,6 @@ EM101 [*] Exception must not use a string literal, assign to variable first 8 | def f_a_short(): 9 | raise RuntimeError("Error") | ^^^^^^^ - | help: Assign to variable; remove string literal | 8 | def f_a_short(): @@ -41,7 +39,6 @@ EM101 [*] Exception must not use a string literal, assign to variable first 12 | def f_a_empty(): 13 | raise RuntimeError("") | ^^ - | help: Assign to variable; remove string literal | 12 | def f_a_empty(): @@ -59,7 +56,6 @@ EM102 [*] Exception must not use an f-string literal, assign to variable first 17 | example = "example" 18 | raise RuntimeError(f"This is an {example} exception") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Assign to variable; remove f-string literal | 17 | example = "example" @@ -76,7 +72,6 @@ EM103 [*] Exception must not use a `.format()` string directly, assign to variab 21 | def f_c(): 22 | raise RuntimeError("This is an {example} exception".format(example="example")) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Assign to variable; remove `.format()` string | 21 | def f_c(): @@ -94,7 +89,6 @@ EM101 [*] Exception must not use a string literal, assign to variable first 31 | msg = "hello" 32 | raise RuntimeError("This is an example exception") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Assign to variable; remove string literal | 31 | msg = "hello" @@ -112,7 +106,6 @@ EM101 [*] Exception must not use a string literal, assign to variable first 38 | 39 | raise RuntimeError("This is an example exception") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Assign to variable; remove string literal | 38 | @@ -129,7 +122,6 @@ EM101 [*] Exception must not use a string literal, assign to variable first 45 | def nested(): 46 | raise RuntimeError("This is an example exception") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Assign to variable; remove string literal | 45 | def nested(): @@ -186,7 +178,6 @@ EM103 [*] Exception must not use a `.format()` string directly, assign to variab 54 | raise RuntimeError(f"This is an exception: {foo}") 55 | raise RuntimeError("This is an exception: {}".format(foo)) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Assign to variable; remove `.format()` string | 54 | raise RuntimeError(f"This is an exception: {foo}") @@ -214,7 +205,6 @@ EM101 Exception must not use a string literal, assign to variable first 59 | if foo: raise RuntimeError("This is an example exception") 60 | if foo: x = 1; raise RuntimeError("This is an example exception") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Assign to variable; remove string literal EM102 [*] Exception must not use an f-string literal, assign to variable first @@ -223,7 +213,6 @@ EM102 [*] Exception must not use an f-string literal, assign to variable first 63 | def f_triple_quoted_string(): 64 | raise RuntimeError(f"""This is an {"example"} exception""") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Assign to variable; remove f-string literal | 63 | def f_triple_quoted_string(): diff --git a/crates/ruff_linter/src/rules/flake8_errmsg/snapshots/ruff_linter__rules__flake8_errmsg__tests__string_exception.snap b/crates/ruff_linter/src/rules/flake8_errmsg/snapshots/ruff_linter__rules__flake8_errmsg__tests__string_exception.snap index 404727a7a7..1a8001fa15 100644 --- a/crates/ruff_linter/src/rules/flake8_errmsg/snapshots/ruff_linter__rules__flake8_errmsg__tests__string_exception.snap +++ b/crates/ruff_linter/src/rules/flake8_errmsg/snapshots/ruff_linter__rules__flake8_errmsg__tests__string_exception.snap @@ -7,7 +7,6 @@ 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") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Assign to variable; remove string literal | 1 | def f_byte(): @@ -24,7 +23,6 @@ EM101 [*] Exception must not use a string literal, assign to variable first 5 | def f_byte_empty(): 6 | raise RuntimeError(b"") | ^^^ - | help: Assign to variable; remove string literal | 5 | def f_byte_empty(): diff --git a/crates/ruff_linter/src/rules/flake8_executable/snapshots/ruff_linter__rules__flake8_executable__tests__EXE004_1.py.snap b/crates/ruff_linter/src/rules/flake8_executable/snapshots/ruff_linter__rules__flake8_executable__tests__EXE004_1.py.snap index f36efc45fc..e311ac219e 100644 --- a/crates/ruff_linter/src/rules/flake8_executable/snapshots/ruff_linter__rules__flake8_executable__tests__EXE004_1.py.snap +++ b/crates/ruff_linter/src/rules/flake8_executable/snapshots/ruff_linter__rules__flake8_executable__tests__EXE004_1.py.snap @@ -6,7 +6,6 @@ EXE004 [*] Avoid whitespace before shebang | 1 | #!/usr/bin/python | ^^^^ - | help: Remove whitespace before shebang | - #!/usr/bin/python diff --git a/crates/ruff_linter/src/rules/flake8_executable/snapshots/ruff_linter__rules__flake8_executable__tests__EXE004_4.py.snap b/crates/ruff_linter/src/rules/flake8_executable/snapshots/ruff_linter__rules__flake8_executable__tests__EXE004_4.py.snap index bd5717279a..49c7601c2a 100644 --- a/crates/ruff_linter/src/rules/flake8_executable/snapshots/ruff_linter__rules__flake8_executable__tests__EXE004_4.py.snap +++ b/crates/ruff_linter/src/rules/flake8_executable/snapshots/ruff_linter__rules__flake8_executable__tests__EXE004_4.py.snap @@ -7,7 +7,6 @@ EXE004 [*] Avoid whitespace before shebang 1 | / 2 | | #!/usr/bin/env python | |____^ - | help: Remove whitespace before shebang | - diff --git a/crates/ruff_linter/src/rules/flake8_executable/snapshots/ruff_linter__rules__flake8_executable__tests__EXE005_1.py.snap b/crates/ruff_linter/src/rules/flake8_executable/snapshots/ruff_linter__rules__flake8_executable__tests__EXE005_1.py.snap index c0adb495ed..5666d9a830 100644 --- a/crates/ruff_linter/src/rules/flake8_executable/snapshots/ruff_linter__rules__flake8_executable__tests__EXE005_1.py.snap +++ b/crates/ruff_linter/src/rules/flake8_executable/snapshots/ruff_linter__rules__flake8_executable__tests__EXE005_1.py.snap @@ -7,4 +7,3 @@ EXE005 Shebang should be at the beginning of the file 2 | # A python comment 3 | #!/usr/bin/python | ^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_executable/snapshots/ruff_linter__rules__flake8_executable__tests__EXE005_2.py.snap b/crates/ruff_linter/src/rules/flake8_executable/snapshots/ruff_linter__rules__flake8_executable__tests__EXE005_2.py.snap index 9cea26ce62..235f19ef69 100644 --- a/crates/ruff_linter/src/rules/flake8_executable/snapshots/ruff_linter__rules__flake8_executable__tests__EXE005_2.py.snap +++ b/crates/ruff_linter/src/rules/flake8_executable/snapshots/ruff_linter__rules__flake8_executable__tests__EXE005_2.py.snap @@ -7,4 +7,3 @@ EXE005 Shebang should be at the beginning of the file 3 | # A python comment 4 | #!/usr/bin/python | ^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_executable/snapshots/ruff_linter__rules__flake8_executable__tests__EXE005_3.py.snap b/crates/ruff_linter/src/rules/flake8_executable/snapshots/ruff_linter__rules__flake8_executable__tests__EXE005_3.py.snap index bd861dada8..29fdfd28fc 100644 --- a/crates/ruff_linter/src/rules/flake8_executable/snapshots/ruff_linter__rules__flake8_executable__tests__EXE005_3.py.snap +++ b/crates/ruff_linter/src/rules/flake8_executable/snapshots/ruff_linter__rules__flake8_executable__tests__EXE005_3.py.snap @@ -8,4 +8,3 @@ EXE005 Shebang should be at the beginning of the file 5 | # A python comment 6 | #!/usr/bin/python | ^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_fixme/snapshots/ruff_linter__rules__flake8_fixme__tests__line-contains-todo_T00.py.snap b/crates/ruff_linter/src/rules/flake8_fixme/snapshots/ruff_linter__rules__flake8_fixme__tests__line-contains-todo_T00.py.snap index 4e5e4c0a79..53dc89f6fc 100644 --- a/crates/ruff_linter/src/rules/flake8_fixme/snapshots/ruff_linter__rules__flake8_fixme__tests__line-contains-todo_T00.py.snap +++ b/crates/ruff_linter/src/rules/flake8_fixme/snapshots/ruff_linter__rules__flake8_fixme__tests__line-contains-todo_T00.py.snap @@ -27,4 +27,3 @@ FIX002 Line contains TODO, consider resolving the issue 9 | 10 | # test # TODO: todo | ^^^^ - | 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 6612ee086f..dfd889c36a 100644 --- a/crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs @@ -9,7 +9,6 @@ mod tests { use test_case::test_case; use crate::registry::Rule; - use crate::settings::types::PreviewMode; use crate::test::test_path; use crate::{assert_diagnostics, settings}; use ruff_python_ast::PythonVersion; @@ -30,10 +29,8 @@ mod tests { let snapshot = path.to_string_lossy().into_owned(); let diagnostics = test_path( Path::new("flake8_future_annotations").join(path).as_path(), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY37.into(), - ..settings::LinterSettings::for_rule(Rule::FutureRewritableTypeAnnotation) - }, + &settings::LinterSettings::for_rule(Rule::FutureRewritableTypeAnnotation) + .with_target_version(PythonVersion::PY37), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) @@ -50,25 +47,8 @@ mod tests { let snapshot = format!("fa102_{}", path.to_string_lossy()); let diagnostics = test_path( Path::new("flake8_future_annotations").join(path).as_path(), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY37.into(), - ..settings::LinterSettings::for_rule(Rule::FutureRequiredTypeAnnotation) - }, - )?; - assert_diagnostics!(snapshot, diagnostics); - Ok(()) - } - - #[test_case(Path::new("no_future_import_uses_preview_generics.py"))] - fn fa102_preview(path: &Path) -> Result<()> { - let snapshot = format!("fa102_preview_{}", path.to_string_lossy()); - let diagnostics = test_path( - Path::new("flake8_future_annotations").join(path).as_path(), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY37.into(), - preview: PreviewMode::Enabled, - ..settings::LinterSettings::for_rule(Rule::FutureRequiredTypeAnnotation) - }, + &settings::LinterSettings::for_rule(Rule::FutureRequiredTypeAnnotation) + .with_target_version(PythonVersion::PY37), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) diff --git a/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__fa102_no_future_import_uses_preview_generics.py.snap b/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__fa102_no_future_import_uses_preview_generics.py.snap index eb7482447a..24dd3a986b 100644 --- a/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__fa102_no_future_import_uses_preview_generics.py.snap +++ b/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__fa102_no_future_import_uses_preview_generics.py.snap @@ -1,6 +1,39 @@ --- source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs --- +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:40:13 + | +39 | def takes_preview_generics( +40 | future: asyncio.Future[int], + | ^^^^^^^^^^^^^^^^^^^ +41 | task: asyncio.Task[str], +42 | deque_object: collections.deque[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:41:11 + | +39 | def takes_preview_generics( +40 | future: asyncio.Future[int], +41 | task: asyncio.Task[str], + | ^^^^^^^^^^^^^^^^^ +42 | deque_object: collections.deque[int], +43 | defaultdict_object: collections.defaultdict[str, int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection --> no_future_import_uses_preview_generics.py:42:19 | @@ -34,3 +67,785 @@ help: Add `from __future__ import annotations` 2 | import asyncio | note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:44:19 + | +42 | deque_object: collections.deque[int], +43 | defaultdict_object: collections.defaultdict[str, int], +44 | ordered_dict: collections.OrderedDict[str, int], + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +45 | counter_obj: collections.Counter[str], +46 | chain_map: collections.ChainMap[str, int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:45:18 + | +43 | defaultdict_object: collections.defaultdict[str, int], +44 | ordered_dict: collections.OrderedDict[str, int], +45 | counter_obj: collections.Counter[str], + | ^^^^^^^^^^^^^^^^^^^^^^^^ +46 | chain_map: collections.ChainMap[str, int], +47 | context_manager: contextlib.AbstractContextManager[str], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:46:16 + | +44 | ordered_dict: collections.OrderedDict[str, int], +45 | counter_obj: collections.Counter[str], +46 | chain_map: collections.ChainMap[str, int], + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +47 | context_manager: contextlib.AbstractContextManager[str], +48 | async_context_manager: contextlib.AbstractAsyncContextManager[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:47:22 + | +45 | counter_obj: collections.Counter[str], +46 | chain_map: collections.ChainMap[str, int], +47 | context_manager: contextlib.AbstractContextManager[str], + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +48 | async_context_manager: contextlib.AbstractAsyncContextManager[int], +49 | dataclass_field: dataclasses.Field[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:48:28 + | +46 | chain_map: collections.ChainMap[str, int], +47 | context_manager: contextlib.AbstractContextManager[str], +48 | async_context_manager: contextlib.AbstractAsyncContextManager[int], + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +49 | dataclass_field: dataclasses.Field[int], +50 | cached_prop: functools.cached_property[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:49:22 + | +47 | context_manager: contextlib.AbstractContextManager[str], +48 | async_context_manager: contextlib.AbstractAsyncContextManager[int], +49 | dataclass_field: dataclasses.Field[int], + | ^^^^^^^^^^^^^^^^^^^^^^ +50 | cached_prop: functools.cached_property[int], +51 | partial_method: functools.partialmethod[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:50:18 + | +48 | async_context_manager: contextlib.AbstractAsyncContextManager[int], +49 | dataclass_field: dataclasses.Field[int], +50 | cached_prop: functools.cached_property[int], + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +51 | partial_method: functools.partialmethod[int], +52 | path_like: os.PathLike[str], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:51:21 + | +49 | dataclass_field: dataclasses.Field[int], +50 | cached_prop: functools.cached_property[int], +51 | partial_method: functools.partialmethod[int], + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +52 | path_like: os.PathLike[str], +53 | lifo_queue: queue.LifoQueue[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:52:16 + | +50 | cached_prop: functools.cached_property[int], +51 | partial_method: functools.partialmethod[int], +52 | path_like: os.PathLike[str], + | ^^^^^^^^^^^^^^^^ +53 | lifo_queue: queue.LifoQueue[int], +54 | regular_queue: queue.Queue[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:53:17 + | +51 | partial_method: functools.partialmethod[int], +52 | path_like: os.PathLike[str], +53 | lifo_queue: queue.LifoQueue[int], + | ^^^^^^^^^^^^^^^^^^^^ +54 | regular_queue: queue.Queue[int], +55 | priority_queue: queue.PriorityQueue[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:54:20 + | +52 | path_like: os.PathLike[str], +53 | lifo_queue: queue.LifoQueue[int], +54 | regular_queue: queue.Queue[int], + | ^^^^^^^^^^^^^^^^ +55 | priority_queue: queue.PriorityQueue[int], +56 | simple_queue: queue.SimpleQueue[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:55:21 + | +53 | lifo_queue: queue.LifoQueue[int], +54 | regular_queue: queue.Queue[int], +55 | priority_queue: queue.PriorityQueue[int], + | ^^^^^^^^^^^^^^^^^^^^^^^^ +56 | simple_queue: queue.SimpleQueue[int], +57 | regex_pattern: re.Pattern[str], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:56:19 + | +54 | regular_queue: queue.Queue[int], +55 | priority_queue: queue.PriorityQueue[int], +56 | simple_queue: queue.SimpleQueue[int], + | ^^^^^^^^^^^^^^^^^^^^^^ +57 | regex_pattern: re.Pattern[str], +58 | regex_match: re.Match[str], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:57:20 + | +55 | priority_queue: queue.PriorityQueue[int], +56 | simple_queue: queue.SimpleQueue[int], +57 | regex_pattern: re.Pattern[str], + | ^^^^^^^^^^^^^^^ +58 | regex_match: re.Match[str], +59 | bsd_db_shelf: shelve.BsdDbShelf[str, int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:58:18 + | +56 | simple_queue: queue.SimpleQueue[int], +57 | regex_pattern: re.Pattern[str], +58 | regex_match: re.Match[str], + | ^^^^^^^^^^^^^ +59 | bsd_db_shelf: shelve.BsdDbShelf[str, int], +60 | db_filename_shelf: shelve.DbfilenameShelf[str, int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:59:19 + | +57 | regex_pattern: re.Pattern[str], +58 | regex_match: re.Match[str], +59 | bsd_db_shelf: shelve.BsdDbShelf[str, int], + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +60 | db_filename_shelf: shelve.DbfilenameShelf[str, int], +61 | shelf_obj: shelve.Shelf[str, int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:60:24 + | +58 | regex_match: re.Match[str], +59 | bsd_db_shelf: shelve.BsdDbShelf[str, int], +60 | db_filename_shelf: shelve.DbfilenameShelf[str, int], + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +61 | shelf_obj: shelve.Shelf[str, int], +62 | mapping_proxy: types.MappingProxyType[str, int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:61:16 + | +59 | bsd_db_shelf: shelve.BsdDbShelf[str, int], +60 | db_filename_shelf: shelve.DbfilenameShelf[str, int], +61 | shelf_obj: shelve.Shelf[str, int], + | ^^^^^^^^^^^^^^^^^^^^^^ +62 | mapping_proxy: types.MappingProxyType[str, int], +63 | weak_key_dict: weakref.WeakKeyDictionary[object, int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:62:20 + | +60 | db_filename_shelf: shelve.DbfilenameShelf[str, int], +61 | shelf_obj: shelve.Shelf[str, int], +62 | mapping_proxy: types.MappingProxyType[str, int], + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +63 | weak_key_dict: weakref.WeakKeyDictionary[object, int], +64 | weak_method: weakref.WeakMethod[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:63:20 + | +61 | shelf_obj: shelve.Shelf[str, int], +62 | mapping_proxy: types.MappingProxyType[str, int], +63 | weak_key_dict: weakref.WeakKeyDictionary[object, int], + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +64 | weak_method: weakref.WeakMethod[int], +65 | weak_set: weakref.WeakSet[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:64:18 + | +62 | mapping_proxy: types.MappingProxyType[str, int], +63 | weak_key_dict: weakref.WeakKeyDictionary[object, int], +64 | weak_method: weakref.WeakMethod[int], + | ^^^^^^^^^^^^^^^^^^^^^^^ +65 | weak_set: weakref.WeakSet[int], +66 | weak_value_dict: weakref.WeakValueDictionary[object, int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:65:15 + | +63 | weak_key_dict: weakref.WeakKeyDictionary[object, int], +64 | weak_method: weakref.WeakMethod[int], +65 | weak_set: weakref.WeakSet[int], + | ^^^^^^^^^^^^^^^^^^^^ +66 | weak_value_dict: weakref.WeakValueDictionary[object, int], +67 | awaitable: Awaitable[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:66:22 + | +64 | weak_method: weakref.WeakMethod[int], +65 | weak_set: weakref.WeakSet[int], +66 | weak_value_dict: weakref.WeakValueDictionary[object, int], + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +67 | awaitable: Awaitable[int], +68 | coroutine: Coroutine[int, None, str], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:67:16 + | +65 | weak_set: weakref.WeakSet[int], +66 | weak_value_dict: weakref.WeakValueDictionary[object, int], +67 | awaitable: Awaitable[int], + | ^^^^^^^^^^^^^^ +68 | coroutine: Coroutine[int, None, str], +69 | async_iterable: AsyncIterable[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:68:16 + | +66 | weak_value_dict: weakref.WeakValueDictionary[object, int], +67 | awaitable: Awaitable[int], +68 | coroutine: Coroutine[int, None, str], + | ^^^^^^^^^^^^^^^^^^^^^^^^^ +69 | async_iterable: AsyncIterable[int], +70 | async_iterator: AsyncIterator[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:69:21 + | +67 | awaitable: Awaitable[int], +68 | coroutine: Coroutine[int, None, str], +69 | async_iterable: AsyncIterable[int], + | ^^^^^^^^^^^^^^^^^^ +70 | async_iterator: AsyncIterator[int], +71 | async_generator: AsyncGenerator[int, None], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:70:21 + | +68 | coroutine: Coroutine[int, None, str], +69 | async_iterable: AsyncIterable[int], +70 | async_iterator: AsyncIterator[int], + | ^^^^^^^^^^^^^^^^^^ +71 | async_generator: AsyncGenerator[int, None], +72 | iterable: Iterable[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:71:22 + | +69 | async_iterable: AsyncIterable[int], +70 | async_iterator: AsyncIterator[int], +71 | async_generator: AsyncGenerator[int, None], + | ^^^^^^^^^^^^^^^^^^^^^^^^^ +72 | iterable: Iterable[int], +73 | iterator: Iterator[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:72:15 + | +70 | async_iterator: AsyncIterator[int], +71 | async_generator: AsyncGenerator[int, None], +72 | iterable: Iterable[int], + | ^^^^^^^^^^^^^ +73 | iterator: Iterator[int], +74 | generator: Generator[int, None, None], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:73:15 + | +71 | async_generator: AsyncGenerator[int, None], +72 | iterable: Iterable[int], +73 | iterator: Iterator[int], + | ^^^^^^^^^^^^^ +74 | generator: Generator[int, None, None], +75 | reversible: Reversible[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:74:16 + | +72 | iterable: Iterable[int], +73 | iterator: Iterator[int], +74 | generator: Generator[int, None, None], + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ +75 | reversible: Reversible[int], +76 | container: Container[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:75:17 + | +73 | iterator: Iterator[int], +74 | generator: Generator[int, None, None], +75 | reversible: Reversible[int], + | ^^^^^^^^^^^^^^^ +76 | container: Container[int], +77 | collection: Collection[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:76:16 + | +74 | generator: Generator[int, None, None], +75 | reversible: Reversible[int], +76 | container: Container[int], + | ^^^^^^^^^^^^^^ +77 | collection: Collection[int], +78 | callable_obj: Callable[[int], str], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:77:17 + | +75 | reversible: Reversible[int], +76 | container: Container[int], +77 | collection: Collection[int], + | ^^^^^^^^^^^^^^^ +78 | callable_obj: Callable[[int], str], +79 | set_obj: Set[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:78:19 + | +76 | container: Container[int], +77 | collection: Collection[int], +78 | callable_obj: Callable[[int], str], + | ^^^^^^^^^^^^^^^^^^^^ +79 | set_obj: Set[int], +80 | mutable_set: MutableSet[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:79:14 + | +77 | collection: Collection[int], +78 | callable_obj: Callable[[int], str], +79 | set_obj: Set[int], + | ^^^^^^^^ +80 | mutable_set: MutableSet[int], +81 | mapping: Mapping[str, int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:80:18 + | +78 | callable_obj: Callable[[int], str], +79 | set_obj: Set[int], +80 | mutable_set: MutableSet[int], + | ^^^^^^^^^^^^^^^ +81 | mapping: Mapping[str, int], +82 | mutable_mapping: MutableMapping[str, int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:81:14 + | +79 | set_obj: Set[int], +80 | mutable_set: MutableSet[int], +81 | mapping: Mapping[str, int], + | ^^^^^^^^^^^^^^^^^ +82 | mutable_mapping: MutableMapping[str, int], +83 | sequence: Sequence[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:82:22 + | +80 | mutable_set: MutableSet[int], +81 | mapping: Mapping[str, int], +82 | mutable_mapping: MutableMapping[str, int], + | ^^^^^^^^^^^^^^^^^^^^^^^^ +83 | sequence: Sequence[int], +84 | mutable_sequence: MutableSequence[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:83:15 + | +81 | mapping: Mapping[str, int], +82 | mutable_mapping: MutableMapping[str, int], +83 | sequence: Sequence[int], + | ^^^^^^^^^^^^^ +84 | mutable_sequence: MutableSequence[int], +85 | byte_string: ByteString[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:84:23 + | +82 | mutable_mapping: MutableMapping[str, int], +83 | sequence: Sequence[int], +84 | mutable_sequence: MutableSequence[int], + | ^^^^^^^^^^^^^^^^^^^^ +85 | byte_string: ByteString[int], +86 | mapping_view: MappingView[str, int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:85:18 + | +83 | sequence: Sequence[int], +84 | mutable_sequence: MutableSequence[int], +85 | byte_string: ByteString[int], + | ^^^^^^^^^^^^^^^ +86 | mapping_view: MappingView[str, int], +87 | keys_view: KeysView[str], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:86:19 + | +84 | mutable_sequence: MutableSequence[int], +85 | byte_string: ByteString[int], +86 | mapping_view: MappingView[str, int], + | ^^^^^^^^^^^^^^^^^^^^^ +87 | keys_view: KeysView[str], +88 | items_view: ItemsView[str, int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:87:16 + | +85 | byte_string: ByteString[int], +86 | mapping_view: MappingView[str, int], +87 | keys_view: KeysView[str], + | ^^^^^^^^^^^^^ +88 | items_view: ItemsView[str, int], +89 | values_view: ValuesView[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:88:17 + | +86 | mapping_view: MappingView[str, int], +87 | keys_view: KeysView[str], +88 | items_view: ItemsView[str, int], + | ^^^^^^^^^^^^^^^^^^^ +89 | values_view: ValuesView[int], +90 | ) -> None: + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:89:18 + | +87 | keys_view: KeysView[str], +88 | items_view: ItemsView[str, int], +89 | values_view: ValuesView[int], + | ^^^^^^^^^^^^^^^ +90 | ) -> None: +91 | ... + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__fa102_preview_no_future_import_uses_preview_generics.py.snap b/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__fa102_preview_no_future_import_uses_preview_generics.py.snap deleted file mode 100644 index 24dd3a986b..0000000000 --- a/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__fa102_preview_no_future_import_uses_preview_generics.py.snap +++ /dev/null @@ -1,851 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs ---- -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:40:13 - | -39 | def takes_preview_generics( -40 | future: asyncio.Future[int], - | ^^^^^^^^^^^^^^^^^^^ -41 | task: asyncio.Task[str], -42 | deque_object: collections.deque[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:41:11 - | -39 | def takes_preview_generics( -40 | future: asyncio.Future[int], -41 | task: asyncio.Task[str], - | ^^^^^^^^^^^^^^^^^ -42 | deque_object: collections.deque[int], -43 | defaultdict_object: collections.defaultdict[str, int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:42:19 - | -40 | future: asyncio.Future[int], -41 | task: asyncio.Task[str], -42 | deque_object: collections.deque[int], - | ^^^^^^^^^^^^^^^^^^^^^^ -43 | defaultdict_object: collections.defaultdict[str, int], -44 | ordered_dict: collections.OrderedDict[str, int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:43:25 - | -41 | task: asyncio.Task[str], -42 | deque_object: collections.deque[int], -43 | defaultdict_object: collections.defaultdict[str, int], - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -44 | ordered_dict: collections.OrderedDict[str, int], -45 | counter_obj: collections.Counter[str], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:44:19 - | -42 | deque_object: collections.deque[int], -43 | defaultdict_object: collections.defaultdict[str, int], -44 | ordered_dict: collections.OrderedDict[str, int], - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -45 | counter_obj: collections.Counter[str], -46 | chain_map: collections.ChainMap[str, int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:45:18 - | -43 | defaultdict_object: collections.defaultdict[str, int], -44 | ordered_dict: collections.OrderedDict[str, int], -45 | counter_obj: collections.Counter[str], - | ^^^^^^^^^^^^^^^^^^^^^^^^ -46 | chain_map: collections.ChainMap[str, int], -47 | context_manager: contextlib.AbstractContextManager[str], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:46:16 - | -44 | ordered_dict: collections.OrderedDict[str, int], -45 | counter_obj: collections.Counter[str], -46 | chain_map: collections.ChainMap[str, int], - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -47 | context_manager: contextlib.AbstractContextManager[str], -48 | async_context_manager: contextlib.AbstractAsyncContextManager[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:47:22 - | -45 | counter_obj: collections.Counter[str], -46 | chain_map: collections.ChainMap[str, int], -47 | context_manager: contextlib.AbstractContextManager[str], - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -48 | async_context_manager: contextlib.AbstractAsyncContextManager[int], -49 | dataclass_field: dataclasses.Field[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:48:28 - | -46 | chain_map: collections.ChainMap[str, int], -47 | context_manager: contextlib.AbstractContextManager[str], -48 | async_context_manager: contextlib.AbstractAsyncContextManager[int], - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -49 | dataclass_field: dataclasses.Field[int], -50 | cached_prop: functools.cached_property[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:49:22 - | -47 | context_manager: contextlib.AbstractContextManager[str], -48 | async_context_manager: contextlib.AbstractAsyncContextManager[int], -49 | dataclass_field: dataclasses.Field[int], - | ^^^^^^^^^^^^^^^^^^^^^^ -50 | cached_prop: functools.cached_property[int], -51 | partial_method: functools.partialmethod[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:50:18 - | -48 | async_context_manager: contextlib.AbstractAsyncContextManager[int], -49 | dataclass_field: dataclasses.Field[int], -50 | cached_prop: functools.cached_property[int], - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -51 | partial_method: functools.partialmethod[int], -52 | path_like: os.PathLike[str], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:51:21 - | -49 | dataclass_field: dataclasses.Field[int], -50 | cached_prop: functools.cached_property[int], -51 | partial_method: functools.partialmethod[int], - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -52 | path_like: os.PathLike[str], -53 | lifo_queue: queue.LifoQueue[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:52:16 - | -50 | cached_prop: functools.cached_property[int], -51 | partial_method: functools.partialmethod[int], -52 | path_like: os.PathLike[str], - | ^^^^^^^^^^^^^^^^ -53 | lifo_queue: queue.LifoQueue[int], -54 | regular_queue: queue.Queue[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:53:17 - | -51 | partial_method: functools.partialmethod[int], -52 | path_like: os.PathLike[str], -53 | lifo_queue: queue.LifoQueue[int], - | ^^^^^^^^^^^^^^^^^^^^ -54 | regular_queue: queue.Queue[int], -55 | priority_queue: queue.PriorityQueue[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:54:20 - | -52 | path_like: os.PathLike[str], -53 | lifo_queue: queue.LifoQueue[int], -54 | regular_queue: queue.Queue[int], - | ^^^^^^^^^^^^^^^^ -55 | priority_queue: queue.PriorityQueue[int], -56 | simple_queue: queue.SimpleQueue[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:55:21 - | -53 | lifo_queue: queue.LifoQueue[int], -54 | regular_queue: queue.Queue[int], -55 | priority_queue: queue.PriorityQueue[int], - | ^^^^^^^^^^^^^^^^^^^^^^^^ -56 | simple_queue: queue.SimpleQueue[int], -57 | regex_pattern: re.Pattern[str], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:56:19 - | -54 | regular_queue: queue.Queue[int], -55 | priority_queue: queue.PriorityQueue[int], -56 | simple_queue: queue.SimpleQueue[int], - | ^^^^^^^^^^^^^^^^^^^^^^ -57 | regex_pattern: re.Pattern[str], -58 | regex_match: re.Match[str], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:57:20 - | -55 | priority_queue: queue.PriorityQueue[int], -56 | simple_queue: queue.SimpleQueue[int], -57 | regex_pattern: re.Pattern[str], - | ^^^^^^^^^^^^^^^ -58 | regex_match: re.Match[str], -59 | bsd_db_shelf: shelve.BsdDbShelf[str, int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:58:18 - | -56 | simple_queue: queue.SimpleQueue[int], -57 | regex_pattern: re.Pattern[str], -58 | regex_match: re.Match[str], - | ^^^^^^^^^^^^^ -59 | bsd_db_shelf: shelve.BsdDbShelf[str, int], -60 | db_filename_shelf: shelve.DbfilenameShelf[str, int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:59:19 - | -57 | regex_pattern: re.Pattern[str], -58 | regex_match: re.Match[str], -59 | bsd_db_shelf: shelve.BsdDbShelf[str, int], - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ -60 | db_filename_shelf: shelve.DbfilenameShelf[str, int], -61 | shelf_obj: shelve.Shelf[str, int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:60:24 - | -58 | regex_match: re.Match[str], -59 | bsd_db_shelf: shelve.BsdDbShelf[str, int], -60 | db_filename_shelf: shelve.DbfilenameShelf[str, int], - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -61 | shelf_obj: shelve.Shelf[str, int], -62 | mapping_proxy: types.MappingProxyType[str, int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:61:16 - | -59 | bsd_db_shelf: shelve.BsdDbShelf[str, int], -60 | db_filename_shelf: shelve.DbfilenameShelf[str, int], -61 | shelf_obj: shelve.Shelf[str, int], - | ^^^^^^^^^^^^^^^^^^^^^^ -62 | mapping_proxy: types.MappingProxyType[str, int], -63 | weak_key_dict: weakref.WeakKeyDictionary[object, int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:62:20 - | -60 | db_filename_shelf: shelve.DbfilenameShelf[str, int], -61 | shelf_obj: shelve.Shelf[str, int], -62 | mapping_proxy: types.MappingProxyType[str, int], - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -63 | weak_key_dict: weakref.WeakKeyDictionary[object, int], -64 | weak_method: weakref.WeakMethod[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:63:20 - | -61 | shelf_obj: shelve.Shelf[str, int], -62 | mapping_proxy: types.MappingProxyType[str, int], -63 | weak_key_dict: weakref.WeakKeyDictionary[object, int], - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -64 | weak_method: weakref.WeakMethod[int], -65 | weak_set: weakref.WeakSet[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:64:18 - | -62 | mapping_proxy: types.MappingProxyType[str, int], -63 | weak_key_dict: weakref.WeakKeyDictionary[object, int], -64 | weak_method: weakref.WeakMethod[int], - | ^^^^^^^^^^^^^^^^^^^^^^^ -65 | weak_set: weakref.WeakSet[int], -66 | weak_value_dict: weakref.WeakValueDictionary[object, int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:65:15 - | -63 | weak_key_dict: weakref.WeakKeyDictionary[object, int], -64 | weak_method: weakref.WeakMethod[int], -65 | weak_set: weakref.WeakSet[int], - | ^^^^^^^^^^^^^^^^^^^^ -66 | weak_value_dict: weakref.WeakValueDictionary[object, int], -67 | awaitable: Awaitable[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:66:22 - | -64 | weak_method: weakref.WeakMethod[int], -65 | weak_set: weakref.WeakSet[int], -66 | weak_value_dict: weakref.WeakValueDictionary[object, int], - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -67 | awaitable: Awaitable[int], -68 | coroutine: Coroutine[int, None, str], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:67:16 - | -65 | weak_set: weakref.WeakSet[int], -66 | weak_value_dict: weakref.WeakValueDictionary[object, int], -67 | awaitable: Awaitable[int], - | ^^^^^^^^^^^^^^ -68 | coroutine: Coroutine[int, None, str], -69 | async_iterable: AsyncIterable[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:68:16 - | -66 | weak_value_dict: weakref.WeakValueDictionary[object, int], -67 | awaitable: Awaitable[int], -68 | coroutine: Coroutine[int, None, str], - | ^^^^^^^^^^^^^^^^^^^^^^^^^ -69 | async_iterable: AsyncIterable[int], -70 | async_iterator: AsyncIterator[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:69:21 - | -67 | awaitable: Awaitable[int], -68 | coroutine: Coroutine[int, None, str], -69 | async_iterable: AsyncIterable[int], - | ^^^^^^^^^^^^^^^^^^ -70 | async_iterator: AsyncIterator[int], -71 | async_generator: AsyncGenerator[int, None], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:70:21 - | -68 | coroutine: Coroutine[int, None, str], -69 | async_iterable: AsyncIterable[int], -70 | async_iterator: AsyncIterator[int], - | ^^^^^^^^^^^^^^^^^^ -71 | async_generator: AsyncGenerator[int, None], -72 | iterable: Iterable[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:71:22 - | -69 | async_iterable: AsyncIterable[int], -70 | async_iterator: AsyncIterator[int], -71 | async_generator: AsyncGenerator[int, None], - | ^^^^^^^^^^^^^^^^^^^^^^^^^ -72 | iterable: Iterable[int], -73 | iterator: Iterator[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:72:15 - | -70 | async_iterator: AsyncIterator[int], -71 | async_generator: AsyncGenerator[int, None], -72 | iterable: Iterable[int], - | ^^^^^^^^^^^^^ -73 | iterator: Iterator[int], -74 | generator: Generator[int, None, None], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:73:15 - | -71 | async_generator: AsyncGenerator[int, None], -72 | iterable: Iterable[int], -73 | iterator: Iterator[int], - | ^^^^^^^^^^^^^ -74 | generator: Generator[int, None, None], -75 | reversible: Reversible[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:74:16 - | -72 | iterable: Iterable[int], -73 | iterator: Iterator[int], -74 | generator: Generator[int, None, None], - | ^^^^^^^^^^^^^^^^^^^^^^^^^^ -75 | reversible: Reversible[int], -76 | container: Container[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:75:17 - | -73 | iterator: Iterator[int], -74 | generator: Generator[int, None, None], -75 | reversible: Reversible[int], - | ^^^^^^^^^^^^^^^ -76 | container: Container[int], -77 | collection: Collection[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:76:16 - | -74 | generator: Generator[int, None, None], -75 | reversible: Reversible[int], -76 | container: Container[int], - | ^^^^^^^^^^^^^^ -77 | collection: Collection[int], -78 | callable_obj: Callable[[int], str], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:77:17 - | -75 | reversible: Reversible[int], -76 | container: Container[int], -77 | collection: Collection[int], - | ^^^^^^^^^^^^^^^ -78 | callable_obj: Callable[[int], str], -79 | set_obj: Set[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:78:19 - | -76 | container: Container[int], -77 | collection: Collection[int], -78 | callable_obj: Callable[[int], str], - | ^^^^^^^^^^^^^^^^^^^^ -79 | set_obj: Set[int], -80 | mutable_set: MutableSet[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:79:14 - | -77 | collection: Collection[int], -78 | callable_obj: Callable[[int], str], -79 | set_obj: Set[int], - | ^^^^^^^^ -80 | mutable_set: MutableSet[int], -81 | mapping: Mapping[str, int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:80:18 - | -78 | callable_obj: Callable[[int], str], -79 | set_obj: Set[int], -80 | mutable_set: MutableSet[int], - | ^^^^^^^^^^^^^^^ -81 | mapping: Mapping[str, int], -82 | mutable_mapping: MutableMapping[str, int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:81:14 - | -79 | set_obj: Set[int], -80 | mutable_set: MutableSet[int], -81 | mapping: Mapping[str, int], - | ^^^^^^^^^^^^^^^^^ -82 | mutable_mapping: MutableMapping[str, int], -83 | sequence: Sequence[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:82:22 - | -80 | mutable_set: MutableSet[int], -81 | mapping: Mapping[str, int], -82 | mutable_mapping: MutableMapping[str, int], - | ^^^^^^^^^^^^^^^^^^^^^^^^ -83 | sequence: Sequence[int], -84 | mutable_sequence: MutableSequence[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:83:15 - | -81 | mapping: Mapping[str, int], -82 | mutable_mapping: MutableMapping[str, int], -83 | sequence: Sequence[int], - | ^^^^^^^^^^^^^ -84 | mutable_sequence: MutableSequence[int], -85 | byte_string: ByteString[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:84:23 - | -82 | mutable_mapping: MutableMapping[str, int], -83 | sequence: Sequence[int], -84 | mutable_sequence: MutableSequence[int], - | ^^^^^^^^^^^^^^^^^^^^ -85 | byte_string: ByteString[int], -86 | mapping_view: MappingView[str, int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:85:18 - | -83 | sequence: Sequence[int], -84 | mutable_sequence: MutableSequence[int], -85 | byte_string: ByteString[int], - | ^^^^^^^^^^^^^^^ -86 | mapping_view: MappingView[str, int], -87 | keys_view: KeysView[str], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:86:19 - | -84 | mutable_sequence: MutableSequence[int], -85 | byte_string: ByteString[int], -86 | mapping_view: MappingView[str, int], - | ^^^^^^^^^^^^^^^^^^^^^ -87 | keys_view: KeysView[str], -88 | items_view: ItemsView[str, int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:87:16 - | -85 | byte_string: ByteString[int], -86 | mapping_view: MappingView[str, int], -87 | keys_view: KeysView[str], - | ^^^^^^^^^^^^^ -88 | items_view: ItemsView[str, int], -89 | values_view: ValuesView[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:88:17 - | -86 | mapping_view: MappingView[str, int], -87 | keys_view: KeysView[str], -88 | items_view: ItemsView[str, int], - | ^^^^^^^^^^^^^^^^^^^ -89 | values_view: ValuesView[int], -90 | ) -> None: - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:89:18 - | -87 | keys_view: KeysView[str], -88 | items_view: ItemsView[str, int], -89 | values_view: ValuesView[int], - | ^^^^^^^^^^^^^^^ -90 | ) -> None: -91 | ... - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/flake8_gettext/mod.rs b/crates/ruff_linter/src/rules/flake8_gettext/mod.rs index e0469f93d1..21d78d8c76 100644 --- a/crates/ruff_linter/src/rules/flake8_gettext/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_gettext/mod.rs @@ -1,8 +1,6 @@ //! Rules from [flake8-gettext](https://pypi.org/project/flake8-gettext/). use crate::checkers::ast::Checker; -use crate::preview::{ - is_extended_i18n_function_matching_enabled, is_plural_ngettext_check_enabled, -}; +use crate::preview::is_plural_ngettext_check_enabled; use ruff_python_ast::name::Name; use ruff_python_ast::{self as ast, Expr}; use ruff_python_semantic::Modules; @@ -44,10 +42,6 @@ pub(crate) fn is_gettext_func_call( return true; } - if !is_extended_i18n_function_matching_enabled(checker.settings()) { - return false; - } - let semantic = checker.semantic(); let Some(qualified_name) = semantic.resolve_qualified_name(func) else { @@ -77,7 +71,6 @@ mod tests { use test_case::test_case; use crate::registry::Rule; - use crate::settings::types::PreviewMode; use crate::test::test_path; use crate::{assert_diagnostics, settings}; @@ -101,10 +94,7 @@ mod tests { let snapshot = format!("preview__{}_{}", rule_code.name(), path.to_string_lossy()); let diagnostics = test_path( Path::new("flake8_gettext").join(path).as_path(), - &settings::LinterSettings { - preview: PreviewMode::Enabled, - ..settings::LinterSettings::for_rule(rule_code) - }, + &settings::LinterSettings::for_rule(rule_code).with_preview_mode(), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) 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 da85f34697..143c76d741 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 @@ -57,7 +57,9 @@ impl Violation for FormatInGetTextFuncCall { if self.is_plural { "`format` method in plural argument is resolved before function call".to_string() } else { - "`format` method argument is resolved before function call; consider `_(\"string %s\") % arg`".to_string() + "`format` method argument is resolved before function call; \ + consider `_(\"string %s\") % arg`" + .to_string() } } } diff --git a/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__f-string-in-get-text-func-call_INT001.py.snap b/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__f-string-in-get-text-func-call_INT001.py.snap index 0285458057..db4755d6ef 100644 --- a/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__f-string-in-get-text-func-call_INT001.py.snap +++ b/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__f-string-in-get-text-func-call_INT001.py.snap @@ -30,6 +30,45 @@ INT001 f-string is resolved before function call; consider `_("string %s") % arg 9 | _gettext(f"{'value'}") # no lint | +INT001 f-string is resolved before function call; consider `_("string %s") % arg` + --> INT001.py:22:21 + | +20 | name = "Guido" +21 | +22 | gettext_mod.gettext(f"Hello, {name}!") + | ^^^^^^^^^^^^^^^^^ +23 | gettext_mod.ngettext(f"Hello, {name}!", f"Hello, {name}s!", 2) +24 | gettext_fn(f"Hello, {name}!") + | + +INT001 f-string is resolved before function call; consider `_("string %s") % arg` + --> INT001.py:23:22 + | +22 | gettext_mod.gettext(f"Hello, {name}!") +23 | gettext_mod.ngettext(f"Hello, {name}!", f"Hello, {name}s!", 2) + | ^^^^^^^^^^^^^^^^^ +24 | gettext_fn(f"Hello, {name}!") +25 | ngettext_fn(f"Hello, {name}!", f"Hello, {name}s!", 2) + | + +INT001 f-string is resolved before function call; consider `_("string %s") % arg` + --> INT001.py:24:12 + | +22 | gettext_mod.gettext(f"Hello, {name}!") +23 | gettext_mod.ngettext(f"Hello, {name}!", f"Hello, {name}s!", 2) +24 | gettext_fn(f"Hello, {name}!") + | ^^^^^^^^^^^^^^^^^ +25 | ngettext_fn(f"Hello, {name}!", f"Hello, {name}s!", 2) + | + +INT001 f-string is resolved before function call; consider `_("string %s") % arg` + --> INT001.py:25:13 + | +23 | gettext_mod.ngettext(f"Hello, {name}!", f"Hello, {name}s!", 2) +24 | gettext_fn(f"Hello, {name}!") +25 | ngettext_fn(f"Hello, {name}!", f"Hello, {name}s!", 2) + | ^^^^^^^^^^^^^^^^^ + INT001 f-string is resolved before function call; consider `_("string %s") % arg` --> INT001.py:31:14 | @@ -90,4 +129,31 @@ INT001 f-string is resolved before function call; consider `_("string %s") % arg 40 | print(_(a)) 41 | print(_(f"{a}")) | ^^^^^^ + +INT001 f-string is resolved before function call; consider `_("string %s") % arg` + --> INT001.py:51:12 + | +49 | builtins.__dict__["gettext"] = gettext_fn +50 | +51 | builtins._(f"{'value'}") + | ^^^^^^^^^^^^ +52 | builtins.gettext(f"{'value'}") +53 | builtins.ngettext(f"{'value'}", f"{'values'}", 2) + | + +INT001 f-string is resolved before function call; consider `_("string %s") % arg` + --> INT001.py:52:18 + | +51 | builtins._(f"{'value'}") +52 | builtins.gettext(f"{'value'}") + | ^^^^^^^^^^^^ +53 | builtins.ngettext(f"{'value'}", f"{'values'}", 2) + | + +INT001 f-string is resolved before function call; consider `_("string %s") % arg` + --> INT001.py:53:19 | +51 | builtins._(f"{'value'}") +52 | builtins.gettext(f"{'value'}") +53 | builtins.ngettext(f"{'value'}", f"{'values'}", 2) + | ^^^^^^^^^^^^ diff --git a/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__format-in-get-text-func-call_INT002.py.snap b/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__format-in-get-text-func-call_INT002.py.snap index b1edf104de..a2da0ebcdb 100644 --- a/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__format-in-get-text-func-call_INT002.py.snap +++ b/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__format-in-get-text-func-call_INT002.py.snap @@ -30,6 +30,45 @@ INT002 `format` method argument is resolved before function call; consider `_("s 5 | _gettext("{}".format("line")) # no lint | +INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg` + --> INT002.py:18:21 + | +16 | name = "Guido" +17 | +18 | gettext_mod.gettext("Hello, {}!".format(name)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^ +19 | gettext_mod.ngettext("Hello, {}!".format(name), "Hello, {}s!".format(name), 2) +20 | gettext_fn("Hello, {}!".format(name)) + | + +INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg` + --> INT002.py:19:22 + | +18 | gettext_mod.gettext("Hello, {}!".format(name)) +19 | gettext_mod.ngettext("Hello, {}!".format(name), "Hello, {}s!".format(name), 2) + | ^^^^^^^^^^^^^^^^^^^^^^^^^ +20 | gettext_fn("Hello, {}!".format(name)) +21 | ngettext_fn("Hello, {}!".format(name), "Hello, {}s!".format(name), 2) + | + +INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg` + --> INT002.py:20:12 + | +18 | gettext_mod.gettext("Hello, {}!".format(name)) +19 | gettext_mod.ngettext("Hello, {}!".format(name), "Hello, {}s!".format(name), 2) +20 | gettext_fn("Hello, {}!".format(name)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^ +21 | ngettext_fn("Hello, {}!".format(name), "Hello, {}s!".format(name), 2) + | + +INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg` + --> INT002.py:21:13 + | +19 | gettext_mod.ngettext("Hello, {}!".format(name), "Hello, {}s!".format(name), 2) +20 | gettext_fn("Hello, {}!".format(name)) +21 | ngettext_fn("Hello, {}!".format(name), "Hello, {}s!".format(name), 2) + | ^^^^^^^^^^^^^^^^^^^^^^^^^ + INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg` --> INT002.py:27:14 | @@ -90,4 +129,31 @@ INT002 `format` method argument is resolved before function call; consider `_("s 36 | print(_(a)) 37 | print(_("{}".format(a))) | ^^^^^^^^^^^^^^ + +INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg` + --> INT002.py:47:12 + | +45 | builtins.__dict__["gettext"] = gettext_fn +46 | +47 | builtins._("{}".format("line")) + | ^^^^^^^^^^^^^^^^^^^ +48 | builtins.gettext("{}".format("line")) +49 | builtins.ngettext("{}".format("line"), "{}".format("lines"), 2) + | + +INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg` + --> INT002.py:48:18 + | +47 | builtins._("{}".format("line")) +48 | builtins.gettext("{}".format("line")) + | ^^^^^^^^^^^^^^^^^^^ +49 | builtins.ngettext("{}".format("line"), "{}".format("lines"), 2) + | + +INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg` + --> INT002.py:49:19 | +47 | builtins._("{}".format("line")) +48 | builtins.gettext("{}".format("line")) +49 | builtins.ngettext("{}".format("line"), "{}".format("lines"), 2) + | ^^^^^^^^^^^^^^^^^^^ diff --git a/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__preview__f-string-in-get-text-func-call_INT001.py.snap b/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__preview__f-string-in-get-text-func-call_INT001.py.snap index db981c06b3..5635a06fe7 100644 --- a/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__preview__f-string-in-get-text-func-call_INT001.py.snap +++ b/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__preview__f-string-in-get-text-func-call_INT001.py.snap @@ -88,7 +88,6 @@ INT001 f-string is resolved before function call; consider `_("string %s") % arg 24 | gettext_fn(f"Hello, {name}!") 25 | ngettext_fn(f"Hello, {name}!", f"Hello, {name}s!", 2) | ^^^^^^^^^^^^^^^^^ - | INT001 f-string in plural argument is resolved before function call --> INT001.py:25:32 @@ -97,7 +96,6 @@ INT001 f-string in plural argument is resolved before function call 24 | gettext_fn(f"Hello, {name}!") 25 | ngettext_fn(f"Hello, {name}!", f"Hello, {name}s!", 2) | ^^^^^^^^^^^^^^^^^^ - | INT001 f-string is resolved before function call; consider `_("string %s") % arg` --> INT001.py:31:14 @@ -159,7 +157,6 @@ INT001 f-string is resolved before function call; consider `_("string %s") % arg 40 | print(_(a)) 41 | print(_(f"{a}")) | ^^^^^^ - | INT001 f-string is resolved before function call; consider `_("string %s") % arg` --> INT001.py:51:12 @@ -188,7 +185,6 @@ INT001 f-string is resolved before function call; consider `_("string %s") % arg 52 | builtins.gettext(f"{'value'}") 53 | builtins.ngettext(f"{'value'}", f"{'values'}", 2) | ^^^^^^^^^^^^ - | INT001 f-string in plural argument is resolved before function call --> INT001.py:53:33 @@ -197,4 +193,3 @@ INT001 f-string in plural argument is resolved before function call 52 | builtins.gettext(f"{'value'}") 53 | builtins.ngettext(f"{'value'}", f"{'values'}", 2) | ^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__preview__format-in-get-text-func-call_INT002.py.snap b/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__preview__format-in-get-text-func-call_INT002.py.snap index ad26c43eff..07279f41cb 100644 --- a/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__preview__format-in-get-text-func-call_INT002.py.snap +++ b/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__preview__format-in-get-text-func-call_INT002.py.snap @@ -89,7 +89,6 @@ INT002 `format` method argument is resolved before function call; consider `_("s 20 | gettext_fn("Hello, {}!".format(name)) 21 | ngettext_fn("Hello, {}!".format(name), "Hello, {}s!".format(name), 2) | ^^^^^^^^^^^^^^^^^^^^^^^^^ - | INT002 `format` method in plural argument is resolved before function call --> INT002.py:21:40 @@ -98,7 +97,6 @@ INT002 `format` method in plural argument is resolved before function call 20 | gettext_fn("Hello, {}!".format(name)) 21 | ngettext_fn("Hello, {}!".format(name), "Hello, {}s!".format(name), 2) | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg` --> INT002.py:27:14 @@ -160,7 +158,6 @@ INT002 `format` method argument is resolved before function call; consider `_("s 36 | print(_(a)) 37 | print(_("{}".format(a))) | ^^^^^^^^^^^^^^ - | INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg` --> INT002.py:47:12 @@ -189,7 +186,6 @@ INT002 `format` method argument is resolved before function call; consider `_("s 48 | builtins.gettext("{}".format("line")) 49 | builtins.ngettext("{}".format("line"), "{}".format("lines"), 2) | ^^^^^^^^^^^^^^^^^^^ - | INT002 `format` method in plural argument is resolved before function call --> INT002.py:49:40 @@ -198,4 +194,3 @@ INT002 `format` method in plural argument is resolved before function call 48 | builtins.gettext("{}".format("line")) 49 | builtins.ngettext("{}".format("line"), "{}".format("lines"), 2) | ^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__preview__printf-in-get-text-func-call_INT003.py.snap b/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__preview__printf-in-get-text-func-call_INT003.py.snap index 496caa891d..9b92dd6f9a 100644 --- a/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__preview__printf-in-get-text-func-call_INT003.py.snap +++ b/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__preview__printf-in-get-text-func-call_INT003.py.snap @@ -89,7 +89,6 @@ INT003 printf-style format is resolved before function call; consider `_("string 20 | gettext_fn("Hello, %s!" % name) 21 | ngettext_fn("Hello, %s!" % name, "Hello, %ss!" % name, 2) | ^^^^^^^^^^^^^^^^^^^ - | INT003 printf-style format in plural argument is resolved before function call --> INT003.py:21:34 @@ -98,7 +97,6 @@ INT003 printf-style format in plural argument is resolved before function call 20 | gettext_fn("Hello, %s!" % name) 21 | ngettext_fn("Hello, %s!" % name, "Hello, %ss!" % name, 2) | ^^^^^^^^^^^^^^^^^^^^ - | INT003 printf-style format is resolved before function call; consider `_("string %s") % arg` --> INT003.py:27:14 @@ -160,7 +158,6 @@ INT003 printf-style format is resolved before function call; consider `_("string 36 | print(_(a)) 37 | print(_("%s" % a)) | ^^^^^^^^ - | INT003 printf-style format is resolved before function call; consider `_("string %s") % arg` --> INT003.py:47:12 @@ -189,7 +186,6 @@ INT003 printf-style format is resolved before function call; consider `_("string 48 | builtins.gettext("%s" % "line") 49 | builtins.ngettext("%s" % "line", "%s" % "lines", 2) | ^^^^^^^^^^^^^ - | INT003 printf-style format in plural argument is resolved before function call --> INT003.py:49:34 @@ -198,4 +194,3 @@ INT003 printf-style format in plural argument is resolved before function call 48 | builtins.gettext("%s" % "line") 49 | builtins.ngettext("%s" % "line", "%s" % "lines", 2) | ^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__printf-in-get-text-func-call_INT003.py.snap b/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__printf-in-get-text-func-call_INT003.py.snap index 91260001b7..9e1254ac45 100644 --- a/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__printf-in-get-text-func-call_INT003.py.snap +++ b/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__printf-in-get-text-func-call_INT003.py.snap @@ -30,6 +30,45 @@ INT003 printf-style format is resolved before function call; consider `_("string 5 | _gettext("%s" % "line") # no lint | +INT003 printf-style format is resolved before function call; consider `_("string %s") % arg` + --> INT003.py:18:21 + | +16 | name = "Guido" +17 | +18 | gettext_mod.gettext("Hello, %s!" % name) + | ^^^^^^^^^^^^^^^^^^^ +19 | gettext_mod.ngettext("Hello, %s!" % name, "Hello, %ss!" % name, 2) +20 | gettext_fn("Hello, %s!" % name) + | + +INT003 printf-style format is resolved before function call; consider `_("string %s") % arg` + --> INT003.py:19:22 + | +18 | gettext_mod.gettext("Hello, %s!" % name) +19 | gettext_mod.ngettext("Hello, %s!" % name, "Hello, %ss!" % name, 2) + | ^^^^^^^^^^^^^^^^^^^ +20 | gettext_fn("Hello, %s!" % name) +21 | ngettext_fn("Hello, %s!" % name, "Hello, %ss!" % name, 2) + | + +INT003 printf-style format is resolved before function call; consider `_("string %s") % arg` + --> INT003.py:20:12 + | +18 | gettext_mod.gettext("Hello, %s!" % name) +19 | gettext_mod.ngettext("Hello, %s!" % name, "Hello, %ss!" % name, 2) +20 | gettext_fn("Hello, %s!" % name) + | ^^^^^^^^^^^^^^^^^^^ +21 | ngettext_fn("Hello, %s!" % name, "Hello, %ss!" % name, 2) + | + +INT003 printf-style format is resolved before function call; consider `_("string %s") % arg` + --> INT003.py:21:13 + | +19 | gettext_mod.ngettext("Hello, %s!" % name, "Hello, %ss!" % name, 2) +20 | gettext_fn("Hello, %s!" % name) +21 | ngettext_fn("Hello, %s!" % name, "Hello, %ss!" % name, 2) + | ^^^^^^^^^^^^^^^^^^^ + INT003 printf-style format is resolved before function call; consider `_("string %s") % arg` --> INT003.py:27:14 | @@ -90,4 +129,31 @@ INT003 printf-style format is resolved before function call; consider `_("string 36 | print(_(a)) 37 | print(_("%s" % a)) | ^^^^^^^^ + +INT003 printf-style format is resolved before function call; consider `_("string %s") % arg` + --> INT003.py:47:12 + | +45 | builtins.__dict__["gettext"] = gettext_fn +46 | +47 | builtins._("%s" % "line") + | ^^^^^^^^^^^^^ +48 | builtins.gettext("%s" % "line") +49 | builtins.ngettext("%s" % "line", "%s" % "lines", 2) + | + +INT003 printf-style format is resolved before function call; consider `_("string %s") % arg` + --> INT003.py:48:18 + | +47 | builtins._("%s" % "line") +48 | builtins.gettext("%s" % "line") + | ^^^^^^^^^^^^^ +49 | builtins.ngettext("%s" % "line", "%s" % "lines", 2) + | + +INT003 printf-style format is resolved before function call; consider `_("string %s") % arg` + --> INT003.py:49:19 | +47 | builtins._("%s" % "line") +48 | builtins.gettext("%s" % "line") +49 | builtins.ngettext("%s" % "line", "%s" % "lines", 2) + | ^^^^^^^^^^^^^ diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs index 6d52ce7d2e..bf68c8a671 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs @@ -49,9 +49,9 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// ## Fix safety /// The fix is safe in that it does not change the semantics of your code. /// However, the issue is that you may often want to change semantics -/// by adding a missing comma. +/// by adding a missing comma. Thus, the fix is always marked as unsafe. #[derive(ViolationMetadata)] -#[violation_metadata(preview_since = "0.14.10")] +#[violation_metadata(stable_since = "0.16.0")] pub(crate) struct ImplicitStringConcatenationInCollectionLiteral; impl Violation for ImplicitStringConcatenationInCollectionLiteral { 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 ab330314ee..9e98634ba0 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 @@ -436,7 +436,6 @@ ISC001 [*] Implicitly concatenated string literals on one line 92 | _ = "\12""foo" # fix should be "\12foo" 93 | _ = "\12" "" # fix should be "\12" | ^^^^^^^^ - | help: Combine string literals | 92 | _ = "\12""foo" # fix should be "\12foo" 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 ac74361ae5..e4f0da91a0 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 @@ -186,12 +186,10 @@ invalid-syntax: f-string: unterminated triple-quoted string 28 | | "i" "j" 29 | | ) | |__^ - | invalid-syntax: f-string: unterminated string - --> ISC_syntax_error.py:30:1 + --> ISC_syntax_error.py:29:3 | 28 | "i" "j" 29 | ) | ^ - | 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_2.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_2.py.snap index 6cd8232919..b63614973d 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC001_ISC_syntax_error_2.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_2.py.snap @@ -121,7 +121,6 @@ ISC001 Implicitly concatenated string literals on one line 6 | f"" f" 7 | f"" f"" f" | ^^^^^^^ - | help: Combine string literals invalid-syntax: f-string: unterminated string @@ -131,4 +130,3 @@ invalid-syntax: f-string: unterminated string 6 | f"" f" 7 | f"" f"" f" | ^ - | diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC002_ISC.py.snap b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC002_ISC.py.snap index 290691d0a7..d7c775dcf7 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC002_ISC.py.snap +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC002_ISC.py.snap @@ -47,4 +47,3 @@ ISC002 Implicitly concatenated string literals over multiple lines | __________^ 209 | | t"def"} g" | |__________^ - | 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 6c645ec5d1..171fb45abe 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 @@ -99,12 +99,10 @@ invalid-syntax: f-string: unterminated triple-quoted string 28 | | "i" "j" 29 | | ) | |__^ - | invalid-syntax: f-string: unterminated string - --> ISC_syntax_error.py:30:1 + --> ISC_syntax_error.py:29:3 | 28 | "i" "j" 29 | ) | ^ - | 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_2.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_2.py.snap index 3f47730b50..0c97d71ca7 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC002_ISC_syntax_error_2.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_2.py.snap @@ -50,4 +50,3 @@ invalid-syntax: f-string: unterminated string 6 | f"" f" 7 | f"" f"" f" | ^ - | 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 71bcd5365a..aef8d1887a 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 @@ -349,7 +349,6 @@ ISC003 Explicitly concatenated string should be implicitly concatenated 223 | | "def" 224 | | ) + "ghi" | |_________^ - | help: Remove redundant '+' operator to implicitly concatenate ISC003 [*] Explicitly concatenated string should be implicitly concatenated 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 ab330314ee..9e98634ba0 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 @@ -436,7 +436,6 @@ ISC001 [*] Implicitly concatenated string literals on one line 92 | _ = "\12""foo" # fix should be "\12foo" 93 | _ = "\12" "" # fix should be "\12" | ^^^^^^^^ - | help: Combine string literals | 92 | _ = "\12""foo" # fix should be "\12foo" diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__multiline_ISC002_ISC.py.snap b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__multiline_ISC002_ISC.py.snap index b0882b15b3..345c1e2f76 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__multiline_ISC002_ISC.py.snap +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__multiline_ISC002_ISC.py.snap @@ -88,7 +88,6 @@ ISC002 Implicitly concatenated string literals over multiple lines | __________^ 209 | | t"def"} g" | |__________^ - | ISC002 Implicitly concatenated string literals over multiple lines --> ISC.py:217:5 diff --git a/crates/ruff_linter/src/rules/flake8_import_conventions/settings.rs b/crates/ruff_linter/src/rules/flake8_import_conventions/settings.rs index 7574d1f2d5..be50ed5255 100644 --- a/crates/ruff_linter/src/rules/flake8_import_conventions/settings.rs +++ b/crates/ruff_linter/src/rules/flake8_import_conventions/settings.rs @@ -53,7 +53,7 @@ impl Display for BannedAliases { impl BannedAliases { /// Returns an iterator over the banned aliases. - pub fn iter(&self) -> impl Iterator { + pub(crate) fn iter(&self) -> impl Iterator { self.0.iter().map(String::as_str) } } diff --git a/crates/ruff_linter/src/rules/flake8_import_conventions/snapshots/ruff_linter__rules__flake8_import_conventions__tests__defaults.snap b/crates/ruff_linter/src/rules/flake8_import_conventions/snapshots/ruff_linter__rules__flake8_import_conventions__tests__defaults.snap index 24522f7090..7a919ec733 100644 --- a/crates/ruff_linter/src/rules/flake8_import_conventions/snapshots/ruff_linter__rules__flake8_import_conventions__tests__defaults.snap +++ b/crates/ruff_linter/src/rules/flake8_import_conventions/snapshots/ruff_linter__rules__flake8_import_conventions__tests__defaults.snap @@ -113,7 +113,6 @@ ICN001 [*] `networkx` should be imported as `nx` 11 | import tkinter 12 | import networkx | ^^^^^^^^ - | help: Alias `networkx` to `nx` | 11 | import tkinter @@ -242,7 +241,6 @@ ICN001 [*] `networkx` should be imported as `nx` 21 | import tkinter as tkr 22 | import networkx as nxy | ^^^ - | help: Alias `networkx` to `nx` | 21 | import tkinter as tkr diff --git a/crates/ruff_linter/src/rules/flake8_import_conventions/snapshots/ruff_linter__rules__flake8_import_conventions__tests__same_name.snap b/crates/ruff_linter/src/rules/flake8_import_conventions/snapshots/ruff_linter__rules__flake8_import_conventions__tests__same_name.snap index 51f5f76376..430456689a 100644 --- a/crates/ruff_linter/src/rules/flake8_import_conventions/snapshots/ruff_linter__rules__flake8_import_conventions__tests__same_name.snap +++ b/crates/ruff_linter/src/rules/flake8_import_conventions/snapshots/ruff_linter__rules__flake8_import_conventions__tests__same_name.snap @@ -7,7 +7,6 @@ ICN001 [*] `django.conf.settings` should be imported as `settings` 9 | def unconventional_alias(): 10 | from django.conf import settings as s | ^ - | help: Alias `django.conf.settings` to `settings` | 9 | def unconventional_alias(): 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 31f5cbee39..1e974df0d0 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 @@ -69,7 +69,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// /// [The documentation]: https://docs.python.org/3/library/logging.html#logging.exception #[derive(ViolationMetadata)] -#[violation_metadata(preview_since = "0.9.5")] +#[violation_metadata(stable_since = "0.16.0")] pub(crate) struct LogExceptionOutsideExceptHandler; impl Violation for LogExceptionOutsideExceptHandler { @@ -125,7 +125,7 @@ pub(crate) fn log_exception_outside_except_handler(checker: &Checker, call: &Exp _ => return, }; - let mut diagnostic = checker.report_diagnostic(LogExceptionOutsideExceptHandler, call.range); + let mut diagnostic = checker.report_diagnostic(LogExceptionOutsideExceptHandler, call.range()); if let Some(fix) = fix { diagnostic.set_fix(fix); 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 48e9087848..c888c1da59 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,6 +1,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::ExprCall; use ruff_python_semantic::Modules; +use ruff_text_size::Ranged; use crate::Violation; use crate::checkers::ast::Checker; @@ -65,5 +66,5 @@ pub(crate) fn root_logger_call(checker: &Checker, call: &ExprCall) { let kind = RootLoggerCall { attr: (*attr).to_string(), }; - checker.report_diagnostic(kind, call.range); + checker.report_diagnostic(kind, call.range()); } diff --git a/crates/ruff_linter/src/rules/flake8_logging/snapshots/ruff_linter__rules__flake8_logging__tests__LOG002_LOG002.py.snap b/crates/ruff_linter/src/rules/flake8_logging/snapshots/ruff_linter__rules__flake8_logging__tests__LOG002_LOG002.py.snap index 2c9233a22f..f3a695620c 100644 --- a/crates/ruff_linter/src/rules/flake8_logging/snapshots/ruff_linter__rules__flake8_logging__tests__LOG002_LOG002.py.snap +++ b/crates/ruff_linter/src/rules/flake8_logging/snapshots/ruff_linter__rules__flake8_logging__tests__LOG002_LOG002.py.snap @@ -61,7 +61,6 @@ LOG002 [*] Use `__name__` with `logging.getLogger()` 14 | logging.getLogger(__cached__) 15 | getLogger(name=__cached__) | ^^^^^^^^^^ - | help: Replace with `__name__` | 14 | logging.getLogger(__cached__) diff --git a/crates/ruff_linter/src/rules/flake8_logging/snapshots/ruff_linter__rules__flake8_logging__tests__LOG004_LOG004_0.py.snap b/crates/ruff_linter/src/rules/flake8_logging/snapshots/ruff_linter__rules__flake8_logging__tests__LOG004_LOG004_0.py.snap index 7d92793da5..e6bbb3636a 100644 --- a/crates/ruff_linter/src/rules/flake8_logging/snapshots/ruff_linter__rules__flake8_logging__tests__LOG004_LOG004_0.py.snap +++ b/crates/ruff_linter/src/rules/flake8_logging/snapshots/ruff_linter__rules__flake8_logging__tests__LOG004_LOG004_0.py.snap @@ -44,7 +44,6 @@ LOG004 `.exception()` call outside exception handlers 14 | logger.exception("") 15 | exc("") | ^^^^^^^ - | help: Replace with `.error()` LOG004 [*] `.exception()` call outside exception handlers @@ -90,5 +89,4 @@ LOG004 `.exception()` call outside exception handlers 20 | logger.exception("") 21 | exc("") | ^^^^^^^ - | help: Replace with `.error()` diff --git a/crates/ruff_linter/src/rules/flake8_logging/snapshots/ruff_linter__rules__flake8_logging__tests__LOG004_LOG004_1.py.snap b/crates/ruff_linter/src/rules/flake8_logging/snapshots/ruff_linter__rules__flake8_logging__tests__LOG004_LOG004_1.py.snap index 4f6f9d5d07..96ff758efa 100644 --- a/crates/ruff_linter/src/rules/flake8_logging/snapshots/ruff_linter__rules__flake8_logging__tests__LOG004_LOG004_1.py.snap +++ b/crates/ruff_linter/src/rules/flake8_logging/snapshots/ruff_linter__rules__flake8_logging__tests__LOG004_LOG004_1.py.snap @@ -6,7 +6,6 @@ LOG004 [*] `.exception()` call outside exception handlers | 4 | logger.exception("") | ^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `.error()` | 3 | diff --git a/crates/ruff_linter/src/rules/flake8_logging/snapshots/ruff_linter__rules__flake8_logging__tests__LOG014_LOG014_0.py.snap b/crates/ruff_linter/src/rules/flake8_logging/snapshots/ruff_linter__rules__flake8_logging__tests__LOG014_LOG014_0.py.snap index 8f8ae21638..9aa12dfdc3 100644 --- a/crates/ruff_linter/src/rules/flake8_logging/snapshots/ruff_linter__rules__flake8_logging__tests__LOG014_LOG014_0.py.snap +++ b/crates/ruff_linter/src/rules/flake8_logging/snapshots/ruff_linter__rules__flake8_logging__tests__LOG014_LOG014_0.py.snap @@ -25,7 +25,6 @@ LOG014 [*] `exc_info=` outside exception handlers 12 | logging.info("", exc_info=True) 13 | logger.info("", exc_info=True) | ^^^^^^^^^^^^^ - | help: Remove `exc_info=` | 12 | logging.info("", exc_info=True) @@ -57,7 +56,6 @@ LOG014 [*] `exc_info=` outside exception handlers 16 | logging.info("", exc_info=1) 17 | logger.info("", exc_info=1) | ^^^^^^^^^^ - | help: Remove `exc_info=` | 16 | logging.info("", exc_info=1) @@ -91,7 +89,6 @@ LOG014 [*] `exc_info=` outside exception handlers 21 | logging.info("", exc_info=True) 22 | logger.info("", exc_info=True) | ^^^^^^^^^^^^^ - | help: Remove `exc_info=` | 21 | logging.info("", exc_info=True) diff --git a/crates/ruff_linter/src/rules/flake8_logging/snapshots/ruff_linter__rules__flake8_logging__tests__LOG014_LOG014_1.py.snap b/crates/ruff_linter/src/rules/flake8_logging/snapshots/ruff_linter__rules__flake8_logging__tests__LOG014_LOG014_1.py.snap index 171f59119e..79c6d2f14b 100644 --- a/crates/ruff_linter/src/rules/flake8_logging/snapshots/ruff_linter__rules__flake8_logging__tests__LOG014_LOG014_1.py.snap +++ b/crates/ruff_linter/src/rules/flake8_logging/snapshots/ruff_linter__rules__flake8_logging__tests__LOG014_LOG014_1.py.snap @@ -6,7 +6,6 @@ LOG014 [*] `exc_info=` outside exception handlers | 4 | logger.info("", exc_info=True) | ^^^^^^^^^^^^^ - | help: Remove `exc_info=` | 3 | diff --git a/crates/ruff_linter/src/rules/flake8_logging/snapshots/ruff_linter__rules__flake8_logging__tests__LOG015_LOG015.py.snap b/crates/ruff_linter/src/rules/flake8_logging/snapshots/ruff_linter__rules__flake8_logging__tests__LOG015_LOG015.py.snap index 27a0e6d28e..d5c73b65a3 100644 --- a/crates/ruff_linter/src/rules/flake8_logging/snapshots/ruff_linter__rules__flake8_logging__tests__LOG015_LOG015.py.snap +++ b/crates/ruff_linter/src/rules/flake8_logging/snapshots/ruff_linter__rules__flake8_logging__tests__LOG015_LOG015.py.snap @@ -90,7 +90,6 @@ LOG015 `exception()` call on root logger 10 | logging.log("adipiscing") 11 | logging.exception("elit.") | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use own logger instead LOG015 `debug()` call on root logger @@ -182,5 +181,4 @@ LOG015 `exception()` call on root logger 32 | log("adipiscing") 33 | exception("elit.") | ^^^^^^^^^^^^^^^^^^ - | help: Use own logger instead diff --git a/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G001.py.snap b/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G001.py.snap index f12fb47ed8..745071fd88 100644 --- a/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G001.py.snap +++ b/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G001.py.snap @@ -156,4 +156,3 @@ G001 Logging statement uses `str.format` 26 | log(level=logging.INFO, msg="Hello {}".format("World!")) 27 | log(msg="Hello {}".format("World!"), level=logging.INFO) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G002.py.snap b/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G002.py.snap index dab094f006..5b9515962f 100644 --- a/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G002.py.snap +++ b/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G002.py.snap @@ -37,4 +37,3 @@ G002 Logging statement uses `%` 8 | info("Hello %s" % "World!") 9 | log(logging.INFO, "Hello %s" % "World!") | ^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G003.py.snap b/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G003.py.snap index 5e17edd42c..7ec90fe193 100644 --- a/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G003.py.snap +++ b/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G003.py.snap @@ -37,4 +37,3 @@ G003 Logging statement uses `+` 8 | info("Hello" + " " + "World!") 9 | log(logging.INFO, "Hello" + " " + "World!") | ^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G004.py.snap b/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G004.py.snap index f02d019ad5..d8464d847d 100644 --- a/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G004.py.snap +++ b/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G004.py.snap @@ -75,7 +75,6 @@ G004 Logging statement uses f-string 23 | directory_path = "/home/hamir/ruff/crates/ruff_linter/resources/test/" 24 | logging.info(f"{count} out of {total} files in {directory_path} checked") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Convert to lazy `%` formatting G004 Logging statement uses f-string @@ -162,7 +161,6 @@ G004 Logging statement uses f-string 43 | logging.info(f"Processing {len(data)} items") 44 | logging.info(f"Status: {data.get('status', 'unknown').upper()}") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Convert to lazy `%` formatting G004 Logging statement uses f-string @@ -217,5 +215,4 @@ G004 Logging statement uses f-string 65 | x = 1 66 | logging.error(f"{x} -> %s", x) | ^^^^^^^^^^^^ - | help: Convert to lazy `%` formatting diff --git a/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G004_arg_order.py.snap b/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G004_arg_order.py.snap index cb976e768a..1c233b7c2f 100644 --- a/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G004_arg_order.py.snap +++ b/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G004_arg_order.py.snap @@ -19,5 +19,4 @@ G004 Logging statement uses f-string 9 | logger.error(f"{X} -> %s", Y) 10 | logger.error(f"{Y} -> %s", X) | ^^^^^^^^^^^^ - | help: Convert to lazy `%` formatting diff --git a/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G004_implicit_concat.py.snap b/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G004_implicit_concat.py.snap index 051bba97a6..5defa6cc4e 100644 --- a/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G004_implicit_concat.py.snap +++ b/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G004_implicit_concat.py.snap @@ -30,5 +30,4 @@ G004 Logging statement uses f-string 7 | log.info("a " f"b {variablename}") 8 | log.info("prefix " f"middle {variablename}" f" suffix") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Convert to lazy `%` formatting diff --git a/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__preview__G004_G004.py.snap b/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__preview__G004_G004.py.snap index a606baa92a..6921cae7d9 100644 --- a/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__preview__G004_G004.py.snap +++ b/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__preview__G004_G004.py.snap @@ -111,7 +111,6 @@ G004 [*] Logging statement uses f-string 23 | directory_path = "/home/hamir/ruff/crates/ruff_linter/resources/test/" 24 | logging.info(f"{count} out of {total} files in {directory_path} checked") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Convert to lazy `%` formatting | 23 | directory_path = "/home/hamir/ruff/crates/ruff_linter/resources/test/" @@ -204,7 +203,6 @@ G004 Logging statement uses f-string 43 | logging.info(f"Processing {len(data)} items") 44 | logging.info(f"Status: {data.get('status', 'unknown').upper()}") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Convert to lazy `%` formatting G004 Logging statement uses f-string @@ -259,5 +257,4 @@ G004 Logging statement uses f-string 65 | x = 1 66 | logging.error(f"{x} -> %s", x) | ^^^^^^^^^^^^ - | help: Convert to lazy `%` formatting diff --git a/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__preview__G004_G004_arg_order.py.snap b/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__preview__G004_G004_arg_order.py.snap index cb976e768a..1c233b7c2f 100644 --- a/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__preview__G004_G004_arg_order.py.snap +++ b/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__preview__G004_G004_arg_order.py.snap @@ -19,5 +19,4 @@ G004 Logging statement uses f-string 9 | logger.error(f"{X} -> %s", Y) 10 | logger.error(f"{Y} -> %s", X) | ^^^^^^^^^^^^ - | help: Convert to lazy `%` formatting diff --git a/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__preview__G004_G004_implicit_concat.py.snap b/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__preview__G004_G004_implicit_concat.py.snap index e8591b58e7..5aa21a2be0 100644 --- a/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__preview__G004_G004_implicit_concat.py.snap +++ b/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__preview__G004_G004_implicit_concat.py.snap @@ -42,7 +42,6 @@ G004 [*] Logging statement uses f-string 7 | log.info("a " f"b {variablename}") 8 | log.info("prefix " f"middle {variablename}" f" suffix") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Convert to lazy `%` formatting | 7 | log.info("a " f"b {variablename}") 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 b608fc4270..5001d3a9e6 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 @@ -90,7 +90,7 @@ pub(crate) fn multiple_starts_ends_with(checker: &Checker, expr: &Expr) { range: _, node_index: _, }, - range: _, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, @@ -154,7 +154,7 @@ pub(crate) fn multiple_starts_ends_with(checker: &Checker, expr: &Expr) { range: _, node_index: _, }, - range: _, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, @@ -214,7 +214,7 @@ pub(crate) fn multiple_starts_ends_with(checker: &Checker, expr: &Expr) { range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, is_cast: false, is_checked_cast: false, diff --git a/crates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE790_PIE790.py.snap b/crates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE790_PIE790.py.snap index 9ca4b319c3..d429ead79e 100644 --- a/crates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE790_PIE790.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE790_PIE790.py.snap @@ -8,7 +8,6 @@ PIE790 [*] Unnecessary `pass` statement 3 | 4 | pass | ^^^^ - | help: Remove unnecessary `pass` | 3 | @@ -23,7 +22,6 @@ PIE790 [*] Unnecessary `pass` statement 8 | """foo""" 9 | pass | ^^^^ - | help: Remove unnecessary `pass` | 8 | """foo""" @@ -38,7 +36,6 @@ PIE790 [*] Unnecessary `pass` statement 13 | """This is a function.""" 14 | pass; print("hello") | ^^^^ - | help: Remove unnecessary `pass` | 13 | """This is a function.""" @@ -54,7 +51,6 @@ PIE790 [*] Unnecessary `pass` statement 20 | """bar""" 21 | pass | ^^^^ - | help: Remove unnecessary `pass` | 20 | """bar""" @@ -69,7 +65,6 @@ PIE790 [*] Unnecessary `pass` statement 27 | """bar""" 28 | pass | ^^^^ - | help: Remove unnecessary `pass` | 27 | """bar""" @@ -84,7 +79,6 @@ PIE790 [*] Unnecessary `pass` statement 34 | """bar""" 35 | pass | ^^^^ - | help: Remove unnecessary `pass` | 34 | """bar""" @@ -99,7 +93,6 @@ PIE790 [*] Unnecessary `pass` statement 41 | """bar""" 42 | pass | ^^^^ - | help: Remove unnecessary `pass` | 41 | """bar""" @@ -114,7 +107,6 @@ PIE790 [*] Unnecessary `pass` statement 49 | 50 | pass | ^^^^ - | help: Remove unnecessary `pass` | 49 | @@ -129,7 +121,6 @@ PIE790 [*] Unnecessary `pass` statement 57 | 58 | pass | ^^^^ - | help: Remove unnecessary `pass` | 57 | @@ -161,7 +152,6 @@ PIE790 [*] Unnecessary `pass` statement 73 | """bar""" 74 | pass | ^^^^ - | help: Remove unnecessary `pass` | 73 | """bar""" @@ -210,7 +200,6 @@ PIE790 [*] Unnecessary `pass` statement 86 | """buzz""" 87 | pass | ^^^^ - | help: Remove unnecessary `pass` | 86 | """buzz""" @@ -242,7 +231,6 @@ PIE790 [*] Unnecessary `pass` statement 95 | """buzz""" 96 | pass | ^^^^ - | help: Remove unnecessary `pass` | 95 | """buzz""" @@ -257,7 +245,6 @@ PIE790 [*] Unnecessary `pass` statement 100 | """buzz""" 101 | pass # bar | ^^^^ - | help: Remove unnecessary `pass` | 100 | """buzz""" @@ -273,7 +260,6 @@ PIE790 [*] Unnecessary `pass` statement 129 | print("foo") 130 | pass | ^^^^ - | help: Remove unnecessary `pass` | 129 | print("foo") @@ -288,7 +274,6 @@ PIE790 [*] Unnecessary `pass` statement 135 | print("foo") 136 | pass | ^^^^ - | help: Remove unnecessary `pass` | 135 | print("foo") @@ -384,7 +369,6 @@ PIE790 [*] Unnecessary `pass` statement 149 | pass # comment 150 | pass | ^^^^ - | help: Remove unnecessary `pass` | 149 | pass # comment @@ -399,7 +383,6 @@ PIE790 [*] Unnecessary `...` literal 154 | print("foo") 155 | ... | ^^^ - | help: Remove unnecessary `...` | 154 | print("foo") @@ -414,7 +397,6 @@ PIE790 [*] Unnecessary `...` literal 160 | print("foo") 161 | ... | ^^^ - | help: Remove unnecessary `...` | 160 | print("foo") @@ -559,7 +541,6 @@ PIE790 [*] Unnecessary `...` literal 208 | """Docstring""" 209 | ... | ^^^ - | help: Remove unnecessary `...` | 208 | """Docstring""" diff --git a/crates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE794_PIE794.py.snap b/crates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE794_PIE794.py.snap index 0cdcf54880..72a0b2dbcf 100644 --- a/crates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE794_PIE794.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE794_PIE794.py.snap @@ -44,7 +44,6 @@ PIE794 [*] Class field `bar` is defined multiple times 22 | # ... 23 | bar = StringField() # PIE794 | ^^^^^^^^^^^^^^^^^^^ - | help: Remove duplicate field definition for `bar` | 22 | # ... @@ -60,7 +59,6 @@ PIE794 [*] Class field `bar` is defined multiple times 39 | # ... 40 | bar = StringField() # PIE794 | ^^^^^^^^^^^^^^^^^^^ - | help: Remove duplicate field definition for `bar` | 39 | # ... @@ -76,7 +74,6 @@ PIE794 [*] Class field `name` is defined multiple times 45 | name = name + " Bar" 46 | name = "Bar" # PIE794 | ^^^^^^^^^^^^ - | help: Remove duplicate field definition for `name` | 45 | name = name + " Bar" @@ -92,7 +89,6 @@ PIE794 [*] Class field `name` is defined multiple times 51 | name: str = name + " Bar" 52 | name: str = "Bar" # PIE794 | ^^^^^^^^^^^^^^^^^ - | help: Remove duplicate field definition for `name` | 51 | name: str = name + " Bar" @@ -108,7 +104,6 @@ PIE794 [*] Class field `start_line` is defined multiple times 56 | start_line: int 57 | start_line: int # PIE794 | ^^^^^^^^^^^^^^^ - | help: Remove duplicate field definition for `start_line` | 56 | start_line: int diff --git a/crates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE796_PIE796.py.snap b/crates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE796_PIE796.py.snap index e164e69c5c..4657641895 100644 --- a/crates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE796_PIE796.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE796_PIE796.py.snap @@ -8,7 +8,6 @@ PIE796 Enum contains duplicate value: `"B"` 7 | B = "B" 8 | C = "B" # PIE796 | ^^^^^^^ - | PIE796 Enum contains duplicate value: `2` --> PIE796.py:14:5 @@ -17,7 +16,6 @@ PIE796 Enum contains duplicate value: `2` 13 | B = 2 14 | C = 2 # PIE796 | ^^^^^ - | PIE796 Enum contains duplicate value: `"2"` --> PIE796.py:20:5 @@ -26,7 +24,6 @@ PIE796 Enum contains duplicate value: `"2"` 19 | B = "2" 20 | C = "2" # PIE796 | ^^^^^^^ - | PIE796 Enum contains duplicate value: `2.5` --> PIE796.py:26:5 @@ -35,7 +32,6 @@ PIE796 Enum contains duplicate value: `2.5` 25 | B = 2.5 26 | C = 2.5 # PIE796 | ^^^^^^^ - | PIE796 Enum contains duplicate value: `False` --> PIE796.py:33:5 @@ -44,7 +40,6 @@ PIE796 Enum contains duplicate value: `False` 32 | C = False 33 | D = False # PIE796 | ^^^^^^^^^ - | PIE796 Enum contains duplicate value: `None` --> PIE796.py:40:5 @@ -53,7 +48,6 @@ PIE796 Enum contains duplicate value: `None` 39 | C = None 40 | D = None # PIE796 | ^^^^^^^^ - | PIE796 Enum contains duplicate value: `2` --> PIE796.py:54:5 @@ -62,7 +56,6 @@ PIE796 Enum contains duplicate value: `2` 53 | B = 2 54 | C = 2 # PIE796 | ^^^^^ - | PIE796 Enum contains duplicate value: `...` --> PIE796.py:71:5 @@ -81,7 +74,6 @@ PIE796 Enum contains duplicate value: `...` 71 | B = ... # PIE796 72 | C = ... # PIE796 | ^^^^^^^ - | PIE796 Enum contains duplicate value: `cast(SomeType, ...)` --> PIE796.py:79:5 @@ -100,4 +92,3 @@ PIE796 Enum contains duplicate value: `cast(SomeType, ...)` 79 | B = cast(SomeType, ...) # PIE796 80 | C = cast(SomeType, ...) # PIE796 | ^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE800_PIE800.py.snap b/crates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE800_PIE800.py.snap index ec85a35557..52ba224af0 100644 --- a/crates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE800_PIE800.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE800_PIE800.py.snap @@ -395,7 +395,6 @@ PIE800 [*] Unnecessary spread `**` 96 | | # Comment 97 | | }), 6: 3} | |_^ - | help: Remove unnecessary dict | 94 | }, 6: 3} diff --git a/crates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE807_PIE807.py.snap b/crates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE807_PIE807.py.snap index 5f88013e2e..3054374f38 100644 --- a/crates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE807_PIE807.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE807_PIE807.py.snap @@ -25,7 +25,6 @@ PIE807 [*] Prefer `dict` over useless lambda 3 | foo: List[str] = field(default_factory=lambda: []) # PIE807 4 | bar: Dict[str, int] = field(default_factory=lambda: {}) # PIE807 | ^^^^^^^^^^ - | help: Replace with `lambda` with `dict` | 3 | foo: List[str] = field(default_factory=lambda: []) # PIE807 @@ -57,7 +56,6 @@ PIE807 [*] Prefer `dict` over useless lambda 8 | foo = fields.ListField(default=lambda: []) # PIE807 9 | bar = fields.ListField(default=lambda: {}) # PIE807 | ^^^^^^^^^^ - | help: Replace with `lambda` with `dict` | 8 | foo = fields.ListField(default=lambda: []) # PIE807 @@ -89,7 +87,6 @@ PIE807 [*] Prefer `dict` over useless lambda 13 | foo = fields.ListField(lambda: []) # PIE807 14 | bar = fields.ListField(default=lambda: {}) # PIE807 | ^^^^^^^^^^ - | help: Replace with `lambda` with `dict` | 13 | foo = fields.ListField(lambda: []) # PIE807 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 9f78459542..ed12ff97e9 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 @@ -41,7 +41,6 @@ PIE808 [*] Unnecessary `start` argument in `range` 18 | # regression test for https://github.com/astral-sh/ruff/pull/18805 19 | range((0), 42) | ^ - | help: Remove `start` argument | 18 | # regression test for https://github.com/astral-sh/ruff/pull/18805 @@ -55,7 +54,6 @@ PIE808 [*] Unnecessary `start` argument in `range` | 27 | range(0, 3) | ^ - | help: Remove `start` argument | 26 | diff --git a/crates/ruff_linter/src/rules/flake8_pyi/mod.rs b/crates/ruff_linter/src/rules/flake8_pyi/mod.rs index e596e7d30f..92b2cc156b 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/mod.rs @@ -11,7 +11,6 @@ mod tests { use crate::registry::Rule; use crate::rules::pep8_naming; - use crate::settings::types::PreviewMode; use crate::source_kind::SourceKind; use crate::test::{test_contents, test_path}; use crate::{assert_diagnostics, assert_diagnostics_diff, settings}; @@ -156,14 +155,8 @@ mod tests { assert_diagnostics_diff!( snapshot, Path::new("flake8_pyi").join(path).as_path(), - &settings::LinterSettings { - preview: PreviewMode::Disabled, - ..settings::LinterSettings::for_rule(rule_code) - }, - &settings::LinterSettings { - preview: PreviewMode::Enabled, - ..settings::LinterSettings::for_rule(rule_code) - }, + &settings::LinterSettings::for_rule(rule_code), + &settings::LinterSettings::for_rule(rule_code).with_preview_mode(), ); Ok(()) } @@ -195,10 +188,7 @@ mod tests { let snapshot = format!("py38_{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( Path::new("flake8_pyi").join(path).as_path(), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY38.into(), - ..settings::LinterSettings::for_rule(rule_code) - }, + &settings::LinterSettings::for_rule(rule_code).with_target_version(PythonVersion::PY38), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) 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 630e267142..ec9a8a0e9d 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 @@ -107,7 +107,9 @@ pub(crate) struct BadVersionInfoOrder; impl Violation for BadVersionInfoOrder { #[derive_message_formats] fn message(&self) -> String { - "Put branches for newer Python versions first when branching on `sys.version_info` comparisons".to_string() + "Put branches for newer Python versions first \ + when branching on `sys.version_info` comparisons" + .to_string() } } @@ -142,8 +144,11 @@ pub(crate) fn bad_version_info_comparison(checker: &Checker, test: &Expr, has_el if matches!(op, CmpOp::Lt) { if checker.is_rule_enabled(Rule::BadVersionInfoOrder) - // See https://github.com/astral-sh/ruff/issues/15347 - && (checker.source_type.is_stub() || is_bad_version_info_in_non_stub_enabled(checker.settings())) + && ( + // See https://github.com/astral-sh/ruff/issues/15347. + checker.source_type.is_stub() + || is_bad_version_info_in_non_stub_enabled(checker.settings()) + ) { if has_else_clause { checker.report_diagnostic(BadVersionInfoOrder, test.range()); 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 cd3703c718..18febc7403 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 @@ -26,7 +26,9 @@ impl Violation for FutureAnnotationsInStub { #[derive_message_formats] fn message(&self) -> String { - "`from __future__ import annotations` has no effect in stub files, since type checkers automatically treat stubs as having those semantics".to_string() + "`from __future__ import annotations` has no effect in stub files, \ + since type checkers automatically treat stubs as having those semantics" + .to_string() } fn fix_title(&self) -> Option { 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 d3ee1a96b8..7f2e72ff2d 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,12 +1,13 @@ use bitflags::bitflags; use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::{AnyParameterRef, Expr, ExprBinOp, Operator, Parameters, PythonVersion}; +use ruff_python_ast::{AnyParameterRef, Expr, ExprBinOp, Operator, PythonVersion, StmtFunctionDef}; use ruff_python_semantic::analyze::typing::traverse_union; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::preview::is_resolve_string_annotation_pyi041_enabled; +use crate::rules::flake8_type_checking::helpers::is_singledispatch_implementation; use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; use super::generate_union_fix; @@ -80,8 +81,16 @@ impl Violation for RedundantNumericUnion { } /// PYI041 -pub(crate) fn redundant_numeric_union(checker: &Checker, parameters: &Parameters) { - for annotation in parameters.iter().filter_map(AnyParameterRef::annotation) { +pub(crate) fn redundant_numeric_union(checker: &Checker, function_def: &StmtFunctionDef) { + let skip_dispatch_annotation = + is_singledispatch_implementation(function_def, checker.semantic()); + + for annotation in function_def + .parameters + .iter() + .filter_map(AnyParameterRef::annotation) + .skip(usize::from(skip_dispatch_annotation)) + { check_annotation(checker, annotation); } } @@ -272,7 +281,7 @@ enum Redundancy { } impl Redundancy { - pub(super) fn from_numeric_flags(numeric_flags: NumericFlags) -> Option { + fn from_numeric_flags(numeric_flags: NumericFlags) -> Option { if numeric_flags == NumericFlags::INT | NumericFlags::FLOAT | NumericFlags::COMPLEX { Some(Self::IntFloatComplex) } else if numeric_flags == NumericFlags::FLOAT | NumericFlags::COMPLEX { @@ -300,7 +309,7 @@ bitflags! { } impl NumericFlags { - pub(super) fn seen_builtin_type(&mut self, name: &str) { + fn seen_builtin_type(&mut self, name: &str) { let flag: NumericFlags = match name { "int" => NumericFlags::INT, "float" => NumericFlags::FLOAT, 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 47b111f97e..185b6fd4de 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 @@ -48,7 +48,9 @@ impl Violation for UnaliasedCollectionsAbcSetImport { #[derive_message_formats] fn message(&self) -> String { - "Use `from collections.abc import Set as AbstractSet` to avoid confusion with the `set` builtin".to_string() + "Use `from collections.abc import Set as AbstractSet` \ + to avoid confusion with the `set` builtin" + .to_string() } fn fix_title(&self) -> Option { diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI002_PYI002.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI002_PYI002.pyi.snap index 744b2850cf..0e2a4e11af 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI002_PYI002.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI002_PYI002.pyi.snap @@ -39,4 +39,3 @@ PYI002 `if` test must be a simple comparison against `sys.platform` or `sys.vers 5 | if hasattr(sys, 'maxint'): ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info 6 | if sys.maxsize == 42: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info | ^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI005_PYI005.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI005_PYI005.pyi.snap index 1dfd4b7b55..ece2f68432 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI005_PYI005.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI005_PYI005.pyi.snap @@ -17,4 +17,3 @@ PYI005 Version comparison must be against a length-2 tuple 4 | if sys.version_info[:1] == (2, 7): ... # Y005 5 | if sys.version_info[:2] == (2,): ... # Y005 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI006_PYI006.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI006_PYI006.py.snap index a7ed2ffa50..cdef1fb4e3 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI006_PYI006.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI006_PYI006.py.snap @@ -81,4 +81,3 @@ PYI006 Use `<` or `>=` for `sys.version_info` comparisons 19 | if python_version > (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons 20 | elif python_version == (3, 11): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons | ^^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI006_PYI006.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI006_PYI006.pyi.snap index 8db85b25ce..2de610d4b8 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI006_PYI006.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI006_PYI006.pyi.snap @@ -81,4 +81,3 @@ PYI006 Use `<` or `>=` for `sys.version_info` comparisons 19 | if python_version > (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons 20 | elif python_version == (3, 11): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons | ^^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI007_PYI007.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI007_PYI007.pyi.snap index 6e444fcbc6..a051575774 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI007_PYI007.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI007_PYI007.pyi.snap @@ -30,4 +30,3 @@ PYI007 Unrecognized `sys.platform` check 10 | 11 | if sys.platform == 10.12: ... # Error: PYI007 Unrecognized sys.platform check | ^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI009_PYI009.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI009_PYI009.pyi.snap index 1959a217ca..047e12d981 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI009_PYI009.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI009_PYI009.pyi.snap @@ -25,7 +25,6 @@ PYI009 [*] Empty body should contain `...`, not `pass` 7 | class Foo: 8 | pass # ERROR PYI009, since we're in a stub file | ^^^^ - | help: Replace `pass` with `...` | 7 | class Foo: diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI013_PYI013.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI013_PYI013.py.snap index a9abb985d5..abaf31f44d 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI013_PYI013.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI013_PYI013.py.snap @@ -8,7 +8,6 @@ PYI013 [*] Non-empty class body must not contain `...` 2 | value: int 3 | ... | ^^^ - | help: Remove unnecessary `...` | 2 | value: int @@ -53,7 +52,6 @@ PYI013 [*] Non-empty class body must not contain `...` 12 | ... 13 | ... | ^^^ - | help: Remove unnecessary `...` | 12 | ... @@ -68,7 +66,6 @@ PYI013 [*] Non-empty class body must not contain `...` 20 | 21 | ... | ^^^ - | help: Remove unnecessary `...` | 20 | @@ -83,7 +80,6 @@ PYI013 [*] Non-empty class body must not contain `...` 25 | value: int 26 | ... | ^^^ - | help: Remove unnecessary `...` | 25 | value: int @@ -130,7 +126,6 @@ PYI013 [*] Non-empty class body must not contain `...` 43 | value: int 44 | ... # preserve me | ^^^ - | help: Remove unnecessary `...` | 43 | value: int 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 6e618a7680..1e7493cc15 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 @@ -774,7 +774,6 @@ PYI016 [*] Duplicate union member `int` 114 | 115 | field35: "int | str | int" # Error | ^^^ - | help: Remove duplicate union member `int` | 114 | 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 8ea26dd248..e528fa5fdc 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 @@ -1003,7 +1003,6 @@ PYI016 [*] Duplicate union member `complex` 134 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex] 135 | field49: typing.Optional[complex | complex] | complex | ^^^^^^^ - | help: Remove duplicate union member `complex` | 134 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex] @@ -1018,7 +1017,6 @@ PYI016 [*] Duplicate union member `complex` 134 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex] 135 | field49: typing.Optional[complex | complex] | complex | ^^^^^^^ - | help: Remove duplicate union member `complex` | 134 | field48: typing.Union[typing.Optional[typing.Union[complex, 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 c66f5a4096..18480905e9 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 @@ -7,7 +7,6 @@ PYI019 [*] Use `Self` instead of custom TypeVar `_S` 6 | class BadClass: 7 | def __new__(cls: type[_S], *args: str, **kwargs: int) -> _S: ... # PYI019 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace TypeVar `_S` with `Self` | 6 | class BadClass: @@ -21,7 +20,6 @@ PYI019 [*] Use `Self` instead of custom TypeVar `_S` | 10 | def bad_instance_method(self: _S, arg: bytes) -> _S: ... # PYI019 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace TypeVar `_S` with `Self` | 9 | @@ -36,7 +34,6 @@ PYI019 [*] Use `Self` instead of custom TypeVar `_S` 13 | @classmethod 14 | def bad_class_method(cls: type[_S], arg: int) -> _S: ... # PYI019 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace TypeVar `_S` with `Self` | 13 | @classmethod @@ -51,7 +48,6 @@ PYI019 [*] Use `Self` instead of custom TypeVar `_S` 17 | @classmethod 18 | def bad_posonly_class_method(cls: type[_S], /) -> _S: ... # PYI019 | ^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace TypeVar `_S` with `Self` | 17 | @classmethod @@ -67,7 +63,6 @@ PYI019 [*] Use `Self` instead of custom TypeVar `S` 38 | class PEP695BadDunderNew[T]: 39 | def __new__[S](cls: type[S], *args: Any, ** kwargs: Any) -> S: ... # PYI019 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace TypeVar `S` with `Self` | 38 | class PEP695BadDunderNew[T]: @@ -81,7 +76,6 @@ PYI019 [*] Use `Self` instead of custom TypeVar `S` | 42 | def generic_instance_method[S](self: S) -> S: ... # PYI019 | ^^^^^^^^^^^^^^^^^ - | help: Replace TypeVar `S` with `Self` | 41 | @@ -97,7 +91,6 @@ PYI019 [*] Use `Self` instead of custom TypeVar `S` 53 | @foo_classmethod 54 | def foo[S](cls: type[S]) -> S: ... # PYI019 | ^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace TypeVar `S` with `Self` | 53 | @foo_classmethod @@ -344,7 +337,6 @@ PYI019 [*] Use `Self` instead of custom TypeVar `_S695` 88 | 89 | def mixing_old_and_new_style_type_vars[T](self: _S695, a: T, b: T) -> _S695: ... | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace TypeVar `_S695` with `Self` | 88 | @@ -377,7 +369,6 @@ PYI019 [*] Use `Self` instead of custom TypeVar `S` 113 | @classmethod 114 | def m[S](cls: type[S]) -> type[S]: ... # PYI019 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace TypeVar `S` with `Self` | 113 | @classmethod @@ -410,7 +401,6 @@ PYI019 [*] Use `Self` instead of custom TypeVar `S` 119 | @classmethod 120 | def n[S](cls: type[S], other: S) -> int: ... | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace TypeVar `S` with `Self` | 119 | @classmethod @@ -613,7 +603,6 @@ PYI019 [*] Use `Self` instead of custom TypeVar `_S` 191 | @classmethod 192 | def bad_class_method_with_string_annotations(cls: "type[_S]") -> "_S": ... # PYI019 | ^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace TypeVar `_S` with `Self` | 191 | @classmethod @@ -628,7 +617,6 @@ PYI019 [*] Use `Self` instead of custom TypeVar `_S` 195 | @classmethod 196 | def bad_class_method_with_mixed_annotations_1(cls: "type[_S]") -> _S: ... # PYI019 | ^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace TypeVar `_S` with `Self` | 195 | @classmethod @@ -643,7 +631,6 @@ PYI019 [*] Use `Self` instead of custom TypeVar `_S` 199 | @classmethod 200 | def bad_class_method_with_mixed_annotations_1(cls: type[_S]) -> "_S": ... # PYI019 | ^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace TypeVar `_S` with `Self` | 199 | @classmethod @@ -659,7 +646,6 @@ PYI019 [*] Use `Self` instead of custom TypeVar `S` 204 | @classmethod 205 | def m[S](cls: "type[S]") -> "type[S]": ... # PYI019 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace TypeVar `S` with `Self` | 204 | @classmethod 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 8e70f15c92..05c83b71d9 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 @@ -7,7 +7,6 @@ PYI019 [*] Use `Self` instead of custom TypeVar `_S` 6 | class BadClass: 7 | def __new__(cls: type[_S], *args: str, **kwargs: int) -> _S: ... # PYI019 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace TypeVar `_S` with `Self` | 6 | class BadClass: @@ -21,7 +20,6 @@ PYI019 [*] Use `Self` instead of custom TypeVar `_S` | 10 | def bad_instance_method(self: _S, arg: bytes) -> _S: ... # PYI019 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace TypeVar `_S` with `Self` | 9 | @@ -36,7 +34,6 @@ PYI019 [*] Use `Self` instead of custom TypeVar `_S` 13 | @classmethod 14 | def bad_class_method(cls: type[_S], arg: int) -> _S: ... # PYI019 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace TypeVar `_S` with `Self` | 13 | @classmethod @@ -51,7 +48,6 @@ PYI019 [*] Use `Self` instead of custom TypeVar `_S` 17 | @classmethod 18 | def bad_posonly_class_method(cls: type[_S], /) -> _S: ... # PYI019 | ^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace TypeVar `_S` with `Self` | 17 | @classmethod @@ -67,7 +63,6 @@ PYI019 [*] Use `Self` instead of custom TypeVar `S` 38 | class PEP695BadDunderNew[T]: 39 | def __new__[S](cls: type[S], *args: Any, ** kwargs: Any) -> S: ... # PYI019 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace TypeVar `S` with `Self` | 38 | class PEP695BadDunderNew[T]: @@ -81,7 +76,6 @@ PYI019 [*] Use `Self` instead of custom TypeVar `S` | 42 | def generic_instance_method[S](self: S) -> S: ... # PYI019 | ^^^^^^^^^^^^^^^^^ - | help: Replace TypeVar `S` with `Self` | 41 | @@ -97,7 +91,6 @@ PYI019 [*] Use `Self` instead of custom TypeVar `S` 53 | @foo_classmethod 54 | def foo[S](cls: type[S]) -> S: ... # PYI019 | ^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace TypeVar `S` with `Self` | 53 | @foo_classmethod @@ -344,7 +337,6 @@ PYI019 [*] Use `Self` instead of custom TypeVar `_S695` 88 | 89 | def mixing_old_and_new_style_type_vars[T](self: _S695, a: T, b: T) -> _S695: ... | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace TypeVar `_S695` with `Self` | 88 | @@ -377,7 +369,6 @@ PYI019 [*] Use `Self` instead of custom TypeVar `S` 113 | @classmethod 114 | def m[S](cls: type[S]) -> type[S]: ... # PYI019 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace TypeVar `S` with `Self` | 113 | @classmethod @@ -392,7 +383,6 @@ PYI019 [*] Use `Self` instead of custom TypeVar `S` 117 | class PEP695TypeParameterAtTheVeryEndOfTheList: 118 | def f[T, S](self: S) -> S: ... | ^^^^^^^^^^^^^^^^^^^^ - | help: Replace TypeVar `S` with `Self` | 117 | class PEP695TypeParameterAtTheVeryEndOfTheList: @@ -466,7 +456,6 @@ PYI019 [*] Use `Self` instead of custom TypeVar `S` 140 | | ] 141 | | ) -> S: ... | |__________^ - | help: Replace TypeVar `S` with `Self` | 133 | @@ -509,7 +498,6 @@ PYI019 [*] Use `Self` instead of custom TypeVar `S` 146 | @classmethod 147 | def n[S](cls: type[S], other: S) -> int: ... | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace TypeVar `S` with `Self` | 146 | @classmethod @@ -576,7 +564,6 @@ PYI019 [*] Use `Self` instead of custom TypeVar `S` 166 | def m[S: int, T: int](self: S, other: T) -> S: ... 167 | def n[T: (int, str), S: (int, str)](self: S, other: T) -> S: ... | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace TypeVar `S` with `Self` | 166 | def m[S: int, T: int](self: S, other: T) -> S: ... @@ -608,7 +595,6 @@ PYI019 [*] Use `Self` instead of custom TypeVar `_S` 183 | @classmethod 184 | def bad_class_method_with_string_annotations(cls: "type[_S]") -> "_S": ... # PYI019 | ^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace TypeVar `_S` with `Self` | 183 | @classmethod @@ -623,7 +609,6 @@ PYI019 [*] Use `Self` instead of custom TypeVar `_S` 187 | @classmethod 188 | def bad_class_method_with_mixed_annotations_1(cls: "type[_S]") -> _S: ... # PYI019 | ^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace TypeVar `_S` with `Self` | 187 | @classmethod @@ -638,7 +623,6 @@ PYI019 [*] Use `Self` instead of custom TypeVar `_S` 191 | @classmethod 192 | def bad_class_method_with_mixed_annotations_1(cls: type[_S]) -> "_S": ... # PYI019 | ^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace TypeVar `_S` with `Self` | 191 | @classmethod @@ -654,7 +638,6 @@ PYI019 [*] Use `Self` instead of custom TypeVar `S` 196 | @classmethod 197 | def m[S](cls: "type[S]") -> "type[S]": ... # PYI019 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace TypeVar `S` with `Self` | 196 | @classmethod diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI019_PYI019_1.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI019_PYI019_1.pyi.snap index c29053ea7f..fbf3fc8400 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI019_PYI019_1.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI019_PYI019_1.pyi.snap @@ -7,7 +7,6 @@ PYI019 [*] Use `Self` instead of custom TypeVar `S` 3 | class F: 4 | def m[S](self: S) -> S: ... | ^^^^^^^^^^^^^^^^^ - | help: Replace TypeVar `S` with `Self` | 3 | class F: diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI025_PYI025_1.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI025_PYI025_1.py.snap index 3e8f027673..c9b60ae0d1 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI025_PYI025_1.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI025_PYI025_1.py.snap @@ -7,7 +7,6 @@ PYI025 [*] Use `from collections.abc import Set as AbstractSet` to avoid confusi 9 | def f(): 10 | from collections.abc import Set # PYI025 | ^^^ - | help: Alias `Set` to `AbstractSet` | 9 | def f(): diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI025_PYI025_3.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI025_PYI025_3.py.snap index 707581ab15..53947d4906 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI025_PYI025_3.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI025_PYI025_3.py.snap @@ -8,7 +8,6 @@ PYI025 [*] Use `from collections.abc import Set as AbstractSet` to avoid confusi 5 | 6 | from collections.abc import Set as Set # PYI025 triggered but fix is not marked as safe | ^^^ - | help: Alias `Set` to `AbstractSet` | 5 | diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI025_PYI025_3.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI025_PYI025_3.pyi.snap index 57895043dd..8411cbc8b5 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI025_PYI025_3.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI025_PYI025_3.pyi.snap @@ -8,7 +8,6 @@ PYI025 [*] Use `from collections.abc import Set as AbstractSet` to avoid confusi 5 | 6 | from collections.abc import Set as Set # PYI025 triggered but fix is not marked as safe | ^^^ - | help: Alias `Set` to `AbstractSet` | 5 | diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI032_PYI032.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI032_PYI032.py.snap index 1d4d845c22..a1234728cd 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI032_PYI032.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI032_PYI032.py.snap @@ -24,7 +24,6 @@ PYI032 [*] Prefer `object` to `Any` for the second parameter to `__ne__` 6 | def __eq__(self, other: Any) -> bool: ... # PYI032 7 | def __ne__(self, other: typing.Any) -> typing.Any: ... # PYI032 | ^^^^^^^^^^ - | help: Replace with `object` | 6 | def __eq__(self, other: Any) -> bool: ... # PYI032 @@ -56,7 +55,6 @@ PYI032 [*] Prefer `object` to `Any` for the second parameter to `__ne__` 27 | def __eq__(self, other: "Any") -> bool: ... # PYI032 28 | def __ne__(self, other: "Any") -> bool: ... # PYI032 | ^^^^^ - | help: Replace with `object` | 27 | def __eq__(self, other: "Any") -> bool: ... # PYI032 diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI032_PYI032.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI032_PYI032.pyi.snap index 929e2b34cf..bd1e6cc98c 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI032_PYI032.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI032_PYI032.pyi.snap @@ -24,7 +24,6 @@ PYI032 [*] Prefer `object` to `Any` for the second parameter to `__ne__` 6 | def __eq__(self, other: Any) -> bool: ... # PYI032 7 | def __ne__(self, other: typing.Any) -> typing.Any: ... # PYI032 | ^^^^^^^^^^ - | help: Replace with `object` | 6 | def __eq__(self, other: Any) -> bool: ... # PYI032 @@ -56,7 +55,6 @@ PYI032 [*] Prefer `object` to `Any` for the second parameter to `__ne__` 26 | def __eq__(self, other: "Any") -> bool: ... # PYI032 27 | def __ne__(self, other: "Any") -> bool: ... # PYI032 | ^^^^^ - | help: Replace with `object` | 26 | def __eq__(self, other: "Any") -> bool: ... # PYI032 diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI034_PYI034.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI034_PYI034.py.snap index b1a75c1202..e812b69d2d 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI034_PYI034.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI034_PYI034.py.snap @@ -273,7 +273,6 @@ PYI034 [*] `__enter__` methods in classes like `Generic1` usually return `self` 334 | def __new__(cls: type[Generic1]) -> Generic1: ... 335 | def __enter__(self: Generic1) -> Generic1: ... | ^^^^^^^^^ - | help: Use `Self` as return type | 334 | def __new__(cls: type[Generic1]) -> Generic1: ... @@ -415,7 +414,6 @@ PYI034 [*] `__enter__` methods in classes like `Generic5` usually return `self` 359 | def __new__(cls: type[Generic5]) -> Generic5: ... 360 | def __enter__(self: Generic5) -> Generic5: ... | ^^^^^^^^^ - | help: Use `Self` as return type | 359 | def __new__(cls: type[Generic5]) -> Generic5: ... @@ -487,7 +485,6 @@ PYI034 [*] `__iadd__` methods in classes like `UsesStringizedForwardReferences` 393 | async def __aenter__(self) -> "UsesStringizedForwardReferences": ... # PYI034 394 | def __iadd__(self, other) -> "UsesStringizedForwardReferences": ... # PYI034 | ^^^^^^^^ - | help: Use `Self` as return type | 393 | async def __aenter__(self) -> "UsesStringizedForwardReferences": ... # PYI034 diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI034_PYI034.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI034_PYI034.pyi.snap index cf6dae8bff..bec2f0f25d 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI034_PYI034.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI034_PYI034.pyi.snap @@ -258,7 +258,6 @@ PYI034 [*] `__enter__` methods in classes like `Generic1` usually return `self` 228 | def __new__(cls: type[Generic1]) -> Generic1: ... 229 | def __enter__(self: Generic1) -> Generic1: ... | ^^^^^^^^^ - | help: Use `Self` as return type | 228 | def __new__(cls: type[Generic1]) -> Generic1: ... @@ -400,7 +399,6 @@ PYI034 [*] `__enter__` methods in classes like `Generic5` usually return `self` 253 | def __new__(cls: type[Generic5]) -> Generic5: ... 254 | def __enter__(self: Generic5) -> Generic5: ... | ^^^^^^^^^ - | help: Use `Self` as return type | 253 | def __new__(cls: type[Generic5]) -> Generic5: ... diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI036_PYI036.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI036_PYI036.pyi.snap index 1fac769954..fe50de855a 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI036_PYI036.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI036_PYI036.pyi.snap @@ -169,7 +169,6 @@ PYI036 All keyword-only arguments in `__aexit__` must have a default value 74 | def __exit__(self, typ, exc, tb, weird_extra_arg, extra_arg2 = None) -> None: ... # PYI036: Extra arg must have default 75 | async def __aexit__(self, typ, exc, tb, *, weird_extra_arg) -> None: ... # PYI036: kwargs must have default | ^^^^^^^^^^^^^^^ - | PYI036 The first argument in `__exit__` should be annotated with `object` or `type[BaseException] | None` --> PYI036.pyi:89:29 @@ -220,4 +219,3 @@ PYI036 Annotations for a three-argument `__exit__` overload (excluding `self`) s 164 | @overload 165 | def __exit__(self, exc_typ: object, exc: Exception, tb: builtins.TracebackType) -> None: ... # PYI036 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | 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 index 904d608326..fe68fdfd7f 100644 --- 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 @@ -6,7 +6,6 @@ PYI041 [*] Use `float` instead of `int | float` | 21 | def f0(arg1: float | int) -> None: ... # PYI041 | ^^^^^^^^^^^ - | help: Remove redundant type | 20 | @@ -20,7 +19,6 @@ PYI041 [*] Use `complex` instead of `float | complex` | 24 | def f1(arg1: float, *, arg2: float | list[str] | type[bool] | complex) -> None: ... # PYI041 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove redundant type | 23 | @@ -34,7 +32,6 @@ PYI041 [*] Use `float` instead of `int | float` | 27 | def f2(arg1: int, /, arg2: int | int | float) -> None: ... # PYI041 | ^^^^^^^^^^^^^^^^^ - | help: Remove redundant type | 26 | @@ -48,7 +45,6 @@ PYI041 [*] Use `float` instead of `int | float` | 30 | def f3(arg1: int, *args: Union[int | int | float]) -> None: ... # PYI041 | ^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove redundant type | 29 | @@ -123,7 +119,6 @@ PYI041 [*] Use `float` instead of `int | float` 48 | 49 | def f5(arg1: int, *args: Union[int, int, float]) -> None: ... # PYI041 | ^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove redundant type | 48 | @@ -137,7 +132,6 @@ PYI041 [*] Use `float` instead of `int | float` | 52 | def f6(arg1: int, *args: Union[Union[int, int, float]]) -> None: ... # PYI041 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove redundant type | 51 | @@ -151,7 +145,6 @@ PYI041 [*] Use `float` instead of `int | float` | 55 | def f7(arg1: int, *args: Union[Union[Union[int, int, float]]]) -> None: ... # PYI041 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove redundant type | 54 | @@ -165,7 +158,6 @@ PYI041 [*] Use `float` instead of `int | float` | 58 | def f8(arg1: int, *args: Union[Union[Union[int | int | float]]]) -> None: ... # PYI041 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove redundant type | 57 | @@ -253,7 +245,6 @@ PYI041 [*] Use `complex` instead of `int | float | complex` 71 | 72 | def bad5(self, arg: int | (float | complex)) -> None: ... # PYI041 | ^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove redundant type | 71 | @@ -287,7 +278,6 @@ PYI041 [*] Use `float` instead of `int | float` 79 | 80 | def f3(self, arg: None | float | None | int | None = None) -> None: ... # PYI041 - with fix | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove redundant type | 79 | diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI045_PYI045.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI045_PYI045.pyi.snap index 7b97f5d3f8..70ad469cc4 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI045_PYI045.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI045_PYI045.pyi.snap @@ -63,4 +63,3 @@ PYI045 `__aiter__` methods should return an `AsyncIterator`, not an `AsyncIterab 48 | class TypingAsyncIterableReturn: 49 | def __aiter__(self) -> typing.AsyncIterable: ... # Error: PYI045 | ^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI052_PYI052.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI052_PYI052.pyi.snap index 2895a6c4f3..59831af205 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI052_PYI052.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI052_PYI052.pyi.snap @@ -150,4 +150,3 @@ PYI052 Need type annotation for `WIZ` 113 | class Bop: 114 | WIZ = 4 | ^ - | diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI054_PYI054.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI054_PYI054.pyi.snap index ac2bbd14ab..335d4c319d 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI054_PYI054.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI054_PYI054.pyi.snap @@ -131,7 +131,6 @@ PYI054 [*] Numeric literals with a string representation longer than ten charact 19 | field16: complex = -1e1234567j 20 | field17: complex = 1e123456789j # Error: PYI054 | ^^^^^^^^^^^^ - | help: Replace with `...` | 19 | field16: complex = -1e1234567j diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI055_PYI055.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI055_PYI055.py.snap index 86fcce77f6..38150e6db7 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI055_PYI055.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI055_PYI055.py.snap @@ -132,7 +132,6 @@ PYI055 [*] Multiple `type` members in a union. Combine them into one, e.g., `typ 10 | y: Union[Union[Union[type[float | int], type[complex]]]] 11 | z: Union[type[complex], Union[Union[type[Union[float, int]]]]] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Combine multiple `type` members | 10 | y: Union[Union[Union[type[float | int], type[complex]]]] @@ -162,7 +161,6 @@ PYI055 [*] Multiple `type` members in a union. Combine them into one, e.g., `typ 28 | # OK 29 | item: type[requests_mock.Mocker] | type[httpretty] = requests_mock.Mocker | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Combine multiple `type` members | 28 | # OK @@ -217,7 +215,6 @@ PYI055 [*] Multiple `type` members in a union. Combine them into one, e.g., `typ 37 | | type[requests_mock.Mocker], # another comment 38 | | type[httpretty], type[str]] = requests_mock.Mocker | |___________________________________^ - | help: Combine multiple `type` members | 35 | y: Union[type[requests_mock.Mocker], type[httpretty], type[str]] = requests_mock.Mocker @@ -235,7 +232,6 @@ PYI055 [*] Multiple `type` members in a union. Combine them into one, e.g., `typ 44 | # PYI055 45 | x: Union[type[requests_mock.Mocker], type[httpretty], type[str]] = requests_mock.Mocker | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Combine multiple `type` members | 44 | # PYI055 diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI055_PYI055.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI055_PYI055.pyi.snap index a7bd149bec..b5defd6f57 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI055_PYI055.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI055_PYI055.pyi.snap @@ -224,7 +224,6 @@ PYI055 [*] Multiple `type` members in a union. Combine them into one, e.g., `typ 30 | | type[requests_mock.Mocker], # another comment 31 | | type[httpretty], type[str]] = requests_mock.Mocker | |___________________________________^ - | help: Combine multiple `type` members | 28 | item2: Union[type[requests_mock.Mocker], type[httpretty], type[str]] = requests_mock.Mocker 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 a0300be88f..19a7941f31 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 @@ -102,7 +102,6 @@ PYI059 `Generic[]` should always be the last base class 44 | # in case of multiple Generic[] inheritance, don't fix it. 45 | class C(Generic[T], Generic[K, V]): ... # PYI059 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Move `Generic[]` to the end PYI059 [*] `Generic[]` should always be the last base class 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 ed0457cc87..bc4a086f4c 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 @@ -52,7 +52,6 @@ PYI059 [*] `Generic[]` should always be the last base class 21 | # the Generic's position issue persists. 22 | class Foo(Generic, LinkedList): ... # PYI059 | ^^^^^^^^^^^^^^^^^^^^^ - | help: Move `Generic[]` to the end | 21 | # the Generic's position issue persists. @@ -101,5 +100,4 @@ PYI059 `Generic[]` should always be the last base class 39 | # in case of multiple Generic[] inheritance, don't fix it. 40 | class C(Generic[T], Generic[K, V]): ... # PYI059 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Move `Generic[]` to the end 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 95ccdb7063..346f1f57fb 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 @@ -282,7 +282,6 @@ PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` 67 | # only emit Y062: 68 | Literal[None, True, None, True] # Y062 Duplicate "Literal[]" member "True" | ^^^^ - | help: Replace with `Literal[...] | None` | 67 | # only emit Y062: @@ -299,7 +298,6 @@ PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` 67 | # only emit Y062: 68 | Literal[None, True, None, True] # Y062 Duplicate "Literal[]" member "True" | ^^^^ - | help: Replace with `Literal[...] | None` | 67 | # only emit Y062: @@ -527,7 +525,6 @@ PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` 91 | # Rewriting this changes `typing.get_args(options)` at runtime, so the fix is unsafe. 92 | options = Literal["foo", "bar", None] | ^^^^ - | help: Replace with `Literal[...] | None` | 91 | # Rewriting this changes `typing.get_args(options)` at runtime, so the fix is unsafe. 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 2dc7d76786..86a44243cc 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 @@ -6,7 +6,6 @@ PYI061 [*] Use `None` rather than `Literal[None]` | 4 | def func1(arg1: Literal[None]): ... | ^^^^ - | help: Replace with `None` | 3 | @@ -20,7 +19,6 @@ PYI061 [*] Use `None` rather than `Literal[None]` | 7 | def func2(arg1: Literal[None] | int): ... | ^^^^ - | help: Replace with `None` | 6 | @@ -34,7 +32,6 @@ PYI061 [*] Use `None` rather than `Literal[None]` | 10 | def func3() -> Literal[None]: ... | ^^^^ - | help: Replace with `None` | 9 | @@ -48,7 +45,6 @@ PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` | 13 | def func4(arg1: Literal[int, None, float]): ... | ^^^^ - | help: Replace with `Literal[...] | None` | 12 | @@ -62,7 +58,6 @@ PYI061 [*] Use `None` rather than `Literal[None]` | 16 | def func5(arg1: Literal[None, None]): ... | ^^^^ - | help: Replace with `None` | 15 | @@ -76,7 +71,6 @@ PYI061 [*] Use `None` rather than `Literal[None]` | 16 | def func5(arg1: Literal[None, None]): ... | ^^^^ - | help: Replace with `None` | 15 | @@ -132,7 +126,6 @@ PYI061 Use `None` rather than `Literal[None]` | 31 | def func8(arg1: Literal[None] | None):... | ^^^^ - | help: Replace with `None` PYI061 [*] Use `None` rather than `Literal[None]` @@ -140,7 +133,6 @@ PYI061 [*] Use `None` rather than `Literal[None]` | 34 | def func9(arg1: Union[Literal[None], None]): ... | ^^^^ - | help: Replace with `None` | 33 | @@ -172,7 +164,6 @@ PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` 42 | Literal[None] # PYI061 None inside "Literal[]" expression. Replace with "None" 43 | Literal[True, None] # PYI061 None inside "Literal[]" expression. Replace with "Literal[True] | None" | ^^^^ - | help: Replace with `Literal[...] | None` | 42 | Literal[None] # PYI061 None inside "Literal[]" expression. Replace with "None" @@ -274,5 +265,4 @@ PYI061 Use `None` rather than `Literal[None]` 54 | d: None | (Literal[None] | None) 55 | e: None | ((None | Literal[None]) | None) | None | ^^^^ - | help: Replace with `None` diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI062_PYI062.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI062_PYI062.py.snap index 628a693de4..5a7f193162 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI062_PYI062.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI062_PYI062.py.snap @@ -298,7 +298,6 @@ PYI062 [*] Duplicate literal member `1` 32 | Literal[Literal[Literal[1], Literal[1]]] # once 33 | Literal[Literal[1], Literal[Literal[Literal[1]]]] # once | ^ - | help: Remove duplicates | 32 | Literal[Literal[Literal[1], Literal[1]]] # once diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI062_PYI062.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI062_PYI062.pyi.snap index a6b44dde8f..28c3c6aa97 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI062_PYI062.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI062_PYI062.pyi.snap @@ -298,7 +298,6 @@ PYI062 [*] Duplicate literal member `1` 32 | Literal[Literal[Literal[1], Literal[1]]] # once 33 | Literal[Literal[1], Literal[Literal[Literal[1]]]] # once | ^ - | help: Remove duplicates | 32 | Literal[Literal[Literal[1], Literal[1]]] # once diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__preview_PYI041_PYI041_4.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__preview_PYI041_PYI041_4.py.snap index 4de04c2e13..917bc5aa77 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__preview_PYI041_PYI041_4.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__preview_PYI041_PYI041_4.py.snap @@ -69,5 +69,4 @@ PYI041 Use `float` instead of `int | float` | ___________^ 8 | | t, float, Foo]") -> "None": ... | |_______________^ - | help: Remove redundant type 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 ea86e80b20..add20bd275 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 @@ -302,7 +302,6 @@ PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]` 67 | # only emit Y062: 68 | Literal[None, True, None, True] # Y062 Duplicate "Literal[]" member "True" | ^^^^ - | help: Replace with `Optional[Literal[...]]` | - from typing import Literal, Union @@ -323,7 +322,6 @@ PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]` 67 | # only emit Y062: 68 | Literal[None, True, None, True] # Y062 Duplicate "Literal[]" member "True" | ^^^^ - | help: Replace with `Optional[Literal[...]]` | - from typing import Literal, Union @@ -579,7 +577,6 @@ PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]` 91 | # Rewriting this changes `typing.get_args(options)` at runtime, so the fix is unsafe. 92 | options = Literal["foo", "bar", None] | ^^^^ - | help: Replace with `Optional[Literal[...]]` | - from typing import Literal, Union 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 2dc7d76786..86a44243cc 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 @@ -6,7 +6,6 @@ PYI061 [*] Use `None` rather than `Literal[None]` | 4 | def func1(arg1: Literal[None]): ... | ^^^^ - | help: Replace with `None` | 3 | @@ -20,7 +19,6 @@ PYI061 [*] Use `None` rather than `Literal[None]` | 7 | def func2(arg1: Literal[None] | int): ... | ^^^^ - | help: Replace with `None` | 6 | @@ -34,7 +32,6 @@ PYI061 [*] Use `None` rather than `Literal[None]` | 10 | def func3() -> Literal[None]: ... | ^^^^ - | help: Replace with `None` | 9 | @@ -48,7 +45,6 @@ PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` | 13 | def func4(arg1: Literal[int, None, float]): ... | ^^^^ - | help: Replace with `Literal[...] | None` | 12 | @@ -62,7 +58,6 @@ PYI061 [*] Use `None` rather than `Literal[None]` | 16 | def func5(arg1: Literal[None, None]): ... | ^^^^ - | help: Replace with `None` | 15 | @@ -76,7 +71,6 @@ PYI061 [*] Use `None` rather than `Literal[None]` | 16 | def func5(arg1: Literal[None, None]): ... | ^^^^ - | help: Replace with `None` | 15 | @@ -132,7 +126,6 @@ PYI061 Use `None` rather than `Literal[None]` | 31 | def func8(arg1: Literal[None] | None):... | ^^^^ - | help: Replace with `None` PYI061 [*] Use `None` rather than `Literal[None]` @@ -140,7 +133,6 @@ PYI061 [*] Use `None` rather than `Literal[None]` | 34 | def func9(arg1: Union[Literal[None], None]): ... | ^^^^ - | help: Replace with `None` | 33 | @@ -172,7 +164,6 @@ PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]` 42 | Literal[None] # PYI061 None inside "Literal[]" expression. Replace with "None" 43 | Literal[True, None] # PYI061 None inside "Literal[]" expression. Replace with "Literal[True] | None" | ^^^^ - | help: Replace with `Literal[...] | None` | 42 | Literal[None] # PYI061 None inside "Literal[]" expression. Replace with "None" @@ -274,5 +265,4 @@ PYI061 Use `None` rather than `Literal[None]` 54 | d: None | (Literal[None] | None) 55 | e: None | ((None | Literal[None]) | None) | None | ^^^^ - | help: Replace with `None` diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__pyi021_pie790_isolation_check.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__pyi021_pie790_isolation_check.snap index c1e4f0b4b3..620f48aa6e 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__pyi021_pie790_isolation_check.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__pyi021_pie790_isolation_check.snap @@ -25,7 +25,6 @@ PIE790 [*] Unnecessary `...` literal 5 | """Will report both, but only fix the first.""" # ERROR PYI021 6 | ... # ERROR PIE790 | ^^^ - | help: Remove unnecessary `...` | 5 | """Will report both, but only fix the first.""" # ERROR PYI021 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 f5db3b1f52..e97adff100 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs @@ -379,6 +379,7 @@ mod tests { } #[test_case(Rule::PytestExtraneousScopeFunction, Path::new("PT003.py"))] + #[test_case(Rule::PytestCompositeAssertion, Path::new("PT018.py"))] fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!( "preview__{}_{}", 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 9b82d8eb95..b529d8485c 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 @@ -28,7 +28,8 @@ use crate::cst::matchers::match_indented_block; use crate::cst::matchers::match_module; use crate::fix::codemods::CodegenStylist; use crate::importer::ImportRequest; -use crate::{Edit, Fix, FixAvailability, Violation}; +use crate::preview::is_fix_pytest_composite_assertion_enabled; +use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; use super::unittest_assert::UnittestAssert; @@ -60,6 +61,14 @@ use super::unittest_assert::UnittestAssert; /// assert not something /// assert not something_else /// ``` +/// +/// ## Fix safety +/// +/// On stable, the rule's fix is always unsafe and not offered when it would remove comments in the +/// compound assertion. In [preview], the fix is only unsafe when it would delete such comments and +/// safe otherwise. +/// +/// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] #[violation_metadata(stable_since = "v0.0.208")] pub(crate) struct PytestCompositeAssertion; @@ -829,16 +838,24 @@ pub(crate) fn composite_condition(checker: &Checker, stmt: &Stmt, test: &Expr, m let composite = is_composite_condition(test); if matches!(composite, CompositionKind::Simple | CompositionKind::Mixed) { let mut diagnostic = checker.report_diagnostic(PytestCompositeAssertion, stmt.range()); + let preview_fix = is_fix_pytest_composite_assertion_enabled(checker.settings()); if matches!(composite, CompositionKind::Simple) && msg.is_none() - && !checker.comment_ranges().intersects(stmt.range()) + && (preview_fix || !checker.comment_ranges().intersects(stmt.range())) && !checker .indexer() .in_multi_statement_line(stmt, checker.source()) { diagnostic.try_set_fix(|| { - fix_composite_condition(stmt, checker.locator(), checker.stylist()) - .map(Fix::unsafe_edit) + fix_composite_condition(stmt, checker.locator(), checker.stylist()).map(|edit| { + let applicability = + if preview_fix && !checker.comment_ranges().intersects(edit.range()) { + Applicability::Safe + } else { + Applicability::Unsafe + }; + Fix::applicable_edit(edit, applicability) + }) }); } } 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 0793cf9a13..248c624f0d 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 @@ -518,6 +518,11 @@ impl Violation for PytestFixtureFinalizerCallback { /// return resource /// ``` /// +/// ## Fix safety +/// +/// This rule's fix is always marked unsafe because removing the `yield` can change the behavior of +/// code that relies on implicit cleanup, such as when a value is garbage-collected. +/// /// ## References /// - [`pytest` documentation: Teardown/Cleanup](https://docs.pytest.org/en/latest/how-to/fixtures.html#teardown-cleanup-aka-fixture-finalization) #[derive(ViolationMetadata)] @@ -718,7 +723,7 @@ fn check_fixture_decorator(checker: &Checker, func_name: &str, decorator: &Decor Expr::Call(ast::ExprCall { func: _, arguments, - range: _, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, @@ -843,9 +848,9 @@ fn check_fixture_returns(checker: &Checker, name: &str, body: &[Stmt], returns: )) }); if let Some(return_type_edit) = return_type_edit { - diagnostic.set_fix(Fix::safe_edits(yield_edit, [return_type_edit])); + diagnostic.set_fix(Fix::unsafe_edits(yield_edit, [return_type_edit])); } else { - diagnostic.set_fix(Fix::safe_edit(yield_edit)); + diagnostic.set_fix(Fix::unsafe_edit(yield_edit)); } } } 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 0c2c49f515..14c81d458c 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 @@ -158,7 +158,7 @@ fn check_mark_parentheses(checker: &Checker, decorator: &Decorator, marker: &str Expr::Call(ast::ExprCall { func: _, arguments, - range: _, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, 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 78ebc5d906..5b47ea519a 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 @@ -228,7 +228,7 @@ impl UnittestAssert { } /// Create a map from argument name to value. - pub(crate) fn args_map<'a>( + fn args_map<'a>( &'a self, args: &'a [Expr], keywords: &'a [Keyword], @@ -397,7 +397,7 @@ impl UnittestAssert { range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, is_cast: false, is_checked_cast: false, @@ -450,7 +450,7 @@ impl UnittestAssert { range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, is_cast: false, is_checked_cast: false, 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 3ee1436118..455249e949 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 @@ -158,7 +158,7 @@ impl Violation for PytestWarnsWithoutWarning { } } -pub(crate) fn is_pytest_warns(func: &Expr, semantic: &SemanticModel) -> bool { +fn is_pytest_warns(func: &Expr, semantic: &SemanticModel) -> bool { semantic .resolve_qualified_name(func) .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["pytest", "warns"])) diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT008.snap b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT008.snap index ca221bd73b..90713cb031 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT008.snap +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT008.snap @@ -123,4 +123,3 @@ PT008 Use `return_value=` instead of patching with `lambda` 47 | mocker.patch.object(obj, "attr", lambda *args, **kwargs: None) 48 | module_mocker.patch.object(obj, "attr", lambda *args, **kwargs: None) | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT009.snap b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT009.snap index 72090c6833..abf2f69567 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT009.snap +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT009.snap @@ -582,7 +582,6 @@ PT009 [*] Use a regular `assert` instead of unittest-style `failIfEqual` 93 | def test_fail_if_equal(self): 94 | self.failIfEqual(1, 2) # Error | ^^^^^^^^^^^^^^^^ - | help: Replace `failIfEqual(...)` with `assert ...` | 93 | def test_fail_if_equal(self): diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT012.snap b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT012.snap index cd6b2a6d20..4f08228382 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT012.snap +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT012.snap @@ -9,7 +9,6 @@ PT012 `pytest.raises()` block should contain a single simple statement 43 | | len([]) 44 | | [].size | |_______________^ - | PT012 `pytest.raises()` block should contain a single simple statement --> PT012.py:48:5 @@ -72,7 +71,6 @@ PT012 `pytest.raises()` block should contain a single simple statement 66 | | if True: 67 | | raise Exception | |_______________________________^ - | PT012 `pytest.raises()` block should contain a single simple statement --> PT012.py:71:5 @@ -84,7 +82,6 @@ PT012 `pytest.raises()` block should contain a single simple statement 74 | | except: 75 | | raise | |_________________^ - | PT012 `pytest.raises()` block should contain a single simple statement --> PT012.py:83:5 @@ -134,4 +131,3 @@ PT012 `pytest.raises()` block should contain a single simple statement 96 | | async for a in b: 97 | | assert foo | |______________________^ - | diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT013.snap b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT013.snap index edf98e63d9..b127438d88 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT013.snap +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT013.snap @@ -28,4 +28,3 @@ PT013 Incorrect import of `pytest`; use `import pytest` instead 12 | from pytest import fixture 13 | from pytest import fixture as other_name | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT017.snap b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT017.snap index 6fc520ff76..19129ac463 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT017.snap +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT017.snap @@ -8,4 +8,3 @@ PT017 Found assertion on exception `e` in `except` block, use `pytest.raises()` 18 | except Exception as e: 19 | assert e.message, "blah blah" | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT018.snap b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT018.snap index 3c27f91e47..15b8ed2c4a 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT018.snap +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT018.snap @@ -1,5 +1,6 @@ --- source: crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs +assertion_line: 358 --- PT018 [*] Assertion should be broken down into multiple parts --> PT018.py:14:5 @@ -258,7 +259,6 @@ PT018 Assertion should be broken down into multiple parts 39 | # detected, but no fix for mixed conditions (e.g. `a or b and c`) 40 | assert not (something or something_else and something_third) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Break down assertion into multiple parts PT018 [*] Assertion should be broken down into multiple parts @@ -286,7 +286,6 @@ PT018 [*] Assertion should be broken down into multiple parts 44 | assert something and something_else # Error 45 | assert something and something_else and something_third # Error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Break down assertion into multiple parts | 44 | assert something and something_else # Error @@ -326,7 +325,6 @@ PT018 Assertion should be broken down into multiple parts 53 | x = 1; \ 54 | assert something and something_else | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Break down assertion into multiple parts PT018 [*] Assertion should be broken down into multiple parts @@ -385,3 +383,14 @@ help: Break down assertion into multiple parts 72 | | note: This is an unsafe fix and may change runtime behavior + +PT018 Assertion should be broken down into multiple parts + --> PT018.py:76:5 + | +75 | def test_comments(): +76 | / assert ( +77 | | # comment +78 | | something and something_else +79 | | ) + | |_____^ +help: Break down assertion into multiple parts diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT022.snap b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT022.snap index 0d4813e187..b460eb6e9e 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT022.snap +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT022.snap @@ -8,7 +8,6 @@ PT022 [*] No teardown in fixture `error`, use `return` instead of `yield` 16 | resource = acquire_resource() 17 | yield resource | ^^^^^^^^^^^^^^ - | help: Replace `yield` with `return` | 16 | resource = acquire_resource() @@ -16,6 +15,7 @@ help: Replace `yield` with `return` 17 + return resource 18 | | +note: This is an unsafe fix and may change runtime behavior PT022 [*] No teardown in fixture `error`, use `return` instead of `yield` --> PT022.py:37:5 @@ -24,7 +24,6 @@ PT022 [*] No teardown in fixture `error`, use `return` instead of `yield` 36 | resource = acquire_resource() 37 | yield resource | ^^^^^^^^^^^^^^ - | help: Replace `yield` with `return` | 34 | @pytest.fixture() @@ -35,6 +34,7 @@ help: Replace `yield` with `return` 37 + return resource 38 | | +note: This is an unsafe fix and may change runtime behavior PT022 [*] No teardown in fixture `error`, use `return` instead of `yield` --> PT022.py:43:5 @@ -43,7 +43,6 @@ PT022 [*] No teardown in fixture `error`, use `return` instead of `yield` 42 | resource = acquire_resource() 43 | yield resource | ^^^^^^^^^^^^^^ - | help: Replace `yield` with `return` | 40 | @pytest.fixture() @@ -53,3 +52,4 @@ help: Replace `yield` with `return` - yield resource 43 + return resource | +note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT028.snap b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT028.snap index d91741bcdd..0d4024d9bd 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT028.snap +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT028.snap @@ -158,7 +158,6 @@ PT028 [*] Test function parameter `a` has default argument 10 | def test_foo(a: (int) = (1)): ... 11 | def test_foo(a=1, /, b=2, *, c=3): ... | ^ - | help: Remove default argument | 10 | def test_foo(a: (int) = (1)): ... @@ -175,7 +174,6 @@ PT028 [*] Test function parameter `b` has default argument 10 | def test_foo(a: (int) = (1)): ... 11 | def test_foo(a=1, /, b=2, *, c=3): ... | ^ - | help: Remove default argument | 10 | def test_foo(a: (int) = (1)): ... @@ -192,7 +190,6 @@ PT028 [*] Test function parameter `c` has default argument 10 | def test_foo(a: (int) = (1)): ... 11 | def test_foo(a=1, /, b=2, *, c=3): ... | ^ - | help: Remove default argument | 10 | def test_foo(a: (int) = (1)): ... diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT031.snap b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT031.snap index 498b32eacf..442182375a 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT031.snap +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT031.snap @@ -9,7 +9,6 @@ PT031 `pytest.warns()` block should contain a single simple statement 43 | | foo() 44 | | bar() | |_____________^ - | PT031 `pytest.warns()` block should contain a single simple statement --> PT031.py:48:5 @@ -72,7 +71,6 @@ PT031 `pytest.warns()` block should contain a single simple statement 66 | | if True: 67 | | foo() | |_____________________^ - | PT031 `pytest.warns()` block should contain a single simple statement --> PT031.py:71:5 @@ -84,7 +82,6 @@ PT031 `pytest.warns()` block should contain a single simple statement 74 | | except: 75 | | raise | |_________________^ - | PT031 `pytest.warns()` block should contain a single simple statement --> PT031.py:83:5 @@ -134,4 +131,3 @@ PT031 `pytest.warns()` block should contain a single simple statement 96 | | async for a in b: 97 | | assert foo | |______________________^ - | diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__is_pytest_test.snap b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__is_pytest_test.snap index d8906b8845..c3597dcf9d 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__is_pytest_test.snap +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__is_pytest_test.snap @@ -61,7 +61,6 @@ PT028 [*] Test function parameter `a` has default argument 7 | def test_this_too_is_a_test(self, a=1): ... 8 | def testAndOfCourseThis(self, a=1): ... | ^ - | help: Remove default argument | 7 | def test_this_too_is_a_test(self, a=1): ... diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__preview__PT018_PT018.py.snap b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__preview__PT018_PT018.py.snap new file mode 100644 index 0000000000..1a299ba99a --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__preview__PT018_PT018.py.snap @@ -0,0 +1,628 @@ +--- +source: crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs +assertion_line: 389 +--- +--- Linter settings --- +-linter.preview = disabled ++linter.preview = enabled + +--- Summary --- +Removed: 14 +Added: 14 + +--- Removed --- +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:14:5 + | +13 | def test_error(): +14 | assert something and something_else + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +15 | assert something and something_else and something_third +16 | assert something and not something_else + | +help: Break down assertion into multiple parts + | +13 | def test_error(): + - assert something and something_else +14 + assert something +15 + assert something_else +16 | assert something and something_else and something_third + | +note: This is an unsafe fix and may change runtime behavior + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:15:5 + | +13 | def test_error(): +14 | assert something and something_else +15 | assert something and something_else and something_third + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +16 | assert something and not something_else +17 | assert something and (something_else or something_third) + | +help: Break down assertion into multiple parts + | +14 | assert something and something_else + - assert something and something_else and something_third +15 + assert something and something_else +16 + assert something_third +17 | assert something and not something_else + | +note: This is an unsafe fix and may change runtime behavior + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:16:5 + | +14 | assert something and something_else +15 | assert something and something_else and something_third +16 | assert something and not something_else + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +17 | assert something and (something_else or something_third) +18 | assert not something and something_else + | +help: Break down assertion into multiple parts + | +15 | assert something and something_else and something_third + - assert something and not something_else +16 + assert something +17 + assert not something_else +18 | assert something and (something_else or something_third) + | +note: This is an unsafe fix and may change runtime behavior + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:17:5 + | +15 | assert something and something_else and something_third +16 | assert something and not something_else +17 | assert something and (something_else or something_third) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +18 | assert not something and something_else +19 | assert not (something or something_else) + | +help: Break down assertion into multiple parts + | +16 | assert something and not something_else + - assert something and (something_else or something_third) +17 + assert something +18 + assert (something_else or something_third) +19 | assert not something and something_else + | +note: This is an unsafe fix and may change runtime behavior + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:18:5 + | +16 | assert something and not something_else +17 | assert something and (something_else or something_third) +18 | assert not something and something_else + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +19 | assert not (something or something_else) +20 | assert not (something or something_else or something_third) + | +help: Break down assertion into multiple parts + | +17 | assert something and (something_else or something_third) + - assert not something and something_else +18 + assert not something +19 + assert something_else +20 | assert not (something or something_else) + | +note: This is an unsafe fix and may change runtime behavior + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:19:5 + | +17 | assert something and (something_else or something_third) +18 | assert not something and something_else +19 | assert not (something or something_else) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +20 | assert not (something or something_else or something_third) +21 | assert something and something_else == """error + | +help: Break down assertion into multiple parts + | +18 | assert not something and something_else + - assert not (something or something_else) +19 + assert not something +20 + assert not something_else +21 | assert not (something or something_else or something_third) + | +note: This is an unsafe fix and may change runtime behavior + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:20:5 + | +18 | assert not something and something_else +19 | assert not (something or something_else) +20 | assert not (something or something_else or something_third) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +21 | assert something and something_else == """error +22 | message + | +help: Break down assertion into multiple parts + | +19 | assert not (something or something_else) + - assert not (something or something_else or something_third) +20 + assert not (something or something_else) +21 + assert not something_third +22 | assert something and something_else == """error + | +note: This is an unsafe fix and may change runtime behavior + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:21:5 + | +19 | assert not (something or something_else) +20 | assert not (something or something_else or something_third) +21 | / assert something and something_else == """error +22 | | message +23 | | """ + | |_______^ +24 | assert ( +25 | something + | +help: Break down assertion into multiple parts + | +20 | assert not (something or something_else or something_third) + - assert something and something_else == """error +21 + assert something +22 + assert something_else == """error +23 | message + | +note: This is an unsafe fix and may change runtime behavior + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:24:5 + | +22 | message +23 | """ +24 | / assert ( +25 | | something +26 | | and something_else +27 | | == """error +28 | | message +29 | | """ +30 | | ) + | |_____^ +31 | +32 | # recursive case + | +help: Break down assertion into multiple parts + | +23 | """ +24 + assert something +25 | assert ( + - something + - and something_else +26 + something_else +27 | == """error + | +note: This is an unsafe fix and may change runtime behavior + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:33:5 + | +32 | # recursive case +33 | assert not (a or not (b or c)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +34 | assert not (a or not (b and c)) + | +help: Break down assertion into multiple parts + | +32 | # recursive case + - assert not (a or not (b or c)) +33 + assert not a +34 + assert (b or c) +35 | assert not (a or not (b and c)) + | +note: This is an unsafe fix and may change runtime behavior + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:34:5 + | +32 | # recursive case +33 | assert not (a or not (b or c)) +34 | assert not (a or not (b and c)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +35 | +36 | # detected, but no fix for messages + | +help: Break down assertion into multiple parts + | +33 | assert not (a or not (b or c)) + - assert not (a or not (b and c)) +34 + assert not a +35 + assert (b and c) +36 | + | +note: This is an unsafe fix and may change runtime behavior + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:59:5 + | +57 | # Regression test for: https://github.com/astral-sh/ruff/issues/7143 +58 | def test_parenthesized_not(): +59 | / assert not ( +60 | | self.find_graph_output(node.output[0]) +61 | | or self.find_graph_input(node.input[0]) +62 | | or self.find_graph_output(node.input[0]) +63 | | ) + | |_____^ +64 | +65 | assert (not ( + | +help: Break down assertion into multiple parts + | +61 | or self.find_graph_input(node.input[0]) + - or self.find_graph_output(node.input[0]) +62 | ) +63 + assert not ( +64 + self.find_graph_output(node.input[0]) +65 + ) +66 | + | +note: This is an unsafe fix and may change runtime behavior + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:65:5 + | +63 | ) +64 | +65 | / assert (not ( +66 | | self.find_graph_output(node.output[0]) +67 | | or self.find_graph_input(node.input[0]) +68 | | or self.find_graph_output(node.input[0]) +69 | | )) + | |______^ +70 | +71 | assert (not self.find_graph_output(node.output[0]) or + | +help: Break down assertion into multiple parts + | +64 | + - assert (not ( +65 + assert not ( +66 | self.find_graph_output(node.output[0]) +67 | or self.find_graph_input(node.input[0]) + - or self.find_graph_output(node.input[0]) + - )) +68 + ) +69 + assert not ( +70 + self.find_graph_output(node.input[0]) +71 + ) +72 | + | +note: This is an unsafe fix and may change runtime behavior + + +PT018 Assertion should be broken down into multiple parts + --> PT018.py:76:5 + | +75 | def test_comments(): +76 | / assert ( +77 | | # comment +78 | | something and something_else +79 | | ) + | |_____^ +help: Break down assertion into multiple parts + + + +--- Added --- +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:14:5 + | +13 | def test_error(): +14 | assert something and something_else + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +15 | assert something and something_else and something_third +16 | assert something and not something_else + | +help: Break down assertion into multiple parts + | +13 | def test_error(): + - assert something and something_else +14 + assert something +15 + assert something_else +16 | assert something and something_else and something_third + | + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:15:5 + | +13 | def test_error(): +14 | assert something and something_else +15 | assert something and something_else and something_third + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +16 | assert something and not something_else +17 | assert something and (something_else or something_third) + | +help: Break down assertion into multiple parts + | +14 | assert something and something_else + - assert something and something_else and something_third +15 + assert something and something_else +16 + assert something_third +17 | assert something and not something_else + | + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:16:5 + | +14 | assert something and something_else +15 | assert something and something_else and something_third +16 | assert something and not something_else + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +17 | assert something and (something_else or something_third) +18 | assert not something and something_else + | +help: Break down assertion into multiple parts + | +15 | assert something and something_else and something_third + - assert something and not something_else +16 + assert something +17 + assert not something_else +18 | assert something and (something_else or something_third) + | + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:17:5 + | +15 | assert something and something_else and something_third +16 | assert something and not something_else +17 | assert something and (something_else or something_third) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +18 | assert not something and something_else +19 | assert not (something or something_else) + | +help: Break down assertion into multiple parts + | +16 | assert something and not something_else + - assert something and (something_else or something_third) +17 + assert something +18 + assert (something_else or something_third) +19 | assert not something and something_else + | + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:18:5 + | +16 | assert something and not something_else +17 | assert something and (something_else or something_third) +18 | assert not something and something_else + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +19 | assert not (something or something_else) +20 | assert not (something or something_else or something_third) + | +help: Break down assertion into multiple parts + | +17 | assert something and (something_else or something_third) + - assert not something and something_else +18 + assert not something +19 + assert something_else +20 | assert not (something or something_else) + | + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:19:5 + | +17 | assert something and (something_else or something_third) +18 | assert not something and something_else +19 | assert not (something or something_else) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +20 | assert not (something or something_else or something_third) +21 | assert something and something_else == """error + | +help: Break down assertion into multiple parts + | +18 | assert not something and something_else + - assert not (something or something_else) +19 + assert not something +20 + assert not something_else +21 | assert not (something or something_else or something_third) + | + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:20:5 + | +18 | assert not something and something_else +19 | assert not (something or something_else) +20 | assert not (something or something_else or something_third) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +21 | assert something and something_else == """error +22 | message + | +help: Break down assertion into multiple parts + | +19 | assert not (something or something_else) + - assert not (something or something_else or something_third) +20 + assert not (something or something_else) +21 + assert not something_third +22 | assert something and something_else == """error + | + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:21:5 + | +19 | assert not (something or something_else) +20 | assert not (something or something_else or something_third) +21 | / assert something and something_else == """error +22 | | message +23 | | """ + | |_______^ +24 | assert ( +25 | something + | +help: Break down assertion into multiple parts + | +20 | assert not (something or something_else or something_third) + - assert something and something_else == """error +21 + assert something +22 + assert something_else == """error +23 | message + | + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:24:5 + | +22 | message +23 | """ +24 | / assert ( +25 | | something +26 | | and something_else +27 | | == """error +28 | | message +29 | | """ +30 | | ) + | |_____^ +31 | +32 | # recursive case + | +help: Break down assertion into multiple parts + | +23 | """ +24 + assert something +25 | assert ( + - something + - and something_else +26 + something_else +27 | == """error + | + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:33:5 + | +32 | # recursive case +33 | assert not (a or not (b or c)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +34 | assert not (a or not (b and c)) + | +help: Break down assertion into multiple parts + | +32 | # recursive case + - assert not (a or not (b or c)) +33 + assert not a +34 + assert (b or c) +35 | assert not (a or not (b and c)) + | + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:34:5 + | +32 | # recursive case +33 | assert not (a or not (b or c)) +34 | assert not (a or not (b and c)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +35 | +36 | # detected, but no fix for messages + | +help: Break down assertion into multiple parts + | +33 | assert not (a or not (b or c)) + - assert not (a or not (b and c)) +34 + assert not a +35 + assert (b and c) +36 | + | + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:59:5 + | +57 | # Regression test for: https://github.com/astral-sh/ruff/issues/7143 +58 | def test_parenthesized_not(): +59 | / assert not ( +60 | | self.find_graph_output(node.output[0]) +61 | | or self.find_graph_input(node.input[0]) +62 | | or self.find_graph_output(node.input[0]) +63 | | ) + | |_____^ +64 | +65 | assert (not ( + | +help: Break down assertion into multiple parts + | +61 | or self.find_graph_input(node.input[0]) + - or self.find_graph_output(node.input[0]) +62 | ) +63 + assert not ( +64 + self.find_graph_output(node.input[0]) +65 + ) +66 | + | + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:65:5 + | +63 | ) +64 | +65 | / assert (not ( +66 | | self.find_graph_output(node.output[0]) +67 | | or self.find_graph_input(node.input[0]) +68 | | or self.find_graph_output(node.input[0]) +69 | | )) + | |______^ +70 | +71 | assert (not self.find_graph_output(node.output[0]) or + | +help: Break down assertion into multiple parts + | +64 | + - assert (not ( +65 + assert not ( +66 | self.find_graph_output(node.output[0]) +67 | or self.find_graph_input(node.input[0]) + - or self.find_graph_output(node.input[0]) + - )) +68 + ) +69 + assert not ( +70 + self.find_graph_output(node.input[0]) +71 + ) +72 | + | + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:76:5 + | +75 | def test_comments(): +76 | / assert ( +77 | | # comment +78 | | something and something_else +79 | | ) + | |_____^ +help: Break down assertion into multiple parts + | +75 | def test_comments(): + - assert ( + - # comment + - something and something_else + - ) +76 + assert something +77 + assert something_else + | +note: This is an unsafe fix and may change runtime behavior 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 07e47c1f83..a10c38b5c9 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 @@ -56,10 +56,14 @@ impl AlwaysFixableViolation for AvoidableEscapedQuote { pub(crate) fn avoidable_escaped_quote(checker: &Checker, string_like: StringLike) { if checker.semantic().in_pep_257_docstring() || checker.semantic().in_string_type_definition() - // 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_interpolated_string_replacement_field() + || ( + // 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_interpolated_string_replacement_field() + ) { return; } 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 43fbf7ad6d..c646029590 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 @@ -376,8 +376,10 @@ fn strings(checker: &Checker, sequence: &[TextRange]) { *range, ))); } 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 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.is_rule_enabled(Rule::BadQuotesInlineString) { diff --git a/crates/ruff_linter/src/rules/flake8_quotes/settings.rs b/crates/ruff_linter/src/rules/flake8_quotes/settings.rs index fe5129d6e3..7da94422e0 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/settings.rs +++ b/crates/ruff_linter/src/rules/flake8_quotes/settings.rs @@ -64,7 +64,7 @@ impl Display for Settings { impl Quote { #[must_use] - pub const fn opposite(self) -> Self { + pub(crate) const fn opposite(self) -> Self { match self { Self::Double => Self::Single, Self::Single => Self::Double, @@ -72,7 +72,7 @@ impl Quote { } /// Get the character used to represent this quote. - pub const fn as_char(self) -> char { + pub(crate) const fn as_char(self) -> char { match self { Self::Double => '"', Self::Single => '\'', diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__only_multiline_doubles_all.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__only_multiline_doubles_all.py.snap index 2e8097b082..e2b969aedb 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__only_multiline_doubles_all.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__only_multiline_doubles_all.py.snap @@ -11,7 +11,6 @@ Q001 [*] Double quote multiline found but single quotes preferred 6 | | double quote string 7 | | """ | |___^ - | help: Replace double multiline quotes with single quotes | 4 | diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_module_multiline.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_module_multiline.py.snap index e44689e598..7935bd5f49 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_module_multiline.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_module_multiline.py.snap @@ -33,7 +33,6 @@ Q001 [*] Double quote multiline found but single quotes preferred 10 | | this is not a docstring 11 | | """ | |___^ - | help: Replace double multiline quotes with single quotes | 8 | pass diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_module_singleline.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_module_singleline.py.snap index 77109e8df4..9520a1fc21 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_module_singleline.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_module_singleline.py.snap @@ -25,7 +25,6 @@ Q001 [*] Double quote multiline found but single quotes preferred 5 | pass 6 | """ this is not a docstring """ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace double multiline quotes with single quotes | 5 | pass diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_class.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_class.py.snap index 7d6abc4bf2..4d53a37f63 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_class.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_class.py.snap @@ -40,7 +40,6 @@ Q002 [*] Single quote docstring found but double quotes preferred 8 | 9 | class Nested(foo()[:]): ''' inline docstring '''; pass | ^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace single quotes docstring with double quotes | 8 | diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_function.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_function.py.snap index 1286d50cb5..a797388c07 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_function.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_function.py.snap @@ -46,7 +46,6 @@ Q002 [*] Single quote docstring found but double quotes preferred 26 | def function_with_single_docstring(a): 27 | 'Single line docstring' | ^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace single quotes docstring with double quotes | 26 | def function_with_single_docstring(a): diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_class_var_1.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_class_var_1.py.snap index f76e2d2544..aafe221fec 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_class_var_1.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_class_var_1.py.snap @@ -60,7 +60,6 @@ Q002 Single quote docstring found but double quotes preferred 8 | 9 | class Nested(foo()[:]): ''"Start with empty string" ' and lint docstring safely'; pass | ^^ - | help: Replace single quotes docstring with double quotes Q002 [*] Single quote docstring found but double quotes preferred @@ -70,7 +69,6 @@ Q002 [*] Single quote docstring found but double quotes preferred 8 | 9 | class Nested(foo()[:]): ''"Start with empty string" ' and lint docstring safely'; pass | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace single quotes docstring with double quotes | 8 | diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_class_var_2.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_class_var_2.py.snap index 50124a4fd0..61253ebe23 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_class_var_2.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_class_var_2.py.snap @@ -72,7 +72,6 @@ Q002 [*] Single quote docstring found but double quotes preferred 8 | 9 | class Nested(foo()[:]): 'Do not'" start with empty string" ' and lint docstring safely'; pass | ^^^^^^^^ - | help: Replace single quotes docstring with double quotes | 8 | @@ -87,7 +86,6 @@ Q002 [*] Single quote docstring found but double quotes preferred 8 | 9 | class Nested(foo()[:]): 'Do not'" start with empty string" ' and lint docstring safely'; pass | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace single quotes docstring with double quotes | 8 | diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_module_singleline_var_1.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_module_singleline_var_1.py.snap index 700b68128e..dbbf6c1b35 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_module_singleline_var_1.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_module_singleline_var_1.py.snap @@ -33,7 +33,6 @@ Q001 [*] Double quote multiline found but single quotes preferred 4 | pass 5 | """ this is not a docstring """ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace double multiline quotes with single quotes | 4 | pass diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_module_singleline_var_2.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_module_singleline_var_2.py.snap index 1f48167adf..48503a33b2 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_module_singleline_var_2.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_module_singleline_var_2.py.snap @@ -38,7 +38,6 @@ Q001 [*] Double quote multiline found but single quotes preferred 4 | pass 5 | """ this is not a docstring """ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace double multiline quotes with single quotes | 4 | pass diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_class.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_class.py.snap index a92f5f6965..d520428d8e 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_class.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_class.py.snap @@ -40,7 +40,6 @@ Q002 [*] Double quote docstring found but single quotes preferred 8 | 9 | class Nested(foo()[:]): """ inline docstring """; pass | ^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace double quotes docstring with single quotes | 8 | diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_function.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_function.py.snap index d4bbf6696f..90a6c15e18 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_function.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_function.py.snap @@ -46,7 +46,6 @@ Q002 [*] Double quote docstring found but single quotes preferred 26 | def function_with_single_docstring(a): 27 | "Single line docstring" | ^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace double quotes docstring with single quotes | 26 | def function_with_single_docstring(a): diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_class_var_1.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_class_var_1.py.snap index 28bd74c2a0..1e01cbb5a8 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_class_var_1.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_class_var_1.py.snap @@ -28,5 +28,4 @@ Q002 Double quote docstring found but single quotes preferred 8 | 9 | class Nested(foo()[:]): ""'Start with empty string' ' and lint docstring safely'; pass | ^^ - | help: Replace double quotes docstring with single quotes diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_class_var_2.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_class_var_2.py.snap index 8d5a27598e..e5cc5e6522 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_class_var_2.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_class_var_2.py.snap @@ -40,7 +40,6 @@ Q002 [*] Double quote docstring found but single quotes preferred 8 | 9 | class Nested(foo()[:]): "Do not"' start with empty string' ' and lint docstring safely'; pass | ^^^^^^^^ - | help: Replace double quotes docstring with single quotes | 8 | diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_module_multiline.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_module_multiline.py.snap index 67f3b4f08c..236d3304e2 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_module_multiline.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_module_multiline.py.snap @@ -33,7 +33,6 @@ Q001 [*] Single quote multiline found but double quotes preferred 10 | | this is not a docstring 11 | | ''' | |___^ - | help: Replace single multiline quotes with double quotes | 8 | pass diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_module_singleline.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_module_singleline.py.snap index 56d20c5bdb..a23fc81535 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_module_singleline.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_module_singleline.py.snap @@ -25,7 +25,6 @@ Q001 [*] Single quote multiline found but double quotes preferred 5 | pass 6 | ''' this is not a docstring ''' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace single multiline quotes with double quotes | 5 | pass diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_escaped.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_escaped.py.snap index 4d59c43c2c..bd4d6fa017 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_escaped.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_escaped.py.snap @@ -179,7 +179,6 @@ Q003 [*] Change outer quotes to avoid escaping inner quotes 38 | f"\"normal\" {f"\"nested\" {"other"} normal"} 'single quotes'" # Q003 39 | f"\"normal\" {f"\"nested\" {"other"} 'single quotes'"} normal" # Q003 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Change outer quotes to avoid escaping inner quotes | 38 | f"\"normal\" {f"\"nested\" {"other"} normal"} 'single quotes'" # Q003 @@ -335,7 +334,6 @@ Q003 [*] Change outer quotes to avoid escaping inner quotes 60 | t"\"normal\" {t"\"nested\" {"other"} normal"} 'single quotes'" # Q003 61 | t"\"normal\" {t"\"nested\" {"other"} 'single quotes'"} normal" # Q003 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Change outer quotes to avoid escaping inner quotes | 60 | t"\"normal\" {t"\"nested\" {"other"} normal"} 'single quotes'" # Q003 diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_escaped_py311.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_escaped_py311.snap index 386a1f1690..2cc3c94994 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_escaped_py311.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_escaped_py311.snap @@ -214,7 +214,6 @@ Q003 [*] Change outer quotes to avoid escaping inner quotes 60 | t"\"normal\" {t"\"nested\" {"other"} normal"} 'single quotes'" # Q003 61 | t"\"normal\" {t"\"nested\" {"other"} 'single quotes'"} normal" # Q003 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Change outer quotes to avoid escaping inner quotes | 60 | t"\"normal\" {t"\"nested\" {"other"} normal"} 'single quotes'" # Q003 diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_escaped_unnecessary.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_escaped_unnecessary.py.snap index e995376b96..08a6364fdc 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_escaped_unnecessary.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_escaped_unnecessary.py.snap @@ -302,7 +302,6 @@ Q004 [*] Unnecessary escape on inner quote character 45 | # Invalid escapes in bytestrings are also triggered: 46 | x = b"\xe7\xeb\x0c\xa1\x1b\x83tN\xce=x\xe9\xbe\x01\xb9\x13B_\xba\xe7\x0c2\xce\'rm\x0e\xcd\xe9.\xf8\xd2" # Q004 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove backslash | 45 | # Invalid escapes in bytestrings are also triggered: diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_implicit.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_implicit.py.snap index f8e725e435..092dc8ec5a 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_implicit.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_implicit.py.snap @@ -112,7 +112,6 @@ Q000 [*] Single quotes found but double quotes preferred 26 | 'This can use "single" quotes' 27 | 'But this needs to be changed' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace single quotes with double quotes | 26 | 'This can use "single" quotes' diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_would_be_triple_quotes.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_would_be_triple_quotes.py.snap index 17507020bd..6260fa19f6 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_would_be_triple_quotes.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_would_be_triple_quotes.py.snap @@ -30,7 +30,6 @@ Q000 [*] Single quotes found but double quotes preferred 1 | s = ''"Start with empty string" ' and lint docstring safely' 2 | s = 'Do not'" start with empty string" ' and lint docstring safely' | ^^^^^^^^ - | help: Replace single quotes with double quotes | 1 | s = ''"Start with empty string" ' and lint docstring safely' @@ -44,7 +43,6 @@ Q000 [*] Single quotes found but double quotes preferred 1 | s = ''"Start with empty string" ' and lint docstring safely' 2 | s = 'Do not'" start with empty string" ' and lint docstring safely' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace single quotes with double quotes | 1 | s = ''"Start with empty string" ' and lint docstring safely' diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_escaped.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_escaped.py.snap index 1bbb2878d7..46600a513b 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_escaped.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_escaped.py.snap @@ -214,7 +214,6 @@ Q003 [*] Change outer quotes to avoid escaping inner quotes 40 | f'\'normal\' {f'\'nested\' {'other'} normal'} "double quotes"' # Q003 41 | f'\'normal\' {f'\'nested\' {'other'} "double quotes"'} normal' # Q00l | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Change outer quotes to avoid escaping inner quotes | 40 | f'\'normal\' {f'\'nested\' {'other'} normal'} "double quotes"' # Q003 @@ -388,7 +387,6 @@ Q003 [*] Change outer quotes to avoid escaping inner quotes 64 | t'\'normal\' {t'\'nested\' {'other'} normal'} "double quotes"' # Q003 65 | t'\'normal\' {t'\'nested\' {'other'} "double quotes"'} normal' # Q00l | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Change outer quotes to avoid escaping inner quotes | 64 | t'\'normal\' {t'\'nested\' {'other'} normal'} "double quotes"' # Q003 diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_escaped_py311.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_escaped_py311.snap index 0efcd426fc..a034a879a6 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_escaped_py311.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_escaped_py311.snap @@ -267,7 +267,6 @@ Q003 [*] Change outer quotes to avoid escaping inner quotes 64 | t'\'normal\' {t'\'nested\' {'other'} normal'} "double quotes"' # Q003 65 | t'\'normal\' {t'\'nested\' {'other'} "double quotes"'} normal' # Q00l | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Change outer quotes to avoid escaping inner quotes | 64 | t'\'normal\' {t'\'nested\' {'other'} normal'} "double quotes"' # Q003 diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_escaped_unnecessary.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_escaped_unnecessary.py.snap index 39c1227564..dcdf0dd56a 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_escaped_unnecessary.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_escaped_unnecessary.py.snap @@ -321,7 +321,6 @@ Q004 [*] Unnecessary escape on inner quote character 44 | this_is_fine = 'This is an \\"escaped\\" quote' 45 | this_should_raise_Q004 = 'This is an \\\"escaped\\\" quote with an extra backslash' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove backslash | 44 | this_is_fine = 'This is an \\"escaped\\" quote' diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_implicit.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_implicit.py.snap index 8d292c1882..4aeda3de02 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_implicit.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_implicit.py.snap @@ -112,7 +112,6 @@ Q000 [*] Double quotes found but single quotes preferred 26 | "This can use 'double' quotes" 27 | "But this needs to be changed" | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace double quotes with single quotes | 26 | "This can use 'double' quotes" diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_would_be_triple_quotes.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_would_be_triple_quotes.py.snap index 4e5583b7f7..abfdc49025 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_would_be_triple_quotes.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_would_be_triple_quotes.py.snap @@ -16,7 +16,6 @@ Q000 [*] Double quotes found but single quotes preferred 1 | s = ""'Start with empty string' ' and lint docstring safely' 2 | s = "Do not"' start with empty string' ' and lint docstring safely' | ^^^^^^^^ - | help: Replace double quotes with single quotes | 1 | s = ""'Start with empty string' ' and lint docstring safely' 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 36f7cafc72..4dc0884934 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 @@ -62,7 +62,7 @@ pub(crate) fn unnecessary_paren_on_raise_exception(checker: &Checker, expr: &Exp let Expr::Call(ast::ExprCall { func, arguments, - range: _, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, 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 8b9f917e85..5e2ff0db58 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 @@ -251,7 +251,6 @@ RSE102 [*] Unnecessary parentheses on raised exception 106 | if future.exception(): 107 | raise future.Exception() | ^^ - | help: Remove unnecessary parentheses | 106 | if future.exception(): @@ -269,7 +268,6 @@ RSE102 [*] Unnecessary parentheses on raised exception 111 | | # comment 112 | | ) | |_^ - | help: Remove unnecessary parentheses | 109 | 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 9db7dc3a4e..af6b9259a6 100644 --- a/crates/ruff_linter/src/rules/flake8_return/rules/function.rs +++ b/crates/ruff_linter/src/rules/flake8_return/rules/function.rs @@ -568,7 +568,7 @@ pub(crate) fn unnecessary_assign(checker: &Checker, function_stmt: &Stmt) { let Some(function_scope) = checker.semantic().function_scope(function_def) else { return; }; - for (assign, return_, stmt) in &stack.assignment_return { + for (assign, return_, stmt, enclosing_finally) in &stack.assignment_return { // Identify, e.g., `return x`. let Some(value) = return_.value.as_ref() else { continue; @@ -617,6 +617,22 @@ pub(crate) fn unnecessary_assign(checker: &Checker, function_stmt: &Stmt) { else { continue; }; + // Ignore assignments whose name is read or deleted in an enclosing `finally`, which runs + // after the `return`. A reference resolving to a later rebinding in the `finally` counts + // too, so check every binding of the name. + if !enclosing_finally.is_empty() + && function_scope + .get_all(assigned_id) + .flat_map(|binding_id| checker.semantic().binding(binding_id).references()) + .map(|reference_id| checker.semantic().reference(reference_id)) + .any(|reference| { + enclosing_finally + .iter() + .any(|finally_range| finally_range.contains_range(reference.range())) + }) + { + 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 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 c814daac27..e741c2ab9b 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 @@ -8,7 +8,6 @@ RET501 [*] Do not explicitly `return None` in function if it is the only possibl 3 | return 4 | return None # error | ^^^^^^^^^^^ - | help: Remove explicit `return None` | 3 | return @@ -44,7 +43,6 @@ RET501 [*] Do not explicitly `return None` in function if it is the only possibl 60 | | None # comment 61 | | ) | |_________^ - | help: Remove explicit `return None` | 58 | 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 2e9440c2b2..eaaa56b939 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 @@ -28,7 +28,6 @@ RET503 [*] Missing explicit `return` at the end of function able to return non-` 29 | | else: 30 | | return 2 | |________________^ - | help: Add explicit `return` statement | 30 | return 2 @@ -46,7 +45,6 @@ RET503 [*] Missing explicit `return` at the end of function able to return non-` 36 | | 37 | | print() # error | |___________^ - | help: Add explicit `return` statement | 37 | print() # error @@ -84,7 +82,6 @@ RET503 [*] Missing explicit `return` at the end of function able to return non-` 52 | | else: 53 | | print() # error | |_______________^ - | help: Add explicit `return` statement | 53 | print() # error @@ -102,7 +99,6 @@ RET503 [*] Missing explicit `return` at the end of function able to return non-` 59 | | return False 60 | | no_such_function() # error | |______________________^ - | help: Add explicit `return` statement | 60 | no_such_function() # error @@ -120,7 +116,6 @@ RET503 [*] Missing explicit `return` at the end of function able to return non-` 66 | | return False 67 | | print("", end="") # error | |_____________________^ - | help: Add explicit `return` statement | 67 | print("", end="") # error @@ -139,7 +134,6 @@ RET503 [*] Missing explicit `return` at the end of function able to return non-` 85 | | return 1 86 | | y += 1 | |______________^ - | help: Add explicit `return` statement | 86 | y += 1 @@ -158,7 +152,6 @@ RET503 [*] Missing explicit `return` at the end of function able to return non-` 116 | | break 117 | | return z | |________________^ - | help: Add explicit `return` statement | 117 | return z @@ -179,7 +172,6 @@ RET503 [*] Missing explicit `return` at the end of function able to return non-` 126 | | return z 127 | | return None | |___________________^ - | help: Add explicit `return` statement | 127 | return None @@ -197,7 +189,6 @@ RET503 [*] Missing explicit `return` at the end of function able to return non-` 133 | | continue 134 | | return z | |________________^ - | help: Add explicit `return` statement | 134 | return z @@ -218,7 +209,6 @@ RET503 [*] Missing explicit `return` at the end of function able to return non-` 143 | | return z 144 | | return None | |___________________^ - | help: Add explicit `return` statement | 144 | return None @@ -237,7 +227,6 @@ RET503 [*] Missing explicit `return` at the end of function able to return non-` 275 | | for value in values: 276 | | print(value) | |____________________^ - | help: Add explicit `return` statement | 276 | print(value) @@ -257,7 +246,6 @@ RET503 [*] Missing explicit `return` at the end of function able to return non-` 291 | | case 1: 292 | | print() # error | |___________________^ - | help: Add explicit `return` statement | 292 | print() # error @@ -274,7 +262,6 @@ RET503 [*] Missing explicit `return` at the end of function able to return non-` 301 | | if True: 302 | | return "" | |_____________________^ - | help: Add explicit `return` statement | 302 | return "" @@ -290,7 +277,6 @@ RET503 [*] Missing explicit `return` at the end of function able to return non-` 306 | | if True: 307 | | return "" | |_____________________^ - | help: Add explicit `return` statement | 307 | return "" @@ -306,7 +292,6 @@ RET503 [*] Missing explicit `return` at the end of function able to return non-` 311 | | if True: 312 | | return "" # type: ignore | |_____________________^ - | help: Add explicit `return` statement | 312 | return "" # type: ignore @@ -322,7 +307,6 @@ RET503 [*] Missing explicit `return` at the end of function able to return non-` 316 | | if True: 317 | | return "" ; | |_____________________^ - | help: Add explicit `return` statement | 317 | return "" ; @@ -356,7 +340,6 @@ RET503 [*] Missing explicit `return` at the end of function able to return non-` 328 | | return 1 329 | | x = 2 \ | |_________^ - | help: Add explicit `return` statement | 330 | @@ -375,7 +358,6 @@ RET503 [*] Missing explicit `return` at the end of function able to return non-` 402 | | with c: 403 | | d | |_____________^ - | help: Add explicit `return` statement | 403 | d @@ -396,7 +378,6 @@ RET503 [*] Missing explicit `return` at the end of function able to return non-` 417 | | return 5 418 | | bar() | |_________^ - | help: Add explicit `return` statement | 417 | return 5 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 a88286f4f4..f86aab6b5a 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 @@ -8,7 +8,6 @@ RET504 [*] Unnecessary assignment to `a` before `return` statement 5 | a = 1 6 | return a # RET504 | ^ - | help: Remove unnecessary assignment | 4 | def x(): @@ -26,7 +25,6 @@ RET504 [*] Unnecessary assignment to `formatted` before `return` statement 22 | formatted = formatted.replace("()", "").replace(" ", " ").strip() 23 | return formatted | ^^^^^^^^^ - | help: Remove unnecessary assignment | 21 | # clean up after any blank components @@ -44,7 +42,6 @@ RET504 [*] Unnecessary assignment to `queryset` before `return` statement 245 | queryset = queryset.filter(c=3) 246 | return queryset | ^^^^^^^^ - | help: Remove unnecessary assignment | 244 | queryset = Model.filter(a=1) @@ -62,7 +59,6 @@ RET504 [*] Unnecessary assignment to `queryset` before `return` statement 250 | queryset = Model.filter(a=1) 251 | return queryset # RET504 | ^^^^^^^^ - | help: Remove unnecessary assignment | 249 | def get_queryset(): @@ -80,7 +76,6 @@ RET504 [*] Unnecessary assignment to `val` before `return` statement 268 | val = 1 269 | return val # RET504 | ^^^ - | help: Remove unnecessary assignment | 267 | return val @@ -98,7 +93,6 @@ RET504 [*] Unnecessary assignment to `x` before `return` statement 320 | x = f.read() 321 | return x # RET504 | ^ - | help: Remove unnecessary assignment | 319 | with open("foo.txt", "r") as f: @@ -116,7 +110,6 @@ RET504 [*] Unnecessary assignment to `b` before `return` statement 341 | b=a 342 | return b # RET504 | ^ - | help: Remove unnecessary assignment | 340 | a = 1 @@ -134,7 +127,6 @@ RET504 [*] Unnecessary assignment to `b` before `return` statement 347 | b =a 348 | return b # RET504 | ^ - | help: Remove unnecessary assignment | 346 | a = 1 @@ -152,7 +144,6 @@ RET504 [*] Unnecessary assignment to `b` before `return` statement 353 | b= a 354 | return b # RET504 | ^ - | help: Remove unnecessary assignment | 352 | a = 1 @@ -170,7 +161,6 @@ RET504 [*] Unnecessary assignment to `a` before `return` statement 358 | a = 1 # Comment 359 | return a | ^ - | help: Remove unnecessary assignment | 357 | def foo(): @@ -188,7 +178,6 @@ RET504 [*] Unnecessary assignment to `D` before `return` statement 364 | D=0.4853881 + 3.6006116*P - 0.0117368*(P-1.3822)**2 365 | return D | ^ - | help: Remove unnecessary assignment | 363 | def mavko_debari(P_kbar): @@ -206,7 +195,6 @@ RET504 [*] Unnecessary assignment to `y` before `return` statement 399 | y = y + 2 400 | return y # RET504 | ^ - | help: Remove unnecessary assignment | 398 | x = 1 @@ -224,7 +212,6 @@ RET504 [*] Unnecessary assignment to `services` before `return` statement 422 | services = a["services"] 423 | return services | ^^^^^^^^ - | help: Remove unnecessary assignment | 421 | if "services" in a: @@ -263,7 +250,6 @@ RET504 [*] Unnecessary assignment to `x` before `return` statement 462 | ) 463 | return x | ^ - | help: Remove unnecessary assignment | 460 | def f(): diff --git a/crates/ruff_linter/src/rules/flake8_return/snapshots/ruff_linter__rules__flake8_return__tests__RET505_RET505.py.snap b/crates/ruff_linter/src/rules/flake8_return/snapshots/ruff_linter__rules__flake8_return__tests__RET505_RET505.py.snap index c53c0627e7..7882189b06 100644 --- a/crates/ruff_linter/src/rules/flake8_return/snapshots/ruff_linter__rules__flake8_return__tests__RET505_RET505.py.snap +++ b/crates/ruff_linter/src/rules/flake8_return/snapshots/ruff_linter__rules__flake8_return__tests__RET505_RET505.py.snap @@ -271,7 +271,6 @@ RET505 [*] Unnecessary `else` after `return` statement 176 | return 177 | else: pass | ^^^^ - | help: Remove unnecessary `else` | 176 | return @@ -305,7 +304,6 @@ RET505 [*] Unnecessary `else` after `return` statement 199 | if self._sb is not None: return self._sb 200 | else: self._sb = '\033[01;%dm'; self._sa = '\033[0;0m'; | ^^^^ - | help: Remove unnecessary `else` | 199 | if self._sb is not None: return self._sb diff --git a/crates/ruff_linter/src/rules/flake8_return/visitor.rs b/crates/ruff_linter/src/rules/flake8_return/visitor.rs index f86bd0e57e..f24d3aaa43 100644 --- a/crates/ruff_linter/src/rules/flake8_return/visitor.rs +++ b/crates/ruff_linter/src/rules/flake8_return/visitor.rs @@ -4,6 +4,7 @@ use rustc_hash::FxHashSet; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; use ruff_python_semantic::SemanticModel; +use ruff_text_size::{Ranged, TextRange}; #[derive(Default)] pub(super) struct Stack<'data> { @@ -30,11 +31,16 @@ pub(super) struct Stack<'data> { pub(super) annotations: FxHashSet<&'data str>, /// Whether the current function is a generator. pub(super) is_generator: bool, - /// The `assignment`-to-`return` statement pairs in the current function. + /// The `assignment`-to-`return` statement pairs in the current function, each paired with the + /// ranges of any enclosing `finally` suites that run after the `return`. /// TODO(charlie): Remove the extra [`Stmt`] here, which is necessary to support statement /// removal for the `return` statement. - pub(super) assignment_return: - Vec<(&'data ast::StmtAssign, &'data ast::StmtReturn, &'data Stmt)>, + pub(super) assignment_return: Vec<( + &'data ast::StmtAssign, + &'data ast::StmtReturn, + &'data Stmt, + Vec, + )>, } pub(super) struct ReturnVisitor<'semantic, 'data> { @@ -57,6 +63,20 @@ impl<'semantic, 'data> ReturnVisitor<'semantic, 'data> { parents: Vec::new(), } } + + /// Return the enclosing `finally` suites that run after this `return`. + fn enclosing_finally(&self, stmt_return: &ast::StmtReturn) -> Vec { + self.parents + .iter() + .filter_map(|parent| parent.as_try_stmt()) + .filter_map(|stmt_try| { + let first = stmt_try.finalbody.first()?; + let last = stmt_try.finalbody.last()?; + Some(TextRange::new(first.start(), last.end())) + }) + .filter(|finally_range| !finally_range.contains_range(stmt_return.range())) + .collect() + } } impl<'a> Visitor<'a> for ReturnVisitor<'_, 'a> { @@ -109,13 +129,12 @@ impl<'a> Visitor<'a> for ReturnVisitor<'_, 'a> { .non_locals .extend(names.iter().map(Identifier::as_str)); } - Stmt::AnnAssign(ast::StmtAnnAssign { target, value, .. }) - // Ex) `x: int` - if value.is_none() => { - if let Expr::Name(name) = target.as_ref() { - self.stack.annotations.insert(name.id.as_str()); - } + // Ex) `x: int` + Stmt::AnnAssign(ast::StmtAnnAssign { target, value, .. }) if value.is_none() => { + if let Expr::Name(name) = target.as_ref() { + self.stack.annotations.insert(name.id.as_str()); } + } Stmt::Return(stmt_return) => { // If the `return` statement is preceded by an `assignment` statement, then the // `assignment` statement may be redundant. @@ -128,9 +147,13 @@ impl<'a> Visitor<'a> for ReturnVisitor<'_, 'a> { // return x // ``` Stmt::Assign(stmt_assign) => { - self.stack - .assignment_return - .push((stmt_assign, stmt_return, stmt)); + let enclosing_finally = self.enclosing_finally(stmt_return); + self.stack.assignment_return.push(( + stmt_assign, + stmt_return, + stmt, + enclosing_finally, + )); } // Example: // ```python @@ -144,10 +167,12 @@ impl<'a> Visitor<'a> for ReturnVisitor<'_, 'a> { with.body.last().and_then(Stmt::as_assign_stmt) { if !has_conditional_body(with, self.semantic) { + let enclosing_finally = self.enclosing_finally(stmt_return); self.stack.assignment_return.push(( stmt_assign, stmt_return, stmt, + enclosing_finally, )); } } diff --git a/crates/ruff_linter/src/rules/flake8_self/settings.rs b/crates/ruff_linter/src/rules/flake8_self/settings.rs index a6d9f1dde3..b1d056c68b 100644 --- a/crates/ruff_linter/src/rules/flake8_self/settings.rs +++ b/crates/ruff_linter/src/rules/flake8_self/settings.rs @@ -8,7 +8,7 @@ use std::fmt::{Display, Formatter}; // By default, ignore the `namedtuple` methods and attributes, as well as the // _sunder_ names in Enum, which are underscore-prefixed to prevent conflicts // with field names. -pub const IGNORE_NAMES: [&str; 7] = [ +const IGNORE_NAMES: [&str; 7] = [ "_make", "_asdict", "_replace", diff --git a/crates/ruff_linter/src/rules/flake8_self/snapshots/ruff_linter__rules__flake8_self__tests__custom_method_decorators.snap b/crates/ruff_linter/src/rules/flake8_self/snapshots/ruff_linter__rules__flake8_self__tests__custom_method_decorators.snap index c584ba4f52..4720565ffa 100644 --- a/crates/ruff_linter/src/rules/flake8_self/snapshots/ruff_linter__rules__flake8_self__tests__custom_method_decorators.snap +++ b/crates/ruff_linter/src/rules/flake8_self/snapshots/ruff_linter__rules__flake8_self__tests__custom_method_decorators.snap @@ -8,4 +8,3 @@ SLF001 Private member accessed: `_x` 18 | def bad_staticmethod(this): 19 | return this._x # error | ^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_self/snapshots/ruff_linter__rules__flake8_self__tests__ignore_names.snap b/crates/ruff_linter/src/rules/flake8_self/snapshots/ruff_linter__rules__flake8_self__tests__ignore_names.snap index 155b9281c2..c57560008c 100644 --- a/crates/ruff_linter/src/rules/flake8_self/snapshots/ruff_linter__rules__flake8_self__tests__ignore_names.snap +++ b/crates/ruff_linter/src/rules/flake8_self/snapshots/ruff_linter__rules__flake8_self__tests__ignore_names.snap @@ -7,7 +7,6 @@ SLF001 Private member accessed: `_asdict` 5 | def foo(obj): 6 | obj._asdict # SLF001 | ^^^^^^^^^^^ - | SLF001 Private member accessed: `_bar` --> SLF001_extended.py:10:5 @@ -15,4 +14,3 @@ SLF001 Private member accessed: `_bar` 9 | def foo(obj): 10 | obj._bar # SLF001 | ^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_self/snapshots/ruff_linter__rules__flake8_self__tests__private-member-access_SLF001_1.py.snap b/crates/ruff_linter/src/rules/flake8_self/snapshots/ruff_linter__rules__flake8_self__tests__private-member-access_SLF001_1.py.snap index ea0102f2f3..b4e340dd01 100644 --- a/crates/ruff_linter/src/rules/flake8_self/snapshots/ruff_linter__rules__flake8_self__tests__private-member-access_SLF001_1.py.snap +++ b/crates/ruff_linter/src/rules/flake8_self/snapshots/ruff_linter__rules__flake8_self__tests__private-member-access_SLF001_1.py.snap @@ -8,4 +8,3 @@ SLF001 Private member accessed: `_x` 69 | alias = self 70 | print(alias._x) # error (self is not an instance parameter) | ^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_simplify/mod.rs b/crates/ruff_linter/src/rules/flake8_simplify/mod.rs index 5fb01cd177..093cd0ffd0 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/mod.rs @@ -11,7 +11,6 @@ mod tests { use crate::registry::Rule; use crate::settings::LinterSettings; - use crate::settings::types::PreviewMode; use crate::test::test_path; use crate::{assert_diagnostics, assert_diagnostics_diff, settings}; @@ -74,10 +73,7 @@ mod tests { ); let diagnostics = test_path( Path::new("flake8_simplify").join(path).as_path(), - &LinterSettings { - preview: PreviewMode::Enabled, - ..LinterSettings::for_rule(rule_code) - }, + &LinterSettings::for_rule(rule_code).with_preview_mode(), )?; 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 85a47a740c..07a8953da2 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 @@ -291,7 +291,7 @@ impl AlwaysFixableViolation for ExprAndFalse { } /// Return `true` if two `Expr` instances are equivalent names. -pub(crate) fn is_same_expr<'a>(a: &'a Expr, b: &'a Expr) -> Option<&'a str> { +fn is_same_expr<'a>(a: &'a Expr, b: &'a Expr) -> Option<&'a str> { if let (Expr::Name(ast::ExprName { id: a, .. }), Expr::Name(ast::ExprName { id: b, .. })) = (&a, &b) { @@ -314,7 +314,7 @@ fn isinstance_target<'a>(call: &'a Expr, semantic: &'a SemanticModel) -> Option< range: _, node_index: _, }, - range: _, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, 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 b2458326ee..aa6eac3984 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 @@ -256,7 +256,7 @@ pub(crate) fn dict_get_with_none_default(checker: &Checker, expr: &Expr) { let Expr::Call(ast::ExprCall { func, arguments: Arguments { args, keywords, .. }, - range: _, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, 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 6fc202a15c..b08d2311b7 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 @@ -196,7 +196,7 @@ pub(crate) fn if_expr_with_true_false( range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, is_cast: false, is_checked_cast: false, 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 faf91a430b..b710404a83 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 @@ -297,7 +297,7 @@ pub(crate) fn double_negation(checker: &Checker, expr: &Expr, op: UnaryOp, opera range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, is_cast: false, is_checked_cast: false, 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 16301c0aca..c8d649dcd5 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 @@ -172,14 +172,14 @@ pub(super) enum NestedIf<'a> { } impl<'a> NestedIf<'a> { - pub(super) fn body(self) -> &'a [Stmt] { + fn body(self) -> &'a [Stmt] { match self { NestedIf::If(stmt_if) => &stmt_if.body, NestedIf::Elif(clause) => &clause.body, } } - pub(super) fn is_elif(self) -> bool { + fn is_elif(self) -> bool { matches!(self, NestedIf::Elif(..)) } } @@ -325,11 +325,7 @@ fn parenthesize_and_operand(expr: libcst_native::Expression) -> libcst_native::E } /// Convert `if a: if b:` to `if a and b:`. -pub(super) fn collapse_nested_if( - locator: &Locator, - stylist: &Stylist, - nested_if: NestedIf, -) -> Result { +fn collapse_nested_if(locator: &Locator, stylist: &Stylist, nested_if: NestedIf) -> Result { // Infer the indentation of the outer block. let Some(outer_indent) = whitespace::indentation(locator.contents(), &nested_if) else { bail!("Unable to fix multiline statement"); 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 51375e7da6..2fe90ec471 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 @@ -210,7 +210,7 @@ pub(crate) fn if_else_block_instead_of_dict_get(checker: &Checker, stmt_if: &ast range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, is_cast: false, is_checked_cast: false, @@ -323,7 +323,7 @@ pub(crate) fn if_exp_instead_of_dict_get( range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, is_cast: false, is_checked_cast: false, 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 2646f9eba4..6ba535f930 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 @@ -60,7 +60,7 @@ fn key_in_dict(checker: &Checker, left: &Expr, right: &Expr, operator: CmpOp, pa let Expr::Call(ast::ExprCall { func, arguments: Arguments { args, keywords, .. }, - range: _, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, 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 f1ddb06f35..626bdd7a89 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 @@ -300,7 +300,7 @@ pub(crate) fn needless_bool(checker: &Checker, stmt: &Stmt) { range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, is_cast: false, is_checked_cast: false, 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 430f92f7fe..23bec830d5 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 @@ -436,7 +436,7 @@ fn return_stmt(id: Name, test: &Expr, target: &Expr, iter: &Expr, generator: Gen range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, is_cast: false, is_checked_cast: false, diff --git a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM101_SIM101.py.snap b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM101_SIM101.py.snap index 0474f054bd..193b47c6de 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM101_SIM101.py.snap +++ b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM101_SIM101.py.snap @@ -346,7 +346,6 @@ SIM101 [*] Multiple `isinstance` calls for `x`, merge into a single call 71 | ((isinstance(x, int)) or isinstance(x, str)) 72 | isinstance(x, int) or (isinstance(x, str)) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Merge `isinstance` calls for `x` | 71 | ((isinstance(x, int)) or isinstance(x, str)) diff --git a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM103_SIM103.py.snap b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM103_SIM103.py.snap index d32b0a7754..5ef3e5bca6 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM103_SIM103.py.snap +++ b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM103_SIM103.py.snap @@ -11,7 +11,6 @@ SIM103 [*] Return the condition `bool(a)` directly 5 | | else: 6 | | return False | |____________________^ - | help: Replace with `return bool(a)` | 2 | # SIM103 @@ -34,7 +33,6 @@ SIM103 [*] Return the condition `a == b` directly 13 | | else: 14 | | return False | |____________________^ - | help: Replace with `return a == b` | 10 | # SIM103 @@ -57,7 +55,6 @@ SIM103 [*] Return the condition `bool(b)` directly 23 | | else: 24 | | return False | |____________________^ - | help: Replace with `return bool(b)` | 20 | return 1 @@ -80,7 +77,6 @@ SIM103 [*] Return the condition `bool(b)` directly 34 | | else: 35 | | return False | |________________________^ - | help: Replace with `return bool(b)` | 31 | else: @@ -103,7 +99,6 @@ SIM103 [*] Return the condition `not a` directly 59 | | else: 60 | | return True | |___________________^ - | help: Replace with `return not a` | 56 | # SIM103 @@ -126,7 +121,6 @@ SIM103 Return the condition directly 85 | | else: 86 | | return False | |____________________^ - | help: Inline condition SIM103 [*] Return the condition `not (keys is not None and notice.key not in keys)` directly @@ -139,7 +133,6 @@ SIM103 [*] Return the condition `not (keys is not None and notice.key not in key 93 | | else: 94 | | return True | |___________________^ - | help: Replace with `return not (keys is not None and notice.key not in keys)` | 90 | # SIM103 @@ -161,7 +154,6 @@ SIM103 [*] Return the condition `bool(a)` directly 105 | | return True 106 | | return False | |________________^ - | help: Replace with `return bool(a)` | 103 | # SIM103 @@ -182,7 +174,6 @@ SIM103 [*] Return the condition `not a` directly 112 | | return False 113 | | return True | |_______________^ - | help: Replace with `return not a` | 110 | # SIM103 @@ -202,7 +193,6 @@ SIM103 [*] Return the condition `10 < a` directly 118 | | return False 119 | | return True | |_______________^ - | help: Replace with `return 10 < a` | 116 | def f(): @@ -222,7 +212,6 @@ SIM103 [*] Return the condition `not 10 < a` directly 124 | | return False 125 | | return True | |_______________^ - | help: Replace with `return not 10 < a` | 122 | def f(): @@ -242,7 +231,6 @@ SIM103 [*] Return the condition `10 not in a` directly 130 | | return False 131 | | return True | |_______________^ - | help: Replace with `return 10 not in a` | 128 | def f(): @@ -262,7 +250,6 @@ SIM103 [*] Return the condition `10 in a` directly 136 | | return False 137 | | return True | |_______________^ - | help: Replace with `return 10 in a` | 134 | def f(): @@ -282,7 +269,6 @@ SIM103 [*] Return the condition `a is not 10` directly 142 | | return False 143 | | return True | |_______________^ - | help: Replace with `return a is not 10` | 140 | def f(): @@ -302,7 +288,6 @@ SIM103 [*] Return the condition `a is 10` directly 148 | | return False 149 | | return True | |_______________^ - | help: Replace with `return a is 10` | 146 | def f(): @@ -322,7 +307,6 @@ SIM103 [*] Return the condition `a != 10` directly 154 | | return False 155 | | return True | |_______________^ - | help: Replace with `return a != 10` | 152 | def f(): @@ -342,7 +326,6 @@ SIM103 [*] Return the condition `a == 10` directly 160 | | return False 161 | | return True | |_______________^ - | help: Replace with `return a == 10` | 158 | def f(): diff --git a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM103_if_let_basedpython.by.snap b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM103_if_let_basedpython.by.snap index e16adddc62..a51e22364a 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM103_if_let_basedpython.by.snap +++ b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM103_if_let_basedpython.by.snap @@ -10,7 +10,6 @@ SIM103 [*] Return the condition `bool(a)` directly 29 | | else: 30 | | return False | |____________________^ - | help: Replace with `return bool(a)` | 26 | def f2(): 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 34ea726443..d4ed691c68 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 @@ -10,7 +10,6 @@ SIM105 [*] Use `contextlib.suppress(ValueError)` instead of `try`-`except`-`pass 8 | | except ValueError: 9 | | pass | |________^ - | help: Replace `try`-`except`-`pass` with `with contextlib.suppress(ValueError): ...` | 1 + import contextlib @@ -144,7 +143,6 @@ SIM105 [*] Use `contextlib.suppress(ValueError)` instead of `try`-`except`-`pass 87 | | except ValueError: 88 | | ... | |___________^ - | help: Replace `try`-`except`-`pass` with `with contextlib.suppress(ValueError): ...` | 1 + import contextlib @@ -263,7 +261,6 @@ SIM105 [*] Use `contextlib.suppress()` instead of `try`-`except`-`pass` 135 | | except (): 136 | | pass | |________^ - | help: Replace `try`-`except`-`pass` with `with contextlib.suppress(): ...` | 1 + import contextlib @@ -288,7 +285,6 @@ SIM105 [*] Use `contextlib.suppress(BaseException)` instead of `try`-`except`-`p 142 | | except BaseException: 143 | | pass | |________^ - | help: Replace `try`-`except`-`pass` with `with contextlib.suppress(BaseException): ...` | 1 + import contextlib diff --git a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM105_SIM105_1.py.snap b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM105_SIM105_1.py.snap index 26c3aad9cf..aad14a9681 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM105_SIM105_1.py.snap +++ b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM105_SIM105_1.py.snap @@ -10,7 +10,6 @@ SIM105 [*] Use `contextlib.suppress(ValueError)` instead of `try`-`except`-`pass 7 | | except ValueError: 8 | | pass | |________^ - | help: Replace `try`-`except`-`pass` with `with contextlib.suppress(ValueError): ...` | 2 | import math diff --git a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM105_SIM105_2.py.snap b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM105_SIM105_2.py.snap index cfb7cfb521..aa96b34644 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM105_SIM105_2.py.snap +++ b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM105_SIM105_2.py.snap @@ -10,7 +10,6 @@ SIM105 [*] Use `contextlib.suppress(ValueError)` instead of `try`-`except`-`pass 12 | | except ValueError: 13 | | pass | |________^ - | help: Replace `try`-`except`-`pass` with `with contextlib.suppress(ValueError): ...` | 9 | # SIM105 diff --git a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM105_SIM105_3.py.snap b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM105_SIM105_3.py.snap index 3f4f3a5e14..6e1aab90c4 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM105_SIM105_3.py.snap +++ b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM105_SIM105_3.py.snap @@ -11,5 +11,4 @@ SIM105 Use `contextlib.suppress(ValueError)` instead of `try`-`except`-`pass` 12 | | except ValueError: 13 | | pass | |____________^ - | help: Replace `try`-`except`-`pass` with `with contextlib.suppress(ValueError): ...` diff --git a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM105_SIM105_4.py.snap b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM105_SIM105_4.py.snap index a5b73e255a..3443d5b709 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM105_SIM105_4.py.snap +++ b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM105_SIM105_4.py.snap @@ -9,7 +9,6 @@ SIM105 [*] Use `contextlib.suppress(ImportError)` instead of `try`-`except`-`pas 3 | | from __builtin__ import bytes, str, open, super, range, zip, round, int, pow, object, input 4 | | except ImportError: pass | |___________________________^ - | help: Replace `try`-`except`-`pass` with `with contextlib.suppress(ImportError): ...` | 1 | #!/usr/bin/env python diff --git a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM107_SIM107.py.snap b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM107_SIM107.py.snap index 705b45e39d..b5605539ba 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM107_SIM107.py.snap +++ b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM107_SIM107.py.snap @@ -8,4 +8,3 @@ SIM107 Don't use `return` in `try`-`except` and `finally` 8 | finally: 9 | return "3" | ^^^^^^^^^^ - | 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 b7c72bcbb5..d18fbf4152 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 @@ -35,7 +35,6 @@ SIM108 [*] Use ternary operator `b = 1 if a else 2` instead of `if`-`else`-block 32 | | else: 33 | | b = 2 | |_____________^ - | help: Replace `if`-`else`-block with `b = 1 if a else 2` | 29 | else: @@ -59,7 +58,6 @@ SIM108 Use ternary operator `abc = x if x > 0 else -x` instead of `if`-`else`-bl 62 | | # test test test 63 | | abc = -x | |____________^ - | help: Replace `if`-`else`-block with `abc = x if x > 0 else -x` SIM108 [*] Use ternary operator `b = "cccccccccccccccccccccccccccccccccß" if a else "ddddddddddddddddddddddddddddddddd💣"` instead of `if`-`else`-block @@ -71,7 +69,6 @@ SIM108 [*] Use ternary operator `b = "cccccccccccccccccccccccccccccccccß" if a 84 | | else: 85 | | b = "ddddddddddddddddddddddddddddddddd💣" | |_____________________________________________^ - | help: Replace `if`-`else`-block with `b = "cccccccccccccccccccccccccccccccccß" if a else "ddddddddddddddddddddddddddddddddd💣"` | 81 | # SIM108 @@ -93,7 +90,6 @@ SIM108 Use ternary operator `exitcode = 0 if True else 1` instead of `if`-`else` 107 | | else: 108 | | exitcode = 1 # Trailing comment | |________________^ - | help: Replace `if`-`else`-block with `exitcode = 0 if True else 1` SIM108 Use ternary operator `x = 3 if True else 5` instead of `if`-`else`-block @@ -103,7 +99,6 @@ SIM108 Use ternary operator `x = 3 if True else 5` instead of `if`-`else`-block 112 | / if True: x = 3 # Foo 113 | | else: x = 5 | |___________^ - | help: Replace `if`-`else`-block with `x = 3 if True else 5` SIM108 Use ternary operator `x = 3 if True else 5` instead of `if`-`else`-block @@ -115,7 +110,6 @@ SIM108 Use ternary operator `x = 3 if True else 5` instead of `if`-`else`-block 119 | | else: 120 | | x = 5 | |_________^ - | help: Replace `if`-`else`-block with `x = 3 if True else 5` SIM108 [*] Use binary operator `z = cond or other_cond` instead of `if`-`else`-block @@ -303,7 +297,6 @@ SIM108 [*] Use ternary operator `z = not foo() if foo() else other` instead of ` 195 | | else: 196 | | z = other | |_____________^ - | help: Replace `if`-`else`-block with `z = not foo() if foo() else other` | 192 | # from Two to one. @@ -351,7 +344,6 @@ SIM108 [*] Use ternary operator `var = "str" if cond else f'{first}-{second}'` i 209 | | else: 210 | | var = f'{first}-{second}' | |_____________________________^ - | help: Replace `if`-`else`-block with `var = "str" if cond else f'{first}-{second}'` | 206 | diff --git a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM110_SIM110.py.snap b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM110_SIM110.py.snap index 5c015e7788..240553f8cf 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM110_SIM110.py.snap +++ b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM110_SIM110.py.snap @@ -11,7 +11,6 @@ SIM110 [*] Use `return any(check(x) for x in iterable)` instead of `for` loop 5 | | return True 6 | | return False | |________________^ - | help: Replace with `return any(check(x) for x in iterable)` | 2 | # SIM110 @@ -34,7 +33,6 @@ SIM110 [*] Use `return all(not check(x) for x in iterable)` instead of `for` loo 27 | | return False 28 | | return True | |_______________^ - | help: Replace with `return all(not check(x) for x in iterable)` | 24 | # SIM111 @@ -57,7 +55,6 @@ SIM110 [*] Use `return all(x.is_empty() for x in iterable)` instead of `for` loo 35 | | return False 36 | | return True | |_______________^ - | help: Replace with `return all(x.is_empty() for x in iterable)` | 32 | # SIM111 @@ -81,7 +78,6 @@ SIM110 [*] Use `return any(check(x) for x in iterable)` instead of `for` loop 58 | | else: 59 | | return False | |____________________^ - | help: Replace with `return any(check(x) for x in iterable)` | 54 | # SIM110 @@ -106,7 +102,6 @@ SIM110 [*] Use `return all(not check(x) for x in iterable)` instead of `for` loo 67 | | else: 68 | | return True | |___________________^ - | help: Replace with `return all(not check(x) for x in iterable)` | 63 | # SIM111 @@ -182,7 +177,6 @@ SIM110 Use `return any(check(x) for x in iterable)` instead of `for` loop 126 | | return True 127 | | return False | |________________^ - | help: Replace with `return any(check(x) for x in iterable)` SIM110 Use `return all(not check(x) for x in iterable)` instead of `for` loop @@ -195,7 +189,6 @@ SIM110 Use `return all(not check(x) for x in iterable)` instead of `for` loop 136 | | return False 137 | | return True | |_______________^ - | help: Replace with `return all(not check(x) for x in iterable)` SIM110 [*] Use `return any(check(x) for x in iterable)` instead of `for` loop @@ -207,7 +200,6 @@ SIM110 [*] Use `return any(check(x) for x in iterable)` instead of `for` loop 146 | | return True 147 | | return False | |________________^ - | help: Replace with `return any(check(x) for x in iterable)` | 143 | # SIM110 @@ -229,7 +221,6 @@ SIM110 [*] Use `return all(not check(x) for x in iterable)` instead of `for` loo 156 | | return False 157 | | return True | |_______________^ - | help: Replace with `return all(not check(x) for x in iterable)` | 153 | # SIM111 @@ -252,7 +243,6 @@ SIM110 [*] Use `return any(x.isdigit() for x in "012ß9💣2ℝ9012ß9💣2ℝ90 164 | | return True 165 | | return False | |________________^ - | help: Replace with `return any(x.isdigit() for x in "012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ")` | 161 | # SIM110 diff --git a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM110_SIM111.py.snap b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM110_SIM111.py.snap index db068d9fb2..d4093cc4ed 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM110_SIM111.py.snap +++ b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM110_SIM111.py.snap @@ -11,7 +11,6 @@ SIM110 [*] Use `return any(check(x) for x in iterable)` instead of `for` loop 5 | | return True 6 | | return False | |________________^ - | help: Replace with `return any(check(x) for x in iterable)` | 2 | # SIM110 @@ -34,7 +33,6 @@ SIM110 [*] Use `return all(not check(x) for x in iterable)` instead of `for` loo 27 | | return False 28 | | return True | |_______________^ - | help: Replace with `return all(not check(x) for x in iterable)` | 24 | # SIM111 @@ -57,7 +55,6 @@ SIM110 [*] Use `return all(x.is_empty() for x in iterable)` instead of `for` loo 35 | | return False 36 | | return True | |_______________^ - | help: Replace with `return all(x.is_empty() for x in iterable)` | 32 | # SIM111 @@ -81,7 +78,6 @@ SIM110 [*] Use `return any(check(x) for x in iterable)` instead of `for` loop 58 | | else: 59 | | return False | |____________________^ - | help: Replace with `return any(check(x) for x in iterable)` | 54 | # SIM110 @@ -106,7 +102,6 @@ SIM110 [*] Use `return all(not check(x) for x in iterable)` instead of `for` loo 67 | | else: 68 | | return True | |___________________^ - | help: Replace with `return all(not check(x) for x in iterable)` | 63 | # SIM111 @@ -182,7 +177,6 @@ SIM110 Use `return any(check(x) for x in iterable)` instead of `for` loop 126 | | return True 127 | | return False | |________________^ - | help: Replace with `return any(check(x) for x in iterable)` SIM110 Use `return all(not check(x) for x in iterable)` instead of `for` loop @@ -195,7 +189,6 @@ SIM110 Use `return all(not check(x) for x in iterable)` instead of `for` loop 136 | | return False 137 | | return True | |_______________^ - | help: Replace with `return all(not check(x) for x in iterable)` SIM110 [*] Use `return any(check(x) for x in iterable)` instead of `for` loop @@ -207,7 +200,6 @@ SIM110 [*] Use `return any(check(x) for x in iterable)` instead of `for` loop 146 | | return True 147 | | return False | |________________^ - | help: Replace with `return any(check(x) for x in iterable)` | 143 | # SIM110 @@ -229,7 +221,6 @@ SIM110 [*] Use `return all(not check(x) for x in iterable)` instead of `for` loo 156 | | return False 157 | | return True | |_______________^ - | help: Replace with `return all(not check(x) for x in iterable)` | 153 | # SIM111 @@ -252,7 +243,6 @@ SIM110 [*] Use `return all(x in y for x in iterable)` instead of `for` loop 164 | | return False 165 | | return True | |_______________^ - | help: Replace with `return all(x in y for x in iterable)` | 161 | # SIM111 @@ -275,7 +265,6 @@ SIM110 [*] Use `return all(x <= y for x in iterable)` instead of `for` loop 172 | | return False 173 | | return True | |_______________^ - | help: Replace with `return all(x <= y for x in iterable)` | 169 | # SIM111 @@ -298,7 +287,6 @@ SIM110 [*] Use `return all(not x.isdigit() for x in "012ß9💣2ℝ9012ß9💣2 180 | | return False 181 | | return True | |_______________^ - | help: Replace with `return all(not x.isdigit() for x in "012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9")` | 177 | # SIM111 diff --git a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM113_SIM113.py.snap b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM113_SIM113.py.snap index 1e4771fe56..6f817cd51f 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM113_SIM113.py.snap +++ b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM113_SIM113.py.snap @@ -28,7 +28,6 @@ SIM113 Use `enumerate()` for index variable `idx` in `for` loop 26 | h(x, y) 27 | idx += 1 | ^^^^^^^^ - | SIM113 Use `enumerate()` for index variable `idx` in `for` loop --> SIM113.py:36:9 @@ -37,7 +36,6 @@ SIM113 Use `enumerate()` for index variable `idx` in `for` loop 35 | sum += h(x, idx) 36 | idx += 1 | ^^^^^^^^ - | SIM113 Use `enumerate()` for index variable `idx` in `for` loop --> SIM113.py:44:9 @@ -67,7 +65,6 @@ SIM113 Use `enumerate()` for index variable `i` in `for` loop 208 | print(f"{i}: {val}") 209 | i += 1 | ^^^^^^ - | SIM113 Use `enumerate()` for index variable `i` in `for` loop --> SIM113.py:220:9 @@ -76,4 +73,3 @@ SIM113 Use `enumerate()` for index variable `i` in `for` loop 219 | print(f"{i}: {val}") 220 | i += 1 | ^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM114_SIM114.py.snap b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM114_SIM114.py.snap index 001ad64df3..73bd00c134 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM114_SIM114.py.snap +++ b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM114_SIM114.py.snap @@ -284,7 +284,6 @@ SIM114 [*] Combine `if` branches using logical `or` operator 73 | | elif result.eofs == "C": 74 | | errors = 1 | |______________^ - | help: Combine `if` branches | 70 | errors = 1 @@ -328,7 +327,6 @@ SIM114 [*] Combine `if` branches using logical `or` operator 124 | | elif b is None: 125 | | return 4 | |________________^ - | help: Combine `if` branches | 121 | return 3 @@ -349,7 +347,6 @@ SIM114 [*] Combine `if` branches using logical `or` operator 134 | | elif a := 1: 135 | | return 3 | |________________^ - | help: Combine `if` branches | 131 | b = False @@ -368,7 +365,6 @@ SIM114 [*] Combine `if` branches using logical `or` operator 140 | | elif c: # but not on the second branch 141 | | b | |_____^ - | help: Combine `if` branches | 137 | @@ -385,7 +381,6 @@ SIM114 [*] Combine `if` branches using logical `or` operator 144 | / if a: b # here's a comment 145 | | elif c: b | |_________^ - | help: Combine `if` branches | 143 | @@ -402,7 +397,6 @@ SIM114 [*] Combine `if` branches using logical `or` operator 149 | | elif(100 < x and x < 200 and 300 < y and y < 800): 150 | | pass | |________^ - | help: Combine `if` branches | 147 | diff --git a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM114_if_let_basedpython.by.snap b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM114_if_let_basedpython.by.snap index e91f3bbac8..2a1044d84b 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM114_if_let_basedpython.by.snap +++ b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM114_if_let_basedpython.by.snap @@ -11,7 +11,6 @@ SIM114 [*] Combine `if` branches using logical `or` operator 52 | | elif b: 53 | | print("twin") | |_________________^ - | help: Combine `if` branches | 49 | diff --git a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM118_SIM118.py.snap b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM118_SIM118.py.snap index 63ac2d757a..9edee0fda8 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM118_SIM118.py.snap +++ b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM118_SIM118.py.snap @@ -352,7 +352,6 @@ SIM118 [*] Use `key in dict` instead of `key in dict.keys()` 64 | d = SneakyDict() 65 | key in d.keys() # SIM118 | ^^^^^^^^^^^^^^^ - | help: Remove `.keys()` | 64 | d = SneakyDict() diff --git a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM210_SIM210.py.snap b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM210_SIM210.py.snap index 122346560d..072f7aac6e 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM210_SIM210.py.snap +++ b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM210_SIM210.py.snap @@ -62,7 +62,6 @@ SIM210 Use `bool(...)` instead of `True if ... else False` 14 | 15 | a = True if b else False | ^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `bool(...) SIM210 [*] Remove unnecessary `True if ... else False` @@ -73,7 +72,6 @@ SIM210 [*] Remove unnecessary `True if ... else False` | ___________^ 20 | | psl.privatesuffix(src.netloc)) else False | |____________________________________________________________________________^ - | help: Remove unnecessary `True if ... else False` | 18 | # Regression test for: https://github.com/astral-sh/ruff/issues/7076 diff --git a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM222_SIM222.py.snap b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM222_SIM222.py.snap index d26697a514..70c8050097 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM222_SIM222.py.snap +++ b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM222_SIM222.py.snap @@ -586,7 +586,6 @@ SIM222 [*] Use `frozenset(frozenset({1}))` instead of `frozenset(frozenset({1})) 96 | 97 | a or frozenset(frozenset({1})) or True or frozenset(frozenset({2})) # SIM222 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `frozenset(frozenset({1}))` | 96 | @@ -968,7 +967,6 @@ SIM222 [*] Use `f"{1}{''}"` instead of `f"{1}{''}" or ...` 168 | print(f"{''}{''}" or "bar") 169 | print(f"{1}{''}" or "bar") | ^^^^^^^^^^^^^^^^^^^ - | help: Replace with `f"{1}{''}"` | 168 | print(f"{''}{''}" or "bar") diff --git a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM223_SIM223.py.snap b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM223_SIM223.py.snap index a2eb3f6295..31886dd5a0 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM223_SIM223.py.snap +++ b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM223_SIM223.py.snap @@ -586,7 +586,6 @@ SIM223 [*] Use `False` instead of `... and False and ...` 91 | 92 | a and frozenset(frozenset({1})) and False and frozenset(frozenset({2})) # SIM222 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `False` | 91 | 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 f27744047f..c8a7d2238f 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 @@ -1174,7 +1174,6 @@ SIM905 [*] Consider using a list literal instead of `str.split` 156 | | "Fred\ Bloggs"@example.com 157 | | "Joe.\\Blow"@example.com""".split("\n") | |_______________________________________^ - | help: Replace with list literal | 134 | # https://github.com/astral-sh/ruff/issues/19581 - embedded quotes in raw strings @@ -1320,7 +1319,6 @@ SIM905 [*] Consider using a list literal instead of `str.split` 172 | " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"] 173 | "a b".split(maxsplit=1) # ["a", "b"] | ^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with list literal | 172 | " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"] 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 70aee21be4..e73560d633 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 @@ -146,7 +146,6 @@ SIM910 [*] Use `dict.get("Cat")` instead of `dict.get("Cat", None)` 56 | dict = {"Tom": 23, "Maria": 23, "Dog": 11} 57 | age = dict.get("Cat", None) | ^^^^^^^^^^^^^^^^^^^^^ - | help: Replace `dict.get("Cat", None)` with `dict.get("Cat")` | 56 | dict = {"Tom": 23, "Maria": 23, "Dog": 11} @@ -215,7 +214,6 @@ SIM910 [*] Use `ages.get(get_key())` instead of `ages.get(get_key(), None)` 83 | ages = {"Tom": 23, "Maria": 23, "Dog": 11} 84 | age = ages.get(get_key(), None) | ^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace `ages.get(get_key(), None)` with `ages.get(get_key())` | 83 | ages = {"Tom": 23, "Maria": 23, "Dog": 11} @@ -234,7 +232,6 @@ SIM910 [*] Use `dict.get()` without default value 90 | | None, 91 | | ) | |_^ - | help: Remove default value | 86 | diff --git a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__preview__SIM113_SIM113.py.snap b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__preview__SIM113_SIM113.py.snap index 5af873a570..3f351f9bd1 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__preview__SIM113_SIM113.py.snap +++ b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__preview__SIM113_SIM113.py.snap @@ -28,7 +28,6 @@ SIM113 Use `enumerate()` for index variable `idx` in `for` loop 26 | h(x, y) 27 | idx += 1 | ^^^^^^^^ - | SIM113 Use `enumerate()` for index variable `idx` in `for` loop --> SIM113.py:36:9 @@ -37,7 +36,6 @@ SIM113 Use `enumerate()` for index variable `idx` in `for` loop 35 | sum += h(x, idx) 36 | idx += 1 | ^^^^^^^^ - | SIM113 Use `enumerate()` for index variable `idx` in `for` loop --> SIM113.py:44:9 @@ -77,7 +75,6 @@ SIM113 Use `enumerate()` for index variable `i` in `for` loop 208 | print(f"{i}: {val}") 209 | i += 1 | ^^^^^^ - | SIM113 Use `enumerate()` for index variable `i` in `for` loop --> SIM113.py:220:9 @@ -86,4 +83,3 @@ SIM113 Use `enumerate()` for index variable `i` in `for` loop 219 | print(f"{i}: {val}") 220 | i += 1 | ^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_tidy_imports/settings.rs b/crates/ruff_linter/src/rules/flake8_tidy_imports/settings.rs index 07bc4c44d2..9bda29060b 100644 --- a/crates/ruff_linter/src/rules/flake8_tidy_imports/settings.rs +++ b/crates/ruff_linter/src/rules/flake8_tidy_imports/settings.rs @@ -11,7 +11,7 @@ use ruff_macros::CacheKey; #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct ApiBan { /// The message to display when the API is used. - pub msg: String, + pub(crate) msg: String, } impl Display for ApiBan { @@ -201,7 +201,7 @@ pub struct Settings { } impl Settings { - pub fn banned_module_level_imports(&self) -> impl Iterator { + pub(crate) fn banned_module_level_imports(&self) -> impl Iterator { self.banned_module_level_imports.iter().map(AsRef::as_ref) } } diff --git a/crates/ruff_linter/src/rules/flake8_tidy_imports/snapshots/ruff_linter__rules__flake8_tidy_imports__tests__ban_all_imports.snap b/crates/ruff_linter/src/rules/flake8_tidy_imports/snapshots/ruff_linter__rules__flake8_tidy_imports__tests__ban_all_imports.snap index 4a04d49a28..57abe5915e 100644 --- a/crates/ruff_linter/src/rules/flake8_tidy_imports/snapshots/ruff_linter__rules__flake8_tidy_imports__tests__ban_all_imports.snap +++ b/crates/ruff_linter/src/rules/flake8_tidy_imports/snapshots/ruff_linter__rules__flake8_tidy_imports__tests__ban_all_imports.snap @@ -203,5 +203,4 @@ TID252 Prefer absolute imports over relative imports 27 | from .........parent import ultragrantparent 28 | from ...........................parent import ultragrantparent | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace relative imports with absolute imports diff --git a/crates/ruff_linter/src/rules/flake8_tidy_imports/snapshots/ruff_linter__rules__flake8_tidy_imports__tests__ban_parent_imports.snap b/crates/ruff_linter/src/rules/flake8_tidy_imports/snapshots/ruff_linter__rules__flake8_tidy_imports__tests__ban_parent_imports.snap index 3f085ace7c..8692306a55 100644 --- a/crates/ruff_linter/src/rules/flake8_tidy_imports/snapshots/ruff_linter__rules__flake8_tidy_imports__tests__ban_parent_imports.snap +++ b/crates/ruff_linter/src/rules/flake8_tidy_imports/snapshots/ruff_linter__rules__flake8_tidy_imports__tests__ban_parent_imports.snap @@ -154,5 +154,4 @@ TID252 Prefer absolute imports over relative imports from parent modules 27 | from .........parent import ultragrantparent 28 | from ...........................parent import ultragrantparent | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace relative imports from parent modules with absolute imports diff --git a/crates/ruff_linter/src/rules/flake8_tidy_imports/snapshots/ruff_linter__rules__flake8_tidy_imports__tests__ban_parent_imports_package.snap b/crates/ruff_linter/src/rules/flake8_tidy_imports/snapshots/ruff_linter__rules__flake8_tidy_imports__tests__ban_parent_imports_package.snap index 8d341f94aa..d608a98dca 100644 --- a/crates/ruff_linter/src/rules/flake8_tidy_imports/snapshots/ruff_linter__rules__flake8_tidy_imports__tests__ban_parent_imports_package.snap +++ b/crates/ruff_linter/src/rules/flake8_tidy_imports/snapshots/ruff_linter__rules__flake8_tidy_imports__tests__ban_parent_imports_package.snap @@ -112,7 +112,6 @@ TID252 [*] Prefer absolute imports over relative imports from parent modules 9 | from . import logger, models 10 | from ..protocol.UpperCaseModule import some_function | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace relative imports from parent modules with absolute imports | 9 | from . import logger, models diff --git a/crates/ruff_linter/src/rules/flake8_tidy_imports/snapshots/ruff_linter__rules__flake8_tidy_imports__tests__banned_api_package.snap b/crates/ruff_linter/src/rules/flake8_tidy_imports/snapshots/ruff_linter__rules__flake8_tidy_imports__tests__banned_api_package.snap index f387a1dc4a..4852b94ebf 100644 --- a/crates/ruff_linter/src/rules/flake8_tidy_imports/snapshots/ruff_linter__rules__flake8_tidy_imports__tests__banned_api_package.snap +++ b/crates/ruff_linter/src/rules/flake8_tidy_imports/snapshots/ruff_linter__rules__flake8_tidy_imports__tests__banned_api_package.snap @@ -29,4 +29,3 @@ TID251 `my_package.sublib.protocol` is banned: The protocol module is deprecated 9 | from . import logger, models 10 | from ..protocol.UpperCaseModule import some_function | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_tidy_imports/snapshots/ruff_linter__rules__flake8_tidy_imports__tests__preview_lazy_import_mismatch.snap b/crates/ruff_linter/src/rules/flake8_tidy_imports/snapshots/ruff_linter__rules__flake8_tidy_imports__tests__preview_lazy_import_mismatch.snap index 87e40f92a6..d88842a4de 100644 --- a/crates/ruff_linter/src/rules/flake8_tidy_imports/snapshots/ruff_linter__rules__flake8_tidy_imports__tests__preview_lazy_import_mismatch.snap +++ b/crates/ruff_linter/src/rules/flake8_tidy_imports/snapshots/ruff_linter__rules__flake8_tidy_imports__tests__preview_lazy_import_mismatch.snap @@ -80,7 +80,6 @@ TID254 [*] `pkg` should be imported lazily 27 | 28 | x = 1; import pkg.submodule | ^^^^^^^^^^^^^ - | help: Convert to a lazy import | 27 | diff --git a/crates/ruff_linter/src/rules/flake8_tidy_imports/snapshots/ruff_linter__rules__flake8_tidy_imports__tests__preview_lazy_import_mismatch_all.snap b/crates/ruff_linter/src/rules/flake8_tidy_imports/snapshots/ruff_linter__rules__flake8_tidy_imports__tests__preview_lazy_import_mismatch_all.snap index 907edfcd04..cbf1915ed7 100644 --- a/crates/ruff_linter/src/rules/flake8_tidy_imports/snapshots/ruff_linter__rules__flake8_tidy_imports__tests__preview_lazy_import_mismatch_all.snap +++ b/crates/ruff_linter/src/rules/flake8_tidy_imports/snapshots/ruff_linter__rules__flake8_tidy_imports__tests__preview_lazy_import_mismatch_all.snap @@ -99,7 +99,6 @@ TID254 [*] Use a `lazy` import instead of an eager import 27 | 28 | x = 1; import pkg.submodule | ^^^^^^^^^^^^^ - | help: Convert to a lazy import | 27 | 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 b46e4d44aa..8735e72a5f 100644 --- a/crates/ruff_linter/src/rules/flake8_todos/rules/todos.rs +++ b/crates/ruff_linter/src/rules/flake8_todos/rules/todos.rs @@ -351,7 +351,7 @@ fn directive_errors(context: &LintContext, directive: &TodoDirective) { } /// Checks for "static" errors in the comment: missing colon, missing author, etc. -pub(crate) fn static_errors( +fn static_errors( context: &LintContext, comment: &str, comment_range: TextRange, diff --git a/crates/ruff_linter/src/rules/flake8_todos/snapshots/ruff_linter__rules__flake8_todos__tests__invalid-todo-capitalization_TD006.py.snap b/crates/ruff_linter/src/rules/flake8_todos/snapshots/ruff_linter__rules__flake8_todos__tests__invalid-todo-capitalization_TD006.py.snap index 5dcfaeaf3b..b6e38a7191 100644 --- a/crates/ruff_linter/src/rules/flake8_todos/snapshots/ruff_linter__rules__flake8_todos__tests__invalid-todo-capitalization_TD006.py.snap +++ b/crates/ruff_linter/src/rules/flake8_todos/snapshots/ruff_linter__rules__flake8_todos__tests__invalid-todo-capitalization_TD006.py.snap @@ -43,7 +43,6 @@ TD006 [*] Invalid TODO capitalization: `todo` should be `TODO` 6 | # todo (evanrittenhouse): another invalid capitalization 7 | # foo # todo: invalid capitalization | ^^^^ - | help: Replace `todo` with `TODO` | 6 | # todo (evanrittenhouse): another invalid capitalization diff --git a/crates/ruff_linter/src/rules/flake8_todos/snapshots/ruff_linter__rules__flake8_todos__tests__invalid-todo-tag_TD001.py.snap b/crates/ruff_linter/src/rules/flake8_todos/snapshots/ruff_linter__rules__flake8_todos__tests__invalid-todo-tag_TD001.py.snap index dbfe98f34b..8479b25af9 100644 --- a/crates/ruff_linter/src/rules/flake8_todos/snapshots/ruff_linter__rules__flake8_todos__tests__invalid-todo-tag_TD001.py.snap +++ b/crates/ruff_linter/src/rules/flake8_todos/snapshots/ruff_linter__rules__flake8_todos__tests__invalid-todo-tag_TD001.py.snap @@ -28,4 +28,3 @@ TD001 Invalid TODO tag: `XXX` 8 | # FIXME (evanrittenhouse): this is not fine 9 | # foo # XXX: this isn't fine either | ^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_todos/snapshots/ruff_linter__rules__flake8_todos__tests__missing-todo-author_TD002.py.snap b/crates/ruff_linter/src/rules/flake8_todos/snapshots/ruff_linter__rules__flake8_todos__tests__missing-todo-author_TD002.py.snap index c502cfd8e0..724b65e540 100644 --- a/crates/ruff_linter/src/rules/flake8_todos/snapshots/ruff_linter__rules__flake8_todos__tests__missing-todo-author_TD002.py.snap +++ b/crates/ruff_linter/src/rules/flake8_todos/snapshots/ruff_linter__rules__flake8_todos__tests__missing-todo-author_TD002.py.snap @@ -40,4 +40,3 @@ TD002 Missing author in TODO; try: `# TODO(): ...` or `# TODO @ bool { 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 8c7a81e3f6..7c7da9b55c 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs @@ -165,10 +165,7 @@ mod tests { 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.into(), - ..settings::LinterSettings::for_rule(rule_code) - }, + &settings::LinterSettings::for_rule(rule_code).with_target_version(PythonVersion::PY39), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) @@ -644,10 +641,8 @@ mod tests { fn contents_preview(contents: &str, snapshot: &str) { let diagnostics = test_snippet( contents, - &settings::LinterSettings { - preview: settings::types::PreviewMode::Enabled, - ..settings::LinterSettings::for_rules(Linter::Flake8TypeChecking.rules()) - }, + &settings::LinterSettings::for_rules(Linter::Flake8TypeChecking.rules()) + .with_preview_mode(), ); assert_diagnostics!(snapshot, diagnostics); } diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__add_future_import__TC001-TC002-TC003_TC001-3_future.py.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__add_future_import__TC001-TC002-TC003_TC001-3_future.py.snap index 48b7904e32..2e3272a28e 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__add_future_import__TC001-TC002-TC003_TC001-3_future.py.snap +++ b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__add_future_import__TC001-TC002-TC003_TC001-3_future.py.snap @@ -55,7 +55,6 @@ TC001 [*] Move application import `.first_party` into a type-checking block 4 | 5 | from . import first_party | ^^^^^^^^^^^ - | help: Move into type-checking block | 4 | diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__add_future_import__TC001_TC001_future_present.py.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__add_future_import__TC001_TC001_future_present.py.snap index ed9a7a20d7..acc284395c 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__add_future_import__TC001_TC001_future_present.py.snap +++ b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__add_future_import__TC001_TC001_future_present.py.snap @@ -8,7 +8,6 @@ TC001 [*] Move application import `.first_party` into a type-checking block 2 | 3 | from . import first_party | ^^^^^^^^^^^ - | help: Move into type-checking block | 2 | diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__empty-type-checking-block_TC005.py.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__empty-type-checking-block_TC005.py.snap index c049165377..4a34254f48 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__empty-type-checking-block_TC005.py.snap +++ b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__empty-type-checking-block_TC005.py.snap @@ -7,7 +7,6 @@ TC005 [*] Found empty type-checking block 3 | if TYPE_CHECKING: 4 | pass # TC005 | ^^^^ - | help: Delete empty type-checking block | 2 | diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quote_runtime-import-in-type-checking-block_quote.py.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quote_runtime-import-in-type-checking-block_quote.py.snap index 14bdada482..d8b6f0a655 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quote_runtime-import-in-type-checking-block_quote.py.snap +++ b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quote_runtime-import-in-type-checking-block_quote.py.snap @@ -10,7 +10,6 @@ TC004 [*] Move import `pandas.DataFrame` out of type-checking block. Import is u 111 | 112 | x: TypeAlias = DataFrame | None | --------- Used at runtime here - | help: Move out of type-checking block | 1 + from pandas import DataFrame 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 f0998ed8e7..f63bd8a044 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 @@ -319,7 +319,6 @@ TC008 [*] Remove quotes from type alias | ___________^ 43 | | ' | None') | |_____________^ - | help: Remove quotes | 41 | | None) @@ -519,7 +518,6 @@ TC008 [*] Remove quotes from type alias 71 | r: TypeAlias = """int | None""" 72 | type R = """int | None""" | ^^^^^^^^^^^^^^^^ - | help: Remove quotes | 71 | r: TypeAlias = """int | None""" diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008_typing_execution_context.py.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008_typing_execution_context.py.snap index 5537a3256c..517f8dc7a5 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008_typing_execution_context.py.snap +++ b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008_typing_execution_context.py.snap @@ -207,7 +207,6 @@ TC008 [*] Remove quotes from type alias | _____________________^ 28 | | ' | None') | |_________________^ - | help: Remove quotes | 26 | | 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 04f568e530..36bef6dbce 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 @@ -8,7 +8,6 @@ TC006 [*] Add quotes to type expression in `typing.cast()` 3 | 4 | cast(int, 3.0) # TC006 | ^^^ - | help: Add quotes | 3 | @@ -24,7 +23,6 @@ TC006 [*] Add quotes to type expression in `typing.cast()` 9 | 10 | cast(list[tuple[bool | float | int | str]], 3.0) # TC006 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add quotes | 9 | @@ -40,7 +38,6 @@ TC006 [*] Add quotes to type expression in `typing.cast()` 15 | 16 | cast(list[tuple[Union[bool, float, int, str]]], 3.0) # TC006 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add quotes | 15 | @@ -56,7 +53,6 @@ TC006 [*] Add quotes to type expression in `typing.cast()` 39 | 40 | typecast(int, 3.0) # TC006 | ^^^ - | help: Add quotes | 39 | @@ -72,7 +68,6 @@ TC006 [*] Add quotes to type expression in `typing.cast()` 45 | 46 | typing.cast(int, 3.0) # TC006 | ^^^ - | help: Add quotes | 45 | @@ -88,7 +83,6 @@ TC006 [*] Add quotes to type expression in `typing.cast()` 51 | 52 | t.cast(t.Literal["3.0", '3'], 3.0) # TC006 | ^^^^^^^^^^^^^^^^^^^^^ - | help: Add quotes | 51 | @@ -124,7 +118,6 @@ TC006 [*] Add quotes to type expression in `typing.cast()` 67 | import typing 68 | typing.cast(M-()) | ^^^^ - | help: Add quotes | 67 | import typing @@ -140,7 +133,6 @@ TC006 [*] Add quotes to type expression in `typing.cast()` 74 | 75 | cast(Literal["A"], 'A') | ^^^^^^^^^^^^ - | help: Add quotes | 74 | @@ -156,7 +148,6 @@ TC006 [*] Add quotes to type expression in `typing.cast()` 81 | 82 | cast(list[Annotated["list['Literal[\"A\"]']", "Foo"]], ['A']) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add quotes | 81 | @@ -205,7 +196,6 @@ 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 | ^^^ - | help: Add quotes | 98 | cast(typ=int, val=3.0) # TC006 diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-cast-value_TC006_basedpython.by.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-cast-value_TC006_basedpython.by.snap index 9ad191ee95..6024220ea1 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-cast-value_TC006_basedpython.by.snap +++ b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-cast-value_TC006_basedpython.by.snap @@ -7,7 +7,6 @@ TC006 [*] Add quotes to type expression in `typing.cast()` 12 | # a real `typing.cast()` call in the same file is still flagged 13 | flagged = cast(int, val) | ^^^ - | help: Add quotes | 12 | # a real `typing.cast()` call in the same file is still flagged 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_TC004_1.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_TC004_1.py.snap index 447edd198f..c1dfe6e37f 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-import-in-type-checking-block_TC004_1.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_TC004_1.py.snap @@ -9,7 +9,6 @@ TC004 [*] Move import `datetime.datetime` out of type-checking block. Import is | ^^^^^^^^ 5 | x = datetime | -------- Used at runtime here - | help: Move out of type-checking block | 1 | from typing import TYPE_CHECKING 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_TC004_11.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_TC004_11.py.snap index 9207a53f02..89d4fe9398 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-import-in-type-checking-block_TC004_11.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_TC004_11.py.snap @@ -10,7 +10,6 @@ TC004 [*] Move import `typing.List` out of type-checking block. Import is used f 5 | 6 | __all__ = ("List",) | ------ Used at runtime here - | help: Move out of type-checking block | 1 | from typing import TYPE_CHECKING 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_TC004_12.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_TC004_12.py.snap index a0a7023c61..be666704c7 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-import-in-type-checking-block_TC004_12.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_TC004_12.py.snap @@ -10,7 +10,6 @@ TC004 [*] Move import `collections.abc.Callable` out of type-checking block. Imp 7 | 8 | AnyCallable: TypeAlias = Callable[..., Any] | -------- Used at runtime here - | help: Move out of type-checking block | 3 | from typing import Any, TYPE_CHECKING, TypeAlias 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_TC004_17.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_TC004_17.py.snap index deb7370308..6c0f3187df 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-import-in-type-checking-block_TC004_17.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_TC004_17.py.snap @@ -13,7 +13,6 @@ TC004 [*] Move import `pandas.DataFrame` out of type-checking block. Import is u 9 | def example() -> DataFrame: 10 | x = DataFrame() | --------- Used at runtime here - | help: Move out of type-checking block | 3 | from typing_extensions import TYPE_CHECKING 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_TC004_2.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_TC004_2.py.snap index 7bec54976d..709bb9b829 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-import-in-type-checking-block_TC004_2.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_TC004_2.py.snap @@ -13,7 +13,6 @@ TC004 [*] Move import `datetime.date` out of type-checking block. Import is used 7 | def example(): 8 | return date() | ---- Used at runtime here - | help: Move out of type-checking block | 1 | from typing import TYPE_CHECKING 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_quote.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_quote.py.snap index 14bdada482..d8b6f0a655 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-import-in-type-checking-block_quote.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_quote.py.snap @@ -10,7 +10,6 @@ TC004 [*] Move import `pandas.DataFrame` out of type-checking block. Import is u 111 | 112 | x: TypeAlias = DataFrame | None | --------- Used at runtime here - | help: Move out of type-checking block | 1 + from pandas import DataFrame 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_runtime_evaluated_base_classes_1.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_runtime_evaluated_base_classes_1.py.snap index e83089f22d..db0d51de8e 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-import-in-type-checking-block_runtime_evaluated_base_classes_1.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_runtime_evaluated_base_classes_1.py.snap @@ -14,7 +14,6 @@ TC004 [*] Move import `datetime` out of type-checking block. Import is used for 17 | class A(pydantic.BaseModel): 18 | x: datetime.datetime | -------- Used at runtime here - | help: Move out of type-checking block | 7 | from pydantic import BaseModel @@ -41,7 +40,6 @@ TC004 [*] Move import `array.array` out of type-checking block. Import is used f 37 | class G(BaseModel): 38 | x: array | ----- Used at runtime here - | help: Move out of type-checking block | 7 | from pydantic import BaseModel @@ -68,7 +66,6 @@ TC004 [*] Move import `pandas` out of type-checking block. Import is used for mo 21 | class B(BaseModel): 22 | x: pandas.DataFrame | ------ Used at runtime here - | help: Move out of type-checking block | 7 | from pydantic import BaseModel 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_runtime_evaluated_decorators_1.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_runtime_evaluated_decorators_1.py.snap index beeba4cb30..c02544dddf 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-import-in-type-checking-block_runtime_evaluated_decorators_1.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_runtime_evaluated_decorators_1.py.snap @@ -15,7 +15,6 @@ TC004 [*] Move import `datetime` out of type-checking block. Import is used for 20 | class A: 21 | x: datetime.datetime | -------- Used at runtime here - | help: Move out of type-checking block | 9 | import numpy @@ -43,7 +42,6 @@ TC004 [*] Move import `array.array` out of type-checking block. Import is used f 35 | class D: 36 | x: array | ----- Used at runtime here - | help: Move out of type-checking block | 9 | import numpy @@ -71,7 +69,6 @@ TC004 [*] Move import `pandas` out of type-checking block. Import is used for mo 25 | class B: 26 | x: pandas.DataFrame | ------ Used at runtime here - | help: Move out of type-checking block | 9 | import numpy 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 index db23b0be41..f1d806a07f 100644 --- 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 @@ -4,13 +4,12 @@ source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs TC004 [*] Move import `builtins` out of type-checking block. Import is used for more than type hinting. --> whitespace.py:5:26 | -3 | from typing import TYPE_CHECKING \ +3 | from typing import TYPE_CHECKING␌\ 4 | 5 | if TYPE_CHECKING: import builtins | ^^^^^^^^ 6 | builtins.print("!") | -------- Used at runtime here - | help: Move out of type-checking block | 2 | # there is a (potentially invisible) unicode formfeed character (000C) between `TYPE_CHECKING` and the backslash diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-string-union_TC010_1.py.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-string-union_TC010_1.py.snap index 559272a6ad..b2693c688b 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-string-union_TC010_1.py.snap +++ b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-string-union_TC010_1.py.snap @@ -8,4 +8,3 @@ TC010 Invalid string member in `X | Y`-style union type 17 | 18 | OldS = TypeVar('OldS', int | 'str', str) # TC010 | ^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-string-union_TC010_2.py.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-string-union_TC010_2.py.snap index 3588e7b241..78ecb96408 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-string-union_TC010_2.py.snap +++ b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-string-union_TC010_2.py.snap @@ -8,4 +8,3 @@ TC010 Invalid string member in `X | Y`-style union type 15 | 16 | OldS = TypeVar('OldS', int | 'str', str) # TC010 | ^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__strict_typing-only-standard-library-import_init_var.py.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__strict_typing-only-standard-library-import_init_var.py.snap index 8f7ea785e7..44fc3c1612 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__strict_typing-only-standard-library-import_init_var.py.snap +++ b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__strict_typing-only-standard-library-import_init_var.py.snap @@ -30,7 +30,6 @@ TC003 [*] Move standard library import `pathlib.Path` into a type-checking block 5 | from dataclasses import FrozenInstanceError, InitVar, dataclass 6 | from pathlib import Path | ^^^^ - | help: Move into type-checking block | 5 | from dataclasses import FrozenInstanceError, InitVar, dataclass diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__strict_typing-only-standard-library-import_kw_only.py.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__strict_typing-only-standard-library-import_kw_only.py.snap index 941e23f2e5..9769c79b31 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__strict_typing-only-standard-library-import_kw_only.py.snap +++ b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__strict_typing-only-standard-library-import_kw_only.py.snap @@ -8,7 +8,6 @@ TC003 [*] Move standard library import `dataclasses.Field` into a type-checking 4 | 5 | from dataclasses import KW_ONLY, dataclass, Field | ^^^^^ - | help: Move into type-checking block | 4 | diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__tc004_precedence_over_tc007.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__tc004_precedence_over_tc007.snap index 1c926868a7..566470801b 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__tc004_precedence_over_tc007.snap +++ b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__tc004_precedence_over_tc007.snap @@ -11,7 +11,6 @@ TC004 [*] Move import `foo.Foo` out of type-checking block. Import is used for m 7 | 8 | a: TypeAlias = Foo | None # OK | --- Used at runtime here - | help: Move out of type-checking block | 4 | from typing import TYPE_CHECKING, TypeAlias diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__tc010_precedence_over_tc008.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__tc010_precedence_over_tc008.snap index da622ad403..55c4cdebc4 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__tc010_precedence_over_tc008.snap +++ b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__tc010_precedence_over_tc008.snap @@ -24,4 +24,3 @@ TC010 Invalid string member in `X | Y`-style union type 6 | a: TypeAlias = 'int | None' # TC008 7 | b: TypeAlias = 'int' | None # TC010 | ^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__typing-only-standard-library-import_init_var.py.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__typing-only-standard-library-import_init_var.py.snap index 2be302e254..6309149d40 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__typing-only-standard-library-import_init_var.py.snap +++ b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__typing-only-standard-library-import_init_var.py.snap @@ -7,7 +7,6 @@ TC003 [*] Move standard library import `pathlib.Path` into a type-checking block 5 | from dataclasses import FrozenInstanceError, InitVar, dataclass 6 | from pathlib import Path | ^^^^ - | help: Move into type-checking block | 5 | from dataclasses import FrozenInstanceError, InitVar, dataclass diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__typing-only-standard-library-import_module__undefined.py.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__typing-only-standard-library-import_module__undefined.py.snap index f1379bd860..b3c19763e7 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__typing-only-standard-library-import_module__undefined.py.snap +++ b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__typing-only-standard-library-import_module__undefined.py.snap @@ -8,7 +8,6 @@ TC003 [*] Move standard library import `collections.abc.Sequence` into a type-ch 2 | 3 | from collections.abc import Sequence | ^^^^^^^^ - | help: Move into type-checking block | 2 | diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__typing-only-third-party-import_runtime_evaluated_decorators_2.py.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__typing-only-third-party-import_runtime_evaluated_decorators_2.py.snap index b857ebb414..4365a7675d 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__typing-only-third-party-import_runtime_evaluated_decorators_2.py.snap +++ b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__typing-only-third-party-import_runtime_evaluated_decorators_2.py.snap @@ -8,7 +8,6 @@ TC002 [*] Move third-party import `numpy` into a type-checking block 9 | 10 | import numpy # TC002 | ^^^^^ - | help: Move into type-checking block | 9 | diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/helpers.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/helpers.rs index 66ba774400..46537dcd2d 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/helpers.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/helpers.rs @@ -103,7 +103,7 @@ pub(crate) fn check_os_pathlib_single_arg_calls( }); } -pub(crate) fn get_name_expr(expr: &Expr) -> Option<&ast::ExprName> { +fn get_name_expr(expr: &Expr) -> Option<&ast::ExprName> { match expr { Expr::Name(name) => Some(name), Expr::Call(ExprCall { func, .. }) => get_name_expr(func), 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 5d0938d5c6..23c0fd4498 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs @@ -93,14 +93,10 @@ mod tests { assert_diagnostics_diff!( snapshot, Path::new("flake8_use_pathlib").join(path).as_path(), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY313.into(), - ..settings::LinterSettings::for_rule(rule_code) - }, - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY314.into(), - ..settings::LinterSettings::for_rule(rule_code) - }, + &settings::LinterSettings::for_rule(rule_code) + .with_target_version(PythonVersion::PY313), + &settings::LinterSettings::for_rule(rule_code) + .with_target_version(PythonVersion::PY314), ); Ok(()) } @@ -162,10 +158,7 @@ mod tests { ); let diagnostics = test_path( Path::new("flake8_use_pathlib").join(path).as_path(), - &settings::LinterSettings { - preview: PreviewMode::Enabled, - ..settings::LinterSettings::for_rule(rule_code) - }, + &settings::LinterSettings::for_rule(rule_code).with_preview_mode(), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) @@ -180,10 +173,8 @@ mod tests { ); let diagnostics = test_path( Path::new("flake8_use_pathlib").join(path).as_path(), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY314.into(), - ..settings::LinterSettings::for_rule(rule_code) - }, + &settings::LinterSettings::for_rule(rule_code) + .with_target_version(PythonVersion::PY314), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) 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 fc66c33855..2c3fa0707d 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 @@ -123,7 +123,7 @@ pub(crate) fn invalid_pathlib_with_suffix(checker: &Checker, call: &ast::ExprCal } let mut diagnostic = - checker.report_diagnostic(InvalidPathlibWithSuffix { single_dot }, call.range); + 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( diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH124_py_path_1.py.snap b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH124_py_path_1.py.snap index 69808fd0ed..3739af48ae 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH124_py_path_1.py.snap +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH124_py_path_1.py.snap @@ -8,4 +8,3 @@ PTH124 `py.path` is in maintenance mode, use `pathlib` instead 2 | 3 | p = py.path.local("../foo") | ^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH124_py_path_2.py.snap b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH124_py_path_2.py.snap index 3b73893a1f..0950c633dd 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH124_py_path_2.py.snap +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH124_py_path_2.py.snap @@ -8,4 +8,3 @@ PTH124 `py.path` is in maintenance mode, use `pathlib` instead 2 | 3 | p = path("/foo") | ^^^^ - | 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 b7175ee1cd..8aeccc3405 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 @@ -246,7 +246,6 @@ PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` 35 | getsize(filename1) 36 | getsize(filename2) | ^^^^^^^ - | help: Replace with `Path(...).stat().st_size` PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` @@ -361,5 +360,4 @@ PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` 81 | 82 | os.path.getsize(pathlib.Path("filename")) | ^^^^^^^^^^^^^^^ - | 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 index 5b4fade357..c5dd82e6be 100644 --- 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 @@ -30,5 +30,4 @@ PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` 4 | os.path.getsize(filename=b"filename") 5 | os.path.getsize(filename=__file__) | ^^^^^^^^^^^^^^^ - | 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 2a3663c845..ca9731a00c 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 @@ -30,7 +30,6 @@ PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime` 6 | os.path.getatime(b"filename") 7 | os.path.getatime(Path("filename")) | ^^^^^^^^^^^^^^^^ - | help: Replace with `Path.stat(...).st_atime` PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime` @@ -60,7 +59,6 @@ PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime` 11 | getatime(b"filename") 12 | getatime(Path("filename")) | ^^^^^^^^ - | help: Replace with `Path.stat(...).st_atime` PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime` @@ -152,5 +150,4 @@ PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime` 34 | 35 | getatime(Path("dir") / "file.txt") | ^^^^^^^^ - | 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 10b08dfdb3..4632c8b424 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 @@ -28,7 +28,6 @@ PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_mtime` 7 | os.path.getmtime(b"filename") 8 | os.path.getmtime(Path("filename")) | ^^^^^^^^^^^^^^^^ - | help: Replace with `Path.stat(...).st_mtime` PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_mtime` @@ -58,5 +57,4 @@ PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_mtime` 12 | getmtime(b"filename") 13 | getmtime(Path("filename")) | ^^^^^^^^ - | 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 65ae0f7219..59184a8260 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 @@ -62,5 +62,4 @@ PTH205 `os.path.getctime` should be replaced by `Path.stat().st_ctime` 11 | getctime(b"filename") 12 | getctime(Path("filename")) | ^^^^^^^^ - | 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__PTH208_PTH208.py.snap b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH208_PTH208.py.snap index b35f942060..9aeb2ced0d 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH208_PTH208.py.snap +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH208_PTH208.py.snap @@ -37,7 +37,6 @@ PTH208 Use `pathlib.Path.iterdir()` instead. 9 | bytes_path = b'.' 10 | os.listdir(bytes_path) | ^^^^^^^^^^ - | PTH208 Use `pathlib.Path.iterdir()` instead. --> PTH208.py:16:1 @@ -45,7 +44,6 @@ PTH208 Use `pathlib.Path.iterdir()` instead. 15 | path_path = Path('.') 16 | os.listdir(path_path) | ^^^^^^^^^^ - | PTH208 Use `pathlib.Path.iterdir()` instead. --> PTH208.py:19:4 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 index cd46c1b704..2b2d8e98dc 100644 --- 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 @@ -94,5 +94,4 @@ PTH211 `os.symlink` should be replaced by `Path.symlink_to` 22 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory= True ) 23 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory="nonboolean") | ^^^^^^^^^^ - | help: Replace with `Path(...).symlink_to(...)` 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 e818dc83d8..bc4e59683c 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 @@ -635,5 +635,4 @@ PTH101 `os.chmod()` should be replaced by `Path.chmod()` 186 | os.chmod(_AttrHolder.fd, 0o644) # Suppressed: resolved as `int` 187 | os.chmod(_AttrHolder.name, 0o644) # Diagnostic + fix: resolved as `str` | ^^^^^^^^ - | help: Replace with `Path(...).chmod(...)` 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 f2b4eeff44..89653f3915 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 @@ -290,4 +290,3 @@ PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, an 30 | foo_p.samefile(p) 31 | foo_p.splitext(p) | ^^^^^^^^^^^^^^ - | 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 60da4269a6..3482761ca3 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 @@ -314,7 +314,6 @@ PTH123 `open()` should be replaced by `Path.open()` 35 | fp.read() 36 | open(p).close() | ^^^^ - | help: Replace with `Path.open()` PTH123 `open()` should be replaced by `Path.open()` @@ -324,7 +323,6 @@ PTH123 `open()` should be replaced by `Path.open()` 42 | 43 | with open(p) as _: ... # Error | ^^^^ - | help: Replace with `Path.open()` PTH104 `os.rename()` should be replaced by `Path.rename()` 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 1499204295..224ed57fba 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 @@ -290,4 +290,3 @@ PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, an 37 | xsamefile(p) 38 | xsplitext(p) | ^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview__PTH201_PTH201.py.snap b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview__PTH201_PTH201.py.snap index 24b0d2fe5f..5af0ac59be 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview__PTH201_PTH201.py.snap +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview__PTH201_PTH201.py.snap @@ -366,7 +366,6 @@ PTH201 [*] Do not pass the current directory explicitly to `Path` 77 | _ = PureWindowsPath(".") 78 | _ = PackagePath(".") | ^^^ - | help: Remove the current directory argument | 77 | _ = PureWindowsPath(".") 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 index 03e14cc83b..1a4120aea6 100644 --- 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 @@ -372,7 +372,6 @@ PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` 35 | getsize(filename1) 36 | getsize(filename2) | ^^^^^^^ - | help: Replace with `Path(...).stat().st_size` | 35 | getsize(filename1) @@ -577,7 +576,6 @@ PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` 81 | 82 | os.path.getsize(pathlib.Path("filename")) | ^^^^^^^^^^^^^^^ - | help: Replace with `Path(...).stat().st_size` | 81 | 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 index 9da2fd4bcd..1d644eb58a 100644 --- 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 @@ -47,7 +47,6 @@ PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` 4 | os.path.getsize(filename=b"filename") 5 | os.path.getsize(filename=__file__) | ^^^^^^^^^^^^^^^ - | help: Replace with `Path(...).stat().st_size` | 1 | import os 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 index 035dc33fe8..8e855df1b5 100644 --- 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 @@ -42,7 +42,6 @@ PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime` 6 | os.path.getatime(b"filename") 7 | os.path.getatime(Path("filename")) | ^^^^^^^^^^^^^^^^ - | help: Replace with `Path.stat(...).st_atime` | 6 | os.path.getatime(b"filename") @@ -90,7 +89,6 @@ PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime` 11 | getatime(b"filename") 12 | getatime(Path("filename")) | ^^^^^^^^ - | help: Replace with `Path.stat(...).st_atime` | 11 | getatime(b"filename") @@ -237,7 +235,6 @@ PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime` 34 | 35 | getatime(Path("dir") / "file.txt") | ^^^^^^^^ - | help: Replace with `Path.stat(...).st_atime` | 34 | 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 index 835040a92f..58cdff1799 100644 --- 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 @@ -40,7 +40,6 @@ PTH204 [*] `os.path.getmtime` should be replaced by `Path.stat().st_mtime` 7 | os.path.getmtime(b"filename") 8 | os.path.getmtime(Path("filename")) | ^^^^^^^^^^^^^^^^ - | help: Replace with `Path.stat(...).st_mtime` | 7 | os.path.getmtime(b"filename") @@ -88,7 +87,6 @@ PTH204 [*] `os.path.getmtime` should be replaced by `Path.stat().st_mtime` 12 | getmtime(b"filename") 13 | getmtime(Path("filename")) | ^^^^^^^^ - | help: Replace with `Path.stat(...).st_mtime` | 12 | getmtime(b"filename") 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 index 6ba2cf7ca7..1a124d5b1b 100644 --- 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 @@ -92,7 +92,6 @@ PTH205 [*] `os.path.getctime` should be replaced by `Path.stat().st_ctime` 11 | getctime(b"filename") 12 | getctime(Path("filename")) | ^^^^^^^^ - | help: Replace with `Path.stat(...).st_ctime` | 11 | getctime(b"filename") 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 index c2a5e81582..968f293563 100644 --- 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 @@ -1008,7 +1008,6 @@ PTH101 [*] `os.chmod()` should be replaced by `Path.chmod()` 186 | os.chmod(_AttrHolder.fd, 0o644) # Suppressed: resolved as `int` 187 | os.chmod(_AttrHolder.name, 0o644) # Diagnostic + fix: resolved as `str` | ^^^^^^^^ - | help: Replace with `Path(...).chmod(...)` | 142 | import sys 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 index 8a9b44e4f8..327e1a1bc0 100644 --- 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 @@ -445,4 +445,3 @@ PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, an 30 | foo_p.samefile(p) 31 | foo_p.splitext(p) | ^^^^^^^^^^^^^^ - | 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 index 9465129e50..0f889292ca 100644 --- 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 @@ -479,7 +479,6 @@ PTH123 [*] `open()` should be replaced by `Path.open()` 35 | fp.read() 36 | open(p).close() | ^^^^ - | help: Replace with `Path.open()` | 4 | from os.path import isabs, join, basename, dirname, samefile, splitext @@ -499,7 +498,6 @@ PTH123 [*] `open()` should be replaced by `Path.open()` 42 | 43 | with open(p) as _: ... # Error | ^^^^ - | help: Replace with `Path.open()` | 4 | from os.path import isabs, join, basename, dirname, samefile, splitext 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 index bf49451513..2e5f5bcf1a 100644 --- 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 @@ -445,4 +445,3 @@ PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, an 37 | xsamefile(p) 38 | xsplitext(p) | ^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flynt/helpers.rs b/crates/ruff_linter/src/rules/flynt/helpers.rs index d3c5168609..62ada435b9 100644 --- a/crates/ruff_linter/src/rules/flynt/helpers.rs +++ b/crates/ruff_linter/src/rules/flynt/helpers.rs @@ -35,7 +35,7 @@ fn is_simple_call(expr: &Expr) -> bool { range: _, node_index: _, }, - range: _, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, diff --git a/crates/ruff_linter/src/rules/isort/categorize.rs b/crates/ruff_linter/src/rules/isort/categorize.rs index d7d036608f..203a375695 100644 --- a/crates/ruff_linter/src/rules/isort/categorize.rs +++ b/crates/ruff_linter/src/rules/isort/categorize.rs @@ -410,20 +410,6 @@ impl KnownModules { }; Some((section, reason)) } - - /// Return the list of user-defined modules, indexed by section. - pub fn user_defined(&self) -> FxHashMap<&str, Vec<&IdentifierPattern>> { - let mut user_defined: FxHashMap<&str, Vec<&IdentifierPattern>> = FxHashMap::default(); - for (module, section) in &self.known { - if let ImportSection::UserDefined(section_name) = section { - user_defined - .entry(section_name.as_str()) - .or_default() - .push(module); - } - } - user_defined - } } impl fmt::Display for KnownModules { diff --git a/crates/ruff_linter/src/rules/isort/mod.rs b/crates/ruff_linter/src/rules/isort/mod.rs index 0bec9a3308..497aa2822b 100644 --- a/crates/ruff_linter/src/rules/isort/mod.rs +++ b/crates/ruff_linter/src/rules/isort/mod.rs @@ -38,11 +38,11 @@ mod types; #[derive(Debug)] pub(crate) struct AnnotatedAliasData<'a> { - pub(crate) name: &'a str, - pub(crate) asname: Option<&'a str>, - pub(crate) atop: Vec>, - pub(crate) inline: Vec>, - pub(crate) trailing: Vec>, + name: &'a str, + asname: Option<&'a str>, + atop: Vec>, + inline: Vec>, + trailing: Vec>, } #[derive(Debug)] diff --git a/crates/ruff_linter/src/rules/isort/order.rs b/crates/ruff_linter/src/rules/isort/order.rs index 40b74662bb..8d6be203bf 100644 --- a/crates/ruff_linter/src/rules/isort/order.rs +++ b/crates/ruff_linter/src/rules/isort/order.rs @@ -14,47 +14,46 @@ pub(crate) fn order_imports<'a>( ) -> Vec> { let straight_imports = block.import.into_iter(); - let from_imports = - // Include all non-re-exports. - block - .import_from - .into_iter() - .chain( - // Include all re-exports. - block - .import_from_as - .into_iter() - .map(|((import_from, ..), body)| (import_from, body)), - ) - .chain( - // Include all star imports. - block.import_from_star, - ) - .map( - |( - import_from, - ImportFromStatement { - first_index, - comments, - aliases, - trailing_comma, - }, - )| { - // Within each `Stmt::ImportFrom`, sort the members. - ( - import_from, - first_index.unwrap_or_default(), - comments, - trailing_comma, - aliases - .into_iter() - .sorted_by_cached_key(|(alias, _)| { - MemberKey::from_member(alias.name, alias.asname, settings) - }) - .collect::>(), - ) + // Include all non-re-exports. + let from_imports = block + .import_from + .into_iter() + .chain( + // Include all re-exports. + block + .import_from_as + .into_iter() + .map(|((import_from, ..), body)| (import_from, body)), + ) + .chain( + // Include all star imports. + block.import_from_star, + ) + .map( + |( + import_from, + ImportFromStatement { + first_index, + comments, + aliases, + trailing_comma, }, - ); + )| { + // Within each `Stmt::ImportFrom`, sort the members. + ( + import_from, + first_index.unwrap_or_default(), + comments, + trailing_comma, + aliases + .into_iter() + .sorted_by_cached_key(|(alias, _)| { + MemberKey::from_member(alias.name, alias.asname, settings) + }) + .collect::>(), + ) + }, + ); if matches!(section, ImportSection::Known(ImportType::Future)) { let ordered_from_imports = from_imports diff --git a/crates/ruff_linter/src/rules/isort/settings.rs b/crates/ruff_linter/src/rules/isort/settings.rs index ced2dfdcb7..b3ce23653f 100644 --- a/crates/ruff_linter/src/rules/isort/settings.rs +++ b/crates/ruff_linter/src/rules/isort/settings.rs @@ -73,13 +73,13 @@ pub struct Settings { } impl Settings { - pub fn requires_module_import(&self, name: String, as_name: Option) -> bool { + pub(crate) fn requires_module_import(&self, name: String, as_name: Option) -> bool { self.required_imports .contains(&NameImport::Import(ModuleNameImport { name: Alias { name, as_name }, })) } - pub fn requires_member_import( + pub(crate) fn requires_member_import( &self, module: Option, name: String, diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__1_separate_subpackage_first_and_third_party_imports.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__1_separate_subpackage_first_and_third_party_imports.py.snap index 474bc9ff24..4af5fa8f98 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__1_separate_subpackage_first_and_third_party_imports.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__1_separate_subpackage_first_and_third_party_imports.py.snap @@ -13,7 +13,6 @@ I001 [*] Import block is un-sorted or un-formatted 7 | | import foo.bar 8 | | import foo.bar.baz | |__________________^ - | help: Organize imports | 1 | import sys diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__2_separate_subpackage_first_and_third_party_imports.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__2_separate_subpackage_first_and_third_party_imports.py.snap index c4740a9f25..887980a2bd 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__2_separate_subpackage_first_and_third_party_imports.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__2_separate_subpackage_first_and_third_party_imports.py.snap @@ -13,7 +13,6 @@ I001 [*] Import block is un-sorted or un-formatted 7 | | import foo.bar 8 | | import foo.bar.baz | |__________________^ - | help: Organize imports | 1 | import sys diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__add_newline_before_comments.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__add_newline_before_comments.py.snap index 26ecb23ee6..555d6ccc20 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__add_newline_before_comments.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__add_newline_before_comments.py.snap @@ -12,7 +12,6 @@ I001 [*] Import block is un-sorted or un-formatted 6 | | # before it. 7 | | import leading_prefix | |_____________________^ - | help: Organize imports | 1 | import os diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__as_imports_comments.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__as_imports_comments.py.snap index 1c5b54e61c..1db22a4a0e 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__as_imports_comments.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__as_imports_comments.py.snap @@ -20,7 +20,6 @@ I001 [*] Import block is un-sorted or un-formatted 14 | | Member # Comment on `Member` 15 | | ) | |_^ - | help: Organize imports | - from foo import ( # Comment on `foo` diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__bom_unsorted.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__bom_unsorted.py.snap index 55511be6a8..181eb565f8 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__bom_unsorted.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__bom_unsorted.py.snap @@ -7,7 +7,6 @@ I001 [*] Import block is un-sorted or un-formatted 1 | / import foo 2 | | import bar | |__________^ - | help: Organize imports | - import foo diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__case_sensitive_case_sensitive.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__case_sensitive_case_sensitive.py.snap index 3604920fb1..d84c4838e7 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__case_sensitive_case_sensitive.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__case_sensitive_case_sensitive.py.snap @@ -14,7 +14,6 @@ I001 [*] Import block is un-sorted or un-formatted 8 | | from g import a, B, c 9 | | from h import A, b, C | |_____________________^ - | help: Organize imports | 2 | import B diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__closest_to_furthest_relative_imports_order.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__closest_to_furthest_relative_imports_order.py.snap index bf0ca0eb2d..a77cb654ef 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__closest_to_furthest_relative_imports_order.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__closest_to_furthest_relative_imports_order.py.snap @@ -8,7 +8,6 @@ I001 [*] Import block is un-sorted or un-formatted 2 | | from .. import b 3 | | from . import c | |_______________^ - | help: Organize imports | 1 + from . import c diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__combine_as_imports.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__combine_as_imports.py.snap index c1931d0594..e43c9f990b 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__combine_as_imports.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__combine_as_imports.py.snap @@ -9,7 +9,6 @@ I001 [*] Import block is un-sorted or un-formatted 3 | | from module import function 4 | | from module import function as f | |________________________________^ - | help: Organize imports | 1 + from module import CONSTANT, function diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__combine_as_imports_combine_as_imports.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__combine_as_imports_combine_as_imports.py.snap index c4f6721d45..9fcbbe57f2 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__combine_as_imports_combine_as_imports.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__combine_as_imports_combine_as_imports.py.snap @@ -9,7 +9,6 @@ I001 [*] Import block is un-sorted or un-formatted 3 | | from module import function 4 | | from module import function as f | |________________________________^ - | help: Organize imports | - from module import Class as C diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__combine_import_from.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__combine_import_from.py.snap index 8305cfb8dc..6350201903 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__combine_import_from.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__combine_import_from.py.snap @@ -10,7 +10,6 @@ I001 [*] Import block is un-sorted or un-formatted 4 | | from collections import ChainMap 5 | | from collections import MutableSequence, MutableMapping | |_______________________________________________________^ - | help: Organize imports | - from collections import Awaitable diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__comments.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__comments.py.snap index c832d00d8c..0927db196b 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__comments.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__comments.py.snap @@ -38,7 +38,6 @@ I001 [*] Import block is un-sorted or un-formatted 32 | | from F import a # Comment 1 33 | | from F import b | |_______________^ - | help: Organize imports | 2 | # Comment 2 diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__deduplicate_imports.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__deduplicate_imports.py.snap index 188dcdb1df..f90f628d5c 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__deduplicate_imports.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__deduplicate_imports.py.snap @@ -9,7 +9,6 @@ I001 [*] Import block is un-sorted or un-formatted 3 | | import os as os1 4 | | import os as os2 | |________________^ - | help: Organize imports | 1 | import os diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__detect_same_package.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__detect_same_package.snap index 2847791bc7..2baad8d7ca 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__detect_same_package.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__detect_same_package.snap @@ -8,7 +8,6 @@ I001 [*] Import block is un-sorted or un-formatted 2 | | import pandas 3 | | import foo.baz | |______________^ - | help: Organize imports | 1 | import os diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__export_imports_basedpython.by.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__export_imports_basedpython.by.snap index 8d240cc4a6..447e45548e 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__export_imports_basedpython.by.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__export_imports_basedpython.by.snap @@ -18,7 +18,6 @@ I001 [*] Import block is un-sorted or un-formatted 12 | | ceil, 13 | | ) | |_^ - | help: Organize imports | - from b export z diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__fit_line_length.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__fit_line_length.py.snap index 2365b71c77..6135ea2c03 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__fit_line_length.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__fit_line_length.py.snap @@ -14,7 +14,6 @@ I001 [*] Import block is un-sorted or un-formatted 13 | | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, 14 | | ) | |_____^ - | help: Organize imports | 7 | from line_with_88 import aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__fit_line_length_comment.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__fit_line_length_comment.py.snap index b416777b81..0be10899f8 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__fit_line_length_comment.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__fit_line_length_comment.py.snap @@ -13,7 +13,6 @@ I001 [*] Import block is un-sorted or un-formatted 7 | | # The next import doesn't fit on one line. 8 | | from h import i # 012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9012ß9💣2ℝ9 | |_______________^ - | help: Organize imports | 1 | import a diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_single_line_force_single_line.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_single_line_force_single_line.py.snap index 1fb1a84062..c48cd441fd 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_single_line_force_single_line.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_single_line_force_single_line.py.snap @@ -32,7 +32,6 @@ I001 [*] Import block is un-sorted or un-formatted 26 | | # comment 9 27 | | from baz import * # comment 10 | |_________________^ - | help: Organize imports | - import sys, math diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_sort_within_sections.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_sort_within_sections.py.snap index 7cf19fe93f..b7f693ac66 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_sort_within_sections.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_sort_within_sections.py.snap @@ -18,7 +18,6 @@ I001 [*] Import block is un-sorted or un-formatted 12 | | from .my.nested import fn2 13 | | from ...grandparent import fn3 | |______________________________^ - | help: Organize imports | - from a import a1 # import_from diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_sort_within_sections_force_sort_within_sections.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_sort_within_sections_force_sort_within_sections.py.snap index 14092c9912..13c4640dcb 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_sort_within_sections_force_sort_within_sections.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_sort_within_sections_force_sort_within_sections.py.snap @@ -18,7 +18,6 @@ I001 [*] Import block is un-sorted or un-formatted 12 | | from .my.nested import fn2 13 | | from ...grandparent import fn3 | |______________________________^ - | help: Organize imports | - from a import a1 # import_from diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_sort_within_sections_force_sort_within_sections_future.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_sort_within_sections_force_sort_within_sections_future.py.snap index ed36520a79..2f8f2c71ba 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_sort_within_sections_force_sort_within_sections_future.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_sort_within_sections_force_sort_within_sections_future.py.snap @@ -7,7 +7,6 @@ I001 [*] Import block is un-sorted or un-formatted 1 | / import __future__ 2 | | from __future__ import annotations | |__________________________________^ - | help: Organize imports | 1 + from __future__ import annotations diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_sort_within_sections_force_sort_within_sections_with_as_names.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_sort_within_sections_force_sort_within_sections_with_as_names.py.snap index d6c305f186..046d53ad5d 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_sort_within_sections_force_sort_within_sections_with_as_names.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_sort_within_sections_force_sort_within_sections_with_as_names.py.snap @@ -10,7 +10,6 @@ I001 [*] Import block is un-sorted or un-formatted 4 | | import datetime as dt 5 | | import datetime | |_______________^ - | help: Organize imports | 1 + import datetime diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_sort_within_sections_lazy_force_sort_within_sections.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_sort_within_sections_lazy_force_sort_within_sections.py.snap index 74ed4fb0a0..87eb58ef31 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_sort_within_sections_lazy_force_sort_within_sections.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_sort_within_sections_lazy_force_sort_within_sections.py.snap @@ -9,7 +9,6 @@ I001 [*] Import block is un-sorted or un-formatted 3 | | lazy import os 4 | | import os | |_________^ - | help: Organize imports | 1 + from math import pi diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_to_top.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_to_top.py.snap index 0e5054e367..b226f8936c 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_to_top.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_to_top.py.snap @@ -28,7 +28,6 @@ I001 [*] Import block is un-sorted or un-formatted 22 | | from lib3.lib4 import foo 23 | | from lib3.lib4.lib5 import foo | |______________________________^ - | help: Organize imports | - import lib6 diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_to_top_force_to_top.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_to_top_force_to_top.py.snap index 211d009b0b..47564887ea 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_to_top_force_to_top.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_to_top_force_to_top.py.snap @@ -28,7 +28,6 @@ I001 [*] Import block is un-sorted or un-formatted 22 | | from lib3.lib4 import foo 23 | | from lib3.lib4.lib5 import foo | |______________________________^ - | help: Organize imports | - import lib6 diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_wrap_aliases.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_wrap_aliases.py.snap index a614900e48..ad36f643ac 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_wrap_aliases.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_wrap_aliases.py.snap @@ -8,7 +8,6 @@ I001 [*] Import block is un-sorted or un-formatted 2 | | from .b import b1 as b1 3 | | from .c import c1 | |_________________^ - | help: Organize imports | - from .a import a1 as a1, a2 as a2 diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_wrap_aliases_force_wrap_aliases.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_wrap_aliases_force_wrap_aliases.py.snap index db642eb36c..55e2ef2493 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_wrap_aliases_force_wrap_aliases.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__force_wrap_aliases_force_wrap_aliases.py.snap @@ -8,7 +8,6 @@ I001 [*] Import block is un-sorted or un-formatted 2 | | from .b import b1 as b1 3 | | from .c import c1 | |_________________^ - | help: Organize imports | - from .a import a1 as a1, a2 as a2 diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__forced_separate.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__forced_separate.py.snap index a8f4bde79e..1f5f81af22 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__forced_separate.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__forced_separate.py.snap @@ -13,7 +13,6 @@ I001 [*] Import block is un-sorted or un-formatted 7 | | from experiments.weird import varieties 8 | | from office_helper.assistants import entity_registry as er | |__________________________________________________________^ - | help: Organize imports | 2 | # but we want tests and experiments to be separated, in that order diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__from_first_lazy_from_first.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__from_first_lazy_from_first.py.snap index 7dae3bb6f6..f35e5a369a 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__from_first_lazy_from_first.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__from_first_lazy_from_first.py.snap @@ -9,7 +9,6 @@ I001 [*] Import block is un-sorted or un-formatted 3 | | lazy import os 4 | | import os | |_________^ - | help: Organize imports | 1 + from math import pi diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__future_from.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__future_from.py.snap index dc1d3a0f3c..782750f0b4 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__future_from.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__future_from.py.snap @@ -7,7 +7,6 @@ I001 [*] Import block is un-sorted or un-formatted 1 | / import __future__ 2 | | from __future__ import annotations | |__________________________________^ - | help: Organize imports | 1 + from __future__ import annotations diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__glob_1_separate_subpackage_first_and_third_party_imports.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__glob_1_separate_subpackage_first_and_third_party_imports.py.snap index 474bc9ff24..4af5fa8f98 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__glob_1_separate_subpackage_first_and_third_party_imports.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__glob_1_separate_subpackage_first_and_third_party_imports.py.snap @@ -13,7 +13,6 @@ I001 [*] Import block is un-sorted or un-formatted 7 | | import foo.bar 8 | | import foo.bar.baz | |__________________^ - | help: Organize imports | 1 | import sys diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__if_elif_else.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__if_elif_else.py.snap index cdd6062828..ed5a1d04ba 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__if_elif_else.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__if_elif_else.py.snap @@ -9,7 +9,6 @@ I001 [*] Import block is un-sorted or un-formatted 6 | / from setuptools.command.sdist import sdist as _sdist 7 | | from distutils.command.sdist import sdist as _sdist | |_______________________________________________________^ - | help: Organize imports | 5 | else: diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_from_after_import.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_from_after_import.py.snap index 0791a31b53..1b72f5f7a5 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_from_after_import.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_from_after_import.py.snap @@ -7,7 +7,6 @@ I001 [*] Import block is un-sorted or un-formatted 1 | / from collections import Collection 2 | | import os | |_________^ - | help: Organize imports | 1 + import os diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_force_sort_within_sections_import_heading_force_sort_within_sections.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_force_sort_within_sections_import_heading_force_sort_within_sections.py.snap index 38f51764de..6319a48b69 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_force_sort_within_sections_import_heading_force_sort_within_sections.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_force_sort_within_sections_import_heading_force_sort_within_sections.py.snap @@ -10,7 +10,6 @@ I001 [*] Import block is un-sorted or un-formatted 4 | | import requests 5 | | import pandas | |_____________^ - | help: Organize imports | 1 + # Future imports diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_import_heading.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_import_heading.py.snap index 1347d28216..97506d845c 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_import_heading.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_import_heading.py.snap @@ -16,7 +16,6 @@ I001 [*] Import block is un-sorted or un-formatted 10 | | 11 | | from . import my_local_folder_object | |____________________________________^ - | help: Organize imports | 1 + # Future imports diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_import_heading_already_present.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_import_heading_already_present.py.snap index 489f917f6f..9c98987e91 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_import_heading_already_present.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_import_heading_already_present.py.snap @@ -20,7 +20,6 @@ I001 [*] Import block is un-sorted or un-formatted 14 | | 15 | | from . import my_local_folder_object | |____________________________________^ - | help: Organize imports | 8 | # Third party imports diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_import_heading_duplicate.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_import_heading_duplicate.py.snap index 2330bd1d34..5011e2658c 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_import_heading_duplicate.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_import_heading_duplicate.py.snap @@ -12,7 +12,6 @@ I001 [*] Import block is un-sorted or un-formatted 6 | | import requests 7 | | import pandas | |_____________^ - | help: Organize imports | 1 | # Standard library imports diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_import_heading_unsorted.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_import_heading_unsorted.py.snap index 28ebdda43a..f8eca59df0 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_import_heading_unsorted.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_import_heading_unsorted.py.snap @@ -12,7 +12,6 @@ I001 [*] Import block is un-sorted or un-formatted 6 | | from my_first_party import my_first_party_object 7 | | from . import my_local_folder_object | |____________________________________^ - | help: Organize imports | - import pandas diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_partial_import_heading_partial.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_partial_import_heading_partial.py.snap index 56235ae6e8..147944e848 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_partial_import_heading_partial.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_partial_import_heading_partial.py.snap @@ -10,7 +10,6 @@ I001 [*] Import block is un-sorted or un-formatted 4 | | import requests 5 | | import pandas | |_____________^ - | help: Organize imports | 1 + # Standard library imports diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_single_section_import_heading_single_section.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_single_section_import_heading_single_section.py.snap index 2f3e5ff007..5be1e45c00 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_single_section_import_heading_single_section.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_single_section_import_heading_single_section.py.snap @@ -7,7 +7,6 @@ I001 [*] Import block is un-sorted or un-formatted 1 | / import requests 2 | | import pandas | |_____________^ - | help: Organize imports | - import requests diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_with_no_lines_before_import_heading_with_no_lines_before.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_with_no_lines_before_import_heading_with_no_lines_before.py.snap index 002504462d..cdf84e9b59 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_with_no_lines_before_import_heading_with_no_lines_before.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_with_no_lines_before_import_heading_with_no_lines_before.py.snap @@ -16,7 +16,6 @@ I001 [*] Import block is un-sorted or un-formatted 10 | | 11 | | from . import my_local_folder_object | |____________________________________^ - | help: Organize imports | 1 + # Future imports diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_wrong_heading_import_heading_wrong_heading.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_wrong_heading_import_heading_wrong_heading.py.snap index b7d617c2c8..80bec7999a 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_wrong_heading_import_heading_wrong_heading.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_wrong_heading_import_heading_wrong_heading.py.snap @@ -14,7 +14,6 @@ I001 [*] Import block is un-sorted or un-formatted 8 | | 9 | | from my_first_party import my_first_party_object | |________________________________________________^ - | help: Organize imports | 1 | # Wrong heading diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__inline_comments.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__inline_comments.py.snap index 48eb4d3c25..1e561c6670 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__inline_comments.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__inline_comments.py.snap @@ -16,7 +16,6 @@ I001 [*] Import block is un-sorted or un-formatted 10 | | 11 | | from d.prometheus.metrics import TERMINAL_CURRENTLY_RUNNING_TOTAL, OTHER_RUNNING_TOTAL # type:ignore[attr-defined] | |______________________________________________________________________________________^ - | help: Organize imports | 3 | ) diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__known_local_folder_closest_separate_local_folder_imports.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__known_local_folder_closest_separate_local_folder_imports.py.snap index 723bbdc0ab..9d1897dd17 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__known_local_folder_closest_separate_local_folder_imports.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__known_local_folder_closest_separate_local_folder_imports.py.snap @@ -12,7 +12,6 @@ I001 [*] Import block is un-sorted or un-formatted 6 | | from .. import trailing_prefix 7 | | from ruff import check | |______________________^ - | help: Organize imports | 1 + import os diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__known_local_folder_separate_local_folder_imports.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__known_local_folder_separate_local_folder_imports.py.snap index 4ecc570021..cfaa6b42c8 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__known_local_folder_separate_local_folder_imports.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__known_local_folder_separate_local_folder_imports.py.snap @@ -12,7 +12,6 @@ I001 [*] Import block is un-sorted or un-formatted 6 | | from .. import trailing_prefix 7 | | from ruff import check | |______________________^ - | help: Organize imports | 1 + import os diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__lazy_imports.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__lazy_imports.py.snap index 5ea9ddb5cb..23ebddd718 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__lazy_imports.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__lazy_imports.py.snap @@ -9,7 +9,6 @@ I001 [*] Import block is un-sorted or un-formatted 3 | | lazy import os 4 | | import os | |_________^ - | help: Organize imports | - lazy from math import pi diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__leading_prefix.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__leading_prefix.py.snap index d924544c1f..77e731cab7 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__leading_prefix.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__leading_prefix.py.snap @@ -44,5 +44,4 @@ I001 Import block is un-sorted or un-formatted 12 | x = 1; \ 13 | import os | ^^^^^^^^^ - | help: Organize imports diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__length_sort__length_sort_from_imports.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__length_sort__length_sort_from_imports.py.snap index 462aead518..52b4750dac 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__length_sort__length_sort_from_imports.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__length_sort__length_sort_from_imports.py.snap @@ -8,7 +8,6 @@ I001 [*] Import block is un-sorted or un-formatted 2 | | from short import b 3 | | from loooooooooooooooooooooog import c | |______________________________________^ - | help: Organize imports | 1 + from short import b diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__length_sort__length_sort_non_ascii_members.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__length_sort__length_sort_non_ascii_members.py.snap index aa6fe68f4d..d8f31f8310 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__length_sort__length_sort_non_ascii_members.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__length_sort__length_sort_non_ascii_members.py.snap @@ -16,7 +16,6 @@ I001 [*] Import block is un-sorted or un-formatted 10 | | λοοοοοοοοοοοοοονγ, 11 | | ) | |_^ - | help: Organize imports | 1 | from module1 import ( diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__length_sort__length_sort_non_ascii_modules.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__length_sort__length_sort_non_ascii_modules.py.snap index fad0099d66..ad038adb64 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__length_sort__length_sort_non_ascii_modules.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__length_sort__length_sort_non_ascii_modules.py.snap @@ -14,7 +14,6 @@ I001 [*] Import block is un-sorted or un-formatted 8 | | import μεδιυυυυυμ 9 | | import looooooooooooooong | |_________________________^ - | help: Organize imports | - import loooooooooooooong diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__length_sort__length_sort_straight_and_from_imports.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__length_sort__length_sort_straight_and_from_imports.py.snap index 696b43cdf0..d969d064fe 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__length_sort__length_sort_straight_and_from_imports.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__length_sort__length_sort_straight_and_from_imports.py.snap @@ -11,7 +11,6 @@ I001 [*] Import block is un-sorted or un-formatted 5 | | from mediuuuum import c 6 | | from short import b | |___________________^ - | help: Organize imports | 1 + import short diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__length_sort__length_sort_straight_imports.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__length_sort__length_sort_straight_imports.py.snap index a4c0b6b8e1..629a252b9c 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__length_sort__length_sort_straight_imports.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__length_sort__length_sort_straight_imports.py.snap @@ -9,7 +9,6 @@ I001 [*] Import block is un-sorted or un-formatted 3 | | import looooooooooooooooong 4 | | import mediuuuuuuma | |___________________^ - | help: Organize imports | 1 + import short diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__length_sort__length_sort_with_relative_imports.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__length_sort__length_sort_with_relative_imports.py.snap index d4f6848db5..f9eae0c402 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__length_sort__length_sort_with_relative_imports.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__length_sort__length_sort_with_relative_imports.py.snap @@ -12,7 +12,6 @@ I001 [*] Import block is un-sorted or un-formatted 6 | | from .mediuuuum import a 7 | | from ......short import b | |_________________________^ - | help: Organize imports | - from ..looooooooooooooong import a diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__length_sort_straight__length_sort_from_imports.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__length_sort_straight__length_sort_from_imports.py.snap index 43ce4317c4..dde79e7594 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__length_sort_straight__length_sort_from_imports.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__length_sort_straight__length_sort_from_imports.py.snap @@ -8,7 +8,6 @@ I001 [*] Import block is un-sorted or un-formatted 2 | | from short import b 3 | | from loooooooooooooooooooooog import c | |______________________________________^ - | help: Organize imports | 1 + from loooooooooooooooooooooog import c diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__length_sort_straight__length_sort_straight_and_from_imports.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__length_sort_straight__length_sort_straight_and_from_imports.py.snap index d3ab243022..3299b6f541 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__length_sort_straight__length_sort_straight_and_from_imports.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__length_sort_straight__length_sort_straight_and_from_imports.py.snap @@ -11,7 +11,6 @@ I001 [*] Import block is un-sorted or un-formatted 5 | | from mediuuuum import c 6 | | from short import b | |___________________^ - | help: Organize imports | 1 + import short diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__length_sort_straight__length_sort_straight_imports.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__length_sort_straight__length_sort_straight_imports.py.snap index a4c0b6b8e1..629a252b9c 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__length_sort_straight__length_sort_straight_imports.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__length_sort_straight__length_sort_straight_imports.py.snap @@ -9,7 +9,6 @@ I001 [*] Import block is un-sorted or un-formatted 3 | | import looooooooooooooooong 4 | | import mediuuuuuuma | |___________________^ - | help: Organize imports | 1 + import short diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__line_ending_crlf.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__line_ending_crlf.py.snap index 03c5d233ad..d6e945ffeb 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__line_ending_crlf.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__line_ending_crlf.py.snap @@ -6,7 +6,6 @@ I001 [*] Import block is un-sorted or un-formatted | 1 | from long_module_name import member_one, member_two, member_three, member_four, member_five | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Organize imports | - from long_module_name import member_one, member_two, member_three, member_four, member_five diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__line_ending_lf.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__line_ending_lf.py.snap index 14b68ac0db..5ff5c8e84a 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__line_ending_lf.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__line_ending_lf.py.snap @@ -6,7 +6,6 @@ I001 [*] Import block is un-sorted or un-formatted | 1 | from long_module_name import member_one, member_two, member_three, member_four, member_five | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Organize imports | - from long_module_name import member_one, member_two, member_three, member_four, member_five diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__lines_after_imports.pyi.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__lines_after_imports.pyi.snap index 93b16f68d2..1653128b16 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__lines_after_imports.pyi.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__lines_after_imports.pyi.snap @@ -14,7 +14,6 @@ I001 [*] Import block is un-sorted or un-formatted 8 | | 9 | | from . import my_local_folder_object | |____________________________________^ - | help: Organize imports | 4 | diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__lines_after_imports_func_after.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__lines_after_imports_func_after.py.snap index 66cdaf79f8..ad9975a69a 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__lines_after_imports_func_after.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__lines_after_imports_func_after.py.snap @@ -14,7 +14,6 @@ I001 [*] Import block is un-sorted or un-formatted 8 | | 9 | | from . import my_local_folder_object | |____________________________________^ - | help: Organize imports | 4 | diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__lines_after_imports_lines_after_imports.pyi.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__lines_after_imports_lines_after_imports.pyi.snap index 93b16f68d2..1653128b16 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__lines_after_imports_lines_after_imports.pyi.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__lines_after_imports_lines_after_imports.pyi.snap @@ -14,7 +14,6 @@ I001 [*] Import block is un-sorted or un-formatted 8 | | 9 | | from . import my_local_folder_object | |____________________________________^ - | help: Organize imports | 4 | diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__lines_after_imports_lines_after_imports_func_after.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__lines_after_imports_lines_after_imports_func_after.py.snap index 0656649411..35fb31db47 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__lines_after_imports_lines_after_imports_func_after.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__lines_after_imports_lines_after_imports_func_after.py.snap @@ -14,7 +14,6 @@ I001 [*] Import block is un-sorted or un-formatted 8 | | 9 | | from . import my_local_folder_object | |____________________________________^ - | help: Organize imports | 4 | diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__lines_after_imports_lines_after_imports_nothing_after.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__lines_after_imports_lines_after_imports_nothing_after.py.snap index f3caf1a0aa..aa1e760692 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__lines_after_imports_lines_after_imports_nothing_after.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__lines_after_imports_lines_after_imports_nothing_after.py.snap @@ -14,7 +14,6 @@ I001 [*] Import block is un-sorted or un-formatted 8 | | 9 | | from . import my_local_folder_object | |____________________________________^ - | help: Organize imports | 4 | diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__lines_between_typeslines_between_types.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__lines_between_typeslines_between_types.py.snap index c4ae989717..890cea62bc 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__lines_between_typeslines_between_types.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__lines_between_typeslines_between_types.py.snap @@ -21,7 +21,6 @@ I001 [*] Import block is un-sorted or un-formatted 15 | | from . import config 16 | | from .data import Data | |______________________^ - | help: Organize imports | 11 | diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__magic_trailing_comma.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__magic_trailing_comma.py.snap index 7a5716dbc5..ac554299c2 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__magic_trailing_comma.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__magic_trailing_comma.py.snap @@ -43,7 +43,6 @@ I001 [*] Import block is un-sorted or un-formatted 37 | | member3, 38 | | ) | |_^ - | help: Organize imports | 1 | # This has a magic trailing comma, will be sorted, but not rolled into one line diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__match_case.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__match_case.py.snap index 5ee663f125..435058e786 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__match_case.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__match_case.py.snap @@ -29,7 +29,6 @@ I001 [*] Import block is un-sorted or un-formatted 6 | / import collections 7 | | import abc | |__________________^ - | help: Organize imports | 5 | case 2: diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__natural_order.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__natural_order.py.snap index 068f8618af..9df996f9bb 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__natural_order.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__natural_order.py.snap @@ -21,7 +21,6 @@ I001 [*] Import block is un-sorted or un-formatted 15 | | uint64, 16 | | ) | |_^ - | help: Organize imports | 1 | import numpy1 diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__no_detect_same_package.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__no_detect_same_package.snap index 9963dc0c55..5c7538732e 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__no_detect_same_package.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__no_detect_same_package.snap @@ -8,7 +8,6 @@ I001 [*] Import block is un-sorted or un-formatted 2 | | import pandas 3 | | import foo.baz | |______________^ - | help: Organize imports | 1 | import os diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__no_lines_before.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__no_lines_before.py.snap index 0e044c85d3..9a01d87bf6 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__no_lines_before.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__no_lines_before.py.snap @@ -14,7 +14,6 @@ I001 [*] Import block is un-sorted or un-formatted 8 | | 9 | | from . import my_local_folder_object | |____________________________________^ - | help: Organize imports | 4 | diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__no_lines_before.py_no_lines_before.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__no_lines_before.py_no_lines_before.py.snap index 7e9b198adf..1f491b556f 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__no_lines_before.py_no_lines_before.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__no_lines_before.py_no_lines_before.py.snap @@ -14,7 +14,6 @@ I001 [*] Import block is un-sorted or un-formatted 8 | | 9 | | from . import my_local_folder_object | |____________________________________^ - | help: Organize imports | 1 | from __future__ import annotations diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__no_lines_before_with_empty_sections.py_no_lines_before_with_empty_sections.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__no_lines_before_with_empty_sections.py_no_lines_before_with_empty_sections.py.snap index 6eae2c4e7c..f400356b5c 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__no_lines_before_with_empty_sections.py_no_lines_before_with_empty_sections.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__no_lines_before_with_empty_sections.py_no_lines_before_with_empty_sections.py.snap @@ -8,7 +8,6 @@ I001 [*] Import block is un-sorted or un-formatted 2 | | from typing import Any 3 | | from . import my_local_folder_object | |____________________________________^ - | help: Organize imports | 2 | from typing import Any diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__no_standard_library_no_standard_library.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__no_standard_library_no_standard_library.py.snap index 86c59f05cf..2329492ec6 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__no_standard_library_no_standard_library.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__no_standard_library_no_standard_library.py.snap @@ -14,7 +14,6 @@ I001 [*] Import block is un-sorted or un-formatted 8 | | from . import local 9 | | import sys | |__________^ - | help: Organize imports | 2 | diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__no_wrap_star.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__no_wrap_star.py.snap index 56cdb2c514..5f930fa4cf 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__no_wrap_star.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__no_wrap_star.py.snap @@ -6,7 +6,6 @@ I001 [*] Import block is un-sorted or un-formatted | 1 | from .subscription import * # type: ignore # some very long comment explaining why this needs a type ignore | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Organize imports | - from .subscription import * # type: ignore # some very long comment explaining why this needs a type ignore diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__order_by_type.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__order_by_type.py.snap index 70564ab0d8..a5c6c7f57d 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__order_by_type.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__order_by_type.py.snap @@ -17,7 +17,6 @@ I001 [*] Import block is un-sorted or un-formatted 11 | | import BAR 12 | | import bar | |__________^ - | help: Organize imports | - import StringIO diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__order_by_type_false_order_by_type.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__order_by_type_false_order_by_type.py.snap index 73ce2c088a..56f6cd71c8 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__order_by_type_false_order_by_type.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__order_by_type_false_order_by_type.py.snap @@ -17,7 +17,6 @@ I001 [*] Import block is un-sorted or un-formatted 11 | | import BAR 12 | | import bar | |__________^ - | help: Organize imports | - import StringIO diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__order_by_type_with_custom_classes.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__order_by_type_with_custom_classes.py.snap index 6b0c0c28ef..6c6d57a9e0 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__order_by_type_with_custom_classes.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__order_by_type_with_custom_classes.py.snap @@ -9,7 +9,6 @@ I001 [*] Import block is un-sorted or un-formatted 3 | | from module import CLASS, Class, CONSTANT, function, BASIC, Apple 4 | | from torch.nn import SELU, AClass, A_CONSTANT | |_____________________________________________^ - | help: Organize imports | - from sklearn.svm import func, SVC, CONST, Klass diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__order_by_type_with_custom_classes_order_by_type_with_custom_classes.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__order_by_type_with_custom_classes_order_by_type_with_custom_classes.py.snap index dacac652e8..13bfb34aea 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__order_by_type_with_custom_classes_order_by_type_with_custom_classes.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__order_by_type_with_custom_classes_order_by_type_with_custom_classes.py.snap @@ -9,7 +9,6 @@ I001 [*] Import block is un-sorted or un-formatted 3 | | from module import CLASS, Class, CONSTANT, function, BASIC, Apple 4 | | from torch.nn import SELU, AClass, A_CONSTANT | |_____________________________________________^ - | help: Organize imports | - from sklearn.svm import func, SVC, CONST, Klass diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__order_by_type_with_custom_constants.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__order_by_type_with_custom_constants.py.snap index 423fdc237b..47c84868ef 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__order_by_type_with_custom_constants.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__order_by_type_with_custom_constants.py.snap @@ -7,7 +7,6 @@ I001 [*] Import block is un-sorted or un-formatted 1 | / from sklearn.svm import XYZ, func, variable, Const, Klass, constant 2 | | from subprocess import First, var, func, Class, konst, A_constant, Last, STDOUT | |_______________________________________________________________________________^ - | help: Organize imports | - from sklearn.svm import XYZ, func, variable, Const, Klass, constant diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__order_by_type_with_custom_constants_order_by_type_with_custom_constants.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__order_by_type_with_custom_constants_order_by_type_with_custom_constants.py.snap index 89f24a9b52..424e09e48c 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__order_by_type_with_custom_constants_order_by_type_with_custom_constants.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__order_by_type_with_custom_constants_order_by_type_with_custom_constants.py.snap @@ -7,7 +7,6 @@ I001 [*] Import block is un-sorted or un-formatted 1 | / from sklearn.svm import XYZ, func, variable, Const, Klass, constant 2 | | from subprocess import First, var, func, Class, konst, A_constant, Last, STDOUT | |_______________________________________________________________________________^ - | help: Organize imports | - from sklearn.svm import XYZ, func, variable, Const, Klass, constant diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__order_by_type_with_custom_variables.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__order_by_type_with_custom_variables.py.snap index 846d879150..6a5a853dde 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__order_by_type_with_custom_variables.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__order_by_type_with_custom_variables.py.snap @@ -7,7 +7,6 @@ I001 [*] Import block is un-sorted or un-formatted 1 | / from sklearn.svm import VAR, Class, MyVar, CONST, abc 2 | | from subprocess import utils, var_ABC, Variable, Klass, CONSTANT, exe | |_____________________________________________________________________^ - | help: Organize imports | - from sklearn.svm import VAR, Class, MyVar, CONST, abc diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__order_by_type_with_custom_variables_order_by_type_with_custom_variables.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__order_by_type_with_custom_variables_order_by_type_with_custom_variables.py.snap index 25a89760fb..f14d7b9081 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__order_by_type_with_custom_variables_order_by_type_with_custom_variables.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__order_by_type_with_custom_variables_order_by_type_with_custom_variables.py.snap @@ -7,7 +7,6 @@ I001 [*] Import block is un-sorted or un-formatted 1 | / from sklearn.svm import VAR, Class, MyVar, CONST, abc 2 | | from subprocess import utils, var_ABC, Variable, Klass, CONSTANT, exe | |_____________________________________________________________________^ - | help: Organize imports | - from sklearn.svm import VAR, Class, MyVar, CONST, abc diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__order_relative_imports_by_level.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__order_relative_imports_by_level.py.snap index cafaec9d2f..9cea317ea3 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__order_relative_imports_by_level.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__order_relative_imports_by_level.py.snap @@ -9,7 +9,6 @@ I001 [*] Import block is un-sorted or un-formatted 3 | | from ..b import a 4 | | from .b import a | |________________^ - | help: Organize imports | - from .a import a diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__preserve_comment_order.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__preserve_comment_order.py.snap index 0b1a3551b2..72714ceb92 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__preserve_comment_order.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__preserve_comment_order.py.snap @@ -16,7 +16,6 @@ I001 [*] Import block is un-sorted or un-formatted 10 | | from errno import EIO 11 | | import abc | |__________^ - | help: Organize imports | 1 + import abc diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__preserve_import_star.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__preserve_import_star.py.snap index 7e78c93b3d..775915d45b 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__preserve_import_star.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__preserve_import_star.py.snap @@ -11,7 +11,6 @@ I001 [*] Import block is un-sorted or un-formatted 5 | | # Above 6 | | from some_module import * # Aside | |_________________________^ - | help: Organize imports | - from some_other_module import some_class diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__preserve_indentation.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__preserve_indentation.py.snap index 170e452d41..50cfe29752 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__preserve_indentation.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__preserve_indentation.py.snap @@ -28,7 +28,6 @@ I001 [*] Import block is un-sorted or un-formatted 5 | / import sys 6 | | import os | |_____________^ - | help: Organize imports | 4 | else: diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__propagate_inline_comments_propagate_inline_comments.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__propagate_inline_comments_propagate_inline_comments.py.snap index 7bcf5b5e36..c9c8f7b9de 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__propagate_inline_comments_propagate_inline_comments.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__propagate_inline_comments_propagate_inline_comments.py.snap @@ -9,7 +9,6 @@ I001 [*] Import block is un-sorted or un-formatted 3 | | items, 4 | | ) | |_^ - | help: Organize imports | 2 | a_long_variable_name_that_causes_problems, diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__reorder_within_section.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__reorder_within_section.py.snap index fc3d806d48..296be519a1 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__reorder_within_section.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__reorder_within_section.py.snap @@ -7,7 +7,6 @@ I001 [*] Import block is un-sorted or un-formatted 1 | / import sys 2 | | import os | |_________^ - | help: Organize imports | 1 + import os diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__required_import_with_useless_alias_this_this.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__required_import_with_useless_alias_this_this.py.snap index 575e4c6553..f459448761 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__required_import_with_useless_alias_this_this.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__required_import_with_useless_alias_this_this.py.snap @@ -6,5 +6,4 @@ PLC0414 Required import does not rename original package. | 1 | import this as this | ^^^^^^^^^^^^ - | help: Change required import or disable rule. diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__required_importfrom_with_useless_alias_this_this_from.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__required_importfrom_with_useless_alias_this_this_from.py.snap index a4eb5e2aaf..320b84a5f9 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__required_importfrom_with_useless_alias_this_this_from.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__required_importfrom_with_useless_alias_this_this_from.py.snap @@ -6,5 +6,4 @@ PLC0414 Required import does not rename original package. | 1 | from module import this as this | ^^^^^^^^^^^^ - | help: Change required import or disable rule. diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__section_order_sections.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__section_order_sections.py.snap index d3d5a6e2fe..e8e384d0cf 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__section_order_sections.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__section_order_sections.py.snap @@ -12,7 +12,6 @@ I001 [*] Import block is un-sorted or un-formatted 6 | | from library import foo 7 | | from . import local | |___________________^ - | help: Organize imports | 1 | from __future__ import annotations diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__sections_sections.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__sections_sections.py.snap index 38ccfca016..6d24a6547e 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__sections_sections.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__sections_sections.py.snap @@ -12,7 +12,6 @@ I001 [*] Import block is un-sorted or un-formatted 6 | | from library import foo 7 | | from . import local | |___________________^ - | help: Organize imports | 1 | from __future__ import annotations diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__separate_first_party_imports.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__separate_first_party_imports.py.snap index 5517a50b31..2999a9b2cb 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__separate_first_party_imports.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__separate_first_party_imports.py.snap @@ -10,7 +10,6 @@ I001 [*] Import block is un-sorted or un-formatted 4 | | import os 5 | | from leading_prefix import Class | |________________________________^ - | help: Organize imports | 1 + import os diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__separate_future_imports.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__separate_future_imports.py.snap index abbd2d55b6..1c9049487b 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__separate_future_imports.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__separate_future_imports.py.snap @@ -8,7 +8,6 @@ I001 [*] Import block is un-sorted or un-formatted 2 | | import os 3 | | from __future__ import annotations | |__________________________________^ - | help: Organize imports | - import sys diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__separate_local_folder_imports.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__separate_local_folder_imports.py.snap index 88b02df616..62e21c5a66 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__separate_local_folder_imports.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__separate_local_folder_imports.py.snap @@ -12,7 +12,6 @@ I001 [*] Import block is un-sorted or un-formatted 6 | | from .. import trailing_prefix 7 | | from ruff import check | |______________________^ - | help: Organize imports | 1 + import os diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__separate_third_party_imports.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__separate_third_party_imports.py.snap index 26fde68ecb..f931ab534b 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__separate_third_party_imports.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__separate_third_party_imports.py.snap @@ -9,7 +9,6 @@ I001 [*] Import block is un-sorted or un-formatted 3 | | import numpy as np 4 | | import os | |_________^ - | help: Organize imports | - import pandas as pd diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__skip.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__skip.py.snap index 34aaaa42a8..6f8bc5b4db 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__skip.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__skip.py.snap @@ -9,7 +9,6 @@ I001 [*] Import block is un-sorted or un-formatted 20 | / import collections 21 | | import abc | |______________^ - | help: Organize imports | 19 | import os # isort: skip @@ -27,7 +26,6 @@ I001 [*] Import block is un-sorted or un-formatted 27 | / import collections 28 | | import abc | |______________^ - | help: Organize imports | 26 | import os # isort:skip @@ -44,7 +42,6 @@ I001 [*] Import block is un-sorted or un-formatted 33 | import sys; import os # isort:skip # isort:skip 34 | import sys; import os | ^^^^^^^^^^^^^^^^^^^^^ - | help: Organize imports | 33 | import sys; import os # isort:skip # isort:skip diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__sort_similar_imports.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__sort_similar_imports.py.snap index 0c589c5d30..6964d1f719 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__sort_similar_imports.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__sort_similar_imports.py.snap @@ -31,7 +31,6 @@ I001 [*] Import block is un-sorted or un-formatted 25 | | import x 26 | | import x as a | |_____________^ - | help: Organize imports | - from a import b diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__split.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__split.py.snap index 4e58973fcb..65bd1fcdf6 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__split.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__split.py.snap @@ -28,7 +28,6 @@ I001 [*] Import block is un-sorted or un-formatted 20 | / import D 21 | | import B | |____________^ - | help: Organize imports | 19 | @@ -46,7 +45,6 @@ I001 [*] Import block is un-sorted or un-formatted 30 | / import d 31 | | import c | |________^ - | help: Organize imports | 29 | diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__split_on_trailing_comma_magic_trailing_comma.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__split_on_trailing_comma_magic_trailing_comma.py.snap index 4ad739b537..2d3ec8531f 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__split_on_trailing_comma_magic_trailing_comma.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__split_on_trailing_comma_magic_trailing_comma.py.snap @@ -43,7 +43,6 @@ I001 [*] Import block is un-sorted or un-formatted 37 | | member3, 38 | | ) | |_^ - | help: Organize imports | 1 | # This has a magic trailing comma, will be sorted, but not rolled into one line diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__star_before_others.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__star_before_others.py.snap index 66a62a417d..109bbf6597 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__star_before_others.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__star_before_others.py.snap @@ -8,7 +8,6 @@ I001 [*] Import block is un-sorted or un-formatted 2 | | from .settings import ENV 3 | | from .settings import * | |_______________________^ - | help: Organize imports | 1 | from .logging import config_logging diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__trailing_comment.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__trailing_comment.py.snap index 2756a1ccab..b28c3f020d 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__trailing_comment.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__trailing_comment.py.snap @@ -115,7 +115,6 @@ I001 [*] Import block is un-sorted or un-formatted 53 | | # e 54 | | ) # f | |_^ - | help: Organize imports | 49 | # a diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__trailing_suffix.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__trailing_suffix.py.snap index db440fdd49..614e80f9e8 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__trailing_suffix.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__trailing_suffix.py.snap @@ -19,5 +19,4 @@ I001 Import block is un-sorted or un-formatted 5 | / import sys 6 | | import os; x = 1 | |_____________^ - | help: Organize imports 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 625a29829f..e39ac5303c 100644 --- a/crates/ruff_linter/src/rules/numpy/rules/legacy_random.rs +++ b/crates/ruff_linter/src/rules/numpy/rules/legacy_random.rs @@ -65,84 +65,68 @@ pub(crate) fn legacy_random(checker: &Checker, expr: &Expr) { return; } - if let Some(method_name) = - checker - .semantic() - .resolve_qualified_name(expr) - .and_then(|qualified_name| { - // seeding state - if matches!( - qualified_name.segments(), - [ - "numpy", - "random", - // Seeds - "seed" | - "get_state" | - "set_state" | - // Simple random data - "rand" | - "ranf" | - "sample" | - "randn" | - "randint" | - "random" | - "random_integers" | - "random_sample" | - "choice" | - "bytes" | - // Permutations - "shuffle" | - "permutation" | - // Distributions - "beta" | - "binomial" | - "chisquare" | - "dirichlet" | - "exponential" | - "f" | - "gamma" | - "geometric" | - "gumbel" | - "hypergeometric" | - "laplace" | - "logistic" | - "lognormal" | - "logseries" | - "multinomial" | - "multivariate_normal" | - "negative_binomial" | - "noncentral_chisquare" | - "noncentral_f" | - "normal" | - "pareto" | - "poisson" | - "power" | - "rayleigh" | - "standard_cauchy" | - "standard_exponential" | - "standard_gamma" | - "standard_normal" | - "standard_t" | - "triangular" | - "uniform" | - "vonmises" | - "wald" | - "weibull" | - "zipf" - ] - ) { - Some(qualified_name.segments()[2]) - } else { - None - } - }) - { - checker.report_diagnostic( - NumpyLegacyRandom { - method_name: method_name.to_string(), - }, - expr.range(), - ); + let Some(method_name) = checker.semantic().resolve_qualified_name(expr) else { + return; + }; + + let ["numpy", "random", method_name] = method_name.segments() else { + return; + }; + + match *method_name { + // seeds + "seed" | "get_state" | "set_state" => {} + + // simple random data + "rand" | "ranf" | "sample" | "randn" | "randint" | "random" | "random_integers" + | "random_sample" | "choice" | "bytes" => {} + + // permutations + "shuffle" | "permutation" => {} + + // distributions + "beta" + | "binomial" + | "chisquare" + | "dirichlet" + | "exponential" + | "f" + | "gamma" + | "geometric" + | "gumbel" + | "hypergeometric" + | "laplace" + | "logistic" + | "lognormal" + | "logseries" + | "multinomial" + | "multivariate_normal" + | "negative_binomial" + | "noncentral_chisquare" + | "noncentral_f" + | "normal" + | "pareto" + | "poisson" + | "power" + | "rayleigh" + | "standard_cauchy" + | "standard_exponential" + | "standard_gamma" + | "standard_normal" + | "standard_t" + | "triangular" + | "uniform" + | "vonmises" + | "wald" + | "weibull" + | "zipf" => {} + _ => return, } + + 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 efe8576b51..59b003d5db 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 @@ -683,12 +683,26 @@ pub(crate) fn numpy_2_0_deprecation(checker: &Checker, expr: &Expr) { compatibility, } => { diagnostic.try_set_fix(|| { + // `numpy.char` is not an importable module path on NumPy 1.x. + let (path, name, attribute) = if matches!( + (path, name), + ("numpy.char", "chararray" | "compare_chararrays") + ) { + ("numpy", "char", Some(name)) + } else { + (path, name, None) + }; let (import_edit, binding) = checker.importer().get_or_import_symbol( &ImportRequest::import_from(path, name), expr.start(), checker.semantic(), )?; - let replacement_edit = Edit::range_replacement(binding, expr.range()); + let replacement = if let Some(attribute) = attribute { + format!("{binding}.{attribute}") + } else { + binding + }; + let replacement_edit = Edit::range_replacement(replacement, expr.range()); Ok(match compatibility { Compatibility::BackwardsCompatible => { Fix::safe_edits(import_edit, [replacement_edit]) diff --git a/crates/ruff_linter/src/rules/numpy/snapshots/ruff_linter__rules__numpy__tests__numpy-deprecated-function_NPY003.py.snap b/crates/ruff_linter/src/rules/numpy/snapshots/ruff_linter__rules__numpy__tests__numpy-deprecated-function_NPY003.py.snap index 2b57eba3db..1a7b461cdf 100644 --- a/crates/ruff_linter/src/rules/numpy/snapshots/ruff_linter__rules__numpy__tests__numpy-deprecated-function_NPY003.py.snap +++ b/crates/ruff_linter/src/rules/numpy/snapshots/ruff_linter__rules__numpy__tests__numpy-deprecated-function_NPY003.py.snap @@ -78,7 +78,6 @@ NPY003 [*] `np.alltrue` is deprecated; use `np.all` instead 7 | np.sometrue(np.random.rand(5, 5)) 8 | np.alltrue(np.random.rand(5, 5)) | ^^^^^^^^^^ - | help: Replace with `np.all` | 7 | np.sometrue(np.random.rand(5, 5)) @@ -176,7 +175,6 @@ NPY003 [*] `np.alltrue` is deprecated; use `np.all` instead 17 | sometrue(np.random.rand(5, 5)) 18 | alltrue(np.random.rand(5, 5)) | ^^^^^^^ - | help: Replace with `np.all` | 1 + from numpy import all diff --git a/crates/ruff_linter/src/rules/numpy/snapshots/ruff_linter__rules__numpy__tests__numpy-deprecated-type-alias_NPY001.py.snap b/crates/ruff_linter/src/rules/numpy/snapshots/ruff_linter__rules__numpy__tests__numpy-deprecated-type-alias_NPY001.py.snap index 8e7371e8f8..e215442bc4 100644 --- a/crates/ruff_linter/src/rules/numpy/snapshots/ruff_linter__rules__numpy__tests__numpy-deprecated-type-alias_NPY001.py.snap +++ b/crates/ruff_linter/src/rules/numpy/snapshots/ruff_linter__rules__numpy__tests__numpy-deprecated-type-alias_NPY001.py.snap @@ -130,7 +130,6 @@ NPY001 [*] Type alias `np.float` is deprecated, replace with builtin type 24 | 25 | float(1) | ^^^^^ - | help: Replace `np.float` with builtin type | 23 | from numpy import float diff --git a/crates/ruff_linter/src/rules/numpy/snapshots/ruff_linter__rules__numpy__tests__numpy-legacy-random_NPY002.py.snap b/crates/ruff_linter/src/rules/numpy/snapshots/ruff_linter__rules__numpy__tests__numpy-legacy-random_NPY002.py.snap index 8a0bf558f0..d1fd359f6b 100644 --- a/crates/ruff_linter/src/rules/numpy/snapshots/ruff_linter__rules__numpy__tests__numpy-legacy-random_NPY002.py.snap +++ b/crates/ruff_linter/src/rules/numpy/snapshots/ruff_linter__rules__numpy__tests__numpy-legacy-random_NPY002.py.snap @@ -565,4 +565,3 @@ NPY002 Replace legacy `np.random.zipf` call with `np.random.Generator` 66 | numpy.random.weibull() 67 | numpy.random.zipf() | ^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/numpy/snapshots/ruff_linter__rules__numpy__tests__numpy2-deprecation_NPY201_2.py.snap b/crates/ruff_linter/src/rules/numpy/snapshots/ruff_linter__rules__numpy__tests__numpy2-deprecation_NPY201_2.py.snap index ecc91ce8c2..0984b70ac5 100644 --- a/crates/ruff_linter/src/rules/numpy/snapshots/ruff_linter__rules__numpy__tests__numpy2-deprecation_NPY201_2.py.snap +++ b/crates/ruff_linter/src/rules/numpy/snapshots/ruff_linter__rules__numpy__tests__numpy2-deprecation_NPY201_2.py.snap @@ -434,13 +434,10 @@ NPY201 [*] `np.compare_chararrays` will be removed in NumPy 2.0. Use `numpy.char | help: Replace with `numpy.char.compare_chararrays` | -1 + from numpy.char import compare_chararrays -2 | def func(): --------------------------------------------------------------------------------- -58 | +57 | - np.compare_chararrays -59 + compare_chararrays -60 | +58 + np.char.compare_chararrays +59 | | NPY201 [*] `np.alltrue` will be removed in NumPy 2.0. Use `numpy.all` instead. diff --git a/crates/ruff_linter/src/rules/numpy/snapshots/ruff_linter__rules__numpy__tests__numpy2-deprecation_NPY201_3.py.snap b/crates/ruff_linter/src/rules/numpy/snapshots/ruff_linter__rules__numpy__tests__numpy2-deprecation_NPY201_3.py.snap index a93e8ae6b2..b1d78c1d85 100644 --- a/crates/ruff_linter/src/rules/numpy/snapshots/ruff_linter__rules__numpy__tests__numpy2-deprecation_NPY201_3.py.snap +++ b/crates/ruff_linter/src/rules/numpy/snapshots/ruff_linter__rules__numpy__tests__numpy2-deprecation_NPY201_3.py.snap @@ -119,13 +119,10 @@ NPY201 [*] `np.chararray` will be removed in NumPy 2.0. Use `numpy.char.chararra | help: Replace with `numpy.char.chararray` | -1 + from numpy.char import chararray -2 | def func(): --------------------------------------------------------------------------------- -14 | +13 | - np.chararray -15 + chararray -16 | +14 + np.char.chararray +15 | | NPY201 [*] `np.format_parser` will be removed in NumPy 2.0. Use `numpy.rec.format_parser` instead. @@ -135,7 +132,6 @@ NPY201 [*] `np.format_parser` will be removed in NumPy 2.0. Use `numpy.rec.forma 15 | 16 | np.format_parser | ^^^^^^^^^^^^^^^^ - | help: Replace with `numpy.rec.format_parser` | 1 + from numpy.rec import format_parser diff --git a/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD002_fail.snap b/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD002_fail.snap index 46ebde74d5..f07b80f849 100644 --- a/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD002_fail.snap +++ b/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD002_fail.snap @@ -8,7 +8,6 @@ PD002 [*] `inplace=True` should be avoided; it has inconsistent behavior 3 | x = pd.DataFrame() 4 | x.drop(["a"], axis=1, inplace=True) | ^^^^^^^^^^^^ - | help: Assign to variable; remove `inplace` arg | 3 | x = pd.DataFrame() diff --git a/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD003_fail.snap b/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD003_fail.snap index 88202b0f96..0fccfaee8d 100644 --- a/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD003_fail.snap +++ b/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD003_fail.snap @@ -7,4 +7,3 @@ PD003 `.isna` is preferred to `.isnull`; functionality is equivalent 2 | import pandas as pd 3 | nulls = pd.isnull(val) | ^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD004_fail.snap b/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD004_fail.snap index acef589141..0ec15ef1d4 100644 --- a/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD004_fail.snap +++ b/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD004_fail.snap @@ -7,4 +7,3 @@ PD004 `.notna` is preferred to `.notnull`; functionality is equivalent 2 | import pandas as pd 3 | not_nulls = pd.notnull(val) | ^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD007_fail.snap b/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD007_fail.snap index 45c7779e8f..cd5ae53cb6 100644 --- a/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD007_fail.snap +++ b/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD007_fail.snap @@ -8,4 +8,3 @@ PD007 `.ix` is deprecated; use more explicit `.loc` or `.iloc` 3 | x = pd.DataFrame() 4 | y = x.ix[[0, 2], "A"] | ^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD008_fail.snap b/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD008_fail.snap index 9c2cfd6bad..e66b6731f2 100644 --- a/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD008_fail.snap +++ b/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD008_fail.snap @@ -8,4 +8,3 @@ PD008 Use `.loc` instead of `.at`. If speed is important, use NumPy. 3 | x = pd.DataFrame() 4 | index = x.at[:, ["B", "A"]] | ^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD009_fail.snap b/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD009_fail.snap index 29aaa732bc..0436f76fae 100644 --- a/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD009_fail.snap +++ b/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD009_fail.snap @@ -8,4 +8,3 @@ PD009 Use `.iloc` instead of `.iat`. If speed is important, use NumPy. 3 | x = pd.DataFrame() 4 | index = x.iat[:, 1:3] | ^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD011_fail_values.snap b/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD011_fail_values.snap index 8663ed6b38..90729a93c5 100644 --- a/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD011_fail_values.snap +++ b/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD011_fail_values.snap @@ -8,4 +8,3 @@ PD011 Use `.to_numpy()` or `.array` instead of `.values` 3 | x = pd.DataFrame() 4 | result = x.values | ^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD013_fail_stack.snap b/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD013_fail_stack.snap index 00b141414e..6673483605 100644 --- a/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD013_fail_stack.snap +++ b/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD013_fail_stack.snap @@ -8,4 +8,3 @@ PD013 `.melt` is preferred to `.stack`; provides same functionality 3 | x = pd.DataFrame() 4 | y = x.stack(level=-1, dropna=True) | ^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD015_fail_merge_on_pandas_object.snap b/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD015_fail_merge_on_pandas_object.snap index a812e66437..6387c0abfe 100644 --- a/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD015_fail_merge_on_pandas_object.snap +++ b/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD015_fail_merge_on_pandas_object.snap @@ -8,4 +8,3 @@ PD015 Use `.merge` method instead of `pd.merge` function. They have equivalent f 4 | y = pd.DataFrame() 5 | pd.merge(x, y) | ^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD901_fail_df_var.snap b/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD901_fail_df_var.snap index 3463a135e4..6f695929aa 100644 --- a/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD901_fail_df_var.snap +++ b/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD901_fail_df_var.snap @@ -7,4 +7,3 @@ PD901 Avoid using the generic variable name `df` for DataFrames 2 | import pandas as pd 3 | df = pd.DataFrame() | ^^ - | 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 f88853faab..0f54c1e474 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 @@ -72,24 +72,32 @@ pub(crate) fn constant_imported_as_non_constant( stmt: &Stmt, ignore_names: &IgnoreNames, ) { - if str::is_cased_uppercase(name) - && !(str::is_cased_uppercase(asname) - // Single-character names are ambiguous. - // It could be a class or a constant, so allow it to be imported - // as `SCREAMING_SNAKE_CASE` *or* `CamelCase`. - || (name.chars().nth(1).is_none() && helpers::is_camelcase(asname))) - { - // Ignore any explicitly-allowed names. - if ignore_names.matches(name) || ignore_names.matches(asname) { - return; - } - let mut diagnostic = checker.report_diagnostic( - ConstantImportedAsNonConstant { - name: name.to_string(), - asname: asname.to_string(), - }, - alias.range(), - ); - diagnostic.set_parent(stmt.start()); + if !str::is_cased_uppercase(name) { + return; } + + if str::is_cased_uppercase(asname) { + return; + } + + // Single-character names are ambiguous. + // It could be a class or a constant, so allow it to be imported + // as `SCREAMING_SNAKE_CASE` *or* `CamelCase`. + if name.chars().nth(1).is_none() && helpers::is_camelcase(asname) { + return; + } + + // Ignore any explicitly-allowed names. + if ignore_names.matches(name) || ignore_names.matches(asname) { + return; + } + + let mut diagnostic = checker.report_diagnostic( + ConstantImportedAsNonConstant { + name: name.to_string(), + asname: asname.to_string(), + }, + alias.range(), + ); + diagnostic.set_parent(stmt.start()); } diff --git a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__N803_N803.py.snap b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__N803_N803.py.snap index b0a87e09ef..66dbfd2482 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__N803_N803.py.snap +++ b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__N803_N803.py.snap @@ -24,14 +24,12 @@ N803 Argument name `A` should be lowercase 21 | @override # Incorrect usage 22 | def func(_, a, A): ... | ^ - | N803 Argument name `A` should be lowercase --> N803.py:25:21 | 25 | func = lambda _, a, A: ... | ^ - | N803 Argument name `A` should be lowercase --> N803.py:29:42 @@ -39,4 +37,3 @@ N803 Argument name `A` should be lowercase 28 | class Extended(Class): 29 | method = override(lambda self, _, a, A: ...) # Incorrect usage | ^ - | diff --git a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__N806_N806.py.snap b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__N806_N806.py.snap index cbbee871b7..e8adb0888f 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__N806_N806.py.snap +++ b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__N806_N806.py.snap @@ -68,7 +68,6 @@ N806 Variable `ValidationError` in function should be lowercase 59 | Address: Type = apps.get_model("zerver", variable) # OK 60 | ValidationError = import_string(variable) # N806 | ^^^^^^^^^^^^^^^ - | N806 Variable `BadName` in function should be lowercase --> N806.py:65:14 diff --git a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__N812_N812.py.snap b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__N812_N812.py.snap index b59c063630..20c4d3142e 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__N812_N812.py.snap +++ b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__N812_N812.py.snap @@ -26,4 +26,3 @@ N812 Lowercase `another_lowercase` imported as non-lowercase `AnotherLowercase` 2 | from mod import lowercase as Lowercase 3 | from mod import another_lowercase as AnotherLowercase | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__N813_N813.py.snap b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__N813_N813.py.snap index 1441706089..0bd3810f53 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__N813_N813.py.snap +++ b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__N813_N813.py.snap @@ -26,4 +26,3 @@ N813 Camelcase `AnotherCamelCase` imported as lowercase `another_camelcase` 2 | from mod import CamelCase as camelcase 3 | from mod import AnotherCamelCase as another_camelcase | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__N817_N817.py.snap b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__N817_N817.py.snap index 0d905c23c3..e377a28d03 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__N817_N817.py.snap +++ b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__N817_N817.py.snap @@ -15,7 +15,6 @@ N817 CamelCase `CamelCase` imported as acronym `CC` 1 | import mod.CaMel as CM 2 | from mod import CamelCase as CC | ^^^^^^^^^^^^^^^ - | N817 CamelCase `ElementTree` imported as acronym `ET` --> N817.py:10:26 @@ -23,4 +22,3 @@ N817 CamelCase `ElementTree` imported as acronym `ET` 9 | # Always an error (relative import) 10 | from ..xml.eltree import ElementTree as ET | ^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__camelcase_imported_as_incorrect_convention.snap b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__camelcase_imported_as_incorrect_convention.snap index 4899f2a8c7..eb1bf5d39e 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__camelcase_imported_as_incorrect_convention.snap +++ b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__camelcase_imported_as_incorrect_convention.snap @@ -15,7 +15,6 @@ N817 CamelCase `CamelCase` imported as acronym `CC` 1 | import mod.CaMel as CM 2 | from mod import CamelCase as CC | ^^^^^^^^^^^^^^^ - | N817 CamelCase `ElementTree` imported as acronym `ET` --> N817.py:6:8 @@ -43,4 +42,3 @@ N817 CamelCase `ElementTree` imported as acronym `ET` 9 | # Always an error (relative import) 10 | from ..xml.eltree import ElementTree as ET | ^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__ignore_names_N806_N806.py.snap b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__ignore_names_N806_N806.py.snap index c23a98ed80..2dd39d031b 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__ignore_names_N806_N806.py.snap +++ b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__ignore_names_N806_N806.py.snap @@ -18,4 +18,3 @@ N806 Variable `STILL_BAD` in function should be lowercase 5 | BAD_ALLOWED = 0 6 | STILL_BAD = 0 | ^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__ignore_names_N811_N811.py.snap b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__ignore_names_N811_N811.py.snap index 10ac76e53f..8efb21b082 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__ignore_names_N811_N811.py.snap +++ b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__ignore_names_N811_N811.py.snap @@ -17,4 +17,3 @@ N811 Constant `STILL_BAD` imported as non-constant `stillBad` 4 | from mod import BAD_ALLOWED as badAllowed 5 | from mod import STILL_BAD as stillBad | ^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__ignore_names_N812_N812.py.snap b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__ignore_names_N812_N812.py.snap index 326c27cf59..943aa1a11c 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__ignore_names_N812_N812.py.snap +++ b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__ignore_names_N812_N812.py.snap @@ -17,4 +17,3 @@ N812 Lowercase `stillbad` imported as non-lowercase `StillBad` 4 | from mod import badallowed as BadAllowed 5 | from mod import stillbad as StillBad | ^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__ignore_names_N813_N813.py.snap b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__ignore_names_N813_N813.py.snap index 7c25edab92..e800f23eb0 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__ignore_names_N813_N813.py.snap +++ b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__ignore_names_N813_N813.py.snap @@ -27,4 +27,3 @@ N813 Camelcase `StillBad` imported as lowercase `still_bad` 7 | from mod import BadAllowed as bad_allowed 8 | from mod import StillBad as still_bad | ^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__ignore_names_N814_N814.py.snap b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__ignore_names_N814_N814.py.snap index 6fe30f7180..fe8213c8b1 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__ignore_names_N814_N814.py.snap +++ b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__ignore_names_N814_N814.py.snap @@ -27,4 +27,3 @@ N814 Camelcase `StillBad` imported as constant `STILL_BAD` 7 | from mod import BadAllowed as BAD_ALLOWED 8 | from mod import StillBad as STILL_BAD | ^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__ignore_names_N815_N815.py.snap b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__ignore_names_N815_N815.py.snap index a4f746e425..60d83d962e 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__ignore_names_N815_N815.py.snap +++ b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__ignore_names_N815_N815.py.snap @@ -59,4 +59,3 @@ N815 Variable `still_Bad` in class scope should not be mixedCase 18 | bad_Allowed: set 19 | still_Bad: set | ^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__ignore_names_N816_N816.py.snap b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__ignore_names_N816_N816.py.snap index 9be58b1c56..f9a457b393 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__ignore_names_N816_N816.py.snap +++ b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__ignore_names_N816_N816.py.snap @@ -27,4 +27,3 @@ N816 Variable `still_Bad` in global scope should not be mixedCase 7 | bad_Allowed = 0 8 | still_Bad = 0 | ^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__ignore_names_N817_N817.py.snap b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__ignore_names_N817_N817.py.snap index b2284aba6f..28677a7fe7 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__ignore_names_N817_N817.py.snap +++ b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__ignore_names_N817_N817.py.snap @@ -17,4 +17,3 @@ N817 CamelCase `StillBad` imported as acronym `SB` 4 | from mod import BadAllowed as BA 5 | from mod import StillBad as SB | ^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/perflint/mod.rs b/crates/ruff_linter/src/rules/perflint/mod.rs index 6f4ae7201f..13ddca3125 100644 --- a/crates/ruff_linter/src/rules/perflint/mod.rs +++ b/crates/ruff_linter/src/rules/perflint/mod.rs @@ -12,7 +12,6 @@ mod tests { use crate::assert_diagnostics; use crate::registry::Rule; use crate::settings::LinterSettings; - use crate::settings::types::PreviewMode; use crate::test::test_path; #[test_case(Rule::UnnecessaryListCast, Path::new("PERF101.py"))] @@ -43,11 +42,9 @@ mod tests { ); let diagnostics = test_path( Path::new("perflint").join(path).as_path(), - &LinterSettings { - preview: PreviewMode::Enabled, - unresolved_target_version: PythonVersion::PY310.into(), - ..LinterSettings::for_rule(rule_code) - }, + &LinterSettings::for_rule(rule_code) + .with_preview_mode() + .with_target_version(PythonVersion::PY310), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) 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 59d0be1c2e..6f7fda9b53 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 @@ -146,7 +146,7 @@ pub(crate) fn manual_list_comprehension(checker: &Checker, for_stmt: &ast::StmtF range: _, node_index: _, }, - range, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, @@ -155,6 +155,7 @@ pub(crate) fn manual_list_comprehension(checker: &Checker, for_stmt: &ast::StmtF else { return; }; + let call_range = value.range(); if !keywords.is_empty() { return; @@ -342,7 +343,7 @@ pub(crate) fn manual_list_comprehension(checker: &Checker, for_stmt: &ast::StmtF is_async: for_stmt.is_async, comprehension_type: Some(comprehension_type), }, - *range, + call_range, ); // TODO: once this fix is stabilized, change the rule to always fixable 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 7002aa30f5..fee6b4773c 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 @@ -2,6 +2,7 @@ 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 ruff_text_size::Ranged; use crate::Violation; use crate::checkers::ast::Checker; @@ -73,7 +74,7 @@ pub(crate) fn manual_list_copy(checker: &Checker, for_stmt: &ast::StmtFor) { range: _, node_index: _, }, - range, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, @@ -82,6 +83,7 @@ pub(crate) fn manual_list_copy(checker: &Checker, for_stmt: &ast::StmtFor) { else { return; }; + let call_range = value.range(); if !keywords.is_empty() { return; @@ -126,5 +128,5 @@ pub(crate) fn manual_list_copy(checker: &Checker, for_stmt: &ast::StmtFor) { return; } - checker.report_diagnostic(ManualListCopy, *range); + checker.report_diagnostic(ManualListCopy, call_range); } 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 c374ef1b96..93a4bc9c5d 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 @@ -3,7 +3,7 @@ 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 ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::fix::edits; @@ -76,7 +76,7 @@ pub(crate) fn unnecessary_list_cast(checker: &Checker, iter: &Expr, body: &[Stmt range: _, node_index: _, }, - range: list_range, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, @@ -107,8 +107,8 @@ pub(crate) fn unnecessary_list_cast(checker: &Checker, iter: &Expr, body: &[Stmt range: iterable_range, .. }) => { - let mut diagnostic = checker.report_diagnostic(UnnecessaryListCast, *list_range); - diagnostic.set_fix(remove_cast(checker, *list_range, *iterable_range)); + let mut diagnostic = checker.report_diagnostic(UnnecessaryListCast, iter.range()); + diagnostic.set_fix(remove_cast(checker, iter.range(), *iterable_range)); } Expr::Name(ast::ExprName { id, @@ -134,8 +134,8 @@ pub(crate) fn unnecessary_list_cast(checker: &Checker, iter: &Expr, body: &[Stmt return; } - let mut diagnostic = checker.report_diagnostic(UnnecessaryListCast, *list_range); - diagnostic.set_fix(remove_cast(checker, *list_range, *iterable_range)); + let mut diagnostic = checker.report_diagnostic(UnnecessaryListCast, iter.range()); + diagnostic.set_fix(remove_cast(checker, iter.range(), *iterable_range)); } } _ => {} @@ -162,12 +162,12 @@ fn remove_cast(checker: &Checker, list_range: TextRange, iterable_range: TextRan /// A [`StatementVisitor`] that (conservatively) identifies mutations to a variable. #[derive(Default)] pub(crate) struct MutationVisitor<'a> { - pub(crate) target: &'a str, - pub(crate) is_mutated: bool, + target: &'a str, + is_mutated: bool, } impl<'a> MutationVisitor<'a> { - pub(crate) fn new(target: &'a str) -> Self { + fn new(target: &'a str) -> Self { Self { target, is_mutated: false, 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 9b92595c24..6da3feb3c0 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 @@ -8,7 +8,6 @@ PERF401 Use a list comprehension to create a transformed list 5 | if i % 2: 6 | result.append(i) # PERF401 | ^^^^^^^^^^^^^^^^ - | help: Replace for loop with list comprehension PERF401 Use a list comprehension to create a transformed list @@ -18,7 +17,6 @@ PERF401 Use a list comprehension to create a transformed list 12 | for i in items: 13 | result.append(i * i) # PERF401 | ^^^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with list comprehension PERF401 Use an async list comprehension to create a transformed list @@ -28,7 +26,6 @@ PERF401 Use an async list comprehension to create a transformed list 81 | if i % 2: 82 | result.append(i) # PERF401 | ^^^^^^^^^^^^^^^^ - | help: Replace for loop with list comprehension PERF401 Use an async list comprehension to create a transformed list @@ -38,7 +35,6 @@ PERF401 Use an async list comprehension to create a transformed list 88 | async for i in items: 89 | result.append(i) # PERF401 | ^^^^^^^^^^^^^^^^ - | help: Replace for loop with list comprehension PERF401 Use `list.extend` with an async comprehension to create a transformed list @@ -48,7 +44,6 @@ PERF401 Use `list.extend` with an async comprehension to create a transformed li 95 | async for i in items: 96 | result.append(i) # PERF401 | ^^^^^^^^^^^^^^^^ - | help: Replace for loop with list.extend PERF401 Use `list.extend` to create a transformed list @@ -58,7 +53,6 @@ PERF401 Use `list.extend` to create a transformed list 101 | for i in range(10): 102 | result.append(i * 2) # PERF401 | ^^^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with list.extend PERF401 Use `list.extend` to create a transformed list @@ -68,7 +62,6 @@ PERF401 Use `list.extend` to create a transformed list 110 | if i % 2: # single-line comment 3 should be protected 111 | result.append(i) # PERF401 | ^^^^^^^^^^^^^^^^ - | help: Replace for loop with list.extend PERF401 Use a list comprehension to create a transformed list @@ -78,7 +71,6 @@ PERF401 Use a list comprehension to create a transformed list 118 | if i % 2: # single-line comment 3 should be protected 119 | result.append(i) # PERF401 | ^^^^^^^^^^^^^^^^ - | help: Replace for loop with list comprehension PERF401 Use a list comprehension to create a transformed list @@ -88,7 +80,6 @@ PERF401 Use a list comprehension to create a transformed list 134 | for value in param: 135 | new_layers.append(value * 3) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with list comprehension PERF401 Use a list comprehension to create a transformed list @@ -98,7 +89,6 @@ PERF401 Use a list comprehension to create a transformed list 141 | for _ in range(10): 142 | result.append(var + 1) # PERF401 | ^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with list comprehension PERF401 Use a list comprehension to create a transformed list @@ -108,7 +98,6 @@ PERF401 Use a list comprehension to create a transformed list 148 | for i in range(10): 149 | result.append(i + 1) # PERF401 | ^^^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with list comprehension PERF401 Use a list comprehension to create a transformed list @@ -118,7 +107,6 @@ PERF401 Use a list comprehension to create a transformed list 155 | for i in range(10): 156 | result.append(i + 1) # PERF401 | ^^^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with list comprehension PERF401 Use a list comprehension to create a transformed list @@ -128,7 +116,6 @@ PERF401 Use a list comprehension to create a transformed list 161 | for i in range(10): 162 | result.append(i * 2) # PERF401 | ^^^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with list comprehension PERF401 Use `list.extend` to create a transformed list @@ -138,7 +125,6 @@ PERF401 Use `list.extend` to create a transformed list 168 | for i in range(10): 169 | result.append(i * 2) # PERF401 | ^^^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with list.extend PERF401 Use a list comprehension to create a transformed list @@ -160,7 +146,6 @@ PERF401 Use a list comprehension to create a transformed list 197 | for i in i: 198 | result.append(i + 1) # PERF401 | ^^^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with list comprehension PERF401 Use a list comprehension to create a transformed list @@ -176,7 +161,6 @@ PERF401 Use a list comprehension to create a transformed list 215 | | ) 216 | | ) # PERF401 | |_____________^ - | help: Replace for loop with list comprehension PERF401 Use a list comprehension to create a transformed list @@ -186,7 +170,6 @@ PERF401 Use a list comprehension to create a transformed list 221 | for i in range(10): 222 | result.append(i * 2) # PERF401 | ^^^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with list comprehension PERF401 Use a list comprehension to create a transformed list @@ -314,5 +297,4 @@ PERF401 Use a list comprehension to create a transformed list 327 | if i > 0: 328 | filtered.append(i) | ^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with list comprehension diff --git a/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF402_PERF402.py.snap b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF402_PERF402.py.snap index 670d80e933..cf55125286 100644 --- a/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF402_PERF402.py.snap +++ b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF402_PERF402.py.snap @@ -8,4 +8,3 @@ PERF402 Use `list` or `list.copy` to create a copy of a list 4 | for i in items: 5 | result.append(i) # PERF402 | ^^^^^^^^^^^^^^^^ - | 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 4a83c843e7..8b6f755c33 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 @@ -8,7 +8,6 @@ PERF403 Use a dictionary comprehension instead of a for-loop 4 | for idx, name in enumerate(fruit): 5 | result[idx] = name # PERF403 | ^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with dict comprehension PERF403 Use a dictionary comprehension instead of a for-loop @@ -18,7 +17,6 @@ PERF403 Use a dictionary comprehension instead of a for-loop 12 | if idx % 2: 13 | result[idx] = name # PERF403 | ^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with dict comprehension PERF403 Use a dictionary comprehension instead of a for-loop @@ -28,7 +26,6 @@ PERF403 Use a dictionary comprehension instead of a for-loop 32 | if idx % 2: 33 | result[idx] = name # PERF403 | ^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with dict comprehension PERF403 Use a dictionary comprehension instead of a for-loop @@ -38,7 +35,6 @@ PERF403 Use a dictionary comprehension instead of a for-loop 62 | if idx % 2: 63 | result[idx] = name # PERF403 | ^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with dict comprehension PERF403 Use a dictionary comprehension instead of a for-loop @@ -48,7 +44,6 @@ PERF403 Use a dictionary comprehension instead of a for-loop 77 | for name in fruit: 78 | result[name] = name # PERF403 | ^^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with dict comprehension PERF403 Use a dictionary comprehension instead of a for-loop @@ -58,7 +53,6 @@ PERF403 Use a dictionary comprehension instead of a for-loop 84 | for idx, name in enumerate(fruit): 85 | result[name] = idx # PERF403 | ^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with dict comprehension PERF403 Use a dictionary comprehension instead of a for-loop @@ -68,7 +62,6 @@ PERF403 Use a dictionary comprehension instead of a for-loop 93 | for idx, name in enumerate(fruit): 94 | result[name] = idx # PERF403 | ^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with dict comprehension PERF403 Use a dictionary comprehension instead of a for-loop @@ -80,7 +73,6 @@ PERF403 Use a dictionary comprehension instead of a for-loop 107 | | name # comment 4 108 | | ] = idx # PERF403 | |_______________^ - | help: Replace for loop with dict comprehension PERF403 Use a dictionary comprehension instead of a for-loop @@ -90,7 +82,6 @@ PERF403 Use a dictionary comprehension instead of a for-loop 114 | for idx, name in enumerate(fruit): 115 | result[name] = idx # PERF403 | ^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with dict comprehension PERF403 Use a dictionary comprehension instead of a for-loop @@ -100,7 +91,6 @@ PERF403 Use a dictionary comprehension instead of a for-loop 121 | for idx, name in enumerate(fruit): 122 | result[name] = idx # PERF403 | ^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with dict comprehension PERF403 Use a dictionary comprehension instead of a for-loop @@ -110,7 +100,6 @@ PERF403 Use a dictionary comprehension instead of a for-loop 128 | for idx, name in enumerate(fruit): 129 | result[name] = idx # PERF403 | ^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with dict comprehension PERF403 Use a dictionary comprehension instead of a for-loop @@ -120,7 +109,6 @@ PERF403 Use a dictionary comprehension instead of a for-loop 136 | for idx, name in enumerate(fruit): 137 | result[name] = idx # PERF403 | ^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with dict comprehension PERF403 Use a dictionary comprehension instead of a for-loop @@ -130,7 +118,6 @@ PERF403 Use a dictionary comprehension instead of a for-loop 144 | if last_idx := idx % 3: 145 | result[name] = idx # PERF403 | ^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with dict comprehension PERF403 Use a dictionary comprehension instead of a for-loop @@ -140,7 +127,6 @@ PERF403 Use a dictionary comprehension instead of a for-loop 152 | for idx, name in indices, fruit: 153 | result[name] = idx # PERF403 | ^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with dict comprehension PERF403 Use a dictionary comprehension instead of a for-loop @@ -174,7 +160,6 @@ PERF403 Use a dictionary comprehension instead of a for-loop 171 | for o,(x,)in(): 172 | v[x,]=o | ^^^^^^^ - | help: Replace for loop with dict comprehension PERF403 Use a dictionary comprehension instead of a for-loop @@ -229,5 +214,4 @@ PERF403 Use a dictionary comprehension instead of a for-loop 232 | ) in ["a", "b", "c"]: 233 | result[k] = k | ^^^^^^^^^^^^^ - | 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 60a2662ecf..11c02e0081 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 @@ -8,7 +8,6 @@ PERF401 [*] Use a list comprehension to create a transformed list 5 | if i % 2: 6 | result.append(i) # PERF401 | ^^^^^^^^^^^^^^^^ - | help: Replace for loop with list comprehension | 2 | items = [1, 2, 3, 4] @@ -28,7 +27,6 @@ PERF401 [*] Use a list comprehension to create a transformed list 12 | for i in items: 13 | result.append(i * i) # PERF401 | ^^^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with list comprehension | 10 | items = [1, 2, 3, 4] @@ -47,7 +45,6 @@ PERF401 [*] Use an async list comprehension to create a transformed list 81 | if i % 2: 82 | result.append(i) # PERF401 | ^^^^^^^^^^^^^^^^ - | help: Replace for loop with list comprehension | 78 | items = [1, 2, 3, 4] @@ -67,7 +64,6 @@ PERF401 [*] Use an async list comprehension to create a transformed list 88 | async for i in items: 89 | result.append(i) # PERF401 | ^^^^^^^^^^^^^^^^ - | help: Replace for loop with list comprehension | 86 | items = [1, 2, 3, 4] @@ -86,7 +82,6 @@ PERF401 [*] Use `list.extend` with an async comprehension to create a transforme 95 | async for i in items: 96 | result.append(i) # PERF401 | ^^^^^^^^^^^^^^^^ - | help: Replace for loop with list.extend | 94 | result = [1, 2] @@ -104,7 +99,6 @@ PERF401 [*] Use `list.extend` to create a transformed list 101 | for i in range(10): 102 | result.append(i * 2) # PERF401 | ^^^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with list.extend | 100 | result, _ = [1, 2, 3, 4], ... @@ -122,7 +116,6 @@ PERF401 [*] Use `list.extend` to create a transformed list 110 | if i % 2: # single-line comment 3 should be protected 111 | result.append(i) # PERF401 | ^^^^^^^^^^^^^^^^ - | help: Replace for loop with list.extend | 107 | if True: @@ -145,7 +138,6 @@ PERF401 [*] Use a list comprehension to create a transformed list 118 | if i % 2: # single-line comment 3 should be protected 119 | result.append(i) # PERF401 | ^^^^^^^^^^^^^^^^ - | help: Replace for loop with list comprehension | 114 | def f(): @@ -170,7 +162,6 @@ PERF401 [*] Use a list comprehension to create a transformed list 134 | for value in param: 135 | new_layers.append(value * 3) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with list comprehension | 132 | if param: @@ -189,7 +180,6 @@ PERF401 [*] Use a list comprehension to create a transformed list 141 | for _ in range(10): 142 | result.append(var + 1) # PERF401 | ^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with list comprehension | 138 | def f(): @@ -209,7 +199,6 @@ PERF401 [*] Use a list comprehension to create a transformed list 148 | for i in range(10): 149 | result.append(i + 1) # PERF401 | ^^^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with list comprehension | 146 | # make sure that `tmp` is not deleted @@ -229,7 +218,6 @@ PERF401 [*] Use a list comprehension to create a transformed list 155 | for i in range(10): 156 | result.append(i + 1) # PERF401 | ^^^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with list comprehension | 153 | # make sure that `tmp` is not deleted @@ -249,7 +237,6 @@ PERF401 [*] Use a list comprehension to create a transformed list 161 | for i in range(10): 162 | result.append(i * 2) # PERF401 | ^^^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with list comprehension | 159 | def f(): @@ -269,7 +256,6 @@ PERF401 [*] Use `list.extend` to create a transformed list 168 | for i in range(10): 169 | result.append(i * 2) # PERF401 | ^^^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with list.extend | 167 | result.append(1) @@ -308,7 +294,6 @@ PERF401 [*] Use a list comprehension to create a transformed list 197 | for i in i: 198 | result.append(i + 1) # PERF401 | ^^^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with list comprehension | 195 | i = [1, 2, 3] @@ -333,7 +318,6 @@ PERF401 [*] Use a list comprehension to create a transformed list 215 | | ) 216 | | ) # PERF401 | |_____________^ - | help: Replace for loop with list comprehension | 201 | def f(): @@ -371,7 +355,6 @@ PERF401 [*] Use a list comprehension to create a transformed list 221 | for i in range(10): 222 | result.append(i * 2) # PERF401 | ^^^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with list comprehension | 219 | def f(): @@ -609,7 +592,6 @@ PERF401 [*] Use a list comprehension to create a transformed list 327 | if i > 0: 328 | filtered.append(i) | ^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with list comprehension | 322 | original = list(range(10000)) 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 index 8935a108b6..df2817b12d 100644 --- 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 @@ -8,7 +8,6 @@ PERF403 [*] Use a dictionary comprehension instead of a for-loop 4 | for idx, name in enumerate(fruit): 5 | result[idx] = name # PERF403 | ^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with dict comprehension | 2 | fruit = ["apple", "pear", "orange"] @@ -27,7 +26,6 @@ PERF403 [*] Use a dictionary comprehension instead of a for-loop 12 | if idx % 2: 13 | result[idx] = name # PERF403 | ^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with dict comprehension | 9 | fruit = ["apple", "pear", "orange"] @@ -47,7 +45,6 @@ PERF403 [*] Use a dictionary comprehension instead of a for-loop 32 | if idx % 2: 33 | result[idx] = name # PERF403 | ^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with dict comprehension | 28 | def foo(): @@ -68,7 +65,6 @@ PERF403 [*] Use `dict.update` instead of a for-loop 62 | if idx % 2: 63 | result[idx] = name # PERF403 | ^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with `dict.update` | 60 | fruit = ["apple", "pear", "orange"] @@ -87,7 +83,6 @@ PERF403 [*] Use a dictionary comprehension instead of a for-loop 77 | for name in fruit: 78 | result[name] = name # PERF403 | ^^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with dict comprehension | 75 | fruit = ["apple", "pear", "orange"] @@ -106,7 +101,6 @@ PERF403 [*] Use a dictionary comprehension instead of a for-loop 84 | for idx, name in enumerate(fruit): 85 | result[name] = idx # PERF403 | ^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with dict comprehension | 82 | fruit = ["apple", "pear", "orange"] @@ -125,7 +119,6 @@ PERF403 [*] Use a dictionary comprehension instead of a for-loop 93 | for idx, name in enumerate(fruit): 94 | result[name] = idx # PERF403 | ^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with dict comprehension | 91 | fruit = ["apple", "pear", "orange"] @@ -146,7 +139,6 @@ PERF403 [*] Use a dictionary comprehension instead of a for-loop 107 | | name # comment 4 108 | | ] = idx # PERF403 | |_______________^ - | help: Replace for loop with dict comprehension | 98 | fruit = ["apple", "pear", "orange"] @@ -176,7 +168,6 @@ PERF403 [*] Use a dictionary comprehension instead of a for-loop 114 | for idx, name in enumerate(fruit): 115 | result[name] = idx # PERF403 | ^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with dict comprehension | 112 | fruit = ["apple", "pear", "orange"] @@ -196,7 +187,6 @@ PERF403 [*] Use `dict.update` instead of a for-loop 121 | for idx, name in enumerate(fruit): 122 | result[name] = idx # PERF403 | ^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with `dict.update` | 120 | result = {"kiwi": 3} @@ -214,7 +204,6 @@ PERF403 [*] Use `dict.update` instead of a for-loop 128 | for idx, name in enumerate(fruit): 129 | result[name] = idx # PERF403 | ^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with `dict.update` | 127 | (_, result) = (None, {"kiwi": 3}) @@ -232,7 +221,6 @@ PERF403 [*] Use `dict.update` instead of a for-loop 136 | for idx, name in enumerate(fruit): 137 | result[name] = idx # PERF403 | ^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with `dict.update` | 135 | print(len(result)) @@ -250,7 +238,6 @@ PERF403 [*] Use a dictionary comprehension instead of a for-loop 144 | if last_idx := idx % 3: 145 | result[name] = idx # PERF403 | ^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with dict comprehension | 141 | fruit = ["apple", "pear", "orange"] @@ -270,7 +257,6 @@ PERF403 [*] Use a dictionary comprehension instead of a for-loop 152 | for idx, name in indices, fruit: 153 | result[name] = idx # PERF403 | ^^^^^^^^^^^^^^^^^^ - | help: Replace for loop with dict comprehension | 150 | indices = [0, 1, 2] @@ -333,7 +319,6 @@ PERF403 [*] Use a dictionary comprehension instead of a for-loop 171 | for o,(x,)in(): 172 | v[x,]=o | ^^^^^^^ - | help: Replace for loop with dict comprehension | 169 | def foo(): @@ -438,7 +423,6 @@ PERF403 [*] Use a dictionary comprehension instead of a for-loop 232 | ) in ["a", "b", "c"]: 233 | result[k] = k | ^^^^^^^^^^^^^ - | help: Replace for loop with dict comprehension | 228 | # comment duplication in target (https://github.com/astral-sh/ruff/issues/18787) diff --git a/crates/ruff_linter/src/rules/pycodestyle/mod.rs b/crates/ruff_linter/src/rules/pycodestyle/mod.rs index 42db4738e9..ee9aa42322 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/mod.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/mod.rs @@ -108,10 +108,7 @@ mod tests { ); let diagnostics = test_path( Path::new("pycodestyle").join(path).as_path(), - &settings::LinterSettings { - preview: PreviewMode::Enabled, - ..settings::LinterSettings::for_rule(rule_code) - }, + &settings::LinterSettings::for_rule(rule_code).with_preview_mode(), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) @@ -127,10 +124,8 @@ mod tests { let tested_notebook = assert_notebook_path( &actual, &expected, - &settings::LinterSettings { - preview: PreviewMode::Enabled, - ..settings::LinterSettings::for_rule(Rule::TooManyNewlinesAtEndOfFile) - }, + &settings::LinterSettings::for_rule(Rule::TooManyNewlinesAtEndOfFile) + .with_preview_mode(), )?; assert_eq!(tested_notebook.diagnostics.len(), 3); 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 0d6ea9c1a6..3642869ef8 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 @@ -167,32 +167,32 @@ fn analyze_escape_chars( // If the next character is a valid escape sequence, skip. // See: https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals. + // + // N.B. 'N', 'u' and 'U' are escape sequences only recognized in string literals if matches!( next_char, - '\n' - | '\\' - | '\'' - | '"' - | 'a' - | 'b' - | 'f' - | 'n' - | 'r' - | 't' - | 'v' - | '0' - | '1' - | '2' - | '3' - | '4' - | '5' - | '6' - | '7' - | 'x' - // Escape sequences only recognized in string literals - | 'N' - | 'u' - | 'U' + '\n' | '\\' + | '\'' + | '"' + | 'a' + | 'b' + | 'f' + | 'n' + | 'r' + | 't' + | 'v' + | '0' + | '1' + | '2' + | '3' + | '4' + | '5' + | '6' + | '7' + | 'x' + | 'N' + | 'u' + | 'U' ) { contains_valid_escape_sequence = true; continue; 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 ffe43270b0..db28627b28 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 @@ -357,7 +357,7 @@ fn diagnostic_kind_for_operator<'a>( } fn is_whitespace_needed(kind: TokenKind) -> bool { - matches!( + if matches!( kind, TokenKind::DoubleStarEqual | TokenKind::StarEqual @@ -386,8 +386,14 @@ fn is_whitespace_needed(kind: TokenKind) -> bool { | TokenKind::ColonEqual | TokenKind::Slash | TokenKind::Percent - ) || kind.is_arithmetic() - || (kind.is_bitwise_or_shift() && - // As a special-case, pycodestyle seems to ignore whitespace around the tilde. - !matches!(kind, TokenKind::Tilde)) + ) { + return true; + } + + if kind.is_arithmetic() { + return true; + } + + // As a special-case, pycodestyle seems to ignore whitespace around the tilde. + kind.is_bitwise_or_shift() && kind != TokenKind::Tilde } 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 bb45d6379a..7d13e32047 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 @@ -118,7 +118,7 @@ pub(crate) struct LogicalLine<'a> { impl<'a> LogicalLine<'a> { /// Returns `true` if this line is positioned at the start of the file. - pub(crate) const fn is_start_of_file(&self) -> bool { + const fn is_start_of_file(&self) -> bool { self.line.tokens_start == 0 } @@ -128,7 +128,7 @@ impl<'a> LogicalLine<'a> { } /// Returns logical line's text including comments, indents, dedent and trailing new lines. - pub(crate) fn text(&self) -> &'a str { + fn text(&self) -> &'a str { let tokens = self.tokens(); match (tokens.first(), tokens.last()) { (Some(first), Some(last)) => self @@ -141,7 +141,7 @@ impl<'a> LogicalLine<'a> { /// Returns the text without any leading or trailing newline, comment, indent, or dedent of this line #[cfg(test)] - pub(crate) fn text_trimmed(&self) -> &'a str { + fn text_trimmed(&self) -> &'a str { let tokens = self.tokens_trimmed(); match (tokens.first(), tokens.last()) { @@ -153,7 +153,7 @@ impl<'a> LogicalLine<'a> { } } - pub(crate) fn tokens_trimmed(&self) -> &'a [LogicalLineToken] { + fn tokens_trimmed(&self) -> &'a [LogicalLineToken] { let tokens = self.tokens(); let start = tokens @@ -173,7 +173,7 @@ impl<'a> LogicalLine<'a> { /// Returns the text after `token` #[inline] - pub(crate) fn text_after(&self, token: &'a LogicalLineToken) -> &str { + fn text_after(&self, token: &'a LogicalLineToken) -> &str { // SAFETY: The line must have at least one token or `token` would not belong to this line. let last_token = self.tokens().last().unwrap(); self.lines @@ -183,7 +183,7 @@ impl<'a> LogicalLine<'a> { /// Returns the text before `token` #[inline] - pub(crate) fn text_before(&self, token: &'a LogicalLineToken) -> &str { + fn text_before(&self, token: &'a LogicalLineToken) -> &str { // SAFETY: The line must have at least one token or `token` would not belong to this line. let first_token = self.tokens().first().unwrap(); self.lines @@ -192,20 +192,17 @@ impl<'a> LogicalLine<'a> { } /// Returns the whitespace *after* the `token` with the byte length - pub(crate) fn trailing_whitespace( - &self, - token: &'a LogicalLineToken, - ) -> (Whitespace, TextSize) { + fn trailing_whitespace(&self, token: &'a LogicalLineToken) -> (Whitespace, TextSize) { Whitespace::leading(self.text_after(token)) } /// Returns the whitespace and whitespace byte-length *before* the `token` - pub(crate) fn leading_whitespace(&self, token: &'a LogicalLineToken) -> (Whitespace, TextSize) { + fn leading_whitespace(&self, token: &'a LogicalLineToken) -> (Whitespace, TextSize) { Whitespace::trailing(self.text_before(token)) } /// Returns all tokens of the line, including comments and trailing new lines. - pub(crate) fn tokens(&self) -> &'a [LogicalLineToken] { + fn tokens(&self) -> &'a [LogicalLineToken] { &self.lines.tokens[self.line.tokens_start as usize..self.line.tokens_end as usize] } diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E101_E101.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E101_E101.py.snap index 8f4ea63379..49d81e6d33 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E101_E101.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E101_E101.py.snap @@ -30,4 +30,3 @@ E101 Indentation contains mixed spaces and tabs 18 | # E101 19 | print("xyz"); | ^^^^^^^ - | 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 45ab478836..23a8aedbd7 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 @@ -45,17 +45,6 @@ invalid-syntax: Unexpected indentation 14 | mimetype = 'application/x-directory' | -invalid-syntax: Expected a statement - --> E11.py:14:1 - | -12 | print() -13 | #: E114 E116 -14 | mimetype = 'application/x-directory' - | ^ -15 | # 'httpd/unix-directory' -16 | create_date = False - | - invalid-syntax: Expected an indented block after `if` statement --> E11.py:45:1 | 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 b2544e4212..791dea7b0f 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 @@ -34,17 +34,6 @@ invalid-syntax: Unexpected indentation 14 | mimetype = 'application/x-directory' | -invalid-syntax: Expected a statement - --> E11.py:14:1 - | -12 | print() -13 | #: E114 E116 -14 | mimetype = 'application/x-directory' - | ^ -15 | # 'httpd/unix-directory' -16 | create_date = False - | - E112 Expected an indented block --> E11.py:45:1 | 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 4fdbcf5e3c..2684210981 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 @@ -34,17 +34,6 @@ invalid-syntax: Unexpected indentation 14 | mimetype = 'application/x-directory' | -invalid-syntax: Expected a statement - --> E11.py:14:1 - | -12 | print() -13 | #: E114 E116 -14 | mimetype = 'application/x-directory' - | ^ -15 | # 'httpd/unix-directory' -16 | create_date = False - | - invalid-syntax: Expected an indented block after `if` statement --> E11.py:45:1 | 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 06eb01f682..5829e15a10 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 @@ -23,17 +23,6 @@ invalid-syntax: Unexpected indentation 14 | mimetype = 'application/x-directory' | -invalid-syntax: Expected a statement - --> E11.py:14:1 - | -12 | print() -13 | #: E114 E116 -14 | mimetype = 'application/x-directory' - | ^ -15 | # 'httpd/unix-directory' -16 | create_date = False - | - E114 Indentation is not a multiple of 4 (comment) --> E11.py:15:1 | 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 b4655dd131..223bfca4b0 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 @@ -23,17 +23,6 @@ invalid-syntax: Unexpected indentation 14 | mimetype = 'application/x-directory' | -invalid-syntax: Expected a statement - --> E11.py:14:1 - | -12 | print() -13 | #: E114 E116 -14 | mimetype = 'application/x-directory' - | ^ -15 | # 'httpd/unix-directory' -16 | create_date = False - | - E115 Expected an indented block (comment) --> E11.py:30:1 | 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 f70307eaa6..33ae7f29b7 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 @@ -23,17 +23,6 @@ invalid-syntax: Unexpected indentation 14 | mimetype = 'application/x-directory' | -invalid-syntax: Expected a statement - --> E11.py:14:1 - | -12 | print() -13 | #: E114 E116 -14 | mimetype = 'application/x-directory' - | ^ -15 | # 'httpd/unix-directory' -16 | create_date = False - | - E116 Unexpected indentation (comment) --> E11.py:15:1 | 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 10f126e602..aa9f692840 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 @@ -34,17 +34,6 @@ invalid-syntax: Unexpected indentation 14 | mimetype = 'application/x-directory' | -invalid-syntax: Expected a statement - --> E11.py:14:1 - | -12 | print() -13 | #: E114 E116 -14 | mimetype = 'application/x-directory' - | ^ -15 | # 'httpd/unix-directory' -16 | create_date = False - | - E117 Over-indented --> E11.py:39:1 | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E203_E20.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E203_E20.py.snap index 3724f16449..b410c5738c 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E203_E20.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E203_E20.py.snap @@ -301,7 +301,6 @@ E203 [*] Whitespace before ':' 204 | #: E203:1:13 205 | t"{ham[lower + 1 :, "columnname"]}" | ^^ - | help: Remove whitespace before ':' | 204 | #: E203:1:13 diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E231_E23.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E231_E23.py.snap index 1310cc21d5..ae646c1068 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E231_E23.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E231_E23.py.snap @@ -799,7 +799,6 @@ E231 [*] Missing whitespace after `:` 160 | #: E231 161 | {len(t's3://{self.s3_bucket_name}/'):1} | ^ - | help: Add missing whitespace | 160 | #: E231 diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E241_E24.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E241_E24.py.snap index 1f0b1dfb8d..3e569d5870 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E241_E24.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E241_E24.py.snap @@ -60,7 +60,6 @@ E241 [*] Multiple spaces after comma 12 | ef, +h, 13 | c, -d] | ^^^ - | help: Replace with single space | 12 | ef, +h, diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E262_E26.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E262_E26.py.snap index 18c9ee84f7..4d68b8a223 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E262_E26.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E262_E26.py.snap @@ -116,7 +116,6 @@ E262 [*] Inline comment should start with `# ` 85 | 86 | a = 1 #:Foo | ^^^^^ - | help: Format space | 85 | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E301_E30_syntax_error.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E301_E30_syntax_error.py.snap index b8c6413c1d..c0ad46e9f3 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E301_E30_syntax_error.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E301_E30_syntax_error.py.snap @@ -39,7 +39,6 @@ invalid-syntax: Expected `)`, found newline 17 | 18 | foo = Foo( | ^ - | invalid-syntax: Expected `)`, found newline --> E30_syntax_error.py:21:9 diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E302_E30_syntax_error.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E302_E30_syntax_error.py.snap index 76c3d31211..f5afe14cc3 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E302_E30_syntax_error.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E302_E30_syntax_error.py.snap @@ -39,7 +39,6 @@ invalid-syntax: Expected `)`, found newline 17 | 18 | foo = Foo( | ^ - | invalid-syntax: Expected `)`, found newline --> E30_syntax_error.py:21:9 diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E303_E30.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E303_E30.py.snap index d388d8d38a..07c466c48c 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E303_E30.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E303_E30.py.snap @@ -80,7 +80,6 @@ E303 [*] Too many blank lines (2) | 687 | # comment | ^^^^^^^^^ - | help: Remove extraneous blank line(s) | 685 | @@ -153,7 +152,6 @@ E303 [*] Too many blank lines (2) | 731 | # comment | ^^^^^^^^^ - | help: Remove extraneous blank line(s) | 729 | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E303_E30_syntax_error.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E303_E30_syntax_error.py.snap index af23f16de9..18ae95026c 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E303_E30_syntax_error.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E303_E30_syntax_error.py.snap @@ -38,7 +38,6 @@ invalid-syntax: Expected `)`, found newline 17 | 18 | foo = Foo( | ^ - | invalid-syntax: Expected `)`, found newline --> E30_syntax_error.py:21:9 diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E305_E30_syntax_error.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E305_E30_syntax_error.py.snap index f72c198e1e..c3ef5976fe 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E305_E30_syntax_error.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E305_E30_syntax_error.py.snap @@ -28,7 +28,6 @@ E305 Expected 2 blank lines after class or function definition, found (1) 17 | 18 | foo = Foo( | ^^^ - | help: Add missing blank line(s) invalid-syntax: Expected `)`, found newline @@ -38,7 +37,6 @@ invalid-syntax: Expected `)`, found newline 17 | 18 | foo = Foo( | ^ - | invalid-syntax: Expected `)`, found newline --> E30_syntax_error.py:21:9 diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E306_E30_syntax_error.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E306_E30_syntax_error.py.snap index 98d00f77af..05a9d59462 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E306_E30_syntax_error.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E306_E30_syntax_error.py.snap @@ -28,7 +28,6 @@ invalid-syntax: Expected `)`, found newline 17 | 18 | foo = Foo( | ^ - | invalid-syntax: Expected `)`, found newline --> E30_syntax_error.py:21:9 diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E401_E40.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E401_E40.py.snap index 664bbf64e4..2d8883eb25 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E401_E40.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E401_E40.py.snap @@ -61,7 +61,6 @@ E401 [*] Multiple imports on one line 67 | 68 | x = 1; import re as regex, string | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Split imports | 67 | @@ -147,7 +146,6 @@ E401 [*] Multiple imports on one line 79 | 80 | if True: import re as regex, string | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Split imports | 79 | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E402_E40.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E402_E40.py.snap index e5e1c75287..cd3266b6d3 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E402_E40.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E402_E40.py.snap @@ -66,5 +66,4 @@ E402 Module level import not at top of file 67 | 68 | x = 1; import re as regex, string | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Move module level imports to top of file diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E402_E402_0.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E402_E402_0.py.snap index 758432087d..ffe25e1169 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E402_E402_0.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E402_E402_0.py.snap @@ -8,7 +8,6 @@ E402 Module level import not at top of file 34 | 35 | import h | ^^^^^^^^ - | help: Move module level imports to top of file E402 Module level import not at top of file @@ -18,7 +17,6 @@ E402 Module level import not at top of file 44 | 45 | import k; import l | ^^^^^^^^ - | help: Move module level imports to top of file E402 Module level import not at top of file @@ -28,5 +26,4 @@ E402 Module level import not at top of file 44 | 45 | import k; import l | ^^^^^^^^ - | help: Move module level imports to top of file diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E402_E402_1.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E402_E402_1.py.snap index 1309f16b55..6fd705b918 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E402_E402_1.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E402_E402_1.py.snap @@ -20,5 +20,4 @@ E402 Module level import not at top of file 8 | 9 | import c | ^^^^^^^^ - | help: Move module level imports to top of file diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E501_E501.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E501_E501.py.snap index 52d785c025..5ee9e29cfa 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E501_E501.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E501_E501.py.snap @@ -17,7 +17,6 @@ E501 Line too long (95 > 88) 15 | _ = "---------------------------------------------------------------------------AAAAAAA" 16 | _ = "---------------------------------------------------------------------------亜亜亜亜亜亜亜" | ^^^^^^^ - | E501 Line too long (127 > 88) --> E501.py:25:89 diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E501_E501_3.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E501_E501_3.py.snap index 3ef339ef61..c381e8de6b 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E501_E501_3.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E501_E501_3.py.snap @@ -17,4 +17,3 @@ E501 Line too long (89 > 88) 16 | # Error (89 characters) 17 | "shape:" + "shape:" + "shape:" + "shape:" + "shape:" + "shape:" + "shape:" + "shape:aaaa" # pyrefly: ignore[missing-attribute] | ^ - | 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 8729718f1c..f74afe8f31 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__E713_E713.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E713_E713.py.snap index 278425f454..b717528565 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E713_E713.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E713_E713.py.snap @@ -96,7 +96,6 @@ E713 [*] Test for membership should be `not in` 39 | assert not (re.search(r"^.:\\Users\\[^\\]*\\Downloads\\.*") is None) 40 | assert not('name' in request)or not request['name'] | ^^^^^^^^^^^^^^^^^ - | help: Convert to `not in` | 39 | assert not (re.search(r"^.:\\Users\\[^\\]*\\Downloads\\.*") is None) diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E714_E714.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E714_E714.py.snap index b4a022023d..3c59266b4c 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E714_E714.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E714_E714.py.snap @@ -42,7 +42,6 @@ E714 [*] Test for object identity should be `is not` 38 | assert [42, not foo] in bar 39 | assert not (re.search(r"^.:\\Users\\[^\\]*\\Downloads\\.*") is None) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Convert to `is not` | 38 | assert [42, not foo] in bar 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 e54e545896..90286da679 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 @@ -8,7 +8,6 @@ E731 [*] Do not assign a `lambda` expression, use a `def` 2 | # E731 3 | f = lambda x: 2 * x | ^^^^^^^^^^^^^^^^^^^ - | help: Rewrite `f` as a `def` | 2 | # E731 @@ -26,7 +25,6 @@ E731 [*] Do not assign a `lambda` expression, use a `def` 7 | # E731 8 | f = lambda x: 2 * x | ^^^^^^^^^^^^^^^^^^^ - | help: Rewrite `f` as a `def` | 7 | # E731 @@ -44,7 +42,6 @@ E731 [*] Do not assign a `lambda` expression, use a `def` 13 | while False: 14 | this = lambda y, z: 2 * x | ^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Rewrite `this` as a `def` | 13 | while False: @@ -62,7 +59,6 @@ E731 [*] Do not assign a `lambda` expression, use a `def` 18 | # E731 19 | f = lambda: (yield 1) | ^^^^^^^^^^^^^^^^^^^^^ - | help: Rewrite `f` as a `def` | 18 | # E731 @@ -80,7 +76,6 @@ E731 [*] Do not assign a `lambda` expression, use a `def` 23 | # E731 24 | f = lambda: (yield from g()) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Rewrite `f` as a `def` | 23 | # E731 @@ -98,7 +93,6 @@ E731 [*] Do not assign a `lambda` expression, use a `def` 56 | # E731 57 | f = lambda x: 2 * x | ^^^^^^^^^^^^^^^^^^^ - | help: Rewrite `f` as a `def` | 56 | # E731 @@ -155,7 +149,6 @@ E731 [*] Do not assign a `lambda` expression, use a `def` 85 | P = ParamSpec("P") 86 | f: Callable[P, int] = lambda *args: len(args) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Rewrite `f` as a `def` | 85 | P = ParamSpec("P") @@ -173,7 +166,6 @@ E731 [*] Do not assign a `lambda` expression, use a `def` 93 | 94 | f: Callable[[], None] = lambda: None | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Rewrite `f` as a `def` | 93 | @@ -191,7 +183,6 @@ E731 [*] Do not assign a `lambda` expression, use a `def` 101 | 102 | f: Callable[..., None] = lambda a, b: None | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Rewrite `f` as a `def` | 101 | @@ -209,7 +200,6 @@ E731 [*] Do not assign a `lambda` expression, use a `def` 109 | 110 | f: Callable[[int], int] = lambda x: 2 * x | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Rewrite `f` as a `def` | 109 | @@ -227,7 +217,6 @@ E731 [*] Do not assign a `lambda` expression, use a `def` 118 | 119 | f: Callable[[str, int], str] = lambda a, b: a * b | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Rewrite `f` as a `def` | 118 | @@ -245,7 +234,6 @@ E731 [*] Do not assign a `lambda` expression, use a `def` 126 | 127 | f: Callable[[str, int], tuple[str, int]] = lambda a, b: (a, b) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Rewrite `f` as a `def` | 126 | @@ -263,7 +251,6 @@ E731 [*] Do not assign a `lambda` expression, use a `def` 134 | 135 | f: Callable[[str, int, list[str]], list[str]] = lambda a, b, /, c: [*c, a * b] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Rewrite `f` as a `def` | 134 | @@ -299,7 +286,6 @@ E731 [*] Do not assign a `lambda` expression, use a `def` 139 | CELSIUS = (lambda deg_c: deg_c) 140 | FAHRENHEIT = (lambda deg_c: deg_c * 9 / 5 + 32) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Rewrite `FAHRENHEIT` as a `def` | 139 | CELSIUS = (lambda deg_c: deg_c) @@ -319,7 +305,6 @@ E731 [*] Do not assign a `lambda` expression, use a `def` 148 | | i := 1, 149 | | ) | |_____^ - | help: Rewrite `f` as a `def` | 146 | @@ -504,7 +489,6 @@ E731 [*] Do not assign a `lambda` expression, use a `def` 200 | f1: Callable[P, str] = lambda x: str(x) 201 | f2: Callable[..., str] = lambda x: str(x) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Rewrite `f2` as a `def` | 200 | f1: Callable[P, str] = lambda x: str(x) diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E741_E741.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E741_E741.py.snap index 70e66d2aec..bff4447600 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E741_E741.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E741_E741.py.snap @@ -119,7 +119,6 @@ E741 Ambiguous variable name: `l` 25 | global l 26 | l = 0 | ^ - | E741 Ambiguous variable name: `l` --> E741.py:30:5 diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W191_W19.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W191_W19.py.snap index 8d88eef99b..5f5763200d 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W191_W19.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W191_W19.py.snap @@ -17,16 +17,6 @@ invalid-syntax: Unexpected indentation 2 | multiline string with tab in it''' | -invalid-syntax: Expected a statement - --> W19.py:5:1 - | -4 | #: W191 -5 | if False: - | ^ -6 | print # indented with 1 tab -7 | #: - | - W191 Indentation contains tabs --> W19.py:6:1 | @@ -271,7 +261,6 @@ W191 Indentation contains tabs 94 | return options.max_line_length, \ 95 | "E501 line too long (%d characters)" % length | ^^^^^^^^ - | W191 Indentation contains tabs --> W19.py:101:1 @@ -302,7 +291,6 @@ W191 Indentation contains tabs 127 | blah == 'yeah': 128 | blah = 'yeahnah' | ^^^^ - | W191 Indentation contains tabs --> W19.py:134:1 diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W292_W292_0.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W292_W292_0.py.snap index 107cf211a3..439b3a2591 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W292_W292_0.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W292_W292_0.py.snap @@ -7,7 +7,6 @@ W292 [*] No newline at end of file 1 | def fn() -> None: 2 | pass | ^ - | help: Add trailing newline | 1 | def fn() -> None: diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W293_W293.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W293_W293.py.snap index 801c1b0687..b06d8bac20 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W293_W293.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W293_W293.py.snap @@ -26,7 +26,6 @@ W293 [*] Blank line contains whitespace 9 | \ 10 | | ^^^^ - | help: Remove whitespace from blank line | 7 | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W605_W605_0.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W605_W605_0.py.snap index 08cd83d2d7..b4f217c17f 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W605_W605_0.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W605_W605_0.py.snap @@ -76,7 +76,6 @@ W605 [*] Invalid escape sequence: `\_` 22 | #: W605:1:38 23 | value = 'new line\nand invalid escape \_ here' | ^^ - | help: Add backslash to escape sequence | 22 | #: W605:1:38 @@ -178,7 +177,6 @@ W605 [*] Invalid escape sequence: `\.` 59 | #: W605:1:13 60 | "foo \t bar \." | ^^ - | help: Add backslash to escape sequence | 59 | #: W605:1:13 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 1556d76ad9..3d261c66d5 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 @@ -76,7 +76,6 @@ W605 [*] Invalid escape sequence: `\_` 24 | #: W605:1:38 25 | value = f'new line\nand invalid escape \_ here' | ^^ - | help: Add backslash to escape sequence | 24 | #: W605:1:38 @@ -267,7 +266,6 @@ W605 [*] Invalid escape sequence: `\I` 67 | # Debug text (should trigger) 68 | t = f"{'\InHere'=}" | ^^ - | help: Use a raw string literal | 67 | # Debug text (should trigger) @@ -351,7 +349,6 @@ W605 [*] Invalid escape sequence: `\_` 93 | #: W605:1:38 94 | value = t'new line\nand invalid escape \_ here' | ^^ - | help: Add backslash to escape sequence | 93 | #: W605:1:38 @@ -542,7 +539,6 @@ W605 [*] Invalid escape sequence: `\I` 136 | # Debug text (should trigger) 137 | t = t"{'\InHere'=}" | ^^ - | help: Use a raw string literal | 136 | # Debug text (should trigger) diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_E304_typing_stub.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_E304_typing_stub.snap index 24b5b733de..57299e6af3 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_E304_typing_stub.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_E304_typing_stub.snap @@ -8,7 +8,6 @@ E304 [*] Blank lines found after function decorator (1) 31 | 32 | def with_blank_line(): ... | ^^^ - | help: Remove extraneous blank line(s) | 30 | @decorated diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_top_level_isort_compatibility-lines-after(-1)-between(0).snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_top_level_isort_compatibility-lines-after(-1)-between(0).snap index 1ec3eb63ca..56db367a6b 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_top_level_isort_compatibility-lines-after(-1)-between(0).snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_top_level_isort_compatibility-lines-after(-1)-between(0).snap @@ -10,7 +10,6 @@ I001 [*] Import block is un-sorted or un-formatted 4 | | 5 | | from typing import Any, Sequence | |________________________________^ - | help: Organize imports | 1 | import json @@ -47,7 +46,6 @@ I001 [*] Import block is un-sorted or un-formatted 29 | | 30 | | from typing_extensions import TypeAlias | |___________________________________________^ - | help: Organize imports | 27 | @@ -110,7 +108,6 @@ E302 [*] Expected 2 blank lines, found 1 61 | 62 | class MissingCommand(TypeError): ... # noqa: N818 | ^^^^^ - | help: Add missing blank line(s) | 61 | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_top_level_isort_compatibility-lines-after(0)-between(0).snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_top_level_isort_compatibility-lines-after(0)-between(0).snap index fdb2845a32..aef8433328 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_top_level_isort_compatibility-lines-after(0)-between(0).snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_top_level_isort_compatibility-lines-after(0)-between(0).snap @@ -10,7 +10,6 @@ I001 [*] Import block is un-sorted or un-formatted 4 | | 5 | | from typing import Any, Sequence | |________________________________^ - | help: Organize imports | 1 | import json @@ -50,7 +49,6 @@ I001 [*] Import block is un-sorted or un-formatted 29 | | 30 | | from typing_extensions import TypeAlias | |___________________________________________^ - | help: Organize imports | 27 | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_top_level_isort_compatibility-lines-after(1)-between(1).snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_top_level_isort_compatibility-lines-after(1)-between(1).snap index 4641dae720..d1de6ad604 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_top_level_isort_compatibility-lines-after(1)-between(1).snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_top_level_isort_compatibility-lines-after(1)-between(1).snap @@ -10,7 +10,6 @@ I001 [*] Import block is un-sorted or un-formatted 4 | | 5 | | from typing import Any, Sequence | |________________________________^ - | help: Organize imports | 2 | @@ -48,7 +47,6 @@ I001 [*] Import block is un-sorted or un-formatted 29 | | 30 | | from typing_extensions import TypeAlias | |___________________________________________^ - | help: Organize imports | 27 | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_top_level_isort_compatibility-lines-after(4)-between(4).snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_top_level_isort_compatibility-lines-after(4)-between(4).snap index 180f36e2d0..7f1cfd0f15 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_top_level_isort_compatibility-lines-after(4)-between(4).snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_top_level_isort_compatibility-lines-after(4)-between(4).snap @@ -10,7 +10,6 @@ I001 [*] Import block is un-sorted or un-formatted 4 | | 5 | | from typing import Any, Sequence | |________________________________^ - | help: Organize imports | 4 | @@ -28,7 +27,6 @@ E302 [*] Expected 4 blank lines, found 2 | 8 | class MissingCommand(TypeError): ... # noqa: N818 | ^^^^^ - | help: Add missing blank line(s) | 7 | @@ -64,7 +62,6 @@ I001 [*] Import block is un-sorted or un-formatted 29 | | 30 | | from typing_extensions import TypeAlias | |___________________________________________^ - | help: Organize imports | 27 | @@ -129,7 +126,6 @@ E302 [*] Expected 4 blank lines, found 1 61 | 62 | class MissingCommand(TypeError): ... # noqa: N818 | ^^^^^ - | help: Add missing blank line(s) | 61 | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_typing_stub_isort.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_typing_stub_isort.snap index 49e8ddcff2..b440fb7b7f 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_typing_stub_isort.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_typing_stub_isort.snap @@ -10,7 +10,6 @@ I001 [*] Import block is un-sorted or un-formatted 4 | | 5 | | from typing import Any, Sequence | |________________________________^ - | help: Organize imports | 1 | import json @@ -27,7 +26,6 @@ E303 [*] Too many blank lines (3) | 5 | from typing import Any, Sequence | ^^^^ - | help: Remove extraneous blank line(s) | 2 | @@ -41,7 +39,6 @@ E303 [*] Too many blank lines (2) | 8 | class MissingCommand(TypeError): ... # noqa: N818 | ^^^^^ - | help: Remove extraneous blank line(s) | 6 | @@ -103,7 +100,6 @@ I001 [*] Import block is un-sorted or un-formatted 29 | | 30 | | from typing_extensions import TypeAlias | |___________________________________________^ - | help: Organize imports | 27 | @@ -117,7 +113,6 @@ E303 [*] Too many blank lines (3) | 30 | from typing_extensions import TypeAlias | ^^^^ - | help: Remove extraneous blank line(s) | 27 | @@ -146,7 +141,6 @@ E303 [*] Too many blank lines (2) | 45 | def _exit(self) -> None: ... | ^^^ - | help: Remove extraneous blank line(s) | 43 | @@ -159,7 +153,6 @@ E303 [*] Too many blank lines (2) | 48 | def _optional_commands(self) -> dict[str, bool]: ... | ^^^ - | help: Remove extraneous blank line(s) | 46 | @@ -172,7 +165,6 @@ E303 [*] Too many blank lines (2) | 51 | def run(argv: Sequence[str]) -> int: ... | ^^^ - | help: Remove extraneous blank line(s) | 49 | @@ -185,7 +177,6 @@ E303 [*] Too many blank lines (2) | 54 | def read_line(fd: int = 0) -> bytearray: ... | ^^^ - | help: Remove extraneous blank line(s) | 52 | @@ -198,7 +189,6 @@ E303 [*] Too many blank lines (2) | 57 | def flush() -> None: ... | ^^^ - | help: Remove extraneous blank line(s) | 55 | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__max_doc_length.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__max_doc_length.snap index 434e683b80..f6b8c41454 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__max_doc_length.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__max_doc_length.snap @@ -7,7 +7,6 @@ W505 Doc line too long (57 > 50) 1 | #!/usr/bin/env python3 2 | """Here's a top-level docstring that's over the limit.""" | ^^^^^^^ - | W505 Doc line too long (56 > 50) --> W505.py:6:51 @@ -45,7 +44,6 @@ W505 Doc line too long (61 > 50) | 18 | "This is also considered a docstring, and is over the limit." | ^^^^^^^^^^^ - | W505 Doc line too long (82 > 50) --> W505.py:24:51 @@ -64,4 +62,3 @@ W505 Doc line too long (85 > 50) 30 | 31 | It's over the limit on this line, which isn't the first line in the docstring.""" | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__max_doc_length_with_utf_8.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__max_doc_length_with_utf_8.snap index 20d826c6d5..41a99a2760 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__max_doc_length_with_utf_8.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__max_doc_length_with_utf_8.snap @@ -7,7 +7,6 @@ W505 Doc line too long (57 > 50) 1 | #!/usr/bin/env python3 2 | """Here's a top-level ß9💣2ℝing that's over theß9💣2ℝ.""" | ^^^^^^ - | W505 Doc line too long (56 > 50) --> W505_utf_8.py:6:49 @@ -45,7 +44,6 @@ W505 Doc line too long (61 > 50) | 18 | "This is also considered a ß9💣2ℝing, and is over theß9💣2ℝ." | ^^^^^^^^^^^ - | W505 Doc line too long (82 > 50) --> W505_utf_8.py:24:50 @@ -64,4 +62,3 @@ W505 Doc line too long (85 > 50) 30 | 31 | It's over theß9💣2ℝ on this line, which isn't the first line in the ß9💣2ℝing.""" | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E40.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E40.py.snap index 178481cc69..565c52fbff 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E40.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E40.py.snap @@ -106,5 +106,4 @@ E402 Module level import not at top of file 67 | 68 | x = 1; import re as regex, string | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Move module level imports to top of file diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_0.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_0.py.snap index 7701ecae60..dcc852a6d6 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_0.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_0.py.snap @@ -8,7 +8,6 @@ E402 [*] Module level import not at top of file 34 | 35 | import h | ^^^^^^^^ - | help: Move module level imports to top of file | 1 | """Top-level docstring.""" @@ -28,7 +27,6 @@ E402 Module level import not at top of file 44 | 45 | import k; import l | ^^^^^^^^ - | help: Move module level imports to top of file E402 Module level import not at top of file @@ -38,5 +36,4 @@ E402 Module level import not at top of file 44 | 45 | import k; import l | ^^^^^^^^ - | help: Move module level imports to top of file diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_1.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_1.py.snap index 45ed06923e..24250ad891 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_1.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_1.py.snap @@ -30,7 +30,6 @@ E402 [*] Module level import not at top of file 8 | 9 | import c | ^^^^^^^^ - | help: Move module level imports to top of file | 1 + import c diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_comments.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_comments.py.snap index 605ceda0f9..58a281d0fb 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_comments.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_comments.py.snap @@ -136,7 +136,6 @@ E402 [*] Module level import not at top of file 24 | | value3, 25 | | ) | |_^ - | help: Move module level imports to top of file | 1 + from late_paren4 import ( diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_docstring.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_docstring.py.snap index 1fc4af4423..11a8669004 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_docstring.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_docstring.py.snap @@ -8,7 +8,6 @@ E402 [*] Module level import not at top of file 4 | 5 | import os | ^^^^^^^^^ - | help: Move module level imports to top of file | 1 | """module docstring""" diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_future.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_future.py.snap index e7598f7f42..398b25c7f7 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_future.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_future.py.snap @@ -8,7 +8,6 @@ E402 [*] Module level import not at top of file 4 | 5 | import os | ^^^^^^^^^ - | help: Move module level imports to top of file | 1 | from __future__ import annotations diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_shebang.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_shebang.py.snap index 0fa105895f..7c270535eb 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_shebang.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_shebang.py.snap @@ -8,7 +8,6 @@ E402 [*] Module level import not at top of file 4 | 5 | import os | ^^^^^^^^^ - | help: Move module level imports to top of file | 2 | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_shebang_docstring_and_future.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_shebang_docstring_and_future.py.snap index 1f30b27fde..d3b7eb8200 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_shebang_docstring_and_future.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_shebang_docstring_and_future.py.snap @@ -8,7 +8,6 @@ E402 [*] Module level import not at top of file 8 | 9 | import os | ^^^^^^^^^ - | help: Move module level imports to top of file | 5 | from __future__ import annotations diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E501_E501_5.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E501_E501_5.py.snap index e7fa6a9f74..3688b98ee9 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E501_E501_5.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E501_E501_5.py.snap @@ -17,4 +17,3 @@ E501 Line too long (89 > 88) 13 | # Error - trailing type: ignore after another comment (89 characters before pragma) 14 | "shape:" + "shape:" + "shape:" + "shape:" + "shape:" + "shape:" + "shape:aaaa" # comment # type: ignore | ^ - | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391.ipynb.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391.ipynb.snap index f6dc7f56ff..bb07af429c 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391.ipynb.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391.ipynb.snap @@ -9,7 +9,7 @@ W391 [*] Too many newlines at end of cell 5 | / 6 | | 7 | | - | |__^ + | |_^ 8 | 9 | 1 + 1 | @@ -31,7 +31,7 @@ W391 [*] Too many newlines at end of cell 12 | | 13 | | 14 | | - | |__^ + | |_^ 15 | 16 | 1+1 | @@ -50,8 +50,7 @@ W391 [*] Too many newlines at end of cell | 19 | / 20 | | - | |__^ - | + | |_^ help: Remove trailing newlines | 19 | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_0.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_0.py.snap index 343bb3d28b..571d9027bf 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_0.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_0.py.snap @@ -8,7 +8,6 @@ W391 [*] Extra newline at end of file 13 | bar() 14 | | ^ - | help: Remove trailing newline | 13 | bar() diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_2.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_2.py.snap index 265c3e0eb0..a53365244c 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_2.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_2.py.snap @@ -10,8 +10,7 @@ W391 [*] Too many newlines at end of file 15 | | 16 | | 17 | | - | |__^ - | + | |_^ help: Remove trailing newlines | 13 | bar() diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__tab_size_1.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__tab_size_1.snap index f9cc43321a..099d35b8df 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__tab_size_1.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__tab_size_1.snap @@ -51,4 +51,3 @@ E501 Line too long (7 > 6) 15 | [1,2] 16 | [1, 2] | ^ - | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__tab_size_2.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__tab_size_2.snap index ff4b41e3d8..0cb88215a4 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__tab_size_2.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__tab_size_2.snap @@ -94,4 +94,3 @@ E501 Line too long (8 > 6) 15 | [1,2] 16 | [1, 2] | ^^ - | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__tab_size_4.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__tab_size_4.snap index 6eedca02da..2f61eeb298 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__tab_size_4.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__tab_size_4.snap @@ -116,4 +116,3 @@ E501 Line too long (10 > 6) 15 | [1,2] 16 | [1, 2] | ^^^^ - | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__tab_size_8.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__tab_size_8.snap index d1f68fe2db..ad5c60f7e2 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__tab_size_8.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__tab_size_8.snap @@ -116,4 +116,3 @@ E501 Line too long (14 > 6) 15 | [1,2] 16 | [1, 2] | ^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__task_tags_false.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__task_tags_false.snap index f7526d15b7..9f0687997f 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__task_tags_false.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__task_tags_false.snap @@ -81,4 +81,3 @@ E501 Line too long (159 > 88) 7 | …ed task-tags sometimes are longer than line-length so that you can easily find them with `git grep` 8 | …gured task-tags sometimes are longer than line-length so that you can easily find them with `git grep` | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__too_many_blank_lines_isort_compatibility-lines-after(-1)-between(0).snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__too_many_blank_lines_isort_compatibility-lines-after(-1)-between(0).snap index 06d473fbd2..426d10c767 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__too_many_blank_lines_isort_compatibility-lines-after(-1)-between(0).snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__too_many_blank_lines_isort_compatibility-lines-after(-1)-between(0).snap @@ -10,7 +10,6 @@ I001 [*] Import block is un-sorted or un-formatted 4 | | 5 | | from typing import Any, Sequence | |________________________________^ - | help: Organize imports | 1 | import json @@ -25,7 +24,6 @@ E303 [*] Too many blank lines (3) | 5 | from typing import Any, Sequence | ^^^^ - | help: Remove extraneous blank line(s) | 3 | @@ -58,7 +56,6 @@ I001 [*] Import block is un-sorted or un-formatted 29 | | 30 | | from typing_extensions import TypeAlias | |___________________________________________^ - | help: Organize imports | 27 | @@ -72,7 +69,6 @@ E303 [*] Too many blank lines (3) | 30 | from typing_extensions import TypeAlias | ^^^^ - | help: Remove extraneous blank line(s) | 27 | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__too_many_blank_lines_isort_compatibility-lines-after(0)-between(0).snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__too_many_blank_lines_isort_compatibility-lines-after(0)-between(0).snap index 226c58725f..783c9fd045 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__too_many_blank_lines_isort_compatibility-lines-after(0)-between(0).snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__too_many_blank_lines_isort_compatibility-lines-after(0)-between(0).snap @@ -10,7 +10,6 @@ I001 [*] Import block is un-sorted or un-formatted 4 | | 5 | | from typing import Any, Sequence | |________________________________^ - | help: Organize imports | 1 | import json @@ -28,7 +27,6 @@ E303 [*] Too many blank lines (3) | 5 | from typing import Any, Sequence | ^^^^ - | help: Remove extraneous blank line(s) | 3 | @@ -41,7 +39,6 @@ E303 [*] Too many blank lines (2) | 8 | class MissingCommand(TypeError): ... # noqa: N818 | ^^^^^ - | help: Remove extraneous blank line(s) | 5 | from typing import Any, Sequence @@ -75,7 +72,6 @@ I001 [*] Import block is un-sorted or un-formatted 29 | | 30 | | from typing_extensions import TypeAlias | |___________________________________________^ - | help: Organize imports | 27 | @@ -89,7 +85,6 @@ E303 [*] Too many blank lines (3) | 30 | from typing_extensions import TypeAlias | ^^^^ - | help: Remove extraneous blank line(s) | 27 | @@ -135,7 +130,6 @@ E303 [*] Too many blank lines (1) 61 | 62 | class MissingCommand(TypeError): ... # noqa: N818 | ^^^^^ - | help: Remove extraneous blank line(s) | 60 | from typing import Any, Sequence diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__too_many_blank_lines_isort_compatibility-lines-after(1)-between(1).snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__too_many_blank_lines_isort_compatibility-lines-after(1)-between(1).snap index b05f821f87..03ef751687 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__too_many_blank_lines_isort_compatibility-lines-after(1)-between(1).snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__too_many_blank_lines_isort_compatibility-lines-after(1)-between(1).snap @@ -10,7 +10,6 @@ I001 [*] Import block is un-sorted or un-formatted 4 | | 5 | | from typing import Any, Sequence | |________________________________^ - | help: Organize imports | 2 | @@ -26,7 +25,6 @@ E303 [*] Too many blank lines (3) | 5 | from typing import Any, Sequence | ^^^^ - | help: Remove extraneous blank line(s) | 3 | @@ -39,7 +37,6 @@ E303 [*] Too many blank lines (2) | 8 | class MissingCommand(TypeError): ... # noqa: N818 | ^^^^^ - | help: Remove extraneous blank line(s) | 6 | @@ -72,7 +69,6 @@ I001 [*] Import block is un-sorted or un-formatted 29 | | 30 | | from typing_extensions import TypeAlias | |___________________________________________^ - | help: Organize imports | 27 | @@ -86,7 +82,6 @@ E303 [*] Too many blank lines (3) | 30 | from typing_extensions import TypeAlias | ^^^^ - | help: Remove extraneous blank line(s) | 27 | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__too_many_blank_lines_isort_compatibility-lines-after(4)-between(4).snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__too_many_blank_lines_isort_compatibility-lines-after(4)-between(4).snap index a057fa67ae..a8c060f35d 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__too_many_blank_lines_isort_compatibility-lines-after(4)-between(4).snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__too_many_blank_lines_isort_compatibility-lines-after(4)-between(4).snap @@ -10,7 +10,6 @@ I001 [*] Import block is un-sorted or un-formatted 4 | | 5 | | from typing import Any, Sequence | |________________________________^ - | help: Organize imports | 4 | @@ -48,7 +47,6 @@ I001 [*] Import block is un-sorted or un-formatted 29 | | 30 | | from typing_extensions import TypeAlias | |___________________________________________^ - | help: Organize imports | 27 | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__w292_4.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__w292_4.snap index 4e72ca6c7d..b12cf346d6 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__w292_4.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__w292_4.snap @@ -6,7 +6,6 @@ W292 [*] No newline at end of file | 1 | | ^ - | help: Add trailing newline | - diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__white_space_syntax_error_compatibility.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__white_space_syntax_error_compatibility.snap index 447a0efd5e..c332e864c4 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__white_space_syntax_error_compatibility.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__white_space_syntax_error_compatibility.snap @@ -6,4 +6,3 @@ invalid-syntax: Expected an expression | 1 | a = (1 or) | ^ - | 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 e15c74fc69..e3ea55510f 100644 --- a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs +++ b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs @@ -1277,20 +1277,19 @@ pub(crate) fn check_docstring( if !definition.is_property(extra_property_decorators, semantic) { if !body_entries.returns.is_empty() { match function_def.returns.as_deref() { + // Ignore it if it's annotated as returning `None` + // or it's a generator function annotated as returning `None`, + // i.e. any of `-> None`, `-> Iterator[...]` or `-> Generator[..., ..., None]` Some(returns) - // Ignore it if it's annotated as returning `None` - // or it's a generator function annotated as returning `None`, - // i.e. any of `-> None`, `-> Iterator[...]` or `-> Generator[..., ..., None]` if !returns.is_none_literal_expr() && !is_generator_function_annotated_as_returning_none( &body_entries, returns, semantic, - ) - => { - checker - .report_diagnostic(DocstringMissingReturns, docstring.range()); - } + ) => + { + checker.report_diagnostic(DocstringMissingReturns, docstring.range()); + } None if body_entries .returns .iter() diff --git a/crates/ruff_linter/src/rules/pydocstyle/mod.rs b/crates/ruff_linter/src/rules/pydocstyle/mod.rs index e94315db80..e3490be532 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/mod.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/mod.rs @@ -85,6 +85,12 @@ mod tests { #[test_case(Rule::MissingSectionNameColon, Path::new("D.py"))] #[test_case(Rule::OverindentedSection, Path::new("sections.py"))] #[test_case(Rule::OverindentedSection, Path::new("D214_module.py"))] + #[test_case(Rule::OverindentedSection, Path::new("sphinx_directive.py"))] + #[test_case(Rule::NonCapitalizedSectionName, Path::new("sphinx_directive.py"))] + #[test_case( + Rule::MissingBlankLineAfterLastSection, + Path::new("sphinx_directive.py") + )] #[test_case(Rule::OverindentedSectionUnderline, Path::new("D215.py"))] #[test_case(Rule::MissingSectionUnderlineAfterName, Path::new("sections.py"))] #[test_case(Rule::MismatchedSectionUnderlineLength, Path::new("sections.py"))] diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/property_docstring_starts_with_verb.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/property_docstring_starts_with_verb.rs index a0bb27401d..8debed2bd7 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/property_docstring_starts_with_verb.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/property_docstring_starts_with_verb.rs @@ -48,7 +48,7 @@ use crate::rules::pydocstyle::settings::Settings; #[derive(ViolationMetadata)] #[violation_metadata(preview_since = "0.15.18")] pub(crate) struct PropertyDocstringStartsWithVerb { - pub(crate) first_word: String, + first_word: String, } impl Violation for PropertyDocstringStartsWithVerb { diff --git a/crates/ruff_linter/src/rules/pydocstyle/settings.rs b/crates/ruff_linter/src/rules/pydocstyle/settings.rs index f2a7389b07..d25dc9f5a3 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/settings.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/settings.rs @@ -95,19 +95,19 @@ pub struct Settings { } impl Settings { - pub fn convention(&self) -> Option { + pub(crate) fn convention(&self) -> Option { self.convention } - pub fn ignore_decorators(&self) -> DecoratorIterator<'_> { + pub(crate) fn ignore_decorators(&self) -> DecoratorIterator<'_> { DecoratorIterator::new(&self.ignore_decorators) } - pub fn property_decorators(&self) -> DecoratorIterator<'_> { + pub(crate) fn property_decorators(&self) -> DecoratorIterator<'_> { DecoratorIterator::new(&self.property_decorators) } - pub fn ignore_var_parameters(&self) -> bool { + pub(crate) fn ignore_var_parameters(&self) -> bool { self.ignore_var_parameters } } diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D103_D.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D103_D.py.snap index a284864e47..6cf32eabde 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D103_D.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D103_D.py.snap @@ -7,4 +7,3 @@ D103 Missing docstring in public function 399 | @expect("D103: Missing docstring in public function") 400 | def oneliner_d102(): return | ^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D200_D.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D200_D.py.snap index 5b0cd2750e..134ff2bb6d 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D200_D.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D200_D.py.snap @@ -10,7 +10,6 @@ D200 [*] One-line docstring should fit on one line 130 | | Wrong. 131 | | """ | |_______^ - | help: Reformat to one line | 128 | def asdlkfasd(): @@ -31,7 +30,6 @@ D200 [*] One-line docstring should fit on one line 598 | | 599 | | Wrong.""" | |_____________^ - | help: Reformat to one line | 596 | def one_liner(): @@ -52,7 +50,6 @@ D200 [*] One-line docstring should fit on one line 607 | | 608 | | """ | |_______^ - | help: Reformat to one line | 605 | def one_liner(): @@ -73,7 +70,6 @@ D200 One-line docstring should fit on one line 616 | | 617 | | """ | |_______^ - | help: Reformat to one line D200 One-line docstring should fit on one line @@ -85,7 +81,6 @@ D200 One-line docstring should fit on one line 625 | | 626 | | "Wrong.""" | |______________^ - | help: Reformat to one line D200 One-line docstring should fit on one line diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D200_D200.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D200_D200.py.snap index d75e5e9466..72eb17cded 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D200_D200.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D200_D200.py.snap @@ -8,7 +8,6 @@ D200 One-line docstring should fit on one line 2 | / """\ 3 | | """ | |_______^ - | help: Reformat to one line D200 [*] One-line docstring should fit on one line @@ -18,7 +17,6 @@ D200 [*] One-line docstring should fit on one line 7 | / """\\ 8 | | """ | |_______^ - | help: Reformat to one line | 6 | def func(): @@ -36,5 +34,4 @@ D200 One-line docstring should fit on one line 12 | / """\ \ 13 | | """ | |_______^ - | help: Reformat to one line diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D201_D.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D201_D.py.snap index c11bf5bda5..de8307724f 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D201_D.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D201_D.py.snap @@ -8,7 +8,6 @@ D201 [*] No blank lines allowed before function docstring (found 1) 136 | 137 | """Leading space.""" | ^^^^^^^^^^^^^^^^^^^^ - | help: Remove blank line(s) before function docstring | 135 | def leading_space(): @@ -43,7 +42,6 @@ D201 [*] No blank lines allowed before function docstring (found 1) 548 | | More content. 549 | | """ | |_______^ - | help: Remove blank line(s) before function docstring | 544 | def multiline_leading_space(): diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D202_D202.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D202_D202.py.snap index 3ce8c73bc0..33e762f4b7 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D202_D202.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D202_D202.py.snap @@ -8,7 +8,6 @@ D202 [*] No blank lines allowed after function docstring (found 2) 56 | def outer(): 57 | """This is a docstring.""" | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove blank line(s) after function docstring | 57 | """This is a docstring.""" @@ -24,7 +23,6 @@ D202 [*] No blank lines allowed after function docstring (found 2) 67 | def outer(): 68 | """This is a docstring.""" | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove blank line(s) after function docstring | 68 | """This is a docstring.""" diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D203_D.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D203_D.py.snap index 8e0f1b125d..992037f4f6 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D203_D.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D203_D.py.snap @@ -7,7 +7,6 @@ D203 [*] 1 blank line required before class docstring 160 | class LeadingSpaceMissing: 161 | """Leading space missing.""" | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Insert 1 blank line before class docstring | 160 | class LeadingSpaceMissing: @@ -75,7 +74,6 @@ D203 [*] 1 blank line required before class docstring 653 | class StatementOnSameLineAsDocstring: 654 | "After this docstring there's another statement on the same line separated by a semicolon."; priorities=1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Insert 1 blank line before class docstring | 653 | class StatementOnSameLineAsDocstring: diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D204_D.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D204_D.py.snap index 12d98ceeef..ed5fa25d85 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D204_D.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D204_D.py.snap @@ -57,7 +57,6 @@ D204 [*] 1 blank line required after class docstring 653 | class StatementOnSameLineAsDocstring: 654 | "After this docstring there's another statement on the same line separated by a semicolon."; priorities=1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Insert 1 blank line after class docstring | 653 | class StatementOnSameLineAsDocstring: diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D205_D.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D205_D.py.snap index 288e0c420a..2b09f1aab3 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D205_D.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D205_D.py.snap @@ -11,7 +11,6 @@ D205 1 blank line required between summary line and description 202 | | 203 | | """ | |_______^ - | help: Insert single blank line D205 [*] 1 blank line required between summary line and description (found 2) @@ -26,7 +25,6 @@ D205 [*] 1 blank line required between summary line and description (found 2) 214 | | 215 | | """ | |_______^ - | help: Insert single blank line | 211 | diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D207_D.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D207_D.py.snap index 6d99cf972b..b9d96c3492 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D207_D.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D207_D.py.snap @@ -26,7 +26,6 @@ D207 [*] Docstring is under-indented 243 | 244 | """ | ^ - | help: Increase indentation | 243 | @@ -58,7 +57,6 @@ D207 [*] Docstring is under-indented 440 | Second Line 441 | """ | ^ - | help: Increase indentation | 440 | Second Line diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D208_D.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D208_D.py.snap index dffaf27ea1..21bddce5ea 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D208_D.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D208_D.py.snap @@ -26,7 +26,6 @@ D208 [*] Docstring is over-indented 263 | 264 | """ | ^ - | help: Remove over-indentation | 263 | diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D208_D208.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D208_D208.py.snap index 8d8c481cc8..1b574c671e 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D208_D208.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D208_D208.py.snap @@ -59,7 +59,6 @@ D208 [*] Docstring is over-indented 9 |     Returns: 10 | """ | ^ - | help: Remove over-indentation | 9 |     Returns: @@ -75,7 +74,6 @@ D208 [*] Docstring is over-indented 23 | Returns: 24 | """ | ^ - | help: Remove over-indentation | 23 | Returns: diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D209_D.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D209_D.py.snap index 839f2de590..3d93975d39 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D209_D.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D209_D.py.snap @@ -10,7 +10,6 @@ D209 [*] Multi-line docstring closing quotes should be on a separate line 282 | | 283 | | Description.""" | |___________________^ - | help: Move closing quotes to new line | 282 | @@ -29,7 +28,6 @@ D209 [*] Multi-line docstring closing quotes should be on a separate line 589 | | 590 | | Description. """ | |_____________________^ - | help: Move closing quotes to new line | 589 | diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D210_D.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D210_D.py.snap index 4ea80dbb18..a984c9b044 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D210_D.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D210_D.py.snap @@ -8,7 +8,6 @@ D210 [*] No whitespaces allowed surrounding docstring text 287 | def endswith(): 288 | """Whitespace at the end. """ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Trim surrounding whitespace | 287 | def endswith(): @@ -24,7 +23,6 @@ D210 [*] No whitespaces allowed surrounding docstring text 292 | def around(): 293 | """ Whitespace at everywhere. """ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Trim surrounding whitespace | 292 | def around(): @@ -43,7 +41,6 @@ D210 [*] No whitespaces allowed surrounding docstring text 301 | | This is the end. 302 | | """ | |_______^ - | help: Trim surrounding whitespace | 298 | def multiline(): @@ -59,5 +56,4 @@ D210 No whitespaces allowed surrounding docstring text 580 | def endswith_quote(): 581 | """Whitespace at the end, but also a quote" """ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Trim surrounding whitespace diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D211_D.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D211_D.py.snap index fe7ea92d97..7f30b2fc71 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D211_D.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D211_D.py.snap @@ -8,7 +8,6 @@ D211 [*] No blank lines allowed before class docstring 169 | 170 | """With leading space.""" | ^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove blank line(s) before class docstring | 168 | class WithLeadingSpace: diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D212_D.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D212_D.py.snap index 7649e7e02e..3660a3edb6 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D212_D.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D212_D.py.snap @@ -10,7 +10,6 @@ D212 [*] Multi-line docstring summary should start at the first line 130 | | Wrong. 131 | | """ | |_______^ - | help: Remove whitespace after opening quotes | 128 | def asdlkfasd(): @@ -29,7 +28,6 @@ D212 [*] Multi-line docstring summary should start at the first line 598 | | 599 | | Wrong.""" | |_____________^ - | help: Remove whitespace after opening quotes | 596 | def one_liner(): @@ -49,7 +47,6 @@ D212 [*] Multi-line docstring summary should start at the first line 625 | | 626 | | "Wrong.""" | |______________^ - | help: Remove whitespace after opening quotes | 623 | def one_liner(): diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D213_D.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D213_D.py.snap index 9d44f3e954..a2cbde6dbc 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D213_D.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D213_D.py.snap @@ -11,7 +11,6 @@ D213 [*] Multi-line docstring summary should start at the second line 202 | | 203 | | """ | |_______^ - | help: Insert line break and indentation after opening quotes | 199 | def multi_line_zero_separating_blanks(): @@ -33,7 +32,6 @@ D213 [*] Multi-line docstring summary should start at the second line 214 | | 215 | | """ | |_______^ - | help: Insert line break and indentation after opening quotes | 209 | def multi_line_two_separating_blanks(): @@ -54,7 +52,6 @@ D213 [*] Multi-line docstring summary should start at the second line 223 | | 224 | | """ | |_______^ - | help: Insert line break and indentation after opening quotes | 219 | def multi_line_one_separating_blanks(): @@ -75,7 +72,6 @@ D213 [*] Multi-line docstring summary should start at the second line 233 | | 234 | | """ | |_______^ - | help: Insert line break and indentation after opening quotes | 229 | def asdfsdf(): @@ -96,7 +92,6 @@ D213 [*] Multi-line docstring summary should start at the second line 243 | | 244 | | """ | |___^ - | help: Insert line break and indentation after opening quotes | 239 | def asdsdfsdffsdf(): @@ -117,7 +112,6 @@ D213 [*] Multi-line docstring summary should start at the second line 253 | | 254 | | """ | |_______^ - | help: Insert line break and indentation after opening quotes | 249 | def asdfsdsdf24(): @@ -138,7 +132,6 @@ D213 [*] Multi-line docstring summary should start at the second line 263 | | 264 | | """ | |___________^ - | help: Insert line break and indentation after opening quotes | 259 | def asdfsdsdfsdf24(): @@ -159,7 +152,6 @@ D213 [*] Multi-line docstring summary should start at the second line 273 | | 274 | | """ | |_______^ - | help: Insert line break and indentation after opening quotes | 269 | def asdfsdfsdsdsdfsdf24(): @@ -178,7 +170,6 @@ D213 [*] Multi-line docstring summary should start at the second line 282 | | 283 | | Description.""" | |___________________^ - | help: Insert line break and indentation after opening quotes | 280 | def asdfljdf24(): @@ -198,7 +189,6 @@ D213 [*] Multi-line docstring summary should start at the second line 301 | | This is the end. 302 | | """ | |_______^ - | help: Insert line break and indentation after opening quotes | 298 | def multiline(): @@ -220,7 +210,6 @@ D213 [*] Multi-line docstring summary should start at the second line 347 | | They are considered to be intentionally unescaped. 348 | | """ | |_______^ - | help: Insert line break and indentation after opening quotes | 342 | def exceptions_of_D301(): @@ -262,7 +251,6 @@ D213 [*] Multi-line docstring summary should start at the second line 395 | | 396 | | """ | |_______^ - | help: Insert line break and indentation after opening quotes | 391 | def old_209(): @@ -283,7 +271,6 @@ D213 [*] Multi-line docstring summary should start at the second line 440 | | Second Line 441 | | """ | |_______^ - | help: Insert line break and indentation after opening quotes | 437 | @expect('D213: Multi-line docstring summary should start at the second line') @@ -304,7 +291,6 @@ D213 [*] Multi-line docstring summary should start at the second line 453 | | 454 | | """ | |_______^ - | help: Insert line break and indentation after opening quotes | 449 | def a_following_valid_function(x=None): @@ -349,7 +335,6 @@ D213 [*] Multi-line docstring summary should start at the second line 548 | | More content. 549 | | """ | |_______^ - | help: Insert line break and indentation after opening quotes | 545 | @@ -412,7 +397,6 @@ D213 [*] Multi-line docstring summary should start at the second line 589 | | 590 | | Description. """ | |_____________________^ - | help: Insert line break and indentation after opening quotes | 587 | def asdfljdjgf24(): @@ -431,7 +415,6 @@ D213 [*] Multi-line docstring summary should start at the second line 607 | | 608 | | """ | |_______^ - | help: Insert line break and indentation after opening quotes | 605 | def one_liner(): @@ -450,7 +433,6 @@ D213 [*] Multi-line docstring summary should start at the second line 616 | | 617 | | """ | |_______^ - | help: Insert line break and indentation after opening quotes | 614 | def one_liner(): @@ -471,7 +453,6 @@ D213 [*] Multi-line docstring summary should start at the second line 675 | | to the one before 676 | | """ | |_______^ - | help: Insert line break and indentation after opening quotes | 670 | def retain_extra_whitespace(): @@ -495,7 +476,6 @@ D213 [*] Multi-line docstring summary should start at the second line 687 | | to the one before 688 | | """ | |_______^ - | help: Insert line break and indentation after opening quotes | 679 | def retain_extra_whitespace_multiple(): @@ -541,7 +521,6 @@ D213 [*] Multi-line docstring summary should start at the second line 707 | | This is overindented 708 | | """ | |_______^ - | help: Insert line break and indentation after opening quotes | 701 | def retain_extra_whitespace_followed_by_same_offset(): @@ -562,7 +541,6 @@ D213 [*] Multi-line docstring summary should start at the second line 716 | | And so is this, but it we should preserve the extra space on this line relative 717 | | """ | |_______^ - | help: Insert line break and indentation after opening quotes | 711 | def retain_extra_whitespace_not_overindented(): @@ -581,7 +559,6 @@ D213 [*] Multi-line docstring summary should start at the second line 723 | |     Returns: 724 | | """ | |_______^ - | help: Insert line break and indentation after opening quotes | 720 | def inconsistent_indent_byte_size(): diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D214_sphinx_directive.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D214_sphinx_directive.py.snap new file mode 100644 index 0000000000..724d6e7d20 --- /dev/null +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D214_sphinx_directive.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/pydocstyle/mod.rs +--- + diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D300_D.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D300_D.py.snap index 2a4bc2b5bb..f11fe7926f 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D300_D.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D300_D.py.snap @@ -8,7 +8,6 @@ D300 [*] Use triple double quotes `"""` 306 | def triple_single_quotes_raw(): 307 | r'''Summary.''' | ^^^^^^^^^^^^^^^ - | help: Convert to triple double quotes | 306 | def triple_single_quotes_raw(): @@ -24,7 +23,6 @@ D300 [*] Use triple double quotes `"""` 311 | def triple_single_quotes_raw_uppercase(): 312 | R'''Summary.''' | ^^^^^^^^^^^^^^^ - | help: Convert to triple double quotes | 311 | def triple_single_quotes_raw_uppercase(): @@ -40,7 +38,6 @@ D300 [*] Use triple double quotes `"""` 316 | def single_quotes_raw(): 317 | r'Summary.' | ^^^^^^^^^^^ - | help: Convert to triple double quotes | 316 | def single_quotes_raw(): @@ -56,7 +53,6 @@ D300 [*] Use triple double quotes `"""` 321 | def single_quotes_raw_uppercase(): 322 | R'Summary.' | ^^^^^^^^^^^ - | help: Convert to triple double quotes | 321 | def single_quotes_raw_uppercase(): @@ -72,7 +68,6 @@ D300 [*] Use triple double quotes `"""` 327 | def single_quotes_raw_uppercase_backslash(): 328 | R'Sum\mary.' | ^^^^^^^^^^^^ - | help: Convert to triple double quotes | 327 | def single_quotes_raw_uppercase_backslash(): @@ -124,7 +119,6 @@ D300 [*] Use triple double quotes `"""` 653 | class StatementOnSameLineAsDocstring: 654 | "After this docstring there's another statement on the same line separated by a semicolon."; priorities=1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Convert to triple double quotes | 653 | class StatementOnSameLineAsDocstring: @@ -157,7 +151,6 @@ D300 [*] Use triple double quotes `"""` 664 | / "We enforce a newline after the closing quote for a multi-line docstring \ 665 | | but continuations shouldn't be considered multi-line" | |_________________________________________________________^ - | help: Convert to triple double quotes | 663 | def newline_after_closing_quote(self): diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D300_D300.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D300_D300.py.snap index b4a3434b4c..794d33e32e 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D300_D300.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D300_D300.py.snap @@ -7,7 +7,6 @@ D300 Use triple double quotes `"""` 5 | def ends_in_quote(): 6 | 'Sum\\mary."' | ^^^^^^^^^^^^^ - | help: Convert to triple double quotes D300 [*] Use triple double quotes `"""` @@ -16,7 +15,6 @@ D300 [*] Use triple double quotes `"""` 9 | def contains_quote(): 10 | 'Sum"\\mary.' | ^^^^^^^^^^^^^ - | help: Convert to triple double quotes | 9 | def contains_quote(): diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D301_D.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D301_D.py.snap index ba64f5b886..9d1f0339a3 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D301_D.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D301_D.py.snap @@ -8,7 +8,6 @@ D301 [*] Use `r"""` if any backslashes in a docstring 332 | def double_quotes_backslash(): 333 | """Sum\\mary.""" | ^^^^^^^^^^^^^^^^ - | help: Add `r` prefix | 332 | def double_quotes_backslash(): diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D301_D301.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D301_D301.py.snap index 28cd882149..a6501bd05b 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D301_D301.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D301_D301.py.snap @@ -7,7 +7,6 @@ D301 [*] Use `r"""` if any backslashes in a docstring 1 | def double_quotes_backslash(): 2 | """Sum\\mary.""" | ^^^^^^^^^^^^^^^^ - | help: Add `r` prefix | 1 | def double_quotes_backslash(): @@ -23,7 +22,6 @@ D301 Use `r"""` if any backslashes in a docstring 36 | def shouldnt_add_raw_here2(): 37 | u"Sum\\mary." | ^^^^^^^^^^^^^ - | help: Add `r` prefix D301 [*] Use `r"""` if any backslashes in a docstring @@ -34,7 +32,6 @@ D301 [*] Use `r"""` if any backslashes in a docstring 94 | | This is single quote escape \". 95 | | """ | |_______^ - | help: Add `r` prefix | 92 | def should_add_raw_for_single_double_quote_escape(): @@ -52,7 +49,6 @@ D301 [*] Use `r"""` if any backslashes in a docstring 100 | | This is single quote escape \'. 101 | | ''' | |_______^ - | help: Add `r` prefix | 98 | def should_add_raw_for_single_single_quote_escape(): diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D400_D.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D400_D.py.snap index 53ff711a74..a51349390f 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D400_D.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D400_D.py.snap @@ -8,7 +8,6 @@ D400 [*] First line should end with a period 354 | def lwnlkjl(): 355 | """Summary""" | ^^^^^^^^^^^^^ - | help: Add period | 354 | def lwnlkjl(): @@ -25,7 +24,6 @@ D400 [*] First line should end with a period 405 | " or exclamation point (not 'r')") 406 | def oneliner_withdoc(): """One liner""" | ^^^^^^^^^^^^^^^ - | help: Add period | 405 | " or exclamation point (not 'r')") @@ -77,7 +75,6 @@ D400 [*] First line should end with a period 421 | @ignored_decorator 422 | def oneliner_ignored_decorator(): """One liner""" | ^^^^^^^^^^^^^^^ - | help: Add period | 421 | @ignored_decorator @@ -94,7 +91,6 @@ D400 [*] First line should end with a period 428 | " or exclamation point (not 'r')") 429 | def oneliner_with_decorator_expecting_errors(): """One liner""" | ^^^^^^^^^^^^^^^ - | help: Add period | 428 | " or exclamation point (not 'r')") @@ -180,7 +176,6 @@ D400 First line should end with a period 513 | def valid_google_string(): # noqa: D400 514 | """Test a valid something!""" | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add period D400 [*] First line should end with a period @@ -190,7 +185,6 @@ D400 [*] First line should end with a period 519 | def bad_google_string(): # noqa: D400 520 | """Test a valid something""" | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add period | 519 | def bad_google_string(): # noqa: D400 @@ -207,7 +201,6 @@ D400 [*] First line should end with a period 580 | def endswith_quote(): 581 | """Whitespace at the end, but also a quote" """ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add period | 580 | def endswith_quote(): @@ -226,7 +219,6 @@ D400 [*] First line should end with a period 616 | | 617 | | """ | |_______^ - | help: Add period | 614 | def one_liner(): @@ -260,7 +252,6 @@ D400 [*] First line should end with a period 640 | 641 | def same_line(): """This is a docstring on the same line""" | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add period | 640 | @@ -277,7 +268,6 @@ D400 [*] First line should end with a period 664 | / "We enforce a newline after the closing quote for a multi-line docstring \ 665 | | but continuations shouldn't be considered multi-line" | |_________________________________________________________^ - | help: Add period | 664 | "We enforce a newline after the closing quote for a multi-line docstring \ diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D400_D400.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D400_D400.py.snap index b41376170d..dc4d4c64e9 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D400_D400.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D400_D400.py.snap @@ -230,7 +230,6 @@ D400 [*] First line should end with a period 101 | | My example explanation 102 | | """ | |_______^ - | help: Add period | 97 | """ diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D401_D401.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D401_D401.py.snap index 55f509319c..7b27950eb2 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D401_D401.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D401_D401.py.snap @@ -7,7 +7,6 @@ D401 First line of docstring should be in imperative mood: "Returns foo." 9 | def bad_liouiwnlkjl(): 10 | """Returns foo.""" | ^^^^^^^^^^^^^^^^^^ - | D401 First line of docstring should be in imperative mood: "Constructor for a foo." --> D401.py:14:5 @@ -15,7 +14,6 @@ D401 First line of docstring should be in imperative mood: "Constructor for a fo 13 | def bad_sdgfsdg23245(): 14 | """Constructor for a foo.""" | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | D401 First line of docstring should be in imperative mood: "Constructor for a boa." --> D401.py:18:5 @@ -27,7 +25,6 @@ D401 First line of docstring should be in imperative mood: "Constructor for a bo 21 | | 22 | | """ | |_______^ - | D401 First line of docstring should be in imperative mood: "Runs something" --> D401.py:26:5 @@ -57,7 +54,6 @@ D401 First line of docstring should be in imperative mood: "Writes a logical lin 36 | | extends to two physical lines. 37 | | """ | |_______^ - | D401 First line of docstring should be in imperative mood: "This method docstring should be written in imperative mood." --> D401.py:74:9 diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D402_D.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D402_D.py.snap index 0f51d90a4f..2919868e4f 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D402_D.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D402_D.py.snap @@ -8,4 +8,3 @@ D402 First line should not be the function's signature 377 | def foobar(): 378 | """Signature: foobar().""" | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D402_D402.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D402_D402.py.snap index f6048d4fb5..d2c56dbeed 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D402_D402.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D402_D402.py.snap @@ -17,4 +17,3 @@ D402 First line should not be the function's signature 7 | def foo(): 8 | """"Use this function; foo().""" | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D403_D403.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D403_D403.py.snap index b5f18422ac..b50c378072 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D403_D403.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D403_D403.py.snap @@ -223,7 +223,6 @@ D403 [*] First word of the docstring should be capitalized: `singleword` -> `Sin 88 | | This is more text. 89 | | """ | |_______^ - | help: Capitalize `singleword` to `Singleword` | 85 | """ diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D404_D.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D404_D.py.snap index d9f57c68d3..202367966d 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D404_D.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D404_D.py.snap @@ -8,7 +8,6 @@ D404 First word of the docstring should not be "This" 630 | def starts_with_this(): 631 | """This is a docstring.""" | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | D404 First word of the docstring should not be "This" --> D.py:636:5 @@ -17,7 +16,6 @@ D404 First word of the docstring should not be "This" 635 | def starts_with_space_then_this(): 636 | """ This is a docstring that starts with a space.""" # noqa: D210 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | D404 First word of the docstring should not be "This" --> D.py:639:17 @@ -35,4 +33,3 @@ D404 First word of the docstring should not be "This" 640 | 641 | def same_line(): """This is a docstring on the same line""" | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D405_sphinx_directive.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D405_sphinx_directive.py.snap new file mode 100644 index 0000000000..724d6e7d20 --- /dev/null +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D405_sphinx_directive.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/pydocstyle/mod.rs +--- + diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D407_sections.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D407_sections.py.snap index a02665f5ed..90e97c63ea 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D407_sections.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D407_sections.py.snap @@ -41,7 +41,6 @@ D407 [*] Missing dashed underline after section ("Returns") 66 | 67 | Returns""" | ^^^^^^^ - | help: Add dashed line under "Returns" | 66 | diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D412_sphinx.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D412_sphinx.py.snap index e4fcd33d57..b3668e3c29 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D412_sphinx.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D412_sphinx.py.snap @@ -8,7 +8,6 @@ D412 [*] No blank lines allowed between a section header and its content ("Examp 12 | """ 13 | Example: | ^^^^^^^ - | help: Remove blank line(s) | 14 | @@ -23,7 +22,6 @@ D412 [*] No blank lines allowed between a section header and its content ("Examp 23 | """ 24 | Example: | ^^^^^^^ - | help: Remove blank line(s) | 25 | diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sections.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sections.py.snap index 014932de86..4f65203e47 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sections.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sections.py.snap @@ -8,7 +8,6 @@ D413 [*] Missing blank line after last section ("Returns") 66 | 67 | Returns""" | ^^^^^^^ - | help: Add blank line after "Returns" | 66 | diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sphinx_directive.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sphinx_directive.py.snap new file mode 100644 index 0000000000..7c9ba10d96 --- /dev/null +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sphinx_directive.py.snap @@ -0,0 +1,53 @@ +--- +source: crates/ruff_linter/src/rules/pydocstyle/mod.rs +--- +D413 [*] Missing blank line after last section ("Returns") + --> sphinx_directive.py:24:5 + | +22 | notes = "also not a section" +23 | +24 | Returns: + | ^^^^^^^ +25 | None +26 | """ + | +help: Add blank line after "Returns" + | +25 | None +26 + +27 | """ + | + +D413 [*] Missing blank line after last section ("Returns") + --> sphinx_directive.py:50:5 + | +48 | warnings = "not a section" +49 | +50 | Returns: + | ^^^^^^^ +51 | None +52 | """ + | +help: Add blank line after "Returns" + | +51 | None +52 + +53 | """ + | + +D413 [*] Missing blank line after last section ("Notes") + --> sphinx_directive.py:62:5 + | +60 | example = "code" +61 | +62 | Notes: + | ^^^^^ +63 | This IS a real section and should still be detected. +64 | """ + | +help: Add blank line after "Notes" + | +63 | This IS a real section and should still be detected. +64 + +65 | """ + | diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D414_sections.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D414_sections.py.snap index 75860e3dc3..2ad9c45caf 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D414_sections.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D414_sections.py.snap @@ -19,7 +19,6 @@ D414 Section has no content ("Returns") 66 | 67 | Returns""" | ^^^^^^^ - | D414 Section has no content ("Returns") --> sections.py:78:5 diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D415_D.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D415_D.py.snap index 7d30b13fa2..1b2f8c941d 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D415_D.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D415_D.py.snap @@ -8,7 +8,6 @@ D415 [*] First line should end with a period, question mark, or exclamation poin 354 | def lwnlkjl(): 355 | """Summary""" | ^^^^^^^^^^^^^ - | help: Add closing punctuation | 354 | def lwnlkjl(): @@ -25,7 +24,6 @@ D415 [*] First line should end with a period, question mark, or exclamation poin 405 | " or exclamation point (not 'r')") 406 | def oneliner_withdoc(): """One liner""" | ^^^^^^^^^^^^^^^ - | help: Add closing punctuation | 405 | " or exclamation point (not 'r')") @@ -77,7 +75,6 @@ D415 [*] First line should end with a period, question mark, or exclamation poin 421 | @ignored_decorator 422 | def oneliner_ignored_decorator(): """One liner""" | ^^^^^^^^^^^^^^^ - | help: Add closing punctuation | 421 | @ignored_decorator @@ -94,7 +91,6 @@ D415 [*] First line should end with a period, question mark, or exclamation poin 428 | " or exclamation point (not 'r')") 429 | def oneliner_with_decorator_expecting_errors(): """One liner""" | ^^^^^^^^^^^^^^^ - | help: Add closing punctuation | 428 | " or exclamation point (not 'r')") @@ -181,7 +177,6 @@ D415 [*] First line should end with a period, question mark, or exclamation poin 519 | def bad_google_string(): # noqa: D400 520 | """Test a valid something""" | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add closing punctuation | 519 | def bad_google_string(): # noqa: D400 @@ -198,7 +193,6 @@ D415 [*] First line should end with a period, question mark, or exclamation poin 580 | def endswith_quote(): 581 | """Whitespace at the end, but also a quote" """ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add closing punctuation | 580 | def endswith_quote(): @@ -217,7 +211,6 @@ D415 [*] First line should end with a period, question mark, or exclamation poin 616 | | 617 | | """ | |_______^ - | help: Add closing punctuation | 614 | def one_liner(): @@ -251,7 +244,6 @@ D415 [*] First line should end with a period, question mark, or exclamation poin 640 | 641 | def same_line(): """This is a docstring on the same line""" | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add closing punctuation | 640 | @@ -268,7 +260,6 @@ D415 [*] First line should end with a period, question mark, or exclamation poin 664 | / "We enforce a newline after the closing quote for a multi-line docstring \ 665 | | but continuations shouldn't be considered multi-line" | |_________________________________________________________^ - | help: Add closing punctuation | 664 | "We enforce a newline after the closing quote for a multi-line docstring \ diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D419_D.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D419_D.py.snap index e6077f93ad..874f4eb8c2 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D419_D.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D419_D.py.snap @@ -29,4 +29,3 @@ D419 Docstring is empty 79 | def nested(): 80 | '' | ^^ - | diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__bom.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__bom.snap index 5bef0800c2..828ef679e3 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__bom.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__bom.snap @@ -6,7 +6,6 @@ D300 [*] Use triple double quotes `"""` | 1 | ''' SAM macro definitions ''' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Convert to triple double quotes | - ''' SAM macro definitions ''' diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__d209_d400.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__d209_d400.snap index a7926e3114..59e45c5e0b 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__d209_d400.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__d209_d400.snap @@ -8,7 +8,6 @@ D209 [*] Multi-line docstring closing quotes should be on a separate line 2 | / """lorem ipsum dolor sit amet consectetur adipiscing elit 3 | | sed do eiusmod tempor incididunt ut labore et dolore magna aliqua""" | |________________________________________________________________________^ - | help: Move closing quotes to new line | 2 | """lorem ipsum dolor sit amet consectetur adipiscing elit @@ -24,7 +23,6 @@ D400 [*] First line should end with a period 2 | / """lorem ipsum dolor sit amet consectetur adipiscing elit 3 | | sed do eiusmod tempor incididunt ut labore et dolore magna aliqua""" | |________________________________________________________________________^ - | help: Add period | 2 | """lorem ipsum dolor sit amet consectetur adipiscing elit diff --git a/crates/ruff_linter/src/rules/pyflakes/mod.rs b/crates/ruff_linter/src/rules/pyflakes/mod.rs index ec67862f1d..c609686038 100644 --- a/crates/ruff_linter/src/rules/pyflakes/mod.rs +++ b/crates/ruff_linter/src/rules/pyflakes/mod.rs @@ -229,10 +229,8 @@ mod tests { fn f821_with_builtin_added_on_new_py_version_but_old_target_version_specified() { let diagnostics = test_snippet( "PythonFinalizationError", - &LinterSettings { - unresolved_target_version: ruff_python_ast::PythonVersion::PY312.into(), - ..LinterSettings::for_rule(Rule::UndefinedName) - }, + &LinterSettings::for_rule(Rule::UndefinedName) + .with_target_version(ruff_python_ast::PythonVersion::PY312), ); assert_diagnostics!(diagnostics); } @@ -242,10 +240,8 @@ mod tests { // frozendict is available starting in Python 3.15. let diagnostics = test_snippet( "frozendict", - &LinterSettings { - unresolved_target_version: ruff_python_ast::PythonVersion::PY315.into(), - ..LinterSettings::for_rule(Rule::UndefinedName) - }, + &LinterSettings::for_rule(Rule::UndefinedName) + .with_target_version(ruff_python_ast::PythonVersion::PY315), ); assert!(diagnostics.is_empty()); } @@ -255,10 +251,8 @@ mod tests { // frozendict is not available before Python 3.15. let diagnostics = test_snippet( "frozendict", - &LinterSettings { - unresolved_target_version: ruff_python_ast::PythonVersion::PY314.into(), - ..LinterSettings::for_rule(Rule::UndefinedName) - }, + &LinterSettings::for_rule(Rule::UndefinedName) + .with_target_version(ruff_python_ast::PythonVersion::PY314), ); assert_diagnostics!(diagnostics); } @@ -280,10 +274,7 @@ mod tests { ); let diagnostics = test_path( Path::new("pyflakes").join(path).as_path(), - &LinterSettings { - preview: PreviewMode::Enabled, - ..LinterSettings::for_rule(rule_code) - }, + &LinterSettings::for_rule(rule_code).with_preview_mode(), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) @@ -478,10 +469,7 @@ mod tests { snapshot, Path::new("pyflakes").join(path).as_path(), &LinterSettings::for_rule(rule_code), - &LinterSettings { - preview: PreviewMode::Enabled, - ..LinterSettings::for_rule(rule_code) - } + &LinterSettings::for_rule(rule_code).with_preview_mode() ); Ok(()) } @@ -620,10 +608,7 @@ mod tests { is_basedpython: false, }, Path::new("f401_preview_submodule.py"), - &LinterSettings { - preview: PreviewMode::Enabled, - ..LinterSettings::for_rule(Rule::UnusedImport) - }, + &LinterSettings::for_rule(Rule::UnusedImport).with_preview_mode(), ) .0; assert_diagnostics!(snapshot, diagnostics); @@ -765,10 +750,7 @@ mod tests { fn f811_annotated_assignment_redefinition() -> Result<()> { let diagnostics = test_path( Path::new("pyflakes/F811_34.py"), - &LinterSettings { - preview: PreviewMode::Enabled, - ..LinterSettings::for_rule(Rule::RedefinedWhileUnused) - }, + &LinterSettings::for_rule(Rule::RedefinedWhileUnused).with_preview_mode(), )?; assert_diagnostics!(diagnostics); Ok(()) diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/imports.rs b/crates/ruff_linter/src/rules/pyflakes/rules/imports.rs index 7a97aa09ab..19f2efeb3c 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/imports.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/imports.rs @@ -36,8 +36,8 @@ use crate::checkers::ast::Checker; #[derive(ViolationMetadata)] #[violation_metadata(stable_since = "v0.0.44")] pub(crate) struct ImportShadowedByLoopVar { - pub(crate) name: String, - pub(crate) row: SourceRow, + name: String, + row: SourceRow, } impl Violation for ImportShadowedByLoopVar { 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 0ebfed46ec..bd67c26bb7 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 @@ -312,7 +312,7 @@ pub(crate) fn redefined_while_unused(checker: &Checker, scope_id: ScopeId, scope info.shadowed, ); - diagnostic.set_primary_message(format_args!("`{name}` redefined here")); + diagnostic.set_primary_annotation_message(format_args!("`{name}` redefined here")); if let Some(range) = info.binding.parent_range(checker.semantic()) { diagnostic.set_parent(range.start()); 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 b64bcc0899..45fa7f984e 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs @@ -164,12 +164,14 @@ impl Violation for UnusedImport { match context { UnusedImportContext::ExceptHandler => { format!( - "`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability" + "`{name}` imported but unused; \ + consider using `importlib.util.find_spec` to test for availability" ) } UnusedImportContext::DunderInitFirstParty { .. } => { format!( - "`{name}` imported but unused; consider removing, adding to `__all__`, or using a redundant alias" + "`{name}` imported but unused; \ + consider removing, adding to `__all__`, or using a redundant alias" ) } UnusedImportContext::Other => format!("`{name}` imported but unused"), @@ -196,7 +198,8 @@ impl Violation for UnusedImport { submodule_import: true, } => { return Some(format!( - "Use an explicit re-export: `import {parent} as {parent}; import {binding}`", + "Use an explicit re-export: \ + `import {parent} as {parent}; import {binding}`", parent = binding .split('.') .next() @@ -409,19 +412,22 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope) { } else if in_init && binding.scope.is_global() && is_first_party(&binding.import, checker) - // In the situation where we have - // ``` - // import a.b # <-- at this binding - // import a.c - // - // __all__ = ["a"] - // ``` - // we should not recommend that we re-export the - // symbol `a` or add it to `__all__`. - // - // So we look up the name `a` and see if it has - // a reference in `__all__`. - && (!is_refined_submodule_import_match_enabled(checker.settings())||!symbol_used_in_dunder_all(checker.semantic(), &binding)) + && ( + // In the situation where we have + // ``` + // import a.b # <-- at this binding + // import a.c + // + // __all__ = ["a"] + // ``` + // we should not recommend that we re-export the + // symbol `a` or add it to `__all__`. + // + // So we look up the name `a` and see if it has + // a reference in `__all__`. + !is_refined_submodule_import_match_enabled(checker.settings()) + || !symbol_used_in_dunder_all(checker.semantic(), &binding) + ) { UnusedImportContext::DunderInitFirstParty { dunder_all_count: DunderAllCount::from(dunder_all_exprs.len()), @@ -667,12 +673,16 @@ fn unused_imports_in_scope<'a, 'b>( .filter(|(_, bdg)| !bdg.is_global() && !bdg.is_nonlocal() && !bdg.is_explicit_export()) .flat_map(|(id, bdg)| { if is_refined_submodule_import_match_enabled(settings) - // No need to apply refined logic if there is only a single binding - && scope.shadowed_bindings(id).nth(1).is_some() - // Only apply the new logic in certain situations to avoid - // complexity, false positives, and intersection with - // `redefined-while-unused` (`F811`). - && has_simple_shadowed_bindings(scope, id, semantic) + && ( + // No need to apply refined logic if there is only a single binding + scope.shadowed_bindings(id).nth(1).is_some() + ) + && ( + // Only apply the new logic in certain situations to avoid + // complexity, false positives, and intersection with + // `redefined-while-unused` (`F811`). + has_simple_shadowed_bindings(scope, id, semantic) + ) { unused_imports_from_binding(semantic, id, scope) } else if bdg.is_used() { @@ -743,11 +753,14 @@ fn unused_imports_from_binding<'a, 'b>( for ref_id in binding.references() { let resolved_reference = semantic.reference(ref_id); if !marked_dunder_all && resolved_reference.in_dunder_all_definition() { - let first = *binding - .as_any_import() - .expect("binding to be import binding since current function called after restricting to these in `unused_imports_in_scope`") - .qualified_name() - .segments().first().expect("import binding to have nonempty qualified name"); + let first = binding + .as_any_import() + .expect( + "The binding should be an import binding since current function \ + called after restricting to these in `unused_imports_in_scope`", + ) + .qualified_name() + .segments()[0]; mark_uses_of_qualified_name(&mut marked, &QualifiedName::user_defined(first)); marked_dunder_all = true; continue; diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_F401_0.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_F401_0.py.snap index 8b81e796ff..d000f803e9 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_F401_0.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_F401_0.py.snap @@ -107,7 +107,6 @@ F401 [*] `pickle` imported but unused 51 | def b(self) -> None: 52 | import pickle | ^^^^^^ - | help: Remove unused import: `pickle` | 51 | def b(self) -> None: @@ -139,7 +138,6 @@ F401 [*] `y` imported but unused 93 | import x 94 | import y | ^ - | help: Remove unused import: `y` | 93 | import x @@ -188,7 +186,6 @@ F401 [*] `a2` imported but unused 106 | 107 | import a2 | ^^ - | help: Remove unused import: `a2` | 106 | @@ -220,7 +217,6 @@ F401 [*] `b2` imported but unused 113 | 114 | import b2 | ^^ - | help: Remove unused import: `b2` | 113 | @@ -234,7 +230,6 @@ F401 [*] `datameta_client_lib.model_helpers.noqa` imported but unused 121 | from datameta_client_lib.model_helpers import ( 122 | noqa ) | ^^^^ - | help: Remove unused import: `datameta_client_lib.model_helpers.noqa` | 120 | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_F401_11.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_F401_11.py.snap index 6a0a8c3aee..7bedc926b3 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_F401_11.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_F401_11.py.snap @@ -7,7 +7,6 @@ F401 [*] `pathlib.PurePath` imported but unused 3 | from typing import List 4 | from pathlib import Path, PurePath | ^^^^^^^^ - | help: Remove unused import: `pathlib.PurePath` | 3 | from typing import List diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_F401_15.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_F401_15.py.snap index 00e0a29386..cba2cf8fea 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_F401_15.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_F401_15.py.snap @@ -7,7 +7,6 @@ F401 [*] `pathlib.Path` imported but unused 4 | if TYPE_CHECKING: 5 | from pathlib import Path | ^^^^ - | help: Remove unused import: `pathlib.Path` | 4 | if TYPE_CHECKING: diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_F401_18.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_F401_18.py.snap index 89a6f6e524..b2901bdbc2 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_F401_18.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_F401_18.py.snap @@ -7,7 +7,6 @@ F401 [*] `__future__` imported but unused 4 | def f(): 5 | import __future__ | ^^^^^^^^^^ - | help: Remove unused import: `__future__` | 4 | def f(): diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_F401_5.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_F401_5.py.snap index 5fc6f974af..b6483be2ed 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_F401_5.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_F401_5.py.snap @@ -57,7 +57,6 @@ F401 [*] `j.k` imported but unused 4 | import h.i 5 | import j.k as l | ^ - | help: Remove unused import: `j.k` | 4 | import h.i diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_F401_6.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_F401_6.py.snap index 5bcdd0e8cb..cf985888c0 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_F401_6.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_F401_6.py.snap @@ -55,7 +55,6 @@ F401 [*] `datastructures` imported but unused 18 | # F401 `datastructures` imported but unused 19 | import datastructures as structures | ^^^^^^^^^^ - | help: Remove unused import: `datastructures` | 18 | # F401 `datastructures` imported but unused diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_F401_7.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_F401_7.py.snap index 794bc0195d..94f60b328e 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_F401_7.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_F401_7.py.snap @@ -25,7 +25,6 @@ F401 [*] `typing.Awaitable` imported but unused 65 | # This should mark F501 as unused. 66 | from typing import Awaitable, AwaitableGenerator # noqa: F501 | ^^^^^^^^^ - | help: Remove unused import | 65 | # This should mark F501 as unused. @@ -38,7 +37,6 @@ F401 [*] `typing.AwaitableGenerator` imported but unused 65 | # This should mark F501 as unused. 66 | from typing import Awaitable, AwaitableGenerator # noqa: F501 | ^^^^^^^^^^^^^^^^^^ - | help: Remove unused import | 65 | # This should mark F501 as unused. diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_F401_9.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_F401_9.py.snap index ab5db54e01..9fcfd33572 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_F401_9.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_F401_9.py.snap @@ -7,7 +7,6 @@ F401 [*] `foo.baz` imported but unused 3 | __all__ = ("bar",) 4 | from foo import bar, baz | ^^^ - | help: Remove unused import: `foo.baz` | 3 | __all__ = ("bar",) diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_deprecated_option_F401_24____init__.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_deprecated_option_F401_24____init__.py.snap index 420d20442e..b22638490d 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_deprecated_option_F401_24____init__.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_deprecated_option_F401_24____init__.py.snap @@ -6,7 +6,6 @@ F401 [*] `sys` imported but unused | 19 | import sys # F401: remove unused | ^^^ - | help: Remove unused import: `sys` | 18 | @@ -20,7 +19,6 @@ F401 [*] `.unused` imported but unused; consider removing, adding to `__all__`, | 33 | from . import unused # F401: change to redundant alias | ^^^^^^ - | help: Remove unused import: `.unused` | 32 | @@ -34,7 +32,6 @@ F401 [*] `.renamed` imported but unused; consider removing, adding to `__all__`, | 36 | from . import renamed as bees # F401: no fix | ^^^^ - | help: Remove unused import: `.renamed` | 35 | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_deprecated_option_F401_25__all_nonempty____init__.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_deprecated_option_F401_25__all_nonempty____init__.py.snap index 18f4794f6c..7816a422e6 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_deprecated_option_F401_25__all_nonempty____init__.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_deprecated_option_F401_25__all_nonempty____init__.py.snap @@ -6,7 +6,6 @@ F401 [*] `sys` imported but unused | 19 | import sys # F401: remove unused | ^^^ - | help: Remove unused import: `sys` | 18 | @@ -20,7 +19,6 @@ F401 [*] `.unused` imported but unused; consider removing, adding to `__all__`, | 36 | from . import unused # F401: add to __all__ | ^^^^^^ - | help: Remove unused import: `.unused` | 35 | @@ -34,7 +32,6 @@ F401 [*] `.renamed` imported but unused; consider removing, adding to `__all__`, | 39 | from . import renamed as bees # F401: add to __all__ | ^^^^ - | help: Remove unused import: `.renamed` | 38 | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_deprecated_option_F401_26__all_empty____init__.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_deprecated_option_F401_26__all_empty____init__.py.snap index 516626dbdc..21fc5fb21f 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_deprecated_option_F401_26__all_empty____init__.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_deprecated_option_F401_26__all_empty____init__.py.snap @@ -6,7 +6,6 @@ F401 [*] `.unused` imported but unused; consider removing, adding to `__all__`, | 5 | from . import unused # F401: add to __all__ | ^^^^^^ - | help: Remove unused import: `.unused` | 4 | @@ -20,7 +19,6 @@ F401 [*] `.renamed` imported but unused; consider removing, adding to `__all__`, | 8 | from . import renamed as bees # F401: add to __all__ | ^^^^ - | help: Remove unused import: `.renamed` | 7 | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_deprecated_option_F401_27__all_mistyped____init__.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_deprecated_option_F401_27__all_mistyped____init__.py.snap index e88d779f91..8d7f2d9e14 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_deprecated_option_F401_27__all_mistyped____init__.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_deprecated_option_F401_27__all_mistyped____init__.py.snap @@ -6,7 +6,6 @@ F401 [*] `.unused` imported but unused; consider removing, adding to `__all__`, | 5 | from . import unused # F401: recommend add to all w/o fix | ^^^^^^ - | help: Remove unused import: `.unused` | 4 | @@ -20,7 +19,6 @@ F401 [*] `.renamed` imported but unused; consider removing, adding to `__all__`, | 8 | from . import renamed as bees # F401: recommend add to all w/o fix | ^^^^ - | help: Remove unused import: `.renamed` | 7 | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_deprecated_option_F401_28__all_multiple____init__.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_deprecated_option_F401_28__all_multiple____init__.py.snap index addc9a5aae..16ba6bec64 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_deprecated_option_F401_28__all_multiple____init__.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_deprecated_option_F401_28__all_multiple____init__.py.snap @@ -6,7 +6,6 @@ F401 [*] `.unused` imported but unused; consider removing, adding to `__all__`, | 5 | from . import unused, renamed as bees # F401: add to __all__ | ^^^^^^ - | help: Remove unused import | 4 | @@ -20,7 +19,6 @@ F401 [*] `.renamed` imported but unused; consider removing, adding to `__all__`, | 5 | from . import unused, renamed as bees # F401: add to __all__ | ^^^^ - | help: Remove unused import | 4 | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_deprecated_option_F401_30.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_deprecated_option_F401_30.py.snap index 790c201d63..4890e7032d 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_deprecated_option_F401_30.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_deprecated_option_F401_30.py.snap @@ -8,7 +8,6 @@ F401 [*] `.main.MaμToMan` imported but unused 5 | 6 | from .main import MaµToMan | ^^^^^^^^ - | help: Remove unused import: `.main.MaμToMan` | 5 | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_stable_F401_24____init__.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_stable_F401_24____init__.py.snap index e1c221c259..3f177105c6 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_stable_F401_24____init__.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_stable_F401_24____init__.py.snap @@ -6,7 +6,6 @@ F401 `sys` imported but unused | 19 | import sys # F401: remove unused | ^^^ - | help: Remove unused import: `sys` F401 `.unused` imported but unused; consider removing, adding to `__all__`, or using a redundant alias @@ -14,7 +13,6 @@ F401 `.unused` imported but unused; consider removing, adding to `__all__`, or u | 33 | from . import unused # F401: change to redundant alias | ^^^^^^ - | help: Use an explicit re-export: `unused as unused` F401 `.renamed` imported but unused; consider removing, adding to `__all__`, or using a redundant alias @@ -22,5 +20,4 @@ F401 `.renamed` imported but unused; consider removing, adding to `__all__`, or | 36 | from . import renamed as bees # F401: no fix | ^^^^ - | help: Use an explicit re-export: `renamed as renamed` diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_stable_F401_25__all_nonempty____init__.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_stable_F401_25__all_nonempty____init__.py.snap index 778c49ba2d..83adc9af29 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_stable_F401_25__all_nonempty____init__.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_stable_F401_25__all_nonempty____init__.py.snap @@ -6,7 +6,6 @@ F401 `sys` imported but unused | 19 | import sys # F401: remove unused | ^^^ - | help: Remove unused import: `sys` F401 `.unused` imported but unused; consider removing, adding to `__all__`, or using a redundant alias @@ -14,7 +13,6 @@ F401 `.unused` imported but unused; consider removing, adding to `__all__`, or u | 36 | from . import unused # F401: add to __all__ | ^^^^^^ - | help: Add unused import `unused` to __all__ F401 `.renamed` imported but unused; consider removing, adding to `__all__`, or using a redundant alias @@ -22,5 +20,4 @@ F401 `.renamed` imported but unused; consider removing, adding to `__all__`, or | 39 | from . import renamed as bees # F401: add to __all__ | ^^^^ - | help: Add unused import `bees` to __all__ diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_stable_F401_26__all_empty____init__.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_stable_F401_26__all_empty____init__.py.snap index 24e342c8ae..1af39f649f 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_stable_F401_26__all_empty____init__.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_stable_F401_26__all_empty____init__.py.snap @@ -6,7 +6,6 @@ F401 `.unused` imported but unused; consider removing, adding to `__all__`, or u | 5 | from . import unused # F401: add to __all__ | ^^^^^^ - | help: Add unused import `unused` to __all__ F401 `.renamed` imported but unused; consider removing, adding to `__all__`, or using a redundant alias @@ -14,5 +13,4 @@ F401 `.renamed` imported but unused; consider removing, adding to `__all__`, or | 8 | from . import renamed as bees # F401: add to __all__ | ^^^^ - | help: Add unused import `bees` to __all__ diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_stable_F401_27__all_mistyped____init__.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_stable_F401_27__all_mistyped____init__.py.snap index f74e91a35b..ecf9aa9a4c 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_stable_F401_27__all_mistyped____init__.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_stable_F401_27__all_mistyped____init__.py.snap @@ -6,7 +6,6 @@ F401 `.unused` imported but unused; consider removing, adding to `__all__`, or u | 5 | from . import unused # F401: recommend add to all w/o fix | ^^^^^^ - | help: Add unused import `unused` to __all__ F401 `.renamed` imported but unused; consider removing, adding to `__all__`, or using a redundant alias @@ -14,5 +13,4 @@ F401 `.renamed` imported but unused; consider removing, adding to `__all__`, or | 8 | from . import renamed as bees # F401: recommend add to all w/o fix | ^^^^ - | help: Add unused import `bees` to __all__ diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_stable_F401_28__all_multiple____init__.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_stable_F401_28__all_multiple____init__.py.snap index 011604a75c..8ad4915685 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_stable_F401_28__all_multiple____init__.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_stable_F401_28__all_multiple____init__.py.snap @@ -6,7 +6,6 @@ F401 `.unused` imported but unused; consider removing, adding to `__all__`, or u | 5 | from . import unused, renamed as bees # F401: add to __all__ | ^^^^^^ - | help: Add unused import `unused` to __all__ F401 `.renamed` imported but unused; consider removing, adding to `__all__`, or using a redundant alias @@ -14,5 +13,4 @@ F401 `.renamed` imported but unused; consider removing, adding to `__all__`, or | 5 | from . import unused, renamed as bees # F401: add to __all__ | ^^^^ - | help: Add unused import `bees` to __all__ diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F404_F404_1.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F404_F404_1.py.snap index af21b5f916..3fe4b7633c 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F404_F404_1.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F404_F404_1.py.snap @@ -8,4 +8,3 @@ F404 `from __future__` imports must occur at the beginning of the file 4 | 5 | from __future__ import absolute_import | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F405_F405.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F405_F405.py.snap index 382d83dce7..3a6dad16aa 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F405_F405.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F405_F405.py.snap @@ -7,7 +7,6 @@ F405 `name` may be undefined, or defined from star imports 4 | def print_name(): 5 | print(name) | ^^^^ - | F405 `a` may be undefined, or defined from star imports --> F405.py:11:12 @@ -16,4 +15,3 @@ F405 `a` may be undefined, or defined from star imports 10 | 11 | __all__ = ['a'] | ^^^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F406_F406.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F406_F406.py.snap index e09f491710..140e3fc15f 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F406_F406.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F406_F406.py.snap @@ -7,7 +7,6 @@ F406 `from F634 import *` only allowed at module level 4 | def f(): 5 | from F634 import * | ^^^^^^^^^^^^^^^^^^ - | F406 `from F634 import *` only allowed at module level --> F406.py:9:5 @@ -15,4 +14,3 @@ F406 `from F634 import *` only allowed at module level 8 | class F: 9 | from F634 import * | ^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F407_F407.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F407_F407.py.snap index 8354bd9884..29d6963cb8 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F407_F407.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F407_F407.py.snap @@ -7,4 +7,3 @@ F407 Future feature `non_existent_feature` is not defined 1 | from __future__ import print_function 2 | from __future__ import non_existent_feature | ^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F502_F502.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F502_F502.py.snap index 25dc86c080..0d2d35cfe3 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F502_F502.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F502_F502.py.snap @@ -73,4 +73,3 @@ F502 `%`-format string expected mapping but got sequence 12 | "%(bob)s" % ("bob" for _ in range(1)) # F202 13 | "%(bob)s" % {"bob" for _ in range(1)} # F202 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | 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 6081ba998f..235818bb47 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 @@ -97,7 +97,6 @@ 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"} | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove extra named arguments: greeting | 19 | # https://github.com/astral-sh/ruff/issues/18806 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 aeed0515b8..a6145805a1 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 @@ -97,7 +97,6 @@ F522 [*] `.format` call has unused named argument(s): greeting 13 | # even though the used argument has a side effect 14 | "Hello, {name}".format(greeting="Pikachu", name=print(1)) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove extra named arguments: greeting | 13 | # even though the used argument has a side effect 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 f48432893c..2c19fe4263 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 @@ -308,7 +308,6 @@ F523 [*] `.format` call has unused arguments at position(s): 1 44 | # even though the used argument has a side effect 45 | "Hello, {0}".format(print(1), "Pikachu") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove extra positional arguments at position(s): 1 | 44 | # even though the used argument has a side effect diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F524_F524.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F524_F524.py.snap index fa097d75ff..5404e9baac 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F524_F524.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F524_F524.py.snap @@ -70,4 +70,3 @@ F524 `.format` call is missing argument(s) for placeholder(s): 8 6 | "{bar} {0}".format() # F524 7 | "{1} {8}".format(0, 1) | ^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F525_F525.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F525_F525.py.snap index c6a8823ae1..00c2da21e3 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F525_F525.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F525_F525.py.snap @@ -15,4 +15,3 @@ F525 `.format` string mixes automatic and manual numbering 1 | "{} {1}".format(1, 2) # F525 2 | "{0} {}".format(1, 2) # F523, F525 | ^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F541_F541.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F541_F541.py.snap index 7c9d0e3f86..07437d8d12 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F541_F541.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F541_F541.py.snap @@ -346,7 +346,6 @@ F541 [*] f-string without any placeholders 43 | f"{v:{f"0.2f"}}" 44 | f"\{{x}}" | ^^^^^^^^^ - | help: Remove extraneous `f` prefix | 43 | f"{v:{f"0.2f"}}" diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F601_F601.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F601_F601.py.snap index 578c2389f7..bbf24bc669 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F601_F601.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F601_F601.py.snap @@ -457,5 +457,4 @@ F601 Dictionary key literal `-1 + 2j` repeated 81 | x = {-1 + 0j: 1, -1: 2} 82 | x = {-1 + 2j: 1, -1 + 2j: 2} | ^^^^^^^ - | help: Remove repeated key literal `-1 + 2j` diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F602_F602.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F602_F602.py.snap index bce704d0d6..2e225de331 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F602_F602.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F602_F602.py.snap @@ -237,7 +237,6 @@ F602 [*] Dictionary key `a` repeated 44 | x = {a: 1, a: 1} 45 | x = {a: 1, b: 2, a: 1} | ^ - | help: Remove repeated key `a` | 44 | x = {a: 1, a: 1} diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F632_F632.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F632_F632.py.snap index b508949bb4..e1df6b06ee 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F632_F632.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F632_F632.py.snap @@ -200,7 +200,6 @@ F632 [*] Use `!=` to compare constant literals 33 | # Regression test for https://github.com/astral-sh/ruff/issues/11736 34 | variable: "123 is not y" | ^^^^^^^^^^^^ - | help: Replace `is not` with `!=` | 33 | # Regression test for https://github.com/astral-sh/ruff/issues/11736 diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F633_F633.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F633_F633.py.snap index 3e06e1bd63..ffa298a8b9 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F633_F633.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F633_F633.py.snap @@ -8,4 +8,3 @@ F633 Use of `>>` is invalid with `print` function 3 | 4 | print >> sys.stderr, "Hello" | ^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F701_F701.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F701_F701.py.snap index cd32ed927c..0f0bc73ada 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F701_F701.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F701_F701.py.snap @@ -19,7 +19,6 @@ F701 `break` outside loop 15 | 16 | break | ^^^^^ - | F701 `break` outside loop --> F701.py:20:5 @@ -27,11 +26,9 @@ F701 `break` outside loop 19 | class Foo: 20 | break | ^^^^^ - | F701 `break` outside loop --> F701.py:23:1 | 23 | break | ^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F702_F702.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F702_F702.py.snap index 5e121ca2c7..08b50691c3 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F702_F702.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F702_F702.py.snap @@ -19,7 +19,6 @@ F702 `continue` not properly in loop 15 | 16 | continue | ^^^^^^^^ - | F702 `continue` not properly in loop --> F702.py:20:5 @@ -27,11 +26,9 @@ F702 `continue` not properly in loop 19 | class Foo: 20 | continue | ^^^^^^^^ - | F702 `continue` not properly in loop --> F702.py:23:1 | 23 | continue | ^^^^^^^^ - | 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 8d3bff4284..5c2625256b 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 @@ -7,7 +7,6 @@ F704 `yield` statement outside of a function 5 | class Foo: 6 | yield 2 | ^^^^^^^ - | F704 `yield` statement outside of a function --> F704.py:9:1 diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F706_F706.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F706_F706.py.snap index a547bebd4f..79dac06195 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F706_F706.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F706_F706.py.snap @@ -7,11 +7,9 @@ F706 `return` statement outside of a function/method 5 | class Foo: 6 | return 2 | ^^^^^^^^ - | F706 `return` statement outside of a function/method --> F706.py:9:1 | 9 | return 3 | ^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F722_F722.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F722_F722.py.snap index 674c6f2bf2..64174f43f5 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F722_F722.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F722_F722.py.snap @@ -72,4 +72,3 @@ F722 Syntax error in forward annotation: unexpected EOF while parsing 43 | | (int 44 | | """ | |___^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F722_F722_1.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F722_F722_1.py.snap index c01fb48cff..cab059100b 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F722_F722_1.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F722_F722_1.py.snap @@ -28,4 +28,3 @@ F722 Syntax error in forward annotation: Unexpected token at the end of an expre 8 | def f(self, arg: "this isn't python") -> "this isn't python either": 9 | x: "this also isn't python" = 1 | ^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_1.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_1.py.snap index 5b01c3bdc7..9b9dac525c 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_1.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_1.py.snap @@ -2,11 +2,10 @@ source: crates/ruff_linter/src/rules/pyflakes/mod.rs --- F811 Redefinition of unused `FU` from line 1 - --> F811_1.py:1:14 + --> F811_1.py:1:25 | 1 | import fu as FU, bar as FU | -- ^^ `FU` redefined here | | | previous definition of `FU` here - | help: Remove definition: `FU` diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_12.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_12.py.snap index 8be7749183..c80929bfe7 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_12.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_12.py.snap @@ -2,7 +2,7 @@ source: crates/ruff_linter/src/rules/pyflakes/mod.rs --- F811 Redefinition of unused `mixer` from line 2 - --> F811_12.py:2:20 + --> F811_12.py:6:20 | 1 | try: 2 | from aa import mixer diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_15.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_15.py.snap index e48bf8e55e..01628182ea 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_15.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_15.py.snap @@ -12,5 +12,4 @@ F811 Redefinition of unused `fu` from line 1 | 1 | import fu | -- previous definition of `fu` here - | help: Remove definition: `fu` diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_16.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_16.py.snap index 704155bf81..da83aef0db 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_16.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_16.py.snap @@ -16,5 +16,4 @@ F811 Redefinition of unused `fu` from line 3 2 | 3 | import fu | -- previous definition of `fu` here - | help: Remove definition: `fu` diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_17.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_17.py.snap index 67cc97dd61..05b281f2b1 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_17.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_17.py.snap @@ -15,7 +15,6 @@ F811 [*] Redefinition of unused `fu` from line 2 1 | """Test that shadowing a global name with a nested function generates a warning.""" 2 | import fu | -- previous definition of `fu` here - | help: Remove definition: `fu` | 5 | def bar(): @@ -24,7 +23,7 @@ help: Remove definition: `fu` | F811 Redefinition of unused `fu` from line 6 - --> F811_17.py:6:12 + --> F811_17.py:9:13 | 5 | def bar(): 6 | import fu diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_2.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_2.py.snap index 7aa2e1019d..9f31e1e537 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_2.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_2.py.snap @@ -2,11 +2,10 @@ source: crates/ruff_linter/src/rules/pyflakes/mod.rs --- F811 Redefinition of unused `FU` from line 1 - --> F811_2.py:1:23 + --> F811_2.py:1:34 | 1 | from moo import fu as FU, bar as FU | -- ^^ `FU` redefined here | | | previous definition of `FU` here - | help: Remove definition: `FU` diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_23.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_23.py.snap index 46b2551427..075c3bcec3 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_23.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_23.py.snap @@ -2,7 +2,7 @@ source: crates/ruff_linter/src/rules/pyflakes/mod.rs --- F811 Redefinition of unused `foo` from line 3 - --> F811_23.py:3:15 + --> F811_23.py:4:15 | 1 | """Test that shadowing an explicit re-export produces a warning.""" 2 | @@ -10,5 +10,4 @@ F811 Redefinition of unused `foo` from line 3 | --- previous definition of `foo` here 4 | import bar as foo | ^^^ `foo` redefined here - | help: Remove definition: `foo` diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_26.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_26.py.snap index 1025efb9fd..5446487548 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_26.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_26.py.snap @@ -2,7 +2,7 @@ source: crates/ruff_linter/src/rules/pyflakes/mod.rs --- F811 Redefinition of unused `func` from line 2 - --> F811_26.py:2:9 + --> F811_26.py:5:9 | 1 | class Class: 2 | def func(self): diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_28.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_28.py.snap index cf120b2cd7..b7c57897d8 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_28.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_28.py.snap @@ -2,7 +2,7 @@ source: crates/ruff_linter/src/rules/pyflakes/mod.rs --- F811 Redefinition of unused `datetime` from line 3 - --> F811_28.py:3:8 + --> F811_28.py:4:22 | 1 | """Regression test for: https://github.com/astral-sh/ruff/issues/10384""" 2 | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_29.pyi.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_29.pyi.snap index a7e005d269..82d384b996 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_29.pyi.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_29.pyi.snap @@ -2,7 +2,7 @@ source: crates/ruff_linter/src/rules/pyflakes/mod.rs --- F811 Redefinition of unused `Bar` from line 3 - --> F811_29.pyi:3:24 + --> F811_29.pyi:8:1 | 1 | """Regression test for: https://github.com/astral-sh/ruff/issues/10509""" 2 | @@ -14,5 +14,4 @@ F811 Redefinition of unused `Bar` from line 3 7 | 8 | Bar = 1 # F811 | ^^^ `Bar` redefined here - | help: Remove definition: `Bar` diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_3.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_3.py.snap index df164062ce..1509fbde81 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_3.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_3.py.snap @@ -2,11 +2,10 @@ source: crates/ruff_linter/src/rules/pyflakes/mod.rs --- F811 Redefinition of unused `fu` from line 1 - --> F811_3.py:1:8 + --> F811_3.py:1:12 | 1 | import fu; fu = 3 | -- ^^ `fu` redefined here | | | previous definition of `fu` here - | help: Remove definition: `fu` diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_30.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_30.py.snap index d0de3c4786..6e19457c89 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_30.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_30.py.snap @@ -2,7 +2,7 @@ source: crates/ruff_linter/src/rules/pyflakes/mod.rs --- F811 Redefinition of unused `bar` from line 10 - --> F811_30.py:10:5 + --> F811_30.py:12:9 | 8 | """Foo.""" 9 | @@ -16,7 +16,7 @@ F811 Redefinition of unused `bar` from line 10 help: Remove definition: `bar` F811 Redefinition of unused `baz` from line 18 - --> F811_30.py:18:9 + --> F811_30.py:21:5 | 16 | class B: 17 | """B.""" @@ -26,11 +26,10 @@ F811 Redefinition of unused `baz` from line 18 20 | 21 | baz = 1 | ^^^ `baz` redefined here - | help: Remove definition: `baz` F811 Redefinition of unused `foo` from line 26 - --> F811_30.py:26:9 + --> F811_30.py:29:12 | 24 | class C: 25 | """C.""" @@ -40,5 +39,4 @@ F811 Redefinition of unused `foo` from line 26 28 | 29 | bar = (foo := 1) | ^^^ `foo` redefined here - | help: Remove definition: `foo` diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_31.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_31.py.snap index 1e8be05daa..06efe5ea8b 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_31.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_31.py.snap @@ -2,7 +2,7 @@ source: crates/ruff_linter/src/rules/pyflakes/mod.rs --- F811 Redefinition of unused `baz` from line 17 - --> F811_31.py:17:5 + --> F811_31.py:19:29 | 16 | try: 17 | baz = None diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_32.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_32.py.snap index 7b6d5a1e70..9d765221ab 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_32.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_32.py.snap @@ -2,7 +2,7 @@ source: crates/ruff_linter/src/rules/pyflakes/mod.rs --- F811 [*] Redefinition of unused `List` from line 4 - --> F811_32.py:4:5 + --> F811_32.py:5:5 | 3 | from typing import ( 4 | List, diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_35.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_35.py.snap index 1225c07666..d71938d88e 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_35.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_35.py.snap @@ -2,7 +2,7 @@ source: crates/ruff_linter/src/rules/pyflakes/mod.rs --- F811 Redefinition of unused `bar` from line 7 - --> F811_35.py:7:9 + --> F811_35.py:9:9 | 6 | class Foo: 7 | def bar(self): ... @@ -10,7 +10,6 @@ F811 Redefinition of unused `bar` from line 7 8 | 9 | def bar(self): ... | ^^^ `bar` redefined here - | help: Remove definition: `bar` F811 Redefinition of unused `bar` from line 3 @@ -25,5 +24,4 @@ F811 Redefinition of unused `bar` from line 3 2 | 3 | from foo import bar | --- previous definition of `bar` here - | help: Remove definition: `bar` diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_4.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_4.py.snap index 0148e0bde1..de3ad8e353 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_4.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_4.py.snap @@ -2,11 +2,10 @@ source: crates/ruff_linter/src/rules/pyflakes/mod.rs --- F811 Redefinition of unused `fu` from line 1 - --> F811_4.py:1:8 + --> F811_4.py:1:12 | 1 | import fu; fu, bar = 3 | -- ^^ `fu` redefined here | | | previous definition of `fu` here - | help: Remove definition: `fu` diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_5.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_5.py.snap index 420172d79e..ce327c3c68 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_5.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_5.py.snap @@ -2,11 +2,10 @@ source: crates/ruff_linter/src/rules/pyflakes/mod.rs --- F811 Redefinition of unused `fu` from line 1 - --> F811_5.py:1:8 + --> F811_5.py:1:13 | 1 | import fu; [fu, bar] = 3 | -- ^^ `fu` redefined here | | | previous definition of `fu` here - | help: Remove definition: `fu` diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_6.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_6.py.snap index ab9ee4bf1c..661e301b48 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_6.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_6.py.snap @@ -2,7 +2,7 @@ source: crates/ruff_linter/src/rules/pyflakes/mod.rs --- F811 [*] Redefinition of unused `os` from line 5 - --> F811_6.py:5:12 + --> F811_6.py:6:12 | 3 | i = 2 4 | if i == 1: diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_8.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_8.py.snap index 3ffa550828..f66e56508e 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_8.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_8.py.snap @@ -2,7 +2,7 @@ source: crates/ruff_linter/src/rules/pyflakes/mod.rs --- F811 [*] Redefinition of unused `os` from line 4 - --> F811_8.py:4:12 + --> F811_8.py:5:12 | 3 | try: 4 | import os diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_0.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_0.py.snap index fd16a70827..f8851825ef 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_0.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_0.py.snap @@ -7,7 +7,6 @@ F821 Undefined name `self` 1 | def get_name(): 2 | return self.name | ^^^^ - | F821 Undefined name `self` --> F821_0.py:6:13 @@ -15,7 +14,6 @@ F821 Undefined name `self` 5 | def get_name(): 6 | return (self.name,) | ^^^^ - | F821 Undefined name `self` --> F821_0.py:10:9 @@ -23,7 +21,6 @@ F821 Undefined name `self` 9 | def get_name(): 10 | del self.name | ^^^^ - | F821 Undefined name `numeric_string` --> F821_0.py:21:12 @@ -31,7 +28,6 @@ F821 Undefined name `numeric_string` 20 | def randdec(maxprec, maxexp): 21 | return numeric_string(maxprec, maxexp) | ^^^^^^^^^^^^^^ - | F821 Undefined name `Bar` --> F821_0.py:58:5 diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_1.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_1.py.snap index 7df73254f3..9121c32648 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_1.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_1.py.snap @@ -7,7 +7,6 @@ F821 Undefined name `Model` 10 | # F821 Undefined name `Model` 11 | x = cast("Model", x) | ^^^^^ - | F821 Undefined name `Model` --> F821_1.py:18:18 @@ -15,7 +14,6 @@ F821 Undefined name `Model` 17 | # F821 Undefined name `Model` 18 | x = typing.cast("Model", x) | ^^^^^ - | F821 Undefined name `Model` --> F821_1.py:24:14 @@ -23,7 +21,6 @@ F821 Undefined name `Model` 23 | # F821 Undefined name `Model` 24 | x = Pattern["Model"] | ^^^^^ - | F821 Undefined name `Model` --> F821_1.py:30:12 @@ -31,4 +28,3 @@ F821 Undefined name `Model` 29 | # F821 Undefined name `Model` 30 | x = Match["Model"] | ^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_11.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_11.py.snap index 18a0d3e859..d1c26106f9 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_11.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_11.py.snap @@ -15,4 +15,3 @@ F821 Undefined name `Baz` 22 | f(Callable[["Bar"], None]) 23 | f(Callable[["Baz"], None]) | ^^^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_12.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_12.py.snap index fda19cffaf..b4cb736957 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_12.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_12.py.snap @@ -15,4 +15,3 @@ F821 Undefined name `Baz` 24 | f(Callable[["Bar"], None]) 25 | f(Callable[["Baz"], None]) | ^^^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_13.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_13.py.snap index 0b977f2213..f9612cabf9 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_13.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_13.py.snap @@ -8,4 +8,3 @@ F821 Undefined name `List` 7 | 8 | Z = TypeVar("X", "List[int]", "int") | ^^^^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_17.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_17.py.snap index fbbd199636..08f808b86e 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_17.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_17.py.snap @@ -48,7 +48,6 @@ F821 Undefined name `T` 41 | def foo[T](t: T) -> None: ... 42 | T # F821: Undefined name `T` - not accessible afterward function scope | ^ - | F821 Undefined name `T` --> F821_17.py:64:17 diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_18.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_18.py.snap index 95d305eb2b..3377400616 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_18.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_18.py.snap @@ -8,4 +8,3 @@ F821 Undefined name `y` 18 | x: (y := 1) 19 | print(y) | ^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_19.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_19.py.snap index 022fe287ba..189c092931 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_19.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_19.py.snap @@ -8,4 +8,3 @@ F821 Undefined name `y` 20 | x: (y := 1) 21 | print(y) | ^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_21.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_21.py.snap index 4e5196ca9d..c4de707981 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_21.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_21.py.snap @@ -7,4 +7,3 @@ F821 Undefined name `display` 3 | x = 1 4 | display(x) | ^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_28.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_28.py.snap index 348d12a438..80ca1633f4 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_28.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_28.py.snap @@ -8,4 +8,3 @@ F821 Undefined name `𝒟` 8 | 9 | print(𝒟) # F821 | ^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_31.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_31.py.snap index c1a10a41ff..9965d0a78f 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_31.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_31.py.snap @@ -28,7 +28,6 @@ F821 Undefined name `B` 8 | def f(self, arg: "B") -> "S": 9 | x: "B" = 1 | ^ - | F821 Undefined name `A` --> F821_31.py:15:13 @@ -55,4 +54,3 @@ F821 Undefined name `A` 15 | def f(arg: "A") -> "R": 16 | x: "A" = 1 | ^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_34.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_34.py.snap index e4a8c02b7e..74562ee54e 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_34.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_34.py.snap @@ -8,7 +8,6 @@ F821 Undefined name `x` 8 | x: int 9 | print(x) | ^ - | F821 Undefined name `x` --> F821_34.py:17:16 diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_34.pyi.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_34.pyi.snap index 25d4503037..ecadafcf34 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_34.pyi.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_34.pyi.snap @@ -18,4 +18,3 @@ F821 Undefined name `Undefined` 21 | # Error: name that was never defined 22 | def g(x: Undefined) -> None: ... # F821 | ^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_4.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_4.py.snap index dc5ceec156..3592edd5eb 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_4.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_4.py.snap @@ -8,7 +8,6 @@ F821 Undefined name `Model` 3 | 4 | _ = List["Model"] | ^^^^^ - | F821 Undefined name `Model` --> F821_4.py:9:12 @@ -17,7 +16,6 @@ F821 Undefined name `Model` 8 | 9 | _ = IList["Model"] | ^^^^^ - | F821 Undefined name `Model` --> F821_4.py:14:16 @@ -26,7 +24,6 @@ F821 Undefined name `Model` 13 | 14 | _ = ItemsView["Model"] | ^^^^^ - | F821 Undefined name `Model` --> F821_4.py:19:32 @@ -35,7 +32,6 @@ F821 Undefined name `Model` 18 | 19 | _ = collections.abc.ItemsView["Model"] | ^^^^^ - | F821 Undefined name `Model` --> F821_4.py:24:20 @@ -44,4 +40,3 @@ F821 Undefined name `Model` 23 | 24 | _ = abc.ItemsView["Model"] | ^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_7.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_7.py.snap index a818f1c1f9..fbb5f6f690 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_7.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_7.py.snap @@ -28,4 +28,3 @@ F821 Undefined name `Undefined` 12 | _ = DefaultNamedArg(type="Undefined", name="some_prop_name") 13 | _ = DefaultNamedArg("Undefined", "some_prop_name") | ^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_9.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_9.py.snap index ce557ed224..988f9cd443 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_9.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_9.py.snap @@ -8,4 +8,3 @@ F821 Undefined name `captured` 21 | case True: 22 | return captured # F821 | ^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_basedpython.by.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_basedpython.by.snap index c3fc83d3dc..ec2ec62e92 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_basedpython.by.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_basedpython.by.snap @@ -155,7 +155,6 @@ F821 Undefined name `Bleu` 124 | chosen: Colour = Green 125 | missing: Colour = Bleu # F821 | ^^^^ - | F821 Undefined name `a_receiver_member` --> F821_basedpython.by:147:1 @@ -163,7 +162,6 @@ F821 Undefined name `a_receiver_member` 146 | # but the deferral stops at the block: the same name outside one is reported 147 | a_receiver_member("x") # F821 | ^^^^^^^^^^^^^^^^^ - | F821 Undefined name `UndefinedPattern` --> F821_basedpython.by:178:12 @@ -223,4 +221,3 @@ F821 Undefined name `no_lookup_for_ne` 224 | Book.objects.filter(data[undefined_index] == 1) # F821 225 | Book.objects.filter(no_lookup_for_ne != 1) # F821 | ^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F822_F822_0.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F822_F822_0.py.snap index f75fb59fdd..65aa9fbc90 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F822_F822_0.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F822_F822_0.py.snap @@ -8,4 +8,3 @@ F822 Undefined name `b` in `__all__` 2 | 3 | __all__ = ["a", "b"] | ^^^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F822_F822_0.pyi.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F822_F822_0.pyi.snap index 761c68b678..6afb496308 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F822_F822_0.pyi.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F822_F822_0.pyi.snap @@ -8,4 +8,3 @@ F822 Undefined name `c` in `__all__` 3 | 4 | __all__ = ["a", "b", "c"] # c is flagged as missing; b is not | ^^^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F822_F822_1.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F822_F822_1.py.snap index ead94dbcaa..69aa7d0528 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F822_F822_1.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F822_F822_1.py.snap @@ -8,4 +8,3 @@ F822 Undefined name `b` in `__all__` 2 | 3 | __all__ = list(["a", "b"]) | ^^^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F822_F822_1b.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F822_F822_1b.py.snap index aa41c02a6f..b683b34ad9 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F822_F822_1b.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F822_F822_1b.py.snap @@ -8,4 +8,3 @@ F822 Undefined name `b` in `__all__` 3 | 4 | __all__ = builtins.list(["a", "b"]) | ^^^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F823_F823.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F823_F823.py.snap index 4fded2b2fb..ba3860ea88 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F823_F823.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F823_F823.py.snap @@ -7,7 +7,6 @@ F823 Local variable `my_var` referenced before assignment 5 | def foo(): 6 | my_var += 1 | ^^^^^^ - | F823 Local variable `my_var` referenced before assignment --> F823.py:32:15 diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F841_F841_0.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F841_F841_0.py.snap index 5a8ebc4758..203edd2357 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F841_F841_0.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F841_F841_0.py.snap @@ -25,7 +25,6 @@ F841 [*] Local variable `z` is assigned to but never used 15 | y = 2 16 | z = x + y | ^ - | help: Remove assignment to unused variable `z` | 15 | y = 2 @@ -96,7 +95,6 @@ F841 [*] Local variable `baz` is assigned to but never used 25 | 26 | (x, y) = baz = bar | ^^^ - | help: Remove assignment to unused variable `baz` | 25 | @@ -226,7 +224,6 @@ F841 [*] Local variable `__class__` is assigned to but never used 167 | def set_class(self, cls): 168 | __class__ = cls # F841 | ^^^^^^^^^ - | help: Remove assignment to unused variable `__class__` | 167 | def set_class(self, cls): @@ -243,7 +240,6 @@ F841 [*] Local variable `__class__` is assigned to but never used 173 | def set_class(self, cls): 174 | __class__ = cls # F841 | ^^^^^^^^^ - | help: Remove assignment to unused variable `__class__` | 173 | def set_class(self, cls): @@ -260,7 +256,6 @@ F841 [*] Local variable `__class__` is assigned to but never used 181 | def set_class(self, cls): 182 | __class__ = cls # F841 | ^^^^^^^^^ - | help: Remove assignment to unused variable `__class__` | 181 | def set_class(self, cls): diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F841_F841_1.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F841_F841_1.py.snap index 01d3b6b092..796f69c96d 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F841_F841_1.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F841_F841_1.py.snap @@ -7,7 +7,6 @@ F841 [*] Local variable `x` is assigned to but never used 5 | def f(): 6 | x, y = 1, 2 # this triggers F841 as it's just a simple assignment where unpacking isn't needed | ^ - | help: Remove assignment to unused variable `x` | 5 | def f(): @@ -23,7 +22,6 @@ F841 [*] Local variable `y` is assigned to but never used 5 | def f(): 6 | x, y = 1, 2 # this triggers F841 as it's just a simple assignment where unpacking isn't needed | ^ - | help: Remove assignment to unused variable `y` | 5 | def f(): @@ -39,7 +37,6 @@ F841 [*] Local variable `coords` is assigned to but never used 15 | def f(): 16 | (x, y) = coords = 1, 2 | ^^^^^^ - | help: Remove assignment to unused variable `coords` | 15 | def f(): @@ -55,7 +52,6 @@ F841 [*] Local variable `coords` is assigned to but never used 19 | def f(): 20 | coords = (x, y) = 1, 2 | ^^^^^^ - | help: Remove assignment to unused variable `coords` | 19 | def f(): @@ -71,7 +67,6 @@ F841 [*] Local variable `a` is assigned to but never used 23 | def f(): 24 | (a, b) = (x, y) = 1, 2 # this triggers F841 on everything | ^ - | help: Remove assignment to unused variable `a` | 23 | def f(): @@ -86,7 +81,6 @@ F841 [*] Local variable `b` is assigned to but never used 23 | def f(): 24 | (a, b) = (x, y) = 1, 2 # this triggers F841 on everything | ^ - | help: Remove assignment to unused variable `b` | 23 | def f(): @@ -101,7 +95,6 @@ F841 [*] Local variable `x` is assigned to but never used 23 | def f(): 24 | (a, b) = (x, y) = 1, 2 # this triggers F841 on everything | ^ - | help: Remove assignment to unused variable `x` | 23 | def f(): @@ -116,7 +109,6 @@ F841 [*] Local variable `y` is assigned to but never used 23 | def f(): 24 | (a, b) = (x, y) = 1, 2 # this triggers F841 on everything | ^ - | help: Remove assignment to unused variable `y` | 23 | def f(): diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F841_F841_3.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F841_F841_3.py.snap index 519c1a3591..50f8c9deb6 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F841_F841_3.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F841_F841_3.py.snap @@ -201,7 +201,6 @@ F841 [*] Local variable `coords3` is assigned to but never used 33 | (x2, y2) = coords2 = (1, 2) 34 | coords3 = (x3, y3) = (1, 2) | ^^^^^^^ - | help: Remove assignment to unused variable `coords3` | 33 | (x2, y2) = coords2 = (1, 2) @@ -396,7 +395,6 @@ F841 [*] Local variable `toplevel` is assigned to but never used 97 | def f(): 98 | toplevel = tt = lexer.get_token() | ^^^^^^^^ - | help: Remove assignment to unused variable `toplevel` | 97 | def f(): @@ -412,7 +410,6 @@ F841 [*] Local variable `tt` is assigned to but never used 97 | def f(): 98 | toplevel = tt = lexer.get_token() | ^^ - | help: Remove assignment to unused variable `tt` | 97 | def f(): @@ -428,7 +425,6 @@ F841 [*] Local variable `toplevel` is assigned to but never used 101 | def f(): 102 | toplevel = (a, b) = lexer.get_token() | ^^^^^^^^ - | help: Remove assignment to unused variable `toplevel` | 101 | def f(): @@ -444,7 +440,6 @@ F841 [*] Local variable `toplevel` is assigned to but never used 105 | def f(): 106 | (a, b) = toplevel = lexer.get_token() | ^^^^^^^^ - | help: Remove assignment to unused variable `toplevel` | 105 | def f(): @@ -460,7 +455,6 @@ F841 [*] Local variable `toplevel` is assigned to but never used 109 | def f(): 110 | toplevel = tt = 1 | ^^^^^^^^ - | help: Remove assignment to unused variable `toplevel` | 109 | def f(): @@ -476,7 +470,6 @@ F841 [*] Local variable `tt` is assigned to but never used 109 | def f(): 110 | toplevel = tt = 1 | ^^ - | help: Remove assignment to unused variable `tt` | 109 | def f(): @@ -592,7 +585,6 @@ F841 [*] Local variable `y` is assigned to but never used 160 | x = 1 161 | y = 2 | ^ - | help: Remove assignment to unused variable `y` | 160 | x = 1 @@ -625,7 +617,6 @@ F841 [*] Local variable `y` is assigned to but never used 166 | 167 | y = 2 | ^ - | help: Remove assignment to unused variable `y` | 166 | @@ -641,7 +632,6 @@ F841 [*] Local variable `x` is assigned to but never used 172 | ((x)) = foo() 173 | (x) = (y.z) = foo() | ^ - | help: Remove assignment to unused variable `x` | 172 | ((x)) = foo() diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F842_F842.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F842_F842.py.snap index 49b28284d8..b81e3bd265 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F842_F842.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F842_F842.py.snap @@ -17,4 +17,3 @@ F842 Local variable `age` is annotated but never used 2 | name: str 3 | age: int | ^^^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F901_F901.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F901_F901.py.snap index 50ca752ceb..a4bbb7c51f 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F901_F901.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F901_F901.py.snap @@ -7,7 +7,6 @@ F901 [*] `raise NotImplemented` should be `raise NotImplementedError` 1 | def f() -> None: 2 | raise NotImplemented() | ^^^^^^^^^^^^^^ - | help: Use `raise NotImplementedError` | 1 | def f() -> None: @@ -22,7 +21,6 @@ F901 [*] `raise NotImplemented` should be `raise NotImplementedError` 5 | def g() -> None: 6 | raise NotImplemented | ^^^^^^^^^^^^^^ - | help: Use `raise NotImplementedError` | 5 | def g() -> None: @@ -38,7 +36,6 @@ F901 [*] `raise NotImplemented` should be `raise NotImplementedError` 10 | NotImplementedError = "foo" 11 | raise NotImplemented | ^^^^^^^^^^^^^^ - | help: Use `raise NotImplementedError` | 1 + import builtins diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__augmented_assignment_after_del.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__augmented_assignment_after_del.snap index bebb611006..06c954b7d4 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__augmented_assignment_after_del.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__augmented_assignment_after_del.snap @@ -8,7 +8,6 @@ F821 Undefined name `x` 9 | # error, because the name is defined in the scope, but unbound. 10 | x += 1 | ^ - | F841 Local variable `x` is assigned to but never used --> :10:5 @@ -17,5 +16,4 @@ F841 Local variable `x` is assigned to but never used 9 | # error, because the name is defined in the scope, but unbound. 10 | x += 1 | ^ - | help: Remove assignment to unused variable `x` diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__default_builtins.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__default_builtins.snap index 3f863c9e60..63b018cd5f 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__default_builtins.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__default_builtins.snap @@ -6,4 +6,3 @@ F821 Undefined name `_` | 1 | _("Translations") | ^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__del_shadowed_global_import_in_local_scope.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__del_shadowed_global_import_in_local_scope.snap index a383786a30..7ee75ac2a2 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__del_shadowed_global_import_in_local_scope.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__del_shadowed_global_import_in_local_scope.snap @@ -17,7 +17,7 @@ help: Remove unused import: `os` | F811 [*] Redefinition of unused `os` from line 2 - --> :2:8 + --> :5:12 | 2 | import os | -- previous definition of `os` here diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__del_shadowed_import_shadow_in_local_scope.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__del_shadowed_import_shadow_in_local_scope.snap index b8432d92eb..c4c0211abd 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__del_shadowed_import_shadow_in_local_scope.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__del_shadowed_import_shadow_in_local_scope.snap @@ -17,7 +17,7 @@ help: Remove unused import: `os` | F811 Redefinition of unused `os` from line 2 - --> :2:8 + --> :5:5 | 2 | import os | -- previous definition of `os` here diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__del_shadowed_local_import_in_local_scope.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__del_shadowed_local_import_in_local_scope.snap index b916366723..3f3e2d46a8 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__del_shadowed_local_import_in_local_scope.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__del_shadowed_local_import_in_local_scope.snap @@ -2,7 +2,7 @@ source: crates/ruff_linter/src/rules/pyflakes/mod.rs --- F811 [*] Redefinition of unused `os` from line 3 - --> :3:12 + --> :4:12 | 2 | def f(): 3 | import os diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__double_del.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__double_del.snap index 51dc063477..f37a84f085 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__double_del.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__double_del.snap @@ -8,4 +8,3 @@ F821 Undefined name `x` 4 | del x 5 | del x | ^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__extra_typing_modules.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__extra_typing_modules.snap index 3fcb7752c8..d73e83eaa7 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__extra_typing_modules.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__extra_typing_modules.snap @@ -7,4 +7,3 @@ F821 Undefined name `Class` 6 | X = Union[Literal[False], Literal["db"]] 7 | y = Optional["Class"] | ^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f401_allowed_unused_imports_option.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f401_allowed_unused_imports_option.snap index 97bb10e95d..52fe179d2b 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f401_allowed_unused_imports_option.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f401_allowed_unused_imports_option.snap @@ -7,7 +7,6 @@ F401 [*] `hvplot.pandas_alias.scatter_matrix` imported but unused 11 | # Errors 12 | from hvplot.pandas_alias import scatter_matrix | ^^^^^^^^^^^^^^ - | help: Remove unused import: `hvplot.pandas_alias.scatter_matrix` | 11 | # Errors diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f401_multiple_unused_submodules.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f401_multiple_unused_submodules.snap index e71f1d6489..b6107c54f5 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f401_multiple_unused_submodules.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f401_multiple_unused_submodules.snap @@ -38,7 +38,6 @@ F401 [*] `a.c` imported but unused 3 | import a.b 4 | import a.c | ^^^ - | help: Remove unused import: `a.c` | 3 | import a.b diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f401_preview_first_party_submodule_no_dunder_all.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f401_preview_first_party_submodule_no_dunder_all.snap index 1c463aa7e2..a3bee1b71b 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f401_preview_first_party_submodule_no_dunder_all.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f401_preview_first_party_submodule_no_dunder_all.snap @@ -6,5 +6,4 @@ F401 `submodule.a` imported but unused; consider removing, adding to `__all__`, | 1 | import submodule.a | ^^^^^^^^^^^ - | help: Use an explicit re-export: `import submodule as submodule; import submodule.a` diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f401_type_checking.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f401_type_checking.snap index 58dd09d89e..5a4fdbc283 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f401_type_checking.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f401_type_checking.snap @@ -54,7 +54,6 @@ F401 [*] `mlflow.pyfunc.loaders.responses_agent` imported but unused 7 | if IS_PYDANTIC_V2_OR_NEWER: 8 | import mlflow.pyfunc.loaders.responses_agent | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove unused import: `mlflow.pyfunc.loaders.responses_agent` | 7 | if IS_PYDANTIC_V2_OR_NEWER: diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f811_annotated_assignment_redefinition.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f811_annotated_assignment_redefinition.snap index 4f60b19931..7d42fbd7c6 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f811_annotated_assignment_redefinition.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f811_annotated_assignment_redefinition.snap @@ -2,7 +2,7 @@ source: crates/ruff_linter/src/rules/pyflakes/mod.rs --- F811 Redefinition of unused `bar` from line 4 - --> F811_34.py:4:1 + --> F811_34.py:5:1 | 3 | # F811: both annotated assignments, first unused 4 | bar: int = 1 @@ -15,7 +15,7 @@ F811 Redefinition of unused `bar` from line 4 help: Remove definition: `bar` F811 Redefinition of unused `x` from line 7 - --> F811_34.py:7:1 + --> F811_34.py:8:1 | 5 | bar: int = 2 # F811 6 | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f821_frozendict_pre_py315_undefined.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f821_frozendict_pre_py315_undefined.snap index 544b0efd4a..4a6ea72cda 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f821_frozendict_pre_py315_undefined.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f821_frozendict_pre_py315_undefined.snap @@ -6,4 +6,3 @@ F821 Undefined name `frozendict`. Consider specifying `requires-python = ">= 3.1 | 1 | frozendict | ^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f821_with_builtin_added_on_new_py_version_but_old_target_version_specified.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f821_with_builtin_added_on_new_py_version_but_old_target_version_specified.snap index 072e06e5ef..95e471d5a4 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f821_with_builtin_added_on_new_py_version_but_old_target_version_specified.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f821_with_builtin_added_on_new_py_version_but_old_target_version_specified.snap @@ -6,4 +6,3 @@ F821 Undefined name `PythonFinalizationError`. Consider specifying `requires-pyt | 1 | PythonFinalizationError | ^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f841_dummy_variable_rgx.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f841_dummy_variable_rgx.snap index d798a87491..c0080449f7 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f841_dummy_variable_rgx.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__f841_dummy_variable_rgx.snap @@ -65,7 +65,6 @@ F841 [*] Local variable `baz` is assigned to but never used 25 | 26 | (x, y) = baz = bar | ^^^ - | help: Remove assignment to unused variable `baz` | 25 | @@ -116,7 +115,6 @@ F841 [*] Local variable `_discarded` is assigned to but never used 36 | __ = 1 37 | _discarded = 1 | ^^^^^^^^^^ - | help: Remove assignment to unused variable `_discarded` | 36 | __ = 1 @@ -262,7 +260,6 @@ F841 [*] Local variable `__class__` is assigned to but never used 167 | def set_class(self, cls): 168 | __class__ = cls # F841 | ^^^^^^^^^ - | help: Remove assignment to unused variable `__class__` | 167 | def set_class(self, cls): @@ -279,7 +276,6 @@ F841 [*] Local variable `__class__` is assigned to but never used 173 | def set_class(self, cls): 174 | __class__ = cls # F841 | ^^^^^^^^^ - | help: Remove assignment to unused variable `__class__` | 173 | def set_class(self, cls): @@ -296,7 +292,6 @@ F841 [*] Local variable `__class__` is assigned to but never used 181 | def set_class(self, cls): 182 | __class__ = cls # F841 | ^^^^^^^^^ - | help: Remove assignment to unused variable `__class__` | 181 | def set_class(self, cls): diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__load_after_unbind_from_class_scope.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__load_after_unbind_from_class_scope.snap index 92d61b65c5..40050a846a 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__load_after_unbind_from_class_scope.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__load_after_unbind_from_class_scope.snap @@ -25,4 +25,3 @@ F821 Undefined name `x` 12 | # `x` in `x = 1`. 13 | print(x) | ^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__multi_statement_lines.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__multi_statement_lines.snap index 6901f7080a..7a2cac0005 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__multi_statement_lines.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__multi_statement_lines.snap @@ -75,7 +75,6 @@ F401 [*] `foo5` imported but unused 13 | if True: 14 | x = 1; import foo5 | ^^^^ - | help: Remove unused import: `foo5` | 13 | if True: @@ -216,7 +215,6 @@ F401 [*] `foo13` imported but unused 46 | \ 47 | import foo13 | ^^^^^ - | help: Remove unused import: `foo13` | 44 | if True: @@ -270,7 +268,6 @@ F401 [*] `foo16` imported but unused 61 | x = 1; \ 62 | import foo16 | ^^^^^ - | help: Remove unused import: `foo16` | 60 | # error.) diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__nested_relative_typing_module.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__nested_relative_typing_module.snap index d9bbd4c321..bc6c7578f7 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__nested_relative_typing_module.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__nested_relative_typing_module.snap @@ -7,7 +7,6 @@ F821 Undefined name `foo` 25 | # F821 26 | x: Literal["foo"] | ^^^ - | F821 Undefined name `foo` --> baz.py:33:17 @@ -15,4 +14,3 @@ F821 Undefined name `foo` 32 | # F821 33 | x: Literal["foo"] | ^^^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F401_F401_24____init__.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F401_F401_24____init__.py.snap index 70a444af06..b5dc23d607 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F401_F401_24____init__.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F401_F401_24____init__.py.snap @@ -6,7 +6,6 @@ F401 [*] `sys` imported but unused | 19 | import sys # F401: remove unused | ^^^ - | help: Remove unused import: `sys` | 18 | @@ -20,7 +19,6 @@ F401 [*] `.unused` imported but unused; consider removing, adding to `__all__`, | 33 | from . import unused # F401: change to redundant alias | ^^^^^^ - | help: Use an explicit re-export: `unused as unused` | 32 | @@ -34,5 +32,4 @@ F401 `.renamed` imported but unused; consider removing, adding to `__all__`, or | 36 | from . import renamed as bees # F401: no fix | ^^^^ - | help: Use an explicit re-export: `renamed as renamed` diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F401_F401_25__all_nonempty____init__.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F401_F401_25__all_nonempty____init__.py.snap index 3c2df79288..23c50e1af7 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F401_F401_25__all_nonempty____init__.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F401_F401_25__all_nonempty____init__.py.snap @@ -6,7 +6,6 @@ F401 [*] `sys` imported but unused | 19 | import sys # F401: remove unused | ^^^ - | help: Remove unused import: `sys` | 18 | @@ -20,7 +19,6 @@ F401 [*] `.unused` imported but unused; consider removing, adding to `__all__`, | 36 | from . import unused # F401: add to __all__ | ^^^^^^ - | help: Add unused import `unused` to __all__ | 41 | @@ -33,7 +31,6 @@ F401 [*] `.renamed` imported but unused; consider removing, adding to `__all__`, | 39 | from . import renamed as bees # F401: add to __all__ | ^^^^ - | help: Add unused import `bees` to __all__ | 41 | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F401_F401_26__all_empty____init__.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F401_F401_26__all_empty____init__.py.snap index 1a89b8c282..83b777d778 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F401_F401_26__all_empty____init__.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F401_F401_26__all_empty____init__.py.snap @@ -6,7 +6,6 @@ F401 [*] `.unused` imported but unused; consider removing, adding to `__all__`, | 5 | from . import unused # F401: add to __all__ | ^^^^^^ - | help: Add unused import `unused` to __all__ | 10 | @@ -19,7 +18,6 @@ F401 [*] `.renamed` imported but unused; consider removing, adding to `__all__`, | 8 | from . import renamed as bees # F401: add to __all__ | ^^^^ - | help: Add unused import `bees` to __all__ | 10 | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F401_F401_27__all_mistyped____init__.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F401_F401_27__all_mistyped____init__.py.snap index f74e91a35b..ecf9aa9a4c 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F401_F401_27__all_mistyped____init__.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F401_F401_27__all_mistyped____init__.py.snap @@ -6,7 +6,6 @@ F401 `.unused` imported but unused; consider removing, adding to `__all__`, or u | 5 | from . import unused # F401: recommend add to all w/o fix | ^^^^^^ - | help: Add unused import `unused` to __all__ F401 `.renamed` imported but unused; consider removing, adding to `__all__`, or using a redundant alias @@ -14,5 +13,4 @@ F401 `.renamed` imported but unused; consider removing, adding to `__all__`, or | 8 | from . import renamed as bees # F401: recommend add to all w/o fix | ^^^^ - | help: Add unused import `bees` to __all__ diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F401_F401_28__all_multiple____init__.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F401_F401_28__all_multiple____init__.py.snap index f10b248c21..40f374f671 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F401_F401_28__all_multiple____init__.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F401_F401_28__all_multiple____init__.py.snap @@ -6,7 +6,6 @@ F401 [*] `.unused` imported but unused; consider removing, adding to `__all__`, | 5 | from . import unused, renamed as bees # F401: add to __all__ | ^^^^^^ - | help: Add unused import `unused` to __all__ | 7 | @@ -19,7 +18,6 @@ F401 [*] `.renamed` imported but unused; consider removing, adding to `__all__`, | 5 | from . import unused, renamed as bees # F401: add to __all__ | ^^^^ - | help: Add unused import `bees` to __all__ | 7 | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F401_F401_33____init__.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F401_F401_33____init__.py.snap index a318d4b91d..683e0d1da5 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F401_F401_33____init__.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F401_F401_33____init__.py.snap @@ -8,7 +8,6 @@ F401 [*] `F401_33.other.Ham` imported but unused 7 | def __init__(self) -> None: 8 | from F401_33.other import Ham | ^^^ - | help: Remove unused import: `F401_33.other.Ham` | 7 | def __init__(self) -> None: diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F811_F811_36.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F811_F811_36.py.snap index a429f37d2a..c9d5172c65 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F811_F811_36.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F811_F811_36.py.snap @@ -2,7 +2,7 @@ source: crates/ruff_linter/src/rules/pyflakes/mod.rs --- F811 [*] Redefinition of unused `BrokerUsecase` from line 2 - --> F811_36.py:2:41 + --> F811_36.py:7:45 | 1 | from typing import TYPE_CHECKING 2 | from faststream._internal.broker import BrokerUsecase diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F822___init__.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F822___init__.py.snap index fc93eac03e..3592f543aa 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F822___init__.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F822___init__.py.snap @@ -8,7 +8,6 @@ F822 Undefined name `a` in `__all__` 4 | 5 | __all__ = ["a", "b", "c"] | ^^^ - | F822 Undefined name `b` in `__all__` --> __init__.py:5:17 @@ -17,7 +16,6 @@ F822 Undefined name `b` in `__all__` 4 | 5 | __all__ = ["a", "b", "c"] | ^^^ - | F822 Undefined name `c` in `__all__` --> __init__.py:5:22 @@ -26,4 +24,3 @@ F822 Undefined name `c` in `__all__` 4 | 5 | __all__ = ["a", "b", "c"] | ^^^ - | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__relative_typing_module.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__relative_typing_module.snap index bb3c7028ef..bd00f6611e 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__relative_typing_module.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__relative_typing_module.snap @@ -7,4 +7,3 @@ F821 Undefined name `foo` 25 | # F821 26 | x: Literal["foo"] | ^^^ - | diff --git a/crates/ruff_linter/src/rules/pygrep_hooks/mod.rs b/crates/ruff_linter/src/rules/pygrep_hooks/mod.rs index 17f2465a18..8b4e7e438c 100644 --- a/crates/ruff_linter/src/rules/pygrep_hooks/mod.rs +++ b/crates/ruff_linter/src/rules/pygrep_hooks/mod.rs @@ -11,7 +11,6 @@ mod tests { use crate::registry::Rule; use crate::settings::LinterSettings; - use crate::settings::types::PreviewMode; use crate::test::test_path; use crate::{assert_diagnostics, assert_diagnostics_diff, settings}; @@ -43,14 +42,8 @@ mod tests { assert_diagnostics_diff!( snapshot, Path::new("pygrep_hooks").join(path).as_path(), - &LinterSettings { - preview: PreviewMode::Disabled, - ..LinterSettings::for_rule(rule_code) - }, - &LinterSettings { - preview: PreviewMode::Enabled, - ..LinterSettings::for_rule(rule_code) - } + &LinterSettings::for_rule(rule_code), + &LinterSettings::for_rule(rule_code).with_preview_mode() ); Ok(()) } diff --git a/crates/ruff_linter/src/rules/pygrep_hooks/snapshots/ruff_linter__rules__pygrep_hooks__tests__PGH004_PGH004_1.py.snap b/crates/ruff_linter/src/rules/pygrep_hooks/snapshots/ruff_linter__rules__pygrep_hooks__tests__PGH004_PGH004_1.py.snap index 5edca0674a..4ceb60c303 100644 --- a/crates/ruff_linter/src/rules/pygrep_hooks/snapshots/ruff_linter__rules__pygrep_hooks__tests__PGH004_PGH004_1.py.snap +++ b/crates/ruff_linter/src/rules/pygrep_hooks/snapshots/ruff_linter__rules__pygrep_hooks__tests__PGH004_PGH004_1.py.snap @@ -6,4 +6,3 @@ PGH004 Use specific rule codes when using `noqa` | 1 | #noqa | ^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pylint/helpers.rs b/crates/ruff_linter/src/rules/pylint/helpers.rs index 95f9054ca2..e3ac9bb5e8 100644 --- a/crates/ruff_linter/src/rules/pylint/helpers.rs +++ b/crates/ruff_linter/src/rules/pylint/helpers.rs @@ -222,99 +222,104 @@ pub(crate) fn is_dunder_operator_method(method: &str) -> bool { /// Returns `true` if a method is a known dunder method. pub(super) fn is_known_dunder_method(method: &str) -> bool { - is_dunder_operator_method(method) - || matches!( - method, - "__abs__" - | "__aenter__" - | "__aexit__" - | "__aiter__" - | "__anext__" - | "__attrs_init__" - | "__attrs_post_init__" - | "__attrs_pre_init__" - | "__await__" - | "__bool__" - | "__buffer__" - | "__bytes__" - | "__call__" - | "__ceil__" - | "__class__" - | "__class_getitem__" - | "__complex__" - | "__contains__" - | "__copy__" - | "__deepcopy__" - | "__del__" - | "__delattr__" - | "__delete__" - | "__delitem__" - | "__dict__" - | "__dir__" - | "__doc__" - | "__enter__" - | "__exit__" - | "__float__" - | "__floor__" - | "__format__" - | "__fspath__" - | "__get__" - | "__getattr__" - | "__getattribute__" - | "__getitem__" - | "__getnewargs__" - | "__getnewargs_ex__" - | "__getstate__" - | "__hash__" - | "__html__" - | "__index__" - | "__init__" - | "__init_subclass__" - | "__instancecheck__" - | "__int__" - | "__invert__" - | "__iter__" - | "__len__" - | "__length_hint__" - | "__missing__" - | "__module__" - | "__mro_entries__" - | "__neg__" - | "__new__" - | "__next__" - | "__pos__" - | "__post_init__" - | "__prepare__" - | "__reduce__" - | "__reduce_ex__" - | "__release_buffer__" - | "__replace__" - | "__repr__" - | "__reversed__" - | "__round__" - | "__set__" - | "__set_name__" - | "__setattr__" - | "__setitem__" - | "__setstate__" - | "__sizeof__" - | "__str__" - | "__subclasscheck__" - | "__subclasses__" - | "__subclasshook__" - | "__trunc__" - | "__weakref__" - // Overridable sunder names from the `Enum` class. - // See: https://docs.python.org/3/library/enum.html#supported-sunder-names - | "_add_alias_" - | "_add_value_alias_" - | "_name_" - | "_value_" - | "_missing_" - | "_ignore_" - | "_order_" - | "_generate_next_value_" - ) + if is_dunder_operator_method(method) { + return true; + } + + match method { + "__abs__" + | "__aenter__" + | "__aexit__" + | "__aiter__" + | "__anext__" + | "__attrs_init__" + | "__attrs_post_init__" + | "__attrs_pre_init__" + | "__await__" + | "__bool__" + | "__buffer__" + | "__bytes__" + | "__call__" + | "__ceil__" + | "__class__" + | "__class_getitem__" + | "__complex__" + | "__contains__" + | "__copy__" + | "__deepcopy__" + | "__del__" + | "__delattr__" + | "__delete__" + | "__delitem__" + | "__dict__" + | "__dir__" + | "__doc__" + | "__enter__" + | "__exit__" + | "__float__" + | "__floor__" + | "__format__" + | "__fspath__" + | "__get__" + | "__getattr__" + | "__getattribute__" + | "__getitem__" + | "__getnewargs__" + | "__getnewargs_ex__" + | "__getstate__" + | "__hash__" + | "__html__" + | "__index__" + | "__init__" + | "__init_subclass__" + | "__instancecheck__" + | "__int__" + | "__invert__" + | "__iter__" + | "__len__" + | "__length_hint__" + | "__missing__" + | "__module__" + | "__mro_entries__" + | "__neg__" + | "__new__" + | "__next__" + | "__pos__" + | "__post_init__" + | "__prepare__" + | "__reduce__" + | "__reduce_ex__" + | "__release_buffer__" + | "__replace__" + | "__repr__" + | "__reversed__" + | "__round__" + | "__set__" + | "__set_name__" + | "__setattr__" + | "__setitem__" + | "__setstate__" + | "__sizeof__" + | "__str__" + | "__subclasscheck__" + | "__subclasses__" + | "__subclasshook__" + | "__trunc__" + | "__weakref__" => true, + + // Overridable sunder names from the `Enum` class. + // See: https://docs.python.org/3/library/enum.html#supported-sunder-names + "_add_alias_" + | "_add_value_alias_" + | "_name_" + | "_value_" + | "_missing_" + | "_ignore_" + | "_order_" + | "_generate_next_value_" => true, + + _ => false, + } } pub(super) fn num_statements(stmts: &[Stmt]) -> usize { diff --git a/crates/ruff_linter/src/rules/pylint/mod.rs b/crates/ruff_linter/src/rules/pylint/mod.rs index d8670cb72c..ed9e7e7b9a 100644 --- a/crates/ruff_linter/src/rules/pylint/mod.rs +++ b/crates/ruff_linter/src/rules/pylint/mod.rs @@ -275,14 +275,8 @@ mod tests { assert_diagnostics_diff!( snapshot, Path::new("pylint").join(path).as_path(), - &LinterSettings { - preview: PreviewMode::Disabled, - ..LinterSettings::for_rule(rule_code) - }, - &LinterSettings { - preview: PreviewMode::Enabled, - ..LinterSettings::for_rule(rule_code) - } + &LinterSettings::for_rule(rule_code), + &LinterSettings::for_rule(rule_code).with_preview_mode() ); Ok(()) } @@ -298,6 +292,17 @@ mod tests { Ok(()) } + #[test] + fn continue_in_finally_python_38() -> Result<()> { + let diagnostics = test_path( + Path::new("pylint/continue_in_finally.py"), + &LinterSettings::for_rule(Rule::ContinueInFinally) + .with_target_version(PythonVersion::PY38), + )?; + assert!(diagnostics.is_empty()); + Ok(()) + } + #[test] fn allow_magic_value_types() -> Result<()> { let diagnostics = test_path( 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 41620db47b..71958f972d 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 @@ -104,7 +104,7 @@ pub(crate) enum StripKind { } impl StripKind { - pub(crate) fn from_str(s: &str) -> Option { + fn from_str(s: &str) -> Option { match s { "strip" => Some(Self::Strip), "lstrip" => Some(Self::LStrip), @@ -132,7 +132,7 @@ pub(crate) enum RemovalKind { } impl RemovalKind { - pub(crate) fn for_strip(s: StripKind) -> Option { + fn for_strip(s: StripKind) -> Option { match s { StripKind::Strip => None, StripKind::LStrip => Some(Self::RemovePrefix), 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 d853f603a5..6aca8f4dd0 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,14 +1,10 @@ -use std::str::FromStr; - use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::{Expr, ExprStringLiteral, StringFlags, StringLiteral}; use ruff_python_literal::{ - cformat::{CFormatErrorType, CFormatString}, format::FormatPart, format::FromTemplate, format::{FormatSpec, FormatSpecError, FormatString}, }; -use ruff_text_size::{Ranged, TextRange}; +use ruff_text_size::TextRange; use crate::Violation; use crate::checkers::ast::Checker; @@ -29,7 +25,7 @@ use crate::checkers::ast::Checker; #[derive(ViolationMetadata)] #[violation_metadata(stable_since = "v0.0.283")] pub(crate) struct BadStringFormatCharacter { - format_char: char, + pub(crate) format_char: char, } impl Violation for BadStringFormatCharacter { @@ -72,26 +68,3 @@ pub(crate) fn call(checker: &Checker, string: &str, range: TextRange) { } } } - -/// PLE1300 -/// Ex) `"%z" % "1"` -pub(crate) fn percent(checker: &Checker, expr: &Expr, format_string: &ExprStringLiteral) { - for StringLiteral { - value: _, - node_index: _, - range, - flags, - } in &format_string.value - { - let string = checker.locator().slice(range); - let string = &string - [usize::from(flags.opener_len())..(string.len() - usize::from(flags.closer_len()))]; - - // 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(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 3822b76b7a..d69e28ecf8 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 @@ -63,11 +63,11 @@ impl FormatType { self, FormatType::Unknown | FormatType::String | FormatType::Repr ), - PythonType::Number(NumberLike::Complex | NumberLike::Bool) => matches!( + PythonType::Number(NumberLike::Complex) => matches!( self, FormatType::Unknown | FormatType::String | FormatType::Repr ), - PythonType::Number(NumberLike::Integer) => matches!( + PythonType::Number(NumberLike::Integer | NumberLike::Bool) => matches!( self, FormatType::Unknown | FormatType::String 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 cd49f9ad69..bb45dd993f 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 @@ -35,7 +35,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: The `__bool__` method](https://docs.python.org/3/reference/datamodel.html#object.__bool__) #[derive(ViolationMetadata)] -#[violation_metadata(preview_since = "v0.3.3")] +#[violation_metadata(stable_since = "0.16.0")] pub(crate) struct InvalidBoolReturnType; impl Violation for InvalidBoolReturnType { 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 8f4b96daec..418ae7b017 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 @@ -137,7 +137,7 @@ fn collect_nested_args(min_max: MinMax, args: &[Expr], semantic: &SemanticModel) range: _, node_index: _, }, - range: _, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, @@ -210,7 +210,7 @@ pub(crate) fn nested_min_max( range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, is_cast: false, is_checked_cast: false, 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 fc9cde49d9..ddf01a3e7b 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 @@ -68,6 +68,13 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// foo += [2] /// assert (foo, bar) == ([1, 2], [1, 2]) /// ``` +/// +/// An augmented assignment can also fail where the plain form succeeds. NumPy +/// writes the result into the target's buffer, so `a *= b` raises where +/// `a = a * b` would broadcast to a new shape or promote the dtype. The same +/// applies to `a @= b`, which requires the product to have the target's shape. +/// +/// The fix replaces the whole statement, so any comments inside it are lost. #[derive(ViolationMetadata)] #[violation_metadata(preview_since = "v0.3.7")] pub(crate) struct NonAugmentedAssignment { @@ -116,10 +123,9 @@ pub(crate) fn non_augmented_assignment(checker: &Checker, assign: &ast::StmtAssi return; } - // If the operator is commutative, match, e.g., `x = 1 + x`, but limit such matches to primitive - // types. + // If the operator is commutative, match, e.g., `x = 1 + x`. if operator.is_commutative() - && (value.left.is_number_literal_expr() || value.left.is_boolean_literal_expr()) + && is_number_or_bool_constant(&value.left) && ComparableExpr::from(target) == ComparableExpr::from(&value.right) { let mut diagnostic = @@ -135,6 +141,16 @@ pub(crate) fn non_augmented_assignment(checker: &Checker, assign: &ast::StmtAssi } } +/// Returns `true` if `expr` evaluates to a number or a boolean, looking through +/// any unary operators applied to a number or boolean literal. +fn is_number_or_bool_constant(mut expr: &Expr) -> bool { + while let Expr::UnaryOp(ast::ExprUnaryOp { operand, .. }) = expr { + expr = operand; + } + + expr.is_number_literal_expr() || expr.is_boolean_literal_expr() +} + /// Generate a fix to convert an assignment statement to an augmented assignment. /// /// For example, given `x = x + 1`, the fix would be `x += 1`. 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 fd8d54441a..9a202324e8 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 @@ -43,7 +43,7 @@ use crate::checkers::ast::Checker; #[derive(ViolationMetadata)] #[violation_metadata(stable_since = "0.5.0")] pub(crate) struct NonlocalAndGlobal { - pub(crate) name: String, + name: String, } impl Violation for NonlocalAndGlobal { 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 67799df816..1fcf2e448d 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 @@ -35,7 +35,7 @@ use crate::checkers::ast::Checker; #[derive(ViolationMetadata)] #[violation_metadata(stable_since = "0.5.0")] pub(crate) struct RedefinedArgumentFromLocal { - pub(crate) name: String, + name: String, } impl Violation for RedefinedArgumentFromLocal { 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 5360046ff4..9fef63b3f7 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 @@ -65,16 +65,17 @@ impl AlwaysFixableViolation for RepeatedEqualityComparison { 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." + "Consider merging multiple comparisons: `{expression}`. \ + Use a `set` if the elements are hashable." ) } (Some(expression), true) => { format!("Consider merging multiple comparisons: `{expression}`.") } - (None, false) => { - "Consider merging multiple comparisons. Use a `set` if the elements are hashable." - .to_string() - } + (None, false) => "\ + Consider merging multiple comparisons. \ + Use a `set` if the elements are hashable." + .to_string(), (None, true) => "Consider merging multiple comparisons.".to_string(), } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/stop_iteration_return.rs b/crates/ruff_linter/src/rules/pylint/rules/stop_iteration_return.rs index 6ae47fe66b..8dbe97fd9c 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/stop_iteration_return.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/stop_iteration_return.rs @@ -38,7 +38,7 @@ use crate::checkers::ast::Checker; /// - [PEP 479](https://peps.python.org/pep-0479/) /// - [Python documentation](https://docs.python.org/3/library/exceptions.html#StopIteration) #[derive(ViolationMetadata)] -#[violation_metadata(preview_since = "0.14.3")] +#[violation_metadata(stable_since = "0.16.0")] pub(crate) struct StopIterationReturn; impl Violation for StopIterationReturn { 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 cfedf33330..f37723ee64 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 @@ -55,7 +55,7 @@ use crate::checkers::ast::Checker; /// /// [override]: https://docs.python.org/3/library/typing.html#typing.override #[derive(ViolationMetadata)] -#[violation_metadata(preview_since = "v0.1.7")] +#[violation_metadata(stable_since = "0.16.0")] pub(crate) struct TooManyPositionalArguments { c_pos: usize, max_pos: usize, 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 2b27d04a66..7bbc469d03 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 @@ -214,20 +214,23 @@ pub(crate) fn unnecessary_dunder_call(checker: &Checker, call: &ast::ExprCall) { if let Some((mut fixed, precedence)) = fixed { let dunder = DunderReplacement::from_method(attr); - // We never need to wrap builtin functions in extra parens - // since function calls have high precedence - let wrap_in_paren = (!matches!(dunder, Some(DunderReplacement::Builtin(_,_)))) - // If parent expression has higher precedence then the new replacement, + // If the parent expression has higher precedence then the new replacement, // it would associate with either the left operand (e.g. naive change from `a * b.__add__(c)` // becomes `a * b + c` which is incorrect) or the right operand (e.g. naive change from // `a.__add__(b).attr` becomes `a + b.attr` which is also incorrect). // This rule doesn't apply to function calls despite them having higher // precedence than any of our replacement, since they already wrap around - // our expression e.g. `print(a.__add__(3))` -> `print(a + 3)` + // our expression e.g. `print(a.__add__(3))` -> `print(a + 3)`. + // + // Note that we never need to wrap *builtin* functions in extra parens + // since function calls have high precedence + let wrap_in_paren = (!matches!(dunder, Some(DunderReplacement::Builtin(_, _)))) && checker .semantic() .current_expression_parent() - .is_some_and(|parent| !parent.is_call_expr() && OperatorPrecedence::from_expr(parent) > precedence); + .is_some_and(|parent| { + !parent.is_call_expr() && OperatorPrecedence::from_expr(parent) > precedence + }); if wrap_in_paren { fixed = format!("({fixed})"); 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 e1a1a20505..900a8d1e31 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 @@ -55,7 +55,9 @@ impl Violation for UselessElseOnLoop { #[derive_message_formats] fn message(&self) -> String { - "`else` clause on loop without a `break` statement; remove the `else` and dedent its contents".to_string() + "`else` clause on loop without a `break` statement; \ + remove the `else` and dedent its contents" + .to_string() } fn fix_title(&self) -> Option { diff --git a/crates/ruff_linter/src/rules/pylint/settings.rs b/crates/ruff_linter/src/rules/pylint/settings.rs index c163f40090..ca7d97f14b 100644 --- a/crates/ruff_linter/src/rules/pylint/settings.rs +++ b/crates/ruff_linter/src/rules/pylint/settings.rs @@ -20,7 +20,7 @@ pub enum ConstantType { } impl ConstantType { - pub fn try_from_literal_expr(literal_expr: LiteralExpressionRef<'_>) -> Option { + pub(crate) fn try_from_literal_expr(literal_expr: LiteralExpressionRef<'_>) -> Option { match literal_expr { LiteralExpressionRef::StringLiteral(_) => Some(Self::Str), LiteralExpressionRef::BytesLiteral(_) => Some(Self::Bytes), 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 index 02ede363e5..9909ef3078 100644 --- 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 @@ -621,7 +621,6 @@ PLC0207 [*] String is split more times than necessary 62 | SEQ.split("(")[0].split("[")[-1] # [missing-maxsplit-arg] 63 | SEQ.split("(")[0].split("[")[0].split(".")[-1] # [missing-maxsplit-arg] | ^^^^^^^^^^^^^^^^^ - | help: Pass `maxsplit=1` into `str.split()` | 62 | SEQ.split("(")[0].split("[")[-1] # [missing-maxsplit-arg] @@ -637,7 +636,6 @@ PLC0207 [*] String is split more times than necessary 62 | SEQ.split("(")[0].split("[")[-1] # [missing-maxsplit-arg] 63 | SEQ.split("(")[0].split("[")[0].split(".")[-1] # [missing-maxsplit-arg] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Pass `maxsplit=1` into `str.split()` | 62 | SEQ.split("(")[0].split("[")[-1] # [missing-maxsplit-arg] @@ -653,7 +651,6 @@ PLC0207 [*] String is split more times than necessary 62 | SEQ.split("(")[0].split("[")[-1] # [missing-maxsplit-arg] 63 | SEQ.split("(")[0].split("[")[0].split(".")[-1] # [missing-maxsplit-arg] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `str.rsplit()` and pass `maxsplit=1` | 62 | SEQ.split("(")[0].split("[")[-1] # [missing-maxsplit-arg] @@ -707,7 +704,6 @@ PLC0207 [*] String is split more times than necessary 188 | kwargs_with_maxsplit = {"sep": ",", "maxsplit": 1} 189 | "1,2,3".split(**kwargs_with_maxsplit)[0] # TODO: false positive | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Pass `maxsplit=1` into `str.split()` | 188 | kwargs_with_maxsplit = {"sep": ",", "maxsplit": 1} @@ -743,7 +739,6 @@ PLC0207 [*] String is split more times than necessary 198 | args_list = [-1] 199 | "1,2,3".split(",", *args_list)[0] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Pass `maxsplit=1` into `str.split()` | 198 | args_list = [-1] diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC0415_import_outside_top_level.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC0415_import_outside_top_level.py.snap index 7c8239ba65..d1af440947 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC0415_import_outside_top_level.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC0415_import_outside_top_level.py.snap @@ -61,7 +61,6 @@ PLC0415 `import` should be at the top-level of a file 14 | from collections import defaultdict # [import-outside-toplevel] 15 | from math import sin as sign, cos as cosplay # [import-outside-toplevel] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | PLC0415 `import` should be at the top-level of a file --> import_outside_top_level.py:19:5 @@ -79,4 +78,3 @@ PLC0415 `import` should be at the top-level of a file 21 | def __init__(self): 22 | import trace # [import-outside-toplevel] | ^^^^^^^^^^^^ - | 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 80837892aa..ce9ddb0d29 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 @@ -329,7 +329,6 @@ PLC1802 [*] `len(set((w + 1) for w in set()))` used as condition without compari 129 | assert len({"1":(v + 1) for v in {}}) # [PLC1802] 130 | assert len(set((w + 1) for w in set())) # [PLC1802] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove `len` | 129 | assert len({"1":(v + 1) for v in {}}) # [PLC1802] diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC2401_non_ascii_name.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC2401_non_ascii_name.py.snap index e5e08000a6..17ecc83542 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC2401_non_ascii_name.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC2401_non_ascii_name.py.snap @@ -40,7 +40,6 @@ PLC2401 Variable name `ápple_count` contains a non-ASCII character 4 | 5 | (ápple_count for ápple_count in y) | ^^^^^^^^^^^ - | help: Rename the variable using ASCII characters PLC2401 Argument name `ápple_count` contains a non-ASCII character diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC2701_import_private_name__submodule____main__.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC2701_import_private_name__submodule____main__.py.snap index eba20b55cd..4b0c527088 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC2701_import_private_name__submodule____main__.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC2701_import_private_name__submodule____main__.py.snap @@ -84,4 +84,3 @@ PLC2701 Private name import `_bar` from external module `foo` 51 | 52 | from foo. _bar import baz | ^^^^ - | 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 7616229e7f..fb62b5f0f3 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 @@ -693,7 +693,6 @@ PLC2801 [*] Unnecessary dunder call to `__add__`. Use `+` operator. 62 | foo = Foo(1) 63 | foo.__add__(2).get_v() # PLC2801 | ^^^^^^^^^^^^^^ - | help: Use `+` operator | 62 | foo = Foo(1) @@ -1208,7 +1207,6 @@ PLC2801 [*] Unnecessary dunder call to `__imatmul__`. Use `@=` operator. 142 | foo.__rmatmul__(a) # PLC2801 143 | foo.__imatmul__(foo) # PLC2801 | ^^^^^^^^^^^^^^^^^^^^ - | help: Use `@=` operator | 142 | foo.__rmatmul__(a) # PLC2801 diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC3002_unnecessary_direct_lambda_call.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC3002_unnecessary_direct_lambda_call.py.snap index 59968f3620..decb43dc5b 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC3002_unnecessary_direct_lambda_call.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC3002_unnecessary_direct_lambda_call.py.snap @@ -17,7 +17,6 @@ PLC3002 Lambda expression called directly. Execute the expression inline instead 4 | y = (lambda x: x**2 + 2*x + 1)(a) # [unnecessary-direct-lambda-call] 5 | y = max((lambda x: x**2)(a), (lambda x: x+1)(a)) # [unnecessary-direct-lambda-call,unnecessary-direct-lambda-call] | ^^^^^^^^^^^^^^^^^^^ - | PLC3002 Lambda expression called directly. Execute the expression inline instead. --> unnecessary_direct_lambda_call.py:5:30 @@ -25,4 +24,3 @@ PLC3002 Lambda expression called directly. Execute the expression inline instead 4 | y = (lambda x: x**2 + 2*x + 1)(a) # [unnecessary-direct-lambda-call] 5 | y = max((lambda x: x**2)(a), (lambda x: x+1)(a)) # [unnecessary-direct-lambda-call,unnecessary-direct-lambda-call] | ^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0100_yield_in_init.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0100_yield_in_init.py.snap index 19a74bf16b..7165c733a5 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0100_yield_in_init.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0100_yield_in_init.py.snap @@ -8,7 +8,6 @@ PLE0100 `__init__` method is a generator 8 | def __init__(self): 9 | yield | ^^^^^ - | PLE0100 `__init__` method is a generator --> yield_in_init.py:14:9 diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0115_nonlocal_and_global.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0115_nonlocal_and_global.py.snap index 94d0f24d68..1013914195 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0115_nonlocal_and_global.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0115_nonlocal_and_global.py.snap @@ -28,7 +28,6 @@ PLE0115 Name `counter` is both `nonlocal` and `global` 30 | counter += 1 31 | global counter | ^^^^^^^ - | PLE0115 Name `counter` is both `nonlocal` and `global` --> nonlocal_and_global.py:36:12 diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0117_nonlocal_without_binding.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0117_nonlocal_without_binding.py.snap index 70e4d1609d..245e582564 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0117_nonlocal_without_binding.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0117_nonlocal_without_binding.py.snap @@ -7,7 +7,6 @@ PLE0117 Nonlocal name `x` found without binding 4 | def f(): 5 | nonlocal x | ^ - | PLE0117 Nonlocal name `y` found without binding --> nonlocal_without_binding.py:9:14 @@ -15,7 +14,6 @@ PLE0117 Nonlocal name `y` found without binding 8 | def f(): 9 | nonlocal y | ^ - | PLE0117 Nonlocal name `y` found without binding --> nonlocal_without_binding.py:19:18 @@ -23,4 +21,3 @@ PLE0117 Nonlocal name `y` found without binding 18 | def f(): 19 | nonlocal y | ^ - | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0303_invalid_return_type_length.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0303_invalid_return_type_length.py.snap index 55b8ad7aa0..f1b30e4fc7 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0303_invalid_return_type_length.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0303_invalid_return_type_length.py.snap @@ -8,7 +8,6 @@ PLE0303 `__len__` does not return a non-negative integer 5 | def __len__(self): 6 | return True # [invalid-length-return] | ^^^^ - | PLE0303 `__len__` does not return a non-negative integer --> invalid_return_type_length.py:11:16 @@ -17,7 +16,6 @@ PLE0303 `__len__` does not return a non-negative integer 10 | def __len__(self): 11 | return 3.05 # [invalid-length-return] | ^^^^ - | PLE0303 `__len__` does not return a non-negative integer --> invalid_return_type_length.py:16:16 @@ -26,7 +24,6 @@ PLE0303 `__len__` does not return a non-negative integer 15 | def __len__(self): 16 | return "ruff" # [invalid-length-return] | ^^^^^^ - | PLE0303 `__len__` does not return a non-negative integer --> invalid_return_type_length.py:20:9 @@ -44,4 +41,3 @@ PLE0303 `__len__` does not return a non-negative integer 25 | def __len__(self): 26 | return -42 # [invalid-length-return] | ^^^ - | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0304_invalid_return_type_bool.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0304_invalid_return_type_bool.py.snap index efbea371b0..226c068e33 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0304_invalid_return_type_bool.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0304_invalid_return_type_bool.py.snap @@ -19,4 +19,3 @@ PLE0304 `__bool__` does not return `bool` 8 | def __bool__(self): 9 | return 0 # [invalid-bool-return] | ^ - | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0305_invalid_return_type_index.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0305_invalid_return_type_index.py.snap index 6e707341df..8be422572f 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0305_invalid_return_type_index.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0305_invalid_return_type_index.py.snap @@ -7,7 +7,6 @@ PLE0305 `__index__` does not return an integer 7 | def __index__(self): 8 | return True # [invalid-index-return] | ^^^^ - | PLE0305 `__index__` does not return an integer --> invalid_return_type_index.py:13:16 @@ -16,7 +15,6 @@ PLE0305 `__index__` does not return an integer 12 | def __index__(self): 13 | return 3.05 # [invalid-index-return] | ^^^^ - | PLE0305 `__index__` does not return an integer --> invalid_return_type_index.py:18:16 @@ -25,7 +23,6 @@ PLE0305 `__index__` does not return an integer 17 | def __index__(self): 18 | return {"1": "1"} # [invalid-index-return] | ^^^^^^^^^^ - | PLE0305 `__index__` does not return an integer --> invalid_return_type_index.py:23:16 @@ -34,7 +31,6 @@ PLE0305 `__index__` does not return an integer 22 | def __index__(self): 23 | return "ruff" # [invalid-index-return] | ^^^^^^ - | PLE0305 `__index__` does not return an integer --> invalid_return_type_index.py:27:9 diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0307_invalid_return_type_str.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0307_invalid_return_type_str.py.snap index 779d6da34a..1704e50eaf 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0307_invalid_return_type_str.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0307_invalid_return_type_str.py.snap @@ -8,7 +8,6 @@ PLE0307 `__str__` does not return `str` 5 | def __str__(self): 6 | return 3.05 | ^^^^ - | PLE0307 `__str__` does not return `str` --> invalid_return_type_str.py:11:16 @@ -17,7 +16,6 @@ PLE0307 `__str__` does not return `str` 10 | def __str__(self): 11 | return 1 | ^ - | PLE0307 `__str__` does not return `str` --> invalid_return_type_str.py:16:16 @@ -26,7 +24,6 @@ PLE0307 `__str__` does not return `str` 15 | def __str__(self): 16 | return 0 | ^ - | PLE0307 `__str__` does not return `str` --> invalid_return_type_str.py:21:16 @@ -35,7 +32,6 @@ PLE0307 `__str__` does not return `str` 20 | def __str__(self): 21 | return False | ^^^^^ - | PLE0307 `__str__` does not return `str` --> invalid_return_type_str.py:58:9 diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0308_invalid_return_type_bytes.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0308_invalid_return_type_bytes.py.snap index 597bc48971..d027b5eaae 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0308_invalid_return_type_bytes.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0308_invalid_return_type_bytes.py.snap @@ -8,7 +8,6 @@ PLE0308 `__bytes__` does not return `bytes` 5 | def __bytes__(self): 6 | return 3.05 # [invalid-bytes-return] | ^^^^ - | PLE0308 `__bytes__` does not return `bytes` --> invalid_return_type_bytes.py:11:16 @@ -17,7 +16,6 @@ PLE0308 `__bytes__` does not return `bytes` 10 | def __bytes__(self): 11 | return 0 # [invalid-bytes-return] | ^ - | PLE0308 `__bytes__` does not return `bytes` --> invalid_return_type_bytes.py:16:16 @@ -26,7 +24,6 @@ PLE0308 `__bytes__` does not return `bytes` 15 | def __bytes__(self): 16 | return "some bytes" # [invalid-bytes-return] | ^^^^^^^^^^^^ - | PLE0308 `__bytes__` does not return `bytes` --> invalid_return_type_bytes.py:20:9 diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0309_invalid_return_type_hash.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0309_invalid_return_type_hash.py.snap index 466ddc53bb..cd21af10bc 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0309_invalid_return_type_hash.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0309_invalid_return_type_hash.py.snap @@ -8,7 +8,6 @@ PLE0309 `__hash__` does not return an integer 5 | def __hash__(self): 6 | return True # [invalid-hash-return] | ^^^^ - | PLE0309 `__hash__` does not return an integer --> invalid_return_type_hash.py:11:16 @@ -17,7 +16,6 @@ PLE0309 `__hash__` does not return an integer 10 | def __hash__(self): 11 | return 3.05 # [invalid-hash-return] | ^^^^ - | PLE0309 `__hash__` does not return an integer --> invalid_return_type_hash.py:16:16 @@ -26,7 +24,6 @@ PLE0309 `__hash__` does not return an integer 15 | def __hash__(self): 16 | return "ruff" # [invalid-hash-return] | ^^^^^^ - | PLE0309 `__hash__` does not return an integer --> invalid_return_type_hash.py:20:9 diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0604_invalid_all_object.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0604_invalid_all_object.py.snap index ade9450cef..53911e8f0f 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0604_invalid_all_object.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0604_invalid_all_object.py.snap @@ -17,4 +17,3 @@ PLE0604 Invalid object in `__all__`, must contain only strings 6 | 7 | __all__ = list([None, "Fruit", "Worm"]) # [invalid-all-object] | ^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0704_misplaced_bare_raise.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0704_misplaced_bare_raise.py.snap index 5d3ee1f406..08da78a714 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0704_misplaced_bare_raise.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0704_misplaced_bare_raise.py.snap @@ -94,4 +94,3 @@ PLE0704 Bare `raise` statement is not inside an exception handler 70 | finally: 71 | raise # [misplaced-bare-raise] | ^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1132_repeated_keyword_argument.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1132_repeated_keyword_argument.py.snap index 8c677630f1..0c08b9d9c2 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1132_repeated_keyword_argument.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1132_repeated_keyword_argument.py.snap @@ -83,4 +83,3 @@ PLE1132 Repeated keyword argument: `c` 19 | func(a=11, b=21, **{"c": 31}, **{"c": 32}) 20 | func(a=11, b=21, **{"c": 31, "c": 32}) | ^^^ - | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1142_await_outside_async.ipynb.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1142_await_outside_async.ipynb.snap index 67bd86f7de..8d39dfd315 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1142_await_outside_async.ipynb.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1142_await_outside_async.ipynb.snap @@ -7,4 +7,3 @@ PLE1142 `await` should be used within an async function 8 | def foo(): 9 | await asyncio.sleep(1) # # [await-outside-async] | ^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1142_await_outside_async.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1142_await_outside_async.py.snap index 5427508ed8..e9a09eda19 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1142_await_outside_async.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1142_await_outside_async.py.snap @@ -7,7 +7,6 @@ PLE1142 `await` should be used within an async function 14 | def not_async(): 15 | print(await nested()) # [await-outside-async] | ^^^^^^^^^^^^^^ - | PLE1142 `await` should be used within an async function --> await_outside_async.py:29:9 @@ -16,7 +15,6 @@ PLE1142 `await` should be used within an async function 28 | def inner_func(): 29 | await asyncio.sleep(1) # [await-outside-async] | ^^^^^^^^^^^^^^^^^^^^^^ - | PLE1142 `await` should be used within an async function --> await_outside_async.py:38:5 @@ -25,7 +23,6 @@ PLE1142 `await` should be used within an async function 38 | / async for x in foo(): 39 | | pass | |____________^ - | PLE1142 `await` should be used within an async function --> await_outside_async.py:43:5 @@ -34,7 +31,6 @@ PLE1142 `await` should be used within an async function 43 | / async with foo(): 44 | | pass | |____________^ - | PLE1142 `await` should be used within an async function --> await_outside_async.py:54:6 @@ -43,7 +39,6 @@ PLE1142 `await` should be used within an async function 53 | def async_for_list_comprehension_elt(): 54 | [await x for x in foo()] | ^^^^^^^ - | PLE1142 `await` should be used within an async function --> await_outside_async.py:59:8 @@ -52,7 +47,6 @@ PLE1142 `await` should be used within an async function 58 | def async_for_list_comprehension(): 59 | [x async for x in foo()] | ^^^^^^^^^^^^^^^^^^^^ - | PLE1142 `await` should be used within an async function --> await_outside_async.py:64:17 @@ -61,7 +55,6 @@ PLE1142 `await` should be used within an async function 63 | def await_generator_iter(): 64 | (x for x in await foo()) | ^^^^^^^^^^^ - | PLE1142 `await` should be used within an async function --> await_outside_async.py:74:17 @@ -70,7 +63,6 @@ PLE1142 `await` should be used within an async function 73 | def async_for_list_comprehension_target(): 74 | [x for x in await foo()] | ^^^^^^^^^^^ - | PLE1142 `await` should be used within an async function --> await_outside_async.py:78:6 @@ -78,7 +70,6 @@ PLE1142 `await` should be used within an async function 77 | def async_for_dictionary_comprehension_key(): 78 | {await x: y for x, y in foo()} | ^^^^^^^ - | PLE1142 `await` should be used within an async function --> await_outside_async.py:82:9 @@ -86,7 +77,6 @@ PLE1142 `await` should be used within an async function 81 | def async_for_dictionary_comprehension_value(): 82 | {y: await x for x, y in foo()} | ^^^^^^^ - | PLE1142 `await` should be used within an async function --> await_outside_async.py:86:11 @@ -94,4 +84,3 @@ PLE1142 `await` should be used within an async function 85 | def async_for_dict_comprehension(): 86 | {x: y async for x, y in foo()} | ^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1300_bad_string_format_character.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1300_bad_string_format_character.py.snap index 8de582760c..4049f2e97d 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1300_bad_string_format_character.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1300_bad_string_format_character.py.snap @@ -66,3 +66,23 @@ PLE1300 Unsupported format character 'y' 20 | "{0:.{prec}g}".format(1.23, prec=15) # OK (cannot validate after nested placeholder) 21 | "{0:.{foo}{bar}{foobar}y}".format(...) # OK (cannot validate after nested placeholders) | + +PLE1300 Unsupported format character 'z' + --> bad_string_format_character.py:34:7 + | +32 | ## Supporting concatenated strings +33 | +34 | print(("%" "z") % 1) + | ^^^^^^^^^^^^^ +35 | +36 | ## `%b` is only valid for bytes formatting. + | + +PLE1300 Unsupported format character 'b' + --> bad_string_format_character.py:37:1 + | +36 | ## `%b` is only valid for bytes formatting. +37 | "%b" % b"25" + | ^^^^^^^^^^^^ +38 | b"%b" % b"25" + | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1310_bad_str_strip_call.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1310_bad_str_strip_call.py.snap index 5c2872ae8b..968e9f1205 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1310_bad_str_strip_call.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1310_bad_str_strip_call.py.snap @@ -229,4 +229,3 @@ PLE1310 String `lstrip` call contains duplicate characters (did you mean `remove 89 | foo.rstrip("//") 90 | bar.lstrip(b"//") | ^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1700_yield_from_in_async_function.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1700_yield_from_in_async_function.py.snap index 1b6adf6316..b2ddc2d81d 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1700_yield_from_in_async_function.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1700_yield_from_in_async_function.py.snap @@ -8,4 +8,3 @@ PLE1700 `yield from` statement in async function; use `async for` instead 6 | l = (1, 2, 3) 7 | yield from l | ^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2502_bidirectional_unicode.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2502_bidirectional_unicode.py.snap index 4a1555ebe8..b93b0eee22 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2502_bidirectional_unicode.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2502_bidirectional_unicode.py.snap @@ -5,8 +5,8 @@ PLE2502 Contains control characters that can permit obfuscated code --> bidirectional_unicode.py:2:1 | 1 | # E2502 -2 | print("שלום") - | ^^^^^^^^^^^^^ +2 | print("שלום�") + | ^^^^^^^^^^^^^^ 3 | 4 | # E2502 | @@ -35,8 +35,8 @@ PLE2502 Contains control characters that can permit obfuscated code --> bidirectional_unicode.py:11:1 | 10 | # E2502 -11 | if access_level != "none": # Check if admin ' and access_level != 'user - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +11 | if access_level != "none��": # Check if admin ��' and access_level != 'user + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 12 | print("You are an admin.") | @@ -45,8 +45,8 @@ PLE2502 Contains control characters that can permit obfuscated code | 15 | # E2502 16 | def subtract_funds(account: str, amount: int): -17 | """Subtract funds from bank account then """ - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +17 | """Subtract funds from bank account then �""" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 18 | return 19 | bank[account] -= amount | 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 c3b3c2b5bd..221dae3339 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 4ae66b4a4a..cefc944be4 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 @@ -100,7 +100,6 @@ PLE2510 Invalid unescaped character backspace, use "\b" instead 12 | # Implicitly concatenated 13 | b = '␈' f'␈' '␈ | ^ - | help: Replace with escape sequence PLE2510 Invalid unescaped character backspace, use "\b" instead @@ -110,7 +109,6 @@ PLE2510 Invalid unescaped character backspace, use "\b" instead 12 | # Implicitly concatenated 13 | b = '␈' f'␈' '␈ | ^ - | help: Replace with escape sequence invalid-syntax: missing closing quote in string literal @@ -120,7 +118,6 @@ invalid-syntax: missing closing quote in string literal 12 | # Implicitly concatenated 13 | b = '␈' f'␈' '␈ | ^^ - | PLE2510 Invalid unescaped character backspace, use "\b" instead --> invalid_characters_syntax_error.py:13:15 @@ -129,5 +126,4 @@ PLE2510 Invalid unescaped character backspace, use "\b" instead 12 | # Implicitly concatenated 13 | b = '␈' f'␈' '␈ | ^ - | help: Replace with escape sequence 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 3775515950..a6244be8a5 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 15642e9569..9460786bff 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 d2e39983d9..880cbc8ef3 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 8f5b94d945..6d3205f333 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__PLR1708_stop_iteration_return.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1708_stop_iteration_return.py.snap index a6775f05d7..89d9672981 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1708_stop_iteration_return.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1708_stop_iteration_return.py.snap @@ -8,7 +8,6 @@ PLR1708 Explicit `raise StopIteration` in generator 37 | yield 2 38 | raise StopIteration # Should trigger | ^^^^^^^^^^^^^^^^^^^ - | help: Use `return` instead PLR1708 Explicit `raise StopIteration` in generator @@ -18,7 +17,6 @@ PLR1708 Explicit `raise StopIteration` in generator 43 | yield 2 44 | raise StopIteration("finished") # Should trigger | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `return` instead PLR1708 Explicit `raise StopIteration` in generator @@ -28,7 +26,6 @@ PLR1708 Explicit `raise StopIteration` in generator 49 | yield 2 50 | raise StopIteration(1 + 2) # Should trigger | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `return` instead PLR1708 Explicit `raise StopIteration` in generator @@ -38,7 +35,6 @@ PLR1708 Explicit `raise StopIteration` in generator 55 | yield 2 56 | raise StopIteration("async") # Should trigger | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `return` instead PLR1708 Explicit `raise StopIteration` in generator @@ -95,7 +91,6 @@ PLR1708 Explicit `raise StopIteration` in generator 97 | yield 1 98 | raise StopIteration # Should trigger (no arguments) | ^^^^^^^^^^^^^^^^^^^ - | help: Use `return` instead PLR1708 Explicit `raise StopIteration` in generator @@ -105,7 +100,6 @@ PLR1708 Explicit `raise StopIteration` in generator 104 | if i == 3: 105 | raise StopIteration("loop") # Should trigger | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `return` instead PLR1708 Explicit `raise StopIteration` in generator @@ -126,5 +120,4 @@ PLR1708 Explicit `raise StopIteration` in generator 153 | def foo(): 154 | raise StopIteration((yield 1)) # Should trigger | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `return` instead diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1711_useless_return.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1711_useless_return.py.snap index 8f605bc56a..bb66211d8f 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1711_useless_return.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1711_useless_return.py.snap @@ -8,7 +8,6 @@ PLR1711 [*] Useless `return` statement at end of function 5 | print(sys.version) 6 | return None # [useless-return] | ^^^^^^^^^^^ - | help: Remove useless `return` statement | 5 | print(sys.version) @@ -23,7 +22,6 @@ PLR1711 [*] Useless `return` statement at end of function 10 | print(sys.version) 11 | return None # [useless-return] | ^^^^^^^^^^^ - | help: Remove useless `return` statement | 10 | print(sys.version) @@ -38,7 +36,6 @@ PLR1711 [*] Useless `return` statement at end of function 15 | print(sys.version) 16 | return None # [useless-return] | ^^^^^^^^^^^ - | help: Remove useless `return` statement | 15 | print(sys.version) @@ -53,7 +50,6 @@ PLR1711 [*] Useless `return` statement at end of function 21 | print(sys.version) 22 | return None # [useless-return] | ^^^^^^^^^^^ - | help: Remove useless `return` statement | 21 | print(sys.version) @@ -68,7 +64,6 @@ PLR1711 [*] Useless `return` statement at end of function 49 | print(sys.version) 50 | return None # [useless-return] | ^^^^^^^^^^^ - | help: Remove useless `return` statement | 49 | print(sys.version) @@ -83,7 +78,6 @@ PLR1711 [*] Useless `return` statement at end of function 59 | print(f"{key} not found") 60 | return None | ^^^^^^^^^^^ - | help: Remove useless `return` statement | 59 | print(f"{key} not found") diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1712_swap_with_temporary_variable.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1712_swap_with_temporary_variable.py.snap index e24fdc6ec8..9df056a3ac 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1712_swap_with_temporary_variable.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1712_swap_with_temporary_variable.py.snap @@ -10,7 +10,6 @@ PLR1712 [*] Unnecessary temporary variable 4 | | x = y 5 | | y = temp | |____________^ - | help: Use `x, y = y, x` instead | 2 | def foo(x: int, y: int): @@ -30,7 +29,6 @@ PLR1712 [*] Unnecessary temporary variable 12 | | x = y 13 | | y = temp | |________________^ - | help: Use `x, y = y, x` instead | 10 | if x > 5: @@ -50,7 +48,6 @@ PLR1712 [*] Unnecessary temporary variable 19 | | x = y 20 | | y = temp | |____________^ - | help: Use `x, y = y, x` instead | 17 | def bar(x: int, y: int): diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1714_repeated_equality_comparison.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1714_repeated_equality_comparison.py.snap index fb21bca8fd..e1284b355c 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1714_repeated_equality_comparison.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1714_repeated_equality_comparison.py.snap @@ -425,7 +425,6 @@ PLR1714 [*] Consider merging multiple comparisons: `foo in {"bar", "bar", "buzz" 78 | 79 | foo == "bar" or foo == "bar" or foo == "buzz" # All but one members identical | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Merge multiple comparisons | 78 | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1716_boolean_chained_comparison.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1716_boolean_chained_comparison.py.snap index 16032c7afb..3e6d4093a7 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1716_boolean_chained_comparison.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1716_boolean_chained_comparison.py.snap @@ -415,7 +415,6 @@ PLR1716 [*] Contains chained boolean comparison that can be simplified 147 | 148 | a < (b) and (((b)) < c) | ^^^^^^^^^^^^^^^^^^^^^^ - | help: Use a single compare expression | 147 | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_0.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_0.py.snap index de87067f7e..8b8dec2666 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_0.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_0.py.snap @@ -23,7 +23,6 @@ PLR1722 [*] Use `sys.exit()` instead of `quit` 1 | exit(0) 2 | quit(0) | ^^^^ - | help: Replace `quit` with `sys.exit()` | 1 + import sys @@ -61,7 +60,6 @@ PLR1722 [*] Use `sys.exit()` instead of `quit` 6 | exit(2) 7 | quit(2) | ^^^^ - | help: Replace `quit` with `sys.exit()` | 1 + import sys diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_1.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_1.py.snap index ac72b8fe8f..fd44455ca8 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_1.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_1.py.snap @@ -25,7 +25,6 @@ PLR1722 [*] Use `sys.exit()` instead of `quit` 3 | exit(0) 4 | quit(0) | ^^^^ - | help: Replace `quit` with `sys.exit()` | 3 | exit(0) @@ -59,7 +58,6 @@ PLR1722 [*] Use `sys.exit()` instead of `quit` 8 | exit(1) 9 | quit(1) | ^^^^ - | help: Replace `quit` with `sys.exit()` | 8 | exit(1) @@ -86,5 +84,4 @@ PLR1722 Use `sys.exit()` instead of `quit` 15 | exit(1) 16 | quit(1) | ^^^^ - | help: Replace `quit` with `sys.exit()` diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_10.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_10.py.snap index c6481fa743..cd58284dba 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_10.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_10.py.snap @@ -7,5 +7,4 @@ PLR1722 Use `sys.exit()` instead of `exit` 7 | def main(): 8 | exit(0) | ^^^^ - | help: Replace `exit` with `sys.exit()` diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_11.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_11.py.snap index 2f6f7be5ea..e6dd0ff9c4 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_11.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_11.py.snap @@ -8,7 +8,6 @@ PLR1722 [*] Use `sys.exit()` instead of `exit` 2 | 3 | exit(0) | ^^^^ - | help: Replace `exit` with `sys.exit()` | 1 | from sys import * diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_12.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_12.py.snap index 453d5bb264..29ae1851cc 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_12.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_12.py.snap @@ -8,7 +8,6 @@ PLR1722 [*] Use `sys.exit()` instead of `exit` 2 | 3 | exit(0) | ^^^^ - | help: Replace `exit` with `sys.exit()` | - import os \ diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_13.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_13.py.snap index 235983f89c..665bec6c9e 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_13.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_13.py.snap @@ -6,7 +6,6 @@ PLR1722 [*] Use `sys.exit()` instead of `exit` | 1 | exit(code=2) | ^^^^ - | help: Replace `exit` with `sys.exit()` | - exit(code=2) diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_14.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_14.py.snap index e190005449..abd4d99f49 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_14.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_14.py.snap @@ -7,5 +7,4 @@ PLR1722 Use `sys.exit()` instead of `exit` 1 | code = {"code": 2} 2 | exit(**code) | ^^^^ - | help: Replace `exit` with `sys.exit()` diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_15.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_15.py.snap index 38cca3c033..a704ab290d 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_15.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_15.py.snap @@ -8,7 +8,6 @@ PLR1722 [*] Use `sys.exit()` instead of `exit` 5 | code = 1 6 | exit(code) | ^^^^ - | help: Replace `exit` with `sys.exit()` | 1 + import sys diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_16.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_16.py.snap index c2b96562d5..e5afb4da80 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_16.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_16.py.snap @@ -53,7 +53,6 @@ PLR1722 Use `sys.exit()` instead of `exit` 16 | # no diagnostic for multiple arguments 17 | exit(2, 3, 4) | ^^^^ - | help: Replace `exit` with `sys.exit()` PLR1722 [*] Use `sys.exit()` instead of `exit` @@ -63,7 +62,6 @@ PLR1722 [*] Use `sys.exit()` instead of `exit` 22 | codes = [1] 23 | exit(*codes) | ^^^^ - | help: Replace `exit` with `sys.exit()` | 1 + import sys diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_2.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_2.py.snap index 9d18e37bcf..9397b6aebb 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_2.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_2.py.snap @@ -25,7 +25,6 @@ PLR1722 [*] Use `sys.exit()` instead of `quit` 3 | exit(0) 4 | quit(0) | ^^^^ - | help: Replace `quit` with `sys.exit()` | 3 | exit(0) @@ -59,7 +58,6 @@ PLR1722 [*] Use `sys.exit()` instead of `quit` 8 | exit(1) 9 | quit(1) | ^^^^ - | help: Replace `quit` with `sys.exit()` | 8 | exit(1) diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_3.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_3.py.snap index 1764a2f47d..7ffc90f08f 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_3.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_3.py.snap @@ -7,7 +7,6 @@ PLR1722 [*] Use `sys.exit()` instead of `quit` 3 | exit(0) 4 | quit(0) | ^^^^ - | help: Replace `quit` with `sys.exit()` | 3 | exit(0) @@ -24,7 +23,6 @@ PLR1722 [*] Use `sys.exit()` instead of `quit` 8 | exit(1) 9 | quit(1) | ^^^^ - | help: Replace `quit` with `sys.exit()` | 8 | exit(1) @@ -40,5 +38,4 @@ PLR1722 Use `sys.exit()` instead of `quit` 15 | exit(1) 16 | quit(1) | ^^^^ - | help: Replace `quit` with `sys.exit()` diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_4.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_4.py.snap index 38e6973e88..4bdd3ae17d 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_4.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_4.py.snap @@ -25,7 +25,6 @@ PLR1722 [*] Use `sys.exit()` instead of `quit` 3 | exit(0) 4 | quit(0) | ^^^^ - | help: Replace `quit` with `sys.exit()` | 3 | exit(0) @@ -59,7 +58,6 @@ PLR1722 [*] Use `sys.exit()` instead of `quit` 8 | exit(1) 9 | quit(1) | ^^^^ - | help: Replace `quit` with `sys.exit()` | 8 | exit(1) diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_5.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_5.py.snap index 60af85491f..7d3d509cfa 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_5.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_5.py.snap @@ -27,7 +27,6 @@ PLR1722 [*] Use `sys.exit()` instead of `quit` 3 | exit(0) 4 | quit(0) | ^^^^ - | help: Replace `quit` with `sys.exit()` | 1 | from sys import * @@ -68,7 +67,6 @@ PLR1722 [*] Use `sys.exit()` instead of `quit` 8 | exit(1) 9 | quit(1) | ^^^^ - | help: Replace `quit` with `sys.exit()` | 1 | from sys import * diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_6.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_6.py.snap index 2d624b367e..440149c059 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_6.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_6.py.snap @@ -23,7 +23,6 @@ PLR1722 [*] Use `sys.exit()` instead of `quit` 1 | exit(0) 2 | quit(0) | ^^^^ - | help: Replace `quit` with `sys.exit()` | 1 + import sys diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_7.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_7.py.snap index 61d707e153..60d5d65e13 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_7.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_7.py.snap @@ -7,7 +7,6 @@ PLR1722 [*] Use `sys.exit()` instead of `exit` 1 | def main(): 2 | exit(0) | ^^^^ - | help: Replace `exit` with `sys.exit()` | 1 + import sys diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_8.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_8.py.snap index 7f52bd45a2..056475d8ec 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_8.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_8.py.snap @@ -7,7 +7,6 @@ PLR1722 [*] Use `sys.exit()` instead of `exit` 4 | def main(): 5 | exit(0) | ^^^^ - | help: Replace `exit` with `sys.exit()` | - from sys import argv diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_9.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_9.py.snap index f011793086..c88151f1f0 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_9.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_9.py.snap @@ -7,7 +7,6 @@ PLR1722 [*] Use `sys.exit()` instead of `exit` 1 | def main(): 2 | exit(0) | ^^^^ - | help: Replace `exit` with `sys.exit()` | 1 + import sys 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 66fb42986f..4e1050ac1a 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 @@ -141,7 +141,6 @@ PLR1730 [*] Replace `if` statement with `b = max(b, a)` 49 | / if a > b: 50 | | b = a | |_________^ - | help: Replace with `b = max(b, a)` | 48 | # case 8: b = max(b, a) @@ -253,7 +252,6 @@ PLR1730 [*] Replace `if` statement with `value = min(value, value2)` 79 | / if value > value2: 80 | | value = value2 | |__________________^ - | help: Replace with `value = min(value, value2)` | 78 | # base case 5: value = min(value, value2) @@ -289,7 +287,6 @@ PLR1730 [*] Replace `if` statement with `A1.value = min(A1.value, 10)` 95 | / if A1.value > 10: 96 | | A1.value = 10 | |_________________^ - | help: Replace with `A1.value = min(A1.value, 10)` | 94 | @@ -556,7 +553,6 @@ PLR1730 [*] Replace `if` statement with `self._max = min(value, self._max)` 219 | / if self._max >= value: 220 | | self._max = value | |_____________________________^ - | help: Replace with `self._max = min(value, self._max)` | 218 | self._min = value @@ -672,7 +668,6 @@ PLR1730 [*] Replace `if` statement with `a = min(b, a)` 250 | / if a >= b: 251 | | a = b # very important comment | |_________^ - | help: Replace with `a = min(b, a)` | 249 | # fix marked safe as preserve comments diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1733_unnecessary_dict_index_lookup.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1733_unnecessary_dict_index_lookup.py.snap index 962e253b45..987539e90e 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1733_unnecessary_dict_index_lookup.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1733_unnecessary_dict_index_lookup.py.snap @@ -94,7 +94,6 @@ PLR1733 [*] Unnecessary lookup of dictionary value by key 10 | blah = FRUITS[fruit_name] # PLR1733 11 | assert FRUITS[fruit_name] == "pear" # PLR1733 | ^^^^^^^^^^^^^^^^^^ - | help: Use existing variable | 10 | blah = FRUITS[fruit_name] # PLR1733 diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1736_unnecessary_list_index_lookup.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1736_unnecessary_list_index_lookup.py.snap index 790c9c7e36..837f1e65d4 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1736_unnecessary_list_index_lookup.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1736_unnecessary_list_index_lookup.py.snap @@ -146,7 +146,6 @@ PLR1736 [*] List index lookup in `enumerate()` loop 18 | blah = letters[index] # PLR1736 19 | assert letters[index] == "d" # PLR1736 | ^^^^^^^^^^^^^^ - | help: Use the loop variable directly | 18 | blah = letters[index] # PLR1736 @@ -180,7 +179,6 @@ PLR1736 [*] List index lookup in `enumerate()` loop 77 | for index, list_item in enumerate(some_list): 78 | print(some_list[index]) | ^^^^^^^^^^^^^^^^ - | help: Use the loop variable directly | 77 | for index, list_item in enumerate(some_list): @@ -196,7 +194,6 @@ PLR1736 [*] List index lookup in `enumerate()` loop 84 | for index, column_name in enumerate(column_names): 85 | _ = data[column_names[index]] # PLR1736 | ^^^^^^^^^^^^^^^^^^^ - | help: Use the loop variable directly | 84 | for index, column_name in enumerate(column_names): diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR2044_empty_comment.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR2044_empty_comment.py.snap index 7637d1a560..da0769a521 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR2044_empty_comment.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR2044_empty_comment.py.snap @@ -57,7 +57,6 @@ PLR2044 [*] Line with empty comment 17 | def foo(): # this comment is OK, the one below is not 18 | pass # | ^ - | help: Delete the empty comment | 17 | def foo(): # this comment is OK, the one below is not @@ -104,7 +103,6 @@ PLR2044 [*] Line with empty comment 57 | α = 1 58 | α# | ^ - | help: Delete the empty comment | 57 | α = 1 diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR6104_non_augmented_assignment.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR6104_non_augmented_assignment.py.snap index 60f426f661..f690326bdc 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR6104_non_augmented_assignment.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR6104_non_augmented_assignment.py.snap @@ -441,7 +441,6 @@ PLR6104 [*] Use `*=` to perform an augmented assignment directly 41 | 42 | index = index * (index + 10) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with augmented assignment | 41 | @@ -458,7 +457,6 @@ PLR6104 [*] Use `+=` to perform an augmented assignment directly 46 | def t(self): 47 | self.a = self.a + 1 | ^^^^^^^^^^^^^^^^^^^ - | help: Replace with augmented assignment | 46 | def t(self): @@ -474,7 +472,6 @@ PLR6104 [*] Use `+=` to perform an augmented assignment directly 50 | obj = T() 51 | obj.a = obj.a + 1 | ^^^^^^^^^^^^^^^^^ - | help: Replace with augmented assignment | 50 | obj = T() @@ -660,7 +657,6 @@ PLR6104 [*] Use `+=` to perform an augmented assignment directly 93 | | \ 94 | | test8 | |_________^ - | help: Replace with augmented assignment | 89 | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0101_unreachable.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0101_unreachable.py.snap index 87c0c9c471..feb106dd80 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0101_unreachable.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0101_unreachable.py.snap @@ -34,4 +34,3 @@ PLW0101 Unreachable code in `multiple_returns` 29 | | return 2 30 | | print("unreachable range should include above return") | |__________________________________________________________^ - | 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 928ac24557..5fbe8caa9b 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 @@ -168,5 +168,4 @@ PLW0108 Lambda may be unnecessary; consider inlining inner function 62 | _ = lambda x: (string := str)(x) 63 | _ = lambda x: ((x := 1) and str)(x) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Inline function call diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0129_assert_on_string_literal.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0129_assert_on_string_literal.py.snap index f840a788e5..0f3bfbb781 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0129_assert_on_string_literal.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0129_assert_on_string_literal.py.snap @@ -8,7 +8,6 @@ PLW0129 Asserting on a non-empty string literal will always pass 2 | a = 9 / 3 3 | assert "No ZeroDivisionError were raised" # [assert-on-string-literal] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | PLW0129 Asserting on a non-empty string literal will always pass --> assert_on_string_literal.py:12:12 diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0131_named_expr_without_context.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0131_named_expr_without_context.py.snap index 4306ba4fc7..193dc8f464 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0131_named_expr_without_context.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0131_named_expr_without_context.py.snap @@ -18,7 +18,6 @@ PLW0131 Named expression used without context 3 | if True: 4 | (b := 1) | ^^^^^^ - | PLW0131 Named expression used without context --> named_expr_without_context.py:8:6 @@ -26,4 +25,3 @@ PLW0131 Named expression used without context 7 | class Foo: 8 | (c := 1) | ^^^^^^ - | 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 538f8dbb9c..b151cc566b 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 @@ -138,4 +138,3 @@ PLW0177 Comparing against a NaN value; use `math.isnan` instead 98 | assert x == float("-NaN ") 99 | assert x == float(" \n+nan \t") | ^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0244_redefined_slots_in_subclass.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0244_redefined_slots_in_subclass.py.snap index aca4d1ddf4..6b81a1b744 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0244_redefined_slots_in_subclass.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0244_redefined_slots_in_subclass.py.snap @@ -27,7 +27,6 @@ PLW0244 Slot `a` redefined from base class `AnotherBase` 22 | class AnotherChild(AnotherBase): 23 | __slots__ = ["a","b","e","f"] | ^^^ - | PLW0244 Slot `b` redefined from base class `AnotherBase` --> redefined_slots_in_subclass.py:23:22 @@ -35,4 +34,3 @@ PLW0244 Slot `b` redefined from base class `AnotherBase` 22 | class AnotherChild(AnotherBase): 23 | __slots__ = ["a","b","e","f"] | ^^^ - | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0406_import_self__module.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0406_import_self__module.py.snap index 741e604b57..cfab103c2d 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0406_import_self__module.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0406_import_self__module.py.snap @@ -26,4 +26,3 @@ PLW0406 Module `import_self.module` imports itself 2 | from import_self import module 3 | from . import module | ^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0602_global_variable_not_assigned.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0602_global_variable_not_assigned.py.snap index 8b06cc77f5..a5fdf4e1ce 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0602_global_variable_not_assigned.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0602_global_variable_not_assigned.py.snap @@ -8,7 +8,6 @@ PLW0602 Using global for `X` but no assignment is done 4 | def f(): 5 | global X | ^ - | PLW0602 Using global for `X` but no assignment is done --> global_variable_not_assigned.py:9:12 diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0642_self_or_cls_assignment.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0642_self_or_cls_assignment.py.snap index 360984514f..27df39b7d2 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0642_self_or_cls_assignment.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0642_self_or_cls_assignment.py.snap @@ -172,5 +172,4 @@ PLW0642 Reassigned `cls` variable in `__new__` method 49 | def __new__(cls): 50 | cls = "apple" # PLW0642 | ^^^ - | help: Consider using a different variable name diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW1501_bad_open_mode.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW1501_bad_open_mode.py.snap index 0566155cc3..85c95f9b1a 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW1501_bad_open_mode.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW1501_bad_open_mode.py.snap @@ -127,4 +127,3 @@ PLW1501 `Ua` is not a valid mode for `open` 36 | import builtins 37 | builtins.open(NAME, "Ua", encoding="utf-8") | ^^^^ - | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW1507_shallow_copy_environ.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW1507_shallow_copy_environ.py.snap index c5c4a85e61..c616da9775 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW1507_shallow_copy_environ.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW1507_shallow_copy_environ.py.snap @@ -8,7 +8,6 @@ PLW1507 [*] Shallow copy of `os.environ` via `copy.copy(os.environ)` 3 | 4 | copied_env = copy.copy(os.environ) # [shallow-copy-environ] | ^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `os.environ.copy()` | 3 | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW2901_redefined_loop_name.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW2901_redefined_loop_name.py.snap index 4a37f5facd..e639eb3059 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW2901_redefined_loop_name.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW2901_redefined_loop_name.py.snap @@ -274,4 +274,3 @@ PLW2901 `for` loop variable `a.i` overwritten by assignment target 179 | for a. i in []: 180 | a.i = 2 # error | ^^^ - | 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 55c6aef741..564dee97b6 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 @@ -291,7 +291,6 @@ PLW3301 [*] Nested `min` calls can be flattened 43 | import builtins 44 | builtins.min(1, min(2, 3)) | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Flatten nested `min` calls | 43 | import builtins diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__conflict_with_definition_rules.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__conflict_with_definition_rules.snap index 41c104b036..b02e55bb9e 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__conflict_with_definition_rules.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__conflict_with_definition_rules.snap @@ -9,7 +9,6 @@ PLR1712 [*] Unnecessary temporary variable 7 | | x = y 8 | | y = temp | |________^ - | help: Use `x, y = y, x` instead | 5 | x, y = 1, 2 diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__continue_in_finally.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__continue_in_finally.snap index d76dcc4236..fc851db825 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__continue_in_finally.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__continue_in_finally.snap @@ -72,7 +72,6 @@ PLE0116 `continue` not supported inside `finally` clause 48 | finally: 49 | continue # [continue-in-finally] | ^^^^^^^^ - | PLE0116 `continue` not supported inside `finally` clause --> continue_in_finally.py:56:9 @@ -136,4 +135,3 @@ PLE0116 `continue` not supported inside `finally` clause 94 | else: 95 | continue # [continue-in-finally] | ^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__import_outside_top_level_with_banned.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__import_outside_top_level_with_banned.snap index f433c3ce4b..84ac47d441 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__import_outside_top_level_with_banned.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__import_outside_top_level_with_banned.snap @@ -101,4 +101,3 @@ PLC0415 `import` should be at the top-level of a file 39 | # this should still trigger an error due to multiple imports 40 | from pkg import foo_allowed, bar_banned # [import-outside-toplevel] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__preview__PLW0133_useless_exception_statement.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__preview__PLW0133_useless_exception_statement.py.snap index 5e10f7ca81..3bc3b056b2 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__preview__PLW0133_useless_exception_statement.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__preview__PLW0133_useless_exception_statement.py.snap @@ -56,7 +56,6 @@ PLW0133 [*] Missing `raise` statement on exception 28 | MySubError("This is a custom error") # PLW0133 29 | MyValueError("This is a custom value error") # PLW0133 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add `raise` keyword | 28 | MySubError("This is a custom error") # PLW0133 @@ -173,7 +172,6 @@ PLW0133 [*] Missing `raise` statement on exception 48 | MySubError("This is an exception") # PLW0133 49 | MyValueError("This is an exception") # PLW0133 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add `raise` keyword | 48 | MySubError("This is an exception") # PLW0133 @@ -230,7 +228,6 @@ PLW0133 [*] Missing `raise` statement on exception 58 | MySubError("This is an exception") # PLW0133 59 | MyValueError("This is an exception") # PLW0133 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add `raise` keyword | 58 | MySubError("This is an exception") # PLW0133 @@ -346,7 +343,6 @@ PLW0133 [*] Missing `raise` statement on exception 78 | MySubError("This is an exception") # PLW0133 79 | MyValueError("This is an exception") # PLW0133 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add `raise` keyword | 78 | MySubError("This is an exception") # PLW0133 @@ -403,7 +399,6 @@ PLW0133 [*] Missing `raise` statement on exception 89 | MySubError("This is an exception") # PLW0133 90 | MyValueError("This is an exception") # PLW0133 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add `raise` keyword | 89 | MySubError("This is an exception") # PLW0133 @@ -460,7 +455,6 @@ PLW0133 [*] Missing `raise` statement on exception 98 | MySubError("This is an exception") # PLW0133 99 | MyValueError("This is an exception") # PLW0133 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add `raise` keyword | 98 | MySubError("This is an exception") # PLW0133 @@ -517,7 +511,6 @@ PLW0133 [*] Missing `raise` statement on exception 106 | (MySubError("This is an exception")) # PLW0133 107 | (MyValueError("This is an exception")) # PLW0133 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add `raise` keyword | 106 | (MySubError("This is an exception")) # PLW0133 @@ -574,7 +567,6 @@ PLW0133 [*] Missing `raise` statement on exception 114 | x = 1; (MySubError("This is an exception")); y = 2 # PLW0133 115 | x = 1; (MyValueError("This is an exception")); y = 2 # PLW0133 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add `raise` keyword | 114 | x = 1; (MySubError("This is an exception")); y = 2 # PLW0133 @@ -592,7 +584,6 @@ PLW0133 [*] Missing `raise` statement on exception 120 | UserWarning("This is a user warning") # PLW0133 121 | MyUserWarning("This is a custom user warning") # PLW0133 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add `raise` keyword | 120 | UserWarning("This is a user warning") # PLW0133 @@ -670,7 +661,6 @@ PLW0133 [*] Missing `raise` statement on exception 138 | 139 | MyUserWarning("This is a custom user warning") # PLW0133 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add `raise` keyword | 138 | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__too_many_public_methods.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__too_many_public_methods.snap index 8fdb140f3f..531340ccd8 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__too_many_public_methods.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__too_many_public_methods.snap @@ -40,4 +40,3 @@ PLR0904 Too many public methods (10 > 7) 37 | | def method9(self): 38 | | pass | |____________^ - | diff --git a/crates/ruff_linter/src/rules/pyupgrade/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/mod.rs index f22238f0a6..dc0abeeb9c 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/mod.rs @@ -225,26 +225,20 @@ mod tests { let snapshot = path.to_string_lossy().to_string(); let diagnostics = test_path( Path::new("pyupgrade").join(path).as_path(), - &settings::LinterSettings { - preview: PreviewMode::Enabled, - unresolved_target_version: PythonVersion::PY312.into(), - ..settings::LinterSettings::for_rule(rule_code) - }, + &settings::LinterSettings::for_rule(rule_code) + .with_preview_mode() + .with_target_version(PythonVersion::PY312), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) } - #[test_case(Rule::TypingTextStrAlias, Path::new("UP019.py"))] #[test_case(Rule::OSErrorAlias, Path::new("UP024_0.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").join(path).as_path(), - &settings::LinterSettings { - preview: PreviewMode::Enabled, - ..settings::LinterSettings::for_rule(rule_code) - }, + &settings::LinterSettings::for_rule(rule_code).with_preview_mode(), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) @@ -255,10 +249,8 @@ mod tests { let snapshot = format!("rules_py313__{}", path.to_string_lossy()); let diagnostics = test_path( Path::new("pyupgrade").join(path).as_path(), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY313.into(), - ..settings::LinterSettings::for_rule(rule_code) - }, + &settings::LinterSettings::for_rule(rule_code) + .with_target_version(PythonVersion::PY313), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) @@ -274,14 +266,8 @@ mod tests { assert_diagnostics_diff!( snapshot, Path::new("pyupgrade").join(path).as_path(), - &settings::LinterSettings { - preview: PreviewMode::Disabled, - ..settings::LinterSettings::for_rule(rule_code) - }, - &settings::LinterSettings { - preview: PreviewMode::Enabled, - ..settings::LinterSettings::for_rule(rule_code) - }, + &settings::LinterSettings::for_rule(rule_code), + &settings::LinterSettings::for_rule(rule_code).with_preview_mode(), ); Ok(()) } @@ -306,10 +292,8 @@ mod tests { fn async_timeout_error_alias_not_applied_py310() -> Result<()> { let diagnostics = test_path( Path::new("pyupgrade/UP041.py"), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY310.into(), - ..settings::LinterSettings::for_rule(Rule::TimeoutErrorAlias) - }, + &settings::LinterSettings::for_rule(Rule::TimeoutErrorAlias) + .with_target_version(PythonVersion::PY310), )?; assert_diagnostics!(diagnostics); Ok(()) @@ -319,10 +303,8 @@ mod tests { fn non_pep695_type_alias_not_applied_py311() -> Result<()> { let diagnostics = test_path( Path::new("pyupgrade/UP040.py"), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY311.into(), - ..settings::LinterSettings::for_rule(Rule::NonPEP695TypeAlias) - }, + &settings::LinterSettings::for_rule(Rule::NonPEP695TypeAlias) + .with_target_version(PythonVersion::PY311), )?; assert_diagnostics!(diagnostics); Ok(()) @@ -364,10 +346,8 @@ mod tests { fn future_annotations_pep_585_p37() -> Result<()> { let diagnostics = test_path( Path::new("pyupgrade/future_annotations.py"), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY37.into(), - ..settings::LinterSettings::for_rule(Rule::NonPEP585Annotation) - }, + &settings::LinterSettings::for_rule(Rule::NonPEP585Annotation) + .with_target_version(PythonVersion::PY37), )?; assert_diagnostics!(diagnostics); Ok(()) @@ -377,10 +357,8 @@ mod tests { fn future_annotations_pep_585_py310() -> Result<()> { let diagnostics = test_path( Path::new("pyupgrade/future_annotations.py"), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY310.into(), - ..settings::LinterSettings::for_rule(Rule::NonPEP585Annotation) - }, + &settings::LinterSettings::for_rule(Rule::NonPEP585Annotation) + .with_target_version(PythonVersion::PY310), )?; assert_diagnostics!(diagnostics); Ok(()) @@ -422,10 +400,8 @@ mod tests { fn datetime_utc_alias_py311() -> Result<()> { let diagnostics = test_path( Path::new("pyupgrade/UP017.py"), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY311.into(), - ..settings::LinterSettings::for_rule(Rule::DatetimeTimezoneUTC) - }, + &settings::LinterSettings::for_rule(Rule::DatetimeTimezoneUTC) + .with_target_version(PythonVersion::PY311), )?; assert_diagnostics!(diagnostics); Ok(()) @@ -435,10 +411,8 @@ mod tests { fn unpack_pep_646_py311() -> Result<()> { let diagnostics = test_path( Path::new("pyupgrade/UP044.py"), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY311.into(), - ..settings::LinterSettings::for_rule(Rule::NonPEP646Unpack) - }, + &settings::LinterSettings::for_rule(Rule::NonPEP646Unpack) + .with_target_version(PythonVersion::PY311), )?; assert_diagnostics!(diagnostics); Ok(()) @@ -484,7 +458,6 @@ mod tests { | 1 | from pipes import quote, Template | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Import from `shlex` | - from pipes import quote, Template @@ -541,10 +514,8 @@ mod tests { let snapshot = "UP043.pyi"; let diagnostics = test_path( Path::new("pyupgrade/UP043.pyi"), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY312.into(), - ..settings::LinterSettings::for_rule(Rule::UnnecessaryDefaultTypeArgs) - }, + &settings::LinterSettings::for_rule(Rule::UnnecessaryDefaultTypeArgs) + .with_target_version(PythonVersion::PY312), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) @@ -554,10 +525,8 @@ mod tests { fn up045_future_annotations_py39() -> Result<()> { let diagnostics = test_path( Path::new("pyupgrade/UP045_py39.py"), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY39.into(), - ..settings::LinterSettings::for_rule(Rule::NonPEP604AnnotationOptional) - }, + &settings::LinterSettings::for_rule(Rule::NonPEP604AnnotationOptional) + .with_target_version(PythonVersion::PY39), )?; 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 21c50c2e7c..c515dfb078 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 @@ -148,7 +148,7 @@ fn match_named_tuple_assign<'a>( let Expr::Call(ast::ExprCall { func, arguments: Arguments { args, keywords, .. }, - range: _, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, 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 d382290a7e..b61758b159 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 @@ -133,7 +133,7 @@ fn match_typed_dict_assign<'a>( let Expr::Call(ast::ExprCall { func, arguments, - range: _, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, @@ -278,7 +278,7 @@ fn match_fields_and_total(arguments: &Arguments) -> Option<(Suite, Option<&Keywo Expr::Call(ast::ExprCall { func, arguments: Arguments { keywords, .. }, - range: _, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, 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 4405cb6c1a..55d921cf42 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 @@ -283,55 +283,54 @@ pub(crate) fn deprecated_mock_attribute(checker: &Checker, attribute: &ast::Expr /// UP026 pub(crate) fn deprecated_mock_import(checker: &Checker, stmt: &Stmt) { match stmt { + // Find all `mock` imports. Stmt::Import(ast::StmtImport { names, is_lazy: _, range: _, node_index: _, - }) - // Find all `mock` imports. - if names - .iter() - .any(|name| &name.name == "mock" || &name.name == "mock.mock") - => { - // Generate the fix, if needed, which is shared between all `mock` imports. - let content = if let Some(indent) = indentation(checker.source(), stmt) { - match format_import(stmt, indent, checker.locator(), checker.stylist()) { - Ok(content) => Some(content), - Err(e) => { - debug!("Failed to rewrite `mock` import: {e}"); - None - } + }) if names + .iter() + .any(|name| &name.name == "mock" || &name.name == "mock.mock") => + { + // Generate the fix, if needed, which is shared between all `mock` imports. + let content = if let Some(indent) = indentation(checker.source(), stmt) { + match format_import(stmt, indent, checker.locator(), checker.stylist()) { + Ok(content) => Some(content), + Err(e) => { + debug!("Failed to rewrite `mock` import: {e}"); + None } - } else { - None - }; + } + } else { + None + }; - // Add a `Diagnostic` for each `mock` import. - for name in names { - if (&name.name == "mock" || &name.name == "mock.mock") - && !is_import_required_by_isort( - &checker.settings().isort.required_imports, - stmt.into(), - name, - ) - { - let mut diagnostic = checker.report_diagnostic( - DeprecatedMockImport { - reference_type: MockReference::Import, - }, - name.range(), - ); - diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Deprecated); - if let Some(content) = content.as_ref() { - diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( - content.clone(), - stmt.range(), - ))); - } + // Add a `Diagnostic` for each `mock` import. + for name in names { + if (&name.name == "mock" || &name.name == "mock.mock") + && !is_import_required_by_isort( + &checker.settings().isort.required_imports, + stmt.into(), + name, + ) + { + let mut diagnostic = checker.report_diagnostic( + DeprecatedMockImport { + reference_type: MockReference::Import, + }, + name.range(), + ); + diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Deprecated); + if let Some(content) = content.as_ref() { + diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( + content.clone(), + stmt.range(), + ))); } } } + } Stmt::ImportFrom(ast::StmtImportFrom { module: Some(module), level, 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 b0839d21d6..b055d9065d 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 @@ -71,7 +71,7 @@ pub(crate) fn lru_cache_with_maxsize_none(checker: &Checker, decorator_list: &[D range: _, node_index: _, }, - range: _, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, 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 7464726f90..b88f200228 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 @@ -59,7 +59,7 @@ pub(crate) fn lru_cache_without_parameters(checker: &Checker, decorator_list: &[ let Expr::Call(ast::ExprCall { func, arguments, - range: _, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, 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 1ef5049c97..946a5cc7c8 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/native_literals.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/native_literals.rs @@ -196,7 +196,7 @@ pub(crate) fn native_literals( range: _, node_index: _, }, - range: call_range, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, @@ -292,7 +292,8 @@ pub(crate) fn native_literals( // 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()) + 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(); @@ -331,7 +332,7 @@ pub(crate) fn native_literals( content.push(' '); } - let applicability = if checker.comment_ranges().intersects(call.range) { + let applicability = if checker.comment_ranges().intersects(call.range()) { Applicability::Unsafe } else { Applicability::Safe 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 18ffafac32..a06f31f801 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs @@ -386,6 +386,20 @@ fn in_nested_context(checker: &Checker) -> bool { .any(|stmt| matches!(stmt, Stmt::ClassDef(_) | Stmt::FunctionDef(_))) } +/// Returns `true` if a type variable without a default follows a type variable with a default. +/// +/// In a PEP 695 type parameter list this is a syntax error: +/// +/// ```python +/// type Pair[T = int, S] = tuple[T, S] # non-default type parameter `S` follows default type parameter +/// ``` +fn non_default_follows_default(type_vars: &[TypeVar]) -> bool { + type_vars + .iter() + .skip_while(|tv| tv.default.is_none()) + .any(|tv| tv.default.is_none()) +} + /// Deduplicate `vars`, returning `None` if `vars` is empty or any duplicates are found. /// Also returns `None` if any `TypeVar` has a default value and the target Python version /// is below 3.13 or preview mode is not enabled. Note that `typing_extensions` backports @@ -404,6 +418,10 @@ fn check_type_vars<'a>(vars: Vec>, checker: &Checker) -> Option Self { + fn new() -> Self { ClassCellReferenceFinder { has_class_cell: false, } } - pub(crate) fn found(&self) -> bool { + fn found(&self) -> bool { self.has_class_cell } } 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 4e69c888b6..175c760374 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 @@ -6,13 +6,10 @@ use ruff_python_semantic::Modules; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; -use crate::preview::is_typing_extensions_str_alias_enabled; use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does -/// Checks for uses of `typing.Text`. -/// -/// In preview mode, also checks for `typing_extensions.Text`. +/// Checks for uses of `typing.Text` and `typing_extensions.Text`. /// /// ## Why is this bad? /// `typing.Text` is an alias for `str`, and only exists for Python 2 @@ -65,11 +62,7 @@ pub(crate) fn typing_text_str_alias(checker: &Checker, expr: &Expr) { let segments = qualified_name.segments(); let module = match segments { ["typing", "Text"] => TypingModule::Typing, - ["typing_extensions", "Text"] - if is_typing_extensions_str_alias_enabled(checker.settings()) => - { - TypingModule::TypingExtensions - } + ["typing_extensions", "Text"] => TypingModule::TypingExtensions, _ => return, }; 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 df8c492c96..b817a55676 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 @@ -24,7 +24,7 @@ impl fmt::Display for CallKind { } impl CallKind { - pub(crate) fn from_name(name: &str) -> Option { + fn from_name(name: &str) -> Option { match name { "isinstance" => Some(CallKind::Isinstance), "issubclass" => Some(CallKind::Issubclass), diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP001.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP001.py.snap index 15420b2197..aad9e7efda 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP001.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP001.py.snap @@ -7,7 +7,6 @@ UP001 [*] `__metaclass__ = type` is implied 1 | class A: 2 | __metaclass__ = type | ^^^^^^^^^^^^^^^^^^^^ - | help: Remove `__metaclass__ = type` | 1 | class A: diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP003.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP003.py.snap index abcdc9c7f3..ecd903ccae 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP003.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP003.py.snap @@ -92,7 +92,6 @@ UP003 [*] Use `str` instead of `type(...)` 13 | # Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722459841 14 | assert isinstance(fullname, type("")is not True) | ^^^^^^^^ - | help: Replace `type(...)` with `str` | 13 | # Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722459841 diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP005.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP005.py.snap index 164ee92e96..14eddffa1e 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP005.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP005.py.snap @@ -61,7 +61,6 @@ UP005 [*] `assertNotRegexpMatches` is deprecated, use `assertNotRegex` 9 | self.failUnlessAlmostEqual(1, 1.1) 10 | self.assertNotRegexpMatches("a", "b") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace `assertNotRegex` with `assertNotRegexpMatches` | 9 | self.failUnlessAlmostEqual(1, 1.1) diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP007_1.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP007_1.py.snap index bdd77d7776..de174b1ede 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP007_1.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP007_1.py.snap @@ -216,7 +216,6 @@ UP007 [*] Use `X | Y` for type annotations 48 | x: Union[str, int] 49 | x: Union["str", "int"] | ^^^^^^^^^^^^^^^^^^^ - | help: Convert to `X | Y` | 48 | x: Union[str, int] @@ -324,7 +323,6 @@ UP007 [*] Use `X | Y` for type annotations 154 | | | Literal["LongLiteralNumberThree"] 155 | | ] | |_____^ - | help: Convert to `X | Y` | 150 | diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP007_basedpython.by.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP007_basedpython.by.snap index 131f1dd974..592261d8e6 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP007_basedpython.by.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP007_basedpython.by.snap @@ -79,7 +79,6 @@ UP007 [*] Use `X | Y` for type annotations 10 | d: Union[list[literal str], int] 11 | e: Union[typeof d, int] | ^^^^^^^^^^^^^^^^^^^^ - | help: Convert to `X | Y` | 10 | d: Union[list[literal str], int] 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 d84e4c2caa..ab341e4a98 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 @@ -47,7 +47,6 @@ UP008 [*] Use `super()` instead of `super(__class__, self)` 21 | | self, 22 | | ).method() # wrong | |_________^ - | help: Remove `super()` parameters | 18 | super(Child, self).method # wrong @@ -116,7 +115,6 @@ UP008 [*] Use `super()` instead of `super(__class__, self)` 115 | def bar(self): 116 | super(__class__, self).foo() | ^^^^^^^^^^^^^^^^^ - | help: Remove `super()` parameters | 115 | def bar(self): @@ -193,7 +191,6 @@ UP008 [*] Use `super()` instead of `super(__class__, self)` 150 | | # also a comment 151 | | ).f() | |_________^ - | help: Remove `super()` parameters | 146 | ).f() @@ -426,7 +423,6 @@ UP008 [*] Use `super()` instead of `super(__class__, self)` 232 | super # Python injects __class__ into scope 233 | builtins.super(ChildD10, self).f() | ^^^^^^^^^^^^^^^^ - | help: Remove `super()` parameters | 232 | super # Python injects __class__ into scope @@ -442,7 +438,6 @@ UP008 [*] Use `super()` instead of `super(__class__, self)` 341 | def __init__(self, foo): 342 | super(Outer.Inner, self).__init__(foo) # UP008: matches enclosing class chain | ^^^^^^^^^^^^^^^^^^^ - | help: Remove `super()` parameters | 341 | def __init__(self, foo): @@ -476,7 +471,6 @@ UP008 [*] Use `super()` instead of `super(__class__, self)` 387 | def f(self): 388 | super (Whitespace, self).f() # can use super() | ^^^^^^^^^^^^^^^^^^ - | help: Remove `super()` parameters | 387 | def f(self): @@ -492,7 +486,6 @@ UP008 [*] Use `super()` instead of `super(__class__, self)` 394 | def f(self): 395 | super(LocalOuter.LocalInner, self).f() # can use super() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove `super()` parameters | 394 | def f(self): @@ -507,7 +500,6 @@ UP008 [*] Use `super()` instead of `super(__class__, self)` 398 | class LambdaMethod(BaseClass): 399 | f = lambda self: super(LambdaMethod, self).f() # can use super() | ^^^^^^^^^^^^^^^^^^^^ - | help: Remove `super()` parameters | 398 | class LambdaMethod(BaseClass): @@ -523,7 +515,6 @@ UP008 [*] Use `super()` instead of `super(__class__, self)` 404 | def f(cls): 405 | super(ClassMethod, cls).f() # can use super() | ^^^^^^^^^^^^^^^^^^ - | help: Remove `super()` parameters | 404 | def f(cls): @@ -539,7 +530,6 @@ UP008 [*] Use `super()` instead of `super(__class__, self)` 409 | async def f(self): 410 | super(AsyncMethod, self).f() # can use super() | ^^^^^^^^^^^^^^^^^^^ - | help: Remove `super()` parameters | 409 | async def f(self): @@ -555,7 +545,6 @@ UP008 [*] Use `super()` instead of `super(__class__, self)` 415 | def f(self): 416 | super (OuterWithWhitespace.Inner, self).f() # can use super() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove `super()` parameters | 415 | def f(self): diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP010_0.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP010_0.py.snap index 6515184e9c..1eba176457 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP010_0.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP010_0.py.snap @@ -171,7 +171,6 @@ UP010 [*] Unnecessary `__future__` import `generators` for target Python version 14 | from __future__ import invalid_module, generators 15 | from __future__ import generators # comment | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove unnecessary `__future__` import | 14 | from __future__ import invalid_module, generators diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP012.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP012.py.snap index 39afd57a55..a0adc52d17 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP012.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP012.py.snap @@ -468,7 +468,6 @@ UP012 [*] Unnecessary UTF-8 `encoding` argument to `encode` 76 | ("unicode text©").encode("utf-8") 77 | ("unicode text©").encode(encoding="utf-8") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove unnecessary `encoding` argument | 76 | ("unicode text©").encode("utf-8") @@ -800,7 +799,6 @@ UP012 [*] Unnecessary call to `encode` as UTF-8 119 | 120 | '\\ u0000 '.encode() | ^^^^^^^^^^^^^^^^^^^^ - | help: Rewrite as bytes literal | 119 | diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP014.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP014.py.snap index 3a508e0f37..7bf610bcde 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP014.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP014.py.snap @@ -102,7 +102,6 @@ UP014 [*] Convert `X` from `NamedTuple` functional to class syntax 37 | | ("some_config", int), # important 38 | | ]) | |__^ - | help: Convert `X` to class syntax | 35 | # Unsafe fix if comments are present diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP015_1.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP015_1.py.snap index 2f519d3769..76b4fe2b95 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP015_1.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP015_1.py.snap @@ -8,7 +8,6 @@ UP015 [*] Unnecessary mode argument 2 | # Refer: https://github.com/astral-sh/ruff/issues/11736 3 | x: 'open("foo", "r")' | ^^^ - | help: Remove mode argument | 2 | # Refer: https://github.com/astral-sh/ruff/issues/11736 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 c81ba0a928..a331620e67 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 @@ -389,7 +389,6 @@ UP018 [*] Unnecessary `float` call (rewrite as a literal) 60 | float(+1.0) 61 | float(-1.0) | ^^^^^^^^^^^ - | help: Replace with float literal | 60 | float(+1.0) @@ -525,7 +524,6 @@ UP018 [*] Unnecessary `int` call (rewrite as a literal) 75 | 76 | await int(-1) # await (-1) | ^^^^^^^ - | help: Replace with integer literal | 75 | @@ -555,7 +553,6 @@ UP018 [*] Unnecessary `float` call (rewrite as a literal) 79 | int(+1) ** 0 80 | float(+1.0)() | ^^^^^^^^^^^ - | help: Replace with float literal | 79 | int(+1) ** 0 @@ -647,7 +644,6 @@ UP018 [*] Unnecessary `bool` call (rewrite as a literal) 91 | float(1.)and None 92 | bool(True)and() | ^^^^^^^^^^ - | help: Replace with boolean literal | 91 | float(1.)and None @@ -866,7 +862,6 @@ UP018 [*] Unnecessary `complex` call (rewrite as a literal) 112 | complex(1j).real 113 | complex(real=1j).real | ^^^^^^^^^^^^^^^^ - | help: Replace with complex literal | 112 | complex(1j).real 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 index cd05516752..b16bc7a8bf 100644 --- 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 @@ -23,7 +23,6 @@ UP018 [*] Unnecessary `int` call (rewrite as a literal) 4 | / int(+ 5 | | 1) | |______^ - | help: Replace with integer literal | 3 | 1) - int(+ 4 + (+ 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 index 5b3d30c93f..3f367e591f 100644 --- 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 @@ -28,7 +28,6 @@ UP018 [*] Unnecessary `int` call (rewrite as a literal) 6 | / int(+ 7 | | 1) | |______^ - | help: Replace with integer literal | 5 | diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP019.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP019.py.snap index 13310b6a7b..142f093d4e 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP019.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP019.py.snap @@ -60,3 +60,48 @@ help: Replace with `str` 19 + def print_fourth_word(word: str) -> None: 20 | print(word) | + +UP019 [*] `typing_extensions.Text` is deprecated, use `str` + --> UP019.py:28:28 + | +28 | def print_fifth_word(word: typing_extensions.Text) -> None: + | ^^^^^^^^^^^^^^^^^^^^^^ +29 | print(word) + | +help: Replace with `str` + | +27 | + - def print_fifth_word(word: typing_extensions.Text) -> None: +28 + def print_fifth_word(word: str) -> None: +29 | print(word) + | + +UP019 [*] `typing_extensions.Text` is deprecated, use `str` + --> UP019.py:32:28 + | +32 | def print_sixth_word(word: TypingExt.Text) -> None: + | ^^^^^^^^^^^^^^ +33 | print(word) + | +help: Replace with `str` + | +31 | + - def print_sixth_word(word: TypingExt.Text) -> None: +32 + def print_sixth_word(word: str) -> None: +33 | print(word) + | + +UP019 [*] `typing_extensions.Text` is deprecated, use `str` + --> UP019.py:36:30 + | +36 | def print_seventh_word(word: TextAlias) -> None: + | ^^^^^^^^^ +37 | print(word) + | +help: Replace with `str` + | +35 | + - def print_seventh_word(word: TextAlias) -> None: +36 + def print_seventh_word(word: str) -> None: +37 | print(word) + | diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP019.py__preview.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP019.py__preview.snap deleted file mode 100644 index 142f093d4e..0000000000 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP019.py__preview.snap +++ /dev/null @@ -1,107 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/pyupgrade/mod.rs ---- -UP019 [*] `typing.Text` is deprecated, use `str` - --> UP019.py:7:22 - | -7 | def print_word(word: Text) -> None: - | ^^^^ -8 | print(word) - | -help: Replace with `str` - | -6 | - - def print_word(word: Text) -> None: -7 + def print_word(word: str) -> None: -8 | print(word) - | - -UP019 [*] `typing.Text` is deprecated, use `str` - --> UP019.py:11:29 - | -11 | def print_second_word(word: typing.Text) -> None: - | ^^^^^^^^^^^ -12 | print(word) - | -help: Replace with `str` - | -10 | - - def print_second_word(word: typing.Text) -> None: -11 + def print_second_word(word: str) -> None: -12 | print(word) - | - -UP019 [*] `typing.Text` is deprecated, use `str` - --> UP019.py:15:28 - | -15 | def print_third_word(word: Hello.Text) -> None: - | ^^^^^^^^^^ -16 | print(word) - | -help: Replace with `str` - | -14 | - - def print_third_word(word: Hello.Text) -> None: -15 + def print_third_word(word: str) -> None: -16 | print(word) - | - -UP019 [*] `typing.Text` is deprecated, use `str` - --> UP019.py:19:29 - | -19 | def print_fourth_word(word: Goodbye) -> None: - | ^^^^^^^ -20 | print(word) - | -help: Replace with `str` - | -18 | - - def print_fourth_word(word: Goodbye) -> None: -19 + def print_fourth_word(word: str) -> None: -20 | print(word) - | - -UP019 [*] `typing_extensions.Text` is deprecated, use `str` - --> UP019.py:28:28 - | -28 | def print_fifth_word(word: typing_extensions.Text) -> None: - | ^^^^^^^^^^^^^^^^^^^^^^ -29 | print(word) - | -help: Replace with `str` - | -27 | - - def print_fifth_word(word: typing_extensions.Text) -> None: -28 + def print_fifth_word(word: str) -> None: -29 | print(word) - | - -UP019 [*] `typing_extensions.Text` is deprecated, use `str` - --> UP019.py:32:28 - | -32 | def print_sixth_word(word: TypingExt.Text) -> None: - | ^^^^^^^^^^^^^^ -33 | print(word) - | -help: Replace with `str` - | -31 | - - def print_sixth_word(word: TypingExt.Text) -> None: -32 + def print_sixth_word(word: str) -> None: -33 | print(word) - | - -UP019 [*] `typing_extensions.Text` is deprecated, use `str` - --> UP019.py:36:30 - | -36 | def print_seventh_word(word: TextAlias) -> None: - | ^^^^^^^^^ -37 | print(word) - | -help: Replace with `str` - | -35 | - - def print_seventh_word(word: TextAlias) -> None: -36 + def print_seventh_word(word: str) -> None: -37 | print(word) - | 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 3f999e9d4e..bd02547a50 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 @@ -391,7 +391,6 @@ UP024 [*] Replace aliased errors with `OSError` 54 | raise EnvironmentError(1) 55 | raise IOError(1, 2) | ^^^^^^^ - | help: Replace `IOError` with builtin `OSError` | 54 | raise EnvironmentError(1) 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 087db8f6f9..a3315333bf 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 @@ -323,7 +323,6 @@ UP025 [*] Remove unicode literals from strings 33 | """"""""""""""""""""u"hi" 34 | ""U"helloooo" | ^^^^^^^^^^^ - | help: Remove unicode prefix | 33 | """"""""""""""""""""u"hi" diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP026.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP026.py.snap index 6ffb0e6bf4..0a8c718d06 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP026.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP026.py.snap @@ -503,7 +503,6 @@ UP026 [*] `mock` is deprecated, use `unittest.mock` 85 | # This should yield multiple, aliased imports. 86 | from mock import mock as foo, mock as bar, mock | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Import from `unittest.mock` instead | 85 | # This should yield multiple, aliased imports. @@ -520,7 +519,6 @@ UP026 [*] `mock` is deprecated, use `unittest.mock` 92 | # Error (`mock.Mock()`). 93 | x = mock.mock.Mock() | ^^^^^^^^^ - | help: Replace `mock.mock` with `mock` | 92 | # Error (`mock.Mock()`). diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP028_0.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP028_0.py.snap index ad2c71231e..c8e7dca2fe 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP028_0.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP028_0.py.snap @@ -8,7 +8,6 @@ UP028 [*] Replace `yield` over `for` loop with `yield from` 2 | / for x in y: 3 | | yield x | |_______________^ - | help: Replace with `yield from` | 1 | def f(): @@ -26,7 +25,6 @@ UP028 [*] Replace `yield` over `for` loop with `yield from` 7 | / for x, y in z: 8 | | yield (x, y) | |____________________^ - | help: Replace with `yield from` | 6 | def g(): @@ -44,7 +42,6 @@ UP028 [*] Replace `yield` over `for` loop with `yield from` 12 | / for x in [1, 2, 3]: 13 | | yield x | |_______________^ - | help: Replace with `yield from` | 11 | def h(): @@ -62,7 +59,6 @@ UP028 [*] Replace `yield` over `for` loop with `yield from` 17 | / for x in {x for x in y}: 18 | | yield x | |_______________^ - | help: Replace with `yield from` | 16 | def i(): @@ -80,7 +76,6 @@ UP028 [*] Replace `yield` over `for` loop with `yield from` 22 | / for x in (1, 2, 3): 23 | | yield x | |_______________^ - | help: Replace with `yield from` | 21 | def j(): @@ -98,7 +93,6 @@ UP028 [*] Replace `yield` over `for` loop with `yield from` 27 | / for x, y in {3: "x", 6: "y"}: 28 | | yield x, y | |__________________^ - | help: Replace with `yield from` | 26 | def k(): @@ -147,7 +141,6 @@ UP028 [*] Replace `yield` over `for` loop with `yield from` 44 | / for x, y in [{3: (3, [44, "long ss"]), 6: "y"}]: 45 | | yield x, y | |__________________^ - | help: Replace with `yield from` | 43 | def f(): @@ -209,7 +202,6 @@ UP028 [*] Replace `yield` over `for` loop with `yield from` 67 | / for z in x: 68 | | yield z | |_______________^ - | help: Replace with `yield from` | 66 | yield x @@ -250,7 +242,6 @@ UP028 [*] Replace `yield` over `for` loop with `yield from` 82 | | ): 83 | | yield h | |_______________^ - | help: Replace with `yield from` | 78 | def _serve_method(fn): @@ -357,7 +348,6 @@ UP028 [*] Replace `yield` over `for` loop with `yield from` 170 | / for a in 1,: 171 | | yield a | |_______________^ - | help: Replace with `yield from` | 169 | def f(): @@ -389,5 +379,4 @@ UP028 Replace `yield` over `for` loop with `yield from` 187 | / for some_non_local in iterable: 188 | | yield some_non_local | |________________________________^ - | help: Replace with `yield from` diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP029_1.py_skip_required_imports.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP029_1.py_skip_required_imports.snap index 52bc289d48..69d538baef 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP029_1.py_skip_required_imports.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP029_1.py_skip_required_imports.snap @@ -6,7 +6,6 @@ UP029 [*] Unnecessary builtin import: `int` | 1 | from builtins import str, int | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove unnecessary builtin import | - from builtins import str, int diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP030_0.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP030_0.py.snap index 207e3e6b2b..1a6e2d88d4 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP030_0.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP030_0.py.snap @@ -486,7 +486,6 @@ UP030 [*] Use implicit references for positional format fields 64 | 65 | "{{{0}}}".format(123) | ^^^^^^^^^^^^^^^^^^^^^ - | help: Remove explicit positional indices | 64 | diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP031_0.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP031_0.py.snap index e578ccf4a0..5251a71ac3 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP031_0.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP031_0.py.snap @@ -455,7 +455,6 @@ UP031 [*] Use format specifiers instead of percent format 57 | 58 | print("%(a)s" % {"a" : 1}) | ^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with format specifiers | 57 | @@ -815,7 +814,6 @@ UP031 [*] Use format specifiers instead of percent format 112 | | x, # comment 113 | | ) | |_^ - | help: Replace with format specifiers | 110 | @@ -830,7 +828,7 @@ UP031 [*] Use format specifiers instead of percent format | 116 | path = "%s-%s-%s.pem" % ( | ________^ -117 | | safe_domain_name(cn), # common name, which should be filename safe because it is IDNA-encoded, but in case of a malformed cert ma… +117 | | safe_domain_name(cn), # common name, which should be filename safe because it is IDNA-encoded, but in case of a malformed cert … 118 | | cert.not_valid_after.date().isoformat().replace("-", ""), # expiration date 119 | | hexlify(cert.fingerprint(hashes.SHA256())).decode("ascii")[0:8], # fingerprint prefix 120 | | ) @@ -1214,5 +1212,4 @@ UP031 Use format specifiers instead of percent format 170 | 171 | "%(and)s" % {"and": 2} | ^^^^^^^^^ - | help: Replace with format specifiers diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_0.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_0.py.snap index 6173f60104..774ff321e9 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_0.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_0.py.snap @@ -713,7 +713,6 @@ UP032 [*] Use f-string instead of `format` call 106 | | "b" 107 | | ).format(a=1) | |_____________^ - | help: Convert to f-string | 104 | ( @@ -731,7 +730,6 @@ UP032 [*] Use f-string instead of `format` call 110 | def d(osname, version, release): 111 | return"{}-{}.{}".format(osname, version, release) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Convert to f-string | 110 | def d(osname, version, release): @@ -746,7 +744,6 @@ UP032 [*] Use f-string instead of `format` call 114 | def e(): 115 | yield"{}".format(1) | ^^^^^^^^^^^^^^ - | help: Convert to f-string | 114 | def e(): @@ -760,7 +757,6 @@ UP032 [*] Use f-string instead of `format` call | 118 | assert"{}".format(1) | ^^^^^^^^^^^^^^ - | help: Convert to f-string | 117 | @@ -775,7 +771,6 @@ UP032 [*] Use f-string instead of `format` call 121 | async def c(): 122 | return "{}".format(await 3) | ^^^^^^^^^^^^^^^^^^^^ - | help: Convert to f-string | 121 | async def c(): @@ -790,7 +785,6 @@ UP032 [*] Use f-string instead of `format` call 125 | async def c(): 126 | return "{}".format(1 + await 3) | ^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Convert to f-string | 125 | async def c(): @@ -927,7 +921,6 @@ UP032 Use f-string instead of `format` call 202 | | 1 # comment 203 | | ) | |_^ - | help: Convert to f-string UP032 [*] Use f-string instead of `format` call @@ -1056,7 +1049,6 @@ UP032 [*] Use f-string instead of `format` call 234 | 235 | ("{}" "{{}}").format(a) | ^^^^^^^^^^^^^^^^^^^^^^^ - | help: Convert to f-string | 234 | @@ -1267,7 +1259,6 @@ UP032 [*] Use f-string instead of `format` call 282 | # Raw string with \N{...} 283 | r"\N{angle}AOB = {angle}°".format(angle=180) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Convert to f-string | 282 | # Raw string with \N{...} diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_1.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_1.py.snap index 51eeb10965..0480f71b84 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_1.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_1.py.snap @@ -6,7 +6,6 @@ UP032 [*] Use f-string instead of `format` call | 1 | "{} {}".format(a, b) # Intentionally at start-of-file, to ensure graceful handling. | ^^^^^^^^^^^^^^^^^^^^ - | help: Convert to f-string | - "{} {}".format(a, b) # Intentionally at start-of-file, to ensure graceful handling. diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_2.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_2.py.snap index d9e8d40d7a..3c6fa7aee7 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_2.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_2.py.snap @@ -426,7 +426,6 @@ UP032 [*] Use f-string instead of `format` call 32 | "{0.real}".format(1_2) 33 | "{a.real}".format(a=1_2) | ^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Convert to f-string | 32 | "{0.real}".format(1_2) 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 fe100e44bf..b84ae75f6a 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 @@ -1130,7 +1130,6 @@ UP035 [*] Import from `re` instead: `Pattern` 133 | # UP035 on py37+ only 134 | from typing.re import Pattern | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Import from `re` | 133 | # UP035 on py37+ only diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_0.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_0.py.snap index d0bce0c8a3..a9c1dba916 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_0.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_0.py.snap @@ -37,7 +37,6 @@ UP037 [*] Remove quotes from type annotation 18 | def foo(var: "MyClass") -> "MyClass": 19 | x: "MyClass" | ^^^^^^^^^ - | help: Remove quotes | 18 | def foo(var: "MyClass") -> "MyClass": @@ -114,7 +113,6 @@ UP037 [*] Remove quotes from type annotation 31 | 32 | x: Callable[["MyClass"], None] | ^^^^^^^^^ - | help: Remove quotes | 31 | @@ -129,7 +127,6 @@ UP037 [*] Remove quotes from type annotation 35 | class Foo(NamedTuple): 36 | x: "MyClass" | ^^^^^^^^^ - | help: Remove quotes | 35 | class Foo(NamedTuple): @@ -144,7 +141,6 @@ UP037 [*] Remove quotes from type annotation 39 | class D(TypedDict): 40 | E: TypedDict("E", foo="int", total=False) | ^^^^^ - | help: Remove quotes | 39 | class D(TypedDict): @@ -159,7 +155,6 @@ UP037 [*] Remove quotes from type annotation 43 | class D(TypedDict): 44 | E: TypedDict("E", {"foo": "int"}) | ^^^^^ - | help: Remove quotes | 43 | class D(TypedDict): @@ -559,7 +554,6 @@ UP037 [*] Remove quotes from type annotation 125 | def foo(bar: "A\n#"): ... 126 | def foo(bar: "A\n#\n"): ... | ^^^^^^^^ - | help: Remove quotes | 125 | def foo(bar: "A\n#"): ... diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_1.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_1.py.snap index 3e8587c3a4..09e59a880d 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_1.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_1.py.snap @@ -24,7 +24,6 @@ UP037 [*] Remove quotes from type annotation 13 | # OK 14 | X: "Tuple[int, int]" = (0, 0) | ^^^^^^^^^^^^^^^^^ - | help: Remove quotes | 13 | # OK diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_2.pyi.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_2.pyi.snap index 645b89f483..5ef03783a7 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_2.pyi.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_2.pyi.snap @@ -8,7 +8,6 @@ UP037 [*] Remove quotes from type annotation 2 | 3 | def f(a: Foo['SingleLine # Comment']): ... | ^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove quotes | 2 | @@ -26,7 +25,6 @@ UP037 [*] Remove quotes from type annotation 7 | | Multi | 8 | | Line]''']): ... | |____________^ - | help: Remove quotes | 5 | @@ -47,7 +45,6 @@ UP037 [*] Remove quotes from type annotation 13 | | Line # Comment 14 | | ]''']): ... | |____^ - | help: Remove quotes | 10 | @@ -68,7 +65,6 @@ UP037 [*] Remove quotes from type annotation 18 | | Multi | 19 | | Line] # Comment''']): ... | |_______________________^ - | help: Remove quotes | 16 | @@ -90,7 +86,6 @@ UP037 [*] Remove quotes from type annotation 24 | | Multi | 25 | | Line] # Comment''']): ... | |_______________________^ - | help: Remove quotes | 21 | @@ -111,7 +106,6 @@ UP037 [*] Remove quotes from type annotation | __________^ 29 | | ''' = []): ... | |_______^ - | help: Remove quotes | 27 | @@ -129,7 +123,6 @@ UP037 [*] Remove quotes from type annotation | ____^ 33 | | list[int]''' = [42] | |____________^ - | help: Remove quotes | 31 | @@ -148,7 +141,6 @@ UP037 [*] Remove quotes from type annotation 37 | | list[int] 38 | | ''' = []): ... | |_______^ - | help: Remove quotes | 35 | @@ -171,7 +163,6 @@ UP037 [*] Remove quotes from type annotation 45 | | Line 46 | | ] # Comment''']): ... | |___________________^ - | help: Remove quotes | 40 | @@ -194,7 +185,6 @@ UP037 [*] Remove quotes from type annotation | ____^ 50 | | [int]''' = [42] | |________^ - | help: Remove quotes | 48 | diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_3.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_3.py.snap index 7094914f7f..f16c938d50 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_3.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_3.py.snap @@ -26,7 +26,6 @@ UP037 [*] Remove quotes from type annotation 16 | # the behavior of _singleton above should match a non-ClassVar 17 | _doubleton: "EmptyCell" | ^^^^^^^^^^^ - | help: Remove quotes | 16 | # the behavior of _singleton above should match a non-ClassVar diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP039.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP039.py.snap index be76b72ac4..29e0906293 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP039.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP039.py.snap @@ -89,7 +89,6 @@ UP039 [*] Unnecessary parentheses after class definition 47 | | # text 48 | | ): ... | |_^ - | help: Remove parentheses | 45 | diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py.snap index e3acbbb7a5..2baabf904c 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py.snap @@ -215,7 +215,6 @@ UP040 [*] Type alias `Decorator` uses `TypeAlias` annotation instead of the `typ 56 | T = typing.TypeVar["T"] 57 | Decorator: TypeAlias = typing.Callable[[T], T] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use the `type` keyword | 56 | T = typing.TypeVar["T"] @@ -352,7 +351,6 @@ UP040 [*] Type alias `PositiveList` uses `TypeAliasType` assignment instead of t 105 | | "PositiveList", list[Annotated[T, Gt(0)]], type_params=(T,) 106 | | ) # this comment should be okay | |_^ - | help: Use the `type` keyword | 103 | T = TypeVar("T") diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py__preview_diff.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py__preview_diff.snap index f2eed99224..81fb3d356d 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py__preview_diff.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py__preview_diff.snap @@ -56,7 +56,6 @@ UP040 [*] Type alias `DefaultList` uses `TypeAlias` annotation instead of the `t 133 | T_default = TypeVar("T_default", default=int) 134 | DefaultList: TypeAlias = list[T_default] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use the `type` keyword | 133 | T_default = TypeVar("T_default", default=int) diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.pyi.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.pyi.snap index 5ce1224285..c37220ce2a 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.pyi.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.pyi.snap @@ -25,7 +25,6 @@ UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keywo 6 | x: typing.TypeAlias = int 7 | x: TypeAlias = int | ^^^^^^^^^^^^^^^^^^ - | help: Use the `type` keyword | 6 | x: typing.TypeAlias = int @@ -69,7 +68,6 @@ UP040 [*] Type alias `T` uses `TypeAlias` annotation instead of the `type` keywo 23 | | # comment7 24 | | ) # comment8 | |_^ - | help: Use the `type` keyword | 15 | diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP042.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP042.py.snap index 2016e4aea4..dae543a0e8 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP042.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP042.py.snap @@ -6,7 +6,6 @@ UP042 [*] Class A inherits from both `str` and `enum.Enum` | 4 | class A(str, Enum): ... | ^ - | help: Inherit from `enum.StrEnum` | - from enum import Enum @@ -24,7 +23,6 @@ UP042 [*] Class B inherits from both `str` and `enum.Enum` | 7 | class B(Enum, str): ... | ^ - | help: Inherit from `enum.StrEnum` | - from enum import Enum @@ -43,7 +41,6 @@ UP042 Class D inherits from both `str` and `enum.Enum` | 10 | class D(int, str, Enum): ... | ^ - | help: Inherit from `enum.StrEnum` UP042 Class E inherits from both `str` and `enum.Enum` @@ -51,5 +48,4 @@ UP042 Class E inherits from both `str` and `enum.Enum` | 13 | class E(str, int, Enum): ... | ^ - | help: Inherit from `enum.StrEnum` diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP045_1.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP045_1.py.snap index 8dd4852157..55a22140f4 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP045_1.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP045_1.py.snap @@ -54,7 +54,6 @@ UP045 Use `X | None` for type annotations 14 | x: Optional[str] 15 | x = Optional[str] | ^^^^^^^^^^^^^ - | help: Convert to `X | None` UP045 [*] Use `X | None` for type annotations @@ -110,7 +109,6 @@ UP045 [*] Use `X | None` for type annotations 38 | | | list[ServiceSpecification] 39 | | ] = None | |_____^ - | help: Convert to `X | None` | 35 | class ServiceRefOrValue: @@ -129,7 +127,6 @@ UP045 [*] Use `X | None` for type annotations 43 | class ServiceRefOrValue: 44 | service_specification: Optional[str]is not True = None | ^^^^^^^^^^^^^ - | help: Convert to `X | None` | 43 | class ServiceRefOrValue: @@ -145,7 +142,6 @@ UP045 Use `X | None` for type annotations 48 | # Optional[None] should not be offered a fix 49 | foo: Optional[None] = None | ^^^^^^^^^^^^^^ - | help: Convert to `X | None` UP045 [*] Use `X | None` for type annotations @@ -225,7 +221,6 @@ UP045 [*] Use `X | None` for type annotations 77 | nested_optional_typing: typing.Optional[Optional[int]] = None 78 | triple_nested_optional: Optional[Optional[Optional[str]]] = None | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Convert to `X | None` | 77 | nested_optional_typing: typing.Optional[Optional[int]] = None @@ -241,7 +236,6 @@ UP045 [*] Use `X | None` for type annotations 77 | nested_optional_typing: typing.Optional[Optional[int]] = None 78 | triple_nested_optional: Optional[Optional[Optional[str]]] = None | ^^^^^^^^^^^^^^^^^^^^^^^ - | help: Convert to `X | None` | 77 | nested_optional_typing: typing.Optional[Optional[int]] = None @@ -257,7 +251,6 @@ UP045 [*] Use `X | None` for type annotations 77 | nested_optional_typing: typing.Optional[Optional[int]] = None 78 | triple_nested_optional: Optional[Optional[Optional[str]]] = None | ^^^^^^^^^^^^^ - | help: Convert to `X | None` | 77 | nested_optional_typing: typing.Optional[Optional[int]] = None @@ -275,7 +268,6 @@ UP045 [*] Use `X | None` for type annotations 83 | | # text 84 | | ] = None | |_^ - | help: Convert to `X | None` | 80 | @@ -366,5 +358,4 @@ UP045 Use `X | None` for type annotations 92 | bar: Optional[None | int | str] = None 93 | bar: Optional[None | None] = None | ^^^^^^^^^^^^^^^^^^^^^ - | help: Convert to `X | None` diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046_0.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046_0.py.snap index dc9ec51021..0dcc52d704 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046_0.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046_0.py.snap @@ -167,7 +167,6 @@ UP046 [*] Generic class `A` uses `Generic` subclass instead of type parameters | 73 | class A(Generic[T]): ... | ^^^^^^^^^^ - | help: Use type parameters | 72 | diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP049_1.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP049_1.py.snap index 3a59718287..36991658e0 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP049_1.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP049_1.py.snap @@ -203,7 +203,6 @@ UP049 Generic class uses private type parameters 38 | # offer a diagnostic 39 | class F[_async]: ... | ^^^^^^ - | help: Rename type parameter to remove leading underscores UP049 Generic class uses private type parameters @@ -223,7 +222,6 @@ UP049 Generic class uses private type parameters | 64 | class C[_0]: ... | ^^ - | help: Rename type parameter to remove leading underscores UP049 Generic class uses private type parameters @@ -241,7 +239,6 @@ UP049 Generic class uses private type parameters 67 | class C[T, _T]: ... 68 | class C[_T, T]: ... | ^^ - | help: Rename type parameter to remove leading underscores UP049 [*] Generic class uses private type parameters diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__add_future_annotation_UP037_0.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__add_future_annotation_UP037_0.py.snap index d0bce0c8a3..a9c1dba916 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__add_future_annotation_UP037_0.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__add_future_annotation_UP037_0.py.snap @@ -37,7 +37,6 @@ UP037 [*] Remove quotes from type annotation 18 | def foo(var: "MyClass") -> "MyClass": 19 | x: "MyClass" | ^^^^^^^^^ - | help: Remove quotes | 18 | def foo(var: "MyClass") -> "MyClass": @@ -114,7 +113,6 @@ UP037 [*] Remove quotes from type annotation 31 | 32 | x: Callable[["MyClass"], None] | ^^^^^^^^^ - | help: Remove quotes | 31 | @@ -129,7 +127,6 @@ UP037 [*] Remove quotes from type annotation 35 | class Foo(NamedTuple): 36 | x: "MyClass" | ^^^^^^^^^ - | help: Remove quotes | 35 | class Foo(NamedTuple): @@ -144,7 +141,6 @@ UP037 [*] Remove quotes from type annotation 39 | class D(TypedDict): 40 | E: TypedDict("E", foo="int", total=False) | ^^^^^ - | help: Remove quotes | 39 | class D(TypedDict): @@ -159,7 +155,6 @@ UP037 [*] Remove quotes from type annotation 43 | class D(TypedDict): 44 | E: TypedDict("E", {"foo": "int"}) | ^^^^^ - | help: Remove quotes | 43 | class D(TypedDict): @@ -559,7 +554,6 @@ UP037 [*] Remove quotes from type annotation 125 | def foo(bar: "A\n#"): ... 126 | def foo(bar: "A\n#\n"): ... | ^^^^^^^^ - | help: Remove quotes | 125 | def foo(bar: "A\n#"): ... diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__add_future_annotation_UP037_1.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__add_future_annotation_UP037_1.py.snap index 3e8587c3a4..09e59a880d 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__add_future_annotation_UP037_1.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__add_future_annotation_UP037_1.py.snap @@ -24,7 +24,6 @@ UP037 [*] Remove quotes from type annotation 13 | # OK 14 | X: "Tuple[int, int]" = (0, 0) | ^^^^^^^^^^^^^^^^^ - | help: Remove quotes | 13 | # OK diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__add_future_annotation_UP037_2.pyi.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__add_future_annotation_UP037_2.pyi.snap index 645b89f483..5ef03783a7 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__add_future_annotation_UP037_2.pyi.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__add_future_annotation_UP037_2.pyi.snap @@ -8,7 +8,6 @@ UP037 [*] Remove quotes from type annotation 2 | 3 | def f(a: Foo['SingleLine # Comment']): ... | ^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove quotes | 2 | @@ -26,7 +25,6 @@ UP037 [*] Remove quotes from type annotation 7 | | Multi | 8 | | Line]''']): ... | |____________^ - | help: Remove quotes | 5 | @@ -47,7 +45,6 @@ UP037 [*] Remove quotes from type annotation 13 | | Line # Comment 14 | | ]''']): ... | |____^ - | help: Remove quotes | 10 | @@ -68,7 +65,6 @@ UP037 [*] Remove quotes from type annotation 18 | | Multi | 19 | | Line] # Comment''']): ... | |_______________________^ - | help: Remove quotes | 16 | @@ -90,7 +86,6 @@ UP037 [*] Remove quotes from type annotation 24 | | Multi | 25 | | Line] # Comment''']): ... | |_______________________^ - | help: Remove quotes | 21 | @@ -111,7 +106,6 @@ UP037 [*] Remove quotes from type annotation | __________^ 29 | | ''' = []): ... | |_______^ - | help: Remove quotes | 27 | @@ -129,7 +123,6 @@ UP037 [*] Remove quotes from type annotation | ____^ 33 | | list[int]''' = [42] | |____________^ - | help: Remove quotes | 31 | @@ -148,7 +141,6 @@ UP037 [*] Remove quotes from type annotation 37 | | list[int] 38 | | ''' = []): ... | |_______^ - | help: Remove quotes | 35 | @@ -171,7 +163,6 @@ UP037 [*] Remove quotes from type annotation 45 | | Line 46 | | ] # Comment''']): ... | |___________________^ - | help: Remove quotes | 40 | @@ -194,7 +185,6 @@ UP037 [*] Remove quotes from type annotation | ____^ 50 | | [int]''' = [42] | |________^ - | help: Remove quotes | 48 | diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__datetime_utc_alias_py311.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__datetime_utc_alias_py311.snap index 557f768046..1759a22bd9 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__datetime_utc_alias_py311.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__datetime_utc_alias_py311.snap @@ -8,7 +8,6 @@ UP017 [*] Use `datetime.UTC` alias 9 | 10 | print(timezone.utc) | ^^^^^^^^^^^^ - | help: Convert to `datetime.UTC` alias | 1 + from datetime import UTC @@ -27,7 +26,6 @@ UP017 [*] Use `datetime.UTC` alias 15 | 16 | print(tz.utc) | ^^^^^^ - | help: Convert to `datetime.UTC` alias | 1 + from datetime import UTC @@ -46,7 +44,6 @@ UP017 [*] Use `datetime.UTC` alias 21 | 22 | print(datetime.timezone.utc) | ^^^^^^^^^^^^^^^^^^^^^ - | help: Convert to `datetime.UTC` alias | 21 | @@ -62,7 +59,6 @@ UP017 [*] Use `datetime.UTC` alias 27 | 28 | print(dt.timezone.utc) | ^^^^^^^^^^^^^^^ - | help: Convert to `datetime.UTC` alias | 27 | diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__future_annotations_keep_runtime_typing_p310.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__future_annotations_keep_runtime_typing_p310.snap index bc78e199be..408d6ee9cb 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__future_annotations_keep_runtime_typing_p310.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__future_annotations_keep_runtime_typing_p310.snap @@ -41,7 +41,6 @@ UP006 [*] Use `list` instead of `List` for type annotation 41 | 42 | MyList: TypeAlias = Union[List[int], List[str]] | ^^^^ - | help: Replace with `list` | 41 | @@ -56,7 +55,6 @@ UP006 [*] Use `list` instead of `List` for type annotation 41 | 42 | MyList: TypeAlias = Union[List[int], List[str]] | ^^^^ - | help: Replace with `list` | 41 | diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__future_annotations_pep_585_py310.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__future_annotations_pep_585_py310.snap index bc78e199be..408d6ee9cb 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__future_annotations_pep_585_py310.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__future_annotations_pep_585_py310.snap @@ -41,7 +41,6 @@ UP006 [*] Use `list` instead of `List` for type annotation 41 | 42 | MyList: TypeAlias = Union[List[int], List[str]] | ^^^^ - | help: Replace with `list` | 41 | @@ -56,7 +55,6 @@ UP006 [*] Use `list` instead of `List` for type annotation 41 | 42 | MyList: TypeAlias = Union[List[int], List[str]] | ^^^^ - | help: Replace with `list` | 41 | 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 1f3bf23257..f1291f2cf3 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 @@ -24,7 +24,6 @@ UP007 [*] Use `X | Y` for type annotations 41 | 42 | MyList: TypeAlias = Union[List[int], List[str]] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Convert to `X | Y` | 41 | diff --git a/crates/ruff_linter/src/rules/refurb/helpers.rs b/crates/ruff_linter/src/rules/refurb/helpers.rs index cdfc8f8049..0d2a451025 100644 --- a/crates/ruff_linter/src/rules/refurb/helpers.rs +++ b/crates/ruff_linter/src/rules/refurb/helpers.rs @@ -37,7 +37,7 @@ pub(super) fn generate_method_call(name: Name, method: &str, generator: Generato range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, is_cast: false, is_checked_cast: false, diff --git a/crates/ruff_linter/src/rules/refurb/mod.rs b/crates/ruff_linter/src/rules/refurb/mod.rs index 72a30f6760..a1338ad22b 100644 --- a/crates/ruff_linter/src/rules/refurb/mod.rs +++ b/crates/ruff_linter/src/rules/refurb/mod.rs @@ -53,6 +53,7 @@ mod tests { #[test_case(Rule::WriteWholeFile, Path::new("FURB103_2.py"))] #[test_case(Rule::FStringNumberFormat, Path::new("FURB116.py"))] #[test_case(Rule::SortedMinMax, Path::new("FURB192.py"))] + #[test_case(Rule::SortedMinMax, Path::new("FURB192_1.py"))] #[test_case(Rule::SliceToRemovePrefixOrSuffix, Path::new("FURB188.py"))] #[test_case(Rule::SubclassBuiltin, Path::new("FURB189.py"))] #[test_case(Rule::FromisoformatReplaceZ, Path::new("FURB162.py"))] 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 2230a35bb5..c74630b624 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 @@ -84,14 +84,20 @@ pub(crate) fn check_and_remove_from_set(checker: &Checker, if_stmt: &ast::StmtIf return; }; - // ` // `set` in the check should be the same as `set` in the body - if check_set.id != remove_set.id - // `element` in the check should be the same as `element` in the body - || !compare(&check_element.into(), &remove_element.into()) - // `element` shouldn't have a side effect, otherwise we might change the semantics of the program. - || contains_effect(check_element, |id| checker.semantic().has_builtin_binding(id)) - { + if check_set.id != remove_set.id { + return; + } + + // `element` in the check should be the same as `element` in the body + if !compare(&check_element.into(), &remove_element.into()) { + return; + } + + // `element` shouldn't have a side effect, otherwise we might change the semantics of the program. + if contains_effect(check_element, |id| { + checker.semantic().has_builtin_binding(id) + }) { return; } @@ -198,7 +204,7 @@ fn make_suggestion(set: &ast::ExprName, element: &Expr, generator: Generator) -> range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, is_cast: false, is_checked_cast: false, 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 ac06100b6f..c909bd044a 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,6 +1,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr, Operator}; use ruff_python_semantic::SemanticModel; +use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::rules::refurb::helpers::replace_with_identity_check; @@ -69,9 +70,9 @@ pub(crate) fn isinstance_type_none(checker: &Checker, call: &ast::ExprCall) { return; } - let fix = replace_with_identity_check(expr, call.range, false, checker); + let fix = replace_with_identity_check(expr, call.range(), false, checker); checker - .report_diagnostic(IsinstanceTypeNone, call.range) + .report_diagnostic(IsinstanceTypeNone, call.range()) .set_fix(fix); } 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 9e228bf483..ee803ba0e0 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,8 +1,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::helpers::{contains_effect, is_empty_f_string}; +use ruff_python_ast::helpers::is_empty_f_string; use ruff_python_ast::{self as ast, Expr}; use ruff_python_codegen::Generator; -use ruff_python_semantic::SemanticModel; use ruff_python_trivia::CommentRanges; use ruff_text_size::Ranged; @@ -33,8 +32,8 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// /// ## Fix safety /// This fix is marked as unsafe if it removes comments or an unused `sep` keyword argument -/// that may have side effects. Removing such arguments may change the program's -/// behavior by skipping the execution of those side effects. +/// that is not known to be a valid separator. Removing such arguments may change the +/// program's behavior by skipping their evaluation or hiding a `TypeError`. /// /// ## References /// - [Python documentation: `print`](https://docs.python.org/3/library/functions.html#print) @@ -98,7 +97,6 @@ pub(crate) fn print_empty_string(checker: &Checker, call: &ast::ExprCall) { EmptyStringFix::from_call( call, Separator::Remove, - checker.semantic(), checker.generator(), checker.comment_ranges(), ) @@ -125,7 +123,6 @@ pub(crate) fn print_empty_string(checker: &Checker, call: &ast::ExprCall) { EmptyStringFix::from_call( call, Separator::Remove, - checker.semantic(), checker.generator(), checker.comment_ranges(), ) @@ -191,7 +188,6 @@ pub(crate) fn print_empty_string(checker: &Checker, call: &ast::ExprCall) { EmptyStringFix::from_call( call, separator, - checker.semantic(), checker.generator(), checker.comment_ranges(), ) @@ -210,6 +206,10 @@ fn is_empty_string(expr: &Expr) -> bool { } } +fn is_known_valid_separator(expr: &Expr) -> bool { + expr.is_string_literal_expr() || expr.is_none_literal_expr() +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Separator { Remove, @@ -225,7 +225,6 @@ impl EmptyStringFix { fn from_call( call: &ast::ExprCall, separator: Separator, - semantic: &SemanticModel, generator: Generator, comment_ranges: &CommentRanges, ) -> Self { @@ -262,7 +261,7 @@ impl EmptyStringFix { return true; } - if contains_effect(&keyword.value, |id| semantic.has_builtin_binding(id)) { + if !is_known_valid_separator(&keyword.value) { applicability = Applicability::Unsafe; } 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 e2404cd5a9..5ef41828c0 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 @@ -183,7 +183,7 @@ fn make_suggestion(open: &FileOpen<'_>, generator: Generator) -> String { range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, is_cast: false, is_checked_cast: false, 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 cecee59c6c..0054a9eabd 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/reimplemented_starmap.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/reimplemented_starmap.rs @@ -212,7 +212,7 @@ impl Ranged for StarmapCandidate<'_> { impl StarmapCandidate<'_> { /// Return the generated element for the candidate. - pub(crate) fn element(&self) -> &Expr { + fn element(&self) -> &Expr { match self { Self::Generator(generator) => generator.elt.as_ref(), Self::ListComp(list_comp) => list_comp.elt.as_ref(), @@ -221,7 +221,7 @@ impl StarmapCandidate<'_> { } /// Return the generator comprehensions for the candidate. - pub(crate) fn generators(&self) -> &[ast::Comprehension] { + fn generators(&self) -> &[ast::Comprehension] { match self { Self::Generator(generator) => generator.generators.as_slice(), Self::ListComp(list_comp) => list_comp.generators.as_slice(), @@ -230,7 +230,7 @@ impl StarmapCandidate<'_> { } /// Try to produce a fix suggestion transforming this node into a call to `starmap`. - pub(crate) fn try_make_suggestion( + fn try_make_suggestion( &self, name: Name, iter: &Expr, @@ -324,7 +324,7 @@ fn construct_starmap_call(starmap_binding: Name, iter: &Expr, func: &Expr) -> as range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, is_cast: false, is_checked_cast: false, @@ -348,7 +348,7 @@ fn wrap_with_call_to(call: ast::ExprCall, func_name: Name) -> ast::ExprCall { range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, is_cast: false, is_checked_cast: false, 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 8f078fc62e..27872dddd8 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/repeated_append.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/repeated_append.rs @@ -369,7 +369,7 @@ fn make_suggestion(group: &AppendGroup, generator: Generator) -> String { range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, is_cast: false, is_checked_cast: false, 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 b9f40acc74..9754f7d50b 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 @@ -126,7 +126,7 @@ fn single_item<'a>(expr: &'a Expr, semantic: &'a SemanticModel) -> Option<&'a Ex Expr::Call(ast::ExprCall { func, arguments, - range: _, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, 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 a729a761b0..f77c3a8cab 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 @@ -362,7 +362,7 @@ fn affix_matches_slice_bound(data: &RemoveAffixData, semantic: &SemanticModel) - ( AffixKind::StartsWith, ast::Expr::Call(ast::ExprCall { - range: _, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, @@ -414,7 +414,7 @@ fn affix_matches_slice_bound(data: &RemoveAffixData, semantic: &SemanticModel) - _, ) => operand.as_call_expr().is_some_and( |ast::ExprCall { - range: _, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, 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 feb1670b20..5a3f8ad36c 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,5 +1,6 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::Number; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::{self as ast, Expr}; use ruff_text_size::Ranged; @@ -52,7 +53,7 @@ use crate::checkers::ast::Checker; /// - [Python documentation: `min`](https://docs.python.org/3/library/functions.html#min) /// - [Python documentation: `max`](https://docs.python.org/3/library/functions.html#max) #[derive(ViolationMetadata)] -#[violation_metadata(preview_since = "v0.4.2")] +#[violation_metadata(stable_since = "0.16.0")] pub(crate) struct SortedMinMax { min_max: MinMax, } @@ -187,14 +188,17 @@ pub(crate) fn sorted_min_max(checker: &Checker, subscript: &ast::ExprSubscript) if checker.semantic().has_builtin_binding(min_max.as_str()) { diagnostic.set_fix({ + // Preserve any parentheses around the argument. Some expressions are + // only valid as a call argument when parenthesized (e.g., `yield`), + // so slicing the bare node would produce invalid syntax. + let list_expr = checker.locator().slice( + parenthesized_range(list_expr.into(), arguments.into(), checker.tokens()) + .unwrap_or(list_expr.range()), + ); let replacement = if let Some(key) = key_keyword_expr { - format!( - "{min_max}({}, {})", - checker.locator().slice(list_expr), - checker.locator().slice(key), - ) + format!("{min_max}({list_expr}, {})", checker.locator().slice(key)) } else { - format!("{min_max}({})", checker.locator().slice(list_expr)) + format!("{min_max}({list_expr})") }; let replacement = Edit::range_replacement(replacement, subscript.range()); 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 af516c2f76..486b4c7eda 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/unnecessary_enumerate.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/unnecessary_enumerate.rs @@ -252,7 +252,7 @@ fn generate_range_len_call(name: Name, generator: Generator) -> String { range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, is_cast: false, is_checked_cast: false, @@ -275,7 +275,7 @@ fn generate_range_len_call(name: Name, generator: Generator) -> String { range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, is_cast: false, is_checked_cast: false, 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 a60a378c66..734618392b 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 @@ -61,7 +61,7 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// - [Python documentation: `decimal`](https://docs.python.org/3/library/decimal.html) /// - [Python documentation: `fractions`](https://docs.python.org/3/library/fractions.html) #[derive(ViolationMetadata)] -#[violation_metadata(preview_since = "v0.3.5")] +#[violation_metadata(stable_since = "0.16.0")] pub(crate) struct UnnecessaryFromFloat { method_name: MethodName, constructor: Constructor, @@ -134,6 +134,7 @@ pub(crate) fn unnecessary_from_float(checker: &Checker, call: &ExprCall) { }; let constructor_name = checker.locator().slice(&**value).to_string(); + let has_comments = checker.comment_ranges().intersects(call.range()); // Special case for non-finite float literals: Decimal.from_float(float("inf")) -> Decimal("inf") if let Some(replacement) = handle_non_finite_float_special_case( @@ -144,7 +145,14 @@ pub(crate) fn unnecessary_from_float(checker: &Checker, call: &ExprCall) { &constructor_name, checker, ) { - diagnostic.set_fix(Fix::safe_edit(replacement)); + diagnostic.set_fix(Fix::applicable_edit( + replacement, + if has_comments { + Applicability::Unsafe + } else { + Applicability::Safe + }, + )); return; } @@ -152,7 +160,7 @@ pub(crate) fn unnecessary_from_float(checker: &Checker, call: &ExprCall) { let is_type_safe = is_valid_argument_type(arg_value, method_name, constructor, checker); // Determine fix safety - let applicability = if is_type_safe && !checker.comment_ranges().intersects(call.range()) { + let applicability = if is_type_safe && !has_comments { Applicability::Safe } else { Applicability::Unsafe diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB105_FURB105.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB105_FURB105.py.snap index 15b5cea41c..3b5583ab03 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB105_FURB105.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB105_FURB105.py.snap @@ -351,6 +351,7 @@ FURB105 [*] Unnecessary empty string and separator passed to `print` 23 | print(f"", sep=",") | ^^^^^^^^^^^^^^^^^^^ 24 | print(f"", end="bar") +25 | print(1, sep=None) | help: Remove empty string and separator | @@ -367,33 +368,67 @@ FURB105 [*] Unnecessary empty string passed to `print` 23 | print(f"", sep=",") 24 | print(f"", end="bar") | ^^^^^^^^^^^^^^^^^^^^^ -25 | -26 | # OK. +25 | print(1, sep=None) | help: Remove empty string | 23 | print(f"", sep=",") - print(f"", end="bar") 24 + print(end="bar") -25 | +25 | print(1, sep=None) | +FURB105 [*] Unnecessary separator passed to `print` + --> FURB105.py:25:1 + | +23 | print(f"", sep=",") +24 | print(f"", end="bar") +25 | print(1, sep=None) + | ^^^^^^^^^^^^^^^^^^ +26 | +27 | def p(sep): + | +help: Remove separator + | +24 | print(f"", end="bar") + - print(1, sep=None) +25 + print(1) +26 | + | + +FURB105 [*] Unnecessary separator passed to `print` + --> FURB105.py:28:5 + | +27 | def p(sep): +28 | print(1, sep=sep) + | ^^^^^^^^^^^^^^^^^ +29 | +30 | # OK. + | +help: Remove separator + | +27 | def p(sep): + - print(1, sep=sep) +28 + print(1) +29 | + | +note: This is an unsafe fix and may change runtime behavior + FURB105 [*] Unnecessary empty string passed to `print` - --> FURB105.py:42:1 + --> FURB105.py:46:1 | -42 | / print( -43 | | # text -44 | | "" -45 | | ) +46 | / print( +47 | | # text +48 | | "" +49 | | ) | |_^ - | help: Remove empty string | -41 | +45 | - print( - # text - "" - ) -42 + print() +46 + print() | note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB113_FURB113.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB113_FURB113.py.snap index c096f7886c..346998d67e 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB113_FURB113.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB113_FURB113.py.snap @@ -132,7 +132,6 @@ FURB113 [*] Use `nums.extend((1, 2, 3))` instead of repeatedly calling `nums.app 63 | | nums.append(2) 64 | | nums.append(3) | |______________^ - | help: Replace with `nums.extend((1, 2, 3))` | 61 | # FURB113 @@ -152,7 +151,6 @@ FURB113 [*] Use `nums.extend((1, 2))` instead of repeatedly calling `nums.append 69 | / nums.append(1) 70 | | nums.append(2) | |__________________^ - | help: Replace with `nums.extend((1, 2))` | 68 | # FURB113 @@ -193,7 +191,6 @@ FURB113 Use `nums.extend((1, 2, 3))` instead of repeatedly calling `nums.append( 84 | | nums.append(2) 85 | | nums.append(3) | |__________________^ - | help: Replace with `nums.extend((1, 2, 3))` FURB113 [*] Use `x.extend((1, 2))` instead of repeatedly calling `x.append()` @@ -204,7 +201,6 @@ FURB113 [*] Use `x.extend((1, 2))` instead of repeatedly calling `x.append()` 90 | / x.append(1) 91 | | x.append(2) | |_______________^ - | help: Replace with `x.extend((1, 2))` | 89 | # FURB113 @@ -223,7 +219,6 @@ FURB113 [*] Use `x.extend((1, 2))` instead of repeatedly calling `x.append()` 96 | / x.append(1) 97 | | x.append(2) | |_______________^ - | help: Replace with `x.extend((1, 2))` | 95 | # FURB113 @@ -242,7 +237,6 @@ FURB113 [*] Use `x.extend((1, 2))` instead of repeatedly calling `x.append()` 102 | / x.append(1) 103 | | x.append(2) | |_______________^ - | help: Replace with `x.extend((1, 2))` | 101 | # FURB113 @@ -261,7 +255,6 @@ FURB113 [*] Use `x.extend((1, 2))` instead of repeatedly calling `x.append()` 108 | / x.append(1) 109 | | x.append(2) | |_______________^ - | help: Replace with `x.extend((1, 2))` | 107 | # FURB113 @@ -282,7 +275,6 @@ FURB113 Use `x.extend((1, 2, 3))` instead of repeatedly calling `x.append()` 116 | | y.append(1) 117 | | x.append(3) | |_______________^ - | help: Replace with `x.extend((1, 2, 3))` FURB113 [*] Use `x.extend((1, 2))` instead of repeatedly calling `x.append()` @@ -293,7 +285,6 @@ FURB113 [*] Use `x.extend((1, 2))` instead of repeatedly calling `x.append()` 122 | / x.append(1) 123 | | x.append(2) | |_______________^ - | help: Replace with `x.extend((1, 2))` | 121 | # FURB113 @@ -315,5 +306,4 @@ FURB113 Use `nums.extend((1, 2, 3))` instead of repeatedly calling `nums.append( 131 | | # comment 132 | | nums.append(3) | |__________________^ - | help: Replace with `nums.extend((1, 2, 3))` 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 7b038caeef..fe95ed16e7 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 @@ -252,7 +252,6 @@ FURB116 [*] Replace `bin` call with `f"{-1:b}"` 43 | # for negatives numbers autofix is display-only 44 | print(bin(-1)[2:]) | ^^^^^^^^^^^ - | help: Replace with `f"{-1:b}"` | 43 | # for negatives numbers autofix is display-only diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB118_FURB118.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB118_FURB118.py.snap index fc66bbb6d3..306415bd82 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB118_FURB118.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB118_FURB118.py.snap @@ -709,7 +709,6 @@ FURB118 [*] Use `operator.itemgetter((0, 1))` instead of defining a lambda 34 | op_itemgetter = lambda x: x[0, 1] 35 | op_itemgetter = lambda x: x[(0, 1)] | ^^^^^^^^^^^^^^^^^^^ - | help: Replace with `operator.itemgetter((0, 1))` | 1 | # Errors. @@ -813,7 +812,6 @@ FURB118 [*] Use `operator.itemgetter((1, 2))` instead of defining a lam 94 | # Without a slice, trivia is retained 95 | op_itemgetter = lambda x: x[1, 2] | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `operator.itemgetter((1, 2))` | 1 | # Errors. 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 78c5e04094..126ca05574 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 @@ -9,7 +9,6 @@ FURB122 [*] Use of `f.write` in a for loop 10 | / for line in lines: 11 | | f.write(line) | |_________________________^ - | help: Replace with `f.writelines` | 9 | with open("file", "w") as f: @@ -27,7 +26,6 @@ FURB122 [*] Use of `f.write` in a for loop 17 | / for line in lines: 18 | | f.write(other_line) | |_______________________________^ - | help: Replace with `f.writelines` | 16 | with Path("file").open("w") as f: @@ -45,7 +43,6 @@ FURB122 [*] Use of `f.write` in a for loop 23 | / for line in lines: 24 | | f.write(line) | |_________________________^ - | help: Replace with `f.writelines` | 22 | with Path("file").open("w") as f: @@ -63,7 +60,6 @@ FURB122 [*] Use of `f.write` in a for loop 29 | / for line in lines: 30 | | f.write(line.encode()) | |__________________________________^ - | help: Replace with `f.writelines` | 28 | with Path("file").open("wb") as f: @@ -81,7 +77,6 @@ FURB122 [*] Use of `f.write` in a for loop 35 | / for line in lines: 36 | | f.write(line.upper()) | |_________________________________^ - | help: Replace with `f.writelines` | 34 | with Path("file").open("w") as f: @@ -99,7 +94,6 @@ FURB122 [*] Use of `f.write` in a for loop 43 | / for line in lines: 44 | | f.write(line) | |_________________________^ - | help: Replace with `f.writelines` | 42 | @@ -118,7 +112,6 @@ FURB122 [*] Use of `f.write` in a for loop 51 | | # a really important comment 52 | | f.write(line) | |_________________________^ - | help: Replace with `f.writelines` | 49 | with open("file","w") as f: @@ -138,7 +131,6 @@ FURB122 [*] Use of `f.write` in a for loop 57 | / for () in a: 58 | | f.write(()) | |_______________________^ - | help: Replace with `f.writelines` | 56 | with open("file", "w") as f: @@ -156,7 +148,6 @@ FURB122 [*] Use of `f.write` in a for loop 63 | / for a, b, c in d: 64 | | f.write((a, b)) | |___________________________^ - | help: Replace with `f.writelines` | 62 | with open("file", "w") as f: @@ -174,7 +165,6 @@ FURB122 [*] Use of `f.write` in a for loop 69 | / for [(), [a.b], (c,)] in d: 70 | | f.write(()) | |_______________________^ - | help: Replace with `f.writelines` | 68 | with open("file", "w") as f: @@ -192,7 +182,6 @@ FURB122 [*] Use of `f.write` in a for loop 75 | / for [([([a[b]],)],), [], (c[d],)] in e: 76 | | f.write(()) | |_______________________^ - | help: Replace with `f.writelines` | 74 | with open("file", "w") as f: @@ -253,7 +242,6 @@ FURB122 [*] Use of `f.write` in a for loop 96 | | ): 97 | | f.write(f"{char}") | |______________________________^ - | help: Replace with `f.writelines` | 92 | with open("file", "w") as f: @@ -276,7 +264,6 @@ FURB122 [*] Use of `f.write` in a for loop 183 | / for l in lambda: 0: 184 | | f.write(f"[{l}]") | |_____________________________^ - | help: Replace with `f.writelines` | 182 | with Path("file.txt").open("w", encoding="utf-8") as f: @@ -294,7 +281,6 @@ FURB122 [*] Use of `f.write` in a for loop 189 | / for l in (1,) if True else (2,): 190 | | f.write(f"[{l}]") | |_____________________________^ - | help: Replace with `f.writelines` | 188 | with Path("file.txt").open("w", encoding="utf-8") as f: @@ -312,7 +298,6 @@ FURB122 [*] Use of `f.write` in a for loop 196 | / for line in lambda: 0: 197 | | f.write(line) | |_________________________^ - | help: Replace with `f.writelines` | 195 | with open("file", "w") as f: @@ -330,7 +315,6 @@ FURB122 [*] Use of `f.write` in a for loop 202 | / for line in (1,) if True else (2,): 203 | | f.write(line) | |_________________________^ - | help: Replace with `f.writelines` | 201 | with open("file", "w") as f: @@ -348,7 +332,6 @@ FURB122 [*] Use of `f.write` in a for loop 209 | / for line in (lambda: 0): 210 | | f.write(f"{line}") | |______________________________^ - | help: Replace with `f.writelines` | 208 | with open("file", "w") as f: @@ -366,7 +349,6 @@ FURB122 [*] Use of `f.write` in a for loop 215 | / for line in ((1,) if True else (2,)): 216 | | f.write(f"{line}") | |______________________________^ - | help: Replace with `f.writelines` | 214 | with open("file", "w") as f: 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 377dd38e57..8adcea8b87 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 @@ -229,7 +229,6 @@ FURB129 [*] Instead of calling `readlines()`, iterate over file object directly 96 | with open("furb129.py") as f: 97 | [line for line in (f).readlines()] | ^^^^^^^^^^^^^^^ - | help: Remove `readlines()` | 96 | with open("furb129.py") as f: diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB131_FURB131.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB131_FURB131.py.snap index 547da9b559..eed82175cc 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB131_FURB131.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB131_FURB131.py.snap @@ -7,7 +7,6 @@ FURB131 [*] Prefer `clear` over deleting a full slice 10 | # FURB131 11 | del nums[:] | ^^^^^^^^^^^ - | help: Replace with `clear()` | 10 | # FURB131 @@ -23,7 +22,6 @@ FURB131 [*] Prefer `clear` over deleting a full slice 14 | # FURB131 15 | del names[:] | ^^^^^^^^^^^^ - | help: Replace with `clear()` | 14 | # FURB131 @@ -39,7 +37,6 @@ FURB131 Prefer `clear` over deleting a full slice 18 | # FURB131 19 | del x, nums[:] | ^^^^^^^^^^^^^^ - | help: Replace with `clear()` FURB131 Prefer `clear` over deleting a full slice @@ -48,7 +45,6 @@ FURB131 Prefer `clear` over deleting a full slice 22 | # FURB131 23 | del y, names[:], x | ^^^^^^^^^^^^^^^^^^ - | help: Replace with `clear()` FURB131 [*] Prefer `clear` over deleting a full slice @@ -58,7 +54,6 @@ FURB131 [*] Prefer `clear` over deleting a full slice 27 | # FURB131 28 | del x[:] | ^^^^^^^^ - | help: Replace with `clear()` | 27 | # FURB131 @@ -75,7 +70,6 @@ FURB131 [*] Prefer `clear` over deleting a full slice 32 | # FURB131 33 | del x[:] | ^^^^^^^^ - | help: Replace with `clear()` | 32 | # FURB131 @@ -92,7 +86,6 @@ FURB131 [*] Prefer `clear` over deleting a full slice 37 | # FURB131 38 | del x[:] | ^^^^^^^^ - | help: Replace with `clear()` | 37 | # FURB131 @@ -109,7 +102,6 @@ FURB131 [*] Prefer `clear` over deleting a full slice 42 | # FURB131 43 | del x[:] | ^^^^^^^^ - | help: Replace with `clear()` | 42 | # FURB131 diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB132_FURB132.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB132_FURB132.py.snap index d58bfdb288..e7e76f38b7 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB132_FURB132.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB132_FURB132.py.snap @@ -8,7 +8,6 @@ FURB132 [*] Use `s.discard("x")` instead of check and `remove` 12 | / if "x" in s: 13 | | s.remove("x") | |_________________^ - | help: Replace with `s.discard("x")` | 11 | # FURB132 @@ -26,7 +25,6 @@ FURB132 [*] Use `s3.discard("x")` instead of check and `remove` 22 | / if "x" in s3: 23 | | s3.remove("x") | |__________________^ - | help: Replace with `s3.discard("x")` | 21 | # FURB132 @@ -45,7 +43,6 @@ FURB132 [*] Use `s.discard(var)` instead of check and `remove` 28 | / if var in s: 29 | | s.remove(var) | |_________________^ - | help: Replace with `s.discard(var)` | 27 | # FURB132 @@ -62,7 +59,6 @@ FURB132 [*] Use `s.discard(f"{var}:{var}")` instead of check and `remove` 32 | / if f"{var}:{var}" in s: 33 | | s.remove(f"{var}:{var}") | |____________________________^ - | help: Replace with `s.discard(f"{var}:{var}")` | 31 | diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB136_FURB136.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB136_FURB136.py.snap index 3cb5077164..64b885d173 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB136_FURB136.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB136_FURB136.py.snap @@ -155,7 +155,6 @@ FURB136 [*] Replace `if` expression with `max(y, x)` 24 | | > y 25 | | ) else y # FURB136 | |________^ - | help: Replace with `max(y, x)` | 21 | diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB140_FURB140.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB140_FURB140.py.snap index ad1625f575..85c6cb9a13 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB140_FURB140.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB140_FURB140.py.snap @@ -47,7 +47,6 @@ FURB140 [*] Use `itertools.starmap` instead of the generator 12 | # FURB140 13 | {print(x, y) for x, y in zipped()} | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `itertools.starmap` | 1 + from itertools import starmap 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 8f8e6b03d1..3ed7c5780d 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 @@ -231,7 +231,6 @@ FURB142 [*] Use of `set.add()` in a for loop 44 | | ): 45 | | s.add(f"{x}") | |_________________^ - | help: Replace with `.update()` | 40 | @@ -390,7 +389,6 @@ FURB142 [*] Use of `set.add()` in a for loop 108 | / for x in ("abc", "def"): 109 | | s.add((c for c in x)) | |_________________________^ - | help: Replace with `.update()` | 107 | # don't add extra parens for already parenthesized generators diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB145_FURB145.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB145_FURB145.py.snap index a0e851b790..e0386246b4 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB145_FURB145.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB145_FURB145.py.snap @@ -116,7 +116,6 @@ FURB145 [*] Prefer `copy` method over slicing 26 | | : 27 | | ] | |_^ - | help: Replace with `copy()` | 23 | diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB152_FURB152.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB152_FURB152.py.snap index 2402ae2e96..e8285f3b89 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB152_FURB152.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB152_FURB152.py.snap @@ -302,7 +302,6 @@ FURB152 [*] Replace `2.7182000000000001` with `math.e` 44 | 45 | e = 2.7182000000000001 # FURB152 | ^^^^^^^^^^^^^^^^^^ - | help: Use `math.e` | 1 + import math diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB154_FURB154.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB154_FURB154.py.snap index d5c1416d79..61de91d468 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB154_FURB154.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB154_FURB154.py.snap @@ -8,7 +8,6 @@ FURB154 [*] Use of repeated consecutive `global` 4 | / global x 5 | | global y | |____________^ - | help: Merge `global` statements | 3 | def f1(): @@ -26,7 +25,6 @@ FURB154 [*] Use of repeated consecutive `global` 10 | | global y 11 | | global z | |____________^ - | help: Merge `global` statements | 8 | def f3(): @@ -64,7 +62,6 @@ FURB154 [*] Use of repeated consecutive `global` 18 | / global x 19 | | global y | |____________^ - | help: Merge `global` statements | 17 | pass @@ -141,7 +138,6 @@ FURB154 [*] Use of repeated consecutive `nonlocal` 38 | / nonlocal x 39 | | nonlocal y | |__________________^ - | help: Merge `nonlocal` statements | 37 | pass @@ -198,7 +194,6 @@ FURB154 [*] Use of repeated consecutive `nonlocal` 53 | / nonlocal y 54 | | nonlocal z | |__________________^ - | help: Merge `nonlocal` statements | 52 | global x @@ -216,7 +211,6 @@ FURB154 [*] Use of repeated consecutive `global` 59 | | global a, b, c 60 | | global d, e, f | |__________________^ - | help: Merge `global` statements | 57 | def f6(): diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB157_FURB157.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB157_FURB157.py.snap index 34b408d739..b23d38312d 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB157_FURB157.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB157_FURB157.py.snap @@ -680,7 +680,6 @@ FURB157 [*] Verbose expression in `Decimal` constructor 92 | Decimal("_+1") # Should flag as verbose 93 | Decimal("_-1_000") # Should flag as verbose | ^^^^^^^^^ - | help: Replace with `-1_000` | 92 | Decimal("_+1") # Should flag as verbose diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB162_FURB162.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB162_FURB162.py.snap index eb507c8a99..4b040a8bb3 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB162_FURB162.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB162_FURB162.py.snap @@ -208,7 +208,6 @@ FURB162 [*] Unnecessary timezone replacement with zero offset 51 | # Edge case 52 | datetime.fromisoformat("Z2025-01-01T00:00:00Z".strip("Z") + "+00:00") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove `.replace()` call | 51 | # Edge case 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 8c84dd51fe..05dc04cac2 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 @@ -156,7 +156,6 @@ FURB163 [*] Prefer `math.log(yield)` over `math.log` with a redundant base 48 | def log(): 49 | yield math.log((yield), math.e) | ^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `math.log(yield)` | 48 | def log(): @@ -301,7 +300,6 @@ 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) | ^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `math.log10(4.14e223)` | 74 | math.log(4.13e223, 2) 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 313bd5ff97..12431abb9b 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 @@ -687,3 +687,25 @@ help: Replace with `Fraction` constructor 75 | ) | note: This is an unsafe fix and may change runtime behavior + +FURB164 [*] Verbose method `from_float` in `Decimal` construction + --> FURB164.py:79:5 + | +77 | ) +78 | +79 | _ = Decimal.from_float( + | _____^ +80 | | # keep this comment +81 | | float("inf") +82 | | ) + | |_^ +help: Replace with `Decimal` constructor + | +78 | + - _ = Decimal.from_float( + - # keep this comment + - float("inf") + - ) +79 + _ = Decimal("inf") + | +note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB166_FURB166.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB166_FURB166.py.snap index f87a780a89..b21c9ec5c1 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB166_FURB166.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB166_FURB166.py.snap @@ -100,7 +100,6 @@ FURB166 [*] Use of `int` with explicit `base=16` after removing prefix 11 | 12 | _ = int(b"0xFFFF"[2:], 16) | ^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `base=0` | 11 | diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB169_FURB169.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB169_FURB169.py.snap index 43cbe9f577..ba454f05e7 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB169_FURB169.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB169_FURB169.py.snap @@ -293,7 +293,6 @@ FURB169 [*] When checking against `None`, use `is not` instead of comparison wit 42 | | a for a in range(0) 43 | | ) is not type(None) | |___________________^ - | help: Replace with `is not None` | 40 | diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB187_FURB187.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB187_FURB187.py.snap index 662129dd06..24f1a54ecc 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB187_FURB187.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB187_FURB187.py.snap @@ -8,7 +8,6 @@ FURB187 [*] Use of assignment of `reversed` on list `l` 5 | l = [] 6 | l = reversed(l) | ^^^^^^^^^^^^^^^ - | help: Replace with `l.reverse()` | 5 | l = [] @@ -25,7 +24,6 @@ FURB187 [*] Use of assignment of `reversed` on list `l` 10 | l = [] 11 | l = list(reversed(l)) | ^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `l.reverse()` | 10 | l = [] @@ -42,7 +40,6 @@ FURB187 [*] Use of assignment of `reversed` on list `l` 15 | l = [] 16 | l = l[::-1] | ^^^^^^^^^^^ - | help: Replace with `l.reverse()` | 15 | l = [] diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB188_FURB188.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB188_FURB188.py.snap index c260010e79..67c26ad968 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB188_FURB188.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB188_FURB188.py.snap @@ -45,7 +45,6 @@ FURB188 [*] Prefer `str.removesuffix()` over conditionally replacing with slice. 20 | def remove_extension_via_ternary(filename: str) -> str: 21 | return filename[:-4] if filename.endswith(".txt") else filename | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use removesuffix instead of ternary expression conditional upon endswith. | 20 | def remove_extension_via_ternary(filename: str) -> str: @@ -60,7 +59,6 @@ FURB188 [*] Prefer `str.removesuffix()` over conditionally replacing with slice. 24 | def remove_extension_via_ternary_with_len(filename: str, extension: str) -> str: 25 | return filename[:-len(extension)] if filename.endswith(extension) else filename | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use removesuffix instead of ternary expression conditional upon endswith. | 24 | def remove_extension_via_ternary_with_len(filename: str, extension: str) -> str: @@ -75,7 +73,6 @@ FURB188 [*] Prefer `str.removeprefix()` over conditionally replacing with slice. 28 | def remove_prefix(filename: str) -> str: 29 | return filename[4:] if filename.startswith("abc-") else filename | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use removeprefix instead of ternary expression conditional upon startswith. | 28 | def remove_prefix(filename: str) -> str: @@ -90,7 +87,6 @@ FURB188 [*] Prefer `str.removeprefix()` over conditionally replacing with slice. 32 | def remove_prefix_via_len(filename: str, prefix: str) -> str: 33 | return filename[len(prefix):] if filename.startswith(prefix) else filename | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use removeprefix instead of ternary expression conditional upon startswith. | 32 | def remove_prefix_via_len(filename: str, prefix: str) -> str: @@ -219,7 +215,6 @@ FURB188 [*] Prefer `str.removeprefix()` over conditionally replacing with slice. 183 | / if text.startswith("ř"): 184 | | text = text[1:] | |_______________________^ - | help: Use removeprefix instead of assignment conditional upon startswith. | 182 | text = "řetězec" diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB192_FURB192_1.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB192_FURB192_1.py.snap new file mode 100644 index 0000000000..5e5ec280ff --- /dev/null +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB192_FURB192_1.py.snap @@ -0,0 +1,92 @@ +--- +source: crates/ruff_linter/src/rules/refurb/mod.rs +--- +FURB192 [*] Prefer `min` over `sorted()` to compute the minimum value in a sequence + --> FURB192_1.py:9:5 + | + 8 | def f(l, key_fn): + 9 | sorted((yield))[0] + | ^^^^^^^^^^^^^^^^^^ +10 | +11 | sorted((yield l))[-1] + | +help: Replace with `min` + | +8 | def f(l, key_fn): + - sorted((yield))[0] +9 + min((yield)) +10 | + | +note: This is an unsafe fix and may change runtime behavior + +FURB192 [*] Prefer `max` over `sorted()` to compute the maximum value in a sequence + --> FURB192_1.py:11:5 + | + 9 | sorted((yield))[0] +10 | +11 | sorted((yield l))[-1] + | ^^^^^^^^^^^^^^^^^^^^^ +12 | +13 | sorted((yield), key=key_fn)[0] + | +help: Replace with `max` + | +10 | + - sorted((yield l))[-1] +11 + max((yield l)) +12 | + | +note: This is an unsafe fix and may change runtime behavior + +FURB192 [*] Prefer `min` over `sorted()` to compute the minimum value in a sequence + --> FURB192_1.py:13:5 + | +11 | sorted((yield l))[-1] +12 | +13 | sorted((yield), key=key_fn)[0] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +14 | +15 | sorted((yield from l))[0] + | +help: Replace with `min` + | +12 | + - sorted((yield), key=key_fn)[0] +13 + min((yield), key=key_fn) +14 | + | +note: This is an unsafe fix and may change runtime behavior + +FURB192 [*] Prefer `min` over `sorted()` to compute the minimum value in a sequence + --> FURB192_1.py:15:5 + | +13 | sorted((yield), key=key_fn)[0] +14 | +15 | sorted((yield from l))[0] + | ^^^^^^^^^^^^^^^^^^^^^^^^^ +16 | +17 | sorted((yield), reverse=True)[-1] + | +help: Replace with `min` + | +14 | + - sorted((yield from l))[0] +15 + min((yield from l)) +16 | + | +note: This is an unsafe fix and may change runtime behavior + +FURB192 [*] Prefer `min` over `sorted()` to compute the minimum value in a sequence + --> FURB192_1.py:17:5 + | +15 | sorted((yield from l))[0] +16 | +17 | sorted((yield), reverse=True)[-1] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +help: Replace with `min` + | +16 | + - sorted((yield), reverse=True)[-1] +17 + min((yield)) + | +note: This is an unsafe fix and may change runtime behavior 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 index 25c70b7636..830f08dc4f 100644 --- 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 @@ -229,7 +229,6 @@ FURB116 [*] Replace `bin` call with `f"{-1:b}"` 43 | # for negatives numbers autofix is display-only 44 | print(bin(-1)[2:]) | ^^^^^^^^^^^ - | help: Replace with `f"{-1:b}"` | 43 | # for negatives numbers autofix is display-only diff --git a/crates/ruff_linter/src/rules/ruff/helpers.rs b/crates/ruff_linter/src/rules/ruff/helpers.rs index a94ece694c..ba28950a92 100644 --- a/crates/ruff_linter/src/rules/ruff/helpers.rs +++ b/crates/ruff_linter/src/rules/ruff/helpers.rs @@ -30,10 +30,10 @@ fn is_attrs_field(func: &Expr, semantic: &SemanticModel) -> bool { semantic .resolve_qualified_name(func) .is_some_and(|qualified_name| { + // See https://github.com/python-attrs/attrs/blob/main/src/attr/__init__.py#L33 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"] ) }) @@ -121,8 +121,8 @@ pub(super) fn dataclass_kind<'a>( }; match qualified_name.segments() { - ["attrs" | "attr", func @ ("define" | "frozen" | "mutable")] // See https://github.com/python-attrs/attrs/blob/main/src/attr/__init__.py#L32 + ["attrs" | "attr", func @ ("define" | "frozen" | "mutable")] | ["attr", func @ ("s" | "attributes" | "attrs")] => { // `.define`, `.frozen` and `.mutable` all default `auto_attribs` to `None`, // whereas `@attr.s` implicitly sets `auto_attribs=False`. diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index 415bc92a88..c95095dca1 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -243,14 +243,10 @@ mod tests { fn missing_fstring_syntax_backslash_py311() -> Result<()> { assert_diagnostics_diff!( Path::new("ruff/RUF027_0.py"), - &LinterSettings { - unresolved_target_version: PythonVersion::PY312.into(), - ..LinterSettings::for_rule(Rule::MissingFStringSyntax) - }, - &LinterSettings { - unresolved_target_version: PythonVersion::PY311.into(), - ..LinterSettings::for_rule(Rule::MissingFStringSyntax) - }, + &LinterSettings::for_rule(Rule::MissingFStringSyntax) + .with_target_version(PythonVersion::PY312), + &LinterSettings::for_rule(Rule::MissingFStringSyntax) + .with_target_version(PythonVersion::PY311), ); Ok(()) } @@ -353,10 +349,8 @@ mod tests { print(None | (int)and 2) ", - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY313.into(), - ..settings::LinterSettings::for_rule(Rule::NoneNotAtEndOfUnion) - }, + &settings::LinterSettings::for_rule(Rule::NoneNotAtEndOfUnion) + .with_target_version(PythonVersion::PY313), ); assert_diagnostics!("PY313_RUF036_runtime_evaluated", diagnostics); } @@ -365,10 +359,8 @@ mod tests { fn quadratic_list_summation_py315() -> Result<()> { let diagnostics = test_path( Path::new("ruff/RUF017_0.py"), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY315.into(), - ..settings::LinterSettings::for_rule(Rule::QuadraticListSummation) - }, + &settings::LinterSettings::for_rule(Rule::QuadraticListSummation) + .with_target_version(PythonVersion::PY315), )?; assert_diagnostics!("PY315_RUF017_RUF017_0.py", diagnostics); Ok(()) @@ -378,12 +370,8 @@ mod tests { fn unnecessary_iterable_allocation_for_first_element_py315() -> Result<()> { let diagnostics = test_path( Path::new("ruff/RUF015_py315.py"), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY315.into(), - ..settings::LinterSettings::for_rule( - Rule::UnnecessaryIterableAllocationForFirstElement, - ) - }, + &settings::LinterSettings::for_rule(Rule::UnnecessaryIterableAllocationForFirstElement) + .with_target_version(PythonVersion::PY315), )?; assert_diagnostics!("PY315_RUF015_RUF015_py315.py", diagnostics); Ok(()) @@ -393,10 +381,8 @@ mod tests { 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) - }, + &LinterSettings::for_rule(Rule::AccessAnnotationsFromClassDict) + .with_target_version(PythonVersion::PY310), )?; assert_diagnostics!(diagnostics); Ok(()) @@ -406,10 +392,8 @@ mod tests { 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) - }, + &LinterSettings::for_rule(Rule::AccessAnnotationsFromClassDict) + .with_target_version(PythonVersion::PY314), )?; assert_diagnostics!(diagnostics); Ok(()) @@ -823,10 +807,7 @@ mod tests { ); let diagnostics = test_path( Path::new("ruff").join(path).as_path(), - &settings::LinterSettings { - preview: PreviewMode::Enabled, - ..settings::LinterSettings::for_rule(rule_code) - }, + &settings::LinterSettings::for_rule(rule_code).with_preview_mode(), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) @@ -841,11 +822,9 @@ mod tests { ); let diagnostics = test_path( Path::new("ruff").join(path).as_path(), - &settings::LinterSettings { - preview: PreviewMode::Enabled, - unresolved_target_version: PythonVersion::PY37.into(), - ..settings::LinterSettings::for_rule(rule_code) - }, + &settings::LinterSettings::for_rule(rule_code) + .with_preview_mode() + .with_target_version(PythonVersion::PY37), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) @@ -860,11 +839,9 @@ mod tests { ); let diagnostics = test_path( Path::new("ruff").join(path).as_path(), - &settings::LinterSettings { - preview: PreviewMode::Enabled, - unresolved_target_version: PythonVersion::PY38.into(), - ..settings::LinterSettings::for_rule(rule_code) - }, + &settings::LinterSettings::for_rule(rule_code) + .with_preview_mode() + .with_target_version(PythonVersion::PY38), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) @@ -907,10 +884,8 @@ mod tests { ); let diagnostics = test_path( Path::new("ruff").join(path).as_path(), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY314.into(), - ..settings::LinterSettings::for_rule(rule_code) - }, + &settings::LinterSettings::for_rule(rule_code) + .with_target_version(PythonVersion::PY314), )?; 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 index 33435c648e..a3ddb2b259 100644 --- 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 @@ -7,8 +7,7 @@ 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. +/// [`lint.typing-extensions`] is enabled. /// /// ## Why is this bad? /// Starting with Python 3.14, directly accessing `__annotations__` via @@ -74,7 +73,7 @@ use ruff_text_size::Ranged; /// ## References /// - [Python Annotations Best Practices](https://docs.python.org/3.14/howto/annotations.html) #[derive(ViolationMetadata)] -#[violation_metadata(preview_since = "0.12.1")] +#[violation_metadata(stable_since = "0.16.0")] pub(crate) struct AccessAnnotationsFromClassDict { python_version: PythonVersion, } 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 e93433377f..857844b974 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 @@ -49,9 +49,23 @@ use crate::checkers::ast::Checker; /// task.add_done_callback(background_tasks.discard) /// ``` /// +/// Or, for Python 3.11 and later, use structured concurrency with +/// `asyncio.TaskGroup` when the tasks should be awaited as part of the current +/// operation: +/// ```python +/// import asyncio +/// +/// +/// async def main() -> None: +/// async with asyncio.TaskGroup() as tg: +/// for i in range(10): +/// tg.create_task(some_coro(param=i)) +/// ``` +/// /// ## References /// - [_The Heisenbug lurking in your async code_](https://textual.textualize.io/blog/2023/02/11/the-heisenbug-lurking-in-your-async-code/) -/// - [The Python Standard Library](https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task) +/// - [Python documentation: `asyncio.create_task`](https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task) +/// - [Python documentation: `asyncio.TaskGroup`](https://docs.python.org/3/library/asyncio-task.html#asyncio.TaskGroup) #[derive(ViolationMetadata)] #[violation_metadata(stable_since = "v0.0.247")] pub(crate) struct AsyncioDanglingTask { diff --git a/crates/ruff_linter/src/rules/ruff/rules/duplicate_entry_in_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/duplicate_entry_in_dunder_all.rs index fceb6ddf74..a16edf98b7 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/duplicate_entry_in_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/duplicate_entry_in_dunder_all.rs @@ -48,7 +48,7 @@ use crate::{FixAvailability, Violation}; /// ] /// ``` #[derive(ViolationMetadata)] -#[violation_metadata(preview_since = "0.14.14")] +#[violation_metadata(stable_since = "0.16.0")] pub(crate) struct DuplicateEntryInDunderAll; impl Violation for DuplicateEntryInDunderAll { @@ -163,7 +163,7 @@ fn duplicate_entry_in_dunder_all(checker: &Checker, target: &ast::Expr, value: & previous_expr, ); - diagnostic.set_primary_message(format_args!("`{name}` duplicated here")); + diagnostic.set_primary_annotation_message(format_args!("`{name}` duplicated here")); diagnostic.try_set_fix(|| { edits::remove_member(elts, index, source).map(|edit| { 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 de3c072bdd..c4a708cd69 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 @@ -48,7 +48,9 @@ impl Violation for FalsyDictGetFallback { #[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() + "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) -> Option { 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 index 0ec70e42de..8428f06eee 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/in_empty_collection.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/in_empty_collection.rs @@ -80,7 +80,7 @@ fn is_empty(expr: &Expr, semantic: &SemanticModel) -> bool { Expr::Call(ast::ExprCall { func, arguments, - range: _, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, 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 index 018e295bb1..ec9477fc2c 100644 --- 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 @@ -248,7 +248,7 @@ fn generate_with_statement( let context_call = ast::ExprCall { node_index: AtomicNodeIndex::NONE, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), is_cast: false, is_checked_cast: false, is_string_tag: false, @@ -273,7 +273,7 @@ fn generate_with_statement( let func_call = ast::ExprCall { node_index: AtomicNodeIndex::NONE, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), is_cast: false, is_checked_cast: false, is_string_tag: false, diff --git a/crates/ruff_linter/src/rules/ruff/rules/logging_eager_conversion.rs b/crates/ruff_linter/src/rules/ruff/rules/logging_eager_conversion.rs index 920f6c73a5..1945816daf 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/logging_eager_conversion.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/logging_eager_conversion.rs @@ -64,8 +64,8 @@ use crate::rules::flake8_logging_format::rules::{LoggingCallType, find_logging_c #[derive(ViolationMetadata)] #[violation_metadata(preview_since = "0.13.2")] pub(crate) struct LoggingEagerConversion { - pub(crate) format_conversion: FormatConversion, - pub(crate) function_name: Option<&'static str>, + format_conversion: FormatConversion, + function_name: Option<&'static str>, } impl Violation for LoggingEagerConversion { @@ -137,7 +137,13 @@ pub(crate) fn logging_eager_conversion(checker: &Checker, call: &ast::ExprCall) None } }) - .zip(call.arguments.args.iter().skip(msg_pos + 1)) + .zip( + call.arguments + .args + .iter() + .skip(msg_pos + 1) + .take_while(|arg| !arg.is_starred_expr()), + ) { // Check if the argument is a call to eagerly format a value if let Expr::Call(ast::ExprCall { 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 939bbcdc27..f1c8414ef1 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 @@ -71,7 +71,7 @@ fn map_call_with_two_arguments<'a>( range: _, node_index: _, }, - range: _, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, 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 773c281e45..1eb6e1fa46 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 @@ -11,7 +11,7 @@ use crate::fix::edits::pad; use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does -/// Checks for type annotations where `None` is not at the end of an union. +/// Checks for type annotations where `None` is not at the end of a union. /// /// ## Why is this bad? /// Type annotation unions are commutative, meaning that the order of the elements @@ -33,7 +33,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// - [Python documentation: `typing.Optional`](https://docs.python.org/3/library/typing.html#typing.Optional) /// - [Python documentation: `None`](https://docs.python.org/3/library/constants.html#None) #[derive(ViolationMetadata)] -#[violation_metadata(preview_since = "0.7.4")] +#[violation_metadata(stable_since = "0.16.0")] pub(crate) struct NoneNotAtEndOfUnion; impl Violation for NoneNotAtEndOfUnion { diff --git a/crates/ruff_linter/src/rules/ruff/rules/noqa_comments.rs b/crates/ruff_linter/src/rules/ruff/rules/noqa_comments.rs index d01abd0e6d..47c210a160 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/noqa_comments.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/noqa_comments.rs @@ -11,11 +11,11 @@ use crate::{ /// ## What it does /// -/// Checks for the use of `noqa` comments instead of Ruff-specific `ruff:ignore` comments. +/// Checks for the use of `noqa` comments instead of Ruff-specific `ruff: ignore` comments. /// /// ## Why is this bad? /// -/// `ruff:ignore` comments allow the use of rule names instead of codes and can be used in more +/// `ruff: ignore` comments allow the use of rule names instead of codes and can be used in more /// places than `noqa` comments. /// /// Note that this is an opinionated, stylistic rule. `noqa` comments may be needed for backwards @@ -30,13 +30,13 @@ use crate::{ /// /// Use instead: /// ```python -/// import os # ruff:ignore[F401] +/// import os # ruff: ignore[F401] /// ``` /// /// Or if you prefer the own-line form: /// /// ```python -/// # ruff:ignore[unused-import] +/// # ruff: ignore[unused-import] /// import os /// ``` /// @@ -56,7 +56,7 @@ use crate::{ /// /// This rule avoids offering a fix if any of the rule codes in a `noqa` comment are unused. See /// `unused-noqa` for a rule that will remove these and allow the remaining codes to be moved into a -/// `ruff:ignore` comment. +/// `ruff: ignore` comment. #[derive(ViolationMetadata)] #[violation_metadata(preview_since = "0.15.22")] pub(crate) struct NoqaComments { @@ -69,17 +69,17 @@ impl Violation for NoqaComments { #[derive_message_formats] fn message(&self) -> String { if !self.file_level { - "`noqa` comment used instead of `ruff:ignore`".to_string() + "`noqa` comment used instead of `ruff: ignore`".to_string() } else { - "`ruff: noqa` comment used instead of `ruff:file-ignore`".to_string() + "`ruff: noqa` comment used instead of `ruff: file-ignore`".to_string() } } fn fix_title(&self) -> Option { Some(if self.file_level { - "Use `ruff:file-ignore` instead".to_string() + "Use `ruff: file-ignore` instead".to_string() } else { - "Use `ruff:ignore` instead".to_string() + "Use `ruff: ignore` instead".to_string() }) } } @@ -143,14 +143,14 @@ pub(crate) fn noqa_comments( // import math // ``` // - // by converting it to a valid `ruff:ignore` comment. + // by converting it to a valid `ruff: ignore` comment. if has_unused_codes { return; } let edit = Edit::range_replacement( format!( - "# ruff:{action}[{codes}]", + "# ruff: {action}[{codes}]", action = if file_level { "file-ignore" } else { "ignore" }, ), codes.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 6e2351aafb..ebb85cf307 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 @@ -40,7 +40,9 @@ pub(crate) struct ParenthesizeChainedOperators; impl AlwaysFixableViolation for ParenthesizeChainedOperators { #[derive_message_formats] fn message(&self) -> String { - "Parenthesize `a and b` expressions when chaining `and` and `or` together, to make the precedence clear".to_string() + "Parenthesize `a and b` expressions when chaining `and` and `or` together, \ + to make the precedence clear" + .to_string() } fn fix_title(&self) -> String { 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 8030e9c9ef..4bcd2d508a 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 @@ -113,7 +113,7 @@ pub(crate) fn quadratic_list_summation(checker: &Checker, call: &ast::ExprCall) let ast::ExprCall { func, arguments, - range, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, @@ -135,7 +135,8 @@ pub(crate) fn quadratic_list_summation(checker: &Checker, call: &ast::ExprCall) } let fix_style = QuadraticListSummationFixStyle::from_target_version(checker.target_version()); - let mut diagnostic = checker.report_diagnostic(QuadraticListSummation { fix_style }, *range); + let mut diagnostic = + checker.report_diagnostic(QuadraticListSummation { fix_style }, call.range()); diagnostic.try_set_fix(|| convert_to_fix(iterable, call, checker, fix_style)); } 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 010679cf23..3b895126b1 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/redirected_noqa.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/redirected_noqa.rs @@ -67,7 +67,7 @@ pub(crate) fn redirected_file_noqa(context: &LintContext, noqa_directives: &File } /// Convert a sequence of [Codes] into [Diagnostic]s and append them to `diagnostics`. -pub(crate) fn build_diagnostics(context: &LintContext, codes: &Codes<'_>) { +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 = context.report_diagnostic( diff --git a/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_suppression_comments.rs b/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_suppression_comments.rs index 53d42e3cc5..3c386f6ab6 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_suppression_comments.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_suppression_comments.rs @@ -11,18 +11,18 @@ use crate::AlwaysFixableViolation; /// Human-readable rule names are easier to understand than rule codes. Using names also avoids /// requiring readers to look up the meaning of each code. /// -/// This rule applies to `ruff:ignore`, `ruff:file-ignore`, `ruff:disable`, and `ruff:enable` +/// This rule applies to `ruff: ignore`, `ruff: file-ignore`, `ruff: disable`, and `ruff: enable` /// comments. /// /// ## Example /// /// ```python -/// import os # ruff:ignore[F401] +/// import os # ruff: ignore[F401] /// ``` /// /// Use instead: /// ```python -/// import os # ruff:ignore[unused-import] +/// import os # ruff: ignore[unused-import] /// ``` #[derive(ViolationMetadata)] #[violation_metadata(preview_since = "0.15.22")] 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 79881b8abc..1205767a2e 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/starmap_zip.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/starmap_zip.rs @@ -107,7 +107,7 @@ pub(crate) fn starmap_zip(checker: &Checker, call: &ExprCall) { return; } - let mut diagnostic = checker.report_diagnostic(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); 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 767e139ae8..2659e71555 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 @@ -117,7 +117,7 @@ pub(crate) fn unnecessary_literal_within_deque_call(checker: &Checker, deque: &a UnnecessaryEmptyIterableWithinDequeCall { has_maxlen: maxlen.is_some(), }, - deque.range, + deque.range(), ); // Return without a fix in the presence of a starred argument because we can't accurately @@ -145,7 +145,7 @@ fn fix_unnecessary_literal_in_deque( ); let len_str = checker.locator().slice(maxlen); let deque_str = format!("{deque_name}(maxlen={len_str})"); - Edit::range_replacement(deque_str, deque.range) + Edit::range_replacement(deque_str, deque.range()) } else { remove_argument( &iterable, 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 2c984740e0..3ad72ffdf5 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 @@ -6,7 +6,7 @@ use ruff_python_ast::{ }; use ruff_python_semantic::analyze::typing::find_binding_value; use ruff_python_semantic::{Modules, SemanticModel}; -use ruff_text_size::TextRange; +use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; @@ -181,7 +181,7 @@ impl<'a> ReFunc<'a> { let (comparison_to_none, range) = match comparison_to_none { Some((cmp, range)) => (Some(cmp), range), - None => (None, call.range), + None => (None, call.range()), }; match (func_name, call.arguments.len()) { @@ -357,7 +357,7 @@ impl<'a> ReFunc<'a> { range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, is_cast: false, is_checked_cast: false, 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 e2ab51e1db..e21e14432d 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_round.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_round.rs @@ -225,5 +225,5 @@ fn unwrap_round_call( rounded_expr.to_string() }; - Edit::range_replacement(new_content, call.range) + Edit::range_replacement(new_content, call.range()) } diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__PY315_RUF015_RUF015_py315.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__PY315_RUF015_RUF015_py315.py.snap index dd82e47487..79c3527454 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__PY315_RUF015_RUF015_py315.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__PY315_RUF015_RUF015_py315.py.snap @@ -25,7 +25,6 @@ RUF015 [*] Prefer `next(*x for x in xs)` over single element slice 3 | [*x for x in xs][0] 4 | list(*x for x in xs)[0] | ^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `next(*x for x in xs)` | 3 | [*x for x in xs][0] diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__PY315_RUF017_RUF017_0.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__PY315_RUF017_RUF017_0.py.snap index 65191c8be0..0824980405 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__PY315_RUF017_RUF017_0.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__PY315_RUF017_RUF017_0.py.snap @@ -104,7 +104,6 @@ RUF017 [*] Avoid quadratic list summation 20 | 21 | sum([x, y], []) | ^^^^^^^^^^^^^^^ - | help: Replace with a starred list comprehension | 20 | @@ -121,7 +120,6 @@ RUF017 [*] Avoid quadratic list summation 25 | def func(): 26 | sum((factor.dims for factor in bases), []) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with a starred list comprehension | 25 | def func(): diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF005_RUF005.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF005_RUF005.py.snap index bbe198bd82..f3c41721a4 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF005_RUF005.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF005_RUF005.py.snap @@ -42,7 +42,6 @@ RUF005 Consider `[*first, 4, 5, 6]` instead of concatenation 22 | | 6, 23 | | ] | |_^ - | help: Replace with `[*first, 4, 5, 6]` RUF005 [*] Consider `[1, 2, 3, *foo]` instead of concatenation diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF006_RUF006.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF006_RUF006.py.snap index 65efeca6a0..9fb29c3cc6 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF006_RUF006.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF006_RUF006.py.snap @@ -8,7 +8,6 @@ RUF006 Store a reference to the return value of `asyncio.create_task` 5 | def f(): 6 | asyncio.create_task(coordinator.ws_connect()) # Error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | RUF006 Store a reference to the return value of `asyncio.ensure_future` --> RUF006.py:11:5 @@ -17,7 +16,6 @@ RUF006 Store a reference to the return value of `asyncio.ensure_future` 10 | def f(): 11 | asyncio.ensure_future(coordinator.ws_connect()) # Error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | RUF006 Store a reference to the return value of `asyncio.create_task` --> RUF006.py:68:12 @@ -26,7 +24,6 @@ RUF006 Store a reference to the return value of `asyncio.create_task` 67 | def f(): 68 | task = asyncio.create_task(coordinator.ws_connect()) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | RUF006 Store a reference to the return value of `loop.create_task` --> RUF006.py:74:26 @@ -35,7 +32,6 @@ RUF006 Store a reference to the return value of `loop.create_task` 73 | loop = asyncio.get_running_loop() 74 | task: asyncio.Task = loop.create_task(coordinator.ws_connect()) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | RUF006 Store a reference to the return value of `loop.create_task` --> RUF006.py:97:5 @@ -44,7 +40,6 @@ RUF006 Store a reference to the return value of `loop.create_task` 96 | loop = asyncio.get_running_loop() 97 | loop.create_task(coordinator.ws_connect()) # Error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | RUF006 Store a reference to the return value of `asyncio.create_task` --> RUF006.py:152:13 diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF007_RUF007.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF007_RUF007.py.snap index 02a04f3566..df6aeed6fe 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF007_RUF007.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF007_RUF007.py.snap @@ -204,7 +204,6 @@ RUF007 [*] Prefer `itertools.pairwise()` over `zip()` when iterating over succes 24 | zip(foo[:-1], foo[1:], strict=False) 25 | zip(foo[:-1], foo[1:], strict=bool(foo)) | ^^^ - | help: Replace `zip()` with `itertools.pairwise()` | 1 + import itertools diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF008_RUF008.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF008_RUF008.py.snap index 790e5ff0d5..4289a1e031 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF008_RUF008.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF008_RUF008.py.snap @@ -52,4 +52,3 @@ RUF008 Do not use mutable default values for dataclass attributes 35 | perfectly_fine: 'list[int]' = field(default_factory=list) 36 | class_variable: 'typing.ClassVar[list[int]]'= [] | ^^ - | 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 c50c1d506a..2e6fb79933 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 @@ -51,7 +51,6 @@ RUF009 Do not perform function call `G` in dataclass defaults 108 | f: F = F() 109 | g: G = G() | ^^^ - | RUF009 Do not perform function call `F` in dataclass defaults --> RUF009_attrs.py:114:12 @@ -70,7 +69,6 @@ RUF009 Do not perform function call `G` in dataclass defaults 114 | f: F = F() 115 | g: G = G() | ^^^ - | RUF009 Do not perform function call `F` in dataclass defaults --> RUF009_attrs.py:120:12 @@ -89,7 +87,6 @@ RUF009 Do not perform function call `G` in dataclass defaults 120 | f: F = F() 121 | g: G = G() | ^^^ - | RUF009 Do not perform function call `F` in dataclass defaults --> RUF009_attrs.py:126:12 @@ -108,7 +105,6 @@ RUF009 Do not perform function call `G` in dataclass defaults 126 | f: F = F() 127 | g: G = G() | ^^^ - | RUF009 Do not perform function call `list` in dataclass defaults --> RUF009_attrs.py:144:20 @@ -117,4 +113,3 @@ RUF009 Do not perform function call `list` in dataclass defaults 143 | class TestAttrAttributes: 144 | x: list[int] = list() # 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 d65283c1b6..a2064e5cc9 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 @@ -619,7 +619,6 @@ RUF010 [*] Use explicit conversion flag 121 | | 1 122 | | ))}" | |__^ - | help: Replace with conversion flag | 118 | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF012_RUF012.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF012_RUF012.py.snap index 8d0a78445e..5cc2fd8c41 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF012_RUF012.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF012_RUF012.py.snap @@ -126,7 +126,6 @@ RUF012 Mutable default value for class attribute 133 | class_variable_without_subscript: 'ClassVar' = [] 134 | final_variable_without_subscript: 'Final' = [] | ^^ - | help: Consider initializing in `__init__` or annotating with `typing.ClassVar` RUF012 Mutable default value for class attribute diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF013_RUF013_4.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF013_RUF013_4.py.snap index ce92f8a321..745e2f89b2 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF013_RUF013_4.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF013_RUF013_4.py.snap @@ -6,7 +6,6 @@ RUF013 [*] PEP 484 prohibits implicit `Optional` | 15 | def multiple_2(arg1: Optional, arg2: Optional = None, arg3: int = None): ... | ^^^ - | help: Convert to `T | None` | 14 | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF015_RUF015.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF015_RUF015.py.snap index b95b8c76ee..87d7c0c429 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF015_RUF015.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF015_RUF015.py.snap @@ -430,7 +430,6 @@ RUF015 [*] Prefer `next(iter(zip(x, y)))` over single element slice 72 | zip = list # Overwrite the builtin zip 73 | list(zip(x, y))[0] | ^^^^^^^^^^^^^^^^^^ - | help: Replace with `next(iter(zip(x, y)))` | 72 | zip = list # Overwrite the builtin zip diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF016_RUF016.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF016_RUF016.py.snap index 089bac11e0..1f37eb3b4c 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF016_RUF016.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF016_RUF016.py.snap @@ -453,4 +453,3 @@ RUF016 Slice in indexed access to type `list` uses type `str` instead of an inte 133 | x = "x" 134 | var = [1, 2, 3][x:"y"] | ^^^ - | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF017_RUF017_0.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF017_RUF017_0.py.snap index 2bcd9ba8f8..601c205bbd 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF017_RUF017_0.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF017_RUF017_0.py.snap @@ -125,7 +125,6 @@ RUF017 [*] Avoid quadratic list summation 20 | 21 | sum([x, y], []) | ^^^^^^^^^^^^^^^ - | help: Replace with `functools.reduce` | 20 | @@ -142,7 +141,6 @@ RUF017 [*] Avoid quadratic list summation 25 | def func(): 26 | sum((factor.dims for factor in bases), []) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `functools.reduce` | 1 + import functools diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF017_RUF017_1.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF017_RUF017_1.py.snap index 66860484aa..28a251a13f 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF017_RUF017_1.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF017_RUF017_1.py.snap @@ -6,7 +6,6 @@ RUF017 [*] Avoid quadratic list summation | 1 | sum((factor.dims for factor in bases), []) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `functools.reduce` | - sum((factor.dims for factor in bases), []) diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF020_RUF020.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF020_RUF020.py.snap index d247be7190..afecdbe964 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF020_RUF020.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF020_RUF020.py.snap @@ -113,7 +113,6 @@ RUF020 [*] `Union[NoReturn, T]` is equivalent to `T` 7 | Union[Union[Never, int], Union[NoReturn, int]] 8 | Union[NoReturn, int, float] | ^^^^^^^^ - | help: Remove `NoReturn` | 7 | Union[Union[Never, int], Union[NoReturn, int]] @@ -173,7 +172,6 @@ RUF020 `Never | T` is equivalent to `T` 16 | a: int | Never | None 17 | b: Never | Never | None | ^^^^^ - | help: Remove `Never` RUF020 `Never | T` is equivalent to `T` @@ -182,7 +180,6 @@ RUF020 `Never | T` is equivalent to `T` 16 | a: int | Never | None 17 | b: Never | Never | None | ^^^^^ - | help: Remove `Never` RUF020 [*] `Never | T` is equivalent to `T` diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF023_RUF023.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF023_RUF023.py.snap index b9b9da498e..e798afe925 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF023_RUF023.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF023_RUF023.py.snap @@ -621,7 +621,6 @@ RUF023 [*] `BezierBuilder4.__slots__` is not sorted 192 | | "baz", "bingo" 193 | | } | |__________________^ - | help: Apply a natural sort to `BezierBuilder4.__slots__` | 190 | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF024_RUF024.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF024_RUF024.py.snap index ab2924d139..8b12e431e1 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF024_RUF024.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF024_RUF024.py.snap @@ -140,7 +140,6 @@ RUF024 [*] Do not pass mutable objects as values to `dict.fromkeys` 38 | key_0 = "z" 39 | dict.fromkeys("ABC", list(key)) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with comprehension | 38 | key_0 = "z" diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF026_RUF026.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF026_RUF026.py.snap index e8cc588cfa..b9a000ba51 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF026_RUF026.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF026_RUF026.py.snap @@ -7,7 +7,6 @@ RUF026 [*] `default_factory` is a positional-only argument to `defaultdict` 10 | def func(): 11 | defaultdict(default_factory=int) # RUF026 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `defaultdict(default_factory=int)` | 10 | def func(): @@ -23,7 +22,6 @@ RUF026 [*] `default_factory` is a positional-only argument to `defaultdict` 14 | def func(): 15 | defaultdict(default_factory=float) # RUF026 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `defaultdict(default_factory=float)` | 14 | def func(): @@ -39,7 +37,6 @@ RUF026 [*] `default_factory` is a positional-only argument to `defaultdict` 18 | def func(): 19 | defaultdict(default_factory=dict) # RUF026 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `defaultdict(default_factory=dict)` | 18 | def func(): @@ -55,7 +52,6 @@ RUF026 [*] `default_factory` is a positional-only argument to `defaultdict` 22 | def func(): 23 | defaultdict(default_factory=list) # RUF026 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `defaultdict(default_factory=list)` | 22 | def func(): @@ -71,7 +67,6 @@ RUF026 [*] `default_factory` is a positional-only argument to `defaultdict` 26 | def func(): 27 | defaultdict(default_factory=tuple) # RUF026 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `defaultdict(default_factory=tuple)` | 26 | def func(): @@ -88,7 +83,6 @@ RUF026 [*] `default_factory` is a positional-only argument to `defaultdict` 33 | 34 | defaultdict(default_factory=foo) # RUF026 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `defaultdict(default_factory=foo)` | 33 | @@ -104,7 +98,6 @@ RUF026 [*] `default_factory` is a positional-only argument to `defaultdict` 37 | def func(): 38 | defaultdict(default_factory=lambda: 1) # RUF026 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `defaultdict(default_factory=lambda: 1)` | 37 | def func(): @@ -121,7 +114,6 @@ RUF026 [*] `default_factory` is a positional-only argument to `defaultdict` 43 | 44 | defaultdict(default_factory=deque) # RUF026 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `defaultdict(default_factory=deque)` | 43 | @@ -138,7 +130,6 @@ RUF026 [*] `default_factory` is a positional-only argument to `defaultdict` 51 | 52 | defaultdict(default_factory=MyCallable()) # RUF026 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `defaultdict(default_factory=MyCallable())` | 51 | @@ -154,7 +145,6 @@ RUF026 [*] `default_factory` is a positional-only argument to `defaultdict` 55 | def func(): 56 | defaultdict(default_factory=tuple, member=1) # RUF026 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `defaultdict(default_factory=tuple)` | 55 | def func(): @@ -170,7 +160,6 @@ RUF026 [*] `default_factory` is a positional-only argument to `defaultdict` 59 | def func(): 60 | defaultdict(member=1, default_factory=tuple) # RUF026 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `defaultdict(default_factory=tuple)` | 59 | def func(): @@ -186,7 +175,6 @@ RUF026 [*] `default_factory` is a positional-only argument to `defaultdict` 63 | def func(): 64 | defaultdict(member=1, default_factory=tuple,) # RUF026 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `defaultdict(default_factory=tuple)` | 63 | def func(): @@ -205,7 +193,6 @@ RUF026 [*] `default_factory` is a positional-only argument to `defaultdict` 70 | | default_factory=tuple, 71 | | ) # RUF026 | |_____^ - | help: Replace with `defaultdict(default_factory=tuple)` | 68 | defaultdict( @@ -225,7 +212,6 @@ RUF026 [*] `default_factory` is a positional-only argument to `defaultdict` 77 | | member=1, 78 | | ) # RUF026 | |_____^ - | help: Replace with `defaultdict(default_factory=tuple)` | 75 | defaultdict( diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF027_RUF027_0.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF027_RUF027_0.py.snap index 3327181c22..5c3d0ab42b 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF027_RUF027_0.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF027_RUF027_0.py.snap @@ -8,7 +8,6 @@ RUF027 [*] Possible f-string without an `f` prefix 4 | 5 | print("but don't ignore this: {val}") # RUF027 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add `f` prefix | 4 | @@ -43,7 +42,6 @@ RUF027 [*] Possible f-string without an `f` prefix 10 | b = "{a}" # RUF027 11 | c = "{a} {b} f'{val}' " # RUF027 | ^^^^^^^^^^^^^^^^^^^ - | help: Add `f` prefix | 10 | b = "{a}" # RUF027 @@ -78,7 +76,6 @@ RUF027 [*] Possible f-string without an `f` prefix 21 | b = r"raw string with formatting: {a}" # RUF027 22 | c = r"raw string with \backslashes\ and \"escaped quotes\": {a}" # RUF027 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add `f` prefix | 21 | b = r"raw string with formatting: {a}" # RUF027 @@ -113,7 +110,6 @@ RUF027 [*] Possible f-string without an `f` prefix 27 | print("Hello, {name}!") # RUF027 28 | print("The test value we're using today is {a}") # RUF027 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add `f` prefix | 27 | print("Hello, {name}!") # RUF027 @@ -130,7 +126,6 @@ RUF027 [*] Possible f-string without an `f` prefix 32 | a = 4 33 | print(do_nothing(do_nothing("{a}"))) # RUF027 | ^^^^^ - | help: Add `f` prefix | 32 | a = 4 @@ -169,7 +164,6 @@ RUF027 [*] Possible f-string without an `f` prefix 42 | | c} d 43 | | """ | |_______^ - | help: Add `f` prefix | 40 | # RUF027 @@ -189,7 +183,6 @@ RUF027 [*] Possible f-string without an `f` prefix 50 | | a} \ 51 | | " | |_____^ - | help: Add `f` prefix | 48 | # RUF027 @@ -224,7 +217,6 @@ RUF027 [*] Possible f-string without an `f` prefix 56 | b = "{a}" "+" "{b}" r" \\ " # RUF027 for the first part only 57 | print(f"{a}" "{a}" f"{b}") # RUF027 | ^^^^^ - | help: Add `f` prefix | 56 | b = "{a}" "+" "{b}" r" \\ " # RUF027 for the first part only @@ -241,7 +233,6 @@ RUF027 [*] Possible f-string without an `f` prefix 61 | a = 4 62 | b = "\"not escaped:\" '{a}' \"escaped:\": '{{c}}'" # RUF027 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add `f` prefix | 61 | a = 4 diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF028_RUF028.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF028_RUF028.py.snap index ae94b664fd..f32c717ea7 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF028_RUF028.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF028_RUF028.py.snap @@ -185,7 +185,6 @@ RUF028 [*] This suppression comment is invalid because it cannot be at the end o 62 | val = 5 # fmt: on 63 | pass # fmt: on | ^^^^^^^^^ - | help: Remove this comment | 62 | val = 5 # fmt: on diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF030_RUF030.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF030_RUF030.py.snap index 41b9366605..5da2b5d855 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF030_RUF030.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF030_RUF030.py.snap @@ -350,7 +350,6 @@ RUF030 [*] `print()` call in `assert` statement is likely unintentional 107 | # - single StringLiteral 108 | assert True, builtins.print("This print should be removed.") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove `print` | 107 | # - single StringLiteral diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF032_RUF032.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF032_RUF032.py.snap index 7efec46017..386dbed26c 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF032_RUF032.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF032_RUF032.py.snap @@ -141,7 +141,6 @@ RUF032 [*] `Decimal()` called with float literal argument 57 | 58 | val = Decimal(-+--++--4.0) # Suggest `Decimal("-4.0")` | ^^^^^^^^^^^ - | help: Replace with string literal | 57 | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF033_RUF033.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF033_RUF033.py.snap index c920f3c4a3..4581b195a4 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF033_RUF033.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF033_RUF033.py.snap @@ -8,7 +8,6 @@ RUF033 `__post_init__` method with argument defaults 18 | 19 | def __post_init__(self, bar = 11, baz = 11) -> None: ... | ^^ - | help: Use `dataclasses.InitVar` instead RUF033 `__post_init__` method with argument defaults @@ -18,7 +17,6 @@ RUF033 `__post_init__` method with argument defaults 18 | 19 | def __post_init__(self, bar = 11, baz = 11) -> None: ... | ^^ - | help: Use `dataclasses.InitVar` instead RUF033 [*] `__post_init__` method with argument defaults @@ -28,7 +26,6 @@ RUF033 [*] `__post_init__` method with argument defaults 24 | class Foo: 25 | def __post_init__(self, bar = 11, baz = 11) -> None: ... | ^^ - | help: Use `dataclasses.InitVar` instead | 24 | class Foo: @@ -46,7 +43,6 @@ RUF033 [*] `__post_init__` method with argument defaults 24 | class Foo: 25 | def __post_init__(self, bar = 11, baz = 11) -> None: ... | ^^ - | help: Use `dataclasses.InitVar` instead | 24 | class Foo: @@ -64,7 +60,6 @@ RUF033 [*] `__post_init__` method with argument defaults 45 | class Foo: 46 | def __post_init__(self, bar: int = 11, baz: Something[Whatever | None] = 11) -> None: ... | ^^ - | help: Use `dataclasses.InitVar` instead | 45 | class Foo: @@ -82,7 +77,6 @@ RUF033 [*] `__post_init__` method with argument defaults 45 | class Foo: 46 | def __post_init__(self, bar: int = 11, baz: Something[Whatever | None] = 11) -> None: ... | ^^ - | help: Use `dataclasses.InitVar` instead | 45 | class Foo: @@ -100,7 +94,6 @@ RUF033 [*] `__post_init__` method with argument defaults 58 | 59 | def __post_init__(self, bar: int = 11, baz: int = 12) -> None: ... | ^^ - | help: Use `dataclasses.InitVar` instead | 58 | @@ -118,7 +111,6 @@ RUF033 [*] `__post_init__` method with argument defaults 58 | 59 | def __post_init__(self, bar: int = 11, baz: int = 12) -> None: ... | ^^ - | help: Use `dataclasses.InitVar` instead | 58 | @@ -136,7 +128,6 @@ RUF033 `__post_init__` method with argument defaults 66 | 67 | def __post_init__(self, bar: str = "ahhh", baz: str = "hmm") -> None: ... | ^^^^^^ - | help: Use `dataclasses.InitVar` instead RUF033 `__post_init__` method with argument defaults @@ -146,7 +137,6 @@ RUF033 `__post_init__` method with argument defaults 66 | 67 | def __post_init__(self, bar: str = "ahhh", baz: str = "hmm") -> None: ... | ^^^^^ - | help: Use `dataclasses.InitVar` instead RUF033 [*] `__post_init__` method with argument defaults diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF034_RUF034.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF034_RUF034.py.snap index 1ab0762fc7..5557b6c13a 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF034_RUF034.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF034_RUF034.py.snap @@ -27,4 +27,3 @@ RUF034 Useless `if`-`else` condition 10 | # Invalid 11 | x = 0.1 if False else 0.1 | ^^^^^^^^^^^^^^^^^^^^^ - | 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 fff7cd66a2..a5b357425c 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 @@ -7,7 +7,6 @@ RUF037 [*] Unnecessary empty iterable within a deque call 5 | def f(): 6 | queue = collections.deque([]) # RUF037 | ^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `deque()` | 5 | def f(): @@ -22,7 +21,6 @@ RUF037 [*] Unnecessary empty iterable within a deque call 9 | def f(): 10 | queue = collections.deque([], maxlen=10) # RUF037 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `deque(maxlen=...)` | 9 | def f(): @@ -37,7 +35,6 @@ RUF037 [*] Unnecessary empty iterable within a deque call 13 | def f(): 14 | queue = deque([]) # RUF037 | ^^^^^^^^^ - | help: Replace with `deque()` | 13 | def f(): @@ -52,7 +49,6 @@ RUF037 [*] Unnecessary empty iterable within a deque call 17 | def f(): 18 | queue = deque(()) # RUF037 | ^^^^^^^^^ - | help: Replace with `deque()` | 17 | def f(): @@ -67,7 +63,6 @@ RUF037 [*] Unnecessary empty iterable within a deque call 21 | def f(): 22 | queue = deque({}) # RUF037 | ^^^^^^^^^ - | help: Replace with `deque()` | 21 | def f(): @@ -82,7 +77,6 @@ RUF037 [*] Unnecessary empty iterable within a deque call 25 | def f(): 26 | queue = deque(set()) # RUF037 | ^^^^^^^^^^^^ - | help: Replace with `deque()` | 25 | def f(): @@ -97,7 +91,6 @@ RUF037 [*] Unnecessary empty iterable within a deque call 29 | def f(): 30 | queue = collections.deque([], maxlen=10) # RUF037 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `deque(maxlen=...)` | 29 | def f(): @@ -112,7 +105,6 @@ RUF037 [*] Unnecessary empty iterable within a deque call 60 | def f(): 61 | x = 0 or(deque)([]) | ^^^^^^^^^^^ - | help: Replace with `deque()` | 60 | def f(): @@ -158,7 +150,6 @@ RUF037 [*] Unnecessary empty iterable within a deque call 67 | deque([], **{"maxlen": 10}) # RUF037 68 | deque([], foo=1) # RUF037 | ^^^^^^^^^^^^^^^^ - | help: Replace with `deque()` | 67 | deque([], **{"maxlen": 10}) # RUF037 @@ -179,7 +170,6 @@ RUF037 [*] Unnecessary empty iterable within a deque call 83 | | maxlen=10, # a comment on maxlen, deleted 84 | | ) # only this is preserved | |_________^ - | help: Replace with `deque(maxlen=...)` | 79 | def f(): @@ -200,7 +190,6 @@ RUF037 [*] Unnecessary empty iterable within a deque call 88 | def f(): 89 | deque([], 10) | ^^^^^^^^^^^^^ - | help: Replace with `deque(maxlen=...)` | 88 | def f(): diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF043_RUF043.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF043_RUF043.py.snap index f849446a0b..52f7c82d14 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF043_RUF043.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF043_RUF043.py.snap @@ -112,7 +112,6 @@ RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped 21 | # https://github.com/astral-sh/ruff/issues/15316 22 | with pytest.raises(ClosingParenthesis, match="foo)"): ... | ^^^^^^ - | help: Use a raw string or `re.escape()` to make the intention explicit RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped nor raw @@ -346,7 +345,6 @@ RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped 46 | with pytest.raises(NonWordCharacter2, match="foobar\\W"): ... 47 | with pytest.raises(EndOfInput2, match="foobar\\z"): ... | ^^^^^^^^^^^ - | help: Use a raw string or `re.escape()` to make the intention explicit RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped nor raw @@ -356,5 +354,4 @@ RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped 51 | 52 | with pytest.raises(NameEscape, match="\\N{EN DASH}"): ... | ^^^^^^^^^^^^^^ - | help: Use a raw string or `re.escape()` to make the intention explicit diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF046_RUF046.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF046_RUF046.py.snap index 595a5ccc9f..cd45cd92f8 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF046_RUF046.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF046_RUF046.py.snap @@ -622,7 +622,6 @@ RUF046 [*] Value being cast to `int` is already an integer 52 | int(1 and 0) 53 | int(0 or -1) | ^^^^^^^^^^^^ - | help: Remove unnecessary `int` call | 52 | int(1 and 0) @@ -833,7 +832,6 @@ RUF046 [*] Value being cast to `int` is already an integer 75 | int(round(unknown)) 76 | int(round(unknown, None)) | ^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove unnecessary `int` call | 75 | int(round(unknown)) @@ -1081,7 +1079,6 @@ RUF046 [*] Value being cast to `int` is already an integer 207 | | # unsafe fix because of this comment 208 | | ) | |_^ - | help: Remove unnecessary `int` call | 202 | 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 index 4c9542624c..1b56a758d3 100644 --- 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 @@ -7,7 +7,6 @@ RUF046 [*] Value being cast to `int` is already an integer 1 | / int(- 2 | | 1) # Carriage return as newline | |______^ - | help: Remove unnecessary `int` call | - int(- 1 + (- 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 index 6c8e85a1ba..fc945a5b78 100644 --- 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 @@ -8,7 +8,6 @@ RUF046 [*] Value being cast to `int` is already an integer 2 | / int(- 3 | | 1) | |______^ - | help: Remove unnecessary `int` call | 1 | # \n as newline diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF047_RUF047_for.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF047_RUF047_for.py.snap index 7dba401011..8d32c11f2f 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF047_RUF047_for.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF047_RUF047_for.py.snap @@ -9,7 +9,6 @@ RUF047 [*] Empty `else` clause 6 | / else: 7 | | pass | |________^ - | help: Remove the `else` clause | 5 | break @@ -26,7 +25,6 @@ RUF047 [*] Empty `else` clause 12 | / else: 13 | | ... | |_______^ - | help: Remove the `else` clause | 11 | belongs_to() # `for` diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF047_RUF047_if.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF047_RUF047_if.py.snap index 170cc86584..54b74d7ff3 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF047_RUF047_if.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF047_RUF047_if.py.snap @@ -9,7 +9,6 @@ RUF047 [*] Empty `else` clause 5 | / else: 6 | | pass | |________^ - | help: Remove the `else` clause | 4 | condition_is_not_evaluated() @@ -26,7 +25,6 @@ RUF047 [*] Empty `else` clause 11 | / else: 12 | | ... | |_______^ - | help: Remove the `else` clause | 10 | belongs_to() # `if` @@ -43,7 +41,6 @@ RUF047 [*] Empty `else` clause 19 | / else: 20 | | pass | |________^ - | help: Remove the `else` clause | 18 | as_if() @@ -79,7 +76,6 @@ RUF047 [*] Empty `else` clause 32 | / else: 33 | | pass | |________^ - | help: Remove the `else` clause | 31 | # `if` @@ -94,7 +90,6 @@ RUF047 [*] Empty `else` clause 43 | if of_course: this() 44 | else: ... | ^^^^^^^^^ - | help: Remove the `else` clause | 43 | if of_course: this() @@ -109,7 +104,6 @@ RUF047 [*] Empty `else` clause 48 | this() # comment 49 | else: ... | ^^^^^^^^^ - | help: Remove the `else` clause | 48 | this() # comment @@ -125,7 +119,6 @@ RUF047 [*] Empty `else` clause 55 | / else: 56 | | ... | |___________^ - | help: Remove the `else` clause | 54 | b() diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF047_RUF047_try.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF047_RUF047_try.py.snap index 6a61b7c63a..8e40273a77 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF047_RUF047_try.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF047_RUF047_try.py.snap @@ -9,7 +9,6 @@ RUF047 [*] Empty `else` clause 7 | / else: 8 | | pass | |________^ - | help: Remove the `else` clause | 6 | pass @@ -26,7 +25,6 @@ RUF047 [*] Empty `else` clause 17 | / else: 18 | | ... | |_______^ - | help: Remove the `else` clause | 16 | to() # `except` diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF047_RUF047_while.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF047_RUF047_while.py.snap index 58f2ea145c..e219f2ae89 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF047_RUF047_while.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF047_RUF047_while.py.snap @@ -9,7 +9,6 @@ RUF047 [*] Empty `else` clause 6 | / else: 7 | | pass | |________^ - | help: Remove the `else` clause | 5 | break @@ -26,7 +25,6 @@ RUF047 [*] Empty `else` clause 12 | / else: 13 | | ... | |_______^ - | help: Remove the `else` clause | 11 | belongs_to() # `for` diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF050_RUF050.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF050_RUF050.py.snap index 8ef5ca160a..e36bfb7344 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF050_RUF050.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF050_RUF050.py.snap @@ -174,7 +174,6 @@ RUF050 [*] Empty `if` statement 43 | / if obj1: 44 | | pass | |____________^ - | help: Remove the `if` statement | 42 | with pytest.raises(ValueError, match=msg): @@ -339,7 +338,6 @@ RUF050 [*] Empty `if` statement 85 | / if foo(): 86 | | pass | |____________^ - | help: Remove the `if` statement | 84 | class Foo: diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF051_RUF051.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF051_RUF051.py.snap index 2badb4f691..257e61a09e 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF051_RUF051.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF051_RUF051.py.snap @@ -514,7 +514,6 @@ RUF051 [*] Use `pop` instead of `key in dict` followed by `del dict[key]` 98 | if b'yt' b'es' in d: 99 | del d[rb"""ytes"""] # This should not make the fix unsafe | ^^^^^^^^^^^^^^^^^^^ - | help: Replace `if` statement with `.pop(..., None)` | 97 | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF053_RUF053.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF053_RUF053.py.snap index 8ee126b30c..ec5fcd2256 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF053_RUF053.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF053_RUF053.py.snap @@ -147,7 +147,6 @@ RUF053 Class with type parameter list inherits from `Generic` 32 | class C[*Ts](Generic[Unpack[_Bs]], tuple[*Bs]): ... 33 | class C[*Ts](Callable[[*_Cs], tuple[*Ts]], Generic[_Cs]): ... # TODO: Type parameter defaults | ^^^^^^^^^^^^ - | help: Remove `Generic` base class RUF053 [*] Class with type parameter list inherits from `Generic` @@ -184,7 +183,6 @@ RUF053 Class with type parameter list inherits from `Generic` 37 | class C[**P](Generic[_P2]): ... 38 | class C[**P](Generic[_P3]): ... # TODO: Type parameter defaults | ^^^^^^^^^^^^ - | help: Remove `Generic` base class RUF053 [*] Class with type parameter list inherits from `Generic` @@ -192,7 +190,6 @@ RUF053 [*] Class with type parameter list inherits from `Generic` | 41 | class C[T](Generic[T, _A]): ... | ^^^^^^^^^^^^^^ - | help: Remove `Generic` base class | 40 | @@ -209,7 +206,6 @@ RUF053 Class with type parameter list inherits from `Generic` 46 | # only simple assignments, so there is no fix. 47 | class C[T: (_Z := TypeVar('_Z'))](Generic[_Z]): ... | ^^^^^^^^^^^ - | help: Remove `Generic` base class RUF053 [*] Class with type parameter list inherits from `Generic` @@ -218,7 +214,6 @@ RUF053 [*] Class with type parameter list inherits from `Generic` 50 | class C(Generic[_B]): 51 | class D[T](Generic[_B, T]): ... | ^^^^^^^^^^^^^^ - | help: Remove `Generic` base class | 50 | class C(Generic[_B]): @@ -234,7 +229,6 @@ RUF053 Class with type parameter list inherits from `Generic` 54 | class C[T]: 55 | class D[U](Generic[T, U]): ... | ^^^^^^^^^^^^^ - | help: Remove `Generic` base class RUF053 [*] Class with type parameter list inherits from `Generic` @@ -262,7 +256,6 @@ RUF053 Class with type parameter list inherits from `Generic` 60 | class C[T](Generic[_C], Generic[_D]): ... 61 | class C[T, _C: (str, bytes)](Generic[_D]): ... # TODO: Type parameter defaults | ^^^^^^^^^^^ - | help: Remove `Generic` base class RUF053 Class with type parameter list inherits from `Generic` @@ -272,7 +265,6 @@ RUF053 Class with type parameter list inherits from `Generic` 65 | T # Comment 66 | ](Generic[_E]): ... # TODO: Type parameter defaults | ^^^^^^^^^^^ - | help: Remove `Generic` base class RUF053 Class with type parameter list inherits from `Generic` @@ -338,7 +330,6 @@ RUF053 Class with type parameter list inherits from `Generic` 73 | class C[T](Generic[Unpack[*_As]]): ... 74 | class C[T](Generic[Unpack[_As, _Bs]]): ... | ^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove `Generic` base class RUF053 [*] Class with type parameter list inherits from `Generic` @@ -382,7 +373,6 @@ RUF053 [*] Class with type parameter list inherits from `Generic` 78 | class C[T](Generic[_A, Unpack[_As]]): ... 79 | class C[T](Generic[*_As, _A]): ... | ^^^^^^^^^^^^^^^^^ - | help: Remove `Generic` base class | 78 | class C[T](Generic[_A, Unpack[_As]]): ... @@ -409,7 +399,6 @@ RUF053 Class with type parameter list inherits from `Generic` 83 | class C[T](Generic[APublicTypeVar]): ... 84 | class C[T](Generic[APublicTypeVar, _A]): ... | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove `Generic` base class RUF053 [*] Class with type parameter list inherits from `Generic` @@ -419,7 +408,6 @@ RUF053 [*] Class with type parameter list inherits from `Generic` 90 | # See also the `_Z` example above. 91 | class C[T](Generic[_G]): ... # Should be moved down below eventually | ^^^^^^^^^^^ - | help: Remove `Generic` base class | 90 | # See also the `_Z` example above. @@ -453,7 +441,6 @@ RUF053 [*] Class with type parameter list inherits from `Generic` 95 | class C[T: (str,)](Generic[_A]): ... 96 | class C[T: [a]](Generic[_A]): ... | ^^^^^^^^^^^ - | help: Remove `Generic` base class | 95 | class C[T: (str,)](Generic[_A]): ... 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 2fa91d5f49..e5b79cbc85 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 @@ -438,7 +438,6 @@ RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test posi 190 | d = {} 191 | not d.get("key", (False)) | ^^^^^ - | help: Remove falsy fallback from `dict.get()` | 190 | d = {} diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF058_RUF058_0.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF058_RUF058_0.py.snap index 89bb8efba8..ef85a191e3 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF058_RUF058_0.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF058_RUF058_0.py.snap @@ -24,7 +24,6 @@ RUF058 [*] `itertools.starmap` called on `zip` iterable 7 | starmap(func, zip()) 8 | starmap(func, zip([])) | ^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `map` instead | 7 | starmap(func, zip()) @@ -38,7 +37,6 @@ RUF058 [*] `itertools.starmap` called on `zip` iterable | 11 | starmap(func, zip(a, b, c,),) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `map` instead | 10 | @@ -183,7 +181,6 @@ RUF058 [*] `itertools.starmap` called on `zip` iterable 59 | | ) 60 | | ) | |_^ - | help: Use `map` instead | 50 | 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 89691e783e..94b7f281df 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 @@ -44,7 +44,6 @@ RUF059 [*] Unpacked variable `x` is never used 25 | 26 | (x, y) = baz = bar | ^ - | help: Prefix it with an underscore or any other dummy variable pattern | 25 | @@ -61,7 +60,6 @@ RUF059 [*] Unpacked variable `y` is never used 25 | 26 | (x, y) = baz = bar | ^ - | help: Prefix it with an underscore or any other dummy variable pattern | 25 | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_1.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_1.py.snap index cd37d69f41..4878184931 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_1.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_1.py.snap @@ -7,7 +7,6 @@ RUF059 [*] Unpacked variable `x` is never used 1 | def f(tup): 2 | x, y = tup | ^ - | help: Prefix it with an underscore or any other dummy variable pattern | 1 | def f(tup): @@ -23,7 +22,6 @@ RUF059 [*] Unpacked variable `y` is never used 1 | def f(tup): 2 | x, y = tup | ^ - | help: Prefix it with an underscore or any other dummy variable pattern | 1 | def f(tup): @@ -57,7 +55,6 @@ RUF059 [*] Unpacked variable `x` is never used 15 | def f(): 16 | (x, y) = coords = 1, 2 | ^ - | help: Prefix it with an underscore or any other dummy variable pattern | 15 | def f(): @@ -73,7 +70,6 @@ RUF059 [*] Unpacked variable `y` is never used 15 | def f(): 16 | (x, y) = coords = 1, 2 | ^ - | help: Prefix it with an underscore or any other dummy variable pattern | 15 | def f(): @@ -89,7 +85,6 @@ RUF059 [*] Unpacked variable `x` is never used 19 | def f(): 20 | coords = (x, y) = 1, 2 | ^ - | help: Prefix it with an underscore or any other dummy variable pattern | 19 | def f(): @@ -105,7 +100,6 @@ RUF059 [*] Unpacked variable `y` is never used 19 | def f(): 20 | coords = (x, y) = 1, 2 | ^ - | help: Prefix it with an underscore or any other dummy variable pattern | 19 | def f(): diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_2.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_2.py.snap index b8533e337a..02a790a6f2 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_2.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_2.py.snap @@ -80,7 +80,6 @@ RUF059 [*] Unpacked variable `x3` is never used 17 | (x2, y2) = coords2 = (1, 2) 18 | coords3 = (x3, y3) = (1, 2) | ^^ - | help: Prefix it with an underscore or any other dummy variable pattern | 17 | (x2, y2) = coords2 = (1, 2) @@ -97,7 +96,6 @@ RUF059 [*] Unpacked variable `y3` is never used 17 | (x2, y2) = coords2 = (1, 2) 18 | coords3 = (x3, y3) = (1, 2) | ^^ - | help: Prefix it with an underscore or any other dummy variable pattern | 17 | (x2, y2) = coords2 = (1, 2) @@ -147,7 +145,6 @@ RUF059 [*] Unpacked variable `a` is never used 26 | def f(): 27 | toplevel = (a, b) = lexer.get_token() | ^ - | help: Prefix it with an underscore or any other dummy variable pattern | 26 | def f(): @@ -163,7 +160,6 @@ RUF059 [*] Unpacked variable `b` is never used 26 | def f(): 27 | toplevel = (a, b) = lexer.get_token() | ^ - | help: Prefix it with an underscore or any other dummy variable pattern | 26 | def f(): @@ -179,7 +175,6 @@ RUF059 [*] Unpacked variable `a` is never used 30 | def f(): 31 | (a, b) = toplevel = lexer.get_token() | ^ - | help: Prefix it with an underscore or any other dummy variable pattern | 30 | def f(): @@ -194,7 +189,6 @@ RUF059 [*] Unpacked variable `b` is never used 30 | def f(): 31 | (a, b) = toplevel = lexer.get_token() | ^ - | help: Prefix it with an underscore or any other dummy variable pattern | 30 | def f(): diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_3.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_3.py.snap index aceb3e4c67..9897ceafd1 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_3.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_3.py.snap @@ -8,7 +8,6 @@ RUF059 [*] Unpacked variable `b` is never used 12 | a = foo() 13 | b, c = foo() | ^ - | help: Prefix it with an underscore or any other dummy variable pattern | 12 | a = foo() @@ -25,7 +24,6 @@ RUF059 [*] Unpacked variable `c` is never used 12 | a = foo() 13 | b, c = foo() | ^ - | help: Prefix it with an underscore or any other dummy variable pattern | 12 | a = foo() 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 index da3217bed8..e55888ceaf 100644 --- 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 @@ -7,7 +7,6 @@ RUF061 [*] Use context-manager form of `pytest.deprecated_call()` 15 | def test_error_trivial(): 16 | pytest.deprecated_call(raise_deprecation_warning, "deprecated") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `pytest.deprecated_call()` as a context-manager | 15 | def test_error_trivial(): @@ -42,7 +41,6 @@ RUF061 [*] Use context-manager form of `pytest.deprecated_call()` 24 | def test_error_lambda(): 25 | pytest.deprecated_call(lambda: warnings.warn("", DeprecationWarning)) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `pytest.deprecated_call()` as a context-manager | 24 | def test_error_lambda(): 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 index 269443d770..64e072723c 100644 --- 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 @@ -7,7 +7,6 @@ RUF061 [*] Use context-manager form of `pytest.raises()` 18 | def test_error_trivial(): 19 | pytest.raises(ZeroDivisionError, func, 1, b=0) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `pytest.raises()` as a context-manager | 18 | def test_error_trivial(): @@ -24,7 +23,6 @@ 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") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `pytest.raises()` as a context-manager | 22 | def test_error_match(): @@ -41,7 +39,6 @@ RUF061 [*] Use context-manager form of `pytest.raises()` 26 | def test_error_assign(): 27 | excinfo = pytest.raises(ZeroDivisionError, func, 1, b=0) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `pytest.raises()` as a context-manager | 26 | def test_error_assign(): @@ -58,7 +55,6 @@ RUF061 [*] Use context-manager form of `pytest.raises()` 30 | def test_error_kwargs(): 31 | pytest.raises(func=func, expected_exception=ZeroDivisionError) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `pytest.raises()` as a context-manager | 30 | def test_error_kwargs(): @@ -93,7 +89,6 @@ RUF061 [*] Use context-manager form of `pytest.raises()` 39 | def test_error_lambda(): 40 | pytest.raises(ZeroDivisionError, lambda: 1 / 0) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `pytest.raises()` as a context-manager | 39 | def test_error_lambda(): 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 index 41b1112254..36dbf41a5f 100644 --- 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 @@ -7,7 +7,6 @@ RUF061 [*] Use context-manager form of `pytest.warns()` 15 | def test_error_trivial(): 16 | pytest.warns(UserWarning, raise_user_warning, "warning") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `pytest.warns()` as a context-manager | 15 | def test_error_trivial(): @@ -42,7 +41,6 @@ RUF061 [*] Use context-manager form of `pytest.warns()` 24 | def test_error_lambda(): 25 | pytest.warns(UserWarning, lambda: warnings.warn("", UserWarning)) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `pytest.warns()` as a context-manager | 24 | def test_error_lambda(): diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF065_RUF065_0.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF065_RUF065_0.py.snap index 9ac438216a..1d36fd3102 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF065_RUF065_0.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF065_RUF065_0.py.snap @@ -158,7 +158,6 @@ RUF065 Unnecessary `hex()` conversion when formatting with `%s`. Use `%#x` inste 56 | logging.info("Hex: %s", hex(42)) 57 | logging.warning("Hex: %s", hex(255)) | ^^^^^^^^ - | RUF065 Unnecessary `ascii()` conversion when formatting with `%s`. Use `%a` instead of `%s` --> RUF065_0.py:63:19 @@ -216,4 +215,3 @@ RUF065 Unnecessary `hex()` conversion when formatting with `%s`. Use `%#x` inste 69 | info("Hex: %s", hex(42)) 70 | log(logging.INFO, "Hex: %s", hex(255)) | ^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF065_RUF065_1.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF065_RUF065_1.py.snap index 56fa9ec243..a6d84c6e30 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF065_RUF065_1.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF065_RUF065_1.py.snap @@ -7,4 +7,3 @@ RUF065 Unnecessary `str()` conversion when formatting with `%s` 16 | # str() with single keyword argument - should be flagged (equivalent to str("!")) 17 | logging.warning("%s", str(object="!")) | ^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF067_RUF067__modules____init__.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF067_RUF067__modules____init__.py.snap index e0318db251..00372ae7fa 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF067_RUF067__modules____init__.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF067_RUF067__modules____init__.py.snap @@ -18,7 +18,6 @@ RUF067 `__init__` module should only contain docstrings and re-exports 14 | 15 | os.environ["FOO"] = 1 | ^^^^^^^^^^^^^^^^^^^^^ - | RUF067 `__init__` module should only contain docstrings and re-exports --> __init__.py:18:1 diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF068_RUF068.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF068_RUF068.py.snap index 8858058a32..a93caed1c4 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF068_RUF068.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF068_RUF068.py.snap @@ -2,7 +2,7 @@ source: crates/ruff_linter/src/rules/ruff/mod.rs --- RUF068 [*] `__all__` contains duplicate entries - --> RUF068.py:15:15 + --> RUF068.py:15:20 | 13 | __all__: typing.Any = ("A", "B") 14 | __all__ = ["A", "B"] @@ -22,7 +22,7 @@ help: Remove duplicate entries from `__all__` | RUF068 [*] `__all__` contains duplicate entries - --> RUF068.py:20:23 + --> RUF068.py:20:33 | 19 | # Bad 20 | __all__: list[str] = ["A", "B", "A"] @@ -41,7 +41,7 @@ help: Remove duplicate entries from `__all__` | RUF068 [*] `__all__` contains duplicate entries - --> RUF068.py:21:29 + --> RUF068.py:21:34 | 19 | # Bad 20 | __all__: list[str] = ["A", "B", "A"] @@ -61,7 +61,7 @@ help: Remove duplicate entries from `__all__` | RUF068 [*] `__all__` contains duplicate entries - --> RUF068.py:22:12 + --> RUF068.py:22:22 | 20 | __all__: list[str] = ["A", "B", "A"] 21 | __all__: typing.Any = ("A", "B", "B") @@ -81,7 +81,7 @@ help: Remove duplicate entries from `__all__` | RUF068 [*] `__all__` contains duplicate entries - --> RUF068.py:23:12 + --> RUF068.py:23:17 | 21 | __all__: typing.Any = ("A", "B", "B") 22 | __all__ = ["A", "B", "A"] @@ -101,7 +101,7 @@ help: Remove duplicate entries from `__all__` | RUF068 [*] `__all__` contains duplicate entries - --> RUF068.py:23:22 + --> RUF068.py:23:27 | 21 | __all__: typing.Any = ("A", "B", "B") 22 | __all__ = ["A", "B", "A"] @@ -121,7 +121,7 @@ help: Remove duplicate entries from `__all__` | RUF068 [*] `__all__` contains duplicate entries - --> RUF068.py:25:5 + --> RUF068.py:26:5 | 23 | __all__ = ["A", "A", "B", "B"] 24 | __all__ = [ @@ -140,7 +140,7 @@ help: Remove duplicate entries from `__all__` | RUF068 [*] `__all__` contains duplicate entries - --> RUF068.py:27:5 + --> RUF068.py:28:5 | 25 | "A", 26 | "A", @@ -159,7 +159,7 @@ help: Remove duplicate entries from `__all__` | RUF068 [*] `__all__` contains duplicate entries - --> RUF068.py:30:13 + --> RUF068.py:30:18 | 28 | "B" 29 | ] @@ -178,7 +178,7 @@ help: Remove duplicate entries from `__all__` | RUF068 [*] `__all__` contains duplicate entries - --> RUF068.py:31:17 + --> RUF068.py:31:22 | 29 | ] 30 | __all__ += ["B", "B"] @@ -198,7 +198,7 @@ help: Remove duplicate entries from `__all__` | RUF068 [*] `__all__` contains duplicate entries - --> RUF068.py:35:5 + --> RUF068.py:36:5 | 33 | # Bad, unsafe 34 | __all__ = [ @@ -217,7 +217,7 @@ help: Remove duplicate entries from `__all__` | RUF068 [*] `__all__` contains duplicate entries - --> RUF068.py:37:5 + --> RUF068.py:39:5 | 35 | "A", 36 | "A", diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF101_RUF101_0.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF101_RUF101_0.py.snap index a028111f87..2a62da7cde 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF101_RUF101_0.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF101_RUF101_0.py.snap @@ -94,7 +94,6 @@ RUF101 [*] `RUF940` is a redirect to `RUF950` 5 | x = 2 # noqa: RUF940, RUF950, RUF940 6 | x = 2 # noqa: RUF940, RUF950, RUF940, RUF950 | ^^^^^^ - | help: Replace with `RUF950` | 5 | x = 2 # noqa: RUF940, RUF950, RUF940 @@ -109,7 +108,6 @@ RUF101 [*] `RUF940` is a redirect to `RUF950` 5 | x = 2 # noqa: RUF940, RUF950, RUF940 6 | x = 2 # noqa: RUF940, RUF950, RUF940, RUF950 | ^^^^^^ - | help: Replace with `RUF950` | 5 | x = 2 # noqa: RUF940, RUF950, RUF940 diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF102_RUF102.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF102_RUF102.py.snap index 80d1704213..1f70be69ba 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF102_RUF102.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF102_RUF102.py.snap @@ -216,7 +216,6 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID123 21 | # Invalid code with trailing reason (single comment) 22 | import pathlib # noqa: INVALID123 some reason | ^^^^^^^^^^^^^^^^^^ - | help: Add non-Ruff rule codes to the `lint.external` configuration option help: Remove the `# noqa` comment | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF200_bleach.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF200_bleach.snap index 069209c9aa..e3aee0ad72 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF200_bleach.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF200_bleach.snap @@ -13,4 +13,3 @@ tinycss2>=1.1.0<1.2 6 | | "tinycss2>=1.1.0<1.2", 7 | | ] | |_^ - | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF200_invalid_author.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF200_invalid_author.snap index 112f023679..d5e4839215 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF200_invalid_author.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF200_invalid_author.snap @@ -11,4 +11,3 @@ RUF200 Failed to parse pyproject.toml: a table with 'name' and/or 'email' keys 6 | | { name = "Z͑ͫ̓ͪ̂ͫ̽͏̴̙̤̞͉͚̯̞̠͍A̴̵̜̰͔ͫ͗͢L̠ͨͧͩ͘G̴̻͈͍͔̹̑͗̎̅͛́Ǫ̵̹̻̝̳͂̌̌͘", email = 1 } 7 | | ] | |_^ - | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__add_future_import_RUF013_4.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__add_future_import_RUF013_4.py.snap index 1f5dd54e86..b84b79359b 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__add_future_import_RUF013_4.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__add_future_import_RUF013_4.py.snap @@ -6,7 +6,6 @@ RUF013 [*] PEP 484 prohibits implicit `Optional` | 15 | def multiple_2(arg1: Optional, arg2: Optional = None, arg3: int = None): ... | ^^^ - | help: Convert to `T | None` | 2 | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__confusables_deferred_annotations_diff.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__confusables_deferred_annotations_diff.snap index 3411053a0d..7f7cbe7e4e 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__confusables_deferred_annotations_diff.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__confusables_deferred_annotations_diff.snap @@ -16,4 +16,3 @@ RUF001 String contains ambiguous `ﮨ` (ARABIC LETTER HEH GOAL INITIAL FORM). Di 60 | from typing import Literal 61 | x: '''"""'Literal["ﮨ"]'"""''' | ^ - | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules_ruff__RUF102.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules_ruff__RUF102.py.snap index 430399d76a..2320e73ff9 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules_ruff__RUF102.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules_ruff__RUF102.py.snap @@ -178,7 +178,6 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID123 21 | # Invalid code with trailing reason (single comment) 22 | import pathlib # noqa: INVALID123 some reason | ^^^^^^^^^^^^^^^^^^ - | help: Add non-Ruff rule codes to the `lint.external` configuration option help: Remove the `# noqa` comment | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__missing_fstring_syntax_backslash_py311.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__missing_fstring_syntax_backslash_py311.snap index f061820b4d..8b3cb6371b 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__missing_fstring_syntax_backslash_py311.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__missing_fstring_syntax_backslash_py311.snap @@ -20,7 +20,6 @@ RUF027 [*] Possible f-string without an `f` prefix 42 | | c} d 43 | | """ | |_______^ - | help: Add `f` prefix | 40 | # RUF027 @@ -41,7 +40,6 @@ RUF027 [*] Possible f-string without an `f` prefix 50 | | a} \ 51 | | " | |_____^ - | help: Add `f` prefix | 48 | # RUF027 diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__noqa.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__noqa.snap index 98bd42a03e..d8164e2f94 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__noqa.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__noqa.snap @@ -8,7 +8,6 @@ E741 Ambiguous variable name: `I` 23 | # logged to user 24 | I = 1 # noqa: E741.F841 | ^ - | F841 [*] Local variable `I` is assigned to but never used --> noqa.py:24:5 @@ -17,7 +16,6 @@ F841 [*] Local variable `I` is assigned to but never used 23 | # logged to user 24 | I = 1 # noqa: E741.F841 | ^ - | help: Remove assignment to unused variable `I` | 23 | # logged to user diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF008_RUF008.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF008_RUF008.py.snap index 04357c897d..075d151388 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF008_RUF008.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF008_RUF008.py.snap @@ -52,7 +52,6 @@ RUF008 Do not use mutable default values for dataclass attributes 35 | perfectly_fine: 'list[int]' = field(default_factory=list) 36 | class_variable: 'typing.ClassVar[list[int]]'= [] | ^^ - | RUF008 Do not use mutable default values for dataclass attributes --> RUF008.py:42:48 diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF012_RUF012_basedpython.by.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF012_RUF012_basedpython.by.snap index 5bc7bd2323..a53ea68766 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF012_RUF012_basedpython.by.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF012_RUF012_basedpython.by.snap @@ -18,5 +18,4 @@ RUF012 Mutable default value for class attribute 18 | var items: list[int] = [] # RUF012 19 | let names: list[str] = [] # RUF012 | ^^ - | help: Consider initializing in `__init__` or annotating with `typing.ClassVar` diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF039_RUF039.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF039_RUF039.py.snap index 38ceb0533e..81ec14c221 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF039_RUF039.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF039_RUF039.py.snap @@ -315,7 +315,6 @@ RUF039 [*] First argument to `regex.template()` is not raw string 28 | | l(?i:ne) 29 | | """, flags = regex.X) | |___^ - | help: Replace with raw string | 24 | @@ -435,5 +434,4 @@ RUF039 First argument to `re.compile()` is not raw string | ____________^ 68 | | b") # without fix | |__^ - | help: Replace with raw string diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF039_RUF039_concat.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF039_RUF039_concat.py.snap index 8e808d77fc..a2cb085e40 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF039_RUF039_concat.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF039_RUF039_concat.py.snap @@ -189,7 +189,6 @@ RUF039 [*] First argument to `re.subn()` is not raw string 37 | ) 38 | re.subn("()"r' am I'"??") | ^^^^ - | help: Replace with raw string | 37 | ) @@ -205,7 +204,6 @@ RUF039 [*] First argument to `re.subn()` is not raw string 37 | ) 38 | re.subn("()"r' am I'"??") | ^^^^ - | help: Replace with raw string | 37 | ) @@ -402,7 +400,6 @@ RUF039 [*] First argument to `regex.subn()` is not raw string 77 | ) 78 | regex.subn("()"r' am I'"??") | ^^^^ - | help: Replace with raw string | 77 | ) @@ -418,7 +415,6 @@ RUF039 [*] First argument to `regex.subn()` is not raw string 77 | ) 78 | regex.subn("()"r' am I'"??") | ^^^^ - | help: Replace with raw string | 77 | ) diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF050_RUF050_basedpython.by.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF050_RUF050_basedpython.by.snap index 27fdec0bfe..ba6ba54956 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF050_RUF050_basedpython.by.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF050_RUF050_basedpython.by.snap @@ -8,7 +8,6 @@ RUF050 [*] Empty `if` statement 18 | / if f(): 19 | | pass | |________^ - | help: Remove the `if` statement | 17 | # a plain condition is still reported diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF054_RUF054.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF054_RUF054.py.snap index 26ed4fcb97..487a17671c 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF054_RUF054.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF054_RUF054.py.snap @@ -6,15 +6,14 @@ RUF054 Indented form feed | 6 | # Errors 7 | -8 | +8 | ␌ | ^ - | help: Remove form feed RUF054 Indented form feed --> RUF054.py:10:3 | -10 | +10 | ␌ | ^ 11 | 12 | def _(): @@ -25,7 +24,7 @@ RUF054 Indented form feed --> RUF054.py:13:2 | 12 | def _(): -13 | pass +13 | ␌ pass | ^ 14 | 15 | if False: @@ -37,7 +36,6 @@ RUF054 Indented form feed | 15 | if False: 16 | print('F') -17 | print('T') +17 | ␌print('T') | ^ - | help: Remove form feed diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_0.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_0.py.snap index c1fd12536f..ac6a7f9132 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_0.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_0.py.snap @@ -7,7 +7,6 @@ RUF055 [*] Plain string pattern passed to `re` function 5 | # this should be replaced with `s.replace("abc", "")` 6 | re.sub("abc", "", s) | ^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `s.replace("abc", "")` | 5 | # this should be replaced with `s.replace("abc", "")` diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_1.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_1.py.snap index d09b20edaf..2d02880aef 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_1.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_1.py.snap @@ -26,7 +26,6 @@ RUF055 [*] Plain string pattern passed to `re` function 16 | repl = "new" 17 | re.sub(r"abc", repl, haystack) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `haystack.replace(r"abc", repl)` | 16 | repl = "new" diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_2.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_2.py.snap index 9befa8492c..65f47a6a8d 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_2.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_2.py.snap @@ -7,7 +7,6 @@ RUF055 [*] Plain string pattern passed to `re` function 6 | # this should be replaced with `"abc" not in s` 7 | re.search("abc", s) is None | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `"abc" not in s` | 6 | # this should be replaced with `"abc" not in s` @@ -22,7 +21,6 @@ RUF055 [*] Plain string pattern passed to `re` function 10 | # this should be replaced with `"abc" in s` 11 | re.search("abc", s) is not None | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `"abc" in s` | 10 | # this should be replaced with `"abc" in s` @@ -37,7 +35,6 @@ RUF055 [*] Plain string pattern passed to `re` function 14 | # this should be replaced with `not s.startswith("abc")` 15 | re.match("abc", s) is None | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `not s.startswith("abc")` | 14 | # this should be replaced with `not s.startswith("abc")` @@ -52,7 +49,6 @@ RUF055 [*] Plain string pattern passed to `re` function 18 | # this should be replaced with `s.startswith("abc")` 19 | re.match("abc", s) is not None | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `s.startswith("abc")` | 18 | # this should be replaced with `s.startswith("abc")` @@ -67,7 +63,6 @@ RUF055 [*] Plain string pattern passed to `re` function 22 | # this should be replaced with `s != "abc"` 23 | re.fullmatch("abc", s) is None | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `s != "abc"` | 22 | # this should be replaced with `s != "abc"` @@ -82,7 +77,6 @@ RUF055 [*] Plain string pattern passed to `re` function 26 | # this should be replaced with `s == "abc"` 27 | re.fullmatch("abc", s) is not None | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `s == "abc"` | 26 | # this should be replaced with `s == "abc"` diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF069_RUF069.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF069_RUF069.py.snap index 0818caa012..39486a6364 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF069_RUF069.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF069_RUF069.py.snap @@ -39,7 +39,6 @@ RUF069 Unreliable floating point equality comparison `x == 0.42` 14 | if x == 0.3: ... 15 | if x == 0.42: ... | ^^^^^^^^^^^^^^^ - | RUF069 Unreliable floating point equality comparison `a == b - 0.1` --> RUF069.py:19:12 @@ -235,4 +234,3 @@ RUF069 Unreliable floating point equality comparison `0.3 if x > 0 else 1 == 0.1 47 | 48 | assert (0.3 if x > 0 else 1) == 0.1 + 0.2 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF070_RUF070.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF070_RUF070.py.snap index 1e156ce0dd..a7233c248b 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF070_RUF070.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF070_RUF070.py.snap @@ -248,7 +248,6 @@ RUF070 [*] Unnecessary assignment to `x` before `yield` statement 62 | x = f.read() 63 | yield x # RUF070 | ^ - | help: Remove unnecessary assignment | 61 | with open("foo.txt") as f: diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF072_RUF072.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF072_RUF072.py.snap index 65b54304a8..fbb131c113 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF072_RUF072.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF072_RUF072.py.snap @@ -314,12 +314,11 @@ help: Remove the `finally` clause RUF072 [*] Empty `finally` clause --> RUF072.py:178:1 | -176 | 1 +176 | ␌ 1 177 | 2 178 | / finally: 179 | | pass | |________^ - | help: Remove the `finally` clause | 174 | # Bare try finally with line starting with a formfeed @@ -336,12 +335,11 @@ help: Remove the `finally` clause RUF072 [*] Empty `finally` clause --> RUF072.py:186:1 | -184 | try: +184 | ␌try: 185 | 1 186 | / finally: 187 | | pass | |________^ - | help: Remove the `finally` clause | 183 | # (`try` is preceded by a form feed below) diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__py37__RUF039_RUF039_py_version_sensitive.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__py37__RUF039_RUF039_py_version_sensitive.py.snap index 58f3127ce7..63463f809f 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__py37__RUF039_RUF039_py_version_sensitive.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__py37__RUF039_RUF039_py_version_sensitive.py.snap @@ -8,5 +8,4 @@ RUF039 First argument to `re.compile()` is not raw string 2 | 3 | re.compile("\N{Partial Differential}") # with unsafe fix if python target is 3.8 or higher, else without fix | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with raw string diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__py38__RUF039_RUF039_py_version_sensitive.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__py38__RUF039_RUF039_py_version_sensitive.py.snap index 2c96acbb9e..db5e3c79f8 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__py38__RUF039_RUF039_py_version_sensitive.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__py38__RUF039_RUF039_py_version_sensitive.py.snap @@ -8,7 +8,6 @@ RUF039 [*] First argument to `re.compile()` is not raw string 2 | 3 | re.compile("\N{Partial Differential}") # with unsafe fix if python target is 3.8 or higher, else without fix | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with raw string | 2 | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__py314__RUF058_RUF058_2.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__py314__RUF058_RUF058_2.py.snap index 298d4b2eb8..3762a50a11 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__py314__RUF058_RUF058_2.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__py314__RUF058_RUF058_2.py.snap @@ -42,7 +42,6 @@ RUF058 [*] `itertools.starmap` called on `zip` iterable 6 | starmap(func, zip(a, b, c, strict=False)) 7 | starmap(func, zip(a, b, c, strict=strict)) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `map` instead | 6 | starmap(func, zip(a, b, c, strict=False)) diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__range_suppressions.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__range_suppressions.snap index 1c4cea3779..c65ef006ef 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__range_suppressions.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__range_suppressions.snap @@ -46,7 +46,6 @@ RUF103 [*] Invalid suppression comment: no matching 'disable' comment 18 | I = 1 19 | # ruff: enable[E741, F841] | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove suppression comment | 18 | I = 1 @@ -111,7 +110,6 @@ RUF100 [*] Unused suppression (non-enabled: `E501`) 47 | I = 1 48 | # ruff: enable[E501] | -------------------- - | help: Remove unused suppression | 45 | # An unused suppression diagnostic should also be logged. @@ -302,7 +300,6 @@ RUF102 [*] Invalid rule code in suppression: YF829 96 | # ruff: enable[F841, RQW320] 97 | # ruff: enable[YF829] | ----- - | help: Add non-Ruff rule codes to the `lint.external` configuration option help: Remove the suppression comment | @@ -381,7 +378,6 @@ F841 [*] Local variable `bar` is assigned to but never used 117 | foo = 0 118 | bar = 0 | ^^^ - | help: Remove assignment to unused variable `bar` | 117 | foo = 0 @@ -397,7 +393,6 @@ F841 [*] Local variable `bar` is assigned to but never used 123 | foo = 0 # ruff: ignore[F841] 124 | bar = 0 | ^^^ - | help: Remove assignment to unused variable `bar` | 123 | foo = 0 # ruff: ignore[F841] @@ -413,7 +408,6 @@ F841 [*] Local variable `bar` is assigned to but never used 131 | """ # ruff: ignore[F841] 132 | bar = 0 | ^^^ - | help: Remove assignment to unused variable `bar` | 131 | """ # ruff: ignore[F841] @@ -487,7 +481,6 @@ RUF100 [*] Unused suppression (non-enabled: `F401`) 176 | print("goodbye") 177 | # ruff:enable[F401] | ------------------- - | help: Remove unused suppression | 174 | # https://github.com/astral-sh/ruff/issues/23235 diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_0.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_0.snap index 1d99fa8755..9925381435 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_0.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_0.snap @@ -209,7 +209,6 @@ E501 Line too long (89 > 88) 92 | 93 | "shape: (6,)\nSeries: '' [duration[μs]]\n[\n\t0µs\n\t1µs\n\t2µs\n\t3µs\n\t4µs\n\t5µs\n]" # noqa: F401 | ^ - | RUF100 [*] Unused `noqa` directive (unused: `F401`) --> RUF100_0.py:93:92 @@ -218,7 +217,6 @@ RUF100 [*] Unused `noqa` directive (unused: `F401`) 92 | 93 | "shape: (6,)\nSeries: '' [duration[μs]]\n[\n\t0µs\n\t1µs\n\t2µs\n\t3µs\n\t4µs\n\t5µs\n]" # noqa: F401 | ^^^^^^^^^^^^ - | help: Remove unused `noqa` directive | 92 | @@ -234,7 +232,6 @@ F841 [*] Local variable `e` is assigned to but never used 107 | d = 1 # …noqa: F841, E50 108 | e = 1 # …noqa: E50 | ^ - | help: Remove assignment to unused variable `e` | 107 | d = 1 # …noqa: F841, E50 @@ -339,7 +336,6 @@ RUF100 [*] Unused `noqa` directive (duplicated: `PGH001`, `S307`) 130 | x = eval(command) # noqa: PGH001, S307, PGH001 131 | x = eval(command) # noqa: PGH001, S307, PGH001, S307 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove unused `noqa` directive | 130 | x = eval(command) # noqa: PGH001, S307, PGH001 diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_0_prefix.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_0_prefix.snap index 1d99fa8755..9925381435 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_0_prefix.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_0_prefix.snap @@ -209,7 +209,6 @@ E501 Line too long (89 > 88) 92 | 93 | "shape: (6,)\nSeries: '' [duration[μs]]\n[\n\t0µs\n\t1µs\n\t2µs\n\t3µs\n\t4µs\n\t5µs\n]" # noqa: F401 | ^ - | RUF100 [*] Unused `noqa` directive (unused: `F401`) --> RUF100_0.py:93:92 @@ -218,7 +217,6 @@ RUF100 [*] Unused `noqa` directive (unused: `F401`) 92 | 93 | "shape: (6,)\nSeries: '' [duration[μs]]\n[\n\t0µs\n\t1µs\n\t2µs\n\t3µs\n\t4µs\n\t5µs\n]" # noqa: F401 | ^^^^^^^^^^^^ - | help: Remove unused `noqa` directive | 92 | @@ -234,7 +232,6 @@ F841 [*] Local variable `e` is assigned to but never used 107 | d = 1 # …noqa: F841, E50 108 | e = 1 # …noqa: E50 | ^ - | help: Remove assignment to unused variable `e` | 107 | d = 1 # …noqa: F841, E50 @@ -339,7 +336,6 @@ RUF100 [*] Unused `noqa` directive (duplicated: `PGH001`, `S307`) 130 | x = eval(command) # noqa: PGH001, S307, PGH001 131 | x = eval(command) # noqa: PGH001, S307, PGH001, S307 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove unused `noqa` directive | 130 | x = eval(command) # noqa: PGH001, S307, PGH001 diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_1.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_1.snap index a362758258..653a8ea302 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_1.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_1.snap @@ -95,7 +95,6 @@ F401 [*] `typing.Awaitable` imported but unused 88 | # This should mark F501 as unused. 89 | from typing import Awaitable, AwaitableGenerator # noqa: F501 | ^^^^^^^^^ - | help: Remove unused import | 88 | # This should mark F501 as unused. @@ -110,7 +109,6 @@ F401 [*] `typing.AwaitableGenerator` imported but unused 88 | # This should mark F501 as unused. 89 | from typing import Awaitable, AwaitableGenerator # noqa: F501 | ^^^^^^^^^^^^^^^^^^ - | help: Remove unused import | 88 | # This should mark F501 as unused. @@ -125,7 +123,6 @@ RUF100 [*] Unused `noqa` directive (non-enabled: `F501`) 88 | # This should mark F501 as unused. 89 | from typing import Awaitable, AwaitableGenerator # noqa: F501 | ^^^^^^^^^^^^ - | help: Remove unused `noqa` directive | 88 | # This should mark F501 as unused. 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 60decc6cd4..3b2f4a86dc 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 @@ -6,7 +6,6 @@ RUF100 [*] Unused `noqa` directive (non-enabled: `F401`) | 1 | import itertools # noqa: F401 | ^^^^^^^^^^^^ - | help: Remove unused `noqa` directive | - import itertools # noqa: F401 diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_3.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_3.snap index ae760882c1..aa8e5eb958 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_3.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_3.snap @@ -422,7 +422,6 @@ RUF100 [*] Unused `noqa` directive (unused: `E501`) 30 | print(a) # comment with unicode µ # noqa: E501 31 | print(a) # comment with unicode µ # noqa: E501, F821 | ^^^^^^^^^^^^^^^^^^ - | help: Remove unused `noqa` directive | 30 | print(a) # comment with unicode µ # noqa: E501 diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_5.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_5.snap index 6422ffed19..2edd512091 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_5.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_5.snap @@ -75,7 +75,6 @@ RUF100 [*] Unused `noqa` directive (non-enabled: `RET504`) 20 | # line below should autofix to `return data` 21 | return data # noqa: RET504 - intentional incorrect noqa, will be removed | ^^^^^^^^^^^^^^ - | help: Remove unused `noqa` directive | 20 | # line below should autofix to `return data` diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_codes.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_codes.snap index 42f0dfd342..6faada6c7b 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_codes.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_codes.snap @@ -7,7 +7,6 @@ F841 [*] Local variable `x` is assigned to but never used 7 | def f(): 8 | x = 1 | ^ - | help: Remove assignment to unused variable `x` | 7 | def f(): diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_filedirective_unused.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_filedirective_unused.snap index e22cbd8e0a..f1c33a1592 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_filedirective_unused.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_filedirective_unused.snap @@ -6,7 +6,6 @@ RUF100 [*] Unused `noqa` directive (non-enabled: `F841`) | 1 | # ruff: noqa: F841 -- intentional unused file directive; will be removed | ^^^^^^^^^^^^^^^^^^ - | help: Remove unused `noqa` directive | - # ruff: noqa: F841 -- intentional unused file directive; will be removed diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_filedirective_unused_last_of_many.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_filedirective_unused_last_of_many.snap index 4a0b525f25..3c138f9c1d 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_filedirective_unused_last_of_many.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_filedirective_unused_last_of_many.snap @@ -21,7 +21,6 @@ RUF100 [*] Unused `noqa` directive (non-enabled: `E701`) 1 | # flake8: noqa: F841, E501 -- used followed by unused code 2 | # ruff: noqa: E701, F541 -- unused followed by used code | ^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove unused `noqa` directive | 1 | # flake8: noqa: F841, E501 -- used followed by unused code diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_invalid.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_invalid.snap index c127d4c20f..04b50369b3 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_invalid.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_invalid.snap @@ -6,7 +6,6 @@ F401 [*] `os` imported but unused | 1 | import os # ruff: noqa: F401 | ^^ - | help: Remove unused import: `os` | - import os # ruff: noqa: F401 @@ -19,7 +18,6 @@ F841 [*] Local variable `x` is assigned to but never used 4 | def f(): 5 | x = 1 | ^ - | help: Remove assignment to unused variable `x` | 4 | def f(): diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__strictly_empty_init_modules_ruf067.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__strictly_empty_init_modules_ruf067.snap index 682497466e..d5e32a443f 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__strictly_empty_init_modules_ruf067.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__strictly_empty_init_modules_ruf067.snap @@ -28,7 +28,6 @@ RUF067 `__init__` module should only contain docstrings and re-exports 14 | 15 | os.environ["FOO"] = 1 | ^^^^^^^^^^^^^^^^^^^^^ - | RUF067 `__init__` module should only contain docstrings and re-exports @@ -186,7 +185,6 @@ RUF067 `__init__` module should not contain any code 14 | 15 | os.environ["FOO"] = 1 | ^^^^^^^^^^^^^^^^^^^^^ - | RUF067 `__init__` module should not contain any code @@ -421,4 +419,3 @@ RUF067 `__init__` module should not contain any code 57 | # also allow `__author__` 58 | __author__ = "The Author" # ok | ^^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__unnecessary_if_and_needless_else.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__unnecessary_if_and_needless_else.snap index 6f3f9fc005..e8ed80b873 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__unnecessary_if_and_needless_else.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__unnecessary_if_and_needless_else.snap @@ -47,7 +47,6 @@ RUF047 [*] Empty `else` clause 21 | / else: 22 | | pass | |________^ - | help: Remove the `else` clause | 20 | pass diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__useless_finally_and_needless_else.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__useless_finally_and_needless_else.snap index c3a43a1770..b8824b9cea 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__useless_finally_and_needless_else.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__useless_finally_and_needless_else.snap @@ -85,7 +85,6 @@ RUF072 [*] Empty `finally` clause 29 | / finally: 30 | | pass | |________^ - | help: Remove the `finally` clause | 28 | baz() diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__useless_finally_and_needless_else_and_suppressible_exception.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__useless_finally_and_needless_else_and_suppressible_exception.snap index f619ab2c8a..d6b9448753 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__useless_finally_and_needless_else_and_suppressible_exception.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__useless_finally_and_needless_else_and_suppressible_exception.snap @@ -28,7 +28,6 @@ RUF072 [*] Empty `finally` clause 9 | / finally: 10 | | pass | |________^ - | help: Remove the `finally` clause | 8 | pass diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__useless_finally_and_suppressible_exception.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__useless_finally_and_suppressible_exception.snap index c0ff748c42..845be65e12 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__useless_finally_and_suppressible_exception.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__useless_finally_and_suppressible_exception.snap @@ -9,7 +9,6 @@ RUF072 [*] Empty `finally` clause 8 | / finally: 9 | | pass | |________^ - | help: Remove the `finally` clause | 7 | pass diff --git a/crates/ruff_linter/src/rules/ruff/typing.rs b/crates/ruff_linter/src/rules/ruff/typing.rs index 0c1b355860..22ebaa401e 100644 --- a/crates/ruff_linter/src/rules/ruff/typing.rs +++ b/crates/ruff_linter/src/rules/ruff/typing.rs @@ -253,9 +253,8 @@ pub(crate) fn type_hint_explicitly_allows_none<'a>( version: ast::PythonVersion, ) -> Option<&'a Expr> { match TypingTarget::try_from_expr(annotation, checker, version) { - None | - // Short circuit on top level `None`, `Any` or `Optional` - Some(TypingTarget::None | TypingTarget::Optional(_) | TypingTarget::Any) => None, + // Short-circuit on top level `None`, `Any` or `Optional` + None | Some(TypingTarget::None | TypingTarget::Optional(_) | TypingTarget::Any) => None, // Top-level `Annotated` node should check for the inner type and // return the inner type if it doesn't allow `None`. If `Annotated` // is found nested inside another type, then the outer type should diff --git a/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__error-instead-of-exception_TRY400.py.snap b/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__error-instead-of-exception_TRY400.py.snap index 67c8f11db9..51a8680efa 100644 --- a/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__error-instead-of-exception_TRY400.py.snap +++ b/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__error-instead-of-exception_TRY400.py.snap @@ -25,7 +25,6 @@ TRY400 [*] Use `logging.exception` instead of `logging.error` 15 | if True: 16 | logging.error("Context message here") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `exception` | 15 | if True: @@ -59,7 +58,6 @@ TRY400 [*] Use `logging.exception` instead of `logging.error` 29 | if True: 30 | logger.error("Context message here") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `exception` | 29 | if True: @@ -94,7 +92,6 @@ TRY400 [*] Use `logging.exception` instead of `logging.error` 39 | if True: 40 | log.error("Context message here") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `exception` | 39 | if True: @@ -129,7 +126,6 @@ TRY400 [*] Use `logging.exception` instead of `logging.error` 49 | if True: 50 | self.logger.error("Context message here") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `exception` | 49 | if True: @@ -163,7 +159,6 @@ TRY400 [*] Use `logging.exception` instead of `logging.error` 102 | if True: 103 | error("Context message here") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `exception` | 102 | if True: @@ -179,7 +174,6 @@ TRY400 [*] Use `logging.exception` instead of `logging.error` 142 | except Exception: 143 | error("Context message here") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `exception` | 142 | except Exception: diff --git a/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__raise-vanilla-args_TRY003.py.snap b/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__raise-vanilla-args_TRY003.py.snap index c9cbe9dd20..b312410cec 100644 --- a/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__raise-vanilla-args_TRY003.py.snap +++ b/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__raise-vanilla-args_TRY003.py.snap @@ -19,7 +19,6 @@ TRY003 Avoid specifying long messages outside the exception class 33 | if a % 2 == 0: 34 | raise BadArgCantBeEven(f"The argument '{a}' should be even") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | TRY003 Avoid specifying long messages outside the exception class --> TRY003.py:39:15 @@ -28,7 +27,6 @@ TRY003 Avoid specifying long messages outside the exception class 38 | if a % 2 == 0: 39 | raise BadArgCantBeEven(f"The argument {a} should not be odd.") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | TRY003 Avoid specifying long messages outside the exception class --> TRY003.py:44:15 @@ -37,4 +35,3 @@ TRY003 Avoid specifying long messages outside the exception class 43 | if a % 2 == 0: 44 | raise BadArgCantBeEven("The argument `a` should not be odd.") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__raise-vanilla-class_TRY002.py.snap b/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__raise-vanilla-class_TRY002.py.snap index a781966a17..87e6ca16ae 100644 --- a/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__raise-vanilla-class_TRY002.py.snap +++ b/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__raise-vanilla-class_TRY002.py.snap @@ -19,7 +19,6 @@ TRY002 Create your own exception 16 | if b == 1: 17 | raise Exception | ^^^^^^^^^ - | TRY002 Create your own exception --> TRY002.py:37:15 @@ -39,4 +38,3 @@ TRY002 Create your own exception 40 | if b == 1: 41 | raise BaseException | ^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__type-check-without-type-error_TRY004.py.snap b/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__type-check-without-type-error_TRY004.py.snap index 893ee904d6..c44177b542 100644 --- a/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__type-check-without-type-error_TRY004.py.snap +++ b/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__type-check-without-type-error_TRY004.py.snap @@ -8,7 +8,6 @@ TRY004 Prefer `TypeError` exception for invalid type 11 | else: 12 | raise Exception("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:19:9 @@ -17,7 +16,6 @@ TRY004 Prefer `TypeError` exception for invalid type 18 | else: 19 | raise Exception("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:30:9 @@ -26,7 +24,6 @@ TRY004 Prefer `TypeError` exception for invalid type 29 | else: 30 | raise Exception("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:37:9 @@ -35,7 +32,6 @@ TRY004 Prefer `TypeError` exception for invalid type 36 | else: 37 | raise Exception("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:44:9 @@ -44,7 +40,6 @@ TRY004 Prefer `TypeError` exception for invalid type 43 | else: 44 | raise ArithmeticError("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:51:9 @@ -53,7 +48,6 @@ TRY004 Prefer `TypeError` exception for invalid type 50 | else: 51 | raise AssertionError("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:58:9 @@ -62,7 +56,6 @@ TRY004 Prefer `TypeError` exception for invalid type 57 | else: 58 | raise AttributeError("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:65:9 @@ -71,7 +64,6 @@ TRY004 Prefer `TypeError` exception for invalid type 64 | else: 65 | raise BufferError # should be typeerror | ^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:72:9 @@ -80,7 +72,6 @@ TRY004 Prefer `TypeError` exception for invalid type 71 | else: 72 | raise EOFError("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:79:9 @@ -89,7 +80,6 @@ TRY004 Prefer `TypeError` exception for invalid type 78 | else: 79 | raise ImportError("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:86:9 @@ -98,7 +88,6 @@ TRY004 Prefer `TypeError` exception for invalid type 85 | else: 86 | raise LookupError("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:95:9 @@ -109,7 +98,6 @@ TRY004 Prefer `TypeError` exception for invalid type 96 | | "..." 97 | | ) | |_________^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:104:9 @@ -118,7 +106,6 @@ TRY004 Prefer `TypeError` exception for invalid type 103 | else: 104 | raise NameError("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:111:9 @@ -127,7 +114,6 @@ TRY004 Prefer `TypeError` exception for invalid type 110 | else: 111 | raise ReferenceError("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:118:9 @@ -136,7 +122,6 @@ TRY004 Prefer `TypeError` exception for invalid type 117 | else: 118 | raise RuntimeError("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:125:9 @@ -145,7 +130,6 @@ TRY004 Prefer `TypeError` exception for invalid type 124 | else: 125 | raise SyntaxError("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:132:9 @@ -154,7 +138,6 @@ TRY004 Prefer `TypeError` exception for invalid type 131 | else: 132 | raise SystemError("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:139:9 @@ -163,7 +146,6 @@ TRY004 Prefer `TypeError` exception for invalid type 138 | else: 139 | raise ValueError("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:146:9 @@ -172,7 +154,6 @@ TRY004 Prefer `TypeError` exception for invalid type 145 | else: 146 | raise Exception("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:153:9 @@ -181,7 +162,6 @@ TRY004 Prefer `TypeError` exception for invalid type 152 | else: 153 | raise Exception("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:160:9 @@ -190,7 +170,6 @@ TRY004 Prefer `TypeError` exception for invalid type 159 | else: 160 | raise Exception("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:167:9 @@ -199,7 +178,6 @@ TRY004 Prefer `TypeError` exception for invalid type 166 | else: 167 | raise Exception("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:174:9 @@ -208,7 +186,6 @@ TRY004 Prefer `TypeError` exception for invalid type 173 | else: 174 | raise Exception("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:181:9 @@ -217,7 +194,6 @@ TRY004 Prefer `TypeError` exception for invalid type 180 | else: 181 | raise Exception("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:188:9 @@ -226,7 +202,6 @@ TRY004 Prefer `TypeError` exception for invalid type 187 | else: 188 | raise Exception("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:195:9 @@ -235,7 +210,6 @@ TRY004 Prefer `TypeError` exception for invalid type 194 | else: 195 | raise Exception("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:202:9 @@ -244,7 +218,6 @@ TRY004 Prefer `TypeError` exception for invalid type 201 | else: 202 | raise Exception("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:209:9 @@ -253,7 +226,6 @@ TRY004 Prefer `TypeError` exception for invalid type 208 | else: 209 | raise Exception("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:216:9 @@ -262,7 +234,6 @@ TRY004 Prefer `TypeError` exception for invalid type 215 | else: 216 | raise Exception("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:223:9 @@ -271,7 +242,6 @@ TRY004 Prefer `TypeError` exception for invalid type 222 | else: 223 | raise Exception("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:230:9 @@ -280,7 +250,6 @@ TRY004 Prefer `TypeError` exception for invalid type 229 | elif isinstance(arg2, int): 230 | raise Exception("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:239:9 @@ -289,7 +258,6 @@ TRY004 Prefer `TypeError` exception for invalid type 238 | else: 239 | raise Exception("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:276:9 @@ -298,7 +266,6 @@ TRY004 Prefer `TypeError` exception for invalid type 275 | if isinstance(some_args, int): 276 | raise ValueError("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:286:9 @@ -329,4 +296,3 @@ TRY004 Prefer `TypeError` exception for invalid type 315 | else: 316 | raise Exception(f"Unknown object type: {obj.__class__.__name__}") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__verbose-log-message_TRY401.py.snap b/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__verbose-log-message_TRY401.py.snap index 87a8944016..6958981373 100644 --- a/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__verbose-log-message_TRY401.py.snap +++ b/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__verbose-log-message_TRY401.py.snap @@ -8,7 +8,6 @@ TRY401 Redundant exception object included in `logging.exception` call 7 | except Exception as ex: 8 | logger.exception(f"Found an error: {ex}") # TRY401 | ^^ - | TRY401 Redundant exception object included in `logging.exception` call --> TRY401.py:19:53 @@ -70,7 +69,6 @@ TRY401 Redundant exception object included in `logging.exception` call 26 | if True: 27 | logger.exception(f"Found an error: {bad}") # TRY401 | ^^^ - | TRY401 Redundant exception object included in `logging.exception` call --> TRY401.py:39:47 @@ -79,7 +77,6 @@ TRY401 Redundant exception object included in `logging.exception` call 38 | except Exception as ex: 39 | logger.exception(f"Logging an error: {ex}") # TRY401 | ^^ - | TRY401 Redundant exception object included in `logging.exception` call --> TRY401.py:46:53 @@ -88,7 +85,6 @@ TRY401 Redundant exception object included in `logging.exception` call 45 | except Exception as ex: 46 | logger.exception("Logging an error: " + str(ex)) # TRY401 | ^^ - | TRY401 Redundant exception object included in `logging.exception` call --> TRY401.py:53:47 @@ -97,7 +93,6 @@ TRY401 Redundant exception object included in `logging.exception` call 52 | except Exception as ex: 53 | logger.exception("Logging an error:", ex) # TRY401 | ^^ - | TRY401 Redundant exception object included in `logging.exception` call --> TRY401.py:77:38 @@ -106,7 +101,6 @@ TRY401 Redundant exception object included in `logging.exception` call 76 | except Exception as ex: 77 | exception(f"Found an error: {ex}") # TRY401 | ^^ - | TRY401 Redundant exception object included in `logging.exception` call --> TRY401.py:88:46 @@ -168,7 +162,6 @@ TRY401 Redundant exception object included in `logging.exception` call 95 | if True: 96 | exception(f"Found an error: {bad}") # TRY401 | ^^^ - | TRY401 Redundant exception object included in `logging.exception` call --> TRY401.py:103:40 @@ -177,7 +170,6 @@ TRY401 Redundant exception object included in `logging.exception` call 102 | except Exception as ex: 103 | exception(f"Logging an error: {ex}") # TRY401 | ^^ - | TRY401 Redundant exception object included in `logging.exception` call --> TRY401.py:110:46 @@ -186,7 +178,6 @@ TRY401 Redundant exception object included in `logging.exception` call 109 | except Exception as ex: 110 | exception("Logging an error: " + str(ex)) # TRY401 | ^^ - | TRY401 Redundant exception object included in `logging.exception` call --> TRY401.py:117:40 @@ -195,7 +186,6 @@ TRY401 Redundant exception object included in `logging.exception` call 116 | except Exception as ex: 117 | exception("Logging an error:", ex) # TRY401 | ^^ - | TRY401 Redundant exception object included in `logging.exception` call --> TRY401.py:139:49 @@ -204,7 +194,6 @@ TRY401 Redundant exception object included in `logging.exception` call 138 | except Exception as ex: 139 | logger.exception(f"Found an error: {ex}") # TRY401 | ^^ - | TRY401 Redundant exception object included in `logging.exception` call --> TRY401.py:150:49 @@ -213,4 +202,3 @@ TRY401 Redundant exception object included in `logging.exception` call 149 | except Exception: 150 | logger.exception(f"Found an error: {ex}") # TRY401 | ^^ - | diff --git a/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__verbose-raise_TRY201.py.snap b/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__verbose-raise_TRY201.py.snap index f40a9fda63..0560eb3a08 100644 --- a/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__verbose-raise_TRY201.py.snap +++ b/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__verbose-raise_TRY201.py.snap @@ -8,7 +8,6 @@ TRY201 [*] Use `raise` without specifying exception name 19 | logger.exception("process failed") 20 | raise e | ^ - | help: Remove exception name | 19 | logger.exception("process failed") @@ -25,7 +24,6 @@ TRY201 [*] Use `raise` without specifying exception name 62 | if True: 63 | raise e | ^ - | help: Remove exception name | 62 | if True: @@ -41,7 +39,6 @@ TRY201 [*] Use `raise` without specifying exception name 73 | def foo(): 74 | raise e | ^ - | help: Remove exception name | 73 | def foo(): diff --git a/crates/ruff_linter/src/settings/fix_safety_table.rs b/crates/ruff_linter/src/settings/fix_safety_table.rs index 2f7f68f901..6b3ad65b11 100644 --- a/crates/ruff_linter/src/settings/fix_safety_table.rs +++ b/crates/ruff_linter/src/settings/fix_safety_table.rs @@ -21,7 +21,7 @@ pub struct FixSafetyTable { } impl FixSafetyTable { - pub const fn resolve_applicability( + pub(crate) const fn resolve_applicability( &self, rule: Rule, applicability: Applicability, @@ -41,10 +41,6 @@ impl FixSafetyTable { } } - pub const fn is_empty(&self) -> bool { - self.forced_safe.is_empty() && self.forced_unsafe.is_empty() - } - pub fn from_rule_selectors( extend_safe_fixes: &[UnresolvedRuleSelector], extend_unsafe_fixes: &[UnresolvedRuleSelector], diff --git a/crates/ruff_linter/src/settings/mod.rs b/crates/ruff_linter/src/settings/mod.rs index 49e9b291a3..10e0c13b60 100644 --- a/crates/ruff_linter/src/settings/mod.rs +++ b/crates/ruff_linter/src/settings/mod.rs @@ -9,12 +9,11 @@ use std::path::{Path, PathBuf}; use std::sync::LazyLock; use types::CompiledPerFileTargetVersionList; -use crate::codes::RuleCodePrefix; use ruff_macros::CacheKey; use ruff_python_ast::{PythonVersion, is_destructure_binder}; use crate::line_width::LineLength; -use crate::registry::{Linter, Rule}; +use crate::registry::Rule; use crate::rules::{ flake8_annotations, flake8_bandit, flake8_boolean_trap, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_copyright, flake8_errmsg, flake8_gettext, @@ -23,7 +22,7 @@ use crate::rules::{ pep8_naming, pycodestyle, pydoclint, pydocstyle, pyflakes, pylint, pyupgrade, ruff, }; use crate::settings::types::{CompiledPerFileIgnoreList, ExtensionMapping, FilePatternSet}; -use crate::{RuleSelector, codes, fs}; +use crate::{RuleSelector, fs}; use super::line_width::IndentWidth; @@ -354,25 +353,8 @@ impl Display for LinterSettings { } } -pub const DEFAULT_SELECTORS: &[RuleSelector] = &[ - RuleSelector::Linter(Linter::Pyflakes), - // Only include pycodestyle rules that do not overlap with the formatter - RuleSelector::Prefix { - prefix: RuleCodePrefix::Pycodestyle(codes::Pycodestyle::E4), - redirected_from: None, - }, - RuleSelector::Prefix { - prefix: RuleCodePrefix::Pycodestyle(codes::Pycodestyle::E7), - redirected_from: None, - }, - RuleSelector::Prefix { - prefix: RuleCodePrefix::Pycodestyle(codes::Pycodestyle::E9), - redirected_from: None, - }, -]; - #[rustfmt::skip] -pub const PREVIEW_DEFAULT_SELECTORS: &[RuleSelector] = &[ +pub const DEFAULT_SELECTORS: &[RuleSelector] = &[ RuleSelector::rule(Rule::CancelScopeNoCheckpoint), // ASYNC100 RuleSelector::rule(Rule::TrioSyncCall), // ASYNC105 RuleSelector::rule(Rule::AsyncZeroSleep), // ASYNC115 @@ -505,6 +487,7 @@ pub const PREVIEW_DEFAULT_SELECTORS: &[RuleSelector] = &[ RuleSelector::rule(Rule::ImplicitCwd), // FURB177 RuleSelector::rule(Rule::HashlibDigestHex), // FURB181 RuleSelector::rule(Rule::SliceToRemovePrefixOrSuffix), // FURB188 + RuleSelector::rule(Rule::SortedMinMax), // FURB192 RuleSelector::rule(Rule::LoggingWarn), // G010 RuleSelector::rule(Rule::LoggingExtraAttrClash), // G101 RuleSelector::rule(Rule::LoggingExcInfo), // G201 @@ -513,6 +496,7 @@ pub const PREVIEW_DEFAULT_SELECTORS: &[RuleSelector] = &[ RuleSelector::rule(Rule::FStringInGetTextFuncCall), // INT001 RuleSelector::rule(Rule::FormatInGetTextFuncCall), // INT002 RuleSelector::rule(Rule::PrintfInGetTextFuncCall), // INT003 + RuleSelector::rule(Rule::ImplicitStringConcatenationInCollectionLiteral), // ISC004 RuleSelector::rule(Rule::DirectLoggerInstantiation), // LOG001 RuleSelector::rule(Rule::InvalidGetLoggerArgument), // LOG002 RuleSelector::rule(Rule::UndocumentedWarn), // LOG009 @@ -546,6 +530,7 @@ pub const PREVIEW_DEFAULT_SELECTORS: &[RuleSelector] = &[ RuleSelector::rule(Rule::NonlocalWithoutBinding), // PLE0117 RuleSelector::rule(Rule::LoadBeforeGlobalDeclaration), // PLE0118 RuleSelector::rule(Rule::InvalidLengthReturnType), // PLE0303 + RuleSelector::rule(Rule::InvalidBoolReturnType), // PLE0304 RuleSelector::rule(Rule::InvalidIndexReturnType), // PLE0305 RuleSelector::rule(Rule::InvalidStrReturnType), // PLE0307 RuleSelector::rule(Rule::InvalidBytesReturnType), // PLE0308 @@ -576,6 +561,7 @@ pub const PREVIEW_DEFAULT_SELECTORS: &[RuleSelector] = &[ RuleSelector::rule(Rule::PropertyWithParameters), // PLR0206 RuleSelector::rule(Rule::ManualFromImport), // PLR0402 RuleSelector::rule(Rule::RedefinedArgumentFromLocal), // PLR1704 + RuleSelector::rule(Rule::StopIterationReturn), // PLR1708 RuleSelector::rule(Rule::UselessReturn), // PLR1711 RuleSelector::rule(Rule::BooleanChainedComparison), // PLR1716 RuleSelector::rule(Rule::SysExitAlias), // PLR1722 @@ -690,6 +676,8 @@ pub const PREVIEW_DEFAULT_SELECTORS: &[RuleSelector] = &[ RuleSelector::rule(Rule::UnnecessaryRound), // RUF057 RuleSelector::rule(Rule::StarmapZip), // RUF058 RuleSelector::rule(Rule::UnusedUnpackedVariable), // RUF059 + RuleSelector::rule(Rule::AccessAnnotationsFromClassDict), // RUF063 + RuleSelector::rule(Rule::DuplicateEntryInDunderAll), // RUF068 RuleSelector::rule(Rule::UnusedNOQA), // RUF100 RuleSelector::rule(Rule::RedirectedNOQA), // RUF101 RuleSelector::rule(Rule::InvalidPyprojectToml), // RUF200 diff --git a/crates/ruff_linter/src/settings/rule_table.rs b/crates/ruff_linter/src/settings/rule_table.rs index 9f7a6e2ec1..69d2fad7b8 100644 --- a/crates/ruff_linter/src/settings/rule_table.rs +++ b/crates/ruff_linter/src/settings/rule_table.rs @@ -36,7 +36,7 @@ impl RuleTable { /// Returns whether violations of the given rule should be fixed. #[inline] - pub const fn should_fix(&self, rule: Rule) -> bool { + pub(crate) const fn should_fix(&self, rule: Rule) -> bool { self.should_fix.contains(rule) } diff --git a/crates/ruff_linter/src/settings/types.rs b/crates/ruff_linter/src/settings/types.rs index 13f8f3555e..8fd9a70d4e 100644 --- a/crates/ruff_linter/src/settings/types.rs +++ b/crates/ruff_linter/src/settings/types.rs @@ -184,10 +184,6 @@ impl GlobPath { let absolute = fs::normalize_path_to(path, escaped); Self { path: absolute } } - - pub fn into_inner(self) -> PathBuf { - self.path - } } impl Deref for GlobPath { @@ -505,13 +501,13 @@ impl ExtensionMapping { } /// Return the [`Language`] for the given file. - pub fn get(&self, path: &Path) -> Option { + fn get(&self, path: &Path) -> Option { let ext = path.extension()?.to_str()?; self.0.get(ext).copied() } /// Return the [`Language`] for a given file extension. - pub fn get_extension(&self, ext: &str) -> Option { + fn get_extension(&self, ext: &str) -> Option { self.0.get(ext).copied() } @@ -709,14 +705,14 @@ impl IdentifierPattern { } } - pub fn matches(&self, candidate: &str) -> bool { + pub(crate) fn matches(&self, candidate: &str) -> bool { match self { Self::Literal(literal) => literal == candidate, Self::Glob(pattern) => pattern.matches(candidate), } } - pub fn as_str(&self) -> &str { + pub(crate) fn as_str(&self) -> &str { match self { Self::Literal(literal) => literal, Self::Glob(pattern) => pattern.as_str(), @@ -741,10 +737,10 @@ impl FromStr for IdentifierPattern { /// Like [`PerFile`] but with string globs compiled to [`GlobMatcher`]s for more efficient usage. #[derive(Debug, Clone)] pub struct CompiledPerFile { - pub absolute_matcher: GlobMatcher, - pub basename_matcher: GlobMatcher, - pub negated: bool, - pub data: T, + absolute_matcher: GlobMatcher, + basename_matcher: GlobMatcher, + negated: bool, + data: T, } impl CompiledPerFile { 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 index e39895d9c6..4d6b3dfb9b 100644 --- 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 @@ -31,4 +31,3 @@ PLE1142 `await` should be used within an async function 10 | / async for _ in elements(1): 11 | | pass | |____________^ - | 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 14c1cea362..fed430c44b 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 @@ -8,4 +8,3 @@ invalid-syntax: cannot use an asynchronous comprehension inside of a synchronous 2 | [x async for x in elements(5)] # okay, async at top level 3 | [[x async for x in elements(5)] for i in range(5)] # error on 3.10, okay after | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__await_scope_notebook.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__await_scope_notebook.snap index 2038653a11..858407cf45 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__await_scope_notebook.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__await_scope_notebook.snap @@ -7,4 +7,3 @@ F704 `await` statement outside of a function 1 | class _: 2 | await 1 # SyntaxError: await outside function | ^^^^^^^ - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__import_sorting.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__import_sorting.snap index b788f7b1fb..9d888d9697 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__import_sorting.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__import_sorting.snap @@ -8,7 +8,6 @@ I001 [*] Import block is un-sorted or un-formatted 2 | | import random 3 | | import math | |___________^ - | help: Organize imports ::: cell 1 | @@ -65,7 +64,6 @@ I001 [*] Import block is un-sorted or un-formatted 7 | / import math 8 | | import abc | |__________^ - | help: Organize imports ::: cell 3 | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__ipy_escape_command.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__ipy_escape_command.snap index 1ddd61a96a..646b1f26c4 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__ipy_escape_command.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__ipy_escape_command.snap @@ -25,7 +25,6 @@ F401 [*] `sys` imported but unused 1 | %%timeit 2 | import sys | ^^^ - | help: Remove unused import: `sys` ::: cell 2 | 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 index 0274914bac..8d7121eb3c 100644 --- 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 @@ -7,4 +7,3 @@ 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 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__return_in_generator.py.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__return_in_generator.py.snap index 2abd1fad09..093c1340f1 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__return_in_generator.py.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__return_in_generator.py.snap @@ -52,4 +52,3 @@ B901 Using `yield` and `return {value}` in a generator function can lead to conf 23 | yield 1 24 | return 10 | ^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__return_outside_function.py.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__return_outside_function.py.snap index dd8114a6dc..cab2c63612 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__return_outside_function.py.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__return_outside_function.py.snap @@ -15,7 +15,6 @@ F706 `return` statement outside of a function/method 13 | return 1 # error 14 | return # error | ^^^^^^ - | F706 `return` statement outside of a function/method --> resources/test/fixtures/syntax_errors/return_outside_function.py:18:5 @@ -23,7 +22,6 @@ F706 `return` statement outside of a function/method 17 | class C: 18 | return 1 # error | ^^^^^^^^ - | F706 `return` statement outside of a function/method --> resources/test/fixtures/syntax_errors/return_outside_function.py:23:9 @@ -32,4 +30,3 @@ F706 `return` statement outside of a function/method 22 | class C: 23 | return 1 # error | ^^^^^^^^ - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_annotated_global.py_3.14.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_annotated_global.py_3.14.snap index bc8140b385..731ac3ee42 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_annotated_global.py_3.14.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_annotated_global.py_3.14.snap @@ -71,4 +71,3 @@ invalid-syntax: annotated name `x` can't be global 37 | global x # error 38 | x: str | ^ - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_duplicate_type_parameter.py_3.12.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_duplicate_type_parameter.py_3.12.snap index 9b3d052471..dc5d76e882 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_duplicate_type_parameter.py_3.12.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_duplicate_type_parameter.py_3.12.snap @@ -6,4 +6,3 @@ invalid-syntax: duplicate type parameter | 1 | class C[T, T]: pass | ^ - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_invalid_star_expression.py_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_invalid_star_expression.py_3.10.snap index f26a2d73a7..f04472006e 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_invalid_star_expression.py_3.10.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_invalid_star_expression.py_3.10.snap @@ -27,4 +27,3 @@ invalid-syntax: Starred expression cannot be used here 7 | def func(): 8 | yield *x | ^^ - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_rebound_comprehension.py_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_rebound_comprehension.py_3.10.snap index 91fa59a6fc..58b567adb0 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_rebound_comprehension.py_3.10.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_rebound_comprehension.py_3.10.snap @@ -25,4 +25,3 @@ invalid-syntax: assignment expression within a comprehension cannot be used in a 4 | class C: 5 | [(x := y) for y in range(3)] | ^^^^^^ - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_single_starred_assignment.py_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_single_starred_assignment.py_3.10.snap index 7c3cc3916b..04ce5f307b 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_single_starred_assignment.py_3.10.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_single_starred_assignment.py_3.10.snap @@ -6,4 +6,3 @@ invalid-syntax: 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__undefined_name.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__undefined_name.snap index 478ccbfead..949a99ac82 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__undefined_name.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__undefined_name.snap @@ -6,4 +6,3 @@ F821 Undefined name `undefined` | 1 | print(undefined) | ^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__unused_variable.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__unused_variable.snap index fc213dcdd2..de53bff608 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__unused_variable.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__unused_variable.snap @@ -26,7 +26,6 @@ F841 [*] Local variable `foo2` is assigned to but never used 2 | foo1 = %matplotlib --list 3 | foo2: list[str] = %matplotlib --list | ^^^^ - | help: Remove assignment to unused variable `foo2` ::: cell 1 | @@ -61,7 +60,6 @@ F841 [*] Local variable `bar2` is assigned to but never used 2 | bar1 = !pwd 3 | bar2: str = !pwd | ^^^^ - | help: Remove assignment to unused variable `bar2` ::: cell 2 | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__yield_from_in_async_function.py.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__yield_from_in_async_function.py.snap index ab60900419..a53ef73460 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__yield_from_in_async_function.py.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__yield_from_in_async_function.py.snap @@ -6,4 +6,3 @@ PLE1700 `yield from` statement in async function; use `async for` instead | 1 | async def f(): yield from x # error | ^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__yield_scope.py.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__yield_scope.py.snap index e32c627fce..09a503ee67 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__yield_scope.py.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__yield_scope.py.snap @@ -48,7 +48,6 @@ F704 `yield` statement outside of a function 4 | await 1 # error 5 | [(yield x) for x in range(3)] # error | ^^^^^^^ - | F704 `yield` statement outside of a function --> resources/test/fixtures/syntax_errors/yield_scope.py:23:9 @@ -110,7 +109,6 @@ F704 `yield` statement outside of a function 28 | {(yield 1): 0 for x in range(3)} # error 29 | {0: (yield 1) for x in range(3)} # error | ^^^^^^^ - | F704 `await` statement outside of a function --> resources/test/fixtures/syntax_errors/yield_scope.py:36:10 @@ -127,4 +125,3 @@ F704 `await` statement outside of a function | 41 | await 1 # error | ^^^^^^^ - | diff --git a/crates/ruff_linter/src/suppression.rs b/crates/ruff_linter/src/suppression.rs index 01fe547eb9..6c5853d5e2 100644 --- a/crates/ruff_linter/src/suppression.rs +++ b/crates/ruff_linter/src/suppression.rs @@ -19,7 +19,7 @@ use crate::checkers::ast::{DiagnosticGuard, LintContext}; use crate::codes::Rule; use crate::comments::shebang::leading_shebang_range; use crate::fix::edits::delete_comment; -use crate::preview::{is_human_readable_names_enabled, is_ruff_ignore_enabled}; +use crate::preview::is_human_readable_names_enabled; use crate::rule_redirects::get_redirect_target; use crate::rules::ruff::rules::{ InvalidRuleCode, InvalidRuleCodeKind, InvalidSuppressionComment, InvalidSuppressionCommentKind, @@ -28,17 +28,17 @@ use crate::rules::ruff::rules::{ }; use crate::settings::LinterSettings; use crate::settings::types::PreviewMode; -use crate::{Locator, Violation, warn_user_once}; +use crate::{Locator, Violation}; #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] enum SuppressionAction { - /// # ruff:file-ignore[...] file level suppression + /// # ruff: file-ignore[...] file level suppression FileIgnore, - /// # ruff:disable[...] start of a block suppression + /// # ruff: disable[...] start of a block suppression Disable, - /// # ruff:enable[...] end of a block suppression + /// # ruff: enable[...] end of a block suppression Enable, - /// # ruff:ignore[...] ignore a single line or multi-line statement + /// # ruff: ignore[...] ignore a single line or multi-line statement Ignore, } @@ -49,8 +49,8 @@ pub(crate) struct SuppressionComment { /// For example: /// /// ```py - /// import math # start # ruff:ignore[F401] reason # end - /// ^^^^^^^^^^^^^^^^^^^^^^^^^^ + /// import math # start # ruff: ignore[F401] reason # end + /// ^^^^^^^^^^^^^^^^^^^^^^^^^^^ /// ``` range: TextRange, @@ -59,8 +59,8 @@ pub(crate) struct SuppressionComment { /// For example: /// /// ```py - /// import math # start # ruff:ignore[F401] reason # end - /// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + /// import math # start # ruff: ignore[F401] reason # end + /// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ /// ``` token_range: TextRange, @@ -136,7 +136,7 @@ impl Suppression { &self.comments.first().codes } - /// Returns whether or not the suppression is a standalone `ruff:ignore` comment. + /// Returns whether or not the suppression is a standalone `ruff: ignore` comment. fn is_ignore(&self) -> bool { matches!( self.comments, @@ -149,7 +149,7 @@ impl Suppression { /// Returns whether the suppression's range applies to a diagnostic. /// - /// `ruff:ignore` comments only need to contain the start of the diagnostic range (or its + /// `ruff: ignore` comments only need to contain the start of the diagnostic range (or its /// parent), while range suppression comments must contain the entire diagnostic range. fn applies_to_diagnostic(&self, range: TextRange, parent: Option) -> bool { if self.is_ignore() { @@ -177,20 +177,20 @@ impl Suppression { #[derive(Debug)] pub(crate) enum SuppressionComments { - /// A #ruff:ignore comment, or #ruff:disable without a matching #ruff:enable + /// A # ruff: ignore comment, or # ruff: disable without a matching # ruff: enable Single(SuppressionComment), - /// A matching pair of #ruff:disable and #ruff:enable comments. + /// A matching pair of # ruff: disable and # ruff: enable comments. DisableEnable(SuppressionComment, SuppressionComment), } impl SuppressionComments { - pub(crate) fn first(&self) -> &SuppressionComment { + fn first(&self) -> &SuppressionComment { match self { SuppressionComments::Single(comment) => comment, SuppressionComments::DisableEnable(comment, _) => comment, } } - pub(crate) fn second(&self) -> Option<&SuppressionComment> { + fn second(&self) -> Option<&SuppressionComment> { match self { SuppressionComments::Single(_) => None, SuppressionComments::DisableEnable(_, comment) => Some(comment), @@ -309,28 +309,28 @@ impl Suppressions { /// the subscript expression: /// /// ```py - /// # ruff:disable[RUF015] + /// # ruff: disable[RUF015] /// value = [ /// *range(10) /// ][0] - /// # ruff:enable[RUF015] + /// # ruff: enable[RUF015] /// ``` /// /// is suppressed, but /// /// ```py - /// # ruff:disable[RUF015] + /// # ruff: disable[RUF015] /// value = [ - /// # ruff:enable[RUF015] + /// # ruff: enable[RUF015] /// *range(10) /// ][0] /// ``` /// - /// is not. For `ruff:ignore`, this rule is augmented to check whether the diagnostic's start + /// is not. For `ruff: ignore`, this rule is augmented to check whether the diagnostic's start /// offset is contained instead, meaning that this _will_ be suppressed: /// /// ```python - /// suppressed = [ # ruff:ignore[RUF015] + /// suppressed = [ # ruff: ignore[RUF015] /// *range(10) /// ][0] /// ``` @@ -703,7 +703,7 @@ impl Suppressions { } } -pub(crate) struct SuppressionsBuilder<'a> { +struct SuppressionsBuilder<'a> { source: &'a str, settings: &'a LinterSettings, @@ -714,7 +714,7 @@ pub(crate) struct SuppressionsBuilder<'a> { } impl<'a> SuppressionsBuilder<'a> { - pub(crate) fn new(source: &'a str, settings: &'a LinterSettings) -> Self { + fn new(source: &'a str, settings: &'a LinterSettings) -> Self { Self { source, settings, @@ -724,7 +724,7 @@ impl<'a> SuppressionsBuilder<'a> { } } - pub(crate) fn load_from_tokens(mut self, tokens: &Tokens, indexer: &Indexer) -> Suppressions { + fn load_from_tokens(mut self, tokens: &Tokens, indexer: &Indexer) -> Suppressions { let mut indents: Vec<&str> = vec![]; let mut errors = Vec::new(); @@ -861,7 +861,7 @@ impl<'a> SuppressionsBuilder<'a> { } } - /// Handles a single-comment suppression like `ruff:ignore` or `ruff:file-ignore` and returns + /// Handles a single-comment suppression like `ruff: ignore` or `ruff: file-ignore` and returns /// `true` if such a comment was found. fn register_standalone_suppression( &mut self, @@ -870,11 +870,9 @@ impl<'a> SuppressionsBuilder<'a> { ) -> bool { match suppression.action { SuppressionAction::Ignore => { - if is_ruff_ignore_enabled(self.settings) { - let (before, after) = tokens.split_at(suppression.token_range.start()); - let range = if indentation_at_offset(suppression.range.start(), self.source) - .is_some() - { + let (before, after) = tokens.split_at(suppression.token_range.start()); + let range = + if indentation_at_offset(suppression.range.start(), self.source).is_some() { // own-line ignore let mut range = Self::standalone_comment_range(suppression.range, before, after); @@ -891,55 +889,44 @@ impl<'a> SuppressionsBuilder<'a> { // trailing ignore self.trailing_comment_range(suppression.token_range, before) }; - for code in suppression.codes_as_str(self.source) { - self.valid.push(Suppression { - code: code.into(), - range, - used: false.into(), - comments: SuppressionComments::Single(suppression.clone()), - }); - } - } else { - warn_user_once!( - "#ruff:ignore comment found but not active, enable preview mode" - ); + for code in suppression.codes_as_str(self.source) { + self.valid.push(Suppression { + code: code.into(), + range, + used: false.into(), + comments: SuppressionComments::Single(suppression.clone()), + }); } true } SuppressionAction::FileIgnore => { - if is_ruff_ignore_enabled(self.settings) { - match indentation_at_offset(suppression.range.start(), self.source) { - // Module scope - Some("") => { - let range = TextRange::up_to(self.source.text_len()); - for code in suppression.codes_as_str(self.source) { - self.valid.push(Suppression { - code: code.into(), - range, - used: false.into(), - comments: SuppressionComments::Single(suppression.clone()), - }); - } - } - // Indented/inside block - Some(_) => { - self.invalid.push(InvalidSuppression { - kind: InvalidSuppressionKind::NotModuleScope, - comment: suppression.clone(), - }); - } - // Trailing - None => { - self.invalid.push(InvalidSuppression { - kind: InvalidSuppressionKind::Trailing, - comment: suppression.clone(), + match indentation_at_offset(suppression.range.start(), self.source) { + // Module scope + Some("") => { + let range = TextRange::up_to(self.source.text_len()); + for code in suppression.codes_as_str(self.source) { + self.valid.push(Suppression { + code: code.into(), + range, + used: false.into(), + comments: SuppressionComments::Single(suppression.clone()), }); } } - } else { - warn_user_once!( - "#ruff:file-ignore comment found but not active, enable preview mode" - ); + // Indented/inside block + Some(_) => { + self.invalid.push(InvalidSuppression { + kind: InvalidSuppressionKind::NotModuleScope, + comment: suppression.clone(), + }); + } + // Trailing + None => { + self.invalid.push(InvalidSuppression { + kind: InvalidSuppressionKind::Trailing, + comment: suppression.clone(), + }); + } } true } @@ -1021,7 +1008,7 @@ impl<'a> SuppressionsBuilder<'a> { /// ```py /// /// # V--- from here - /// # ruff:ignore[code] + /// # ruff: ignore[code] /// foo = [ /// 1, /// 2, @@ -1029,7 +1016,7 @@ impl<'a> SuppressionsBuilder<'a> { /// # ^--- to here /// /// # V--- from here - /// # ruff:ignore[code] + /// # ruff: ignore[code] /// def foo( /// arg1, /// arg2, @@ -1047,7 +1034,7 @@ impl<'a> SuppressionsBuilder<'a> { /// /// foo = [ /// # V--- from here - /// # ruff:ignore[code] + /// # ruff: ignore[code] /// 1, /// # ^--- to here /// 2, @@ -1112,13 +1099,13 @@ impl<'a> SuppressionsBuilder<'a> { /// /// ```py /// # V-- from here - /// foo = 1 # ruff:ignore[code] - /// # to here -----------------^ + /// foo = 1 # ruff: ignore[code] + /// # to here ------------------^ /// /// foo = [ /// # V--- from here - /// 1, # ruff:ignore[code] - /// # to here ------------^ + /// 1, # ruff: ignore[code] + /// # to here -------------^ /// ] /// ``` /// @@ -1130,8 +1117,8 @@ impl<'a> SuppressionsBuilder<'a> { /// # V--- from here /// value = """ /// some text - /// """ # ruff:ignore[code] - /// # to here -------------^ + /// """ # ruff: ignore[code] + /// # to here --------------^ /// /// ``` /// diff --git a/crates/ruff_linter/src/test.rs b/crates/ruff_linter/src/test.rs index 230776646a..21426c79ac 100644 --- a/crates/ruff_linter/src/test.rs +++ b/crates/ruff_linter/src/test.rs @@ -487,7 +487,6 @@ pub(crate) fn print_jupyter_messages( .format(DiagnosticFormat::Full) .hide_severity(true) .with_show_fix_status(true) - .show_fix_diff(true) .with_fix_applicability(Applicability::DisplayOnly); DisplayDiagnostics::new( @@ -506,7 +505,6 @@ pub(crate) fn print_messages(diagnostics: &[Diagnostic]) -> String { .format(DiagnosticFormat::Full) .hide_severity(true) .with_show_fix_status(true) - .show_fix_diff(true) .with_fix_applicability(Applicability::DisplayOnly); DisplayDiagnostics::new( diff --git a/crates/ruff_macros/Cargo.toml b/crates/ruff_macros/Cargo.toml index 3b0320b491..13d495f8bb 100644 --- a/crates/ruff_macros/Cargo.toml +++ b/crates/ruff_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_macros" -version = "0.0.5" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_macros/README.md b/crates/ruff_macros/README.md index 127dbd027c..ac925be274 100644 --- a/crates/ruff_macros/README.md +++ b/crates/ruff_macros/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_macros). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_macros). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_macros/src/rule_namespace.rs b/crates/ruff_macros/src/rule_namespace.rs index 0957bb2a85..fcb23e5b0b 100644 --- a/crates/ruff_macros/src/rule_namespace.rs +++ b/crates/ruff_macros/src/rule_namespace.rs @@ -38,18 +38,44 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result return Err(Error::new(lit.span(), "expected prefix string to be non-empty")), - Some(c) => if !first_chars.insert(c) { - return Err(Error::new(lit.span(), format!("this variant already has another prefix starting with the character '{c}'"))) + None => { + return Err(Error::new( + lit.span(), + "expected prefix string to be non-empty", + )); + } + Some(c) => { + if !first_chars.insert(c) { + return Err(Error::new( + lit.span(), + format!( + "this variant already has another prefix \ + starting with the character '{c}'" + ), + )); + } } } if !all_prefixes.insert(str.clone()) { - return Err(Error::new(lit.span(), "prefix has already been defined before")); + return Err(Error::new( + lit.span(), + "prefix has already been defined before", + )); } Ok(str) }) @@ -165,7 +191,8 @@ fn parse_doc_attr(doc_attr: &Attribute) -> syn::Result<(String, String)> { .ok_or_else(|| { Error::new( doc_lit.span(), - "expected doc comment to be in the form of `/// [name](https://example.com/)`", + "expected doc comment to be in the form of \ + `/// [name](https://example.com/)`", ) }) } diff --git a/crates/ruff_markdown/Cargo.toml b/crates/ruff_markdown/Cargo.toml index e7106dcf34..ec98be0bc9 100644 --- a/crates/ruff_markdown/Cargo.toml +++ b/crates/ruff_markdown/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_markdown" -version = "0.0.5" +version = "0.0.8" description = "This is an internal component crate of Ruff" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/ruff_markdown/README.md b/crates/ruff_markdown/README.md index f6007c1178..9320691af7 100644 --- a/crates/ruff_markdown/README.md +++ b/crates/ruff_markdown/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_markdown). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_markdown). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_mdtest/src/db.rs b/crates/ruff_mdtest/src/db.rs index 4770ee591f..402d0c606d 100644 --- a/crates/ruff_mdtest/src/db.rs +++ b/crates/ruff_mdtest/src/db.rs @@ -40,10 +40,6 @@ impl SourceDb for Db { fn files(&self) -> &Files { &self.files } - - fn python_version(&self) -> ruff_python_ast::PythonVersion { - ruff_python_ast::PythonVersion::latest() - } } #[salsa::db] diff --git a/crates/ruff_mdtest/src/lib.rs b/crates/ruff_mdtest/src/lib.rs index 3082d15a4c..ecd94bfbe3 100644 --- a/crates/ruff_mdtest/src/lib.rs +++ b/crates/ruff_mdtest/src/lib.rs @@ -149,9 +149,20 @@ fn run_test( }; normalize_diagnostics(test_file.file, &mut diagnostics); + let path = test_file + .file + .path(db) + .as_system_path() + .expect("mdtest files are on the system"); + let python_version = settings + .linter + .resolve_target_version(path.as_std_path()) + .parser_version(); + let failure = match matcher::match_file( db, test_file.file, + python_version, &diagnostics, mdtest::RunOptions::default(), ) diff --git a/crates/ruff_memory_usage/Cargo.toml b/crates/ruff_memory_usage/Cargo.toml index 5758dc8979..852b0e46b5 100644 --- a/crates/ruff_memory_usage/Cargo.toml +++ b/crates/ruff_memory_usage/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_memory_usage" -version = "0.0.5" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_memory_usage/README.md b/crates/ruff_memory_usage/README.md index 75700ae5d3..bdb69d27c9 100644 --- a/crates/ruff_memory_usage/README.md +++ b/crates/ruff_memory_usage/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_memory_usage). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_memory_usage). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_notebook/Cargo.toml b/crates/ruff_notebook/Cargo.toml index c5216cb7e2..477f3fa14c 100644 --- a/crates/ruff_notebook/Cargo.toml +++ b/crates/ruff_notebook/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_notebook" -version = "0.0.5" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_notebook/README.md b/crates/ruff_notebook/README.md index 1f01efbdc5..bcf6f56751 100644 --- a/crates/ruff_notebook/README.md +++ b/crates/ruff_notebook/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_notebook). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_notebook). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_notebook/src/notebook.rs b/crates/ruff_notebook/src/notebook.rs index 76f8fa854f..37d32214a4 100644 --- a/crates/ruff_notebook/src/notebook.rs +++ b/crates/ruff_notebook/src/notebook.rs @@ -443,10 +443,6 @@ impl Notebook { &self.raw.cells } - pub fn metadata(&self) -> &RawNotebookMetadata { - &self.raw.metadata - } - /// Check if it's a Python notebook. /// /// This is determined by checking the `language_info` or `kernelspec` in the notebook diff --git a/crates/ruff_options_metadata/Cargo.toml b/crates/ruff_options_metadata/Cargo.toml index 4e6b438efe..203bf5f5ce 100644 --- a/crates/ruff_options_metadata/Cargo.toml +++ b/crates/ruff_options_metadata/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_options_metadata" -version = "0.0.5" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_options_metadata/README.md b/crates/ruff_options_metadata/README.md index 4a425586f4..6ae0120c4a 100644 --- a/crates/ruff_options_metadata/README.md +++ b/crates/ruff_options_metadata/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_options_metadata). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_options_metadata). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_ast/Cargo.toml b/crates/ruff_python_ast/Cargo.toml index f838d6cc4d..f3f65747e1 100644 --- a/crates/ruff_python_ast/Cargo.toml +++ b/crates/ruff_python_ast/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_ast" -version = "0.0.5" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_ast/README.md b/crates/ruff_python_ast/README.md index 246cbdc200..77078685db 100644 --- a/crates/ruff_python_ast/README.md +++ b/crates/ruff_python_ast/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_python_ast). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_python_ast). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_ast/ast.toml b/crates/ruff_python_ast/ast.toml index 20360b8965..46ba64d331 100644 --- a/crates/ruff_python_ast/ast.toml +++ b/crates/ruff_python_ast/ast.toml @@ -477,8 +477,16 @@ fields = [ custom_source_order = true [Expr.nodes.ExprCall] -doc = "See also [Call](https://docs.python.org/3/library/ast.html#ast.Call)" +doc = """A call expression whose end offset is derived from its arguments. + +The parser and error-recovery code must ensure that the call and its arguments +end at the same offset. + +See also [Call](https://docs.python.org/3/library/ast.html#ast.Call)""" +custom_debug = true +custom_range = true fields = [ + { name = "range_start", type = "ruff_text_size::TextSize", skip_visit = true }, { name = "func", type = "Expr" }, { name = "arguments", type = "Arguments" }, { name = "is_cast", type = "bool", doc = """basedpython: when true, this call represents a ` cast ` diff --git a/crates/ruff_python_ast/generate.py b/crates/ruff_python_ast/generate.py index db33a644af..1c9ffa82d7 100644 --- a/crates/ruff_python_ast/generate.py +++ b/crates/ruff_python_ast/generate.py @@ -148,7 +148,9 @@ class Node: fields: list[Field] | None derives: list[str] attrs: list[str] + custom_debug: bool custom_source_order: bool + custom_range: bool source_order: list[str] | None def __init__(self, group: Group, node_name: str, node: dict[str, Any]) -> None: @@ -159,7 +161,9 @@ def __init__(self, group: Group, node_name: str, node: dict[str, Any]) -> None: fields = node.get("fields") if fields is not None: self.fields = [Field(f) for f in fields] + self.custom_debug = node.get("custom_debug", False) self.custom_source_order = node.get("custom_source_order", False) + self.custom_range = node.get("custom_range", False) self.derives = node.get("derives", []) self.attrs = node.get("attrs", []) self.doc = node.get("doc") @@ -463,6 +467,8 @@ def write_owned_enum(out: list[str], ast: Ast) -> None: out.append("}") for node in ast.all_nodes: + if node.custom_range: + continue out.append(f""" impl ruff_text_size::Ranged for {node.ty} {{ fn range(&self) -> ruff_text_size::TextRange {{ @@ -1052,7 +1058,9 @@ def write_node(out: list[str], ast: Ast) -> None: if node.doc is not None: write_rustdoc(out, node.doc) out.append( - "#[derive(Clone, Debug, PartialEq" + "#[derive(Clone" + + ("" if node.custom_debug else ", Debug") + + ", PartialEq" + "".join(f", {derive}" for derive in node.derives) + ")]" ) @@ -1062,7 +1070,8 @@ def write_node(out: list[str], ast: Ast) -> None: name = node.name out.append(f"pub struct {name} {{") out.append("pub node_index: crate::AtomicNodeIndex,") - out.append("pub range: ruff_text_size::TextRange,") + if not node.custom_range: + out.append("pub range: ruff_text_size::TextRange,") for field in node.fields: if field.doc is not None: write_rustdoc(out, field.doc) @@ -1109,7 +1118,8 @@ def write_source_order(out: list[str], ast: Ast) -> None: fields_list += f"{field.name}: _,\n" else: fields_list += f"{field.name},\n" - fields_list += "range: _,\n" + if not node.custom_range: + fields_list += "range: _,\n" fields_list += "node_index: _,\n" for field in node.fields_in_source_order(): diff --git a/crates/ruff_python_ast/src/comparable.rs b/crates/ruff_python_ast/src/comparable.rs index 8f199cde44..e730a14707 100644 --- a/crates/ruff_python_ast/src/comparable.rs +++ b/crates/ruff_python_ast/src/comparable.rs @@ -1292,7 +1292,7 @@ impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> { ast::Expr::Call(ast::ExprCall { func, arguments, - range: _, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, diff --git a/crates/ruff_python_ast/src/generated.rs b/crates/ruff_python_ast/src/generated.rs index e3d7f097ce..cb424d7fa8 100644 --- a/crates/ruff_python_ast/src/generated.rs +++ b/crates/ruff_python_ast/src/generated.rs @@ -4104,12 +4104,6 @@ impl ruff_text_size::Ranged for crate::ExprCompare { } } -impl ruff_text_size::Ranged for crate::ExprCall { - fn range(&self) -> ruff_text_size::TextRange { - self.range - } -} - impl ruff_text_size::Ranged for crate::ExprFString { fn range(&self) -> ruff_text_size::TextRange { self.range @@ -10432,12 +10426,17 @@ pub struct ExprCompare { pub comparators: Box<[Expr]>, } +/// A call expression whose end offset is derived from its arguments. +/// +/// The parser and error-recovery code must ensure that the call and its arguments +/// end at the same offset. +/// /// See also [Call](https://docs.python.org/3/library/ast.html#ast.Call) -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, 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 range_start: ruff_text_size::TextSize, pub func: Box, pub arguments: crate::Arguments, /// basedpython: when true, this call represents a ` cast ` @@ -11719,12 +11718,12 @@ impl ExprCall { V: SourceOrderVisitor<'a> + ?Sized, { let ExprCall { + range_start: _, func, arguments, is_cast: _, is_checked_cast: _, is_string_tag: _, - range: _, node_index: _, } = self; visitor.visit_expr(func); diff --git a/crates/ruff_python_ast/src/helpers.rs b/crates/ruff_python_ast/src/helpers.rs index 1546e47553..ee167cdcc6 100644 --- a/crates/ruff_python_ast/src/helpers.rs +++ b/crates/ruff_python_ast/src/helpers.rs @@ -400,17 +400,21 @@ where Expr::Call(ast::ExprCall { func: call_func, arguments, - range: _, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, is_string_tag: _, }) => { + // Note that this is the evaluation order but not necessarily the declaration order + // (e.g. for `f(*args, a=2, *args2, **kwargs)` it's not) any_over_expr(call_func, &mut *func) - // Note that this is the evaluation order but not necessarily the declaration order - // (e.g. for `f(*args, a=2, *args2, **kwargs)` it's not) - || arguments.args.iter().any(|expr| any_over_expr(expr, &mut *func)) - || arguments.keywords + || arguments + .args + .iter() + .any(|expr| any_over_expr(expr, &mut *func)) + || arguments + .keywords .iter() .any(|keyword| any_over_expr(&keyword.value, &mut *func)) } @@ -1012,7 +1016,7 @@ pub fn is_assignment_to_a_dunder(stmt: &Stmt) -> bool { /// Return `true` if the [`Expr`] is a singleton (`None`, `True`, `False`, or /// `...`). -pub const fn is_singleton(expr: &Expr) -> bool { +const fn is_singleton(expr: &Expr) -> bool { matches!( expr, Expr::NoneLiteral(_) | Expr::BooleanLiteral(_) | Expr::EllipsisLiteral(_) diff --git a/crates/ruff_python_ast/src/identifier.rs b/crates/ruff_python_ast/src/identifier.rs index c8a54cbadf..b6737c182c 100644 --- a/crates/ruff_python_ast/src/identifier.rs +++ b/crates/ruff_python_ast/src/identifier.rs @@ -160,14 +160,14 @@ pub(crate) struct IdentifierTokenizer<'a> { } impl<'a> IdentifierTokenizer<'a> { - pub(crate) fn new(source: &'a str, range: TextRange) -> Self { + fn new(source: &'a str, range: TextRange) -> Self { Self { cursor: Cursor::new(&source[range]), offset: range.start(), } } - pub(crate) fn starts_at(offset: TextSize, source: &'a str) -> Self { + fn starts_at(offset: TextSize, source: &'a str) -> Self { let range = TextRange::new(offset, source.text_len()); Self::new(source, range) } diff --git a/crates/ruff_python_ast/src/int.rs b/crates/ruff_python_ast/src/int.rs index eacfd8b54a..5f8aedb308 100644 --- a/crates/ruff_python_ast/src/int.rs +++ b/crates/ruff_python_ast/src/int.rs @@ -82,7 +82,7 @@ impl Int { } /// Return the [`Int`] as an u32, if it can be represented as that data type. - pub fn as_u32(&self) -> Option { + fn as_u32(&self) -> Option { match &self.0 { Number::Small(small) => u32::try_from(*small).ok(), Number::Big(_) => None, @@ -114,7 +114,7 @@ impl Int { } /// Return the [`Int`] as an i16, if it can be represented as that data type. - pub fn as_i16(&self) -> Option { + fn as_i16(&self) -> Option { match &self.0 { Number::Small(small) => i16::try_from(*small).ok(), Number::Big(_) => None, diff --git a/crates/ruff_python_ast/src/name.rs b/crates/ruff_python_ast/src/name.rs index cc4b20f570..585a9c88ed 100644 --- a/crates/ruff_python_ast/src/name.rs +++ b/crates/ruff_python_ast/src/name.rs @@ -454,7 +454,7 @@ impl<'a> QualifiedNameBuilder<'a> { } #[inline] - pub fn is_empty(&self) -> bool { + pub(crate) fn is_empty(&self) -> bool { self.segments.is_empty() } @@ -464,7 +464,7 @@ impl<'a> QualifiedNameBuilder<'a> { } #[inline] - pub fn pop(&mut self) { + pub(crate) fn pop(&mut self) { self.segments.pop(); } @@ -474,7 +474,7 @@ impl<'a> QualifiedNameBuilder<'a> { } #[inline] - pub fn extend_from_slice(&mut self, segments: &[&'a str]) { + pub(crate) fn extend_from_slice(&mut self, segments: &[&'a str]) { self.segments.extend_from_slice(segments); } @@ -638,7 +638,7 @@ impl<'a> UnqualifiedName<'a> { } #[inline] - pub fn from_slice(segments: &[&'a str]) -> Self { + fn from_slice(segments: &[&'a str]) -> Self { Self(SegmentsVec::from_slice(segments)) } diff --git a/crates/ruff_python_ast/src/node_index.rs b/crates/ruff_python_ast/src/node_index.rs index f936e97bf2..9b82c285b0 100644 --- a/crates/ruff_python_ast/src/node_index.rs +++ b/crates/ruff_python_ast/src/node_index.rs @@ -62,7 +62,7 @@ pub struct NodeIndex(NonZeroU32); impl NodeIndex { /// A placeholder `NodeIndex`. - pub const NONE: NodeIndex = NodeIndex(NonZeroU32::new(NodeIndex::_NONE).unwrap()); + const NONE: NodeIndex = NodeIndex(NonZeroU32::new(NodeIndex::_NONE).unwrap()); // Note that the index `u32::MAX` is reserved for the `NonZeroU32` niche, and // this placeholder also reserves the second highest index. diff --git a/crates/ruff_python_ast/src/nodes.rs b/crates/ruff_python_ast/src/nodes.rs index 4ba93eb27e..ed18b0bddf 100644 --- a/crates/ruff_python_ast/src/nodes.rs +++ b/crates/ruff_python_ast/src/nodes.rs @@ -2,7 +2,7 @@ use crate::AtomicNodeIndex; use crate::generated::{ - ExprBytesLiteral, ExprDict, ExprFString, ExprList, ExprName, ExprNamed, ExprSet, + ExprBytesLiteral, ExprCall, ExprDict, ExprFString, ExprList, ExprName, ExprNamed, ExprSet, ExprStringLiteral, ExprTString, ExprTuple, PatternMatchAnd, PatternMatchAs, PatternMatchOr, StmtClassDef, }; @@ -1461,6 +1461,34 @@ impl ExprStringLiteral { } } +impl Ranged for ExprCall { + fn range(&self) -> TextRange { + TextRange::new(self.range_start, self.arguments.end()) + } +} + +#[expect( + clippy::missing_fields_in_debug, + reason = "`range_start` is represented by the reconstructed `range` field" +)] +impl fmt::Debug for ExprCall { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("ExprCall") + .field("node_index", &self.node_index) + .field("range", &self.range()) + .field("func", &self.func) + .field("arguments", &self.arguments) + // basedpython: the surface form a call was written in — `a cast T`, + // `a cast? T`, a string tag — is not recoverable from `func` and + // `arguments` alone, so the parser snapshots have to show it + .field("is_cast", &self.is_cast) + .field("is_checked_cast", &self.is_checked_cast) + .field("is_string_tag", &self.is_string_tag) + .finish() + } +} + /// The value representing a [`ExprStringLiteral`]. #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] @@ -1501,10 +1529,10 @@ impl StringLiteralValue { "Use `StringLiteralValue::single` to create single-part strings" ); Self { - inner: StringLiteralValueInner::Concatenated(ConcatenatedStringLiteral { + inner: StringLiteralValueInner::Concatenated(Box::new(ConcatenatedStringLiteral { strings, value: OnceLock::new(), - }), + })), } } @@ -1627,7 +1655,7 @@ enum StringLiteralValueInner { Single(StringLiteral), /// An implicitly concatenated string literals i.e., `"foo" "bar"`. - Concatenated(ConcatenatedStringLiteral), + Concatenated(Box), } bitflags! { @@ -4384,6 +4412,8 @@ mod tests { assert_eq!(std::mem::size_of::(), 72); assert_eq!(std::mem::size_of::(), 56); assert_eq!(std::mem::size_of::(), 40); + // basedpython: an expression carries the `local` / `once` lifetime modifiers, + // which is a word wider than upstream assert_eq!(std::mem::size_of::(), 72); assert_eq!(std::mem::size_of::(), 56); assert_eq!(std::mem::size_of::(), 24); @@ -4411,7 +4441,7 @@ mod tests { 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::(), 64); + assert_eq!(std::mem::size_of::(), 48); assert_eq!(std::mem::size_of::(), 32); // basedpython: the callable-parameter shape a tuple can carry (`/` and // `*` marker positions, `local` / `once` modifiers) is boxed behind an diff --git a/crates/ruff_python_ast/src/parenthesize.rs b/crates/ruff_python_ast/src/parenthesize.rs index 786ca0572c..8ad309fb1f 100644 --- a/crates/ruff_python_ast/src/parenthesize.rs +++ b/crates/ruff_python_ast/src/parenthesize.rs @@ -13,7 +13,7 @@ use crate::ExprRef; /// generally prefer [`parenthesized_range`]. /// /// Prefer [`crate::token::parentheses_iterator`] if you have access to [`crate::token::Tokens`]. -pub fn parentheses_iterator<'a>( +fn parentheses_iterator<'a>( expr: ExprRef<'a>, parent: Option, comment_ranges: &'a CommentRanges, diff --git a/crates/ruff_python_ast/src/relocate.rs b/crates/ruff_python_ast/src/relocate.rs index 77165142b2..605ec752a8 100644 --- a/crates/ruff_python_ast/src/relocate.rs +++ b/crates/ruff_python_ast/src/relocate.rs @@ -66,8 +66,13 @@ impl Transformer for Relocator { Expr::Compare(ast::ExprCompare { range, .. }) => { *range = self.range; } - Expr::Call(ast::ExprCall { range, .. }) => { - *range = self.range; + Expr::Call(ast::ExprCall { + range_start, + arguments, + .. + }) => { + *range_start = self.range.start(); + arguments.range = self.range; } Expr::FString(ast::ExprFString { range, .. }) => { *range = self.range; diff --git a/crates/ruff_python_ast/src/script.rs b/crates/ruff_python_ast/src/script.rs index 9180b3ccdb..00c0fc7c30 100644 --- a/crates/ruff_python_ast/src/script.rs +++ b/crates/ruff_python_ast/src/script.rs @@ -54,30 +54,22 @@ impl ScriptTag { /// /// See: pub fn parse(contents: &[u8]) -> Option { - // Identify the opening pragma. - let index = FINDER.find(contents)?; - - Self::parse_at(contents, index) + FINDER + .find_iter(contents) + .find_map(|index| Self::parse_at(contents, index)) } - /// Extracts a `script` metadata block known to start at `index`. - /// - /// Returns `None` if `index` does not point to an exact opening pragma at the start of a line. - pub fn parse_at(contents: &[u8], index: usize) -> Option { - let (prelude, contents) = contents.split_at_checked(index)?; - + fn parse_at(contents: &[u8], index: usize) -> Option { // The opening pragma must be the first line, or immediately preceded by a newline. - if prelude - .last() - .is_some_and(|byte| !matches!(*byte, b'\r' | b'\n')) - { + if !(index == 0 || matches!(contents[index - 1], b'\r' | b'\n')) { return None; } // Extract the preceding content. - let prelude = std::str::from_utf8(prelude).ok()?; + let prelude = std::str::from_utf8(&contents[..index]).ok()?; // Decode as UTF-8. + let contents = &contents[index..]; let contents = std::str::from_utf8(contents).ok()?; let mut lines = contents.lines(); diff --git a/crates/ruff_python_ast/src/str.rs b/crates/ruff_python_ast/src/str.rs index a058cf4c8e..a24c51a87f 100644 --- a/crates/ruff_python_ast/src/str.rs +++ b/crates/ruff_python_ast/src/str.rs @@ -163,7 +163,7 @@ const SINGLE_QUOTE_STR_PREFIXES: &[&str] = &[ /// /// See: #[rustfmt::skip] -pub const TRIPLE_QUOTE_BYTE_PREFIXES: &[&str] = &[ +pub(crate) const TRIPLE_QUOTE_BYTE_PREFIXES: &[&str] = &[ "BR\"\"\"", "Br\"\"\"", "bR\"\"\"", @@ -187,7 +187,7 @@ pub const TRIPLE_QUOTE_BYTE_PREFIXES: &[&str] = &[ ]; #[rustfmt::skip] -pub const SINGLE_QUOTE_BYTE_PREFIXES: &[&str] = &[ +pub(crate) const SINGLE_QUOTE_BYTE_PREFIXES: &[&str] = &[ "BR\"", "Br\"", "bR\"", @@ -215,7 +215,7 @@ pub const SINGLE_QUOTE_BYTE_PREFIXES: &[&str] = &[ /// /// See: #[rustfmt::skip] -pub const TRIPLE_QUOTE_TEMPLATE_PREFIXES: &[&str] = &[ +pub(crate) const TRIPLE_QUOTE_TEMPLATE_PREFIXES: &[&str] = &[ "TR\"\"\"", "Tr\"\"\"", "tR\"\"\"", @@ -239,7 +239,7 @@ pub const TRIPLE_QUOTE_TEMPLATE_PREFIXES: &[&str] = &[ ]; #[rustfmt::skip] -pub const SINGLE_QUOTE_TEMPLATE_PREFIXES: &[&str] = &[ +pub(crate) const SINGLE_QUOTE_TEMPLATE_PREFIXES: &[&str] = &[ "TR\"", "Tr\"", "tR\"", @@ -271,7 +271,7 @@ pub fn raw_contents(contents: &str) -> Option<&str> { Some(&contents[range]) } -pub fn raw_contents_range(contents: &str) -> Option { +fn raw_contents_range(contents: &str) -> Option { let leading_quote_str = leading_quote(contents)?; let trailing_quote_str = trailing_quote(contents)?; diff --git a/crates/ruff_python_ast/src/token/tokens.rs b/crates/ruff_python_ast/src/token/tokens.rs index 1c1de82d25..7037ae1e34 100644 --- a/crates/ruff_python_ast/src/token/tokens.rs +++ b/crates/ruff_python_ast/src/token/tokens.rs @@ -27,7 +27,7 @@ impl Tokens { /// Unlike `binary_search_by_key`, this method ensures that if multiple tokens start at the same offset, /// it returns the index of the first one. Multiple tokens can start at the same offset in cases where /// zero-length tokens are involved (like `Dedent` or `Newline` at the end of the file). - pub fn binary_search_by_start(&self, offset: TextSize) -> Result { + fn binary_search_by_start(&self, offset: TextSize) -> Result { let partition_point = self.partition_point(|token| token.start() < offset); let after = &self[partition_point..]; diff --git a/crates/ruff_python_ast/src/visitor.rs b/crates/ruff_python_ast/src/visitor.rs index 03f456a171..52ef04eea3 100644 --- a/crates/ruff_python_ast/src/visitor.rs +++ b/crates/ruff_python_ast/src/visitor.rs @@ -292,10 +292,7 @@ pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a Stmt) { visitor.visit_expr(test); visitor.visit_body(body); for clause in elif_else_clauses { - if let Some(test) = &clause.test { - visitor.visit_expr(test); - } - walk_elif_else_clause(visitor, clause); + visitor.visit_elif_else_clause(clause); } } Stmt::With(ast::StmtWith { items, body, .. }) => { @@ -574,7 +571,7 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) { Expr::Call(ast::ExprCall { func, arguments, - range: _, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, diff --git a/crates/ruff_python_ast/src/visitor/transformer.rs b/crates/ruff_python_ast/src/visitor/transformer.rs index f8ed0e9bee..f0515084bf 100644 --- a/crates/ruff_python_ast/src/visitor/transformer.rs +++ b/crates/ruff_python_ast/src/visitor/transformer.rs @@ -279,7 +279,7 @@ pub fn walk_stmt(visitor: &V, stmt: &mut Stmt) { visitor.visit_expr(test); visitor.visit_body(body); for clause in elif_else_clauses { - walk_elif_else_clause(visitor, clause); + visitor.visit_elif_else_clause(clause); } } Stmt::With(ast::StmtWith { items, body, .. }) => { @@ -558,7 +558,7 @@ pub fn walk_expr(visitor: &V, expr: &mut Expr) { Expr::Call(ast::ExprCall { func, arguments, - range: _, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, diff --git a/crates/ruff_python_codegen/Cargo.toml b/crates/ruff_python_codegen/Cargo.toml index 14d83eaf54..15eb55c7fe 100644 --- a/crates/ruff_python_codegen/Cargo.toml +++ b/crates/ruff_python_codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_codegen" -version = "0.0.5" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_codegen/README.md b/crates/ruff_python_codegen/README.md index c998c19eaf..eb260851fa 100644 --- a/crates/ruff_python_codegen/README.md +++ b/crates/ruff_python_codegen/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_python_codegen). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_python_codegen). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_codegen/src/generator.rs b/crates/ruff_python_codegen/src/generator.rs index 6886b16e36..2221c4bd53 100644 --- a/crates/ruff_python_codegen/src/generator.rs +++ b/crates/ruff_python_codegen/src/generator.rs @@ -280,7 +280,7 @@ impl<'a> Generator<'a> { } } - pub(crate) fn unparse_stmt(&mut self, ast: &Stmt) { + fn unparse_stmt(&mut self, ast: &Stmt) { macro_rules! statement { ($body:block) => {{ if !std::mem::take(&mut self.inline_statement) { @@ -1083,7 +1083,7 @@ impl<'a> Generator<'a> { self.p("]"); } - pub(crate) fn unparse_type_param(&mut self, ast: &TypeParam) { + fn unparse_type_param(&mut self, ast: &TypeParam) { // basedpython: the `reified` modifier is surface syntax with no python // spelling, so it is only re-emitted when rendering basedpython if ast.is_reified() && self.mode == Mode::BasedPython { @@ -1150,7 +1150,7 @@ impl<'a> Generator<'a> { } } - pub(crate) fn unparse_expr(&mut self, ast: &Expr, level: u8) { + fn unparse_expr(&mut self, ast: &Expr, level: u8) { macro_rules! opprec { ($opty:ident, $x:expr, $enu:path, $($var:ident($op:literal, $prec:ident)),*$(,)?) => { match $x { @@ -1465,7 +1465,7 @@ impl<'a> Generator<'a> { Expr::Call(ast::ExprCall { func, arguments, - range: _, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, @@ -1843,7 +1843,7 @@ impl<'a> Generator<'a> { self.unparse_expr(&named.value, precedence::EXPR); } - pub(crate) fn unparse_singleton(&mut self, singleton: Singleton) { + fn unparse_singleton(&mut self, singleton: Singleton) { match singleton { Singleton::None => self.p("None"), Singleton::True => self.p("True"), diff --git a/crates/ruff_python_formatter/Cargo.toml b/crates/ruff_python_formatter/Cargo.toml index c7fe7d8f8d..cfa6732104 100644 --- a/crates/ruff_python_formatter/Cargo.toml +++ b/crates/ruff_python_formatter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_formatter" -version = "0.0.5" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_formatter/README.md b/crates/ruff_python_formatter/README.md index af1d9cb289..c9a942f1bc 100644 --- a/crates/ruff_python_formatter/README.md +++ b/crates/ruff_python_formatter/README.md @@ -32,8 +32,8 @@ Head to [The Ruff Formatter](https://docs.astral.sh/ruff/formatter/) for usage i This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_python_formatter). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_python_formatter). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_formatter/src/comments/format.rs b/crates/ruff_python_formatter/src/comments/format.rs index 6348ddea1c..64e19c2d09 100644 --- a/crates/ruff_python_formatter/src/comments/format.rs +++ b/crates/ruff_python_formatter/src/comments/format.rs @@ -361,7 +361,7 @@ impl Format> for FormatEmptyLines { /// * Black normalization of `SourceComment`. /// * Line suffix with reserved width for the final, normalized content. /// * Expands parent node. -pub(crate) const fn trailing_end_of_line_comment( +const fn trailing_end_of_line_comment( comment: &SourceComment, ) -> FormatTrailingEndOfLineComment<'_> { FormatTrailingEndOfLineComment { comment } @@ -428,7 +428,7 @@ impl Format> for FormatTrailingEndOfLineComment<'_> { /// unnecessary allocations. /// * If the content is modified then make as few allocations as possible and use /// a dynamic text element at the original slice's start position. -pub(crate) const fn format_normalized_comment( +const fn format_normalized_comment( comment: Cow<'_, str>, range: TextRange, ) -> FormatNormalizedComment<'_> { diff --git a/crates/ruff_python_formatter/src/comments/mod.rs b/crates/ruff_python_formatter/src/comments/mod.rs index 18db8c6ea8..7d1149c836 100644 --- a/crates/ruff_python_formatter/src/comments/mod.rs +++ b/crates/ruff_python_formatter/src/comments/mod.rs @@ -162,7 +162,7 @@ impl SourceComment { } /// Returns a nice debug representation that prints the source code for every comment (and not just the range). - pub(crate) fn debug<'a>(&'a self, source_code: SourceCode<'a>) -> DebugComment<'a> { + fn debug<'a>(&'a self, source_code: SourceCode<'a>) -> DebugComment<'a> { DebugComment::new(self, source_code) } diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index cb277520c8..0d1ac46075 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -627,10 +627,9 @@ fn handle_own_line_comment_between_branches<'a>( // pass // ``` || { - comment_indentation - // This can be any positive number - we just - // want to hit the `Less` branch below - + TextSize::new(1) + // We could use any positive number here, it doesn't have to be `1` + // - we just want to hit the `Less` branch below + comment_indentation + TextSize::new(1) }, ruff_text_size::TextLen::text_len, ); diff --git a/crates/ruff_python_formatter/src/context.rs b/crates/ruff_python_formatter/src/context.rs index bf6a6290c8..3258615edf 100644 --- a/crates/ruff_python_formatter/src/context.rs +++ b/crates/ruff_python_formatter/src/context.rs @@ -134,7 +134,7 @@ impl<'a> PyFormatContext<'a> { self.interpolated_string_state } - pub(crate) fn set_interpolated_string_state( + fn set_interpolated_string_state( &mut self, interpolated_string_state: InterpolatedStringState, ) { diff --git a/crates/ruff_python_formatter/src/expression/expr_attribute.rs b/crates/ruff_python_formatter/src/expression/expr_attribute.rs index 88953a27ed..e3a03ec98e 100644 --- a/crates/ruff_python_formatter/src/expression/expr_attribute.rs +++ b/crates/ruff_python_formatter/src/expression/expr_attribute.rs @@ -127,10 +127,12 @@ impl FormatNodeRule for FormatExprAttribute { if parenthesize_value || value.is_call_expr() || value.is_subscript_expr() - // Remember to update the doc-comment above when - // stabilizing this behavior. - || (is_fluent_layout_split_first_call_enabled(f.context()) - && call_chain_layout.is_first_call_like()) + || ( + // Remember to update the doc-comment above when + // stabilizing this behavior. + is_fluent_layout_split_first_call_enabled(f.context()) + && call_chain_layout.is_first_call_like() + ) { soft_line_break().fmt(f)?; } diff --git a/crates/ruff_python_formatter/src/expression/expr_call.rs b/crates/ruff_python_formatter/src/expression/expr_call.rs index b0dcf88cbc..d601fc5388 100644 --- a/crates/ruff_python_formatter/src/expression/expr_call.rs +++ b/crates/ruff_python_formatter/src/expression/expr_call.rs @@ -25,7 +25,7 @@ impl FormatRuleWithOptions> for FormatExprCall { impl FormatNodeRule for FormatExprCall { fn fmt_fields(&self, item: &ExprCall, f: &mut PyFormatter) -> FormatResult<()> { let ExprCall { - range: _, + range_start: _, node_index: _, is_cast, is_checked_cast, diff --git a/crates/ruff_python_formatter/src/expression/expr_slice.rs b/crates/ruff_python_formatter/src/expression/expr_slice.rs index 4f4b0f001d..eb6678ac66 100644 --- a/crates/ruff_python_formatter/src/expression/expr_slice.rs +++ b/crates/ruff_python_formatter/src/expression/expr_slice.rs @@ -153,7 +153,7 @@ impl FormatNodeRule for FormatExprSlice { /// to find out whether there is a second one, too, e.g. `[1:2]` and `[1:10:2]`. /// /// Returns the first and optionally the second colon. -pub(crate) fn find_colons( +fn find_colons( contents: &str, range: TextRange, lower: Option<&Expr>, diff --git a/crates/ruff_python_formatter/src/expression/mod.rs b/crates/ruff_python_formatter/src/expression/mod.rs index a620904fdf..745d2607a5 100644 --- a/crates/ruff_python_formatter/src/expression/mod.rs +++ b/crates/ruff_python_formatter/src/expression/mod.rs @@ -750,7 +750,7 @@ impl<'input> CanOmitOptionalParenthesesVisitor<'input> { ); } Expr::Call(ast::ExprCall { - range: _, + range_start: _, node_index: _, is_cast: _, is_checked_cast: _, @@ -972,7 +972,7 @@ impl CallChainLayout { /// Returns new state decreasing count of remaining calls/subscripts /// to traverse, or the state `FirstCallOrSubscript`, as appropriate. #[must_use] - pub(crate) fn decrement_call_like_count(self) -> Self { + fn decrement_call_like_count(self) -> Self { match self { Self::Fluent(AttributeState::CallLikePreceding(x)) => { if x > 1 { @@ -994,7 +994,7 @@ impl CallChainLayout { /// `FirstCallOrSubscript` -> `BeforeFirstCallOrSubscript` /// and otherwise returns unchanged. #[must_use] - pub(crate) fn transition_after_attribute(self) -> Self { + fn transition_after_attribute(self) -> Self { match self { Self::Fluent(AttributeState::FirstCallLike) => { Self::Fluent(AttributeState::BeforeFirstCallLike) @@ -1003,7 +1003,7 @@ impl CallChainLayout { } } - pub(crate) fn is_first_call_like(self) -> bool { + fn is_first_call_like(self) -> bool { matches!(self, Self::Fluent(AttributeState::FirstCallLike)) } @@ -1025,7 +1025,7 @@ impl CallChainLayout { /// 3. If the root is parenthesized, add 1 to that value. /// 4. If the total is at least 2, return `Fluent`. Otherwise /// return `NonFluent` - pub(crate) fn from_expression(mut expr: ExprRef, context: &PyFormatContext) -> Self { + fn from_expression(mut expr: ExprRef, context: &PyFormatContext) -> Self { // TODO(dylan): Once the fluent layout preview style is // stabilized, see if it is possible to simplify some of // the logic around parenthesized roots. (While supporting @@ -1167,7 +1167,7 @@ impl CallChainLayout { /// Determine whether to actually apply fluent layout in attribute, call and subscript /// formatting - pub(crate) fn apply_in_node<'a>( + fn apply_in_node<'a>( self, item: impl Into>, f: &mut PyFormatter, @@ -1184,7 +1184,7 @@ impl CallChainLayout { } } - pub(crate) fn is_fluent(self) -> bool { + fn is_fluent(self) -> bool { matches!(self, CallChainLayout::Fluent(_)) } } diff --git a/crates/ruff_python_formatter/src/lib.rs b/crates/ruff_python_formatter/src/lib.rs index 2b9721d018..d1a78bb083 100644 --- a/crates/ruff_python_formatter/src/lib.rs +++ b/crates/ruff_python_formatter/src/lib.rs @@ -1,3 +1,4 @@ +use ruff_db::PythonFile; use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity}; use ruff_db::files::File; use ruff_db::parsed::parsed_module; @@ -180,7 +181,7 @@ where pub fn formatted_file(db: &dyn Db, file: File) -> Result, FormatModuleError> { let options = db.format_options(file); - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, PythonFile::new(db, file, options.target_version())).load(db); if let Some(first) = parsed.errors().first() { return Err(FormatModuleError::ParseError(first.clone())); diff --git a/crates/ruff_python_formatter/src/main.rs b/crates/ruff_python_formatter/src/main.rs index 09e5ea2ae6..72769b74ac 100644 --- a/crates/ruff_python_formatter/src/main.rs +++ b/crates/ruff_python_formatter/src/main.rs @@ -8,7 +8,7 @@ use clap::Parser as ClapParser; use ruff_python_formatter::cli::{Cli, Emit, format_and_debug_print}; /// Read a `String` from `stdin`. -pub(crate) fn read_from_stdin() -> Result { +fn read_from_stdin() -> Result { let mut buffer = String::new(); io::stdin().lock().read_to_string(&mut buffer)?; Ok(buffer) diff --git a/crates/ruff_python_formatter/src/options.rs b/crates/ruff_python_formatter/src/options.rs index 029bdc95e3..762be4124e 100644 --- a/crates/ruff_python_formatter/src/options.rs +++ b/crates/ruff_python_formatter/src/options.rs @@ -150,7 +150,7 @@ impl PyFormatOptions { self.source_type } - pub const fn source_map_generation(&self) -> SourceMapGeneration { + pub(crate) const fn source_map_generation(&self) -> SourceMapGeneration { self.source_map_generation } @@ -323,7 +323,7 @@ pub enum QuoteStyle { } impl QuoteStyle { - pub const fn is_preserve(self) -> bool { + pub(crate) const fn is_preserve(self) -> bool { matches!(self, QuoteStyle::Preserve) } @@ -370,7 +370,7 @@ pub enum MagicTrailingComma { } impl MagicTrailingComma { - pub const fn is_respect(self) -> bool { + pub(crate) const fn is_respect(self) -> bool { matches!(self, Self::Respect) } @@ -489,7 +489,7 @@ pub enum NestedStringQuoteStyle { } impl NestedStringQuoteStyle { - pub const fn is_preferred(self) -> bool { + pub(crate) const fn is_preferred(self) -> bool { matches!(self, NestedStringQuoteStyle::Preferred) } } @@ -515,7 +515,7 @@ pub enum DocstringCode { } impl DocstringCode { - pub const fn is_enabled(self) -> bool { + pub(crate) const fn is_enabled(self) -> bool { matches!(self, DocstringCode::Enabled) } } diff --git a/crates/ruff_python_formatter/src/other/interpolated_string_element.rs b/crates/ruff_python_formatter/src/other/interpolated_string_element.rs index a93b942540..a252d0a4c7 100644 --- a/crates/ruff_python_formatter/src/other/interpolated_string_element.rs +++ b/crates/ruff_python_formatter/src/other/interpolated_string_element.rs @@ -57,10 +57,7 @@ pub(crate) struct FormatFStringLiteralElement<'a> { } impl<'a> FormatFStringLiteralElement<'a> { - pub(crate) fn new( - element: &'a InterpolatedStringLiteralElement, - fstring_flags: AnyStringFlags, - ) -> Self { + fn new(element: &'a InterpolatedStringLiteralElement, fstring_flags: AnyStringFlags) -> Self { Self { element, fstring_flags, diff --git a/crates/ruff_python_formatter/src/other/parameters.rs b/crates/ruff_python_formatter/src/other/parameters.rs index 1c6682bab1..e1dd1153a3 100644 --- a/crates/ruff_python_formatter/src/other/parameters.rs +++ b/crates/ruff_python_formatter/src/other/parameters.rs @@ -329,11 +329,11 @@ impl Format> for CommentsAroundText<'_> { #[derive(Debug)] pub(crate) struct ParameterSeparator { /// The end of the last node or separator before this separator - pub(crate) preceding_end: TextSize, + preceding_end: TextSize, /// The range of the separator itself - pub(crate) separator: TextRange, + separator: TextRange, /// The start of the first node or separator following this separator - pub(crate) following_start: TextSize, + following_start: TextSize, } /// Finds slash and star in `f(a, /, b, *, c)` or `lambda a, /, b, *, c: 1`. diff --git a/crates/ruff_python_formatter/src/pattern/mod.rs b/crates/ruff_python_formatter/src/pattern/mod.rs index 302463d158..91784fe93f 100644 --- a/crates/ruff_python_formatter/src/pattern/mod.rs +++ b/crates/ruff_python_formatter/src/pattern/mod.rs @@ -227,10 +227,7 @@ impl Format> for MaybeParenthesizePattern<'_> { /// /// The layout is only applied when the parenthesized pattern is the first or last item in the pattern. /// For example, the layout isn't used for `a | [b, c] | d` because that would look weird. -pub(crate) fn can_pattern_omit_optional_parentheses( - pattern: &Pattern, - context: &PyFormatContext, -) -> bool { +fn can_pattern_omit_optional_parentheses(pattern: &Pattern, context: &PyFormatContext) -> bool { let mut visitor = CanOmitOptionalParenthesesVisitor::default(); visitor.visit_pattern(pattern, context); @@ -297,16 +294,20 @@ impl<'a> CanOmitOptionalParenthesesVisitor<'a> { } Pattern::MatchValue(value) => match &*value.value { - Expr::StringLiteral(_) | - Expr::BytesLiteral(_) | - // 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(_) => { + Expr::StringLiteral(_) + | Expr::BytesLiteral(_) + | Expr::TString(_) + | Expr::NumberLiteral(_) + | Expr::Attribute(_) + | Expr::UnaryOp(_) => { // require no state update other than visit_pattern does. } + Expr::FString(_) => { + // 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. + } + // `case 4+3j:` or `case 4-3j: // Cannot contain arbitrary expressions. Limited to complex numbers. Expr::BinOp(_) => { diff --git a/crates/ruff_python_formatter/src/statement/clause.rs b/crates/ruff_python_formatter/src/statement/clause.rs index 8ee5998a6f..a0d2f26be5 100644 --- a/crates/ruff_python_formatter/src/statement/clause.rs +++ b/crates/ruff_python_formatter/src/statement/clause.rs @@ -51,7 +51,7 @@ impl<'a> ClauseHeader<'a> { /// /// This is similar to [`ruff_python_ast::AnyNodeRef::last_child_in_body`] /// but restricted to the clause. - pub(crate) fn last_child_in_clause(self) -> Option> { + fn last_child_in_clause(self) -> Option> { match self { ClauseHeader::Class(StmtClassDef { body, .. }) | ClauseHeader::Function(StmtFunctionDef { body, .. }) diff --git a/crates/ruff_python_formatter/src/statement/stmt_assign.rs b/crates/ruff_python_formatter/src/statement/stmt_assign.rs index 057d7b7520..a8ba08c612 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_assign.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_assign.rs @@ -1386,7 +1386,7 @@ pub(super) fn has_target_own_parentheses(target: &Expr, context: &PyFormatContex matches!(target, Expr::Tuple(_)) || has_own_parentheses(target, context).is_some() } -pub(super) fn should_parenthesize_target(target: &Expr, context: &PyFormatContext) -> bool { +fn should_parenthesize_target(target: &Expr, context: &PyFormatContext) -> bool { !(has_target_own_parentheses(target, context) || is_attribute_with_parenthesized_value(target, context)) } diff --git a/crates/ruff_python_formatter/src/statement/suite.rs b/crates/ruff_python_formatter/src/statement/suite.rs index f2044bc52a..e600d358a2 100644 --- a/crates/ruff_python_formatter/src/statement/suite.rs +++ b/crates/ruff_python_formatter/src/statement/suite.rs @@ -539,34 +539,33 @@ fn trailing_function_or_class_def<'a>( preceding.map(AnyNodeRef::from), AnyNodeRef::last_child_in_body, ) - .take_while(|last_child| - // If there is a comment between preceding and following the empty lines were - // inserted before the comment by preceding and there are no extra empty lines - // after the comment. - // ```python - // class Test: - // def a(self): - // pass - // # trailing comment - // - // - // # two lines before, one line after - // - // c = 30 - // ```` - // This also includes nested class/function definitions, so we stop recursing - // once we see a node with a trailing own line comment: - // ```python - // def f(): - // if True: - // - // def double(s): - // return s + s - // - // # nested trailing own line comment - // print("below function with trailing own line comment") - // ``` - !comments.has_trailing_own_line(*last_child)) + // If there is a comment between preceding and following the empty lines were + // inserted before the comment by preceding and there are no extra empty lines + // after the comment. + // ```python + // class Test: + // def a(self): + // pass + // # trailing comment + // + // + // # two lines before, one line after + // + // c = 30 + // ```` + // This also includes nested class/function definitions, so we stop recursing + // once we see a node with a trailing own line comment: + // ```python + // def f(): + // if True: + // + // def double(s): + // return s + s + // + // # nested trailing own line comment + // print("below function with trailing own line comment") + // ``` + .take_while(|last_child| !comments.has_trailing_own_line(*last_child)) .find(|last_child| { // basedpython: a trailing lambda block parses as a `FunctionDef` but // reads as a call statement — no definition blank lines after it @@ -765,7 +764,7 @@ fn stub_suite_can_omit_empty_line(preceding: &Stmt, following: &Stmt, f: &PyForm } /// Returns `true` if a function or class body contains only an ellipsis with no comments. -pub(crate) fn contains_only_an_ellipsis(body: &[Stmt], comments: &Comments) -> bool { +fn contains_only_an_ellipsis(body: &[Stmt], comments: &Comments) -> bool { as_only_an_ellipsis(body, comments).is_some() } diff --git a/crates/ruff_python_formatter/src/string/normalize.rs b/crates/ruff_python_formatter/src/string/normalize.rs index f02187355d..7c4e6181a7 100644 --- a/crates/ruff_python_formatter/src/string/normalize.rs +++ b/crates/ruff_python_formatter/src/string/normalize.rs @@ -318,7 +318,7 @@ impl QuoteMetadata { } } - pub(crate) fn from_str(text: &str, flags: AnyStringFlags, preferred_quote: Quote) -> Self { + fn from_str(text: &str, flags: AnyStringFlags, preferred_quote: Quote) -> Self { let kind = if flags.is_raw_string() { QuoteMetadataKind::raw(text, preferred_quote, flags.triple_quotes()) } else if flags.is_triple_quoted() { diff --git a/crates/ruff_python_importer/Cargo.toml b/crates/ruff_python_importer/Cargo.toml index 2f65a28a62..98b44197b0 100644 --- a/crates/ruff_python_importer/Cargo.toml +++ b/crates/ruff_python_importer/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_importer" -version = "0.0.5" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_importer/README.md b/crates/ruff_python_importer/README.md index 594ef8b98b..31a810a657 100644 --- a/crates/ruff_python_importer/README.md +++ b/crates/ruff_python_importer/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_python_importer). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_python_importer). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_index/Cargo.toml b/crates/ruff_python_index/Cargo.toml index 8278131bbd..a177107337 100644 --- a/crates/ruff_python_index/Cargo.toml +++ b/crates/ruff_python_index/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_index" -version = "0.0.5" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_index/README.md b/crates/ruff_python_index/README.md index 0a11540f61..dc83a2c02e 100644 --- a/crates/ruff_python_index/README.md +++ b/crates/ruff_python_index/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_python_index). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_python_index). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_index/src/indexer.rs b/crates/ruff_python_index/src/indexer.rs index a193ced98d..fe45645826 100644 --- a/crates/ruff_python_index/src/indexer.rs +++ b/crates/ruff_python_index/src/indexer.rs @@ -123,7 +123,7 @@ impl Indexer { } /// Returns `true` if the given offset is part of a continuation line. - pub fn is_continuation(&self, offset: TextSize, source: &str) -> bool { + fn is_continuation(&self, offset: TextSize, source: &str) -> bool { let line_start = source.line_start(offset); self.continuation_lines.binary_search(&line_start).is_ok() } diff --git a/crates/ruff_python_literal/Cargo.toml b/crates/ruff_python_literal/Cargo.toml index 1fd8bebb7f..7aa7cdd0a4 100644 --- a/crates/ruff_python_literal/Cargo.toml +++ b/crates/ruff_python_literal/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_literal" -version = "0.0.5" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = ["Charlie Marsh ", "RustPython Team"] edition = { workspace = true } diff --git a/crates/ruff_python_literal/README.md b/crates/ruff_python_literal/README.md index afdcd0368b..94ee9e81bc 100644 --- a/crates/ruff_python_literal/README.md +++ b/crates/ruff_python_literal/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_python_literal). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_python_literal). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_literal/src/cformat.rs b/crates/ruff_python_literal/src/cformat.rs index 5427e7a854..5d2050d6b9 100644 --- a/crates/ruff_python_literal/src/cformat.rs +++ b/crates/ruff_python_literal/src/cformat.rs @@ -74,6 +74,12 @@ pub enum CFormatType { String(CFormatConversion), } +#[derive(Debug, PartialEq, Copy, Clone)] +pub enum CFormatContext { + Str, + Bytes, +} + #[derive(Debug, PartialEq)] pub enum CFormatPrecision { Quantity(CFormatQuantity), @@ -123,14 +129,17 @@ impl FromStr for CFormatSpec { return Err((CFormatErrorType::MissingModuloSign, 1)); } - CFormatSpec::parse(&mut chars) + CFormatSpec::parse(&mut chars, CFormatContext::Str) } } pub type ParseIter = Peekable>; impl CFormatSpec { - pub fn parse(iter: &mut ParseIter) -> Result + pub fn parse( + iter: &mut ParseIter, + context: CFormatContext, + ) -> Result where T: Into + Copy, I: Iterator, @@ -140,7 +149,7 @@ impl CFormatSpec { let min_field_width = parse_quantity(iter)?; let precision = parse_precision(iter)?; consume_length(iter); - let (format_type, format_char) = parse_format_type(iter)?; + let (format_type, format_char) = parse_format_type(iter, context)?; Ok(CFormatSpec { mapping_key, @@ -204,7 +213,10 @@ where } } -fn parse_format_type(iter: &mut ParseIter) -> Result<(CFormatType, char), ParsingError> +fn parse_format_type( + iter: &mut ParseIter, + context: CFormatContext, +) -> Result<(CFormatType, char), ParsingError> where T: Into, I: Iterator, @@ -234,7 +246,9 @@ where 'c' => CFormatType::Character, 'r' => CFormatType::String(CFormatConversion::Repr), 's' => CFormatType::String(CFormatConversion::Str), - 'b' => CFormatType::String(CFormatConversion::Bytes), + // `%b` is only valid for bytes formatting (e.g. `b"%b" % b"x"`), not for string + // formatting. + 'b' if context == CFormatContext::Bytes => CFormatType::String(CFormatConversion::Bytes), 'a' => CFormatType::String(CFormatConversion::Ascii), _ => return Err((CFormatErrorType::UnsupportedFormatChar(c), index)), }; @@ -363,9 +377,11 @@ impl CFormatBytes { CFormatPart::Literal(std::mem::take(&mut literal)), )); } - let spec = CFormatSpec::parse(iter).map_err(|err| CFormatError { - typ: err.0, - index: err.1, + let spec = CFormatSpec::parse(iter, CFormatContext::Bytes).map_err(|err| { + CFormatError { + typ: err.0, + index: err.1, + } })?; parts.push((index, CFormatPart::Spec(spec))); if let Some(&(index, _)) = iter.peek() { @@ -418,9 +434,11 @@ impl CFormatString { CFormatPart::Literal(std::mem::take(&mut literal)), )); } - let spec = CFormatSpec::parse(iter).map_err(|err| CFormatError { - typ: err.0, - index: err.1, + let spec = CFormatSpec::parse(iter, CFormatContext::Str).map_err(|err| { + CFormatError { + typ: err.0, + index: err.1, + } })?; parts.push((index, CFormatPart::Spec(spec))); if let Some(&(index, _)) = iter.peek() { diff --git a/crates/ruff_python_literal/src/char.rs b/crates/ruff_python_literal/src/char.rs index 98117acfb4..46233c0cc8 100644 --- a/crates/ruff_python_literal/src/char.rs +++ b/crates/ruff_python_literal/src/char.rs @@ -9,7 +9,7 @@ use icu_properties::props::{EnumeratedProperty, GeneralCategory}; /// * Zl Separator, Line ('\u2028', LINE SEPARATOR) /// * Zp Separator, Paragraph ('\u2029', PARAGRAPH SEPARATOR) /// * Zs (Separator, Space) other than ASCII space('\x20'). -pub fn is_printable(c: char) -> bool { +pub(crate) fn is_printable(c: char) -> bool { let cat = GeneralCategory::for_char(c); !matches!( diff --git a/crates/ruff_python_literal/src/format.rs b/crates/ruff_python_literal/src/format.rs index 043e7112e5..b042afe18d 100644 --- a/crates/ruff_python_literal/src/format.rs +++ b/crates/ruff_python_literal/src/format.rs @@ -27,7 +27,7 @@ impl FormatConversion { } impl FormatConversion { - pub fn from_char(c: char) -> Option { + fn from_char(c: char) -> Option { match c { 's' => Some(FormatConversion::Str), 'r' => Some(FormatConversion::Repr), diff --git a/crates/ruff_python_literal/src/lib.rs b/crates/ruff_python_literal/src/lib.rs index 1fa7869e85..fadf4b2519 100644 --- a/crates/ruff_python_literal/src/lib.rs +++ b/crates/ruff_python_literal/src/lib.rs @@ -1,5 +1,5 @@ pub mod cformat; -pub mod char; +mod char; pub mod escape; pub mod float; pub mod format; diff --git a/crates/ruff_python_parser/Cargo.toml b/crates/ruff_python_parser/Cargo.toml index aebe2f1ae3..9766dbb59c 100644 --- a/crates/ruff_python_parser/Cargo.toml +++ b/crates/ruff_python_parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_parser" -version = "0.0.5" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = ["Charlie Marsh ", "RustPython Team"] edition = { workspace = true } diff --git a/crates/ruff_python_parser/README.md b/crates/ruff_python_parser/README.md index e8a07ae1e2..20d8066dc4 100644 --- a/crates/ruff_python_parser/README.md +++ b/crates/ruff_python_parser/README.md @@ -19,8 +19,8 @@ Refer to the [contributing guidelines](./CONTRIBUTING.md) to get started and Git This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_python_parser). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_python_parser). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_parser/resources/invalid/statements/if_extra_indent.py b/crates/ruff_python_parser/resources/invalid/statements/if_extra_indent.py index 647626cb5d..2ab639c6cb 100644 --- a/crates/ruff_python_parser/resources/invalid/statements/if_extra_indent.py +++ b/crates/ruff_python_parser/resources/invalid/statements/if_extra_indent.py @@ -1,4 +1,4 @@ -# Improving the recovery would require changing the lexer to emit an extra dedent token after `a + b`. +# On invalid indentation, recover as if the indentation wasn't there if True: pass a + b @@ -6,3 +6,45 @@ pass a = 10 + +# Multiple nested unexpected indents. +if True: + before_nested + first_nested + second_nested + after_nested + +outside_nested + +# A valid compound statement inside recovered indentation. +if True: + before_compound + if condition: + nested_compound + recovered_compound + after_compound + +outside_compound + +# Multiple independent unexpected-indent regions in the same body. +if True: + before_regions + first_region + middle_region + second_region + after_region + +outside_regions + +# An independent syntax error inside recovered indentation stays visible. +if True: + before_error + broken(,) + after_error + +outside_error + +# Outstanding unexpected indents are flushed at EOF. +if True: + before_eof + final_eof diff --git a/crates/ruff_python_parser/src/lexer.rs b/crates/ruff_python_parser/src/lexer.rs index ad4d984360..5305d635b6 100644 --- a/crates/ruff_python_parser/src/lexer.rs +++ b/crates/ruff_python_parser/src/lexer.rs @@ -1543,7 +1543,7 @@ impl<'src> Lexer<'src> { self.errors.truncate(errors_position); } - pub fn finish(self) -> Vec { + pub(crate) fn finish(self) -> Vec { self.errors } } diff --git a/crates/ruff_python_parser/src/lexer/indentation.rs b/crates/ruff_python_parser/src/lexer/indentation.rs index c2193c9e7b..acdf53a2fb 100644 --- a/crates/ruff_python_parser/src/lexer/indentation.rs +++ b/crates/ruff_python_parser/src/lexer/indentation.rs @@ -12,7 +12,7 @@ use ruff_python_trivia::tab_offset_u32; pub(super) struct Column(u32); impl Column { - pub(super) const fn new(column: u32) -> Self { + const fn new(column: u32) -> Self { Self(column) } } @@ -22,7 +22,7 @@ impl Column { pub(super) struct Character(u32); impl Character { - pub(super) const fn new(characters: u32) -> Self { + const fn new(characters: u32) -> Self { Self(characters) } } @@ -45,7 +45,7 @@ impl Indentation { } #[cfg(test)] - pub(super) const fn new(column: Column, character: Character) -> Self { + const fn new(column: Column, character: Character) -> Self { Self { column, character } } diff --git a/crates/ruff_python_parser/src/lib.rs b/crates/ruff_python_parser/src/lib.rs index 6217e7d90e..7732b25c1f 100644 --- a/crates/ruff_python_parser/src/lib.rs +++ b/crates/ruff_python_parser/src/lib.rs @@ -418,7 +418,7 @@ impl Parsed { } /// Consumes the [`Parsed`] output and returns a list of syntax errors found during parsing. - pub fn into_errors(self) -> Vec { + fn into_errors(self) -> Vec { self.errors } @@ -474,7 +474,7 @@ impl Parsed { /// /// Note that any [`unsupported_syntax_errors`](Parsed::unsupported_syntax_errors) will not /// cause [`Err`] to be returned. - pub(crate) fn into_result(self) -> Result, ParseError> { + fn into_result(self) -> Result, ParseError> { if self.has_valid_syntax() { Ok(self) } else { @@ -510,7 +510,7 @@ impl Parsed { /// Otherwise, it returns [`None`]. /// /// [`Some(Parsed)`]: Some - pub fn try_into_expression(self) -> Option> { + fn try_into_expression(self) -> Option> { match self.syntax { Mod::Module(_) => None, Mod::Expression(expression) => Some(Parsed { @@ -542,7 +542,7 @@ impl Parsed { } /// Returns a mutable reference to the expression contained in this parsed output. - pub fn expr_mut(&mut self) -> &mut Expr { + fn expr_mut(&mut self) -> &mut Expr { &mut self.syntax.body } diff --git a/crates/ruff_python_parser/src/parser/expression.rs b/crates/ruff_python_parser/src/parser/expression.rs index 56527c772a..4b9e8f884a 100644 --- a/crates/ruff_python_parser/src/parser/expression.rs +++ b/crates/ruff_python_parser/src/parser/expression.rs @@ -68,7 +68,7 @@ pub(super) const EXPR_SET: TokenSet = TokenSet::new([ .union(LITERAL_SET); /// Tokens that can appear after an expression. -pub(super) const END_EXPR_SET: TokenSet = TokenSet::new([ +const END_EXPR_SET: TokenSet = TokenSet::new([ // Ex) `expr` (without a newline) TokenKind::EndOfFile, // Ex) `expr` @@ -402,7 +402,7 @@ impl<'src> Parser<'src> { self.parse_binary_expression_or_higher_recursive(lhs, left_precedence, context, start) } - pub(super) fn parse_binary_expression_or_higher_recursive( + fn parse_binary_expression_or_higher_recursive( &mut self, mut left: ParsedExpr, left_precedence: OperatorPrecedence, @@ -489,7 +489,7 @@ impl<'src> Parser<'src> { expr: Expr::Call(ast::ExprCall { func: Box::new(func), arguments, - range: self.node_range(start), + range_start: start, node_index: AtomicNodeIndex::NONE, is_cast: false, is_checked_cast: true, @@ -539,7 +539,7 @@ impl<'src> Parser<'src> { expr: Expr::Call(ast::ExprCall { func: Box::new(func), arguments, - range: self.node_range(start), + range_start: start, node_index: AtomicNodeIndex::NONE, is_cast: true, is_checked_cast: false, @@ -792,11 +792,11 @@ impl<'src> Parser<'src> { ); } } else { + // > The power operator `**` binds less tightly than an arithmetic + // > or bitwise unary operator on its right, that is, 2**-1 is 0.5. + // + // Reference: https://docs.python.org/3/reference/expressions.html#id21 if left_precedence > OperatorPrecedence::PosNegBitNot - // > The power operator `**` binds less tightly than an arithmetic - // > or bitwise unary operator on its right, that is, 2**-1 is 0.5. - // - // Reference: https://docs.python.org/3/reference/expressions.html#id21 && left_precedence != OperatorPrecedence::Exponent { self.add_error( @@ -1259,7 +1259,7 @@ impl<'src> Parser<'src> { /// expression, `[` for a subscript expression, or `.` for an attribute expression. /// /// This method does nothing if the current token is not a candidate for a postfix expression. - pub(super) fn parse_postfix_expression( + fn parse_postfix_expression( &mut self, mut lhs: Expr, start: TextSize, @@ -1371,7 +1371,7 @@ impl<'src> Parser<'src> { Expr::Call(ast::ExprCall { func: Box::new(lhs), arguments, - range: self.node_range(start), + range_start: start, node_index: AtomicNodeIndex::NONE, is_cast: false, is_checked_cast: false, @@ -1483,13 +1483,14 @@ impl<'src> Parser<'src> { /// If the parser isn't position at a `(` token. /// /// See: - pub(super) fn parse_call_expression(&mut self, func: Expr, start: TextSize) -> ast::ExprCall { + fn parse_call_expression(&mut self, func: Expr, start: TextSize) -> ast::ExprCall { let arguments = self.parse_arguments(ArgumentsContext::Call); + debug_assert_eq!(self.node_range(start).end(), arguments.end()); ast::ExprCall { func: Box::new(func), arguments, - range: self.node_range(start), + range_start: start, node_index: AtomicNodeIndex::NONE, is_cast: false, is_checked_cast: false, @@ -4706,7 +4707,7 @@ impl<'src> Parser<'src> { /// parenthesized or the first token of the expression. /// /// See: - pub(super) fn parse_generator_expression( + fn parse_generator_expression( &mut self, element: Expr, start: TextSize, @@ -4957,11 +4958,7 @@ impl<'src> Parser<'src> { /// If the parser isn't positioned at a `:=` token. /// /// See: - pub(super) fn parse_named_expression( - &mut self, - mut target: Expr, - start: TextSize, - ) -> ast::ExprNamed { + fn parse_named_expression(&mut self, mut target: Expr, start: TextSize) -> ast::ExprNamed { self.bump(TokenKind::ColonEqual); if !target.is_name_expr() { @@ -5238,7 +5235,7 @@ impl ParsedExpr { } #[inline] - pub(super) const fn is_unparenthesized_named_expr(&self) -> bool { + const fn is_unparenthesized_named_expr(&self) -> bool { !self.is_parenthesized && self.expr.is_named_expr() } } @@ -5409,7 +5406,7 @@ impl ExpressionContext { ExpressionContext::starred_bitwise_or().with_yield_expression_allowed() } - pub(super) fn disallow_starred_expressions(self) -> Self { + fn disallow_starred_expressions(self) -> Self { let flags = self.0 & !ExpressionContextFlags::ALLOW_STARRED_EXPRESSION; ExpressionContext(flags) } diff --git a/crates/ruff_python_parser/src/parser/mod.rs b/crates/ruff_python_parser/src/parser/mod.rs index 56760c8550..a33e5e7f64 100644 --- a/crates/ruff_python_parser/src/parser/mod.rs +++ b/crates/ruff_python_parser/src/parser/mod.rs @@ -428,16 +428,19 @@ impl<'src> Parser<'src> { /// Moves the parser to the next token. fn do_bump(&mut self, kind: TokenKind) { - if !matches!( - self.current_token_kind(), + if match self.current_token_kind() { // TODO explore including everything up to the dedent as part of the body. - TokenKind::Dedent + TokenKind::Dedent => false, + // Don't include newlines in the body - | TokenKind::Newline + TokenKind::Newline => false, + // TODO(micha): Including the semi feels more correct but it isn't compatible with lalrpop and breaks the // formatters semicolon detection. Exclude it for now - | TokenKind::Semi - ) { + TokenKind::Semi => false, + + _ => true, + } { self.prev_token_end = self.current_token_range().end(); } @@ -675,7 +678,7 @@ impl<'src> Parser<'src> { /// # Panics /// /// If the current token is not a soft keyword. - pub(crate) fn bump_soft_keyword_as_name(&mut self) { + fn bump_soft_keyword_as_name(&mut self) { assert!(self.at_soft_keyword()); self.do_bump(TokenKind::Name); @@ -799,6 +802,7 @@ impl<'src> Parser<'src> { mut parse_element: impl FnMut(&mut Parser<'src>), ) { let mut progress = ParserProgress::default(); + let mut unexpected_indents = 0; let saved_context = self.recovery_context; self.recovery_context = self @@ -808,7 +812,12 @@ impl<'src> Parser<'src> { loop { progress.assert_progressing(self); - if recovery_context_kind.is_list_element(self) { + if 0 < unexpected_indents && self.at(TokenKind::Dedent) { + // Ignore this `Dedent` like we ignored the `Indent`, avoiding extra errors from + // being imbalanced + unexpected_indents -= 1; + self.bump(TokenKind::Dedent); + } else if recovery_context_kind.is_list_element(self) { parse_element(self); } else if recovery_context_kind.is_regular_list_terminator(self) { break; @@ -826,6 +835,14 @@ impl<'src> Parser<'src> { self.current_token_range(), ); + if matches!( + recovery_context_kind, + RecoveryContextKind::ModuleStatements | RecoveryContextKind::BlockStatements + ) && self.at(TokenKind::Indent) + { + // For this invalid `Indent`, ensure the matching `Dedent` gets consumed as well + unexpected_indents += 1; + } self.bump_any(); } } @@ -1287,24 +1304,26 @@ enum RecoveryContextKind { impl RecoveryContextKind { /// Returns `true` if a trailing comma is allowed in the current context. const fn allow_trailing_comma(self) -> bool { - matches!( - self, + match self { RecoveryContextKind::Slices - | RecoveryContextKind::TupleElements(_) - | RecoveryContextKind::SetElements - | RecoveryContextKind::ListElements - | RecoveryContextKind::DictElements - | RecoveryContextKind::Arguments - | RecoveryContextKind::MatchPatternMapping - | RecoveryContextKind::SequenceMatchPattern(_) - | RecoveryContextKind::MatchPatternClassArguments - // Only allow a trailing comma if the with item itself is parenthesized - | RecoveryContextKind::WithItems(WithItemKind::Parenthesized) - | RecoveryContextKind::Parameters(_) - | RecoveryContextKind::TypeParams - | RecoveryContextKind::DeleteTargets - | RecoveryContextKind::ImportFromAsNames(Parenthesized::Yes) - ) + | RecoveryContextKind::TupleElements(_) + | RecoveryContextKind::SetElements + | RecoveryContextKind::ListElements + | RecoveryContextKind::DictElements + | RecoveryContextKind::Arguments + | RecoveryContextKind::MatchPatternMapping + | RecoveryContextKind::SequenceMatchPattern(_) + | RecoveryContextKind::MatchPatternClassArguments + | RecoveryContextKind::Parameters(_) + | RecoveryContextKind::TypeParams + | RecoveryContextKind::DeleteTargets + | RecoveryContextKind::ImportFromAsNames(Parenthesized::Yes) => true, + + // Only allow a trailing comma if the with item itself is parenthesized + RecoveryContextKind::WithItems(WithItemKind::Parenthesized) => true, + + _ => false, + } } /// Returns `true` if the parser is at a token that terminates the list as per the context. diff --git a/crates/ruff_python_parser/src/parser/recovery.rs b/crates/ruff_python_parser/src/parser/recovery.rs index 1cfeb91d81..17b2725dcf 100644 --- a/crates/ruff_python_parser/src/parser/recovery.rs +++ b/crates/ruff_python_parser/src/parser/recovery.rs @@ -94,33 +94,37 @@ pub(super) fn pattern_to_expr(pattern: Pattern) -> Expr { node_index, cls, arguments, - }) => Expr::Call(ast::ExprCall { - range, - node_index: node_index.clone(), - func: cls, - arguments: ast::Arguments { - range: arguments.range, + }) => { + debug_assert_eq!(range.end(), arguments.end()); + + Expr::Call(ast::ExprCall { + range_start: range.start(), node_index: node_index.clone(), - args: arguments - .patterns - .into_iter() - .map(pattern_to_expr) - .collect(), - keywords: arguments - .keywords - .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(), - }, - is_cast: false, - is_checked_cast: false, - is_string_tag: false, - }), + func: cls, + arguments: ast::Arguments { + range: arguments.range, + node_index: node_index.clone(), + args: arguments + .patterns + .into_iter() + .map(pattern_to_expr) + .collect(), + keywords: arguments + .keywords + .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(), + }, + is_cast: false, + is_checked_cast: false, + is_string_tag: false, + }) + } Pattern::MatchStar(ast::PatternMatchStar { range, node_index, diff --git a/crates/ruff_python_parser/src/parser/statement.rs b/crates/ruff_python_parser/src/parser/statement.rs index 6459403733..1e33af88c8 100644 --- a/crates/ruff_python_parser/src/parser/statement.rs +++ b/crates/ruff_python_parser/src/parser/statement.rs @@ -3517,8 +3517,9 @@ impl<'src> Parser<'src> { } else { parser.add_error( ParseErrorType::OtherError( - "Only integer literals are allowed in subscript expressions in help end escape command" - .to_string() + "Only integer literals are allowed in subscript expressions \ + in help end escape command" + .to_string(), ), slice.range(), ); @@ -3535,8 +3536,9 @@ impl<'src> Parser<'src> { _ => { parser.add_error( ParseErrorType::OtherError( - "Expected name, subscript or attribute expression in help end escape command" - .to_string() + "Expected name, subscript or attribute expression \ + in help end escape command" + .to_string(), ), expr, ); @@ -6087,11 +6089,13 @@ impl<'src> Parser<'src> { self.add_error(error, &parsed_with_item.item.context_expr); } } else if self.at(TokenKind::Rpar) - // test_err with_items_parenthesized_missing_colon - // # `)` followed by a newline - // with (item1, item2) - // pass - && matches!(self.peek(), TokenKind::Colon | TokenKind::Newline) + && ( + // test_err with_items_parenthesized_missing_colon + // # `)` followed by a newline + // with (item1, item2) + // pass + matches!(self.peek(), TokenKind::Colon | TokenKind::Newline) + ) { if parsed_with_items.is_empty() { // No with items, treat it as a parenthesized expression to create an empty @@ -6806,7 +6810,9 @@ impl<'src> Parser<'src> { // x = 1 self.add_error( ParseErrorType::OtherError( - "Expected class, function definition or async function definition after decorator".to_string(), + "Expected class, function definition or async function definition \ + after decorator" + .to_string(), ), self.current_token_range(), ); @@ -7288,11 +7294,15 @@ impl<'src> Parser<'src> { let star_range = parser.current_token_range(); parser.bump(TokenKind::Star); - kwonlyargs_snapshot - .get_or_insert_with(|| parser.parameter_scratch.snapshot()); + kwonlyargs_snapshot.get_or_insert_with(|| parser.parameter_scratch.snapshot()); if parser.at_name_or_soft_keyword() { - let param = parser.parse_parameter(param_start, function_kind, AllowStarAnnotation::Yes, AllowContextModifier::No); + let param = parser.parse_parameter( + param_start, + function_kind, + AllowStarAnnotation::Yes, + AllowContextModifier::No, + ); let param_star_range = parser.node_range(star_range.start()); if parser.at(TokenKind::Equal) { @@ -7344,7 +7354,8 @@ impl<'src> Parser<'src> { // def foo(a, *args, b, c, *): ... parser.add_error( ParseErrorType::OtherError( - "Keyword-only parameter separator not allowed after '*' parameter" + "Keyword-only parameter separator not allowed \ + after '*' parameter" .to_string(), ), star_range, @@ -7359,7 +7370,12 @@ impl<'src> Parser<'src> { let double_star_range = parser.current_token_range(); parser.bump(TokenKind::DoubleStar); - let param = parser.parse_parameter(param_start, function_kind, AllowStarAnnotation::KeywordPackOnly, AllowContextModifier::No); + let param = parser.parse_parameter( + param_start, + function_kind, + AllowStarAnnotation::KeywordPackOnly, + AllowContextModifier::No, + ); let param_double_star_range = parser.node_range(double_star_range.start()); if parameters.kwarg.is_some() { @@ -7494,8 +7510,7 @@ impl<'src> Parser<'src> { // test_err params_non_default_after_default // def foo(a=10, b, c: int): ... - parser - .add_error(ParseErrorType::NonDefaultParamAfterDefaultParam, ¶m); + parser.add_error(ParseErrorType::NonDefaultParamAfterDefaultParam, ¶m); } seen_default_param |= param.default.is_some(); diff --git a/crates/ruff_python_parser/src/token_source.rs b/crates/ruff_python_parser/src/token_source.rs index 42e7139621..4557b283e4 100644 --- a/crates/ruff_python_parser/src/token_source.rs +++ b/crates/ruff_python_parser/src/token_source.rs @@ -20,7 +20,7 @@ pub(crate) struct TokenSource<'src> { impl<'src> TokenSource<'src> { /// Create a new token source for the given lexer. - pub(crate) fn new(lexer: Lexer<'src>, source: &str, start_offset: TextSize) -> Self { + fn new(lexer: Lexer<'src>, source: &str, start_offset: TextSize) -> Self { TokenSource { lexer, tokens: allocate_tokens_vec(&source[start_offset.to_usize()..]), diff --git a/crates/ruff_python_parser/tests/fixtures.rs b/crates/ruff_python_parser/tests/fixtures.rs index 928defd32f..ca6560362a 100644 --- a/crates/ruff_python_parser/tests/fixtures.rs +++ b/crates/ruff_python_parser/tests/fixtures.rs @@ -2,9 +2,9 @@ use std::cell::RefCell; use std::cmp::Ordering; use std::fmt::{Formatter, Write}; +use annotate_snippets::{AnnotationKind, Group, Level, Renderer, Snippet}; use datatest_stable::Utf8Path; use itertools::Itertools; -use ruff_annotate_snippets::{Level, Renderer, Snippet}; use ruff_python_ast::token::{Token, Tokens}; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::visitor::source_order::{SourceOrderVisitor, TraversalSignal, walk_module}; @@ -69,7 +69,10 @@ fn test_valid_syntax(input_path: &Utf8Path, source: &str, root: &str) { let line_index = LineIndex::from_source_text(source); let source_code = SourceCode::new(source, &line_index); - let mut message = "Expected no syntax errors for a valid program but the parser generated the following errors:\n".to_string(); + let mut message = "\ + Expected no syntax errors for a valid program \ + but the parser generated the following errors:\n" + .to_string(); for error in parsed.errors() { writeln!( @@ -218,7 +221,8 @@ fn test_invalid_syntax(input_path: &Utf8Path, source: &str, root: &str) { assert!( parsed.has_syntax_errors() || !semantic_syntax_errors.is_empty(), - "Expected parser to generate at least one syntax error for a program containing syntax errors." + "Expected parser to generate at least one syntax error \ + for a program containing syntax errors." ); if !semantic_syntax_errors.is_empty() { @@ -380,14 +384,14 @@ impl std::fmt::Display for CodeFrame<'_> { let label = format!("Syntax Error: {error}", error = self.error); let span = usize::from(annotation_range.start())..usize::from(annotation_range.end()); - let annotation = Level::Error.span(span).label(&label); + let annotation = AnnotationKind::Primary.span(span).label(&label); let snippet = Snippet::source(source) .line_start(start_index.get()) .annotation(annotation) .fold(false); - let message = Level::None.title("").snippet(snippet); + let message = Group::with_level(Level::ERROR).element(snippet); let renderer = Renderer::plain().cut_indicator("…"); - let rendered = renderer.render(message); + let rendered = renderer.render(&[message]); writeln!(f, "{rendered}") } } @@ -463,7 +467,13 @@ impl ValidateAstVisitor<'_> { // At this point, next_token.end() > node.start() assert!( next.start() >= node.start(), - "The start of the node falls within a token.\nNode: {node:#?}\n\nToken: {next:#?}\n\nRoot: {root:#?}", + "\ +The start of the node falls within a token. +Node: {node:#?} + +Token: {next:#?} + +Root: {root:#?}", root = self.parents.first() ); } @@ -482,7 +492,13 @@ impl ValidateAstVisitor<'_> { // At this point, `next_token.end() > node.end()` assert!( next.start() >= node.end(), - "The end of the node falls within a token.\nNode: {node:#?}\n\nToken: {next:#?}\n\nRoot: {root:#?}", + "\ +The end of the node falls within a token. +Node: {node:#?} + +Token: {next:#?} + +Root: {root:#?}", root = self.parents.first() ); } @@ -500,7 +516,13 @@ impl<'ast> SourceOrderVisitor<'ast> for ValidateAstVisitor<'ast> { assert_ne!( previous.range().ordering(node.range()), Ordering::Greater, - "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:#?}", + "\ +The ranges of the nodes are not strictly increasing when traversing the AST in pre-order. +Previous node: {previous:#?} + +Current node: {node:#?} + +Root: {root:#?}", root = self.parents.first() ); } @@ -508,7 +530,13 @@ impl<'ast> SourceOrderVisitor<'ast> for ValidateAstVisitor<'ast> { if let Some(parent) = self.parents.last() { assert!( parent.range().contains_range(node.range()), - "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:#?}", + "\ +The range of the parent node does not fully enclose the range of the child node. +Parent node: {parent:#?} + +Child node: {node:#?} + +Root: {root:#?}", root = self.parents.first() ); } diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@and_pattern_in_alternative.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@and_pattern_in_alternative.py.snap index 98a7256963..2670786dbc 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@and_pattern_in_alternative.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@and_pattern_in_alternative.py.snap @@ -138,7 +138,6 @@ Module( 1 | match x: 2 | case int() | (str() and "x"): ... | ^^^^^^^^^^^^^ Syntax Error: an `and` pattern is basedpython syntax and is not valid in .py files - | ## Semantic Syntax Errors @@ -147,4 +146,3 @@ Module( 1 | match x: 2 | case int() | (str() and "x"): ... | ^^^^^^^^^^^^^ Syntax Error: an `and` pattern cannot be written inside an alternative of a `|` pattern - | 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 21eb2b1619..32da2cab8d 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 @@ -219,7 +219,6 @@ Module( 3 | x: yield from b = 1 4 | x: y := int = 1 | ^^ Syntax Error: Expected a statement - | ## Semantic Syntax Errors 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 328e9318e0..2cf0f3fc6f 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 @@ -594,4 +594,3 @@ Module( 9 | [x]: int = 1 10 | [x, y]: int = 1, 2 | ^^^^^^ Syntax Error: Only single target (not list) can be annotated - | 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 f9ad11979a..2123f14a29 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 @@ -246,4 +246,3 @@ Module( 2 | x: Any = x := 1 3 | x: list = [x, *a | b, *a or b] | ^^^^^^ Syntax Error: Boolean expression cannot be used here - | 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 babd340987..db6d7e8c2d 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 @@ -43,4 +43,3 @@ Module( | 1 | x: int = | ^ Syntax Error: Expected an expression - | 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 9f4fa79111..8d3525dd17 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 @@ -121,4 +121,3 @@ Module( 1 | a: type X = int 2 | lambda: type X = int | ^ Syntax Error: Simple statements must be separated by newlines or semicolons - | 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 c5d463f5cc..1bb59a1ae0 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 @@ -355,4 +355,3 @@ Module( 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 fd78d8760b..2b647a1887 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 @@ -34,4 +34,3 @@ Module( | 1 | assert x, | ^ Syntax Error: Expected an expression - | 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 2c64815756..14a4dd1844 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 @@ -34,4 +34,3 @@ Module( | 1 | assert | ^ Syntax Error: Expected an expression - | 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 2cc454ed48..b8edfb474b 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 @@ -175,4 +175,3 @@ Module( 3 | assert False, yield x 4 | assert False, x := 1 | ^^ Syntax Error: Expected a statement - | 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 87c0dcf672..23311b018f 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 @@ -160,4 +160,3 @@ Module( 3 | assert yield x 4 | assert x := 1 | ^^ Syntax Error: Expected a statement - | 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 7516e2d842..1b06e85b42 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 @@ -276,7 +276,6 @@ Module( 3 | x = 1 = y = 2 = z 4 | ["a", "b"] = ["a", "b"] | ^^^ Syntax Error: Invalid assignment target - | | @@ -284,4 +283,3 @@ Module( 3 | x = 1 = y = 2 = z 4 | ["a", "b"] = ["a", "b"] | ^^^ Syntax Error: Invalid assignment target - | 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 3d35d2ae74..d3d5391377 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 @@ -362,4 +362,3 @@ Module( 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_starred_expr_value.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_starred_expr_value.py.snap index 2eab83b92f..4be2f342ce 100644 --- 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 @@ -220,4 +220,3 @@ Module( 3 | _ = *list() 4 | _ = *(p + q) | ^^^^^^^^ Syntax Error: Starred expression cannot be used here - | 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 dbe201539e..4fd519628a 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 @@ -255,4 +255,3 @@ Module( 5 | x += pass 6 | (x + y) += 1 | ^^^^^ Syntax Error: Invalid augmented assignment target - | 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 8013340f7e..c6c9011d3e 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 @@ -283,4 +283,3 @@ Module( 4 | x += *lambda x: x 5 | x += y := 1 | ^^ Syntax Error: Expected a statement - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@backslash_continuation_indentation_error.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@backslash_continuation_indentation_error.py.snap index 62318e2ddb..683cec4644 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@backslash_continuation_indentation_error.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@backslash_continuation_indentation_error.py.snap @@ -69,11 +69,3 @@ Module( 3 | / \ 4 | | 2 | |____^ Syntax Error: Unexpected indentation - | - - - | -3 | \ -4 | 2 - | ^ Syntax Error: Expected a statement - | 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 952ba305f7..5b757520e6 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 @@ -92,4 +92,3 @@ Module( 2 | case 1: 3 | case 2: ... | ^^^^ Syntax Error: Expected an indented block after `case` block - | 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 b12cc050bf..7a88362450 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 @@ -91,4 +91,3 @@ Module( 2 | class Foo(): 3 | x = 42 | ^ Syntax Error: Expected an indented block after `class` definition - | 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 218b289dc4..ffb66e8ee6 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 @@ -155,4 +155,3 @@ Module( 2 | class (): ... 3 | class (metaclass=ABC): ... | ^ Syntax Error: Expected an identifier - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_def_unparenthesized_generator_argument.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_def_unparenthesized_generator_argument.py.snap index 08770ff67f..6a5ebeb17c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_def_unparenthesized_generator_argument.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_def_unparenthesized_generator_argument.py.snap @@ -94,4 +94,3 @@ Module( | 1 | class Foo(base for base in bases): ... | ^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: Unparenthesized generator expression cannot be used here - | 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 28740810e2..a03fdfcb23 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 @@ -206,7 +206,6 @@ Module( 2 | class Foo[S: (str, bytes), T: float, *Ts, **P]: ... 3 | class Foo[]: ... | ^ Syntax Error: Type parameter list cannot be empty - | ## Unsupported Syntax Errors @@ -224,4 +223,3 @@ Module( 2 | class Foo[S: (str, bytes), T: float, *Ts, **P]: ... 3 | class Foo[]: ... | ^^ Syntax Error: Cannot use type parameter lists on Python 3.11 (syntax was added in Python 3.12) - | 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 36bf93cea3..828414aa03 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 @@ -69,4 +69,3 @@ Module( 5 | # at the newline token after `:` 6 | if True: | ^ Syntax Error: Expected an indented block after `if` statement - | 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 7f62aabbbd..c1e9cea650 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 @@ -58,4 +58,3 @@ Module( | 1 | if True: if True: pass | ^^ Syntax Error: Expected a simple statement - | 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 0a4f6534cb..0ee30f46db 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 @@ -72,10 +72,8 @@ Module( | 1 | call(**x := 1) | ^^ Syntax Error: Expected `,`, found `:=` - | | 1 | call(**x := 1) | ^ Syntax Error: Positional argument cannot follow keyword argument unpacking - | 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 4da8a91dea..69e493cafc 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 @@ -62,4 +62,3 @@ Module( 1 | # The comma between the first two elements is expected in `parse_list_expression`. 2 | [0, 1 2] | ^ Syntax Error: Expected `,`, found int - | 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 07e9b5e272..2c50e8d7c5 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 @@ -61,4 +61,3 @@ Module( | 1 | [0, 1, , 2] | ^ Syntax Error: Expected an expression or a ']' - | 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 0574c47423..d43012a40c 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 @@ -58,4 +58,3 @@ Module( | 1 | call(= 1) | ^ Syntax Error: Expected an expression or a ')' - | 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 a31a055919..dc0abcd271 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 @@ -86,4 +86,3 @@ Module( 1 | (async) 2 | (x async x in iter) | ^ Syntax Error: Expected `for`, found name - | 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 97da492040..32afc33ad9 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 @@ -114,4 +114,3 @@ Module( 1 | class __debug__: ... # class name 2 | class C[__debug__]: ... # type parameter name | ^^^^^^^^^ Syntax Error: cannot assign to `__debug__` - | 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 a959862398..e14373a0fe 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 @@ -272,4 +272,3 @@ Module( 3 | def f(__debug__): ... # parameter name 4 | lambda __debug__: 0 # lambda parameter name | ^^^^^^^^^ Syntax Error: cannot assign to `__debug__` - | 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 99dc1207b9..ca510e6d23 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 @@ -153,4 +153,3 @@ Module( 3 | from x import __debug__ 4 | from x import debug as __debug__ | ^^^^^^^^^ Syntax Error: cannot assign to `__debug__` - | 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 0fe9d38d82..6d47e009d4 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 @@ -69,4 +69,3 @@ Module( 1 | match x: 2 | case __debug__: ... | ^^^^^^^^^ Syntax Error: cannot assign to `__debug__` - | 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 1514d68600..827e2c62ac 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 @@ -82,4 +82,3 @@ Module( 1 | try: ... 2 | except Exception as __debug__: ... | ^^^^^^^^^ Syntax Error: cannot assign to `__debug__` - | 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 e4f2d20a92..9b5741407c 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 @@ -124,4 +124,3 @@ Module( 1 | type __debug__ = list[int] # visited as an Expr but still flagged 2 | type Debug[__debug__] = str | ^^^^^^^^^ Syntax Error: cannot assign to `__debug__` - | 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 3658cb79bc..8697455519 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 @@ -102,4 +102,3 @@ Module( | 1 | with open("foo.txt") as __debug__: ... | ^^^^^^^^^ Syntax Error: cannot assign to `__debug__` - | 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 806195b2b8..c32217d1da 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 @@ -288,4 +288,3 @@ Module( 7 | | @ 8 | | class Test | |__________^ Syntax Error: class without body requires `: ...` in .py files - | 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 d0eeacc7f2..23688558c2 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 @@ -186,4 +186,3 @@ Module( 2 | @x async def foo(): ... 3 | @x class Foo: ... | ^^^^^ Syntax Error: Expected newline, found `class` - | 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 9ee2833748..b27f2fd36b 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 @@ -174,4 +174,3 @@ Module( 3 | @foo 4 | x = 1 | ^ Syntax Error: Expected class, function definition or async function definition after decorator - | 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 83f2d82287..dbb86fe822 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 @@ -36,4 +36,3 @@ Module( 1 | # parse_options: {"target-version": "3.9"} 2 | del __debug__ | ^^^^^^^^^ Syntax Error: cannot delete `__debug__` on Python 3.9 (syntax was removed in 3.9) - | 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 07a2650425..007ffee8e2 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 @@ -139,4 +139,3 @@ Module( 3 | del x, y[ 4 | z | ^ Syntax Error: unexpected EOF while parsing - | 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 a769520c8a..28619bfea2 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 @@ -26,4 +26,3 @@ Module( | 1 | del | ^ Syntax Error: Delete statement must have at least one target - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@different_match_pattern_bindings.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@different_match_pattern_bindings.py.snap index 588dbe3cd8..9f689a303d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@different_match_pattern_bindings.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@different_match_pattern_bindings.py.snap @@ -1230,4 +1230,3 @@ Module( 12 | case [C(D(a))] | [C(D(b))]: ... 13 | case [(a, b)] | [(c, d)]: ... | ^^^^^^^^^^^^^^^^^^^ Syntax Error: alternative patterns bind different names - | 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 dd982db907..ebad60ec0a 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 @@ -91,11 +91,9 @@ Module( 1 | import a..b 2 | import a...b | ^^^ Syntax Error: Simple statements must be separated by newlines or semicolons - | | 1 | import a..b 2 | import a...b | ^ Syntax Error: Simple statements must be separated by newlines or semicolons - | 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 83e2d732da..94f9ad317c 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 @@ -820,7 +820,6 @@ Module( 5 | case [{}, {"x": x, "y": Foo(x=1, x=2)}]: ... 6 | case Class(x=1, d={"x": 1, "x": 2}, other=Class(x=1, x=2)): ... | ^^^ Syntax Error: mapping pattern checks duplicate key `"x"` - | | @@ -828,4 +827,3 @@ Module( 5 | case [{}, {"x": x, "y": Foo(x=1, x=2)}]: ... 6 | case Class(x=1, d={"x": 1, "x": 2}, other=Class(x=1, x=2)): ... | ^ Syntax Error: attribute name `x` repeated in class pattern - | 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 3fae8b2b2a..b3910e3204 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 @@ -819,4 +819,3 @@ Module( 6 | def f[T, *T](): ... # star is still duplicate 7 | def f[T, **T](): ... # as is double star | ^^^ Syntax Error: duplicate type parameter - | 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 668f37596b..56a1931678 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 @@ -158,4 +158,3 @@ Module( 4 | except* KeyError: ... 5 | except * Error: ... | ^ Syntax Error: Cannot use `except*` on Python 3.10 (syntax was added in Python 3.11) - | 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 ca2b53da7d..1b7e7563ee 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 @@ -251,7 +251,6 @@ Module( 4 | 5 | call(**x := 1) | ^^ Syntax Error: Expected `,`, found `:=` - | | @@ -259,4 +258,3 @@ Module( 4 | 5 | call(**x := 1) | ^ Syntax Error: Positional argument cannot follow keyword argument unpacking - | 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 5e0276c0e9..0c53c68065 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 @@ -149,10 +149,8 @@ Module( | 1 | foo(a=1, b=2, c=3, b=4, a=5) | ^^^ Syntax Error: Duplicate keyword argument "b" - | | 1 | foo(a=1, b=2, c=3, b=4, a=5) | ^^^ Syntax Error: Duplicate keyword argument "a" - | 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 b38a743f22..b35b985456 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 @@ -235,4 +235,3 @@ Module( 4 | call(yield x) 5 | call(yield from x) | ^^^^^^^^^^^^ Syntax Error: Yield expression cannot be used here - | 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 1d7c7ab01d..5b5ed3c883 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 @@ -271,4 +271,3 @@ Module( 3 | call(x = *y) 4 | call(x = (*y)) | ^^ Syntax Error: Starred expression cannot be used here - | 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 e61937e2b4..1b5b812e2f 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 @@ -358,4 +358,3 @@ Module( 4 | call(**kwargs, *args) 5 | call(**kwargs, (*args)) | ^^^^^ Syntax Error: Starred expression cannot be used here - | 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 32e305c995..de858fc886 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 @@ -65,4 +65,3 @@ Module( | 1 | call(x,,y) | ^ Syntax Error: Expected an expression or a ')' - | 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 ce15e4184a..d698050cdd 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 @@ -65,4 +65,3 @@ Module( | 1 | call(x y) | ^ Syntax Error: Expected `,`, found name - | 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 c6a21cb1ed..c55cd121a7 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 @@ -209,7 +209,6 @@ Module( 2 | call(*yield x) 3 | call(*yield from x) | ^^^^^^^^^^^^ Syntax Error: Yield expression cannot be used here - | ## Unsupported Syntax Errors 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 d91483aaca..2cb1cd9ba2 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 @@ -156,4 +156,3 @@ Module( 2 | x.1.0 3 | x.[0] | ^ Syntax Error: Expected an identifier - | 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 ee6569ca97..3ddda3dc18 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 @@ -168,7 +168,6 @@ Module( 2 | multiple....dots 3 | multiple.....dots | ^^^ Syntax Error: Simple statements must be separated by newlines or semicolons - | | @@ -176,4 +175,3 @@ Module( 2 | multiple....dots 3 | multiple.....dots | ^ Syntax Error: Expected an identifier - | 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 6703d57834..9d6dc68e42 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 @@ -98,4 +98,3 @@ Module( 5 | # No member access after the dot. 6 | last. | ^ Syntax Error: Expected an identifier - | 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 3959d0b642..d705109b95 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 @@ -379,4 +379,3 @@ Module( 16 | await ~x 17 | await not x | ^^^^^ Syntax Error: Boolean 'not' expression cannot be used here - | 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 0bfdfcc3df..d6f20c86c2 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 @@ -132,4 +132,3 @@ Module( 2 | 3 | x - yield y | ^^^^^^^ Syntax Error: Yield expression cannot be used here - | 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 a8a160249b..e66339024f 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 @@ -138,4 +138,3 @@ Module( 1 | x - y := (1, 2) 2 | x / y := 2 | ^^ Syntax Error: Expected a statement - | 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 5080466f7f..2e97b80a96 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 @@ -99,4 +99,3 @@ Module( 1 | x + *y 2 | x ** *y | ^^ Syntax Error: Starred expression cannot be used here - | 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 71ef87c324..cd969d4885 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 @@ -136,4 +136,3 @@ Module( 2 | 3 | x or yield y | ^^^^^^^ Syntax Error: Yield expression cannot be used here - | 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 7becb2d219..1a9fd28213 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 @@ -33,4 +33,3 @@ Module( | 1 | and y | ^^^ Syntax Error: Expected a statement - | 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 c599119ee6..4b0e330e7f 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 @@ -117,4 +117,3 @@ Module( 1 | x and a := b 2 | x or a := b | ^^ Syntax Error: Expected a statement - | 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 a63aa3e177..c9ef42bbbd 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 @@ -103,4 +103,3 @@ Module( 1 | x and *y 2 | x or *y | ^^ Syntax Error: Starred expression cannot be used here - | 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 ccc649ea7c..ff77fb79c9 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 @@ -169,18 +169,15 @@ Module( 6 | # Same here as well, `not` without `in` is considered to be a unary operator 7 | x not is y | ^^^ Syntax Error: Simple statements must be separated by newlines or semicolons - | | 6 | # Same here as well, `not` without `in` is considered to be a unary operator 7 | x not is y | ^^ Syntax Error: Expected an identifier, but found a keyword `is` that cannot be used here - | | 6 | # Same here as well, `not` without `in` is considered to be a unary operator 7 | x not is y | ^ Syntax Error: Simple statements must be separated by newlines or semicolons - | 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 88a5802b23..642c1d4402 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 @@ -140,4 +140,3 @@ Module( 2 | 3 | x == yield y | ^^^^^^^ Syntax Error: Yield expression cannot be used here - | 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 8b9b199bcd..7b1a47a58d 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 @@ -146,4 +146,3 @@ Module( 1 | x not in y := (1, 2) 2 | x > y := 2 | ^^ Syntax Error: Expected a statement - | 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 0de6ae37e0..4b546bf98c 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 @@ -205,4 +205,3 @@ Module( 4 | *x < y 5 | *x is not y | ^^^^^^^^^^ Syntax Error: Comparison expression cannot be used here - | 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 8d4a87d0c5..a574264843 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 @@ -970,4 +970,3 @@ Module( 16 | {x: y for x in data if yield from y} 17 | {x: y for x in data if lambda y: y} | ^^^^^^^^^^^ Syntax Error: Lambda expression cannot be used here - | 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 156967b382..d8ba224fd8 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 @@ -637,4 +637,3 @@ Module( 11 | {**x not in y} 12 | {**x < y} | ^^^^^ Syntax Error: Comparison expression cannot be used here - | 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 947af69e3c..890cd03b71 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 @@ -107,11 +107,9 @@ Module( 3 | def foo(): 4 | pass | ^^^^ Syntax Error: Expected an identifier, but found a keyword `pass` that cannot be used here - | | 3 | def foo(): 4 | pass | ^ Syntax Error: unexpected EOF while parsing - | 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 c53bcf8e92..3fda1e1433 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 @@ -72,4 +72,3 @@ Module( 2 | 3 | 1 + 2 | ^ Syntax Error: unexpected EOF while parsing - | 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 b1ad5d8255..c26c1b247d 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 @@ -558,7 +558,6 @@ Module( 23 | {*x: y, z: a, *b: c} 24 | {x: *y, z: *a} | ^^ Syntax Error: Starred expression cannot be used here - | | @@ -566,4 +565,3 @@ Module( 23 | {*x: y, z: a, *b: c} 24 | {x: *y, z: *a} | ^^ Syntax Error: Starred expression cannot be used here - | 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 ba444eb959..52729b5c7a 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 @@ -129,7 +129,6 @@ Module( 6 | # comment 7 | 🐶) | ^^ Syntax Error: Got unexpected token 🐶 - | | @@ -137,7 +136,6 @@ Module( 6 | # comment 7 | 🐶) | ^ Syntax Error: Expected a statement - | | @@ -145,4 +143,3 @@ Module( 6 | # comment 7 | 🐶) | ^ Syntax Error: Expected a statement - | 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 20d9f0812e..6b0e5ccbb9 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 @@ -18,4 +18,3 @@ Module( | 1 | 👍 | ^^ Syntax Error: Got unexpected token 👍 - | 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 75300342e4..3c5a63e70d 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 @@ -408,4 +408,3 @@ Module( 9 | x if expr else yield y 10 | x if expr else yield from y | ^^^^^^^^^^^^ Syntax Error: Yield expression cannot be used here - | 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 4c502c9325..8afcc02643 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 @@ -117,4 +117,3 @@ Module( | 1 | lambda a, b=20, c: 1 | ^ Syntax Error: Parameter without a default cannot follow a parameter with a default - | 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 066943c456..acab99ca68 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 @@ -374,7 +374,6 @@ Module( 8 | 9 | lambda a, *, **a: 1 | ^^^ Syntax Error: Expected one or more keyword parameter after `*` separator - | ## Semantic Syntax Errors @@ -422,4 +421,3 @@ Module( 8 | 9 | lambda a, *, **a: 1 | ^ Syntax Error: Duplicate parameter "a" - | 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 1c7dbb26c7..53d485de95 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 @@ -1304,7 +1304,6 @@ Module( 21 | [*x if x else y for x in z] 22 | [x if x else *y for x in z] | ^^ Syntax Error: Starred expression cannot be used here - | ## Unsupported Syntax Errors 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 27415fde73..1129c6f661 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 @@ -44,4 +44,3 @@ Module( 2 | 3 | [ | ^ Syntax Error: unexpected EOF while parsing - | 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 d53b5a8291..8ac3aa77ce 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 @@ -59,4 +59,3 @@ Module( 5 | 6 | x + y | ^ Syntax Error: unexpected EOF while parsing - | 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 3bfaaa689e..347768333c 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 @@ -68,4 +68,3 @@ Module( 5 | 6 | x + y | ^ Syntax Error: unexpected EOF while parsing - | 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 3fa2a32578..8188090268 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 @@ -344,4 +344,3 @@ Module( 19 | 20 | [*] | ^ Syntax Error: Expected an expression - | 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 40d0b6a03b..52c1ef0432 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 @@ -526,4 +526,3 @@ Module( 9 | [*lambda x: x, z] 10 | [*x := 2, z] | ^^ Syntax Error: Assignment expression target must be an identifier - | 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 60b30409c1..4b587a4cee 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 @@ -236,4 +236,3 @@ Module( 5 | (*x := 1) 6 | ([x, y] := [1, 2]) | ^^^^^^ Syntax Error: Assignment expression target must be an identifier - | 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 d434114a50..80360bc643 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 @@ -35,7 +35,6 @@ Module( 2 | 3 | x := | ^^ Syntax Error: Expected a statement - | | @@ -43,4 +42,3 @@ Module( 2 | 3 | x := | ^ Syntax Error: Expected a statement - | 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 d37bcfe97a..6adc7cf738 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 @@ -49,4 +49,3 @@ Module( 2 | 3 | (x := | ^ Syntax Error: unexpected EOF while parsing - | 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 7770661c8f..fa6735ca52 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 @@ -105,11 +105,9 @@ Module( 5 | def foo(): 6 | pass | ^^^^ Syntax Error: Expected an identifier, but found a keyword `pass` that cannot be used here - | | 5 | def foo(): 6 | pass | ^ Syntax Error: unexpected EOF while parsing - | 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 181c17c668..56494fe2b3 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 @@ -64,4 +64,3 @@ Module( 5 | 6 | x + y | ^ Syntax Error: unexpected EOF while parsing - | 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 77e2d9dc6a..617832fc69 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 @@ -103,22 +103,18 @@ Module( | 1 | (x := 1, for x in y) | ^^^ Syntax Error: Expected an identifier, but found a keyword `for` that cannot be used here - | | 1 | (x := 1, for x in y) | ^ Syntax Error: Expected `)`, found name - | | 1 | (x := 1, for x in y) | ^ Syntax Error: Expected a statement - | | 1 | (x := 1, for x in y) | ^ Syntax Error: Expected a statement - | 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 ed9f13abc6..be74ea3c8a 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 @@ -35,4 +35,3 @@ Module( 2 | 3 | ( | ^ Syntax Error: unexpected EOF while parsing - | 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 80fe213fc2..53ae176db3 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 @@ -50,4 +50,3 @@ Module( 5 | 6 | x + y | ^ Syntax Error: unexpected EOF while parsing - | 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 a156d20209..8986a789b1 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 @@ -73,4 +73,3 @@ Module( 5 | 6 | x + y | ^ Syntax Error: unexpected EOF while parsing - | 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 8622f0cd0b..fdd6c4a504 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 @@ -102,4 +102,3 @@ Module( 6 | def foo(): 7 | pass | ^ Syntax Error: unexpected EOF while parsing - | 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 2dc3440241..9f0372217c 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 @@ -79,4 +79,3 @@ Module( 4 | # Unparenthesized named expression is allowed. 5 | x := 1 | ^^ Syntax Error: Expected a statement - | 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 a7f4f955de..a4360ab7e8 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 @@ -457,4 +457,3 @@ Module( 20 | # Unparenthesized named expression is not allowed 21 | x, y := 2, z | ^^ Syntax Error: Expected `,`, found `:=` - | 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 a82f3fe003..3dcdb1c7b9 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 @@ -1475,7 +1475,6 @@ Module( 9 | (*lambda x: x, z, *lambda x: x) 10 | (*x := 2, z, *x := 2) | ^^ Syntax Error: Assignment expression target must be an identifier - | | @@ -1483,7 +1482,6 @@ Module( 9 | (*lambda x: x, z, *lambda x: x) 10 | (*x := 2, z, *x := 2) | ^^ Syntax Error: Assignment expression target must be an identifier - | | @@ -1607,7 +1605,6 @@ Module( 19 | *lambda x: x, z, *lambda x: x 20 | *x := 2, z, *x := 2 | ^^ Syntax Error: Expected a statement - | | @@ -1615,4 +1612,3 @@ Module( 19 | *lambda x: x, z, *lambda x: x 20 | *x := 2, z, *x := 2 | ^^ Syntax Error: Expected `,`, found `:=` - | 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 148391acf9..e8a8d7d55a 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 @@ -850,4 +850,3 @@ Module( 16 | {x for x in data if yield from y} 17 | {x for x in data if lambda y: y} | ^^^^^^^^^^^ Syntax Error: Lambda expression cannot be used here - | 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 759f2c99c0..b520e3eea9 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 @@ -43,4 +43,3 @@ Module( 2 | 3 | { | ^ Syntax Error: unexpected EOF while parsing - | 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 01bfdf4692..b746257d0b 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 @@ -58,4 +58,3 @@ Module( 5 | 6 | x + y | ^ Syntax Error: unexpected EOF while parsing - | 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 ead70d568e..ef03e861b0 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 @@ -67,4 +67,3 @@ Module( 5 | 6 | x + y | ^ Syntax Error: unexpected EOF while parsing - | 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 74e95fe8c7..8954c554fa 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 @@ -332,4 +332,3 @@ Module( 21 | 22 | [*] | ^ Syntax Error: Expected an expression - | 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 2e59be0ea3..5059784a2b 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 @@ -518,4 +518,3 @@ Module( 9 | {*lambda x: x, z} 10 | {*x := 2, z} | ^^ Syntax Error: Assignment expression target must be an identifier - | 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 c2d7c24b59..8861144dd9 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 @@ -343,4 +343,3 @@ Module( 11 | # Mixed starred expression and named expression 12 | x[*x := 1] | ^^ Syntax Error: Assignment expression target must be an identifier - | 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 dcbaa48129..7587ac212a 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 @@ -76,4 +76,3 @@ Module( 2 | 3 | x + y | ^ Syntax Error: unexpected EOF while parsing - | 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 34de4e5869..965535f99a 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 @@ -117,11 +117,9 @@ Module( 3 | def foo(): 4 | pass | ^^^^ Syntax Error: Expected an identifier, but found a keyword `pass` that cannot be used here - | | 3 | def foo(): 4 | pass | ^ Syntax Error: unexpected EOF while parsing - | 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 b8f917a7dd..3510eb8a3c 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 @@ -55,4 +55,3 @@ Module( | 1 | not x := 1 | ^^ Syntax Error: Expected a statement - | 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 45a740ba23..a410a5cb73 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 @@ -99,4 +99,3 @@ Module( 1 | -x := 1 2 | not x := 1 | ^^ Syntax Error: Expected a statement - | 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 f8a2586958..d9a7de3a48 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 @@ -130,4 +130,3 @@ Module( 3 | 4 | yield 1, x := 2, 3 | ^^ Syntax Error: Expected `,`, found `:=` - | 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 a55de3a489..a116751210 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 @@ -127,7 +127,6 @@ Module( 3 | 4 | yield *x and y, z | ^^^^^^^ Syntax Error: Boolean expression cannot be used here - | ## Semantic Syntax Errors 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 1a30c39dab..4bd15ed098 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 @@ -105,4 +105,3 @@ Module( 3 | yield from *x 4 | yield from *x, y | ^^ Syntax Error: Starred expression cannot be used here - | 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 183348188e..209121f13c 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 @@ -182,4 +182,3 @@ Module( 8 | # vvvvvvvvvvvvv 9 | yield from (x, *x and y) | ^^^^^^^ Syntax Error: Boolean expression cannot be used here - | 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 index 63c3670334..aef6b79cbf 100644 --- 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 @@ -176,7 +176,6 @@ Module( 2 | t"{x! s}" 3 | f"{x! z}" | ^ Syntax Error: f-string: conversion type must come right after the exclamation mark - | | @@ -184,4 +183,3 @@ Module( 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 f18251146b..68b565cf3b 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 @@ -121,4 +121,3 @@ Module( 1 | f"{}" 2 | f"{ }" | ^ Syntax Error: Expected an expression - | 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 a5603a2dd3..2086898197 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 @@ -66,4 +66,3 @@ Module( | 1 | f"{x!z}" | ^ Syntax Error: f-string: invalid conversion character - | 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 f69dee9e5c..a2ad5f30d0 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 @@ -121,4 +121,3 @@ Module( 1 | f"{x!123}" 2 | f"{x!'a'}" | ^^^ Syntax Error: f-string: invalid conversion character - | 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 20091bbe02..56eacd2d90 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 @@ -226,4 +226,3 @@ Module( 3 | f"{*x and y}" 4 | f"{*yield x}" | ^^^^^^^ Syntax Error: Yield expression cannot be used here - | 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 81cd34b517..6729e07eb1 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 @@ -110,22 +110,18 @@ Module( | 1 | f"{lambda x: x}" | ^^ Syntax Error: Expected an expression - | | 1 | f"{lambda x: x}" | ^^^^^^^^^ Syntax Error: f-string: lambda expressions are not allowed without parentheses - | | 1 | f"{lambda x: x}" | ^^ Syntax Error: f-string: expecting `}` - | | 1 | f"{lambda x: x}" | ^ Syntax Error: Expected an element of or the end of the f-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 3fa3b46130..acff579347 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 @@ -297,4 +297,3 @@ Module( 4 | f"{" 5 | f"""{""" | ^^^ Syntax Error: Expected an expression - | 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 ac1d7c98f4..1ac023ab76 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 @@ -155,4 +155,3 @@ Module( 1 | f"hello {x:" 2 | f"hello {x:.3f" | ^ Syntax Error: f-string: expecting `}` - | 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 394d0e8783..3356fbe7f8 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 @@ -250,7 +250,6 @@ Module( 3 | for x in a, *b: ... 4 | for x in *a, *b: ... | ^^ Syntax Error: Cannot use iterable unpacking in `for` statements on Python 3.8 (syntax was added in Python 3.9) - | | @@ -258,4 +257,3 @@ Module( 3 | for x in a, *b: ... 4 | for x in *a, *b: ... | ^^ Syntax Error: Cannot use iterable unpacking in `for` statements on Python 3.8 (syntax was added in Python 3.9) - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_destructure_in_python_file.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_destructure_in_python_file.py.snap index 447029a5f4..3332d197eb 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_destructure_in_python_file.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_destructure_in_python_file.py.snap @@ -90,4 +90,3 @@ Module( | 1 | for Point(x, y) in points: ... | ^^^^^^^^^^^ Syntax Error: Invalid assignment target - | 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 c952ad0d26..851af7737d 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 @@ -196,7 +196,6 @@ Module( 2 | for x in yield a: ... 3 | for target in x := 1: ... | ^^ Syntax Error: Expected `:`, found `:=` - | | @@ -204,7 +203,6 @@ Module( 2 | for x in yield a: ... 3 | for target in x := 1: ... | ^ Syntax Error: Invalid annotated assignment target - | ## Semantic Syntax Errors 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 b226ffa696..6da0d01b1f 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 @@ -516,7 +516,6 @@ Module( 6 | for yield x in y: ... 7 | for [x, 1, y, *["a"]] in z: ... | ^ Syntax Error: Invalid assignment target - | | @@ -524,7 +523,6 @@ Module( 6 | for yield x in y: ... 7 | for [x, 1, y, *["a"]] in z: ... | ^^^ Syntax Error: Invalid assignment target - | ## Semantic Syntax Errors 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 9bcfa6b099..ffcc5f43b8 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 @@ -385,4 +385,3 @@ Module( 5 | for not x in y: ... 6 | for x | y in z: ... | ^^^^^ Syntax Error: Invalid assignment target - | 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 e752accca9..bd028fd566 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 @@ -509,4 +509,3 @@ Module( 5 | for [x in y, z] in iter: ... 6 | for {x in y, z} in iter: ... | ^^^^^^^^^^^ Syntax Error: Invalid assignment target - | 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 188dc9fe70..9db22c978e 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 @@ -105,4 +105,3 @@ Module( 1 | for a b: ... 2 | for a: ... | ^ Syntax Error: Expected `in`, found `:` - | 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 e72c0fa5e6..b2c3ff0424 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 @@ -58,4 +58,3 @@ Module( | 1 | for in x: ... | ^^ Syntax Error: Expected an identifier - | 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 b095c48659..6ef0484859 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 @@ -191,7 +191,6 @@ Module( 2 | from x import a.b 3 | from x import a, b.c, d, e.f, g | ^ Syntax Error: Expected `,`, found `.` - | | @@ -199,4 +198,3 @@ Module( 2 | from x import a.b 3 | from x import a, b.c, d, e.f, g | ^ Syntax Error: Expected `,`, found `.` - | 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 cb5b3143b1..811a1a3804 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 @@ -88,7 +88,6 @@ Module( 2 | from x import () 3 | from x import ,, | ^ Syntax Error: Expected an import name - | | @@ -96,7 +95,6 @@ Module( 2 | from x import () 3 | from x import ,, | ^ Syntax Error: Expected an import name - | | @@ -104,4 +102,3 @@ Module( 2 | from x import () 3 | from x import ,, | ^ Syntax Error: Expected one or more symbol names after import - | 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 c706b79a93..e5b8da2347 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 @@ -60,4 +60,3 @@ Module( 1 | from 2 | from import x | ^^^^^^ Syntax Error: Expected a module name - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_parenthesized_star.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_parenthesized_star.py.snap index b8ba0a49db..30839311f4 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_parenthesized_star.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_parenthesized_star.py.snap @@ -47,4 +47,3 @@ Module( | 1 | from x import (*) | ^^ Syntax Error: Star import cannot be parenthesized - | 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 43a57285ba..12e70a6758 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 @@ -225,4 +225,3 @@ Module( 3 | from x import *, a as b 4 | from x import *, *, a | ^^^^^^^ Syntax Error: Star import must be the only import - | 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 1904ca8e1d..e3142b3641 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 @@ -137,4 +137,3 @@ Module( 2 | from a import b as c, 3 | from a import b, c, | ^ Syntax Error: Trailing comma not allowed - | 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 69be299166..eacf9a76ba 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 @@ -119,4 +119,3 @@ Module( 2 | def foo() -> int: 3 | x = 42 | ^ Syntax Error: Expected an indented block after function definition - | 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 076d93c769..ba8efecd69 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 @@ -209,7 +209,6 @@ Module( 2 | def foo() -> (*int): ... 3 | def foo() -> yield x: ... | ^^^^^^^ Syntax Error: Yield expression cannot be used here - | ## Semantic Syntax Errors @@ -219,4 +218,3 @@ Module( 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 b298920539..45d64eee8a 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 @@ -118,4 +118,3 @@ Module( 1 | def (): ... 2 | def () -> int: ... | ^ Syntax Error: Expected an identifier - | 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 78c3244c4c..27fc4ed597 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 @@ -60,4 +60,3 @@ Module( | 1 | def foo() -> : ... | ^ Syntax Error: Expected an expression - | 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 0fe18efb49..0f944236de 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 @@ -270,11 +270,9 @@ Module( 4 | def foo(a: int, b: str 5 | x = 10 | ^ Syntax Error: Expected `,`, found name - | | 4 | def foo(a: int, b: str 5 | x = 10 | ^ Syntax Error: unexpected EOF while parsing - | 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 0ec10f6677..ef8778297b 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 @@ -163,4 +163,3 @@ Module( 1 | def foo() -> int,: ... 2 | def foo() -> int, str: ... | ^^^^^^^^ Syntax Error: Multiple return types must be parenthesized - | 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 0227e64775..5df781ab9b 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 @@ -146,7 +146,6 @@ Module( 2 | def foo[T](): ... 3 | def foo[](): ... | ^ Syntax Error: Type parameter list cannot be empty - | ## Unsupported Syntax Errors @@ -164,4 +163,3 @@ Module( 2 | def foo[T](): ... 3 | def foo[](): ... | ^^ Syntax Error: Cannot use type parameter lists on Python 3.11 (syntax was added in Python 3.12) - | 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 5971aa62c7..92375a3dfb 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 @@ -26,4 +26,3 @@ Module( | 1 | global | ^ Syntax Error: Global statement must have at least one name - | 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 c80bc04505..018d9049ee 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 @@ -54,4 +54,3 @@ Module( | 1 | global x + 1 | ^ Syntax Error: Simple statements must be separated by newlines or semicolons - | 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 6891697042..22cbfb0016 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 @@ -83,4 +83,3 @@ Module( 2 | global x, 3 | global x, y, | ^ Syntax Error: Trailing comma not allowed - | 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 6dcd52ac68..61d5c7c1c6 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 @@ -67,4 +67,3 @@ Module( 1 | if True: 2 | 1 + 1 | ^ Syntax Error: Expected an indented block after `if` statement - | 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 240eecd1e6..a155dae8c7 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 @@ -148,4 +148,3 @@ Module( 2 | if yield x: ... 3 | if yield from x: ... | ^^^^^^^^^^^^ Syntax Error: Yield expression cannot be used here - | 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 b4ef47b9ad..d7a8a6c557 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 @@ -49,4 +49,3 @@ Module( | 1 | if : ... | ^ Syntax Error: Expected an expression - | 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 2fa70ca9c1..5a3f8488d2 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 @@ -99,8 +99,8 @@ Module( | 3 | elf: 4 | pass - | ^ Syntax Error: Expected a statement 5 | else: + | ^^^^ Syntax Error: Expected a statement 6 | pass | @@ -128,11 +128,3 @@ Module( 5 | else: 6 | pass | ^^^^ Syntax Error: Unexpected indentation - | - - - | -5 | else: -6 | pass - | ^ Syntax Error: Expected a statement - | 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 873a39cf58..47f3fd9311 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 @@ -38,4 +38,3 @@ Module( | 1 | import x as | ^ Syntax Error: Expected symbol after `as` - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_from_star.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_from_star.py.snap index 898d46ed82..b3e52b7258 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_from_star.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_from_star.py.snap @@ -246,7 +246,6 @@ Module( 7 | def f3(): 8 | from module import *, * | ^^^^ Syntax Error: Star import must be the only import - | ## Semantic Syntax Errors @@ -285,4 +284,3 @@ Module( 7 | def f3(): 8 | from module import *, * | ^^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: `from module import *` only allowed at module level - | 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 3743669828..6f254cafc4 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 @@ -27,4 +27,3 @@ Module( | 1 | import | ^ Syntax Error: Expected one or more symbol names after import - | 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 3e7dfff172..b482abc1aa 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 @@ -93,4 +93,3 @@ Module( 1 | import (a) 2 | import (a, b) | ^ Syntax Error: Expected one or more symbol names after import - | 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 02ffff01e6..89a4ac84b0 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 @@ -125,18 +125,15 @@ Module( 1 | import * 2 | import x, *, y | ^ Syntax Error: Trailing comma not allowed - | | 1 | import * 2 | import x, *, y | ^ Syntax Error: Simple statements must be separated by newlines or semicolons - | | 1 | import * 2 | import x, *, y | ^ Syntax Error: Expected an expression - | 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 7114dbf8d2..37a9842027 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 @@ -71,4 +71,3 @@ Module( 1 | import , 2 | import x, y, | ^ Syntax Error: Trailing comma not allowed - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@incomplete_attribute_before_for_in_delimiter.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@incomplete_attribute_before_for_in_delimiter.py.snap index 84fe57d6c2..8c3cf3eaf0 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@incomplete_attribute_before_for_in_delimiter.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@incomplete_attribute_before_for_in_delimiter.py.snap @@ -405,4 +405,3 @@ Module( 5 | [item for item. in xs] 6 | for item. in xs: ... | ^^ Syntax Error: Expected an identifier - | 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 0040758a2b..0b5fd6de2b 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 @@ -651,4 +651,3 @@ Module( 6 | class M[T]((await 1)): ... 7 | class N[T: (await 1)]: ... | ^^^^^^^ Syntax Error: await expression cannot be used within a TypeVar bound - | 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 0edb5dce5a..2b70e3ffda 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 @@ -2085,4 +2085,3 @@ Module( 19 | def v[*Ts = (await 1)](): ... # await in TypeVarTuple default 20 | def w[**Ts = (await 1)](): ... # await in ParamSpec default | ^^^^^^^ Syntax Error: await expression cannot be used within a ParamSpec 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 563a2fa8b9..bf557defe8 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 @@ -701,4 +701,3 @@ Module( 10 | def f() -> (await 1): ... 11 | def g(arg: (await 1)): ... | ^^^^^^^ Syntax Error: await expression cannot be used within a type annotation - | 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 92bd8f4547..9ff06ab9a0 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 @@ -243,4 +243,3 @@ Module( 6 | async def outer(): 7 | d: (await 1) | ^^^^^^^ Syntax Error: await expression cannot be used within a type annotation - | 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 311bf69757..54b057ca25 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 @@ -555,4 +555,3 @@ Module( 7 | type Y[T: (await 1)] = int # await in bound 8 | type Y = (await 1) # await in value | ^^^^^^^ Syntax Error: await expression cannot be used within a type alias - | 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 a330b1ce96..0de2a88389 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 @@ -120,4 +120,3 @@ Module( 2 | rb"a𝐁c123" 3 | b"""123a𝐁c""" | ^ Syntax Error: bytes can only contain ASCII literal characters - | 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 5834d7311a..cc7fbd303a 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 @@ -254,7 +254,6 @@ Module( 3 | del {'x', 'y'} 4 | del None, True, False, 1, 1.0, "abc" | ^^^^ Syntax Error: Invalid delete target - | | @@ -262,7 +261,6 @@ Module( 3 | del {'x', 'y'} 4 | del None, True, False, 1, 1.0, "abc" | ^^^^ Syntax Error: Invalid delete target - | | @@ -270,7 +268,6 @@ Module( 3 | del {'x', 'y'} 4 | del None, True, False, 1, 1.0, "abc" | ^^^^^ Syntax Error: Invalid delete target - | | @@ -278,7 +275,6 @@ Module( 3 | del {'x', 'y'} 4 | del None, True, False, 1, 1.0, "abc" | ^ Syntax Error: Invalid delete target - | | @@ -286,7 +282,6 @@ Module( 3 | del {'x', 'y'} 4 | del None, True, False, 1, 1.0, "abc" | ^^^ Syntax Error: Invalid delete target - | | @@ -294,4 +289,3 @@ Module( 3 | del {'x', 'y'} 4 | del None, True, False, 1, 1.0, "abc" | ^^^^^ Syntax Error: Invalid delete target - | 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 0d2f651c21..fc74da89bf 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 @@ -101,4 +101,3 @@ Module( 1 | f'hello \N{INVALID} world' 2 | f"""hello \N{INVALID} world""" | ^^^^^^^ Syntax Error: Got unexpected unicode - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_future_feature.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_future_feature.py.snap index c336defaf4..64f6b1d6fa 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_future_feature.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_future_feature.py.snap @@ -141,7 +141,6 @@ Module( 2 | from __future__ import annotations, invalid_feature 3 | from __future__ import invalid_feature_1, invalid_feature_2 | ^^^^^^^^^^^^^^^^^ Syntax Error: Future feature `invalid_feature_1` is not defined - | | @@ -149,4 +148,3 @@ Module( 2 | from __future__ import annotations, invalid_feature 3 | from __future__ import invalid_feature_1, invalid_feature_2 | ^^^^^^^^^^^^^^^^^ Syntax Error: Future feature `invalid_feature_2` is not defined - | 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 72cdf3cf6a..4d20a34438 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 @@ -81,4 +81,3 @@ Module( 1 | 'hello \N{INVALID} world' 2 | """hello \N{INVALID} world""" | ^^^^^^^ Syntax Error: Got unexpected unicode - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ipython_help_escape_command_error_recovery_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ipython_help_escape_command_error_recovery_1.py.snap index 7afa2fdbe9..c158b49647 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ipython_help_escape_command_error_recovery_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ipython_help_escape_command_error_recovery_1.py.snap @@ -83,4 +83,3 @@ Module( 2 | with (a, ?b) 3 | ? | ^ Syntax Error: Expected an indented block after `with` statement - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ipython_help_escape_command_error_recovery_2.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ipython_help_escape_command_error_recovery_2.py.snap index a97c39186d..e4fef9fe84 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ipython_help_escape_command_error_recovery_2.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ipython_help_escape_command_error_recovery_2.py.snap @@ -89,11 +89,9 @@ Module( 2 | with (a, ?b 3 | ? | ^ Syntax Error: Expected `,`, found `?` - | | 2 | with (a, ?b 3 | ? | ^ Syntax Error: unexpected EOF while parsing - | 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 6043236c7a..29b2a00df5 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 @@ -171,4 +171,3 @@ Module( 2 | rest = (4, 5, 6) 3 | def f(): return 1, 2, 3, *rest | ^^^^^ Syntax Error: Cannot use iterable unpacking in return statements on Python 3.7 (syntax was added in Python 3.8) - | 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 c0ec9d11b3..ebe2730cd1 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 @@ -307,4 +307,3 @@ Module( 3 | def g(): yield 1, 2, 3, *rest 4 | def h(): yield 1, (yield 2, *rest), 3 | ^^^^^ Syntax Error: Cannot use iterable unpacking in yield expressions on Python 3.7 (syntax was added in Python 3.8) - | 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 52ea84132a..f97ca22f73 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 @@ -332,4 +332,3 @@ Module( 3 | lambda x: *y, z 4 | lambda x: *y and z | ^^^^^^^^ Syntax Error: Starred expression cannot be used here - | 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 a87dc880a2..b19cfe9d61 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 @@ -143,4 +143,3 @@ Module( 1 | lambda x: yield y 2 | lambda x: yield from y | ^^^^^^^^^^^^ Syntax Error: Yield expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lazy_import_invalid_context_py315.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lazy_import_invalid_context_py315.py.snap index 1e433b0bd1..c014b6a345 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lazy_import_invalid_context_py315.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lazy_import_invalid_context_py315.py.snap @@ -385,4 +385,3 @@ Module( 22 | class Inner: 23 | lazy import json | ^^^^^^^^^^^^^^^^ Syntax Error: lazy import not allowed inside classes - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lazy_import_invalid_from_py315.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lazy_import_invalid_from_py315.py.snap index ba06fa6088..92145e8fa9 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lazy_import_invalid_from_py315.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lazy_import_invalid_from_py315.py.snap @@ -151,11 +151,9 @@ Module( 5 | def func(): 6 | lazy from sys import * | ^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: lazy from ... import not allowed inside functions - | | 5 | def func(): 6 | lazy from sys import * | ^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: `from sys import *` only allowed at module level - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lazy_import_stmt_py314.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lazy_import_stmt_py314.py.snap index 493c831566..1e8c6b1536 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lazy_import_stmt_py314.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lazy_import_stmt_py314.py.snap @@ -76,4 +76,3 @@ Module( 2 | lazy import foo 3 | lazy from bar import baz | ^^^^ Syntax Error: Cannot use `lazy` import statement on Python 3.14 (syntax was added in Python 3.15) - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@let_stmt_in_python_file.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@let_stmt_in_python_file.py.snap index 124730149f..9f484d0fd2 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@let_stmt_in_python_file.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@let_stmt_in_python_file.py.snap @@ -83,4 +83,3 @@ Module( | 1 | let Point(x, y) := origin | ^^^^^^^^^^^^^^^ Syntax Error: a destructuring `let` is not valid in .py files - | 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 60781de936..f8ff37f51a 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 @@ -63,11 +63,9 @@ Module( 1 | match foo: 2 | case _: ... | ^^^^ Syntax Error: Expected an indented block after `match` statement - | | 1 | match foo: 2 | case _: ... | ^ Syntax Error: Expected dedent, found end of file - | 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 8aaaa11d01..561ee48a26 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 @@ -126,8 +126,8 @@ Module( | 1 | match x: 2 | x = 1 - | ^ Syntax Error: Expected a statement 3 | match x: + | ^ Syntax Error: Expected a statement 4 | match y: 5 | case _: ... | @@ -146,4 +146,3 @@ Module( 4 | match y: 5 | case _: ... | ^ Syntax Error: Expected a statement - | 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 967dfc2d2e..5ae960f29c 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 @@ -236,4 +236,3 @@ Module( 5 | match x: 6 | case y if yield x: ... | ^^^^^^^ Syntax Error: Yield expression cannot be used here - | 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 bf66acae17..880d99610c 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 @@ -69,4 +69,3 @@ Module( 1 | match x: 2 | case y if: ... | ^ Syntax Error: Expected an expression - | 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 84fad76972..17247bca62 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 @@ -69,4 +69,3 @@ Module( 1 | match x: 2 | case : ... | ^ Syntax Error: Expected a pattern - | 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 7888bcd48e..7d49a7f558 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 @@ -62,10 +62,8 @@ Module( | 1 | match foo: case _: ... | ^^^^ Syntax Error: Expected newline, found `case` - | | 1 | match foo: case _: ... | ^ Syntax Error: Expected dedent, found end of file - | 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 6ac75db9d5..829f518bed 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 @@ -197,4 +197,3 @@ Module( 2 | f'first' b'second' 3 | 'first' f'second' b'third' | ^^^^^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: Bytes literal cannot be mixed with non-bytes literals - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_assignment_in_and_pattern.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_assignment_in_and_pattern.py.snap index 59f6584e39..f064517ded 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_assignment_in_and_pattern.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_assignment_in_and_pattern.py.snap @@ -121,7 +121,6 @@ Module( 1 | match 2: 2 | case Class(x) and [x]: ... | ^^^^^^^^^^^^^^^^ Syntax Error: an `and` pattern is basedpython syntax and is not valid in .py files - | ## Semantic Syntax Errors @@ -130,4 +129,3 @@ Module( 1 | match 2: 2 | case Class(x) and [x]: ... | ^ Syntax Error: multiple assignments to name `x` in pattern - | 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 3aed113557..234f5001e0 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 @@ -819,4 +819,3 @@ Module( 9 | case [x] | {1: x} | Class(y=x, z=x): ... # MatchOr 10 | case x as x: ... # MatchAs | ^ Syntax Error: multiple assignments to name `x` in pattern - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_assignment_in_let_pattern.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_assignment_in_let_pattern.py.snap index c6ce49b19c..2bd234a4fd 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_assignment_in_let_pattern.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_assignment_in_let_pattern.py.snap @@ -140,13 +140,11 @@ Module( | 1 | let Point(x, y) and Point(x, y) := origin | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: an `and` pattern is basedpython syntax and is not valid in .py files - | | 1 | let Point(x, y) and Point(x, y) := origin | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: a destructuring `let` is not valid in .py files - | ## Semantic Syntax Errors @@ -154,10 +152,8 @@ Module( | 1 | let Point(x, y) and Point(x, y) := origin | ^ Syntax Error: multiple assignments to name `x` in pattern - | | 1 | let Point(x, y) and Point(x, y) := origin | ^ Syntax Error: multiple assignments to name `y` in pattern - | 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 322c58d57c..8e3f83bb81 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 @@ -420,7 +420,6 @@ Module( 5 | try: pass except exc: pass else: pass finally: pass 6 | try: pass; except exc: pass; else: pass; finally: pass | ^^^^^^ Syntax Error: Expected newline, found `except` - | | @@ -428,7 +427,6 @@ Module( 5 | try: pass except exc: pass else: pass finally: pass 6 | try: pass; except exc: pass; else: pass; finally: pass | ^^^^ Syntax Error: Expected newline, found `else` - | | @@ -436,4 +434,3 @@ Module( 5 | try: pass except exc: pass else: pass finally: pass 6 | try: pass; except exc: pass; else: pass; finally: pass | ^^^^^^^ Syntax Error: Expected newline, found `finally` - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_starred_assignment_target.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_starred_assignment_target.py.snap index 18d091a02d..8a7d9614d1 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_starred_assignment_target.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_starred_assignment_target.py.snap @@ -545,7 +545,6 @@ Module( 4 | [*a, *b, c] = (1, 2, 3) 5 | (*a, *b, (*c, *d)) = (1, 2) | ^^^^^^^^^^^^^^^^^^ Syntax Error: Two starred expressions in assignment - | | @@ -553,4 +552,3 @@ Module( 4 | [*a, *b, c] = (1, 2, 3) 5 | (*a, *b, (*c, *d)) = (1, 2) | ^^^^^^^^ Syntax Error: Two starred expressions in assignment - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_starred_names_in_sequence_pattern.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_starred_names_in_sequence_pattern.py.snap index 62a1d0db00..c289a7895a 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_starred_names_in_sequence_pattern.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_starred_names_in_sequence_pattern.py.snap @@ -102,11 +102,9 @@ Module( 1 | match subject: 2 | case *first, *second, *third: ... | ^^^^^^^ Syntax Error: multiple starred names in sequence pattern - | | 1 | match subject: 2 | case *first, *second, *third: ... | ^^^^^^ Syntax Error: multiple starred names in sequence pattern - | 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 e870a50e6f..3fbb01c0c2 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 @@ -269,7 +269,6 @@ Module( 3 | lst[1:x:=1] 4 | lst[1:3:x:=1] | ^^ Syntax Error: Expected `]`, found `:=` - | | @@ -277,7 +276,6 @@ Module( 3 | lst[1:x:=1] 4 | lst[1:3:x:=1] | ^ Syntax Error: Expected a statement - | | @@ -285,4 +283,3 @@ Module( 3 | lst[1:x:=1] 4 | lst[1:3:x:=1] | ^ Syntax Error: Expected a statement - | 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 e3a588825a..f707943ab3 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 @@ -93,4 +93,3 @@ Module( 2 | # before 3.9, only emit the parse error, not the unsupported syntax error 3 | lst[x:=1:-1] | ^^^^ Syntax Error: Unparenthesized named expression cannot be used here - | 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 3629176dba..f44a14b8c8 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 @@ -996,4 +996,3 @@ Module( 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 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@nested_quote_in_format_spec_py312.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nested_quote_in_format_spec_py312.py.snap index 7eec20f80b..187240a204 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nested_quote_in_format_spec_py312.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nested_quote_in_format_spec_py312.py.snap @@ -89,4 +89,3 @@ Module( 1 | # parse_options: {"target-version": "3.12"} 2 | f"{1:""}" # this is a ParseError on all versions | ^ Syntax Error: f-string: expecting `}` - | 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 8cc3a105df..c1bb66bda6 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 @@ -134,11 +134,9 @@ Module( 2 | def bar(): ... 3 | def baz | ^ Syntax Error: Expected `(`, found newline - | | 2 | def bar(): ... 3 | def baz | ^ Syntax Error: Expected `)`, found end of file - | 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 index ecfbbe06d2..5f81d3505d 100644 --- 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 @@ -58,4 +58,3 @@ Module( 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 089bf26f13..b4faad46ed 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 @@ -56,4 +56,3 @@ Module( 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 b91b72eda5..27a4e7f628 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 @@ -84,4 +84,3 @@ Module( 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 aa9dc7dbfa..029198b821 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 @@ -115,4 +115,3 @@ Module( 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_destructure_in_python_file.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_destructure_in_python_file.py.snap index 2e9206577d..0485145549 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_destructure_in_python_file.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_destructure_in_python_file.py.snap @@ -124,16 +124,13 @@ Module( | 1 | def foo(Point(x, y): Point): ... | ^ Syntax Error: Expected `)`, found `(` - | | 1 | def foo(Point(x, y): Point): ... | ^ Syntax Error: Expected a statement - | | 1 | def foo(Point(x, y): Point): ... | ^ Syntax Error: Expected a statement - | 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 1ffafa2c9b..938fe9df82 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 @@ -147,4 +147,3 @@ Module( 1 | def foo(x:): ... 2 | def foo(x:,): ... | ^ Syntax Error: Expected an expression - | 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 690040d80f..12130c5f0e 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 @@ -156,4 +156,3 @@ Module( 1 | def foo(x=): ... 2 | def foo(x: int = ): ... | ^ Syntax Error: Expected an expression - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_some_annotation_py.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_some_annotation_py.py.snap index f209a71330..723adbd440 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_some_annotation_py.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_some_annotation_py.py.snap @@ -137,4 +137,3 @@ Module( | 1 | def f(s: some str) -> str: ... | ^^^^ Syntax Error: `some` parameter annotations are not valid in .py files - | 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 b58ec93918..454ee42204 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 @@ -277,7 +277,6 @@ Module( 2 | def foo(arg: yield int): ... 3 | def foo(arg: x := int): ... | ^^ Syntax Error: Expected `,`, found `:=` - | ## Semantic Syntax Errors 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 d8c89a953e..d0c859ca58 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 @@ -266,4 +266,3 @@ Module( 2 | def foo(x=(*int)): ... 3 | def foo(x=yield y): ... | ^^^^^^^ Syntax Error: Yield expression cannot be used here - | 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 f213607583..acf0fb357b 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 @@ -91,4 +91,3 @@ Module( 1 | # parse_options: {"target-version": "3.10"} 2 | def foo(*args: *Ts): ... | ^^^ Syntax Error: Cannot use star annotation on Python 3.10 (syntax was added in Python 3.11) - | 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 3b5e0a0bd8..2ae4922f28 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 @@ -181,28 +181,23 @@ Module( | 1 | def foo(a, a=10, *a, a, a: str, **a): ... | ^ Syntax Error: Duplicate parameter "a" - | | 1 | def foo(a, a=10, *a, a, a: str, **a): ... | ^ Syntax Error: Duplicate parameter "a" - | | 1 | def foo(a, a=10, *a, a, a: str, **a): ... | ^ Syntax Error: Duplicate parameter "a" - | | 1 | def foo(a, a=10, *a, a, a: str, **a): ... | ^ Syntax Error: Duplicate parameter "a" - | | 1 | def foo(a, a=10, *a, a, a: str, **a): ... | ^ Syntax Error: Duplicate parameter "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 98efaa623a..5573ffca99 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 @@ -314,4 +314,3 @@ Module( 4 | def foo(a, *,): ... 5 | def foo(*, **kwargs): ... | ^^^^^^^^ Syntax Error: Expected one or more keyword parameter after `*` separator - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_follows_var_keyword_param.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_follows_var_keyword_param.py.snap index a43346e381..14299e27a9 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_follows_var_keyword_param.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_follows_var_keyword_param.py.snap @@ -136,28 +136,23 @@ Module( | 1 | def foo(**kwargs, a, /, b=10, *, *args): ... | ^ Syntax Error: Parameter cannot follow var-keyword parameter - | | 1 | def foo(**kwargs, a, /, b=10, *, *args): ... | ^ Syntax Error: Parameter cannot follow var-keyword parameter - | | 1 | def foo(**kwargs, a, /, b=10, *, *args): ... | ^ Syntax Error: Parameter cannot follow var-keyword parameter - | | 1 | def foo(**kwargs, a, /, b=10, *, *args): ... | ^ Syntax Error: Parameter cannot follow var-keyword parameter - | | 1 | def foo(**kwargs, a, /, b=10, *, *args): ... | ^ Syntax Error: Parameter cannot follow var-keyword parameter - | 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 e9925e2ba4..a61aba1e2d 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 @@ -74,4 +74,3 @@ Module( | 1 | def foo(*, **kwargs): ... | ^^^^^^^^ Syntax Error: Expected one or more keyword parameter after `*` separator - | 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 13c343774e..2805e75387 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 @@ -93,4 +93,3 @@ Module( | 1 | def foo(a, **kwargs1, **kwargs2): ... | ^^ Syntax Error: Parameter cannot follow var-keyword parameter - | 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 ff40188d3e..491dac8505 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 @@ -203,4 +203,3 @@ Module( 1 | def foo(a, /, /, b): ... 2 | def foo(a, /, b, c, /): ... | ^ Syntax Error: Only one '/' separator allowed - | 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 34fde49d73..91223c8759 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 @@ -203,4 +203,3 @@ Module( 1 | def foo(a, *, *, b): ... 2 | def foo(a, *, b, c, *): ... | ^ Syntax Error: Only one '*' separator allowed - | 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 01bdfabaae..64f70f7869 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 @@ -335,4 +335,3 @@ Module( 3 | def foo(a, *args1, *args2, b): ... 4 | def foo(a, *args1, b, c, *args2): ... | ^^^^^^ Syntax Error: Only one '*' parameter allowed - | 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 a79954a602..71a8d73ba5 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 @@ -128,4 +128,3 @@ Module( 1 | def foo(/): ... 2 | def foo(/, a): ... | ^ Syntax Error: Position-only parameter separator not allowed as first parameter - | 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 61886af95a..6db317a0fa 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 @@ -134,10 +134,8 @@ Module( | 1 | def foo(a=10, b, c: int): ... | ^ Syntax Error: Parameter without a default cannot follow a parameter with a default - | | 1 | def foo(a=10, b, c: int): ... | ^^^^^^ Syntax Error: Parameter without a default cannot follow a parameter with a default - | 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 c6e78077f2..621742c3a3 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 @@ -389,4 +389,3 @@ Module( 3 | def foo(a, *, /, b): ... 4 | def foo(a, *, b, c, /, d): ... | ^ Syntax Error: '/' parameter must appear before '*' parameter - | 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 565c3fbdd0..9f189875a2 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 @@ -231,4 +231,3 @@ Module( 1 | def foo(a, *args, *, b): ... 2 | def foo(a, *args, b, c, *): ... | ^ Syntax Error: Keyword-only parameter separator not allowed after '*' parameter - | 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 e5ccb5a77d..890e6e448e 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 @@ -174,22 +174,18 @@ Module( | 1 | def foo(a, **kwargs={'b': 1, 'c': 2}): ... | ^ Syntax Error: Parameter with `*` or `**` cannot have default value - | | 1 | def foo(a, **kwargs={'b': 1, 'c': 2}): ... | ^ Syntax Error: Expected `)`, found `{` - | | 1 | def foo(a, **kwargs={'b': 1, 'c': 2}): ... | ^ Syntax Error: Expected a statement - | | 1 | def foo(a, **kwargs={'b': 1, 'c': 2}): ... | ^ Syntax Error: Expected a statement - | 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 4471231f82..bd12a14c92 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 @@ -130,22 +130,18 @@ Module( | 1 | def foo(a, *args=(1, 2)): ... | ^ Syntax Error: Parameter with `*` or `**` cannot have default value - | | 1 | def foo(a, *args=(1, 2)): ... | ^ Syntax Error: Expected `)`, found `(` - | | 1 | def foo(a, *args=(1, 2)): ... | ^ Syntax Error: Expected a statement - | | 1 | def foo(a, *args=(1, 2)): ... | ^ Syntax Error: Expected a statement - | 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 b7e63dcf67..e231b81d8b 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 @@ -228,4 +228,3 @@ Module( 3 | with (foo, bar as y): ... 4 | with (foo as x, bar): ... | ^ Syntax Error: Cannot use parentheses within a `with` statement on Python 3.8 (syntax was added in Python 3.9) - | 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 2d86b36b75..6bbb5b42ca 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 @@ -189,4 +189,3 @@ Module( 3 | f((a) = 1) 4 | f( ( a ) = 1) | ^^^^^ Syntax Error: Cannot use parenthesized keyword argument name on Python 3.8 (syntax was removed in Python 3.8) - | 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 ce322c15bd..319c15431e 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 @@ -1088,7 +1088,6 @@ Module( 14 | f"""{f"""{x}"""}""" # mark the whole triple quote 15 | f"{'\n'.join(['\t', '\v', '\r'])}" # multiple escape sequences, multiple errors | ^ Syntax Error: Cannot use an escape sequence (backslash) in f-strings on Python 3.11 (syntax was added in Python 3.12) - | | @@ -1096,7 +1095,6 @@ Module( 14 | f"""{f"""{x}"""}""" # mark the whole triple quote 15 | f"{'\n'.join(['\t', '\v', '\r'])}" # multiple escape sequences, multiple errors | ^ Syntax Error: Cannot use an escape sequence (backslash) in f-strings on Python 3.11 (syntax was added in Python 3.12) - | | @@ -1104,7 +1102,6 @@ Module( 14 | f"""{f"""{x}"""}""" # mark the whole triple quote 15 | f"{'\n'.join(['\t', '\v', '\r'])}" # multiple escape sequences, multiple errors | ^ Syntax Error: Cannot use an escape sequence (backslash) in f-strings on Python 3.11 (syntax was added in Python 3.12) - | | @@ -1112,4 +1109,3 @@ Module( 14 | f"""{f"""{x}"""}""" # mark the whole triple quote 15 | f"{'\n'.join(['\t', '\v', '\r'])}" # multiple escape sequences, multiple errors | ^ Syntax Error: Cannot use an escape sequence (backslash) in f-strings on Python 3.11 (syntax was added in Python 3.12) - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep701_nested_interpolation_py311.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep701_nested_interpolation_py311.py.snap index 5699059e91..51ca8d4dda 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep701_nested_interpolation_py311.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep701_nested_interpolation_py311.py.snap @@ -228,4 +228,3 @@ Module( 3 | f'{1: abcd "{'aa'}" }' 4 | f'{1: abcd "{"\n"}" }' | ^ Syntax Error: Cannot use an escape sequence (backslash) in f-strings on Python 3.11 (syntax was added in Python 3.12) - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep_798_invalid_dict_unpacking_comprehensions_py315.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep_798_invalid_dict_unpacking_comprehensions_py315.py.snap index 6ef15bf5a3..e2e3bae605 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep_798_invalid_dict_unpacking_comprehensions_py315.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep_798_invalid_dict_unpacking_comprehensions_py315.py.snap @@ -367,4 +367,3 @@ Module( 4 | {**k: v for k, v in items} 5 | {k: **v for k, v in items} | ^^^ Syntax Error: Starred expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep_798_unpacking_comprehensions_py314.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep_798_unpacking_comprehensions_py314.py.snap index d0426710ed..c39248099f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep_798_unpacking_comprehensions_py314.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep_798_unpacking_comprehensions_py314.py.snap @@ -334,4 +334,3 @@ Module( 5 | (*x for x in y) 6 | f(*x for x in y) | ^^ Syntax Error: Cannot use iterable unpacking in a generator expression on Python 3.14 (syntax was added in Python 3.15) - | 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 49991b0f4c..b0de2231cb 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 @@ -332,7 +332,6 @@ Module( 4 | def foo(a, *args, /, b): ... 5 | def foo(a, //): ... | ^^ Syntax Error: Expected `,`, found `//` - | ## Unsupported Syntax Errors 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 4e9bad06a4..a26ddcf6ec 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 @@ -59,4 +59,3 @@ Module( 1 | raise from exc 2 | raise from None | ^^^^ Syntax Error: Exception missing in `raise` statement with cause - | 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 c9559c557f..5554450d32 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 @@ -145,4 +145,3 @@ Module( 2 | raise x from yield y 3 | raise x from y := 1 | ^^ Syntax Error: Expected a statement - | 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 6886f22bad..02dab5618e 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 @@ -118,4 +118,3 @@ Module( 2 | raise yield x 3 | raise x := 1 | ^^ Syntax Error: Expected a statement - | 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 9ba56e56b1..2613fd3001 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 @@ -115,4 +115,3 @@ Module( 1 | raise x from y, 2 | raise x from y, z | ^^^^ Syntax Error: Unparenthesized tuple expression cannot be used here - | 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 0490db769f..1034d6d5d0 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 @@ -155,4 +155,3 @@ Module( 2 | raise x, y 3 | raise x, y from z | ^^^^ Syntax Error: Unparenthesized tuple expression cannot be used here - | 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 435727987b..46a052c149 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 @@ -1012,4 +1012,3 @@ Module( 56 | def bar(): 57 | pass | ^ Syntax Error: Expected a statement - | 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 87f5e6806f..0ac569442e 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 @@ -119,12 +119,10 @@ Module( ## Errors | -1 | if call(foo, [a, b def bar(): pass - | ^^^ Syntax Error: Expected `]`, found `def` - | +1 | if call(foo, [a, b␍ def bar():␍ pass + | ^^^ Syntax Error: Expected `]`, found `def` | -1 | if call(foo, [a, b def bar(): pass +1 | if call(foo, [a, b␍ def bar():␍ pass | ^ Syntax Error: Expected `)`, found newline - | 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 2f87730112..655bb4ca11 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 @@ -518,52 +518,37 @@ Module( | - | - 9 | 'format spec'} -10 | - | ^ Syntax Error: Expected a statement -11 | f'middle {'string':\\\ -12 | 'format spec'} - | - - | 11 | f'middle {'string':\\\ 12 | 'format spec'} | ^ Syntax Error: f-string: expecting `}` - | | 11 | f'middle {'string':\\\ 12 | 'format spec'} | ^^^^^^ Syntax Error: Simple statements must be separated by newlines or semicolons - | | 11 | f'middle {'string':\\\ 12 | 'format spec'} | ^^^^ Syntax Error: Simple statements must be separated by newlines or semicolons - | | 11 | f'middle {'string':\\\ 12 | 'format spec'} | ^ Syntax Error: custom string tags are not valid in .py files - | | 11 | f'middle {'string':\\\ 12 | 'format spec'} | ^ Syntax Error: t-string: single `}` is not allowed - | | 11 | f'middle {'string':\\\ 12 | 'format spec'} | ^ Syntax Error: t-string: unterminated string - | 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 f58010350b..8fcddba72a 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 @@ -77,14 +77,12 @@ Module( | ___________________________^ 6 | | y = 1 | |_____^ Syntax Error: f-string: unterminated triple-quoted string - | | 5 | f"""hello {x # comment 6 | y = 1 | ^ Syntax Error: f-string: expecting `}` - | | @@ -94,11 +92,9 @@ Module( | ___________________________^ 6 | | y = 1 | |_____^ Syntax Error: Expected FStringEnd, found FStringMiddle - | | 5 | f"""hello {x # comment 6 | y = 1 | ^ Syntax Error: Expected a statement - | 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 7a5f85ab4a..290b20e86a 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 @@ -81,4 +81,3 @@ Module( 5 | f'''{foo:.3f 6 | ''' | ^^^ Syntax Error: f-string: expecting `}` - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__ty_1828.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__ty_1828.py.snap index 7089f36f0c..1a73263c4d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__ty_1828.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__ty_1828.py.snap @@ -234,7 +234,6 @@ Module( 4 | | class A: 5 | | pass | |_________^ Syntax Error: f-string: unterminated triple-quoted string - | | @@ -263,11 +262,9 @@ Module( 4 | | class A: 5 | | pass | |_________^ Syntax Error: Expected a statement - | | 4 | class A: 5 | pass | ^ Syntax Error: unexpected EOF while parsing - | 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 b096edbd0f..4d0d9f6191 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 @@ -1061,7 +1061,6 @@ Module( 8 | [(a := 0) for a in range (0) for b in range(0)] 9 | [((a := 0), (b := 1)) for a in range (0) for b in range(0)] | ^ Syntax Error: assignment expression cannot rebind comprehension variable - | | @@ -1069,4 +1068,3 @@ Module( 8 | [(a := 0) for a in range (0) for b in range(0)] 9 | [((a := 0), (b := 1)) for a in range (0) for b in range(0)] | ^ Syntax Error: assignment expression cannot rebind comprehension variable - | 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 c6e921af83..d00b8aadbf 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 @@ -198,7 +198,6 @@ Module( 4 | return x := 1 5 | return *x and y | ^^^^^^^ Syntax Error: Boolean expression cannot be used here - | ## Semantic Syntax Errors @@ -216,4 +215,3 @@ Module( 4 | return x := 1 5 | return *x and y | ^^^^^^^^ 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 9cfc54ae81..4c8076d42e 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 @@ -71,4 +71,3 @@ Module( | 1 | a; if b: pass; b | ^^ Syntax Error: Compound statements are not allowed on the same line as simple statements - | 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 b8880754e1..f6015876da 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 @@ -119,4 +119,3 @@ Module( 1 | if True: pass if False: pass 2 | if True: pass; if False: pass | ^^ Syntax Error: Compound statements are not allowed on the same line as simple statements - | 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 aee35aa868..a3812b0385 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 @@ -155,7 +155,6 @@ Module( 2 | a + b c + d 3 | break; continue pass; continue break | ^^^^ Syntax Error: Simple statements must be separated by newlines or semicolons - | | @@ -163,4 +162,3 @@ Module( 2 | a + b c + d 3 | break; continue pass; continue break | ^^^^^ Syntax Error: Simple statements must be separated by newlines or semicolons - | 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 3b24125241..1e85297dfd 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 @@ -68,10 +68,8 @@ Module( | 1 | if True: break; continue pass; continue break | ^^^^ Syntax Error: Simple statements must be separated by newlines or semicolons - | | 1 | if True: break; continue pass; continue break | ^^^^^ Syntax Error: Simple statements must be separated by newlines or semicolons - | 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 f64334531b..fd2e4c71f5 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 @@ -119,4 +119,3 @@ Module( 1 | for _ in *x: ... 2 | for *x in xs: ... | ^^ 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 9b82c695b6..9096a79c42 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 @@ -71,4 +71,3 @@ Module( | 1 | def f(): return *x | ^^ 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 a272276c99..66802849cc 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 @@ -77,4 +77,3 @@ Module( | 1 | def f(): yield *x | ^^ 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 7a1e915980..8883f64f6d 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 @@ -65,4 +65,3 @@ Module( | 1 | *a = (1,) | ^^ Syntax Error: starred assignment target must be in a list or tuple - | 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 02783de3b8..a0db61de60 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 @@ -498,4 +498,3 @@ Module( 6 | lst[*a, *b] # multiple unpacks 7 | array[3:5, *idxs] # mixed with slices | ^^^^^ Syntax Error: Cannot use star expression in index on Python 3.10 (syntax was added in Python 3.11) - | 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 547fb9f3ac..0a9596eb04 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 @@ -82,10 +82,8 @@ Module( | 1 | array[*start:*end] | ^^^^^^ Syntax Error: Starred expression cannot be used here - | | 1 | array[*start:*end] | ^^^^ Syntax Error: Starred expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@starred_comprehension_target.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@starred_comprehension_target.py.snap index 02d17ed47a..bf5f2b33eb 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@starred_comprehension_target.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@starred_comprehension_target.py.snap @@ -70,4 +70,3 @@ Module( | 1 | [item for *items in source] | ^^^^^^ Syntax Error: Starred expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@starred_list_comp_py314.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@starred_list_comp_py314.py.snap index 75daac8756..e567871623 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@starred_list_comp_py314.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@starred_list_comp_py314.py.snap @@ -71,4 +71,3 @@ Module( 1 | # parse_options: {"target-version": "3.14"} 2 | [*x for x in y] | ^^ Syntax Error: Cannot use iterable unpacking in a list comprehension on Python 3.14 (syntax was added in Python 3.15) - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@starred_starred_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@starred_starred_expression.py.snap index 87d9d15677..b16fd83a28 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@starred_starred_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@starred_starred_expression.py.snap @@ -133,4 +133,3 @@ Module( 2 | *[]) 3 | print(* *[]) | ^^^ Syntax Error: Starred expression cannot be used here - | 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 7fe4145e9c..c13dfa6f4e 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 @@ -514,7 +514,6 @@ Module( 18 | 19 | def multiple_commas_and_recovery[A,,100](): ... | ^ Syntax Error: Expected a type parameter or the end of the type parameter list - | | @@ -522,7 +521,6 @@ Module( 18 | 19 | def multiple_commas_and_recovery[A,,100](): ... | ^^^ Syntax Error: Expected `]`, found int - | | @@ -530,7 +528,6 @@ Module( 18 | 19 | def multiple_commas_and_recovery[A,,100](): ... | ^ Syntax Error: Expected a statement - | | @@ -538,4 +535,3 @@ Module( 18 | 19 | def multiple_commas_and_recovery[A,,100](): ... | ^^ Syntax Error: Only single target (not tuple) can be annotated - | 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 1f87152905..d7df054b4a 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 @@ -75,11 +75,3 @@ Module( 2 | if True)): 3 | pass | ^^^^ Syntax Error: Unexpected indentation - | - - - | -2 | if True)): -3 | pass - | ^ Syntax Error: Expected a statement - | 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 838f513a36..009af5e59b 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 @@ -8,17 +8,17 @@ input_file: crates/ruff_python_parser/resources/invalid/statements/if_extra_inde Module( ModModule { node_index: NodeIndex(None), - range: 0..153, + range: 0..905, body: [ If( StmtIf { node_index: NodeIndex(None), - range: 103..134, + range: 69..110, pattern: None, test: BooleanLiteral( ExprBooleanLiteral { node_index: NodeIndex(None), - range: 106..110, + range: 72..76, value: true, }, ), @@ -26,21 +26,21 @@ Module( Pass( StmtPass { node_index: NodeIndex(None), - range: 116..120, + range: 82..86, }, ), Expr( StmtExpr { node_index: NodeIndex(None), - range: 129..134, + range: 95..100, value: BinOp( ExprBinOp { node_index: NodeIndex(None), - range: 129..134, + range: 95..100, left: Name( ExprName { node_index: NodeIndex(None), - range: 129..130, + range: 95..96, id: Name("a"), ctx: Load, }, @@ -49,7 +49,7 @@ Module( right: Name( ExprName { node_index: NodeIndex(None), - range: 133..134, + range: 99..100, id: Name("b"), ctx: Load, }, @@ -58,25 +58,25 @@ Module( ), }, ), + Pass( + StmtPass { + node_index: NodeIndex(None), + range: 106..110, + }, + ), ], elif_else_clauses: [], }, ), - Pass( - StmtPass { - node_index: NodeIndex(None), - range: 140..144, - }, - ), Assign( StmtAssign { node_index: NodeIndex(None), - range: 146..152, + range: 112..118, targets: [ Name( ExprName { node_index: NodeIndex(None), - range: 146..147, + range: 112..113, id: Name("a"), ctx: Store, }, @@ -85,7 +85,7 @@ Module( value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), - range: 150..152, + range: 116..118, value: Int( 10, ), @@ -93,6 +93,432 @@ Module( ), }, ), + If( + StmtIf { + node_index: NodeIndex(None), + range: 158..248, + pattern: None, + test: BooleanLiteral( + ExprBooleanLiteral { + node_index: NodeIndex(None), + range: 161..165, + value: true, + }, + ), + body: [ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 171..184, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 171..184, + id: Name("before_nested"), + ctx: Load, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 193..205, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 193..205, + id: Name("first_nested"), + ctx: Load, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 218..231, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 218..231, + id: Name("second_nested"), + ctx: Load, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 236..248, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 236..248, + id: Name("after_nested"), + ctx: Load, + }, + ), + }, + ), + ], + elif_else_clauses: [], + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 250..264, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 250..264, + id: Name("outside_nested"), + ctx: Load, + }, + ), + }, + ), + If( + StmtIf { + node_index: NodeIndex(None), + range: 325..449, + pattern: None, + test: BooleanLiteral( + ExprBooleanLiteral { + node_index: NodeIndex(None), + range: 328..332, + value: true, + }, + ), + body: [ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 338..353, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 338..353, + id: Name("before_compound"), + ctx: Load, + }, + ), + }, + ), + If( + StmtIf { + node_index: NodeIndex(None), + range: 362..403, + pattern: None, + test: Name( + ExprName { + node_index: NodeIndex(None), + range: 365..374, + id: Name("condition"), + ctx: Load, + }, + ), + body: [ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 388..403, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 388..403, + id: Name("nested_compound"), + ctx: Load, + }, + ), + }, + ), + ], + elif_else_clauses: [], + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 412..430, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 412..430, + id: Name("recovered_compound"), + ctx: Load, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 435..449, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 435..449, + id: Name("after_compound"), + ctx: Load, + }, + ), + }, + ), + ], + elif_else_clauses: [], + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 451..467, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 451..467, + id: Name("outside_compound"), + ctx: Load, + }, + ), + }, + ), + If( + StmtIf { + node_index: NodeIndex(None), + range: 536..641, + pattern: None, + test: BooleanLiteral( + ExprBooleanLiteral { + node_index: NodeIndex(None), + range: 539..543, + value: true, + }, + ), + body: [ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 549..563, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 549..563, + id: Name("before_regions"), + ctx: Load, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 572..584, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 572..584, + id: Name("first_region"), + ctx: Load, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 589..602, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 589..602, + id: Name("middle_region"), + ctx: Load, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 611..624, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 611..624, + id: Name("second_region"), + ctx: Load, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 629..641, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 629..641, + id: Name("after_region"), + ctx: Load, + }, + ), + }, + ), + ], + elif_else_clauses: [], + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 643..658, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 643..658, + id: Name("outside_regions"), + ctx: Load, + }, + ), + }, + ), + If( + StmtIf { + node_index: NodeIndex(None), + range: 734..793, + pattern: None, + test: BooleanLiteral( + ExprBooleanLiteral { + node_index: NodeIndex(None), + range: 737..741, + value: true, + }, + ), + body: [ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 747..759, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 747..759, + id: Name("before_error"), + ctx: Load, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 768..777, + value: Call( + ExprCall { + node_index: NodeIndex(None), + range: 768..777, + func: Name( + ExprName { + node_index: NodeIndex(None), + range: 768..774, + id: Name("broken"), + ctx: Load, + }, + ), + arguments: Arguments { + range: 774..777, + node_index: NodeIndex(None), + args: [], + keywords: [], + }, + is_cast: false, + is_checked_cast: false, + is_string_tag: false, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 782..793, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 782..793, + id: Name("after_error"), + ctx: Load, + }, + ), + }, + ), + ], + elif_else_clauses: [], + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 795..808, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 795..808, + id: Name("outside_error"), + ctx: Load, + }, + ), + }, + ), + If( + StmtIf { + node_index: NodeIndex(None), + range: 863..904, + pattern: None, + test: BooleanLiteral( + ExprBooleanLiteral { + node_index: NodeIndex(None), + range: 866..870, + value: true, + }, + ), + body: [ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 876..886, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 876..886, + id: Name("before_eof"), + ctx: Load, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 895..904, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 895..904, + id: Name("final_eof"), + ctx: Load, + }, + ), + }, + ), + ], + elif_else_clauses: [], + }, + ), ], }, ) @@ -109,9 +535,74 @@ Module( | - | -6 | pass -7 | - | ^ Syntax Error: Expected a statement -8 | a = 10 - | + | +11 | if True: +12 | before_nested +13 | first_nested + | ^^^^^^^^ Syntax Error: Unexpected indentation +14 | second_nested +15 | after_nested + | + + + | +12 | before_nested +13 | first_nested +14 | second_nested + | ^^^^^^^^^^^^ Syntax Error: Unexpected indentation +15 | after_nested + | + + + | +20 | if True: +21 | before_compound +22 | if condition: + | ^^^^^^^^ Syntax Error: Unexpected indentation +23 | nested_compound +24 | recovered_compound + | + + + | +30 | if True: +31 | before_regions +32 | first_region + | ^^^^^^^^ Syntax Error: Unexpected indentation +33 | middle_region +34 | second_region + | + + + | +32 | first_region +33 | middle_region +34 | second_region + | ^^^^^^^^ Syntax Error: Unexpected indentation +35 | after_region + | + + + | +40 | if True: +41 | before_error +42 | broken(,) + | ^^^^^^^^ Syntax Error: Unexpected indentation +43 | after_error + | + + + | +40 | if True: +41 | before_error +42 | broken(,) + | ^ Syntax Error: Expected an expression or a ')' +43 | after_error + | + + + | +48 | if True: +49 | before_eof +50 | final_eof + | ^^^^^^^^ Syntax Error: Unexpected indentation 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 48df9358f8..7607f010f7 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 @@ -1909,7 +1909,6 @@ Module( 41 | [[a, b], [[42]], d] = [[1, 2], [[3]], 4] 42 | (x, foo(), y) = (42, 42, 42) | ^^^^^ Syntax Error: Invalid assignment target - | ## Semantic Syntax Errors 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 6ed7c1b669..997bb375cf 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 @@ -1764,4 +1764,3 @@ Module( 33 | [[a, b], [[42]], d] += [[1, 2], [[3]], 4] 34 | (x, foo(), y) += (42, 42, 42) | ^^^^^^^^^^^^^ Syntax Error: Invalid augmented assignment target - | 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 4bd787208c..f02af5a34e 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 @@ -130,11 +130,9 @@ Module( 4 | case x as y + 1j: 5 | pass | ^^^^^^^^ Syntax Error: Expected dedent, found indent - | | 4 | case x as y + 1j: 5 | pass | ^ Syntax Error: Expected a statement - | 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 ad4dbb0ec8..eb00a56bdd 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 @@ -159,11 +159,9 @@ Module( 4 | case {(x as y): 1}: 5 | pass | ^^^^^^^^ Syntax Error: Unexpected indentation - | | 4 | case {(x as y): 1}: 5 | pass | ^ Syntax Error: Expected a statement - | 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 d38a3cf88c..dc679b844c 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 @@ -649,4 +649,3 @@ Module( 22 | match subject: 23 | case {Foo(a as b): 1}: ... | ^^^^^^^^^^^ Syntax Error: Invalid mapping pattern key - | 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 99a0b4c25a..be31739a8b 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 @@ -1949,7 +1949,6 @@ Module( 31 | with (item as f1) as f2: ... 32 | with (item1 as f, item2 := 0): ... | ^^^^^^^^^^ Syntax Error: Unparenthesized named expression cannot be used here - | ## Unsupported Syntax Errors 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 a08bf38030..edc82215b6 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 @@ -83,4 +83,3 @@ Module( 2 | 3 | x + y | ^ Syntax Error: unexpected EOF while parsing - | 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 9b8b7fe54b..a2ec5a6286 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 @@ -43,4 +43,3 @@ Module( | 1 | with ( | ^ Syntax Error: unexpected EOF while parsing - | 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 6d159552ed..de99cb6ca4 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 @@ -412,4 +412,3 @@ Module( 8 | with item1 as f, *item2: pass 9 | with item := 0 as f: pass | ^^ Syntax Error: Expected `,`, found `:=` - | 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 index de21eac870..7133e8b63f 100644 --- 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 @@ -119,4 +119,3 @@ Module( 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 index b1b3890a11..55f34a110b 100644 --- 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 @@ -65,4 +65,3 @@ Module( 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 index ef9242f4f8..0c729b6c50 100644 --- 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 @@ -119,4 +119,3 @@ Module( 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 index d906c8c837..2939393293 100644 --- 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 @@ -221,4 +221,3 @@ Module( 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 index f38714687f..2754960030 100644 --- 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 @@ -109,25 +109,21 @@ Module( 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 element of or the end of the 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 index f6537ec7ee..e755d104ba 100644 --- 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 @@ -289,4 +289,3 @@ Module( 5 | t"{" 6 | t"""{""" | ^^^ Syntax Error: Expected an expression - | 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 index 9789ed8922..5efaaaebe1 100644 --- 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 @@ -153,4 +153,3 @@ Module( 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 index 1eed668827..aaa2aed20f 100644 --- 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 @@ -166,4 +166,3 @@ Module( 4 | / t"""what's 5 | | happening?""" | |_____________^ 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 0bcf4355aa..193c1c8269 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 @@ -79,11 +79,3 @@ Module( 5 | else: 6 | pass | ^^^^ Syntax Error: Unexpected indentation - | - - - | -5 | else: -6 | pass - | ^ Syntax Error: Expected a statement - | 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 cdba080314..6368dc0cd1 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 @@ -73,4 +73,3 @@ Module( 5 | else: 6 | pass | ^ Syntax Error: Expected `except` or `finally` after `try` block - | 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 1803efb100..e65db427d0 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 @@ -219,8 +219,8 @@ Module( | 3 | exept: # spellchecker:disable-line 4 | pass - | ^ Syntax Error: Expected a statement 5 | finally: + | ^^^^^^^ Syntax Error: Expected a statement 6 | pass 7 | a = 1 | @@ -257,16 +257,6 @@ Module( | - | -5 | finally: -6 | pass - | ^ Syntax Error: Expected a statement -7 | a = 1 -8 | try: -9 | pass - | - - | 10 | except: 11 | pass @@ -284,11 +274,3 @@ Module( | ^^^^ Syntax Error: Unexpected indentation 14 | b = 1 | - - - | -12 | exept: # spellchecker:disable-line -13 | pass - | ^ Syntax Error: Expected a statement -14 | b = 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 98f032e8c9..62db5271c7 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 @@ -276,4 +276,3 @@ Module( 21 | / except* ExceptionGroup: 22 | | pass | |________^ Syntax Error: Cannot have both 'except' and 'except*' on the same 'try' - | 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 426758bd6c..ebf0585fc8 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 @@ -194,4 +194,3 @@ Module( 10 | ): ... 11 | with (foo,): ... | ^ Syntax Error: Cannot use parentheses within a `with` statement on Python 3.8 (syntax was added in Python 3.9) - | 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 ae883b9ec6..9a50063290 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 @@ -96,4 +96,3 @@ Module( 2 | type x 3 | type x = | ^ Syntax Error: Expected an expression - | 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 2716dcd07d..1bf3cfd28d 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 @@ -183,7 +183,6 @@ Module( 3 | type x = yield from y 4 | type x = x := 1 | ^^ Syntax Error: Expected a statement - | ## Semantic Syntax Errors diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_bound_range_py.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_bound_range_py.py.snap index 790c854368..adcd6321e5 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_bound_range_py.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_bound_range_py.py.snap @@ -84,4 +84,3 @@ Module( | 1 | type X[T: int..object] = int | ^^ Syntax Error: type parameter bound ranges are not valid in `.py` files - | 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 5240fcb2f6..089c58c35a 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 @@ -390,7 +390,6 @@ Module( 4 | class C[T = int](): ... 5 | class D[S, T = int, U = uint](): ... | ^^^^^ Syntax Error: Cannot set default type for a type parameter on Python 3.12 (syntax was added in Python 3.13) - | | @@ -398,4 +397,3 @@ Module( 4 | class C[T = int](): ... 5 | class D[S, T = int, U = uint](): ... | ^^^^^^ Syntax Error: Cannot set default type for a type parameter on Python 3.12 (syntax was added in Python 3.13) - | 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 93d316b3d9..a9f5af1319 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 @@ -346,7 +346,6 @@ Module( 3 | type X[T: yield from x] = int 4 | type X[T: x := int] = int | ^^ Syntax Error: Expected `,`, found `:=` - | ## Semantic Syntax Errors 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 4e5f4a555a..c896623c87 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 @@ -157,4 +157,3 @@ Module( 1 | type X[T: ] = int 2 | type X[T1: , T2] = int | ^ Syntax Error: Expected an expression - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_missing_type_mapping.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_missing_type_mapping.py.snap index 1635600f14..87041f0376 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_missing_type_mapping.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_missing_type_mapping.py.snap @@ -164,11 +164,9 @@ Module( 1 | type X[T in ] = int 2 | type X[T1 in , T2] = int | ^^ Syntax Error: type mappings are a basedpython feature and are not valid in `.py` files - | | 1 | type X[T in ] = int 2 | type X[T1 in , T2] = int | ^ Syntax Error: Expected an expression - | 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 09c9d63939..1be8cba1a9 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 @@ -196,4 +196,3 @@ Module( 1 | type X[**T: int] = int 2 | type X[**T: **{"a": int}] = int | ^^ Syntax Error: a bound on a keyword-variadic pack is a basedpython feature and is not valid in .py files - | 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 4188f3ab1e..25eb6c2385 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 @@ -408,7 +408,6 @@ Module( 4 | type X[**P = x := int] = int 5 | type X[**P = *int] = int | ^^^^ Syntax Error: Starred expression cannot be used here - | ## Semantic Syntax Errors diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_missing_bound.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_missing_bound.py.snap index efca6991c5..2d2cb89b1d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_missing_bound.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_missing_bound.py.snap @@ -149,4 +149,3 @@ Module( 1 | type X[**T:] = int 2 | type X[**T:, T2] = int | ^ Syntax Error: a bound on a keyword-variadic pack is a basedpython feature and is not valid in .py files - | 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 a8d00aaaa3..e149241c4e 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 @@ -149,4 +149,3 @@ Module( 1 | type X[**P =] = int 2 | type X[**P =, T2] = int | ^ Syntax Error: Expected an expression - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_reified_py.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_reified_py.py.snap index 218f50ab26..7d673c2933 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_reified_py.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_reified_py.py.snap @@ -224,4 +224,3 @@ Module( 2 | class C[reified T]: ... 3 | type X[reified T] = int | ^^^^^^^ Syntax Error: reified type parameters are not valid in `.py` files - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_mapping_py.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_mapping_py.py.snap index db99dd094d..6cbc097d42 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_mapping_py.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_mapping_py.py.snap @@ -317,4 +317,3 @@ Module( 2 | def f[T in (int, str)](): ... 3 | class C[T in (int, str)]: ... | ^^ Syntax Error: type mappings are a basedpython feature and are not valid in `.py` files - | 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 ec759dccbd..2bd589ee76 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 @@ -510,7 +510,6 @@ Module( 5 | type X[T = x := int] = int 6 | type X[T: int = *int] = int | ^^^^ Syntax Error: Starred expression cannot be used here - | ## Semantic Syntax Errors 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 87e01f9d68..f6b06a504f 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 @@ -232,4 +232,3 @@ Module( 2 | type X[T: int =] = int 3 | type X[T1 =, T2] = int | ^ Syntax Error: Expected an expression - | 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 fd80f9f237..7a3df752a5 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 @@ -178,4 +178,3 @@ Module( 1 | type X[*T: int] = int 2 | type X[*T: *(int, str)] = int | ^ Syntax Error: a bound on a `TypeVarTuple` is a basedpython feature and is not valid in .py files - | 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 7a05fb256d..9d3215c734 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 @@ -417,7 +417,6 @@ Module( 4 | type X[*Ts = yield from x] = int 5 | type X[*Ts = x := int] = int | ^^ Syntax Error: Expected `,`, found `:=` - | ## Semantic Syntax Errors @@ -446,4 +445,3 @@ Module( 4 | type X[*Ts = yield from x] = int 5 | type X[*Ts = x := int] = int | ^^^ Syntax Error: non default type parameter `int` follows default type parameter - | 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 45b0fc23b5..53f2242dc2 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 @@ -149,4 +149,3 @@ Module( 1 | type X[*Ts =] = int 2 | type X[*Ts =, T2] = int | ^ Syntax Error: Expected an expression - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_parameter_default_order.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_parameter_default_order.py.snap index a165ecc32f..ba6e44f8a7 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_parameter_default_order.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_parameter_default_order.py.snap @@ -444,4 +444,3 @@ Module( 3 | def f[T = int, U](): ... 4 | type Alias[T = int, U] = ... | ^ Syntax Error: non default type parameter `U` follows default type parameter - | 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 390013a91f..ee97618778 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 @@ -128,4 +128,3 @@ Module( 2 | pass 3 | type ListOrSet[] = list | set | ^ Syntax Error: Type parameter list cannot be empty - | 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 e3bdeabbdb..d44fde3403 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 @@ -45,4 +45,3 @@ Module( 1 | # parse_options: {"target-version": "3.11"} 2 | type x = int | ^^^^ Syntax Error: Cannot use `type` alias statement on Python 3.11 (syntax was added in Python 3.12) - | 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 ef1b54613c..01a3cc83d2 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 @@ -65,4 +65,3 @@ Module( 1 | # parse_options: {"target-version": "3.8"} 2 | lst[x:=1] | ^^^^ Syntax Error: Cannot use unparenthesized assignment expression in a sequence index on Python 3.8 (syntax was added in Python 3.9) - | 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 4eff00bd83..104142177c 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 @@ -103,4 +103,3 @@ Module( 1 | # parse_options: {"target-version": "3.8"} 2 | {last := x for x in range(3)} | ^^^^^^^^^ Syntax Error: Cannot use unparenthesized assignment expression as an element in a set comprehension on Python 3.8 (syntax was added in Python 3.9) - | 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 bf277fe095..3d999f8721 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 @@ -204,4 +204,3 @@ Module( 3 | {1, x := 2, 3} 4 | {1, 2, x := 3} | ^^^^^^ Syntax Error: Cannot use unparenthesized assignment expression as an element in a set literal on Python 3.8 (syntax was added in Python 3.9) - | 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 dc64a3105d..e712c56940 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 @@ -49,4 +49,3 @@ Module( 1 | # parse_options: { "target-version": "3.7" } 2 | (x := 1) | ^^^^^^ Syntax Error: Cannot use named assignment expression (`:=`) on Python 3.7 (syntax was added in Python 3.8) - | 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 acdd7532ad..e6637b1f49 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 @@ -211,4 +211,3 @@ Module( 3 | while a, b: ... 4 | while a := 1, b: ... | ^ Syntax Error: Expected `:`, found `,` - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@with_item_destructure_in_python_file.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@with_item_destructure_in_python_file.py.snap index 00a6e4a3f9..694fa29c00 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@with_item_destructure_in_python_file.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@with_item_destructure_in_python_file.py.snap @@ -112,4 +112,3 @@ Module( | 1 | with ctx() as Point(x, y): ... | ^^^^^^^^^^^ Syntax Error: Invalid assignment target - | 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 042c0f71be..e76410e3d3 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 @@ -393,11 +393,9 @@ Module( 4 | with (item1, item2 as f1 item3, item4): ... 5 | with (item1, item2: ... | ^ Syntax Error: anonymous named tuple is not valid in .py files - | | 4 | with (item1, item2 as f1 item3, item4): ... 5 | with (item1, item2: ... | ^ Syntax Error: unexpected EOF while parsing - | 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 b04c602aee..60a731de66 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 @@ -232,4 +232,3 @@ Module( 3 | __debug__ = 1 4 | x, y, __debug__, z = 1, 2, 3, 4 | ^^^^^^^^^ Syntax Error: cannot assign to `__debug__` - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@yield_after_comma.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@yield_after_comma.py.snap index edd49583b5..fb83dc87fd 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@yield_after_comma.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@yield_after_comma.py.snap @@ -94,4 +94,3 @@ Module( | 1 | def f(): 1, yield 1 | ^^^^^^^ Syntax Error: Yield expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@yield_from_in_async_function.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@yield_from_in_async_function.py.snap index e22c78fec9..287b5562f8 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@yield_from_in_async_function.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@yield_from_in_async_function.py.snap @@ -68,4 +68,3 @@ Module( | 1 | async def f(): yield from x | ^^^^^^^^^^^^ Syntax Error: `yield from` statement in async function; use `async for` instead - | diff --git a/crates/ruff_python_semantic/Cargo.toml b/crates/ruff_python_semantic/Cargo.toml index ed753c2ed0..c73d7ea449 100644 --- a/crates/ruff_python_semantic/Cargo.toml +++ b/crates/ruff_python_semantic/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_semantic" -version = "0.0.5" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_semantic/README.md b/crates/ruff_python_semantic/README.md index bd19e639c7..b0275afaa4 100644 --- a/crates/ruff_python_semantic/README.md +++ b/crates/ruff_python_semantic/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_python_semantic). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_python_semantic). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_semantic/src/analyze/class.rs b/crates/ruff_python_semantic/src/analyze/class.rs index fd28d375dd..439124bcf5 100644 --- a/crates/ruff_python_semantic/src/analyze/class.rs +++ b/crates/ruff_python_semantic/src/analyze/class.rs @@ -157,16 +157,6 @@ pub enum ClassMemberBoundness { Bound, } -impl ClassMemberBoundness { - pub const fn is_bound(self) -> bool { - matches!(self, Self::Bound) - } - - pub const fn is_possibly_unbound(self) -> bool { - matches!(self, Self::PossiblyUnbound) - } -} - #[derive(Copy, Clone, Debug)] pub enum ClassMemberKind<'a> { Assign(&'a ast::StmtAssign), diff --git a/crates/ruff_python_semantic/src/analyze/terminal.rs b/crates/ruff_python_semantic/src/analyze/terminal.rs index 894bbe6f34..c0f6a75663 100644 --- a/crates/ruff_python_semantic/src/analyze/terminal.rs +++ b/crates/ruff_python_semantic/src/analyze/terminal.rs @@ -28,7 +28,7 @@ impl Terminal { } /// Returns `true` if the [`Terminal`] behavior includes at least one `return` path. - pub fn has_any_return(self) -> bool { + fn has_any_return(self) -> bool { matches!( self, Self::Return | Self::RaiseOrReturn | Self::ConditionalReturn diff --git a/crates/ruff_python_semantic/src/analyze/type_inference.rs b/crates/ruff_python_semantic/src/analyze/type_inference.rs index e866c41072..e4b28d5bfc 100644 --- a/crates/ruff_python_semantic/src/analyze/type_inference.rs +++ b/crates/ruff_python_semantic/src/analyze/type_inference.rs @@ -454,7 +454,7 @@ pub enum NumberLike { impl NumberLike { /// Coerces two number-like types to the "highest" number-like type. #[must_use] - pub fn coerce(self, other: NumberLike) -> NumberLike { + fn coerce(self, other: NumberLike) -> NumberLike { match (self, other) { (NumberLike::Complex, _) | (_, NumberLike::Complex) => NumberLike::Complex, (NumberLike::Float, _) | (_, NumberLike::Float) => NumberLike::Float, diff --git a/crates/ruff_python_semantic/src/analyze/typing.rs b/crates/ruff_python_semantic/src/analyze/typing.rs index 4c8c8bbd6b..1d1d682f2a 100644 --- a/crates/ruff_python_semantic/src/analyze/typing.rs +++ b/crates/ruff_python_semantic/src/analyze/typing.rs @@ -147,46 +147,64 @@ pub fn to_pep585_generic(expr: &Expr, semantic: &SemanticModel) -> Option bool { +pub fn is_pep585_generic(expr: &Expr, semantic: &SemanticModel) -> bool { semantic .resolve_qualified_name(expr) - .is_some_and(|qualified_name| match qualified_name.segments() { - ["", "dict" | "frozenset" | "list" | "set" | "tuple" | "type"] - | ["collections", "deque" | "defaultdict"] => true, - ["asyncio", "Future" | "Task"] - | ["collections", "ChainMap" | "Counter" | "OrderedDict"] - | [ - "contextlib", - "AbstractAsyncContextManager" | "AbstractContextManager", - ] - | ["dataclasses", "Field"] - | ["functools", "cached_property" | "partialmethod"] - | ["os", "PathLike"] - | [ - "queue", - "LifoQueue" | "PriorityQueue" | "Queue" | "SimpleQueue", - ] - | ["re", "Match" | "Pattern"] - | ["shelve", "BsdDbShelf" | "DbfilenameShelf" | "Shelf"] - | ["types", "MappingProxyType"] - | [ - "weakref", - "WeakKeyDictionary" | "WeakMethod" | "WeakSet" | "WeakValueDictionary", - ] - | [ - "collections", - "abc", - "AsyncGenerator" | "AsyncIterable" | "AsyncIterator" | "Awaitable" | "ByteString" - | "Callable" | "Collection" | "Container" | "Coroutine" | "Generator" | "ItemsView" - | "Iterable" | "Iterator" | "KeysView" | "Mapping" | "MappingView" - | "MutableMapping" | "MutableSequence" | "MutableSet" | "Reversible" | "Sequence" - | "Set" | "ValuesView", - ] => include_preview_generics, - _ => false, + .is_some_and(|qualified_name| { + matches!( + qualified_name.segments(), + ["", "dict" | "frozenset" | "list" | "set" | "tuple" | "type"] + | [ + "collections", + "deque" | "defaultdict" | "ChainMap" | "Counter" | "OrderedDict" + ] + | ["asyncio", "Future" | "Task"] + | [ + "contextlib", + "AbstractAsyncContextManager" | "AbstractContextManager" + ] + | ["dataclasses", "Field"] + | ["functools", "cached_property" | "partialmethod"] + | ["os", "PathLike"] + | [ + "queue", + "LifoQueue" | "PriorityQueue" | "Queue" | "SimpleQueue" + ] + | ["re", "Match" | "Pattern"] + | ["shelve", "BsdDbShelf" | "DbfilenameShelf" | "Shelf"] + | ["types", "MappingProxyType"] + | [ + "weakref", + "WeakKeyDictionary" | "WeakMethod" | "WeakSet" | "WeakValueDictionary" + ] + | [ + "collections", + "abc", + "AsyncGenerator" + | "AsyncIterable" + | "AsyncIterator" + | "Awaitable" + | "ByteString" + | "Callable" + | "Collection" + | "Container" + | "Coroutine" + | "Generator" + | "ItemsView" + | "Iterable" + | "Iterator" + | "KeysView" + | "Mapping" + | "MappingView" + | "MutableMapping" + | "MutableSequence" + | "MutableSet" + | "Reversible" + | "Sequence" + | "Set" + | "ValuesView" + ] + ) }) } @@ -427,9 +445,8 @@ pub fn is_type_checking_block(stmt: &ast::StmtIf, semantic: &SemanticModel) -> b // for this specific check even if it's defined somewhere else, like the current module. // Ex) `if TYPE_CHECKING:` Expr::Name(ast::ExprName { id, .. }) => { - id == "TYPE_CHECKING" - // Ex) `if TC:` with `from typing import TYPE_CHECKING as TC` - || semantic.match_typing_expr(test, "TYPE_CHECKING") + // Ex) `if TC:` with `from typing import TYPE_CHECKING as TC` + id == "TYPE_CHECKING" || semantic.match_typing_expr(test, "TYPE_CHECKING") } // Ex) `if typing.TYPE_CHECKING:` Expr::Attribute(ast::ExprAttribute { attr, .. }) => attr == "TYPE_CHECKING", @@ -846,7 +863,7 @@ impl BuiltinTypeChecker for FloatChecker { const EXPR_TYPE: PythonType = PythonType::Number(NumberLike::Float); } -pub struct IoBaseChecker; +struct IoBaseChecker; impl TypeChecker for IoBaseChecker { fn match_annotation(annotation: &Expr, semantic: &SemanticModel) -> bool { @@ -959,7 +976,7 @@ impl TypeChecker for PathlibPathChecker { } } -pub struct FastApiRouteChecker; +struct FastApiRouteChecker; impl FastApiRouteChecker { fn is_fastapi_route_constructor(semantic: &SemanticModel, expr: &Expr) -> bool { @@ -988,7 +1005,7 @@ impl TypeChecker for FastApiRouteChecker { } } -pub struct TypeVarLikeChecker; +struct TypeVarLikeChecker; impl TypeVarLikeChecker { /// Returns `true` if an [`Expr`] is a `TypeVar`, `TypeVarTuple`, or `ParamSpec` call. @@ -1131,7 +1148,7 @@ pub fn is_fastapi_route(binding: &Binding, semantic: &SemanticModel) -> bool { } /// Test whether the given binding is for an old-style `TypeVar`, `TypeVarTuple` or a `ParamSpec`. -pub fn is_type_var_like(binding: &Binding, semantic: &SemanticModel) -> bool { +pub(crate) fn is_type_var_like(binding: &Binding, semantic: &SemanticModel) -> bool { check_type::(binding, semantic) } diff --git a/crates/ruff_python_semantic/src/binding.rs b/crates/ruff_python_semantic/src/binding.rs index 099afc7ce9..83d42a20f2 100644 --- a/crates/ruff_python_semantic/src/binding.rs +++ b/crates/ruff_python_semantic/src/binding.rs @@ -463,7 +463,7 @@ impl<'a> Bindings<'a> { } /// Pushes a new [`Binding`] and returns its [`BindingId`]. - pub fn push(&mut self, binding: Binding<'a>) -> BindingId { + pub(crate) fn push(&mut self, binding: Binding<'a>) -> BindingId { self.0.push(binding) } } diff --git a/crates/ruff_python_semantic/src/cfg/graph.rs b/crates/ruff_python_semantic/src/cfg/graph.rs index babe234402..b6e719862f 100644 --- a/crates/ruff_python_semantic/src/cfg/graph.rs +++ b/crates/ruff_python_semantic/src/cfg/graph.rs @@ -28,7 +28,7 @@ impl<'stmt> ControlFlowGraph<'stmt> { } /// Index of terminal block - pub fn terminal(&self) -> BlockId { + pub(crate) fn terminal(&self) -> BlockId { self.terminal } @@ -126,12 +126,12 @@ impl Edges { } /// Returns iterator over indices of blocks targeted by given edges - pub fn targets(&self) -> impl ExactSizeIterator + '_ { + pub(crate) fn targets(&self) -> impl ExactSizeIterator + '_ { self.targets.iter().copied() } /// Returns iterator over [`Condition`]s which must be satisfied to traverse corresponding edge - pub fn conditions(&self) -> impl ExactSizeIterator { + pub(crate) fn conditions(&self) -> impl ExactSizeIterator { self.conditions.iter() } diff --git a/crates/ruff_python_semantic/src/definition.rs b/crates/ruff_python_semantic/src/definition.rs index 7ff94cbed2..78dc611938 100644 --- a/crates/ruff_python_semantic/src/definition.rs +++ b/crates/ruff_python_semantic/src/definition.rs @@ -24,7 +24,7 @@ pub struct DefinitionId; impl DefinitionId { /// Returns the ID for the module definition. #[inline] - pub const fn module() -> Self { + pub(crate) const fn module() -> Self { DefinitionId::from_u32(0) } } @@ -69,7 +69,7 @@ impl<'a> Module<'a> { } /// Return the name of the module. - pub const fn name(&self) -> Option<&'a str> { + pub(crate) const fn name(&self) -> Option<&'a str> { self.name } } @@ -97,7 +97,7 @@ pub struct Member<'a> { impl<'a> Member<'a> { /// Return the name of the member. - pub fn name(&self) -> &'a str { + fn name(&self) -> &'a str { match self.kind { MemberKind::Class(class) => &class.name, MemberKind::NestedClass(class) => &class.name, @@ -201,7 +201,7 @@ impl<'a> Definition<'a> { pub struct Definitions<'a>(IndexVec>); impl<'a> Definitions<'a> { - pub fn for_module(definition: Module<'a>) -> Self { + pub(crate) fn for_module(definition: Module<'a>) -> Self { Self(IndexVec::from_raw(vec![Definition::Module(definition)])) } diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index a868d01d06..718f5e1a59 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -307,7 +307,7 @@ impl<'a> SemanticModel<'a> { } /// Create a new [`Binding`] for a builtin. - pub fn push_builtin(&mut self) -> BindingId { + fn push_builtin(&mut self) -> BindingId { self.bindings.push(Binding { range: TextRange::default(), kind: BindingKind::Builtin, @@ -1769,7 +1769,7 @@ impl<'a> SemanticModel<'a> { } /// Returns a mutable reference to the global [`Scope`]. - pub fn global_scope_mut(&mut self) -> &mut Scope<'a> { + fn global_scope_mut(&mut self) -> &mut Scope<'a> { self.scopes.global_mut() } @@ -1794,12 +1794,12 @@ impl<'a> SemanticModel<'a> { } /// Returns the parent of the given [`Scope`], if any. - pub fn parent_scope(&self, scope: &Scope) -> Option<&Scope<'a>> { + fn parent_scope(&self, scope: &Scope) -> Option<&Scope<'a>> { scope.parent.map(|scope_id| &self.scopes[scope_id]) } /// Returns the ID of the parent of the given [`ScopeId`], if any. - pub fn parent_scope_id(&self, scope_id: ScopeId) -> Option { + fn parent_scope_id(&self, scope_id: ScopeId) -> Option { self.scopes[scope_id].parent } @@ -1842,7 +1842,7 @@ impl<'a> SemanticModel<'a> { /// Given a [`NodeId`], return its parent, if any. #[inline] - pub fn parent_expression(&self, node_id: NodeId) -> Option<&'a Expr> { + pub(crate) fn parent_expression(&self, node_id: NodeId) -> Option<&'a Expr> { let parent_node_id = self.nodes.ancestor_ids(node_id).nth(1)?; self.nodes[parent_node_id].as_expression() } @@ -2279,7 +2279,7 @@ impl<'a> SemanticModel<'a> { } /// Return the union of all handled exceptions as an [`Exceptions`] bitflag. - pub fn exceptions(&self) -> Exceptions { + fn exceptions(&self) -> Exceptions { let mut exceptions = Exceptions::empty(); for exception in &self.handled_exceptions { exceptions.insert(*exception); @@ -2374,7 +2374,7 @@ impl<'a> SemanticModel<'a> { /// Return `true` if the model is visiting a "`__future__` type definition" /// that was previously deferred when initially traversing the AST - pub const fn in_future_type_definition(&self) -> bool { + const fn in_future_type_definition(&self) -> bool { self.flags .intersects(SemanticModelFlags::FUTURE_TYPE_DEFINITION) } @@ -2401,7 +2401,7 @@ impl<'a> SemanticModel<'a> { /// cast("Thread", x) # Forward reference /// cast(Thread, x) # Non-forward reference /// ``` - pub const fn in_forward_reference(&self) -> bool { + const fn in_forward_reference(&self) -> bool { self.in_string_type_definition() || (self.in_future_type_definition() && self.in_typing_only_annotation()) } @@ -2463,7 +2463,7 @@ impl<'a> SemanticModel<'a> { } /// Return `true` if the model is in a t-string. - pub const fn in_t_string(&self) -> bool { + const fn in_t_string(&self) -> bool { self.flags.intersects(SemanticModelFlags::T_STRING) } @@ -2675,7 +2675,7 @@ impl TypingOnlyBindingsStatus { matches!(self, TypingOnlyBindingsStatus::Allowed) } - pub const fn is_disallowed(self) -> bool { + const fn is_disallowed(self) -> bool { matches!(self, TypingOnlyBindingsStatus::Disallowed) } } @@ -3164,7 +3164,7 @@ bitflags! { } impl SemanticModelFlags { - pub fn new(path: &Path) -> Self { + fn new(path: &Path) -> Self { let source_type = PySourceType::from(path); let mut flags = Self::default(); if source_type.is_stub() { diff --git a/crates/ruff_python_semantic/src/model/all.rs b/crates/ruff_python_semantic/src/model/all.rs index 17ff47540c..4ffefcd717 100644 --- a/crates/ruff_python_semantic/src/model/all.rs +++ b/crates/ruff_python_semantic/src/model/all.rs @@ -159,42 +159,40 @@ impl SemanticModel<'_> { // Allow comprehensions, even though we can't statically analyze them. return (None, DunderAllFlags::empty()); } - Expr::Name(ast::ExprName { id, .. }) - // Ex) `__all__ = __all__ + multiprocessing.__all__` - if id == "__all__" => { - return (None, DunderAllFlags::empty()); - } - Expr::Attribute(ast::ExprAttribute { attr, .. }) - // Ex) `__all__ = __all__ + multiprocessing.__all__` - if attr == "__all__" => { - return (None, DunderAllFlags::empty()); - } + // Ex) `__all__ = __all__ + multiprocessing.__all__` + Expr::Name(ast::ExprName { id, .. }) if id == "__all__" => { + return (None, DunderAllFlags::empty()); + } + // Ex) `__all__ = __all__ + multiprocessing.__all__` + Expr::Attribute(ast::ExprAttribute { attr, .. }) if attr == "__all__" => { + return (None, DunderAllFlags::empty()); + } + // Allow `tuple()`, `list()`, and their generic forms, like `list[int]()`. Expr::Call(ast::ExprCall { func, arguments, .. - }) - // Allow `tuple()`, `list()`, and their generic forms, like `list[int]()`. - if arguments.keywords.is_empty() && arguments.args.len() <= 1 - && self - .resolve_builtin_symbol(map_subscript(func)) - .is_some_and(|symbol| matches!(symbol, "tuple" | "list")) - => { - let [arg] = arguments.args.as_ref() else { - return (None, DunderAllFlags::empty()); - }; - match arg { - Expr::List(ast::ExprList { elts, .. }) - | Expr::Set(ast::ExprSet { elts, .. }) - | Expr::Tuple(ast::ExprTuple { elts, .. }) => { - return (Some(elts), DunderAllFlags::empty()); - } - _ => { - // We can't analyze other expressions, but they must be - // valid, since the `list` or `tuple` call will ultimately - // evaluate to a list or tuple. - return (None, DunderAllFlags::empty()); - } - } + }) if arguments.keywords.is_empty() + && arguments.args.len() <= 1 + && self + .resolve_builtin_symbol(map_subscript(func)) + .is_some_and(|symbol| matches!(symbol, "tuple" | "list")) => + { + let [arg] = arguments.args.as_ref() else { + return (None, DunderAllFlags::empty()); + }; + match arg { + Expr::List(ast::ExprList { elts, .. }) + | Expr::Set(ast::ExprSet { elts, .. }) + | Expr::Tuple(ast::ExprTuple { elts, .. }) => { + return (Some(elts), DunderAllFlags::empty()); } + _ => { + // We can't analyze other expressions, but they must be + // valid, since the `list` or `tuple` call will ultimately + // evaluate to a list or tuple. + return (None, DunderAllFlags::empty()); + } + } + } Expr::Named(ast::ExprNamed { value, .. }) => { // Allow, e.g., `__all__ += (value := ["A", "B"])`. return self.extract_dunder_all_elts(value); diff --git a/crates/ruff_python_semantic/src/nodes.rs b/crates/ruff_python_semantic/src/nodes.rs index d1e6358ad9..daef0a26a0 100644 --- a/crates/ruff_python_semantic/src/nodes.rs +++ b/crates/ruff_python_semantic/src/nodes.rs @@ -49,7 +49,7 @@ impl<'a> Nodes<'a> { /// Return the [`NodeId`] of the parent node. #[inline] - pub fn parent_id(&self, node_id: NodeId) -> Option { + pub(crate) fn parent_id(&self, node_id: NodeId) -> Option { self.nodes[node_id].parent } @@ -89,7 +89,7 @@ pub enum NodeRef<'a> { impl<'a> NodeRef<'a> { /// Returns the [`Stmt`] if this is a statement, or `None` if the reference is to another /// kind of AST node. - pub fn as_statement(&self) -> Option<&'a Stmt> { + pub(crate) fn as_statement(&self) -> Option<&'a Stmt> { match self { NodeRef::Stmt(stmt) => Some(stmt), NodeRef::Expr(_) => None, @@ -98,18 +98,18 @@ impl<'a> NodeRef<'a> { /// Returns the [`Expr`] if this is a expression, or `None` if the reference is to another /// kind of AST node. - pub fn as_expression(&self) -> Option<&'a Expr> { + pub(crate) fn as_expression(&self) -> Option<&'a Expr> { match self { NodeRef::Stmt(_) => None, NodeRef::Expr(expr) => Some(expr), } } - pub fn is_statement(&self) -> bool { + pub(crate) fn is_statement(&self) -> bool { self.as_statement().is_some() } - pub fn is_expression(&self) -> bool { + pub(crate) fn is_expression(&self) -> bool { self.as_expression().is_some() } } diff --git a/crates/ruff_python_semantic/src/scope.rs b/crates/ruff_python_semantic/src/scope.rs index fceaf14e75..eb80fda937 100644 --- a/crates/ruff_python_semantic/src/scope.rs +++ b/crates/ruff_python_semantic/src/scope.rs @@ -16,7 +16,7 @@ pub struct Scope<'a> { pub kind: ScopeKind<'a>, /// The parent scope, if any. - pub parent: Option, + pub(crate) parent: Option, /// A list of star imports in this scope. These represent _module_ imports (e.g., `sys` in /// `from sys import *`), rather than individual bindings (e.g., individual members in `sys`). @@ -45,7 +45,7 @@ pub struct Scope<'a> { } impl<'a> Scope<'a> { - pub fn global() -> Self { + fn global() -> Self { Scope { kind: ScopeKind::Module, parent: None, @@ -57,7 +57,7 @@ impl<'a> Scope<'a> { } } - pub fn local(kind: ScopeKind<'a>, parent: ScopeId) -> Self { + fn local(kind: ScopeKind<'a>, parent: ScopeId) -> Self { Scope { kind, parent: Some(parent), diff --git a/crates/ruff_python_stdlib/Cargo.toml b/crates/ruff_python_stdlib/Cargo.toml index 61e9b6309f..c3e59fa976 100644 --- a/crates/ruff_python_stdlib/Cargo.toml +++ b/crates/ruff_python_stdlib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_stdlib" -version = "0.0.5" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_stdlib/README.md b/crates/ruff_python_stdlib/README.md index 720253982a..d38692821e 100644 --- a/crates/ruff_python_stdlib/README.md +++ b/crates/ruff_python_stdlib/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_python_stdlib). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_python_stdlib). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_stdlib/src/builtins.rs b/crates/ruff_python_stdlib/src/builtins.rs index c13705c187..08247aacda 100644 --- a/crates/ruff_python_stdlib/src/builtins.rs +++ b/crates/ruff_python_stdlib/src/builtins.rs @@ -15,7 +15,7 @@ const IPYTHON_BUILTINS: &[&str] = &["__IPYTHON__", "display", "get_ipython"]; /// Globally defined names which are not attributes of the builtins module, or /// are only present on some platforms. -pub const MAGIC_GLOBALS: &[&str] = &[ +const MAGIC_GLOBALS: &[&str] = &[ "WindowsError", "__annotations__", "__builtins__", diff --git a/crates/ruff_python_stdlib/src/open_mode.rs b/crates/ruff_python_stdlib/src/open_mode.rs index e2257ebe73..3a145662ba 100644 --- a/crates/ruff_python_stdlib/src/open_mode.rs +++ b/crates/ruff_python_stdlib/src/open_mode.rs @@ -43,7 +43,11 @@ impl OpenMode { if open_mode.contains(OpenMode::UNIVERSAL_NEWLINES) && open_mode.intersects(OpenMode::WRITE | OpenMode::APPEND | OpenMode::CREATE) { - return Err("Open mode cannot contain the universal newlines (`U`) flag with write (`w`), append (`a`), or create (`x`) flags".to_string()); + return Err( + "Open mode cannot contain the universal newlines (`U`) flag \ + with write (`w`), append (`a`), or create (`x`) flags" + .to_string(), + ); } // Otherwise, reading, writing, creating, and appending are mutually exclusive. @@ -58,7 +62,11 @@ impl OpenMode { .count() != 1 { - return Err("Open mode must contain exactly one of the following flags: read (`r`), write (`w`), create (`x`), or append (`a`)".to_string()); + return Err( + "Open mode must contain exactly one of the following flags: \ + read (`r`), write (`w`), create (`x`), or append (`a`)" + .to_string(), + ); } Ok(open_mode) diff --git a/crates/ruff_python_stdlib/src/path.rs b/crates/ruff_python_stdlib/src/path.rs index afac1c3f12..8f00e7a8b6 100644 --- a/crates/ruff_python_stdlib/src/path.rs +++ b/crates/ruff_python_stdlib/src/path.rs @@ -1,12 +1,6 @@ use std::ffi::OsStr; use std::path::Path; -/// Return `true` if the [`Path`] is named `pyproject.toml`. -pub fn is_pyproject_toml(path: &Path) -> bool { - path.file_name() - .is_some_and(|name| name == "pyproject.toml") -} - /// Return `true` if a [`Path`] should use the name of its parent directory as its module name. pub fn is_module_file(path: &Path) -> bool { matches!( diff --git a/crates/ruff_python_stdlib/src/typing.rs b/crates/ruff_python_stdlib/src/typing.rs index d3c97889fe..7e80065446 100644 --- a/crates/ruff_python_stdlib/src/typing.rs +++ b/crates/ruff_python_stdlib/src/typing.rs @@ -324,6 +324,7 @@ pub fn is_immutable_return_type(qualified_name: &[&str]) -> bool { | "float" | "frozenset" | "int" + | "range" | "str" | "tuple" | "slice" diff --git a/crates/ruff_python_trivia/Cargo.toml b/crates/ruff_python_trivia/Cargo.toml index e00ccd86b4..465877e15c 100644 --- a/crates/ruff_python_trivia/Cargo.toml +++ b/crates/ruff_python_trivia/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_trivia" -version = "0.0.5" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_trivia/README.md b/crates/ruff_python_trivia/README.md index ea990d2d33..b467967292 100644 --- a/crates/ruff_python_trivia/README.md +++ b/crates/ruff_python_trivia/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_python_trivia). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_python_trivia). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_trivia/src/cursor.rs b/crates/ruff_python_trivia/src/cursor.rs index a2c7e17f2b..d1f54ccf8c 100644 --- a/crates/ruff_python_trivia/src/cursor.rs +++ b/crates/ruff_python_trivia/src/cursor.rs @@ -57,7 +57,7 @@ impl<'a> Cursor<'a> { /// Peeks the next character from the input stream without consuming it. /// Returns [`EOF_CHAR`] if the file is at the end of the file. - pub fn last(&self) -> char { + fn last(&self) -> char { self.chars.clone().next_back().unwrap_or(EOF_CHAR) } @@ -84,7 +84,7 @@ impl<'a> Cursor<'a> { } /// Consumes the next character from the back - pub fn bump_back(&mut self) -> Option { + pub(crate) fn bump_back(&mut self) -> Option { self.chars.next_back() } @@ -124,7 +124,7 @@ impl<'a> Cursor<'a> { } } - pub fn eat_char_back(&mut self, c: char) -> bool { + pub(crate) fn eat_char_back(&mut self, c: char) -> bool { if self.last() == c { self.bump_back(); true @@ -153,7 +153,7 @@ impl<'a> Cursor<'a> { } /// Eats symbols from the back while predicate returns true or until the beginning of file is reached. - pub fn eat_back_while(&mut self, mut predicate: impl FnMut(char) -> bool) { + pub(crate) fn eat_back_while(&mut self, mut predicate: impl FnMut(char) -> bool) { // It was tried making optimized version of this for eg. line comments, but // LLVM can inline all of this and compile it down to fast iteration over bytes. while predicate(self.last()) && !self.is_eof() { diff --git a/crates/ruff_python_trivia/src/pragmas.rs b/crates/ruff_python_trivia/src/pragmas.rs index 9f62e5e662..dfd17e84bf 100644 --- a/crates/ruff_python_trivia/src/pragmas.rs +++ b/crates/ruff_python_trivia/src/pragmas.rs @@ -18,16 +18,25 @@ pub fn is_pragma_comment(comment: &str) -> bool { let trimmed = content.trim_start(); // Case-insensitive match against `noqa` (which doesn't require a trailing colon). - matches!( + if matches!( trimmed.as_bytes(), [b'n' | b'N', b'o' | b'O', b'q' | b'Q', b'a' | b'A', ..] - ) || - // Case-insensitive match against pragmas that don't require a trailing colon. - trimmed.starts_with("nosec") || - // 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" | "pyrefly" | "pylint" | "flake8" | "ruff" | "ty")) + ) { + return true; + } + + // Case-insensitive match against pragmas that don't require a trailing colon. + if trimmed.starts_with("nosec") { + return true; + } + + // 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" | "pyrefly" | "pylint" | "flake8" | "ruff" | "ty" + ) + }) } /// Returns the byte offset within `comment` where a trailing pragma comment starts, diff --git a/crates/ruff_python_trivia/src/tokenizer.rs b/crates/ruff_python_trivia/src/tokenizer.rs index d43b65462e..37547a8959 100644 --- a/crates/ruff_python_trivia/src/tokenizer.rs +++ b/crates/ruff_python_trivia/src/tokenizer.rs @@ -848,7 +848,7 @@ impl<'a> BackwardsTokenizer<'a> { self.filter(|t| !t.kind().is_trivia()) } - pub fn next_token(&mut self) -> SimpleToken { + fn next_token(&mut self) -> SimpleToken { self.cursor.start_token(); self.back_offset = self.cursor.text_len() + self.offset; diff --git a/crates/ruff_ranged_value/Cargo.toml b/crates/ruff_ranged_value/Cargo.toml index df3e113867..8f699f3a38 100644 --- a/crates/ruff_ranged_value/Cargo.toml +++ b/crates/ruff_ranged_value/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_ranged_value" -version = "0.0.5" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_ranged_value/README.md b/crates/ruff_ranged_value/README.md index eb36cea302..068105df68 100644 --- a/crates/ruff_ranged_value/README.md +++ b/crates/ruff_ranged_value/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_ranged_value). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_ranged_value). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_ranged_value/src/lib.rs b/crates/ruff_ranged_value/src/lib.rs index 9bd87ac955..f1305e61fd 100644 --- a/crates/ruff_ranged_value/src/lib.rs +++ b/crates/ruff_ranged_value/src/lib.rs @@ -29,6 +29,9 @@ pub enum ValueSource { /// or if the value was auto-discovered by the editor /// (e.g., the Python environment) Editor, + + /// The value was provided by `uv workspace metadata`. + UvWorkspace, } impl ValueSource { @@ -37,12 +40,9 @@ impl ValueSource { ValueSource::File(path) => Some(&**path), ValueSource::Cli => None, ValueSource::Editor => None, + ValueSource::UvWorkspace => None, } } - - pub const fn is_cli(&self) -> bool { - matches!(self, ValueSource::Cli) - } } thread_local! { @@ -151,7 +151,7 @@ impl RangedValue { Self::with_range(value, ValueSource::Editor, TextRange::default()) } - pub fn with_range(value: T, source: ValueSource, range: TextRange) -> Self { + fn with_range(value: T, source: ValueSource, range: TextRange) -> Self { Self { value, range: Some(range), @@ -167,12 +167,6 @@ impl RangedValue { &self.source } - #[must_use] - pub fn with_source(mut self, source: ValueSource) -> Self { - self.source = source; - self - } - #[must_use] pub fn map_value(self, f: impl FnOnce(T) -> R) -> RangedValue { RangedValue { diff --git a/crates/ruff_server/Cargo.toml b/crates/ruff_server/Cargo.toml index 808f812876..465d5b7d10 100644 --- a/crates/ruff_server/Cargo.toml +++ b/crates/ruff_server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_server" -version = "0.0.5" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_server/README.md b/crates/ruff_server/README.md index 9f1ed8c62f..fbb2f6b627 100644 --- a/crates/ruff_server/README.md +++ b/crates/ruff_server/README.md @@ -24,8 +24,8 @@ You can also join us on [**Discord**](https://discord.com/invite/astral-sh). This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_server). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_server). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_server/src/edit/notebook.rs b/crates/ruff_server/src/edit/notebook.rs index ba15031389..6901ced8e7 100644 --- a/crates/ruff_server/src/edit/notebook.rs +++ b/crates/ruff_server/src/edit/notebook.rs @@ -63,25 +63,32 @@ impl NotebookDocument { let cells = self .cells .iter() - .map(|cell| match cell.kind { - NotebookCellKind::Code => ruff_notebook::Cell::Code(ruff_notebook::CodeCell { - execution_count: None, - id: None, - metadata: CellMetadata::default(), - outputs: vec![], - source: ruff_notebook::SourceValue::String( - cell.document.contents().to_string(), - ), - }), + .filter_map(|cell| match cell.kind { + NotebookCellKind::Code => { + Some(ruff_notebook::Cell::Code(ruff_notebook::CodeCell { + execution_count: None, + id: None, + metadata: CellMetadata::default(), + outputs: vec![], + source: ruff_notebook::SourceValue::String( + cell.document.contents().to_string(), + ), + })) + } NotebookCellKind::Markup => { - ruff_notebook::Cell::Markdown(ruff_notebook::MarkdownCell { + Some(ruff_notebook::Cell::Markdown(ruff_notebook::MarkdownCell { attachments: None, id: None, metadata: CellMetadata::default(), source: ruff_notebook::SourceValue::String( cell.document.contents().to_string(), ), - }) + })) + } + NotebookCellKind::Custom(_) => { + // Ignore unsupported cell kinds. This arm should never be reached unless a + // client sends a value which is not mentioned/supported in the LSP. + None } }) .collect(); @@ -92,8 +99,12 @@ impl NotebookDocument { nbformat_minor: 5, }; - 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}")) + 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}" + ) + }) } pub(crate) fn update( @@ -232,11 +243,7 @@ impl NotebookDocument { } impl NotebookCell { - pub(crate) fn new( - cell: lsp_types::NotebookCell, - contents: String, - version: DocumentVersion, - ) -> Self { + fn new(cell: lsp_types::NotebookCell, contents: String, version: DocumentVersion) -> Self { Self { uri: cell.document, kind: cell.kind, diff --git a/crates/ruff_server/src/edit/text_document.rs b/crates/ruff_server/src/edit/text_document.rs index 142db788c8..1e19a0794c 100644 --- a/crates/ruff_server/src/edit/text_document.rs +++ b/crates/ruff_server/src/edit/text_document.rs @@ -28,7 +28,7 @@ pub struct TextDocument { } #[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum LanguageId { +pub(crate) enum LanguageId { Python, Markdown, Other, @@ -56,12 +56,12 @@ impl TextDocument { } #[must_use] - pub fn with_language_id(mut self, language_id: LanguageKind) -> Self { + pub(crate) fn with_language_id(mut self, language_id: LanguageKind) -> Self { self.language_id = Some(LanguageId::from(language_id)); self } - pub fn into_contents(self) -> String { + pub(crate) fn into_contents(self) -> String { self.contents } @@ -69,15 +69,15 @@ impl TextDocument { &self.contents } - pub fn index(&self) -> &LineIndex { + pub(crate) fn index(&self) -> &LineIndex { &self.index } - pub fn version(&self) -> DocumentVersion { + pub(crate) fn version(&self) -> DocumentVersion { self.version } - pub fn language_id(&self) -> Option { + pub(crate) fn language_id(&self) -> Option { self.language_id } @@ -131,7 +131,7 @@ impl TextDocument { }); } - pub fn update_version(&mut self, new_version: DocumentVersion) { + pub(crate) fn update_version(&mut self, new_version: DocumentVersion) { self.modify_with_manual_index(|_, version, _| { *version = new_version; }); diff --git a/crates/ruff_server/src/fix.rs b/crates/ruff_server/src/fix.rs index 82dd1654cc..04b6bee64e 100644 --- a/crates/ruff_server/src/fix.rs +++ b/crates/ruff_server/src/fix.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; -use ruff_python_ast::SourceType; +use ruff_python_ast::{SourceType, TomlSourceType}; use rustc_hash::FxHashMap; use crate::{ @@ -14,6 +14,7 @@ use ruff_linter::{ linter::FixerResult, packaging::detect_package_root, settings::{LinterSettings, flags}, + toml::lint_fix_toml, }; use ruff_notebook::SourceValue; use ruff_source_file::LineIndex; @@ -30,11 +31,6 @@ pub(crate) fn fix_all( let settings = query.settings(); let document_path = query.virtual_file_path(); - let SourceType::Python(source_type) = query.source_type_for_lint() else { - return Ok(Fixes::default()); - }; - let source_kind = query.make_python_source_kind(source_type); - // If the document is excluded, return an empty list of fixes. if is_document_excluded_for_linting( &document_path, @@ -45,6 +41,15 @@ pub(crate) fn fix_all( return Ok(Fixes::default()); } + let source_type = match query.source_type_for_lint() { + SourceType::Python(source_type) => source_type, + SourceType::Toml(source_type @ (TomlSourceType::Pyproject | TomlSourceType::Ruff)) => { + return fix_toml(query, linter_settings, source_type, encoding); + } + SourceType::Toml(_) | SourceType::Markdown => return Ok(Fixes::default()), + }; + let source_kind = query.make_python_source_kind(source_type); + let file_path = query.file_path(); let package = if let Some(file_path) = &file_path { detect_package_root( @@ -132,28 +137,69 @@ pub(crate) fn fix_all( } Ok(fixes) } else { - let source_index = LineIndex::from_source_text(source_kind.source_code()); + Ok(text_document_fixes( + query, + source_kind.source_code(), + transformed.source_code(), + encoding, + )) + } +} - let modified = transformed.source_code(); - let modified_index = LineIndex::from_source_text(modified); +fn fix_toml( + query: &DocumentQuery, + linter_settings: &LinterSettings, + source_type: TomlSourceType, + encoding: PositionEncoding, +) -> crate::Result { + let document = query.as_single_document()?; + let transformed = lint_fix_toml( + &query.virtual_file_path(), + document.contents(), + linter_settings, + source_type, + query.settings().unsafe_fixes, + ) + .transformed; - let Replacement { - source_range, - modified_range, - } = Replacement::between( - source_kind.source_code(), - source_index.line_starts(), - modified, - modified_index.line_starts(), - ); - Ok([( - query.make_key().into_uri(), - vec![lsp_types::TextEdit { - range: source_range.to_range(source_kind.source_code(), &source_index, encoding), - new_text: modified[modified_range].to_owned(), - }], - )] - .into_iter() - .collect()) + if let Cow::Borrowed(_) = transformed { + return Ok(Fixes::default()); } + + Ok(text_document_fixes( + query, + document.contents(), + transformed.as_ref(), + encoding, + )) +} + +fn text_document_fixes( + query: &DocumentQuery, + source: &str, + modified: &str, + encoding: PositionEncoding, +) -> Fixes { + let source_index = LineIndex::from_source_text(source); + let modified_index = LineIndex::from_source_text(modified); + + let Replacement { + source_range, + modified_range, + } = Replacement::between( + source, + source_index.line_starts(), + modified, + modified_index.line_starts(), + ); + + [( + query.make_key().into_uri(), + vec![lsp_types::TextEdit { + range: source_range.to_range(source, &source_index, encoding), + new_text: modified[modified_range].to_owned(), + }], + )] + .into_iter() + .collect() } diff --git a/crates/ruff_server/src/format.rs b/crates/ruff_server/src/format.rs index d5ddb2ecb0..0d42d9ffb0 100644 --- a/crates/ruff_server/src/format.rs +++ b/crates/ruff_server/src/format.rs @@ -33,14 +33,13 @@ pub(crate) enum FormatBackend { pub(crate) enum FormatResult { Formatted(String), Unchanged, - PreviewOnly { file_format: &'static str }, } impl FormatResult { fn into_formatted(self) -> Option { match self { Self::Formatted(formatted) => Some(formatted), - Self::Unchanged | Self::PreviewOnly { .. } => None, + Self::Unchanged => None, } } } @@ -91,13 +90,6 @@ fn format_internal( } } SourceType::Markdown => { - if !formatter_settings.preview.is_enabled() { - tracing::warn!("Markdown formatting is experimental, enable preview mode."); - return Ok(FormatResult::PreviewOnly { - file_format: "Markdown", - }); - } - match format_code_blocks(document.contents(), Some(path), formatter_settings) { MarkdownResult::Formatted(formatted) => Ok(FormatResult::Formatted(formatted)), MarkdownResult::Unchanged => Ok(FormatResult::Unchanged), @@ -309,7 +301,7 @@ impl UvFormatCommand { } /// Execute the format command on the given source. - pub(crate) fn format( + fn format( &self, source: &str, path: &Path, @@ -365,12 +357,12 @@ impl UvFormatCommand { } /// Format the entire document. - pub(crate) fn format_document(&self, source: &str, path: &Path) -> crate::Result { + fn format_document(&self, source: &str, path: &Path) -> crate::Result { self.format(source, path, None) } /// Format a specific range. - pub(crate) fn format_range( + fn format_range( &self, source: &str, range: TextRange, diff --git a/crates/ruff_server/src/lib.rs b/crates/ruff_server/src/lib.rs index 2177fe2092..2df7a8bdfd 100644 --- a/crates/ruff_server/src/lib.rs +++ b/crates/ruff_server/src/lib.rs @@ -21,22 +21,22 @@ mod server; mod session; mod workspace; -pub(crate) const SERVER_NAME: &str = "ruff"; +const SERVER_NAME: &str = "ruff"; pub(crate) const DIAGNOSTIC_NAME: &str = "Ruff"; -pub(crate) const SOURCE_FIX_ALL_RUFF: CodeActionKind = CodeActionKind::new("source.fixAll.ruff"); -pub(crate) const SOURCE_ORGANIZE_IMPORTS_RUFF: CodeActionKind = +const SOURCE_FIX_ALL_RUFF: CodeActionKind = CodeActionKind::new("source.fixAll.ruff"); +const SOURCE_ORGANIZE_IMPORTS_RUFF: CodeActionKind = CodeActionKind::new("source.organizeImports.ruff"); -pub(crate) const NOTEBOOK_SOURCE_FIX_ALL_RUFF: CodeActionKind = +const NOTEBOOK_SOURCE_FIX_ALL_RUFF: CodeActionKind = CodeActionKind::new("notebook.source.fixAll.ruff"); -pub(crate) const NOTEBOOK_SOURCE_ORGANIZE_IMPORTS_RUFF: CodeActionKind = +const NOTEBOOK_SOURCE_ORGANIZE_IMPORTS_RUFF: CodeActionKind = CodeActionKind::new("notebook.source.organizeImports.ruff"); /// 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 { +fn version() -> &'static str { ruff_linter::VERSION } diff --git a/crates/ruff_server/src/lint.rs b/crates/ruff_server/src/lint.rs index 9e84d15b8b..834119a9d3 100644 --- a/crates/ruff_server/src/lint.rs +++ b/crates/ruff_server/src/lint.rs @@ -3,12 +3,13 @@ use std::fmt::Write; use std::path::Path; -use ruff_python_ast::SourceType; +use ruff_python_ast::{SourceType, TomlSourceType}; +use ruff_workspace::Settings; use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use crate::{ - DIAGNOSTIC_NAME, PositionEncoding, + DIAGNOSTIC_NAME, PositionEncoding, TextDocument, edit::{NotebookDocument, NotebookRange, ToRangeExt}, resolve::is_document_excluded_for_linting, session::DocumentQuery, @@ -23,11 +24,12 @@ use ruff_linter::{ package::PackageRoot, packaging::detect_package_root, preview::is_human_readable_names_enabled, - settings::{LinterSettings, flags}, + settings::flags, source_kind::SourceKind, suppression::Suppressions, + toml::lint_toml, }; -use ruff_notebook::Notebook; +use ruff_notebook::{Notebook, NotebookIndex}; use ruff_python_codegen::Stylist; use ruff_python_index::Indexer; use ruff_source_file::LineIndex; @@ -37,14 +39,14 @@ use ruff_text_size::{Ranged, TextRange}; #[derive(Serialize, Deserialize, Debug, Clone)] pub(crate) struct AssociatedDiagnosticData { /// The message describing what the fix does, if it exists, or the diagnostic name otherwise. - pub(crate) title: String, + title: String, /// Edits to fix the diagnostic. If this is empty, a fix /// does not exist. - pub(crate) edits: Vec, + edits: Vec, /// The identifier displayed for the diagnostic. - pub(crate) code: String, + code: String, /// Possible edit to add a suppression comment which will disable this diagnostic. - pub(crate) noqa_edit: Option, + noqa_edit: Option, } /// Describes a fix for `fixed_diagnostic` that may have quick fix @@ -76,13 +78,6 @@ pub(crate) fn check( let settings = query.settings(); let document_path = query.virtual_file_path(); - let SourceType::Python(source_type) = query.source_type_for_lint() else { - return DiagnosticsMap::default(); - }; - let source_kind = query.make_python_source_kind(source_type); - let document_uri = query.make_key().into_uri(); - let notebook = query.as_notebook(); - // If the document is excluded, return an empty list of diagnostics. if is_document_excluded_for_linting( &document_path, @@ -93,6 +88,88 @@ pub(crate) fn check( return DiagnosticsMap::default(); } + let result = match query.source_type_for_lint() { + SourceType::Python(source_type) => check_python(query, source_type), + SourceType::Toml(source_type @ (TomlSourceType::Pyproject | TomlSourceType::Ruff)) => { + let Ok(document) = query.as_single_document() else { + return DiagnosticsMap::default(); + }; + check_toml(query, document, source_type) + } + SourceType::Toml(_) | SourceType::Markdown => return DiagnosticsMap::default(), + }; + + let CheckResult { + diagnostics, + suppression_edits, + document, + } = result; + let document_uri = query.make_key().into_uri(); + let context = LspDiagnosticContext { + source: document.source(), + index: document.index(), + notebook_index: document.notebook_index(), + encoding, + document_path: &document_path, + document_uri: &document_uri, + notebook: query.as_notebook(), + supports_related_information, + settings, + }; + + let mut diagnostics_map = DiagnosticsMap::default(); + + // Populates all relevant URLs with an empty diagnostic list. + // This ensures that documents without diagnostics still get updated. + if let Some(notebook) = query.as_notebook() { + for uri in notebook.uris() { + diagnostics_map.entry(uri.clone()).or_default(); + } + } else { + diagnostics_map + .entry(query.make_key().into_uri()) + .or_default(); + } + + let mut suppression_edits = suppression_edits.into_iter(); + let lsp_diagnostics = diagnostics.into_iter().filter_map(|message| { + let suppression_edit = suppression_edits.next().flatten(); + if message.is_invalid_syntax() && !show_syntax_errors { + None + } else { + Some(to_lsp_diagnostic(&message, suppression_edit, &context)) + } + }); + + if let Some(notebook) = query.as_notebook() { + for (index, diagnostic) in lsp_diagnostics { + let Some(uri) = notebook.cell_uri_by_index(index) else { + tracing::warn!("Unable to find notebook cell at index {index}."); + continue; + }; + diagnostics_map + .entry(uri.clone()) + .or_default() + .push(diagnostic); + } + } else { + diagnostics_map + .entry(query.make_key().into_uri()) + .or_default() + .extend(lsp_diagnostics.map(|(_, diagnostic)| diagnostic)); + } + + diagnostics_map +} + +fn check_python( + query: &DocumentQuery, + source_type: ruff_python_ast::PySourceType, +) -> CheckResult<'_> { + let settings = query.settings(); + let document_path = query.virtual_file_path(); + let source_kind = query.make_python_source_kind(source_type); + let file_path = query.file_path(); let package = if let Some(file_path) = &file_path { detect_package_root( @@ -157,68 +234,54 @@ pub(crate) fn check( &directives.noqa_line_for, stylist.line_ending(), &suppressions, - if is_human_readable_names_enabled(settings.linter.preview) { + if is_human_readable_names_enabled(settings.linter.preview) + && !settings.output_prefer_rule_codes + { SuppressionKind::Ignore } else { SuppressionKind::Noqa }, + settings.linter.preview, ); - let context = LspDiagnosticContext { - source_kind: &source_kind, - index: locator.to_index(), - encoding, - document_path: document_path.as_ref(), - document_uri: &document_uri, - notebook, - supports_related_information, - settings: &settings.linter, - }; - - let mut diagnostics_map = DiagnosticsMap::default(); - - // Populates all relevant URLs with an empty diagnostic list. - // This ensures that documents without diagnostics still get updated. - if let Some(notebook) = query.as_notebook() { - for uri in notebook.uris() { - diagnostics_map.entry(uri.clone()).or_default(); - } - } else { - diagnostics_map - .entry(query.make_key().into_uri()) - .or_default(); + let index = locator.to_index().clone(); + + CheckResult { + diagnostics, + suppression_edits, + document: CheckedDocument::Python { + source: source_kind, + index, + }, } +} - let lsp_diagnostics = - diagnostics - .into_iter() - .zip(suppression_edits) - .filter_map(|(message, noqa_edit)| { - if message.is_invalid_syntax() && !show_syntax_errors { - None - } else { - Some(to_lsp_diagnostic(&message, noqa_edit, &context)) - } - }); - - if let Some(notebook) = query.as_notebook() { - for (index, diagnostic) in lsp_diagnostics { - let Some(uri) = notebook.cell_uri_by_index(index) else { - tracing::warn!("Unable to find notebook cell at index {index}."); - continue; - }; - diagnostics_map - .entry(uri.clone()) - .or_default() - .push(diagnostic); - } +fn check_toml<'a>( + query: &DocumentQuery, + document: &'a TextDocument, + source_type: TomlSourceType, +) -> CheckResult<'a> { + let settings = query.settings(); + let diagnostics = if settings + .linter + .rules + .iter_enabled() + .any(|rule| rule.lint_source().is_toml()) + { + lint_toml( + &query.virtual_file_path(), + document.contents(), + &settings.linter, + source_type, + ) } else { - diagnostics_map - .entry(query.make_key().into_uri()) - .or_default() - .extend(lsp_diagnostics.map(|(_, diagnostic)| diagnostic)); - } + Vec::new() + }; - diagnostics_map + CheckResult { + diagnostics, + suppression_edits: Vec::new(), + document: CheckedDocument::Toml(document), + } } /// Converts LSP diagnostics to a list of `DiagnosticFix`es by deserializing associated data on each diagnostic. @@ -249,15 +312,53 @@ pub(crate) fn fixes_for_diagnostics( .collect() } +enum CheckedDocument<'a> { + Python { + source: SourceKind, + index: LineIndex, + }, + Toml(&'a TextDocument), +} + +impl CheckedDocument<'_> { + fn source(&self) -> &str { + match self { + Self::Python { source, .. } => source.source_code(), + Self::Toml(document) => document.contents(), + } + } + + fn index(&self) -> &LineIndex { + match self { + Self::Python { index, .. } => index, + Self::Toml(document) => document.index(), + } + } + + fn notebook_index(&self) -> Option<&NotebookIndex> { + match self { + Self::Python { source, .. } => source.as_ipy_notebook().map(Notebook::index), + Self::Toml(_) => None, + } + } +} + +struct CheckResult<'a> { + diagnostics: Vec, + suppression_edits: Vec>, + document: CheckedDocument<'a>, +} + struct LspDiagnosticContext<'a> { - source_kind: &'a SourceKind, + source: &'a str, index: &'a LineIndex, + notebook_index: Option<&'a NotebookIndex>, encoding: PositionEncoding, document_path: &'a Path, document_uri: &'a lsp_types::Uri, notebook: Option<&'a NotebookDocument>, supports_related_information: bool, - settings: &'a LinterSettings, + settings: &'a Settings, } /// Generates an LSP diagnostic with an associated cell index for the diagnostic to go in. @@ -275,7 +376,9 @@ fn to_lsp_diagnostic( let (severity, code) = if let Some(code) = diagnostic.secondary_code() { let severity = severity(code); - let code = if is_human_readable_names_enabled(context.settings.preview) { + let code = if is_human_readable_names_enabled(context.settings.linter.preview) + && !context.settings.output_prefer_rule_codes + { name.to_string() } else { code.to_string() @@ -299,22 +402,12 @@ fn to_lsp_diagnostic( .into_iter() .flat_map(Fix::edits) .map(|edit| lsp_types::TextEdit { - range: diagnostic_edit_range( - edit.range(), - context.source_kind, - context.index, - context.encoding, - ), + range: diagnostic_edit_range(edit.range(), context), new_text: edit.content().unwrap_or_default().to_string(), }) .collect(); let noqa_edit = noqa_edit.map(|noqa_edit| lsp_types::TextEdit { - range: diagnostic_edit_range( - noqa_edit.range(), - context.source_kind, - context.index, - context.encoding, - ), + range: diagnostic_edit_range(noqa_edit.range(), context), new_text: noqa_edit.into_content().unwrap_or_default().into_string(), }); serde_json::to_value(AssociatedDiagnosticData { @@ -330,20 +423,16 @@ fn to_lsp_diagnostic( let range: lsp_types::Range; let cell: usize; - if let Some(notebook_index) = context.source_kind.as_ipy_notebook().map(Notebook::index) { + if let Some(notebook_index) = context.notebook_index { NotebookRange { cell, range } = diagnostic_range.to_notebook_range( - context.source_kind.source_code(), + context.source, context.index, notebook_index, context.encoding, ); } else { cell = usize::default(); - range = diagnostic_range.to_range( - context.source_kind.source_code(), - context.index, - context.encoding, - ); + range = diagnostic_range.to_range(context.source, context.index, context.encoding); } let related_information = @@ -375,9 +464,9 @@ fn to_lsp_diagnostic( .primary_annotation() .and_then(Annotation::get_message) { - format!("{}: {annotation_message}", diagnostic.primary_message()) + format!("{}: {annotation_message}", diagnostic.headline_message()) } else { - diagnostic.primary_message().to_string() + diagnostic.headline_message().to_string() } } else { diagnostic.concise_message().to_string() @@ -450,7 +539,7 @@ fn span_to_location(span: &Span, context: &LspDiagnosticContext) -> Option Option lsp_types::Range { - if let Some(notebook_index) = source_kind.as_ipy_notebook().map(Notebook::index) { +fn diagnostic_edit_range(range: TextRange, context: &LspDiagnosticContext) -> lsp_types::Range { + if let Some(notebook_index) = context.notebook_index { range - .to_notebook_range(source_kind.source_code(), index, notebook_index, encoding) + .to_notebook_range( + context.source, + context.index, + notebook_index, + context.encoding, + ) .range } else { - range.to_range(source_kind.source_code(), index, encoding) + range.to_range(context.source, context.index, context.encoding) } } @@ -515,6 +604,7 @@ fn tags(diagnostic: &Diagnostic) -> Option> { #[cfg(test)] mod tests { use ruff_db::diagnostic::{DiagnosticId, Severity, SubDiagnosticSeverity}; + use ruff_linter::source_kind::SourceKind; use ruff_source_file::SourceFileBuilder; use ruff_text_size::{TextRange, TextSize}; @@ -565,10 +655,11 @@ mod tests { }; let index = LineIndex::from_source_text(source); let uri = lsp_types::Uri::parse("file:///test.py").expect("URI to be valid"); - let settings = LinterSettings::default(); + let settings = Settings::default(); let context = LspDiagnosticContext { - source_kind: &source_kind, + source: source_kind.source_code(), index: &index, + notebook_index: None, encoding: PositionEncoding::UTF8, document_path: Path::new("test.py"), document_uri: &uri, diff --git a/crates/ruff_server/src/server.rs b/crates/ruff_server/src/server.rs index 58cd033ae5..c02b008a71 100644 --- a/crates/ruff_server/src/server.rs +++ b/crates/ruff_server/src/server.rs @@ -59,7 +59,8 @@ impl Server { 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 server_capabilities = + Self::server_capabilities(position_encoding, &client_capabilities); let connection = connection.initialize_finish( id, @@ -150,7 +151,41 @@ impl Server { .unwrap_or_default() } - fn server_capabilities(position_encoding: PositionEncoding) -> types::ServerCapabilities { + fn supports_dynamic_formatting(client_capabilities: &ClientCapabilities) -> bool { + client_capabilities + .text_document + .as_ref() + .and_then(|text_document| text_document.formatting) + .and_then(|formatting| formatting.dynamic_registration) + .unwrap_or_default() + } + + fn supports_dynamic_range_formatting(client_capabilities: &ClientCapabilities) -> bool { + client_capabilities + .text_document + .as_ref() + .and_then(|text_document| text_document.range_formatting) + .and_then(|range_formatting| range_formatting.dynamic_registration) + .unwrap_or_default() + } + + fn server_capabilities( + position_encoding: PositionEncoding, + client_capabilities: &ClientCapabilities, + ) -> types::ServerCapabilities { + let document_formatting_provider = if Self::supports_dynamic_formatting(client_capabilities) + { + None + } else { + Some(true.into()) + }; + let document_range_formatting_provider = + if Self::supports_dynamic_range_formatting(client_capabilities) { + None + } else { + Some(true.into()) + }; + types::ServerCapabilities { position_encoding: Some(position_encoding.into()), code_action_provider: Some( @@ -176,8 +211,8 @@ impl Server { file_operations: None, text_document_content: None, }), - document_formatting_provider: Some(true.into()), - document_range_formatting_provider: Some(true.into()), + document_formatting_provider, + document_range_formatting_provider, diagnostic_provider: Some( DiagnosticOptions { identifier: Some(crate::DIAGNOSTIC_NAME.into()), diff --git a/crates/ruff_server/src/server/api.rs b/crates/ruff_server/src/server/api.rs index 0c695e3188..d811dc7839 100644 --- a/crates/ruff_server/src/server/api.rs +++ b/crates/ruff_server/src/server/api.rs @@ -99,9 +99,7 @@ pub(super) fn request(req: server::Request) -> Task { pub(super) fn notification(notif: server::Notification) -> Task { match LspNotificationMethod::from(notif.method.as_str()) { - notification::DidChange::METHOD => { - sync_notification_task::(notif) - } + notification::DidChange::METHOD => sync_notification_task::(notif), notification::DidChangeConfiguration::METHOD => { sync_notification_task::(notif) } @@ -138,7 +136,8 @@ pub(super) fn notification(notif: server::Notification) -> Task { tracing::error!("Encountered error when routing notification: {err}"); Task::sync(|_session, client| { client.show_error_message( - "Ruff failed to handle a notification from the editor. Check the logs for more details." + "Ruff failed to handle a notification from the editor. \ + Check the logs for more details.", ); }) }) @@ -309,8 +308,11 @@ where 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.") + 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) @@ -361,8 +363,11 @@ where 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.") + 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)?, @@ -386,7 +391,7 @@ impl> LSPResult for core::result::Result { } impl Error { - pub(crate) fn new(err: anyhow::Error, code: server::ErrorCode) -> Self { + fn new(err: anyhow::Error, code: server::ErrorCode) -> Self { Self { code, error: err } } } 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 5f280b8629..fedf2239bd 100644 --- a/crates/ruff_server/src/server/api/requests/code_action.rs +++ b/crates/ruff_server/src/server/api/requests/code_action.rs @@ -1,6 +1,6 @@ use lsp_server::ErrorCode; use lsp_types::{self as types, CodeActionRequest, CodeActionResponse}; -use ruff_python_ast::SourceType; +use ruff_python_ast::{SourceType, TomlSourceType}; use rustc_hash::FxHashSet; use types::CodeActionKind; @@ -41,9 +41,10 @@ impl super::BackgroundDocumentRequestHandler for CodeActions { let query = snapshot.query(); - // Don't provide code actions for non-Python documents (e.g., markdown files). - let SourceType::Python(_) = query.source_type_for_lint() else { - return Ok(Some(response)); + let is_python = match query.source_type_for_lint() { + SourceType::Python(_) => true, + SourceType::Toml(TomlSourceType::Pyproject | TomlSourceType::Ruff) => false, + SourceType::Toml(_) | SourceType::Markdown => return Ok(Some(response)), }; let document_path = query.virtual_file_path(); @@ -70,7 +71,8 @@ impl super::BackgroundDocumentRequestHandler for CodeActions { .extend(quick_fix(&snapshot, &fixes).with_failure_code(ErrorCode::InternalError)?); } - if snapshot.client_settings().noqa_comments() + if is_python + && snapshot.client_settings().noqa_comments() && supported_code_actions.contains(&SupportedCodeAction::QuickFix) { response.extend(noqa_comments(&snapshot, &fixes)); @@ -93,7 +95,7 @@ impl super::BackgroundDocumentRequestHandler for CodeActions { } } - if snapshot.client_settings().organize_imports() { + if is_python && snapshot.client_settings().organize_imports() { if supported_code_actions.contains(&SupportedCodeAction::SourceOrganizeImports) { if snapshot.is_notebook_cell() { // This is ignore here because the client requests this code action for each 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 9bfc8cec54..93c26f361e 100644 --- a/crates/ruff_server/src/server/api/requests/execute_command.rs +++ b/crates/ruff_server/src/server/api/requests/execute_command.rs @@ -67,7 +67,12 @@ impl super::SyncRequestHandler for ExecuteCommand { // check if we can apply a workspace edit if !session.resolved_client_capabilities().apply_edit { - return Err(anyhow::anyhow!("Cannot execute the '{}' command: the client does not support `workspace/applyEdit`", command.label())).with_failure_code(ErrorCode::InternalError); + return Err(anyhow::anyhow!( + "Cannot execute the '{}' command: \ + the client does not support `workspace/applyEdit`", + command.label() + )) + .with_failure_code(ErrorCode::InternalError); } let mut arguments: Vec = params @@ -99,7 +104,7 @@ impl super::SyncRequestHandler for ExecuteCommand { .with_failure_code(ErrorCode::InternalError)?; } SupportedCommand::Format => { - let fixes = super::format::format_full_document(&snapshot, client)?; + let fixes = super::format::format_full_document(&snapshot)?; edit_tracker .set_fixes_for_document(fixes, version) .with_failure_code(ErrorCode::InternalError)?; diff --git a/crates/ruff_server/src/server/api/requests/format.rs b/crates/ruff_server/src/server/api/requests/format.rs index 4c2fb585cf..36948e5266 100644 --- a/crates/ruff_server/src/server/api/requests/format.rs +++ b/crates/ruff_server/src/server/api/requests/format.rs @@ -24,7 +24,7 @@ impl super::BackgroundDocumentRequestHandler for Format { fn run_with_snapshot( snapshot: Self::Snapshot, - client: &Client, + _client: &Client, _params: types::DocumentFormattingParams, ) -> Result { let snapshot = match snapshot { @@ -37,12 +37,12 @@ impl super::BackgroundDocumentRequestHandler for Format { } }; - format_document(&snapshot, client) + format_document(&snapshot) } } /// Formats either a full text document or each individual cell in a single notebook document. -pub(super) fn format_full_document(snapshot: &DocumentSnapshot, client: &Client) -> Result { +pub(super) fn format_full_document(snapshot: &DocumentSnapshot) -> Result { let mut fixes = Fixes::default(); let query = snapshot.query(); let backend = snapshot @@ -56,21 +56,16 @@ pub(super) fn format_full_document(snapshot: &DocumentSnapshot, client: &Client) .uris() .map(|uri| (uri.clone(), notebook.cell_document_by_uri(uri).unwrap())) { - if let Some(changes) = format_text_document( - text_document, - query, - snapshot.encoding(), - true, - backend, - client, - )? { + if let Some(changes) = + format_text_document(text_document, query, snapshot.encoding(), true, backend)? + { fixes.insert(uri, changes); } } } DocumentQuery::Text { document, .. } => { if let Some(changes) = - format_text_document(document, query, snapshot.encoding(), false, backend, client)? + format_text_document(document, query, snapshot.encoding(), false, backend)? { fixes.insert(snapshot.query().make_key().into_uri(), changes); } @@ -82,10 +77,7 @@ pub(super) fn format_full_document(snapshot: &DocumentSnapshot, client: &Client) /// Formats either a full text document or an specific notebook cell. If the query within the snapshot is a notebook document /// with no selected cell, this will throw an error. -pub(super) fn format_document( - snapshot: &DocumentSnapshot, - client: &Client, -) -> Result { +fn format_document(snapshot: &DocumentSnapshot) -> Result { let text_document = snapshot .query() .as_single_document() @@ -102,7 +94,6 @@ pub(super) fn format_document( snapshot.encoding(), query.as_notebook().is_some(), backend, - client, ) } @@ -112,7 +103,6 @@ fn format_text_document( encoding: PositionEncoding, is_notebook: bool, backend: crate::format::FormatBackend, - client: &Client, ) -> Result { let settings = query.settings(); let file_path = query.virtual_file_path(); @@ -140,14 +130,6 @@ fn format_text_document( let mut formatted = match formatted { FormatResult::Formatted(formatted) => formatted, FormatResult::Unchanged => return Ok(None), - FormatResult::PreviewOnly { file_format } => { - client.show_warning_message( - format_args!( - "{file_format} formatting is available only in preview mode. Enable `format.preview = true` in your Ruff configuration." - ), - ); - return Ok(None); - } }; // special case - avoid adding a newline to a notebook cell if it didn't already exist diff --git a/crates/ruff_server/src/server/api/requests/hover.rs b/crates/ruff_server/src/server/api/requests/hover.rs index 2b30e67040..5085a02c13 100644 --- a/crates/ruff_server/src/server/api/requests/hover.rs +++ b/crates/ruff_server/src/server/api/requests/hover.rs @@ -43,7 +43,7 @@ impl super::BackgroundDocumentRequestHandler for Hover { } } -pub(crate) fn hover( +fn hover( snapshot: &DocumentSnapshot, position: &types::TextDocumentPositionParams, ) -> Option { diff --git a/crates/ruff_server/src/server/main_loop.rs b/crates/ruff_server/src/server/main_loop.rs index 370e84c7f1..34357d22f5 100644 --- a/crates/ruff_server/src/server/main_loop.rs +++ b/crates/ruff_server/src/server/main_loop.rs @@ -3,6 +3,7 @@ use crossbeam::select; use lsp_server::Message; use lsp_types::{ self as types, DidChangeWatchedFilesRegistrationOptions, FileSystemWatcher, Notification as _, + Request as _, }; use crate::{ @@ -135,20 +136,27 @@ impl Server { } fn initialize(&mut self, client: &Client) { - let dynamic_registration = self + let supports_watched_files = 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(); + let supports_formatting = Self::supports_dynamic_formatting(&self.client_capabilities); + let supports_range_formatting = + Self::supports_dynamic_range_formatting(&self.client_capabilities); + let dynamic_registration = + supports_watched_files || supports_formatting || supports_range_formatting; + if dynamic_registration { // Register all dynamic capabilities here + let mut registrations = vec![]; - // `workspace/didChangeWatchedFiles` - // (this registers the configuration file watcher) - let params = lsp_types::RegistrationParams { - registrations: vec![lsp_types::Registration { + if supports_watched_files { + // `workspace/didChangeWatchedFiles` + // (this registers the configuration file watcher) + registrations.push(lsp_types::Registration { id: "ruff-server-watch".into(), method: "workspace/didChangeWatchedFiles".into(), register_options: Some( @@ -176,11 +184,71 @@ impl Server { }) .unwrap(), ), - }], - }; + }); + } + + if supports_formatting || supports_range_formatting { + let document_selector = vec![ + types::TextDocumentFilter::Language(types::TextDocumentFilterLanguage { + language: "python".to_string(), + scheme: None, + pattern: None, + }) + .into(), + types::TextDocumentFilter::Language(types::TextDocumentFilterLanguage { + language: "markdown".to_string(), + scheme: None, + pattern: None, + }) + .into(), + types::NotebookCellTextDocumentFilter { + notebook: "*".into(), + language: Some("python".into()), + } + .into(), + ]; + + let text_document_registration_options = types::TextDocumentRegistrationOptions { + document_selector: Some(document_selector), + }; + + if supports_formatting { + registrations.push(types::Registration { + id: "ruff-server-format".into(), + method: types::DocumentFormattingRequest::METHOD.to_string(), + register_options: Some( + serde_json::to_value(types::DocumentFormattingRegistrationOptions { + text_document_registration_options: + text_document_registration_options.clone(), + document_formatting_options: + types::DocumentFormattingOptions::default(), + }) + .unwrap(), + ), + }); + } + + if supports_range_formatting { + registrations.push(types::Registration { + id: "ruff-server-format-range".into(), + method: types::DocumentRangeFormattingRequest::METHOD.to_string(), + register_options: Some( + serde_json::to_value( + types::DocumentRangeFormattingRegistrationOptions { + text_document_registration_options, + document_range_formatting_options: + types::DocumentRangeFormattingOptions::default(), + }, + ) + .unwrap(), + ), + }); + } + } + let params = types::RegistrationParams { registrations }; let response_handler = |_: &Client, ()| { - tracing::info!("Configuration file watcher successfully registered"); + tracing::info!("Dynamic capabilities successfully registered"); }; if let Err(err) = client.send_request::( @@ -189,7 +257,7 @@ impl Server { response_handler, ) { tracing::error!( - "An error occurred when trying to register the configuration file watcher: {err}" + "An error occurred when trying to register dynamic capabilities: {err}" ); } } else { diff --git a/crates/ruff_server/src/server/schedule/thread/pool.rs b/crates/ruff_server/src/server/schedule/thread/pool.rs index ac3e072ab8..dcf33500f9 100644 --- a/crates/ruff_server/src/server/schedule/thread/pool.rs +++ b/crates/ruff_server/src/server/schedule/thread/pool.rs @@ -127,7 +127,7 @@ impl Pool { } #[expect(dead_code)] - pub(super) fn len(&self) -> usize { + fn len(&self) -> usize { self.extant_tasks.load(Ordering::SeqCst) } } diff --git a/crates/ruff_server/src/session/client.rs b/crates/ruff_server/src/session/client.rs index e2896e2e57..99316b290f 100644 --- a/crates/ruff_server/src/session/client.rs +++ b/crates/ruff_server/src/session/client.rs @@ -114,7 +114,7 @@ impl 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<()> { + fn send_notification_no_params(&self, method: &str) -> crate::Result<()> { self.client_sender .send(lsp_server::Message::Notification(Notification::new( method.to_string(), diff --git a/crates/ruff_server/src/session/index.rs b/crates/ruff_server/src/session/index.rs index 10c6cb4208..a5055ecb07 100644 --- a/crates/ruff_server/src/session/index.rs +++ b/crates/ruff_server/src/session/index.rs @@ -512,28 +512,28 @@ impl DocumentController { } } - pub(crate) fn as_notebook_mut(&mut self) -> Option<&mut NotebookDocument> { + 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> { + fn as_notebook(&self) -> Option<&NotebookDocument> { match self { Self::Notebook(notebook) => Some(notebook), Self::Text(_) => None, } } - pub(crate) fn as_text(&self) -> Option<&TextDocument> { + 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> { + fn as_text_mut(&mut self) -> Option<&mut TextDocument> { Some(match self { Self::Text(document) => Arc::make_mut(document), Self::Notebook(_) => return None, @@ -621,7 +621,7 @@ impl DocumentQuery { } /// Get the URI for the document selected by this query. - pub(crate) fn file_uri(&self) -> &Uri { + fn file_uri(&self) -> &Uri { match self { Self::Text { file_uri, .. } | Self::Notebook { file_uri, .. } => file_uri, } diff --git a/crates/ruff_server/src/session/index/ruff_settings.rs b/crates/ruff_server/src/session/index/ruff_settings.rs index 64e7fbfe91..d1ab2c317c 100644 --- a/crates/ruff_server/src/session/index/ruff_settings.rs +++ b/crates/ruff_server/src/session/index/ruff_settings.rs @@ -295,10 +295,14 @@ impl RuffSettingsIndex { return WalkState::Continue; } + let depth = entry.depth(); let directory = entry.into_path(); - // If the directory is excluded from the workspace, skip it. - if let Some(file_name) = directory.file_name() { + // An explicitly opened workspace root must be indexed even if an ancestor + // configuration excludes it. Excluded descendants can still be skipped. + if depth > 0 + && let Some(file_name) = directory.file_name() + { let settings = index .read() .unwrap() @@ -617,13 +621,13 @@ mod tests { let configuration = toml::from_str( r#" [lint.isort] - required-imports = ["from collections.abc import Set"] + required-imports = ["import numpy"] "#, )?; let editor_settings = EditorSettings { configuration: Some(ResolvedConfiguration::Inline(Box::new(configuration))), select: Some(vec![UnresolvedRuleSelector::new( - "PYI025", + "ICN001", ValueSource::Editor, )]), ..Default::default() @@ -636,7 +640,7 @@ mod tests { !settings .linter .rules - .enabled(Rule::UnaliasedCollectionsAbcSetImport) + .enabled(Rule::UnconventionalImportAlias) ); Ok(()) } diff --git a/crates/ruff_server/src/session/options.rs b/crates/ruff_server/src/session/options.rs index 476379e060..b1d2d8f2ed 100644 --- a/crates/ruff_server/src/session/options.rs +++ b/crates/ruff_server/src/session/options.rs @@ -58,12 +58,12 @@ pub(crate) struct GlobalOptions { } impl GlobalOptions { - pub(crate) fn set_preview(&mut self, preview: bool) { + fn set_preview(&mut self, preview: bool) { self.client.set_preview(preview); } #[cfg(test)] - pub(crate) fn client(&self) -> &ClientOptions { + fn client(&self) -> &ClientOptions { &self.client } @@ -169,7 +169,7 @@ impl ClientOptions { } /// Update the preview flag for the linter and the formatter with the given value. - pub(crate) fn set_preview(&mut self, preview: bool) { + 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), @@ -350,8 +350,14 @@ impl AllOptions { 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."); + 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(), ) diff --git a/crates/ruff_server/src/session/request_queue.rs b/crates/ruff_server/src/session/request_queue.rs index 68696050bf..3abd5d3a87 100644 --- a/crates/ruff_server/src/session/request_queue.rs +++ b/crates/ruff_server/src/session/request_queue.rs @@ -77,7 +77,7 @@ impl Incoming { /// 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 { + fn is_pending(&self, request_id: &RequestId) -> bool { self.pending.contains_key(request_id) } diff --git a/crates/ruff_server/src/session/settings.rs b/crates/ruff_server/src/session/settings.rs index 69f7e0b8a0..70151a4114 100644 --- a/crates/ruff_server/src/session/settings.rs +++ b/crates/ruff_server/src/session/settings.rs @@ -41,7 +41,8 @@ impl GlobalClientSettings { Ok(settings) => settings, Err(settings) => { self.client.show_error_message( - "Ruff received invalid settings from the editor. Refer to the logs for more information." + "Ruff received invalid settings from the editor. \ + Refer to the logs for more information.", ); settings } diff --git a/crates/ruff_server/src/workspace.rs b/crates/ruff_server/src/workspace.rs index 056a080992..75d311494b 100644 --- a/crates/ruff_server/src/workspace.rs +++ b/crates/ruff_server/src/workspace.rs @@ -91,7 +91,7 @@ impl Workspace { } /// Create a new default workspace with the given root URI. - pub(crate) fn default(uri: Uri) -> Self { + fn default(uri: Uri) -> Self { Self { uri, options: None, @@ -101,7 +101,7 @@ impl Workspace { /// Set the client options for this workspace. #[must_use] - pub(crate) fn with_options(mut self, options: ClientOptions) -> Self { + fn with_options(mut self, options: ClientOptions) -> Self { self.options = Some(options); self } diff --git a/crates/ruff_server/tests/e2e/capabilities.rs b/crates/ruff_server/tests/e2e/capabilities.rs new file mode 100644 index 0000000000..fe47aac6ec --- /dev/null +++ b/crates/ruff_server/tests/e2e/capabilities.rs @@ -0,0 +1,77 @@ +use anyhow::Result; +use insta::assert_json_snapshot; +use lsp_types::Request as _; +use lsp_types::{DocumentFormattingRequest, DocumentRangeFormattingRequest, RegistrationRequest}; + +use crate::TestServerBuilder; + +#[test] +fn statically_registers_formatting_when_dynamic_registration_is_unsupported() -> Result<()> { + let server = TestServerBuilder::new()?.build(); + let capabilities = &server + .initialization_result() + .expect("Server should return initialization capabilities") + .capabilities; + + assert_eq!(capabilities.document_formatting_provider, Some(true.into())); + assert_eq!( + capabilities.document_range_formatting_provider, + Some(true.into()) + ); + + Ok(()) +} + +#[test] +fn dynamically_registers_formatting_and_range_formatting_for_python_and_markdown() -> Result<()> { + let mut server = TestServerBuilder::new()? + .enable_formatting_dynamic_registration(true) + .enable_range_formatting_dynamic_registration(true) + .build(); + let capabilities = &server + .initialization_result() + .expect("Server should return initialization capabilities") + .capabilities; + + assert_eq!(capabilities.document_formatting_provider, None); + assert_eq!(capabilities.document_range_formatting_provider, None); + + let (_, params) = server.await_request::(); + let [formatting, range_formatting] = params.registrations.as_slice() else { + panic!("Expected both dynamic formatting registrations"); + }; + + assert_eq!( + formatting.method, + DocumentFormattingRequest::METHOD.as_str() + ); + assert_eq!( + range_formatting.method, + DocumentRangeFormattingRequest::METHOD.as_str() + ); + assert_json_snapshot!( + formatting.register_options, + @r#" + { + "documentSelector": [ + { + "language": "python" + }, + { + "language": "markdown" + }, + { + "language": "python", + "notebook": "*" + } + ] + } + "# + ); + assert_eq!( + range_formatting.register_options, + formatting.register_options + ); + + Ok(()) +} diff --git a/crates/ruff_server/tests/e2e/code_action.rs b/crates/ruff_server/tests/e2e/code_action.rs index 68c43ee24a..11f8a3c92a 100644 --- a/crates/ruff_server/tests/e2e/code_action.rs +++ b/crates/ruff_server/tests/e2e/code_action.rs @@ -81,6 +81,110 @@ fn code_actions_for_python() -> Result<()> { Ok(()) } +#[test] +fn code_actions_for_toml() -> Result<()> { + let source = r#" +[lint] +preview = true +select = ["rule-codes-in-selectors"] +extend-select = ["F401"] +"#; + let mut server = TestServerBuilder::new()? + .with_workspace(".")? + .with_file("ruff.toml", source)? + .build(); + + server.open_text_document_with_language_id("ruff.toml", "toml", source, 1); + + let diagnostics = match server.document_diagnostic_request("ruff.toml", None) { + DocumentDiagnosticReport::RelatedFullDocumentDiagnosticReport(report) => { + report.full_document_diagnostic_report.items + } + DocumentDiagnosticReport::RelatedUnchangedDocumentDiagnosticReport(_) => { + panic!("Expected a full diagnostic report"); + } + }; + let actions = server + .code_action_request("ruff.toml", diagnostics) + .expect("Expected code actions"); + + assert_json_snapshot!(actions, @r#" + [ + { + "title": "Ruff (rule-codes-in-selectors): Replace rule code with `unused-import`", + "kind": "quickfix", + "diagnostics": [ + { + "range": { + "start": { + "line": 4, + "character": 18 + }, + "end": { + "line": 4, + "character": 22 + } + }, + "severity": 2, + "code": "rule-codes-in-selectors", + "codeDescription": { + "href": "https://kotlinisland.github.io/basedpython/rules/rule-codes-in-selectors" + }, + "source": "Ruff", + "message": "Rule code used instead of name in `lint.extend-select`\n\nhelp: Replace rule code with `unused-import`", + "tags": [] + } + ], + "edit": { + "changes": { + "file:///ruff.toml": [ + { + "range": { + "start": { + "line": 4, + "character": 18 + }, + "end": { + "line": 4, + "character": 22 + } + }, + "newText": "unused-import" + } + ] + } + }, + "data": "file:///ruff.toml" + }, + { + "title": "Ruff: Fix all auto-fixable problems", + "kind": "source.fixAll.ruff", + "edit": { + "changes": { + "file:///ruff.toml": [ + { + "range": { + "start": { + "line": 4, + "character": 0 + }, + "end": { + "line": 5, + "character": 0 + } + }, + "newText": "extend-select = [\"unused-import\"]\n" + } + ] + } + } + } + ] + "#); + + Ok(()) +} + #[test] fn human_readable_rule_names() -> Result<()> { let mut server = TestServerBuilder::new()? diff --git a/crates/ruff_server/tests/e2e/custom_extension.rs b/crates/ruff_server/tests/e2e/custom_extension.rs index 3d9ebad714..87ad69ba91 100644 --- a/crates/ruff_server/tests/e2e/custom_extension.rs +++ b/crates/ruff_server/tests/e2e/custom_extension.rs @@ -5,11 +5,7 @@ use lsp_types::{Position, Range}; use crate::TestServerBuilder; const CUSTOM_EXTENSION_CONFIG: &str = r#"[tool.ruff] -preview = true extension = { thing = "markdown" } - -[tool.ruff.format] -preview = true "#; const CUSTOM_EXTENSION_MARKDOWN: &str = "# title\n\n```python\nx='hi'\n```\n"; diff --git a/crates/ruff_server/tests/e2e/diagnostics.rs b/crates/ruff_server/tests/e2e/diagnostics.rs index 0165c34379..a3f95fb4eb 100644 --- a/crates/ruff_server/tests/e2e/diagnostics.rs +++ b/crates/ruff_server/tests/e2e/diagnostics.rs @@ -58,7 +58,7 @@ fn uses_human_readable_names_in_preview() -> Result<()> { } ], "noqa_edit": { - "newText": " # ruff:ignore[unused-import]\n", + "newText": " # ruff: ignore[unused-import]\n", "range": { "end": { "character": 0, @@ -81,3 +81,111 @@ fn uses_human_readable_names_in_preview() -> Result<()> { Ok(()) } + +#[test] +fn toml_diagnostics() -> Result<()> { + let source = r#" +[lint] +preview = true +select = ["rule-codes-in-selectors"] +extend-select = ["F401"] +"#; + let mut server = TestServerBuilder::new()? + .with_workspace(".")? + .with_file("ruff.toml", source)? + .build(); + + server.open_text_document_with_language_id("ruff.toml", "toml", source, 1); + + let diagnostics = server.document_diagnostic_request("ruff.toml", None); + + assert_json_snapshot!(diagnostics, @r#" + { + "items": [ + { + "range": { + "start": { + "line": 4, + "character": 18 + }, + "end": { + "line": 4, + "character": 22 + } + }, + "severity": 2, + "code": "rule-codes-in-selectors", + "codeDescription": { + "href": "https://kotlinisland.github.io/basedpython/rules/rule-codes-in-selectors" + }, + "source": "Ruff", + "message": "Rule code used instead of name in `lint.extend-select`\n\nhelp: Replace rule code with `unused-import`", + "tags": [], + "data": { + "code": "rule-codes-in-selectors", + "edits": [ + { + "newText": "unused-import", + "range": { + "end": { + "character": 22, + "line": 4 + }, + "start": { + "character": 18, + "line": 4 + } + } + } + ], + "noqa_edit": null, + "title": "Replace rule code with `unused-import`" + } + } + ], + "kind": "full" + } + "#); + + Ok(()) +} + +#[test] +fn invalid_pyproject_toml_diagnostic() -> Result<()> { + let source = "[project]\nname = 1\n"; + let mut server = TestServerBuilder::new()?.with_workspace(".")?.build(); + + server.open_text_document_with_language_id("pyproject.toml", "toml", source, 1); + + let diagnostics = server.document_diagnostic_request("pyproject.toml", None); + + assert_json_snapshot!(diagnostics, @r#" + { + "items": [ + { + "range": { + "start": { + "line": 1, + "character": 7 + }, + "end": { + "line": 1, + "character": 8 + } + }, + "severity": 2, + "code": "RUF200", + "codeDescription": { + "href": "https://kotlinisland.github.io/basedpython/rules/invalid-pyproject-toml" + }, + "source": "Ruff", + "message": "Failed to parse pyproject.toml: invalid type: integer `1`, expected a string", + "tags": [] + } + ], + "kind": "full" + } + "#); + + Ok(()) +} diff --git a/crates/ruff_server/tests/e2e/main.rs b/crates/ruff_server/tests/e2e/main.rs index 6349ed3c0c..d8e53ee40d 100644 --- a/crates/ruff_server/tests/e2e/main.rs +++ b/crates/ruff_server/tests/e2e/main.rs @@ -25,6 +25,7 @@ //! [`await_request`]: TestServer::await_request //! [`await_notification`]: TestServer::await_notification +mod capabilities; mod code_action; mod custom_extension; mod diagnostics; @@ -530,7 +531,6 @@ impl TestServer { /// /// If receiving the request fails. #[track_caller] - #[expect(dead_code)] pub(crate) fn await_request(&mut self) -> (RequestId, R::Params) { match self.try_await_request::(None) { Ok(result) => result, @@ -653,7 +653,6 @@ impl TestServer { } /// Get the initialization result - #[expect(dead_code)] pub(crate) fn initialization_result(&self) -> Option<&InitializeResult> { self.initialize_response.as_ref() } @@ -1062,6 +1061,26 @@ impl TestServerBuilder { self } + pub(crate) fn enable_formatting_dynamic_registration(mut self, enabled: bool) -> Self { + self.client_capabilities + .text_document + .get_or_insert_default() + .formatting + .get_or_insert_default() + .dynamic_registration = Some(enabled); + self + } + + pub(crate) fn enable_range_formatting_dynamic_registration(mut self, enabled: bool) -> Self { + self.client_capabilities + .text_document + .get_or_insert_default() + .range_formatting + .get_or_insert_default() + .dynamic_registration = Some(enabled); + self + } + /// Enable or disable workspace configuration capability #[expect(dead_code)] pub(crate) fn enable_workspace_configuration(mut self, enabled: bool) -> Self { diff --git a/crates/ruff_server/tests/e2e/workspace.rs b/crates/ruff_server/tests/e2e/workspace.rs index aad0ba4296..60f695127e 100644 --- a/crates/ruff_server/tests/e2e/workspace.rs +++ b/crates/ruff_server/tests/e2e/workspace.rs @@ -1,7 +1,9 @@ -use anyhow::Result; -use insta::assert_json_snapshot; +use anyhow::{Context, Result}; +use insta::{assert_json_snapshot, assert_snapshot}; -use crate::TestServerBuilder; +use crate::{TestServer, TestServerBuilder}; + +const SOURCE: &str = "value= \"hello\"\n"; #[test] fn selects_the_correct_workspace_settings_for_multi_root_workspaces() -> Result<()> { @@ -104,6 +106,181 @@ ignore = ["F401"] Ok(()) } +#[test] +fn nested_workspace_root_is_not_excluded_by_an_ancestor() -> Result<()> { + let mut server = nested_workspace_server(&["sub"], WorkspaceExclusion::Exclude)?; + + assert_snapshot!( + open_and_format(&mut server, "sub/test.py", SOURCE) + .context("nested workspace should be formatted")?, + @"value = 'hello'" + ); + // Explicitly opening `sub` does not override its own exclusion of `foo`. + assert!(open_and_format(&mut server, "sub/foo/test.py", SOURCE).is_none()); + + Ok(()) +} + +#[test] +fn nested_workspace_root_is_not_excluded_by_an_ancestor_in_a_multi_root_workspace() -> Result<()> { + const ISSUE_SOURCE: &str = r#"print("This line is long enough to wrap.") +"#; + + let mut server = TestServerBuilder::new()? + .with_workspace(".")? + .with_workspace("sub")? + .with_file( + ".ruff.toml", + r#"target-version = "py312" +line-length = 40 + +extend-exclude = [ + "sub", +] +"#, + )? + .with_file( + "sub/.ruff.toml", + r#"target-version = "py312" +line-length = 40 + +extend-exclude = [ + "foo", +] +"#, + )? + .with_file("test.py", ISSUE_SOURCE)? + .with_file("sub/test.py", ISSUE_SOURCE)? + .with_file("sub/foo/test.py", ISSUE_SOURCE)? + .build(); + + assert_snapshot!( + open_and_format(&mut server, "test.py", ISSUE_SOURCE) + .context("parent workspace should be formatted")?, + @r#" + print( + "This line is long enough to wrap." + ) + "# + ); + assert_snapshot!( + open_and_format(&mut server, "sub/test.py", ISSUE_SOURCE) + .context("nested workspace should be formatted")?, + @r#" + print( + "This line is long enough to wrap." + ) + "# + ); + assert!(open_and_format(&mut server, "sub/foo/test.py", ISSUE_SOURCE).is_none()); + + Ok(()) +} + +#[test] +fn nested_workspace_remains_excluded_without_explicit_registration() -> Result<()> { + let mut server = nested_workspace_server(&["."], WorkspaceExclusion::ExtendExclude)?; + + assert!(open_and_format(&mut server, "sub/test.py", SOURCE).is_none()); + assert!(open_and_format(&mut server, "sub/foo/test.py", SOURCE).is_none()); + + Ok(()) +} + +#[test] +fn unrelated_file_outside_workspace_uses_fallback_configuration() -> Result<()> { + let mut server = nested_workspace_server(&["sub"], WorkspaceExclusion::ExtendExclude)?; + + assert_snapshot!( + open_and_format(&mut server, "unrelated/test.py", SOURCE) + .context("unrelated file should use fallback formatting")?, + @r#"value = "hello""# + ); + + Ok(()) +} + +#[test] +fn single_file_mode_does_not_index_nested_configuration() -> Result<()> { + let mut server = TestServerBuilder::new()? + .with_file("nested/.ruff.toml", "[format]\nquote-style = \"single\"\n")? + .with_file("nested/test.py", SOURCE)? + .with_file("unrelated/test.py", SOURCE)? + .build(); + + assert_snapshot!( + open_and_format(&mut server, "nested/test.py", SOURCE) + .context("nested file should use fallback formatting")?, + @r#"value = "hello""# + ); + assert_snapshot!( + open_and_format(&mut server, "unrelated/test.py", SOURCE) + .context("unrelated file should use fallback formatting")?, + @r#"value = "hello""# + ); + + Ok(()) +} + +#[derive(Clone, Copy)] +enum WorkspaceExclusion { + Exclude, + ExtendExclude, +} + +/// Creates a test server for the following temporary workspace: +/// +/// ```text +/// / +/// ├── .ruff.toml # exclude or extend-exclude = ["sub"] +/// ├── test.py +/// ├── sub/ +/// │ ├── .ruff.toml # extend-exclude = ["foo"] +/// │ │ # format.quote-style = "single" +/// │ ├── test.py +/// │ └── foo/ +/// │ └── test.py +/// └── unrelated/ +/// └── test.py +/// ``` +fn nested_workspace_server( + workspaces: &[&str], + exclusion: WorkspaceExclusion, +) -> Result { + let mut builder = TestServerBuilder::new()?; + for workspace in workspaces { + builder = builder.with_workspace(workspace)?; + } + + let server = builder + .with_file( + ".ruff.toml", + match exclusion { + WorkspaceExclusion::Exclude => "exclude = [\"sub\"]\n", + WorkspaceExclusion::ExtendExclude => "extend-exclude = [\"sub\"]\n", + }, + )? + .with_file( + "sub/.ruff.toml", + "extend-exclude = [\"foo\"]\n[format]\nquote-style = \"single\"\n", + )? + .with_file("test.py", SOURCE)? + .with_file("sub/test.py", SOURCE)? + .with_file("sub/foo/test.py", SOURCE)? + .with_file("unrelated/test.py", SOURCE)? + .build(); + + Ok(server) +} + +fn open_and_format(server: &mut TestServer, path: &str, source: &str) -> Option { + server.open_text_document(path, source, 1); + server + .format_request(path) + .and_then(|edits| edits.into_iter().next()) + .map(|edit| edit.new_text) +} + #[test] fn unavailable_document_diagnostic_returns_empty_response() -> Result<()> { let mut server = TestServerBuilder::new()?.with_workspace(".")?.build(); diff --git a/crates/ruff_source_file/Cargo.toml b/crates/ruff_source_file/Cargo.toml index b713313d5a..e918a959a0 100644 --- a/crates/ruff_source_file/Cargo.toml +++ b/crates/ruff_source_file/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_source_file" -version = "0.0.5" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_source_file/README.md b/crates/ruff_source_file/README.md index 693ee2da7a..2d61cd4f33 100644 --- a/crates/ruff_source_file/README.md +++ b/crates/ruff_source_file/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_source_file). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_source_file). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_source_file/src/lib.rs b/crates/ruff_source_file/src/lib.rs index 64121dac56..23f07de888 100644 --- a/crates/ruff_source_file/src/lib.rs +++ b/crates/ruff_source_file/src/lib.rs @@ -6,7 +6,7 @@ use std::sync::{Arc, OnceLock}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use ruff_text_size::{Ranged, TextRange, TextSize}; +use ruff_text_size::{Ranged, TextSize}; pub use crate::line_index::{LineIndex, OneIndexed, PositionEncoding}; pub use crate::line_ranges::LineRanges; @@ -55,19 +55,7 @@ impl<'src, 'index> SourceCode<'src, 'index> { self.index.line_index(offset) } - /// Take the source code up to the given [`TextSize`]. - #[inline] - pub fn up_to(&self, offset: TextSize) -> &'src str { - &self.text[TextRange::up_to(offset)] - } - - /// Take the source code after the given [`TextSize`]. - #[inline] - pub fn after(&self, offset: TextSize) -> &'src str { - &self.text[usize::from(offset)..] - } - - /// Take the source code between the given [`TextRange`]. + /// Take the source code between the given [`ruff_text_size::TextRange`]. pub fn slice(&self, ranged: T) -> &'src str { &self.text[ranged.range()] } @@ -84,10 +72,6 @@ impl<'src, 'index> SourceCode<'src, 'index> { self.index.line_end_exclusive(line, self.text) } - pub fn line_range(&self, line: OneIndexed) -> TextRange { - self.index.line_range(line, self.text) - } - /// Returns the source text of the line with the given index #[inline] pub fn line_text(&self, index: OneIndexed) -> &'src str { @@ -132,16 +116,6 @@ impl SourceFileBuilder { } } - #[must_use] - pub fn line_index(mut self, index: LineIndex) -> Self { - self.index = Some(index); - self - } - - pub fn set_line_index(&mut self, index: LineIndex) { - self.index = Some(index); - } - /// Consumes `self` and returns the [`SourceFile`]. pub fn finish(self) -> SourceFile { let index = if let Some(index) = self.index { @@ -185,11 +159,6 @@ impl SourceFile { &self.inner.name } - #[inline] - pub fn slice(&self, range: TextRange) -> &str { - &self.source_text()[range] - } - pub fn to_source_code(&self) -> SourceCode<'_, '_> { SourceCode { text: self.source_text(), diff --git a/crates/ruff_source_file/src/line_index.rs b/crates/ruff_source_file/src/line_index.rs index cf4d85c76e..6968839b71 100644 --- a/crates/ruff_source_file/src/line_index.rs +++ b/crates/ruff_source_file/src/line_index.rs @@ -224,7 +224,7 @@ impl LineIndex { } /// Returns `true` if the text only consists of ASCII characters - pub fn is_ascii(&self) -> bool { + fn is_ascii(&self) -> bool { self.kind().is_ascii() } @@ -286,7 +286,7 @@ impl LineIndex { /// Returns the [byte offset](TextSize) of the `line`'s end. /// The offset is the end of the line, excluding the newline character ending the line (if any). - pub fn line_end_exclusive(&self, line: OneIndexed, contents: &str) -> TextSize { + pub(crate) fn line_end_exclusive(&self, line: OneIndexed, contents: &str) -> TextSize { let row_index = line.to_zero_indexed(); let starts = self.line_starts(); @@ -580,7 +580,7 @@ impl OneIndexed { // SAFETY: These constants are being initialized with non-zero values /// The smallest value that can be represented by this integer type. pub const MIN: Self = Self::new(1).unwrap(); - pub const ONE: NonZeroUsize = NonZeroUsize::new(1).unwrap(); + const ONE: NonZeroUsize = NonZeroUsize::new(1).unwrap(); /// Creates a non-zero if the given value is not zero. pub const fn new(value: usize) -> Option { diff --git a/crates/ruff_source_file/src/newlines.rs b/crates/ruff_source_file/src/newlines.rs index 1078750b35..50b6111fe1 100644 --- a/crates/ruff_source_file/src/newlines.rs +++ b/crates/ruff_source_file/src/newlines.rs @@ -269,7 +269,7 @@ impl<'a> Line<'a> { } #[inline] - pub fn full_text_len(&self) -> TextSize { + fn full_text_len(&self) -> TextSize { self.text.text_len() } } diff --git a/crates/ruff_text_size/Cargo.toml b/crates/ruff_text_size/Cargo.toml index fcd8305cde..3ad9d4da5f 100644 --- a/crates/ruff_text_size/Cargo.toml +++ b/crates/ruff_text_size/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_text_size" -version = "0.0.5" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_text_size/README.md b/crates/ruff_text_size/README.md index 21887ec343..e14fefa0f7 100644 --- a/crates/ruff_text_size/README.md +++ b/crates/ruff_text_size/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_text_size). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_text_size). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_wasm/Cargo.toml b/crates/ruff_wasm/Cargo.toml index f7c7ac1871..1e3b3000f7 100644 --- a/crates/ruff_wasm/Cargo.toml +++ b/crates/ruff_wasm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_wasm" -version = "0.15.22" +version = "0.16.2" description = "WebAssembly bindings for Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_wasm/README.md b/crates/ruff_wasm/README.md index b58015c3f6..29b092ebfd 100644 --- a/crates/ruff_wasm/README.md +++ b/crates/ruff_wasm/README.md @@ -25,7 +25,7 @@ const exampleDocument = `print('hello'); print("world")`; await init(); // Initializes WASM module -// These are default settings just to illustrate configuring Ruff +// These settings illustrate configuring Ruff // Settings info: https://docs.astral.sh/ruff/settings const workspace = new Workspace( { @@ -55,8 +55,8 @@ const formatted = workspace.format(exampleDocument); This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.15.22) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_wasm). +This version (0.16.2) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_wasm). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_wasm/src/lib.rs b/crates/ruff_wasm/src/lib.rs index 06d5a9cbe0..6ed24e4eea 100644 --- a/crates/ruff_wasm/src/lib.rs +++ b/crates/ruff_wasm/src/lib.rs @@ -430,7 +430,8 @@ impl Workspace { }) .collect(); - let code = if !is_human_readable_names_enabled(self.settings.linter.preview) + let code = if (!is_human_readable_names_enabled(self.settings.linter.preview) + || self.settings.output_prefer_rule_codes) && let Some(code) = msg.secondary_code() { code.as_str() diff --git a/crates/ruff_workspace/Cargo.toml b/crates/ruff_workspace/Cargo.toml index 7aaae335ca..2ad0e6af61 100644 --- a/crates/ruff_workspace/Cargo.toml +++ b/crates/ruff_workspace/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_workspace" -version = "0.0.5" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_workspace/README.md b/crates/ruff_workspace/README.md index 6a9c1eb472..996e2998cb 100644 --- a/crates/ruff_workspace/README.md +++ b/crates/ruff_workspace/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_workspace). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_workspace). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index 279a3d543f..468a56c056 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -34,8 +34,7 @@ use ruff_linter::settings::types::{ RequiredVersion, UnsafeFixes, }; use ruff_linter::settings::{ - DEFAULT_SELECTORS, DUMMY_VARIABLE_RGX, LinterSettings, PREVIEW_DEFAULT_SELECTORS, TASK_TAGS, - TargetVersion, + DEFAULT_SELECTORS, DUMMY_VARIABLE_RGX, LinterSettings, TASK_TAGS, TargetVersion, }; use ruff_linter::{ RuleSelector, UnresolvedRuleSelector, fs, warn_user_once, warn_user_once_by_id, @@ -180,6 +179,7 @@ pub struct Configuration { pub fix_only: Option, pub unsafe_fixes: Option, pub output_format: Option, + pub output_prefer_rule_codes: Option, pub preview: Option, pub required_version: Option, pub extension: Option, @@ -327,6 +327,7 @@ impl Configuration { fix_only: self.fix_only.unwrap_or(false), unsafe_fixes: self.unsafe_fixes.unwrap_or_default(), output_format: self.output_format.unwrap_or_default(), + output_prefer_rule_codes: self.output_prefer_rule_codes.unwrap_or_default(), show_fixes: self.show_fixes.unwrap_or(false), file_resolver: FileResolverSettings { @@ -336,24 +337,20 @@ impl Configuration { extend_exclude: FilePatternSet::try_from_iter(self.extend_exclude)?, extend_include: FilePatternSet::try_from_iter(self.extend_include)?, force_exclude: self.force_exclude.unwrap_or(false), - include: match global_preview { - PreviewMode::Disabled => FilePatternSet::try_from_iter( - self.include.unwrap_or_else(|| INCLUDE.to_vec()), - )?, - PreviewMode::Enabled => { - FilePatternSet::try_from_iter(self.include.unwrap_or_else(|| { - let mut patterns = INCLUDE_PREVIEW.to_vec(); - if let Some(extension_map) = &self.extension { - patterns.extend( - extension_map - .extensions() - .map(|ext| FilePattern::Config(format!("*.{ext}"))), - ); - } - patterns - }))? + include: FilePatternSet::try_from_iter(self.include.unwrap_or_else(|| { + let mut patterns = match global_preview { + PreviewMode::Disabled => INCLUDE.to_vec(), + PreviewMode::Enabled => INCLUDE_PREVIEW.to_vec(), + }; + if let Some(extension_map) = &self.extension { + patterns.extend( + extension_map + .extensions() + .map(|ext| FilePattern::Config(format!("*.{ext}"))), + ); } - }, + patterns + }))?, respect_gitignore: self.respect_gitignore.unwrap_or(true), project_root: project_root.to_path_buf(), }, @@ -610,6 +607,7 @@ impl Configuration { fix_only: options.fix_only, unsafe_fixes: options.unsafe_fixes.map(UnsafeFixes::from), output_format: options.output_format, + output_prefer_rule_codes: options.output_prefer_rule_codes, force_exclude: options.force_exclude, line_length: options.line_length, indent_width: options.indent_width, @@ -673,6 +671,9 @@ impl Configuration { fix_only: self.fix_only.or(config.fix_only), unsafe_fixes: self.unsafe_fixes.or(config.unsafe_fixes), output_format: self.output_format.or(config.output_format), + output_prefer_rule_codes: self + .output_prefer_rule_codes + .or(config.output_prefer_rule_codes), force_exclude: self.force_exclude.or(config.force_exclude), line_length: self.line_length.or(config.line_length), indent_width: self.indent_width.or(config.indent_width), @@ -695,7 +696,7 @@ impl Configuration { } #[must_use] - pub fn apply_fallbacks( + pub(crate) fn apply_fallbacks( mut self, origin: ConfigurationOrigin, initial_config_path: &Path, @@ -801,7 +802,11 @@ impl LintConfiguration { 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." + "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 @@ -902,14 +907,8 @@ impl LintConfiguration { require_explicit: self.explicit_preview_rules.unwrap_or_default(), }; - let selectors = if preview.mode.is_enabled() { - PREVIEW_DEFAULT_SELECTORS - } else { - DEFAULT_SELECTORS - }; - // The select_set keeps track of which rules have been selected. - let mut select_set: RuleSet = selectors + let mut select_set: RuleSet = DEFAULT_SELECTORS .iter() .flat_map(|selector| selector.rules(&preview)) .collect(); @@ -1180,11 +1179,15 @@ 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." + "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(); + let mut message = "\ + Selection of deprecated rules is not allowed \ + when preview is enabled. Remove selection of:" + .to_string(); for selection in deprecated_selectors { let (prefix, code) = selection.prefix_and_code(); message.push_str("\n\t- "); @@ -1238,7 +1241,7 @@ impl LintConfiguration { } #[must_use] - pub fn combine(self, config: Self) -> Self { + fn combine(self, config: Self) -> Self { let mut rule_selections = config.rule_selections; rule_selections.extend(self.rule_selections); @@ -1372,7 +1375,7 @@ impl FormatConfiguration { } #[must_use] - pub fn combine(self, config: Self) -> Self { + fn combine(self, config: Self) -> Self { Self { exclude: self.exclude.or(config.exclude), preview: self.preview.or(config.preview), @@ -1434,7 +1437,7 @@ impl AnalyzeConfiguration { } #[must_use] - pub fn combine(self, config: Self) -> Self { + fn combine(self, config: Self) -> Self { Self { exclude: self.exclude.or(config.exclude), preview: self.preview.or(config.preview), @@ -1467,7 +1470,7 @@ impl CombinePluginOptions for Option { /// Given a list of source paths, which could include glob patterns, resolve the /// matching paths. -pub fn resolve_src(src: &[String], project_root: &Path) -> Result> { +fn resolve_src(src: &[String], project_root: &Path) -> Result> { let expansions = src .iter() .map(shellexpand::full) @@ -1734,8 +1737,10 @@ fn warn_about_deprecated_top_level_lint_options( ); warn_user_once_by_message!( - "The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. \ - Please update the following options in {thing_to_update}:\n {options_mapping}", + "The top-level linter settings are deprecated \ + in favour of their counterparts in the `lint` section. \ + Please update the following options in {thing_to_update}:\n \ + {options_mapping}", ); } @@ -2122,7 +2127,7 @@ mod tests { fn select_linter_preview() -> Result<()> { let actual = resolve_rules( [RuleSelection { - select: Some(vec![UnresolvedRuleSelector::cli("CPY")]), + select: Some(vec![UnresolvedRuleSelector::cli("RUF91")]), ..RuleSelection::default() }], Some(PreviewOptions { @@ -2135,7 +2140,7 @@ mod tests { let actual = resolve_rules( [RuleSelection { - select: Some(vec![UnresolvedRuleSelector::cli("CPY")]), + select: Some(vec![UnresolvedRuleSelector::cli("RUF91")]), ..RuleSelection::default() }], Some(PreviewOptions { @@ -2143,7 +2148,7 @@ mod tests { ..PreviewOptions::default() }), )?; - let expected = RuleSet::from_rule(Rule::MissingCopyrightNotice); + let expected = RuleSet::from_rule(Rule::PreviewTestRule); assert_eq!(actual, expected); Ok(()) } @@ -2152,7 +2157,7 @@ mod tests { fn select_prefix_preview() -> Result<()> { let actual = resolve_rules( [RuleSelection { - select: Some(vec![UnresolvedRuleSelector::cli("CPY0")]), + select: Some(vec![UnresolvedRuleSelector::cli("RUF91")]), ..RuleSelection::default() }], Some(PreviewOptions { @@ -2165,7 +2170,7 @@ mod tests { let actual = resolve_rules( [RuleSelection { - select: Some(vec![UnresolvedRuleSelector::cli("CPY0")]), + select: Some(vec![UnresolvedRuleSelector::cli("RUF91")]), ..RuleSelection::default() }], Some(PreviewOptions { @@ -2173,7 +2178,7 @@ mod tests { ..PreviewOptions::default() }), )?; - let expected = RuleSet::from_rule(Rule::MissingCopyrightNotice); + let expected = RuleSet::from_rule(Rule::PreviewTestRule); assert_eq!(actual, expected); Ok(()) } diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index 9c3241fd66..9ec56bce5b 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -107,6 +107,30 @@ pub struct Options { )] pub output_format: Option, + /// Whether to prefer rule codes over human-readable rule names in diagnostic output, even + /// when preview mode is enabled. + /// + /// Diagnostics without rule codes, such as syntax errors and formatting diagnostics, will + /// continue to use the human-readable name, but those corresponding to lint rules will use the + /// rule's code. For example, the concise diagnostic for an unused import will use the code + /// `F401` instead of the name `unused-import`: + /// + /// ```console + /// $ ruff check --preview --config 'output-prefer-rule-codes = true' --output-format=concise example.py + /// example.py:1:8: F401 [*] `math` imported but unused + /// $ ruff check --preview --config 'output-prefer-rule-codes = false' --output-format=concise example.py + /// example.py:1:8: unused-import: [*] `math` imported but unused + /// ``` + #[option( + default = "false", + value_type = "bool", + example = r#" + # Display rule codes instead of human-readable rule names. + output-prefer-rule-codes = true + "# + )] + pub output_prefer_rule_codes: Option, + /// Enable fix behavior by-default when running `ruff` (overridden /// by the `--fix` and `--no-fix` command-line flags). /// Only includes automatic fixes unless `--unsafe-fixes` is provided. @@ -582,12 +606,13 @@ pub struct LintOptions { pub future_annotations: Option, } -pub fn validate_required_version(required_version: &RequiredVersion) -> anyhow::Result<()> { +pub(crate) fn validate_required_version(required_version: &RequiredVersion) -> anyhow::Result<()> { let ruff_pkg_version = pep440_rs::Version::from_str(RUFF_PKG_VERSION) .expect("RUFF_PKG_VERSION is not a valid PEP 440 version specifier"); if !required_version.contains(&ruff_pkg_version) { return Err(anyhow::anyhow!( - "Required version `{required_version}` does not match the running version `{RUFF_PKG_VERSION}`" + "Required version `{required_version}` does not match the running version \ + `{RUFF_PKG_VERSION}`" )); } Ok(()) @@ -596,7 +621,7 @@ pub fn validate_required_version(required_version: &RequiredVersion) -> anyhow:: /// Newtype wrapper for [`LintCommonOptions`] that allows customizing the JSON schema and omitting the fields from the [`OptionsMetadata`]. #[derive(Clone, Debug, PartialEq, Eq, Default, Serialize)] #[serde(transparent)] -pub struct DeprecatedTopLevelLintOptions(pub LintCommonOptions); +pub struct DeprecatedTopLevelLintOptions(pub(crate) LintCommonOptions); impl<'de> Deserialize<'de> for DeprecatedTopLevelLintOptions { fn deserialize(deserializer: D) -> Result @@ -706,9 +731,9 @@ pub struct LintCommonOptions { extend-ignore = ["F841"] "# )] - #[deprecated( - note = "The `extend-ignore` option is now interchangeable with [`ignore`](#lint_ignore). Please update your configuration to use the [`ignore`](#lint_ignore) option instead." - )] + #[deprecated(note = "The `extend-ignore` option is now interchangeable with \ + [`ignore`](#lint_ignore). Please update your configuration to use the \ + [`ignore`](#lint_ignore) option instead.")] pub extend_ignore: Option>, /// A list of rule codes or prefixes to enable, in addition to those @@ -724,7 +749,7 @@ pub struct LintCommonOptions { /// /// ```toml /// [tool.ruff.lint] - /// # Adds flake8-bugbear on top of the default rules (E4, E7, E9, F). + /// # Adds flake8-bugbear on top of the default rules. /// extend-select = ["B"] /// ``` /// @@ -734,7 +759,7 @@ pub struct LintCommonOptions { default = "[]", value_type = "list[RuleSelector]", example = r#" - # On top of the default `select` (`E4`, E7`, `E9`, and `F`), enable flake8-bugbear (`B`) and flake8-quotes (`Q`). + # On top of the default `select`, enable flake8-bugbear (`B`) and flake8-quotes (`Q`). extend-select = ["B", "Q"] "# )] @@ -754,9 +779,9 @@ pub struct LintCommonOptions { /// A list of rule codes or prefixes to consider non-auto-fixable, in addition to those /// specified by [`unfixable`](#lint_unfixable). - #[deprecated( - note = "The `extend-unfixable` option is now interchangeable with [`unfixable`](#lint_unfixable). Please update your configuration to use the `unfixable` option instead." - )] + #[deprecated(note = "The `extend-unfixable` option is now interchangeable with \ + [`unfixable`](#lint_unfixable). Please update your configuration to \ + use the `unfixable` option instead.")] pub extend_unfixable: Option>, /// A list of rule codes or prefixes that are unsupported by Ruff, but should be @@ -844,7 +869,10 @@ pub struct LintCommonOptions { )] #[deprecated( since = "0.4.4", - note = "`ignore-init-module-imports` will be removed in a future version because F401 now recommends appropriate fixes for unused imports in `__init__.py` (currently in preview mode). See documentation for more information and please update your configuration." + note = "`ignore-init-module-imports` will be removed in a future version because F401 now \ + recommends appropriate fixes for unused imports in `__init__.py` (currently in \ + preview mode). See documentation for more information and please update your \ + configuration." )] pub ignore_init_module_imports: Option, @@ -882,11 +910,11 @@ pub struct LintCommonOptions { /// specific prefixes. `ignore` takes precedence over `select` if the /// same prefix appears in both. #[option( - default = r#"["E4", "E7", "E9", "F"]"#, + default = r#"See https://docs.astral.sh/ruff/default-rules/ or run `ruff check --show-settings --isolated`"#, value_type = "list[RuleSelector]", example = r#" - # On top of the defaults (`E4`, E7`, `E9`, and `F`), enable flake8-bugbear (`B`) and flake8-quotes (`Q`). - select = ["E4", "E7", "E9", "F", "B", "Q"] + # On top of the defaults, enable flake8-bugbear (`B`) and flake8-quotes (`Q`). + extend-select = ["B", "Q"] "# )] pub select: Option>, @@ -1094,7 +1122,7 @@ pub struct Flake8AnnotationsOptions { value_type = "bool", example = "mypy-init-return = true" )] - pub mypy_init_return: Option, + mypy_init_return: Option, /// Whether to suppress `ANN000`-level violations for arguments matching the /// "dummy" variable regex (like `_`). @@ -1103,7 +1131,7 @@ pub struct Flake8AnnotationsOptions { value_type = "bool", example = "suppress-dummy-args = true" )] - pub suppress_dummy_args: Option, + suppress_dummy_args: Option, /// Whether to suppress `ANN200`-level violations for functions that meet /// either of the following criteria: @@ -1116,7 +1144,7 @@ pub struct Flake8AnnotationsOptions { value_type = "bool", example = "suppress-none-returning = true" )] - pub suppress_none_returning: Option, + suppress_none_returning: Option, /// Whether to suppress `ANN401` for dynamically typed `*args` and /// `**kwargs` arguments. @@ -1125,7 +1153,7 @@ pub struct Flake8AnnotationsOptions { value_type = "bool", example = "allow-star-arg-any = true" )] - pub allow_star_arg_any: Option, + allow_star_arg_any: Option, /// Whether to suppress `ANN*` rules for any declaration /// that hasn't been typed at all. @@ -1135,11 +1163,13 @@ pub struct Flake8AnnotationsOptions { value_type = "bool", example = "ignore-fully-untyped = true" )] - pub ignore_fully_untyped: Option, + ignore_fully_untyped: Option, } impl Flake8AnnotationsOptions { - pub fn into_settings(self) -> ruff_linter::rules::flake8_annotations::settings::Settings { + pub(crate) fn into_settings( + self, + ) -> ruff_linter::rules::flake8_annotations::settings::Settings { ruff_linter::rules::flake8_annotations::settings::Settings { mypy_init_return: self.mypy_init_return.unwrap_or(false), suppress_dummy_args: self.suppress_dummy_args.unwrap_or(false), @@ -1163,7 +1193,7 @@ pub struct Flake8BanditOptions { value_type = "list[str]", example = "hardcoded-tmp-directory = [\"/foo/bar\"]" )] - pub hardcoded_tmp_directory: Option>, + hardcoded_tmp_directory: Option>, /// A list of directories to consider temporary, in addition to those /// specified by [`hardcoded-tmp-directory`](#lint_flake8-bandit_hardcoded-tmp-directory) (see `S108`). @@ -1172,7 +1202,7 @@ pub struct Flake8BanditOptions { value_type = "list[str]", example = "hardcoded-tmp-directory-extend = [\"/foo/bar\"]" )] - pub hardcoded_tmp_directory_extend: Option>, + hardcoded_tmp_directory_extend: Option>, /// Whether to disallow `try`-`except`-`pass` (`S110`) for specific /// exception types. By default, `try`-`except`-`pass` is only @@ -1182,7 +1212,7 @@ pub struct Flake8BanditOptions { value_type = "bool", example = "check-typed-exception = true" )] - pub check_typed_exception: Option, + check_typed_exception: Option, /// A list of additional callable names that behave like /// [`markupsafe.Markup`](https://markupsafe.palletsprojects.com/en/stable/escaping/#markupsafe.Markup). @@ -1194,7 +1224,7 @@ pub struct Flake8BanditOptions { value_type = "list[str]", example = "extend-markup-names = [\"webhelpers.html.literal\", \"my_package.Markup\"]" )] - pub extend_markup_names: Option>, + extend_markup_names: Option>, /// A list of callable names, whose result may be safely passed into /// [`markupsafe.Markup`](https://markupsafe.palletsprojects.com/en/stable/escaping/#markupsafe.Markup). @@ -1226,11 +1256,11 @@ pub struct Flake8BanditOptions { value_type = "list[str]", example = "allowed-markup-calls = [\"bleach.clean\", \"my_package.sanitize\"]" )] - pub allowed_markup_calls: Option>, + allowed_markup_calls: Option>, } impl Flake8BanditOptions { - pub fn into_settings( + pub(crate) fn into_settings( self, ruff_options: Option<&RuffOptions>, ) -> ruff_linter::rules::flake8_bandit::settings::Settings { @@ -1276,11 +1306,13 @@ pub struct Flake8BooleanTrapOptions { value_type = "list[str]", example = "extend-allowed-calls = [\"pydantic.Field\", \"django.db.models.Value\"]" )] - pub extend_allowed_calls: Option>, + extend_allowed_calls: Option>, } impl Flake8BooleanTrapOptions { - pub fn into_settings(self) -> ruff_linter::rules::flake8_boolean_trap::settings::Settings { + pub(crate) fn into_settings( + self, + ) -> ruff_linter::rules::flake8_boolean_trap::settings::Settings { ruff_linter::rules::flake8_boolean_trap::settings::Settings { extend_allowed_calls: self.extend_allowed_calls.unwrap_or_default(), } @@ -1308,11 +1340,11 @@ pub struct Flake8BugbearOptions { extend-immutable-calls = ["fastapi.Depends", "fastapi.Query"] "# )] - pub extend_immutable_calls: Option>, + extend_immutable_calls: Option>, } impl Flake8BugbearOptions { - pub fn into_settings(self) -> ruff_linter::rules::flake8_bugbear::settings::Settings { + pub(crate) fn into_settings(self) -> ruff_linter::rules::flake8_bugbear::settings::Settings { ruff_linter::rules::flake8_bugbear::settings::Settings { extend_immutable_calls: self.extend_immutable_calls.unwrap_or_default(), } @@ -1340,7 +1372,7 @@ pub struct Flake8BuiltinsOptions { since = "0.10.0", note = "`builtins-ignorelist` has been renamed to `ignorelist`. Use that instead." )] - pub builtins_ignorelist: Option>, + pub(crate) builtins_ignorelist: Option>, /// Ignore list of builtins. #[option( @@ -1348,7 +1380,7 @@ pub struct Flake8BuiltinsOptions { value_type = "list[str]", example = "ignorelist = [\"id\"]" )] - pub ignorelist: Option>, + pub(crate) ignorelist: Option>, /// DEPRECATED: This option has been renamed to `allowed-modules`. Use `allowed-modules` instead. /// @@ -1362,9 +1394,10 @@ pub struct Flake8BuiltinsOptions { )] #[deprecated( since = "0.10.0", - note = "`builtins-allowed-modules` has been renamed to `allowed-modules`. Use that instead." + note = "`builtins-allowed-modules` has been renamed to `allowed-modules`. \ + Use that instead." )] - pub builtins_allowed_modules: Option>, + pub(crate) builtins_allowed_modules: Option>, /// List of builtin module names to allow. #[option( @@ -1372,7 +1405,7 @@ pub struct Flake8BuiltinsOptions { value_type = "list[str]", example = "allowed-modules = [\"secrets\"]" )] - pub allowed_modules: Option>, + pub(crate) allowed_modules: Option>, /// DEPRECATED: This option has been renamed to `strict-checking`. Use `strict-checking` instead. /// @@ -1386,9 +1419,10 @@ pub struct Flake8BuiltinsOptions { )] #[deprecated( since = "0.10.0", - note = "`builtins-strict-checking` has been renamed to `strict-checking`. Use that instead." + note = "`builtins-strict-checking` has been renamed to `strict-checking`. \ + Use that instead." )] - pub builtins_strict_checking: Option, + pub(crate) builtins_strict_checking: Option, /// Compare module names instead of full module paths. /// @@ -1398,11 +1432,11 @@ pub struct Flake8BuiltinsOptions { value_type = "bool", example = "strict-checking = true" )] - pub strict_checking: Option, + pub(crate) strict_checking: Option, } impl Flake8BuiltinsOptions { - pub fn into_settings(self) -> ruff_linter::rules::flake8_builtins::settings::Settings { + pub(crate) fn into_settings(self) -> ruff_linter::rules::flake8_builtins::settings::Settings { #[expect(deprecated)] ruff_linter::rules::flake8_builtins::settings::Settings { ignorelist: self @@ -1435,11 +1469,13 @@ pub struct Flake8ComprehensionsOptions { value_type = "bool", example = "allow-dict-calls-with-keyword-arguments = true" )] - pub allow_dict_calls_with_keyword_arguments: Option, + allow_dict_calls_with_keyword_arguments: Option, } impl Flake8ComprehensionsOptions { - pub fn into_settings(self) -> ruff_linter::rules::flake8_comprehensions::settings::Settings { + pub(crate) fn into_settings( + self, + ) -> ruff_linter::rules::flake8_comprehensions::settings::Settings { ruff_linter::rules::flake8_comprehensions::settings::Settings { allow_dict_calls_with_keyword_arguments: self .allow_dict_calls_with_keyword_arguments @@ -1470,12 +1506,12 @@ pub struct Flake8CopyrightOptions { value_type = "str", example = r#"notice-rgx = "(?i)Copyright \\(C\\) \\d{4}""# )] - pub notice_rgx: Option, + notice_rgx: Option, /// Author to enforce within the copyright notice. If provided, the /// author must be present immediately following the copyright notice. #[option(default = "null", value_type = "str", example = r#"author = "Ruff""#)] - pub author: Option, + author: Option, /// A minimum file size (in bytes) required for a copyright notice to /// be enforced. By default, all files are validated. @@ -1487,11 +1523,11 @@ pub struct Flake8CopyrightOptions { min-file-size = 1024 "# )] - pub min_file_size: Option, + min_file_size: Option, } impl Flake8CopyrightOptions { - pub fn try_into_settings(self) -> anyhow::Result { + pub(crate) fn try_into_settings(self) -> anyhow::Result { Ok(flake8_copyright::settings::Settings { notice_rgx: self .notice_rgx @@ -1513,11 +1549,11 @@ impl Flake8CopyrightOptions { pub struct Flake8ErrMsgOptions { /// Maximum string length for string literals in exception messages. #[option(default = "0", value_type = "int", example = "max-string-length = 20")] - pub max_string_length: Option, + max_string_length: Option, } impl Flake8ErrMsgOptions { - pub fn into_settings(self) -> flake8_errmsg::settings::Settings { + pub(crate) fn into_settings(self) -> flake8_errmsg::settings::Settings { flake8_errmsg::settings::Settings { max_string_length: self.max_string_length.unwrap_or_default(), } @@ -1537,7 +1573,7 @@ pub struct Flake8GetTextOptions { value_type = "list[str]", example = r#"function-names = ["_", "gettext", "ngettext", "ugettetxt"]"# )] - pub function_names: Option>, + function_names: Option>, /// Additional function names to consider as internationalization calls, in addition to those /// included in [`function-names`](#lint_flake8-gettext_function-names). @@ -1546,11 +1582,11 @@ pub struct Flake8GetTextOptions { value_type = "list[str]", example = r#"extend-function-names = ["ugettetxt"]"# )] - pub extend_function_names: Option>, + extend_function_names: Option>, } impl Flake8GetTextOptions { - pub fn into_settings(self) -> flake8_gettext::settings::Settings { + pub(crate) fn into_settings(self) -> flake8_gettext::settings::Settings { flake8_gettext::settings::Settings { function_names: self .function_names @@ -1586,11 +1622,11 @@ pub struct Flake8ImplicitStrConcatOptions { allow-multiline = false "# )] - pub allow_multiline: Option, + allow_multiline: Option, } impl Flake8ImplicitStrConcatOptions { - pub fn into_settings(self) -> flake8_implicit_str_concat::settings::Settings { + pub(crate) fn into_settings(self) -> flake8_implicit_str_concat::settings::Settings { flake8_implicit_str_concat::settings::Settings { allow_multiline: self.allow_multiline.unwrap_or(true), } @@ -1620,10 +1656,11 @@ pub struct Flake8ImportConventionsOptions { scipy = "sp" "# )] - pub aliases: Option>, + aliases: Option>, /// A mapping from module to conventional import alias. These aliases will - /// be added to the [`aliases`](#lint_flake8-import-conventions_aliases) mapping. + /// be added to the [`aliases`](#lint_flake8-import-conventions_aliases) mapping + /// and will override any existing `aliases` if the two settings overlap. #[option( default = r#"{}"#, value_type = "dict[str, str]", @@ -1633,7 +1670,7 @@ pub struct Flake8ImportConventionsOptions { "dask.dataframe" = "dd" "# )] - pub extend_aliases: Option>, + extend_aliases: Option>, /// A mapping from module to its banned import aliases. #[option( @@ -1645,7 +1682,7 @@ pub struct Flake8ImportConventionsOptions { "tensorflow.keras.backend" = ["K"] "# )] - pub banned_aliases: Option>, + banned_aliases: Option>, /// A list of modules that should not be imported from using the /// `from ... import ...` syntax. @@ -1660,7 +1697,7 @@ pub struct Flake8ImportConventionsOptions { banned-from = ["typing"] "# )] - pub banned_from: Option>, + banned_from: Option>, } #[derive(Clone, Debug, PartialEq, Eq, Hash, Default, Serialize)] @@ -1668,7 +1705,7 @@ pub struct Flake8ImportConventionsOptions { pub struct ModuleName(String); impl ModuleName { - pub fn into_string(self) -> String { + fn into_string(self) -> String { self.0 } } @@ -1695,7 +1732,7 @@ impl<'de> Deserialize<'de> for ModuleName { pub struct Alias(String); impl Alias { - pub fn into_string(self) -> String { + fn into_string(self) -> String { self.0 } } @@ -1727,7 +1764,7 @@ impl<'de> Deserialize<'de> for Alias { } impl Flake8ImportConventionsOptions { - pub fn try_into_settings( + pub(crate) fn try_into_settings( self, preview: PreviewMode, ) -> anyhow::Result { @@ -1755,7 +1792,8 @@ impl Flake8ImportConventionsOptions { let normalized_alias = alias.nfkc().collect::(); if normalized_alias == "__debug__" { anyhow::bail!( - "Invalid alias for module '{module}': alias normalizes to '__debug__', which is not allowed." + "Invalid alias for module '{module}': alias normalizes to '__debug__', \ + which is not allowed." ); } normalized_aliases.insert(module, normalized_alias); @@ -1790,7 +1828,7 @@ pub struct Flake8PytestStyleOptions { value_type = "bool", example = "fixture-parentheses = true" )] - pub fixture_parentheses: Option, + fixture_parentheses: Option, /// Expected type for multiple argument names in `@pytest.mark.parametrize`. /// The following values are supported: @@ -1805,7 +1843,7 @@ pub struct Flake8PytestStyleOptions { value_type = r#""csv" | "tuple" | "list""#, example = "parametrize-names-type = \"list\"" )] - pub parametrize_names_type: Option, + parametrize_names_type: Option, /// Expected type for the list of values rows in `@pytest.mark.parametrize`. /// The following values are supported: @@ -1817,7 +1855,7 @@ pub struct Flake8PytestStyleOptions { value_type = r#""tuple" | "list""#, example = "parametrize-values-type = \"tuple\"" )] - pub parametrize_values_type: Option, + parametrize_values_type: Option, /// Expected type for each row of values in `@pytest.mark.parametrize` in /// case of multiple parameters. The following values are supported: @@ -1831,7 +1869,7 @@ pub struct Flake8PytestStyleOptions { value_type = r#""tuple" | "list""#, example = "parametrize-values-row-type = \"list\"" )] - pub parametrize_values_row_type: Option, + parametrize_values_row_type: Option, /// List of exception names that require a match= parameter in a /// `pytest.raises()` call. @@ -1843,7 +1881,7 @@ pub struct Flake8PytestStyleOptions { value_type = "list[str]", example = "raises-require-match-for = [\"requests.RequestException\"]" )] - pub raises_require_match_for: Option>, + raises_require_match_for: Option>, /// List of additional exception names that require a match= parameter in a /// `pytest.raises()` call. This extends the default list of exceptions @@ -1861,7 +1899,7 @@ pub struct Flake8PytestStyleOptions { value_type = "list[str]", example = "raises-extend-require-match-for = [\"requests.RequestException\"]" )] - pub raises_extend_require_match_for: Option>, + raises_extend_require_match_for: Option>, /// Boolean flag specifying whether `@pytest.mark.foo()` without parameters /// should have parentheses. If the option is set to `false` (the @@ -1873,7 +1911,7 @@ pub struct Flake8PytestStyleOptions { value_type = "bool", example = "mark-parentheses = true" )] - pub mark_parentheses: Option, + mark_parentheses: Option, /// List of warning names that require a match= parameter in a /// `pytest.warns()` call. @@ -1885,7 +1923,7 @@ pub struct Flake8PytestStyleOptions { value_type = "list[str]", example = "warns-require-match-for = [\"requests.RequestsWarning\"]" )] - pub warns_require_match_for: Option>, + warns_require_match_for: Option>, /// List of additional warning names that require a match= parameter in a /// `pytest.warns()` call. This extends the default list of warnings that @@ -1905,11 +1943,13 @@ pub struct Flake8PytestStyleOptions { value_type = "list[str]", example = "warns-extend-require-match-for = [\"requests.RequestsWarning\"]" )] - pub warns_extend_require_match_for: Option>, + warns_extend_require_match_for: Option>, } impl Flake8PytestStyleOptions { - pub fn try_into_settings(self) -> anyhow::Result { + pub(crate) fn try_into_settings( + self, + ) -> anyhow::Result { Ok(flake8_pytest_style::settings::Settings { fixture_parentheses: self.fixture_parentheses.unwrap_or_default(), parametrize_names_type: self.parametrize_names_type.unwrap_or_default(), @@ -1983,7 +2023,7 @@ pub struct Flake8QuotesOptions { inline-quotes = "single" "# )] - pub inline_quotes: Option, + inline_quotes: Option, /// Quote style to prefer for multiline strings (either "single" or /// "double"). @@ -1997,7 +2037,7 @@ pub struct Flake8QuotesOptions { multiline-quotes = "single" "# )] - pub multiline_quotes: Option, + multiline_quotes: Option, /// Quote style to prefer for docstrings (either "single" or "double"). /// @@ -2010,7 +2050,7 @@ pub struct Flake8QuotesOptions { docstring-quotes = "single" "# )] - pub docstring_quotes: Option, + docstring_quotes: Option, /// Whether to avoid using single quotes if a string contains single quotes, /// or vice-versa with double quotes, as per [PEP 8](https://peps.python.org/pep-0008/#string-quotes). @@ -2023,11 +2063,11 @@ pub struct Flake8QuotesOptions { avoid-escape = false "# )] - pub avoid_escape: Option, + avoid_escape: Option, } impl Flake8QuotesOptions { - pub fn into_settings(self) -> flake8_quotes::settings::Settings { + pub(crate) fn into_settings(self) -> flake8_quotes::settings::Settings { flake8_quotes::settings::Settings { inline_quotes: self.inline_quotes.unwrap_or_default(), multiline_quotes: self.multiline_quotes.unwrap_or_default(), @@ -2052,7 +2092,7 @@ pub struct Flake8SelfOptions { ignore-names = ["_new"] "# )] - pub ignore_names: Option>, + ignore_names: Option>, /// Additional names to ignore when considering `flake8-self` violations, /// in addition to those included in [`ignore-names`](#lint_flake8-self_ignore-names). @@ -2061,11 +2101,11 @@ pub struct Flake8SelfOptions { value_type = "list[str]", example = r#"extend-ignore-names = ["_base_manager", "_default_manager", "_meta"]"# )] - pub extend_ignore_names: Option>, + extend_ignore_names: Option>, } impl Flake8SelfOptions { - pub fn into_settings(self) -> flake8_self::settings::Settings { + pub(crate) fn into_settings(self) -> flake8_self::settings::Settings { let defaults = flake8_self::settings::Settings::default(); flake8_self::settings::Settings { ignore_names: self @@ -2095,7 +2135,7 @@ pub struct Flake8TidyImportsOptions { ban-relative-imports = "all" "# )] - pub ban_relative_imports: Option, + ban_relative_imports: Option, /// Specific modules or module members that may not be imported or accessed. /// Note that this rule is only meant to flag accidental uses, @@ -2109,7 +2149,7 @@ pub struct Flake8TidyImportsOptions { "typing.TypedDict".msg = "Use typing_extensions.TypedDict instead." "# )] - pub banned_api: Option>, + banned_api: Option>, /// 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:` @@ -2124,7 +2164,7 @@ pub struct Flake8TidyImportsOptions { banned-module-level-imports = ["torch", "tensorflow"] "# )] - pub banned_module_level_imports: Option>, + banned_module_level_imports: Option>, /// Specific modules that must be imported lazily in contexts where `lazy import` is legal, or /// `"all"` to require every lazily-convertible import to use the `lazy` keyword. Ruff ignores @@ -2142,7 +2182,7 @@ pub struct Flake8TidyImportsOptions { require-lazy = { include = "all", exclude = ["sitecustomize"] } "# )] - pub require_lazy: Option, + require_lazy: Option, /// Specific modules that may not be imported lazily, or `"all"` to forbid lazy imports except /// for any modules excluded from the selector. This rule is only enforced when targeting @@ -2158,11 +2198,11 @@ pub struct Flake8TidyImportsOptions { ban-lazy = { include = "all", exclude = ["typing"] } "# )] - pub ban_lazy: Option, + ban_lazy: Option, } impl Flake8TidyImportsOptions { - pub fn try_into_settings(self) -> Result { + pub(crate) fn try_into_settings(self) -> Result { let require_lazy = self.require_lazy.unwrap_or_default(); let ban_lazy = self.ban_lazy.unwrap_or_default(); @@ -2257,7 +2297,7 @@ pub struct Flake8TypeCheckingOptions { strict = true "# )] - pub strict: Option, + strict: Option, /// Exempt certain modules from needing to be moved into type-checking /// blocks. @@ -2268,7 +2308,7 @@ pub struct Flake8TypeCheckingOptions { exempt-modules = ["typing", "typing_extensions"] "# )] - pub exempt_modules: Option>, + exempt_modules: Option>, /// Exempt classes that list any of the enumerated classes as a base class /// from needing to be moved into type-checking blocks. @@ -2287,7 +2327,7 @@ pub struct Flake8TypeCheckingOptions { runtime-evaluated-base-classes = ["pydantic.BaseModel", "sqlalchemy.orm.DeclarativeBase"] "# )] - pub runtime_evaluated_base_classes: Option>, + runtime_evaluated_base_classes: Option>, /// Exempt classes and functions decorated with any of the enumerated /// decorators from being moved into type-checking blocks. @@ -2316,7 +2356,7 @@ pub struct Flake8TypeCheckingOptions { runtime-evaluated-decorators = ["pydantic.validate_call", "attrs.define"] "# )] - pub runtime_evaluated_decorators: Option>, + runtime_evaluated_decorators: Option>, /// Whether to add quotes around type annotations, if doing so would allow /// the corresponding import to be moved into a type-checking block. @@ -2368,11 +2408,11 @@ pub struct Flake8TypeCheckingOptions { quote-annotations = true "# )] - pub quote_annotations: Option, + quote_annotations: Option, } impl Flake8TypeCheckingOptions { - pub fn into_settings(self) -> flake8_type_checking::settings::Settings { + pub(crate) fn into_settings(self) -> flake8_type_checking::settings::Settings { flake8_type_checking::settings::Settings { strict: self.strict.unwrap_or(false), exempt_modules: self @@ -2398,11 +2438,11 @@ pub struct Flake8UnusedArgumentsOptions { value_type = "bool", example = "ignore-variadic-names = true" )] - pub ignore_variadic_names: Option, + ignore_variadic_names: Option, } impl Flake8UnusedArgumentsOptions { - pub fn into_settings(self) -> flake8_unused_arguments::settings::Settings { + pub(crate) fn into_settings(self) -> flake8_unused_arguments::settings::Settings { flake8_unused_arguments::settings::Settings { ignore_variadic_names: self.ignore_variadic_names.unwrap_or_default(), } @@ -2443,7 +2483,7 @@ pub struct IsortOptions { combine-as-imports = true "# )] - pub force_wrap_aliases: Option, + force_wrap_aliases: Option, /// Forces all from imports to appear on their own line. #[option( @@ -2451,7 +2491,7 @@ pub struct IsortOptions { value_type = "bool", example = r#"force-single-line = true"# )] - pub force_single_line: Option, + force_single_line: Option, /// One or more modules to exclude from the single line rule. #[option( @@ -2461,7 +2501,7 @@ pub struct IsortOptions { single-line-exclusions = ["os", "json"] "# )] - pub single_line_exclusions: Option>, + single_line_exclusions: Option>, /// Combines as imports on the same line. See isort's [`combine-as-imports`](https://pycqa.github.io/isort/docs/configuration/options.html#combine-as-imports) /// option. @@ -2472,7 +2512,7 @@ pub struct IsortOptions { combine-as-imports = true "# )] - pub combine_as_imports: Option, + combine_as_imports: Option, /// If a comma is placed after the last member in a multi-line import, then /// the imports will never be folded into one line. @@ -2488,7 +2528,7 @@ pub struct IsortOptions { split-on-trailing-comma = false "# )] - pub split_on_trailing_comma: Option, + split_on_trailing_comma: Option, /// Order imports by type, which is determined by case, in addition to /// alphabetically. @@ -2502,7 +2542,7 @@ pub struct IsortOptions { order-by-type = true "# )] - pub order_by_type: Option, + order_by_type: Option, /// Don't sort straight-style imports (like `import sys`) before from-style /// imports (like `from itertools import groupby`). Instead, sort the @@ -2514,7 +2554,7 @@ pub struct IsortOptions { force-sort-within-sections = true "# )] - pub force_sort_within_sections: Option, + force_sort_within_sections: Option, /// Sort imports taking into account case sensitivity. /// @@ -2527,7 +2567,7 @@ pub struct IsortOptions { case-sensitive = true "# )] - pub case_sensitive: Option, + case_sensitive: Option, /// Force specific imports to the top of their appropriate section. #[option( @@ -2537,7 +2577,7 @@ pub struct IsortOptions { force-to-top = ["src"] "# )] - pub force_to_top: Option>, + force_to_top: Option>, /// A list of modules to consider first-party, regardless of whether they /// can be identified as such via introspection of the local filesystem. @@ -2551,7 +2591,7 @@ pub struct IsortOptions { known-first-party = ["src"] "# )] - pub known_first_party: Option>, + known_first_party: Option>, /// A list of modules to consider third-party, regardless of whether they /// can be identified as such via introspection of the local filesystem. @@ -2565,7 +2605,7 @@ pub struct IsortOptions { known-third-party = ["src"] "# )] - pub known_third_party: Option>, + known_third_party: Option>, /// A list of modules to consider being a local folder. /// Generally, this is reserved for relative imports (`from . import module`). @@ -2579,7 +2619,7 @@ pub struct IsortOptions { known-local-folder = ["src"] "# )] - pub known_local_folder: Option>, + known_local_folder: Option>, /// A list of modules to consider standard-library, in addition to those /// known to Ruff in advance. @@ -2593,7 +2633,7 @@ pub struct IsortOptions { extra-standard-library = ["path"] "# )] - pub extra_standard_library: Option>, + extra_standard_library: Option>, /// Whether to place "closer" imports (fewer `.` characters, most local) /// before "further" imports (more `.` characters, least local), or vice @@ -2610,7 +2650,7 @@ pub struct IsortOptions { relative-imports-order = "closest-to-furthest" "# )] - pub relative_imports_order: Option, + relative_imports_order: Option, /// Add the specified import line to all files. #[option( @@ -2620,7 +2660,7 @@ pub struct IsortOptions { required-imports = ["from __future__ import annotations"] "# )] - pub required_imports: Option>, + required_imports: Option>, /// An override list of tokens to always recognize as a Class for /// [`order-by-type`](#lint_isort_order-by-type) regardless of casing. @@ -2631,7 +2671,7 @@ pub struct IsortOptions { classes = ["SVC"] "# )] - pub classes: Option>, + classes: Option>, /// An override list of tokens to always recognize as a CONSTANT /// for [`order-by-type`](#lint_isort_order-by-type) regardless of casing. @@ -2642,7 +2682,7 @@ pub struct IsortOptions { constants = ["constant"] "# )] - pub constants: Option>, + constants: Option>, /// An override list of tokens to always recognize as a var /// for [`order-by-type`](#lint_isort_order-by-type) regardless of casing. @@ -2653,7 +2693,7 @@ pub struct IsortOptions { variables = ["VAR"] "# )] - pub variables: Option>, + variables: Option>, /// A list of sections that should _not_ be delineated from the previous /// section via empty lines. @@ -2664,7 +2704,7 @@ pub struct IsortOptions { no-lines-before = ["future", "standard-library"] "# )] - pub no_lines_before: Option>, + no_lines_before: Option>, /// A mapping from import section names to their heading comments. /// @@ -2685,7 +2725,7 @@ pub struct IsortOptions { local-folder = "Local folder imports" "# )] - pub import_heading: Option>, + import_heading: Option>, /// The number of blank lines to place after imports. /// Use `-1` for automatic determination. @@ -2703,7 +2743,7 @@ pub struct IsortOptions { lines-after-imports = 1 "# )] - pub lines_after_imports: Option, + lines_after_imports: Option, /// The number of lines to place between "direct" and `import from` imports. /// @@ -2717,7 +2757,7 @@ pub struct IsortOptions { lines-between-types = 1 "# )] - pub lines_between_types: Option, + lines_between_types: Option, /// A list of modules to separate into auxiliary block(s) of imports, /// in the order specified. @@ -2728,7 +2768,7 @@ pub struct IsortOptions { forced-separate = ["tests"] "# )] - pub forced_separate: Option>, + forced_separate: Option>, /// Override in which order the sections should be output. Can be used to move custom sections. #[option( @@ -2738,7 +2778,7 @@ pub struct IsortOptions { section-order = ["future", "standard-library", "first-party", "local-folder", "third-party"] "# )] - pub section_order: Option>, + section_order: Option>, /// Define a default section for any imports that don't fit into the specified [`section-order`](#lint_isort_section-order). #[option( @@ -2748,7 +2788,7 @@ pub struct IsortOptions { default-section = "first-party" "# )] - pub default_section: Option, + default_section: Option, /// Put all imports into the same section bucket. /// @@ -2775,7 +2815,7 @@ pub struct IsortOptions { no-sections = true "# )] - pub no_sections: Option, + no_sections: Option, /// Whether to automatically mark imports from within the same package as first-party. /// For example, when `detect-same-package = true`, then when analyzing files within the @@ -2791,7 +2831,7 @@ pub struct IsortOptions { detect-same-package = false "# )] - pub detect_same_package: Option, + detect_same_package: Option, /// Whether to place `import from` imports before straight imports when sorting. /// @@ -2817,7 +2857,7 @@ pub struct IsortOptions { from-first = true "# )] - pub from_first: Option, + from_first: Option, /// Sort imports by their string length, such that shorter imports appear /// before longer imports. For example, by default, imports will be sorted @@ -2840,7 +2880,7 @@ pub struct IsortOptions { length-sort = true "# )] - pub length_sort: Option, + length_sort: Option, /// Sort straight imports by their string length. Similar to [`length-sort`](#lint_isort_length-sort), /// but applies only to straight imports and doesn't affect `from` imports. @@ -2851,7 +2891,7 @@ pub struct IsortOptions { length-sort-straight = true "# )] - pub length_sort_straight: Option, + length_sort_straight: Option, // Tables are required to go last. /// A list of mappings from section names to modules. @@ -2895,11 +2935,11 @@ pub struct IsortOptions { "django" = ["django"] "# )] - pub sections: Option>>, + sections: Option>>, } impl IsortOptions { - pub fn try_into_settings( + pub(crate) fn try_into_settings( self, ) -> Result { // Verify that if `no_sections` is set, then `section_order` is empty. @@ -2919,7 +2959,8 @@ impl IsortOptions { 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`" + "`lines-between-types` is ignored when `force-sort-within-sections` \ + is set to `true`" ); } @@ -3119,11 +3160,11 @@ pub struct McCabeOptions { max-complexity = 5 "# )] - pub max_complexity: Option, + max_complexity: Option, } impl McCabeOptions { - pub fn into_settings(self) -> mccabe::settings::Settings { + pub(crate) fn into_settings(self) -> mccabe::settings::Settings { mccabe::settings::Settings { max_complexity: self .max_complexity @@ -3151,7 +3192,7 @@ pub struct Pep8NamingOptions { ignore-names = ["callMethod"] "# )] - pub ignore_names: Option>, + ignore_names: Option>, /// Additional names (or patterns) to ignore when considering `pep8-naming` violations, /// in addition to those included in [`ignore-names`](#lint_pep8-naming_ignore-names). @@ -3164,7 +3205,7 @@ pub struct Pep8NamingOptions { value_type = "list[str]", example = r#"extend-ignore-names = ["callMethod"]"# )] - pub extend_ignore_names: Option>, + extend_ignore_names: Option>, /// A list of decorators that, when applied to a method, indicate that the /// method should be treated as a class method (in addition to the builtin @@ -3190,7 +3231,7 @@ pub struct Pep8NamingOptions { ] "# )] - pub classmethod_decorators: Option>, + classmethod_decorators: Option>, /// A list of decorators that, when applied to a method, indicate that the /// method should be treated as a static method (in addition to the builtin @@ -3210,11 +3251,11 @@ pub struct Pep8NamingOptions { staticmethod-decorators = ["belay.Device.teardown"] "# )] - pub staticmethod_decorators: Option>, + staticmethod_decorators: Option>, } impl Pep8NamingOptions { - pub fn try_into_settings( + pub(crate) fn try_into_settings( self, ) -> Result { Ok(pep8_naming::settings::Settings { @@ -3291,7 +3332,10 @@ pub struct PycodestyleOptions { } impl PycodestyleOptions { - pub fn into_settings(self, global_line_length: LineLength) -> pycodestyle::settings::Settings { + pub(crate) fn into_settings( + self, + global_line_length: LineLength, + ) -> pycodestyle::settings::Settings { pycodestyle::settings::Settings { max_doc_length: self.max_doc_length, max_line_length: self.max_line_length.unwrap_or(global_line_length), @@ -3399,7 +3443,7 @@ pub struct PydocstyleOptions { convention = "google" "# )] - pub convention: Option, + pub(crate) convention: Option, /// Ignore docstrings for functions or methods decorated with the /// specified fully-qualified decorators. @@ -3410,7 +3454,7 @@ pub struct PydocstyleOptions { ignore-decorators = ["typing.overload"] "# )] - pub ignore_decorators: Option>, + pub(crate) ignore_decorators: Option>, /// A list of decorators that, when applied to a method, indicate that the /// method should be treated as a property (in addition to the builtin @@ -3425,7 +3469,7 @@ pub struct PydocstyleOptions { property-decorators = ["gi.repository.GObject.Property"] "# )] - pub property_decorators: Option>, + pub(crate) property_decorators: Option>, /// If set to `true`, ignore missing documentation for `*args` and `**kwargs` parameters. #[option( @@ -3435,11 +3479,11 @@ pub struct PydocstyleOptions { ignore-var-parameters = true "# )] - pub ignore_var_parameters: Option, + pub(crate) ignore_var_parameters: Option, } impl PydocstyleOptions { - pub fn into_settings(self) -> pydocstyle::settings::Settings { + pub(crate) fn into_settings(self) -> pydocstyle::settings::Settings { let PydocstyleOptions { convention, ignore_decorators, @@ -3474,11 +3518,11 @@ pub struct PydoclintOptions { ignore-one-line-docstrings = true "# )] - pub ignore_one_line_docstrings: Option, + ignore_one_line_docstrings: Option, } impl PydoclintOptions { - pub fn into_settings(self) -> pydoclint::settings::Settings { + pub(crate) fn into_settings(self) -> pydoclint::settings::Settings { pydoclint::settings::Settings { ignore_one_line_docstrings: self.ignore_one_line_docstrings.unwrap_or_default(), } @@ -3503,7 +3547,7 @@ pub struct PyflakesOptions { value_type = "list[str]", example = "extend-generics = [\"django.db.models.ForeignKey\"]" )] - pub extend_generics: Option>, + extend_generics: Option>, /// A list of modules to ignore when considering unused imports. /// @@ -3518,11 +3562,11 @@ pub struct PyflakesOptions { value_type = "list[str]", example = r#"allowed-unused-imports = ["hvplot.pandas"]"# )] - pub allowed_unused_imports: Option>, + allowed_unused_imports: Option>, } impl PyflakesOptions { - pub fn into_settings(self) -> pyflakes::settings::Settings { + pub(crate) fn into_settings(self) -> pyflakes::settings::Settings { pyflakes::settings::Settings { extend_generics: self.extend_generics.unwrap_or_default(), allowed_unused_imports: self.allowed_unused_imports.unwrap_or_default(), @@ -3545,7 +3589,7 @@ pub struct PylintOptions { allow-magic-value-types = ["int"] "# )] - pub allow_magic_value_types: Option>, + allow_magic_value_types: Option>, /// Dunder methods name to allow, in addition to the default set from the /// Python standard library (see `PLW3201`). @@ -3556,21 +3600,21 @@ pub struct PylintOptions { allow-dunder-method-names = ["__tablename__", "__table_args__"] "# )] - pub allow_dunder_method_names: Option>, + allow_dunder_method_names: Option>, /// Maximum number of branches allowed for a function or method body (see `PLR0912`). #[option(default = r"12", value_type = "int", example = r"max-branches = 15")] - pub max_branches: Option, + max_branches: Option, /// Maximum number of return statements allowed for a function or method /// body (see `PLR0911`) #[option(default = r"6", value_type = "int", example = r"max-returns = 10")] - pub max_returns: Option, + max_returns: Option, /// Maximum number of arguments allowed for a function or method definition /// (see `PLR0913`). #[option(default = r"5", value_type = "int", example = r"max-args = 10")] - pub max_args: Option, + max_args: Option, /// Maximum number of positional arguments allowed for a function or method definition /// (see `PLR0917`). @@ -3581,15 +3625,15 @@ pub struct PylintOptions { value_type = "int", example = r"max-positional-args = 3" )] - pub max_positional_args: Option, + max_positional_args: Option, /// Maximum number of local variables allowed for a function or method body (see `PLR0914`). #[option(default = r"15", value_type = "int", example = r"max-locals = 20")] - pub max_locals: Option, + max_locals: Option, /// Maximum number of statements allowed for a function or method body (see `PLR0915`). #[option(default = r"50", value_type = "int", example = r"max-statements = 75")] - pub max_statements: Option, + max_statements: Option, /// Maximum number of statements allowed for a try clause body (see `W0717`). #[option( @@ -3597,7 +3641,7 @@ pub struct PylintOptions { value_type = "int", example = r"max-statements-in-try = 10" )] - pub max_statements_in_try: Option, + max_statements_in_try: Option, /// Maximum number of public methods allowed for a class (see `PLR0904`). #[option( @@ -3605,12 +3649,12 @@ pub struct PylintOptions { value_type = "int", example = r"max-public-methods = 30" )] - pub max_public_methods: Option, + max_public_methods: Option, /// Maximum number of Boolean expressions allowed within a single `if` statement /// (see `PLR0916`). #[option(default = r"5", value_type = "int", example = r"max-bool-expr = 10")] - pub max_bool_expr: Option, + max_bool_expr: Option, /// Maximum number of nested blocks allowed within a function or method body /// (see `PLR1702`). @@ -3619,11 +3663,11 @@ pub struct PylintOptions { value_type = "int", example = r"max-nested-blocks = 10" )] - pub max_nested_blocks: Option, + max_nested_blocks: Option, } impl PylintOptions { - pub fn into_settings(self) -> pylint::settings::Settings { + pub(crate) fn into_settings(self) -> pylint::settings::Settings { let defaults = pylint::settings::Settings::default(); pylint::settings::Settings { allow_magic_value_types: self @@ -3696,11 +3740,11 @@ pub struct PyUpgradeOptions { keep-runtime-typing = true "# )] - pub keep_runtime_typing: Option, + keep_runtime_typing: Option, } impl PyUpgradeOptions { - pub fn into_settings(self) -> pyupgrade::settings::Settings { + pub(crate) fn into_settings(self) -> pyupgrade::settings::Settings { pyupgrade::settings::Settings { keep_runtime_typing: self.keep_runtime_typing.unwrap_or_default(), } @@ -3724,7 +3768,7 @@ pub struct RuffOptions { parenthesize-tuple-in-subscript = true "# )] - pub parenthesize_tuple_in_subscript: Option, + parenthesize_tuple_in_subscript: Option, /// A list of additional callable names that behave like /// [`markupsafe.Markup`](https://markupsafe.palletsprojects.com/en/stable/escaping/#markupsafe.Markup). @@ -3738,9 +3782,10 @@ pub struct RuffOptions { )] #[deprecated( since = "0.10.0", - note = "The `extend-markup-names` option has been moved to the `flake8-bandit` section of the configuration." + note = "The `extend-markup-names` option has been moved to the `flake8-bandit` section of \ + the configuration." )] - pub extend_markup_names: Option>, + extend_markup_names: Option>, /// A list of callable names, whose result may be safely passed into /// [`markupsafe.Markup`](https://markupsafe.palletsprojects.com/en/stable/escaping/#markupsafe.Markup). @@ -3774,9 +3819,10 @@ pub struct RuffOptions { )] #[deprecated( since = "0.10.0", - note = "The `allowed-markup-names` option has been moved to the `flake8-bandit` section of the configuration." + note = "The `allowed-markup-names` option has been moved to the `flake8-bandit` section \ + of the configuration." )] - pub allowed_markup_calls: Option>, + allowed_markup_calls: Option>, /// Whether to require `__init__.py` files to contain no code at all, including imports and /// docstrings (see `RUF067`). #[option( @@ -3787,11 +3833,11 @@ pub struct RuffOptions { strictly-empty-init-modules = true "# )] - pub strictly_empty_init_modules: Option, + strictly_empty_init_modules: Option, } impl RuffOptions { - pub fn into_settings(self) -> ruff::settings::Settings { + pub(crate) fn into_settings(self) -> ruff::settings::Settings { ruff::settings::Settings { parenthesize_tuple_in_subscript: self .parenthesize_tuple_in_subscript @@ -4173,7 +4219,7 @@ pub struct AnalyzeOptions { exclude = ["generated"] "# )] - pub exclude: Option>, + pub(crate) exclude: Option>, /// Whether to enable preview mode. When preview mode is enabled, Ruff will expose unstable /// commands. #[option( @@ -4184,7 +4230,7 @@ pub struct AnalyzeOptions { preview = true "# )] - pub preview: Option, + pub(crate) preview: Option, /// Whether to generate a map from file to files that it depends on (dependencies) or files that /// depend on it (dependents). #[option( @@ -4194,7 +4240,7 @@ pub struct AnalyzeOptions { direction = "dependencies" "# )] - pub direction: Option, + pub(crate) direction: Option, /// Whether to detect imports from string literals. When enabled, Ruff will search for string /// literals that "look like" import paths, and include them in the import map, if they resolve /// to valid Python modules. @@ -4205,7 +4251,7 @@ pub struct AnalyzeOptions { detect-string-imports = true "# )] - pub detect_string_imports: Option, + pub(crate) detect_string_imports: Option, /// The minimum number of dots in a string to consider it a valid import. /// /// This setting is only relevant when [`detect-string-imports`](#detect-string-imports) is enabled. @@ -4218,7 +4264,7 @@ pub struct AnalyzeOptions { string-imports-min-dots = 2 "# )] - pub string_imports_min_dots: Option, + pub(crate) string_imports_min_dots: Option, /// A map from file path to the list of Python or non-Python file paths or globs that should be /// considered dependencies of that file, regardless of whether relevant imports are detected. #[option( @@ -4230,7 +4276,7 @@ pub struct AnalyzeOptions { "foo/baz/reader.py" = ["configs/bar.json"] "# )] - pub include_dependencies: Option>>, + pub(crate) include_dependencies: Option>>, /// Whether to include imports that are only used for type checking (i.e., imports within `if TYPE_CHECKING:` blocks). /// When enabled (default), type-checking-only imports are included in the import graph. /// When disabled, they are excluded. @@ -4242,7 +4288,7 @@ pub struct AnalyzeOptions { type-checking-imports = false "# )] - pub type_checking_imports: Option, + pub(crate) type_checking_imports: Option, } /// Like [`LintCommonOptions`], but with any `#[serde(flatten)]` fields inlined. This leads to far, diff --git a/crates/ruff_workspace/src/pyproject.rs b/crates/ruff_workspace/src/pyproject.rs index f841cfa83f..e8b64d0f78 100644 --- a/crates/ruff_workspace/src/pyproject.rs +++ b/crates/ruff_workspace/src/pyproject.rs @@ -33,17 +33,6 @@ pub struct Pyproject { project: Option, } -impl Pyproject { - pub const fn new(options: Options) -> Self { - Self { - tool: Some(Tools { - ruff: Some(options), - }), - project: None, - } - } -} - fn parse_toml, T: DeserializeOwned>(path: P, table_path: &[&str]) -> Result { let path = path.as_ref(); @@ -87,7 +76,7 @@ fn parse_pyproject_toml>(path: P) -> Result { } /// Return `true` if a `pyproject.toml` contains a `[tool.ruff]` section. -pub fn ruff_enabled>(path: P) -> Result { +fn ruff_enabled>(path: P) -> Result { let pyproject = parse_pyproject_toml(path)?; Ok(pyproject.tool.and_then(|tool| tool.ruff).is_some()) } diff --git a/crates/ruff_workspace/src/resolver.rs b/crates/ruff_workspace/src/resolver.rs index 5ebf2f3f46..02662ba9a4 100644 --- a/crates/ruff_workspace/src/resolver.rs +++ b/crates/ruff_workspace/src/resolver.rs @@ -68,12 +68,7 @@ pub enum PyprojectDiscoveryStrategy { impl PyprojectDiscoveryStrategy { #[inline] - pub const fn is_fixed(self) -> bool { - matches!(self, PyprojectDiscoveryStrategy::Fixed) - } - - #[inline] - pub const fn is_hierarchical(self) -> bool { + const fn is_hierarchical(self) -> bool { matches!(self, PyprojectDiscoveryStrategy::Hierarchical) } } @@ -89,7 +84,7 @@ pub enum Relativity { } impl Relativity { - pub fn resolve(self, path: &Path) -> &Path { + fn resolve(self, path: &Path) -> &Path { match self { Relativity::Parent => path .parent() @@ -126,7 +121,7 @@ impl<'a> Resolver<'a> { /// Return `true` if the [`Resolver`] is using a hierarchical discovery strategy. #[inline] - pub fn is_hierarchical(&self) -> bool { + fn is_hierarchical(&self) -> bool { self.pyproject_config.strategy.is_hierarchical() } @@ -138,7 +133,7 @@ impl<'a> Resolver<'a> { /// Return `true` if the [`Resolver`] should respect `.gitignore` files. #[inline] - pub fn respect_gitignore(&self) -> bool { + fn respect_gitignore(&self) -> bool { self.pyproject_config .settings .file_resolver @@ -838,7 +833,7 @@ pub fn match_exclusion, R: AsRef>( /// Return `true` if the given candidates should be ignored based on the exclusion /// criteria. -pub fn match_candidate_exclusion( +fn match_candidate_exclusion( file_path: &Candidate, file_basename: &Candidate, exclusion: &GlobSet, diff --git a/crates/ruff_workspace/src/settings.rs b/crates/ruff_workspace/src/settings.rs index 6fa186ab00..22dfe9c989 100644 --- a/crates/ruff_workspace/src/settings.rs +++ b/crates/ruff_workspace/src/settings.rs @@ -18,6 +18,7 @@ use ruff_source_file::find_newline; use std::fmt; use std::path::{Path, PathBuf}; +#[expect(clippy::struct_excessive_bools)] #[derive(Debug, CacheKey)] pub struct Settings { #[cache_key(ignore)] @@ -31,6 +32,8 @@ pub struct Settings { #[cache_key(ignore)] pub output_format: OutputFormat, #[cache_key(ignore)] + pub output_prefer_rule_codes: bool, + #[cache_key(ignore)] pub show_fixes: bool, pub file_resolver: FileResolverSettings, @@ -47,6 +50,7 @@ impl Default for Settings { fix: false, fix_only: false, output_format: OutputFormat::default(), + output_prefer_rule_codes: false, show_fixes: false, unsafe_fixes: UnsafeFixes::default(), linter: LinterSettings::new(project_root), @@ -67,6 +71,7 @@ impl fmt::Display for Settings { self.fix, self.fix_only, self.output_format, + self.output_prefer_rule_codes, self.show_fixes, self.unsafe_fixes, self.file_resolver | nested, @@ -147,6 +152,7 @@ pub(crate) static INCLUDE: &[FilePattern] = &[ FilePattern::Builtin("**/pyproject.toml"), FilePattern::Builtin("**/ruff.toml"), FilePattern::Builtin("**/.ruff.toml"), + FilePattern::Builtin("*.md"), ]; pub(crate) static INCLUDE_PREVIEW: &[FilePattern] = &[ FilePattern::Builtin("*.py"), diff --git a/crates/ty/CONTRIBUTING.md b/crates/ty/CONTRIBUTING.md index 293a5a1718..0313be6d4e 100644 --- a/crates/ty/CONTRIBUTING.md +++ b/crates/ty/CONTRIBUTING.md @@ -144,7 +144,7 @@ ensure that the changes do not break any of the properties. ## Ecosystem CI (`ecosystem-analyzer`) GitHub Actions will run your changes against a number of real-world projects from GitHub and report -any differences in ty's diagnostic output. You can use [`setup_primer_project.py`](./scripts/setup_primer_project.py) +any differences in ty's diagnostic output. You can use [`setup_primer_project.py`](../../scripts/setup_primer_project.py) to reproduce the same testing conditions locally. ## Coding guidelines diff --git a/crates/ty/Cargo.toml b/crates/ty/Cargo.toml index 232180cfb7..6aac5d7c24 100644 --- a/crates/ty/Cargo.toml +++ b/crates/ty/Cargo.toml @@ -80,6 +80,7 @@ toml = { workspace = true } [features] default = [] +test-uv = [] [lints] workspace = true diff --git a/crates/ty/docs/cli.md b/crates/ty/docs/cli.md index a58bd04ed3..5be6350ca3 100644 --- a/crates/ty/docs/cli.md +++ b/crates/ty/docs/cli.md @@ -61,6 +61,7 @@ over all configuration files.

Cannot be used in combination with --exit-zero or --exit-zero-on-warning.

--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__/**.

+
--exclude-scripts

Exclude files containing PEP 723 inline script metadata unless passed explicitly. Use --include-scripts to disable

--exit-zero

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

Cannot be used in combination with --error-on-warning.

--exit-zero-on-warning

Use exit code 0 if there are no error-level diagnostics.

diff --git a/crates/ty/docs/configuration.md b/crates/ty/docs/configuration.md index 762904db61..cd908af001 100644 --- a/crates/ty/docs/configuration.md +++ b/crates/ty/docs/configuration.md @@ -765,6 +765,49 @@ Defaults to `false`. --- +### `strict-generic-narrowing` + +Whether ty should use strict narrowing for unspecialized generic classes in +`isinstance()` and `issubclass()` checks, as well as `match` class patterns. + +When enabled, ty narrows to the top materialization of the class. For example, +`isinstance(value, list)` narrows a value of type `object` to `Top[list[Unknown]]`, +representing the (infinite) union of all possible `list` specializations. Iterating +over the list would yield values of type `object`. + +When disabled, ty uses gradual generic narrowing, preserving compatible type +arguments from the original type where possible. For example, +`isinstance(value, list)` narrows a value of type `Sequence[int]` to `list[int]`. +If no specialization is available, the same check narrows a value of type `object` +to `list[Unknown]`; items of any type can then be appended to the list. Class +patterns such as `case list():` follow the same behavior. + +Defaults to `false`. + +**Default value**: `false` + +**Type**: `bool` + +**Example usage**: + +=== "pyproject.toml" + + ```toml + [tool.ty.analysis] + # Use the top materialization when narrowing to an unspecialized generic class + strict-generic-narrowing = true + ``` + +=== "ty.toml" + + ```toml + [analysis] + # Use the top materialization when narrowing to an unspecialized generic class + strict-generic-narrowing = true + ``` + +--- + ## `environment` ### `extra-paths` @@ -1864,6 +1907,49 @@ Defaults to `false`. --- +#### `strict-generic-narrowing` + +Whether ty should use strict narrowing for unspecialized generic classes in +`isinstance()` and `issubclass()` checks, as well as `match` class patterns. + +When enabled, ty narrows to the top materialization of the class. For example, +`isinstance(value, list)` narrows a value of type `object` to `Top[list[Unknown]]`, +representing the (infinite) union of all possible `list` specializations. Iterating +over the list would yield values of type `object`. + +When disabled, ty uses gradual generic narrowing, preserving compatible type +arguments from the original type where possible. For example, +`isinstance(value, list)` narrows a value of type `Sequence[int]` to `list[int]`. +If no specialization is available, the same check narrows a value of type `object` +to `list[Unknown]`; items of any type can then be appended to the list. Class +patterns such as `case list():` follow the same behavior. + +Defaults to `false`. + +**Default value**: `false` + +**Type**: `bool` + +**Example usage**: + +=== "pyproject.toml" + + ```toml + [tool.ty.overrides.analysis] + # Use the top materialization when narrowing to an unspecialized generic class + strict-generic-narrowing = true + ``` + +=== "ty.toml" + + ```toml + [overrides.analysis] + # Use the top materialization when narrowing to an unspecialized generic class + strict-generic-narrowing = true + ``` + +--- + ## `run` ### `main` @@ -1982,6 +2068,33 @@ to re-include `dist` use `exclude = ["!dist"]` --- +### `exclude-scripts` + +Whether to exclude files containing PEP 723 inline script metadata unless they are +explicitly passed on the command line. + +**Default value**: `false` + +**Type**: `bool` + +**Example usage**: + +=== "pyproject.toml" + + ```toml + [tool.ty.src] + exclude-scripts = true + ``` + +=== "ty.toml" + + ```toml + [src] + exclude-scripts = true + ``` + +--- + ### `include` A list of files and directories to check. The `include` option @@ -2061,43 +2174,6 @@ Enabled by default. --- -### `root` - -!!! warning "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. -The project root (`.`) is always included. Additionally, the following directories are included -if they exist and are not packages (i.e. they do not contain `__init__.py` or `__init__.pyi` files): - -* `./src` -* `./` (if a `.//` directory exists) -* `./python` - -**Default value**: `null` - -**Type**: `str` - -**Example usage**: - -=== "pyproject.toml" - - ```toml - [tool.ty.src] - root = "./app" - ``` - -=== "ty.toml" - - ```toml - [src] - root = "./app" - ``` - ---- - ## `terminal` ### `error-on-warning` diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 5098fe1ee8..ed69cca418 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -2,13 +2,49 @@ # Rules +## `abstract-and-final-method` + + +Default level: error · +Added in 0.0.64 · +Related issues · +View source + + + +**What it does** + + +Checks for methods decorated with both `@abstractmethod` and `@final`. + +**Why is this bad?** + + +An abstract method must be overridden for a subclass to become concrete, but a final +method cannot be overridden. Combining the decorators therefore makes it impossible +for a subclass to provide a concrete implementation. + +**Example** + + +```python +from abc import ABC, abstractmethod +from typing import final + + +class Base(ABC): + @final + @abstractmethod + def method(self) -> None: ... # error +``` + ## `abstract-method-in-final-class` Default level: error · Added in 0.0.13 · Related issues · -View source +View source @@ -54,7 +90,7 @@ class Derived(Base): # error Default level: error · Added in 0.0.61 · Related issues · -View source +View source @@ -87,7 +123,7 @@ f(1, b=s1) # ok — explicit Default level: error · Added in 0.0.1-alpha.39 · Related issues · -View source +View source @@ -123,7 +159,7 @@ report(Celsius()) # error: two conversions apply Default level: error · Added in 0.0.1-alpha.3 · Related issues · -View source +View source @@ -158,7 +194,7 @@ extension list: Default level: warn · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -222,7 +258,7 @@ class SubProto(BaseProto, Protocol): Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -269,7 +305,7 @@ def _(x: int): Default level: ignore · Added in 0.0.57 · Related issues · -View source +View source @@ -305,7 +341,7 @@ value = unknown # ty: ignore[unresolved-reference] Default level: warn · Added in 0.0.61 · Related issues · -View source +View source @@ -348,7 +384,7 @@ a4 = True + 1 # ok — a boolean used as a boolean Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -403,7 +439,7 @@ Foo.method() # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -431,7 +467,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. Default level: error · Added in 0.0.7 · Related issues · -View source +View source @@ -466,7 +502,7 @@ def f(x: object): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -500,7 +536,7 @@ a = 1 # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -535,7 +571,7 @@ class C(A, B): ... # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -571,7 +607,7 @@ class B(A): ... # error Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -607,7 +643,7 @@ type B = A # error Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -644,7 +680,7 @@ class Example: Default level: warn · Added in 0.0.1-alpha.16 · Related issues · -View source +View source @@ -683,7 +719,7 @@ old_func() # error: [deprecated] Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -716,7 +752,7 @@ false positives it can produce. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -747,7 +783,7 @@ class B(A, A): ... # error Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -790,7 +826,7 @@ class A: # error Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -840,7 +876,7 @@ def bar() -> str: # error: [empty-body] Default level: warn · Added in 0.0.61 · Related issues · -View source +View source @@ -901,7 +937,7 @@ def h(x: object): Default level: error · Added in 0.0.1-alpha.3 · Related issues · -View source +View source @@ -979,7 +1015,7 @@ def foo() -> "intt\b": ... # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1018,7 +1054,7 @@ def f(local fn: () -> None): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1059,7 +1095,7 @@ for x in [1, 2, 3]: Default level: warn · Added in 0.0.50 · Related issues · -View source +View source @@ -1099,7 +1135,7 @@ def g(value: ~A) -> None: ... # error: [experimental-syntax] Default level: error · Added in 0.0.20 · Related issues · -View source +View source @@ -1134,7 +1170,7 @@ def my_function() -> int: Default level: error · Added in 0.0.40 · Related issues · -View source +View source @@ -1169,7 +1205,7 @@ let a = 1 Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -1206,7 +1242,7 @@ INITIALIZED_CONSTANT: Final[int] = 1 Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1285,7 +1321,7 @@ def test() -> "Literal[5]": Default level: warn · Added in 0.0.68 · Related issues · -View source +View source @@ -1360,7 +1396,7 @@ print(Labelled) # warning: prints `` Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1396,7 +1432,7 @@ class C(A, B): ... # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1426,7 +1462,7 @@ t[3] # error Default level: warn · Added in 0.0.1-alpha.33 · Related issues · -View source +View source @@ -1463,7 +1499,7 @@ class MyClass: ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -1564,7 +1600,7 @@ an atypical memory layout. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1596,7 +1632,7 @@ func("foo") # error: [invalid-argument-type] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1627,7 +1663,7 @@ a: int = "" # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1685,7 +1721,7 @@ C.instance_only_var = 56 # error Default level: error · Added in 0.0.33 · Related issues · -View source +View source @@ -1731,7 +1767,7 @@ class Sub(Base): Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -1773,7 +1809,7 @@ asyncio.run(main()) Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1800,7 +1836,7 @@ class A(42): ... # error: [invalid-base] Default level: error · Added in 0.0.1-alpha.5 · Related issues · -View source +View source @@ -1837,7 +1873,7 @@ extension str(A): # error: `str` does not answer every member of `A` Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1867,7 +1903,7 @@ with 1: # error Default level: error · Added in 0.0.1-alpha.39 · Related issues · -View source +View source @@ -1900,7 +1936,7 @@ class Fahrenheit: Default level: error · Added in 0.0.12 · Related issues · -View source +View source @@ -1953,7 +1989,7 @@ See: Default level: error · Added in 0.0.13 · Related issues · -View source +View source @@ -1989,7 +2025,7 @@ class A: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2021,7 +2057,7 @@ a: str # error Default level: warn · Added in 0.0.20 · Related issues · -View source +View source @@ -2078,7 +2114,7 @@ class Pet(Enum): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2142,7 +2178,7 @@ This rule corresponds to Ruff's [`except-with-non-exception-classes` (`B030`)](h Default level: error · Added in 0.0.1-alpha.28 · Related issues · -View source +View source @@ -2195,7 +2231,7 @@ class D(A): Default level: error · Added in 0.0.1-alpha.3 · Related issues · -View source +View source @@ -2228,7 +2264,7 @@ extension list[T: int]: # error: `list` declares no type parameter `T` Default level: error · Added in 0.0.1-alpha.4 · Related issues · -View source +View source @@ -2257,7 +2293,7 @@ Author.objects.filter(name__startswith=1) # error: lookup wants `str` Default level: error · Added in 0.0.1-alpha.36 · Related issues · -View source +View source @@ -2293,7 +2329,7 @@ def test_user(user: int) -> None: # error: fixture provides `str` Default level: error · Added in 0.0.68 · Related issues · -View source +View source @@ -2341,7 +2377,7 @@ f"{'name':>10}" # ok Default level: error · Added in 0.0.1-alpha.35 · Related issues · -View source +View source @@ -2392,7 +2428,7 @@ class NonFrozenChild(FrozenBase): # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2447,7 +2483,7 @@ class E(Generic[V]): # error Default level: error · Added in 0.0.12 · Related issues · -View source +View source @@ -2509,7 +2545,7 @@ x: G[int] Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2543,7 +2579,7 @@ a = 20 / 0 # type: ignore Default level: error · Added in 0.0.1-alpha.17 · Related issues · -View source +View source @@ -2591,7 +2627,7 @@ carol = Person(name="Carol", aeg=25) # typo! Default level: warn · Added in 0.0.15 · Related issues · -View source +View source @@ -2653,7 +2689,7 @@ def f(x, y, /): # Python 3.8+ syntax Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2693,7 +2729,7 @@ def f(t: TypeVar("U")): ... # ty: ignore[invalid-type-form] Default level: error · Added in 0.0.18 · Related issues · -View source +View source @@ -2743,7 +2779,7 @@ match object(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2778,7 +2814,7 @@ class B(metaclass=42): ... # error Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -2896,7 +2932,7 @@ Correct use of `@override` is enforced by ty's [`invalid-explicit-override`](#in Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -2947,13 +2983,23 @@ without a type annotation will raise an `AttributeError` at runtime. AttributeError: Cannot overwrite NamedTuple attribute _asdict ``` +Finally, `NamedTuple` field annotations cannot use the `ClassVar` or `Final` type +qualifiers. These qualifiers also cause a runtime error when annotations are evaluated eagerly: + +```pycon +>>> from typing import ClassVar, NamedTuple +>>> class Foo(NamedTuple): +... x: ClassVar[int] +TypeError: typing.ClassVar[int] is not valid as type argument +``` + ## `invalid-named-tuple-override` Default level: warn · Added in 0.0.31 · Related issues · -View source +View source @@ -3001,7 +3047,7 @@ admin[0] # "Alice" Default level: error · Added in 0.0.1-alpha.27 · Related issues · -View source +View source @@ -3039,7 +3085,7 @@ Baz = NewType("Baz", int | str) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3096,7 +3142,7 @@ def foo(x: int) -> int: ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3125,7 +3171,7 @@ def f(a: int = ""): ... # error Default level: error · Added in 0.0.1-alpha.36 · Related issues · -View source +View source @@ -3158,7 +3204,7 @@ def test_add(a: int, b: int) -> None: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3194,7 +3240,7 @@ P2 = ParamSpec() # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3230,7 +3276,7 @@ TypeError: Protocols can only inherit from other protocols, got Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3301,7 +3347,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.37 · Related issues · -View source +View source @@ -3329,7 +3375,7 @@ def f() raises int: # error: `int` is not an exception Default level: error · Added in 0.0.1-alpha.36 · Related issues · -View source +View source @@ -3362,7 +3408,7 @@ if m := re.match("(a)(b)", "ab"): Default level: error · Added in 0.0.62 · Related issues · -View source +View source @@ -3393,7 +3439,7 @@ class C[reified T]: # error: a class type parameter is never reified Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3539,7 +3585,7 @@ def detail(request, pk: int): ... # ok Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3650,7 +3696,7 @@ class C: ... Default level: error · Added in 0.0.10 · Related issues · -View source +View source @@ -3701,7 +3747,7 @@ class MyClass: Default level: error · Added in 0.0.1-alpha.6 · Related issues · -View source +View source @@ -3724,7 +3770,7 @@ python-version = "3.12" ``` ```python -from typing import TypeAliasType +from typing import TypeAliasType, TypeVar def get_name() -> str: @@ -3734,6 +3780,11 @@ def get_name() -> str: IntOrStr = TypeAliasType("IntOrStr", int | str) # okay # TypeAliasType name must be a string literal NewAlias = TypeAliasType(get_name(), int) # error + +T = TypeVar("T") +GenericAlias = TypeAliasType("GenericAlias", list[T], type_params=(T,)) # okay +# TypeAliasType type parameters must be type variables +InvalidAlias = TypeAliasType("InvalidAlias", list[T], type_params=(list[T],)) # error ``` ## `invalid-type-arguments` @@ -3742,7 +3793,7 @@ NewAlias = TypeAliasType(get_name(), int) # error Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -3809,7 +3860,7 @@ Bar[int] # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3842,7 +3893,7 @@ TYPE_CHECKING = "" # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3878,7 +3929,7 @@ b: Annotated[int] # error Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -3935,7 +3986,7 @@ class C: Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -3979,7 +4030,7 @@ def g[U, T: U](): ... # error: [invalid-type-variable-bound] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4036,7 +4087,7 @@ V = TypeVar("V", list[int], int) # valid constrained Type Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -4078,7 +4129,7 @@ U = TypeVar("U", int, str, default=bytes) # error: [invalid-type-variable-defau Default level: error · Added in 0.0.28 · Related issues · -View source +View source @@ -4114,7 +4165,7 @@ class Child(Base): Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -4157,7 +4208,7 @@ def f(options: dict[str, object]): Default level: error · Added in 0.0.9 · Related issues · -View source +View source @@ -4192,7 +4243,7 @@ class Foo(TypedDict): Default level: error · Added in 0.0.62 · Related issues · -View source +View source @@ -4233,7 +4284,7 @@ type Alias[out T] = list[T] # error: `list` is invariant Default level: error · Added in 0.0.25 · Related issues · -View source +View source @@ -4268,7 +4319,7 @@ def gen() -> Iterator[int]: Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -4335,7 +4386,7 @@ def h(arg2: type): Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -4385,7 +4436,7 @@ def g(arg: object): Default level: warn · Added in 0.0.1-alpha.36 · Related issues · -View source +View source @@ -4416,7 +4467,7 @@ def f(s: str): Default level: warn · Added in 0.0.30 · Related issues · -View source +View source @@ -4459,7 +4510,7 @@ Movie = TypedDict("Film", {"title": str}) # error: [mismatched-type-name] Default level: warn · Added in 0.0.1-alpha.39 · Related issues · -View source +View source @@ -4527,7 +4578,7 @@ and nothing is reported. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4558,7 +4609,7 @@ func() # error Default level: error · Added in 0.0.61 · Related issues · -View source +View source @@ -4589,7 +4640,7 @@ f(1) # ok — `s` is passed implicitly Default level: warn · Added in 0.0.1-alpha.4 · Related issues · -View source +View source @@ -4617,7 +4668,7 @@ from django.db import models # warning: install `django-stubs` for precise type Default level: ignore · Added in 0.0.41 · Related issues · -View source +View source @@ -4676,7 +4727,7 @@ class ExplicitChild(Parent): Default level: ignore · Added in 0.0.45 · Related issues · -View source +View source @@ -4715,7 +4766,7 @@ def handle(m: re.Match[str]) -> str: Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -4754,7 +4805,7 @@ alice["age"] # KeyError Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4788,7 +4839,7 @@ def f(a: int | None): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4826,7 +4877,7 @@ func("string") # error: [no-matching-overload] Default level: error · Added in 0.0.30 · Related issues · -View source +View source @@ -4864,7 +4915,7 @@ class Sub(Super): ... # error: [non-callable-init-subclass] Default level: error · Added in 0.0.1-alpha.39 · Related issues · -View source +View source @@ -4895,7 +4946,7 @@ def f(x: int | str) -> int: Default level: warn · Added in 0.0.61 · Related issues · -View source +View source @@ -4924,7 +4975,7 @@ def f(a: object): Default level: warn · Added in 0.0.62 · Related issues · -View source +View source @@ -4968,7 +5019,7 @@ def g(o: object, shape: Shape): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4997,7 +5048,7 @@ for i in 34: # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -5025,7 +5076,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -5054,7 +5105,7 @@ def f(once done: () -> None): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -5082,7 +5133,7 @@ def f(once done: () -> None): Default level: warn · Added in 0.0.61 · Related issues · -View source +View source @@ -5120,7 +5171,7 @@ def f(x: int?): Default level: warn · Added in 0.0.62 · Related issues · -View source +View source @@ -5175,7 +5226,7 @@ def g(name: str | None): Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -5212,7 +5263,7 @@ class B(A): Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -5249,7 +5300,7 @@ class B(A): Default level: ignore · Added in 0.0.1-alpha.38 · Related issues · -View source +View source @@ -5292,7 +5343,7 @@ def main(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -5323,7 +5374,7 @@ f(1, x=2) # error Default level: error · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -5354,7 +5405,7 @@ f(x=1) # error Default level: ignore · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -5393,7 +5444,7 @@ A.c # error Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -5432,7 +5483,7 @@ A()[0] # error Default level: ignore · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -5478,7 +5529,7 @@ from module import a # error Default level: warn · Added in 0.0.23 · Related issues · -View source +View source @@ -5510,7 +5561,7 @@ html.parser # error Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -5547,7 +5598,7 @@ print(x) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -5579,7 +5630,7 @@ from helpers import Key # error: `Key` is private to `helpers` Default level: warn · Added in 0.0.60 · Related issues · -View source +View source @@ -5654,7 +5705,7 @@ def test() -> "int": Default level: warn · Added in 0.0.62 · Related issues · -View source +View source @@ -5696,7 +5747,7 @@ def g(a: bool | None): Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -5731,7 +5782,7 @@ cast(int, f()) # error Default level: warn · Added in 0.0.62 · Related issues · -View source +View source @@ -5785,7 +5836,7 @@ if sys.version_info >= (3, 12): # ok — artificially constant Default level: warn · Added in 0.0.18 · Related issues · -View source +View source @@ -5823,7 +5874,7 @@ class C: Default level: warn · Added in 0.0.62 · Related issues · -View source +View source @@ -5878,7 +5929,7 @@ class Sub(Base): Default level: error · Added in 0.0.62 · Related issues · -View source +View source @@ -5928,7 +5979,7 @@ def f(value: int | str) -> int: Default level: error · Added in 0.0.1-alpha.3 · Related issues · -View source +View source @@ -5961,7 +6012,7 @@ class C: Default level: error · Added in 0.0.20 · Related issues · -View source +View source @@ -6005,7 +6056,7 @@ class Outer[T]: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -6040,7 +6091,7 @@ static_assert(int(2.0 * 3.0) == 6) # error Default level: warn · Added in 0.0.39 · Related issues · -View source +View source @@ -6091,7 +6142,7 @@ Consider using [`functools.total_ordering`][total_ordering] instead, which does Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -6125,7 +6176,7 @@ class B(A): ... # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -6157,7 +6208,7 @@ class Circle(Shape): ... # error: `Shape` is sealed in another workspace Default level: error · Added in 0.0.1-alpha.30 · Related issues · -View source +View source @@ -6266,7 +6317,7 @@ class Book: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -6296,7 +6347,7 @@ f("foo") # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -6335,7 +6386,7 @@ def find(items: list[int]) -> int: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -6369,7 +6420,7 @@ f: # error: the block binds only `it`, so `"two"` has nowhere to go Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -6404,7 +6455,7 @@ f: # error: the block returns `None`, not `str` Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -6443,7 +6494,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.4 · Related issues · -View source +View source @@ -6474,7 +6525,7 @@ class User(BaseModel): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -6532,7 +6583,7 @@ class A: Default level: error · Added in 0.0.20 · Related issues · -View source +View source @@ -6605,7 +6656,7 @@ the project registers with `@register.simple_block_tag`. Default level: warn · Added in 0.0.1-alpha.39 · Related issues · -View source +View source @@ -6659,7 +6710,7 @@ reported. Default level: error · Added in 0.0.1-alpha.37 · Related issues · -View source +View source @@ -6688,7 +6739,7 @@ def f() raises TypeError: Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -6717,7 +6768,7 @@ reveal_type(1) # revealed: Literal[1] Default level: error · Added in 0.0.1-alpha.37 · Related issues · -View source +View source @@ -6747,7 +6798,7 @@ def main(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -6778,7 +6829,7 @@ f(x=1, y=2) # error Default level: ignore · Added in 0.0.1-alpha.36 · Related issues · -View source +View source @@ -6989,7 +7040,7 @@ page does not render at all. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -7022,7 +7073,7 @@ A().foo # error Default level: warn · Added in 0.0.1-alpha.15 · Related issues · -View source +View source @@ -7097,7 +7148,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -7126,7 +7177,7 @@ import foo # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -7156,7 +7207,7 @@ def check(value: int | None) -> asserts values: # error: `values` is nothing Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -7268,13 +7319,299 @@ is one whose template set cannot be established. {% extends "blog/bass.html" %} {# error: the template is `blog/base.html` #} ``` +## `unsound-return-statement` + + +Default level: ignore · +Added in 0.0.70 · +Related issues · +View source + + + +**What it does** + + +Detects `return` statements that unsoundly return a type that is not a [subtype] of the function's +annotated return type. + +This lint is a stricter version of [`invalid-return-type`](#invalid-return-type). + +**Why is this bad?** + + +By default, type checkers consider a `return` statement valid if the inferred type of the object +being returned is [assignable] to the annotated return type of the function it's in. However, this +makes it easy for incorrect types to percolate through your code unexpectedly due to a single +expression being inferred as `Any`. This can easily lead to runtime errors that are not caught by +the type checker: + +```py +from typing import Any + + +def returns_any() -> Any: + return "foo" + + +def returns_int() -> int: + # error: "Unsound return statement: `Any` is not a subtype of `int`" + return returns_any() + + +# fails at runtime, even though the type checker infers both operands as being of type `int`! +returns_int() + 42 +``` + +This rule allows you to use ["fully static"][fully-static] return types as "typed boundaries" for +your code. With this rule enabled, ty would emit an error on the `return returns_any()` statement +in `returns_int`, since the `returns_any()` call is inferred as having type `Any`, and `Any` is not +a subtype of `int`. This helps prevent the unsoundness from spreading far from its original source +(in this case, the return type of the `returns_any` function). + +Note that this rule is only applied to functions annotated as returning +[fully static][fully-static] types. It will not trigger if `Any` or `Unknown` appear anywhere in +your return type, either implicitly or explicitly: + +```py +from typing import Any + + +def returns_any() -> Any: + return "foo" + + +# error: [missing-type-argument] +def returns_unparameterized_tuple() -> tuple: + # no error, since the return type is implicitly `tuple[Unknown, ...]` + # (which is what the `missing-type-argument` error is complaining about on the line above!) + return returns_any() + + +def returns_list_of_any() -> list[Any]: + # no error, since the return type is explicitly `list[Any]` + return returns_any() +``` + +This rule works especially well when combined with ty's +[`missing-type-argument`](#missing-type-argument) rule, and the Ruff rules [`ANN201`][ann201], +[`ANN202`][ann202], [`ANN204`][ann204], [`ANN205`][ann205], and [`ANN206`][ann206]. Enabling all +these rules at once effectively makes it much less likely that a `return` statement can lead to +unsoundness "leaking" out of a function unless that function has been *explicitly* annotated with +a dynamic type in some way (`-> Any` or `-> tuple[Any]`, for example). + +This rule is analogous to mypy's [`no-any-return`][no-any-return] error code, which is enabled by +mypy’s [`--strict`][mypy-strict] mode and can also be enabled on its own using mypy’s +[`--warn-return-any`][warn-return-any] option. + +**Examples** + + +```py +from typing import Any + + +def returns_any() -> Any: + return 42 + + +def returns_int() -> int: + # error: "Unsound return statement: `Any` is not a subtype of `int`" + return returns_any() +``` + +Narrow the type to a subtype of `int` to fix the diagnostic: + +```py +from typing import Any +from typing_extensions import reveal_type + + +def returns_any() -> Any: + return 42 + + +def returns_int() -> int: + my_int = returns_any() + assert isinstance(my_int, int) + reveal_type(my_int) # revealed: Any & int + return my_int # no error: `Any & int` is a subtype of `int` +``` + +**Default level** + + +This rule is disabled by default. It is intended for advanced users wanting additional soundness +checks from their type checker, not for users who have just started to use type checkers on their +Python code. + +**See also** + + +- [`unsound-yield`](#unsound-yield) is a similar rule that triggers on unsound `yield` expressions rather than unsound `return` statements + +[ann201]: https://docs.astral.sh/ruff/rules/missing-return-type-undocumented-public-function/ +[ann202]: https://docs.astral.sh/ruff/rules/missing-return-type-private-function/ +[ann204]: https://docs.astral.sh/ruff/rules/missing-return-type-special-method/ +[ann205]: https://docs.astral.sh/ruff/rules/missing-return-type-static-method/ +[ann206]: https://docs.astral.sh/ruff/rules/missing-return-type-class-method/ +[assignable]: https://typing.python.org/en/latest/spec/glossary.html#term-assignable +[fully-static]: https://typing.python.org/en/latest/spec/glossary.html#term-fully-static-type +[mypy-strict]: https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-strict +[no-any-return]: https://mypy.readthedocs.io/en/stable/error_code_list2.html#code-no-any-return +[subtype]: https://typing.python.org/en/latest/spec/glossary.html#term-subtype +[warn-return-any]: https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-warn-return-any + +## `unsound-yield` + + +Default level: ignore · +Added in 0.0.70 · +Related issues · +View source + + + +**What it does** + + +Detects `yield` and `yield from` expressions that unsoundly yield a type that is not a [subtype] of +the generator function's annotated yield type. + +This lint is a stricter version of [`invalid-yield`](#invalid-yield). + +**Why is this bad?** + + +By default, type checkers consider a yielded value valid if its inferred type is [assignable] to the +generator's annotated yield type. However, this +makes it easy for incorrect types to percolate through your code unexpectedly due to a single +expression being inferred as `Any`. This can easily lead to runtime errors that are not caught by +the type checker: + +```py +from typing import Any, Generator + + +def returns_any() -> Any: + return "not an integer" + + +def integers() -> Generator[int]: + # error: "Unsound `yield`: `Any` is not a subtype of `int`" + yield returns_any() + + +# Fails at runtime, even though the type checker infers `integers` as yielding only `int`s! +sum(integers()) +``` + +This rule treats [fully static][fully-static] yield types as "typed boundaries" for your code. With this rule enabled, ty would emit an error on the `yield returns_any()` statement +in `integers`, since the `returns_any()` call is inferred as having type `Any`, and `Any` is not +a subtype of `int`. This helps prevent the unsoundness from spreading far from its original source +(in this case, the return type of the `returns_any` function). + +Note that this rule is only applied to functions annotated as yielding +[fully static][fully-static] types. It will not trigger if `Any` or `Unknown` appear anywhere in +your function's yield type, either implicitly or explicitly. It will still trigger on functions that have non-fully-static send and/or return types, however: + +```py +from typing import Any, Generator + + +def returns_any() -> Any: + return "not an integer" + + +def dynamic_yield_type() -> Generator[Any]: + yield returns_any() + + +def static_yield_type() -> Generator[int, Any, Any]: + # error: "Unsound `yield`: `Any` is not a subtype of `int`" + yield returns_any() +``` + +This rule works especially well when combined with ty's +[`missing-type-argument`](#missing-type-argument) rule, and the Ruff rules [`ANN201`][ann201], +[`ANN202`][ann202], [`ANN204`][ann204], [`ANN205`][ann205], and [`ANN206`][ann206]. Enabling all +these rules at once effectively makes it much less likely that a `yield` expression can lead to +unsoundness "leaking" out of a function unless that function has been *explicitly* annotated with +a dynamic type in some way (`-> Generator[Any]` or `-> Generator[tuple[Any]]`, for example). + +**Examples** + + +```py +from typing import Any, Iterator + + +def returns_any() -> Any: + return "foo" + + +def any_iterator() -> Iterator[Any]: + yield "foo" + + +def integers() -> Iterator[int]: + # error: "Unsound `yield`: `Any` is not a subtype of `int`" + yield returns_any() + # error: "Unsound `yield from`: `Any` is not a subtype of `int`" + yield from any_iterator() +``` + +Narrow the value before yielding it to fix the diagnostics: + +```py +from typing import Any, Iterator + + +def returns_any() -> Any: + return 42 + + +def any_iterator() -> Iterator[Any]: + yield "foo" + + +def integers() -> Iterator[int]: + value = returns_any() + assert isinstance(value, int) + yield value + + for value in any_iterator(): + assert isinstance(value, int) + yield value +``` + +**Default level** + + +This rule is disabled by default. It is intended for users who want stricter soundness checks at +generator boundaries. + +**See also** + + +- [`unsound-return-statement`](#unsound-return-statement) is a similar rule that triggers on unsound `return` statements rather than unsound `yield` expressions + +[ann201]: https://docs.astral.sh/ruff/rules/missing-return-type-undocumented-public-function/ +[ann202]: https://docs.astral.sh/ruff/rules/missing-return-type-private-function/ +[ann204]: https://docs.astral.sh/ruff/rules/missing-return-type-special-method/ +[ann205]: https://docs.astral.sh/ruff/rules/missing-return-type-static-method/ +[ann206]: https://docs.astral.sh/ruff/rules/missing-return-type-class-method/ +[assignable]: https://typing.python.org/en/latest/spec/glossary.html#term-assignable +[fully-static]: https://typing.python.org/en/latest/spec/glossary.html#term-fully-static-type +[subtype]: https://typing.python.org/en/latest/spec/glossary.html#term-subtype + ## `unspecialized-reified-generic` Default level: error · Added in 0.0.1-alpha.3 · Related issues · -View source +View source @@ -7316,7 +7653,7 @@ g(1) # ok — transpiles to g[int](1) Default level: warn · Added in 0.0.1-alpha.7 · Related issues · -View source +View source @@ -7363,7 +7700,7 @@ class D(C): ... # error: [unsupported-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -7412,7 +7749,7 @@ b1 < b2 < b1 # error Default level: ignore · Added in 0.0.12 · Related issues · -View source +View source @@ -7459,7 +7796,7 @@ def factory(base: type[Base]) -> type: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -7492,7 +7829,7 @@ A() + A() # error Default level: warn · Added in 0.0.21 · Related issues · -View source +View source @@ -7530,7 +7867,7 @@ async def main() -> None: Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -7571,7 +7908,7 @@ to `false` to prevent this rule from reporting unused `type: ignore` comments. Default level: warn · Added in 0.0.14 · Related issues · -View source +View source @@ -7612,7 +7949,7 @@ to `false`. Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -7691,7 +8028,7 @@ def foo(x: int | str) -> int | str: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source diff --git a/crates/ty/src/args.rs b/crates/ty/src/args.rs index e94bdc2def..50e93e4fd6 100644 --- a/crates/ty/src/args.rs +++ b/crates/ty/src/args.rs @@ -266,11 +266,11 @@ pub(crate) struct CheckCommand { /// /// [`sys.prefix`]: https://docs.python.org/3/library/sys.html#sys.prefix #[arg(long, value_name = "PATH", alias = "venv")] - pub(crate) python: Option, + python: Option, /// Custom directory to use for stdlib typeshed stubs. #[arg(long, value_name = "PATH", alias = "custom-typeshed-dir")] - pub(crate) typeshed: Option, + typeshed: Option, /// Additional path to use as a module-resolution source (can be passed multiple times). /// @@ -278,7 +278,7 @@ pub(crate) struct CheckCommand { /// modules that are not installed into your Python environment in a conventional way. /// Use `--python` to point ty to your Python environment if it is in an unusual location. #[arg(long, value_name = "PATH")] - pub(crate) extra_search_path: Option>, + extra_search_path: Option>, /// Python version to assume when resolving types. /// @@ -293,7 +293,7 @@ pub(crate) struct CheckCommand { /// and attempt to infer the Python version of that environment /// 3. Fall back to the latest stable Python version supported by ty (see `ty check --help` output) #[arg(long, value_name = "VERSION", alias = "target-version", value_enum)] - pub(crate) python_version: Option, + python_version: Option, /// Target platform to assume when resolving types. /// @@ -302,16 +302,16 @@ pub(crate) struct CheckCommand { /// 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, + python_platform: Option, #[clap(flatten)] pub(crate) verbosity: Verbosity, #[clap(flatten)] - pub(crate) rules: RulesArg, + rules: RulesArg, #[clap(flatten)] - pub(crate) config: ConfigsArg, + config: ConfigsArg, /// The path to a `basedpython.toml` or `ty.toml` file to use for configuration. /// @@ -321,13 +321,13 @@ pub(crate) struct CheckCommand { /// The format to use for printing diagnostic messages. #[arg(long, env = EnvVars::TY_OUTPUT_FORMAT)] - pub(crate) output_format: Option, + output_format: Option, /// Use exit code 1 if there are any warning-level diagnostics. /// /// Cannot be used in combination with `--exit-zero` or `--exit-zero-on-warning`. #[arg(long, conflicts_with = "exit_zero", default_missing_value = "true", num_args=0..1)] - pub(crate) error_on_warning: Option, + error_on_warning: Option, /// Always use exit code 0, even when there are error-level diagnostics. /// @@ -339,7 +339,7 @@ pub(crate) struct CheckCommand { /// /// Cannot be used in combination with `--error-on-warning`. #[arg(long, conflicts_with = "error_on_warning")] - pub(crate) exit_zero_on_warning: bool, + exit_zero_on_warning: bool, /// Watch files for changes and recheck files related to the changed files. #[arg(long, short = 'W')] @@ -369,6 +369,19 @@ pub(crate) struct CheckCommand { #[clap(long, overrides_with("force_exclude"), hide = true)] no_force_exclude: bool, + /// Exclude files containing PEP 723 inline script metadata unless passed explicitly. + /// Use `--include-scripts` to disable. + #[arg( + long, + overrides_with("include_scripts"), + help_heading = "File selection", + default_missing_value = "true", + num_args = 0..1 + )] + exclude_scripts: Option, + #[clap(long, overrides_with("exclude_scripts"), hide = true)] + include_scripts: bool, + /// Glob patterns for files to exclude from type checking. /// /// Uses gitignore-style syntax to exclude files and directories from type checking. @@ -416,6 +429,10 @@ impl CheckCommand { .no_respect_ignore_files .then_some(false) .or(self.respect_ignore_files); + let exclude_scripts = self + .include_scripts + .then_some(false) + .or(self.exclude_scripts); let error_on_warning = self .exit_zero_on_warning .then_some(false) @@ -444,6 +461,7 @@ impl CheckCommand { }), src: Some(SrcOptions { respect_ignore_files, + exclude_scripts, exclude: self.exclude.map(|excludes| { RangedValue::cli(excludes.iter().map(RelativeGlobPattern::cli).collect()) }), @@ -517,7 +535,11 @@ impl clap::Args for RulesArg { clap::Arg::new("error") .long("error") .action(ArgAction::Append) - .help("Treat the given rule as having severity 'error'. Can be specified multiple times. Use 'all' to apply to all rules.") + .help( + "Treat the given rule as having severity 'error'. \ + Can be specified multiple times. \ + Use 'all' to apply to all rules.", + ) .value_name("RULE") .help_heading(HELP_HEADING), ) @@ -525,7 +547,11 @@ impl clap::Args for RulesArg { clap::Arg::new("warn") .long("warn") .action(ArgAction::Append) - .help("Treat the given rule as having severity 'warn'. Can be specified multiple times. Use 'all' to apply to all rules.") + .help( + "Treat the given rule as having severity 'warn'. \ + Can be specified multiple times. \ + Use 'all' to apply to all rules.", + ) .value_name("RULE") .help_heading(HELP_HEADING), ) @@ -533,7 +559,11 @@ impl clap::Args for RulesArg { clap::Arg::new("ignore") .long("ignore") .action(ArgAction::Append) - .help("Disables the rule. Can be specified multiple times. Use 'all' to apply to all rules.") + .help( + "Disables the rule. \ + Can be specified multiple times. \ + Use 'all' to apply to all rules.", + ) .value_name("RULE") .help_heading(HELP_HEADING), ) @@ -662,7 +692,7 @@ over all configuration files.", } impl ConfigsArg { - pub(crate) fn into_options(self) -> Option { + fn into_options(self) -> Option { self.0 } } diff --git a/crates/ty/src/by_commands.rs b/crates/ty/src/by_commands.rs index 9947bda80f..620df4ed27 100644 --- a/crates/ty/src/by_commands.rs +++ b/crates/ty/src/by_commands.rs @@ -33,7 +33,9 @@ fn configured_min_version(cwd: &Path) -> PythonVersion { return Config::default().min_version; }; let db = ProjectDatabase::use_defaults(metadata, system); - ruff_db::Db::python_version(&db) + db.project() + .program(&db) + .python_version(&db) .to_string() .parse() .unwrap_or_else(|_| Config::default().min_version) @@ -270,10 +272,13 @@ pub(crate) fn cmd_run( /// src-layout project, `src/` before the project root. Only roots inside the /// project are kept: an emitted tree can only mirror what is being built. fn module_roots(db: &ProjectDatabase, cwd: &Path) -> Vec { - let mut roots: Vec = ty_module_resolver::system_module_search_paths(db) - .map(|path| PathBuf::from(path.as_str())) - .filter(|path| path.starts_with(cwd)) - .collect(); + let mut roots: Vec = ty_module_resolver::system_module_search_paths( + db, + db.project().program(db).resolver_environment(db), + ) + .map(|path| PathBuf::from(path.as_str())) + .filter(|path| path.starts_with(cwd)) + .collect(); // a nested root shadows the one containing it, so the deepest match wins roots.sort_by_key(|root| std::cmp::Reverse(root.components().count())); roots @@ -463,8 +468,9 @@ pub(crate) fn cmd_compile( .and_then(|stem| stem.to_str()) .context("a source file has no usable module name")?; - let parsed = ruff_db::parsed::parsed_module(&db, *file).load(&db); - let model = ty_python_semantic::SemanticModel::new(&db, *file); + let program_file = ty_python_semantic::Db::program_file(&db, *file); + let parsed = ruff_db::parsed::parsed_module(&db, program_file.python_file(&db)).load(&db); + let model = ty_python_semantic::SemanticModel::new(&db, program_file); // a `.py` source needs no transpiling to be its own interpreted fallback let mut options = options.clone(); if path.extension().is_some_and(|x| x == "py") { @@ -472,6 +478,7 @@ pub(crate) fn cmd_compile( } let mut lowered = by_irbuild::build_module( &db, + &model.program_environment(), &model, parsed.suite(), module, @@ -1160,7 +1167,6 @@ fn render_diagnostics(db: &ProjectDatabase, diagnostics: &[Diagnostic]) -> anyho let display_config = DisplayDiagnosticConfig::new("ty") .color(colored::control::SHOULD_COLORIZE.should_colorize()) - .show_fix_diff(true) .context(0); let mut stderr = std::io::stderr().lock(); write!( diff --git a/crates/ty/src/lib.rs b/crates/ty/src/lib.rs index 529151d403..d496342e88 100644 --- a/crates/ty/src/lib.rs +++ b/crates/ty/src/lib.rs @@ -21,7 +21,7 @@ use ruff_db::diagnostic::{ Diagnostic, DiagnosticId, DisplayDiagnosticConfig, DisplayDiagnostics, Severity, UnifiedFile, }; use ruff_db::files::File; -use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf}; +use ruff_db::system::{OsSystem, System, SystemPath, SystemPathBuf}; use ruff_db::{STACK_SIZE, max_parallelism}; use ruff_diagnostics::Applicability; use salsa::Database; @@ -152,6 +152,7 @@ fn run_generate_api_file( use ruff_ranged_value::RangedValue; use ty_project::metadata::options::EnvironmentOptions; use ty_project::metadata::value::RelativePathBuf; + use ty_python_semantic::ProgramEnvironment; use ty_python_semantic::api_lockfile::generate_api_lockfile; let cwd = { @@ -220,9 +221,12 @@ fn run_generate_api_file( { return false; } - ty_module_resolver::file_to_module(&db, *file) - .and_then(|module| module.search_path(&db).cloned()) - .is_some_and(|sp| sp.is_first_party()) + ty_module_resolver::file_to_module( + &db, + ty_python_semantic::Db::program_file(&db, *file).resolver_file(&db), + ) + .and_then(|module| module.search_path(&db).cloned()) + .is_some_and(|sp| sp.is_first_party()) }) .collect(); first_party_files.sort_by_key(|file| file.path(&db).as_str().to_string()); @@ -230,7 +234,11 @@ fn run_generate_api_file( let python_version_str = python_version .map(|v| v.to_string()) .unwrap_or_else(|| "default".to_owned()); - let lockfile = generate_api_lockfile(&db, first_party_files, &python_version_str); + let env = first_party_files + .first() + .map(|file| ProgramEnvironment::from_file(ty_python_semantic::Db::program_file(&db, *file))) + .unwrap_or_else(|| ProgramEnvironment::from_program(db.project().program(&db))); + let lockfile = generate_api_lockfile(&db, &env, first_party_files, &python_version_str); if stdout { use std::io::Write; @@ -263,13 +271,13 @@ fn run_check(args: CheckCommand) -> anyhow::Result { // 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() - ) - })? + 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 @@ -315,9 +323,20 @@ fn run_check(args: CheckCommand) -> anyhow::Result { Some(config_file) => { ProjectMetadata::from_config_file(config_file.clone(), &project_path, &system)? } + None if check_paths.iter().any(|path| system.is_file(path)) => { + // `uv check --script` passes a file as its check path. Disable uv workspace metadata + // for scripts until script integration is implemented in a follow-up. + ProjectMetadata::discover_without_uv(&project_path, &system)? + } None => ProjectMetadata::discover(&project_path, &system)?, }; + if watch && project_metadata.has_uv_workspace() { + return Err(anyhow!( + "`--watch` is not supported with uv workspace integration" + )); + } + project_metadata.apply_configuration_files(&system)?; project_metadata.apply_override_options(args.into_options()); @@ -381,7 +400,8 @@ fn run_check(args: CheckCommand) -> anyhow::Result { Some("json") => writeln!(stdout, "{}", db.salsa_memory_dump().to_json())?, Some(other) => { tracing::warn!( - "Unknown value for `TY_MEMORY_REPORT`: `{other}`. Valid values are `short`, `full`, and `json`." + "Unknown value for `TY_MEMORY_REPORT`: `{other}`. \ + Valid values are `short`, `full`, and `json`." ); } None => {} @@ -420,7 +440,7 @@ pub enum ExitStatus { } impl ExitStatus { - pub const fn is_internal_error(self) -> bool { + const fn is_internal_error(self) -> bool { matches!(self, ExitStatus::InternalError) } } @@ -440,6 +460,10 @@ struct MainLoop { /// Receiver for the messages sent **to** the main loop. receiver: crossbeam_channel::Receiver, + /// Capacity-one channel used to coalesce pending workspace checks. + check_sender: crossbeam_channel::Sender<()>, + check_receiver: crossbeam_channel::Receiver<()>, + /// The file system watcher, if running in watch mode. watcher: Option, @@ -455,6 +479,7 @@ struct MainLoop { impl MainLoop { fn new(mode: MainLoopMode, printer: Printer) -> (Self, MainLoopCancellationToken) { let (sender, receiver) = crossbeam_channel::bounded(10); + let (check_sender, check_receiver) = crossbeam_channel::bounded(1); let cancellation_token_source = CancellationTokenSource::new(); let cancellation_token = cancellation_token_source.token(); @@ -464,6 +489,8 @@ impl MainLoop { mode, sender: sender.clone(), receiver, + check_sender, + check_receiver, watcher: None, printer, cancellation_token, @@ -487,7 +514,7 @@ impl MainLoop { } fn run(self, db: &mut ProjectDatabase) -> Result { - self.sender.send(MainLoopMessage::CheckWorkspace).unwrap(); + self.request_check(); let result = self.main_loop(db); @@ -496,13 +523,22 @@ impl MainLoop { result } + fn request_check(&self) { + // A pending request already represents a check of the latest database revision. + let _ = self.check_sender.try_send(()); + } + 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() { + // Apply all queued changes before starting a pending check because every applied change + // cancels the running check. + while let Ok(message) = crossbeam_channel::select_biased! { + recv(self.receiver) -> message => message, + recv(self.check_receiver) -> request => request.map(|()| MainLoopMessage::CheckWorkspace), + } { match message { MainLoopMessage::CheckWorkspace => { let db = db.clone(); @@ -539,7 +575,8 @@ impl MainLoop { } => { if check_revision != revision { tracing::debug!( - "Discarding check result for outdated revision: current: {revision}, result revision: {check_revision}" + "Discarding check result for outdated revision: \ + current: {revision}, result revision: {check_revision}" ); continue; } @@ -629,7 +666,9 @@ impl MainLoop { if exit_status.is_internal_error() { tracing::warn!( - "A fatal error occurred while checking some files. Not all project files were analyzed. See the diagnostics list above for details." + "A fatal error occurred while checking some files. \ + Not all project files were analyzed. \ + See the diagnostics list above for details." ); } @@ -650,7 +689,7 @@ impl MainLoop { watcher.update(db); } - self.sender.send(MainLoopMessage::CheckWorkspace).unwrap(); + self.request_check(); } MainLoopMessage::Exit => { // Cancel any pending queries and wait for them to complete. @@ -695,7 +734,6 @@ impl MainLoop { .format(terminal_settings.output_format.into()) .color(colored::control::SHOULD_COLORIZE.should_colorize()) .with_cancellation_token(Some(self.cancellation_token.clone())) - .show_fix_diff(true) .context(0); write!( @@ -711,7 +749,8 @@ impl MainLoop { let total = fixed + diagnostics_count; writeln!( self.printer.stream_for_failure_summary(), - "Found {total} diagnostic{} ({fixed} fixed, {diagnostics_count} remaining).", + "Found {total} diagnostic{} \ + ({fixed} fixed, {diagnostics_count} remaining).", if total == 1 { "" } else { "s" } )?; } else { diff --git a/crates/ty/src/logging.rs b/crates/ty/src/logging.rs index 0e1c4a9efd..0f891e78d3 100644 --- a/crates/ty/src/logging.rs +++ b/crates/ty/src/logging.rs @@ -97,11 +97,11 @@ impl VerbosityLevel { } } - pub(crate) const fn is_trace(self) -> bool { + const fn is_trace(self) -> bool { matches!(self, VerbosityLevel::Trace) } - pub(crate) const fn is_extra_verbose(self) -> bool { + const fn is_extra_verbose(self) -> bool { matches!(self, VerbosityLevel::ExtraVerbose) } } diff --git a/crates/ty/src/main.rs b/crates/ty/src/main.rs index 4375444983..4838868743 100644 --- a/crates/ty/src/main.rs +++ b/crates/ty/src/main.rs @@ -18,7 +18,7 @@ use ty::{ExitStatus, run}; #[global_allocator] static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; -pub fn main() -> ExitStatus { +fn main() -> ExitStatus { run().unwrap_or_else(|error| { use io::Write; diff --git a/crates/ty/tests/cli/analysis_options.rs b/crates/ty/tests/cli/analysis_options.rs index 40a29be16b..1e7be8186e 100644 --- a/crates/ty/tests/cli/analysis_options.rs +++ b/crates/ty/tests/cli/analysis_options.rs @@ -31,7 +31,6 @@ fn respect_type_ignore_comments_is_turned_off() -> anyhow::Result<()> { | 2 | y = a + 5 # type: ignore | ^ - | Found 1 diagnostic @@ -81,7 +80,6 @@ fn overrides_basic() -> anyhow::Result<()> { | 2 | print(x) # type: ignore # ignore not-respected (override) | ^ - | Found 1 diagnostic @@ -137,7 +135,6 @@ fn overrides_precedence() -> anyhow::Result<()> { | 2 | print(y) # type: ignore (should be an error, because type ignores are disabled) | ^ - | Found 1 diagnostic @@ -189,14 +186,12 @@ fn overrides_inherit_global() -> anyhow::Result<()> { | 2 | print(y) # type: ignore ignore not-respected (global) | ^ - | error[unresolved-reference]: Name `y` used when not defined --> tests/test_main.py:2:7 | 2 | print(y) # type: ignore ignore respected (inherited from global) | ^ - | Found 2 diagnostics @@ -272,13 +267,11 @@ fn sound_types_is_per_module() -> anyhow::Result<()> { | 5 | f("wrong") | ^^^^^^^ Argument type `Literal["wrong"]` does not satisfy `int`, inferred for parameter `a` - | info: Parameter declared here --> sound/lib.py:2:7 | 2 | def f(a=1): ... | ^ - | Found 1 diagnostic @@ -343,7 +336,7 @@ fn precise_unsolved_typevars_is_per_module() -> anyhow::Result<()> { ), ])?; - assert_cmd_snapshot!(case.command(), @r" + assert_cmd_snapshot!(case.command(), @" success: true exit_code: 0 ----- stdout ----- @@ -352,14 +345,12 @@ fn precise_unsolved_typevars_is_per_module() -> anyhow::Result<()> { | 6 | reveal_type(f()) | ^^^ `Never` - | info[revealed-type]: Revealed type --> precise/main.py:6:13 | 6 | reveal_type(g()) | ^^^ `Unknown` - | Found 2 diagnostics @@ -438,13 +429,12 @@ fn bivariant_private_attributes_is_per_module() -> anyhow::Result<()> { exit_code: 1 ----- stdout ----- error[invalid-assignment]: Object of type `Covariant[A]` is not assignable to `Covariant[B]` - --> bivariant/main.py:9:11 + --> bivariant/main.py:9:26 | 9 | narrowed: Covariant[B] = Covariant[A]() | ------------ ^^^^^^^^^^^^^^ Incompatible value of type `Covariant[A]` | | | Declared type - | Found 1 diagnostic @@ -488,7 +478,6 @@ fn overlapping_condition_exempt_types_rejects_a_malformed_name() -> anyhow::Resu 2 | [analysis] 3 | overlapping-condition-exempt-types = ["int", "list[int]"] | ^^^^^^^^^^^ Expected a bare or qualified class name, such as `int` or `decimal.Decimal` - | "#); Ok(()) @@ -525,7 +514,6 @@ fn overlapping_condition_exempt_types_accepts_an_unresolvable_name() -> anyhow:: | 3 | if not a: | ^^^^^ - | info: `str | None` is tested for falsiness help: Compare against the specific value instead of testing truthiness @@ -564,7 +552,6 @@ fn implicit_object_repr_report_types_rejects_a_malformed_name() -> anyhow::Resul 2 | [analysis] 3 | implicit-object-repr-report-types = ["types.FunctionType", "not a class"] | ^^^^^^^^^^^^^ Expected a bare or qualified class name, such as `int` or `decimal.Decimal` - | "#); Ok(()) @@ -603,7 +590,6 @@ fn implicit_object_repr_exempt_types_silences_a_default() -> anyhow::Result<()> | 5 | print(int) | ^^^ - | info: nothing in its hierarchy defines one, so the output is the interpreter's default, which identifies the class rather than the value Found 1 diagnostic @@ -672,7 +658,6 @@ fn redundant_return_annotation_is_gated_on_infer_unannotated_signatures() -> any | 2 | def f() -> None: | ^^^^ - | info: a `def` that leaves out its return type already returns `None` help: Remove the annotation @@ -681,7 +666,6 @@ fn redundant_return_annotation_is_gated_on_infer_unannotated_signatures() -> any | 2 | def s() -> None: ... | ^^^^ - | info: a `def` that leaves out its return type already returns `None` help: Remove the annotation diff --git a/crates/ty/tests/cli/api_lockfile.rs b/crates/ty/tests/cli/api_lockfile.rs index 160d24e919..7a7759d383 100644 --- a/crates/ty/tests/cli/api_lockfile.rs +++ b/crates/ty/tests/cli/api_lockfile.rs @@ -41,7 +41,7 @@ def _private() -> None: exit_code: 0 ----- stdout ----- #api-lock:v=1 - #tool:by=0.0.5 + #tool:by=0.0.8 #python:default #modules:1 module.CONST:v=builtins.int @@ -90,7 +90,7 @@ class Dog(Animal): exit_code: 0 ----- stdout ----- #api-lock:v=1 - #tool:by=0.0.5 + #tool:by=0.0.8 #python:default #modules:2 base.Animal.speak:d(self:base.Animal)->builtins.str @@ -127,7 +127,7 @@ def f(a: int, b: str = '', /, c: float = 0.0, *args: bytes, d: bool = False, **k exit_code: 0 ----- stdout ----- #api-lock:v=1 - #tool:by=0.0.5 + #tool:by=0.0.8 #python:default #modules:1 sigs.f:d(a:builtins.int,b:builtins.str=,/,c:builtins.float | builtins.int=,*args:builtins.bytes,d:builtins.bool=,**kwargs:builtins.int)->None @@ -172,7 +172,7 @@ class D[T, U]: exit_code: 0 ----- stdout ----- #api-lock:v=1 - #tool:by=0.0.5 + #tool:by=0.0.8 #python:default #modules:1 g.A.f:d(self:Self)->T @@ -224,7 +224,7 @@ class Mutable[T]: exit_code: 0 ----- stdout ----- #api-lock:v=1 - #tool:by=0.0.5 + #tool:by=0.0.8 #python:default #modules:1 fr.Frozen.x:v=T @@ -273,7 +273,7 @@ class Cell(Generic[T]): exit_code: 0 ----- stdout ----- #api-lock:v=1 - #tool:by=0.0.5 + #tool:by=0.0.8 #python:default #modules:1 ex.Box.get:d(self:Self)->T_co diff --git a/crates/ty/tests/cli/config_option.rs b/crates/ty/tests/cli/config_option.rs index bfcfcb4e44..d0e6c75c79 100644 --- a/crates/ty/tests/cli/config_option.rs +++ b/crates/ty/tests/cli/config_option.rs @@ -16,7 +16,6 @@ fn cli_config_args_toml_string_basic() -> anyhow::Result<()> { | 1 | print(x) # [unresolved-reference] | ^ - | Found 1 diagnostic @@ -33,7 +32,6 @@ fn cli_config_args_toml_string_basic() -> anyhow::Result<()> { | 1 | print(x) # [unresolved-reference] | ^ - | Found 1 diagnostic @@ -66,7 +64,6 @@ fn cli_config_args_overrides_ty_toml() -> anyhow::Result<()> { | 1 | print(x) # [unresolved-reference] | ^ - | Found 1 diagnostic @@ -83,7 +80,6 @@ fn cli_config_args_overrides_ty_toml() -> anyhow::Result<()> { | 1 | print(x) # [unresolved-reference] | ^ - | Found 1 diagnostic @@ -105,7 +101,6 @@ fn cli_config_args_later_overrides_earlier() -> anyhow::Result<()> { | 1 | print(x) # [unresolved-reference] | ^ - | Found 1 diagnostic @@ -164,7 +159,6 @@ fn config_file_override() -> anyhow::Result<()> { | 1 | print(x) # [unresolved-reference] | ^ - | Found 1 diagnostic @@ -181,7 +175,6 @@ fn config_file_override() -> anyhow::Result<()> { | 1 | print(x) # [unresolved-reference] | ^ - | Found 1 diagnostic diff --git a/crates/ty/tests/cli/exit_code.rs b/crates/ty/tests/cli/exit_code.rs index 382532a6e6..b553751e98 100644 --- a/crates/ty/tests/cli/exit_code.rs +++ b/crates/ty/tests/cli/exit_code.rs @@ -15,7 +15,6 @@ fn only_warnings() -> anyhow::Result<()> { | 1 | print(x) # [unresolved-reference] | ^ - | Found 1 diagnostic @@ -85,7 +84,6 @@ fn only_info() -> anyhow::Result<()> { | 3 | reveal_type(1) | ^ `Literal[1]` - | Found 1 diagnostic @@ -114,7 +112,6 @@ fn only_info_and_error_on_warning_is_true() -> anyhow::Result<()> { | 3 | reveal_type(1) | ^ `Literal[1]` - | Found 1 diagnostic @@ -146,7 +143,6 @@ fn only_warnings_and_error_on_warning_overrides_configuration() -> anyhow::Resul | 1 | print(x) # [unresolved-reference] | ^ - | Found 1 diagnostic @@ -178,7 +174,6 @@ fn only_warnings_and_error_on_warning_is_disabled_in_configuration() -> anyhow:: | 1 | print(x) # [unresolved-reference] | ^ - | Found 1 diagnostic @@ -207,14 +202,12 @@ fn both_warnings_and_errors() -> anyhow::Result<()> { | 2 | print(x) # [unresolved-reference] | ^ - | error[not-subscriptable]: Cannot subscript object of type `Literal[4]` with no `__getitem__` method --> test.py:3:7 | 3 | print(4[1]) # [not-subscriptable] | ^^^^ - | Found 2 diagnostics @@ -243,14 +236,12 @@ fn both_warnings_and_errors_and_exit_zero_on_warning() -> anyhow::Result<()> { | 2 | print(x) # [unresolved-reference] | ^ - | error[not-subscriptable]: Cannot subscript object of type `Literal[4]` with no `__getitem__` method --> test.py:3:7 | 3 | print(4[1]) # [not-subscriptable] | ^^^^ - | Found 2 diagnostics @@ -279,14 +270,12 @@ fn exit_zero_is_true() -> anyhow::Result<()> { | 2 | print(x) # [unresolved-reference] | ^ - | error[not-subscriptable]: Cannot subscript object of type `Literal[4]` with no `__getitem__` method --> test.py:3:7 | 3 | print(4[1]) # [not-subscriptable] | ^^^^ - | Found 2 diagnostics diff --git a/crates/ty/tests/cli/file_selection.rs b/crates/ty/tests/cli/file_selection.rs index 95f5fb55f1..841e259434 100644 --- a/crates/ty/tests/cli/file_selection.rs +++ b/crates/ty/tests/cli/file_selection.rs @@ -2,6 +2,94 @@ use insta_cmd::assert_cmd_snapshot; use crate::CliTest; +#[test] +fn exclude_scripts_only_applies_to_implicitly_discovered_files() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ("main.py", "value: int = 'project'"), + ( + "script.py", + r#" + # /// script + # dependencies = [] + # /// + value: int = "script" + "#, + ), + ( + "nested/script.py", + r#" + # /// script + # dependencies = [] + # /// + value: int = "nested-script" + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command().env("TY_OUTPUT_FORMAT", "concise"), @r#" + success: false + exit_code: 1 + ----- stdout ----- + main.py:1:14: error[invalid-assignment] Object of type `Literal["project"]` is not assignable to `int` + nested/script.py:5:14: error[invalid-assignment] Object of type `Literal["nested-script"]` is not assignable to `int` + script.py:5:14: error[invalid-assignment] Object of type `Literal["script"]` is not assignable to `int` + Found 3 diagnostics + + ----- stderr ----- + "#); + + assert_cmd_snapshot!(case.command().env("TY_OUTPUT_FORMAT", "concise").arg("--exclude-scripts"), @r#" + success: false + exit_code: 1 + ----- stdout ----- + main.py:1:14: error[invalid-assignment] Object of type `Literal["project"]` is not assignable to `int` + Found 1 diagnostic + + ----- stderr ----- + "#); + + assert_cmd_snapshot!(case.command().env("TY_OUTPUT_FORMAT", "concise").arg("--exclude-scripts").arg("script.py"), @r#" + success: false + exit_code: 1 + ----- stdout ----- + script.py:5:14: error[invalid-assignment] Object of type `Literal["script"]` is not assignable to `int` + Found 1 diagnostic + + ----- stderr ----- + "#); + + case.write_file( + "ty.toml", + r#" + [src] + exclude-scripts = true + "#, + )?; + + assert_cmd_snapshot!(case.command().env("TY_OUTPUT_FORMAT", "concise"), @r#" + success: false + exit_code: 1 + ----- stdout ----- + main.py:1:14: error[invalid-assignment] Object of type `Literal["project"]` is not assignable to `int` + Found 1 diagnostic + + ----- stderr ----- + "#); + assert_cmd_snapshot!(case.command().env("TY_OUTPUT_FORMAT", "concise").arg("--include-scripts"), @r#" + success: false + exit_code: 1 + ----- stdout ----- + main.py:1:14: error[invalid-assignment] Object of type `Literal["project"]` is not assignable to `int` + nested/script.py:5:14: error[invalid-assignment] Object of type `Literal["nested-script"]` is not assignable to `int` + script.py:5:14: error[invalid-assignment] Object of type `Literal["script"]` is not assignable to `int` + Found 3 diagnostics + + ----- stderr ----- + "#); + + Ok(()) +} + /// Test exclude CLI argument functionality #[test] fn exclude_argument() -> anyhow::Result<()> { @@ -36,14 +124,12 @@ fn exclude_argument() -> anyhow::Result<()> { | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | error[unresolved-reference]: Name `temp_undefined_var` used when not defined --> temp_file.py:2:7 | 2 | print(temp_undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^^^^^^ - | Found 2 diagnostics @@ -60,7 +146,6 @@ fn exclude_argument() -> anyhow::Result<()> { | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 1 diagnostic @@ -112,7 +197,6 @@ fn configuration_include() -> anyhow::Result<()> { | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 1 diagnostic @@ -137,14 +221,12 @@ fn configuration_include() -> anyhow::Result<()> { | 2 | print(other_undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^^^^^^^ - | error[unresolved-reference]: Name `undefined_var` used when not defined --> src/main.py:2:7 | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 2 diagnostics @@ -202,7 +284,6 @@ fn configuration_include_no_extension() -> anyhow::Result<()> { | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 1 diagnostic @@ -254,14 +335,12 @@ fn configuration_exclude() -> anyhow::Result<()> { | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | error[unresolved-reference]: Name `temp_undefined_var` used when not defined --> temp_file.py:2:7 | 2 | print(temp_undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^^^^^^ - | Found 2 diagnostics @@ -286,7 +365,6 @@ fn configuration_exclude() -> anyhow::Result<()> { | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 1 diagnostic @@ -339,7 +417,6 @@ fn exclude_precedence_over_include() -> anyhow::Result<()> { | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 1 diagnostic @@ -391,7 +468,6 @@ fn exclude_argument_precedence_include_argument() -> anyhow::Result<()> { | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 1 diagnostic @@ -429,7 +505,6 @@ fn remove_default_exclude() -> anyhow::Result<()> { | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 1 diagnostic @@ -454,14 +529,12 @@ fn remove_default_exclude() -> anyhow::Result<()> { | 2 | print(another_undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^^^^^^^^^ - | error[unresolved-reference]: Name `undefined_var` used when not defined --> src/main.py:2:7 | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 2 diagnostics @@ -508,7 +581,6 @@ fn cli_removes_config_exclude() -> anyhow::Result<()> { | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 1 diagnostic @@ -525,14 +597,12 @@ fn cli_removes_config_exclude() -> anyhow::Result<()> { | 2 | print(build_undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^^^^^^^ - | error[unresolved-reference]: Name `undefined_var` used when not defined --> src/main.py:2:7 | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 2 diagnostics @@ -583,7 +653,6 @@ fn explicit_path_overrides_exclude() -> anyhow::Result<()> { | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 1 diagnostic @@ -600,7 +669,6 @@ fn explicit_path_overrides_exclude() -> anyhow::Result<()> { | 2 | print(dist_undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^^^^^^ - | Found 1 diagnostic @@ -617,7 +685,6 @@ fn explicit_path_overrides_exclude() -> anyhow::Result<()> { | 2 | print(other_undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^^^^^^^ - | Found 1 diagnostic @@ -668,14 +735,12 @@ fn explicit_path_overrides_exclude_force_exclude() -> anyhow::Result<()> { | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | error[unresolved-reference]: Name `dist_undefined_var` used when not defined --> tests/generated.py:2:7 | 2 | print(dist_undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^^^^^^ - | Found 2 diagnostics @@ -692,7 +757,6 @@ fn explicit_path_overrides_exclude_force_exclude() -> anyhow::Result<()> { | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 1 diagnostic @@ -709,14 +773,12 @@ fn explicit_path_overrides_exclude_force_exclude() -> anyhow::Result<()> { | 2 | print(other_undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^^^^^^^ - | error[unresolved-reference]: Name `undefined_var` used when not defined --> src/main.py:2:7 | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 2 diagnostics @@ -733,7 +795,6 @@ fn explicit_path_overrides_exclude_force_exclude() -> anyhow::Result<()> { | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 1 diagnostic @@ -780,14 +841,12 @@ fn force_exclude_directory_exclusion() -> anyhow::Result<()> { | 3 | if base_path not in CMAKE_PREFIX_PATH: | ^^^^^^^^^^^^^^^^^ - | error[unresolved-reference]: Name `CMAKE_PREFIX_PATH` used when not defined --> out/amd64/install/_setup_util.py:4:5 | 4 | CMAKE_PREFIX_PATH.insert(0, base_path) | ^^^^^^^^^^^^^^^^^ - | Found 2 diagnostics @@ -847,14 +906,12 @@ fn cli_and_configuration_exclude() -> anyhow::Result<()> { | 2 | print(other_undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^^^^^^^ - | error[unresolved-reference]: Name `undefined_var` used when not defined --> src/main.py:2:7 | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 2 diagnostics @@ -870,7 +927,6 @@ fn cli_and_configuration_exclude() -> anyhow::Result<()> { | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 1 diagnostic @@ -1054,14 +1110,12 @@ print(other_undefined) # error: unresolved-reference | 3 | return missing_value # error: unresolved-reference | ^^^^^^^^^^^^^ - | error[unresolved-reference]: Name `undefined_var` used when not defined --> main.py:5:7 | 5 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 2 diagnostics @@ -1078,7 +1132,6 @@ print(other_undefined) # error: unresolved-reference | 5 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 1 diagnostic @@ -1131,21 +1184,18 @@ print(regular_undefined) # error: unresolved-reference | 2 | print(regular_undefined) # error: unresolved-reference | ^^^^^^^^^^^^^^^^^ - | error[unresolved-reference]: Name `undefined_var` used when not defined --> src/module.py:3:12 | 3 | return undefined_var # error: unresolved-reference | ^^^^^^^^^^^^^ - | error[unresolved-reference]: Name `missing_value` used when not defined --> src/utils.py:3:12 | 3 | return missing_value # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 3 diagnostics @@ -1162,21 +1212,18 @@ print(regular_undefined) # error: unresolved-reference | 3 | return undefined_var # error: unresolved-reference | ^^^^^^^^^^^^^ - | error[unresolved-reference]: Name `missing_value` used when not defined --> generated_utils.py:3:12 | 3 | return missing_value # error: unresolved-reference | ^^^^^^^^^^^^^ - | error[unresolved-reference]: Name `regular_undefined` used when not defined --> regular.py:2:7 | 2 | print(regular_undefined) # error: unresolved-reference | ^^^^^^^^^^^^^^^^^ - | Found 3 diagnostics @@ -1193,7 +1240,6 @@ print(regular_undefined) # error: unresolved-reference | 3 | return undefined_var # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 1 diagnostic diff --git a/crates/ty/tests/cli/fixes.rs b/crates/ty/tests/cli/fixes.rs index 6f5f3233fd..21424a58e0 100644 --- a/crates/ty/tests/cli/fixes.rs +++ b/crates/ty/tests/cli/fixes.rs @@ -138,23 +138,20 @@ fn add_ignore_unfixable() -> anyhow::Result<()> { info[revealed-type]: Revealed type --> different_violations.py:6:13 | - 6 | reveal_type(x) # ty:ignore[undefined-reveal] + 6 | reveal_type(x) # ty: ignore[undefined-reveal] | ^ `Unknown` - | error[unresolved-reference]: Name `x` used when not defined --> has_syntax_error.py:1:7 | 1 | print(x # [unresolved-reference] | ^ - | error[invalid-syntax]: unexpected EOF while parsing --> has_syntax_error.py:1:34 | 1 | print(x # [unresolved-reference] | ^ - | Found 3 diagnostics Added 5 ignore comments @@ -224,9 +221,10 @@ fn fix_unfixable() -> anyhow::Result<()> { exit_code: 1 ----- stdout ----- error[invalid-syntax]: unexpected EOF while parsing - --> has_syntax_error.py:1:1 - | - | + --> has_syntax_error.py:2:1 + | + 2 | + | ^ Found 2 diagnostics (1 fixed, 1 remaining). diff --git a/crates/ty/tests/cli/main.rs b/crates/ty/tests/cli/main.rs index 1327161160..e18aace660 100644 --- a/crates/ty/tests/cli/main.rs +++ b/crates/ty/tests/cli/main.rs @@ -9,6 +9,7 @@ mod python_environment; mod rule; mod rule_selection; mod scripts; +mod uv_workspace; use anyhow::Context as _; use insta::Settings; @@ -52,13 +53,12 @@ fn test_quiet_output() -> anyhow::Result<()> { exit_code: 1 ----- stdout ----- error[invalid-assignment]: Object of type `Literal["foo"]` is not assignable to `int` - --> test.py:1:4 + --> test.py:1:10 | 1 | x: int = 'foo' | --- ^^^^^ Incompatible value of type `Literal["foo"]` | | | Declared type - | Found 1 diagnostic @@ -133,7 +133,6 @@ fn test_run_in_sub_directory() -> anyhow::Result<()> { | 1 | ~ | ^ - | Found 1 diagnostic @@ -154,7 +153,6 @@ fn test_include_hidden_files_by_default() -> anyhow::Result<()> { | 1 | ~ | ^ - | Found 1 diagnostic @@ -187,7 +185,6 @@ fn test_respect_ignore_files() -> anyhow::Result<()> { | 1 | ~ | ^ - | Found 1 diagnostic @@ -205,7 +202,6 @@ fn test_respect_ignore_files() -> anyhow::Result<()> { | 1 | ~ | ^ - | Found 1 diagnostic @@ -223,7 +219,6 @@ fn test_respect_ignore_files() -> anyhow::Result<()> { | 1 | ~ | ^ - | Found 1 diagnostic @@ -284,7 +279,6 @@ fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> { | 2 | from utils import add | ^^^^^ - | info: Searched in the following paths during module resolution: info: 1. / (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -397,7 +391,6 @@ fn user_configuration() -> anyhow::Result<()> { | 2 | y = 4 / 0 | ^^^^^ - | info: rule `division-by-zero` was selected in the configuration file error[unresolved-reference]: Name `prin` used when not defined @@ -405,7 +398,6 @@ fn user_configuration() -> anyhow::Result<()> { | 7 | prin(x) | ^^^^ - | info: rule `unresolved-reference` is enabled by default Found 2 diagnostics @@ -438,7 +430,6 @@ fn user_configuration() -> anyhow::Result<()> { | 2 | y = 4 / 0 | ^^^^^ - | info: rule `division-by-zero` was selected in the configuration file warning[unresolved-reference]: Name `prin` used when not defined @@ -446,7 +437,6 @@ fn user_configuration() -> anyhow::Result<()> { | 7 | prin(x) | ^^^^ - | info: rule `unresolved-reference` was selected in the configuration file Found 2 diagnostics @@ -513,14 +503,12 @@ fn basedpython_configuration_file() -> anyhow::Result<()> { | 2 | y = 4 / 0 | ^^^^^ - | warning[unresolved-reference]: Name `prin` used when not defined --> main.py:7:1 | 7 | prin(x) | ^^^^ - | Found 2 diagnostics @@ -568,7 +556,6 @@ fn check_specific_paths() -> anyhow::Result<()> { | 2 | from main2 import z # error: unresolved-import | ^^^^^ - | info: Searched in the following paths during module resolution: info: 1. / (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -579,7 +566,6 @@ fn check_specific_paths() -> anyhow::Result<()> { | 2 | import does_not_exist # error: unresolved-import | ^^^^^^^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. / (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -604,7 +590,6 @@ fn check_specific_paths() -> anyhow::Result<()> { | 2 | from main2 import z # error: unresolved-import | ^^^^^ - | info: Searched in the following paths during module resolution: info: 1. / (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -615,7 +600,6 @@ fn check_specific_paths() -> anyhow::Result<()> { | 2 | import does_not_exist # error: unresolved-import | ^^^^^^^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. / (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -676,7 +660,6 @@ fn check_file_without_extension() -> anyhow::Result<()> { | 1 | a = b | ^ - | Found 1 diagnostic @@ -915,7 +898,6 @@ fn can_handle_large_binop_expressions() -> anyhow::Result<()> { | 4 | reveal_type(total) | ^^^^^ `Literal[2000]` - | Found 1 diagnostic @@ -953,6 +935,7 @@ impl CliTest { let mut settings = insta::Settings::clone_current(); settings.add_filter(&tempdir_filter(&project_dir), "/"); + settings.add_filter(r"\bty\.exe\b", "ty"); settings.add_filter(r#"\\(\w\w|\s|\.|")"#, "/$1"); // 0.003s settings.add_filter(r"\d.\d\d\ds", "0.000s"); diff --git a/crates/ty/tests/cli/python_environment.rs b/crates/ty/tests/cli/python_environment.rs index 1db0e62c19..18a59f79b6 100644 --- a/crates/ty/tests/cli/python_environment.rs +++ b/crates/ty/tests/cli/python_environment.rs @@ -35,14 +35,12 @@ fn config_override_python_version() -> anyhow::Result<()> { | 5 | print(sys.last_exc) | ^^^^^^^^^^^^ - | info: The member may be available on other Python versions or platforms info: Python 3.11 was assumed when resolving the `last_exc` attribute --> pyproject.toml:3:18 | 3 | python-version = "3.11" | ^^^^^^ Python version configuration - | Found 1 diagnostic @@ -92,7 +90,6 @@ fn config_override_python_platform() -> anyhow::Result<()> { | 5 | reveal_type(sys.platform) | ^^^^^^^^^^^^ `Literal["linux"]` - | Found 1 diagnostic @@ -108,7 +105,6 @@ fn config_override_python_platform() -> anyhow::Result<()> { | 5 | reveal_type(sys.platform) | ^^^^^^^^^^^^ `LiteralString` - | Found 1 diagnostic @@ -145,14 +141,12 @@ fn config_file_annotation_showing_where_python_version_set_typing_error() -> any | 2 | PythonFinalizationError | ^^^^^^^^^^^^^^^^^^^^^^^ - | info: `PythonFinalizationError` was added as a builtin in Python 3.13 info: Python 3.12 was assumed when resolving types --> pyproject.toml:3:18 | 3 | python-version = "3.12" | ^^^^^^ Python version configuration - | Found 1 diagnostic @@ -168,7 +162,6 @@ fn config_file_annotation_showing_where_python_version_set_typing_error() -> any | 2 | PythonFinalizationError | ^^^^^^^^^^^^^^^^^^^^^^^ - | info: `PythonFinalizationError` was added as a builtin in Python 3.13 info: Python 3.12 was assumed when resolving types because it was specified on the command line @@ -202,7 +195,6 @@ fn src_subdirectory_takes_precedence_over_repo_root() -> anyhow::Result<()> { | 1 | from . import nonexistent_submodule | ^^^^^^^^^^^^^^^^^^^^^ - | Found 1 diagnostic @@ -266,7 +258,6 @@ fn python_version_inferred_from_system_installation() -> anyhow::Result<()> { | 1 | PythonFinalizationError | ^^^^^^^^^^^^^^^^^^^^^^^ - | info: `PythonFinalizationError` was added as a builtin in Python 3.13 info: Python 3.12 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.12/site-packages/` @@ -292,7 +283,6 @@ fn python_version_inferred_from_system_installation() -> anyhow::Result<()> { | 1 | PythonFinalizationError | ^^^^^^^^^^^^^^^^^^^^^^^ - | info: `PythonFinalizationError` was added as a builtin in Python 3.13 info: Python 3.12 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.12/site-packages/` @@ -321,7 +311,6 @@ fn python_version_inferred_from_system_installation() -> anyhow::Result<()> { | 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/` @@ -404,7 +393,6 @@ import colorama | 1 | import foo | ^^^ - | info: Searched in the following paths during module resolution: info: 1. /project (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -416,7 +404,6 @@ import colorama | 3 | import colorama | ^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. /project (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -439,7 +426,6 @@ import colorama | 2 | import bar | ^^^ - | info: Searched in the following paths during module resolution: info: 1. /project (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -451,7 +437,6 @@ import colorama | 3 | import colorama | ^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. /project (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -474,7 +459,6 @@ import colorama | 2 | import bar | ^^^ - | info: Searched in the following paths during module resolution: info: 1. /project (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -486,7 +470,6 @@ import colorama | 3 | import colorama | ^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. /project (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -509,7 +492,6 @@ import colorama | 2 | import bar | ^^^ - | info: Searched in the following paths during module resolution: info: 1. /project (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -521,7 +503,6 @@ import colorama | 3 | import colorama | ^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. /project (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -575,7 +556,6 @@ import bar", | 1 | import foo | ^^^ - | info: Searched in the following paths during module resolution: info: 1. / (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -622,7 +602,6 @@ fn lib64_site_packages_directory_on_unix() -> anyhow::Result<()> { | 1 | import foo, bar, baz | ^^^ - | info: Searched in the following paths during module resolution: info: 1. / (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -666,7 +645,6 @@ fn many_search_paths() -> anyhow::Result<()> { | 1 | import foo1, baz | ^^^ - | info: Searched in the following paths during module resolution: info: 1. /extra1 (extra search path specified on the CLI or in your config file) info: 2. /extra2 (extra search path specified on the CLI or in your config file) @@ -699,7 +677,6 @@ fn many_search_paths() -> anyhow::Result<()> { | 1 | import foo1, baz | ^^^ - | info: Searched in the following paths during module resolution: info: 1. /extra1 (extra search path specified on the CLI or in your config file) info: 2. /extra2 (extra search path specified on the CLI or in your config file) @@ -734,7 +711,6 @@ fn many_search_paths() -> anyhow::Result<()> { | 1 | import foo1, baz | ^^^ - | info: Searched in the following paths during module resolution: info: 1. /extra1 (extra search path specified on the CLI or in your config file) info: 2. /extra2 (extra search path specified on the CLI or in your config file) @@ -794,14 +770,12 @@ fn pyvenv_cfg_file_annotation_showing_where_python_version_set() -> anyhow::Resu | 1 | PythonFinalizationError | ^^^^^^^^^^^^^^^^^^^^^^^ - | info: `PythonFinalizationError` was added as a builtin in Python 3.13 info: Python 3.12 was assumed when resolving types because of your virtual environment --> venv/pyvenv.cfg:2:11 | 2 | version = 3.12 | ^^^^ Virtual environment metadata - | info: No Python version was specified on the command line or in a configuration file Found 1 diagnostic @@ -850,14 +824,12 @@ fn pyvenv_cfg_file_annotation_no_trailing_newline() -> anyhow::Result<()> { | 1 | PythonFinalizationError | ^^^^^^^^^^^^^^^^^^^^^^^ - | info: `PythonFinalizationError` was added as a builtin in Python 3.13 info: Python 3.12 was assumed when resolving types because of your virtual environment --> venv/pyvenv.cfg:3:23 | 3 | version = 3.12 | ^^^^ Virtual environment metadata - | info: No Python version was specified on the command line or in a configuration file Found 1 diagnostic @@ -899,13 +871,11 @@ fn config_file_annotation_showing_where_python_version_set_syntax_error() -> any | 2 | match object(): | ^^^^^ - | info: Python 3.8 was assumed when parsing syntax --> pyproject.toml:3:19 | 3 | requires-python = ">=3.8" | ^^^^^^^ Python version configuration - | Found 1 diagnostic @@ -921,7 +891,6 @@ fn config_file_annotation_showing_where_python_version_set_syntax_error() -> any | 2 | match object(): | ^^^^^ - | info: Python 3.9 was assumed when parsing syntax because it was specified on the command line Found 1 diagnostic @@ -1082,7 +1051,6 @@ fn config_file_broken_python_setting() -> anyhow::Result<()> { 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) "#); @@ -1172,7 +1140,6 @@ fn config_file_python_setting_directory_with_no_site_packages() -> anyhow::Resul 2 | [tool.ty.environment] 3 | python = "directory-but-no-site-packages" | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Could not find a `site-packages` directory for this Python installation/executable - | "#); Ok(()) @@ -1217,7 +1184,6 @@ fn config_file_python_setting_directory_with_unsupported_python_version() -> any | 2 | version_info = 3.16.0 | ^^^^^^ - | info: Expected one of `3.7`, `3.8`, `3.9`, `3.10`, `3.11`, `3.12`, `3.13`, `3.14`, `3.15`. info: Set `environment.python-version` explicitly to override the inferred version. info: The version was inferred from your virtual environment metadata. @@ -1262,7 +1228,6 @@ fn unix_system_installation_with_no_lib_directory() -> anyhow::Result<()> { 2 | [tool.ty.environment] 3 | python = "directory-but-no-site-packages" | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | "#); Ok(()) @@ -1303,28 +1268,24 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> { | 4 | os.grantpt(1) # only available on unix, Python 3.13 or newer | ^^^^^^^^^^ - | info: The member may be available on other Python versions or platforms info: Python 3.10 was assumed when resolving the `grantpt` attribute --> ty.toml:3:18 | 3 | python-version = "3.10" | ^^^^^^ Python version configuration - | error[unresolved-import]: Module `typing` has no member `LiteralString` --> main.py:6:20 | 6 | from typing import LiteralString # added in Python 3.11 | ^^^^^^^^^^^^^ - | info: The member may be available on other Python versions or platforms info: Python 3.10 was assumed when resolving imports --> ty.toml:3:18 | 3 | python-version = "3.10" | ^^^^^^ Python version configuration - | Found 2 diagnostics @@ -1522,7 +1483,6 @@ home = ./ | 4 | from package1 import WorkingVenv | ^^^^^^^^^^^ - | Found 1 diagnostic @@ -1541,7 +1501,6 @@ home = ./ | 2 | from package1 import ActiveVenv | ^^^^^^^^^^ - | Found 1 diagnostic @@ -1560,7 +1519,6 @@ home = ./ | 3 | from package1 import ChildConda | ^^^^^^^^^^ - | Found 1 diagnostic @@ -1580,7 +1538,6 @@ home = ./ | 4 | from package1 import WorkingVenv | ^^^^^^^^^^^ - | Found 1 diagnostic @@ -1602,7 +1559,6 @@ home = ./ | 2 | from package1 import ActiveVenv | ^^^^^^^^^^ - | Found 1 diagnostic @@ -1622,7 +1578,6 @@ home = ./ | 3 | from package1 import ChildConda | ^^^^^^^^^^ - | Found 1 diagnostic @@ -1642,7 +1597,6 @@ home = ./ | 3 | from package1 import ChildConda | ^^^^^^^^^^ - | Found 1 diagnostic @@ -1662,7 +1616,6 @@ home = ./ | 5 | from package1 import BaseConda | ^^^^^^^^^ - | Found 1 diagnostic @@ -1749,7 +1702,6 @@ home = ./ | 2 | from package1 import ActiveVenv | ^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. /project (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -1760,7 +1712,6 @@ home = ./ | 3 | from package1 import ChildConda | ^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. /project (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -1771,7 +1722,6 @@ home = ./ | 4 | from package1 import WorkingVenv | ^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. /project (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -1782,7 +1732,6 @@ home = ./ | 5 | from package1 import BaseConda | ^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. /project (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -1805,7 +1754,6 @@ home = ./ | 2 | from package1 import ActiveVenv | ^^^^^^^^^^ - | Found 1 diagnostic @@ -1824,7 +1772,6 @@ home = ./ | 3 | from package1 import ChildConda | ^^^^^^^^^^ - | Found 1 diagnostic @@ -1844,7 +1791,6 @@ home = ./ | 5 | from package1 import BaseConda | ^^^^^^^^^ - | Found 1 diagnostic @@ -1866,7 +1812,6 @@ home = ./ | 2 | from package1 import ActiveVenv | ^^^^^^^^^^ - | Found 1 diagnostic @@ -1886,7 +1831,6 @@ home = ./ | 5 | from package1 import BaseConda | ^^^^^^^^^ - | Found 1 diagnostic @@ -1906,7 +1850,6 @@ home = ./ | 3 | from package1 import ChildConda | ^^^^^^^^^^ - | Found 1 diagnostic @@ -1926,7 +1869,6 @@ home = ./ | 5 | from package1 import BaseConda | ^^^^^^^^^ - | Found 1 diagnostic @@ -2058,7 +2000,6 @@ fn ty_environment_and_discovered_venv() -> anyhow::Result<()> { | 9 | from shared_package import FromLocalVenv | ^^^^^^^^^^^^^ - | Found 1 diagnostic @@ -2132,7 +2073,6 @@ fn ty_environment_and_active_environment() -> anyhow::Result<()> { | 2 | from ty_package import TyEnvClass | ^^^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. / (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -2251,7 +2191,6 @@ fn ty_system_environment_and_local_venv() -> anyhow::Result<()> { | 3 | from system_package import SystemEnvClass | ^^^^^^^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. / (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -2266,116 +2205,6 @@ fn ty_system_environment_and_local_venv() -> anyhow::Result<()> { 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: false - exit_code: 1 - ----- stdout ----- - warning[deprecated-setting]: The `src.root` setting is deprecated. Use `environment.root` instead. - --> pyproject.toml:3:8 - | - 3 | root = "./src" - | ^^^^^^^ - | - - Found 1 diagnostic - - ----- stderr ----- - "#); - - 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: false - exit_code: 1 - ----- stdout ----- - warning[deprecated-setting]: The `src.root` setting is deprecated. Use `environment.root` instead. - --> pyproject.toml:3:8 - | - 3 | root = "./src" - | ^^^^^^^ - | - info: The `src.root` setting was ignored in favor of the `environment.root` setting - - Found 1 diagnostic - - ----- stderr ----- - "#); - - 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: false - exit_code: 1 - ----- stdout ----- - warning[deprecated-setting]: The `src.root` setting is deprecated. Use `environment.root` instead. - --> pyproject.toml:3:8 - | - 3 | root = "./src" - | ^^^^^^^ - | - info: The `src.root` setting was ignored in favor of the `environment.root` setting - - Found 1 diagnostic - - ----- stderr ----- - "#); - - Ok(()) -} - #[test] fn default_root_src_layout() -> anyhow::Result<()> { let case = CliTest::with_files([ @@ -2554,7 +2383,6 @@ fn default_root_tests_package() -> anyhow::Result<()> { | 3 | from bar import bar # expected unresolved import | ^^^ - | info: Searched in the following paths during module resolution: info: 1. /src (first-party code) info: 2. / (first-party code) @@ -2624,7 +2452,6 @@ fn default_root_python_package() -> anyhow::Result<()> { | 3 | from bar import bar # expected unresolved import | ^^^ - | info: Searched in the following paths during module resolution: info: 1. /src (first-party code) info: 2. / (first-party code) @@ -2666,7 +2493,6 @@ fn default_root_python_package_pyi() -> anyhow::Result<()> { | 3 | from bar import bar # expected unresolved import | ^^^ - | info: Searched in the following paths during module resolution: info: 1. /src (first-party code) info: 2. / (first-party code) @@ -2704,7 +2530,6 @@ fn pythonpath_is_respected() -> anyhow::Result<()> { | 2 | import baz | ^^^ - | info: Searched in the following paths during module resolution: info: 1. /src (first-party code) info: 2. / (first-party code) @@ -2757,7 +2582,6 @@ fn pythonpath_multiple_dirs_is_respected() -> anyhow::Result<()> { | 2 | import baz | ^^^ - | info: Searched in the following paths during module resolution: info: 1. /src (first-party code) info: 2. / (first-party code) @@ -2769,7 +2593,6 @@ fn pythonpath_multiple_dirs_is_respected() -> anyhow::Result<()> { | 3 | import foo | ^^^ - | info: Searched in the following paths during module resolution: info: 1. /src (first-party code) info: 2. / (first-party code) diff --git a/crates/ty/tests/cli/rule_selection.rs b/crates/ty/tests/cli/rule_selection.rs index e8ee23bf29..6a2ac6ee84 100644 --- a/crates/ty/tests/cli/rule_selection.rs +++ b/crates/ty/tests/cli/rule_selection.rs @@ -27,7 +27,6 @@ fn configuration_rule_severity() -> anyhow::Result<()> { | 7 | prin(x) # unresolved-reference | ^^^^ - | info: rule `unresolved-reference` is enabled by default Found 1 diagnostic @@ -54,7 +53,6 @@ fn configuration_rule_severity() -> anyhow::Result<()> { | 2 | y = 4 / 0 | ^^^^^ - | info: rule `division-by-zero` was selected in the configuration file Found 1 diagnostic @@ -102,14 +100,12 @@ fn basedpython_configuration_section() -> anyhow::Result<()> { | 2 | y = 4 / 0 | ^^^^^ - | error[unresolved-reference]: Name `prin` used when not defined --> test.py:7:1 | 7 | prin(x) # unresolved-reference | ^^^^ - | Found 2 diagnostics @@ -147,7 +143,6 @@ fn cli_rule_severity() -> anyhow::Result<()> { | 2 | import does_not_exit | ^^^^^^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. / (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -159,7 +154,6 @@ fn cli_rule_severity() -> anyhow::Result<()> { | 9 | prin(x) # unresolved-reference | ^^^^ - | info: rule `unresolved-reference` is enabled by default Found 2 diagnostics @@ -187,7 +181,6 @@ fn cli_rule_severity() -> anyhow::Result<()> { | 2 | import does_not_exit | ^^^^^^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. / (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -199,7 +192,6 @@ fn cli_rule_severity() -> anyhow::Result<()> { | 4 | y = 4 / 0 | ^^^^^ - | info: rule `division-by-zero` was selected on the command line Found 2 diagnostics @@ -238,7 +230,6 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> { | 7 | prin(x) # unresolved-reference | ^^^^ - | info: rule `unresolved-reference` is enabled by default Found 1 diagnostic @@ -266,7 +257,6 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> { | 2 | y = 4 / 0 | ^^^^^ - | info: rule `division-by-zero` was selected on the command line Found 1 diagnostic @@ -302,7 +292,6 @@ fn configuration_unknown_rules() -> anyhow::Result<()> { | 3 | division-by-zer = "warn" # incorrect rule name | ^^^^^^^^^^^^^^^ - | Found 1 diagnostic @@ -377,7 +366,6 @@ fn overrides_basic() -> anyhow::Result<()> { | 2 | y = 4 / 0 # division-by-zero: error (global) | ^^^^^ - | info: rule `division-by-zero` was selected in the configuration file error[unresolved-reference]: Name `prin` used when not defined @@ -385,7 +373,6 @@ fn overrides_basic() -> anyhow::Result<()> { | 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 @@ -393,7 +380,6 @@ fn overrides_basic() -> anyhow::Result<()> { | 2 | y = 4 / 0 # division-by-zero: warn (override) | ^^^^^ - | info: rule `division-by-zero` was selected in the configuration file Found 3 diagnostics @@ -451,7 +437,6 @@ fn overrides_precedence() -> anyhow::Result<()> { | 2 | y = 4 / 0 # division-by-zero: warn (first override) | ^^^^^ - | info: rule `division-by-zero` was selected in the configuration file Found 1 diagnostic @@ -501,7 +486,6 @@ fn multiple_overrides_inherit_cli_rules() -> anyhow::Result<()> { | 2 | y = 4 / 0 | ^^^^^ - | Found 1 diagnostic @@ -552,7 +536,6 @@ fn overrides_exclude() -> anyhow::Result<()> { | 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 @@ -560,7 +543,6 @@ fn overrides_exclude() -> anyhow::Result<()> { | 2 | y = 4 / 0 # division-by-zero: warn (override applies) | ^^^^^ - | info: rule `division-by-zero` was selected in the configuration file Found 2 diagnostics @@ -616,7 +598,6 @@ fn overrides_inherit_global() -> anyhow::Result<()> { | 2 | y = 4 / 0 # division-by-zero: warn (global) | ^^^^^ - | info: rule `division-by-zero` was selected in the configuration file error[unresolved-reference]: Name `prin` used when not defined @@ -624,7 +605,6 @@ fn overrides_inherit_global() -> anyhow::Result<()> { | 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 @@ -632,7 +612,6 @@ fn overrides_inherit_global() -> anyhow::Result<()> { | 3 | prin(y) # unresolved-reference: error (inherited from global) | ^^^^ - | info: rule `unresolved-reference` was selected in the configuration file Found 3 diagnostics @@ -769,7 +748,6 @@ fn overrides_missing_include_exclude() -> anyhow::Result<()> { | 5 | [[tool.ty.overrides]] | ^^^^^^^^^^^^^^^^^^^^^ This overrides section applies to all files - | 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 @@ -779,7 +757,6 @@ fn overrides_missing_include_exclude() -> anyhow::Result<()> { | 2 | y = 4 / 0 | ^^^^^ - | info: rule `division-by-zero` was selected in the configuration file Found 2 diagnostics @@ -824,7 +801,6 @@ fn overrides_empty_include() -> anyhow::Result<()> { | 6 | include = [] # Empty include - won't match any files | ^^ This `include` list is empty - | 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 @@ -832,7 +808,6 @@ fn overrides_empty_include() -> anyhow::Result<()> { | 2 | y = 4 / 0 | ^^^^^ - | info: rule `division-by-zero` was selected in the configuration file Found 2 diagnostics @@ -876,7 +851,6 @@ fn overrides_no_actual_overrides() -> anyhow::Result<()> { | 5 | [[tool.ty.overrides]] | ^^^^^^^^^^^^^^^^^^^^^ This overrides section overrides no settings - | info: It has no `rules` or `analysis` table info: Add a `[overrides.rules]` or `[overrides.analysis]` table... info: or remove the `[[overrides]]` section if there's nothing to override @@ -886,7 +860,6 @@ fn overrides_no_actual_overrides() -> anyhow::Result<()> { | 2 | y = 4 / 0 | ^^^^^ - | info: rule `division-by-zero` was selected in the configuration file Found 2 diagnostics @@ -939,7 +912,6 @@ fn overrides_unknown_rules() -> anyhow::Result<()> { | 2 | y = 4 / 0 | ^^^^^ - | info: rule `division-by-zero` was selected in the configuration file warning[unknown-rule]: Unknown rule `division-by-zer`. Did you mean `division-by-zero`? @@ -947,14 +919,12 @@ fn overrides_unknown_rules() -> anyhow::Result<()> { | 10 | division-by-zer = "error" # incorrect rule name | ^^^^^^^^^^^^^^^ - | 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 @@ -1025,7 +995,6 @@ fn cli_all_rules_warn() -> anyhow::Result<()> { | 2 | prin(x) # unresolved-reference | ^^^^ - | info: rule `unresolved-reference` was selected on the command line warning[unresolved-reference]: Name `x` used when not defined @@ -1033,7 +1002,6 @@ fn cli_all_rules_warn() -> anyhow::Result<()> { | 2 | prin(x) # unresolved-reference | ^ - | info: rule `unresolved-reference` was selected on the command line Found 2 diagnostics @@ -1079,7 +1047,6 @@ fn cli_all_rules_precedence() -> anyhow::Result<()> { | 6 | prin(y) # unresolved-reference | ^^^^ - | info: rule `unresolved-reference` was selected on the command line Found 1 diagnostic @@ -1159,7 +1126,6 @@ fn configuration_all_rules() -> anyhow::Result<()> { | 6 | prin(y) # unresolved-reference | ^^^^ - | info: rule `unresolved-reference` was selected in the configuration file Found 1 diagnostic @@ -1207,7 +1173,7 @@ fn configuration_all_rules_with_rule_sorting_before_all() -> anyhow::Result<()> exit_code: 1 ----- stdout ----- error[abstract-method-in-final-class]: Final class `Derived` has unimplemented abstract methods - --> test.py:6:5 + --> test.py:11:7 | 6 | / @abstractmethod 7 | | def foo(self) -> int: @@ -1218,7 +1184,6 @@ fn configuration_all_rules_with_rule_sorting_before_all() -> anyhow::Result<()> | ------ 11 | class Derived(Base): | ^^^^^^^ `foo` is unimplemented - | info: rule `abstract-method-in-final-class` was selected in the configuration file Found 1 diagnostic @@ -1270,7 +1235,7 @@ fn overrides_all_rules_with_rule_sorting_before_all() -> anyhow::Result<()> { exit_code: 1 ----- stdout ----- error[abstract-method-in-final-class]: Final class `Derived` has unimplemented abstract methods - --> src/test.py:6:5 + --> src/test.py:11:7 | 6 | / @abstractmethod 7 | | def foo(self) -> int: @@ -1281,7 +1246,6 @@ fn overrides_all_rules_with_rule_sorting_before_all() -> anyhow::Result<()> { | ------ 11 | class Derived(Base): | ^^^^^^^ `foo` is unimplemented - | info: rule `abstract-method-in-final-class` was selected in the configuration file Found 1 diagnostic @@ -1337,7 +1301,6 @@ fn all_overrides() -> anyhow::Result<()> { | 2 | y = 4 / 0 # division-by-zero: error (global) | ^^^^^ - | info: rule `division-by-zero` was selected in the configuration file error[unresolved-reference]: Name `prin` used when not defined @@ -1345,7 +1308,6 @@ fn all_overrides() -> anyhow::Result<()> { | 4 | prin(x) # unresolved-reference: error (global) | ^^^^ - | info: rule `unresolved-reference` was selected in the configuration file error[division-by-zero]: Cannot divide object of type `Literal[4]` by zero @@ -1353,7 +1315,6 @@ fn all_overrides() -> anyhow::Result<()> { | 2 | y = 4 / 0 # division-by-zero: error (global) | ^^^^^ - | info: rule `division-by-zero` was selected in the configuration file warning[unresolved-reference]: Name `prin` used when not defined @@ -1361,7 +1322,6 @@ fn all_overrides() -> anyhow::Result<()> { | 4 | prin(x) # unresolved-reference: warn (override) | ^^^^ - | info: rule `unresolved-reference` was selected in the configuration file Found 4 diagnostics diff --git a/crates/ty/tests/cli/scripts.rs b/crates/ty/tests/cli/scripts.rs index 0fcd39d537..680c4f9d7e 100644 --- a/crates/ty/tests/cli/scripts.rs +++ b/crates/ty/tests/cli/scripts.rs @@ -40,7 +40,6 @@ fn project_settings_and_overrides_do_not_apply() -> anyhow::Result<()> { | 7 | print(missing) | ^^^^^^^ - | Found 1 diagnostic @@ -83,7 +82,6 @@ fn basedpython_metadata_applies() -> anyhow::Result<()> { | 7 | print(4 / 0) | ^^^^^ - | Found 1 diagnostic @@ -124,13 +122,12 @@ fn metadata_without_tool_ty_uses_default_settings() -> anyhow::Result<()> { exit_code: 1 ----- stdout ----- error[invalid-assignment]: Object of type `Literal["not an int"]` is not assignable to `int` - --> script.py:6:8 + --> script.py:6:14 | 6 | value: int = "not an int" | --- ^^^^^^^^^^^^ Incompatible value of type `Literal["not an int"]` | | | Declared type - | Found 1 diagnostic @@ -178,7 +175,6 @@ fn environment_options() -> anyhow::Result<()> { | 12 | reveal_type(sys.version_info[:2] == (3, 12)) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `Literal[True]` - | Found 1 diagnostic @@ -218,7 +214,6 @@ fn inline_overrides_are_ignored() -> anyhow::Result<()> { | 13 | print(missing) | ^^^^^^^ - | Found 1 diagnostic @@ -255,7 +250,6 @@ fn inline_terminal_settings_do_not_apply() -> anyhow::Result<()> { | 10 | print(missing) | ^^^^^^^ - | Found 1 diagnostic @@ -301,7 +295,6 @@ fn inline_settings_override_user_configuration() -> anyhow::Result<()> { | 10 | print(missing) # type: ignore | ^^^^^^^ - | Found 1 diagnostic @@ -344,14 +337,12 @@ fn user_configuration_applies() -> anyhow::Result<()> { | 6 | print(missing) | ^^^^^^^ - | warning[unresolved-reference]: Name `suppressed` used when not defined --> script.py:7:7 | 7 | print(suppressed) # type: ignore | ^^^^^^^^^^ - | Found 2 diagnostics @@ -394,7 +385,6 @@ fn cli_arguments_override_script_options() -> anyhow::Result<()> { | 10 | print(missing) | ^^^^^^^ - | Found 1 diagnostic @@ -446,7 +436,6 @@ fn explicit_config_replaces_inline_metadata() -> anyhow::Result<()> { | 10 | print(missing) | ^^^^^^^ - | Found 1 diagnostic diff --git a/crates/ty/tests/cli/snapshots/cli__api_lockfile__honors_dunder_all.snap b/crates/ty/tests/cli/snapshots/cli__api_lockfile__honors_dunder_all.snap index 92153f8ddd..5fecd8b856 100644 --- a/crates/ty/tests/cli/snapshots/cli__api_lockfile__honors_dunder_all.snap +++ b/crates/ty/tests/cli/snapshots/cli__api_lockfile__honors_dunder_all.snap @@ -10,7 +10,7 @@ success: true exit_code: 0 ----- stdout ----- #api-lock:v=1 -#tool:by=0.0.5 +#tool:by=0.0.8 #python:default #modules:1 al._underscore_public:d()->None diff --git a/crates/ty/tests/cli/snapshots/cli__api_lockfile__renders_class_kind_flags.snap b/crates/ty/tests/cli/snapshots/cli__api_lockfile__renders_class_kind_flags.snap index ddbc765e60..bd3d18c4f5 100644 --- a/crates/ty/tests/cli/snapshots/cli__api_lockfile__renders_class_kind_flags.snap +++ b/crates/ty/tests/cli/snapshots/cli__api_lockfile__renders_class_kind_flags.snap @@ -10,7 +10,7 @@ success: true exit_code: 0 ----- stdout ----- #api-lock:v=1 -#tool:by=0.0.5 +#tool:by=0.0.8 #python:default #modules:1 kind.Colors.BLUE:v=Literal[2] diff --git a/crates/ty/tests/cli/snapshots/cli__api_lockfile__renders_decorators_on_methods.snap b/crates/ty/tests/cli/snapshots/cli__api_lockfile__renders_decorators_on_methods.snap index 853224b997..d6f6c8233a 100644 --- a/crates/ty/tests/cli/snapshots/cli__api_lockfile__renders_decorators_on_methods.snap +++ b/crates/ty/tests/cli/snapshots/cli__api_lockfile__renders_decorators_on_methods.snap @@ -10,7 +10,7 @@ success: true exit_code: 0 ----- stdout ----- #api-lock:v=1 -#tool:by=0.0.5 +#tool:by=0.0.8 #python:default #modules:1 deco.C.abs:d{abstractmethod}(self:deco.C)->builtins.int diff --git a/crates/ty/tests/cli/snapshots/cli__api_lockfile__renders_generic_type_alias.snap b/crates/ty/tests/cli/snapshots/cli__api_lockfile__renders_generic_type_alias.snap index eec4e9c153..db6addcc5e 100644 --- a/crates/ty/tests/cli/snapshots/cli__api_lockfile__renders_generic_type_alias.snap +++ b/crates/ty/tests/cli/snapshots/cli__api_lockfile__renders_generic_type_alias.snap @@ -10,7 +10,7 @@ success: true exit_code: 0 ----- stdout ----- #api-lock:v=1 -#tool:by=0.0.5 +#tool:by=0.0.8 #python:default #modules:1 ta.Plain:t=builtins.int | builtins.str diff --git a/crates/ty/tests/cli/snapshots/cli__api_lockfile__renders_instance_attributes.snap b/crates/ty/tests/cli/snapshots/cli__api_lockfile__renders_instance_attributes.snap index 81455cf639..76d8648ec4 100644 --- a/crates/ty/tests/cli/snapshots/cli__api_lockfile__renders_instance_attributes.snap +++ b/crates/ty/tests/cli/snapshots/cli__api_lockfile__renders_instance_attributes.snap @@ -10,7 +10,7 @@ success: true exit_code: 0 ----- stdout ----- #api-lock:v=1 -#tool:by=0.0.5 +#tool:by=0.0.8 #python:default #modules:1 ia.C.__init__:d(self:ia.C,x:builtins.int)->None diff --git a/crates/ty/tests/cli/snapshots/cli__api_lockfile__renders_property_accessors.snap b/crates/ty/tests/cli/snapshots/cli__api_lockfile__renders_property_accessors.snap index 6dd5fdc2ed..be8994d49a 100644 --- a/crates/ty/tests/cli/snapshots/cli__api_lockfile__renders_property_accessors.snap +++ b/crates/ty/tests/cli/snapshots/cli__api_lockfile__renders_property_accessors.snap @@ -10,7 +10,7 @@ success: true exit_code: 0 ----- stdout ----- #api-lock:v=1 -#tool:by=0.0.5 +#tool:by=0.0.8 #python:default #modules:1 p.C.ro:p[getter]=builtins.int diff --git a/crates/ty/tests/cli/snapshots/cli__api_lockfile__renders_qualifiers_on_variables.snap b/crates/ty/tests/cli/snapshots/cli__api_lockfile__renders_qualifiers_on_variables.snap index cf7abb9388..cd5437f834 100644 --- a/crates/ty/tests/cli/snapshots/cli__api_lockfile__renders_qualifiers_on_variables.snap +++ b/crates/ty/tests/cli/snapshots/cli__api_lockfile__renders_qualifiers_on_variables.snap @@ -10,7 +10,7 @@ success: true exit_code: 0 ----- stdout ----- #api-lock:v=1 -#tool:by=0.0.5 +#tool:by=0.0.8 #python:default #modules:1 q.C.a:v[classvar]=builtins.int diff --git a/crates/ty/tests/cli/uv_workspace.rs b/crates/ty/tests/cli/uv_workspace.rs new file mode 100644 index 0000000000..a0c6b0fcd1 --- /dev/null +++ b/crates/ty/tests/cli/uv_workspace.rs @@ -0,0 +1,427 @@ +//! Integration tests for ty's side of `uv check`. +//! +//! Corresponding uv-side workspace tests live at +//! . + +#[cfg(feature = "test-uv")] +use std::{path::Path, process::Command}; + +use insta_cmd::assert_cmd_snapshot; + +use crate::CliTest; + +fn workspace_case() -> anyhow::Result { + CliTest::with_files([ + ( + "pyproject.toml", + r#" +[tool.uv.workspace] +members = ["packages/*"] +"#, + ), + ( + "packages/member/pyproject.toml", + r#" +[project] +name = "member" +version = "0.1.0" +requires-python = ">=3.8" +"#, + ), + ( + "packages/member/member.py", + "value: int = 'selected-member'", + ), + ( + "packages/sibling/pyproject.toml", + r#" +[project] +name = "sibling" +version = "0.1.0" +requires-python = ">=3.8" +"#, + ), + ( + "packages/sibling/sibling.py", + "value: int = 'unselected-sibling'", + ), + ]) +} + +#[cfg(feature = "test-uv")] +fn command_with_uv(case: &CliTest, virtual_env: Option<&Path>) -> anyhow::Result { + let mut sync = Command::new("uv"); + sync.current_dir(case.root()) + .args(["workspace", "metadata", "--sync"]) + .env("UV_CACHE_DIR", case.root().join("cache")) + .env("UV_OFFLINE", "1") + .env("UV_PYTHON_DOWNLOADS", "never"); + if let Some(virtual_env) = virtual_env { + sync.arg("--active").env("VIRTUAL_ENV", virtual_env); + } + anyhow::ensure!( + sync.output()?.status.success(), + "failed to prepare uv workspace" + ); + + let mut command = case.command(); + command + .env("TY_UV", "1") + .env("UV", "uv") + .env("UV_CACHE_DIR", case.root().join("cache")) + .env("UV_OFFLINE", "1") + .env("UV_PYTHON_DOWNLOADS", "never") + .env("TY_OUTPUT_FORMAT", "concise") + .env("PATH", std::env::var_os("PATH").unwrap_or_default()); + #[cfg(windows)] + if let Some(path_ext) = std::env::var_os("PATHEXT") { + command.env("PATHEXT", path_ext); + } + if let Some(virtual_env) = virtual_env { + command.env("VIRTUAL_ENV", virtual_env); + } + + Ok(command) +} + +/// The workspace root provides first-party imports without expanding analysis to unselected +/// sibling members. +#[cfg(feature = "test-uv")] +#[test] +fn uses_uv_workspace_root_without_checking_siblings() -> anyhow::Result<()> { + let case = workspace_case()?; + case.write_file("shared.py", "value: int = 'unselected-workspace-root'")?; + case.write_file( + "packages/member/member.py", + "import shared\nvalue: int = 'selected-member'", + )?; + + let mut command = command_with_uv(&case, None)?; + command + .current_dir(case.root().join("packages/member")) + .arg("."); + + assert_cmd_snapshot!(command, @r#" + success: false + exit_code: 1 + ----- stdout ----- + member.py:2:14: error[invalid-assignment] Object of type `Literal["selected-member"]` is not assignable to `int` + Found 1 diagnostic + + ----- stderr ----- + "#); + assert!(case.root().join(".venv").is_dir()); + + Ok(()) +} + +/// An explicit file is treated as a script, so workspace discovery stays disabled even when +/// `TY_UV` is set. +#[cfg(feature = "test-uv")] +#[test] +fn explicit_file_path_disables_uv_workspace_discovery() -> anyhow::Result<()> { + let case = workspace_case()?; + case.write_file("shared.py", "value: int = 'unselected-workspace-root'")?; + case.write_file( + "packages/member/member.py", + "import shared\nvalue: int = 'selected-script'", + )?; + + let mut command = command_with_uv(&case, None)?; + command + .current_dir(case.root().join("packages/member")) + .arg("member.py"); + + assert_cmd_snapshot!(command, @r#" + success: false + exit_code: 1 + ----- stdout ----- + member.py:1:8: error[unresolved-import] Cannot resolve imported module `shared` + member.py:2:14: error[invalid-assignment] Object of type `Literal["selected-script"]` is not assignable to `int` + Found 2 diagnostics + + ----- stderr ----- + "#); + + Ok(()) +} + +/// An explicitly selected member inherits ty rule configuration from the uv workspace root. +#[cfg(feature = "test-uv")] +#[test] +fn explicit_workspace_member_directory_uses_workspace_configuration() -> anyhow::Result<()> { + let case = workspace_case()?; + case.write_file( + "pyproject.toml", + r#" +[tool.uv.workspace] +members = ["packages/*"] + +[tool.ty.rules] +invalid-assignment = "ignore" +"#, + )?; + let mut command = command_with_uv(&case, None)?; + command.arg("packages/member"); + + assert_cmd_snapshot!(command, @" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + "); + + Ok(()) +} + +/// Workspace configuration still applies when the selected member lives outside the workspace +/// root's directory tree. +#[cfg(feature = "test-uv")] +#[test] +fn external_workspace_member_uses_workspace_configuration() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" +[tool.uv.workspace] +members = ["../external-package"] + +[tool.ty.rules] +invalid-assignment = "ignore" +"#, + ), + ( + "../external-package/pyproject.toml", + r#" +[project] +name = "external-package" +version = "0.1.0" +requires-python = ">=3.8" +"#, + ), + ( + "../external-package/member.py", + "value: int = 'selected-external-member'", + ), + ])?; + + let mut command = command_with_uv(&case, None)?; + command + .args(["--project", "../external-package", "../external-package"]) + .env("UV_PROJECT", case.root()); + + assert_cmd_snapshot!(command, @" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + "); + + Ok(()) +} + +/// Excludes passed by `uv check` prevent an unselected nested member from being analyzed. +#[cfg(feature = "test-uv")] +#[test] +fn selected_workspace_member_excludes_nested_member() -> anyhow::Result<()> { + let case = workspace_case()?; + case.write_file( + "pyproject.toml", + r#" +[tool.uv.workspace] +members = ["packages/*", "packages/member/nested"] +"#, + )?; + case.write_file( + "packages/member/nested/pyproject.toml", + r#" +[project] +name = "nested" +version = "0.1.0" +requires-python = ">=3.8" +"#, + )?; + case.write_file( + "packages/member/nested/nested.py", + "value: int = 'unselected-nested-member'", + )?; + + let mut command = command_with_uv(&case, None)?; + command.args(["--exclude", "packages/member/nested", "packages/member"]); + + assert_cmd_snapshot!(command, @r#" + success: false + exit_code: 1 + ----- stdout ----- + packages/member/member.py:1:14: error[invalid-assignment] Object of type `Literal["selected-member"]` is not assignable to `int` + Found 1 diagnostic + + ----- stderr ----- + "#); + + Ok(()) +} + +/// Metadata discovery preserves uv's active isolated environment instead of using an invalid +/// Python environment configured in the workspace. +#[cfg(feature = "test-uv")] +#[test] +fn forwards_active_environment_to_uv() -> anyhow::Result<()> { + let case = workspace_case()?; + case.write_file( + "pyproject.toml", + r#" +[tool.uv.workspace] +members = ["packages/*"] + +[tool.ty.environment] +python = "missing-configured-environment" +"#, + )?; + let environment = case.root().join("isolated"); + let mut command = command_with_uv(&case, Some(&environment))?; + command + .current_dir(case.root().join("packages/member")) + .arg(".") + .env_remove("UV_PROJECT_ENVIRONMENT"); + + assert_cmd_snapshot!(command, @r#" + success: false + exit_code: 1 + ----- stdout ----- + member.py:1:14: error[invalid-assignment] Object of type `Literal["selected-member"]` is not assignable to `int` + Found 1 diagnostic + + ----- stderr ----- + "#); + + assert!(environment.is_dir()); + assert!(!case.root().join(".venv").exists()); + + Ok(()) +} + +/// Merely exposing the uv executable must not change ordinary ty project discovery without +/// `TY_UV`. +#[test] +fn uv_workspace_discovery_is_opt_in() -> anyhow::Result<()> { + let case = workspace_case()?; + case.write_file("shared.py", "value: int = 'unselected-workspace-root'")?; + case.write_file( + "packages/member/member.py", + "import shared\nvalue: int = 'selected-member'", + )?; + + let mut command = case.command(); + command + .current_dir(case.root().join("packages/member")) + .env("UV", "uv") + .env("TY_OUTPUT_FORMAT", "concise") + .env_remove("TY_UV"); + + assert_cmd_snapshot!(command, @r#" + success: false + exit_code: 1 + ----- stdout ----- + member.py:1:8: error[unresolved-import] Cannot resolve imported module `shared` + member.py:2:14: error[invalid-assignment] Object of type `Literal["selected-member"]` is not assignable to `int` + Found 2 diagnostics + + ----- stderr ----- + "#); + + Ok(()) +} + +/// Failures to locate uv are visible by default instead of silently disabling integration. +#[test] +fn warns_when_uv_workspace_metadata_cannot_be_loaded() -> anyhow::Result<()> { + let case = workspace_case()?.with_filter( + "no path to search and provided name is not an absolute path", + "cannot find binary path", + ); + case.write_file("packages/member/member.py", "value: int = 1")?; + + let mut command = case.command(); + command + .current_dir(case.root().join("packages/member")) + .arg(".") + .env("TY_UV", "1") + .env_remove("UV") + .env("PATH", "") + .env("TY_OUTPUT_FORMAT", "concise"); + + assert_cmd_snapshot!(command, @" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + WARN Failed to invoke `uv workspace metadata`: failed to resolve uv executable: cannot find binary path + "); + + Ok(()) +} + +/// Workspace discovery can find uv on `PATH` when the `UV` executable override is absent. +#[cfg(feature = "test-uv")] +#[test] +fn finds_uv_on_path_without_uv_environment_variable() -> anyhow::Result<()> { + let case = workspace_case()?; + case.write_file("shared.py", "value: int = 'unselected-workspace-root'")?; + case.write_file( + "packages/member/member.py", + "import shared\nvalue: int = 'selected-member'", + )?; + + let mut command = command_with_uv(&case, None)?; + command + .current_dir(case.root().join("packages/member")) + .arg(".") + .env_remove("UV"); + + assert_cmd_snapshot!(command, @r#" + success: false + exit_code: 1 + ----- stdout ----- + member.py:2:14: error[invalid-assignment] Object of type `Literal["selected-member"]` is not assignable to `int` + Found 1 diagnostic + + ----- stderr ----- + "#); + + Ok(()) +} + +/// Version-sensitive diagnostics attribute their assumed Python version to workspace metadata, +/// not to a command-line override. +#[cfg(feature = "test-uv")] +#[test] +fn reports_uv_workspace_python_version_source() -> anyhow::Result<()> { + let case = workspace_case()?; + case.write_file("packages/member/member.py", "frozendict")?; + + for output_format in ["full", "concise"] { + let mut command = command_with_uv(&case, None)?; + command + .current_dir(case.root().join("packages/member")) + .arg(".") + .arg("--output-format") + .arg(output_format); + + let output = command.output()?; + let stdout = String::from_utf8(output.stdout)?; + assert!(!output.status.success()); + assert!(!stdout.contains("specified on the command line")); + if output_format == "full" { + assert!(stdout.contains("provided by uv workspace metadata")); + } + } + + Ok(()) +} diff --git a/crates/ty/tests/file_watching.rs b/crates/ty/tests/file_watching.rs index 6a79143958..8999c21ea5 100644 --- a/crates/ty/tests/file_watching.rs +++ b/crates/ty/tests/file_watching.rs @@ -11,7 +11,7 @@ use ruff_db::system::{ }; use ruff_python_ast::PythonVersion; use ruff_ranged_value::{RangedValue, ValueSource}; -use ty_module_resolver::{Module, ModuleName, resolve_module_confident}; +use ty_module_resolver::{Module, ModuleName}; use ty_project::metadata::options::{EnvironmentOptions, Options, SrcOptions}; use ty_project::metadata::pyproject::{PyProject, Tool}; use ty_project::metadata::python_version::SupportedPythonVersion; @@ -32,6 +32,17 @@ struct TestCase { root_dir: SystemPathBuf, } +fn resolve_module_confident<'db>( + db: &'db ProjectDatabase, + module_name: &ModuleName, +) -> Option> { + ty_module_resolver::resolve_module_confident( + db, + db.project().program(db).resolver_environment(db), + module_name, + ) +} + impl TestCase { fn project_path(&self, relative: impl AsRef) -> SystemPathBuf { SystemPath::absolute(relative, self.db.project().root(&self.db)) @@ -1401,11 +1412,11 @@ print(sys.last_exc, os.getegid()) assert_eq!(diagnostics.len(), 2); assert_eq!( - diagnostics[0].primary_message(), + diagnostics[0].headline_message(), "Module `sys` has no member `last_exc`" ); assert_eq!( - diagnostics[1].primary_message(), + diagnostics[1].headline_message(), "Module `os` has no member `getegid`" ); @@ -1459,7 +1470,7 @@ fn reloading_options_updates_inferred_python_version_diagnostics_when_metadata_i assert_eq!(diagnostics.len(), 1); assert_eq!( - diagnostics[0].primary_message(), + diagnostics[0].headline_message(), format!( "Ignoring unsupported inferred Python version `3.{unsupported_minor}`; ty will use Python {} instead.", PythonVersion::latest_ty() diff --git a/crates/ty_combine/Cargo.toml b/crates/ty_combine/Cargo.toml index acafd2457a..99241b4673 100644 --- a/crates/ty_combine/Cargo.toml +++ b/crates/ty_combine/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_combine" -version = "0.0.5" +version = "0.0.8" description = "This is an internal component crate of Ruff" edition.workspace = true rust-version.workspace = true diff --git a/crates/ty_combine/README.md b/crates/ty_combine/README.md index 6542e860cc..de9f6647aa 100644 --- a/crates/ty_combine/README.md +++ b/crates/ty_combine/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ty_combine). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ty_combine). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_completion_bench/src/main.rs b/crates/ty_completion_bench/src/main.rs index 914fe754a3..4b809604e7 100644 --- a/crates/ty_completion_bench/src/main.rs +++ b/crates/ty_completion_bench/src/main.rs @@ -17,7 +17,7 @@ use ty_ide::{Completion, CompletionCapabilities}; use ty_project::metadata::Options; use ty_project::metadata::options::EnvironmentOptions; use ty_project::metadata::value::RelativePathBuf; -use ty_project::{ProjectDatabase, ProjectMetadata}; +use ty_project::{ProjectDatabase, ProjectMetadata, SemanticDb as _}; #[derive(Debug, clap::Parser)] #[command( @@ -138,11 +138,13 @@ fn get_completions<'db>( let file = system_path_to_file(db, path) .with_context(|| format!("failed to get database file for `{path}`"))?; let settings = ty_ide::CompletionSettings::default(); + let program_file = db.program_file(file); Ok(ty_ide::completion( db, + &ty_ide::ProgramEnvironment::from_file(program_file), &settings, CompletionCapabilities::default(), - file, + program_file, offset, )) } diff --git a/crates/ty_completion_eval/completion-evaluation-tasks.csv b/crates/ty_completion_eval/completion-evaluation-tasks.csv index 83b14bc4d3..04d45b6473 100644 --- a/crates/ty_completion_eval/completion-evaluation-tasks.csv +++ b/crates/ty_completion_eval/completion-evaluation-tasks.csv @@ -19,8 +19,22 @@ import-deprioritizes-sunder,main.py,0,1 import-deprioritizes-type_check_only,main.py,0,1 import-deprioritizes-type_check_only,main.py,1,1 import-deprioritizes-type_check_only,main.py,2,1 -import-deprioritizes-type_check_only,main.py,3,2 -import-deprioritizes-type_check_only,main.py,4,3 +import-deprioritizes-type_check_only,main.py,3,1 +import-deprioritizes-type_check_only,main.py,4,1 +import-deprioritizes-type_check_only,main.py,5,2 +import-deprioritizes-type_check_only,main.py,6,3 +import-deprioritizes-type_check_only,main.py,7,1 +import-deprioritizes-type_check_only,main.py,8,1 +import-deprioritizes-type_check_only,main.py,9,1 +import-deprioritizes-type_check_only,main.py,10,1 +import-deprioritizes-type_check_only,main.py,11,1 +import-deprioritizes-type_check_only,main.py,12,1 +import-deprioritizes-type_check_only,main.py,13,1 +import-deprioritizes-type_check_only,main.py,14,1 +import-deprioritizes-type_check_only,main.py,15,1 +import-deprioritizes-type_check_only,main.py,16,1 +import-deprioritizes-type_check_only,main.pyi,0,1 +import-deprioritizes-type_check_only,main.pyi,1,1 import-keyword-completion,main.py,0,1 internal-typeshed-hidden,main.py,0,1 local-over-auto-import,main.py,0,1 @@ -46,3 +60,15 @@ typing-gets-priority,main.py,1,1 typing-gets-priority,main.py,2,1 typing-gets-priority,main.py,3,1 typing-gets-priority,main.py,4,1 +typing-only-auto-import-ranking,main.py,0,1 +typing-only-auto-import-ranking,main.py,1,1 +typing-only-auto-import-ranking,main.py,2,1 +typing-only-auto-import-ranking,main.py,3,2 +typing-only-auto-import-ranking,main.py,4,1 +typing-only-auto-import-ranking,main.py,5,1 +typing-only-auto-import-ranking,main.py,6,1 +typing-only-auto-import-ranking,main.py,7,1 +typing-only-auto-import-ranking,main.py,8,1 +typing-only-auto-import-ranking,main.pyi,0,1 +typing-only-auto-import-ranking,main.pyi,1,1 +typing-only-auto-import-ranking,main.pyi,2,1 diff --git a/crates/ty_completion_eval/src/main.rs b/crates/ty_completion_eval/src/main.rs index 87496beace..abb7a5a63a 100644 --- a/crates/ty_completion_eval/src/main.rs +++ b/crates/ty_completion_eval/src/main.rs @@ -17,7 +17,7 @@ use ty_module_resolver::ModuleName; use ty_project::metadata::Options; use ty_project::metadata::options::EnvironmentOptions; use ty_project::metadata::value::RelativePathBuf; -use ty_project::{ProjectDatabase, ProjectMetadata}; +use ty_project::{ProjectDatabase, ProjectMetadata, SemanticDb as _}; #[derive(Debug, clap::Parser)] #[command( @@ -326,11 +326,13 @@ impl Task { self.cursor.offset ) })?; + let program_file = self.db.program_file(file); let completions = ty_ide::completion( &self.db, + &ty_ide::ProgramEnvironment::from_file(program_file), &self.settings, CompletionCapabilities::default(), - file, + program_file, offset, ); Ok(completions) @@ -545,6 +547,7 @@ fn copy_project(src_dir: &SystemPath, dst_dir: &SystemPath) -> anyhow::Result from module import unique_prefix_ +from private_stub import _Al from module import Class Class.meth_ +private_stub._Al # TODO: bound methods don't preserve type-check-only-ness, this is a bug Class().meth_ # TODO: auto-imports don't take type-check-only-ness into account, this is a bug UniquePrefixA + +if TYPE_CHECKING: + from module import UniquePrefixA + from module import unique_prefix_ + from private_stub import _Al + + Class.meth_ + private_stub._Al + + def declared_in_type_checking_block() -> None: + private_stub._Al + + +def function_scope() -> None: + if TYPE_CHECKING: + from private_stub import _Al + + private_stub._Al + + +if not TYPE_CHECKING: + pass +else: + private_stub._Al + + +if TYPE_CHECKING: + pass +else: + from private_stub import _Al diff --git a/crates/ty_completion_eval/truth/import-deprioritizes-type_check_only/main.pyi b/crates/ty_completion_eval/truth/import-deprioritizes-type_check_only/main.pyi new file mode 100644 index 0000000000..e19bc4ed36 --- /dev/null +++ b/crates/ty_completion_eval/truth/import-deprioritizes-type_check_only/main.pyi @@ -0,0 +1,3 @@ +# Typing-only declarations are not penalized when completing a stub. +from module import UniquePrefixA +from module import unique_prefix_ diff --git a/crates/ty_completion_eval/truth/import-deprioritizes-type_check_only/private_stub.pyi b/crates/ty_completion_eval/truth/import-deprioritizes-type_check_only/private_stub.pyi new file mode 100644 index 0000000000..0d5750ca18 --- /dev/null +++ b/crates/ty_completion_eval/truth/import-deprioritizes-type_check_only/private_stub.pyi @@ -0,0 +1,4 @@ +from typing import TypeVar + +_Alpha = TypeVar("_Alpha") +_Alzeta = 1 diff --git a/crates/ty_completion_eval/truth/typing-only-auto-import-ranking/completion.toml b/crates/ty_completion_eval/truth/typing-only-auto-import-ranking/completion.toml new file mode 100644 index 0000000000..cbd5805f07 --- /dev/null +++ b/crates/ty_completion_eval/truth/typing-only-auto-import-ranking/completion.toml @@ -0,0 +1,2 @@ +[settings] +auto-import = true diff --git a/crates/ty_completion_eval/truth/typing-only-auto-import-ranking/main.py b/crates/ty_completion_eval/truth/typing-only-auto-import-ranking/main.py new file mode 100644 index 0000000000..013ee5c43f --- /dev/null +++ b/crates/ty_completion_eval/truth/typing-only-auto-import-ranking/main.py @@ -0,0 +1,21 @@ +from typing import TYPE_CHECKING + +# Runtime symbols outrank alternatives from typing-only modules in Python files. +deprecated +NoneTy +Not + +# Typing-only symbols are included in auto-import suggestions. +static_ass +is_equiv +TypedDictFall + +# Typing-only symbols retain their usual ranking inside TYPE_CHECKING blocks. +if TYPE_CHECKING: + deprecated + NoneTy + + +def function_scope() -> None: + if TYPE_CHECKING: + deprecated diff --git a/crates/ty_completion_eval/truth/typing-only-auto-import-ranking/main.pyi b/crates/ty_completion_eval/truth/typing-only-auto-import-ranking/main.pyi new file mode 100644 index 0000000000..1ae2ffaf50 --- /dev/null +++ b/crates/ty_completion_eval/truth/typing-only-auto-import-ranking/main.pyi @@ -0,0 +1,4 @@ +# Typing-only symbols retain their usual ranking in stub files. +deprecated +NoneTy +static_ass diff --git a/crates/ty_completion_eval/truth/typing-only-auto-import-ranking/pyproject.toml b/crates/ty_completion_eval/truth/typing-only-auto-import-ranking/pyproject.toml new file mode 100644 index 0000000000..cd277d8097 --- /dev/null +++ b/crates/ty_completion_eval/truth/typing-only-auto-import-ranking/pyproject.toml @@ -0,0 +1,5 @@ +[project] +name = "test" +version = "0.1.0" +requires-python = ">=3.13" +dependencies = [] diff --git a/crates/ty_completion_eval/truth/typing-only-auto-import-ranking/uv.lock b/crates/ty_completion_eval/truth/typing-only-auto-import-ranking/uv.lock new file mode 100644 index 0000000000..a4937d10d3 --- /dev/null +++ b/crates/ty_completion_eval/truth/typing-only-auto-import-ranking/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "test" +version = "0.1.0" +source = { virtual = "." } diff --git a/crates/ty_ide/src/add_dependency.rs b/crates/ty_ide/src/add_dependency.rs index 6b4e69f9be..1b126a22eb 100644 --- a/crates/ty_ide/src/add_dependency.rs +++ b/crates/ty_ide/src/add_dependency.rs @@ -18,6 +18,7 @@ use ty_project::Db; use ty_python_semantic::dependencies::{GroupName, available_groups}; use crate::code_action::{FileEdit, QuickFix}; +use ty_module_resolver::ImportingFile; /// The actions that declare the distribution an import at `range` needs. /// @@ -70,7 +71,7 @@ pub(crate) fn code_actions(db: &dyn Db, file: File, range: TextRange) -> Vec Option { - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, db.program_file(file).python_file(db)).load(db); let covering = covering_node(parsed.syntax().into(), range); // `import a.b` anchors the diagnostic on the alias, `from a.b import c` on @@ -103,9 +104,15 @@ fn distribution_to_declare( ) -> Option { let root = ModuleName::new(module_name.first_component())?; - match resolve_module(db, file, module_name) { + match resolve_module( + db, + ImportingFile::File(file, db.program_file(file).resolver_environment(db)), + module_name, + ) { Some(module) => { - let owners = ty_module_resolver::distribution_index(db).owners_of(db, module); + let resolver_environment = db.program_file(file).resolver_environment(db); + let owners = ty_module_resolver::distribution_index(db, resolver_environment) + .owners_of(db, module); let available = available_groups(db, file); owners .iter() diff --git a/crates/ty_ide/src/all_symbols.rs b/crates/ty_ide/src/all_symbols.rs index ea346b6d94..5b9edffedb 100644 --- a/crates/ty_ide/src/all_symbols.rs +++ b/crates/ty_ide/src/all_symbols.rs @@ -1,8 +1,9 @@ use compact_str::CompactString; use rayon::prelude::*; use ruff_db::files::File; -use ty_module_resolver::{Module, ModuleName, all_modules, resolve_real_shadowable_module}; +use ty_module_resolver::{Module, all_modules}; use ty_project::{Db, parallel::ParallelIteratorExt}; +use ty_python_core::ProgramFile; use ty_python_semantic::dependencies::{self, ImportStanding}; use crate::{ @@ -16,7 +17,7 @@ use crate::{ /// by the query. pub fn all_symbols<'db>( db: &'db dyn Db, - importing_from: File, + importing_from: ProgramFile<'db>, query: &QueryPattern, ) -> Vec> { // If the query is empty, return immediately to avoid expensive file scanning @@ -27,16 +28,13 @@ pub fn all_symbols<'db>( let all_symbols_span = tracing::debug_span!("all_symbols"); let _span = all_symbols_span.enter(); - let typing_extensions = ModuleName::new_static("typing_extensions").unwrap(); - let is_typing_extensions_available = importing_from.is_stub(db) - || resolve_real_shadowable_module(db, importing_from, &typing_extensions).is_some(); + let program = importing_from.program(db); + let importing_file = importing_from.file(db); + let resolver_environment = importing_from.resolver_environment(db); - let results = all_modules(db) + let results = all_modules(db, resolver_environment) .into_par_iter() .map_with_db(db, |db, module| { - let Some(file) = module.file(db) else { - return Vec::new(); - }; let name = module.name(db); // Note that this will always consider namespace @@ -47,9 +45,11 @@ pub fn all_symbols<'db>( // namespace packages in auto-import anyway.) let is_non_first_party = module.search_path(db).is_none_or(|sp| !sp.is_first_party()); - // Filter out non-first-party modules that are conventionally - // regarded as private or tests. - if is_non_first_party && (name.is_private() || name.is_test_module()) { + // Filter out non-first-party test and private modules, while retaining private + // typeshed packages that are useful when writing type annotations. + if is_non_first_party + && (name.is_test_module() || name.is_private() && !module.is_type_check_only(db)) + { return Vec::new(); } @@ -58,17 +58,16 @@ pub fn all_symbols<'db>( // so there is no way for the user to ask for one on purpose here; // offering them means offering imports that break on a fresh install. if matches!( - dependencies::import_standing(db, importing_from, module), + dependencies::import_standing(db, importing_file, module), ImportStanding::Undeclared { .. } ) { return Vec::new(); } - // TODO: also make it available in `TYPE_CHECKING` blocks - // (we'd need https://github.com/astral-sh/ty/issues/1553 to do this well) - if !is_typing_extensions_available && name == &typing_extensions { + let Some(file) = module.file(db) else { return Vec::new(); - } + }; + let program_file = ProgramFile::new(db, file, program); let symbols_for_file_span = tracing::debug_span!( parent: &all_symbols_span, @@ -84,7 +83,7 @@ pub fn all_symbols<'db>( // a `private` symbol is module-internal; auto-importing it would // land a `private-import` error let private = ty_python_semantic::private_symbols(db, file); - for (_, symbol) in symbols_for_file_global_only(db, file).search(query) { + for (_, symbol) in symbols_for_file_global_only(db, program_file).search(query) { // Test functions (starting with `test_`) in third-party // packages are almost never useful to import. if is_non_first_party && symbol.name.starts_with("test_") { @@ -211,7 +210,7 @@ impl<'db> AllSymbolInfo<'db> { /// /// This is only available for symbols that have been imported /// into `Self::module()` *and* are determined to be re-exports. - pub(crate) fn imported_from(&self) -> Option<&ImportedFrom> { + fn imported_from(&self) -> Option<&ImportedFrom> { self.symbol .as_ref() .and_then(|symbol| symbol.imported_from.as_ref()) @@ -633,7 +632,6 @@ def zqzqzq(): | 2 | from pandas.io.api import zqzqzq | ^^^^^^ - | info: Function zqzqzq "); } @@ -678,7 +676,6 @@ def zqzqzq(): | 2 | from pandas.io.api import zqzqzq as zqzqzq | ^^^^^^ - | info: Function zqzqzq "); } @@ -723,11 +720,14 @@ def zqzqzq(): | 2 | from pandas.io.api import * | ^ - | info: Function zqzqzq "); - let symbols = all_symbols(&test.db, test.cursor.file, &QueryPattern::fuzzy("zqzqzq")); + let symbols = all_symbols( + &test.db, + test.program_file(test.cursor.file), + &QueryPattern::fuzzy("zqzqzq"), + ); let symbol = symbols .iter() .find_map(|info| info.symbol.as_ref()) @@ -781,7 +781,6 @@ def zqzqzq(): | 2 | from pandas.io.parsers import zqzqzq | ^^^^^^ - | info: Function zqzqzq info[all-symbols]: AllSymbolInfo @@ -789,7 +788,6 @@ def zqzqzq(): | 2 | from pandas.io.parsers.readers import zqzqzq | ^^^^^^ - | info: Function zqzqzq "); } @@ -842,7 +840,6 @@ __all__ = ['zqzqzq'] | 2 | def zqzqzq(): | ^^^^^^ - | info: Function zqzqzq "); } @@ -873,7 +870,6 @@ def zqzqzq(): | 2 | def zqzqzq(): | ^^^^^^ - | info: Function zqzqzq info[all-symbols]: AllSymbolInfo @@ -881,7 +877,6 @@ def zqzqzq(): | 1 | from pandas import zqzqzq as zqzqzq | ^^^^^^ - | info: Function zqzqzq "); } @@ -915,7 +910,6 @@ def zqzqzq(): | 2 | def zqzqzq(): | ^^^^^^ - | info: Function zqzqzq info[all-symbols]: AllSymbolInfo @@ -923,7 +917,6 @@ def zqzqzq(): | 1 | from pandas import zqzqzq as zqzqzq | ^^^^^^ - | info: Function zqzqzq info[all-symbols]: AllSymbolInfo @@ -931,7 +924,6 @@ def zqzqzq(): | 1 | from pandas import zqzqzq as zqzqzq | ^^^^^^ - | info: Function zqzqzq "); } @@ -973,7 +965,6 @@ ABCDEFGHIJKLMNOP = 'https://api.example.com' | 2 | ABCDEFGHIJKLMNOP = 'https://api.example.com' | ^^^^^^^^^^^^^^^^ - | info: Constant ABCDEFGHIJKLMNOP info[all-symbols]: AllSymbolInfo @@ -981,7 +972,6 @@ ABCDEFGHIJKLMNOP = 'https://api.example.com' | 2 | class Abcdefghijklmnop: | ^^^^^^^^^^^^^^^^ - | info: Class Abcdefghijklmnop info[all-symbols]: AllSymbolInfo @@ -989,7 +979,6 @@ ABCDEFGHIJKLMNOP = 'https://api.example.com' | 2 | def abcdefghijklmnop(): | ^^^^^^^^^^^^^^^^ - | info: Function abcdefghijklmnop "); } @@ -1020,7 +1009,6 @@ def test_helper_xyzxyzxyz(): | 2 | def test_helper_xyzxyzxyz(): | ^^^^^^^^^^^^^^^^^^^^^ - | info: Function test_helper_xyzxyzxyz "); } @@ -1056,7 +1044,6 @@ def test_helper_xyzxyzxyz(): | 1 | def helper_xyzxyzxyz(): pass | ^^^^^^^^^^^^^^^^ - | info: Function helper_xyzxyzxyz info[all-symbols]: AllSymbolInfo @@ -1064,7 +1051,6 @@ def test_helper_xyzxyzxyz(): | 1 | def useful_xyzxyzxyz(): pass | ^^^^^^^^^^^^^^^^ - | info: Function useful_xyzxyzxyz "); } @@ -1089,7 +1075,6 @@ def test_helper_xyzxyzxyz(): | 1 | ZQZQZQ = 1 | ^^^^^^ - | info: Constant ZQZQZQ info[all-symbols]: AllSymbolInfo @@ -1097,7 +1082,6 @@ def test_helper_xyzxyzxyz(): | 1 | ZQZQZQ = 1 | ^^^^^^ - | info: Constant ZQZQZQ info[all-symbols]: AllSymbolInfo @@ -1105,7 +1089,6 @@ def test_helper_xyzxyzxyz(): | 1 | ZQZQZQ = 1 | ^^^^^^ - | info: Constant ZQZQZQ info[all-symbols]: AllSymbolInfo @@ -1113,7 +1096,6 @@ def test_helper_xyzxyzxyz(): | 1 | ZQZQZQ = 1 | ^^^^^^ - | info: Constant ZQZQZQ "); } @@ -1137,7 +1119,6 @@ def test_helper_xyzxyzxyz(): | 1 | ZQZQZQ = 1 | ^^^^^^ - | info: Constant ZQZQZQ "); } @@ -1166,14 +1147,17 @@ private protocol Zqzqzqzq_reader: | 4 | type Zqzqzqzq_open = str | ^^^^^^^^^^^^^ - | info: Variable Zqzqzqzq_open "); } impl CursorTest { fn all_symbols(&self, query: &str) -> String { - let symbols = all_symbols(&self.db, self.cursor.file, &QueryPattern::fuzzy(query)); + let symbols = all_symbols( + &self.db, + self.program_file(self.cursor.file), + &QueryPattern::fuzzy(query), + ); if symbols.is_empty() { return "No symbols found".to_string(); diff --git a/crates/ty_ide/src/call_hierarchy.rs b/crates/ty_ide/src/call_hierarchy.rs index 7fd38b180c..7dd33cad8a 100644 --- a/crates/ty_ide/src/call_hierarchy.rs +++ b/crates/ty_ide/src/call_hierarchy.rs @@ -20,6 +20,8 @@ use ruff_python_ast::name::Name; use ruff_python_ast::token::Tokens; use ruff_python_ast::{self as ast, AnyNodeRef}; use ruff_text_size::{Ranged, TextRange, TextSize}; +use ty_module_resolver::ResolverFile; +use ty_python_core::ProgramFile; use ty_python_core::definition::DefinitionKind; use ty_python_semantic::{ImportAliasResolution, ResolvedDefinition, SemanticModel}; @@ -32,10 +34,10 @@ use ty_python_semantic::{ImportAliasResolution, ResolvedDefinition, SemanticMode /// cursor on a specific `@overload def` yields just that one. pub fn prepare_call_hierarchy( db: &dyn Db, - file: File, + file: ProgramFile<'_>, offset: TextSize, ) -> Option> { - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, file.python_file(db)).load(db); let model = SemanticModel::new(db, file); let goto_target = find_goto_target(&model, &module, offset)?; let definitions = goto_target @@ -48,7 +50,7 @@ pub fn prepare_call_hierarchy( continue; }; - let module_ref = parsed_module(db, def.file(db)).load(db); + let module_ref = parsed_module(db, def.python_file(db)).load(db); if let Some(item) = CallHierarchyItem::from_definition(db, resolved, &module_ref) { items.push(item); @@ -108,7 +110,7 @@ impl CallHierarchyItem { Some(CallHierarchyItem { name: Name::new(name), kind, - detail: module_detail(db, def_file), + detail: module_detail(db, def.program_file(db).resolver_file(db)), file: def_file, full_range: def.full_range(db, module).range(), selection_range: def.focus_range(db, module).range(), @@ -116,7 +118,7 @@ impl CallHierarchyItem { } } -fn module_detail(db: &dyn Db, file: File) -> Option { +fn module_detail(db: &dyn Db, file: ResolverFile<'_>) -> Option { ty_module_resolver::file_to_module(db, file).map(|module| module.name(db).to_string()) } @@ -196,7 +198,11 @@ mod tests { impl CursorTest { pub(super) fn prepare_calls(&self) -> Option> { - prepare_call_hierarchy(&self.db, self.cursor.file, self.cursor.offset) + prepare_call_hierarchy( + &self.db, + self.program_file(self.cursor.file), + self.cursor.offset, + ) } fn prepare_call_hierarchy(&self) -> String { @@ -246,7 +252,6 @@ mod tests { | 2 | def foo(): | ^^^ - | "); } @@ -264,7 +269,6 @@ mod tests { | 2 | class MyClass: | ^^^^^^^ - | "); } @@ -283,7 +287,6 @@ mod tests { | 3 | def method(self): | ^^^^^^ - | "); } @@ -303,7 +306,6 @@ mod tests { | 2 | def foo(): | ^^^ - | "); } @@ -340,21 +342,18 @@ mod tests { | 5 | def foo(x: int) -> int: ... | ^^^ - | info[prepare-call-hierarchy]: Function: `foo` (`main`) --> main.py:7:5 | 7 | def foo(x: str) -> str: ... | ^^^ - | info[prepare-call-hierarchy]: Function: `foo` (`main`) --> main.py:8:5 | 8 | def foo(x): | ^^^ - | "); } @@ -374,7 +373,6 @@ mod tests { | 2 | async def foo(): | ^^^ - | "); } @@ -394,7 +392,6 @@ mod tests { | 4 | def method(): | ^^^^^^ - | "); } @@ -414,7 +411,6 @@ mod tests { | 4 | def method(cls): | ^^^^^^ - | "); } } diff --git a/crates/ty_ide/src/call_hierarchy/incoming_calls.rs b/crates/ty_ide/src/call_hierarchy/incoming_calls.rs index 8d2c2e04c3..e674db8b42 100644 --- a/crates/ty_ide/src/call_hierarchy/incoming_calls.rs +++ b/crates/ty_ide/src/call_hierarchy/incoming_calls.rs @@ -1,6 +1,6 @@ use crate::call_hierarchy::{CalleeLeaf, module_detail}; use crate::goto::{Definitions, GotoTarget, find_goto_target}; -use crate::references::{contains_identifier, has_any_external_visible_definitions}; +use crate::references::has_any_external_visible_definitions; use crate::{CallHierarchyItem, Db, SymbolKind}; use rayon::prelude::*; use ruff_db::files::File; @@ -12,11 +12,15 @@ use ruff_python_ast::visitor::source_order::{SourceOrderVisitor, TraversalSignal use ruff_python_ast::{self as ast, AnyNodeRef}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use rustc_hash::FxHashMap; +use ty_module_resolver::ResolverFile; use ty_project::parallel::{ParallelIteratorExt, minimum_parallel_job_len}; +use ty_python_core::ProgramFile; use ty_python_core::scope::{NodeWithScopeKind, ScopeKind}; use ty_python_semantic::types::ide_support::static_member_type_for_attribute; use ty_python_semantic::types::{PropertyAccessorRole, Type}; -use ty_python_semantic::{HasDefinition as _, HasType as _, ImportAliasResolution, SemanticModel}; +use ty_python_semantic::{ + HasDefinition as _, HasType as _, ImportAliasResolution, SemanticModel, contains_identifier, +}; /// Salsa snapshots coordinate clone and drop through shared state. For ordinary targets, most /// files are rejected by the text prefilter, so process enough files per job to amortize that @@ -26,8 +30,9 @@ const MAX_MIN_FILES_PER_PARALLEL_JOB: usize = 16; /// Find every place in the project that calls the symbol at `offset`, grouped /// by enclosing function/method/class/module. -pub fn incoming_calls(db: &dyn Db, file: File, offset: TextSize) -> Vec { - let module = parsed_module(db, file).load(db); +pub fn incoming_calls(db: &dyn Db, file: ProgramFile<'_>, offset: TextSize) -> Vec { + let module = parsed_module(db, file.python_file(db)).load(db); + let source_file = file.file(db); let model = SemanticModel::new(db, file); let Some(goto_target) = find_goto_target(&model, &module, offset) else { return Vec::new(); @@ -71,11 +76,12 @@ pub fn incoming_calls(db: &dyn Db, file: File, offset: TextSize) -> Vec = files .iter() .copied() - .filter(|other| *other != file) + .filter(|other| *other != source_file) .collect(); let minimum_job_len = minimum_parallel_job_len(files.len(), MAX_MIN_FILES_PER_PARALLEL_JOB); // The byte-level text prefilter still pays off as a coarse gate: @@ -96,7 +102,13 @@ pub fn incoming_calls(db: &dyn Db, file: File, offset: TextSize) -> Vec>(); @@ -159,12 +171,12 @@ struct EnclosingKey { /// `target_definitions`. fn call_sites_for_file( db: &dyn Db, - file: File, + file: ProgramFile<'_>, target_definitions: &Definitions<'_>, target_role: Option, needle: Option<&str>, ) -> Vec { - let parsed = parsed_module(db, file); + let parsed = parsed_module(db, file.python_file(db)); let module = parsed.load(db); let model = SemanticModel::new(db, file); let mut sites = Vec::new(); @@ -301,6 +313,7 @@ impl<'a> CallSitesFinder<'a, '_> { /// accessor: a read calls the getter, a write calls the setter, and a /// `del` calls the deleter. fn check_property_access(&mut self, attribute: &'a ast::ExprAttribute) { + let db = self.db; let Some(Type::PropertyInstance(property)) = static_member_type_for_attribute(self.model, attribute) else { @@ -333,7 +346,7 @@ impl<'a> CallSitesFinder<'a, '_> { let intersects = current_definitions.iter().any(|resolved| { let role = resolved .definition() - .and_then(|def| property.accessor_role(self.db, def)); + .and_then(|def| property.accessor_role(db, def)); let matches_site_kind = match attribute.ctx { ast::ExprContext::Load => { matches!(role, Some(PropertyAccessorRole::Getter) | None) @@ -378,7 +391,9 @@ impl<'a> CallSitesFinder<'a, '_> { /// method's AST node. Comprehension and annotation scopes have no callable /// hierarchy item of their own, so walk outward until reaching one that does. fn enclosing_scope_item(&self, scope_node: AnyNodeRef<'_>) -> CallHierarchyItem { - let file = self.model.file(); + let program_file = self.model.program_file(); + let resolver_file = program_file.resolver_file(self.db); + let file = program_file.file(self.db); let mut ancestors = self.model.ancestor_scopes(scope_node); let Some((_, enclosing)) = ancestors.find(|(_, ancestor)| { matches!( @@ -386,11 +401,11 @@ impl<'a> CallSitesFinder<'a, '_> { ScopeKind::Module | ScopeKind::Function | ScopeKind::Class | ScopeKind::Lambda ) }) else { - return module_item(self.db, file); + return module_item(self.db, resolver_file); }; match enclosing.node() { - NodeWithScopeKind::Module => module_item(self.db, file), + NodeWithScopeKind::Module => module_item(self.db, resolver_file), NodeWithScopeKind::Function(func) => { let func = func.node(self.module); let is_method = ancestors @@ -409,7 +424,7 @@ impl<'a> CallSitesFinder<'a, '_> { } else { SymbolKind::Function }, - detail: module_detail(self.db, file), + detail: module_detail(self.db, resolver_file), file, full_range: func.range(), selection_range: func.name.range(), @@ -420,7 +435,7 @@ impl<'a> CallSitesFinder<'a, '_> { CallHierarchyItem { name: class.name.id.clone(), kind: SymbolKind::Class, - detail: module_detail(self.db, file), + detail: module_detail(self.db, resolver_file), file, full_range: class.range(), selection_range: class.name.range(), @@ -437,13 +452,13 @@ impl<'a> CallSitesFinder<'a, '_> { CallHierarchyItem { name: Name::new_static("(lambda)"), kind: SymbolKind::Function, - detail: module_detail(self.db, file), + detail: module_detail(self.db, resolver_file), file, full_range: lambda.range(), selection_range: TextRange::new(lambda.start(), end), } } - _ => module_item(self.db, file), + _ => module_item(self.db, resolver_file), } } } @@ -454,7 +469,7 @@ struct RawCallSite { } /// Build an item for the module-level enclosing scope (no enclosing function). -fn module_item(db: &dyn Db, file: File) -> CallHierarchyItem { +fn module_item(db: &dyn Db, file: ResolverFile<'_>) -> CallHierarchyItem { let name = ty_module_resolver::file_to_module(db, file) .map(|module| Name::new(module.name(db).last_component())) .unwrap_or_else(|| Name::new_static("")); @@ -462,7 +477,7 @@ fn module_item(db: &dyn Db, file: File) -> CallHierarchyItem { name, kind: SymbolKind::Module, detail: None, - file, + file: file.file(db), full_range: TextRange::default(), selection_range: TextRange::default(), } @@ -512,7 +527,11 @@ mod tests { else { return "No incoming calls found".to_string(); }; - let calls = incoming_calls(&self.db, target.file, target.selection_range.start()); + let calls = incoming_calls( + &self.db, + self.program_file(target.file), + target.selection_range.start(), + ); if calls.is_empty() { return "No incoming calls found".to_string(); } @@ -577,13 +596,11 @@ mod tests { | 6 | foo() | ^^^ Call site - | info: Function: `caller` (`main`) --> main.py:5:5 | 5 | def caller(): | ^^^^^^ - | "); } @@ -605,13 +622,11 @@ mod tests { | 7 | foo() # this is a call — should appear once | ^^^ Call site - | info: Function: `caller` (`main`) --> main.py:5:5 | 5 | def caller(): | ^^^^^^ - | "); } @@ -641,13 +656,11 @@ def use(): | 5 | foo() | ^^^ Call site - | info: Function: `use` (`caller`) --> caller.py:4:5 | 4 | def use(): | ^^^ - | "); } @@ -677,13 +690,11 @@ def use(): | 5 | bar() | ^^^ Call site - | info: Function: `use` (`caller`) --> caller.py:4:5 | 4 | def use(): | ^^^ - | "); } @@ -714,13 +725,11 @@ def invoke(value: Callable) -> int: | 5 | return value() | ^^^^^ Call site - | info: Function: `invoke` (`caller`) --> caller.py:4:5 | 4 | def invoke(value: Callable) -> int: | ^^^^^^ - | "); } @@ -741,13 +750,11 @@ def invoke(value: Callable) -> int: | 6 | foo(x=1) | ^^^ Call site - | info: Function: `caller` (`main`) --> main.py:5:5 | 5 | def caller(): | ^^^^^^ - | "); } @@ -767,7 +774,6 @@ def invoke(value: Callable) -> int: | 5 | foo() | ^^^ Call site - | info: Module: `main` --> main.py:1:1 "); @@ -792,7 +798,6 @@ def invoke(value: Callable) -> int: | 5 | @foo | ^^^ Call site - | info: Module: `main` --> main.py:1:1 "); @@ -823,13 +828,11 @@ class C: | 9 | def method(self, value=default()): | ^^^^^^^ Call site - | info: Class: `C` (`main`) --> main.py:7:7 | 7 | class C: | ^ - | "); } @@ -857,13 +860,11 @@ class C: | 11 | a.foo() | ^^^ Call site - | info: Function: `use` (`main`) --> main.py:10:5 | 10 | def use(a: A, b: B): | ^^^ - | "); } @@ -888,13 +889,11 @@ class C: | 8 | super().m() | ^ Call site - | info: Method: `m` (`main`) --> main.py:7:9 | 7 | def m(self): | ^ - | "); } @@ -925,13 +924,11 @@ def make() -> C: | 5 | return C() | ^ Call site - | info: Function: `make` (`caller`) --> caller.py:4:5 | 4 | def make() -> C: | ^^^^ - | "); } @@ -960,13 +957,11 @@ def make() -> C: | 8 | return c.prop | ^^^^ Call site - | info: Function: `read` (`main`) --> main.py:7:5 | 7 | def read(c: C) -> int: | ^^^^ - | "); } @@ -998,13 +993,11 @@ def make() -> C: | 12 | c.prop = 5 | ^^^^ Call site - | info: Function: `write` (`main`) --> main.py:11:5 | 11 | def write(c: C) -> None: | ^^^^^ - | "); } @@ -1034,13 +1027,11 @@ def make() -> C: | 12 | del c.prop | ^^^^ Call site - | info: Function: `remove` (`main`) --> main.py:11:5 | 11 | def remove(c: C) -> None: | ^^^^^^ - | "); } @@ -1097,13 +1088,11 @@ def make() -> C: | 7 | return c.method() | ^^^^^^ Call site - | info: Function: `use` (`main`) --> main.py:6:5 | 6 | def use(c: C) -> int: | ^^^ - | "); } @@ -1146,13 +1135,11 @@ def make() -> C: | 5 | f = lambda x: target(x) | ^^^^^^ Call site - | info: Function: `(lambda)` (`main`) --> main.py:5:5 | 5 | f = lambda x: target(x) | ^^^^^^^^ - | "); let Some(target) = test .prepare_calls() @@ -1160,7 +1147,11 @@ def make() -> C: else { panic!("expected a call hierarchy target"); }; - let incoming = incoming_calls(&test.db, target.file, target.selection_range.start()); + let incoming = incoming_calls( + &test.db, + test.program_file(target.file), + target.selection_range.start(), + ); // The selection identifies the anonymous callable header. let sel = incoming[0].from.selection_range; let source = test.cursor.source.as_str(); @@ -1189,26 +1180,22 @@ def make() -> C: | 5 | a = lambda x: target(x) | ^^^^^^ Call site - | info: Function: `(lambda)` (`main`) --> main.py:5:5 | 5 | a = lambda x: target(x) | ^^^^^^^^ - | info[incoming-calls]: Incoming calls to `target` --> main.py:6:13 | 6 | b = lambda: target(0) | ^^^^^^ Call site - | info: Function: `(lambda)` (`main`) --> main.py:6:5 | 6 | b = lambda: target(0) | ^^^^^^ - | "); } @@ -1233,13 +1220,11 @@ def make() -> C: | 6 | f = lambda x: target(x) | ^^^^^^ Call site - | info: Function: `(lambda)` (`main`) --> main.py:6:9 | 6 | f = lambda x: target(x) | ^^^^^^^^ - | "); } @@ -1263,13 +1248,11 @@ def make() -> C: | 6 | return [target(x) for x in xs] | ^^^^^^ Call site - | info: Function: `caller` (`main`) --> main.py:5:5 | 5 | def caller(xs): | ^^^^^^ - | "); } @@ -1297,14 +1280,18 @@ def make() -> C: else { panic!("expected a call hierarchy target"); }; - let incoming = incoming_calls(&test.db, target.file, target.selection_range.start()); + let incoming = incoming_calls( + &test.db, + test.program_file(target.file), + target.selection_range.start(), + ); assert_eq!(incoming.len(), 1, "got {incoming:?}"); let lambda_item = &incoming[0].from; assert_eq!(lambda_item.name.as_str(), "(lambda)"); let follow_up_incoming = incoming_calls( &test.db, - lambda_item.file, + test.program_file(lambda_item.file), lambda_item.selection_range.start(), ); assert!( @@ -1314,7 +1301,7 @@ def make() -> C: let follow_up_outgoing = outgoing_calls( &test.db, - lambda_item.file, + test.program_file(lambda_item.file), lambda_item.selection_range.start(), ); assert!( diff --git a/crates/ty_ide/src/call_hierarchy/outgoing_calls.rs b/crates/ty_ide/src/call_hierarchy/outgoing_calls.rs index b653779f44..cd80dfa3f5 100644 --- a/crates/ty_ide/src/call_hierarchy/outgoing_calls.rs +++ b/crates/ty_ide/src/call_hierarchy/outgoing_calls.rs @@ -15,6 +15,7 @@ use ruff_python_ast::{ }; use ruff_text_size::{Ranged, TextRange, TextSize}; use rustc_hash::FxHashMap; +use ty_python_core::ProgramFile; use ty_python_core::definition::DefinitionKind; use ty_python_semantic::{ImportAliasResolution, SemanticModel}; @@ -29,8 +30,8 @@ use ty_python_semantic::{ImportAliasResolution, SemanticModel}; /// are reported when the nested callable is expanded separately. Declaration /// expressions attached to a nested callable are still included while /// traversing the containing item's body. -pub fn outgoing_calls(db: &dyn Db, file: File, offset: TextSize) -> Vec { - let module = parsed_module(db, file).load(db); +pub fn outgoing_calls(db: &dyn Db, file: ProgramFile<'_>, offset: TextSize) -> Vec { + let module = parsed_module(db, file.python_file(db)).load(db); let model = SemanticModel::new(db, file); let Some(goto_target) = find_goto_target(&model, &module, offset) else { return Vec::new(); @@ -51,10 +52,9 @@ pub fn outgoing_calls(db: &dyn Db, file: File, offset: TextSize) -> Vec OutgoingCallsFinder<'a, '_> { _ => continue, } let def_file = def.file(self.db); - let module_ref = parsed_module(self.db, def_file).load(self.db); + let module_ref = parsed_module(self.db, def.python_file(self.db)).load(self.db); let selection_range = def.focus_range(self.db, &module_ref).range(); let key = CalleeKey { @@ -304,7 +304,11 @@ mod tests { else { return "No outgoing calls found".to_string(); }; - let calls = outgoing_calls(&self.db, target.file, target.selection_range.start()); + let calls = outgoing_calls( + &self.db, + self.program_file(target.file), + target.selection_range.start(), + ); if calls.is_empty() { return "No outgoing calls found".to_string(); } @@ -368,13 +372,11 @@ mod tests { | 6 | helper() | ^^^^^^ Call site - | info: Function: `helper` (`main`) --> main.py:2:5 | 2 | def helper(): | ^^^^^^ - | "); } @@ -396,13 +398,11 @@ mod tests { | 7 | c.m() | ^ Call site - | info: Method: `m` (`main`) --> main.py:3:9 | 3 | def m(self): | ^ - | "); } @@ -423,13 +423,11 @@ mod tests { | 6 | C() | ^ Call site - | info: Class: `C` (`main`) --> main.py:2:7 | 2 | class C: | ^ - | "); } @@ -453,13 +451,11 @@ mod tests { | ^^^^^^ Call site 7 | helper() | ^^^^^^ Call site - | info: Function: `helper` (`main`) --> main.py:2:5 | 2 | def helper(): | ^^^^^^ - | "); } @@ -521,65 +517,55 @@ mod tests { | 17 | @cls_deco | ^^^^^^^^ Call site - | info: Function: `cls_deco` (`main`) --> main.py:2:5 | 2 | def cls_deco(cls): | ^^^^^^^^ - | info[outgoing-calls]: Outgoing calls from `Cls` --> main.py:18:11 | 18 | class Cls(base_factory()): | ^^^^^^^^^^^^ Call site - | info: Function: `base_factory` (`main`) --> main.py:5:5 | 5 | def base_factory(): | ^^^^^^^^^^^^ - | info[outgoing-calls]: Outgoing calls from `Cls` --> main.py:19:12 | 19 | attr = class_body_helper() | ^^^^^^^^^^^^^^^^^ Call site - | info: Function: `class_body_helper` (`main`) --> main.py:8:5 | 8 | def class_body_helper(): | ^^^^^^^^^^^^^^^^^ - | info[outgoing-calls]: Outgoing calls from `Cls` --> main.py:21:6 | 21 | @method_deco | ^^^^^^^^^^^ Call site - | info: Function: `method_deco` (`main`) --> main.py:11:5 | 11 | def method_deco(fn): | ^^^^^^^^^^^ - | info[outgoing-calls]: Outgoing calls from `Cls` --> main.py:22:19 | 22 | def m(self, x=default_factory()): | ^^^^^^^^^^^^^^^ Call site - | info: Function: `default_factory` (`main`) --> main.py:14:5 | 14 | def default_factory(): | ^^^^^^^^^^^^^^^ - | "); } @@ -605,13 +591,11 @@ mod tests { | 8 | nested() | ^^^^^^ Call site - | info: Function: `nested` (`main`) --> main.py:6:9 | 6 | def nested(): | ^^^^^^ - | "); } @@ -634,13 +618,11 @@ mod tests { | 5 | def foo(x=default_factory()): | ^^^^^^^^^^^^^^^ Call site - | info: Function: `default_factory` (`main`) --> main.py:2:5 | 2 | def default_factory(): | ^^^^^^^^^^^^^^^ - | "); } @@ -663,13 +645,11 @@ mod tests { | 5 | class Derived(base_factory()): | ^^^^^^^^^^^^ Call site - | info: Function: `base_factory` (`main`) --> main.py:2:5 | 2 | def base_factory(): | ^^^^^^^^^^^^ - | "); } @@ -698,13 +678,11 @@ mod tests { | 9 | f = lambda x=default_factory(): lambda_body_helper() | ^^^^^^^^^^^^^^^ Call site - | info: Function: `default_factory` (`main`) --> main.py:2:5 | 2 | def default_factory(): | ^^^^^^^^^^^^^^^ - | "); } @@ -723,26 +701,22 @@ mod tests { | LL | print("hi") # builtins resolve via stubs, so this *does* appear | ^^^^^ Call site - | info: Function: `print` (`builtins`) --> stdlib/builtins.byi:LL:5 | LL | def print( | ^^^^^ - | info[outgoing-calls]: Outgoing calls from `foo` --> main.py:LL:5 | LL | print("hi") # builtins resolve via stubs, so this *does* appear | ^^^^^ Call site - | info: Function: `print` (`builtins`) --> stdlib/builtins.byi:LL:5 | LL | def print( | ^^^^^ - | "#); } @@ -767,26 +741,22 @@ mod tests { | 8 | super().m() | ^ Call site - | info: Method: `m` (`main`) --> main.py:3:9 | 3 | def m(self): | ^ - | info[outgoing-calls]: Outgoing calls from `m` --> main.py:LL:9 | LL | super().m() | ^^^^^ Call site - | info: Class: `super` (`builtins`) --> stdlib/builtins.byi:LL:7 | LL | class super: | ^^^^^ - | "); } @@ -817,13 +787,11 @@ def foo(): | 5 | helper() | ^^^^^^ Call site - | info: Function: `helper` (`lib`) --> lib.py:2:5 | 2 | def helper(): | ^^^^^^ - | "); } } diff --git a/crates/ty_ide/src/code_action.rs b/crates/ty_ide/src/code_action.rs index 9bcd6d84ef..184eddf831 100644 --- a/crates/ty_ide/src/code_action.rs +++ b/crates/ty_ide/src/code_action.rs @@ -8,6 +8,7 @@ use ruff_diagnostics::Edit; use ruff_python_ast::find_node::covering_node; use ruff_text_size::TextRange; use ty_project::Db; +use ty_python_core::ProgramFile; use ty_python_semantic::lint::LintId; use ty_python_semantic::suppress_single; use ty_python_semantic::types::{ @@ -57,7 +58,7 @@ impl QuickFix { /// parsing it as python. pub fn code_actions( db: &dyn Db, - file: File, + file: ProgramFile<'_>, diagnostic_range: TextRange, diagnostic_id: &str, template: bool, @@ -68,7 +69,7 @@ pub fn code_actions( }; if template { - return django_template_code_actions(db, file, diagnostic_range, lint_id); + return django_template_code_actions(db, file.file(db), diagnostic_range, lint_id); } let mut actions = Vec::new(); @@ -88,14 +89,18 @@ pub fn code_actions( || lint_id == LintId::of(&MISPLACED_DEPENDENCY) || lint_id == LintId::of(&UNRESOLVED_IMPORT) { - actions.extend(add_dependency::code_actions(db, file, diagnostic_range)); + actions.extend(add_dependency::code_actions( + db, + file.file(db), + diagnostic_range, + )); } // Suggest just suppressing the lint (always a valid option, but never ideal) actions.push(QuickFix::new( format!("Ignore '{}' for this line", lint_id.name()), - file, - suppress_single(db, file, lint_id, diagnostic_range).into_edits(), + file.file(db), + suppress_single(db, file.python_file(db), lint_id, diagnostic_range).into_edits(), false, )); @@ -104,17 +109,16 @@ pub fn code_actions( fn unresolved_fixes( db: &dyn Db, - file: File, + file: ProgramFile<'_>, diagnostic_range: TextRange, ) -> Option> { - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, file.python_file(db)).load(db); let node = covering_node(parsed.syntax().into(), diagnostic_range).node(); let symbol = &node.expr_name()?.id; - Some( completion::unresolved_fixes(db, file, &parsed, symbol, node) .into_iter() - .map(move |import| QuickFix::new(import.label, file, vec![import.edit], true)), + .map(move |import| QuickFix::new(import.label, file.file(db), vec![import.edit], true)), ) } @@ -136,6 +140,7 @@ mod tests { use ruff_python_trivia::textwrap::dedent; use ruff_text_size::{TextRange, TextSize}; use ty_project::ProjectMetadata; + use ty_python_core::ProgramFile; use ty_python_semantic::{ default_lint_registry, lint::LintMetadata, @@ -153,9 +158,8 @@ mod tests { 1 | b = a / 10 | ^ | - | - b = a / 10 - 1 + b = a / 10 # ty:ignore[unresolved-reference] + 1 + b = a / 10 # ty: ignore[unresolved-reference] | "); } @@ -171,9 +175,8 @@ mod tests { 1 | b = a / 10 # fmt: off | ^ | - | - b = a / 10 # fmt: off - 1 + b = a / 10 # fmt: off # ty:ignore[unresolved-reference] + 1 + b = a / 10 # fmt: off # ty: ignore[unresolved-reference] | "); } @@ -201,7 +204,6 @@ mod tests { 2 | b = a / 0 # ty:ignore[division-by-zero] | ^ | - | 1 | - b = a / 0 # ty:ignore[division-by-zero] 2 + b = a / 0 # ty:ignore[division-by-zero, unresolved-reference] @@ -224,7 +226,6 @@ mod tests { 2 | b = a / 10 # ty:ignore[] | ^ | - | 1 | - b = a / 10 # ty:ignore[] 2 + b = a / 10 # ty:ignore[unresolved-reference] @@ -249,7 +250,6 @@ mod tests { 4 | b = a / 10 | ^ | - | 2 | seen_code = True - # ty:ignore[] 3 + # ty:ignore[unresolved-reference] @@ -280,7 +280,6 @@ mod tests { 3 | # ty:ignore[] # ty:ignore[not-a-rule] # ty:ignore[division-by-zero] | ^^^^^^^^^^ | - | 2 | seen_code = True - # ty:ignore[] # ty:ignore[not-a-rule] # ty:ignore[division-by-zero] 3 + # ty:ignore[ignore-comment-unknown-rule] # ty:ignore[not-a-rule] # ty:ignore[division-by-zero] @@ -310,7 +309,6 @@ mod tests { 7 | absent, | ^^^^^^ | - | 2 | seen_code = True - # ty:ignore[] 3 + # ty:ignore[unresolved-reference] @@ -342,7 +340,6 @@ mod tests { 9 | absent, | ^^^^^^ | - | 4 | seen_code = True - # ty:ignore[invalid-assignment] 5 + # ty:ignore[invalid-assignment, unresolved-reference] @@ -366,7 +363,6 @@ mod tests { 2 | b = a / 0 # type:ignore[ty:division-by-zero] | ^ | - | 1 | - b = a / 0 # type:ignore[ty:division-by-zero] 2 + b = a / 0 # type:ignore[ty:division-by-zero, ty:unresolved-reference] @@ -389,10 +385,9 @@ mod tests { 2 | b = a / 0 # type:ignore[mypy-code] | ^ | - | 1 | - b = a / 0 # type:ignore[mypy-code] - 2 + b = a / 0 # type:ignore[mypy-code] # ty:ignore[unresolved-reference] + 2 + b = a / 0 # type:ignore[mypy-code] # ty: ignore[unresolved-reference] | "); } @@ -414,10 +409,9 @@ mod tests { 4 | b = a / 0 | ^ | - | 3 | - b = a / 0 - 4 + b = a / 0 # ty:ignore[unresolved-reference] + 4 + b = a / 0 # ty: ignore[unresolved-reference] | "); } @@ -437,7 +431,6 @@ mod tests { 2 | b = a / 0 # ty:ignore[division-by-zero,] | ^ | - | 1 | - b = a / 0 # ty:ignore[division-by-zero,] 2 + b = a / 0 # ty:ignore[division-by-zero, unresolved-reference] @@ -460,7 +453,6 @@ mod tests { 2 | b = a / 0 # ty:ignore[division-by-zero ] | ^ | - | 1 | - b = a / 0 # ty:ignore[division-by-zero ] 2 + b = a / 0 # ty:ignore[division-by-zero, unresolved-reference ] @@ -483,10 +475,9 @@ mod tests { 2 | b = a / 0 # ty:ignore[division-by-zero] some explanation | ^ | - | 1 | - b = a / 0 # ty:ignore[division-by-zero] some explanation - 2 + b = a / 0 # ty:ignore[division-by-zero] some explanation # ty:ignore[unresolved-reference] + 2 + b = a / 0 # ty:ignore[division-by-zero] some explanation # ty: ignore[unresolved-reference] | "); } @@ -512,7 +503,6 @@ mod tests { 5 | | 0 | |_________^ | - | 2 | b = ( - a # ty:ignore[division-by-zero] 3 + a # ty:ignore[division-by-zero, unresolved-reference] @@ -542,7 +532,6 @@ mod tests { 5 | | 0 # ty:ignore[division-by-zero] | |_________^ | - | 4 | / - 0 # ty:ignore[division-by-zero] 5 + 0 # ty:ignore[division-by-zero, unresolved-reference] @@ -572,7 +561,6 @@ mod tests { 5 | | 0 # ty:ignore[division-by-zero] | |_________^ | - | 2 | b = ( - a # ty:ignore[division-by-zero] 3 + a # ty:ignore[division-by-zero, unresolved-reference] @@ -599,10 +587,9 @@ mod tests { 3 | {a} | ^ | - | 4 | more text - """ - 5 + """ # ty:ignore[unresolved-reference] + 5 + """ # ty: ignore[unresolved-reference] | "#); } @@ -627,10 +614,9 @@ mod tests { 4 | a | ^ | - | 3 | { - a - 4 + a # ty:ignore[unresolved-reference] + 4 + a # ty: ignore[unresolved-reference] 5 | } | "); @@ -653,10 +639,9 @@ mod tests { 2 | b = a + """ | ^ | - | 3 | more text - """ - 4 + """ # ty:ignore[unresolved-reference] + 4 + """ # ty: ignore[unresolved-reference] | "#); } @@ -677,10 +662,9 @@ mod tests { 2 | b = a \ | ^ | - | 2 | b = a \ - + "test" - 3 + + "test" # ty:ignore[unresolved-reference] + 3 + + "test" # ty: ignore[unresolved-reference] | "#); } @@ -704,10 +688,9 @@ mod tests { 4 | + ddd \ | ^^^ | - | 4 | + ddd \ - - 5 + # ty:ignore[unresolved-reference] + 5 + # ty: ignore[unresolved-reference] 6 | ] # test | "); @@ -727,23 +710,32 @@ mod tests { | 2 | reveal_type(1) | ^^^^^^^^^^^ - | help: This is a preferred code action | 1 + from typing import reveal_type 2 | | - info[code-action]: Ignore 'undefined-reveal' for this line + info[code-action]: import typing_extensions.reveal_type --> main.py:2:1 | 2 | reveal_type(1) | ^^^^^^^^^^^ + help: This is a preferred code action + | + 1 + from typing_extensions import reveal_type + 2 | + | + + info[code-action]: Ignore 'undefined-reveal' for this line + --> main.py:2:1 | + 2 | reveal_type(1) + | ^^^^^^^^^^^ | 1 | - reveal_type(1) - 2 + reveal_type(1) # ty:ignore[undefined-reveal] + 2 + reveal_type(1) # ty: ignore[undefined-reveal] | "); } @@ -763,23 +755,32 @@ mod tests { | 2 | @deprecated("do not use") | ^^^^^^^^^^ - | help: This is a preferred code action | 1 + from warnings import deprecated 2 | | - info[code-action]: Ignore 'unresolved-reference' for this line + info[code-action]: import typing_extensions.deprecated --> main.py:2:2 | 2 | @deprecated("do not use") | ^^^^^^^^^^ + help: This is a preferred code action | + 1 + from typing_extensions import deprecated + 2 | + | + + info[code-action]: Ignore 'unresolved-reference' for this line + --> main.py:2:2 + | + 2 | @deprecated("do not use") + | ^^^^^^^^^^ | 1 | - @deprecated("do not use") - 2 + @deprecated("do not use") # ty:ignore[unresolved-reference] + 2 + @deprecated("do not use") # ty: ignore[unresolved-reference] 3 | def my_func(): ... | "#); @@ -802,19 +803,28 @@ mod tests { | 4 | @deprecated("do not use") | ^^^^^^^^^^ - | help: This is a preferred code action | 1 + from warnings import deprecated 2 | | - info[code-action]: qualify warnings.deprecated + info[code-action]: import typing_extensions.deprecated --> main.py:4:2 | 4 | @deprecated("do not use") | ^^^^^^^^^^ + help: This is a preferred code action + | + 1 + from typing_extensions import deprecated + 2 | | + + info[code-action]: qualify warnings.deprecated + --> main.py:4:2 + | + 4 | @deprecated("do not use") + | ^^^^^^^^^^ help: This is a preferred code action | 3 | @@ -829,10 +839,9 @@ mod tests { 4 | @deprecated("do not use") | ^^^^^^^^^^ | - | 3 | - @deprecated("do not use") - 4 + @deprecated("do not use") # ty:ignore[unresolved-reference] + 4 + @deprecated("do not use") # ty: ignore[unresolved-reference] 5 | def my_func(): ... | "#); @@ -853,7 +862,6 @@ mod tests { | 2 | ExecutionLoader | ^^^^^^^^^^^^^^^ - | help: This is a preferred code action | 1 + from importlib.abc import ExecutionLoader @@ -866,10 +874,9 @@ mod tests { 2 | ExecutionLoader | ^^^^^^^^^^^^^^^ | - | 1 | - ExecutionLoader - 2 + ExecutionLoader # ty:ignore[unresolved-reference] + 2 + ExecutionLoader # ty: ignore[unresolved-reference] | "); } @@ -893,7 +900,6 @@ mod tests { | 3 | ExecutionLoader | ^^^^^^^^^^^^^^^ - | help: This is a preferred code action | 1 + from importlib.abc import ExecutionLoader @@ -906,10 +912,9 @@ mod tests { 3 | ExecutionLoader | ^^^^^^^^^^^^^^^ | - | 2 | import importlib - ExecutionLoader - 3 + ExecutionLoader # ty:ignore[unresolved-reference] + 3 + ExecutionLoader # ty: ignore[unresolved-reference] | "); } @@ -930,7 +935,6 @@ mod tests { | 3 | ExecutionLoader | ^^^^^^^^^^^^^^^ - | help: This is a preferred code action | 1 + from importlib.abc import ExecutionLoader @@ -942,7 +946,6 @@ mod tests { | 3 | ExecutionLoader | ^^^^^^^^^^^^^^^ - | help: This is a preferred code action | 2 | import importlib.abc @@ -956,27 +959,24 @@ mod tests { 3 | ExecutionLoader | ^^^^^^^^^^^^^^^ | - | 2 | import importlib.abc - ExecutionLoader - 3 + ExecutionLoader # ty:ignore[unresolved-reference] + 3 + ExecutionLoader # ty: ignore[unresolved-reference] | "); } - pub(super) struct CodeActionTest { - pub(super) db: ty_project::TestDb, - pub(super) file: File, - pub(super) diagnostic_range: TextRange, + struct CodeActionTest { + db: ty_project::TestDb, + file: File, + diagnostic_range: TextRange, } impl CodeActionTest { - pub(super) fn with_source(source: &str) -> Self { + fn with_source(source: &str) -> Self { let mut db = ty_project::TestDb::new(ProjectMetadata::new("test", SystemPathBuf::from("/"))); - db.init_program().unwrap(); - let mut cleansed = dedent(source).to_string(); let start = cleansed @@ -1007,14 +1007,13 @@ mod tests { } } - pub(super) fn code_actions(&self, lint: &LintMetadata) -> String { + fn code_actions(&self, lint: &LintMetadata) -> String { use std::fmt::Write; let mut buf = String::new(); let config = DisplayDiagnosticConfig::new("ty") .color(false) - .show_fix_diff(true) .context(0) .format(DiagnosticFormat::Full); @@ -1022,7 +1021,11 @@ mod tests { // have their own tests for action in code_actions( &self.db, - self.file, + ProgramFile::new( + &self.db, + self.file, + self.db.program_environment().program(&self.db), + ), self.diagnostic_range, &lint.name, false, diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index 164d318dd2..39216d815c 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -1,8 +1,9 @@ +use std::cell::OnceCell; use std::cmp::Ordering; use std::collections::{BinaryHeap, binary_heap}; +use ty_python_semantic::ProgramEnvironment; use compact_str::{CompactString, CompactStringExt}; -use ruff_db::files::File; use ruff_db::parsed::{ParsedModuleRef, parsed_module}; use ruff_db::source::{SourceText, source_text}; use ruff_diagnostics::Edit; @@ -18,7 +19,10 @@ use ruff_python_literal::mini_language::FormatSpecComponent; use ruff_python_literal::strftime; use ruff_text_size::{Ranged, TextRange, TextSize}; use rustc_hash::FxHashSet; -use ty_module_resolver::{KnownModule, Module, ModuleName}; +use ty_module_resolver::{ + ImportingFile, KnownModule, Module, ModuleName, resolve_real_shadowable_module, +}; +use ty_python_core::{ProgramFile, semantic_index}; use ty_python_semantic::HasType; use ty_python_semantic::dependencies::{self, ImportStanding}; use ty_python_semantic::types::format::{SpecLanguage, spec_language}; @@ -37,26 +41,32 @@ use crate::goto::Definitions; use crate::importer::{ImportRequest, Importer}; use crate::symbols::QueryPattern; use crate::{Db, all_symbols, signature_help}; +use ruff_db::files::File; pub fn completion<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, settings: &CompletionSettings, capabilities: CompletionCapabilities, - file: File, + file: ProgramFile<'db>, offset: TextSize, ) -> Vec> { - let parsed = parsed_module(db, file).load(db); + let program_file = file; + let parsed = parsed_module(db, file.python_file(db)).load(db); + let file = file.file(db); let source = source_text(db, file); let source_type = file.source_type(db); - let Some(context) = Context::new(db, file, &parsed, &source, offset) else { + let Some(context) = Context::new(db, program_file, &parsed, &source, offset) else { return vec![]; }; - let model = SemanticModel::new(db, file); + let model = SemanticModel::new(db, program_file); // a format spec is inside an f-string's literal text, so this has to come // before the string branch below, which has nothing to offer there - if let Some(completions) = format_spec_completions(db, &model, &source, &context.cursor) { + if let Some(completions) = + format_spec_completions(db, env, program_file, &model, &source, &context.cursor) + { return completions.into_completions(); } @@ -65,8 +75,12 @@ pub fn completion<'db>( return vec![]; }; - let mut completions = - Completions::new(db, CollectionContext::none(), UserQuery::fuzzy(None)); + let mut completions = Completions::new( + db, + program_file, + CollectionContext::none(), + UserQuery::fuzzy(None), + ); add_django_name_completions(db, &context.cursor, &mut completions); add_string_literal_completions( @@ -82,6 +96,7 @@ pub fn completion<'db>( let query = UserQuery::fuzzy(context.cursor.typed); let mut completions = Completions::new( db, + program_file, context.collection_context(db, &model, settings, capabilities, source_type), query, ); @@ -98,7 +113,7 @@ pub fn completion<'db>( } } ContextKind::Import(ref import) => { - import.add_completions(db, file, &mut completions); + import.add_completions(db, program_file, &mut completions); } ContextKind::NonImport(ref non_import) => match non_import.target { CompletionTargetAst::ObjectDot { expr } => { @@ -116,6 +131,7 @@ pub fn completion<'db>( ); } CompletionTargetAst::Scoped(scoped) => { + let env = model.program_environment(); for semantic_completion in model.scoped_completions(scoped.node) { let module_dependency_kind = if semantic_completion.builtin { ModuleDependencyKind::Builtin @@ -123,11 +139,11 @@ pub fn completion<'db>( ModuleDependencyKind::Current }; completions.add( - CompletionBuilder::from_semantic_completion(db, semantic_completion) + CompletionBuilder::from_semantic_completion(db, &env, semantic_completion) .module_dependency_kind(module_dependency_kind), ); } - add_keyword_completions(db, &mut completions); + add_keyword_completions(db, &env, &mut completions); add_type_keyword_completions( &context.cursor, &model, @@ -156,11 +172,17 @@ pub fn completion<'db>( capabilities, &mut completions, ); - add_argument_completions(db, &model, &context.cursor, &mut completions); + add_argument_completions( + db, + program_file, + &model, + &context.cursor, + &mut completions, + ); if settings.auto_import { add_unimported_completions( db, - file, + program_file, &parsed, scoped, |module_name: &ModuleName, symbol: &str| { @@ -192,6 +214,7 @@ impl CompletionCapabilities { /// A collection of completions built up from various sources. struct Completions<'db> { db: &'db dyn Db, + program_file: ProgramFile<'db>, context: CollectionContext<'db>, items: BinaryHeap>, /// The query used to match against candidate completions. @@ -215,9 +238,15 @@ impl<'db> Completions<'db> { /// the user has typed as part of the next symbol they are writing. /// This collection will treat it as a query when present, and only /// add completions that match it. - fn new(db: &'db dyn Db, context: CollectionContext<'db>, query: UserQuery) -> Completions<'db> { + fn new( + db: &'db dyn Db, + program_file: ProgramFile<'db>, + context: CollectionContext<'db>, + query: UserQuery, + ) -> Completions<'db> { Completions { db, + program_file, context, items: BinaryHeap::new(), query, @@ -299,9 +328,14 @@ impl<'db> Completions<'db> { /// Attempts to add the given semantic completion to this collection. /// /// When added, `true` is returned. - fn add_semantic(&mut self, completion: SemanticCompletion<'db>) -> bool { + fn add_semantic( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + completion: SemanticCompletion<'db>, + ) -> bool { self.add(CompletionBuilder::from_semantic_completion( - self.db, completion, + db, env, completion, )) } @@ -319,7 +353,8 @@ impl<'db> Completions<'db> { if self.context.exclude(self.db, &builder) { return false; } - let completion = CompletionRanker(builder.build(self.db, &self.context, &self.query)); + let completion = + CompletionRanker(builder.build(self.db, self.program_file, &self.context, &self.query)); if self.items.len() >= Completions::LIMIT { // OK because `self.items` is guaranteed to be non-empty here. let worst = self.items.peek_mut().unwrap(); @@ -338,8 +373,10 @@ impl<'db> Extend> for Completions<'db> { where T: IntoIterator>, { + let db = self.db; + let env = ProgramEnvironment::from_file(self.program_file); for c in it { - self.add_semantic(c); + self.add_semantic(db, &env, c); } } } @@ -387,6 +424,9 @@ pub struct Completion<'db> { /// completion that replaces more than the word under the cursor has to say /// what that whole span will read as. pub filter: Option, + /// An editor action the client should perform after applying this + /// completion, if any. See [`CompletionCommand`]. + pub command: Option, /// The type of this completion, if available. /// /// Generally speaking, this is always available @@ -515,13 +555,15 @@ impl<'db> CompletionBuilder<'db> { fn from_semantic_completion( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, semantic: SemanticCompletion<'db>, ) -> CompletionBuilder<'db> { - let definition = semantic.ty.and_then(|ty| Definitions::from_ty(db, ty)); + let definition = semantic.ty.and_then(|ty| Definitions::from_ty(db, env, ty)); let documentation = definition.and_then(|def| def.docstring(db)); Completion::builder(semantic.name) .ty(semantic.ty) .builtin(semantic.builtin) + .type_check_only(semantic.is_type_check_only) .docstring(documentation) } @@ -547,7 +589,7 @@ impl<'db> CompletionBuilder<'db> { /// Use this builder to construct a `Completion`. /// - /// `ctx` is any information about the position of the + /// `env` is any information about the position of the /// cursor in the source code that could impact the relevance /// ranking of the completion. /// @@ -556,11 +598,12 @@ impl<'db> CompletionBuilder<'db> { fn build( mut self, db: &'db dyn Db, - ctx: &CollectionContext<'db>, + program_file: ProgramFile<'db>, + collection_context: &CollectionContext<'db>, query: &UserQuery, ) -> Completion<'db> { if let Some(ty) = self.ty { - self.is_type_check_only = ty.is_type_check_only(db); + self.is_type_check_only |= ty.is_type_check_only(db); // Tags completions with context-specific if they are // known to be usable in an exception context and we have // determined an `exception_ty`. @@ -568,10 +611,11 @@ impl<'db> CompletionBuilder<'db> { // It's possible that some completions are usable in an exception // but aren't marked here. That is, false negatives are // possible but false positives are not. - if let Some(exception_ty) = ctx.exception_ty { - self.is_context_specific |= ty.is_assignable_to(db, exception_ty); + if let Some(exception_ty) = collection_context.exception_ty { + let env = ProgramEnvironment::from_file(program_file); + self.is_context_specific |= ty.is_assignable_to(db, &env, exception_ty); } - if ctx.is_in_class_def() { + if collection_context.is_in_class_def() { self.is_context_specific |= ty.is_class_literal() || matches!( ty, @@ -589,28 +633,30 @@ impl<'db> CompletionBuilder<'db> { let kind = self .kind .or_else(|| self.ty.and_then(|ty| completion_kind_from_type(db, ty))); - let relevance = Relevance::new(ctx, query, &self); - let (label, insert, insert_text_format) = if ctx.should_complete_callable_parentheses(kind) - { - let label = self.insert.unwrap_or_else(|| self.name.clone()); - if ctx.capabilities.snippets { - let insert = compact_str::format_compact!("{label}($0)"); - ( - Some(label), - Some(insert), - CompletionInsertTextFormat::Snippet, - ) + let relevance = Relevance::new(db, program_file, collection_context, query, &self); + let (label, insert, insert_text_format, command) = + if collection_context.should_complete_callable_parentheses(kind) { + let label = self.insert.unwrap_or_else(|| self.name.clone()); + if collection_context.capabilities.snippets { + let insert = compact_str::format_compact!("{label}($0)"); + ( + Some(label), + Some(insert), + CompletionInsertTextFormat::Snippet, + Some(CompletionCommand::TriggerSignatureHelp), + ) + } else { + let insert = compact_str::format_compact!("{label}()"); + ( + Some(label), + Some(insert), + CompletionInsertTextFormat::PlainText, + None, + ) + } } else { - let insert = compact_str::format_compact!("{label}()"); - ( - Some(label), - Some(insert), - CompletionInsertTextFormat::PlainText, - ) - } - } else { - (self.label, self.insert, self.insert_text_format) - }; + (self.label, self.insert, self.insert_text_format, None) + }; Completion { name: self.name, label, @@ -619,6 +665,7 @@ impl<'db> CompletionBuilder<'db> { insert_text_format, replace: self.replace, filter: self.filter, + command, ty: self.ty, kind, module_name: self.module_name, @@ -699,6 +746,11 @@ impl<'db> CompletionBuilder<'db> { self } + fn type_check_only(mut self, yes: bool) -> CompletionBuilder<'db> { + self.is_type_check_only = yes; + self + } + fn context_specific(mut self, yes: bool) -> CompletionBuilder<'db> { self.is_context_specific = yes; self @@ -771,6 +823,22 @@ pub enum CompletionKind { TypeParameter, } +/// An editor action the client should perform after applying this completion. +/// +/// This is an editor-neutral *intent* produced by the analysis layer. The +/// language server maps it to a concrete command (for example +/// `ty.triggerParameterHints`) when building the LSP response, and only +/// attaches it when the client advertised support for that command, so this +/// enum never names a particular editor. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum CompletionCommand { + /// The completion inserts an opening parenthesis with the cursor placed + /// inside it (e.g. `foo($0)`). Because that parenthesis is inserted + /// programmatically rather than typed, the client will not auto-trigger + /// signature help, so it should be asked to open it explicitly. + TriggerSignatureHelp, +} + /// The format of a completion's insertion text. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub enum CompletionInsertTextFormat { @@ -870,7 +938,7 @@ impl<'m> Context<'m> { /// Create a new context for finding completions. fn new( db: &'_ dyn Db, - file: File, + file: ProgramFile<'_>, parsed: &'m ParsedModuleRef, source: &'m SourceText, offset: TextSize, @@ -882,11 +950,15 @@ impl<'m> Context<'m> { let kind = if let Some(keywords) = cursor.incomplete_keywords() { ContextKind::Keywords(keywords) - } else if let Some(modifiers) = cursor.type_param_modifiers(file.source_type(db)) { + } else if let Some(modifiers) = cursor.type_param_modifiers(file.file(db).source_type(db)) { ContextKind::TypeParamModifiers(modifiers) } else if cursor.is_in_definition_place() { return None; - } else if let Some(import) = ImportStatement::detect(db, file, &cursor) { + } else if let Some(import) = ImportStatement::detect( + db, + ImportingFile::File(file.file(db), file.resolver_environment(db)), + &cursor, + ) { ContextKind::Import(import) } else { let target_token = CompletionTargetTokens::find(&cursor)?; @@ -906,12 +978,18 @@ impl<'m> Context<'m> { capabilities: CompletionCapabilities, source_type: PySourceType, ) -> CollectionContext<'db> { + let type_checking_block = Some(TypeCheckingBlock::at_cursor(self.cursor.range)); + match self.kind { ContextKind::Keywords(_) | ContextKind::TypeParamModifiers(_) - | ContextKind::Import(_) => CollectionContext::none(), + | ContextKind::Import(_) => CollectionContext { + type_checking_block, + ..CollectionContext::none() + }, ContextKind::NonImport(_) => { - let exception_ty = self.cursor.exception_ty(db); + let env = model.program_environment(); + let exception_ty = self.cursor.exception_ty(db, &env); let complete_callable_parentheses = settings.complete_function_parentheses && !self.cursor.suppress_callable_parentheses(); let existing_class_bases = self.cursor.enclosing_class_def().map(|class_def| { @@ -928,6 +1006,7 @@ impl<'m> Context<'m> { CollectionContext { exception_ty, is_raising_exception: exception_ty.is_some(), + type_checking_block, complete_class_parentheses: complete_callable_parentheses && existing_class_bases.is_none() && !self.cursor.suppress_class_parentheses(model), @@ -1495,16 +1574,22 @@ impl<'m> ContextCursor<'m> { /// /// The return value is always `None` if the cursor is not /// inside a `raise` or `except` context. - fn exception_ty<'db>(&self, db: &'db dyn Db) -> Option> { - let base_exception_ty = KnownClass::BaseException.to_subclass_of(db); - let base_exception_instance = KnownClass::BaseException.to_instance(db); - let raise_ty = UnionType::from_elements(db, [base_exception_ty, base_exception_instance]); - let cause_ty = UnionType::from_elements(db, [raise_ty, Type::none(db)]); + fn exception_ty<'db>( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { + let base_exception_ty = KnownClass::BaseException.to_subclass_of(db, env); + let base_exception_instance = KnownClass::BaseException.to_instance(db, env); + let raise_ty = + UnionType::from_elements(db, env, [base_exception_ty, base_exception_instance]); + let cause_ty = UnionType::from_elements(db, env, [raise_ty, Type::none(db, env)]); let except_ty = UnionType::from_elements( db, + env, [ base_exception_ty, - Type::homogeneous_tuple(db, base_exception_ty), + Type::homogeneous_tuple(db, env, base_exception_ty), ], ); @@ -1948,6 +2033,40 @@ impl UserQuery { } } +#[derive(Clone, Debug)] +struct TypeCheckingBlock { + range: TextRange, + is_inside: OnceCell, +} + +impl TypeCheckingBlock { + fn at_cursor(range: TextRange) -> Self { + Self { + range, + is_inside: OnceCell::new(), + } + } + + fn is_inside<'db>(&self, db: &'db dyn Db, file: ProgramFile<'db>) -> bool { + // Most completions are ranked independently of `TYPE_CHECKING`, so only query the + // semantic index when a typing-only completion needs to know the cursor's context. + *self.is_inside.get_or_init(|| { + let parsed = parsed_module(db, file.python_file(db)).load(db); + let index = semantic_index(db, file); + + covering_node(parsed.syntax().into(), self.range) + .ancestors() + .find_map(|node| { + let ast::AnyNodeRef::StmtIf(statement) = node else { + return None; + }; + index.try_expression_scope_id(statement.test.as_ref()) + }) + .is_some_and(|scope| index.is_in_type_checking_block(scope, self.range)) + }) + } +} + /// Context used to help filter completions when collecting them. #[derive(Clone, Debug, Default)] struct CollectionContext<'db> { @@ -1958,6 +2077,8 @@ struct CollectionContext<'db> { exception_ty: Option>, /// Whether we're in an exception context (`raise` or `except`) or not. is_raising_exception: bool, + /// Whether the cursor is inside a type-checking-only block, if a cursor is available. + type_checking_block: Option, /// Names of base classes that are already specified in the class definition, /// including the class being defined (unless its name was previously bound). /// Used to filter out duplicate and self-referential base class suggestions. @@ -2092,7 +2213,7 @@ struct Relevance { /// the user's project. is_module: Sort, /// Sorts based on whether this symbol is only available during - /// type checking and not at runtime. + /// type checking and not at runtime. This does not lower its rank in a stub file. type_check_only: Sort, /// Deprecated symbols appear lower in the completion result. deprecated: Sort, @@ -2113,7 +2234,13 @@ impl Relevance { /// /// A smaller rank means the completion should appear higher in the /// results shown to end users. - fn new(_ctx: &CollectionContext, query: &UserQuery, c: &CompletionBuilder) -> Relevance { + fn new<'db>( + db: &'db dyn Db, + program_file: ProgramFile<'db>, + ctx: &CollectionContext, + query: &UserQuery, + c: &CompletionBuilder, + ) -> Relevance { Relevance { definitively_usable: if c.is_context_specific { Sort::Higher @@ -2136,10 +2263,9 @@ impl Relevance { } else { Sort::Even }, + // We only up-rank top-level modules. + // Doing this for sub-modules generates too much noise. is_module: if c.kind == Some(CompletionKind::Module) - // We only up-rank top-level modules. - // Doing this for sub-modules generates too - // much noise. && !c .qualified .as_ref() @@ -2150,7 +2276,13 @@ impl Relevance { } else { Sort::Even }, - type_check_only: if c.is_type_check_only { + type_check_only: if c.is_type_check_only + && !program_file.file(db).source_type(db).is_stub() + && !ctx + .type_checking_block + .as_ref() + .is_some_and(|block| block.is_inside(db, program_file)) + { Sort::Lower } else { Sort::Even @@ -2311,6 +2443,32 @@ impl ModuleDependencyKind { } } +/// Returns whether importing this module would require a typing-only runtime context. +fn is_type_check_only_module<'db>( + db: &'db dyn Db, + importing_from: ProgramFile<'db>, + module: Module<'db>, +) -> bool { + if !module.is_type_check_only(db) { + return false; + } + + if module.name(db).first_component() != "typing_extensions" { + return true; + } + + // typeshed bundles `typing_extensions` with its standard-library stubs even + // though the actual module is a third-party package. Since the bundled stub + // takes precedence during module resolution, look past it to check whether a + // corresponding runtime module is also available from the project or site-packages. + let importing_file = ImportingFile::File( + importing_from.file(db), + importing_from.resolver_environment(db), + ); + resolve_real_shadowable_module(db, importing_file, &KnownModule::TypingExtensions.name()) + .is_none() +} + /// An instruction to indicate an ordering preference. #[derive(Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord)] enum Sort { @@ -2329,6 +2487,7 @@ enum Sort { /// Detect and add completions for unset arguments. fn add_argument_completions<'db>( db: &'db dyn Db, + file: ProgramFile<'db>, model: &SemanticModel<'db>, cursor: &ContextCursor<'_>, completions: &mut Completions<'db>, @@ -2348,7 +2507,7 @@ fn add_argument_completions<'db>( } ast::AnyNodeRef::ExprCall(_) => { if in_arguments { - add_function_arg_completions(db, model.file(), cursor, completions); + add_function_arg_completions(db, file, cursor, completions); } return; } @@ -2380,29 +2539,31 @@ fn add_class_arg_completions<'db>( class_def: &ast::StmtClassDef, completions: &mut Completions<'db>, ) { + let db = model.db(); let is_set = |name| { class_def .arguments .as_ref() .is_some_and(|args| args.find_keyword(name).is_some()) }; + let env = model.program_environment(); if !is_set("metaclass") { - let ty = KnownClass::Type.to_subclass_of(model.db()); + let ty = KnownClass::Type.to_subclass_of(db, &env); completions.add(CompletionBuilder::argument("metaclass").ty(ty)); } let is_typed_dict = class_def .inferred_type(model) .and_then(Type::as_class_literal) - .is_some_and(|t| t.is_typed_dict(model.db())); + .is_some_and(|t| t.is_typed_dict(db)); // TODO: Handle PEP 728 that adds two extra keywords, // closed and extra_items. // // See https://peps.python.org/pep-0728/ if is_typed_dict && !is_set("total") { - let ty = KnownClass::Bool.to_instance(model.db()); + let ty = KnownClass::Bool.to_instance(db, &env); completions.add(CompletionBuilder::argument("total").ty(ty)); } } @@ -2414,7 +2575,7 @@ fn add_class_arg_completions<'db>( /// set and 2) been defined as positional-only. fn add_function_arg_completions<'db>( db: &'db dyn Db, - file: File, + file: ProgramFile<'db>, cursor: &ContextCursor<'_>, completions: &mut Completions<'db>, ) { @@ -2493,9 +2654,9 @@ pub(crate) struct ImportEdit { } /// Get fixes that would resolve an unresolved reference -pub(crate) fn unresolved_fixes( - db: &dyn Db, - file: File, +pub(crate) fn unresolved_fixes<'db>( + db: &'db dyn Db, + file: ProgramFile<'db>, parsed: &ParsedModuleRef, symbol: &str, node: AnyNodeRef, @@ -2506,7 +2667,7 @@ pub(crate) fn unresolved_fixes( let ctx = CollectionContext::none(); // Request imports we could add to put the symbol in scope - let mut completions = Completions::new(db, ctx.clone(), query.clone()); + let mut completions = Completions::new(db, file, ctx.clone(), query.clone()); add_unimported_completions( db, file, @@ -2520,7 +2681,7 @@ pub(crate) fn unresolved_fixes( results.extend(completions.into_imports()); // Request qualifications we could apply to the symbol to make it resolve - let mut completions = Completions::new(db, ctx, query); + let mut completions = Completions::new(db, file, ctx, query); add_unimported_completions( db, file, @@ -2541,9 +2702,13 @@ pub(crate) fn unresolved_fixes( /// This should generally only be used when offering "scoped" completions. /// This will include keywords corresponding to Python values (like `None`) /// and general language keywords (like `raise`). -fn add_keyword_completions<'db>(db: &'db dyn Db, completions: &mut Completions<'db>) { +fn add_keyword_completions<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + completions: &mut Completions<'db>, +) { let keyword_values = [ - ("None", Type::none(db)), + ("None", Type::none(db, env)), ("True", Type::bool_literal(true)), ("False", Type::bool_literal(false)), ]; @@ -2610,13 +2775,14 @@ fn add_context_sensitive_completions<'db>( source_type: PySourceType, completions: &mut Completions<'db>, ) { + let env = &model.program_environment(); if !source_type.is_basedpython() { return; } let Some(target) = cursor.expected_type(model) else { return; }; - for (name, ty) in context_sensitive_members(model.db(), target) { + for (name, ty) in context_sensitive_members(model.db(), env, target) { completions.add( Completion::builder(name.as_str()) .ty(ty) @@ -2636,10 +2802,11 @@ fn add_extension_completions<'db>( file: File, completions: &mut Completions<'db>, ) { + let env = &model.program_environment(); let Some(receiver) = expr.value.inferred_type(model) else { return; }; - for (name, ty) in extension_members(model.db(), file, receiver) { + for (name, ty) in extension_members(model.db(), env, file, receiver) { completions.add( Completion::builder(name.as_str()) .ty(ty) @@ -2661,6 +2828,7 @@ fn add_override_completions<'db>( capabilities: CompletionCapabilities, completions: &mut Completions<'db>, ) { + let env = &model.program_environment(); if !source_type.is_basedpython() || !cursor.is_at_statement_start() || !cursor.is_in_class_body() @@ -2674,7 +2842,7 @@ fn add_override_completions<'db>( return; }; - for member in overridable_members(model.db(), class) { + for member in overridable_members(model.db(), env, class) { let header = format!("override def {}{}:", member.name, member.signature); let body = if capabilities.snippets { "$0" } else { "" }; let builder = Completion::builder(member.name.as_str()) @@ -3113,6 +3281,7 @@ fn add_postfix_completions<'db>( capabilities: CompletionCapabilities, completions: &mut Completions<'db>, ) { + let env = &model.program_environment(); if expr.ctx.is_store() || expr.ctx.is_del() || cursor.is_in_type_expression(model) { return; } @@ -3125,7 +3294,7 @@ fn add_postfix_completions<'db>( && expr .value .inferred_type(model) - .is_none_or(|ty| is_awaitable(model.db(), ty)) + .is_none_or(|ty| is_awaitable(model.db(), env, ty)) { completions.add( CompletionBuilder::keyword("await") @@ -3182,6 +3351,8 @@ fn add_postfix_completions<'db>( /// what is wanted fn format_spec_completions<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + file: ProgramFile<'db>, model: &SemanticModel<'db>, source: &str, cursor: &ContextCursor<'_>, @@ -3207,9 +3378,10 @@ fn format_spec_completions<'db>( let language = field .expression .inferred_type(model) - .and_then(|ty| spec_language(db, ty)); + .and_then(|ty| spec_language(db, env, ty)); - let mut completions = Completions::new(db, CollectionContext::none(), UserQuery::fuzzy(None)); + let mut completions = + Completions::new(db, file, CollectionContext::none(), UserQuery::fuzzy(None)); let mut offer = |insert: String, summary: &str, documentation: String| { completions.add_skip_query( Completion::builder(insert) @@ -3374,7 +3546,7 @@ fn add_string_literal_completions<'db>( /// when selected into `File`. fn add_unimported_completions<'db>( db: &'db dyn Db, - file: File, + file: ProgramFile<'db>, parsed: &ParsedModuleRef, scoped: ScopedTarget<'_>, create_import_request: impl for<'a> Fn(&'a ModuleName, &'a str) -> ImportRequest<'a>, @@ -3388,13 +3560,15 @@ fn add_unimported_completions<'db>( return; } - let source = source_text(db, file); + let source_file = file.file(db); + let source = source_text(db, source_file); let stylist = Stylist::from_tokens(parsed.tokens(), source.as_str()); let importer = Importer::new(db, &stylist, file, source.as_str(), parsed); let members = importer.members_in_scope_at(scoped.node, scoped.node.start()); + let importing_file = ImportingFile::File(source_file, file.resolver_environment(db)); for symbol in all_symbols(db, file, &completions.query.pattern) { - if symbol.file() == file || symbol.module().is_known(db, KnownModule::Builtins) { + if symbol.file() == source_file || symbol.module().is_known(db, KnownModule::Builtins) { continue; } @@ -3408,7 +3582,7 @@ fn add_unimported_completions<'db>( }); // Don't suggest symbols that are already imported. - if members.satisfies(db, file, &request) { + if members.satisfies(db, importing_file, &request) { continue; } @@ -3424,9 +3598,10 @@ fn add_unimported_completions<'db>( .module_name(module_name) .import(import_action.import().cloned()) .deprecated(symbol.deprecated()) + .type_check_only(is_type_check_only_module(db, file, symbol.module())) .module_dependency_kind(ModuleDependencyKind::from_module( db, - file, + file.file(db), symbol.module(), )), ); @@ -3677,7 +3852,7 @@ impl<'a> ImportStatement<'a> { /// `tokens`. fn detect( db: &'_ dyn Db, - file: File, + file: ImportingFile<'_>, cursor: &ContextCursor<'a>, ) -> Option> { use TokenKind as TK; @@ -4075,19 +4250,24 @@ impl<'a> ImportStatement<'a> { fn add_completions<'db>( &self, db: &'db dyn Db, - file: File, + file: ProgramFile<'db>, completions: &mut Completions<'db>, ) { let model = SemanticModel::new(db, file); match *self { ImportStatement::Import(Import { ref kind, .. }) => match *kind { ImportKind::Module => { - add_import_completions(db, file, completions, model.import_completions()); + add_import_completions( + db, + file.file(db), + completions, + model.import_completions(), + ); } ImportKind::Submodule { ref parent } => { add_import_completions( db, - file, + file.file(db), completions, model.import_submodule_completions_for_name(parent), ); @@ -4095,12 +4275,17 @@ impl<'a> ImportStatement<'a> { }, ImportStatement::FromImport(FromImport { ast, ref kind }) => match *kind { FromImportKind::Module => { - add_import_completions(db, file, completions, model.import_completions()); + add_import_completions( + db, + file.file(db), + completions, + model.import_completions(), + ); } FromImportKind::Submodule { ref parent } => { add_import_completions( db, - file, + file.file(db), completions, model.import_submodule_completions_for_name(parent), ); @@ -4111,7 +4296,7 @@ impl<'a> ImportStatement<'a> { } => { add_import_completions( db, - file, + file.file(db), completions, model.import_submodule_completions_for_name(parent), ); @@ -4122,7 +4307,7 @@ impl<'a> ImportStatement<'a> { FromImportKind::Attribute => { let module_dependency_kind = model .resolve_module(ast.module.as_ref().map(ast::Identifier::as_str), ast.level) - .map(|module| ModuleDependencyKind::from_module(db, file, module)); + .map(|module| ModuleDependencyKind::from_module(db, file.file(db), module)); add_import_completions_with_module_dependency_kind( db, completions, @@ -4191,11 +4376,20 @@ fn add_import_completions_impl<'db>( &SemanticCompletion<'db>, ) -> Option>, ) { + let env = ProgramEnvironment::from_file(completions.program_file); for semantic in semantic_completions { let Some(module_dependency_kind) = classify(completions, &semantic) else { continue; }; - let mut builder = CompletionBuilder::from_semantic_completion(db, semantic); + let is_from_type_check_only_module = matches!( + semantic.ty, + Some(Type::ModuleLiteral(module)) + if is_type_check_only_module(db, completions.program_file, module.module(db)) + ); + let mut builder = CompletionBuilder::from_semantic_completion(db, &env, semantic); + if is_from_type_check_only_module { + builder = builder.type_check_only(true); + } if let Some(module_dependency_kind) = module_dependency_kind { builder = builder.module_dependency_kind(module_dependency_kind); } @@ -4677,7 +4871,8 @@ re. .source( "package/__init__.pyi", r#"\ -from typing import TypeAlias, Literal, TypeVar, ParamSpec, TypeVarTuple, Protocol +from types import UnionType +from typing import TYPE_CHECKING, Literal, ParamSpec, Protocol, TypeAlias, TypeVar, TypeVarTuple, type_check_only public_name = 1 _private_name = 1 @@ -4700,11 +4895,34 @@ _private_explicit_type_alias: TypeAlias = Literal[1] public_implicit_union_alias = int | str _private_implicit_union_alias = int | str +def make_union() -> UnionType: ... +def make_typevar() -> TypeVar: ... +def identity[T](value: T) -> T: ... + +_private_runtime_union = make_union() +_private_runtime_typevar = make_typevar() +_private_precise_runtime_union = identity(int | str) + class PublicProtocol(Protocol): def method(self) -> None: ... class _PrivateProtocol(Protocol): def method(self) -> None: ... + +@type_check_only +class PublicTypeOnlyProtocol(Protocol): + def method(self) -> None: ... + +@type_check_only +class _PrivateTypeOnlyProtocol(Protocol): + def method(self) -> None: ... + +if TYPE_CHECKING: + class PublicTypeCheckingProtocol(Protocol): + def method(self) -> None: ... + + class _PrivateTypeCheckingProtocol(Protocol): + def method(self) -> None: ... "#, ) .source("main.py", "import package; package.") @@ -4716,18 +4934,95 @@ class _PrivateProtocol(Protocol): test.contains("__mangled_name"); test.contains("__dunder_name__"); test.contains("public_type_var"); - test.not_contains("_private_type_var"); - test.not_contains("__mangled_type_var"); test.contains("public_param_spec"); - test.not_contains("_private_param_spec"); test.contains("public_type_var_tuple"); - test.not_contains("_private_type_var_tuple"); test.contains("public_explicit_type_alias"); - test.not_contains("_private_explicit_type_alias"); test.contains("public_implicit_union_alias"); - test.not_contains("_private_implicit_union_alias"); + test.contains("_private_runtime_union"); + test.contains("_private_runtime_typevar"); + test.contains("_private_precise_runtime_union"); test.contains("PublicProtocol"); - test.not_contains("_PrivateProtocol"); + test.contains("_PrivateProtocol"); + + for name in [ + "_private_type_var", + "__mangled_type_var", + "_private_param_spec", + "_private_type_var_tuple", + "_private_explicit_type_alias", + "_private_implicit_union_alias", + "PublicTypeOnlyProtocol", + "_PrivateTypeOnlyProtocol", + "PublicTypeCheckingProtocol", + "_PrivateTypeCheckingProtocol", + ] { + assert!( + test.completions() + .iter() + .any(|completion| completion.name == name && completion.is_type_check_only), + "Expected `{name}` to be marked as typing-only", + ); + } + } + + #[test] + fn private_stub_symbols_rank_below_runtime_values() { + let builder = CursorTest::builder() + .source( + "package/__init__.pyi", + "from typing import TypeVar\n_Alpha = TypeVar(\"_Alpha\")\n_Zeta = 1", + ) + .source("main.py", "import package; package._") + .completion_test_builder() + .filter(|completion| matches!(completion.name.as_str(), "_Alpha" | "_Zeta")); + + let test = builder.build(); + let completions = test + .completions() + .iter() + .map(|completion| (completion.name.as_str(), completion.is_type_check_only)) + .collect::>(); + + assert_eq!(completions, [("_Zeta", false), ("_Alpha", true)]); + } + + #[test] + fn type_checking_import_includes_private_stub_symbols() { + let builder = CursorTest::builder() + .source( + "package/__init__.pyi", + "from typing import TypeAlias, TypeVar\n_Alias: TypeAlias = int\n_T = TypeVar(\"_T\")", + ) + .source( + "main.py", + "from typing import TYPE_CHECKING\nif TYPE_CHECKING:\n from package import _", + ) + .completion_test_builder(); + + let test = builder.build(); + for name in ["_Alias", "_T"] { + assert!( + test.completions() + .iter() + .any(|completion| completion.name == name && completion.is_type_check_only), + "Expected `{name}` to be available as a typing-only completion", + ); + } + } + + #[test] + fn typing_only_project_builtins_not_suggested_implicitly() { + let builder = CursorTest::builder() + .source( + "__builtins__.pyi", + "from typing import TypeVar\n_typing_only = TypeVar(\"_typing_only\")\n_runtime: int", + ) + .source("main.py", "_") + .completion_test_builder() + .skip_auto_import(); + + let test = builder.build(); + test.contains("_runtime").not_contains("_typing_only"); } /// Unlike [`private_symbols_in_stub`], this test doesn't use a `.pyi` file so all of the names @@ -11717,7 +12012,10 @@ if foo: #[test] fn from_import_no_space_not_suggests_import() { let builder = completion_test_builder("from typing"); - assert_snapshot!(builder.build().snapshot(), @"typing"); + assert_snapshot!(builder.build().snapshot(), @" + typing + typing_extensions + "); } #[test] @@ -11928,19 +12226,111 @@ from .imp } #[test] - fn typing_extensions_excluded_from_import() { + fn bundled_typing_extensions_module_completion() { let builder = completion_test_builder("from typing").module_names(); - assert_snapshot!(builder.build().snapshot(), @"typing :: "); + let completions = builder.build(); + assert!(completions.completions().iter().any(|completion| { + completion.name == "typing_extensions" && completion.is_type_check_only + })); + assert_snapshot!(completions.snapshot(), @" + typing :: + typing_extensions :: + "); + } + + #[test] + fn bundled_ty_extensions_module_completion() { + let builder = completion_test_builder("from ty_ex") + .module_names() + .filter(|completion| completion.name == "ty_extensions"); + let completions = builder.build(); + assert!( + completions + .completions() + .iter() + .all(|completion| completion.is_type_check_only) + ); + assert_snapshot!(completions.snapshot(), @"ty_extensions :: "); + } + + #[test] + fn bundled_typeshed_module_completion() { + let builder = completion_test_builder("from _type") + .module_names() + .filter(|completion| completion.name == "_typeshed"); + let completions = builder.build(); + assert!( + completions + .completions() + .iter() + .all(|completion| completion.is_type_check_only) + ); + assert_snapshot!(completions.snapshot(), @"_typeshed :: "); } #[test] - fn typing_extensions_excluded_from_auto_import() { - let builder = completion_test_builder("deprecated").module_names(); - assert_snapshot!(builder.build().snapshot(), @"deprecated :: warnings"); + fn runtime_ty_extensions_auto_import_is_not_type_check_only() { + let builder = CursorTest::builder() + .source("ty_extensions.py", "static_assert = 1") + .source("main.py", "static_ass") + .completion_test_builder() + .module_names() + .filter(|completion| completion.name == "static_assert"); + let completions = builder.build(); + assert!( + completions + .completions() + .iter() + .all(|completion| !completion.is_type_check_only) + ); + assert_snapshot!(completions.snapshot(), @"static_assert :: ty_extensions"); } #[test] - fn typing_extensions_included_from_import() { + fn ty_extensions_pydantic_auto_import_generates_import_edit() { + let builder = completion_test_builder("LaxDa") + .module_names() + .imports() + .filter(|completion| completion.name == "LaxDate"); + assert_snapshot!(builder.build().snapshot(), @"LaxDate :: ty_extensions.pydantic :: from ty_extensions.pydantic import LaxDate"); + } + + #[test] + fn ty_extensions_auto_import_is_type_check_only_in_stub() { + let builder = CursorTest::builder() + .source("main.pyi", "static_ass") + .completion_test_builder() + .module_names() + .filter(|completion| completion.name == "static_assert"); + let completions = builder.build(); + assert!( + completions + .completions() + .iter() + .all(|completion| completion.is_type_check_only) + ); + assert_snapshot!(completions.snapshot(), @"static_assert :: ty_extensions"); + } + + #[test] + fn typeshed_auto_import_is_type_check_only_in_stub() { + let builder = CursorTest::builder() + .source("main.pyi", "TypedDictFall") + .completion_test_builder() + .module_names() + .filter(|completion| completion.name == "TypedDictFallback"); + let completions = builder.build(); + assert!( + completions + .completions() + .iter() + .all(|completion| completion.is_type_check_only) + ); + assert_snapshot!(completions.snapshot(), @"TypedDictFallback :: _typeshed._type_checker_internals"); + } + + #[test] + fn runtime_typing_extensions_module_completion() { let builder = CursorTest::builder() .source("typing_extensions.py", "deprecated = 1") .source("foo.py", "from typing") @@ -11953,37 +12343,54 @@ from .imp } #[test] - fn typing_extensions_included_from_auto_import() { + fn runtime_typing_extensions_auto_import_is_not_type_check_only() { let builder = CursorTest::builder() .source("typing_extensions.py", "deprecated = 1") .source("foo.py", "deprecated") .completion_test_builder() .module_names(); - assert_snapshot!(builder.build().snapshot(), @" + let completions = builder.build(); + assert!( + completions.completions().iter().any(|completion| { + completion.module_name.map(ModuleName::as_str) == Some("typing_extensions") + && !completion.is_type_check_only + }), + "runtime `typing_extensions` should not be downranked", + ); + assert_snapshot!(completions.snapshot(), @" deprecated :: typing_extensions deprecated :: warnings "); } #[test] - fn typing_extensions_included_from_import_in_stub() { + fn typing_extensions_module_completion_is_type_check_only_in_stub() { let builder = CursorTest::builder() .source("foo.pyi", "from typing") .completion_test_builder() .module_names(); - assert_snapshot!(builder.build().snapshot(), @" + let completions = builder.build(); + assert!(completions.completions().iter().any(|completion| { + completion.name == "typing_extensions" && completion.is_type_check_only + })); + assert_snapshot!(completions.snapshot(), @" typing :: typing_extensions :: "); } #[test] - fn typing_extensions_included_from_auto_import_in_stub() { + fn typing_extensions_auto_import_is_type_check_only_in_stub() { let builder = CursorTest::builder() .source("foo.pyi", "deprecated") .completion_test_builder() .module_names(); - assert_snapshot!(builder.build().snapshot(), @" + let completions = builder.build(); + assert!(completions.completions().iter().any(|completion| { + completion.module_name.map(ModuleName::as_str) == Some("typing_extensions") + && completion.is_type_check_only + })); + assert_snapshot!(completions.snapshot(), @" deprecated :: typing_extensions deprecated :: warnings "); @@ -12895,15 +13302,17 @@ import typing from typing import Callable TypedDi ", - ); + ) + .imports() + .filter(|completion| matches!(completion.name.as_str(), "TypedDict" | "is_typeddict")); assert_snapshot!( - builder.imports().build().snapshot(), + builder.build().snapshot(), @" TypedDict :: , TypedDict is_typeddict :: , is_typeddict - _FilterConfigurationTypedDict :: from logging.config import _FilterConfigurationTypedDict + TypedDict :: from typing_extensions import TypedDict - _FormatterConfigurationTypedDict :: from logging.config import _FormatterConfigurationTypedDict + is_typeddict :: from typing_extensions import is_typeddict ", ); } @@ -13392,11 +13801,15 @@ raise impl CompletionTestBuilder { /// Returns completions based on this configuration. fn build(&self) -> CompletionTest<'_> { + let env = &self + .cursor_test + .program_environment(self.cursor_test.cursor.file); let original = completion( &self.cursor_test.db, + env, &self.settings, self.capabilities, - self.cursor_test.cursor.file, + self.cursor_test.program_file(self.cursor_test.cursor.file), self.cursor_test.cursor.offset, ); let filtered = original @@ -13552,6 +13965,7 @@ raise impl<'db> CompletionTest<'db> { fn snapshot(&self) -> String { + let db = self.db; if self.original.is_empty() { return "".to_string(); } else if self.filtered.is_empty() { @@ -13563,13 +13977,14 @@ raise // ---AG return "".to_string(); } + let env = self.db.program_environment(); self.filtered .iter() .map(|c| { let mut snapshot = c.insert.as_deref().unwrap_or(c.label()).to_string(); if self.type_signatures { let ty = - c.ty.map(|ty| ty.display(self.db).to_string()) + c.ty.map(|ty| ty.display(db, &env).to_string()) .or_else(|| c.detail.as_ref().map(ToString::to_string)) .unwrap_or_else(|| "Unavailable".to_string()); snapshot = format!("{snapshot} :: {ty}"); diff --git a/crates/ty_ide/src/django_template.rs b/crates/ty_ide/src/django_template.rs index 661b7adce2..76cfeb27bb 100644 --- a/crates/ty_ide/src/django_template.rs +++ b/crates/ty_ide/src/django_template.rs @@ -60,6 +60,8 @@ use crate::semantic_tokens::SemanticTokens; use crate::{FoldingRange, InlayHintSettings, NavigationTargets, RangedValue, ReferenceTarget}; use index::TemplateIndex; +use ty_python_core::ProgramFile; +use ty_python_semantic::ProgramEnvironment; /// how many `{% extends %}` hops a parent chain is followed const MAX_INHERITANCE_DEPTH: usize = 16; @@ -222,8 +224,12 @@ pub fn django_template_diagnostics(db: &dyn Db, file: File) -> Vec { /// the file's suppression comments are deliberately *not* applied: these are /// folded into the type checker's own diagnostics, which is where a `ty: ignore` /// is honoured and counted used — see [`ty_python_semantic::check_file_with`]. -pub fn django_python_diagnostics(db: &dyn Db, file: File) -> Vec { - routes::diagnostics(db, file) +pub fn django_python_diagnostics( + db: &dyn Db, + env: &ProgramEnvironment<'_>, + file: File, +) -> Vec { + routes::diagnostics(db, env, file) } /// django's checks, as something [`ty_project::Project::check`] can run @@ -266,8 +272,9 @@ impl ty_project::ProjectChecker for DjangoChecker { django_template_diagnostics(db, file) } - fn check_python_file(&self, db: &dyn Db, file: File) -> Vec { - django_python_diagnostics(db, file) + fn check_python_file(&self, db: &dyn Db, file: ProgramFile<'_>) -> Vec { + let env = &ProgramEnvironment::from_file(file); + django_python_diagnostics(db, env, file.file(db)) } fn django_settings_file(&self, db: &dyn Db) -> Option { @@ -339,16 +346,18 @@ pub fn django_references( /// template pub fn django_template_signature_help( db: &dyn Db, + env: &ProgramEnvironment<'_>, file: File, offset: TextSize, ) -> Option { let source = source_text(db, file); - signature_help::signature_help(db, template_index(db, file), source.as_str(), offset) + signature_help::signature_help(db, env, template_index(db, file), source.as_str(), offset) } /// the hints `range` of `file` shows, read as a django template pub fn django_template_inlay_hints( db: &dyn Db, + env: &ProgramEnvironment<'_>, file: File, range: TextRange, settings: &InlayHintSettings, @@ -356,6 +365,7 @@ pub fn django_template_inlay_hints( let source = source_text(db, file); inlay_hints::inlay_hints( db, + env, file, template_index(db, file), source.as_str(), @@ -392,11 +402,19 @@ pub fn django_python_code_lenses(db: &dyn Db, file: File) -> Vec /// the completions for `offset` in `file`, read as a django template pub fn django_template_completions( db: &dyn Db, + env: &ProgramEnvironment<'_>, file: File, offset: TextSize, ) -> Vec { let source = source_text(db, file); - completion::completions(db, file, template_index(db, file), source.as_str(), offset) + completion::completions( + db, + env, + file, + template_index(db, file), + source.as_str(), + offset, + ) } #[cfg(test)] @@ -416,6 +434,7 @@ pub(crate) mod tests { use ty_python_core::program::{FallibleStrategy, Program, ProgramSettings}; use ty_python_semantic::PythonVersionWithSource; use ty_python_semantic::lint::Level; + use ty_python_semantic::{Db as _, ProgramEnvironment}; use crate::MarkupKind; @@ -547,6 +566,11 @@ pub(crate) mod tests { } impl TemplateTest { + /// the environment the file under test is checked in + pub(crate) fn program_environment(&self) -> ProgramEnvironment<'_> { + ProgramEnvironment::from_file(self.db.program_file(self.file)) + } + /// build a project from `(path, contents)` pairs pub(crate) fn new(sources: &[(&str, &str)]) -> Self { let mut db = TestDb::new(ProjectMetadata::new("test", SystemPathBuf::from("/"))); @@ -617,14 +641,15 @@ pub(crate) mod tests { .to_search_paths(db.system(), db.vendored(), &FallibleStrategy) .expect("valid search paths"); - Program::from_settings( - &db, - ProgramSettings { - python_version: PythonVersionWithSource::default(), - python_platform: PythonPlatform::default(), - search_paths, - }, - ); + let settings = ProgramSettings { + python_version: PythonVersionWithSource::default(), + python_platform: PythonPlatform::default(), + search_paths, + }; + Program::from_settings(&db, settings.clone()); + // a project-level query (`has_django`) resolves against the project's own + // settings rather than a file's, so they have to carry the search paths too + ty_project::Db::project(&db).update_program(&mut db, settings); db.files().try_add_root(&db, &root, FileRootKind::Project); db.files() @@ -692,9 +717,10 @@ pub(crate) mod tests { pub(crate) fn python_diagnostics(&self, path: &str) -> Vec { let file = system_path_to_file(&self.db, self.root.join(path)) .expect("the file to have been written"); + let env = &ProgramEnvironment::from_file(self.db.program_file(file)); let source = ruff_db::source::source_text(&self.db, file); - django_python_diagnostics(&self.db, file) + django_python_diagnostics(&self.db, env, file) .into_iter() .map(|diagnostic| { let range = diagnostic @@ -706,7 +732,7 @@ pub(crate) mod tests { "{} {:?}: {} [{}]", diagnostic.id(), diagnostic.severity(), - diagnostic.primary_message(), + diagnostic.headline_message(), &source[range] ) }) @@ -729,9 +755,10 @@ pub(crate) mod tests { let file = system_path_to_file(&self.db, self.root.join(path)) .expect("the file to have been written"); - let external = django_python_diagnostics(&self.db, file); + let env = &ProgramEnvironment::from_file(self.db.program_file(file)); + let external = django_python_diagnostics(&self.db, env, file); - ty_python_semantic::check_file_with(&self.db, file, external) + ty_python_semantic::check_file_with(&self.db, self.db.program_file(file), external) .expect("the file to be readable") .iter() // the mock django the fixtures install is annotated no further than @@ -743,7 +770,7 @@ pub(crate) mod tests { .as_lint() .is_some_and(|name| REPORTED.contains(&name.as_str())) }) - .map(|diagnostic| format!("{}: {}", diagnostic.id(), diagnostic.primary_message())) + .map(|diagnostic| format!("{}: {}", diagnostic.id(), diagnostic.headline_message())) .collect() } @@ -764,7 +791,7 @@ pub(crate) mod tests { "{} {:?}: {} [{}]", diagnostic.id(), diagnostic.severity(), - diagnostic.primary_message(), + diagnostic.headline_message(), &source[range] ) }) @@ -795,7 +822,8 @@ pub(crate) mod tests { /// the labels of the completions at the cursor, in the order offered pub(crate) fn completions(&self) -> Vec { - django_template_completions(&self.db, self.file, self.offset) + let env = &self.program_environment(); + django_template_completions(&self.db, env, self.file, self.offset) .into_iter() .map(|completion| completion.label) .collect() @@ -803,7 +831,8 @@ pub(crate) mod tests { /// the labels of the completions at the cursor django will not render pub(crate) fn unusable(&self) -> Vec { - django_template_completions(&self.db, self.file, self.offset) + let env = &self.program_environment(); + django_template_completions(&self.db, env, self.file, self.offset) .into_iter() .filter(|completion| completion.unusable) .map(|completion| completion.label) @@ -812,7 +841,8 @@ pub(crate) mod tests { /// the completions at the cursor, rendered as `label — detail` pub(crate) fn detailed(&self) -> Vec { - django_template_completions(&self.db, self.file, self.offset) + let env = &self.program_environment(); + django_template_completions(&self.db, env, self.file, self.offset) .into_iter() .map(|completion| match completion.detail { Some(detail) => format!("{} — {detail}", completion.label), @@ -851,7 +881,9 @@ pub(crate) mod tests { /// the signature help at the cursor, as /// `label [parameter] — documentation` pub(crate) fn signature(&self) -> String { - let Some(signature) = django_template_signature_help(&self.db, self.file, self.offset) + let env = &self.program_environment(); + let Some(signature) = + django_template_signature_help(&self.db, env, self.file, self.offset) else { return "no signature".to_string(); }; @@ -898,9 +930,10 @@ pub(crate) mod tests { } fn rendered_hints(&self, range: TextRange, settings: &InlayHintSettings) -> Vec { + let env = &self.program_environment(); let source = ruff_db::source::source_text(&self.db, self.file); - django_template_inlay_hints(&self.db, self.file, range, settings) + django_template_inlay_hints(&self.db, env, self.file, range, settings) .into_iter() .map(|hint| { let line_start = source.as_str()[..usize::from(hint.position)] diff --git a/crates/ty_ide/src/django_template/code_lens.rs b/crates/ty_ide/src/django_template/code_lens.rs index 7862d154f5..fc2408d7e7 100644 --- a/crates/ty_ide/src/django_template/code_lens.rs +++ b/crates/ty_ide/src/django_template/code_lens.rs @@ -80,7 +80,7 @@ pub(super) fn template_code_lenses(db: &dyn Db, file: File) -> Vec Vec // a test, unlike the two above, is addressed by the dotted module path the // test runner imports it by, so this half does need the module resolver - if let Some(module) = file_to_module(db, file) { + if let Some(module) = file_to_module(db, db.program_file(file).resolver_file(db)) { lenses.extend(test_lenses(db, file, &module.name(db).to_string())); } @@ -168,7 +168,7 @@ fn test_lenses(db: &dyn Db, file: File, module: &str) -> Vec { // a test class reaches `unittest.TestCase` through its bases, and every base // it is followed through has to be resolved. a file that declares no class at // all is the common case and costs a parse this query has already paid for - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, db.program_file(file).python_file(db)).load(db); let mut lenses = Vec::new(); diff --git a/crates/ty_ide/src/django_template/completion.rs b/crates/ty_ide/src/django_template/completion.rs index 71a2781cab..84fad0a842 100644 --- a/crates/ty_ide/src/django_template/completion.rs +++ b/crates/ty_ide/src/django_template/completion.rs @@ -27,6 +27,7 @@ use super::lexer::{Construct, ConstructKind, Token, TokenKind, string_contents}; use super::project::{self, LibrarySource, Registration, RegistrationKind}; use super::resolve; use super::uses::URL_TAG; +use ty_python_semantic::ProgramEnvironment; /// an edit a completion carries alongside the text it inserts #[derive(Debug, Clone, PartialEq, Eq)] @@ -98,6 +99,7 @@ impl TemplateCompletion { /// the suggestions for `offset` in the template `file` pub(crate) fn completions( db: &dyn Db, + env: &ProgramEnvironment<'_>, file: File, index: &TemplateIndex, source: &str, @@ -115,12 +117,14 @@ pub(crate) fn completions( Context::None => Vec::new(), Context::TagName => tag_names(db, index, source, &cursor), Context::FilterName => filter_names(db, index, &cursor), - Context::Member(path) => members(db, file, index, source, offset, &path, &cursor), - Context::Variable => variables(db, file, index, offset, &cursor), + Context::Member(path) => members(db, env, file, index, source, offset, &path, &cursor), + Context::Variable => variables(db, env, file, index, offset, &cursor), Context::TemplatePath => template_paths(db, &cursor), Context::StaticPath => static_paths(db, &cursor), Context::UrlName => url_names(db, &cursor), - Context::RouteArgument(route) => route_arguments(db, file, index, offset, &cursor, &route), + Context::RouteArgument(route) => { + route_arguments(db, env, file, index, offset, &cursor, &route) + } Context::Library => libraries(db, index, &cursor), Context::BlockName => block_names(db, file, index, &cursor), Context::PartialName => partial_names(db, file, index, &cursor), @@ -606,8 +610,10 @@ fn filter_names( completions } +#[expect(clippy::too_many_arguments)] fn members( db: &dyn Db, + env: &ProgramEnvironment<'_>, file: File, index: &TemplateIndex, source: &str, @@ -616,18 +622,18 @@ fn members( cursor: &Cursor<'_>, ) -> Vec { let segments: Vec<&str> = path.iter().map(CompactString::as_str).collect(); - let Some(ty) = resolve::path_type(db, file, index, source, offset, &segments) else { + let Some(ty) = resolve::path_type(db, env, file, index, source, offset, &segments) else { return Vec::new(); }; - let mut members: Vec<_> = resolve::members(db, ty) + let mut members: Vec<_> = resolve::members(db, env, ty) .into_iter() .map(|member| { let refused = - template_lookup(db, ty, &member.name, member.ty) == TemplateLookup::Refuses; + template_lookup(db, env, ty, &member.name, member.ty) == TemplateLookup::Refuses; TemplateCompletion::new(member.name.as_str(), CompletionKind::Field, cursor.range) - .detail(member.ty.display(db).to_string()) + .detail(member.ty.display(db, env).to_string()) .unusable(refused) }) .collect(); @@ -640,6 +646,7 @@ fn members( fn variables( db: &dyn Db, + env: &ProgramEnvironment<'_>, file: File, index: &TemplateIndex, offset: TextSize, @@ -676,7 +683,7 @@ fn variables( completion.detail = variable .value .and_then(|value| resolve::expression_type(db, variable.file, value)) - .map(|ty| ty.display(db).to_string()) + .map(|ty| ty.display(db, env).to_string()) .or_else(|| Some(variable.source.description().to_string())); completions.push(completion); } @@ -733,6 +740,7 @@ fn url_names(db: &dyn Db, cursor: &Cursor<'_>) -> Vec { /// of them. fn route_arguments( db: &dyn Db, + env: &ProgramEnvironment<'_>, file: File, index: &TemplateIndex, offset: TextSize, @@ -763,7 +771,7 @@ fn route_arguments( } } - completions.extend(variables(db, file, index, offset, cursor)); + completions.extend(variables(db, env, file, index, offset, cursor)); completions } @@ -1224,7 +1232,12 @@ mod tests { fn a_partially_typed_tag_name_is_replaced_whole() { let source = "{% ext %}"; let test = project(source); - let completions = django_template_completions(&test.db, test.file, test.offset); + let completions = django_template_completions( + &test.db, + &test.program_environment(), + test.file, + test.offset, + ); let extends = completions .iter() @@ -1256,7 +1269,12 @@ mod tests { #[test] fn a_tag_from_an_unloaded_library_carries_the_load_it_needs() { let test = project("{% %}"); - let completions = django_template_completions(&test.db, test.file, test.offset); + let completions = django_template_completions( + &test.db, + &test.program_environment(), + test.file, + test.offset, + ); let edit = completions .iter() @@ -1276,7 +1294,12 @@ mod tests { fn a_load_is_written_below_an_extends_rather_than_above_it() { let source = "{% extends 'blog/base.html' %}\n{% %}"; let test = project(source); - let completions = django_template_completions(&test.db, test.file, test.offset); + let completions = django_template_completions( + &test.db, + &test.program_environment(), + test.file, + test.offset, + ); let edit = completions .iter() @@ -1294,7 +1317,12 @@ mod tests { #[test] fn an_already_loaded_library_needs_no_load() { let test = project("{% load static %}{% %}"); - let completions = django_template_completions(&test.db, test.file, test.offset); + let completions = django_template_completions( + &test.db, + &test.program_environment(), + test.file, + test.offset, + ); let static_tag = completions .iter() @@ -1319,7 +1347,12 @@ mod tests { fn a_template_path_completion_replaces_the_string_but_not_its_quotes() { let source = "{% extends 'blog/' %}"; let test = project(source); - let completions = django_template_completions(&test.db, test.file, test.offset); + let completions = django_template_completions( + &test.db, + &test.program_environment(), + test.file, + test.offset, + ); let range = completions[0].range; assert_eq!(&source[usize::from(range.start())..], "blog/' %}"); @@ -1328,7 +1361,12 @@ mod tests { #[test] fn a_template_path_offered_outside_a_literal_brings_its_quotes() { let test = project("{% extends %}"); - let completions = django_template_completions(&test.db, test.file, test.offset); + let completions = django_template_completions( + &test.db, + &test.program_environment(), + test.file, + test.offset, + ); let first = completions.first().expect("a template to be offered"); assert_eq!(first.label, "blog/base.html"); @@ -1342,7 +1380,12 @@ mod tests { #[test] fn a_template_path_offered_inside_a_literal_does_not() { let test = project("{% extends '' %}"); - let completions = django_template_completions(&test.db, test.file, test.offset); + let completions = django_template_completions( + &test.db, + &test.program_environment(), + test.file, + test.offset, + ); assert_eq!(completions[0].insert, None); } @@ -1350,7 +1393,12 @@ mod tests { #[test] fn a_url_name_offered_outside_a_literal_brings_its_quotes() { let test = project("{% url %}"); - let completions = django_template_completions(&test.db, test.file, test.offset); + let completions = django_template_completions( + &test.db, + &test.program_environment(), + test.file, + test.offset, + ); let first = completions.first().expect("a route to be offered"); assert_eq!(first.insert.as_deref(), Some("'blog:detail'")); @@ -1417,7 +1465,12 @@ mod tests { #[test] fn a_route_argument_is_offered_with_the_equals_that_names_it() { let test = project("{% url 'blog:detail' %}"); - let completions = django_template_completions(&test.db, test.file, test.offset); + let completions = django_template_completions( + &test.db, + &test.program_environment(), + test.file, + test.offset, + ); assert_eq!(completions[0].insert.as_deref(), Some("pk=")); } @@ -1571,10 +1624,15 @@ mod tests { "django's own `humanize` filter is as available as the table's are" ); - let edit = django_template_completions(&test.db, test.file, test.offset) - .into_iter() - .find(|completion| completion.label == "intcomma") - .and_then(|completion| completion.additional_edit); + let edit = django_template_completions( + &test.db, + &test.program_environment(), + test.file, + test.offset, + ) + .into_iter() + .find(|completion| completion.label == "intcomma") + .and_then(|completion| completion.additional_edit); assert!( edit.is_none(), "the template loaded it already, so no second `{{% load %}}` is written" @@ -1585,11 +1643,16 @@ mod tests { fn a_filter_from_an_unloaded_installed_library_brings_its_load_with_it() { let test = with_humanize("{{ x| }}", ""); - let edit = django_template_completions(&test.db, test.file, test.offset) - .into_iter() - .find(|completion| completion.label == "intcomma") - .and_then(|completion| completion.additional_edit) - .expect("`intcomma` to come with the load it needs"); + let edit = django_template_completions( + &test.db, + &test.program_environment(), + test.file, + test.offset, + ) + .into_iter() + .find(|completion| completion.label == "intcomma") + .and_then(|completion| completion.additional_edit) + .expect("`intcomma` to come with the load it needs"); assert_eq!(edit.text, "{% load humanize %}\n"); } @@ -1605,10 +1668,15 @@ mod tests { ); let test = with_humanize("{{ x| }}", options); - let intcomma = django_template_completions(&test.db, test.file, test.offset) - .into_iter() - .find(|completion| completion.label == "intcomma") - .expect("the filter to be offered without a `{% load %}`"); + let intcomma = django_template_completions( + &test.db, + &test.program_environment(), + test.file, + test.offset, + ) + .into_iter() + .find(|completion| completion.label == "intcomma") + .expect("the filter to be offered without a `{% load %}`"); assert!(intcomma.additional_edit.is_none()); } @@ -1824,10 +1892,15 @@ mod tests { "django registers it into every template, so it is offered with no `{{% load %}}`" ); - let squish = django_template_completions(&test.db, test.file, test.offset) - .into_iter() - .find(|completion| completion.label == "squish") - .expect("the tag to be offered"); + let squish = django_template_completions( + &test.db, + &test.program_environment(), + test.file, + test.offset, + ) + .into_iter() + .find(|completion| completion.label == "squish") + .expect("the tag to be offered"); assert!(squish.additional_edit.is_none()); assert_eq!(squish.documentation.as_deref(), Some("squishes its body.")); } diff --git a/crates/ty_ide/src/django_template/diagnostics.rs b/crates/ty_ide/src/django_template/diagnostics.rs index 580cd52dda..e4273d2da6 100644 --- a/crates/ty_ide/src/django_template/diagnostics.rs +++ b/crates/ty_ide/src/django_template/diagnostics.rs @@ -43,6 +43,7 @@ use super::project::{self, Parameter, RegistrationKind, UrlName}; use super::resolve; use super::uses::URL_TAG; use super::{ancestors, builtins}; +use ty_python_semantic::ProgramEnvironment; /// the tag that names a static file const STATIC_TAG: &str = "static"; @@ -718,6 +719,7 @@ impl Checker<'_> { /// `alters_data` before it attempts the call, so a write method that also /// needs arguments is refused rather than uncallable. fn members_needing_arguments(&mut self) { + let env = &ProgramEnvironment::from_file(self.db.program_file(self.file)); self.decides(&[ &TEMPLATE_MEMBER_NEEDS_ARGUMENTS, &TEMPLATE_MEMBER_ALTERS_DATA, @@ -737,6 +739,7 @@ impl Checker<'_> { for length in 1..segments.len() { let Some(ty) = resolve::path_type( self.db, + env, self.file, self.index, self.source, @@ -746,16 +749,16 @@ impl Checker<'_> { break; }; let name = segments[length]; - let Some(member) = resolve::uncalled_member_type(self.db, ty, name) else { + let Some(member) = resolve::uncalled_member_type(self.db, env, ty, name) else { break; }; - if template_lookup(self.db, ty, name, member) == TemplateLookup::Refuses { + if template_lookup(self.db, env, ty, name, member) == TemplateLookup::Refuses { refused.push((name.to_compact_string(), path[length].range)); break; } - if callable_needs_arguments(self.db, member) { + if callable_needs_arguments(self.db, env, member) { found.push((name.to_compact_string(), path[length].range)); break; } diff --git a/crates/ty_ide/src/django_template/goto.rs b/crates/ty_ide/src/django_template/goto.rs index 8044157aed..38f5abb56c 100644 --- a/crates/ty_ide/src/django_template/goto.rs +++ b/crates/ty_ide/src/django_template/goto.rs @@ -16,6 +16,7 @@ use super::index::TemplateIndex; use super::lexer::{ConstructKind, Token, TokenKind, string_contents}; use super::project::{self, RegistrationKind}; use super::resolve::{self, Origin}; +use ty_python_semantic::ProgramEnvironment; /// where the name at `offset` of the template `file` is defined pub(crate) fn goto_definition( @@ -74,6 +75,7 @@ impl Site<'_> { } fn definition(&self) -> Option { + let env = &ProgramEnvironment::from_file(self.db.program_file(self.file)); match self.token.kind { TokenKind::TagName => Some(self.registration(false)), TokenKind::FilterName => Some(self.registration(true)), @@ -86,13 +88,14 @@ impl Site<'_> { let segments = path_up_to(self.source, self.tokens, self.token, false); let ty = resolve::path_type( self.db, + env, self.file, self.index, self.source, self.offset, &segments, )?; - Some(ty.navigation_targets(self.db)) + Some(ty.navigation_targets(self.db, env)) } _ => None, } diff --git a/crates/ty_ide/src/django_template/hover.rs b/crates/ty_ide/src/django_template/hover.rs index a82d4152b8..e80275a0ed 100644 --- a/crates/ty_ide/src/django_template/hover.rs +++ b/crates/ty_ide/src/django_template/hover.rs @@ -22,6 +22,7 @@ use super::index::TemplateIndex; use super::lexer::{ConstructKind, Token, TokenKind, string_contents}; use super::project::{self, RegistrationKind}; use super::resolve::{self, Origin}; +use ty_python_semantic::ProgramEnvironment; /// the language a template construct is rendered as, for a client that knows it const DJANGO: &str = "django-html"; @@ -311,10 +312,12 @@ impl Site<'_> { /// the type of the path the name ends fn path_contents(&self) -> Vec { + let env = &ProgramEnvironment::from_file(self.db.program_file(self.file)); let segments = path_up_to(self.source, self.tokens, self.token, true); if let Some(ty) = resolve::path_type( self.db, + env, self.file, self.index, self.source, @@ -323,7 +326,7 @@ impl Site<'_> { ) { return vec![Content::Code { language: "python", - text: format!("{}: {}", self.text(), ty.display(self.db)), + text: format!("{}: {}", self.text(), ty.display(self.db, env)), }]; } diff --git a/crates/ty_ide/src/django_template/inlay_hints.rs b/crates/ty_ide/src/django_template/inlay_hints.rs index 6c98ccb43d..cd8376c5cd 100644 --- a/crates/ty_ide/src/django_template/inlay_hints.rs +++ b/crates/ty_ide/src/django_template/inlay_hints.rs @@ -16,6 +16,7 @@ use super::index::{BindingOrigin, TemplateIndex, TemplateReference}; use super::lexer::TokenKind; use super::project; use super::resolve; +use ty_python_semantic::ProgramEnvironment; /// a hint written into a template between what the template itself says #[derive(Debug, Clone, PartialEq, Eq)] @@ -37,6 +38,7 @@ pub enum TemplateInlayHintKind { /// every hint `range` of the template `file` shows pub(crate) fn inlay_hints( db: &dyn Db, + env: &ProgramEnvironment<'_>, file: File, index: &TemplateIndex, source: &str, @@ -46,7 +48,7 @@ pub(crate) fn inlay_hints( let mut hints = Vec::new(); if settings.template_binding_types { - binding_types(db, file, index, source, range, &mut hints); + binding_types(db, env, file, index, source, range, &mut hints); } if settings.resolved_templates { @@ -62,6 +64,7 @@ pub(crate) fn inlay_hints( /// the element type each `{% for %}` binding takes fn binding_types( db: &dyn Db, + env: &ProgramEnvironment<'_>, file: File, index: &TemplateIndex, source: &str, @@ -93,6 +96,7 @@ fn binding_types( let Some(ty) = resolve::path_type( db, + env, file, index, source, @@ -104,7 +108,7 @@ fn binding_types( hints.push(TemplateInlayHint { position: binding.range.end(), - label: format!(": {}", ty.display(db)), + label: format!(": {}", ty.display(db, env)), kind: TemplateInlayHintKind::Type, }); } diff --git a/crates/ty_ide/src/django_template/project.rs b/crates/ty_ide/src/django_template/project.rs index 237a391ea8..1d862409a8 100644 --- a/crates/ty_ide/src/django_template/project.rs +++ b/crates/ty_ide/src/django_template/project.rs @@ -24,11 +24,13 @@ use ruff_python_ast::visitor::{Visitor, walk_expr, walk_stmt}; use ruff_python_ast::{self as ast, AnyNodeRef, Expr, Stmt}; use ruff_text_size::{Ranged, TextRange, TextSize}; use rustc_hash::{FxHashMap, FxHashSet}; +use ty_module_resolver::ImportingFile; use ty_module_resolver::{ Module, ModuleName, file_to_module, resolve_module_confident, resolve_real_module, }; use ty_project::{Db, Project}; use ty_python_core::definition::DefinitionKind; +use ty_python_semantic::ProgramEnvironment; use ty_python_semantic::SemanticModel; use ty_python_semantic::django_settings::{self as settings_source, SettingsNaming}; use ty_python_semantic::types::ide_support::{ @@ -441,8 +443,12 @@ impl Parameter { /// expression goes through no converter at all, and one whose converter the /// project registered itself yields whatever that converter's `to_python` /// returns. - pub(crate) fn value_type<'db>(&self, db: &'db dyn Db) -> Option> { - self.converter?.value_type(db) + pub(crate) fn value_type<'db>( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { + self.converter?.value_type(db, env) } } @@ -483,12 +489,12 @@ impl Converter { /// this is the other half of what a converter is: [`Self::matches`] says what /// the url may hold, and this says what comes out of `to_python` on the other /// side. - fn value_type(self, db: &dyn Db) -> Option> { + fn value_type<'db>(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Option> { match self { // three of django's five converters differ only in what they match - Self::Str | Self::Slug | Self::Path => Some(KnownClass::Str.to_instance(db)), - Self::Int => Some(KnownClass::Int.to_instance(db)), - Self::Uuid => instance_of_class(db, "uuid", "UUID"), + Self::Str | Self::Slug | Self::Path => Some(KnownClass::Str.to_instance(db, env)), + Self::Int => Some(KnownClass::Int.to_instance(db, env)), + Self::Uuid => instance_of_class(db, env, "uuid", "UUID"), } } @@ -704,9 +710,10 @@ impl DiscoveredFile { /// tree — that django is what defines. asking first is also what keeps a project /// with no django from paying for the file-system walks the discovery does. #[salsa::tracked(returns(copy))] -pub(crate) fn has_django(db: &dyn Db, _project: Project) -> bool { +pub(crate) fn has_django(db: &dyn Db, project: Project) -> bool { + let environment = project.program(db).resolver_environment(db); ModuleName::new_static(DJANGO_PACKAGE) - .and_then(|name| resolve_module_confident(db, &name)) + .and_then(|name| resolve_module_confident(db, environment, &name)) .is_some() } @@ -918,7 +925,15 @@ fn always_loaded_library( source: LibrarySource, ) -> Option { let name = ModuleName::new(path)?; - let file = resolve_real_module(db, importing, &name)?.file(db)?; + let file = resolve_real_module( + db, + ImportingFile::File( + importing, + db.program_file(importing).resolver_environment(db), + ), + &name, + )? + .file(db)?; library(db, file, source, true) } @@ -940,7 +955,16 @@ pub(crate) fn django_is_authoritative(db: &dyn Db, project: Project) -> bool { DEFAULT_BUILTIN_MODULES.iter().all(|path| { ModuleName::new(path) - .and_then(|name| resolve_real_module(db, importing, &name)) + .and_then(|name| { + resolve_real_module( + db, + ImportingFile::File( + importing, + db.program_file(importing).resolver_environment(db), + ), + &name, + ) + }) .and_then(|module| module.file(db)) .is_some() }) @@ -958,7 +982,14 @@ fn app_package<'db>(db: &'db dyn Db, importing: File, app: &str) -> Option( let mut name = package.name(db).clone(); name.extend(&ModuleName::new(TEMPLATETAGS_PACKAGE)?); - resolve_real_module(db, importing, &name) + resolve_real_module( + db, + ImportingFile::File( + importing, + db.program_file(importing).resolver_environment(db), + ), + &name, + ) } /// whether a module is one of django's own @@ -1046,7 +1084,15 @@ fn root_urlconf(db: &dyn Db, project: Project) -> Option { let importing = (*settings_file(db, project))?; let root = django_settings(db, project).root_urlconf.as_ref()?; - resolve_real_module(db, importing, &ModuleName::new(root)?)?.file(db) + resolve_real_module( + db, + ImportingFile::File( + importing, + db.program_file(importing).resolver_environment(db), + ), + &ModuleName::new(root)?, + )? + .file(db) } /// the names a module defines read on its own, the way the flat scan reads them @@ -1144,6 +1190,7 @@ impl UrlWalk<'_> { prefix: Option<&str>, depth: usize, ) { + let db = self.db; let prefix = join_routes(prefix, include.prefix.as_deref()); match &include.target { @@ -1153,7 +1200,16 @@ impl UrlWalk<'_> { } IncludeTarget::Module(module) => { let Some(included) = ModuleName::new(module) - .and_then(|module| resolve_real_module(self.db, file, &module)) + .and_then(|module| { + resolve_real_module( + self.db, + ImportingFile::File( + file, + db.program_file(file).resolver_environment(db), + ), + &module, + ) + }) .and_then(|module| module.file(self.db)) else { // a urlconf that can't be reached holds names the walk will @@ -1308,7 +1364,7 @@ fn template_uses_in_file(db: &dyn Db, file: File) -> Box<[NameUse]> { return Box::default(); } - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, db.program_file(file).python_file(db)).load(db); let mut visitor = TemplateUseVisitor { db, file, @@ -1346,7 +1402,7 @@ fn resolves_outside_django(db: &dyn Db, file: File, func: &Expr) -> bool { | DefinitionKind::ImportFrom(_) | DefinitionKind::ImportFromSubmodule(_) | DefinitionKind::StarImport(_) - ) && file_to_module(db, definition.file(db)) + ) && file_to_module(db, db.program_file(definition.file(db)).resolver_file(db)) .is_none_or(|module| !is_djangos(db, module)) }) }) @@ -1400,7 +1456,7 @@ fn route_uses_in_file(db: &dyn Db, file: File) -> Box<[NameUse]> { return Box::default(); } - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, db.program_file(file).python_file(db)).load(db); let mut visitor = RouteUseVisitor { db, file, @@ -1487,7 +1543,7 @@ pub(crate) fn bound_names(db: &dyn Db, names: &[&str]) -> Vec { continue; } - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, db.program_file(file).python_file(db)).load(db); bindings_in(file, parsed.suite(), names, &mut found); } @@ -1780,7 +1836,7 @@ fn registrations_in_file(db: &dyn Db, file: File) -> Box<[Registration]> { }; let library = library.to_compact_string(); - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, db.program_file(file).python_file(db)).load(db); let mut visitor = RegistrationVisitor { file, library, @@ -2085,7 +2141,7 @@ fn django_settings(db: &dyn Db, project: Project) -> DjangoSettings { return DjangoSettings::default(); }; - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, db.program_file(file).python_file(db)).load(db); let mut reader = SettingsReader { paths: PathEvaluator { @@ -2426,7 +2482,7 @@ fn urlconf(db: &dyn Db, file: File) -> UrlConf { return UrlConf::default(); } - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, db.program_file(file).python_file(db)).load(db); // django namespaces an included module's names under its `app_name` unless // the include says otherwise @@ -2733,7 +2789,7 @@ pub(crate) fn resolved_class( .find_map(|resolved| { let definition = resolved.definition()?; let defining = definition.file(db); - let parsed = parsed_module(db, defining).load(db); + let parsed = parsed_module(db, db.program_file(defining).python_file(db)).load(db); let class = definition.kind(db).as_class()?.node(&parsed); Some(read(defining, class)) @@ -2742,7 +2798,7 @@ pub(crate) fn resolved_class( /// every definition `expr` resolves to, import aliases followed to their source fn definitions_of<'db>(db: &'db dyn Db, file: File, expr: &Expr) -> Vec> { - let model = SemanticModel::new(db, file); + let model = SemanticModel::new(db, db.program_file(file)); match expr { Expr::Name(name) => definitions_for_name( @@ -2763,7 +2819,7 @@ fn resolved_target(db: &dyn Db, file: File, expr: &Expr) -> Option { .find_map(|resolved| { let definition = resolved.definition()?; let defining = definition.file(db); - let parsed = parsed_module(db, defining).load(db); + let parsed = parsed_module(db, db.program_file(defining).python_file(db)).load(db); let (name, kind, full_range) = match definition.kind(db) { kind if kind.as_class().is_some() => { @@ -3074,7 +3130,7 @@ fn template_contexts_in_file(db: &dyn Db, file: File) -> Box<[TemplateContext]> return Box::default(); } - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, db.program_file(file).python_file(db)).load(db); let mut visitor = ContextVisitor { db, @@ -3594,13 +3650,22 @@ fn processor_variables(db: &dyn Db, importing: File, name: &str) -> Vec Some(definition), _ => None, @@ -3677,7 +3742,7 @@ pub(crate) fn django_classes_in_file(db: &dyn Db, file: File) -> Box<[DjangoClas return Box::default(); } - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, db.program_file(file).python_file(db)).load(db); let mut visitor = DjangoClassVisitor { db, file, @@ -3797,7 +3862,7 @@ pub(crate) fn is_test_class(db: &dyn Db, file: File, class: &ast::StmtClassDef) /// whether `file` is one of `unittest`'s own modules fn is_unittests_own(db: &dyn Db, file: File) -> bool { - file_to_module(db, file) + file_to_module(db, db.program_file(file).resolver_file(db)) .is_some_and(|module| module.name(db).components().next() == Some(UNITTEST_PACKAGE)) } @@ -3811,7 +3876,8 @@ fn class_ref(db: &dyn Db, file: File, expr: &Expr) -> Option { /// whether `file` is one of django's own modules fn is_djangos_own(db: &dyn Db, file: File) -> bool { - file_to_module(db, file).is_some_and(|module| is_djangos(db, module)) + file_to_module(db, db.program_file(file).resolver_file(db)) + .is_some_and(|module| is_djangos(db, module)) } /// what the admin registrations of one scope say @@ -3876,7 +3942,7 @@ fn admin_registrations_in_file(db: &dyn Db, file: File) -> AdminRegistrations { return AdminRegistrations::default(); } - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, db.program_file(file).python_file(db)).load(db); let mut visitor = AdminRegistrationVisitor { db, file, diff --git a/crates/ty_ide/src/django_template/resolve.rs b/crates/ty_ide/src/django_template/resolve.rs index 1b8713a339..eaee1a03dc 100644 --- a/crates/ty_ide/src/django_template/resolve.rs +++ b/crates/ty_ide/src/django_template/resolve.rs @@ -23,6 +23,7 @@ use ty_python_semantic::{HasType, SemanticModel}; use super::index::{Binding, BindingOrigin, TemplateIndex}; use super::lexer::TokenKind; use super::project::{self, ContextVariable}; +use ty_python_semantic::ProgramEnvironment; /// how many `{% with %}` hops a path is followed through before giving up /// @@ -99,6 +100,7 @@ pub(crate) fn template_name(db: &dyn Db, file: File) -> Option { /// `book.author.name`. the empty path has no type. pub(crate) fn path_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, template: File, index: &TemplateIndex, source: &str, @@ -107,6 +109,7 @@ pub(crate) fn path_type<'db>( ) -> Option> { resolve_path( db, + env, template, index, source, @@ -116,8 +119,10 @@ pub(crate) fn path_type<'db>( ) } +#[expect(clippy::too_many_arguments)] fn resolve_path<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, template: File, index: &TemplateIndex, source: &str, @@ -126,18 +131,20 @@ fn resolve_path<'db>( fuel: u32, ) -> Option> { let (root, rest) = segments.split_first()?; - let mut ty = root_type(db, template, index, source, offset, root, fuel)?; + let mut ty = root_type(db, env, template, index, source, offset, root, fuel)?; for segment in rest { - ty = member_type(db, ty, segment)?; + ty = member_type(db, env, ty, segment)?; } Some(ty) } /// the type of the leading name of a path +#[expect(clippy::too_many_arguments)] fn root_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, template: File, index: &TemplateIndex, source: &str, @@ -161,18 +168,17 @@ fn root_type<'db>( let segments = path_segments(index, source, value); let value_type = resolve_path( db, + env, template, index, source, - // the tag's value expression is written in the tag itself, so it - // resolves in the scope *before* this binding takes effect binding.range.start(), &segments, fuel - 1, )?; match binding.origin { - BindingOrigin::LoopVariable => iterable_element_type(db, value_type), + BindingOrigin::LoopVariable => iterable_element_type(db, env, value_type), BindingOrigin::Alias | BindingOrigin::ForLoop => Some(value_type), } } @@ -207,11 +213,16 @@ pub(crate) fn path_segments<'src>( /// method. /// /// [resolved]: https://docs.djangoproject.com/en/stable/ref/templates/language/#variables -pub(crate) fn member_type<'db>(db: &'db dyn Db, ty: Type<'db>, name: &str) -> Option> { - let member = uncalled_member_type(db, ty, name)?; +pub(crate) fn member_type<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + name: &str, +) -> Option> { + let member = uncalled_member_type(db, env, ty, name)?; - match template_lookup(db, ty, name, member) { - TemplateLookup::Calls => Some(resolved(db, member)), + match template_lookup(db, env, ty, name, member) { + TemplateLookup::Calls => Some(resolved(db, env, member)), TemplateLookup::UsesUncalled => Some(member), // django renders `string_if_invalid` here, which is configurable and by // default the empty string. nothing useful can be said about a path @@ -228,10 +239,11 @@ pub(crate) fn member_type<'db>(db: &'db dyn Db, ty: Type<'db>, name: &str) -> Op /// member itself. pub(crate) fn uncalled_member_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, name: &str, ) -> Option> { - members(db, ty) + members(db, env, ty) .into_iter() .find(|member| member.name == name) .map(|member| member.ty) @@ -241,8 +253,8 @@ pub(crate) fn uncalled_member_type<'db>( /// /// django calls whatever the lookup found if it is callable, so a member that /// takes no arguments contributes its return type rather than its own. -fn resolved<'db>(db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { - no_argument_call_return_type(db, ty).unwrap_or(ty) +fn resolved<'db>(db: &'db dyn Db, env: &ProgramEnvironment<'db>, ty: Type<'db>) -> Type<'db> { + no_argument_call_return_type(db, env, ty).unwrap_or(ty) } /// every attribute a value of type `ty` has @@ -250,10 +262,14 @@ fn resolved<'db>(db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { /// the type's own class comes first and the rest follows, each alphabetically. a /// django model inherits several dozen members from `models.Model`, and sorting /// its fields in among them puts `title` below `save_base`. -pub(crate) fn members<'db>(db: &'db dyn Db, ty: Type<'db>) -> Vec> { - let own = own_class_member_names(db, ty); +pub(crate) fn members<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Vec> { + let own = own_class_member_names(db, env, ty); - let mut members: Vec<_> = all_members(db, ty) + let mut members: Vec<_> = all_members(db, env, ty) .into_iter() // a template can only write a `\w+` name after a dot, so a dunder is both // unreachable and noise @@ -270,7 +286,7 @@ pub(crate) fn members<'db>(db: &'db dyn Db, ty: Type<'db>) -> Vec> { /// the type of the python expression at `range` of `file` pub(crate) fn expression_type(db: &dyn Db, file: File, range: TextRange) -> Option> { - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, db.program_file(file).python_file(db)).load(db); let covering = covering_node(parsed.syntax().into(), range); // the smallest node covering the range must be the expression itself; a @@ -280,5 +296,5 @@ pub(crate) fn expression_type(db: &dyn Db, file: File, range: TextRange) -> Opti } let expression = covering.node().as_expr_ref()?; - expression.inferred_type(&SemanticModel::new(db, file)) + expression.inferred_type(&SemanticModel::new(db, db.program_file(file))) } diff --git a/crates/ty_ide/src/django_template/routes.rs b/crates/ty_ide/src/django_template/routes.rs index ac039184fe..114913a0d1 100644 --- a/crates/ty_ide/src/django_template/routes.rs +++ b/crates/ty_ide/src/django_template/routes.rs @@ -34,6 +34,7 @@ use ty_python_semantic::types::ide_support::{ use ty_python_semantic::{HasType, SemanticModel}; use super::project::{self, Parameter, RouteView, TargetKind, UrlName}; +use ty_python_semantic::ProgramEnvironment; /// the methods a django class-based view serves a request through /// @@ -50,7 +51,11 @@ const HANDLER_METHODS: &[&str] = &[ /// nothing is reported unless the url tree was walked in full: a route's /// arguments include the ones contributed by every pattern it is mounted behind, /// and a walk that stopped short has read only some of them. -pub(crate) fn diagnostics(db: &dyn Db, file: File) -> Vec { +pub(crate) fn diagnostics( + db: &dyn Db, + env: &ProgramEnvironment<'_>, + file: File, +) -> Vec { if !project::routes_are_authoritative(db, db.project()) { return Vec::new(); } @@ -61,7 +66,7 @@ pub(crate) fn diagnostics(db: &dyn Db, file: File) -> Vec { .iter() .filter(|route| route.file == file) { - check_route(db, file, route, &mut found); + check_route(db, env, file, route, &mut found); } found.sort_by_key(|diagnostic| { @@ -75,15 +80,21 @@ pub(crate) fn diagnostics(db: &dyn Db, file: File) -> Vec { } /// check one route against every handler it reaches -fn check_route(db: &dyn Db, file: File, route: &UrlName, found: &mut Vec) { +fn check_route( + db: &dyn Db, + env: &ProgramEnvironment<'_>, + file: File, + route: &UrlName, + found: &mut Vec, +) { // a view nothing could resolve to a definition, and a pattern this could not // read in full, are both routes there is nothing to say about let (Some(view), Some(parameters)) = (route.view.as_ref(), route.parameters()) else { return; }; - for handler in handlers(db, view) { - for complaint in handler.complaints(db, ¶meters, route.extra_arguments) { + for handler in handlers(db, env, view) { + for complaint in handler.complaints(db, env, ¶meters, route.extra_arguments) { report(db, file, view, &handler, &complaint, found); } } @@ -132,8 +143,12 @@ fn report( /// hierarchy it is declared. what a project inherits from django needs no /// exception: every handler django ships takes `**kwargs`, and a `**kwargs` /// silences the check on its own. -fn handlers<'db>(db: &'db dyn Db, view: &RouteView) -> Vec> { - let parsed = parsed_module(db, view.target.file).load(db); +fn handlers<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + view: &RouteView, +) -> Vec> { + let parsed = parsed_module(db, db.program_file(view.target.file).python_file(db)).load(db); let declaration = parsed .suite() @@ -142,14 +157,14 @@ fn handlers<'db>(db: &'db dyn Db, view: &RouteView) -> Vec> { match (view.target.kind, declaration) { (TargetKind::Function, Some(Declaration::Function(function))) => { - Handler::of(db, view.target.file, function, None) + Handler::of(db, env, view.target.file, function, None) .into_iter() .collect() } (TargetKind::Class, Some(Declaration::Class(class))) if view.class_based => HANDLER_METHODS .iter() .filter_map(|method| { - match declared_handler(db, view.target.file, class, method, MAX_BASE_DEPTH)? { + match declared_handler(db, env, view.target.file, class, method, MAX_BASE_DEPTH)? { Declared::Handler(handler) => Some(handler), Declared::Anything => None, } @@ -186,6 +201,7 @@ enum Declared<'db> { /// declares the method, and what a search cannot see it must not report around. fn declared_handler<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, class: &ast::StmtClassDef, method: &str, @@ -196,7 +212,7 @@ fn declared_handler<'db>( _ => None, }) { return Some( - match Handler::of(db, file, function, Some(class.name.id.as_str())) { + match Handler::of(db, env, file, function, Some(class.name.id.as_str())) { Some(handler) => Declared::Handler(handler), None => Declared::Anything, }, @@ -209,7 +225,7 @@ fn declared_handler<'db>( for base in class.bases() { let followed = project::resolved_class(db, file, base, |defining, base_class| { - declared_handler(db, defining, base_class, method, depth - 1) + declared_handler(db, env, defining, base_class, method, depth - 1) }); match followed { @@ -270,12 +286,13 @@ impl<'db> Handler<'db> { /// type checker cannot see through leaves a signature this refuses to read. fn of( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, function: &ast::StmtFunctionDef, class: Option<&str>, ) -> Option { - let declared = function.inferred_type(&SemanticModel::new(db, file))?; - let mut parameters = callable_parameters(db, declared)?; + let declared = function.inferred_type(&SemanticModel::new(db, db.program_file(file)))?; + let mut parameters = callable_parameters(db, env, declared)?; // a method is called through the instance, which fills its receiver if class.is_some() && !parameters.is_empty() { @@ -319,6 +336,7 @@ impl<'db> Handler<'db> { fn complaints( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, parameters: &[Parameter], extra_arguments: bool, ) -> Vec { @@ -343,20 +361,24 @@ impl<'db> Handler<'db> { else { continue; }; - let Some(value) = parameter.value_type(db) else { + let Some(value) = parameter.value_type(db, env) else { continue; }; - if value.is_assignable_to(db, declared) { + if value.is_assignable_to(db, env, declared) { continue; } complaints.push(Complaint { kind: ComplaintKind::ParameterType, - message: format!("takes `{}` as `{}`", parameter.name, declared.display(db)), + message: format!( + "takes `{}` as `{}`", + parameter.name, + declared.display(db, env) + ), help: format!( "the route's `{}` converter gives a `{}`", converter.name(), - value.display(db) + value.display(db, env) ), }); } diff --git a/crates/ty_ide/src/django_template/signature_help.rs b/crates/ty_ide/src/django_template/signature_help.rs index cffbbe161e..5d00b8d284 100644 --- a/crates/ty_ide/src/django_template/signature_help.rs +++ b/crates/ty_ide/src/django_template/signature_help.rs @@ -24,6 +24,7 @@ use super::builtins; use super::index::TemplateIndex; use super::lexer::{ConstructKind, Token, TokenKind}; use super::project::{self, Registration, RegistrationKind}; +use ty_python_semantic::ProgramEnvironment; /// what django names the flag it fills in itself, on the decorator and on the /// parameter it fills @@ -45,6 +46,7 @@ pub struct TemplateSignature { /// what the filter whose argument `offset` sits in takes pub(crate) fn signature_help( db: &dyn Db, + env: &ProgramEnvironment<'_>, index: &TemplateIndex, source: &str, offset: TextSize, @@ -56,7 +58,7 @@ pub(crate) fn signature_help( let name = filter_argument_at(source, index.lexed().construct_tokens(construct), offset)?; - signature(db, name) + signature(db, env, name) } /// the filter whose argument `offset` sits in @@ -105,7 +107,7 @@ fn is_operator(source: &str, token: &Token, operator: &str) -> bool { } /// what the filter `name` takes -fn signature(db: &dyn Db, name: &str) -> Option { +fn signature(db: &dyn Db, env: &ProgramEnvironment<'_>, name: &str) -> Option { let registered = filter_registration(db, name); // as for hover: the table documents django's own filters, but which of them @@ -132,7 +134,7 @@ fn signature(db: &dyn Db, name: &str) -> Option { // a function taking only the value takes no argument, and a filter that takes // no argument has nothing to say about the one that was written - let parameter = argument_parameter(db, registration)?; + let parameter = argument_parameter(db, env, registration)?; Some(TemplateSignature { label: format!("|{name}:{parameter}"), @@ -157,8 +159,12 @@ fn filter_registration<'db>(db: &'db dyn Db, name: &str) -> Option<&'db Registra /// filters annotate almost none of theirs, and what an unannotated parameter /// infers to says nothing the reader wants: `def date(value, arg=None)` would put /// `arg: Unknown | None` in front of somebody looking for a format string. -fn argument_parameter(db: &dyn Db, registration: &Registration) -> Option { - let parsed = parsed_module(db, registration.file).load(db); +fn argument_parameter( + db: &dyn Db, + env: &ProgramEnvironment<'_>, + registration: &Registration, +) -> Option { + let parsed = parsed_module(db, db.program_file(registration.file).python_file(db)).load(db); let covering = covering_node(parsed.syntax().into(), registration.range) .find_first(|node| node.is_stmt_function_def()) .ok()?; @@ -178,10 +184,10 @@ fn argument_parameter(db: &dyn Db, registration: &Registration) -> Option Some(format!("{name}: {}", ty.display(db))), + Some(ty) => Some(format!("{name}: {}", ty.display(db, env))), None => Some(name.to_string()), } } diff --git a/crates/ty_ide/src/django_template/uses.rs b/crates/ty_ide/src/django_template/uses.rs index 0fdac214a0..9dc9cc7910 100644 --- a/crates/ty_ide/src/django_template/uses.rs +++ b/crates/ty_ide/src/django_template/uses.rs @@ -259,7 +259,7 @@ fn at_python(db: &dyn Db, file: File, offset: TextSize) -> Option<(Named, TextRa return Some((Named::RouteDeclaration, url.range)); } - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, db.program_file(file).python_file(db)).load(db); let (names, value, range) = super::python::name_at(&parsed, offset)?; Some(match names { @@ -945,7 +945,7 @@ fn templates(db: &dyn Db) -> Vec> { /// the value and contents range of the python string literal spanning `range` fn python_literal(db: &dyn Db, file: File, range: TextRange) -> Option<(CompactString, TextRange)> { - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, db.program_file(file).python_file(db)).load(db); let covering = covering_node(parsed.syntax().into(), range); let string = covering.ancestors().find_map(|node| match node { diff --git a/crates/ty_ide/src/doc_highlights.rs b/crates/ty_ide/src/doc_highlights.rs index 66375681e0..5f3116ecc4 100644 --- a/crates/ty_ide/src/doc_highlights.rs +++ b/crates/ty_ide/src/doc_highlights.rs @@ -1,18 +1,18 @@ use crate::goto::find_goto_target; use crate::references::{ReferencesMode, references}; use crate::{Db, ReferenceTarget}; -use ruff_db::files::File; use ruff_text_size::TextSize; +use ty_python_core::ProgramFile; use ty_python_semantic::SemanticModel; /// Find all document highlights for a symbol at the given position. /// Document highlights are limited to the current file only. pub fn document_highlights( db: &dyn Db, - file: File, + file: ProgramFile<'_>, offset: TextSize, ) -> Option> { - let parsed = ruff_db::parsed::parsed_module(db, file); + let parsed = ruff_db::parsed::parsed_module(db, file.python_file(db)); let module = parsed.load(db); let model = SemanticModel::new(db, file); @@ -34,9 +34,11 @@ mod tests { impl CursorTest { fn document_highlights(&self) -> String { - let Some(highlight_results) = - document_highlights(&self.db, self.cursor.file, self.cursor.offset) - else { + let Some(highlight_results) = document_highlights( + &self.db, + self.program_file(self.cursor.file), + self.cursor.offset, + ) else { return "No highlights found".to_string(); }; @@ -72,14 +74,12 @@ mod tests { | 1 | from . import module_a | ^^^^^^^^ - | info[document_highlights]: Highlight 2 (Read) --> mypackage/__init__.py:2:5 | 2 | x = module_a | ^^^^^^^^ - | "); } @@ -127,28 +127,24 @@ def calculate_sum(): | 3 | value = 10 | ^^^^^ - | info[document_highlights]: Highlight 2 (Read) --> main.py:4:15 | 4 | doubled = value * 2 | ^^^^^ - | info[document_highlights]: Highlight 3 (Read) --> main.py:5:14 | 5 | result = value + doubled | ^^^^^ - | info[document_highlights]: Highlight 4 (Read) --> main.py:6:12 | 6 | return value | ^^^^^ - | "); } @@ -170,28 +166,24 @@ def process_data(data): | 2 | def process_data(data): | ^^^^ - | info[document_highlights]: Highlight 2 (Read) --> main.py:3:8 | 3 | if data: | ^^^^ - | info[document_highlights]: Highlight 3 (Read) --> main.py:4:21 | 4 | processed = data.upper() | ^^^^ - | info[document_highlights]: Highlight 4 (Read) --> main.py:6:12 | 6 | return data | ^^^^ - | "); } @@ -213,14 +205,12 @@ calc = Calculator() | 2 | class Calculator: | ^^^^^^^^^^ - | info[document_highlights]: Highlight 2 (Read) --> main.py:6:8 | 6 | calc = Calculator() | ^^^^^^^^^^ - | "); } @@ -259,21 +249,18 @@ def test(): | 2 | a: str = "test" | ^ - | info[document_highlights]: Highlight 2 (Write) --> main.py:4:1 | 4 | a: int = 10 | ^ - | info[document_highlights]: Highlight 3 (Read) --> main.py:6:7 | 6 | print(a) | ^ - | "#); } } diff --git a/crates/ty_ide/src/docstring.rs b/crates/ty_ide/src/docstring.rs index f61a733138..ca4f0ae042 100644 --- a/crates/ty_ide/src/docstring.rs +++ b/crates/ty_ide/src/docstring.rs @@ -9,20 +9,10 @@ mod document; mod markdown; -use indexmap::IndexMap; -use regex::Regex; use ruff_python_trivia::{PythonWhitespace, expand_tabs, leading_indentation}; use ruff_source_file::UniversalNewlines; -use std::sync::LazyLock; -use crate::MarkupKind; - -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")); +use crate::{FxIndexMap, MarkupKind}; /// A docstring which hasn't yet been interpreted or rendered /// @@ -57,9 +47,9 @@ impl Docstring { /// Extract parameter documentation from popular docstring formats. /// Returns a map of parameter names to their documentation. - pub fn parameter_documentation(&self) -> IndexMap { + pub fn parameter_documentation(&self) -> FxIndexMap { let normalized_source = documentation_trim(&self.0); - document::parameter_documentation(&normalized_source, extract_numpy_style_params(&self.0)) + document::parameter_documentation(&normalized_source) } } @@ -156,183 +146,6 @@ fn documentation_trim(docs: &str) -> String { output } -/// Calculate the indentation level of a line. -/// -/// Based on python's expandtabs (where tabs are considered 8 spaces). -fn get_indentation_level(line: &str) -> usize { - leading_indentation(line) - .chars() - .map(|s| if s == '\t' { 8 } else { 1 }) - .sum() -} - -/// Extract parameter documentation from NumPy-style docstrings. -fn extract_numpy_style_params(docstring: &str) -> IndexMap { - let mut param_docs = IndexMap::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 -} - #[cfg(test)] mod tests { use insta::Settings; @@ -1363,22 +1176,22 @@ Summary. "); assert_snapshot!(docstring.render_markdown(), @" - 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 + 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 "); } @@ -1511,10 +1324,9 @@ Summary. **param2**: `int` Another Google-style parameter - Parameters - ---------- - param3 : bool -     NumPy-style parameter + ## Parameters + **param3**: `bool` + NumPy-style parameter "); } @@ -1675,12 +1487,12 @@ Summary. **param3** Another reST-style parameter - Parameters - ---------- - param3 : str -     NumPy-style duplicate parameter - param4 : bool -     NumPy-style parameter + ## Parameters + **param3**: `str` + NumPy-style duplicate parameter + + **param4**: `bool` + NumPy-style parameter "); } @@ -1734,17 +1546,18 @@ Summary. "); assert_snapshot!(docstring.render_markdown(), @" - 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 + 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 "); } diff --git a/crates/ty_ide/src/docstring/document.rs b/crates/ty_ide/src/docstring/document.rs index 3a233e222d..ddb6635384 100644 --- a/crates/ty_ide/src/docstring/document.rs +++ b/crates/ty_ide/src/docstring/document.rs @@ -1,10 +1,11 @@ -use indexmap::IndexMap; use ruff_text_size::{TextRange, TextSize}; use strum_macros::EnumIter; use self::syntax::{indentation, starts_with_markdown_list_item}; +use crate::FxIndexMap; pub(super) mod google; +pub(super) mod numpy; pub(super) mod preformatted; pub(super) mod rst; pub(in crate::docstring) mod syntax; @@ -13,12 +14,9 @@ pub(in crate::docstring) mod syntax; /// /// `normalized_source` must have already undergone PEP-257 trimming and universal newline /// normalization. -pub(super) fn parameter_documentation( - normalized_source: &str, - numpy_parameters: IndexMap, -) -> IndexMap { +pub(super) fn parameter_documentation(normalized_source: &str) -> FxIndexMap { let mut parameters = google::parameter_documentation(normalized_source); - parameters.extend(numpy_parameters); + parameters.extend(numpy::parameter_documentation(normalized_source)); parameters.extend(rst::parameter_documentation(normalized_source)); parameters } diff --git a/crates/ty_ide/src/docstring/document/google.rs b/crates/ty_ide/src/docstring/document/google.rs index 85322bffbf..85e1700d30 100644 --- a/crates/ty_ide/src/docstring/document/google.rs +++ b/crates/ty_ide/src/docstring/document/google.rs @@ -30,23 +30,24 @@ //! retries: Number of retries. //! ``` -use indexmap::IndexMap; use ruff_python_stdlib::identifiers::is_identifier; use ruff_python_trivia::Cursor; -use ruff_text_size::{TextRange, TextSize}; +use ruff_text_size::{Ranged, TextRange, TextSize}; use super::preformatted::PreformattedBlockScanner; use super::syntax::{ - ParsedLine, consume_quoted_string, container_block_end, indentation, is_dotted_identifier, - parsed_lines, split_once_at_top_level_colon, split_trailing_parenthetical, + InlineMarkupScanner, InlineMarkupToken, ParsedLine, consume_quoted_string, container_block_end, + indentation, is_dotted_identifier, parsed_lines, split_once_at_top_level_colon, + split_trailing_parenthetical, }; use super::{DescriptionBuilder, HeaderKind, SectionKind}; +use crate::FxIndexMap; /// Returns parameter documentation from recognized Google-style parameter sections. /// /// `normalized_source` must have already undergone PEP-257 trimming and universal newline /// normalization. -pub(super) fn parameter_documentation(normalized_source: &str) -> IndexMap { +pub(super) fn parameter_documentation(normalized_source: &str) -> FxIndexMap { let mut parameters = Parameters::default(); for section in sections(normalized_source) { let Section { @@ -184,7 +185,7 @@ impl<'a> ParameterDisplayName<'a> { } #[derive(Default)] -struct Parameters(IndexMap); +struct Parameters(FxIndexMap); impl Parameters { fn extend_fragments(&mut self, fragments: Vec) { @@ -209,7 +210,7 @@ impl Parameters { } } - fn into_inner(self) -> IndexMap { + fn into_inner(self) -> FxIndexMap { self.0 } } @@ -685,12 +686,18 @@ impl<'a> ItemLine<'a> { item_indent: TextSize, ) -> bool { // More deeply indented lines are unambiguously part of the current item. - line_indent > item_indent - // Although the style guide suggests indenting continuation lines, - // aligned parameter prose is common in practice. - || (line_indent == item_indent && section_kind.is_parameter_section()) - // Aligned URLs and paths are continuations despite resembling item headers. - || (line_indent == item_indent && self.is_item_like_continuation) + if line_indent > item_indent { + return true; + } + + // Although the style guide suggests indenting continuation lines, + // aligned parameter prose is common in practice. + if line_indent == item_indent && section_kind.is_parameter_section() { + return true; + } + + // Aligned URLs and paths are continuations despite resembling item headers. + line_indent == item_indent && self.is_item_like_continuation } fn classify( @@ -847,42 +854,14 @@ fn split_once_at_field_delimiter(line: &str) -> Option<(&str, &str)> { /// :exc:`ValueError` /// ``` fn consume_rest_prefix_role(cursor: &mut Cursor<'_>) -> bool { - let mut role = cursor.clone(); - - // First, require the candidate delimiter to be the opening colon of a role. - if !role.eat_char(':') { - return false; - } - - // Role names start with a Unicode alphanumeric run. Rejecting punctuation here preserves the - // first colon in `value::class:` as the field delimiter. - if !role.eat_if(char::is_alphanumeric) { + let Some(InlineMarkupToken::RestPrefixRole(role)) = + InlineMarkupScanner::new(cursor.as_str()).next() + else { return false; - } - - // Next, scan the rest of the role name until its closing colon and the opening content - // backtick. - loop { - role.eat_while(char::is_alphanumeric); - if role.eat_char2(':', '`') { - break; - } - - // `-._+:` separators are allowed, but only internally to alphanumeric characters. - if !role.eat_if(|character| matches!(character, '-' | '.' | '_' | '+' | ':')) - || !role.eat_if(char::is_alphanumeric) - { - return false; - } - } - - // Finally, skip the role content so delimiter scanning resumes after its closing backtick. - role.eat_while(|character| character != '`'); - if !role.eat_char('`') { - return false; - } + }; - *cursor = role; + // Resume delimiter scanning after the closing backtick in e.g., `` :exc:`ValueError` ``. + cursor.skip_bytes(role.span().end().to_usize()); true } @@ -1614,6 +1593,7 @@ Returns: for (line, description) in [ ("value:foo..bar:`X`", "foo..bar:`X`"), ("value:foo-:`X`", "foo-:`X`"), + ("value:class:``X``", "class:``X``"), ] { assert_eq!( split_once_at_field_delimiter(line), diff --git a/crates/ty_ide/src/docstring/document/numpy.rs b/crates/ty_ide/src/docstring/document/numpy.rs new file mode 100644 index 0000000000..eea746546d --- /dev/null +++ b/crates/ty_ide/src/docstring/document/numpy.rs @@ -0,0 +1,1382 @@ +//! Parsing for NumPy-style docstring sections. +//! +//! The [numpydoc style guide](https://numpydoc.readthedocs.io/en/latest/format.html) +//! organizes documentation into sections whose headings are underlined with hyphens. Item-oriented +//! sections conventionally use a `name : type` line followed by an indented description. This +//! parser recognizes `Parameters`, `Other Parameters`, `Attributes`, `Returns`, `Yields`, and +//! `Raises`. +//! +//! Example: +//! +//! ```text +//! Compute the mean of a sequence. +//! +//! Parameters +//! ---------- +//! values : sequence of float +//! Values to average. +//! axis : int, optional +//! Axis along which to compute the mean. +//! +//! Returns +//! ------- +//! float +//! The arithmetic mean. +//! ``` + +use ruff_text_size::{TextRange, TextSize}; + +use super::preformatted::{PreformattedBlockScanner, starts_preformatted_block}; +use super::syntax::{ + ParsedLine, container_block_end, is_dotted_identifier, is_wrapped_in_markdown_code_span, + parsed_lines, split_once_at_top_level_colon, starts_container_block, +}; +use super::{DescriptionBuilder, HeaderKind, SectionKind}; +use crate::FxIndexMap; + +/// Returns parameter documentation from recognized NumPy-style parameter sections. +/// +/// `normalized_source` must have already undergone PEP-257 trimming and universal newline +/// normalization. +pub(super) fn parameter_documentation(normalized_source: &str) -> FxIndexMap { + let mut parameters = Parameters::default(); + + for section in sections(normalized_source) { + let Section { + kind, + range: _, + body, + } = section; + if matches!(kind, SectionKind::Parameters | SectionKind::OtherParameters) { + parameters.extend_fragments(body.into_fragments()); + } + } + + parameters.into_inner() +} + +#[derive(Default)] +struct Parameters(FxIndexMap); + +impl Parameters { + fn extend_fragments(&mut self, fragments: Vec) { + for fragment in fragments { + let BodyFragment::Item(item) = fragment else { + continue; + }; + let Item { + display_name, + ty: _, + description, + } = item; + let Some(display_name) = display_name else { + continue; + }; + let description = description.trim(); + if description.is_empty() { + continue; + } + let Some(names) = parameter_lookup_names(&display_name) else { + continue; + }; + for name in names { + self.0.insert(name, description.to_string()); + } + } + } + + fn into_inner(self) -> FxIndexMap { + self.0 + } +} + +fn parameter_lookup_names(display_name: &str) -> Option> { + let mut lookup_names = Vec::new(); + for name in display_name.split(',').map(str::trim) { + if name == "..." { + continue; + } + + if !is_item_name_part(name) { + return None; + } + lookup_names.push(name.to_string()); + } + + (!lookup_names.is_empty()).then_some(lookup_names) +} + +/// Returns recognized NumPy-style sections in source order. +/// +/// `source` must have already undergone PEP-257 trimming and universal newline normalization +/// (typically via `docstring::documentation_trim`). +pub(in crate::docstring) fn sections(source: &str) -> Vec
{ + Parser::new(parsed_lines(source)).parse() +} + +/// A recognized NumPy-style docstring section. +pub(in crate::docstring) type Section = super::Section>; + +type SectionBody = super::SectionBody>; + +/// One parsed fragment in a NumPy section body. +pub(in crate::docstring) type BodyFragment = super::BodyFragment>; + +/// A named or anonymous item in a NumPy section. +type Item = super::Item>; + +struct Parser<'a> { + lines: Vec>, + current_line: usize, + sections: Vec
, + current_section: Option>, + scanner: PreformattedBlockScanner<'a>, +} + +impl<'a> Parser<'a> { + fn new(lines: Vec>) -> Self { + Self { + lines, + current_line: 0, + sections: Vec::new(), + current_section: None, + scanner: PreformattedBlockScanner::default(), + } + } + + fn parse(mut self) -> Vec
{ + while self.current_line < self.lines.len() { + self.parse_line(); + } + + if let Some(section) = self.current_section.take() { + self.finish_section(section); + } + + self.sections + } + + fn parse_line(&mut self) { + let line = self.lines[self.current_line]; + let line_header = self.parse_header(self.current_line); + let index = self.current_line; + self.current_line += 1; + + // First, attempt to add the current line to the current section. + if let Some(mut section) = self.current_section.take() { + if section.push_line(line, line_header, &self.lines[self.current_line..]) { + self.current_section = Some(section); + return; + } + + self.finish_section(section); + } + + // Second, skip content owned by a preformatted or container block, where nested headers + // are inert. + if self.scanner.consume_preformatted_line(line.text) { + return; + } + if let Some(end) = container_block_end(&self.lines, index) { + self.current_line = end; + return; + } + + // Finally, start a new section from a standalone header, or observe syntax that may + // introduce a preformatted block. + if let Some(header) = line_header { + self.current_section = Some(SectionBuilder::new(header)); + self.current_line += 1; + } else { + self.scanner + .observe_line_outside_preformatted_block(line.text); + } + } + + fn parse_header(&self, index: usize) -> Option
{ + let line = self.lines[index]; + let underline = self.lines.get(index + 1)?; + + if line.text.trim().is_empty() || !is_underline(underline.text) { + return None; + } + + let indent = if index == 0 { + // PEP 257 trimming strips the indentation from the first line, + // so instead use the underline to determine this section's indentation. + underline.indent + } else if underline.indent == line.indent { + line.indent + } else { + // After the first line, each underline must align with its section title. + return None; + }; + + Some(Header { + kind: section_kind(line.text) + .map(HeaderKind::Structured) + .unwrap_or(HeaderKind::Opaque), + indent, + range: TextRange::new(line.range.start(), underline.range.end()), + }) + } + + fn finish_section(&mut self, section: SectionBuilder<'a>) { + if let Some(section) = section.finish() { + self.sections.push(section); + } + } +} + +struct SectionBuilder<'a> { + section_header: Header, + range: TextRange, + pending_blank_lines: Vec>, + preformatted: PreformattedBlockScanner<'a>, + has_seen_item_block: bool, + body: BodyBuilder<'a>, +} + +impl<'a> SectionBuilder<'a> { + fn new(section_header: Header) -> Self { + Self { + range: section_header.range, + pending_blank_lines: Vec::new(), + preformatted: PreformattedBlockScanner::default(), + has_seen_item_block: false, + body: BodyBuilder::new(section_header.kind, section_header.indent), + section_header, + } + } + + /// Returns `false` when `line` belongs outside this section. + fn push_line( + &mut self, + line: ParsedLine<'a>, + line_header: Option
, + following_lines: &[ParsedLine<'_>], + ) -> bool { + // Let an active preformatted block consume the line before interpreting it. + let preformatted_block_is_active = self.preformatted.is_active(); + let line_is_preformatted = self.preformatted.consume_preformatted_line(line.text); + if preformatted_block_is_active && line_is_preformatted { + self.push_body_line(line, None); + return true; + } + + // Defer blank lines until the next content line determines their ownership. + if line.text.trim().is_empty() { + self.pending_blank_lines.push(line); + return true; + } + + // Omit a marker for a static substitution from extracted parameter + // documentation, but keep scanning explicit parameters and leave the + // section raw when rendering. + // + // This is an edge case, but static substitutions commonly appear in + // some popular libraries (e.g., SciPy and Matplotlib). + if self.section_header.kind.is_parameter_section() + && line_header.is_none() + && !line_is_preformatted + && is_static_substitution(line, self.section_header.indent) + { + self.push_static_substitution(line); + return true; + } + + // Parse the line as an item and determine whether it belongs to this section. + let item_line = ItemLine::parse(self.section_header, line, following_lines); + let starts_item_block = item_line.is_some(); + let has_leading_blank_lines = !self.pending_blank_lines.is_empty(); + if !self.line_belongs_to_section( + line, + line_header, + starts_item_block, + has_leading_blank_lines, + ) { + return false; + } + + // Finally, commit the accepted line and update the state used to classify later lines. + self.push_body_line(line, item_line); + self.has_seen_item_block |= starts_item_block; + if !line_is_preformatted { + self.preformatted + .observe_line_outside_preformatted_block(line.text); + } + + true + } + + fn line_belongs_to_section( + &self, + line: ParsedLine<'_>, + line_header: Option
, + starts_item_block: bool, + has_leading_blank_lines: bool, + ) -> bool { + // A sibling-level underlined header starts a new section. + // Every section, including an opaque one, ends at a sibling or shallower header. + if line_header.is_some_and(|header| header.indent <= self.section_header.indent) { + return false; + } + + // Items are not parsed in opaque sections so only the above header can end them. + if self.section_header.kind == HeaderKind::Opaque { + return true; + } + + match line.indent.cmp(&self.section_header.indent) { + std::cmp::Ordering::Less => false, + std::cmp::Ordering::Greater => true, + std::cmp::Ordering::Equal => { + if self.section_header.kind.is_parameter_section() { + // Parameter sections may contain leading prose and aligned continuations. + // After an item establishes the list, a blank line followed by an aligned + // non-item ends the section. + !self.has_seen_item_block || !has_leading_blank_lines || starts_item_block + } else { + starts_item_block + } + } + } + } + + fn push_static_substitution(&mut self, line: ParsedLine<'a>) { + self.commit_pending_blank_lines(); + self.range = self.range.cover(line.range); + + if let BodyBuilder::ItemList(builder) = &mut self.body { + // At item indentation, the substitution may expand into more items, so end the + // preceding item. An indented substitution remains within the current description. + if line.indent == self.section_header.indent { + builder.finish_current_item(); + self.has_seen_item_block = true; + } + + // The unknown expansion cannot be reproduced by structured rendering. + builder.has_structural_ambiguity = true; + } + } + + fn commit_pending_blank_lines(&mut self) { + for line in self.pending_blank_lines.drain(..) { + self.range = self.range.cover(line.range); + self.body.push_blank_line(); + } + } + + fn push_body_line(&mut self, line: ParsedLine<'a>, item_line: Option>) { + self.commit_pending_blank_lines(); + self.range = self.range.cover(line.range); + self.body.push_line(line, item_line); + } + + fn finish(self) -> Option
{ + let HeaderKind::Structured(kind) = self.section_header.kind else { + return None; + }; + + Some(Section { + kind, + range: self.range, + body: self.body.finish(), + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct Header { + kind: HeaderKind, + indent: TextSize, + range: TextRange, +} + +fn section_kind(line: &str) -> Option { + match line.trim().to_ascii_lowercase().as_str() { + "parameters" => Some(SectionKind::Parameters), + "other parameters" => Some(SectionKind::OtherParameters), + "attributes" => Some(SectionKind::Attributes), + "returns" => Some(SectionKind::Returns), + "yields" => Some(SectionKind::Yields), + "raises" => Some(SectionKind::Raises), + _ => None, + } +} + +fn is_underline(line: &str) -> bool { + let line = line.trim(); + line.len() >= 3 && line.chars().all(|char| char == '-') +} + +/// Recognizes standalone percent- and dollar-style substitutions. +/// +/// Percent substitutions may use any nonempty name without parentheses: +/// +/// ```python +/// "%(name)s" +/// "%(Class:kwdoc)s" +/// ``` +/// +/// Dollar substitutions require a dotted Python identifier: +/// +/// ```python +/// "$name" +/// "${package.name}" +/// ``` +fn is_static_substitution(line: ParsedLine<'_>, section_indent: TextSize) -> bool { + let text = line.text.trim(); + let is_percent_marker = text + .strip_prefix("%(") + .and_then(|line| line.strip_suffix(")s")) + .is_some_and(|name| !name.is_empty() && !name.contains('(') && !name.contains(')')); + let is_dollar_marker = text + .strip_prefix("${") + .and_then(|line| line.strip_suffix('}')) + .or_else(|| text.strip_prefix('$')) + .is_some_and(is_dotted_identifier); + + line.indent >= section_indent && (is_percent_marker || is_dollar_marker) +} + +/// Accepts description-backed items, plus single-token types without one. +fn is_anonymous_return_item(line: &str, has_description: bool) -> bool { + !line.is_empty() + && !line.ends_with(['.', ':']) + && (has_description || !line.chars().any(char::is_whitespace)) +} + +enum BodyBuilder<'a> { + /// A recognized section whose body consists of named items and their descriptions. + ItemList(ItemListBuilder<'a>), + /// An underlined section that participates in boundary detection but is not parsed. + Opaque, +} + +impl<'a> BodyBuilder<'a> { + fn new(kind: HeaderKind, required_item_indent: TextSize) -> Self { + match kind { + HeaderKind::Structured(_) => Self::ItemList(ItemListBuilder::new( + kind.is_parameter_section(), + required_item_indent, + )), + HeaderKind::Opaque => Self::Opaque, + } + } + + fn push_blank_line(&mut self) { + if let Self::ItemList(builder) = self { + builder.push_blank_line(); + } + } + + fn push_line(&mut self, line: ParsedLine<'a>, item_line: Option>) { + if let Self::ItemList(builder) = self { + builder.push_line(line, item_line); + } + } + + fn finish(self) -> SectionBody { + match self { + Self::ItemList(builder) => builder.finish(), + Self::Opaque => SectionBody::Opaque, + } + } +} + +struct ItemListBuilder<'a> { + fragments: Vec, + current_item: Option>, + leading_prose: DescriptionBuilder<'a>, + required_item_indent: TextSize, + preserve_leading_prose: bool, + has_structural_ambiguity: bool, +} + +impl<'a> ItemListBuilder<'a> { + fn new(preserve_leading_prose: bool, required_item_indent: TextSize) -> Self { + Self { + fragments: Vec::new(), + current_item: None, + leading_prose: DescriptionBuilder::default(), + required_item_indent, + preserve_leading_prose, + has_structural_ambiguity: false, + } + } + + fn push_blank_line(&mut self) { + if let Some(item) = &mut self.current_item { + item.description.push_continuation(""); + } else if self.preserve_leading_prose { + self.leading_prose.push_continuation(""); + } + } + + fn push_line(&mut self, line: ParsedLine<'a>, item_line: Option>) { + let is_at_item_indent = line.indent == self.required_item_indent; + + // An item starts a new fragment; its description is collected from later lines. + if let Some(ItemLine { + item, + has_structural_ambiguity, + }) = item_line + { + self.finish_pending_fragments(); + self.current_item = Some(item); + self.has_structural_ambiguity |= has_structural_ambiguity; + return; + } + + // Record when preserving the remaining line as prose or a continuation loses structure. + if is_at_item_indent { + // Aligned prose after an item may instead be another, malformed item. + if self.current_item.is_some() { + self.has_structural_ambiguity = true; + } + } else if self.current_item.is_none() && self.preserve_leading_prose { + // Indented content before the first item may be nested content or code rather than + // section-level prose, so interpreting it as prose could discard meaningful structure. + self.has_structural_ambiguity = true; + } + + // Preserve the line as an item continuation or leading prose when supported. + if let Some(item) = &mut self.current_item { + item.description.push_continuation(line.text); + } else if self.preserve_leading_prose { + self.leading_prose.push_line(line.text); + } else { + self.has_structural_ambiguity = true; + } + } + + fn finish_pending_fragments(&mut self) { + self.finish_leading_prose(); + self.finish_current_item(); + } + + fn finish_leading_prose(&mut self) { + let prose = std::mem::take(&mut self.leading_prose).finish(); + if !prose.is_empty() { + self.fragments.push(BodyFragment::Prose(prose)); + } + } + + fn finish_current_item(&mut self) { + if let Some(item) = self.current_item.take() { + self.fragments.push(BodyFragment::Item(item.finish())); + } + } + + fn finish(mut self) -> SectionBody { + if self.preserve_leading_prose && self.current_item.is_none() && self.fragments.is_empty() { + return SectionBody::Opaque; + } + + self.finish_pending_fragments(); + SectionBody::Parsed { + fragments: self.fragments, + has_structural_ambiguity: self.has_structural_ambiguity, + } + } +} + +struct ItemLine<'a> { + item: ItemBuilder<'a>, + has_structural_ambiguity: bool, +} + +impl<'a> ItemLine<'a> { + fn parse( + section_header: Header, + line: ParsedLine<'a>, + following_lines: &[ParsedLine<'_>], + ) -> Option { + // Only aligned lines can start items. Other lines are prose or item continuations. + if line.indent != section_header.indent { + return None; + } + + // Each structured section has its own item grammar. Opaque sections only delimit content. + let HeaderKind::Structured(kind) = section_header.kind else { + return None; + }; + + match kind { + SectionKind::Parameters + | SectionKind::KeywordArguments + | SectionKind::OtherParameters + | SectionKind::Attributes => Self::parse_named_item(line, following_lines), + SectionKind::Returns | SectionKind::Yields => { + Self::parse_return_item(line, following_lines) + } + SectionKind::Raises => Self::parse_raise_item(line), + } + } + + fn parse_named_item(line: ParsedLine<'a>, following_lines: &[ParsedLine<'_>]) -> Option { + let text = line.text.trim(); + + let Some(separator) = parse_type_separator(text) else { + // Named items may omit their type. + return is_item_name(text) + .then(|| Self::new(ItemBuilder::new(Some(text), None, ""), false)); + }; + + let name_is_valid = is_item_name(separator.name); + let item = ItemBuilder::new(Some(separator.name), Some(separator.ty), ""); + + // Conventional `name : type` syntax establishes an item boundary even when the name is + // invalid, preventing it from absorbing adjacent items. + if separator.has_whitespace_before_colon { + return Some(Self::new(item, !name_is_valid)); + } + + // Compact syntax requires a valid name and either a type or description. + if name_is_valid + && (!separator.ty.is_empty() || has_indented_description(&line, following_lines)) + { + return Some(Self::new(item, separator.has_structural_ambiguity)); + } + + None + } + + fn parse_return_item(line: ParsedLine<'a>, following_lines: &[ParsedLine<'_>]) -> Option { + let text = line.text.trim(); + + // Block openers at item indentation belong outside the section, not to a return item. + if starts_preformatted_block(text) || starts_container_block(text) { + return None; + } + + // A complete code span is an anonymous type even when its contents contain a colon. + if is_wrapped_in_markdown_code_span(text) { + return Some(Self::new(ItemBuilder::new(None, Some(text), ""), false)); + } + + // Next, prefer the named `name : type` form. A colon adjacent to the name needs a + // description block to distinguish it from prose. + let Some(separator) = + parse_type_separator(text).filter(|separator| !separator.name.is_empty()) + else { + // Otherwise, accept an anonymous type only when its shape or description + // distinguishes it from prose. + let has_description = has_indented_description(&line, following_lines); + return is_anonymous_return_item(text, has_description) + .then(|| Self::new(ItemBuilder::new(None, Some(text), ""), false)); + }; + + let item = ItemBuilder::new(Some(separator.name), Some(separator.ty), ""); + + // Conventional `name : type` syntax is sufficient on its own. + if separator.has_whitespace_before_colon { + return Some(Self::new(item, separator.has_structural_ambiguity)); + } + + // A compact separator needs a description to distinguish it from prose. + has_indented_description(&line, following_lines) + .then(|| Self::new(item, separator.has_structural_ambiguity)) + } + + fn parse_raise_item(line: ParsedLine<'a>) -> Option { + let text = line.text.trim(); + + // Raises use a named item, with an optional inline description after the first colon. + let (name, description) = text + .split_once(':') + .map_or((text, ""), |(name, description)| { + (name.trim(), description.trim()) + }); + if !is_item_name(name) && !is_wrapped_in_markdown_code_span(name) { + return None; + } + + Some(Self::new( + ItemBuilder::new(Some(name), None, description), + false, + )) + } + + fn new(item: ItemBuilder<'a>, has_structural_ambiguity: bool) -> Self { + Self { + item, + has_structural_ambiguity, + } + } +} + +struct ItemBuilder<'a> { + display_name: Option<&'a str>, + ty: Option<&'a str>, + description: DescriptionBuilder<'a>, +} + +impl<'a> ItemBuilder<'a> { + fn new( + display_name: Option<&'a str>, + ty: Option<&'a str>, + inline_description: &'a str, + ) -> Self { + Self { + display_name, + ty, + description: DescriptionBuilder::with_inline(inline_description), + } + } + + fn finish(self) -> Item { + Item { + display_name: self.display_name.map(str::to_string), + ty: self.ty.map(str::to_string), + description: self.description.finish(), + } + } +} + +/// A parsed NumPy-style `name : type` separator. +struct TypeSeparator<'a> { + /// The documented item name. + name: &'a str, + /// The documented item type. + ty: &'a str, + /// Whether whitespace before the colon identifies conventional NumPy item syntax. + has_whitespace_before_colon: bool, + /// Whether the separator omits whitespace on both sides. + has_structural_ambiguity: bool, +} + +/// Parses a NumPy-style `name : type` separator. +fn parse_type_separator(line: &str) -> Option> { + let (name, ty) = split_once_at_top_level_colon(line)?; + let has_whitespace_before_colon = name.ends_with(char::is_whitespace); + let has_whitespace_after_colon = ty.starts_with(char::is_whitespace); + let has_structural_ambiguity = + !has_whitespace_before_colon && !has_whitespace_after_colon && !ty.is_empty(); + + Some(TypeSeparator { + name: name.trim(), + ty: ty.trim(), + has_whitespace_before_colon, + has_structural_ambiguity, + }) +} + +fn has_indented_description(line: &ParsedLine<'_>, following_lines: &[ParsedLine<'_>]) -> bool { + following_lines + .iter() + .find(|line| !line.text.trim().is_empty()) + .is_some_and(|next| next.indent > line.indent) +} + +/// Returns whether `name` is a valid NumPy-style item name or comma-separated name list. +fn is_item_name(name: &str) -> bool { + let mut has_lookup_name = false; + + for part in name.split(',') { + let part = part.trim(); + if part == "..." { + continue; + } + + if !is_item_name_part(part) { + return false; + } + + has_lookup_name = true; + } + + has_lookup_name +} + +fn is_item_name_part(name: &str) -> bool { + let name = name + .strip_prefix("**") + .or_else(|| name.strip_prefix('*')) + .unwrap_or(name); + + is_dotted_identifier(name) +} + +#[cfg(test)] +mod tests { + use insta::assert_snapshot; + use itertools::Itertools; + + use super::{BodyFragment, Item, SectionBody, parameter_documentation, sections}; + + #[test] + fn extracts_supported_numpy_parameter_items() { + let raw = r#" + This is a function description. + + Parameters + ---------- + param1 : str + The first parameter description + + This is a second paragraph. + This is a continuation of the first parameter description. + param2, param4, ... : int + The shared parameter description + param3 + A parameter without type annotation + *args : object + Extra positional arguments + **kwargs : object + Extra keyword arguments + options.mode : str + Nested field documentation + π : int + A Unicode parameter + override_repr: callable, optional + Replacement representation function + formats, names : + undocumented + copy : bool + Whether to copy the input + + Other Parameters + ---------------- + kw_only : str, optional + A less commonly used keyword-only parameter + "#; + + assert_snapshot!(display_parameters(raw), @" + param1: + │ The first parameter description + │ + │ This is a second paragraph. + │ This is a continuation of the first parameter description. + param2: + │ The shared parameter description + param4: + │ The shared parameter description + param3: + │ A parameter without type annotation + *args: + │ Extra positional arguments + **kwargs: + │ Extra keyword arguments + options.mode: + │ Nested field documentation + π: + │ A Unicode parameter + override_repr: + │ Replacement representation function + copy: + │ Whether to copy the input + kw_only: + │ A less commonly used keyword-only parameter + "); + } + + #[test] + fn uses_last_documentation_for_duplicate_parameter() { + let source = normalized( + r#" + Parameters + ---------- + value : str + First documentation. + value : str + Replacement documentation. + "#, + ); + + assert_eq!( + parameter_documentation(&source)["value"], + "Replacement documentation." + ); + } + + #[test] + fn extracts_shifted_top_level_numpy_sections() { + let raw = "\ +A decoded newline follows: +This line starts at column zero. + + Parameters + ---------- + shifted : int + Documentation in a shifted section. + + Returns + ------- + bool + Result."; + + assert_snapshot!(display_parameters(raw), @" + shifted: + │ Documentation in a shifted section. + "); + } + + #[test] + fn ignores_numpy_items_nested_in_section_preambles() { + let raw = "\ +Parameters +---------- +Choose one of the following. + nested : int + Example-only text. +beta : float + Useful documentation."; + + assert_snapshot!(display_parameters(raw), @" + beta: + │ Useful documentation. + "); + } + + #[test] + fn ignores_numpy_sections_in_containers() { + let raw = "\ +Summary. + +- Example data: + Parameters + ---------- + nested : int + Not parameter documentation."; + + assert_snapshot!(display_parameters(raw), @""); + } + + #[test] + fn ignores_numpy_sections_in_rest_literal_blocks() { + let raw = "\ +Summary. + +Example:: + + Other Parameters + ---------------- + nested : int + Literal content."; + + assert_snapshot!(display_parameters(raw), @""); + } + + #[test] + fn finds_numpy_section_after_first_line_rest_literal_block() { + let raw = "\ +Example:: + + sample output + + Parameters + ---------- + value : int + Parameter documentation."; + + assert_snapshot!(display_parameters(raw), @" + value: + │ Parameter documentation. + "); + } + + #[test] + fn ignores_numpy_sections_nested_in_other_sections() { + let raw = "\ +Examples +-------- + Parameters + ---------- + nested : int + Not parameter documentation. + +Notes +----- +More details. + +Parameters +---------- +value : int + Parameter documentation."; + + assert_snapshot!(display_parameters(raw), @" + value: + │ Parameter documentation. + "); + } + + #[test] + fn extracts_parameters_from_a_first_line_section() { + let raw = "\ +Parameters + ---------- + value : int + Description. + +Examples: + Example prose."; + + assert_snapshot!(display_parameters(raw), @" + value: + │ Description. + "); + + let source = normalized(raw); + assert!( + sections(&source) + .first() + .is_some_and(|section| &source[section.range] + == "\ +Parameters + ---------- + value : int + Description.") + ); + } + + #[test] + fn preserves_blank_lines_in_preformatted_parameter_descriptions() { + let raw = "\ +Parameters +---------- +value : str + ```text + first + + second + ``` +other : int + Another value."; + + assert_snapshot!(display_parameters(raw), @" + value: + │ ```text + │ first + │ + │ second + │ ``` + other: + │ Another value. + "); + } + + #[test] + fn leaves_misaligned_parameter_section_opaque() { + let raw = "\ +Parameters +---------- + value : int + Description. + other : str + Other."; + + let source = normalized(raw); + assert!( + sections(&source) + .first() + .is_some_and(|section| matches!(section.body, SectionBody::Opaque)) + ); + } + + #[test] + fn extracts_compact_parameters_without_rendering_them_structurally() { + let raw = "\ +Parameters +---------- +d:int + Parameter d."; + + assert_snapshot!(display_parameters(raw), @" + d: + │ Parameter d. + "); + + assert_section_is_structurally_ambiguous(raw); + } + + #[test] + fn leaves_indented_parameter_preambles_raw() { + let raw = "\ +Parameters +---------- +Choose one form. + foo() +beta : int + Useful documentation."; + + assert_snapshot!(display_parameters(raw), @" + beta: + │ Useful documentation. + "); + + assert_section_is_structurally_ambiguous(raw); + } + + #[test] + fn leaves_unconfirmed_parameter_item_opaque() { + let source = normalized( + "\ +Summary. + +Parameters +---------- +Note:", + ); + assert!( + sections(&source) + .first() + .is_some_and(|section| matches!(section.body, SectionBody::Opaque)) + ); + } + + #[test] + fn extracts_later_parameters_from_an_ambiguous_section() { + let raw = "\ +Parameters +---------- +value : int + Description. +Ambiguous prose. +other : str + Other."; + + assert_snapshot!(display_parameters(raw), @" + value: + │ Description. + │ Ambiguous prose. + other: + │ Other. + "); + + assert_section_is_structurally_ambiguous(raw); + } + + #[test] + fn skips_an_invalid_item_without_ending_the_parameter_list() { + let raw = "\ +Parameters +---------- +value : int + Description. + +malformed name : str + Not value documentation. + +other : str + Other."; + + assert_snapshot!(display_parameters(raw), @" + value: + │ Description. + other: + │ Other. + "); + + assert_section_is_structurally_ambiguous(raw); + } + + #[test] + fn skips_static_substitutions() { + let raw = "\ +Summary. + Parameters + ---------- + first : int + Before. + %(description)s + $DESCRIPTION + After. + + $ITEM + Expansion content. + ${OTHER} + second : int + Description. +%(OUTSIDE)s + outside : int + Not parameter documentation. + + Parameters + ---------- + %(boundary)s + + %(left)s or %(right)s + hidden : int + Also not parameter documentation."; + + assert_snapshot!(display_parameters(raw), @" + first: + │ Before. + │ After. + second: + │ Description. + "); + + let source = normalized(raw); + assert!( + sections(&source) + .into_iter() + .all(|section| section.into_renderable_fragments().is_none()) + ); + } + + #[test] + fn extracts_later_parameters_after_an_unconfirmed_item() { + let raw = "\ +Parameters +---------- +value : int + Description. +Note: +other : str + Other."; + + assert_snapshot!(display_parameters(raw), @" + value: + │ Description. + │ Note: + other: + │ Other. + "); + + assert_section_is_structurally_ambiguous(raw); + } + + #[test] + fn treats_indented_return_items_as_structurally_ambiguous() { + let raw = "\ +Returns +------- + foo() + result"; + + assert_section_is_structurally_ambiguous(raw); + } + + #[test] + fn ends_parameters_before_preformatted_block() { + let source = normalized( + "\ +Parameters +---------- +value : int + Description. + +```text +other : str +```", + ); + + assert!(sections(&source).first().is_some_and(|section| { + matches!( + section.body, + SectionBody::Parsed { + has_structural_ambiguity: false, + .. + } + ) && &source[section.range] + == "\ +Parameters +---------- +value : int + Description." + })); + } + + #[test] + fn parses_attributes_without_descriptions() { + let source = normalized( + "\ +Attributes +---------- +dtype : np.dtype +index", + ); + + assert_eq!( + sections(&source).first().map(|section| §ion.body), + Some(&SectionBody::Parsed { + fragments: vec![ + BodyFragment::Item(Item { + display_name: Some("dtype".to_string()), + ty: Some("np.dtype".to_string()), + description: String::new(), + }), + BodyFragment::Item(Item { + display_name: Some("index".to_string()), + ty: None, + description: String::new(), + }), + ], + has_structural_ambiguity: false, + }) + ); + } + + #[test] + fn parses_named_and_anonymous_return_items() { + let source = normalized( + "\ +Returns +------- +np.ndarray, bool + The values and a flag. +angular separation : Quantity + The angle between two points. +`module:Type`", + ); + + assert_eq!( + sections(&source).first().map(|section| §ion.body), + Some(&SectionBody::Parsed { + fragments: vec![ + BodyFragment::Item(Item { + display_name: None, + ty: Some("np.ndarray, bool".to_string()), + description: "The values and a flag.".to_string(), + }), + BodyFragment::Item(Item { + display_name: Some("angular separation".to_string()), + ty: Some("Quantity".to_string()), + description: "The angle between two points.".to_string(), + }), + BodyFragment::Item(Item { + display_name: None, + ty: Some("`module:Type`".to_string()), + description: String::new(), + }), + ], + has_structural_ambiguity: false, + }) + ); + } + + fn assert_section_is_structurally_ambiguous(raw: &str) { + let source = normalized(raw); + assert!(sections(&source).first().is_some_and(|section| matches!( + section.body, + SectionBody::Parsed { + has_structural_ambiguity: true, + .. + } + ))); + } + + fn display_parameters(raw: &str) -> String { + let normalized_source = crate::docstring::documentation_trim(raw); + parameter_documentation(&normalized_source) + .into_iter() + .map(|(name, documentation)| { + let documentation = documentation + .lines() + .map(|line| match line { + "" => " │".to_string(), + _ => format!(" │ {line}"), + }) + .join("\n"); + format!("{name}:\n{documentation}") + }) + .join("\n") + } + + fn normalized(raw: &str) -> String { + crate::docstring::documentation_trim(raw) + } +} diff --git a/crates/ty_ide/src/docstring/document/preformatted.rs b/crates/ty_ide/src/docstring/document/preformatted.rs index 40f6e30f55..df64de37ce 100644 --- a/crates/ty_ide/src/docstring/document/preformatted.rs +++ b/crates/ty_ide/src/docstring/document/preformatted.rs @@ -45,6 +45,13 @@ impl<'a> MarkdownFence<'a> { } } +/// Returns whether `line` starts a recognized preformatted block. +pub(super) fn starts_preformatted_block(line: &str) -> bool { + PreformattedBlockScanner::line_starts_doctest(line) + || MarkdownFence::find(line).is_some() + || RestLiteralBlockScanner::line_starts_literal_block(line.trim_start()) +} + /// Recognizes preformatted blocks that may occur within a docstring. #[derive(Debug, Default, Clone, PartialEq, Eq)] pub(super) struct PreformattedBlockScanner<'a> { @@ -126,13 +133,13 @@ pub(super) struct RestLiteralBlockScanner { impl RestLiteralBlockScanner { /// Updates internal state for a possible reST literal block marker. - pub(super) fn observe_marker_in_line(&mut self, line: &str) { + fn observe_marker_in_line(&mut self, line: &str) { self.observe_marker(line, indentation(line)); } /// Updates internal state for a possible reST literal block marker whose text has already /// been split out from its source line. - pub(super) fn observe_marker(&mut self, line: &str, marker_indent: TextSize) { + fn observe_marker(&mut self, line: &str, marker_indent: TextSize) { let line = line.trim_start(); if matches!(self.state, RestLiteralBlockState::Inactive) && Self::line_starts_literal_block(line) @@ -145,7 +152,7 @@ impl RestLiteralBlockScanner { } /// Consumes a line if it is inside a reST literal block already observed by `observe_marker`. - pub(super) fn consume_line(&mut self, line: &str) -> bool { + fn consume_line(&mut self, line: &str) -> bool { let current_indent = indentation(line); let line_is_empty = line.trim_start().is_empty(); diff --git a/crates/ty_ide/src/docstring/document/rst.rs b/crates/ty_ide/src/docstring/document/rst.rs index 8ccd608d3e..64bf275a91 100644 --- a/crates/ty_ide/src/docstring/document/rst.rs +++ b/crates/ty_ide/src/docstring/document/rst.rs @@ -1,12 +1,12 @@ use std::iter::{Enumerate, Peekable}; use compact_str::{CompactString, ToCompactString}; -use indexmap::IndexMap; use ruff_python_trivia::leading_indentation; use ruff_source_file::{Line as SourceLine, UniversalNewlineIterator, UniversalNewlines}; use ruff_text_size::{Ranged, TextRange, TextSize}; use super::preformatted::PreformattedBlockScanner; +use crate::FxIndexMap; /// Parses all reST field lists in a docstring. fn field_lists(raw: &str) -> Vec { @@ -35,8 +35,8 @@ pub(in crate::docstring) fn top_level_field_lists( /// /// `normalized_source` must have already undergone PEP-257 trimming and universal newline /// normalization. -pub(super) fn parameter_documentation(normalized_source: &str) -> IndexMap { - let mut parameters = IndexMap::new(); +pub(super) fn parameter_documentation(normalized_source: &str) -> FxIndexMap { + let mut parameters = FxIndexMap::default(); for field_list in top_level_field_lists(normalized_source) { for field in field_list.fields { diff --git a/crates/ty_ide/src/docstring/document/syntax.rs b/crates/ty_ide/src/docstring/document/syntax.rs index 401b7b5277..2406b11004 100644 --- a/crates/ty_ide/src/docstring/document/syntax.rs +++ b/crates/ty_ide/src/docstring/document/syntax.rs @@ -1,7 +1,7 @@ use ruff_python_stdlib::identifiers::is_identifier; use ruff_python_trivia::{Cursor, leading_indentation, tab_offset_u32}; use ruff_source_file::UniversalNewlines; -use ruff_text_size::{TextRange, TextSize}; +use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use super::rst::is_field_list_marker; @@ -58,60 +58,402 @@ pub(in crate::docstring) fn starts_with_markdown_list_item(line: &str) -> bool { && matches!(bytes.get(digits + 1), Some(b' ' | b'\t')) } -/// Returns whether `text` consists of a complete Markdown code span. +/// Returns whether `text` is wrapped in a Markdown code span. /// /// For example, this returns `true` for ``"`value`"`` and `false` for /// ``"`value` trailing"``. -pub(in crate::docstring) fn is_markdown_code_span(text: &str) -> bool { - find_backtick_run(text, TextSize::ZERO).and_then(|opening| markdown_code_span(text, opening)) - == Some(TextRange::up_to(TextSize::of(text))) +pub(crate) fn is_wrapped_in_markdown_code_span(text: &str) -> bool { + let mut tokens = InlineMarkupScanner::new(text); + let Some(InlineMarkupToken::Code(_)) = tokens.next() else { + return false; + }; + + tokens.next().is_none() } -/// Returns the byte range of the first consecutive backtick run at or after `from`. +/// Emits non-overlapping tokens that completely span the source text. /// -/// For example, searching ``"value `code`"`` from the start returns the range covering the -/// opening ``"`"``. -pub(in crate::docstring) fn find_backtick_run(text: &str, from: TextSize) -> Option { - let from = from.to_usize(); - let start = from + text.get(from..)?.find('`')?; - let len = text[start..] - .bytes() - .take_while(|byte| *byte == b'`') - .count(); - Some(TextRange::new( - TextSize::of(&text[..start]), - TextSize::of(&text[..start + len]), - )) +/// Supports complete backtick-delimited segments, reStructuredText prefix roles, and plain text. +/// +/// For example: +/// +/// ```text +/// InlineMarkupScanner::new("before :class:`Value` and `code`") +/// => Text("before "), RestPrefixRole("class", "Value"), Text(" and "), Code("code") +/// ``` +pub(crate) struct InlineMarkupScanner<'a> { + /// The scanner used to find complete code spans. + scanner: BacktickScanner<'a>, + /// The end of the last token returned to the caller. + last_token_end: TextSize, + /// A token saved while its preceding text is returned first. + pending_token: Option>, +} + +impl<'a> InlineMarkupScanner<'a> { + /// Creates a lossless iterator over plain text and complete inline markup. + /// + /// Escaped or unmatched backticks remain part of an [`InlineMarkupToken::Text`] token. + pub(crate) fn new(source: &'a str) -> Self { + Self { + scanner: BacktickScanner::new(source), + last_token_end: TextSize::ZERO, + pending_token: None, + } + } + + fn take_remaining_text(&mut self) -> Option> { + let source_end = self.scanner.source.text_len(); + let remaining = TextRange::new(self.last_token_end, source_end); + self.last_token_end = source_end; + (!remaining.is_empty()).then(|| InlineMarkupToken::Text(&self.scanner.source[remaining])) + } } -/// Returns the Markdown code span delimited by `opening`, if it has a matching closing run. +impl<'a> Iterator for InlineMarkupScanner<'a> { + type Item = InlineMarkupToken<'a>; + + fn next(&mut self) -> Option { + if let Some(token) = self.pending_token.take() { + return Some(token); + } + + let span = loop { + // Without another backtick run, the remaining source is all plain text. + let Some(opening) = self.scanner.next() else { + return self.take_remaining_text(); + }; + + // Escaped runs are literal source text, so continue looking for the next possible + // opening without emitting a token boundary. + if opening.is_escaped() { + continue; + } + + // Without a closing delimiter, callers cannot treat the opening or any later runs as + // structured markup. Emit the remainder as one text token. + let Some(span) = self.scanner.eat_span(opening) else { + return self.take_remaining_text(); + }; + break span; + }; + + let preceding_range = TextRange::new(self.last_token_end, span.start()); + let preceding_text = &self.scanner.source[preceding_range]; + let (preceding_text, token) = if span.is_single() + && let Some((preceding_text, name)) = split_trailing_rest_prefix_role(preceding_text) + { + ( + preceding_text, + InlineMarkupToken::RestPrefixRole(Role { name, span }), + ) + } else { + (preceding_text, InlineMarkupToken::Code(span)) + }; + + self.last_token_end = span.end(); + if preceding_text.is_empty() { + Some(token) + } else { + self.pending_token = Some(token); + Some(InlineMarkupToken::Text(preceding_text)) + } + } +} + +/// One lossless token produced by [`InlineMarkupScanner`]. /// -/// For example, the opening run in "``value`with:ticks`` trailing" produces the range covering -/// "``value`with:ticks``". -pub(in crate::docstring) fn markdown_code_span( - text: &str, - opening: TextRange, -) -> Option { - let mut search_from = opening.end(); - loop { - let closing = find_backtick_run(text, search_from)?; - if closing.len() == opening.len() { - return Some(opening.cover(closing)); +/// For example: +/// +/// ```text +/// source "before :class:`Value` and `code`" +/// tokens Text("before "), RestPrefixRole("class", "Value"), Text(" and "), Code("code") +/// ``` +/// +/// Escaped and unmatched backticks remain text. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum InlineMarkupToken<'a> { + /// Source text outside a complete, unescaped backtick span. + Text(&'a str), + /// A complete code span whose backtick delimiters have equal lengths. + Code(BacktickSpan<'a>), + /// A reStructuredText prefix-role pattern and its single-backtick span. + RestPrefixRole(Role<'a>), +} + +/// A reStructuredText prefix role recognized by [`InlineMarkupScanner`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct Role<'a> { + name: &'a str, + span: BacktickSpan<'a>, +} + +impl<'a> Role<'a> { + /// Returns the complete single-backtick span following the role name. + /// + /// For `` :class:`Model ` ``, this returns the span + /// `` `Model ` ``. + pub(crate) fn span(self) -> BacktickSpan<'a> { + self.span + } + + /// Returns the source between the role's backtick delimiters. + /// + /// For `` :class:`Model ` ``, this returns `Model `. + pub(crate) fn content(self) -> &'a str { + self.span.content() + } + + /// Returns the explicit display title, if present. + /// + /// For `` :class:`Model ` ``, this returns `Some("Model")`. + pub(crate) fn explicit_title(self) -> Option<&'a str> { + self.content() + .strip_suffix('>') + .and_then(|content| content.split_once('<')) + .map(|(title, _)| title.trim_end()) + } + + /// Returns whether this is a Sphinx Python-domain cross-reference role. + /// + /// For example, this returns `true` for `class`, `py:func`, and + /// `external+python:py:obj`. + pub(crate) fn is_python_domain_cross_reference(self) -> bool { + let mut components = self.name.rsplit(':'); + let Some(role) = components.next() else { + return false; + }; + + matches!(components.next(), None | Some("py")) + && matches!( + role, + "attr" + | "class" + | "const" + | "data" + | "deco" + | "exc" + | "func" + | "meth" + | "mod" + | "obj" + | "type" + ) + } +} + +/// Splits a trailing reStructuredText prefix-role pattern from its preceding text. +/// +/// This deliberately recognizes role-shaped markup common in docstrings +/// (e.g. roles immediately after `=`, using a plural `s` immediately after a role) +/// without enforcing reStructuredText's surrounding inline-markup boundaries. +/// +/// For example, `"before :py:class:"` becomes `("before ", "py:class")`. +fn split_trailing_rest_prefix_role(text: &str) -> Option<(&str, &str)> { + let without_closing_colon = text.strip_suffix(':')?; + let mut role_start = without_closing_colon.len(); + let mut expects_alphanumeric = true; + + // Walk `before :py:class` backwards; separators must be between alphanumeric components. + for (index, character) in without_closing_colon.char_indices().rev() { + if character.is_alphanumeric() { + expects_alphanumeric = false; + role_start = index; + continue; + } + if !matches!(character, '-' | '.' | '_' | '+' | ':') { + break; } - search_from = closing.end(); + if expects_alphanumeric { + break; + } + + expects_alphanumeric = true; + role_start = index; + } + + if !expects_alphanumeric { + return None; + } + + let role_name = without_closing_colon[role_start..].strip_prefix(':')?; + Some((&without_closing_colon[..role_start], role_name)) +} + +/// Source text delimited by ordered backtick runs of equal length. +/// +/// For example: +/// +/// ```text +/// source "before ``code`` after" +/// range() 7..15 +/// is_single() false +/// content() "code" +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct BacktickSpan<'a> { + /// The source between the opening and closing delimiters. + content: &'a str, + /// The byte range including both delimiters. + range: TextRange, + /// The byte length of either delimiter. + delimiter_len: TextSize, +} + +impl<'a> BacktickSpan<'a> { + /// Returns whether both delimiters consist of one backtick. + pub(crate) fn is_single(self) -> bool { + self.delimiter_len == TextSize::new(1) + } + + /// Returns the source between the opening and closing runs. + pub(crate) fn content(self) -> &'a str { + self.content + } +} + +impl Ranged for BacktickSpan<'_> { + fn range(&self) -> TextRange { + self.range } } -/// Returns whether the backtick run at `index` is escaped by a preceding backslash. +/// Scans consecutive backtick runs in source order. +/// +/// The scanner can consume a complete span after returning its opening run. For example: /// -/// For example, the backtick in ``"\`"`` is escaped, while the backtick in ``"\\`"`` is not. -pub(in crate::docstring) fn is_backtick_run_escaped(text: &str, index: usize) -> bool { - !text[..index] - .bytes() - .rev() - .take_while(|byte| *byte == b'\\') - .count() - .is_multiple_of(2) +/// ```text +/// source "prefix ``code`` suffix" +/// opening = next() Some(BacktickRun("``")) +/// as_str() "code`` suffix" +/// eat_span(opening) Some(BacktickSpan("``code``")) +/// as_str() " suffix" +/// ``` +#[derive(Clone)] +pub(crate) struct BacktickScanner<'a> { + /// The complete source whose runs are returned. + source: &'a str, + /// The current scan position within `source`. + cursor: Cursor<'a>, +} + +impl<'a> BacktickScanner<'a> { + /// Creates a scanner positioned at the start of `source`. + pub(crate) fn new(source: &'a str) -> Self { + Self { + source, + cursor: Cursor::new(source), + } + } + + /// Creates a scanner positioned at `offset` within `source`. + fn starts_at(offset: TextSize, source: &'a str) -> Self { + let mut scanner = Self::new(source); + scanner.cursor.skip_bytes(offset.to_usize()); + scanner + } + + /// Returns the remaining source. + pub(crate) fn as_str(&self) -> &'a str { + self.cursor.as_str() + } + + /// Consumes the closing run that matches the most recently returned `opening`. + /// + /// Returns `None` without advancing when no matching run exists. + pub(crate) fn eat_span(&mut self, opening: BacktickRun) -> Option> { + debug_assert_eq!(opening.end(), self.cursor.offset()); + + let mut lookahead = self.clone(); + while let Some(closing) = lookahead.next() { + if let Some(span) = self.span(opening, closing) { + *self = lookahead; + return Some(span); + } + } + None + } + + /// Creates a span from two ordered runs of equal length. + /// + /// Both runs must use ranges in this scanner's source. + pub(crate) fn span( + &self, + opening: BacktickRun, + closing: BacktickRun, + ) -> Option> { + debug_assert!(opening.end() <= closing.start()); + + if opening.range.len() != closing.range.len() { + return None; + } + + Some(BacktickSpan { + content: &self.source[TextRange::new(opening.end(), closing.start())], + range: opening.range.cover(closing.range), + delimiter_len: opening.range.len(), + }) + } +} + +impl Iterator for BacktickScanner<'_> { + type Item = BacktickRun; + + fn next(&mut self) -> Option { + self.cursor.eat_while(|character| character != '`'); + if self.cursor.is_eof() { + return None; + } + + let start = self.cursor.offset(); + self.cursor.eat_while(|character| character == '`'); + let range = TextRange::new(start, self.cursor.offset()); + + let preceding_backslashes = self.source[..start.to_usize()] + .bytes() + .rev() + .take_while(|byte| *byte == b'\\') + .count(); + let escaped = !preceding_backslashes.is_multiple_of(2); + + Some(BacktickRun { range, escaped }) + } +} + +/// One consecutive run of backticks found by [`BacktickScanner`]. +/// +/// For example: +/// +/// ```text +/// source "before \\`` after" +/// range() 8..10 +/// is_single() false +/// is_escaped() true +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct BacktickRun { + /// The byte range of the consecutive backticks. + range: TextRange, + /// Whether an odd-length backslash run escapes the first backtick. + escaped: bool, +} + +impl BacktickRun { + /// Returns whether this run consists of one backtick. + pub(crate) fn is_single(self) -> bool { + self.range.len() == TextSize::new(1) + } + + /// Returns whether a preceding odd-length backslash run escapes this run. + pub(crate) fn is_escaped(self) -> bool { + self.escaped + } +} + +impl Ranged for BacktickRun { + fn range(&self) -> TextRange { + self.range + } } /// Returns the end of an indented Markdown or reStructuredText container block. @@ -120,10 +462,7 @@ pub(in crate::docstring) fn is_backtick_run_escaped(text: &str, index: usize) -> /// at index 3. pub(super) fn container_block_end(lines: &[ParsedLine<'_>], index: usize) -> Option { let marker = lines.get(index)?; - if !is_rest_directive_marker(marker.text) - && !is_field_list_marker(marker.text) - && !starts_with_markdown_list_item(marker.text.trim_start()) - { + if !starts_container_block(marker.text) { return None; } @@ -137,6 +476,13 @@ pub(super) fn container_block_end(lines: &[ParsedLine<'_>], index: usize) -> Opt ) } +/// Returns whether `line` starts a block that owns its indented contents. +pub(super) fn starts_container_block(line: &str) -> bool { + is_rest_directive_marker(line) + || is_field_list_marker(line) + || starts_with_markdown_list_item(line.trim_start()) +} + fn is_rest_directive_marker(line: &str) -> bool { let Some(directive) = line.trim_start().strip_prefix(".. ") else { return false; @@ -232,18 +578,31 @@ pub(super) fn split_trailing_parenthetical(value: &str) -> Option<(&str, &str)> let mut outermost_opening = None; let mut cursor = Cursor::new(value); - while let Some(character) = cursor.bump() { - let index = cursor.offset().to_usize() - character.len_utf8(); + loop { + let start = cursor.offset(); + let Some(character) = cursor.bump() else { + break; + }; + match character { - '\'' | '"' => consume_quoted_string(&mut cursor, character), - '`' if !is_backtick_run_escaped(value, index) => { - let opening = find_backtick_run(value, TextSize::of(&value[..index]))?; - let span = markdown_code_span(value, opening).unwrap_or(opening); - cursor.skip_bytes((span.end() - cursor.offset()).to_usize()); + quote @ ('\'' | '"') => consume_quoted_string(&mut cursor, quote), + '`' => { + let mut scanner = BacktickScanner::starts_at(start, value); + let opening = scanner.next()?; + if opening.is_escaped() { + // The loop has consumed only the first, escaped backtick. Leave the rest of + // the run for the next iteration, where it may open a shorter span. + continue; + } + + let end = scanner + .eat_span(opening) + .map_or_else(|| opening.end(), |span| span.end()); + cursor.skip_bytes((end - cursor.offset()).to_usize()); } '(' => { if depth == 0 { - outermost_opening = Some(index); + outermost_opening = Some(start); } depth += 1; } @@ -251,9 +610,9 @@ pub(super) fn split_trailing_parenthetical(value: &str) -> Option<(&str, &str)> depth = depth.checked_sub(1)?; if depth == 0 && cursor.is_eof() { let opening = outermost_opening?; - let prefix = value[..opening].trim(); - let contents = value[opening + '('.len_utf8()..index].trim(); - return Some((prefix, contents)); + let (prefix, parenthetical) = value.split_at(opening.to_usize()); + let contents = parenthetical.strip_prefix('(')?.strip_suffix(')')?; + return Some((prefix.trim(), contents.trim())); } } _ => {} @@ -278,11 +637,86 @@ pub(super) fn indentation(line: &str) -> TextSize { #[cfg(test)] mod tests { use super::{ - is_markdown_code_span, split_once_at_top_level_colon, split_trailing_parenthetical, + BacktickScanner, InlineMarkupScanner, InlineMarkupToken, TextSize, + is_wrapped_in_markdown_code_span, split_once_at_top_level_colon, + split_trailing_parenthetical, }; #[test] - fn recognizes_complete_markdown_code_spans() { + fn scans_backtick_runs_and_spans() { + let mut scanner = BacktickScanner::starts_at(TextSize::new(7), "prefix ``code`` suffix"); + let opening = scanner.next().expect("an opening backtick run"); + + assert!(!opening.is_single()); + assert!(!opening.is_escaped()); + assert_eq!(scanner.as_str(), "code`` suffix"); + + let span = scanner.eat_span(opening).expect("a matching backtick run"); + assert!(!span.is_single()); + assert_eq!(span.content(), "code"); + assert_eq!(scanner.as_str(), " suffix"); + } + + #[test] + fn scans_text_and_code_tokens() { + let source = "é :class:`~pkg.Widget` or ``literal`tick`` β"; + + assert_eq!( + token_contents(source), + vec![ + ("text", "é "), + ("rest role", "~pkg.Widget"), + ("text", " or "), + ("code", "literal`tick"), + ("text", " β"), + ] + ); + } + + #[test] + fn separates_rest_prefix_roles_from_preceding_text() { + assert_eq!( + token_contents("int-:class:`pkg.Model`"), + vec![("text", "int-"), ("rest role", "pkg.Model")] + ); + } + + #[test] + fn recognizes_common_role_uses_outside_rest_boundaries() { + assert_eq!( + token_contents("callable, default=:func:`sklearn.covariance.empirical_covariance`"), + vec![ + ("text", "callable, default="), + ("rest role", "sklearn.covariance.empirical_covariance"), + ] + ); + assert_eq!( + token_contents("sequence of :class:`numpy.array`s"), + vec![ + ("text", "sequence of "), + ("rest role", "numpy.array"), + ("text", "s"), + ] + ); + } + + #[test] + fn scans_code_at_source_boundaries() { + assert_eq!( + token_contents("`first` and `last`"), + vec![("code", "first"), ("text", " and "), ("code", "last")] + ); + } + + #[test] + fn preserves_escaped_and_unmatched_backticks_as_text() { + let source = r"\`literal\` and `unfinished"; + + assert_eq!(token_contents(source), vec![("text", source)]); + } + + #[test] + fn recognizes_wrapped_markdown_code_spans() { for (text, expected) in [ ("`value`", true), ("``value`with:ticks``", true), @@ -293,7 +727,30 @@ mod tests { ("``", false), ("value", false), ] { - assert_eq!(is_markdown_code_span(text), expected, "{text:?}"); + assert_eq!(is_wrapped_in_markdown_code_span(text), expected, "{text:?}"); + } + } + + #[test] + fn recognizes_rest_prefix_roles() { + for (source, expected) in [ + (":class:`Value`", Some(("class", "Value"))), + (":py:class:`Value`", Some(("py:class", "Value"))), + ( + ":external+python:py:class:`Value`", + Some(("external+python:py:class", "Value")), + ), + (":étiquette:`valeur`", Some(("étiquette", "valeur"))), + (":foo..bar:`Value`", None), + ] { + let actual = InlineMarkupScanner::new(source).next().and_then(|token| { + if let InlineMarkupToken::RestPrefixRole(role) = token { + Some((role.name, role.content())) + } else { + None + } + }); + assert_eq!(actual, expected, "{source:?}"); } } @@ -371,6 +828,22 @@ mod tests { ); } + #[test] + fn ignores_parentheses_inside_code_spans_after_escaped_backtick() { + assert_eq!( + split_trailing_parenthetical(r"value (\``)`)"), + Some(("value", r"\``)`")) + ); + } + + #[test] + fn treats_unmatched_backticks_as_plain_parenthetical_text() { + assert_eq!( + split_trailing_parenthetical("value (`unfinished)"), + Some(("value", "`unfinished")) + ); + } + #[test] fn ignores_parentheses_after_escaped_quotes() { assert_eq!( @@ -388,4 +861,14 @@ mod tests { fn rejects_parenthesized_group_before_trailing_text() { assert_eq!(split_trailing_parenthetical("value (str) or None"), None); } + + fn token_contents(source: &str) -> Vec<(&'static str, &str)> { + InlineMarkupScanner::new(source) + .map(|token| match token { + InlineMarkupToken::Text(text) => ("text", text), + InlineMarkupToken::Code(code) => ("code", code.content()), + InlineMarkupToken::RestPrefixRole(role) => ("rest role", role.content()), + }) + .collect() + } } diff --git a/crates/ty_ide/src/docstring/markdown/general/inline.rs b/crates/ty_ide/src/docstring/markdown/general/inline.rs index 2c79fa8852..4c8d583051 100644 --- a/crates/ty_ide/src/docstring/markdown/general/inline.rs +++ b/crates/ty_ide/src/docstring/markdown/general/inline.rs @@ -49,11 +49,9 @@ use std::borrow::Cow; -use ruff_text_size::TextSize; +use ruff_text_size::{Ranged, TextSize}; -use crate::docstring::document::syntax::{ - find_backtick_run, is_backtick_run_escaped, markdown_code_span, -}; +use crate::docstring::document::syntax::BacktickScanner; /// Exposes an interface for rendering a line of prose that may contain a hyperlink. #[derive(Default)] @@ -278,28 +276,26 @@ enum Candidate<'a> { /// Finds the first complete hyperlink or plausible wrapped candidate in `input`. fn find_link(input: &str) -> Option<(usize, Candidate<'_>)> { - let mut offset = TextSize::ZERO; + let mut scanner = BacktickScanner::new(input); // Visit each backtick run that could delimit inline markup. - while let Some(run) = find_backtick_run(input, offset) { + while let Some(run) = scanner.next() { let index = run.start().to_usize(); // An escaped run is literal text, so continue immediately after it. - if is_backtick_run_escaped(input, index) { - offset = run.end(); + if run.is_escaped() { continue; } - // Try parsing a link only when a single backtick has valid surrounding characters. - if run.len() == TextSize::new(1) - && is_link_start(input, index) + // Try parsing a link only when the backtick run has valid surrounding characters. + if is_link_start(input, index) && let Some(candidate) = parse_candidate(&input[index..]) { return Some((index, candidate)); } // Skip other backtick-delimited spans rather than searching inside them. - offset = markdown_code_span(input, run)?.end(); + scanner.eat_span(run)?; } None @@ -310,7 +306,13 @@ fn find_link(input: &str) -> Option<(usize, Candidate<'_>)> { /// Plausible wrapped labels without a closing backtick remain pending; /// malformed or unsupported forms return `None`. fn parse_candidate(input: &str) -> Option> { - let after_opening = input.strip_prefix('`')?; + let mut scanner = BacktickScanner::new(input); + let opening = scanner.next()?; + if opening.start() != TextSize::ZERO { + return None; + } + + let after_opening = scanner.as_str(); if after_opening .chars() .next() @@ -319,7 +321,11 @@ fn parse_candidate(input: &str) -> Option> { return None; } - let Some(closing) = find_backtick_run(input, TextSize::new(1)) else { + let Some(closing) = scanner.next() else { + if !opening.is_single() { + return None; + } + // Eliminate candidates whose content already contains a disallowed // backslash or closing `>`, or whose target cannot become HTTP(S). A // partial URI scheme remains valid so it can wrap immediately after @@ -333,21 +339,22 @@ fn parse_candidate(input: &str) -> Option> { } return Some(Candidate::Pending); }; - if closing.len() != TextSize::new(1) { + let span = scanner.span(opening, closing)?; + if !span.is_single() { return None; } - let content = &input[1..closing.start().to_usize()]; + let content = span.content(); if content.contains('\\') { return None; } - let after_closing = &input[closing.end().to_usize()..]; + let after_closing = scanner.as_str(); let underscore_count = after_closing .bytes() .take_while(|byte| *byte == b'_') .count(); - let len = closing.end().to_usize() + underscore_count; + let len = span.end().to_usize() + underscore_count; if !(1..=2).contains(&underscore_count) || !is_link_suffix(&after_closing[underscore_count..]) { return None; } diff --git a/crates/ty_ide/src/docstring/markdown/structured.rs b/crates/ty_ide/src/docstring/markdown/structured.rs index ac693750b8..283e88f743 100644 --- a/crates/ty_ide/src/docstring/markdown/structured.rs +++ b/crates/ty_ide/src/docstring/markdown/structured.rs @@ -6,9 +6,13 @@ use strum::IntoEnumIterator; use super::general; use crate::docstring::document::SectionKind; use crate::docstring::document::preformatted::MarkdownFence; -use crate::docstring::document::syntax::{is_markdown_code_span, starts_with_markdown_list_item}; +use crate::docstring::document::syntax::{ + InlineMarkupScanner, InlineMarkupToken, is_wrapped_in_markdown_code_span, + starts_with_markdown_list_item, +}; mod google; +mod numpy; mod rst; /// Renders a docstring as Markdown. @@ -18,6 +22,7 @@ mod rst; pub(super) fn render_into(output: &mut String, source: &str) { let mut sections = rst::structured_sections(source); sections.extend(google::structured_sections(source)); + sections.extend(numpy::structured_sections(source)); render_sections_into(output, source, sections); } @@ -249,7 +254,7 @@ impl SectionItem { if let Some(name) = self.display_name.as_deref() { if matches!(self.kind, SectionKind::Raises) { - render_code_span_into(output, name); + render_type_code_span_into(output, name); } else { render_bold_text_into(output, name); } @@ -370,14 +375,98 @@ fn description_block_start(description: &str) -> Option { fn render_type_code_span_into(output: &mut String, ty: &str) { let normalized = normalize_type_for_code_span(ty); - if is_markdown_code_span(&normalized) { + // Preserve existing code spans, except for abbreviated Sphinx references + // such as "`~pkg.Model`" (whose display should be normalized to "Model"). + if is_wrapped_in_markdown_code_span(&normalized) && !normalized.starts_with("`~") { output.push_str(&normalized); return; } + let normalized = normalize_embedded_type_markup(&normalized); render_code_span_into(output, normalized.as_ref()); } +/// Removes embedded markup before wrapping the normalize type label in a code span. +/// +/// For example: +/// - ``"str or :class:`pkg.Type` or `pkg.Other`"`` becomes `"str or pkg.Type or pkg.Other"` +/// - `"-\\|>"` becomes `"-|>"`. +fn normalize_embedded_type_markup(ty: &str) -> Cow<'_, str> { + if !ty.contains('`') && !ty.contains('\\') { + return Cow::Borrowed(ty); + } + + let mut normalized = String::with_capacity(ty.len()); + for token in InlineMarkupScanner::new(ty) { + match token { + InlineMarkupToken::Text(text) => push_unescaped(&mut normalized, text), + InlineMarkupToken::Code(span) => { + let markup = span.content(); + + // "`~pkg.Widget`" becomes "Widget" + // "``literal`tick``" becomes "literal`tick". + let display_text = if span.is_single() { + interpreted_text_label(markup, false) + } else { + markup + }; + + push_unescaped(&mut normalized, display_text); + } + InlineMarkupToken::RestPrefixRole(role) => { + let markup = role.content(); + + // ":class:`Model `" becomes "Model" + // ":obj:`.lines.line`" becomes "lines.line". + let display_text = role.explicit_title().unwrap_or_else(|| { + interpreted_text_label(markup, role.is_python_domain_cross_reference()) + }); + + push_unescaped(&mut normalized, display_text); + } + } + } + + Cow::Owned(normalized) +} + +/// Returns the display label for reStructuredText interpreted text. +/// +/// For example, "~pkg.Widget" becomes "Widget"; a Python role target like +/// ".lines.line" becomes "lines.line". +fn interpreted_text_label(text: &str, is_python_role_target: bool) -> &str { + let (is_abbreviated, target) = text + .strip_prefix('~') + .map_or((false, text), |target| (true, target)); + let target = if is_python_role_target { + target.strip_prefix('.').unwrap_or(target) + } else { + target + }; + if target.is_empty() { + return text; + } + + if is_abbreviated { + target.rsplit_once('.').map_or(target, |(_, label)| label) + } else { + target + } +} + +fn push_unescaped(output: &mut String, text: &str) { + let mut characters = text.chars().peekable(); + while let Some(character) = characters.next() { + if character == '\\' + && let Some(escaped) = characters.next_if(char::is_ascii_punctuation) + { + output.push(escaped); + } else { + output.push(character); + } + } +} + /// Normalizes type text so it fits in a single Markdown code span. /// /// One-line types are returned unchanged. Multi-line types are trimmed line by @@ -601,6 +690,109 @@ mod tests { "); } + #[test] + fn section_items_normalize_source_markup_in_types() { + let _snap = bind_markdown_snapshot_filters(); + let section = section_block(vec![ + SectionItem::new( + SectionKind::Parameters, + Some("rng"), + Some("{None, int, `numpy.random.Generator`, `numpy.random.RandomState`}, optional"), + "Random number generator.", + ), + SectionItem::new( + SectionKind::Parameters, + Some("arrowstyle"), + Some(r"str (default='-\|>')"), + "Arrow style.", + ), + SectionItem::new( + SectionKind::Parameters, + Some("model"), + Some("str or :class:`pkg.Model`"), + "Model type.", + ), + ]); + + assert_snapshot!(render_markdown(§ion), @" + ## Parameters + **rng**: `{None, int, numpy.random.Generator, numpy.random.RandomState}, optional` + Random number generator. + + **arrowstyle**: `str (default='-|>')` + Arrow style. + + **model**: `str or pkg.Model` + Model type. + "); + } + + #[test] + fn section_items_remove_rest_roles_from_types() { + let _snap = bind_markdown_snapshot_filters(); + let section = section_block(vec![ + SectionItem::new( + SectionKind::Parameters, + Some("colormap"), + Some( + "str or :class:`~matplotlib.colors.Colormap` or :mod:`matplotlib.colors` or `~.pandas.Index`", + ), + "Color mapping.", + ), + SectionItem::new( + SectionKind::Parameters, + Some("model"), + Some("`~astropy.modeling.core.Model`"), + "Model.", + ), + SectionItem::new( + SectionKind::Parameters, + Some("extension"), + Some("`.py`"), + "File extension.", + ), + SectionItem::new( + SectionKind::Parameters, + Some("config"), + Some("str or `.env` or `.Figure` or `.lines.Line2D`"), + "Configuration source.", + ), + SectionItem::new( + SectionKind::Parameters, + Some("line"), + Some(":py:obj:`.lines.line`"), + "Line helper.", + ), + SectionItem::new( + SectionKind::Parameters, + Some("model"), + Some(":class:`Model `"), + "Named model.", + ), + ]); + + assert_snapshot!(render_markdown(§ion), @" + ## Parameters + **colormap**: `str or Colormap or matplotlib.colors or Index` + Color mapping. + + **model**: `Model` + Model. + + **extension**: `.py` + File extension. + + **config**: `str or .env or .Figure or .lines.Line2D` + Configuration source. + + **line**: `lines.line` + Line helper. + + **model**: `Model` + Named model. + "); + } + #[test] fn section_items_keep_block_descriptions_in_block_context() { let _snap = bind_markdown_snapshot_filters(); diff --git a/crates/ty_ide/src/docstring/markdown/structured/numpy.rs b/crates/ty_ide/src/docstring/markdown/structured/numpy.rs new file mode 100644 index 0000000000..990086ae44 --- /dev/null +++ b/crates/ty_ide/src/docstring/markdown/structured/numpy.rs @@ -0,0 +1,370 @@ +use crate::docstring::document::numpy; + +use super::{Section, SectionItem, SectionKind}; + +/// Returns NumPy-style sections that can be rendered structurally. +pub(super) fn structured_sections(normalized_source: &str) -> Vec
{ + numpy::sections(normalized_source) + .into_iter() + .filter_map(section) + .collect() +} + +fn section(parsed: numpy::Section) -> Option
{ + let kind = parsed.kind(); + let range = parsed.range(); + let fragments = parsed.into_renderable_fragments()?; + + if fragments.is_empty() { + return None; + } + + let items = fragments + .into_iter() + .map(|fragment| section_item(kind, fragment)) + .collect(); + + Section::new(range, items) +} + +fn section_item(kind: SectionKind, fragment: numpy::BodyFragment) -> SectionItem { + match fragment { + numpy::BodyFragment::Prose(description) => { + SectionItem::from_owned_parts(kind, None, None, description) + } + numpy::BodyFragment::Item(item) => { + let (display_name, ty, description) = item.into_display_name_type_and_description(); + SectionItem::from_owned_parts(kind, display_name, ty, description) + } + } +} + +#[cfg(test)] +mod tests { + use insta::{Settings, assert_snapshot}; + + use super::super::render_sections_into; + use super::structured_sections; + + #[test] + fn renders_supported_sections() { + let _snap = bind_markdown_snapshot_filters(); + let docstring = "\ +Summary. + +Parameters +---------- +value, alias : str + The value. + + A second paragraph. +other + Another value. +*args : object + Extra positional arguments. +**kwargs : object + Extra keyword arguments. +options.mode : str + Nested field documentation. +π : int + A Unicode parameter. +a1, a2, ... : sequence of array_like + Arrays to combine. +override_repr: callable, optional + Replacement representation function. +formats, names : +undocumented + +Other Parameters +---------------- +kw_only: bool + Less common option. + +Attributes +---------- +name : str + Display name. + +Returns +------- +result : bool + Whether validation passed. + +Yields +------ +int + Next value. + +Raises +------ +ValueError + If invalid. +`TypeError` + If unsupported. +"; + + assert_snapshot!(render_numpy(docstring), @r" + Summary. + + ## Parameters + **value, alias**: `str` + The value. + + A second paragraph. + + **other** + Another value. + + **\*args**: `object` + Extra positional arguments. + + **\*\*kwargs**: `object` + Extra keyword arguments. + + **options.mode**: `str` + Nested field documentation. + + **π**: `int` + A Unicode parameter. + + **a1, a2, ...**: `sequence of array_like` + Arrays to combine. + + **override\_repr**: `callable, optional` + Replacement representation function. + + **formats, names** + + **undocumented** + + ## Other Parameters + **kw\_only**: `bool` + Less common option. + + ## Attributes + **name**: `str` + Display name. + + ## Returns + **result**: `bool` + Whether validation passed. + + ## Yields + `int` + Next value. + + ## Raises + `ValueError` + If invalid. + + `TypeError` + If unsupported. + "); + } + + #[test] + fn renders_preformatted_parameter_description() { + let _snap = bind_markdown_snapshot_filters(); + let docstring = "\ +Parameters +---------- +value : str + Example:: + ``` +other : int + Another value. +"; + + assert_snapshot!(render_numpy(docstring), @" + ## Parameters + **value**: `str` + Example: + + ```````````python + ``` + ``````````` + + **other**: `int` + Another value. + "); + } + + #[test] + fn renders_parameter_section_preamble() { + let _snap = bind_markdown_snapshot_filters(); + let docstring = "\ +Parameters +---------- +Either x or y must be provided. + +beta : float + Useful documentation. +"; + + assert_snapshot!(render_numpy(docstring), @" + ## Parameters + Either x or y must be provided. + + **beta**: `float` + Useful documentation. + "); + } + + #[test] + fn renders_shifted_top_level_sections() { + let _snap = bind_markdown_snapshot_filters(); + let docstring = "\ +A decoded newline follows: +This line starts at column zero. + + Parameters + ---------- + shifted : int + Documentation in a shifted section. +"; + + assert_snapshot!(render_numpy(docstring), @" + A decoded newline follows: + This line starts at column zero. + + ## Parameters + **shifted**: `int` + Documentation in a shifted section. + "); + } + + #[test] + fn renders_parenthesized_return_names() { + let _snap = bind_markdown_snapshot_filters(); + let docstring = "\ +Returns +------- +((node1, node2), ancestor) : tuple[tuple[object, object], object] + A node pair and its lowest common ancestor. +"; + + assert_snapshot!(render_numpy(docstring), @" + ## Returns + **((node1, node2), ancestor)**: `tuple[tuple[object, object], object]` + A node pair and its lowest common ancestor. + "); + } + + #[test] + fn renders_return_prose_outside_the_structured_section() { + let _snap = bind_markdown_snapshot_filters(); + let docstring = "\ +Returns +------- +list of nodes + The nodes in traversal order +necessarily returned in a stable order +"; + + assert_snapshot!(render_numpy(docstring), @" + ## Returns + `list of nodes` + The nodes in traversal order + + necessarily returned in a stable order + "); + } + + #[test] + fn declines_to_render_nested_parameter_items() { + let docstring = "\ +Parameters +---------- +Choose one of the following. + nested : int + Example-only text. +beta : float + Useful documentation. +"; + + assert!(parsed_sections(docstring).is_empty()); + } + + #[test] + fn declines_to_render_structurally_ambiguous_section() { + let docstring = "\ +Parameters +---------- + value : int + Description. + Ambiguous prose. + other : str + Other. +"; + + assert!(parsed_sections(docstring).is_empty()); + } + + #[test] + fn declines_to_render_section_nested_in_container() { + let docstring = "\ +Summary. + +- Example data: + Parameters + ---------- + nested : int + Not parameter documentation. +"; + + assert!(parsed_sections(docstring).is_empty()); + } + + #[test] + fn declines_to_render_prose_only_return_section() { + let docstring = "\ +Returns +------- + The created object. +"; + + assert!(parsed_sections(docstring).is_empty()); + } + + #[test] + fn declines_to_render_unclosed_return_fence() { + let docstring = "\ +Returns +------- +```python + result = 1 +"; + + assert!(parsed_sections(docstring).is_empty()); + } + + #[test] + fn declines_to_render_empty_return_section() { + let docstring = "\ +Returns +------- + +Notes +----- +Not a return value. +"; + + assert!(parsed_sections(docstring).is_empty()); + } + + fn render_numpy(source: &str) -> String { + let mut output = String::new(); + render_sections_into(&mut output, source, parsed_sections(source)); + output + } + + fn parsed_sections(source: &str) -> Vec { + structured_sections(source) + } + + fn bind_markdown_snapshot_filters() -> impl Drop { + let mut settings = Settings::clone_current(); + settings.add_filter(" \n", "\n"); + settings.bind_to_scope() + } +} diff --git a/crates/ty_ide/src/document_symbols.rs b/crates/ty_ide/src/document_symbols.rs index 54e94cf40d..8d84d1212f 100644 --- a/crates/ty_ide/src/document_symbols.rs +++ b/crates/ty_ide/src/document_symbols.rs @@ -1,22 +1,23 @@ use crate::symbols::{FlatSymbols, symbols_for_file}; -use ruff_db::files::File; use ty_project::Db; +use ty_python_core::ProgramFile; /// Get all document symbols for a file with the given options. -pub fn document_symbols(db: &dyn Db, file: File) -> &FlatSymbols { +pub fn document_symbols<'db>(db: &'db dyn Db, file: ProgramFile<'db>) -> &'db FlatSymbols { symbols_for_file(db, file) } #[cfg(test)] mod tests { use super::*; - use crate::symbols::{HierarchicalSymbols, SymbolId, SymbolInfo}; + use crate::symbols::{HierarchicalSymbols, SymbolId, SymbolInfo, SymbolKind}; use crate::tests::{CursorTest, IntoDiagnostic, cursor_test}; use insta::assert_snapshot; use ruff_db::diagnostic::{ Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic, SubDiagnosticSeverity, }; + use ruff_db::files::File; /// basedpython: an accessor property is one member in the source, so it is /// one symbol in the outline — not the getter, backing field and setter the @@ -87,7 +88,6 @@ class World: | 2 | def hello(): | ^^^^^ - | info: Function hello info[document-symbols]: SymbolInfo @@ -95,7 +95,6 @@ class World: | 5 | class World: | ^^^^^ - | info: Class World info[document-symbols]: SymbolInfo @@ -103,7 +102,6 @@ class World: | 6 | def method(self): | ^^^^^^ - | info: Method method "); } @@ -146,7 +144,6 @@ def standalone_function(): | 5 | CONSTANT = 42 | ^^^^^^^^ - | info: Constant CONSTANT info[document-symbols]: SymbolInfo @@ -154,7 +151,6 @@ def standalone_function(): | 6 | variable = 'hello' | ^^^^^^^^ - | info: Variable variable info[document-symbols]: SymbolInfo @@ -162,7 +158,6 @@ def standalone_function(): | 7 | typed_global: str = 'typed' | ^^^^^^^^^^^^ - | info: Variable typed_global info[document-symbols]: SymbolInfo @@ -170,7 +165,6 @@ def standalone_function(): | 8 | annotated_only: int | ^^^^^^^^^^^^^^ - | info: Variable annotated_only info[document-symbols]: SymbolInfo @@ -178,7 +172,6 @@ def standalone_function(): | 10 | class MyClass: | ^^^^^^^ - | info: Class MyClass info[document-symbols]: SymbolInfo @@ -186,7 +179,6 @@ def standalone_function(): | 11 | class_var = 100 | ^^^^^^^^^ - | info: Field class_var info[document-symbols]: SymbolInfo @@ -194,7 +186,6 @@ def standalone_function(): | 12 | typed_class_var: str = 'class_typed' | ^^^^^^^^^^^^^^^ - | info: Field typed_class_var info[document-symbols]: SymbolInfo @@ -202,7 +193,6 @@ def standalone_function(): | 13 | annotated_class_var: float | ^^^^^^^^^^^^^^^^^^^ - | info: Field annotated_class_var info[document-symbols]: SymbolInfo @@ -210,7 +200,6 @@ def standalone_function(): | 15 | def __init__(self): | ^^^^^^^^ - | info: Constructor __init__ info[document-symbols]: SymbolInfo @@ -218,7 +207,6 @@ def standalone_function(): | 18 | def public_method(self): | ^^^^^^^^^^^^^ - | info: Method public_method info[document-symbols]: SymbolInfo @@ -226,7 +214,6 @@ def standalone_function(): | 21 | def _private_method(self): | ^^^^^^^^^^^^^^^ - | info: Method _private_method info[document-symbols]: SymbolInfo @@ -234,7 +221,6 @@ def standalone_function(): | 24 | def standalone_function(): | ^^^^^^^^^^^^^^^^^^^ - | info: Function standalone_function "); } @@ -261,7 +247,6 @@ class OuterClass: | 2 | class OuterClass: | ^^^^^^^^^^ - | info: Class OuterClass info[document-symbols]: SymbolInfo @@ -269,7 +254,6 @@ class OuterClass: | 3 | OUTER_CONSTANT = 100 | ^^^^^^^^^^^^^^ - | info: Constant OUTER_CONSTANT info[document-symbols]: SymbolInfo @@ -277,7 +261,6 @@ class OuterClass: | 5 | def outer_method(self): | ^^^^^^^^^^^^ - | info: Method outer_method info[document-symbols]: SymbolInfo @@ -285,7 +268,6 @@ class OuterClass: | 8 | class InnerClass: | ^^^^^^^^^^ - | info: Class InnerClass info[document-symbols]: SymbolInfo @@ -293,7 +275,6 @@ class OuterClass: | 9 | def inner_method(self): | ^^^^^^^^^^^^ - | info: Method inner_method "); } @@ -315,7 +296,6 @@ class Aliases: | 2 | type IntList = list[int] | ^^^^^^^ - | info: Variable IntList info[document-symbols]: SymbolInfo @@ -323,7 +303,6 @@ class Aliases: | 4 | class Aliases: | ^^^^^^^ - | info: Class Aliases info[document-symbols]: SymbolInfo @@ -331,14 +310,347 @@ class Aliases: | 5 | type Item = int | ^^^^ - | info: Variable Item "); } + #[test] + fn document_symbols_with_statement_targets() { + let test = cursor_test( + " +from contextlib import nullcontext + +with nullcontext() as module_target, nullcontext((1, 2)) as (left, right): + body_target = 1 + +class C: + with nullcontext() as class_target: + body_field = 1 + +def function(): + with nullcontext() as local_target: + pass +", + ); + + assert_snapshot!(test.document_symbols(), @" + info[document-symbols]: SymbolInfo + --> main.py:4:23 + | + 4 | with nullcontext() as module_target, nullcontext((1, 2)) as (left, right): + | ^^^^^^^^^^^^^ + info: Variable module_target + + info[document-symbols]: SymbolInfo + --> main.py:4:62 + | + 4 | with nullcontext() as module_target, nullcontext((1, 2)) as (left, right): + | ^^^^ + info: Variable left + + info[document-symbols]: SymbolInfo + --> main.py:4:68 + | + 4 | with nullcontext() as module_target, nullcontext((1, 2)) as (left, right): + | ^^^^^ + info: Variable right + + info[document-symbols]: SymbolInfo + --> main.py:5:5 + | + 5 | body_target = 1 + | ^^^^^^^^^^^ + info: Variable body_target + + info[document-symbols]: SymbolInfo + --> main.py:7:7 + | + 7 | class C: + | ^ + info: Class C + + info[document-symbols]: SymbolInfo + --> main.py:8:27 + | + 8 | with nullcontext() as class_target: + | ^^^^^^^^^^^^ + info: Field class_target + + info[document-symbols]: SymbolInfo + --> main.py:9:9 + | + 9 | body_field = 1 + | ^^^^^^^^^^ + info: Field body_field + + info[document-symbols]: SymbolInfo + --> main.py:11:5 + | + 11 | def function(): + | ^^^^^^^^ + info: Function function + "); + } + + #[test] + fn document_symbols_augmented_assignment_targets() { + let test = cursor_test( + " +items = [1] +items[(index := 0)] += 1 +(obj := factory()).value += 1 +items += (rhs := [1]) +", + ); + + assert_snapshot!(test.document_symbols(), @" + info[document-symbols]: SymbolInfo + --> main.py:2:1 + | + 2 | items = [1] + | ^^^^^ + info: Variable items + + info[document-symbols]: SymbolInfo + --> main.py:3:8 + | + 3 | items[(index := 0)] += 1 + | ^^^^^ + info: Variable index + + info[document-symbols]: SymbolInfo + --> main.py:4:2 + | + 4 | (obj := factory()).value += 1 + | ^^^ + info: Variable obj + + info[document-symbols]: SymbolInfo + --> main.py:5:11 + | + 5 | items += (rhs := [1]) + | ^^^ + info: Variable rhs + "); + } + + #[test] + fn document_symbols_store_context_targets() { + let test = cursor_test( + " +first, *rest, LAST = values + +for loop_left, [loop_right, *loop_rest] in rows: + loop_body = 1 + +with manager() as [with_left, *with_rest], manager() as WITH_CONSTANT: + with_body = 1 + +captured = (walrus := 1) + +def function(): + function_local = 1 + with manager() as function_target: + pass +", + ); + + let symbols = document_symbols(&test.db, test.program_file(test.cursor.file)) + .iter() + .map(|(_, symbol)| (symbol.name.into_owned(), symbol.kind)) + .collect::>(); + + assert_eq!( + symbols, + [ + ("first", SymbolKind::Variable), + ("rest", SymbolKind::Variable), + ("LAST", SymbolKind::Constant), + ("loop_left", SymbolKind::Variable), + ("loop_right", SymbolKind::Variable), + ("loop_rest", SymbolKind::Variable), + ("loop_body", SymbolKind::Variable), + ("with_left", SymbolKind::Variable), + ("with_rest", SymbolKind::Variable), + ("WITH_CONSTANT", SymbolKind::Constant), + ("with_body", SymbolKind::Variable), + ("captured", SymbolKind::Variable), + ("walrus", SymbolKind::Variable), + ("function", SymbolKind::Function), + ] + .map(|(name, kind)| (name.to_owned(), kind)) + ); + } + + #[test] + fn document_symbols_match_pattern_bindings() { + let test = cursor_test( + " +match subject: + case [first, *middle, last] as sequence: + body_target = 1 + case {\"key\": mapping_value, **remaining}: + fallback_target = 2 + case Point(positional, named=keyword): + pass + case (0 as alternative) | (1 as alternative): + pass + case _: + wildcard_body = 3 + +match other: + case CONSTANT_CAPTURE: + pass +", + ); + + let symbols = document_symbols(&test.db, test.program_file(test.cursor.file)) + .iter() + .map(|(_, symbol)| (symbol.name.into_owned(), symbol.kind)) + .collect::>(); + + assert_eq!( + symbols, + [ + ("first", SymbolKind::Variable), + ("middle", SymbolKind::Variable), + ("last", SymbolKind::Variable), + ("sequence", SymbolKind::Variable), + ("body_target", SymbolKind::Variable), + ("mapping_value", SymbolKind::Variable), + ("remaining", SymbolKind::Variable), + ("fallback_target", SymbolKind::Variable), + ("positional", SymbolKind::Variable), + ("keyword", SymbolKind::Variable), + ("alternative", SymbolKind::Variable), + ("alternative", SymbolKind::Variable), + ("wildcard_body", SymbolKind::Variable), + ("CONSTANT_CAPTURE", SymbolKind::Constant), + ] + .map(|(name, kind)| (name.to_owned(), kind)) + ); + } + + #[test] + fn document_symbols_ignore_invalid_pattern_bindings() { + let test = cursor_test( + " +match subject: + case [*]: + pass +", + ); + + assert!(document_symbols(&test.db, test.program_file(test.cursor.file)).is_empty()); + } + + #[test] + fn document_symbols_reports_mapping_pattern_bindings_in_source_order() { + let test = cursor_test( + " +match subject: + case {\"a\": before, **between, \"b\": after}: + pass +", + ); + + let names = document_symbols(&test.db, test.program_file(test.cursor.file)) + .iter() + .map(|(_, symbol)| symbol.name.into_owned()) + .collect::>(); + + assert_eq!(names, ["before", "between", "after"]); + } + + #[test] + fn document_symbols_match_pattern_scopes() { + let test = cursor_test( + " +class C: + match subject: + case class_capture: + body_field = 1 + +def function(): + match subject: + case local_capture: + pass +", + ); + + let symbols = document_symbols(&test.db, test.program_file(test.cursor.file)) + .iter() + .map(|(_, symbol)| (symbol.name.into_owned(), symbol.kind)) + .collect::>(); + + assert_eq!( + symbols, + [ + ("C", SymbolKind::Class), + ("class_capture", SymbolKind::Field), + ("body_field", SymbolKind::Field), + ("function", SymbolKind::Function), + ] + .map(|(name, kind)| (name.to_owned(), kind)) + ); + } + + #[test] + fn document_symbols_comprehension_and_lambda_scopes() { + let test = cursor_test( + " +result = [item for item in values if (leaked := item)] +generator = (other for other in values) +lambda_value = lambda: (lambda_local := 1) +", + ); + + let names = document_symbols(&test.db, test.program_file(test.cursor.file)) + .iter() + .map(|(_, symbol)| symbol.name.into_owned()) + .collect::>(); + + assert_eq!(names, ["result", "leaked", "generator", "lambda_value"]); + } + + #[test] + fn document_symbols_function_and_class_header_bindings() { + let test = cursor_test( + " +@(function_decorator := decorate) +def function(value=(default_value := 1)): + function_local = 1 + +@(class_decorator := decorate) +class Example((class_base := Base)): + class_field = 1 +", + ); + + let symbols = document_symbols(&test.db, test.program_file(test.cursor.file)) + .iter() + .map(|(_, symbol)| (symbol.name.into_owned(), symbol.kind)) + .collect::>(); + + assert_eq!( + symbols, + [ + ("function_decorator", SymbolKind::Variable), + ("function", SymbolKind::Function), + ("default_value", SymbolKind::Variable), + ("class_decorator", SymbolKind::Variable), + ("Example", SymbolKind::Class), + ("class_base", SymbolKind::Variable), + ("class_field", SymbolKind::Field), + ] + .map(|(name, kind)| (name.to_owned(), kind)) + ); + } + impl CursorTest { fn document_symbols(&self) -> String { - let symbols = document_symbols(&self.db, self.cursor.file).to_hierarchical(); + let symbols = + document_symbols(&self.db, self.program_file(self.cursor.file)).to_hierarchical(); if symbols.is_empty() { return "No symbols found".to_string(); diff --git a/crates/ty_ide/src/find_references.rs b/crates/ty_ide/src/find_references.rs index a490398d08..f0b999175f 100644 --- a/crates/ty_ide/src/find_references.rs +++ b/crates/ty_ide/src/find_references.rs @@ -1,19 +1,19 @@ use crate::goto::find_goto_target; use crate::references::{ReferencesMode, references}; use crate::{Db, ReferenceTarget}; -use ruff_db::files::File; use ruff_text_size::TextSize; +use ty_python_core::ProgramFile; use ty_python_semantic::SemanticModel; /// Find all references to a symbol at the given position. /// Search for references across all files in the project. pub fn find_references( db: &dyn Db, - file: File, + file: ProgramFile<'_>, offset: TextSize, include_declaration: bool, ) -> Option> { - let parsed = ruff_db::parsed::parsed_module(db, file); + let parsed = ruff_db::parsed::parsed_module(db, file.python_file(db)); let module = parsed.load(db); let model = SemanticModel::new(db, file); @@ -48,7 +48,7 @@ mod tests { fn references_with_include_declaration(&self, include_declaration: bool) -> String { let Some(mut reference_results) = find_references( &self.db, - self.cursor.file, + self.program_file(self.cursor.file), self.cursor.offset, include_declaration, ) else { @@ -89,6 +89,115 @@ mod tests { } } + #[test] + fn references_do_not_mix_global_and_nonlocal_comprehension_walruses() { + let test = cursor_test( + " +last = 0 + +def outer(): + last = 1 + + def write_global(): + global last + [(last := global_item) for global_item in [2]] + + def write_nonlocal(): + nonlocal last + [(last := nonlocal_item) for nonlocal_item in [3]] + + write_global() + write_nonlocal() + return last +", + ); + + assert_snapshot!(test.references(), @" + info[references]: Found 4 references + --> main.py:5:5 + | + 5 | last = 1 + | ---- + | + ::: main.py:12:18 + | + 12 | nonlocal last + | ---- + 13 | [(last := nonlocal_item) for nonlocal_item in [3]] + | ---- + 14 | + 15 | write_global() + 16 | write_nonlocal() + 17 | return last + | ---- + "); + } + + #[test] + fn comprehension_walrus_references_in_function() { + let test = cursor_test( + " +def f(items): + [(last := item) for item in items] + return last +", + ); + + assert_snapshot!(test.references(), @" + info[references]: Found 2 references + --> main.py:3:7 + | + 3 | [(last := item) for item in items] + | ---- + 4 | return last + | ---- + "); + } + + #[test] + fn nested_comprehension_walrus_references_in_function() { + let test = cursor_test( + " +def f(items): + [[(last := item) for item in items] for _ in [1]] + return last +", + ); + + assert_snapshot!(test.references(), @" + info[references]: Found 2 references + --> main.py:3:8 + | + 3 | [[(last := item) for item in items] for _ in [1]] + | ---- + 4 | return last + | ---- + "); + } + + #[test] + fn comprehension_walrus_references_across_files() { + let test = CursorTest::builder() + .source("lib.py", "[(last := item) for item in [1]]\n") + .source("main.py", "from lib import last\nprint(last)\n") + .build(); + + assert_snapshot!(test.references(), @" + info[references]: Found 3 references + --> lib.py:1:3 + | + 1 | [(last := item) for item in [1]] + | ---- + | + ::: main.py:1:17 + | + 1 | from lib import last + | ---- + 2 | print(last) + | ---- + "); + } + #[test] fn parameter_references_in_function() { let test = cursor_test( @@ -119,7 +228,6 @@ result = calculate_sum(value=42) 7 | # Call with keyword argument 8 | result = calculate_sum(value=42) | ----- - | "); } @@ -180,7 +288,6 @@ def outer_function(): 18 | decrement() 19 | final = counter | ------- - | "); } @@ -238,7 +345,6 @@ final_value = global_counter 17 | decrement_global() 18 | final_value = global_counter | -------------- - | "); } @@ -276,7 +382,6 @@ except ValueError as err: | --- 11 | print(f'Different error: {err}') | --- - | "); } @@ -303,7 +408,6 @@ match x: | ------- 5 | return pattern | ------- - | "); } @@ -331,7 +435,6 @@ match data: | ---- 6 | return rest | ---- - | "); } @@ -378,7 +481,6 @@ value = my_function | ----------- 14 | value = my_function | ----------- - | "); } @@ -434,7 +536,6 @@ test("test") 3 | 4 | test("test") | ---- - | "#); } @@ -481,7 +582,6 @@ cls = MyClass | 15 | cls = MyClass | ------- - | "); } @@ -505,7 +605,6 @@ cls = MyClass 3 | 4 | class MyClass: | ------- - | "#); } @@ -526,7 +625,6 @@ cls = MyClass | 2 | a: "MyClass" = 1 | ------- - | "#); } @@ -550,7 +648,6 @@ cls = MyClass 3 | 4 | class MyClass: | ------- - | "#); } @@ -588,7 +685,6 @@ cls = MyClass 3 | 4 | class MyClass: | ------- - | "#); } @@ -640,7 +736,6 @@ cls = MyClass 3 | 4 | class MyClass: | ------- - | "#); } @@ -672,7 +767,6 @@ cls = MyClass | 2 | ab: "ab" | -- -- - | "#); } @@ -706,7 +800,6 @@ cls = MyClass | -- 5 | x = ab | -- - | "#); } @@ -729,7 +822,6 @@ cls = MyClass | -- 5 | x = ab | -- - | "#); } @@ -752,7 +844,6 @@ cls = MyClass | -- 5 | x = ab | -- - | "#); } @@ -775,7 +866,6 @@ cls = MyClass | -- 5 | x = ab | -- - | "#); } @@ -798,7 +888,6 @@ cls = MyClass | -- 5 | x = ab | -- - | "#); } @@ -821,7 +910,6 @@ cls = MyClass | -- 5 | x = ab | -- - | "#); } @@ -850,7 +938,6 @@ cls = MyClass | -- 11 | x = ab | -- - | "); } @@ -879,7 +966,6 @@ cls = MyClass | -- 11 | x = ab | -- - | "); } @@ -914,7 +1000,6 @@ cls = MyClass 9 | match event: 10 | case Click(x, button=ab): | ----- - | "); } @@ -952,7 +1037,6 @@ cls = MyClass | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] | -- -- -- - | "); } @@ -970,7 +1054,6 @@ cls = MyClass | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] | -- -- -- - | "); } @@ -989,7 +1072,6 @@ cls = MyClass | 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] | -- -- -- - | "); } @@ -1008,7 +1090,6 @@ cls = MyClass | 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] | -- -- -- - | "); } @@ -1026,7 +1107,6 @@ cls = MyClass | 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] | -- -- -- - | "); } @@ -1044,7 +1124,6 @@ cls = MyClass | 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] | -- -- -- - | "); } @@ -1111,7 +1190,6 @@ class DataProcessor: | 2 | def func(x): | ---- - | "); } @@ -1161,7 +1239,6 @@ def process_model(): 5 | def get_attribute(self): 6 | return MyModel.attr | ---- - | "); } @@ -1199,7 +1276,6 @@ instance = ExampleClass(old_name="test") | -------- 4 | self.old_name = old_name | -------- - | "#); } @@ -1227,7 +1303,6 @@ TD(f=1) 7 | 8 | TD(f=1) | - - | "); } @@ -1255,7 +1330,6 @@ TD(f=1) 7 | 8 | TD(f=1) | - - | "); } @@ -1283,7 +1357,6 @@ NT(f=1) 7 | 8 | NT(f=1) | - - | "); } @@ -1312,7 +1385,6 @@ DC(f=1) 8 | 9 | DC(f=1) | - - | "); } @@ -1349,7 +1421,6 @@ result = func(value=42) | ----- 3 | return value * 2 | ----- - | "); } @@ -1391,7 +1462,6 @@ result = func(value=1) 4 | 5 | result = func(value=42) | ----- - | "); } @@ -1429,7 +1499,6 @@ async def main(): | ----- 3 | return value * 2 | ----- - | "); } @@ -1460,7 +1529,6 @@ instance = ExampleClass(old_name="test") | 4 | self.old_name = old_name | -------- - | "); } @@ -1498,7 +1566,6 @@ result = func(value=10) | ----- 4 | return value * 2 | ----- - | "); } @@ -1540,7 +1607,6 @@ result = instance.method(old_name="world") | -------- 4 | self.old_name = old_name | -------- - | "#); } @@ -1577,7 +1643,6 @@ func_alias() 3 | 4 | func_alias() | ---------- - | "); } @@ -1623,7 +1688,6 @@ func_alias() | 2 | class Path: | ---- - | "#); } @@ -1651,7 +1715,6 @@ func_alias() 4 | 5 | x = abc | --- - | "); } @@ -1679,7 +1742,6 @@ func_alias() 4 | 5 | x = abc | --- - | "); } @@ -1708,7 +1770,6 @@ func_alias() 4 | 5 | y = xyz | --- - | "); } @@ -1737,7 +1798,6 @@ func_alias() 4 | 5 | y = xyz | --- - | "); } @@ -1768,7 +1828,6 @@ func_alias() | 4 | x = subpkg | ------ - | "); } @@ -1901,7 +1960,6 @@ func_alias() | 2 | subpkg: int = 10 | ------ - | "); } @@ -1938,7 +1996,6 @@ func_alias() | 2 | subpkg: int = 10 | ------ - | "); } @@ -1970,7 +2027,6 @@ func_alias() 5 | 6 | print(a) | - - | "#); } @@ -1989,7 +2045,6 @@ print(x) | 3 | print(x) | - - | "); } @@ -2011,7 +2066,6 @@ print(x) | - 4 | print(x) | - - | "); } @@ -2033,7 +2087,6 @@ print(x) | - 4 | print(x) | - - | "); } @@ -2053,7 +2106,6 @@ print(x) | 4 | print(x) | - - | "); } @@ -2072,7 +2124,6 @@ value: Box | 3 | value: Box | --- - | "); } @@ -2096,7 +2147,6 @@ def test(flag: bool): | 8 | print(x) | - - | "); } @@ -2120,7 +2170,6 @@ def f(flag: bool): | - 6 | print(x) | - - | "); } @@ -2142,7 +2191,6 @@ print(x) | 6 | print(x) | - - | "); } @@ -2165,7 +2213,6 @@ class C: | 7 | print(self.x) | - - | "); } @@ -2190,7 +2237,6 @@ class C: | 9 | print(self.x) | - - | "); } } diff --git a/crates/ty_ide/src/folding_range.rs b/crates/ty_ide/src/folding_range.rs index 0dd3fb0d6c..6cb0b3d51e 100644 --- a/crates/ty_ide/src/folding_range.rs +++ b/crates/ty_ide/src/folding_range.rs @@ -1,4 +1,4 @@ -use ruff_db::files::File; +use ruff_db::PythonFile; use ruff_db::parsed::parsed_module; use ruff_db::source::source_text; use ruff_python_ast::token::{TokenKind, Tokens, parenthesized_range}; @@ -56,10 +56,11 @@ impl From for FoldingRange { /// Returns a list of folding ranges for the given file. pub fn folding_ranges( db: &dyn Db, - file: File, + file: PythonFile<'_>, range_filter: Option, ) -> Vec { let parsed = parsed_module(db, file).load(db); + let file = file.file(db); let source = source_text(db, file); let mut visitor = FoldingRangeVisitor { @@ -707,11 +708,10 @@ impl<'a> SourceOrderVisitor<'a> for FoldingRangeVisitor<'a> { AnyNodeRef::ExprList(_) | AnyNodeRef::ExprListComp(_) | AnyNodeRef::TypeParams(_) => { self.add_delimited_expression_range(node.range(), BRACKETS); } - AnyNodeRef::ExprTuple(tuple) - // Only fold parenthesized tuples. - if tuple.parenthesized => { - self.add_delimited_expression_range(node.range(), PARENTHESES); - } + // Only fold parenthesized tuples. + AnyNodeRef::ExprTuple(tuple) if tuple.parenthesized => { + self.add_delimited_expression_range(node.range(), PARENTHESES); + } AnyNodeRef::ExprDict(_) | AnyNodeRef::ExprSet(_) | AnyNodeRef::ExprSetComp(_) @@ -770,6 +770,7 @@ mod tests { use crate::tests::CursorTest; use insta::assert_snapshot; use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span}; + use ruff_db::files::File; /// `init(...)` carries a synthetic decorator spanning the `init` keyword — /// the same text its `__init__` name is anchored to — so the header search @@ -797,7 +798,6 @@ class A[T]: 3 | | t: T 4 | | init(self, t: T) | |____________________^ - | "); } @@ -830,7 +830,6 @@ class MyClass: 6 | | def method(self): 7 | | return self.value | |_________________________^ - | info[folding-range]: Folding Range --> main.py:3:24 @@ -839,7 +838,6 @@ class MyClass: | ________________________^ 4 | | self.value = 1 | |______________________^ - | info[folding-range]: Folding Range --> main.py:6:22 @@ -848,7 +846,6 @@ class MyClass: | ______________________^ 7 | | return self.value | |_________________________^ - | "); } @@ -883,7 +880,6 @@ class MyClass: 7 | | attribute comment. 8 | | """ | |___________^ - | info[folding-range]: Folding Range --> main.py:3:24 @@ -896,7 +892,6 @@ class MyClass: 7 | | attribute comment. 8 | | """ | |___________^ - | info[folding-range]: Folding Range --> main.py:5:9 @@ -906,7 +901,6 @@ class MyClass: 7 | | attribute comment. 8 | | """ | |___________^ - | "#); } @@ -934,7 +928,6 @@ def main(): 3 | | import sys 4 | | from typing import List, Dict | |_____________________________^ - | info[folding-range]: Folding Range --> main.py:6:12 @@ -943,7 +936,6 @@ def main(): | ____________^ 7 | | pass | |________^ - | "); } @@ -972,7 +964,6 @@ import requests 2 | / import os 3 | | import sys | |__________^ - | info[folding-range]: Folding Range (imports) --> main.py:5:1 @@ -981,7 +972,6 @@ import requests 6 | | import pandas 7 | | import requests | |_______________^ - | "); } @@ -1016,7 +1006,6 @@ from fastapi import FastAPI 2 | / import os 3 | | from math import prod | |_____________________^ - | info[folding-range]: Folding Range (imports) --> main.py:12:1 @@ -1024,7 +1013,6 @@ from fastapi import FastAPI 12 | / import requests 13 | | from fastapi import FastAPI | |___________________________^ - | info[folding-range]: Folding Range --> main.py:5:5 @@ -1034,7 +1022,6 @@ from fastapi import FastAPI 6 | | import foo 7 | | import bar | |______________^ - | info[folding-range]: Folding Range (imports) --> main.py:6:5 @@ -1042,7 +1029,6 @@ from fastapi import FastAPI 6 | / import foo 7 | | import bar | |______________^ - | info[folding-range]: Folding Range --> main.py:8:20 @@ -1052,7 +1038,6 @@ from fastapi import FastAPI 9 | | first = None 10 | | bar = None | |______________^ - | "); } @@ -1094,7 +1079,6 @@ class MyClass: 8 | | 9 | | do_something() | |__________________^ - | info[folding-range]: Folding Range (imports) --> main.py:3:5 @@ -1102,7 +1086,6 @@ class MyClass: 3 | / import os 4 | | import sys | |______________^ - | info[folding-range]: Folding Range (imports) --> main.py:6:5 @@ -1110,7 +1093,6 @@ class MyClass: 6 | / import numpy 7 | | import pandas | |_________________^ - | info[folding-range]: Folding Range --> main.py:12:15 @@ -1120,7 +1102,6 @@ class MyClass: 13 | | import typing 14 | | import collections | |______________________^ - | info[folding-range]: Folding Range (imports) --> main.py:13:5 @@ -1128,7 +1109,6 @@ class MyClass: 13 | / import typing 14 | | import collections | |______________________^ - | "); } @@ -1168,7 +1148,6 @@ else: | ______________^ 3 | | do_something() | |__________________^ - | info[folding-range]: Folding Range --> main.py:4:12 @@ -1177,7 +1156,6 @@ else: | ____________^ 5 | | do_other() | |______________^ - | info[folding-range]: Folding Range --> main.py:6:6 @@ -1186,7 +1164,6 @@ else: | ______^ 7 | | default() | |_____________^ - | info[folding-range]: Folding Range --> main.py:9:19 @@ -1195,7 +1172,6 @@ else: | ___________________^ 10 | | process(item) | |_________________^ - | info[folding-range]: Folding Range --> main.py:11:6 @@ -1204,7 +1180,6 @@ else: | ______^ 12 | | okay() | |__________^ - | info[folding-range]: Folding Range --> main.py:14:15 @@ -1213,7 +1188,6 @@ else: | _______________^ 15 | | continue_work() | |___________________^ - | info[folding-range]: Folding Range --> main.py:16:6 @@ -1222,7 +1196,6 @@ else: | ______^ 17 | | doit() | |__________^ - | "); } @@ -1295,7 +1268,6 @@ match value: 14 | | ): 15 | | return fallback | |_______________________^ - | info[folding-range]: Folding Range --> main.py:5:3 @@ -1313,7 +1285,6 @@ match value: 14 | | ): 15 | | return fallback | |_______________________^ - | info[folding-range]: Folding Range --> main.py:6:12 @@ -1326,7 +1297,6 @@ match value: 10 | | ): 11 | | return value | |____________________^ - | info[folding-range]: Folding Range --> main.py:10:7 @@ -1335,7 +1305,6 @@ match value: | _______^ 11 | | return value | |____________________^ - | info[folding-range]: Folding Range --> main.py:7:10 @@ -1345,7 +1314,6 @@ match value: 8 | | value, 9 | | ] | |________^ - | info[folding-range]: Folding Range --> main.py:12:11 @@ -1356,7 +1324,6 @@ match value: 14 | | ): 15 | | return fallback | |_______________________^ - | info[folding-range]: Folding Range --> main.py:14:7 @@ -1365,7 +1332,6 @@ match value: | _______^ 15 | | return fallback | |_______________________^ - | info[folding-range]: Folding Range --> main.py:17:18 @@ -1380,7 +1346,6 @@ match value: 23 | | ): 24 | | pass | |________^ - | info[folding-range]: Folding Range --> main.py:23:3 @@ -1389,7 +1354,6 @@ match value: | ___^ 24 | | pass | |________^ - | info[folding-range]: Folding Range --> main.py:26:5 @@ -1398,7 +1362,6 @@ match value: | _____^ 27 | | pass | |________^ - | info[folding-range]: Folding Range --> main.py:28:9 @@ -1410,7 +1373,6 @@ match value: 31 | | ) as error: 32 | | raise error | |_______________^ - | info[folding-range]: Folding Range --> main.py:31:12 @@ -1419,7 +1381,6 @@ match value: | ____________^ 32 | | raise error | |_______________^ - | info[folding-range]: Folding Range --> main.py:34:13 @@ -1432,7 +1393,6 @@ match value: 38 | | }: 39 | | handle_mapping() | |________________________^ - | info[folding-range]: Folding Range --> main.py:35:11 @@ -1444,7 +1404,6 @@ match value: 38 | | }: 39 | | handle_mapping() | |________________________^ - | info[folding-range]: Folding Range --> main.py:38:7 @@ -1453,7 +1412,6 @@ match value: | _______^ 39 | | handle_mapping() | |________________________^ - | "#); } @@ -1486,7 +1444,6 @@ def foo(x=[ 6 | | qux = x[0] + 1 7 | | return qux | |______________^ - | info[folding-range]: Folding Range --> main.py:5:4 @@ -1496,7 +1453,6 @@ def foo(x=[ 6 | | qux = x[0] + 1 7 | | return qux | |______________^ - | "); } @@ -1521,7 +1477,6 @@ if condition: # why | _____________________^ 3 | | do_work() | |_____________^ - | "); } @@ -1561,7 +1516,6 @@ if condition: 9 | | and_maybe_this() 10 | | and_maybe_this() | |____________________________^ - | info[folding-range]: Folding Range --> main.py:3:19 @@ -1576,7 +1530,6 @@ if condition: 9 | | and_maybe_this() 10 | | and_maybe_this() | |____________________________^ - | info[folding-range]: Folding Range --> main.py:6:18 @@ -1588,7 +1541,6 @@ if condition: 9 | | and_maybe_this() 10 | | and_maybe_this() | |____________________________^ - | "); } @@ -1625,7 +1577,6 @@ else: 3 | | process(item) 4 | | validate(item) | |__________________^ - | info[folding-range]: Folding Range --> main.py:5:6 @@ -1635,7 +1586,6 @@ else: 6 | | log_success() 7 | | notify_complete() | |_____________________^ - | info[folding-range]: Folding Range --> main.py:9:17 @@ -1645,7 +1595,6 @@ else: 10 | | do_work() 11 | | check_status() | |__________________^ - | info[folding-range]: Folding Range --> main.py:12:6 @@ -1655,7 +1604,6 @@ else: 13 | | handle_done() 14 | | cleanup_resources() | |_______________________^ - | "); } @@ -1688,7 +1636,6 @@ finally: | _____^ 3 | | risky_operation() | |_____________________^ - | info[folding-range]: Folding Range --> main.py:8:6 @@ -1697,7 +1644,6 @@ finally: | ______^ 9 | | success_action() | |____________________^ - | info[folding-range]: Folding Range --> main.py:10:9 @@ -1706,7 +1652,6 @@ finally: | _________^ 11 | | cleanup() | |_____________^ - | info[folding-range]: Folding Range --> main.py:4:19 @@ -1715,7 +1660,6 @@ finally: | ___________________^ 5 | | handle_value_error() | |________________________^ - | info[folding-range]: Folding Range --> main.py:6:18 @@ -1724,7 +1668,6 @@ finally: | __________________^ 7 | | handle_type_error() | |_______________________^ - | "); } @@ -1772,7 +1715,6 @@ my_list_with_trailing_own_line_comment = [ 4 | | 2, 5 | | 3, | |_______^ - | info[folding-range]: Folding Range --> main.py:8:12 @@ -1782,7 +1724,6 @@ my_list_with_trailing_own_line_comment = [ 9 | | "a": 1, 10 | | "b": 2, | |____________^ - | info[folding-range]: Folding Range --> main.py:13:42 @@ -1793,7 +1734,6 @@ my_list_with_trailing_own_line_comment = [ 15 | | 2, 16 | | 3, # reason | |_________________^ - | info[folding-range]: Folding Range --> main.py:19:43 @@ -1805,7 +1745,6 @@ my_list_with_trailing_own_line_comment = [ 22 | | 3, 23 | | # comment | |______________^ - | "#); } @@ -1868,7 +1807,6 @@ type Alias[ 3 | | first, 4 | | second, | |____________^ - | info[folding-range]: Folding Range --> main.py:7:11 @@ -1878,7 +1816,6 @@ type Alias[ 8 | | "a", 9 | | "b", | |_________^ - | info[folding-range]: Folding Range --> main.py:12:13 @@ -1888,7 +1825,6 @@ type Alias[ 13 | | first, 14 | | second, | |____________^ - | info[folding-range]: Folding Range --> main.py:17:17 @@ -1898,7 +1834,6 @@ type Alias[ 18 | | item 19 | | for item in items | |______________________^ - | info[folding-range]: Folding Range --> main.py:22:17 @@ -1908,7 +1843,6 @@ type Alias[ 23 | | item 24 | | for item in items | |______________________^ - | info[folding-range]: Folding Range --> main.py:27:16 @@ -1918,7 +1852,6 @@ type Alias[ 28 | | item 29 | | for item in items | |______________________^ - | info[folding-range]: Folding Range --> main.py:32:17 @@ -1928,7 +1861,6 @@ type Alias[ 33 | | key: value 34 | | for key, value in items | |____________________________^ - | info[folding-range]: Folding Range --> main.py:37:12 @@ -1938,7 +1870,6 @@ type Alias[ 38 | | T, 39 | | U, | |_______^ - | "#); } @@ -1978,7 +1909,6 @@ chained_call = ( | ___________________________^ 3 | | factory | |____________^ - | info[folding-range]: Folding Range --> main.py:4:3 @@ -1987,7 +1917,6 @@ chained_call = ( | ___^ 5 | | arg, | |_________^ - | info[folding-range]: Folding Range --> main.py:8:34 @@ -1996,7 +1925,6 @@ chained_call = ( | __________________________________^ 9 | | factory | |____________^ - | info[folding-range]: Folding Range --> main.py:16:3 @@ -2005,7 +1933,6 @@ chained_call = ( | ___^ 17 | | second, | |____________^ - | info[folding-range]: Folding Range --> main.py:12:17 @@ -2014,7 +1941,6 @@ chained_call = ( | _________________^ 13 | | factory | |____________^ - | info[folding-range]: Folding Range --> main.py:14:3 @@ -2023,7 +1949,6 @@ chained_call = ( | ___^ 15 | | first, | |___________^ - | "); } @@ -2053,7 +1978,6 @@ parenthesized_subscript_value = ( | ____________________________^ 3 | | key | |________^ - | info[folding-range]: Folding Range --> main.py:6:34 @@ -2062,7 +1986,6 @@ parenthesized_subscript_value = ( | __________________________________^ 7 | | data | |_________^ - | "); } @@ -2107,7 +2030,6 @@ multiline t-string 4 | | multiline string 5 | | """ | |___^ - | info[folding-range]: Folding Range --> main.py:7:19 @@ -2118,7 +2040,6 @@ multiline t-string 9 | | multiline bytes 10 | | """ | |___^ - | info[folding-range]: Folding Range --> main.py:12:21 @@ -2129,7 +2050,6 @@ multiline t-string 14 | | multiline f-string 15 | | """ | |___^ - | info[folding-range]: Folding Range --> main.py:17:21 @@ -2140,7 +2060,6 @@ multiline t-string 19 | | multiline t-string 20 | | """ | |___^ - | "#); } @@ -2175,7 +2094,6 @@ match value: 7 | | case _: 8 | | default() | |_________________^ - | info[folding-range]: Folding Range --> main.py:3:12 @@ -2184,7 +2102,6 @@ match value: | ____________^ 4 | | one() | |_____________^ - | info[folding-range]: Folding Range --> main.py:5:12 @@ -2193,7 +2110,6 @@ match value: | ____________^ 6 | | two() | |_____________^ - | info[folding-range]: Folding Range --> main.py:7:12 @@ -2202,7 +2118,6 @@ match value: | ____________^ 8 | | default() | |_________________^ - | "); } @@ -2233,7 +2148,6 @@ def main(): 3 | / import os 4 | | import sys | |__________^ - | info[folding-range]: Folding Range --> main.py:8:12 @@ -2242,7 +2156,6 @@ def main(): | ____________^ 9 | | pass | |________^ - | info[folding-range]: Folding Range (region) --> main.py:2:1 @@ -2252,7 +2165,6 @@ def main(): 4 | | import sys 5 | | # endregion | |___________^ - | info[folding-range]: Folding Range (region) --> main.py:7:1 @@ -2262,7 +2174,6 @@ def main(): 9 | | pass 10 | | # endregion | |___________^ - | "); } @@ -2293,7 +2204,6 @@ message = f""" 5 | | # endregion 6 | | """ | |___^ - | "#); } @@ -2326,7 +2236,6 @@ def my_function(): 6 | | """ 7 | | pass | |________^ - | info[folding-range]: Folding Range (comment) --> main.py:3:5 @@ -2336,7 +2245,6 @@ def my_function(): 5 | | docstring. 6 | | """ | |_______^ - | info[folding-range]: Folding Range --> main.py:3:5 @@ -2346,7 +2254,6 @@ def my_function(): 5 | | docstring. 6 | | """ | |_______^ - | "#); } @@ -2396,7 +2303,6 @@ def with_rawstring_doc(): 6 | | """ 7 | | pass | |________^ - | info[folding-range]: Folding Range (comment) --> main.py:3:5 @@ -2406,7 +2312,6 @@ def with_rawstring_doc(): 5 | | used as a docstring. 6 | | """ | |_______^ - | info[folding-range]: Folding Range --> main.py:3:5 @@ -2416,7 +2321,6 @@ def with_rawstring_doc(): 5 | | used as a docstring. 6 | | """ | |_______^ - | info[folding-range]: Folding Range --> main.py:10:24 @@ -2429,7 +2333,6 @@ def with_rawstring_doc(): 14 | | """ 15 | | pass | |________^ - | info[folding-range]: Folding Range (comment) --> main.py:11:5 @@ -2439,7 +2342,6 @@ def with_rawstring_doc(): 13 | | used as a docstring. 14 | | """ | |_______^ - | info[folding-range]: Folding Range --> main.py:11:5 @@ -2449,7 +2351,6 @@ def with_rawstring_doc(): 13 | | used as a docstring. 14 | | """ | |_______^ - | info[folding-range]: Folding Range --> main.py:18:26 @@ -2462,7 +2363,6 @@ def with_rawstring_doc(): 22 | | """ 23 | | pass | |________^ - | info[folding-range]: Folding Range (comment) --> main.py:19:5 @@ -2472,7 +2372,6 @@ def with_rawstring_doc(): 21 | | used as a docstring. 22 | | """ | |_______^ - | info[folding-range]: Folding Range --> main.py:19:5 @@ -2482,7 +2381,6 @@ def with_rawstring_doc(): 21 | | used as a docstring. 22 | | """ | |_______^ - | "#); } @@ -2516,7 +2414,6 @@ def foo(): | ___________^ 7 | | pass | |________^ - | info[folding-range]: Folding Range (comment) --> main.py:2:1 @@ -2525,7 +2422,6 @@ def foo(): 3 | | # that spans multiple lines 4 | | # explaining something important | |________________________________^ - | info[folding-range]: Folding Range (comment) --> main.py:9:1 @@ -2533,7 +2429,6 @@ def foo(): 9 | / # Another comment block 10 | | # with more details | |___________________^ - | ", ); } @@ -2561,7 +2456,6 @@ with open("file.txt") as f: 3 | | content = f.read() 4 | | process(content) | |____________________^ - | "#); } @@ -2615,7 +2509,6 @@ with open("file.txt") as f: 16 | | # Don't exceed the overall end date or max_days limit 17 | | c = 30 | |______________________________^ - | info[folding-range]: Folding Range (comment) --> main.py:3:21 @@ -2626,7 +2519,6 @@ with open("file.txt") as f: 6 | | into smaller chunks that can be requested individually. 7 | | """ | |_______________________^ - | info[folding-range]: Folding Range --> main.py:3:21 @@ -2637,7 +2529,6 @@ with open("file.txt") as f: 6 | | into smaller chunks that can be requested individually. 7 | | """ | |_______________________^ - | info[folding-range]: Folding Range --> main.py:11:42 @@ -2651,7 +2542,6 @@ with open("file.txt") as f: 16 | | # Don't exceed the overall end date or max_days limit 17 | | c = 30 | |______________________________^ - | info[folding-range]: Folding Range (comment) --> main.py:12:1 @@ -2659,7 +2549,6 @@ with open("file.txt") as f: 12 | / # Calculate the end of the current chunk 13 | | # Go to the last day of the current month | |_________________________________________________________________^ - | "#); } @@ -2693,7 +2582,6 @@ with open("file.txt") as f: | _______________^ 2 | | pass | |________^ - | "); // So does a single CRLF new-line. @@ -2708,7 +2596,6 @@ with open("file.txt") as f: | _______________^ 2 | | pass | |________^ - | "); // And so to does a single CR new-line. @@ -2723,13 +2610,16 @@ with open("file.txt") as f: | _______________^ 2 | | pass | |________^ - | "); } impl CursorTest { fn folding_ranges(&self) -> String { - let ranges = folding_ranges(&self.db, self.cursor.file, None); + let ranges = folding_ranges( + &self.db, + self.program_file(self.cursor.file).python_file(&self.db), + None, + ); if ranges.is_empty() { return "No folding ranges found".to_string(); @@ -2766,7 +2656,6 @@ def my_function(): | ___________________^ 4 | | pass | |________^ - | "); } @@ -2794,7 +2683,6 @@ def my_function(): | ___________________^ 6 | | pass | |________^ - | "); } @@ -2823,7 +2711,6 @@ class MyClass: 4 | | value: int 5 | | name: str | |_____________^ - | "); } @@ -2850,7 +2737,6 @@ class MyClass: | _______________^ 5 | | value: int | |______________^ - | "); } @@ -2876,7 +2762,6 @@ async def my_async_function(): | _______________________________^ 4 | | pass | |________^ - | "); } @@ -2905,7 +2790,6 @@ def outer_function(): 4 | | def inner_function(): 5 | | pass | |____________^ - | info[folding-range]: Folding Range --> main.py:4:26 @@ -2914,7 +2798,6 @@ def outer_function(): | __________________________^ 5 | | pass | |____________^ - | "); } @@ -2943,7 +2826,6 @@ class MyClass: 4 | | async def my_async_method(self): 5 | | pass | |____________^ - | info[folding-range]: Folding Range --> main.py:4:37 @@ -2952,7 +2834,6 @@ class MyClass: | _____________________________________^ 5 | | pass | |____________^ - | "); } diff --git a/crates/ty_ide/src/goto.rs b/crates/ty_ide/src/goto.rs index b969ac180c..d36f20226e 100644 --- a/crates/ty_ide/src/goto.rs +++ b/crates/ty_ide/src/goto.rs @@ -2,6 +2,7 @@ use crate::docstring::Docstring; pub use crate::goto_declaration::goto_declaration; pub use crate::goto_definition::goto_definition; pub use crate::goto_type_definition::goto_type_definition; +use ty_python_semantic::Db; use std::borrow::Cow; @@ -13,17 +14,18 @@ use ruff_python_ast::token::{Token, TokenAt, TokenKind, Tokens}; use ruff_python_ast::{self as ast, AnyNodeRef, ExprRef}; use ruff_text_size::{Ranged, TextRange, TextSize}; +use ty_python_core::ProgramFile; use ty_python_core::definition::{Definition, DefinitionKind}; -use ty_python_semantic::ResolvedDefinition; use ty_python_semantic::types::Type; use ty_python_semantic::types::ide_support::{ call_signature_details, call_type_simplified_by_overloads, constructor_signature, definitions_and_overloads_for_function, definitions_for_keyword_argument, typed_dict_key_definition, }; +use ty_python_semantic::{Db as SemanticDb, ResolvedDefinition}; use ty_python_semantic::{ - HasDefinition, HasType, ImportAliasResolution, SemanticModel, TypeQualifiers, - definitions_for_imported_symbol, definitions_for_name, + HasDefinition, HasType, ImportAliasResolution, ProgramEnvironment, SemanticModel, + TypeQualifiers, definitions_for_imported_symbol, definitions_for_name, }; #[derive(Clone, Debug)] @@ -246,7 +248,7 @@ pub(crate) enum GotoTarget<'a> { pub(crate) struct Definitions<'db>(Vec>); impl<'db> Definitions<'db> { - fn new(mut resolved: Vec>) -> Self { + pub(crate) fn new(mut resolved: Vec>) -> Self { for index in (1..resolved.len()).rev() { if resolved[..index].contains(&resolved[index]) { resolved.remove(index); @@ -256,11 +258,15 @@ impl<'db> Definitions<'db> { Self(resolved) } - pub(crate) fn from_ty(db: &'db dyn crate::Db, ty: Type<'db>) -> Option { - let ty_def = ty.definition(db)?; + pub(crate) fn from_ty( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + ) -> Option { + let ty_def = ty.definition(db, env)?; let resolved = match ty_def { ty_python_semantic::types::TypeDefinition::Module(module) => { - ResolvedDefinition::Module(module.file(db)?) + ResolvedDefinition::Module(ProgramFile::new(db, module.file(db)?, env.program(db))) } ty_python_semantic::types::TypeDefinition::StaticClass(definition) | ty_python_semantic::types::TypeDefinition::DynamicClass(definition) @@ -339,8 +345,43 @@ impl<'db> Definitions<'db> { goto_target: &GotoTarget<'_>, ) -> Option> { let definitions = self.goto_declaration(model, goto_target)?; - let resolved = StubMapper::new(model.db()).map_definitions(definitions.0); - Some(Self::new(resolved)) + Some(definitions.map_stubs(model.db())) + } + + /// Map definitions from stub files to corresponding source implementations. + fn map_stubs(self, db: &'db dyn ty_python_semantic::Db) -> Definitions<'db> { + let resolved = StubMapper::new(db).map_definitions(self.0); + Self::new(resolved) + } + + /// Map stub definitions to corresponding source implementations for implementation lookup. + /// + /// Stub definitions without source mappings are discarded. Returns `None` if no definitions + /// remain. + pub(crate) fn map_stubs_for_implementation( + self, + db: &'db dyn ty_python_semantic::Db, + ) -> Option> { + let stub_mapper = StubMapper::new(db); + let resolved: Vec<_> = self + .0 + .into_iter() + .flat_map(|definition| { + if definition.focus_range(db).file().is_stub(db) { + stub_mapper + .map_definition_to_source(&definition) + .unwrap_or_default() + } else { + vec![definition] + } + }) + .collect(); + + if resolved.is_empty() { + None + } else { + Some(Self::new(resolved)) + } } /// Convert these semantic definitions to editor-facing navigation targets. @@ -352,8 +393,8 @@ impl<'db> Definitions<'db> { .into_iter() .map(|definition| match definition { ResolvedDefinition::Definition(definition) => { - let file = definition.file(db); - let module = ruff_db::parsed::parsed_module(db, file).load(db); + let module = + ruff_db::parsed::parsed_module(db, definition.python_file(db)).load(db); let focus_range = definition.focus_range(db, &module); let full_range = definition.full_range(db, &module); @@ -365,7 +406,7 @@ impl<'db> Definitions<'db> { } } ResolvedDefinition::Module(file) => { - NavigationTarget::new(file, TextRange::default()) + NavigationTarget::new(file.file(db), TextRange::default()) } ResolvedDefinition::FileWithRange(file_range) => NavigationTarget::from(file_range), }) @@ -377,7 +418,7 @@ impl<'db> Definitions<'db> { /// Typically documentation only appears on implementations and not stubs, /// so this will check both the goto-declarations and goto-definitions (in that order) /// and return the first one found. - pub(crate) fn docstring(self, db: &'db dyn crate::Db) -> Option { + pub(crate) fn docstring(self, db: &'db dyn SemanticDb) -> Option { for definition in &self { // If we got a docstring from the original definition, use it if let Some(docstring) = definition.docstring(db) { @@ -439,7 +480,7 @@ impl<'a, 'db> IntoIterator for &'a Definitions<'db> { /// Shared by hover and signature help so both surfaces render the same /// docstring for a given call site. pub(crate) fn docstring_for_call_definition<'db>( - db: &'db dyn crate::Db, + db: &'db dyn Db, definition: Definition<'db>, ) -> Option { let resolved = ResolvedDefinition::Definition(definition); @@ -464,6 +505,7 @@ impl GotoTarget<'_> { // (i.e. the type of `MyClass` in `MyClass()` is `` and not `() -> MyClass`) GotoTarget::Call { callable, .. } => callable.inferred_type(model), GotoTarget::TypeParamTypeVarName(typevar) => typevar.inferred_type(model), + GotoTarget::TypeParamParamSpecName(typevar) => typevar.inferred_type(model), GotoTarget::ImportModuleComponent { module_name, component_index, @@ -525,7 +567,6 @@ impl GotoTarget<'_> { | GotoTarget::PatternKeywordArgument(_) | GotoTarget::PatternMatchStarName(_) | GotoTarget::PatternMatchAsName(_) - | GotoTarget::TypeParamParamSpecName(_) | GotoTarget::TypeParamTypeVarTupleName(_) | GotoTarget::NonLocal { .. } | GotoTarget::Globals { .. } => None, @@ -550,6 +591,24 @@ impl GotoTarget<'_> { } } + /// Gets definitions for the underlying expression, excluding call dispatch targets. + pub(crate) fn expression_definitions<'db>( + &self, + model: &SemanticModel<'db>, + alias_resolution: ImportAliasResolution, + ) -> Option> { + let expression = match self { + GotoTarget::Expression(expression) + | GotoTarget::Call { + callable: expression, + .. + } => *expression, + _ => return None, + }; + + definitions_for_expression(model, expression, alias_resolution).map(Definitions::new) + } + /// Gets the definitions for this goto target. /// /// The `alias_resolution` parameter controls whether import aliases @@ -1374,7 +1433,7 @@ pub(crate) fn find_goto_target<'a>( find_goto_target_impl(model, parsed.tokens(), parsed.syntax().into(), offset) } -pub(crate) fn find_goto_target_impl<'a>( +fn find_goto_target_impl<'a>( model: &'a SemanticModel, tokens: &'a Tokens, syntax: AnyNodeRef<'a>, @@ -1423,7 +1482,7 @@ fn definitions_for_module<'db>( level: u32, ) -> Option>> { let module = model.resolve_module(module, level)?; - let file = module.file(model.db())?; + let file = ProgramFile::new(model.db(), module.file(model.db())?, model.program()); Some(vec![ResolvedDefinition::Module(file)]) } diff --git a/crates/ty_ide/src/goto_declaration.rs b/crates/ty_ide/src/goto_declaration.rs index d0786770df..f20df34040 100644 --- a/crates/ty_ide/src/goto_declaration.rs +++ b/crates/ty_ide/src/goto_declaration.rs @@ -1,8 +1,9 @@ use crate::goto::{django_lookup_definitions, find_goto_target}; use crate::{Db, NavigationTargets, RangedValue}; -use ruff_db::files::{File, FileRange}; +use ruff_db::files::FileRange; use ruff_db::parsed::parsed_module; use ruff_text_size::{Ranged, TextSize}; +use ty_python_core::ProgramFile; use ty_python_semantic::{ImportAliasResolution, SemanticModel}; /// Navigate to the declaration of a symbol. @@ -12,10 +13,10 @@ use ty_python_semantic::{ImportAliasResolution, SemanticModel}; /// is needed because Python doesn't require formal declarations of variables like most languages do. pub fn goto_declaration( db: &dyn Db, - file: File, + file: ProgramFile<'_>, offset: TextSize, ) -> Option> { - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, file.python_file(db)).load(db); let model = SemanticModel::new(db, file); let goto_target = find_goto_target(&model, &module, offset)?; @@ -29,7 +30,7 @@ pub fn goto_declaration( .into_navigation_targets(model.db()); Some(RangedValue { - range: FileRange::new(file, goto_target.range()), + range: FileRange::new(file.file(db), goto_target.range()), value: declaration_targets, }) } @@ -57,13 +58,11 @@ mod tests { | 5 | result = my_function(1, 2) | ^^^^^^^^^^^ Clicking here - | info: Found 1 declaration --> main.py:2:5 | 2 | def my_function(x, y): | ----------- - | "); } @@ -82,13 +81,11 @@ mod tests { | 3 | y = x | ^ Clicking here - | info: Found 1 declaration --> main.py:2:1 | 2 | x = 42 | - - | "); } @@ -113,13 +110,11 @@ mod tests { | 9 | person["name"] | ^^^^^^ Clicking here - | info: Found 1 declaration --> main.py:5:5 | 5 | name: str | ---- - | "#); } @@ -141,13 +136,11 @@ mod tests { | 6 | instance = MyClass() | ^^^^^^^ Clicking here - | info: Found 1 declaration --> main.py:2:7 | 2 | class MyClass: | ------- - | "); } @@ -166,13 +159,11 @@ mod tests { | 3 | return param * 2 | ^^^^^ Clicking here - | info: Found 1 declaration --> main.py:2:9 | 2 | def foo(param): | ----- - | "); } @@ -192,13 +183,11 @@ mod tests { | 3 | v: T = value | ^ Clicking here - | info: Found 1 declaration --> main.py:2:18 | 2 | def generic_func[T](value: T) -> T: | - - | "); } @@ -218,13 +207,11 @@ mod tests { | 3 | def __init__(self, value: T): | ^ Clicking here - | info: Found 1 declaration --> main.py:2:20 | 2 | class GenericClass[T]: | - - | "); } @@ -246,13 +233,11 @@ mod tests { | 5 | return x # Should find outer x | ^ Clicking here - | info: Found 1 declaration --> main.py:2:1 | 2 | x = "outer" | - - | "#); } @@ -301,13 +286,11 @@ variable = 42 | 3 | print(mymodule.function()) | ^^^^^^^^ Clicking here - | info: Found 1 declaration --> mymodule.py:1:1 | 1 | | - - | "); } @@ -339,13 +322,11 @@ def other_function(): | 3 | print(my_function()) | ^^^^^^^^^^^ Clicking here - | info: Found 1 declaration --> mymodule.py:2:5 | 2 | def my_function(): | ----------- - | "); } @@ -380,13 +361,11 @@ FOO = 0 | 3 | print(sub.helper()) | ^^^ Clicking here - | info: Found 1 declaration --> mymodule/submodule.py:1:1 | 1 | | - - | "); } @@ -405,11 +384,11 @@ FOO = 0 | 1 | from lib import module | ^^^^^^ Clicking here - | info: Found 1 declaration - --> lib/module.py:1:1 - | - | + --> lib/module.py:1:1 + | + 1 | + | - "); } @@ -439,13 +418,11 @@ def func(arg): | 3 | print(h("test")) | ^ Clicking here - | info: Found 1 declaration --> utils.py:2:5 | 2 | def func(arg): | ---- - | "#); } @@ -481,13 +458,11 @@ def shared_function(): | 3 | print(shared_function()) | ^^^^^^^^^^^^^^^ Clicking here - | info: Found 1 declaration --> original.py:2:5 | 2 | def shared_function(): | --------------- - | "); } @@ -521,13 +496,11 @@ def multiply_numbers(a, b): | 3 | result = add_numbers(5, 3) | ^^^^^^^^^^^ Clicking here - | info: Found 1 declaration --> math_utils.py:2:5 | 2 | def add_numbers(a, b): | ----------- - | "); } @@ -568,13 +541,11 @@ def another_helper(): | 3 | result = helper_function("test") | ^^^^^^^^^^^^^^^ Clicking here - | info: Found 1 declaration --> package/utils.py:2:5 | 2 | def helper_function(arg): | --------------- - | "#); } @@ -614,13 +585,11 @@ def another_helper(): | 3 | result = helper_function("test") | ^^^^^^^^^^^^^^^ Clicking here - | info: Found 1 declaration --> package/utils.py:2:5 | 2 | def helper_function(arg): | --------------- - | "#); } @@ -654,13 +623,11 @@ FOO = 0 | 2 | import mymodule.submodule as sub | ^^^ Clicking here - | info: Found 1 declaration --> mymodule/submodule.py:1:1 | 1 | | - - | "); } @@ -694,13 +661,11 @@ FOO = 0 | 2 | import mymodule.submodule as sub | ^^^^^^^^^ Clicking here - | info: Found 1 declaration --> mymodule/submodule.py:1:1 | 1 | | - - | "); } @@ -738,13 +703,11 @@ def another_helper(path): | 2 | from mypackage.utils import helper as h | ^^^^^^ Clicking here - | info: Found 1 declaration --> mypackage/utils.py:2:5 | 2 | def helper(a, b): | ------ - | "); } @@ -782,13 +745,11 @@ def another_helper(path): | 2 | from mypackage.utils import helper as h | ^ Clicking here - | info: Found 1 declaration --> mypackage/utils.py:2:5 | 2 | def helper(a, b): | ------ - | "); } @@ -826,13 +787,11 @@ def another_helper(path): | 2 | from mypackage.utils import helper as h | ^^^^^ Clicking here - | info: Found 1 declaration --> mypackage/utils.py:1:1 | 1 | | - - | "); } @@ -855,13 +814,11 @@ def another_helper(path): | 7 | y = c.x | ^ Clicking here - | info: Found 1 declaration --> main.py:4:9 | 4 | self.x: int = 1 | ------ - | "); } @@ -882,13 +839,11 @@ def another_helper(path): | 2 | a: "MyClass" = 1 | ^^^^^^^ Clicking here - | info: Found 1 declaration --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -909,13 +864,11 @@ def another_helper(path): | 2 | a: "None | MyClass" = 1 | ^^^^^^^ Clicking here - | info: Found 1 declaration --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -950,13 +903,11 @@ def another_helper(path): | 2 | a: "None | MyClass" = 1 | ^^^^^^^ Clicking here - | info: Found 1 declaration --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -1005,13 +956,11 @@ def another_helper(path): | 2 | a: "MyClass | No" = 1 | ^^^^^^^ Clicking here - | info: Found 1 declaration --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -1043,13 +992,11 @@ def another_helper(path): | 2 | ab: "ab" | ^^ Clicking here - | info: Found 1 declaration --> main.py:2:1 | 2 | ab: "ab" | -- - | "#); } @@ -1081,13 +1028,11 @@ def another_helper(path): | 2 | x: "list['MyClass | int'] | None" | ^^^^^^^ Clicking here - | info: Found 1 declaration --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -1108,13 +1053,11 @@ def another_helper(path): | 2 | x: "list['int | MyClass'] | None" | ^^^^^^^ Clicking here - | info: Found 1 declaration --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -1135,13 +1078,11 @@ def another_helper(path): | 2 | x: "list['int | None'] | MyClass" | ^^^^^^^ Clicking here - | info: Found 1 declaration --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -1162,13 +1103,11 @@ def another_helper(path): | 2 | x: "list['int' | 'MyClass'] | None" | ^^^^^^^ Clicking here - | info: Found 1 declaration --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -1189,13 +1128,11 @@ def another_helper(path): | 2 | x: "list['MyClass' | 'str'] | None" | ^^^^^^^ Clicking here - | info: Found 1 declaration --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -1230,13 +1167,11 @@ def another_helper(path): | 2 | x: """'list["int" | "str"]' | MyClass""" | ^^^^^^^ Clicking here - | info: Found 1 declaration --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -1263,13 +1198,11 @@ def another_helper(path): | 11 | y = d.y.x | ^ Clicking here - | info: Found 1 declaration --> main.py:4:9 | 4 | self.x: int = 1 | ------ - | "); } @@ -1292,13 +1225,11 @@ def another_helper(path): | 7 | y = c.x | ^ Clicking here - | info: Found 1 declaration --> main.py:4:9 | 4 | self.x = 1 | ------ - | "); } @@ -1321,13 +1252,11 @@ def another_helper(path): | 7 | res = c.foo() | ^^^ Clicking here - | info: Found 1 declaration --> main.py:3:9 | 3 | def foo(self): | --- - | "); } @@ -1391,13 +1320,11 @@ def outer(): | 8 | return x # Should find the nonlocal x declaration in outer scope | ^ Clicking here - | info: Found 1 declaration --> main.py:3:5 | 3 | x = "outer_value" | - - | "#); } @@ -1424,13 +1351,11 @@ def outer(): | 6 | nonlocal xy | ^^ Clicking here - | info: Found 1 declaration --> main.py:3:5 | 3 | xy = "outer_value" | -- - | "#); } @@ -1454,13 +1379,11 @@ def function(): | 7 | return global_var # Should find the global variable declaration | ^^^^^^^^^^ Clicking here - | info: Found 1 declaration --> main.py:2:1 | 2 | global_var = "global_value" | ---------- - | "#); } @@ -1484,13 +1407,11 @@ def function(): | 5 | global global_var | ^^^^^^^^^^ Clicking here - | info: Found 1 declaration --> main.py:2:1 | 2 | global_var = "global_value" | ---------- - | "#); } @@ -1515,13 +1436,11 @@ def function(): | 9 | y = b.x | ^ Clicking here - | info: Found 1 declaration --> main.py:3:5 | 3 | x = 10 | - - | "); } @@ -1542,13 +1461,11 @@ def function(): | 4 | case ["get", ab]: | ^^ Clicking here - | info: Found 1 declaration --> main.py:4:22 | 4 | case ["get", ab]: | -- - | "#); } @@ -1569,13 +1486,11 @@ def function(): | 5 | x = ab | ^^ Clicking here - | info: Found 1 declaration --> main.py:4:22 | 4 | case ["get", ab]: | -- - | "#); } @@ -1596,13 +1511,11 @@ def function(): | 4 | case ["get", *ab]: | ^^ Clicking here - | info: Found 1 declaration --> main.py:4:23 | 4 | case ["get", *ab]: | -- - | "#); } @@ -1623,13 +1536,11 @@ def function(): | 5 | x = ab | ^^ Clicking here - | info: Found 1 declaration --> main.py:4:23 | 4 | case ["get", *ab]: | -- - | "#); } @@ -1650,13 +1561,11 @@ def function(): | 4 | case ["get", ("a" | "b") as ab]: | ^^ Clicking here - | info: Found 1 declaration --> main.py:4:37 | 4 | case ["get", ("a" | "b") as ab]: | -- - | "#); } @@ -1677,13 +1586,11 @@ def function(): | 5 | x = ab | ^^ Clicking here - | info: Found 1 declaration --> main.py:4:37 | 4 | case ["get", ("a" | "b") as ab]: | -- - | "#); } @@ -1710,13 +1617,11 @@ def function(): | 10 | case Click(x, button=ab): | ^^ Clicking here - | info: Found 1 declaration --> main.py:10:30 | 10 | case Click(x, button=ab): | -- - | "); } @@ -1743,13 +1648,11 @@ def function(): | 11 | x = ab | ^^ Clicking here - | info: Found 1 declaration --> main.py:10:30 | 10 | case Click(x, button=ab): | -- - | "); } @@ -1776,13 +1679,11 @@ def function(): | 10 | case Click(x, button=ab): | ^^^^^ Clicking here - | info: Found 1 declaration --> main.py:2:7 | 2 | class Click: | ----- - | "); } @@ -1820,13 +1721,11 @@ def function(): | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] | ^^ Clicking here - | info: Found 1 declaration --> main.py:2:13 | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] | -- - | "); } @@ -1844,13 +1743,11 @@ def function(): | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] | ^^ Clicking here - | info: Found 1 declaration --> main.py:2:13 | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] | -- - | "); } @@ -1869,13 +1766,11 @@ def function(): | 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] | ^^ Clicking here - | info: Found 1 declaration --> main.py:3:15 | 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] | -- - | "); } @@ -1894,13 +1789,11 @@ def function(): | 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] | ^^ Clicking here - | info: Found 1 declaration --> main.py:3:15 | 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] | -- - | "); } @@ -1918,13 +1811,11 @@ def function(): | 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] | ^^ Clicking here - | info: Found 1 declaration --> main.py:2:14 | 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] | -- - | "); } @@ -1942,13 +1833,11 @@ def function(): | 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] | ^^ Clicking here - | info: Found 1 declaration --> main.py:2:14 | 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] | -- - | "); } @@ -1975,13 +1864,11 @@ def function(): | 11 | c.value = 42 | ^^^^^ Clicking here - | info: Found 1 declaration --> main.py:7:9 | 7 | def value(self): | ----- - | "); } @@ -2048,13 +1935,11 @@ def function(): | 9 | obj.name | ^^^^ Clicking here - | info: Found 1 declaration --> main.py:6:5 | 6 | name: str | ---- - | "); } @@ -2077,13 +1962,11 @@ class MyClass: | 5 | def generic_method[T](self, value: ClassType) -> T: | ^^^^^^^^^ Clicking here - | info: Found 1 declaration --> main.py:3:5 | 3 | ClassType = int | --------- - | "); } @@ -2104,13 +1987,11 @@ class MyClass: | 5 | result = my_function(1, y=2, z=3) | ^ Clicking here - | info: Found 1 declaration --> main.py:2:20 | 2 | def my_function(x, y, z=10): | - - | "); } @@ -2141,7 +2022,6 @@ class MyClass: | 14 | result = process("hello", format="json") | ^^^^^^ Clicking here - | info: Found 2 declarations --> main.py:5:24 | @@ -2151,7 +2031,6 @@ class MyClass: 7 | @overload 8 | def process(data: int, format: int) -> int: ... | ------ - | "#); } @@ -2175,13 +2054,11 @@ class MyClass: | 8 | TD(f=1) | ^ Clicking here - | info: Found 1 declaration --> main.py:5:5 | 5 | f: int | - - | "); } @@ -2205,13 +2082,11 @@ class MyClass: | 8 | NT(f=1) | ^ Clicking here - | info: Found 1 declaration --> main.py:5:5 | 5 | f: int | - - | "); } @@ -2236,13 +2111,11 @@ class MyClass: | 9 | DC(f=1) | ^ Clicking here - | info: Found 1 declaration --> main.py:6:5 | 6 | f: int | - - | "); } @@ -2269,13 +2142,11 @@ class MyClass: | 11 | DC(f=1) | ^ Clicking here - | info: Found 1 declaration --> main.py:9:24 | 9 | def __init__(self, f: int) -> None: ... | - - | "); } @@ -2303,13 +2174,11 @@ class MyClass: | 12 | DC(g=1) | ^ Clicking here - | info: Found 1 declaration --> main.py:10:5 | 10 | f: int = Field(alias='g') | - - | "); } @@ -2351,7 +2220,6 @@ def ab(a: str): ... | 4 | ab(1) | ^^ Clicking here - | info: Found 2 declarations --> mymodule.pyi:5:5 | @@ -2361,7 +2229,6 @@ def ab(a: str): ... 7 | @overload 8 | def ab(a: str): ... | -- - | "); } @@ -2403,7 +2270,6 @@ def ab(a: str): ... | 4 | ab("hello") | ^^ Clicking here - | info: Found 2 declarations --> mymodule.pyi:5:5 | @@ -2413,7 +2279,6 @@ def ab(a: str): ... 7 | @overload 8 | def ab(a: str): ... | -- - | "#); } @@ -2455,7 +2320,6 @@ def ab(a: int): ... | 4 | ab(1, 2) | ^^ Clicking here - | info: Found 2 declarations --> mymodule.pyi:5:5 | @@ -2465,7 +2329,6 @@ def ab(a: int): ... 7 | @overload 8 | def ab(a: int): ... | -- - | "); } @@ -2507,7 +2370,6 @@ def ab(a: int): ... | 4 | ab(1) | ^^ Clicking here - | info: Found 2 declarations --> mymodule.pyi:5:5 | @@ -2517,7 +2379,6 @@ def ab(a: int): ... 7 | @overload 8 | def ab(a: int): ... | -- - | "); } @@ -2562,7 +2423,6 @@ def ab(a: int, *, c: int): ... | 4 | ab(1, b=2) | ^^ Clicking here - | info: Found 3 declarations --> mymodule.pyi:5:5 | @@ -2576,7 +2436,6 @@ def ab(a: int, *, c: int): ... 10 | @overload 11 | def ab(a: int, *, c: int): ... | -- - | "); } @@ -2621,7 +2480,6 @@ def ab(a: int, *, c: int): ... | 4 | ab(1, c=2) | ^^ Clicking here - | info: Found 3 declarations --> mymodule.pyi:5:5 | @@ -2635,7 +2493,6 @@ def ab(a: int, *, c: int): ... 10 | @overload 11 | def ab(a: int, *, c: int): ... | -- - | "); } @@ -2665,13 +2522,11 @@ def ab(a: int, *, c: int): ... | 4 | x = subpkg | ^^^^^^ Clicking here - | info: Found 1 declaration --> mypackage/__init__.py:2:7 | 2 | from .subpkg.submod import val | ------ - | "); } @@ -2705,11 +2560,11 @@ def ab(a: int, *, c: int): ... | 2 | from .subpkg.submod import val | ^^^^^^ Clicking here - | info: Found 1 declaration - --> mypackage/subpkg/__init__.py:1:1 - | - | + --> mypackage/subpkg/__init__.py:1:1 + | + 1 | + | - "); } @@ -2764,13 +2619,11 @@ def ab(a: int, *, c: int): ... | 2 | from .subpkg.submod import val | ^^^^^^ Clicking here - | info: Found 1 declaration --> mypackage/subpkg/submod.py:1:1 | 1 | | - - | "); } @@ -2800,13 +2653,11 @@ def ab(a: int, *, c: int): ... | 2 | from .subpkg import subpkg | ^^^^^^ Clicking here - | info: Found 1 declaration --> mypackage/subpkg/__init__.py:1:1 | 1 | | - - | "); } @@ -2836,13 +2687,11 @@ def ab(a: int, *, c: int): ... | 2 | from .subpkg import subpkg | ^^^^^^ Clicking here - | info: Found 1 declaration --> mypackage/subpkg/__init__.py:2:1 | 2 | subpkg: int = 10 | ------ - | "); } @@ -2873,13 +2722,11 @@ def ab(a: int, *, c: int): ... | 4 | x = subpkg | ^^^^^^ Clicking here - | info: Found 1 declaration --> mypackage/subpkg/__init__.py:2:1 | 2 | subpkg: int = 10 | ------ - | "); } @@ -2907,7 +2754,6 @@ def ab(a: int, *, c: int): ... | 6 | print(a) | ^ Clicking here - | info: Found 3 declarations --> main.py:2:1 | @@ -2921,7 +2767,6 @@ def ab(a: int, *, c: int): ... 7 | 8 | a: bool = True | - - | "#); } @@ -2944,20 +2789,22 @@ def q(): | 5 | return Book.objects.filter(author.name == "x") | ^^^^^^ Clicking here - | info: Found 1 declaration --> src/blog/models.py:10:5 | 10 | author = models.ForeignKey(Author, on_delete=models.CASCADE) | ------ - | "#); } impl CursorTest { fn goto_declaration(&self) -> String { let Some(targets) = salsa::attach(&self.db, || { - goto_declaration(&self.db, self.cursor.file, self.cursor.offset) + goto_declaration( + &self.db, + self.program_file(self.cursor.file), + self.cursor.offset, + ) }) else { return "No goto target found".to_string(); }; diff --git a/crates/ty_ide/src/goto_definition.rs b/crates/ty_ide/src/goto_definition.rs index 86add5cfbb..fb222e7b40 100644 --- a/crates/ty_ide/src/goto_definition.rs +++ b/crates/ty_ide/src/goto_definition.rs @@ -1,9 +1,10 @@ use crate::django_template::django_string_definition; use crate::goto::{django_lookup_definitions, find_goto_target}; use crate::{Db, NavigationTargets, RangedValue}; -use ruff_db::files::{File, FileRange}; +use ruff_db::files::FileRange; use ruff_db::parsed::parsed_module; use ruff_text_size::{Ranged, TextSize}; +use ty_python_core::ProgramFile; use ty_python_semantic::{ImportAliasResolution, SemanticModel}; /// Navigate to the definition of a symbol. @@ -14,14 +15,14 @@ use ty_python_semantic::{ImportAliasResolution, SemanticModel}; /// source file implementations using the `StubMapper`. pub fn goto_definition( db: &dyn Db, - file: File, + file: ProgramFile<'_>, offset: TextSize, ) -> Option> { - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, file.python_file(db)).load(db); // a template name and a url name are plain strings, so python's own answer // for one is whatever `str` is — which is never where the user wanted to go - if let Some(named) = django_string_definition(db, file, &module, offset) { + if let Some(named) = django_string_definition(db, file.file(db), &module, offset) { return Some(named); } @@ -35,7 +36,7 @@ pub fn goto_definition( .into_navigation_targets(model.db()); Some(RangedValue { - range: FileRange::new(file, goto_target.range()), + range: FileRange::new(file.file(db), goto_target.range()), value: definition_targets, }) } @@ -43,7 +44,7 @@ pub fn goto_definition( #[cfg(test)] pub(super) mod test { - use crate::tests::{CursorTest, IntoDiagnostic}; + use crate::tests::{CursorTest, IntoDiagnostic, cursor_test}; use crate::{NavigationTargets, RangedValue, goto_definition}; use insta::assert_snapshot; use ruff_db::diagnostic::{ @@ -52,6 +53,152 @@ pub(super) mod test { }; use ruff_text_size::Ranged; + #[test] + fn goto_definition_does_not_mix_global_and_nonlocal_comprehension_walruses() { + let test = cursor_test( + " +last = 0 + +def outer(): + last = 1 + + def write_global(): + global last + [(last := global_item) for global_item in [2]] + + def write_nonlocal(): + nonlocal last + [(last := nonlocal_item) for nonlocal_item in [3]] + + write_global() + write_nonlocal() + return last +", + ); + + assert_snapshot!(test.goto_definition(), @" + info[goto-definition]: Go to definition + --> main.py:17:12 + | + 17 | return last + | ^^^^ Clicking here + info: Found 2 definitions + --> main.py:5:5 + | + 5 | last = 1 + | ---- + | + ::: main.py:13:11 + | + 13 | [(last := nonlocal_item) for nonlocal_item in [3]] + | ---- + "); + } + + #[test] + fn goto_definition_comprehension_walrus_in_function() { + let test = cursor_test( + " +def f(items): + [(last := item) for item in items] + return last +", + ); + + assert_snapshot!(test.goto_definition(), @" + info[goto-definition]: Go to definition + --> main.py:4:12 + | + 4 | return last + | ^^^^ Clicking here + info: Found 1 definition + --> main.py:3:7 + | + 3 | [(last := item) for item in items] + | ---- + "); + } + + #[test] + fn goto_definition_nested_comprehension_walrus_in_function() { + let test = cursor_test( + " +def f(items): + [[(last := item) for item in items] for _ in [1]] + return last +", + ); + + assert_snapshot!(test.goto_definition(), @" + info[goto-definition]: Go to definition + --> main.py:4:12 + | + 4 | return last + | ^^^^ Clicking here + info: Found 1 definition + --> main.py:3:8 + | + 3 | [[(last := item) for item in items] for _ in [1]] + | ---- + "); + } + + #[test] + fn goto_definition_imported_comprehension_walrus() { + let test = CursorTest::builder() + .source("lib.py", "[(last := item) for item in [1]]\n") + .source("main.py", "from lib import last\nprint(last)\n") + .build(); + + assert_snapshot!(test.goto_definition(), @" + info[goto-definition]: Go to definition + --> main.py:2:7 + | + 2 | print(last) + | ^^^^ Clicking here + info: Found 1 definition + --> lib.py:1:3 + | + 1 | [(last := item) for item in [1]] + | ---- + "); + } + + #[test] + fn goto_definition_nonlocal_comprehension_walrus() { + let test = cursor_test( + " +def outer(items): + last = 0 + + def inner(): + nonlocal last + [(last := item) for item in items] + return last + + return inner() +", + ); + + assert_snapshot!(test.goto_definition(), @" + info[goto-definition]: Go to definition + --> main.py:8:16 + | + 8 | return last + | ^^^^ Clicking here + info: Found 2 definitions + --> main.py:3:5 + | + 3 | last = 0 + | ---- + 4 | + 5 | def inner(): + 6 | nonlocal last + 7 | [(last := item) for item in items] + | ---- + "); + } + #[test] fn goto_definition_relative_import() { let test = CursorTest::builder() @@ -65,13 +212,11 @@ pub(super) mod test { | 1 | from . import module_a | ^^^^^^^^ Clicking here - | info: Found 1 definition --> mypackage/module_a.py:1:1 | 1 | class Test: ... | - - | "); } @@ -91,13 +236,11 @@ pub(super) mod test { | 2 | x = module_a | ^^^^^^^^ Clicking here - | info: Found 1 definition --> mypackage/module_a.py:1:1 | 1 | class Test: ... | - - | "); } @@ -118,13 +261,11 @@ pub(super) mod test { | 2 | x = module_a | ^^^^^^^^ Clicking here - | info: Found 1 definition --> mypackage/module_a.py:1:1 | 1 | class Test: ... | - - | "); } @@ -162,13 +303,11 @@ def my_function(): ... | 2 | from mymodule import my_function | ^^^^^^^^ Clicking here - | info: Found 1 definition --> mymodule.py:1:1 | 1 | | - - | "); } @@ -204,13 +343,11 @@ def my_function(): ... | 3 | x = mymodule | ^^^^^^^^ Clicking here - | info: Found 1 definition --> mymodule.py:1:1 | 1 | | - - | "); } @@ -251,13 +388,11 @@ def other_function(): ... | 3 | print(my_function()) | ^^^^^^^^^^^ Clicking here - | info: Found 1 definition --> mymodule.py:2:5 | 2 | def my_function(): | ----------- - | "); } @@ -288,13 +423,11 @@ def bar() -> None: | 3 | bar() | ^^^ Clicking here - | info: Found 1 definition --> a/impl.py:2:5 | 2 | def bar() -> None: | --- - | "); } @@ -328,13 +461,11 @@ def other_function(): ... | 2 | def my_function(): ... | ^^^^^^^^^^^ Clicking here - | info: Found 1 definition --> mymodule.py:2:5 | 2 | def my_function(): | ----------- - | "); } @@ -385,7 +516,6 @@ def other_function(): ... | 3 | print(my_function()) | ^^^^^^^^^^^ Clicking here - | info: Found 3 definitions --> mymodule.py:2:5 | @@ -399,7 +529,6 @@ def other_function(): ... 7 | 8 | def my_function(): | ----------- - | "#); } @@ -444,13 +573,11 @@ class MyOtherClass: | 3 | x = MyClass | ^^^^^^^ Clicking here - | info: Found 1 definition --> mymodule.py:2:7 | 2 | class MyClass: | ------- - | "); } @@ -488,13 +615,11 @@ class MyOtherClass: | 2 | class MyClass: | ^^^^^^^ Clicking here - | info: Found 1 definition --> mymodule.py:2:7 | 2 | class MyClass: | ------- - | "); } @@ -539,13 +664,11 @@ class MyOtherClass: | 3 | x = MyClass(0) | ^^^^^^^ Clicking here - | info: Found 1 definition --> mymodule.py:2:7 | 2 | class MyClass: | ------- - | "); } @@ -594,13 +717,92 @@ class MyOtherClass: | 4 | x.action() | ^^^^^^ Clicking here - | info: Found 1 definition --> mymodule.py:5:9 | 5 | def action(self): | ------ + "); + } + + /// goto-definition on a class attribute should go to the .py not the .pyi + #[test] + fn goto_definition_stub_map_class_attribute() { + let test = CursorTest::builder() + .source( + "main.py", + " +from mymodule import MyClass +def f(x: MyClass): + x.sound +", + ) + .source( + "mymodule.py", + r#" +class MyClass: + sound: str = "generic" +"#, + ) + .source( + "mymodule.pyi", + r#" +class MyClass: + sound: str +"#, + ) + .build(); + + assert_snapshot!(test.goto_definition(), @r#" + info[goto-definition]: Go to definition + --> main.py:4:7 + | + 4 | x.sound + | ^^^^^ Clicking here + info: Found 1 definition + --> mymodule.py:3:5 + | + 3 | sound: str = "generic" + | ----- + "#); + } + + /// goto-definition on a module-level variable should go to the .py not the .pyi + #[test] + fn goto_definition_stub_map_module_variable() { + let test = CursorTest::builder() + .source( + "main.py", + " +import mymodule +mymodule.COUNT +", + ) + .source( + "mymodule.py", + r#" +COUNT = 0 +"#, + ) + .source( + "mymodule.pyi", + r#" +COUNT: int +"#, + ) + .build(); + + assert_snapshot!(test.goto_definition(), @r" + info[goto-definition]: Go to definition + --> main.py:3:10 + | + 3 | mymodule.COUNT + | ^^^^^ Clicking here + info: Found 1 definition + --> mymodule.py:2:1 | + 2 | COUNT = 0 + | ----- "); } @@ -648,13 +850,11 @@ class MyOtherClass: | 3 | x = MyClass.action() | ^^^^^^ Clicking here - | info: Found 1 definition --> mymodule.py:5:9 | 5 | def action(): | ------ - | "); } @@ -688,13 +888,11 @@ class MyClass: ... | 2 | from mymodule import MyClass | ^^^^^^^ Clicking here - | info: Found 1 definition --> mymodule.py:2:7 | 2 | class MyClass: ... | ------- - | "); } @@ -722,13 +920,11 @@ my_func(my_other_func(ab=5, y=2), 0) | 5 | my_other_func(my_func(ab=5, y=2), 0) | ^^ Clicking here - | info: Found 1 definition --> main.py:2:13 | 2 | def my_func(ab, y, z = None): ... | -- - | "); } @@ -756,13 +952,11 @@ my_func(my_other_func(ab=5, y=2), 0) | 6 | my_func(my_other_func(ab=5, y=2), 0) | ^^ Clicking here - | info: Found 1 definition --> main.py:3:19 | 3 | def my_other_func(ab, y): ... | -- - | "); } @@ -790,13 +984,11 @@ my_func(my_other_func(ab=5, y=2), 0) | 5 | my_other_func(my_func(ab=5, y=2), 0) | ^^ Clicking here - | info: Found 1 definition --> main.py:2:13 | 2 | def my_func(ab, y): ... | -- - | "); } @@ -824,13 +1016,11 @@ my_func(my_other_func(ab=5, y=2), 0) | 6 | my_func(my_other_func(ab=5, y=2), 0) | ^^ Clicking here - | info: Found 1 definition --> main.py:3:19 | 3 | def my_other_func(ab, y): ... | -- - | "); } @@ -872,13 +1062,11 @@ def ab(a: str): ... | 4 | ab(1) | ^^ Clicking here - | info: Found 1 definition --> mymodule.py:2:5 | 2 | def ab(a): | -- - | "); } @@ -920,13 +1108,11 @@ def ab(a: str): ... | 4 | ab("hello") | ^^ Clicking here - | info: Found 1 definition --> mymodule.py:2:5 | 2 | def ab(a): | -- - | "#); } @@ -968,13 +1154,11 @@ def ab(a: int): ... | 4 | ab(1, 2) | ^^ Clicking here - | info: Found 1 definition --> mymodule.py:2:5 | 2 | def ab(a, b = None): | -- - | "); } @@ -1016,13 +1200,11 @@ def ab(a: int): ... | 4 | ab(1) | ^^ Clicking here - | info: Found 1 definition --> mymodule.py:2:5 | 2 | def ab(a, b = None): | -- - | "); } @@ -1067,13 +1249,11 @@ def ab(a: int, *, c: int): ... | 4 | ab(1, b=2) | ^^ Clicking here - | info: Found 1 definition --> mymodule.py:2:5 | 2 | def ab(a, *, b = None, c = None): | -- - | "); } @@ -1118,13 +1298,11 @@ def ab(a: int, *, c: int): ... | 4 | ab(1, c=2) | ^^ Clicking here - | info: Found 1 definition --> mymodule.py:2:5 | 2 | def ab(a, *, b = None, c = None): | -- - | "); } @@ -1153,13 +1331,11 @@ a + b | 10 | a + b | ^ Clicking here - | info: Found 1 definition --> main.py:3:9 | 3 | def __add__(self, other): | ------- - | "); } @@ -1186,13 +1362,11 @@ B() + A() | 8 | B() + A() | ^ Clicking here - | info: Found 1 definition --> main.py:3:9 | 3 | def __radd__(self, other) -> A: | -------- - | "); } @@ -1221,13 +1395,11 @@ a+b | 10 | a+b | ^ Clicking here - | info: Found 1 definition --> main.py:3:9 | 3 | def __add__(self, other): | ------- - | "); } @@ -1256,13 +1428,11 @@ a+b | 10 | a+b | ^ Clicking here - | info: Found 1 definition --> main.py:8:1 | 8 | b = Test() | - - | "); } @@ -1310,13 +1480,11 @@ a = Test() | 7 | ~a | ^ Clicking here - | info: Found 1 definition --> main.py:3:9 | 3 | def __invert__(self) -> 'Test': ... | ---------- - | "); } @@ -1347,13 +1515,11 @@ a = \"asdf\" | 8 | ~a | ^ Clicking here - | info: Found 1 definition --> main.by:3:9 | 3 | def __invert__(self) -> str: | ---------- - | "); } @@ -1383,13 +1549,11 @@ a + b | 11 | a + b | ^ Clicking here - | info: Found 1 definition --> main.by:5:9 | 5 | def __add__(self, other: Money) -> Money: | ------- - | "); } @@ -1416,13 +1580,11 @@ a = Test() | 7 | ~a | ^ Clicking here - | info: Found 1 definition --> main.py:3:9 | 3 | def __invert__(self, extra_arg) -> 'Test': ... | ---------- - | "); } @@ -1448,13 +1610,11 @@ a = Test() | 7 | ~ a | ^ Clicking here - | info: Found 1 definition --> main.py:3:9 | 3 | def __invert__(self) -> 'Test': ... | ---------- - | "); } @@ -1480,13 +1640,11 @@ a = Test() | 7 | -a | ^ Clicking here - | info: Found 1 definition --> main.py:5:1 | 5 | a = Test() | - - | "); } @@ -1512,13 +1670,11 @@ a = Test() | 7 | not a | ^^^ Clicking here - | info: Found 1 definition --> main.py:3:9 | 3 | def __bool__(self) -> bool: ... | -------- - | "); } @@ -1544,13 +1700,11 @@ a = Test() | 7 | not a | ^^^ Clicking here - | info: Found 1 definition --> main.py:3:9 | 3 | def __len__(self) -> 42: ... | ------- - | "); } @@ -1580,13 +1734,11 @@ a = Test() | 8 | not a | ^^^ Clicking here - | info: Found 1 definition --> main.py:3:9 | 3 | def __bool__(self, extra_arg) -> bool: ... | -------- - | "); } @@ -1616,13 +1768,11 @@ a = Test() | 7 | not a | ^^^ Clicking here - | info: Found 1 definition --> main.py:3:9 | 3 | def __len__(self, extra_arg) -> 42: ... | ------- - | "); } @@ -1643,7 +1793,6 @@ a: float = 3.14 | LL | a: float = 3.14 | ^^^^^ Clicking here - | info: Found 2 definitions --> stdlib/builtins.byi:LL:7 | @@ -1654,7 +1803,6 @@ a: float = 3.14 | LL | class float: | ----- - | "); } @@ -1675,7 +1823,6 @@ a: complex = 3.14 | LL | a: complex = 3.14 | ^^^^^^^ Clicking here - | info: Found 3 definitions --> stdlib/builtins.byi:LL:7 | @@ -1691,7 +1838,6 @@ a: complex = 3.14 | LL | class complex: | ------- - | "); } @@ -1733,13 +1879,11 @@ x = MyClass() | 5 | x = MyClass() | ^^^^^^^ Clicking here - | info: Found 1 definition --> main.py:3:9 | 3 | def __init__(self, val): | -------- - | "); } @@ -1764,13 +1908,11 @@ x = MyClass() | 5 | x = MyClass() | ^^^^^^^ Clicking here - | info: Found 1 definition --> main.py:3:9 | 3 | def __init__(self, val): | -------- - | "); } @@ -1821,13 +1963,11 @@ x = MyClass(foo) | 7 | x = MyClass(foo) | ^^^ Clicking here - | info: Found 1 definition --> main.py:2:1 | 2 | foo = 1 | --- - | ", ); } @@ -1855,7 +1995,6 @@ x = MyClass() | 7 | x = MyClass() | ^^^^^^^ Clicking here - | info: Found 2 definitions --> main.py:3:9 | @@ -1864,7 +2003,6 @@ x = MyClass() 4 | self.val = val 5 | def __new__(self, val): | ------- - | "); } @@ -1888,13 +2026,11 @@ x = DynClass() | 4 | x = DynClass() | ^^^^^^^^ Clicking here - | info: Found 1 definition --> main.py:2:1 | 2 | DynClass = type("DynClass", (), {}) | -------- - | "#); } @@ -1922,13 +2058,11 @@ x = DynClass() | LL | x = DynClass() | ^^^^^^^^ Clicking here - | info: Found 1 definition --> stdlib/builtins.byi:LL:9 | LL | def __new__(cls) -> Self | ------- - | "); } @@ -1970,13 +2104,11 @@ p = Point(1, 2) | 6 | p = Point(1, 2) | ^^^^^ Clicking here - | info: Found 1 definition --> main.py:4:1 | 4 | Point = namedtuple("Point", ["x", "y"]) | ----- - | "#); } @@ -2008,13 +2140,11 @@ p = Point(1, 2) | 6 | p = Point(1, 2) | ^^^^^ Clicking here - | info: Found 1 definition --> main.py:4:1 | 4 | Point = namedtuple("Point", ["x", "y"]) | ----- - | "#); } @@ -2042,7 +2172,6 @@ p = Point(1, 2) | 6 | print(a) | ^ Clicking here - | info: Found 3 definitions --> main.py:2:1 | @@ -2056,7 +2185,6 @@ p = Point(1, 2) 7 | 8 | a: bool = True | - - | "#); } @@ -2083,7 +2211,6 @@ p = Point(1, 2) | 8 | test.a | ^ Clicking here - | info: Found 2 definitions --> main.py:3:5 | @@ -2091,7 +2218,6 @@ p = Point(1, 2) | - 4 | a: str | - - | "); } @@ -2123,7 +2249,6 @@ p = Point(1, 2) | 13 | test.a | ^ Clicking here - | info: Found 2 definitions --> main.py:4:9 | @@ -2134,7 +2259,6 @@ p = Point(1, 2) | 8 | def a(self, value: str) -> None: | - - | "); } @@ -2158,13 +2282,11 @@ p = Point(1, 2) | LL | Foo.__dictoffset__ | ^^^^^^^^^^^^^^ Clicking here - | info: Found 1 definition --> stdlib/builtins.byi:LL:9 | LL | let __dictoffset__: int | -------------- - | "); } @@ -2190,13 +2312,11 @@ p = Point(1, 2) | 6 | Bar.a | ^ Clicking here - | info: Found 1 definition --> main.py:3:5 | 3 | a: int | - - | "); } @@ -2246,13 +2366,11 @@ p = Point(1, 2) | LL | type.__dictoffset__ | ^^^^^^^^^^^^^^ Clicking here - | info: Found 1 definition --> stdlib/builtins.byi:LL:9 | LL | let __dictoffset__: int | -------------- - | "); } @@ -2277,13 +2395,11 @@ while True: | 5 | variable | ^^^^^^^^ Clicking here - | info: Found 1 definition --> main.py:3:5 | 3 | variable = 1 | -------- - | "); } @@ -2310,13 +2426,11 @@ TD(f=1) | 8 | TD(f=1) | ^ Clicking here - | info: Found 1 definition --> main.py:5:5 | 5 | f: int | - - | "); } @@ -2344,13 +2458,11 @@ td.update(f=2) | 9 | td.update(f=2) | ^ Clicking here - | info: Found 1 definition --> main.py:5:5 | 5 | f: int | - - | "); } @@ -2379,13 +2491,11 @@ func(f=1) | 10 | func(f=1) | ^ Clicking here - | info: Found 1 definition --> main.py:5:5 | 5 | f: int | - - | "); } @@ -2412,13 +2522,11 @@ NT(f=1) | 8 | NT(f=1) | ^ Clicking here - | info: Found 1 definition --> main.py:5:5 | 5 | f: int | - - | "); } @@ -2446,13 +2554,11 @@ DC(f=1) | 9 | DC(f=1) | ^ Clicking here - | info: Found 1 definition --> main.py:6:5 | 6 | f: int | - - | "); } @@ -2482,13 +2588,11 @@ DC(f=1) | 11 | DC(f=1) | ^ Clicking here - | info: Found 1 definition --> main.py:9:24 | 9 | def __init__(self, f: int) -> None: ... | - - | "); } @@ -2519,13 +2623,11 @@ DC(g=1) | 12 | DC(g=1) | ^ Clicking here - | info: Found 1 definition --> main.py:10:5 | 10 | f: int = Field(alias='g') | - - | "); } @@ -2550,13 +2652,11 @@ for x in range(10): | 5 | variable | ^^^^^^^^ Clicking here - | info: Found 1 definition --> main.py:3:5 | 3 | variable = 1 | -------- - | "); } @@ -2584,13 +2684,11 @@ class Bar(Foo): | 8 | super().__init__(x) | ^^^^^^^^ Clicking here - | info: Found 1 definition --> main.py:3:9 | 3 | def __init__(self, x: int) -> None: | -------- - | "); } @@ -2619,13 +2717,11 @@ class GenericFoo[T](Base): | 8 | super().__init__(x) | ^^^^^^^^ Clicking here - | info: Found 1 definition --> main.py:3:9 | 3 | def __init__(self, x: int) -> None: | -------- - | "); } @@ -2663,13 +2759,11 @@ def show(request): | 3 | return render(request, "blog/post.html") | ^^^^^^^^^^^^^^ Clicking here - | info: Found 1 definition --> blog/templates/blog/post.html:1:1 | 1 |

{{ post.title }}

| - - | "#); } @@ -2688,13 +2782,11 @@ def show(request): | 3 | return TemplateResponse(request, "blog/list.html") | ^^^^^^^^^^^^^^ Clicking here - | info: Found 1 definition --> blog/templates/blog/list.html:1:1 | 1 |
    | - - | "#); } @@ -2713,13 +2805,11 @@ class PostDetail(DetailView): | 3 | template_name = "blog/post.html" | ^^^^^^^^^^^^^^ Clicking here - | info: Found 1 definition --> blog/templates/blog/post.html:1:1 | 1 |

    {{ post.title }}

    | - - | "#); } @@ -2738,13 +2828,11 @@ def show(request): | 3 | return redirect(reverse("blog:detail")) | ^^^^^^^^^^^ Clicking here - | info: Found 1 definition --> blog/urls.py:5:36 | 5 | path("/", detail, name="detail"), | -------- - | "#); } @@ -2763,13 +2851,11 @@ def show(request): | 3 | return redirect("blog:detail") | ^^^^^^^^^^^ Clicking here - | info: Found 1 definition --> blog/urls.py:5:36 | 5 | path("/", detail, name="detail"), | -------- - | "#); } @@ -2941,13 +3027,11 @@ def q(): | 5 | return Book.objects.filter(author.name == "x") | ^^^^^^ Clicking here - | info: Found 1 definition --> src/blog/models.py:10:5 | 10 | author = models.ForeignKey(Author, on_delete=models.CASCADE) | ------ - | "#); } @@ -2970,13 +3054,11 @@ def q(): | 5 | return Book.objects.filter(author.name == "x") | ^^^^ Clicking here - | info: Found 1 definition --> src/blog/models.py:5:5 | 5 | name = models.CharField(max_length=100) | ---- - | "#); } @@ -2997,13 +3079,11 @@ def q(): | 5 | return Book.objects.filter(published > 1) | ^^^^^^^^^ Clicking here - | info: Found 1 definition --> src/blog/models.py:9:5 | 9 | published = models.DateField() | --------- - | "); } @@ -3024,13 +3104,11 @@ def q(): | 5 | return Book.objects.exclude(title == "x") | ^^^^^ Clicking here - | info: Found 1 definition --> src/blog/models.py:8:5 | 8 | title = models.CharField(max_length=200) | ----- - | "#); } @@ -3051,13 +3129,11 @@ def q(): | 5 | return Doc.objects.get(data["key"] == 1) | ^^^^ Clicking here - | info: Found 1 definition --> src/blog/models.py:13:5 | 13 | data = models.JSONField() | ---- - | "#); } @@ -3098,13 +3174,11 @@ def q(): | 5 | return Book.objects.filter(pk == 1) | ^^ Clicking here - | info: Found 1 definition --> site-packages/django/db/models/base.pyi:7:5 | 7 | pk: Any | -- - | "); } @@ -3143,13 +3217,11 @@ def q(): | 6 | return Book.objects.filter(title == "x") | ^^^^^ Clicking here - | info: Found 1 definition --> src/blog/queries.by:5:5 | 5 | title = "x" | ----- - | "#); } @@ -3177,7 +3249,11 @@ def q(): impl CursorTest { fn goto_definition(&self) -> String { let Some(targets) = salsa::attach(&self.db, || { - goto_definition(&self.db, self.cursor.file, self.cursor.offset) + goto_definition( + &self.db, + self.program_file(self.cursor.file), + self.cursor.offset, + ) }) else { return "No goto target found".to_string(); }; @@ -3241,6 +3317,7 @@ def q(): Definition, Declaration, TypeDefinition, + Implementation, } impl GotoAction { @@ -3249,6 +3326,7 @@ def q(): GotoAction::Definition => "goto-definition", GotoAction::Declaration => "goto-declaration", GotoAction::TypeDefinition => "goto-type definition", + GotoAction::Implementation => "goto-implementation", } } @@ -3257,6 +3335,7 @@ def q(): GotoAction::Definition => "Go to definition", GotoAction::Declaration => "Go to declaration", GotoAction::TypeDefinition => "Go to type definition", + GotoAction::Implementation => "Go to implementation", } } @@ -3265,6 +3344,7 @@ def q(): GotoAction::Definition => "definition", GotoAction::Declaration => "declaration", GotoAction::TypeDefinition => "type definition", + GotoAction::Implementation => "implementation", } } } diff --git a/crates/ty_ide/src/goto_implementation.rs b/crates/ty_ide/src/goto_implementation.rs new file mode 100644 index 0000000000..64f441f249 --- /dev/null +++ b/crates/ty_ide/src/goto_implementation.rs @@ -0,0 +1,2160 @@ +//! Finds known implementations of classes and class members. +//! +//! This module implements the `textDocument/implementation` request, commonly exposed as **Go to +//! Implementation** in an editor. It follows nominal inheritance and uses receiver type to decide +//! where to start searching. +//! +//! For example, consider this class hierarchy: +//! +//! ```python +//! class Animal: +//! sound = "unknown" +//! +//! def speak(self) -> str: +//! return self.sound +//! +//! class Dog(Animal): +//! sound = "woof" +//! +//! def speak(self) -> str: +//! return self.sound +//! +//! def make_sound(animal: Animal) -> str: +//! return animal.speak() +//! ``` +//! +//! A request on `animal.speak()` starts from `Animal`, so it returns both `Animal.speak` and +//! `Dog.speak`. A request on a value known to be a `Dog` returns only the implementation selected +//! for `Dog`. +//! +//! # Supported request locations +//! +//! - A method or data attribute use, such as `animal.speak()` or `animal.sound`. +//! - The name in a method declaration, such as `speak` in the definition of `Animal` above. The +//! containing class becomes the starting point. +//! - The name in a class declaration. The result includes that class and its known subclasses. +//! - A class name used as a base class, type annotation, or constructor call. Qualified names such +//! as `module.Animal` and `Outer.Inner` are also supported. +//! +//! # Selecting results +//! +//! - An overloaded method resolves to its implementation body when one is available. +//! - Reading, assigning, or deleting a property selects its getter, setter, or deleter (the +//! corresponding property accessor). +//! - A declaration in a `.pyi` stub file maps to the corresponding source definition when +//! possible. +//! - A class or member definition that cannot run in the configured Python environment is +//! excluded. A request directly on an unreachable class, method, property getter, setter, or +//! deleter returns no result. +//! +//! # Limits +//! +//! - Classes are not discovered just because they provide the methods required by a +//! `typing.Protocol` (structural subtyping); they must explicitly inherit from that protocol. +//! - Properties created with Python's built-in `property` are recognized. Other objects that +//! customize what happens when an attribute is read, assigned, or deleted (descriptors) are not +//! interpreted as properties. + +use crate::goto::{Definitions, GotoTarget, find_goto_target}; +use crate::{Db, NavigationTarget, NavigationTargets, RangedValue}; +use rayon::prelude::*; +use ruff_db::files::{File, FileRange}; +use ruff_db::parsed::parsed_module; +use ruff_text_size::{Ranged, TextSize}; +use ty_project::parallel::ParallelIteratorExt; +use ty_python_core::ProgramFile; +use ty_python_semantic::{ + ImplementationsFinder, ImportAliasResolution, ResolvedDefinition, SemanticModel, +}; + +/// Returns the known implementations for the supported target at `offset`. +/// +/// Returns `None` when the cursor is not on a supported target or no implementation can be +/// identified. +pub fn goto_implementation( + db: &dyn Db, + file: ProgramFile<'_>, + offset: TextSize, +) -> Option> { + let module = parsed_module(db, file.python_file(db)).load(db); + let model = SemanticModel::new(db, file); + let goto_target = find_goto_target(&model, &module, offset)?; + let finder = prepare_implementations_finder_for_goto_target(&model, &goto_target)?; + let source_file = file.file(db); + let program = file.program(db); + + let mut candidate_files: Vec = db + .project() + .files(db) + .iter() + .copied() + .filter(|candidate| *candidate != source_file) + .collect(); + candidate_files.push(source_file); + + let batches = candidate_files + .into_par_iter() + .map_with_db(db, |db, file| { + let file = ProgramFile::new(db, file, program); + let definitions = finder.implementations_for_file(db, file); + definitions_to_implementation_targets(db, definitions) + }) + .collect::>(); + + let mut implementation_targets = + definitions_to_implementation_targets(db, finder.into_initial_definitions()); + implementation_targets.extend(batches.into_iter().flatten()); + + if implementation_targets.is_empty() { + return None; + } + + let implementation_targets = implementation_targets.into_iter().collect(); + + Some(RangedValue { + range: FileRange::new(source_file, goto_target.range()), + value: implementation_targets, + }) +} + +/// Select and prepare the appropriate `ImplementationsFinder` for `goto_target`. +fn prepare_implementations_finder_for_goto_target<'db>( + model: &SemanticModel<'db>, + goto_target: &GotoTarget<'_>, +) -> Option> { + let db = model.db(); + let env = model.program_environment(); + match goto_target { + GotoTarget::Expression(expression) + | GotoTarget::Call { + callable: expression, + .. + } if matches!( + expression, + ruff_python_ast::ExprRef::Name(_) | ruff_python_ast::ExprRef::Attribute(_) + ) => + { + goto_target + .expression_definitions(model, ImportAliasResolution::ResolveAliases) + .and_then(|definitions| { + ImplementationsFinder::for_class_reference( + db, + &env, + definitions.iter().as_slice(), + ) + }) + .or_else(|| match expression { + ruff_python_ast::ExprRef::Attribute(attribute) => { + ImplementationsFinder::for_attribute(model, attribute) + } + _ => None, + }) + } + GotoTarget::StringAnnotationSubexpr { .. } => goto_target + .definitions(model, ImportAliasResolution::ResolveAliases) + .and_then(|definitions| { + ImplementationsFinder::for_class_reference(db, &env, definitions.iter().as_slice()) + }), + GotoTarget::FunctionDef(function) => ImplementationsFinder::for_method(model, function), + GotoTarget::ClassDef(class) => ImplementationsFinder::for_class(model, class), + _ => None, + } +} + +fn definitions_to_implementation_targets( + db: &dyn Db, + definitions: Vec, +) -> Vec { + Definitions::new(definitions) + .map_stubs_for_implementation(db) + .map(|definitions| { + definitions + .into_navigation_targets(db) + .into_iter() + .collect() + }) + .unwrap_or_default() +} + +#[cfg(test)] +mod tests { + use crate::goto_implementation; + use crate::tests::{CursorTest, cursor_test}; + use insta::assert_snapshot; + use ruff_db::system::SystemPathBuf; + use ty_project::Db as _; + + #[test] + fn implementation_method_family_from_attribute() { + let test = cursor_test( + r#" + class Animal: + def speak(self): ... + + class Dog(Animal): + def speak(self): ... + + class Cat(Animal): + def speak(self): ... + + def f(animal: Animal): + animal.speak() + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:12:12 + | + 12 | animal.speak() + | ^^^^^ Clicking here + info: Found 3 implementations + --> main.py:3:9 + | + 3 | def speak(self): ... + | ----- + 4 | + 5 | class Dog(Animal): + 6 | def speak(self): ... + | ----- + 7 | + 8 | class Cat(Animal): + 9 | def speak(self): ... + | ----- + "); + } + + #[test] + fn implementation_abstract_root_method_is_included() { + let test = cursor_test( + r#" + from abc import ABC, abstractmethod + + class Animal(ABC): + @abstractmethod + def speak(self) -> str: ... + + class Dog(Animal): + def speak(self) -> str: + return "woof" + + class Cat(Animal): + def speak(self) -> str: + return "meow" + + def f(animal: Animal): + animal.speak() + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:17:12 + | + 17 | animal.speak() + | ^^^^^ Clicking here + info: Found 3 implementations + --> main.py:6:9 + | + 6 | def speak(self) -> str: ... + | ----- + 7 | + 8 | class Dog(Animal): + 9 | def speak(self) -> str: + | ----- + | + ::: main.py:13:9 + | + 13 | def speak(self) -> str: + | ----- + "); + } + + #[test] + fn implementation_transitive_subclass_overrides() { + let test = cursor_test( + r#" + class Animal: + def speak(self): ... + + class Mammal(Animal): + pass + + class Dog(Mammal): + def speak(self): ... + + def f(animal: Animal): + animal.speak() + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:12:12 + | + 12 | animal.speak() + | ^^^^^ Clicking here + info: Found 2 implementations + --> main.py:3:9 + | + 3 | def speak(self): ... + | ----- + | + ::: main.py:9:9 + | + 9 | def speak(self): ... + | ----- + "); + } + + #[test] + fn implementation_inherited_method_from_concrete_receiver() { + let test = cursor_test( + r#" + class Animal: + def speak(self): ... + + class Dog(Animal): + pass + + dog = Dog() + dog.speak() + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:9:5 + | + 9 | dog.speak() + | ^^^^^ Clicking here + info: Found 1 implementation + --> main.py:3:9 + | + 3 | def speak(self): ... + | ----- + "); + } + + #[test] + fn implementation_overridden_method_from_concrete_receiver() { + let test = cursor_test( + r#" + class Animal: + def speak(self): ... + + class Dog(Animal): + def speak(self): ... + + class Cat(Animal): + def speak(self): ... + + def f(dog: Dog): + dog.speak() + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:12:9 + | + 12 | dog.speak() + | ^^^^^ Clicking here + info: Found 1 implementation + --> main.py:6:9 + | + 6 | def speak(self): ... + | ----- + "); + } + + #[test] + fn implementation_shadowed_inherited_method_from_concrete_receiver() { + let test = cursor_test( + r#" + class Animal: + def speak(self): ... + + class Dog(Animal): + speak = 1 + + dog = Dog() + dog.speak() + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r" + info[goto-implementation]: Go to implementation + --> main.py:9:5 + | + 9 | dog.speak() + | ^^^^^ Clicking here + info: Found 1 implementation + --> main.py:6:5 + | + 6 | speak = 1 + | ----- + "); + } + + #[test] + fn implementation_unresolved_root_does_not_scan_subclasses() { + let test = cursor_test( + r#" + class Dog: + def speak(self): ... + + def f(value: object): + value.speak() + "#, + ); + + assert_snapshot!(test.goto_implementation(), @"No goto target found"); + } + + #[test] + fn implementation_overloaded_method_returns_implementation() { + let test = cursor_test( + r#" + from typing import overload + + class Animal: + @overload + def speak(self, volume: int) -> int: ... + @overload + def speak(self, volume: str) -> str: ... + def speak(self, volume: int | str) -> int | str: + return volume + + def f(animal: Animal): + animal.speak(1) + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:13:12 + | + 13 | animal.speak(1) + | ^^^^^ Clicking here + info: Found 1 implementation + --> main.py:9:9 + | + 9 | def speak(self, volume: int | str) -> int | str: + | ----- + "); + } + + #[test] + fn implementation_overload_only_root_scans_subclasses() { + let test = cursor_test( + r#" + from typing import overload + + class Animal: + @overload + def speak(self, volume: int) -> int: ... + @overload + def speak(self, volume: str) -> str: ... + + class Dog(Animal): + def speak(self, volume: int | str) -> int | str: + return volume + + def f(animal: Animal): + animal.speak(1) + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:15:12 + | + 15 | animal.speak(1) + | ^^^^^ Clicking here + info: Found 1 implementation + --> main.py:11:9 + | + 11 | def speak(self, volume: int | str) -> int | str: + | ----- + "); + } + + #[test] + fn implementation_property_setter_definition() { + let test = cursor_test( + r#" + class Base: + @property + def value(self) -> int: ... + + @value.setter + def value(self, value: int) -> None: ... + + @value.deleter + def value(self) -> None: ... + + class Child(Base): + @property + def value(self) -> int: ... + + @value.setter + def value(self, value: int) -> None: ... + + @value.deleter + def value(self) -> None: ... + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r" + info[goto-implementation]: Go to implementation + --> main.py:7:9 + | + 7 | def value(self, value: int) -> None: ... + | ^^^^^ Clicking here + info: Found 2 implementations + --> main.py:7:9 + | + 7 | def value(self, value: int) -> None: ... + | ----- + | + ::: main.py:17:9 + | + 17 | def value(self, value: int) -> None: ... + | ----- + "); + } + + #[test] + fn implementation_property_read() { + let test = cursor_test( + r#" + class Base: + @property + def value(self) -> int: ... + + @value.setter + def value(self, value: int) -> None: ... + + @value.deleter + def value(self) -> None: ... + + class Child(Base): + @property + def value(self) -> int: ... + + @value.setter + def value(self, value: int) -> None: ... + + @value.deleter + def value(self) -> None: ... + + def f(base: Base): + return base.value + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r" + info[goto-implementation]: Go to implementation + --> main.py:23:17 + | + 23 | return base.value + | ^^^^^ Clicking here + info: Found 2 implementations + --> main.py:4:9 + | + 4 | def value(self) -> int: ... + | ----- + | + ::: main.py:14:9 + | + 14 | def value(self) -> int: ... + | ----- + "); + } + + #[test] + fn implementation_property_write() { + let test = cursor_test( + r#" + class Base: + @property + def value(self) -> int: ... + + @value.setter + def value(self, value: int) -> None: ... + + @value.deleter + def value(self) -> None: ... + + class Child(Base): + @property + def value(self) -> int: ... + + @value.setter + def value(self, value: int) -> None: ... + + @value.deleter + def value(self) -> None: ... + + def f(base: Base, value: int): + base.value = value + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r" + info[goto-implementation]: Go to implementation + --> main.py:23:10 + | + 23 | base.value = value + | ^^^^^ Clicking here + info: Found 2 implementations + --> main.py:7:9 + | + 7 | def value(self, value: int) -> None: ... + | ----- + | + ::: main.py:17:9 + | + 17 | def value(self, value: int) -> None: ... + | ----- + "); + } + + #[test] + fn implementation_inherited_method_from_union_receivers_deduplicates() { + let test = cursor_test( + r#" + class Animal: + def speak(self): ... + + class Dog(Animal): + pass + + class Cat(Animal): + pass + + def f(pet: Dog | Cat): + pet.speak() + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:12:9 + | + 12 | pet.speak() + | ^^^^^ Clicking here + info: Found 1 implementation + --> main.py:3:9 + | + 3 | def speak(self): ... + | ----- + "); + } + + #[test] + fn implementation_typevar_bound_receiver() { + let test = cursor_test( + r#" + class Animal: + def speak(self): ... + + class Dog(Animal): + def speak(self): ... + + def f[T: Animal](animal: T): + animal.speak() + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:9:12 + | + 9 | animal.speak() + | ^^^^^ Clicking here + info: Found 2 implementations + --> main.py:3:9 + | + 3 | def speak(self): ... + | ----- + 4 | + 5 | class Dog(Animal): + 6 | def speak(self): ... + | ----- + "); + } + + #[test] + fn implementation_classmethod_receiver() { + let test = cursor_test( + r#" + class Animal: + @classmethod + def speak(cls): ... + + @classmethod + def call(cls): + cls.speak() + + class Dog(Animal): + @classmethod + def speak(cls): ... + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r" + info[goto-implementation]: Go to implementation + --> main.py:8:13 + | + 8 | cls.speak() + | ^^^^^ Clicking here + info: Found 2 implementations + --> main.py:4:9 + | + 4 | def speak(cls): ... + | ----- + | + ::: main.py:12:9 + | + 12 | def speak(cls): ... + | ----- + "); + } + + #[test] + fn implementation_typevar_bound_class_object_receiver() { + let test = cursor_test( + r#" + class Animal: + @classmethod + def speak(cls): ... + + class Dog(Animal): + @classmethod + def speak(cls): ... + + def f[T: Animal](cls: type[T]): + cls.speak() + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r" + info[goto-implementation]: Go to implementation + --> main.py:11:9 + | + 11 | cls.speak() + | ^^^^^ Clicking here + info: Found 2 implementations + --> main.py:4:9 + | + 4 | def speak(cls): ... + | ----- + 5 | + 6 | class Dog(Animal): + 7 | @classmethod + 8 | def speak(cls): ... + | ----- + "); + } + + #[test] + fn implementation_subclass_through_import_alias() { + let test = CursorTest::builder() + .source( + "base.py", + r#" + class Base: + def method(self): ... + "#, + ) + .source( + "aliases.py", + r#" + from base import Base as B + "#, + ) + .source( + "child.py", + r#" + from aliases import B + + class Child(B): + def method(self): ... + "#, + ) + .build(); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> base.py:3:9 + | + 3 | def method(self): ... + | ^^^^^^ Clicking here + info: Found 2 implementations + --> base.py:3:9 + | + 3 | def method(self): ... + | ------ + | + ::: child.py:5:9 + | + 5 | def method(self): ... + | ------ + "); + } + + #[test] + fn implementation_parallel_candidate_batches_preserve_order() { + let test = CursorTest::builder() + .source( + "base.py", + r#" + class Base: + def method(self): ... + "#, + ) + .source( + "z_child.py", + r#" + from base import Base + + class ZChild(Base): + def method(self): ... + "#, + ) + .source( + "a_child.py", + r#" + from base import Base + + class AChild(Base): + def method(self): ... + "#, + ) + .build(); + + let targets = salsa::attach(&test.db, || { + goto_implementation( + &test.db, + test.program_file(test.cursor.file), + test.cursor.offset, + ) + .expect("implementation targets") + }); + let paths = targets + .into_iter() + .map(|target| target.file().path(&test.db).to_string()) + .collect::>(); + + assert_eq!(paths, ["/base.py", "/z_child.py", "/a_child.py"]); + } + + #[test] + fn implementation_stub_map_class_method() { + let test = CursorTest::builder() + .source( + "main.py", + " +from mymodule import MyClass +x = MyClass(0) +x.action() +", + ) + .source( + "mymodule.py", + r#" +class MyClass: + def __init__(self, val): + self.val = val + def action(self): + print(self.val) +"#, + ) + .source( + "mymodule.pyi", + r#" +class MyClass: + def __init__(self, val: bool): ... + def action(self): ... +"#, + ) + .build(); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:4:3 + | + 4 | x.action() + | ^^^^^^ Clicking here + info: Found 1 implementation + --> mymodule.py:5:9 + | + 5 | def action(self): + | ------ + "); + } + + #[test] + fn implementation_stub_map_overloaded_class_method() { + let test = CursorTest::builder() + .source( + "main.py", + " +from mymodule import MyClass +x = MyClass(0) +x.action(1) +", + ) + .source( + "mymodule.py", + r#" +class MyClass: + def __init__(self, val): + self.val = val + def action(self, value): + return value +"#, + ) + .source( + "mymodule.pyi", + r#" +from typing import overload + +class MyClass: + def __init__(self, val: bool): ... + @overload + def action(self, value: int) -> int: ... + @overload + def action(self, value: str) -> str: ... +"#, + ) + .build(); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:4:3 + | + 4 | x.action(1) + | ^^^^^^ Clicking here + info: Found 1 implementation + --> mymodule.py:5:9 + | + 5 | def action(self, value): + | ------ + "); + } + + #[test] + fn implementation_stub_only_overloaded_class_method() { + let test = CursorTest::builder() + .source( + "main.py", + " +from mymodule import MyClass +x = MyClass(0) +x.action(1) +", + ) + .source( + "mymodule.pyi", + r#" +from typing import overload + +class MyClass: + def __init__(self, val: bool): ... + @overload + def action(self, value: int) -> int: ... + @overload + def action(self, value: str) -> str: ... +"#, + ) + .build(); + + assert_snapshot!(test.goto_implementation(), @"No goto target found"); + } + + #[test] + fn implementation_method_declaration_root() { + let test = cursor_test( + r#" + class Animal: + def speak(self): ... + + class Dog(Animal): + def speak(self): ... + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:3:9 + | + 3 | def speak(self): ... + | ^^^^^ Clicking here + info: Found 2 implementations + --> main.py:3:9 + | + 3 | def speak(self): ... + | ----- + 4 | + 5 | class Dog(Animal): + 6 | def speak(self): ... + | ----- + "); + } + + #[test] + fn implementation_unsupported_target() { + let test = cursor_test( + r#" + def function(): ... + + function() + "#, + ); + + assert_snapshot!(test.goto_implementation(), @"No goto target found"); + } + + #[test] + fn implementation_class_family() { + let test = cursor_test( + r#" + from abc import ABC + + class Animal(ABC): + pass + + class Dog(Animal): + pass + + class Cat(Animal): + pass + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:4:7 + | + 4 | class Animal(ABC): + | ^^^^^^ Clicking here + info: Found 3 implementations + --> main.py:4:7 + | + 4 | class Animal(ABC): + | ------ + 5 | pass + 6 | + 7 | class Dog(Animal): + | --- + 8 | pass + 9 | + 10 | class Cat(Animal): + | --- + "); + } + + #[test] + fn implementation_class_family_in_request_file_excluded_from_project() { + let mut test = CursorTest::builder() + .source( + "main.py", + r#" + class Animal: + pass + + class Dog(Animal): + pass + "#, + ) + .source("included.py", "") + .build(); + + test.db + .project() + .set_included_paths(&mut test.db, vec![SystemPathBuf::from("/included.py")]); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:2:7 + | + 2 | class Animal: + | ^^^^^^ Clicking here + info: Found 2 implementations + --> main.py:2:7 + | + 2 | class Animal: + | ------ + 3 | pass + 4 | + 5 | class Dog(Animal): + | --- + "); + } + + #[test] + fn implementation_class_no_subclasses() { + let test = cursor_test( + r#" + class Widget: + pass + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:2:7 + | + 2 | class Widget: + | ^^^^^^ Clicking here + info: Found 1 implementation + --> main.py:2:7 + | + 2 | class Widget: + | ------ + "); + } + + #[test] + fn implementation_class_intermediate_root() { + let test = cursor_test( + r#" + class Animal: + pass + + class Mammal(Animal): + pass + + class Dog(Mammal): + pass + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:5:7 + | + 5 | class Mammal(Animal): + | ^^^^^^ Clicking here + info: Found 2 implementations + --> main.py:5:7 + | + 5 | class Mammal(Animal): + | ------ + 6 | pass + 7 | + 8 | class Dog(Mammal): + | --- + "); + } + + #[test] + fn implementation_class_diamond_dedup() { + let test = cursor_test( + r#" + class Base: + pass + + class Left(Base): + pass + + class Right(Base): + pass + + class Diamond(Left, Right): + pass + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:2:7 + | + 2 | class Base: + | ^^^^ Clicking here + info: Found 4 implementations + --> main.py:2:7 + | + 2 | class Base: + | ---- + 3 | pass + 4 | + 5 | class Left(Base): + | ---- + 6 | pass + 7 | + 8 | class Right(Base): + | ----- + 9 | pass + 10 | + 11 | class Diamond(Left, Right): + | ------- + "); + } + + #[test] + fn implementation_class_generic_base() { + let test = cursor_test( + r#" + class Container[T]: + pass + + class IntContainer(Container[int]): + pass + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:2:7 + | + 2 | class Container[T]: + | ^^^^^^^^^ Clicking here + info: Found 2 implementations + --> main.py:2:7 + | + 2 | class Container[T]: + | --------- + 3 | pass + 4 | + 5 | class IntContainer(Container[int]): + | ------------ + "); + } + + #[test] + fn implementation_class_reference_in_annotation() { + let test = cursor_test( + r#" + class Animal: + pass + + class Dog(Animal): + pass + + def f(x: Animal): + pass + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r" + info[goto-implementation]: Go to implementation + --> main.py:8:10 + | + 8 | def f(x: Animal): + | ^^^^^^ Clicking here + info: Found 2 implementations + --> main.py:2:7 + | + 2 | class Animal: + | ------ + 3 | pass + 4 | + 5 | class Dog(Animal): + | --- + "); + } + + #[test] + fn implementation_class_reference_in_string_annotation() { + let test = cursor_test( + r#" + class Animal: + pass + + class Dog(Animal): + pass + + def f(x: "Animal"): + pass + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r#" + info[goto-implementation]: Go to implementation + --> main.py:8:11 + | + 8 | def f(x: "Animal"): + | ^^^^^^ Clicking here + info: Found 2 implementations + --> main.py:2:7 + | + 2 | class Animal: + | ------ + 3 | pass + 4 | + 5 | class Dog(Animal): + | --- + "#); + } + + #[test] + fn implementation_qualified_class_reference_in_base_list() { + let test = CursorTest::builder() + .source( + "animals.py", + r#" + class Animal: + pass + "#, + ) + .source( + "main.py", + r#" + import animals + + class Dog(animals.Animal): + pass + "#, + ) + .build(); + + assert_snapshot!(test.goto_implementation(), @r" + info[goto-implementation]: Go to implementation + --> main.py:4:19 + | + 4 | class Dog(animals.Animal): + | ^^^^^^ Clicking here + info: Found 2 implementations + --> animals.py:2:7 + | + 2 | class Animal: + | ------ + | + ::: main.py:4:7 + | + 4 | class Dog(animals.Animal): + | --- + "); + } + + #[test] + fn implementation_qualified_class_reference_in_instantiation() { + let test = CursorTest::builder() + .source( + "animals.py", + r#" + class Animal: + pass + "#, + ) + .source( + "main.py", + r#" + import animals + + class Dog(animals.Animal): + pass + + animals.Animal() + "#, + ) + .build(); + + assert_snapshot!(test.goto_implementation(), @r" + info[goto-implementation]: Go to implementation + --> main.py:7:9 + | + 7 | animals.Animal() + | ^^^^^^ Clicking here + info: Found 2 implementations + --> animals.py:2:7 + | + 2 | class Animal: + | ------ + | + ::: main.py:4:7 + | + 4 | class Dog(animals.Animal): + | --- + "); + } + + #[test] + fn implementation_class_call_with_assigned_constructor() { + let test = cursor_test( + r#" + def init(self): + pass + + class Base: + __init__ = init + + class Child(Base): + pass + + Base() + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:11:1 + | + 11 | Base() + | ^^^^ Clicking here + info: Found 2 implementations + --> main.py:5:7 + | + 5 | class Base: + | ---- + 6 | __init__ = init + 7 | + 8 | class Child(Base): + | ----- + "); + } + + #[test] + fn implementation_nested_class_reference() { + let test = cursor_test( + r#" + class Outer: + class Inner: + pass + + class SubInner(Outer.Inner): + pass + + def f(x: Outer.Inner): + pass + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r" + info[goto-implementation]: Go to implementation + --> main.py:9:16 + | + 9 | def f(x: Outer.Inner): + | ^^^^^ Clicking here + info: Found 2 implementations + --> main.py:3:11 + | + 3 | class Inner: + | ----- + 4 | pass + 5 | + 6 | class SubInner(Outer.Inner): + | -------- + "); + } + + #[test] + fn implementation_attribute_bound_to_class() { + // An attribute that resolves to a class object is a class reference, not a member + // lookup, matching how a bare name bound to a class behaves. + let test = cursor_test( + r#" + class Dog: + pass + + class Factory: + dog_cls = Dog + + def f(factory: Factory): + factory.dog_cls + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r" + info[goto-implementation]: Go to implementation + --> main.py:9:13 + | + 9 | factory.dog_cls + | ^^^^^^^ Clicking here + info: Found 1 implementation + --> main.py:2:7 + | + 2 | class Dog: + | --- + "); + } + + #[test] + fn implementation_mixed_class_value_attribute_uses_member_bindings() { + let test = cursor_test( + r#" + flag: bool + + class Dog: + pass + + class Puppy(Dog): + pass + + class Factory: + item: type[Dog] | int + if flag: + item = Dog + else: + item = 0 + + factory = Factory() + factory.item + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:18:9 + | + 18 | factory.item + | ^^^^ Clicking here + info: Found 3 implementations + --> main.py:11:5 + | + 11 | item: type[Dog] | int + | ---- + 12 | if flag: + 13 | item = Dog + | ---- + 14 | else: + 15 | item = 0 + | ---- + "); + } + + #[test] + fn implementation_mixed_class_and_module_binding_is_unsupported() { + let test = CursorTest::builder() + .source("flag_source.py", "flag: bool") + .source("helper.py", "value = 1") + .source( + "main.py", + r#" + import flag_source + + class Dog: + pass + + class Puppy(Dog): + pass + + if flag_source.flag: + import helper as Item + else: + Item = Dog + + Item + "#, + ) + .build(); + + assert_snapshot!(test.goto_implementation(), @"No goto target found"); + } + + #[test] + fn implementation_class_instance_reference_is_unsupported() { + // A bare reference to an instance is not a class reference, so it does not resolve to the + // class implementation family. + let test = cursor_test( + r#" + class Animal: + pass + + class Dog(Animal): + pass + + def f(animal: Animal): + animal + "#, + ); + + assert_snapshot!(test.goto_implementation(), @"No goto target found"); + } + + #[test] + fn implementation_class_stub_mapped_subclass() { + let test = CursorTest::builder() + .source( + "main.py", + r#" + class Base: + pass + "#, + ) + .source( + "mymodule.py", + r#" + from main import Base + + class Derived(Base): + pass + "#, + ) + .source( + "mymodule.pyi", + r#" + from main import Base + + class Derived(Base): ... + "#, + ) + .build(); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:2:7 + | + 2 | class Base: + | ^^^^ Clicking here + info: Found 2 implementations + --> main.py:2:7 + | + 2 | class Base: + | ---- + | + ::: mymodule.py:4:7 + | + 4 | class Derived(Base): + | ------- + "); + } + + #[test] + fn implementation_attribute_family_from_base_receiver() { + let test = cursor_test( + r#" + class Animal: + sound: str = "generic" + + class Dog(Animal): + sound: str = "woof" + + class Cat(Animal): + sound: str = "meow" + + def f(animal: Animal): + animal.sound + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r#" + info[goto-implementation]: Go to implementation + --> main.py:12:12 + | + 12 | animal.sound + | ^^^^^ Clicking here + info: Found 3 implementations + --> main.py:3:5 + | + 3 | sound: str = "generic" + | ----- + 4 | + 5 | class Dog(Animal): + 6 | sound: str = "woof" + | ----- + 7 | + 8 | class Cat(Animal): + 9 | sound: str = "meow" + | ----- + "#); + } + + #[test] + fn implementation_attribute_plain_assignment() { + let test = cursor_test( + r#" + class Animal: + sound = "generic" + + class Dog(Animal): + sound = "woof" + + def f(animal: Animal): + animal.sound + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r#" + info[goto-implementation]: Go to implementation + --> main.py:9:12 + | + 9 | animal.sound + | ^^^^^ Clicking here + info: Found 2 implementations + --> main.py:3:5 + | + 3 | sound = "generic" + | ----- + 4 | + 5 | class Dog(Animal): + 6 | sound = "woof" + | ----- + "#); + } + + #[test] + fn implementation_attribute_bare_annotation_declaration() { + let test = cursor_test( + r#" + class Animal: + sound: str + + class Dog(Animal): + sound: str = "woof" + + def f(animal: Animal): + animal.sound + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r#" + info[goto-implementation]: Go to implementation + --> main.py:9:12 + | + 9 | animal.sound + | ^^^^^ Clicking here + info: Found 2 implementations + --> main.py:3:5 + | + 3 | sound: str + | ----- + 4 | + 5 | class Dog(Animal): + 6 | sound: str = "woof" + | ----- + "#); + } + + #[test] + fn implementation_attribute_method_and_data_mixed() { + let test = cursor_test( + r#" + class Animal: + def speak(self): ... + + class Dog(Animal): + speak = 1 + + def f(animal: Animal): + animal.speak + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r" + info[goto-implementation]: Go to implementation + --> main.py:9:12 + | + 9 | animal.speak + | ^^^^^ Clicking here + info: Found 2 implementations + --> main.py:3:9 + | + 3 | def speak(self): ... + | ----- + 4 | + 5 | class Dog(Animal): + 6 | speak = 1 + | ----- + "); + } + + #[test] + fn implementation_attribute_instance_attribute_family() { + let test = cursor_test( + r#" + class Animal: + def __init__(self): + self.sound = "generic" + + class Dog(Animal): + def __init__(self): + self.sound = "woof" + + def f(animal: Animal): + animal.sound + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r#" + info[goto-implementation]: Go to implementation + --> main.py:11:12 + | + 11 | animal.sound + | ^^^^^ Clicking here + info: Found 2 implementations + --> main.py:4:9 + | + 4 | self.sound = "generic" + | ---------- + 5 | + 6 | class Dog(Animal): + 7 | def __init__(self): + 8 | self.sound = "woof" + | ---------- + "#); + } + + #[test] + fn implementation_attribute_instance_attribute_from_concrete_receiver() { + let test = cursor_test( + r#" + class Animal: + def __init__(self): + self.sound = "generic" + + class Dog(Animal): + pass + + class Cat(Animal): + def __init__(self): + self.sound = "meow" + + def f(dog: Dog): + dog.sound + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r#" + info[goto-implementation]: Go to implementation + --> main.py:14:9 + | + 14 | dog.sound + | ^^^^^ Clicking here + info: Found 1 implementation + --> main.py:4:9 + | + 4 | self.sound = "generic" + | ---------- + "#); + } + + #[test] + fn implementation_attribute_class_body_and_instance_mixed() { + let test = cursor_test( + r#" + class Animal: + sound: str = "generic" + + class Dog(Animal): + def __init__(self): + self.sound = "woof" + + def f(animal: Animal): + animal.sound + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r#" + info[goto-implementation]: Go to implementation + --> main.py:10:12 + | + 10 | animal.sound + | ^^^^^ Clicking here + info: Found 2 implementations + --> main.py:3:5 + | + 3 | sound: str = "generic" + | ----- + 4 | + 5 | class Dog(Animal): + 6 | def __init__(self): + 7 | self.sound = "woof" + | ---------- + "#); + } + + #[test] + fn implementation_attribute_class_body_takes_priority_over_instance() { + // When a class defines the attribute both in its body and on `self`, the class-body + // definition wins for that class, matching the goto-definition lookup. + let test = cursor_test( + r#" + class Animal: + sound: str = "generic" + def __init__(self): + self.sound = "override" + + def f(animal: Animal): + animal.sound + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r#" + info[goto-implementation]: Go to implementation + --> main.py:8:12 + | + 8 | animal.sound + | ^^^^^ Clicking here + info: Found 1 implementation + --> main.py:3:5 + | + 3 | sound: str = "generic" + | ----- + "#); + } + + #[test] + fn implementation_attribute_stub_mapped() { + let test = CursorTest::builder() + .source( + "main.py", + " +from mymodule import MyClass +def f(x: MyClass): + x.sound +", + ) + .source( + "mymodule.py", + r#" +class MyClass: + sound: str = "generic" +"#, + ) + .source( + "mymodule.pyi", + r#" +class MyClass: + sound: str +"#, + ) + .build(); + + assert_snapshot!(test.goto_implementation(), @r#" + info[goto-implementation]: Go to implementation + --> main.py:4:7 + | + 4 | x.sound + | ^^^^^ Clicking here + info: Found 1 implementation + --> mymodule.py:3:5 + | + 3 | sound: str = "generic" + | ----- + "#); + } + + #[test] + fn implementation_attribute_protocol_method_nominal_only() { + // TODO: the receiver is a `Protocol`, so implementations should be determined by structural + // subtyping and return all three `speak` definitions (`Speaker`, `Dog`, and `Cat`). We + // currently use nominal inheritance only and return `Speaker.speak` and `Cat.speak`. See + // https://github.com/astral-sh/ruff/pull/25410#discussion_r3344203732. + let test = cursor_test( + r#" + from typing import Protocol + + class Speaker(Protocol): + def speak(self) -> None: ... + + class Dog: + def speak(self) -> None: ... + + class Cat(Speaker): + def speak(self) -> None: ... + + def f(speaker: Speaker): + speaker.speak() + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:14:13 + | + 14 | speaker.speak() + | ^^^^^ Clicking here + info: Found 2 implementations + --> main.py:5:9 + | + 5 | def speak(self) -> None: ... + | ----- + | + ::: main.py:11:9 + | + 11 | def speak(self) -> None: ... + | ----- + "); + } + + #[test] + fn implementation_attribute_unreachable_override_excluded() { + // `FutureDog.speak` is defined in an unreachable block, so member lookup must not return + // it as an override. + let test = cursor_test( + r#" + import sys + + class Animal: + def speak(self): ... + + if sys.version_info >= (3, 5): + class Dog(Animal): + def speak(self): ... + + if sys.version_info >= (3, 999): + class FutureDog(Animal): + def speak(self): ... + + def f(animal: Animal): + animal.speak() + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r" + info[goto-implementation]: Go to implementation + --> main.py:16:12 + | + 16 | animal.speak() + | ^^^^^ Clicking here + info: Found 2 implementations + --> main.py:5:9 + | + 5 | def speak(self): ... + | ----- + 6 | + 7 | if sys.version_info >= (3, 5): + 8 | class Dog(Animal): + 9 | def speak(self): ... + | ----- + "); + } + + #[test] + fn implementation_attribute_unreachable_method_in_reachable_class_excluded() { + let test = cursor_test( + r#" + import sys + + class Animal: + def speak(self): ... + + class Dog(Animal): + if sys.version_info >= (3, 999): + def speak(self): ... + + def f(animal: Animal): + animal.speak() + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:12:12 + | + 12 | animal.speak() + | ^^^^^ Clicking here + info: Found 1 implementation + --> main.py:5:9 + | + 5 | def speak(self): ... + | ----- + "); + } + + #[test] + fn implementation_attribute_unreachable_data_in_reachable_class_excluded() { + let test = cursor_test( + r#" + import sys + + class Animal: + sound: str = "generic" + + class Dog(Animal): + def __init__(self): + if sys.version_info >= (3, 999): + self.sound = "woof" + + def f(animal: Animal): + animal.sound + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r#" + info[goto-implementation]: Go to implementation + --> main.py:13:12 + | + 13 | animal.sound + | ^^^^^ Clicking here + info: Found 1 implementation + --> main.py:5:5 + | + 5 | sound: str = "generic" + | ----- + "#); + } + + #[test] + fn implementation_unreachable_class_declaration_is_unsupported() { + let test = cursor_test( + r#" + import sys + + if sys.version_info >= (3, 999): + class Animal: + pass + + class Child(Animal): + pass + "#, + ); + + assert_snapshot!(test.goto_implementation(), @"No goto target found"); + } + + #[test] + fn implementation_unreachable_class_reference_is_unsupported() { + let test = cursor_test( + r#" + import sys + + if sys.version_info >= (3, 999): + class Animal: + pass + + class Child(Animal): + pass + + value: Animal + "#, + ); + + assert_snapshot!(test.goto_implementation(), @"No goto target found"); + } + + #[test] + fn implementation_unreachable_method_declaration_is_unsupported() { + let test = cursor_test( + r#" + import sys + + class Animal: + def speak(self): ... + + class Dog(Animal): + if sys.version_info >= (3, 999): + def speak(self): ... + + class Pup(Dog): + def speak(self): ... + "#, + ); + + assert_snapshot!(test.goto_implementation(), @"No goto target found"); + } + + impl CursorTest { + fn goto_implementation(&self) -> String { + let Some(targets) = salsa::attach(&self.db, || { + goto_implementation( + &self.db, + self.program_file(self.cursor.file), + self.cursor.offset, + ) + }) else { + return "No goto target found".to_string(); + }; + + self.render_diagnostics([crate::goto_definition::test::GotoDiagnostic::new( + crate::goto_definition::test::GotoAction::Implementation, + targets, + )]) + } + } +} diff --git a/crates/ty_ide/src/goto_type_definition.rs b/crates/ty_ide/src/goto_type_definition.rs index 6943cf1c95..710c3697da 100644 --- a/crates/ty_ide/src/goto_type_definition.rs +++ b/crates/ty_ide/src/goto_type_definition.rs @@ -1,27 +1,29 @@ use crate::goto::find_goto_target; use crate::{Db, HasNavigationTargets, NavigationTargets, RangedValue}; -use ruff_db::files::{File, FileRange}; +use ruff_db::files::FileRange; use ruff_db::parsed::parsed_module; use ruff_text_size::{Ranged, TextSize}; +use ty_python_core::ProgramFile; use ty_python_semantic::SemanticModel; pub fn goto_type_definition( db: &dyn Db, - file: File, + file: ProgramFile<'_>, offset: TextSize, ) -> Option> { - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, file.python_file(db)).load(db); let model = SemanticModel::new(db, file); let goto_target = find_goto_target(&model, &module, offset)?; let ty = goto_target.inferred_type(&model)?; + let env = model.program_environment(); - tracing::debug!("Inferred type of covering node is {}", ty.display(db)); + tracing::debug!("Inferred type of covering node is {}", ty.display(db, &env)); - let navigation_targets = ty.navigation_targets(db); + let navigation_targets = ty.navigation_targets(db, &env); Some(RangedValue { - range: FileRange::new(file, goto_target.range()), + range: FileRange::new(file.file(db), goto_target.range()), value: navigation_targets, }) } @@ -48,13 +50,11 @@ mod tests { | 4 | ab = Test() | ^^ Clicking here - | info: Found 1 type definition --> main.py:2:7 | 2 | class Test: ... | ---- - | "); } @@ -74,13 +74,11 @@ mod tests { | LL | ab = Literal | ^^ Clicking here - | info: Found 1 type definition --> stdlib/typing.byi:LL:1 | LL | Literal: _SpecialForm | ------- - | "); } @@ -102,13 +100,11 @@ mod tests { | LL | ab = Any | ^^ Clicking here - | info: Found 1 type definition --> stdlib/typing.byi:LL:7 | LL | class Any: | --- - | "); } @@ -129,13 +125,11 @@ mod tests { | LL | ab = Generic | ^^ Clicking here - | info: Found 1 type definition --> stdlib/typing.byi:LL:1 | LL | Generic: type[_Generic] | ------- - | "); } @@ -155,13 +149,11 @@ mod tests { | LL | ab = AlwaysTruthy | ^^ Clicking here - | info: Found 1 type definition --> stdlib/ty_extensions/__init__.pyi:LL:1 | LL | AlwaysTruthy: _SpecialForm | ------------ - | "); } @@ -183,13 +175,11 @@ mod tests { | LL | D().x | ^ Clicking here - | info: Found 1 type definition --> stdlib/ty_extensions/_internal.pyi:LL:1 | LL | Divergent: _SpecialForm | --------- - | "); } @@ -211,13 +201,11 @@ mod tests { | 6 | ab | ^^ Clicking here - | info: Found 1 type definition --> main.py:2:5 | 2 | def foo(a, b): ... | --- - | "); } @@ -245,7 +233,6 @@ mod tests { | 12 | a | ^ Clicking here - | info: Found 2 type definitions --> main.py:3:5 | @@ -254,7 +241,6 @@ mod tests { 4 | 5 | def bar(a, b): ... | --- - | "); } @@ -282,13 +268,11 @@ mod tests { | 12 | color | ^^^^^ Clicking here - | info: Found 1 type definition --> main.py:6:5 | 6 | BLUE = 2 | ---- - | "#); } @@ -317,7 +301,6 @@ mod tests { | 13 | color | ^^^^^ Clicking here - | info: Found 2 type definitions --> main.py:6:5 | @@ -325,7 +308,6 @@ mod tests { | ----- 7 | BLUE = 3 | ---- - | "#); } @@ -345,13 +327,11 @@ mod tests { | 2 | import lib | ^^^ Clicking here - | info: Found 1 type definition --> lib.py:1:1 | 1 | a = 10 | ------ - | "); } @@ -372,13 +352,11 @@ mod tests { | 2 | import lib.submod | ^^^ Clicking here - | info: Found 1 type definition --> lib/__init__.py:1:1 | 1 | b = 7 | ----- - | "); } @@ -399,13 +377,11 @@ mod tests { | 2 | import lib.submod | ^^^^^^ Clicking here - | info: Found 1 type definition --> lib/submod.py:1:1 | 1 | a = 10 | ------ - | "); } @@ -425,13 +401,11 @@ mod tests { | 2 | from lib import a | ^^^ Clicking here - | info: Found 1 type definition --> lib.py:1:1 | 1 | a = 10 | ------ - | "); } @@ -452,13 +426,11 @@ mod tests { | 2 | from lib.submod import a | ^^^ Clicking here - | info: Found 1 type definition --> lib/__init__.py:1:1 | 1 | b = 7 | ----- - | "); } @@ -479,13 +451,11 @@ mod tests { | 2 | from lib.submod import a | ^^^^^^ Clicking here - | info: Found 1 type definition --> lib/submod.py:1:1 | 1 | a = 10 | ------ - | "); } @@ -515,13 +485,11 @@ mod tests { | 2 | from .bot.botmod import * | ^^^^^^ Clicking here - | info: Found 1 type definition --> lib/sub/bot/botmod.py:1:1 | 1 | botmod = 31 | ----------- - | "); } @@ -551,13 +519,11 @@ mod tests { | 2 | from .bot.botmod import * | ^^^ Clicking here - | info: Found 1 type definition --> lib/sub/bot/__init__.py:1:1 | 1 | bot = 3 | ------- - | "); } @@ -587,13 +553,11 @@ mod tests { | 2 | from .bot.botmod import * | ^^^ Clicking here - | info: Found 1 type definition --> lib/sub/bot/__init__.py:1:1 | 1 | bot = 3 | ------- - | "); } @@ -638,13 +602,11 @@ mod tests { | 4 | lib | ^^^ Clicking here - | info: Found 1 type definition --> lib.py:1:1 | 1 | a = 10 | ------ - | "); } @@ -664,13 +626,11 @@ mod tests { | LL | a | ^ Clicking here - | info: Found 1 type definition --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | --- - | "); } #[test] @@ -687,13 +647,11 @@ mod tests { | LL | a: str = "test" | ^^^^^^ Clicking here - | info: Found 1 type definition --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | --- - | "#); } @@ -711,13 +669,11 @@ mod tests { | 2 | type Alias[T: int = bool] = list[T] | ^ Clicking here - | info: Found 1 type definition --> main.py:2:12 | 2 | type Alias[T: int = bool] = list[T] | - - | "); } @@ -735,13 +691,11 @@ mod tests { | 2 | type Alias[**P = [int, str]] = Callable[P, int] | ^ Clicking here - | info: Found 1 type definition --> main.py:2:14 | 2 | type Alias[**P = [int, str]] = Callable[P, int] | - - | "); } @@ -759,13 +713,11 @@ mod tests { | 2 | type Alias[*Ts = ()] = tuple[*Ts] | ^^ Clicking here - | info: Found 1 type definition --> main.py:2:13 | 2 | type Alias[*Ts = ()] = tuple[*Ts] | -- - | "); } @@ -787,13 +739,11 @@ mod tests { | 6 | Alias | ^^^^^ Clicking here - | info: Found 1 type definition --> main.py:4:1 | 4 | Alias = TypeAliasType("Alias", tuple[int, int]) | ----- - | "#); } @@ -814,13 +764,11 @@ mod tests { | 2 | a: "MyClass" = 1 | ^^^^^^^ Clicking here - | info: Found 1 type definition --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -841,13 +789,11 @@ mod tests { | 2 | a: "None | MyClass" = 1 | ^^^^^^^ Clicking here - | info: Found 1 type definition --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -868,7 +814,6 @@ mod tests { | LL | a: "None | MyClass" = 1 | ^^^^^^^^^^^^^^^^ Clicking here - | info: Found 2 type definitions --> main.py:LL:7 | @@ -879,7 +824,6 @@ mod tests { | LL | final class NoneType: | -------- - | "#); } @@ -900,13 +844,11 @@ mod tests { | 2 | a: "None | MyClass" = 1 | ^^^^^^^ Clicking here - | info: Found 1 type definition --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -927,7 +869,6 @@ mod tests { | LL | a: "None | MyClass" = 1 | ^^^^^^^^^^^^^^^^ Clicking here - | info: Found 2 type definitions --> main.py:LL:7 | @@ -938,7 +879,6 @@ mod tests { | LL | final class NoneType: | -------- - | "#); } @@ -959,13 +899,11 @@ mod tests { | LL | a: "MyClass |" = 1 | ^^^^^^^^^^^ Clicking here - | info: Found 1 type definition - --> stdlib/ty_extensions/__init__.pyi:LL:1 + --> stdlib/ty_extensions/_internal.pyi:LL:1 | LL | Unknown: _SpecialForm | ------- - | "#); } @@ -986,13 +924,11 @@ mod tests { | 2 | a: "MyClass | No" = 1 | ^^^^^^^ Clicking here - | info: Found 1 type definition --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -1013,13 +949,11 @@ mod tests { | LL | a: "MyClass | No" = 1 | ^^ Clicking here - | info: Found 1 type definition - --> stdlib/ty_extensions/__init__.pyi:LL:1 + --> stdlib/ty_extensions/_internal.pyi:LL:1 | LL | Unknown: _SpecialForm | ------- - | "#); } @@ -1037,13 +971,11 @@ mod tests { | LL | ab: "ab" | ^^ Clicking here - | info: Found 1 type definition - --> stdlib/ty_extensions/__init__.pyi:LL:1 + --> stdlib/ty_extensions/_internal.pyi:LL:1 | LL | Unknown: _SpecialForm | ------- - | "#); } @@ -1061,13 +993,11 @@ mod tests { | LL | x: "foobar" | ^^^^^^ Clicking here - | info: Found 1 type definition - --> stdlib/ty_extensions/__init__.pyi:LL:1 + --> stdlib/ty_extensions/_internal.pyi:LL:1 | LL | Unknown: _SpecialForm | ------- - | "#); } @@ -1088,13 +1018,11 @@ mod tests { | 2 | x: "list['MyClass | int'] | None" | ^^^^^^^ Clicking here - | info: Found 1 type definition --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -1115,13 +1043,11 @@ mod tests { | 2 | x: "list['int | MyClass'] | None" | ^^^^^^^ Clicking here - | info: Found 1 type definition --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -1142,13 +1068,11 @@ mod tests { | 2 | x: "list['int | None'] | MyClass" | ^^^^^^^ Clicking here - | info: Found 1 type definition --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -1169,13 +1093,11 @@ mod tests { | 2 | x: "list['int' | 'MyClass'] | None" | ^^^^^^^ Clicking here - | info: Found 1 type definition --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -1196,13 +1118,11 @@ mod tests { | 2 | x: "list['MyClass' | 'str'] | None" | ^^^^^^^ Clicking here - | info: Found 1 type definition --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -1223,13 +1143,11 @@ mod tests { | LL | x: """'list["MyClass" | "str"]' | None""" | ^^^^^^^^^ Clicking here - | info: Found 1 type definition - --> stdlib/ty_extensions/__init__.pyi:LL:1 + --> stdlib/ty_extensions/_internal.pyi:LL:1 | LL | Unknown: _SpecialForm | ------- - | "#); } @@ -1250,13 +1168,11 @@ mod tests { | 2 | x: """'list["int" | "str"]' | MyClass""" | ^^^^^^^ Clicking here - | info: Found 1 type definition --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -1291,13 +1207,11 @@ mod tests { | LL | x = ab | ^^ Clicking here - | info: Found 1 type definition --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | --- - | "); } @@ -1332,13 +1246,11 @@ mod tests { | LL | x = ab | ^^ Clicking here - | info: Found 1 type definition --> stdlib/builtins.byi:LL:7 | LL | class list[in out Element](MutableSequence[Element]): | ---- - | "); } @@ -1373,13 +1285,11 @@ mod tests { | LL | x = ab | ^^ Clicking here - | info: Found 1 type definition --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | --- - | "); } @@ -1426,13 +1336,11 @@ mod tests { | LL | x = ab | ^^ Clicking here - | info: Found 1 type definition --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | --- - | "); } @@ -1459,13 +1367,11 @@ mod tests { | 10 | case Click(x, button=ab): | ^^^^^ Clicking here - | info: Found 1 type definition --> main.py:2:7 | 2 | class Click: | ----- - | "); } @@ -1503,13 +1409,11 @@ mod tests { | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] | ^^ Clicking here - | info: Found 1 type definition --> main.py:2:13 | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] | -- - | "); } @@ -1527,13 +1431,11 @@ mod tests { | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] | ^^ Clicking here - | info: Found 1 type definition --> main.py:2:13 | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] | -- - | "); } @@ -1546,7 +1448,18 @@ mod tests { "#, ); - assert_snapshot!(test.goto_type_definition(), @"No goto target found"); + assert_snapshot!(test.goto_type_definition(), @" + info[goto-type definition]: Go to type definition + --> main.py:3:15 + | + 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] + | ^^ Clicking here + info: Found 1 type definition + --> main.py:3:15 + | + 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] + | -- + "); } #[test] @@ -1586,13 +1499,11 @@ mod tests { | 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] | ^^ Clicking here - | info: Found 1 type definition --> main.py:2:14 | 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] | -- - | "); } @@ -1612,13 +1523,11 @@ mod tests { | LL | test(a= "123") | ^ Clicking here - | info: Found 1 type definition --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | --- - | "#); } @@ -1641,13 +1550,11 @@ mod tests { | LL | test(a= 123) | ^ Clicking here - | info: Found 1 type definition --> stdlib/builtins.byi:LL:7 | LL | class int: | --- - | "); } @@ -1669,13 +1576,11 @@ f(**kwargs) | LL | f(**kwargs) | ^^^^^^ Clicking here - | info: Found 1 type definition --> stdlib/builtins.byi:LL:7 | LL | class dict[in out Key: Hashable, in out Value](MutableMapping[Key, Value]): | ---- - | "); } @@ -1702,13 +1607,11 @@ def outer(): | LL | return x # Should find the nonlocal x declaration in outer scope | ^ Clicking here - | info: Found 1 type definition --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | --- - | "); } @@ -1752,13 +1655,11 @@ def function(): | LL | return global_var # Should find the global variable declaration | ^^^^^^^^^^ Clicking here - | info: Found 1 type definition --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | --- - | "); } @@ -1794,13 +1695,11 @@ def function(): | LL | a | ^ Clicking here - | info: Found 1 type definition --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | --- - | "); } @@ -1823,13 +1722,11 @@ def function(): | 7 | x.foo() | ^ Clicking here - | info: Found 1 type definition --> main.py:2:7 | 2 | class X: | - - | "); } @@ -1849,13 +1746,11 @@ def function(): | 4 | foo() | ^^^ Clicking here - | info: Found 1 type definition --> main.py:2:5 | 2 | def foo(a, b): ... | --- - | "); } @@ -1875,13 +1770,11 @@ def function(): | LL | print(a) | ^ Clicking here - | info: Found 1 type definition --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | --- - | "); } @@ -1900,7 +1793,6 @@ def function(): | LL | a | ^ Clicking here - | info: Found 2 type definitions --> stdlib/builtins.byi:LL:7 | @@ -1911,7 +1803,6 @@ def function(): | LL | final class NoneType: | -------- - | "); } @@ -1942,11 +1833,11 @@ def function(): | 4 | x = subpkg | ^^^^^^ Clicking here - | info: Found 1 type definition - --> mypackage/subpkg/__init__.py:1:1 - | - | + --> mypackage/subpkg/__init__.py:1:1 + | + 1 | + | - "); } @@ -1977,11 +1868,11 @@ def function(): | 2 | from .subpkg.submod import val | ^^^^^^ Clicking here - | info: Found 1 type definition - --> mypackage/subpkg/__init__.py:1:1 - | - | + --> mypackage/subpkg/__init__.py:1:1 + | + 1 | + | - "); } @@ -2012,13 +1903,11 @@ def function(): | LL | x = submod | ^^^^^^ Clicking here - | info: Found 1 type definition - --> stdlib/ty_extensions/__init__.pyi:LL:1 + --> stdlib/ty_extensions/_internal.pyi:LL:1 | LL | Unknown: _SpecialForm | ------- - | "); } @@ -2049,14 +1938,12 @@ def function(): | 2 | from .subpkg.submod import val | ^^^^^^ Clicking here - | info: Found 1 type definition --> mypackage/subpkg/submod.py:1:1 | 1 | / 2 | | val: int = 0 | |_____________- - | "); } @@ -2086,14 +1973,12 @@ def function(): | 2 | from .subpkg import subpkg | ^^^^^^ Clicking here - | info: Found 1 type definition --> mypackage/subpkg/__init__.py:1:1 | 1 | / 2 | | subpkg: int = 10 | |_________________- - | "); } @@ -2123,13 +2008,11 @@ def function(): | LL | from .subpkg import subpkg | ^^^^^^ Clicking here - | info: Found 1 type definition --> stdlib/builtins.byi:LL:7 | LL | class int: | --- - | "); } @@ -2159,20 +2042,22 @@ def function(): | LL | x = subpkg | ^^^^^^ Clicking here - | info: Found 1 type definition --> stdlib/builtins.byi:LL:7 | LL | class int: | --- - | "); } impl CursorTest { fn goto_type_definition(&self) -> String { let Some(targets) = salsa::attach(&self.db, || { - goto_type_definition(&self.db, self.cursor.file, self.cursor.offset) + goto_type_definition( + &self.db, + self.program_file(self.cursor.file), + self.cursor.offset, + ) }) else { return "No goto target found".to_string(); }; diff --git a/crates/ty_ide/src/hints.rs b/crates/ty_ide/src/hints.rs index a358326f74..1581bc3382 100644 --- a/crates/ty_ide/src/hints.rs +++ b/crates/ty_ide/src/hints.rs @@ -1,6 +1,6 @@ -use ruff_db::files::File; use ruff_python_ast::name::Name; use ruff_text_size::TextRange; +use ty_python_core::ProgramFile; use ty_python_semantic::types::ide_support::{ UnreachableKind, unreachable_ranges, unused_bindings, }; @@ -26,7 +26,7 @@ pub enum HintKind { } impl HintKind { - pub fn message(&self) -> String { + fn message(&self) -> String { match self { Self::UnusedBinding(name) => format!("`{name}` is unused"), Self::UnreachableCode(UnreachableKind::Unconditional) => { @@ -40,8 +40,9 @@ impl HintKind { } } -pub fn hints(db: &dyn Db, file: File) -> Vec { - if !db.should_check_file(file) { +pub fn hints(db: &dyn Db, file: ProgramFile<'_>) -> Vec { + let source_file = file.file(db); + if !db.should_check_file(source_file) { return Vec::new(); } diff --git a/crates/ty_ide/src/hover.rs b/crates/ty_ide/src/hover.rs index 2e787348dd..b565c27db9 100644 --- a/crates/ty_ide/src/hover.rs +++ b/crates/ty_ide/src/hover.rs @@ -1,7 +1,7 @@ use crate::docstring::{Docstring, DocstringFragment}; use crate::goto::{Definitions, GotoTarget, docstring_for_call_definition, find_goto_target}; use crate::{Db, MarkupKind, RangedValue}; -use ruff_db::files::{File, FileRange}; +use ruff_db::files::FileRange; use ruff_db::parsed::{ParsedModuleRef, parsed_module}; use ruff_db::source::source_text; use ruff_python_ast::find_node::covering_node; @@ -11,19 +11,32 @@ use ruff_python_literal::strftime; use ruff_text_size::{Ranged, TextRange, TextSize}; use std::fmt; use std::fmt::Formatter; +use ty_python_core::ProgramFile; +use ty_python_semantic::ProgramEnvironment; use ty_python_semantic::types::ide_support::{resolved_call_signature, typed_dict_key_hover}; use ty_python_semantic::types::{KnownInstanceType, Type, TypeAliasType, TypeVarVariance}; use ty_python_semantic::types::format::{SpecLanguage, spec_language}; -use ty_python_semantic::{DisplaySettings, HasType, SemanticModel, TypeQualifiers}; +use ty_python_semantic::{HasType, SemanticModel, TypeQualifiers}; -pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option>> { - let parsed = parsed_module(db, file).load(db); +pub fn hover<'db>( + db: &'db dyn Db, + file: ProgramFile<'db>, + offset: TextSize, +) -> Option>> { + let parsed = parsed_module(db, file.python_file(db)).load(db); let model = SemanticModel::new(db, file); // a format spec is text rather than code, so it has no goto target of its // own and has to be recognised before one is looked for - if let Some(hover) = format_spec_hover(db, &model, &parsed, file, offset) { + if let Some(hover) = format_spec_hover( + db, + &model.program_environment(), + &model, + &parsed, + file, + offset, + ) { return Some(hover); } @@ -35,6 +48,7 @@ pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option Option { @@ -111,10 +125,10 @@ pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option { let value_ty = alias.value_type(db); - alias_docstring = Definitions::from_ty(db, ty) + alias_docstring = Definitions::from_ty(db, &env, ty) .and_then(|def| def.docstring(db)) .or_else(|| { - Definitions::from_ty(db, value_ty).and_then(|def| def.docstring(db)) + Definitions::from_ty(db, &env, value_ty).and_then(|def| def.docstring(db)) }); HoverContent::TypeAlias { alias, qualifiers } @@ -145,8 +159,11 @@ pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option Option( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, model: &SemanticModel<'db>, parsed: &ParsedModuleRef, - file: File, + file: ProgramFile<'db>, offset: TextSize, ) -> Option>> { let covering = covering_node(parsed.syntax().into(), TextRange::empty(offset)); @@ -246,13 +264,13 @@ fn format_spec_hover<'db>( return None; } - let source = source_text(db, file); + let source = source_text(db, file.file(db)); let written = source.get(spec.range().start().to_usize()..spec.range().end().to_usize())?; let language = field .expression .inferred_type(model) - .and_then(|ty| spec_language(db, ty)); + .and_then(|ty| spec_language(db, env, ty)); let (sample, clauses) = match language { // `date`, `time` and `datetime` read strftime directives, a language // the mini-language's clauses say nothing about @@ -297,8 +315,11 @@ fn format_spec_hover<'db>( clauses, }))]; Some(RangedValue { - range: FileRange::new(file, spec.range()), - value: Hover { contents }, + range: FileRange::new(file.file(db), spec.range()), + value: Hover { + program_file: file, + contents, + }, }) } @@ -335,6 +356,7 @@ impl fmt::Display for FormatSpecHover { } pub struct Hover<'db> { + program_file: ProgramFile<'db>, contents: Vec>, } @@ -379,13 +401,15 @@ pub struct DisplayHover<'db, 'a> { impl fmt::Display for DisplayHover<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let db = self.db; let mut first = true; + let env = ProgramEnvironment::from_file(self.hover.program_file); for content in &self.hover.contents { if !first { self.kind.horizontal_line().fmt(f)?; } - content.display(self.db, self.kind).fmt(f)?; + content.display(db, &env, self.kind).fmt(f)?; first = false; } @@ -422,9 +446,15 @@ pub enum HoverContent<'db> { } impl<'db> HoverContent<'db> { - fn display(&self, db: &'db dyn Db, kind: MarkupKind) -> DisplayHoverContent<'_, 'db> { + fn display<'a>( + &'a self, + db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, + kind: MarkupKind, + ) -> DisplayHoverContent<'a, 'db> { DisplayHoverContent { db, + env, content: self, kind, } @@ -433,17 +463,17 @@ impl<'db> HoverContent<'db> { pub(crate) struct DisplayHoverContent<'a, 'db> { db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, content: &'a HoverContent<'db>, kind: MarkupKind, } impl<'db> DisplayHoverContent<'_, 'db> { fn ty_string_and_syntax(&self, ty: &Type<'db>) -> (String, &'static str) { + let db = self.db; // Special types like `` // render poorly with python syntax-highlighting but well as xml - let ty_string = ty - .display_with(self.db, DisplaySettings::default().multiline()) - .to_string(); + let ty_string = ty.display(db, self.env).multiline().to_string(); let syntax = if ty_string.starts_with('<') { "xml" } else { @@ -468,6 +498,7 @@ fn create_qualifier_suffix(qualifiers: TypeQualifiers) -> String { impl fmt::Display for DisplayHoverContent<'_, '_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let db = self.db; match self.content { HoverContent::Signature(signature) => { self.kind.fenced_code_block(&signature, "python").fmt(f) @@ -497,7 +528,7 @@ impl fmt::Display for DisplayHoverContent<'_, '_> { } HoverContent::TypeAlias { alias, qualifiers } => { let qualifier_suffix = create_qualifier_suffix(*qualifiers); - let declaration = alias.display_declaration(self.db); + let declaration = alias.display_declaration(db, self.env); self.kind .fenced_code_block(format!("{declaration}{qualifier_suffix}"), "python") @@ -579,7 +610,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -619,7 +649,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -651,11 +680,10 @@ mod tests { --> main.py:3:15 | 3 | print(f"{name:+}") - | - + | ^ | | | source | Cursor offset - | "#); } @@ -687,7 +715,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -723,7 +750,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -763,7 +789,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -792,7 +817,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -833,7 +857,6 @@ mod tests { | ^- Cursor offset | | | source - | "); } @@ -891,7 +914,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -947,7 +969,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -997,7 +1018,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -1059,7 +1079,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -1119,7 +1138,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -1173,7 +1191,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -1227,7 +1244,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -1288,7 +1304,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -1326,7 +1341,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -1374,7 +1388,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -1404,7 +1417,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -1442,7 +1454,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -1490,7 +1501,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -1536,7 +1546,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -1582,11 +1591,10 @@ mod tests { --> main.py:12:5 | 12 | x = S(1) - | - + | ^ | | | source | Cursor offset - | "); } @@ -1624,11 +1632,10 @@ mod tests { --> main.py:12:5 | 12 | x = S(1) - | - + | ^ | | | source | Cursor offset - | "); } @@ -1661,7 +1668,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -1694,7 +1700,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -1735,7 +1740,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -1781,7 +1785,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -1826,7 +1829,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -1867,7 +1869,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -1908,7 +1909,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -1980,7 +1980,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -2024,7 +2023,6 @@ mod tests { | || | |Cursor offset | source - | "); } @@ -2065,7 +2063,6 @@ mod tests { | | | | | Cursor offset | source - | "#); let literal_string = hover_test( @@ -2105,7 +2102,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -2151,7 +2147,6 @@ mod tests { | || | |Cursor offset | source - | "); } @@ -2191,7 +2186,6 @@ mod tests { | || | |Cursor offset | source - | "); } @@ -2240,7 +2234,6 @@ mod tests { | || | |Cursor offset | source - | "); } @@ -2297,7 +2290,6 @@ mod tests { | || | |Cursor offset | source - | "); } @@ -2350,7 +2342,6 @@ mod tests { | || | |Cursor offset | source - | "); } @@ -2402,7 +2393,6 @@ mod tests { | || | |Cursor offset | source - | "); } @@ -2455,7 +2445,6 @@ mod tests { | || | |Cursor offset | source - | "); } @@ -2490,11 +2479,10 @@ mod tests { --> main.py:14:5 | 14 | foo.a - | - + | ^ | | | source | Cursor offset - | "); } @@ -2528,7 +2516,6 @@ mod tests { | ^^^- Cursor offset | | | source - | "); } @@ -2556,7 +2543,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -2596,7 +2582,6 @@ mod tests { | || | |Cursor offset | source - | "); } @@ -2638,7 +2623,6 @@ mod tests { | ^^^^^- Cursor offset | | | source - | "); } @@ -2668,7 +2652,6 @@ mod tests { | || | |Cursor offset | source - | "); } @@ -2708,7 +2691,6 @@ mod tests { | || | |Cursor offset | source - | "); } @@ -2748,7 +2730,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -2798,7 +2779,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -2845,7 +2825,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -2882,7 +2861,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -2915,7 +2893,6 @@ mod tests { | || | |Cursor offset | source - | "); } @@ -2955,7 +2932,6 @@ mod tests { | ^- Cursor offset | | | source - | "); } @@ -2990,7 +2966,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -3025,7 +3000,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -3073,7 +3047,6 @@ mod tests { | ^^^^^^^- Cursor offset | | | source - | "#); } @@ -3136,7 +3109,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -3166,7 +3138,6 @@ mod tests { | || | |Cursor offset | source - | "#); } @@ -3193,7 +3164,6 @@ mod tests { | || | |Cursor offset | source - | "#); } @@ -3220,7 +3190,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -3255,7 +3224,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -3290,7 +3258,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -3325,7 +3292,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -3360,7 +3326,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -3395,7 +3360,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -3425,7 +3389,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -3460,7 +3423,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -3518,7 +3480,6 @@ def ab(a: str): ... | || | |Cursor offset | source - | "); } @@ -3564,7 +3525,6 @@ def bar() -> None: | | | | | Cursor offset | source - | "); } @@ -3622,7 +3582,6 @@ def ab(a: str): | || | |Cursor offset | source - | "#); } @@ -3686,7 +3645,6 @@ def ab(a: int): | || | |Cursor offset | source - | "); } @@ -3744,7 +3702,6 @@ def ab(a: int): | || | |Cursor offset | source - | "); } @@ -3814,7 +3771,6 @@ def ab(a: int, *, c: int): | || | |Cursor offset | source - | "); } @@ -3884,7 +3840,6 @@ def ab(a: int, *, c: int): | || | |Cursor offset | source - | "); } @@ -3946,7 +3901,6 @@ def ab(a: int, *, c: int): | ^^^- Cursor offset | | | source - | "); } @@ -3996,7 +3950,6 @@ def ab(a: int, *, c: int): | ^^^- Cursor offset | | | source - | "); } @@ -4047,7 +4000,6 @@ def ab(a: int, *, c: int): | | | | | Cursor offset | source - | "); } @@ -4082,7 +4034,6 @@ def outer(): | ^- Cursor offset | | | source - | "#); } @@ -4135,7 +4086,6 @@ def function(): | | | | | Cursor offset | source - | "#); } @@ -4196,7 +4146,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -4240,7 +4189,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -4284,7 +4232,6 @@ def function(): | || | |Cursor offset | source - | "#); } @@ -4340,7 +4287,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -4376,7 +4322,6 @@ def function(): | | | | | Cursor offset | source - | "); } @@ -4423,7 +4368,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -4450,7 +4394,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -4463,7 +4406,22 @@ def function(): "#, ); - assert_snapshot!(test.hover(), @"Hover provided no content"); + assert_snapshot!(test.hover(), @" + AB@Alias2 (contravariant) + --------------------------------------------- + ```python + AB@Alias2 (contravariant) + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:3:15 + | + 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] + | ^- + | || + | |Cursor offset + | source + "); } #[test] @@ -4492,7 +4450,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -4530,7 +4487,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -4581,7 +4537,6 @@ def function(): | | | | | Cursor offset | source - | "); } @@ -4638,7 +4593,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -4652,10 +4606,10 @@ def function(): // TODO: Should this be constravariant instead? assert_snapshot!(test.hover(), @" - P@Alias (bivariant) + P@Alias (covariant) --------------------------------------------- ```python - P@Alias (bivariant) + P@Alias (covariant) ``` --------------------------------------------- info[hover]: Hovered content is @@ -4665,7 +4619,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -4691,7 +4644,6 @@ def function(): | ^^- Cursor offset | | | source - | "); } @@ -4730,7 +4682,6 @@ def function(): | ^^^^^- Cursor offset | | | source - | "); } @@ -4778,7 +4729,6 @@ def function(): | ^^^^^- Cursor offset | | | source - | "); } @@ -4824,7 +4774,6 @@ def function(): | ^^^^- Cursor offset | | | source - | "); } @@ -4872,7 +4821,6 @@ def function(): | ^^^^- Cursor offset | | | source - | "); } @@ -4912,7 +4860,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -4952,7 +4899,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -4995,7 +4941,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -5036,7 +4981,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -5080,7 +5024,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -5110,7 +5053,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -5138,7 +5080,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -5167,7 +5108,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -5199,7 +5139,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -5231,7 +5170,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -5265,7 +5203,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -5332,7 +5269,6 @@ def function(): | | | | | Cursor offset | source - | "); } @@ -5501,7 +5437,6 @@ def function(): | | | | | Cursor offset | source - | "#); } @@ -5539,7 +5474,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -5569,7 +5503,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -5599,7 +5532,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -5645,7 +5577,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -5683,7 +5614,6 @@ def function(): | | | | | Cursor offset | source - | "); } @@ -5712,7 +5642,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -5747,7 +5676,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -5782,7 +5710,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -5818,7 +5745,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -5855,7 +5781,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -5893,7 +5818,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -5922,7 +5846,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -5950,7 +5873,6 @@ def function(): | ^- Cursor offset | | | source - | "); let test = hover_test( @@ -5975,7 +5897,6 @@ def function(): | ^- Cursor offset | | | source - | "); let test = hover_test( @@ -6000,7 +5921,6 @@ def function(): | ^- Cursor offset | | | source - | "); let test = hover_test( @@ -6025,7 +5945,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -6052,7 +5971,6 @@ def function(): | ^- Cursor offset | | | source - | "); let test = hover_test( @@ -6076,7 +5994,6 @@ def function(): | ^- Cursor offset | | | source - | "); let test = hover_test( @@ -6100,7 +6017,6 @@ def function(): | ^- Cursor offset | | | source - | "); let test = hover_test( @@ -6124,7 +6040,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -6150,7 +6065,6 @@ def function(): | ^- Cursor offset | | | source - | "); let test = hover_test( @@ -6173,7 +6087,6 @@ def function(): | ^- Cursor offset | | | source - | "); let test = hover_test( @@ -6196,7 +6109,6 @@ def function(): | ^- Cursor offset | | | source - | "); let test = hover_test( @@ -6219,7 +6131,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -6245,7 +6156,6 @@ def function(): | ^^^- Cursor offset | | | source - | "); } @@ -6282,7 +6192,6 @@ def function(): | ^^^^^^^- Cursor offset | | | source - | "); let test = hover_test( @@ -6307,7 +6216,6 @@ def function(): | | | | | Cursor offset | source - | "); } @@ -6338,7 +6246,6 @@ def function(): | ^- Cursor offset | | | source - | "); let test = hover_test( @@ -6366,7 +6273,6 @@ def function(): | ^- Cursor offset | | | source - | "); let test = hover_test( @@ -6394,7 +6300,6 @@ def function(): | ^- Cursor offset | | | source - | "); let test = hover_test( @@ -6422,7 +6327,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -6450,11 +6354,10 @@ def function(): --> main.py:2:12 | 2 | result = 5 + 3 - | - + | ^ | | | source | Cursor offset - | "); } @@ -6494,11 +6397,10 @@ def function(): --> main.py:15:8 | 15 | Test() + Test() - | - + | ^ | | | source | Cursor offset - | "); } @@ -6535,7 +6437,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -6566,10 +6467,64 @@ def function(): | ^^^^^- Cursor offset | | | source - | "); } + #[test] + fn hover_shadowed_numeric_builtin() { + let test = hover_test( + r#" + import builtins + + class float: ... + + def f(x: builtins.float | float): + x + "#, + ); + + assert_snapshot!(test.hover()); + } + + #[test] + fn hover_shadowed_numeric_builtin_in_selected_signature() { + let test = hover_test( + r#" + import builtins + from typing import overload + + class float: ... + + @overload + def choose(value: builtins.float | float) -> None: ... + @overload + def choose(value: str) -> None: ... + def choose(value: object) -> None: ... + + choose(1.0) + "#, + ); + + assert_snapshot!(test.hover()); + } + + #[test] + fn hover_shadowed_numeric_builtin_in_keyword_parameter() { + let test = hover_test( + r#" + import builtins + + class float: ... + + def choose(*, value: builtins.float | float) -> None: ... + + choose(value=1.0) + "#, + ); + + assert_snapshot!(test.hover()); + } + #[test] fn hover_bare_final_annotation() { let test = hover_test( @@ -6630,7 +6585,6 @@ def function(): | ^^^^^- Cursor offset | | | source - | "); let test = hover_test( @@ -6655,7 +6609,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -6681,7 +6634,6 @@ def function(): | ^^^- Cursor offset | | | source - | "); let test = hover_test( @@ -6704,7 +6656,6 @@ def function(): | ^^^- Cursor offset | | | source - | "); } @@ -6734,7 +6685,6 @@ def function(): | ^- Cursor offset | | | source - | "); let test = hover_test( @@ -6758,7 +6708,6 @@ def function(): | ^- Cursor offset | | | source - | "); let test = hover_test( @@ -6785,7 +6734,6 @@ def function(): | ^- Cursor offset | | | source - | "); let test = hover_test( @@ -6809,7 +6757,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -6849,7 +6796,6 @@ def function(): | | | | | Cursor offset | source - | "); } @@ -6889,7 +6835,6 @@ def function(): | | | | | Cursor offset | source - | "); } @@ -6929,7 +6874,6 @@ def function(): | | | | | Cursor offset | source - | "); } @@ -6969,7 +6913,6 @@ def function(): | | | | | Cursor offset | source - | "); } @@ -7008,7 +6951,6 @@ def function(): | | | | | Cursor offset | source - | "); } @@ -7047,7 +6989,6 @@ def function(): | | | | | Cursor offset | source - | "); } @@ -7086,7 +7027,6 @@ def function(): | | | | | Cursor offset | source - | "); } @@ -7130,7 +7070,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -7158,7 +7097,6 @@ def function(): | | | | | Cursor offset | source - | "); } @@ -7212,7 +7150,6 @@ class CoolType(str): | ^- Cursor offset | | | source - | "); } @@ -7248,7 +7185,6 @@ type U = MyType | ^- Cursor offset | | | source - | "); } @@ -7298,7 +7234,6 @@ type U = MyType | | | | | Cursor offset | source - | "); } @@ -7347,7 +7282,6 @@ type U = MyType | ^^^^^- Cursor offset | | | source - | "); } @@ -7394,7 +7328,6 @@ type U = MyType | ^^^^- Cursor offset | | | source - | "); } @@ -7402,7 +7335,11 @@ type U = MyType fn hover(&self) -> String { use std::fmt::Write; - let Some(hover) = hover(&self.db, self.cursor.file, self.cursor.offset) else { + let Some(hover) = hover( + &self.db, + self.program_file(self.cursor.file), + self.cursor.offset, + ) else { return "Hover provided no content".to_string(); }; diff --git a/crates/ty_ide/src/importer.rs b/crates/ty_ide/src/importer.rs index 1821696246..4c5fc4e0fb 100644 --- a/crates/ty_ide/src/importer.rs +++ b/crates/ty_ide/src/importer.rs @@ -18,8 +18,9 @@ The main differences here are: use rustc_hash::FxHashMap; -use ruff_db::files::File; use ruff_db::parsed::ParsedModuleRef; + +use ruff_db::files::File; use ruff_db::source::source_text; use ruff_diagnostics::Edit; use ruff_python_ast as ast; @@ -29,8 +30,9 @@ use ruff_python_ast::visitor::source_order::{SourceOrderVisitor, TraversalSignal use ruff_python_codegen::Stylist; use ruff_python_importer::Insertion; use ruff_text_size::{Ranged, TextRange, TextSize}; -use ty_module_resolver::ModuleName; +use ty_module_resolver::{ImportingFile, ModuleName}; use ty_project::Db; +use ty_python_core::ProgramFile; use ty_python_core::definition::DefinitionKind; use ty_python_semantic::types::Type; use ty_python_semantic::{MemberDefinition, SemanticModel}; @@ -40,7 +42,7 @@ pub(crate) struct Importer<'a> { db: &'a dyn Db, /// The file corresponding to the module that /// we want to insert an import statement into. - file: File, + file: ProgramFile<'a>, /// The parsed module ref. parsed: &'a ParsedModuleRef, /// The tokens representing the Python AST. @@ -73,7 +75,7 @@ impl<'a> Importer<'a> { pub(crate) fn new( db: &'a dyn Db, stylist: &'a Stylist<'a>, - file: File, + file: ProgramFile<'a>, source: &'a str, parsed: &'a ParsedModuleRef, ) -> Self { @@ -145,13 +147,17 @@ impl<'a> Importer<'a> { request: ImportRequest<'_>, members: &MembersInScope, ) -> ImportAction { - let request = request.avoid_conflicts(self.db, self.file, members); + let importing_file = ImportingFile::File( + self.file.file(self.db), + self.file.resolver_environment(self.db), + ); + let request = request.avoid_conflicts(self.db, importing_file, members); let mut symbol_text: Box = request.member.unwrap_or(request.module).into(); - let Some(response) = self.find(&request, members.at) else { + let Some(response) = self.find(importing_file, &request, members.at) else { let insertion = if let Some(future) = self.find_last_future_import(members.at) { Insertion::end_of_statement(future.stmt, self.source, self.stylist) } else { - let range = source_text(self.db, self.file) + let range = source_text(self.db, self.file.file(self.db)) .as_notebook() .and_then(|notebook| notebook.cell_offsets().containing_range(members.at)); @@ -222,11 +228,12 @@ impl<'a> Importer<'a> { /// satisfies the request. fn find<'importer>( &'importer self, + importing_file: ImportingFile<'_>, request: &ImportRequest<'_>, available_at: TextSize, ) -> Option> { let mut choice = None; - let source = source_text(self.db, self.file); + let source = source_text(self.db, self.file.file(self.db)); let notebook = source.as_notebook(); for import in &self.imports { @@ -247,7 +254,7 @@ impl<'a> Importer<'a> { return choice; } - if let Some(response) = import.satisfies(self.db, self.file, request) { + if let Some(response) = import.satisfies(self.db, importing_file, request) { let partial = matches!(response.kind, ImportResponseKind::Partial { .. }); // The LSP doesn't support edits across cell boundaries. @@ -283,7 +290,7 @@ impl<'a> Importer<'a> { /// Find the last `from __future__` import statement in the AST. fn find_last_future_import(&self, at: TextSize) -> Option<&'a AstImport> { - let source = source_text(self.db, self.file); + let source = source_text(self.db, self.file.file(self.db)); let notebook = source.as_notebook(); self.imports @@ -330,7 +337,7 @@ pub struct MembersInScope<'ast> { impl<'ast> MembersInScope<'ast> { fn new( db: &'ast dyn Db, - file: File, + file: ProgramFile<'ast>, parsed: &'ast ParsedModuleRef, node: ast::AnyNodeRef<'_>, at: TextSize, @@ -366,16 +373,13 @@ impl<'ast> MembersInScope<'ast> { } pub(crate) fn find_member(&self, symbol_name: &str) -> Option<&MemberInScope> { - self.map - .iter() - .find(|(name, _)| *name == symbol_name) - .map(|(_, member)| member) + self.map.get(symbol_name) } pub(crate) fn satisfies( &self, db: &dyn Db, - importing_file: File, + importing_file: ImportingFile<'_>, request: &ImportRequest<'_>, ) -> bool { let symbol_text = request.member.unwrap_or(request.module); @@ -411,7 +415,7 @@ impl<'ast> MemberInScope<'ast> { fn satisfies_anywhere( &self, db: &dyn Db, - importing_file: File, + importing_file: ImportingFile<'_>, request: &ImportRequest<'_>, ) -> bool { let MemberImportKind::Imported(ref ast_import) = self.kind else { @@ -483,7 +487,7 @@ impl<'ast> AstImport<'ast> { fn satisfies<'importer>( &'importer self, db: &'_ dyn Db, - importing_file: File, + importing_file: ImportingFile<'_>, request: &ImportRequest<'_>, ) -> Option> { self.kind @@ -514,7 +518,7 @@ impl<'ast> AstImportKind<'ast> { fn satisfies<'importer>( &'importer self, db: &'_ dyn Db, - importing_file: File, + importing_file: ImportingFile<'_>, request: &ImportRequest<'_>, ) -> Option> { match *self { @@ -638,7 +642,12 @@ impl<'a> ImportRequest<'a> { /// Attempts to change the import request style so that the chances /// of an import conflict are minimized (although not always reduced /// to zero). - fn avoid_conflicts(self, db: &dyn Db, importing_file: File, members: &MembersInScope) -> Self { + fn avoid_conflicts( + self, + db: &dyn Db, + importing_file: ImportingFile<'_>, + members: &MembersInScope, + ) -> Self { let Some(member) = self.member else { return Self { style: ImportStyle::Import, @@ -921,6 +930,7 @@ mod tests { use ty_module_resolver::SearchPathSettings; use ty_project::ProjectMetadata; use ty_python_core::program::{Program, ProgramSettings}; + use ty_python_semantic::Db as _; use ty_python_semantic::{PythonVersionWithSource, SemanticModel}; use super::*; @@ -975,7 +985,11 @@ mod tests { Importer::new( &self.db, &self.cursor.stylist, - self.cursor.file, + ProgramFile::new( + &self.db, + self.cursor.file, + self.db.program_environment().program(&self.db), + ), self.cursor.source.as_str(), &self.cursor.parsed, ) diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index c12e11834b..0f5a509bd5 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -1,11 +1,11 @@ use std::{fmt, vec}; +use ty_python_semantic::ProgramEnvironment; use itertools::{Either, Itertools}; use rustc_hash::FxHashMap; use crate::importer::{ImportAction, ImportRequest, Importer, MembersInScope}; use crate::{Db, HasNavigationTargets, NavigationTarget}; -use ruff_db::files::File; use ruff_db::parsed::parsed_module; use ruff_db::source::source_text; use ruff_python_ast::name::Name; @@ -17,6 +17,7 @@ use ruff_python_codegen::Stylist; use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextRange, TextSize}; use ty_module_resolver::file_to_module; +use ty_python_core::ProgramFile; use ty_python_semantic::reified::inferred_reified_type_param_names; use ty_python_semantic::types::context_params::implicit_context_arguments; use ty_python_semantic::types::ide_support::{ @@ -36,11 +37,11 @@ pub struct InlayHint { } impl InlayHint { - fn variable_type( - context: InlayHintImportContext, + fn variable_type<'db>( + context: InlayHintImportContext<'_, 'db>, expr: &Expr, rhs: &Expr, - ty: Type, + ty: Type<'db>, mut allow_edits: bool, named_type_arguments: bool, ) -> Option { @@ -52,14 +53,15 @@ impl InlayHint { } = context; let position = expr.range().end(); + let env = ProgramEnvironment::from_file(file); // Render the type to a string, and get subspans for all the types that make it up - let settings = DisplaySettings::from_possibly_ambiguous_types(db, [ty]); + let settings = DisplaySettings::from_possibly_ambiguous_types(db, &env, [ty]); let settings = if named_type_arguments { settings.with_named_type_arguments() } else { settings }; - let details = ty.display_with(db, settings).to_string_parts(); + let details = ty.display_with(db, &env, settings).to_string_parts(); // Filter out repetitive hints like `x: T = T()` if call_matches_name(rhs, &details.label) { @@ -95,8 +97,8 @@ impl InlayHint { } // Possibly import the current type and return the qualified name - let mut qualified_name = |dynamic_importer: &mut DynamicImporter| { - let type_definition = ty.definition(db)?; + let mut qualified_name = |dynamic_importer: &mut DynamicImporter<'_, 'db>| { + let type_definition = ty.definition(db, &env)?; let definition = type_definition.definition()?; // Only module-level names can be imported with `from import `. @@ -108,7 +110,7 @@ impl InlayHint { // Don't try to import symbols in scope let definition_file = definition.file(db); - if definition_file == file { + if definition_file == file.file(db) { return None; } @@ -119,7 +121,8 @@ impl InlayHint { .as_deref() .unwrap_or(&details.label[start..end]); - let module = file_to_module(db, definition_file)?; + let file = definition.program_file(db); + let module = file_to_module(db, file.resolver_file(db))?; if should_skip_import(db, module, *ty) { return None; @@ -129,6 +132,7 @@ impl InlayHint { dynamic_importer.import_symbol( db, + &env, ty, module_name, definition_name, @@ -148,7 +152,7 @@ impl InlayHint { qualified_name.len().cast_signed() - (end - start).cast_signed(); } - let target = ty.navigation_targets(db).into_iter().next(); + let target = ty.navigation_targets(db, &env).into_iter().next(); // Always use original text for the label part label_parts.push( @@ -211,12 +215,17 @@ impl InlayHint { /// basedpython: the exception set inferred for a function with no `raises` /// clause, shown where the clause would be written. - fn inferred_raises(db: &dyn Db, position: TextSize, raised: Type) -> Self { + fn inferred_raises( + db: &dyn Db, + env: &ProgramEnvironment<'_>, + position: TextSize, + raised: Type, + ) -> Self { Self { position, kind: InlayHintKind::Raises, label: InlayHintLabel { - parts: vec![format!(" raises {}", raised.display(db)).into()], + parts: vec![format!(" raises {}", raised.display(db, env)).into()], }, text_edits: vec![], } @@ -271,6 +280,7 @@ impl InlayHint { /// and its argument list — where an explicit specialization would be written. fn call_type_arguments( db: &dyn Db, + env: &ProgramEnvironment<'_>, position: TextSize, arguments: &[(Name, Type)], named: bool, @@ -289,8 +299,8 @@ impl InlayHint { parts.push(format!("{parameter}=").into()); } parts.push( - InlayHintLabelPart::new(argument.display(db).to_string()) - .with_target(argument.navigation_targets(db).into_iter().next()), + InlayHintLabelPart::new(argument.display(db, env).to_string()) + .with_target(argument.navigation_targets(db, env).into_iter().next()), ); } @@ -355,12 +365,17 @@ impl InlayHint { } /// The type a `reveal_type` call reveals, shown at the end of its line. - fn revealed_type(db: &dyn Db, position: TextSize, revealed: Type) -> Self { + fn revealed_type( + db: &dyn Db, + env: &ProgramEnvironment<'_>, + position: TextSize, + revealed: Type, + ) -> Self { Self { position, kind: InlayHintKind::RevealedType, label: InlayHintLabel { - parts: vec![format!(" revealed: {}", revealed.display(db)).into()], + parts: vec![format!(" revealed: {}", revealed.display(db, env)).into()], }, text_edits: vec![], } @@ -369,6 +384,7 @@ impl InlayHint { /// The parameters a source never spells, shown where they would be written. fn implicit_parameters( db: &dyn Db, + env: &ProgramEnvironment<'_>, position: TextSize, parameters: &[(&str, Option)], parameter_follows: bool, @@ -381,7 +397,7 @@ impl InlayHint { } parts.push(InlayHintLabelPart::new(*name)); if let Some(ty) = ty { - parts.push(format!(": {}", ty.display(db)).into()); + parts.push(format!(": {}", ty.display(db, env)).into()); } } @@ -398,12 +414,17 @@ impl InlayHint { } /// The inferred type of an unannotated lambda parameter. - fn lambda_parameter_type(db: &dyn Db, position: TextSize, ty: Type) -> Self { + fn lambda_parameter_type( + db: &dyn Db, + env: &ProgramEnvironment<'_>, + position: TextSize, + ty: Type, + ) -> Self { Self { position, kind: InlayHintKind::Type, label: InlayHintLabel { - parts: vec![format!(": {}", ty.display(db)).into()], + parts: vec![format!(": {}", ty.display(db, env)).into()], }, text_edits: vec![], } @@ -525,24 +546,27 @@ pub struct InlayHintTextEdit { pub fn inlay_hints( db: &dyn Db, - file: File, + file: ProgramFile<'_>, range: TextRange, settings: &InlayHintSettings, ) -> Vec { // a hint is read as source, so it must spell types the way the file is // written — `1`, not `Literal[1]`, in a `.by` file - with_display_for_file(db, file, || inlay_hints_inner(db, file, range, settings)) + with_display_for_file(db, file.file(db), || { + inlay_hints_inner(db, file, range, settings) + }) } fn inlay_hints_inner( db: &dyn Db, - file: File, + file: ProgramFile<'_>, range: TextRange, settings: &InlayHintSettings, ) -> Vec { - let ast = parsed_module(db, file).load(db); + let ast = parsed_module(db, file.python_file(db)).load(db); + let source_file = file.file(db); - let source = source_text(db, file); + let source = source_text(db, source_file); let stylist = Stylist::from_tokens(ast.tokens(), source.as_str()); let importer = Importer::new(db, &stylist, file, source.as_str(), &ast); @@ -789,7 +813,7 @@ impl Default for InlayHintSettings { struct InlayHintImportContext<'a, 'db> { db: &'db dyn Db, - file: File, + file: ProgramFile<'db>, importer: &'a Importer<'db>, dynamic_imports: &'a mut FxHashMap, } @@ -823,7 +847,7 @@ struct InlayHintVisitor<'a, 'db> { impl<'a, 'db> InlayHintVisitor<'a, 'db> { fn new( db: &'db dyn Db, - file: File, + file: ProgramFile<'db>, importer: Importer<'db>, source: &'a str, range: TextRange, @@ -840,7 +864,7 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> { range, settings, in_no_edits_allowed: false, - source_type: file.source_type(db), + source_type: file.file(db).source_type(db), in_type_expression: false, in_lambda: false, enclosing_class: None, @@ -871,7 +895,7 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> { let context = InlayHintImportContext { db: self.db, - file: self.model.file(), + file: self.model.program_file(), importer: &self.importer, dynamic_imports: &mut self.dynamic_imports, }; @@ -888,13 +912,14 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> { /// The hint sits where the clause would be written — after the return /// annotation, before the `:` — so accepting it reads as ordinary source. fn add_inferred_raises(&mut self, function: &ast::StmtFunctionDef) { + let env = &self.model.program_environment(); if !self.settings.inferred_raises || function.raises.is_some() { return; } let Some(raised) = function .inferred_type(&self.model) - .and_then(|ty| inferred_raises(self.db, ty)) + .and_then(|ty| inferred_raises(self.db, env, ty)) else { return; }; @@ -905,7 +930,7 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> { .map_or_else(|| function.parameters.end(), Ranged::end); self.hints - .push(InlayHint::inferred_raises(self.db, position, raised)); + .push(InlayHint::inferred_raises(self.db, env, position, raised)); } /// basedpython: hint the variance ty infers for each type parameter of @@ -979,6 +1004,7 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> { /// basedpython: hint `override` on a method that overrides a superclass /// member without saying so. fn add_inferred_override(&mut self, function: &ast::StmtFunctionDef) { + let env = &self.model.program_environment(); if !self.settings.inferred_override || !self.is_basedpython() { return; } @@ -989,7 +1015,7 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> { let Some(superclass) = function .inferred_type(&self.model) - .and_then(|ty| inferred_override(self.db, class_ty, ty, &function.name)) + .and_then(|ty| inferred_override(self.db, env, class_ty, ty, &function.name)) else { return; }; @@ -998,12 +1024,16 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> { // position even on a decorated method self.hints.push(InlayHint::inferred_override( function.range().start(), - superclass.navigation_targets(self.db).into_iter().next(), + superclass + .navigation_targets(self.db, env) + .into_iter() + .next(), )); } /// Hint the type arguments inferred for a generic call. fn add_call_type_arguments(&mut self, call: &ast::ExprCall, arguments: &[(Name, Type<'db>)]) { + let env = &self.model.program_environment(); if !self.settings.call_type_arguments || arguments.is_empty() { return; } @@ -1018,6 +1048,7 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> { self.hints.push(InlayHint::call_type_arguments( self.db, + env, call.func.range().end(), arguments, self.names_type_arguments(), @@ -1037,6 +1068,7 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> { /// `context` declarations in scope, written where the lowering writes them /// — after the explicit arguments, by keyword. fn add_implicit_context_arguments(&mut self, call: &ast::ExprCall, callee: Option>) { + let env = &self.model.program_environment(); if !self.settings.implicit_arguments || !self.is_basedpython() { return; } @@ -1045,7 +1077,7 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> { return; }; - let arguments = implicit_context_arguments(self.db, self.model.file(), callee, call); + let arguments = implicit_context_arguments(self.db, env, self.model.file(), callee, call); if arguments.is_empty() { return; } @@ -1108,13 +1140,14 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> { /// Hint the extra arms the typing spec's numeric promotion adds to a /// `float` / `complex` type expression. fn add_numeric_promotion(&mut self, expr: &Expr) { + let env = &self.model.program_environment(); if !self.settings.numeric_promotions || !self.in_type_expression { return; } let Some(arms) = expr .inferred_type(&self.model) - .and_then(|ty| numeric_promotion(self.db, self.model.file(), ty)) + .and_then(|ty| numeric_promotion(self.db, env, self.model.file(), ty)) else { return; }; @@ -1125,6 +1158,7 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> { /// Hint the type a `reveal_type` call reveals, at the end of its line. fn add_revealed_type(&mut self, call: &ast::ExprCall) { + let env = &self.model.program_environment(); if !self.settings.revealed_types { return; } @@ -1142,6 +1176,7 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> { self.hints.push(InlayHint::revealed_type( self.db, + env, self.source.line_end(call.range().end()), revealed, )); @@ -1155,6 +1190,7 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> { /// and skipped before this is reached — the construct is hinted at its /// head instead. fn add_implicit_self(&mut self, parameter: &ast::Parameter) { + let env = &self.model.program_environment(); if !self.settings.implicit_self || !self.is_basedpython() || !parameter.range().is_empty() { return; } @@ -1164,6 +1200,7 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> { self.hints.push(InlayHint::implicit_parameters( self.db, + env, position, &[(parameter.name.as_str(), ty)], self.parameter_follows(position), @@ -1177,6 +1214,7 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> { /// binding belongs to the suite that opens after it, so the hint sits past /// the colon rather than between the callee and it. fn add_trailing_lambda_parameter(&mut self, function: &ast::StmtFunctionDef) { + let env = &self.model.program_environment(); if !self.settings.implicit_parameters { return; } @@ -1197,6 +1235,7 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> { self.hints.push(InlayHint::implicit_parameters( self.db, + env, colon + TextSize::from(1), ¶meters, false, @@ -1213,6 +1252,7 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> { /// Hint the inferred type of an unannotated lambda parameter. fn add_lambda_parameter_type(&mut self, parameter: &ast::Parameter) { + let env = &self.model.program_environment(); if !self.settings.lambda_parameter_types || !self.in_lambda || parameter.annotation.is_some() @@ -1227,6 +1267,7 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> { self.hints.push(InlayHint::lambda_parameter_type( self.db, + env, parameter.name.range().end(), ty, )); @@ -1592,19 +1633,23 @@ fn type_hint_is_excessive_for_expr(expr: &Expr) -> bool { Expr::Tuple(expr_tuple) => expr_tuple.elts.iter().all(type_hint_is_excessive_for_expr), // Various Literal[...] types which are always excessive to hint - | Expr::BytesLiteral(_) + Expr::BytesLiteral(_) | Expr::NumberLiteral(_) | Expr::BooleanLiteral(_) - | Expr::StringLiteral(_) + | Expr::StringLiteral(_) => true, // `None` isn't terribly verbose, but still redundant - | Expr::NoneLiteral(_) + Expr::NoneLiteral(_) => true, // This one expands to `str` which isn't verbose but is redundant - | Expr::FString(_) + Expr::FString(_) => true, // This one expands to `Template` which isn't verbose but is redundant - | Expr::TString(_)=> true, + Expr::TString(_) => true, // You too `+1 and `-1`, get back here - Expr::UnaryOp(ExprUnaryOp { op: UnaryOp::UAdd | UnaryOp::USub, operand, .. }) => matches!(**operand, Expr::NumberLiteral(_)), + Expr::UnaryOp(ExprUnaryOp { + op: UnaryOp::UAdd | UnaryOp::USub, + operand, + .. + }) => matches!(**operand, Expr::NumberLiteral(_)), // Everything else is reasonable _ => false, @@ -1678,8 +1723,9 @@ impl<'a, 'db> DynamicImporter<'a, 'db> { /// If the symbol in the text edit needs to be qualified, we return the qualified symbol text. fn import_symbol( &mut self, - db: &dyn Db, - ty: &Type, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: &Type<'db>, module_name: &str, symbol_name: &str, label_text: &str, @@ -1696,7 +1742,7 @@ impl<'a, 'db> DynamicImporter<'a, 'db> { let mut is_possibly_qualified_name = label_text.contains('.'); if let Some(member) = members.find_member(symbol_name) { - if member.ty.definition(db) == ty.definition(db) { + if member.ty.definition(db, env) == ty.definition(db, env) { return None; } @@ -1813,8 +1859,6 @@ mod tests { } let mut db = ty_project::TestDb::new(metadata); - db.init_program().unwrap(); - let source = dedent(source); let start = source.find(START); @@ -1852,9 +1896,9 @@ mod tests { } pub(super) struct InlayHintTest { - pub(super) db: ty_project::TestDb, - pub(super) file: File, - pub(super) range: TextRange, + db: ty_project::TestDb, + file: File, + range: TextRange, _insta_settings_guard: SettingsBindDropGuard, } @@ -1875,7 +1919,16 @@ mod tests { /// Returns the inlay hints for the given test case with custom settings. fn inlay_hints_with_settings(&mut self, settings: &InlayHintSettings) -> String { - let hints = inlay_hints(&self.db, self.file, self.range, settings); + let hints = inlay_hints( + &self.db, + ProgramFile::new( + &self.db, + self.file, + self.db.program_environment().program(&self.db), + ), + self.range, + settings, + ); let mut inlay_hint_buf = source_text(&self.db, self.file).as_str().to_string(); let mut text_edit_buf = inlay_hint_buf.clone(); @@ -1977,7 +2030,6 @@ Source with applied edits: let config = DisplayDiagnosticConfig::new("ty") .color(false) - .show_fix_diff(true) .context(0) .format(DiagnosticFormat::Full); @@ -2059,13 +2111,11 @@ Source with applied edits: | LL | class int: | ^^^ - | info: Source --> main2.py:LL:17 | LL | y[: int] = compute() | ^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -2113,78 +2163,66 @@ Source with applied edits: | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:5 | LL | y[: Literal[1]] = x | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:13 | LL | y[: Literal[1]] = x | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:5 | LL | z[: int] = i(1) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:5 | LL | w[: int] = z | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing.byi:LL:1 | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:6 | LL | bb[: Literal[b"foo"]] = aa | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class bytes(Sequence[int]): | ^^^^^ - | info: Source --> main2.py:LL:14 | LL | bb[: Literal[b"foo"]] = aa | ^^^^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -2241,104 +2279,88 @@ Source with applied edits: | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:6 | LL | x2[: Literal[1]], y2[: Literal["abc"]] = (x1, y1) | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:14 | LL | x2[: Literal[1]], y2[: Literal["abc"]] = (x1, y1) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing.byi:LL:1 | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:24 | LL | x2[: Literal[1]], y2[: Literal["abc"]] = (x1, y1) | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:32 | LL | x2[: Literal[1]], y2[: Literal["abc"]] = (x1, y1) | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:6 | LL | x3[: int], y3[: str] = (i(1), s('abc')) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:17 | LL | x3[: int], y3[: str] = (i(1), s('abc')) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:6 | LL | x4[: int], y4[: str] = (x3, y3) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:17 | LL | x4[: int], y4[: str] = (x3, y3) | ^^^ - | "#); } @@ -2362,39 +2384,33 @@ Source with applied edits: | LL | class int: | ^^^ - | info: Source --> main2.py:LL:10 | LL | (a[: int], *b[: list[int]]) = x | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class list[in out Element](MutableSequence[Element]): | ^^^^ - | info: Source --> main2.py:LL:21 | LL | (a[: int], *b[: list[int]]) = x | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:26 | LL | (a[: int], *b[: list[int]]) = x | ^^^ - | "); } @@ -2452,26 +2468,22 @@ Source with applied edits: | LL | class int: | ^^^ - | info: Source --> main2.py:LL:5 | LL | x[: int], _ignored = (i(1), s('abc')) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:16 | LL | __ignored, y[: str] = (i(1), s('abc')) | ^^^ - | "); } @@ -2499,13 +2511,11 @@ Source with applied edits: | LL | class int: | ^^^ - | info: Source --> main2.py:LL:15 | LL | __special__[: int] = i(1) | ^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -2552,104 +2562,88 @@ Source with applied edits: | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:6 | LL | x2[: Literal[1]], y2[: Literal["abc"]] = x1, y1 | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:14 | LL | x2[: Literal[1]], y2[: Literal["abc"]] = x1, y1 | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing.byi:LL:1 | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:24 | LL | x2[: Literal[1]], y2[: Literal["abc"]] = x1, y1 | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:32 | LL | x2[: Literal[1]], y2[: Literal["abc"]] = x1, y1 | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:6 | LL | x3[: int], y3[: str] = i(1), s('abc') | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:17 | LL | x3[: int], y3[: str] = i(1), s('abc') | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:6 | LL | x4[: int], y4[: str] = x3, y3 | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:17 | LL | x4[: int], y4[: str] = x3, y3 | ^^^ - | "#); } @@ -2687,143 +2681,121 @@ Source with applied edits: | LL | class tuple[out Element](Sequence[Element]): | ^^^^^ - | info: Source --> main2.py:LL:5 | LL | y[: tuple[Literal[1], Literal["abc"]]] = x | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing.byi:LL:1 | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:11 | LL | y[: tuple[Literal[1], Literal["abc"]]] = x | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:19 | LL | y[: tuple[Literal[1], Literal["abc"]]] = x | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing.byi:LL:1 | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:23 | LL | y[: tuple[Literal[1], Literal["abc"]]] = x | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:31 | LL | y[: tuple[Literal[1], Literal["abc"]]] = x | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class tuple[out Element](Sequence[Element]): | ^^^^^ - | info: Source --> main2.py:LL:5 | LL | z[: tuple[int, str]] = (i(1), s('abc')) | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:11 | LL | z[: tuple[int, str]] = (i(1), s('abc')) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:16 | LL | z[: tuple[int, str]] = (i(1), s('abc')) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class tuple[out Element](Sequence[Element]): | ^^^^^ - | info: Source --> main2.py:LL:5 | LL | w[: tuple[int, str]] = z | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:11 | LL | w[: tuple[int, str]] = z | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:16 | LL | w[: tuple[int, str]] = z | ^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -2875,156 +2847,132 @@ Source with applied edits: | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:6 | LL | x2[: Literal[1]], (y2[: Literal["abc"]], z2[: Literal[2]]) = (x1, (y1, z1)) | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:14 | LL | x2[: Literal[1]], (y2[: Literal["abc"]], z2[: Literal[2]]) = (x1, (y1, z1)) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing.byi:LL:1 | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:25 | LL | x2[: Literal[1]], (y2[: Literal["abc"]], z2[: Literal[2]]) = (x1, (y1, z1)) | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:33 | LL | x2[: Literal[1]], (y2[: Literal["abc"]], z2[: Literal[2]]) = (x1, (y1, z1)) | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing.byi:LL:1 | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:47 | LL | x2[: Literal[1]], (y2[: Literal["abc"]], z2[: Literal[2]]) = (x1, (y1, z1)) | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:55 | LL | x2[: Literal[1]], (y2[: Literal["abc"]], z2[: Literal[2]]) = (x1, (y1, z1)) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:6 | LL | x3[: int], (y3[: str], z3[: int]) = (i(1), (s('abc'), i(2))) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:18 | LL | x3[: int], (y3[: str], z3[: int]) = (i(1), (s('abc'), i(2))) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:29 | LL | x3[: int], (y3[: str], z3[: int]) = (i(1), (s('abc'), i(2))) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:6 | LL | x4[: int], (y4[: str], z4[: int]) = (x3, (y3, z3)) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:18 | LL | x4[: int], (y4[: str], z4[: int]) = (x3, (y3, z3)) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:29 | LL | x4[: int], (y4[: str], z4[: int]) = (x3, (y3, z3)) | ^^^ - | "#); } @@ -3056,39 +3004,33 @@ Source with applied edits: | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:5 | LL | y[: Literal[1]] = x | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:13 | LL | y[: Literal[1]] = x | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:5 | LL | w[: int] = z | ^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -3129,13 +3071,11 @@ Source with applied edits: | LL | class int: | ^^^ - | info: Source --> main2.py:LL:5 | LL | x[: int] = i(1) | ^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -3179,13 +3119,11 @@ Source with applied edits: | 3 | def __init__(self, y): | ^ - | info: Source --> main2.py:7:8 | 7 | a = A([y=]2) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -3223,13 +3161,11 @@ Source with applied edits: | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:17 | LL | x[: str] = ab | ^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -3266,26 +3202,22 @@ Source with applied edits: | LL | class list[in out Element](MutableSequence[Element]): | ^^^^ - | info: Source --> main2.py:LL:17 | LL | x[: list[str]] = ab | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:22 | LL | x[: list[str]] = ab | ^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -3322,39 +3254,33 @@ Source with applied edits: | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:17 | LL | x[: Literal["a", "b"]] = ab | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:25 | LL | x[: Literal["a", "b"]] = ab | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:30 | LL | x[: Literal["a", "b"]] = ab | ^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -3407,13 +3333,11 @@ Source with applied edits: | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:17 | LL | x[: str] = ab | ^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -3644,344 +3568,292 @@ Source with applied edits: | LL | class list[in out Element](MutableSequence[Element]): | ^^^^ - | info: Source --> main2.py:LL:5 | LL | a[: list[int]] = [1, 2] | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:10 | LL | a[: list[int]] = [1, 2] | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class list[in out Element](MutableSequence[Element]): | ^^^^ - | info: Source --> main2.py:LL:5 | LL | b[: list[int | float]] = [1.0, 2.0] | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:10 | LL | b[: list[int | float]] = [1.0, 2.0] | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class float: | ^^^^^ - | info: Source --> main2.py:LL:16 | LL | b[: list[int | float]] = [1.0, 2.0] | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class list[in out Element](MutableSequence[Element]): | ^^^^ - | info: Source --> main2.py:LL:5 | LL | c[: list[bool]] = [True, False] | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:13 | LL | final class bool(int): | ^^^^ - | info: Source --> main2.py:LL:10 | LL | c[: list[bool]] = [True, False] | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class list[in out Element](MutableSequence[Element]): | ^^^^ - | info: Source --> main2.py:LL:5 | LL | d[: list[None | Unknown]] = [None, None] | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/types.byi:LL:13 | LL | final class NoneType: | ^^^^^^^^ - | info: Source --> main2.py:LL:10 | LL | d[: list[None | Unknown]] = [None, None] | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target - --> stdlib/ty_extensions/__init__.pyi:LL:1 + --> stdlib/ty_extensions/_internal.pyi:LL:1 | LL | Unknown: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:17 | LL | d[: list[None | Unknown]] = [None, None] | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class list[in out Element](MutableSequence[Element]): | ^^^^ - | info: Source --> main2.py:LL:5 | LL | e[: list[str]] = ["hel", "lo"] | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:10 | LL | e[: list[str]] = ["hel", "lo"] | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class list[in out Element](MutableSequence[Element]): | ^^^^ - | info: Source --> main2.py:LL:5 | LL | f[: list[str]] = ['the', 're'] | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:10 | LL | f[: list[str]] = ['the', 're'] | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class list[in out Element](MutableSequence[Element]): | ^^^^ - | info: Source --> main2.py:LL:5 | LL | g[: list[str]] = [f"{ft}", f"{ft}"] | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:10 | LL | g[: list[str]] = [f"{ft}", f"{ft}"] | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class list[in out Element](MutableSequence[Element]): | ^^^^ - | info: Source --> main2.py:LL:5 | LL | h[: list[Template]] = [t"wow %d", t"wow %d"] | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/string/templatelib.byi:LL:13 | LL | final class Template: # TODO: consider making `Template` generic on `TypeVarTuple` | ^^^^^^^^ - | info: Source --> main2.py:LL:10 | LL | h[: list[Template]] = [t"wow %d", t"wow %d"] | ^^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class list[in out Element](MutableSequence[Element]): | ^^^^ - | info: Source --> main2.py:LL:5 | LL | i[: list[bytes]] = [b'/x01', b'/x02'] | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class bytes(Sequence[int]): | ^^^^^ - | info: Source --> main2.py:LL:10 | LL | i[: list[bytes]] = [b'/x01', b'/x02'] | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class list[in out Element](MutableSequence[Element]): | ^^^^ - | info: Source --> main2.py:LL:5 | LL | j[: list[int | float]] = [+1, +2.0] | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:10 | LL | j[: list[int | float]] = [+1, +2.0] | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class float: | ^^^^^ - | info: Source --> main2.py:LL:16 | LL | j[: list[int | float]] = [+1, +2.0] | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class list[in out Element](MutableSequence[Element]): | ^^^^ - | info: Source --> main2.py:LL:5 | LL | k[: list[int | float]] = [-1, -2.0] | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:10 | LL | k[: list[int | float]] = [-1, -2.0] | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class float: | ^^^^^ - | info: Source --> main2.py:LL:16 | LL | k[: list[int | float]] = [-1, -2.0] | ^^^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits --> main.py:1:1 | - 1 + from ty_extensions import Unknown + 1 + from ty_extensions._internal import Unknown 2 + from string.templatelib import Template 3 | - a = [1, 2] @@ -4040,39 +3912,33 @@ Source with applied edits: | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:5 | LL | x[: Literal[Color.RED]] = Color.RED | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:4:7 | 4 | class Color(Enum): | ^^^^^ - | info: Source --> main2.py:8:13 | 8 | x[: Literal[Color.RED]] = Color.RED | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:5:5 | 5 | RED = 1 | ^^^ - | info: Source --> main2.py:8:19 | 8 | x[: Literal[Color.RED]] = Color.RED | ^^^ - | "); } @@ -4108,91 +3974,77 @@ Source with applied edits: | LL | class tuple[out Element](Sequence[Element]): | ^^^^^ - | info: Source --> main2.py:LL:5 | LL | y[: tuple[MyClass, MyClass]] = (MyClass(), MyClass()) | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:7 | 2 | class MyClass: | ^^^^^^^ - | info: Source --> main2.py:7:11 | 7 | y[: tuple[MyClass, MyClass]] = (MyClass(), MyClass()) | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:7 | 2 | class MyClass: | ^^^^^^^ - | info: Source --> main2.py:7:20 | 7 | y[: tuple[MyClass, MyClass]] = (MyClass(), MyClass()) | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:7 | 2 | class MyClass: | ^^^^^^^ - | info: Source --> main2.py:8:5 | 8 | a[: MyClass], b[: MyClass] = MyClass(), MyClass() | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:7 | 2 | class MyClass: | ^^^^^^^ - | info: Source --> main2.py:8:19 | 8 | a[: MyClass], b[: MyClass] = MyClass(), MyClass() | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:7 | 2 | class MyClass: | ^^^^^^^ - | info: Source --> main2.py:9:5 | 9 | c[: MyClass], d[: MyClass] = (MyClass(), MyClass()) | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:7 | 2 | class MyClass: | ^^^^^^^ - | info: Source --> main2.py:9:19 | 9 | c[: MyClass], d[: MyClass] = (MyClass(), MyClass()) | ^^^^^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -4240,676 +4092,572 @@ Source with applied edits: | LL | class list[in out Element](MutableSequence[Element]): | ^^^^ - | info: Source --> main2.py:LL:18 | LL | self.x[: list[T@MyClass]] = x | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class tuple[out Element](Sequence[Element]): | ^^^^^ - | info: Source --> main2.py:LL:18 | LL | self.y[: tuple[U@MyClass, U@MyClass]] = y | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:7 | 2 | class MyClass[T, U]: | ^^^^^^^ - | info: Source --> main2.py:7:5 | 7 | x[: MyClass[int, str]] = MyClass[[int, str]]([x=][42], [y=]("a", "b")) | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:13 | LL | x[: MyClass[int, str]] = MyClass[[int, str]]([x=][42], [y=]("a", "b")) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:18 | LL | x[: MyClass[int, str]] = MyClass[[int, str]]([x=][42], [y=]("a", "b")) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:35 | LL | x[: MyClass[int, str]] = MyClass[[int, str]]([x=][42], [y=]("a", "b")) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:40 | LL | x[: MyClass[int, str]] = MyClass[[int, str]]([x=][42], [y=]("a", "b")) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:24 | 3 | def __init__(self, x: list[T], y: tuple[U, U]): | ^ - | info: Source --> main2.py:7:47 | 7 | x[: MyClass[int, str]] = MyClass[[int, str]]([x=][42], [y=]("a", "b")) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:36 | 3 | def __init__(self, x: list[T], y: tuple[U, U]): | ^ - | info: Source --> main2.py:7:57 | 7 | x[: MyClass[int, str]] = MyClass[[int, str]]([x=][42], [y=]("a", "b")) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class tuple[out Element](Sequence[Element]): | ^^^^^ - | info: Source --> main2.py:LL:5 | LL | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=](… | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:7 | 2 | class MyClass[T, U]: | ^^^^^^^ - | info: Source --> main2.py:8:11 | 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("… | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:19 | LL | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=](… | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:24 | LL | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=](… | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:7 | 2 | class MyClass[T, U]: | ^^^^^^^ - | info: Source --> main2.py:8:30 | 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("… | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:38 | LL | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=](… | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:43 | LL | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=](… | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:62 | LL | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=](… | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:67 | LL | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=](… | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:24 | 3 | def __init__(self, x: list[T], y: tuple[U, U]): | ^ - | info: Source --> main2.py:8:74 | 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("… | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:36 | 3 | def __init__(self, x: list[T], y: tuple[U, U]): | ^ - | info: Source --> main2.py:8:84 | 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("… | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:109 | LL | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=](… | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:114 | LL | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=](… | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:24 | 3 | def __init__(self, x: list[T], y: tuple[U, U]): | ^ - | info: Source --> main2.py:8:121 | 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("… | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:36 | 3 | def __init__(self, x: list[T], y: tuple[U, U]): | ^ - | info: Source --> main2.py:8:131 | 8 | …, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("a", "b"))) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:7 | 2 | class MyClass[T, U]: | ^^^^^^^ - | info: Source --> main2.py:9:5 | - 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("a"… + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("a",… | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:13 | LL | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("a"… | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:18 | LL | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("a"… | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:7 | 2 | class MyClass[T, U]: | ^^^^^^^ - | info: Source --> main2.py:9:29 | - 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("a"… + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("a",… | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:37 | LL | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("a"… | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:42 | LL | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("a"… | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:59 | LL | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("a"… | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:64 | LL | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("a"… | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:24 | 3 | def __init__(self, x: list[T], y: tuple[U, U]): | ^ - | info: Source --> main2.py:9:71 | - 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("a"… + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("a",… | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:36 | 3 | def __init__(self, x: list[T], y: tuple[U, U]): | ^ - | info: Source --> main2.py:9:81 | - 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("a"… + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("a",… | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:106 | LL | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("a"… | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:111 | LL | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("a"… | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:24 | 3 | def __init__(self, x: list[T], y: tuple[U, U]): | ^ - | info: Source --> main2.py:9:118 | - 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("a"… + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("a",… | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:36 | 3 | def __init__(self, x: list[T], y: tuple[U, U]): | ^ - | info: Source --> main2.py:9:128 | - 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("a"… + 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("a",… | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:7 | 2 | class MyClass[T, U]: | ^^^^^^^ - | info: Source --> main2.py:10:5 | 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("a… | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:13 | LL | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("a… | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:18 | LL | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("a… | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:7 | 2 | class MyClass[T, U]: | ^^^^^^^ - | info: Source --> main2.py:10:29 | 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("a… | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:37 | LL | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("a… | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:42 | LL | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("a… | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:60 | LL | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("a… | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:65 | LL | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("a… | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:24 | 3 | def __init__(self, x: list[T], y: tuple[U, U]): | ^ - | info: Source --> main2.py:10:72 | 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("a… | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:36 | 3 | def __init__(self, x: list[T], y: tuple[U, U]): | ^ - | info: Source --> main2.py:10:82 | 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("a… | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:107 | LL | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("a… | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:112 | LL | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("a… | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:24 | 3 | def __init__(self, x: list[T], y: tuple[U, U]): | ^ - | info: Source --> main2.py:10:119 | 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("a… | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:36 | 3 | def __init__(self, x: list[T], y: tuple[U, U]): | ^ - | info: Source --> main2.py:10:129 | 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass[[int, str]]([x=][42], [y=]("a", "b")), MyClass[[int, str]]([x=][42], [y=]("a… | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -4972,13 +4720,11 @@ Source with applied edits: | 2 | def foo(x: int): pass | ^ - | info: Source --> main2.py:3:6 | 3 | foo([x=]1) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -5015,13 +4761,11 @@ Source with applied edits: | 2 | def foo(x: int): pass | ^ - | info: Source --> main2.py:6:6 | 6 | foo([x=]y) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -5066,13 +4810,11 @@ Source with applied edits: | 2 | def foo(x: int): pass | ^ - | info: Source --> main2.py:10:6 | 10 | foo([x=]val.y) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -5118,13 +4860,11 @@ Source with applied edits: | 2 | def foo(x: int): pass | ^ - | info: Source --> main2.py:10:6 | 10 | foo([x=]x.y) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -5173,13 +4913,11 @@ Source with applied edits: | 2 | def foo(x: int): pass | ^ - | info: Source --> main2.py:12:6 | 12 | foo([x=]val.y()) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -5232,13 +4970,11 @@ Source with applied edits: | 4 | def foo(x: int): pass | ^ - | info: Source --> main2.py:14:6 | 14 | foo([x=]val.y()[1]) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -5277,65 +5013,55 @@ Source with applied edits: | LL | class list[in out Element](MutableSequence[Element]): | ^^^^ - | info: Source --> main2.py:LL:5 | LL | x[: list[int]] = [1] | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:10 | LL | x[: list[int]] = [1] | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class list[in out Element](MutableSequence[Element]): | ^^^^ - | info: Source --> main2.py:LL:5 | LL | y[: list[int]] = [2] | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:10 | LL | y[: list[int]] = [2] | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:9 | 2 | def foo(x: int): pass | ^ - | info: Source --> main2.py:7:6 | 7 | foo([x=]y[0]) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -5437,13 +5163,11 @@ Source with applied edits: | 2 | def foo(a: str, b: int, c: int, d: str): ... | ^ - | info: Source --> main2.py:4:6 | 4 | foo([a=]'foo', *t, d='bar') | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -5477,39 +5201,33 @@ Source with applied edits: | 2 | def foo(a: str, b: int, c: str): ... | ^ - | info: Source --> main2.py:4:6 | 4 | foo([a=]'foo', [b=]*t, [c=]'bar') | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:17 | 2 | def foo(a: str, b: int, c: str): ... | ^ - | info: Source --> main2.py:4:17 | 4 | foo([a=]'foo', [b=]*t, [c=]'bar') | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:25 | 2 | def foo(a: str, b: int, c: str): ... | ^ - | info: Source --> main2.py:4:25 | 4 | foo([a=]'foo', [b=]*t, [c=]'bar') | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -5542,26 +5260,22 @@ Source with applied edits: | 2 | def foo(a: int, b: int): ... | ^ - | info: Source --> main2.py:4:6 | 4 | foo([a=]1, [b=]*t) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:17 | 2 | def foo(a: int, b: int): ... | ^ - | info: Source --> main2.py:4:13 | 4 | foo([a=]1, [b=]*t) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -5594,13 +5308,11 @@ Source with applied edits: | 2 | def foo(a: int): ... | ^ - | info: Source --> main2.py:4:6 | 4 | foo([a=]*t) | ^ - | "); } @@ -5622,13 +5334,11 @@ Source with applied edits: | 2 | def foo(x: int, /, y: int): pass | ^ - | info: Source --> main2.py:3:9 | 3 | foo(1, [y=]2) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -5693,26 +5403,22 @@ Source with applied edits: | 3 | def __init__(self, x: int): pass | ^ - | info: Source --> main2.py:4:6 | 4 | Foo([x=]1) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:24 | 3 | def __init__(self, x: int): pass | ^ - | info: Source --> main2.py:5:10 | 5 | f = Foo([x=]1) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -5755,26 +5461,22 @@ Source with applied edits: | 5 | x: int | ^ - | info: Source --> main2.py:8:6 | 8 | Foo([x=]1, [y=]'a') | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:6:5 | 6 | y: str | ^ - | info: Source --> main2.py:8:13 | 8 | Foo([x=]1, [y=]'a') | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -5809,26 +5511,22 @@ Source with applied edits: | 3 | def __new__(cls, x: int): pass | ^ - | info: Source --> main2.py:4:6 | 4 | Foo([x=]1) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:22 | 3 | def __new__(cls, x: int): pass | ^ - | info: Source --> main2.py:5:10 | 5 | f = Foo([x=]1) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -5867,13 +5565,11 @@ Source with applied edits: | 3 | def __call__(self, x: int): pass | ^ - | info: Source --> main2.py:6:6 | 6 | Foo([x=]1) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -5923,13 +5619,11 @@ Source with applied edits: | 3 | def bar(self, y: int): pass | ^ - | info: Source --> main2.py:4:12 | 4 | Foo().bar([y=]2) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -5984,26 +5678,22 @@ Source with applied edits: | 8 | def choose(self: "Parent", parent_value: int) -> None: ... | ^^^^^^^^^^^^ - | info: Source --> main2.py:14:20 | 14 | parent.choose([parent_value=]1) | ^^^^^^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:6:31 | 6 | def choose(self: "Child", child_value: int) -> None: ... | ^^^^^^^^^^^ - | info: Source --> main2.py:15:19 | 15 | child.choose([child_value=]2) | ^^^^^^^^^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -6040,13 +5730,11 @@ Source with applied edits: | 4 | def bar(cls, y: int): pass | ^ - | info: Source --> main2.py:5:10 | 5 | Foo.bar([y=]2) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -6081,13 +5769,11 @@ Source with applied edits: | 4 | def bar(y: int): pass | ^ - | info: Source --> main2.py:5:10 | 5 | Foo.bar([y=]2) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -6120,26 +5806,22 @@ Source with applied edits: | 2 | def foo(x: int | str): pass | ^ - | info: Source --> main2.py:3:6 | 3 | foo([x=]1) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:9 | 2 | def foo(x: int | str): pass | ^ - | info: Source --> main2.py:4:6 | 4 | foo([x=]'abc') | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -6172,39 +5854,33 @@ Source with applied edits: | 2 | def foo(x: int, y: str, z: bool): pass | ^ - | info: Source --> main2.py:3:6 | 3 | foo([x=]1, [y=]'hello', [z=]True) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:17 | 2 | def foo(x: int, y: str, z: bool): pass | ^ - | info: Source --> main2.py:3:13 | 3 | foo([x=]1, [y=]'hello', [z=]True) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:25 | 2 | def foo(x: int, y: str, z: bool): pass | ^ - | info: Source --> main2.py:3:26 | 3 | foo([x=]1, [y=]'hello', [z=]True) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -6239,39 +5915,33 @@ Source with applied edits: | LL | class int: | ^^^ - | info: Source --> main2.py:LL:9 | LL | total[: int] = add([x=]3, [b=]2, y=4) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:9 | 2 | def add(x: int, b, y: int) -> int: | ^ - | info: Source --> main2.py:5:21 | 5 | total[: int] = add([x=]3, [b=]2, y=4) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:17 | 2 | def add(x: int, b, y: int) -> int: | ^ - | info: Source --> main2.py:5:28 | 5 | total[: int] = add([x=]3, [b=]2, y=4) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -6302,13 +5972,11 @@ Source with applied edits: | 2 | def foo(x: int, y: str, z: bool): pass | ^ - | info: Source --> main2.py:3:6 | 3 | foo([x=]1, z=True, y='hello') | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -6341,13 +6009,11 @@ Source with applied edits: | 2 | def foo(x: int, y: str): pass | ^ - | info: Source --> main2.py:3:17 | 3 | foo(y='hello', [y=]1) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -6382,78 +6048,66 @@ Source with applied edits: | 2 | def foo(x: int, y: str = 'default', z: bool = False): pass | ^ - | info: Source --> main2.py:3:6 | 3 | foo([x=]1) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:9 | 2 | def foo(x: int, y: str = 'default', z: bool = False): pass | ^ - | info: Source --> main2.py:4:6 | 4 | foo([x=]1, [y=]'custom') | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:17 | 2 | def foo(x: int, y: str = 'default', z: bool = False): pass | ^ - | info: Source --> main2.py:4:13 | 4 | foo([x=]1, [y=]'custom') | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:9 | 2 | def foo(x: int, y: str = 'default', z: bool = False): pass | ^ - | info: Source --> main2.py:5:6 | 5 | foo([x=]1, [y=]'custom', [z=]True) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:17 | 2 | def foo(x: int, y: str = 'default', z: bool = False): pass | ^ - | info: Source --> main2.py:5:13 | 5 | foo([x=]1, [y=]'custom', [z=]True) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:37 | 2 | def foo(x: int, y: str = 'default', z: bool = False): pass | ^ - | info: Source --> main2.py:5:27 | 5 | foo([x=]1, [y=]'custom', [z=]True) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -6502,78 +6156,66 @@ Source with applied edits: | 8 | def baz(a: int, b: str, c: bool): pass | ^ - | info: Source --> main2.py:10:6 | 10 | baz([a=]foo([x=]5), [b=]bar([y=]bar([y=]'test')), [c=]True) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:9 | 2 | def foo(x: int) -> int: | ^ - | info: Source --> main2.py:10:14 | 10 | baz([a=]foo([x=]5), [b=]bar([y=]bar([y=]'test')), [c=]True) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:8:17 | 8 | def baz(a: int, b: str, c: bool): pass | ^ - | info: Source --> main2.py:10:22 | 10 | baz([a=]foo([x=]5), [b=]bar([y=]bar([y=]'test')), [c=]True) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:5:9 | 5 | def bar(y: str) -> str: | ^ - | info: Source --> main2.py:10:30 | 10 | baz([a=]foo([x=]5), [b=]bar([y=]bar([y=]'test')), [c=]True) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:5:9 | 5 | def bar(y: str) -> str: | ^ - | info: Source --> main2.py:10:38 | 10 | baz([a=]foo([x=]5), [b=]bar([y=]bar([y=]'test')), [c=]True) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:8:25 | 8 | def baz(a: int, b: str, c: bool): pass | ^ - | info: Source --> main2.py:10:52 | 10 | baz([a=]foo([x=]5), [b=]bar([y=]bar([y=]'test')), [c=]True) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -6614,26 +6256,22 @@ Source with applied edits: | 3 | def foo(self, value: int) -> 'A': | ^^^^^ - | info: Source --> main2.py:8:10 | 8 | A().foo([value=]42).bar([name=]'test').baz() | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:5:19 | 5 | def bar(self, name: str) -> 'A': | ^^^^ - | info: Source --> main2.py:8:26 | 8 | A().foo([value=]42).bar([name=]'test').baz() | ^^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -6670,13 +6308,11 @@ Source with applied edits: | 2 | def foo(x: str) -> str: | ^ - | info: Source --> main2.py:5:12 | 5 | bar(y=foo([x=]'test')) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -6707,30 +6343,26 @@ Source with applied edits: bar([a=]1, [b=]2) --------------------------------------------- info[inlay-hint-location]: Inlay Hint Target - --> stdlib/ty_extensions/__init__.pyi:LL:1 + --> stdlib/ty_extensions/_internal.pyi:LL:1 | LL | Unknown: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:14 | LL | foo[: (x) -> Unknown] = lambda x: x * 2 | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target - --> stdlib/ty_extensions/__init__.pyi:LL:1 + --> stdlib/ty_extensions/_internal.pyi:LL:1 | LL | Unknown: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:17 | LL | bar[: (a, b) -> Unknown] = lambda a, b: a + b | ^^^^^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -6765,15 +6397,13 @@ Source with applied edits: info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing_extensions.byi:LL:9 | - LL | LiteralString as LiteralString, - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | + LL | LiteralString, + | ^^^^^^^^^^^^^ info: Source --> main2.py:LL:9 | LL | y[: LiteralString] = x | ^^^^^^^^^^^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -6825,78 +6455,66 @@ Source with applied edits: | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:9 | LL | y[: Literal[1, 2, 3, "hello"] | None] = x | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:17 | LL | y[: Literal[1, 2, 3, "hello"] | None] = x | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:20 | LL | y[: Literal[1, 2, 3, "hello"] | None] = x | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:23 | LL | y[: Literal[1, 2, 3, "hello"] | None] = x | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:26 | LL | y[: Literal[1, 2, 3, "hello"] | None] = x | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/types.byi:LL:13 | LL | final class NoneType: | ^^^^^^^^ - | info: Source --> main2.py:LL:37 | LL | y[: Literal[1, 2, 3, "hello"] | None] = x | ^^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -6932,26 +6550,22 @@ Source with applied edits: | 2 | class Foo[T]: ... | ^^^ - | info: Source --> main2.py:4:13 | 4 | a[: ] = Foo[int] | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:17 | LL | a[: ] = Foo[int] | ^^^ - | "); } @@ -6973,39 +6587,33 @@ Source with applied edits: | LL | class type: | ^^^^ - | info: Source --> main2.py:LL:9 | LL | y[: type[list[str]]] = type(x) | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class list[in out Element](MutableSequence[Element]): | ^^^^ - | info: Source --> main2.py:LL:14 | LL | y[: type[list[str]]] = type(x) | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:19 | LL | y[: type[list[str]]] = type(x) | ^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -7042,13 +6650,11 @@ Source with applied edits: | 4 | def whatever(self): ... | ^^^^^^^^ - | info: Source --> main2.py:6:6 | 6 | ab[: property] = F.whatever | ^^^^^^^^ - | "); } @@ -7072,39 +6678,33 @@ Source with applied edits: | 2 | def foo(a: int, b: str, /, c: float, d: bool = True, *, e: int, f: str = 'default'): pass | ^ - | info: Source --> main2.py:3:16 | 3 | foo(1, 'pos', [c=]3.14, [d=]False, e=42) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:38 | 2 | def foo(a: int, b: str, /, c: float, d: bool = True, *, e: int, f: str = 'default'): pass | ^ - | info: Source --> main2.py:3:26 | 3 | foo(1, 'pos', [c=]3.14, [d=]False, e=42) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:28 | 2 | def foo(a: int, b: str, /, c: float, d: bool = True, *, e: int, f: str = 'default'): pass | ^ - | info: Source --> main2.py:4:16 | 4 | foo(1, 'pos', [c=]3.14, e=42, f='custom') | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -7146,13 +6746,11 @@ Source with applied edits: | 2 | def bar(x: int | str): | ^ - | info: Source --> main2.py:4:6 | 4 | bar([x=]1) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -7201,26 +6799,22 @@ Source with applied edits: | 5 | def foo(x: int) -> str: ... | ^ - | info: Source --> main2.py:11:6 | 11 | foo([x=]42) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:7:9 | 7 | def foo(x: str) -> int: ... | ^ - | info: Source --> main2.py:12:6 | 12 | foo([x=]'hello') | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -7273,26 +6867,22 @@ Source with applied edits: | LL | class Sequence[out Element](Reversible[Element], Collection[Element]): | ^^^^^^^^ - | info: Source --> main2.py:LL:5 | LL | b[: Sequence[str]] = S('x', 'y') | ^^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:14 | LL | b[: Sequence[str]] = S('x', 'y') | ^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -7344,13 +6934,11 @@ Source with applied edits: | 5 | def f(x: int) -> str: ... | ^ - | info: Source --> main2.py:11:4 | 11 | f([x=][]) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -7482,39 +7070,33 @@ Source with applied edits: | 2 | def foo(param: int): pass | ^^^^^ - | info: Source --> main2.py:7:6 | 7 | foo([param=]param2) | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:9 | 2 | def foo(param: int): pass | ^^^^^ - | info: Source --> main2.py:8:6 | 8 | foo([param=]my_param2) | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:9 | 2 | def foo(param: int): pass | ^^^^^ - | info: Source --> main2.py:9:6 | 9 | foo([param=]parameter) | ^^^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -7569,13 +7151,11 @@ Source with applied edits: | 2 | def foo(focus_range: int): pass | ^^^^^^^^^^^ - | info: Source --> main2.py:13:6 | 13 | foo([focus_range=]focus_end_range) | ^^^^^^^^^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -7610,13 +7190,11 @@ Source with applied edits: | 2 | def foo(x: int): pass | ^ - | info: Source --> main2.py:4:6 | 4 | foo([x=]1) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -7648,13 +7226,11 @@ Source with applied edits: | 2 | def foo(_x: int, y: int): pass | ^ - | info: Source --> main2.py:3:9 | 3 | foo(1, [y=]2) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -7693,26 +7269,22 @@ Source with applied edits: | 3 | x: int, | ^ - | info: Source --> main2.py:7:6 | 7 | foo([x=]1, [y=]2) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:4:5 | 4 | y: int | ^ - | info: Source --> main2.py:7:13 | 7 | foo([x=]1, [y=]2) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -7745,78 +7317,66 @@ Source with applied edits: | LL | class int: | ^^^ - | info: Source --> main2.py:LL:16 | LL | a[: def foo(x: int, *y: bool, *, z: str | int | list[str])] = foo | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:13 | LL | final class bool(int): | ^^^^ - | info: Source --> main2.py:LL:25 | LL | a[: def foo(x: int, *y: bool, *, z: str | int | list[str])] = foo | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:37 | LL | a[: def foo(x: int, *y: bool, *, z: str | int | list[str])] = foo | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:43 | LL | a[: def foo(x: int, *y: bool, *, z: str | int | list[str])] = foo | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class list[in out Element](MutableSequence[Element]): | ^^^^ - | info: Source --> main2.py:LL:49 | LL | a[: def foo(x: int, *y: bool, *, z: str | int | list[str])] = foo | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:54 | LL | a[: def foo(x: int, *y: bool, *, z: str | int | list[str])] = foo | ^^^ - | "); } @@ -7842,26 +7402,22 @@ Source with applied edits: | LL | class ModuleType: | ^^^^^^^^^^ - | info: Source --> main2.py:LL:6 | LL | a[: ] = foo | ^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> foo.py:1:1 | 1 | '''Foo module''' | ^^^^^^^^^^^^^^^^ - | info: Source --> main2.py:4:14 | 4 | a[: ] = foo | ^^^ - | "); } @@ -7885,52 +7441,44 @@ Source with applied edits: | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:20 | LL | a[: ] = Literal['a', 'b', 'c'] | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:28 | LL | a[: ] = Literal['a', 'b', 'c'] | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:33 | LL | a[: ] = Literal['a', 'b', 'c'] | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:38 | LL | a[: ] = Literal['a', 'b', 'c'] | ^^^ - | "#); } @@ -7954,26 +7502,22 @@ Source with applied edits: | LL | final class WrapperDescriptorType: | ^^^^^^^^^^^^^^^^^^^^^ - | info: Source --> main2.py:LL:6 | LL | a[: ] = FunctionType.__get__ | ^^^^^^^^^^^^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/types.byi:LL:13 | LL | final class FunctionType: | ^^^^^^^^^^^^ - | info: Source --> main2.py:LL:39 | LL | a[: ] = FunctionType.__get__ | ^^^^^^^^ - | "); } @@ -7997,52 +7541,44 @@ Source with applied edits: | LL | final class MethodWrapperType: | ^^^^^^^^^^^^^^^^^ - | info: Source --> main2.py:LL:6 | LL | a[: ] = f.__call__ | ^^^^^^^^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/types.byi:LL:9 | LL | def __call__(self, *args: dynamic, **kwargs: dynamic) -> dynamic: | ^^^^^^^^ - | info: Source --> main2.py:LL:22 | LL | a[: ] = f.__call__ | ^^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/types.byi:LL:13 | LL | final class FunctionType: | ^^^^^^^^^^^^ - | info: Source --> main2.py:LL:35 | LL | a[: ] = f.__call__ | ^^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:5 | 2 | def f(): ... | ^ - | info: Source --> main2.py:4:45 | 4 | a[: ] = f.__call__ | ^ - | "); } @@ -8070,78 +7606,66 @@ Source with applied edits: | LL | class NewType: | ^^^^^^^ - | info: Source --> main2.py:LL:6 | LL | N[: ] = NewType([name=]'N', [tp=]str) | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:4:1 | 4 | N = NewType('N', str) | ^ - | info: Source --> main2.py:4:28 | 4 | N[: ] = NewType([name=]'N', [tp=]str) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing.byi:LL:16 | LL | init(self, name: str, tp: dynamic) # AnnotationForm | ^^^^ - | info: Source --> main2.py:LL:44 | LL | N[: ] = NewType([name=]'N', [tp=]str) | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing.byi:LL:27 | LL | init(self, name: str, tp: dynamic) # AnnotationForm | ^^ - | info: Source --> main2.py:LL:56 | LL | N[: ] = NewType([name=]'N', [tp=]str) | ^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing.byi:LL:7 | LL | class NewType: | ^^^^^^^ - | info: Source --> main2.py:LL:6 | LL | Y[: ] = N | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:4:1 | 4 | N = NewType('N', str) | ^ - | info: Source --> main2.py:6:28 | 6 | Y[: ] = N | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -8173,26 +7697,22 @@ Source with applied edits: | LL | class type: | ^^^^ - | info: Source --> main2.py:LL:9 | LL | y[: type[T@f]] = x | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:7 | 2 | def f[T](x: type[T]): | ^ - | info: Source --> main2.py:3:14 | 3 | y[: type[T@f]] = x | ^^^ - | "); } @@ -8216,39 +7736,33 @@ Source with applied edits: | LL | name: str, | ^^^^ - | info: Source --> main2.py:LL:14 | LL | T = TypeVar([name=]'T') | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing.byi:LL:1 | LL | Protocol: _SpecialForm | ^^^^^^^^ - | info: Source --> main2.py:LL:26 | LL | Strange[: ] = Protocol[T] | ^^^^^^^^^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:1 | 3 | T = TypeVar('T') | ^ - | info: Source --> main2.py:4:42 | 4 | Strange[: ] = Protocol[T] | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -8280,13 +7794,11 @@ Source with applied edits: | LL | name: str, | ^^^^ - | info: Source --> main2.py:LL:16 | LL | P = ParamSpec([name=]'P') | ^^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -8317,26 +7829,22 @@ Source with applied edits: | LL | def __new__(cls, name: str, value: dynamic, *, type_params: (*: _TypeParameter) = ()) -> Self | ^^^^ - | info: Source --> main2.py:LL:20 | LL | A = TypeAliasType([name=]'A', [value=]str) | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing.byi:LL:37 | LL | def __new__(cls, name: str, value: dynamic, *, type_params: (*: _TypeParameter) = ()) -> Self | ^^^^^ - | info: Source --> main2.py:LL:32 | LL | A = TypeAliasType([name=]'A', [value=]str) | ^^^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -8367,13 +7875,11 @@ Source with applied edits: | LL | name: str, | ^^^^ - | info: Source --> main2.py:LL:20 | LL | Ts = TypeVarTuple([name=]'Ts') | ^^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -8412,39 +7918,33 @@ Source with applied edits: | LL | Top: _SpecialForm | ^^^ - | info: Source --> main2.py:LL:9 | LL | x[: Top[list[Any]]] = xyxy | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class list[in out Element](MutableSequence[Element]): | ^^^^ - | info: Source --> main2.py:LL:13 | LL | x[: Top[list[Any]]] = xyxy | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing.byi:LL:7 | LL | class Any: | ^^^ - | info: Source --> main2.py:LL:18 | LL | x[: Top[list[Any]]] = xyxy | ^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -8501,117 +8001,99 @@ Source with applied edits: | 6 | class B[T]: ... | ^ - | info: Source --> main2.py:4:5 | 4 | a[: B[A[D[int, list[str | A[B[int]]]]]]] = foo.C().foo() | ^ - | info[inlay-hint-location]: Inlay Hint Target --> foo.py:4:19 | 4 | class A[T]: ... | ^ - | info: Source --> main2.py:4:7 | 4 | a[: B[A[D[int, list[str | A[B[int]]]]]]] = foo.C().foo() | ^ - | info[inlay-hint-location]: Inlay Hint Target --> bar.py:2:19 | 2 | class D[T, U]: ... | ^ - | info: Source --> main2.py:4:9 | 4 | a[: B[A[D[int, list[str | A[B[int]]]]]]] = foo.C().foo() | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:11 | LL | a[: B[A[D[int, list[str | A[B[int]]]]]]] = foo.C().foo() | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class list[in out Element](MutableSequence[Element]): | ^^^^ - | info: Source --> main2.py:LL:16 | LL | a[: B[A[D[int, list[str | A[B[int]]]]]]] = foo.C().foo() | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:21 | LL | a[: B[A[D[int, list[str | A[B[int]]]]]]] = foo.C().foo() | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> foo.py:4:19 | 4 | class A[T]: ... | ^ - | info: Source --> main2.py:4:27 | 4 | a[: B[A[D[int, list[str | A[B[int]]]]]]] = foo.C().foo() | ^ - | info[inlay-hint-location]: Inlay Hint Target --> foo.py:6:19 | 6 | class B[T]: ... | ^ - | info: Source --> main2.py:4:29 | 4 | a[: B[A[D[int, list[str | A[B[int]]]]]]] = foo.C().foo() | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:31 | LL | a[: B[A[D[int, list[str | A[B[int]]]]]]] = foo.C().foo() | ^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -8671,117 +8153,99 @@ Source with applied edits: | 6 | class B[T]: ... | ^ - | info: Source --> main2.py:4:5 | 4 | a[: B[A[D[int, list[str | A[B[int]]]]]]] = C().foo() | ^ - | info[inlay-hint-location]: Inlay Hint Target --> foo.py:4:19 | 4 | class A[T]: ... | ^ - | info: Source --> main2.py:4:7 | 4 | a[: B[A[D[int, list[str | A[B[int]]]]]]] = C().foo() | ^ - | info[inlay-hint-location]: Inlay Hint Target --> bar.py:2:19 | 2 | class D[T, U]: ... | ^ - | info: Source --> main2.py:4:9 | 4 | a[: B[A[D[int, list[str | A[B[int]]]]]]] = C().foo() | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:11 | LL | a[: B[A[D[int, list[str | A[B[int]]]]]]] = C().foo() | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class list[in out Element](MutableSequence[Element]): | ^^^^ - | info: Source --> main2.py:LL:16 | LL | a[: B[A[D[int, list[str | A[B[int]]]]]]] = C().foo() | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:21 | LL | a[: B[A[D[int, list[str | A[B[int]]]]]]] = C().foo() | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> foo.py:4:19 | 4 | class A[T]: ... | ^ - | info: Source --> main2.py:4:27 | 4 | a[: B[A[D[int, list[str | A[B[int]]]]]]] = C().foo() | ^ - | info[inlay-hint-location]: Inlay Hint Target --> foo.py:6:19 | 6 | class B[T]: ... | ^ - | info: Source --> main2.py:4:29 | 4 | a[: B[A[D[int, list[str | A[B[int]]]]]]] = C().foo() | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:31 | LL | a[: B[A[D[int, list[str | A[B[int]]]]]]] = C().foo() | ^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -8840,52 +8304,44 @@ Source with applied edits: | 2 | class D[T]: | ^ - | info: Source --> main2.py:6:5 | 6 | a[: D[Baz]] = D[[Baz]]([x=]Baz) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:4:7 | 4 | class Baz: ... | ^^^ - | info: Source --> main2.py:6:7 | 6 | a[: D[Baz]] = D[[Baz]]([x=]Baz) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:4:7 | 4 | class Baz: ... | ^^^ - | info: Source --> main2.py:6:18 | 6 | a[: D[Baz]] = D[[Baz]]([x=]Baz) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> foo/bar.py:3:36 | 3 | def __init__(self, x: type[T]): | ^ - | info: Source --> main2.py:6:25 | 6 | a[: D[Baz]] = D[[Baz]]([x=]Baz) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -8922,52 +8378,44 @@ Source with applied edits: | LL | class Any: | ^^^ - | info: Source --> main2.py:LL:9 | LL | a[: Any | Literal["some"]] = getattr[[Literal["some"]]](x, 'foo', "some") | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing.byi:LL:1 | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:15 | LL | a[: Any | Literal["some"]] = getattr[[Literal["some"]]](x, 'foo', "some") | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:23 | LL | a[: Any | Literal["some"]] = getattr[[Literal["some"]]](x, 'foo', "some") | ^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:43 | LL | a[: Any | Literal["some"]] = getattr[[Literal["some"]]](x, 'foo', "some") | ^^^^^^^^^^^^^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -9015,52 +8463,44 @@ Source with applied edits: | LL | class dict[in out Key: Hashable, in out Value](MutableMapping[Key, Value]): | ^^^^ - | info: Source --> main2.py:LL:5 | LL | a[: dict[TypeVar, Any] | None] = foo() | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing.byi:LL:13 | LL | final class TypeVar: | ^^^^^^^ - | info: Source --> main2.py:LL:10 | LL | a[: dict[TypeVar, Any] | None] = foo() | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing.byi:LL:7 | LL | class Any: | ^^^ - | info: Source --> main2.py:LL:19 | LL | a[: dict[TypeVar, Any] | None] = foo() | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/types.byi:LL:13 | LL | final class NoneType: | ^^^^^^^^ - | info: Source --> main2.py:LL:26 | LL | a[: dict[TypeVar, Any] | None] = foo() | ^^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -9127,26 +8567,22 @@ Source with applied edits: | 2 | class A: ... | ^ - | info: Source --> main2.py:4:5 | 4 | a[: bar.A | baz.A] = foo() | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> baz.py:2:19 | 2 | class A: ... | ^ - | info: Source --> main2.py:4:13 | 4 | a[: bar.A | baz.A] = foo() | ^^^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -9218,65 +8654,55 @@ Source with applied edits: | 2 | class A: ... | ^ - | info: Source --> main2.py:5:5 | 5 | a[: bar.A | baz.A | list[bar.A | baz.A]] = foo() | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> baz.py:2:22 | 2 | class A: ... | ^ - | info: Source --> main2.py:5:13 | 5 | a[: bar.A | baz.A | list[bar.A | baz.A]] = foo() | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class list[in out Element](MutableSequence[Element]): | ^^^^ - | info: Source --> main2.py:LL:21 | LL | a[: bar.A | baz.A | list[bar.A | baz.A]] = foo() | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> bar.py:2:22 | 2 | class A: ... | ^ - | info: Source --> main2.py:5:26 | 5 | a[: bar.A | baz.A | list[bar.A | baz.A]] = foo() | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> baz.py:2:22 | 2 | class A: ... | ^ - | info: Source --> main2.py:5:34 | 5 | a[: bar.A | baz.A | list[bar.A | baz.A]] = foo() | ^^^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -9340,52 +8766,44 @@ Source with applied edits: | 8 | class B[T]: | ^ - | info: Source --> main2.py:11:5 | 11 | b[: B[A]] = B[[A]]([x=]foo.A()) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> foo.py:2:19 | 2 | class A: ... | ^ - | info: Source --> main2.py:11:7 | 11 | b[: B[A]] = B[[A]]([x=]foo.A()) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> foo.py:2:19 | 2 | class A: ... | ^ - | info: Source --> main2.py:11:16 | 11 | b[: B[A]] = B[[A]]([x=]foo.A()) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:9:5 | 9 | x: T | ^ - | info: Source --> main2.py:11:21 | 11 | b[: B[A]] = B[[A]]([x=]foo.A()) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -9431,39 +8849,33 @@ Source with applied edits: | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:5 | LL | x[: Literal[Color.RED]] = Color.RED | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> test.py:4:19 | 4 | class Color(Enum): | ^^^^^ - | info: Source --> main2.py:4:13 | 4 | x[: Literal[Color.RED]] = Color.RED | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> test.py:5:17 | 5 | RED = 1 | ^^^ - | info: Source --> main2.py:4:19 | 4 | x[: Literal[Color.RED]] = Color.RED | ^^^ - | "); } @@ -9508,52 +8920,44 @@ Source with applied edits: | LL | class list[in out Element](MutableSequence[Element]): | ^^^^ - | info: Source --> main2.py:LL:5 | LL | y[: list[Inner]] = wrap[[Inner]]([x=]Outer.Inner()) | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> module.py:3:23 | 3 | class Inner: ... | ^^^^^ - | info: Source --> main2.py:8:10 | 8 | y[: list[Inner]] = wrap[[Inner]]([x=]Outer.Inner()) | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> module.py:3:23 | 3 | class Inner: ... | ^^^^^ - | info: Source --> main2.py:8:26 | 8 | y[: list[Inner]] = wrap[[Inner]]([x=]Outer.Inner()) | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:5:13 | 5 | def wrap[T](x: T) -> list[T]: | ^ - | info: Source --> main2.py:8:35 | 8 | y[: list[Inner]] = wrap[[Inner]]([x=]Outer.Inner()) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -9599,39 +9003,33 @@ Source with applied edits: | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:5 | LL | x[: Literal[Color.RED]] = test.Color.RED | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> test.py:4:19 | 4 | class Color(Enum): | ^^^^^ - | info: Source --> main2.py:4:13 | 4 | x[: Literal[Color.RED]] = test.Color.RED | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> test.py:5:17 | 5 | RED = 1 | ^^^ - | info: Source --> main2.py:4:19 | 4 | x[: Literal[Color.RED]] = test.Color.RED | ^^^ - | "); } @@ -9668,13 +9066,11 @@ Source with applied edits: | 3 | class Inner: ... | ^^^^^ - | info: Source --> main2.py:4:5 | 4 | x[: Inner] = Outer().make() | ^^^^^ - | "); } @@ -9708,13 +9104,11 @@ Source with applied edits: | 3 | class Inner: ... | ^^^^^ - | info: Source --> main2.py:8:5 | 8 | x[: Inner] = Outer().make() | ^^^^^ - | "#); } diff --git a/crates/ty_ide/src/lib.rs b/crates/ty_ide/src/lib.rs index 92de5c6a57..6f01a4dc68 100644 --- a/crates/ty_ide/src/lib.rs +++ b/crates/ty_ide/src/lib.rs @@ -16,6 +16,7 @@ mod folding_range; mod goto; mod goto_declaration; mod goto_definition; +mod goto_implementation; mod goto_type_definition; mod hints; mod hover; @@ -38,8 +39,8 @@ pub use call_hierarchy::outgoing_calls::{OutgoingCall, outgoing_calls}; pub use call_hierarchy::{CallHierarchyItem, prepare_call_hierarchy}; pub use code_action::{FileEdit, QuickFix, code_actions}; pub use completion::{ - Completion, CompletionCapabilities, CompletionInsertTextFormat, CompletionKind, - CompletionSettings, completion, + Completion, CompletionCapabilities, CompletionCommand, CompletionInsertTextFormat, + CompletionKind, CompletionSettings, completion, }; pub use django_template::{ DisplayTemplateHover, DjangoChecker, DjangoCodeLens, DjangoLensAction, DjangoLensTarget, @@ -57,6 +58,7 @@ pub use document_symbols::document_symbols; pub use find_references::find_references; pub use folding_range::{FoldingRange, FoldingRangeKind, folding_ranges}; pub use goto::{goto_declaration, goto_definition, goto_type_definition}; +pub use goto_implementation::goto_implementation; pub use hints::{Hint, HintKind, hints}; pub use hover::hover; pub use inlay_hints::{ @@ -82,11 +84,14 @@ use ruff_db::{ vendored::VendoredPath, }; use ruff_text_size::{Ranged, TextRange}; -use rustc_hash::FxHashSet; +use rustc_hash::{FxBuildHasher, FxHashSet}; use std::ops::{Deref, DerefMut}; use ty_project::Db; +pub use ty_python_semantic::ProgramEnvironment; use ty_python_semantic::types::{Type, TypeDefinition}; +type FxIndexMap = indexmap::IndexMap; + /// Information associated with a text range. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct RangedValue { @@ -144,7 +149,7 @@ pub struct NavigationTarget { impl NavigationTarget { /// Creates a new `NavigationTarget` where the focus and full range are identical. - pub fn new(file: File, range: TextRange) -> Self { + fn new(file: File, range: TextRange) -> Self { Self { file, focus_range: range, @@ -202,7 +207,7 @@ pub struct ReferenceTarget { impl ReferenceTarget { /// Creates a new `ReferenceTarget`. - pub fn new(file: File, range: TextRange, kind: ReferenceKind) -> Self { + fn new(file: File, range: TextRange, kind: ReferenceKind) -> Self { Self { file_range: FileRange::new(file, range), kind, @@ -288,23 +293,23 @@ impl FromIterator for NavigationTargets { } pub trait HasNavigationTargets { - fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets; + fn navigation_targets(&self, db: &dyn Db, env: &ProgramEnvironment<'_>) -> NavigationTargets; } impl HasNavigationTargets for Type<'_> { - fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets { + fn navigation_targets(&self, db: &dyn Db, env: &ProgramEnvironment<'_>) -> NavigationTargets { match self { Type::Union(union) => union .elements(db) .iter() - .flat_map(|target| target.navigation_targets(db)) + .flat_map(|target| target.navigation_targets(db, env)) .collect(), Type::Intersection(intersection) => { - if let Some(alternatives) = intersection.finite_alternatives(db) { + if let Some(alternatives) = intersection.finite_alternatives(db, env) { return alternatives .iter() - .flat_map(|alternative| alternative.navigation_targets(db)) + .flat_map(|alternative| alternative.navigation_targets(db, env)) .collect(); } @@ -321,26 +326,26 @@ impl HasNavigationTargets for Type<'_> { // because the type is the intersection of all those types. NavigationTargets::empty() } - None => first.navigation_targets(db), + None => first.navigation_targets(db, env), } } Type::EnumComplement(complement) => complement - .remaining_literal_types(db) + .remaining_literal_types(db, env) .iter() - .flat_map(|alternative| alternative.navigation_targets(db)) + .flat_map(|alternative| alternative.navigation_targets(db, env)) .collect(), ty => ty - .definition(db) - .map(|definition| definition.navigation_targets(db)) + .definition(db, env) + .map(|definition| definition.navigation_targets(db, env)) .unwrap_or_else(NavigationTargets::empty), } } } impl HasNavigationTargets for TypeDefinition<'_> { - fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets { + fn navigation_targets(&self, db: &dyn Db, _: &ProgramEnvironment<'_>) -> NavigationTargets { let Some(full_range) = self.full_range(db) else { return NavigationTargets::empty(); }; @@ -354,7 +359,7 @@ impl HasNavigationTargets for TypeDefinition<'_> { } /// Get the cache-relative path where vendored paths should be written to. -pub fn relative_cached_vendored_root() -> SystemPathBuf { +fn relative_cached_vendored_root() -> SystemPathBuf { // The vendored files are uniquely identified by the source commit. SystemPathBuf::from(format!("vendored/typeshed/{}", ty_vendored::SOURCE_COMMIT)) } @@ -408,6 +413,7 @@ pub fn map_system_to_vendored<'a>( mod tests { use camino::Utf8Component; use insta::internals::SettingsBindDropGuard; + use ty_python_semantic::ProgramEnvironment; use ruff_db::Db; use ruff_db::diagnostic::{ @@ -416,15 +422,16 @@ mod tests { use ruff_db::files::{File, FileRootKind, system_path_to_file}; use ruff_db::parsed::{ParsedModuleRef, parsed_module}; use ruff_db::source::{SourceText, source_text}; - use ruff_db::system::{DbWithTestSystem, DbWithWritableSystem, SystemPath, SystemPathBuf}; + use ruff_db::system::{DbWithWritableSystem, SystemPath, SystemPathBuf}; use ruff_python_ast::PythonVersion; use ruff_python_codegen::Stylist; use ruff_python_trivia::textwrap::dedent; use ruff_text_size::TextSize; use ty_module_resolver::SearchPathSettings; - use ty_project::{Db as _, ProjectMetadata}; + use ty_project::{Db as _, ProjectMetadata, SemanticDb as _}; + use ty_python_core::ProgramFile; use ty_python_core::platform::PythonPlatform; - use ty_python_core::program::{FallibleStrategy, Program, ProgramSettings}; + use ty_python_core::program::{FallibleStrategy, ProgramSettings}; use ty_python_semantic::PythonVersionWithSource; /// A way to create a simple single-file (named `main.py`) cursor test. @@ -446,6 +453,14 @@ mod tests { CursorTestBuilder::default() } + pub(super) fn program_file(&self, file: File) -> ProgramFile<'_> { + self.db.program_file(file) + } + + pub(super) fn program_environment(&self, file: File) -> ProgramEnvironment<'_> { + ProgramEnvironment::from_file(self.program_file(file)) + } + pub(super) fn write_file( &mut self, path: impl AsRef, @@ -527,10 +542,9 @@ mod tests { let mut db = ty_project::TestDb::new(ProjectMetadata::new("test", SystemPathBuf::from("/"))); - db.init_program_with_python_version( - self.python_version.unwrap_or_else(PythonVersion::latest_ty), - ) - .unwrap(); + if let Some(python_version) = self.python_version { + db.set_python_version(python_version); + } let mut cursor: Option = None; for &Source { @@ -570,7 +584,8 @@ mod tests { db.project().open_file(&mut db, file); let source = source_text(&db, file); - let parsed = parsed_module(&db, file).load(&db); + let parsed = + parsed_module(&db, db.program_file(file).python_file(&db)).load(&db); let stylist = Stylist::from_tokens(parsed.tokens(), source.as_str()).into_owned(); cursor = Some(Cursor { @@ -657,7 +672,7 @@ mod tests { let mut db = ty_project::TestDb::new(ProjectMetadata::new("test", project_root.clone())); - // Write site-packages files first (before init) + // Write site-packages files first. for Source { path, contents, @@ -669,11 +684,6 @@ mod tests { .expect("write to memory file system to be successful"); } - // Create /src directory for first-party code - db.memory_file_system() - .create_directory_all(&project_root) - .expect("create /src directory"); - // Configure search paths with site-packages let search_paths = SearchPathSettings { src_roots: vec![project_root.clone()], @@ -683,8 +693,8 @@ mod tests { .to_search_paths(db.system(), db.vendored(), &FallibleStrategy) .expect("valid search paths"); - Program::from_settings( - &db, + db.project().update_program( + &mut db, ProgramSettings { python_version: PythonVersionWithSource::default(), python_platform: PythonPlatform::default(), @@ -722,7 +732,8 @@ mod tests { db.project().open_file(&mut db, file); let source = source_text(&db, file); - let parsed = parsed_module(&db, file).load(&db); + let parsed = + parsed_module(&db, db.program_file(file).python_file(&db)).load(&db); let stylist = Stylist::from_tokens(parsed.tokens(), source.as_str()).into_owned(); cursor = Some(Cursor { diff --git a/crates/ty_ide/src/references.rs b/crates/ty_ide/src/references.rs index 7d42fbfa1c..0bbadfd39a 100644 --- a/crates/ty_ide/src/references.rs +++ b/crates/ty_ide/src/references.rs @@ -13,7 +13,7 @@ use crate::goto::{Definitions, GotoTarget}; use crate::{Db, ReferenceKind, ReferenceTarget}; use rayon::prelude::*; -use ruff_db::files::File; +use ruff_db::parsed::parsed_module; use ruff_python_ast::find_node::{CoveringNode, covering_node}; use ruff_python_ast::token::Tokens; use ruff_python_ast::{ @@ -22,9 +22,12 @@ use ruff_python_ast::{ }; use ruff_text_size::Ranged; use ty_project::parallel::{ParallelIteratorExt, minimum_parallel_job_len}; +use ty_python_core::ProgramFile; use ty_python_core::definition::{Definition, DefinitionKind, DefinitionState}; use ty_python_core::scope::{FileScopeId, NodeWithScopeKind, ScopeKind}; -use ty_python_semantic::{ImportAliasResolution, ResolvedDefinition, SemanticModel}; +use ty_python_semantic::{ + ImportAliasResolution, ResolvedDefinition, SemanticModel, contains_identifier, +}; /// Salsa snapshots coordinate clone and drop through shared state. For cached files that don't /// contain the target, that coordination can cost more than the file scan and scales poorly when @@ -84,10 +87,11 @@ impl ReferencesMode { /// Search for references across all files in the project. pub(crate) fn references( db: &dyn Db, - file: File, + file: ProgramFile<'_>, goto_target: &GotoTarget, mode: ReferencesMode, ) -> Option> { + let source_file = file.file(db); let model = SemanticModel::new(db, file); let target_definitions = goto_target.definitions(&model, mode.to_import_alias_resolution())?; let is_externally_visible_symbol = @@ -114,11 +118,12 @@ pub(crate) fn references( let is_parameter = parameter_owner_is_externally_visible(db, &target_definitions); if search_across_files && (is_parameter || is_externally_visible_symbol) { + let program = model.program(); let files = db.project().files(db); let files: Vec<_> = files .iter() .copied() - .filter(|other| *other != file) + .filter(|other| *other != source_file) .collect(); let minimum_job_len = minimum_parallel_job_len(files.len(), MAX_MIN_FILES_PER_PARALLEL_JOB); let other_references = files @@ -130,6 +135,8 @@ pub(crate) fn references( return Vec::new(); } + let other_file = ProgramFile::new(db, other_file, program); + if is_externally_visible_symbol { references_for_file(db, other_file, &target_definitions, &target_text, mode) } else { @@ -157,7 +164,7 @@ pub(crate) fn references( fn references_for_keyword_arguments_in_file( db: &dyn Db, - file: File, + file: ProgramFile<'_>, target_definitions: &Definitions<'_>, target_text: &str, mode: ReferencesMode, @@ -169,7 +176,7 @@ fn references_for_keyword_arguments_in_file( "keyword-label cross-file scan should not run in DocumentHighlights mode" ); - let parsed = ruff_db::parsed::parsed_module(db, file); + let parsed = parsed_module(db, file.python_file(db)); let module = parsed.load(db); let model = SemanticModel::new(db, file); let mut references = Vec::new(); @@ -189,36 +196,6 @@ fn references_for_keyword_arguments_in_file( references } -/// Cheap text prefilter for identifier references before AST/semantic validation. -/// -/// Heuristically matches an ASCII approximation of `\b{name}\b`. -pub(crate) fn contains_identifier(source: &str, name: &str) -> bool { - if name.is_empty() { - return false; - } - - let bytes = source.as_bytes(); - let needle = name.as_bytes(); - - memchr::memmem::find_iter(bytes, needle).any(move |pos| { - let after = pos + needle.len(); - - // Skip this entry if it is within an identifier. E.g. skip - // this entry when searching for `x` and this is a match - // within `exclude = 10` - let boundary_before = pos == 0 || !is_ascii_identifier_continue(bytes[pos - 1]); - let boundary_after = bytes - .get(after) - .is_none_or(|byte| !is_ascii_identifier_continue(*byte)); - - boundary_before && boundary_after - }) -} - -fn is_ascii_identifier_continue(byte: u8) -> bool { - byte.is_ascii_alphanumeric() || byte == b'_' -} - /// Returns whether `node` assigns `value` to the sole target `__slots__`, e.g. /// `__slots__ = (...)` or `__slots__: tuple = (...)`. fn is_slots_assignment(node: AnyNodeRef<'_>, value: AnyNodeRef<'_>) -> bool { @@ -248,12 +225,12 @@ fn is_slots_assignment(node: AnyNodeRef<'_>, value: AnyNodeRef<'_>) -> bool { /// The behavior depends on the provided mode. fn references_for_file( db: &dyn Db, - file: File, + file: ProgramFile<'_>, target_definitions: &Definitions<'_>, target_text: &str, mode: ReferencesMode, ) -> Vec { - let parsed = ruff_db::parsed::parsed_module(db, file); + let parsed = parsed_module(db, file.python_file(db)); let module = parsed.load(db); let model = SemanticModel::new(db, file); let mut references = Vec::new(); @@ -281,10 +258,16 @@ pub(crate) fn has_any_external_visible_definitions( definitions.iter().any(|definition| match definition { ResolvedDefinition::Definition(definition) => match definition.scope(db).scope(db).kind() { ScopeKind::Module | ScopeKind::Class => true, + ScopeKind::Comprehension => { + matches!(definition.kind(db), DefinitionKind::NamedExpression(_)) + && definition.place(db).as_symbol().is_some_and(|symbol_id| { + ty_python_core::semantic_index(db, definition.program_file(db)) + .symbol_resolves_to_global_scope(symbol_id, definition.file_scope(db)) + }) + } ScopeKind::TypeParams | ScopeKind::Function | ScopeKind::Lambda - | ScopeKind::Comprehension | ScopeKind::TypeAlias => false, }, ResolvedDefinition::Module(_) | ResolvedDefinition::FileWithRange(_) => true, @@ -306,11 +289,13 @@ fn parameter_owner_is_externally_visible( fn parameter_owner_is_externally_visible_for_target( db: &dyn Db, - definition: &ResolvedDefinition, + resolved: &ResolvedDefinition, ) -> bool { - let target = definition.focus_range(db); - let file = target.file(); - let parsed = ruff_db::parsed::parsed_module(db, file); + let Some(definition) = resolved.definition() else { + return false; + }; + let parsed = parsed_module(db, definition.python_file(db)); + let target = definition.focus_range(db, &parsed.load(db)); let module = parsed.load(db); let covering = covering_node(module.syntax().into(), target.range()); @@ -721,8 +706,8 @@ impl<'a> LocalReferencesFinder<'a> { let db = self.model.db(); let file = self.model.file(); let class_range = class.range(); - let module = ruff_db::parsed::parsed_module(db, file).load(db); - let index = ty_python_core::semantic_index(db, file); + let module = ruff_db::parsed::parsed_module(db, self.model.python_file()).load(db); + let index = ty_python_core::semantic_index(db, self.model.program_file()); // The nearest class scope lexically enclosing `scope`, if any. `ancestor_scopes` skips // class scopes for name resolution, so we walk the lexical parents directly to stop at the @@ -779,7 +764,7 @@ impl<'a> LocalReferencesFinder<'a> { }; let file = local_definition.file(db); - let module = ruff_db::parsed::parsed_module(db, file).load(db); + let module = ruff_db::parsed::parsed_module(db, local_definition.python_file(db)).load(db); let kind = local_definition.kind(db); let category = kind.category(file.is_stub(db), &module); @@ -823,7 +808,7 @@ mod tests { use crate::tests::{CursorTest, cursor_test}; fn cursor_target_is_externally_visible(test: &CursorTest) -> bool { - let model = SemanticModel::new(&test.db, test.cursor.file); + let model = SemanticModel::new(&test.db, test.program_file(test.cursor.file)); let goto_target = find_goto_target(&model, &test.cursor.parsed, test.cursor.offset).unwrap(); let definitions = goto_target @@ -840,6 +825,23 @@ mod tests { fn externally_visible_definitions_can_have_cross_file_references() { for (case, source) in [ ("module-global", "x = 1"), + ( + "module comprehension walrus", + "[(x := item) for item in [1]]", + ), + ( + "nested module comprehension walrus", + "[[(x := item) for item in [1]] for _ in [1]]", + ), + ( + "explicit global comprehension walrus", + " +x = 0 +def f(): + global x + [(x := item) for item in [1]] +", + ), ( "class", " @@ -866,17 +868,42 @@ def f(): ), ("lambda", "f = lambda x: x"), ("comprehension", "xs = [x for x in range(3)]"), + ( + "function comprehension walrus", + " +def f(): + [(x := item) for item in [1]] + return x +", + ), + ( + "nested function comprehension walrus", + " +def f(): + [[(x := item) for item in [1]] for _ in [1]] + return x +", + ), + ( + "lambda comprehension walrus", + "f = lambda: [(x := item) for item in [1]]", + ), + ( + "explicit nonlocal comprehension walrus", + " +def outer(): + x = 0 + def inner(): + nonlocal x + [(x := item) for item in [1]] + inner() + return x +", + ), ("type parameters", "type Alias[T] = list[T]"), ] { let test = cursor_test(source); assert!(!cursor_target_is_externally_visible(&test), "{case}"); } } - - #[test] - fn source_candidate_prefilters_use_identifier_boundaries() { - for (source, name) in [("x = 1", "x"), ("obj.x", "x"), ("x()", "x")] { - assert!(contains_identifier(source, name)); - } - } } diff --git a/crates/ty_ide/src/rename.rs b/crates/ty_ide/src/rename.rs index dea7353930..273cbfe69d 100644 --- a/crates/ty_ide/src/rename.rs +++ b/crates/ty_ide/src/rename.rs @@ -3,12 +3,18 @@ use crate::references::{ReferencesMode, references}; use crate::{Db, ReferenceTarget}; use ruff_db::files::File; use ruff_text_size::{Ranged, TextSize}; +use ty_python_core::ProgramFile; use ty_python_semantic::SemanticModel; /// Returns the range of the symbol if it can be renamed, None if not. -pub fn can_rename(db: &dyn Db, file: File, offset: TextSize) -> Option { - let parsed = ruff_db::parsed::parsed_module(db, file); +pub fn can_rename( + db: &dyn Db, + file: ProgramFile<'_>, + offset: TextSize, +) -> Option { + let parsed = ruff_db::parsed::parsed_module(db, file.python_file(db)); let module = parsed.load(db); + let source_file = file.file(db); let model = SemanticModel::new(db, file); // Get the definitions for the symbol at the offset @@ -22,7 +28,7 @@ pub fn can_rename(db: &dyn Db, file: File, offset: TextSize) -> Option Option Option, offset: TextSize, new_name: &str, ) -> Option> { - let parsed = ruff_db::parsed::parsed_module(db, file); + let parsed = ruff_db::parsed::parsed_module(db, file.python_file(db)); let module = parsed.load(db); let model = SemanticModel::new(db, file); @@ -68,7 +74,7 @@ pub fn rename( // Determine if we should do a multi-file rename or single-file rename // based on whether the current file is part of the project - let current_file_in_project = is_file_in_project(db, file); + let current_file_in_project = is_file_in_project(db, file.file(db)); // Choose the appropriate rename mode: // - If current file is in project, do multi-file rename @@ -100,7 +106,11 @@ mod tests { impl CursorTest { fn prepare_rename(&self) -> String { let Some(range) = salsa::attach(&self.db, || { - can_rename(&self.db, self.cursor.file, self.cursor.offset) + can_rename( + &self.db, + self.program_file(self.cursor.file), + self.cursor.offset, + ) }) else { return "Cannot rename".to_string(); }; @@ -110,9 +120,18 @@ mod tests { fn rename(&self, new_name: &str) -> String { let rename_results = salsa::attach(&self.db, || { - can_rename(&self.db, self.cursor.file, self.cursor.offset)?; - - rename(&self.db, self.cursor.file, self.cursor.offset, new_name) + can_rename( + &self.db, + self.program_file(self.cursor.file), + self.cursor.offset, + )?; + + rename( + &self.db, + self.program_file(self.cursor.file), + self.cursor.offset, + new_name, + ) }); let Some(rename_results) = rename_results else { @@ -165,6 +184,94 @@ mod tests { } } + #[test] + fn rename_does_not_mix_global_and_nonlocal_comprehension_walruses() { + let test = cursor_test( + " +last = 0 + +def outer(): + last = 1 + + def write_global(): + global last + [(last := global_item) for global_item in [2]] + + def write_nonlocal(): + nonlocal last + [(last := nonlocal_item) for nonlocal_item in [3]] + + write_global() + write_nonlocal() + return last +", + ); + + assert_snapshot!(test.rename("result"), @" + info[rename]: Rename symbol (found 4 locations) + --> main.py:5:5 + | + 5 | last = 1 + | ^^^^ + | + ::: main.py:12:18 + | + 12 | nonlocal last + | ---- + 13 | [(last := nonlocal_item) for nonlocal_item in [3]] + | ---- + 14 | + 15 | write_global() + 16 | write_nonlocal() + 17 | return last + | ---- + "); + } + + #[test] + fn rename_comprehension_walrus_in_function() { + let test = cursor_test( + " +def f(items): + [(last := item) for item in items] + return last +", + ); + + assert_snapshot!(test.rename("result"), @" + info[rename]: Rename symbol (found 2 locations) + --> main.py:3:7 + | + 3 | [(last := item) for item in items] + | ^^^^ + 4 | return last + | ---- + "); + } + + #[test] + fn rename_comprehension_walrus_across_files() { + let test = CursorTest::builder() + .source("lib.py", "[(last := item) for item in [1]]\n") + .source("main.py", "from lib import last\nprint(last)\n") + .build(); + + assert_snapshot!(test.rename("result"), @" + info[rename]: Rename symbol (found 3 locations) + --> lib.py:1:3 + | + 1 | [(last := item) for item in [1]] + | ^^^^ + | + ::: main.py:1:17 + | + 1 | from lib import last + | ---- + 2 | print(last) + | ---- + "); + } + #[test] fn prepare_rename_parameter() { let test = cursor_test( @@ -205,7 +312,6 @@ func(value=42) 5 | 6 | func(value=42) | ----- - | "); } @@ -233,7 +339,6 @@ x = func | ---- 6 | x = func | ---- - | "); } @@ -263,7 +368,6 @@ cls = MyClass | ------- 7 | cls = MyClass | ------- - | "); } @@ -283,7 +387,6 @@ def func(): | 2 | def func(): | ^^^^ - | "); } @@ -336,7 +439,6 @@ class DataProcessor: 4 | def test(data): 5 | return func(data) | ---- - | "); } @@ -374,7 +476,6 @@ instance = ExampleClass(old_name="test") | 4 | instance = ExampleClass(old_name="test") | -------- - | "#); } @@ -398,7 +499,6 @@ instance = ExampleClass(old_name="test") 3 | 4 | class MyClass: | ------- - | "#); } @@ -422,7 +522,6 @@ instance = ExampleClass(old_name="test") 3 | 4 | class MyClass: | ------- - | "#); } @@ -460,7 +559,6 @@ instance = ExampleClass(old_name="test") 3 | 4 | class MyClass: | ------- - | "#); } @@ -512,7 +610,6 @@ instance = ExampleClass(old_name="test") 3 | 4 | class MyClass: | ------- - | "#); } @@ -549,7 +646,6 @@ instance = ExampleClass(old_name="test") | ^^ 5 | x = ab | -- - | "#); } @@ -572,7 +668,6 @@ instance = ExampleClass(old_name="test") | ^^ 5 | x = ab | -- - | "#); } @@ -595,7 +690,6 @@ instance = ExampleClass(old_name="test") | ^^ 5 | x = ab | -- - | "#); } @@ -618,7 +712,6 @@ instance = ExampleClass(old_name="test") | ^^ 5 | x = ab | -- - | "#); } @@ -641,7 +734,6 @@ instance = ExampleClass(old_name="test") | ^^ 5 | x = ab | -- - | "#); } @@ -664,7 +756,6 @@ instance = ExampleClass(old_name="test") | ^^ 5 | x = ab | -- - | "#); } @@ -693,7 +784,6 @@ instance = ExampleClass(old_name="test") | ^^ 11 | x = ab | -- - | "); } @@ -722,7 +812,6 @@ instance = ExampleClass(old_name="test") | ^^ 11 | x = ab | -- - | "); } @@ -757,7 +846,6 @@ instance = ExampleClass(old_name="test") 9 | match event: 10 | case Click(x, button=ab): | ----- - | "); } @@ -795,7 +883,6 @@ instance = ExampleClass(old_name="test") | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] | ^^ -- -- - | "); } @@ -813,7 +900,6 @@ instance = ExampleClass(old_name="test") | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] | ^^ -- -- - | "); } @@ -832,7 +918,6 @@ instance = ExampleClass(old_name="test") | 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] | ^^ -- -- - | "); } @@ -851,7 +936,6 @@ instance = ExampleClass(old_name="test") | 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] | ^^ -- -- - | "); } @@ -869,7 +953,6 @@ instance = ExampleClass(old_name="test") | 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] | ^^ -- -- - | "); } @@ -887,7 +970,6 @@ instance = ExampleClass(old_name="test") | 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] | ^^ -- -- - | "); } @@ -957,7 +1039,6 @@ result = alias() | ^^^^^ 3 | result = alias() | ----- - | "); } @@ -988,7 +1069,6 @@ result = alias() | ^^^^^ 3 | result = alias() | ----- - | "); } @@ -1053,7 +1133,6 @@ value1 = func_alias() 6 | 7 | result = original_function() | ----------------- - | "); } @@ -1105,7 +1184,6 @@ class App: 3 | 4 | func2() | ----- - | "); } @@ -1137,6 +1215,18 @@ def convert_to_number(value): assert_snapshot!(test.prepare_rename(), @"Cannot rename"); } + #[test] + fn cannot_rename_private_builtin_helper() { + // Unresolved references must not resolve to a private typeshed helper that likely does not + // exist at runtime or rename matching unresolved references in other files. + let test = CursorTest::builder() + .source("other.py", "_T_co\n") + .source("main.py", "_T_co\n") + .build(); + + assert_snapshot!(test.prepare_rename(), @"Cannot rename"); + } + #[test] fn rename_keyword_argument() { // Test renaming a keyword argument and its corresponding parameter @@ -1160,7 +1250,6 @@ result = func(10, y=20) 4 | 5 | result = func(10, y=20) | - - | "); } @@ -1187,7 +1276,6 @@ result = func(10, y=20) 4 | 5 | result = func(10, y=20) | - - | "); } @@ -1215,7 +1303,6 @@ TD(f=1) 7 | 8 | TD(f=1) | - - | "); } @@ -1243,7 +1330,6 @@ TD(f=1) 7 | 8 | TD(f=1) | - - | "); } @@ -1271,7 +1357,6 @@ NT(f=1) 7 | 8 | NT(f=1) | - - | "); } @@ -1300,7 +1385,6 @@ DC(f=1) 8 | 9 | DC(f=1) | - - | "); } @@ -1328,7 +1412,6 @@ DC(f=1) 4 | 5 | x = abc | --- - | "); } @@ -1355,7 +1438,6 @@ DC(f=1) 3 | 4 | x = lib2 | ---- - | "); } @@ -1387,7 +1469,6 @@ DC(f=1) | 1 | def deprecated(): pass | ---------- - | "); } @@ -1415,7 +1496,6 @@ DC(f=1) 4 | 5 | x = abc | --- - | "); } @@ -1446,7 +1526,6 @@ DC(f=1) | 4 | x = subpkg | ^^^^^^ - | "); } @@ -1579,7 +1658,6 @@ DC(f=1) | 2 | subpkg: int = 10 | ------ - | "); } @@ -1617,7 +1695,6 @@ DC(f=1) | 2 | subpkg: int = 10 | ------ - | "); } @@ -1673,7 +1750,6 @@ DC(f=1) 3 | 4 | test("test") | ---- - | "#); } @@ -1728,7 +1804,6 @@ DC(f=1) | 4 | Test().test("test") | ---- - | "#); } @@ -1785,7 +1860,6 @@ DC(f=1) 10 | 11 | def test(a: Any) -> Any: | ---- - | "#); } @@ -1822,7 +1896,6 @@ DC(f=1) | 4 | print(Foo().my_property) | ----------- - | "); } @@ -1872,7 +1945,6 @@ DC(f=1) | ----------- 5 | Foo().my_property = 56 | ----------- - | "); } @@ -1922,7 +1994,6 @@ DC(f=1) | ----------- 5 | del Foo().my_property | ----------- - | "); } @@ -1985,7 +2056,6 @@ DC(f=1) | ----------- 6 | del Foo().my_property | ----------- - | "); } @@ -2037,7 +2107,6 @@ DC(f=1) | ----------- 5 | Foo().my_property = 56 | ----------- - | "); } @@ -2089,7 +2158,6 @@ DC(f=1) | ----------- 5 | Foo().my_property = 56 | ----------- - | "); } @@ -2141,7 +2209,6 @@ DC(f=1) | ----------- 8 | def my_property(self, value: int) -> None: | ----------- - | "); } @@ -2185,7 +2252,6 @@ DC(f=1) | ----- 8 | def alpha(self, value: int) -> None: | ----- - | "); } @@ -2225,7 +2291,6 @@ DC(f=1) 10 | 11 | @my_func.setter | ------- - | "); } @@ -2262,13 +2327,12 @@ DC(f=1) // position-aware binding resolution in `definitions_for_name`. assert_snapshot!(test.rename("better_name"), @" info[rename]: Rename symbol (found 2 locations) - --> lib.py:11:2 + --> lib.py:12:5 | 11 | @my_func.setter | ------- 12 | def my_func(): | ^^^^^^^ - | "); } @@ -2303,7 +2367,6 @@ DC(f=1) 6 | 7 | @my_getter.setter | --------- - | "); } @@ -2345,7 +2408,6 @@ DC(f=1) 11 | 12 | @f.register | - - | "#); } @@ -2390,7 +2452,6 @@ DC(f=1) 12 | 13 | @f.register(str) | - - | "#); } @@ -2433,7 +2494,6 @@ DC(f=1) 12 | 13 | @f.register | - - | "#); } @@ -2480,7 +2540,6 @@ DC(f=1) 14 | 15 | @f.register | - - | "#); } @@ -2530,7 +2589,6 @@ DC(f=1) | - 16 | @f.register(float) | - - | "#); } @@ -2581,7 +2639,6 @@ DC(f=1) | --------- 16 | c.attribute = "new_value" | --------- - | "#); } @@ -2631,7 +2688,6 @@ DC(f=1) | ----------- 8 | def my_property(self, value: int) -> None: | ----------- - | "); } @@ -2670,7 +2726,6 @@ DC(f=1) | 4 | self.attribute = value | ^^^^^^^^^ - | "); } @@ -2702,7 +2757,6 @@ DC(f=1) 5 | 6 | print(a) | - - | "#); } @@ -2728,7 +2782,6 @@ class C: 5 | def __init__(self): 6 | self.value = 1 | ----- - | "#); } @@ -2754,7 +2807,6 @@ class C: 5 | def __init__(self): 6 | self.value = 1 | ----- - | "#); } @@ -2780,7 +2832,6 @@ class C: 5 | def __init__(self): 6 | self.value = 1 | ----- - | "#); } @@ -2806,7 +2857,6 @@ class C: 5 | def __init__(self): 6 | self.value = 1 | ----- - | "#); } @@ -2852,7 +2902,6 @@ class D: 5 | def __init__(self): 6 | self.value = 1 | ----- - | "#); } @@ -2878,7 +2927,6 @@ class C: 5 | def __init__(self): 6 | self.value = 1 | ----- - | "#); } @@ -2900,7 +2948,6 @@ class C: | ^^^^^ 4 | value: int | ----- - | "#); } @@ -2922,7 +2969,6 @@ class C: | 6 | self.value = 1 | ^^^^^ - | "); } @@ -2947,7 +2993,6 @@ class C: | ^^^^^ 4 | value: int = ... | ----- - | "#); } @@ -2973,7 +3018,6 @@ class C: | ^^^^^ 6 | self.value = value | ----- - | "#); } @@ -2998,7 +3042,6 @@ class Outer: | 7 | self.value = 1 | ^^^^^ - | "#); } } diff --git a/crates/ty_ide/src/selection_range.rs b/crates/ty_ide/src/selection_range.rs index d1560c14c4..686ec08ad1 100644 --- a/crates/ty_ide/src/selection_range.rs +++ b/crates/ty_ide/src/selection_range.rs @@ -1,4 +1,4 @@ -use ruff_db::files::File; +use ruff_db::PythonFile; use ruff_db::parsed::parsed_module; use ruff_python_ast::find_node::covering_node; use ruff_text_size::{Ranged, TextRange, TextSize}; @@ -7,7 +7,7 @@ use crate::Db; /// Returns a list of nested selection ranges, where each range contains the next one. /// The first range in the list is the largest range containing the cursor position. -pub fn selection_range(db: &dyn Db, file: File, offset: TextSize) -> Vec { +pub fn selection_range(db: &dyn Db, file: PythonFile<'_>, offset: TextSize) -> Vec { let parsed = parsed_module(db, file).load(db); let range = TextRange::empty(offset); @@ -85,28 +85,24 @@ x = 1 + 2 1 | / 2 | | x = 1 + 2 | |__________^ - | info[selection-range]: Selection Range 1 --> main.py:2:1 | 2 | x = 1 + 2 | ^^^^^^^^^ - | info[selection-range]: Selection Range 2 --> main.py:2:5 | 2 | x = 1 + 2 | ^^^^^ - | info[selection-range]: Selection Range 3 --> main.py:2:9 | 2 | x = 1 + 2 | ^ - | "); } @@ -129,35 +125,30 @@ print(\"hello\") 1 | / 2 | | print("hello") | |_______________^ - | info[selection-range]: Selection Range 1 --> main.py:2:1 | 2 | print("hello") | ^^^^^^^^^^^^^^ - | info[selection-range]: Selection Range 2 --> main.py:2:6 | 2 | print("hello") | ^^^^^^^^^ - | info[selection-range]: Selection Range 3 --> main.py:2:7 | 2 | print("hello") | ^^^^^^^ - | info[selection-range]: Selection Range 4 --> main.py:2:8 | 2 | print("hello") | ^^^^^ - | "#); } @@ -180,14 +171,12 @@ r"hello" 1 | / 2 | | r"hello" | |_________^ - | info[selection-range]: Selection Range 1 --> main.py:2:1 | 2 | r"hello" | ^^^^^^^^ - | "#); } @@ -204,14 +193,12 @@ r"hello" | 1 | f"foo" b"bar" | ^^^^^^^^^^^^^ - | info[selection-range]: Selection Range 1 --> main.py:1:8 | 1 | f"foo" b"bar" | ^^^^^^ - | "#); } @@ -236,7 +223,6 @@ def my_function(): 2 | | def my_function(): 3 | | return 42 | |______________^ - | info[selection-range]: Selection Range 1 --> main.py:2:1 @@ -244,14 +230,12 @@ def my_function(): 2 | / def my_function(): 3 | | return 42 | |_____________^ - | info[selection-range]: Selection Range 2 --> main.py:2:5 | 2 | def my_function(): | ^^^^^^^^^^^ - | "); } @@ -278,7 +262,6 @@ class MyClass: 3 | | def __init__(self): 4 | | self.value = 1 | |_______________________^ - | info[selection-range]: Selection Range 1 --> main.py:2:1 @@ -287,14 +270,12 @@ class MyClass: 3 | | def __init__(self): 4 | | self.value = 1 | |______________________^ - | info[selection-range]: Selection Range 2 --> main.py:2:7 | 2 | class MyClass: | ^^^^^^^ - | "); } @@ -317,56 +298,48 @@ result = [(lambda x: x[key.attr])(item) for item in data if item is not 1 | / 2 | | result = [(lambda x: x[key.attr])(item) for item in data if item is not None] | |______________________________________________________________________________^ - | info[selection-range]: Selection Range 1 --> main.py:2:1 | 2 | result = [(lambda x: x[key.attr])(item) for item in data if item is not None] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | info[selection-range]: Selection Range 2 --> main.py:2:10 | 2 | result = [(lambda x: x[key.attr])(item) for item in data if item is not None] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | info[selection-range]: Selection Range 3 --> main.py:2:11 | 2 | result = [(lambda x: x[key.attr])(item) for item in data if item is not None] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | info[selection-range]: Selection Range 4 --> main.py:2:12 | 2 | result = [(lambda x: x[key.attr])(item) for item in data if item is not None] | ^^^^^^^^^^^^^^^^^^^^^ - | info[selection-range]: Selection Range 5 --> main.py:2:22 | 2 | result = [(lambda x: x[key.attr])(item) for item in data if item is not None] | ^^^^^^^^^^^ - | info[selection-range]: Selection Range 6 --> main.py:2:24 | 2 | result = [(lambda x: x[key.attr])(item) for item in data if item is not None] | ^^^^^^^^ - | info[selection-range]: Selection Range 7 --> main.py:2:28 | 2 | result = [(lambda x: x[key.attr])(item) for item in data if item is not None] | ^^^^ - | "); } @@ -389,14 +362,12 @@ result = [(lambda x: x[key.attr])(item) for item in data if item is not 1 | / 2 | | "" | |___^ - | info[selection-range]: Selection Range 1 --> main.py:2:1 | 2 | "" | ^^ - | "#); } @@ -419,21 +390,18 @@ b"hello" 1 | / 2 | | b"hello" | |_________^ - | info[selection-range]: Selection Range 1 --> main.py:2:1 | 2 | b"hello" | ^^^^^^^^ - | info[selection-range]: Selection Range 2 --> main.py:2:3 | 2 | b"hello" | ^^^^^ - | "#); } @@ -456,20 +424,22 @@ b"123a𝐁c" 1 | / 2 | | b"123a𝐁c" | |__________^ - | info[selection-range]: Selection Range 1 --> main.py:2:1 | 2 | b"123a𝐁c" | ^^^^^^^^^ - | "#); } impl CursorTest { fn selection_range(&self) -> String { - let ranges = selection_range(&self.db, self.cursor.file, self.cursor.offset); + let ranges = selection_range( + &self.db, + self.program_file(self.cursor.file).python_file(&self.db), + self.cursor.offset, + ); if ranges.is_empty() { return "No selection range found".to_string(); diff --git a/crates/ty_ide/src/semantic_tokens.rs b/crates/ty_ide/src/semantic_tokens.rs index 5fa3ee4e45..95c6b11b6e 100644 --- a/crates/ty_ide/src/semantic_tokens.rs +++ b/crates/ty_ide/src/semantic_tokens.rs @@ -28,7 +28,6 @@ use crate::Db; use bitflags::bitflags; -use ruff_db::files::File; use ruff_db::parsed::parsed_module; use ruff_db::source::source_text; use ruff_python_ast::helpers::{ @@ -48,6 +47,7 @@ use ruff_python_literal::mini_language::FormatSpecComponent; use ruff_python_literal::strftime; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use std::ops::Deref; +use ty_python_core::ProgramFile; use ty_python_core::definition::{Definition, DefinitionKind, ParameterDefinitionNodeKind}; use ty_python_semantic::{ HasType, ImportAliasResolution, ResolvedDefinition, SemanticModel, definitions_for_attribute, @@ -201,7 +201,7 @@ pub struct SemanticTokens { impl SemanticTokens { /// Create a new `SemanticTokens` instance. - pub fn new(tokens: Vec) -> Self { + pub(crate) fn new(tokens: Vec) -> Self { Self { tokens } } } @@ -216,10 +216,14 @@ impl Deref for SemanticTokens { /// 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); +pub fn semantic_tokens( + db: &dyn Db, + file: ProgramFile<'_>, + range: Option, +) -> SemanticTokens { + let parsed = parsed_module(db, file.python_file(db)).load(db); let model = SemanticModel::new(db, file); - let source = source_text(db, file); + let source = source_text(db, file.file(db)); let mut visitor = SemanticTokenVisitor::new(&model, &source, range); visitor.expecting_docstring = true; @@ -674,8 +678,7 @@ impl<'db> SemanticTokenVisitor<'db> { ) -> Option<(SemanticTokenType, SemanticTokenModifier)> { let mut modifiers = SemanticTokenModifier::empty(); let db = self.model.db(); - let file = definition.file(db); - let model = SemanticModel::new(db, file); + let model = SemanticModel::new(db, definition.program_file(db)); if model.is_type_alias_definition(definition) { return Some((SemanticTokenType::Class, modifiers)); @@ -700,7 +703,7 @@ impl<'db> SemanticTokenVisitor<'db> { Some((SemanticTokenType::TypeParameter, modifiers)) } DefinitionKind::Parameter(ParameterDefinitionNodeKind::Parameter(parameter)) => { - let parsed = parsed_module(db, file); + let parsed = parsed_module(db, definition.python_file(db)); let ty = parameter.node(&parsed.load(db)).inferred_type(&model); if let Some(ty) = ty { @@ -744,7 +747,7 @@ impl<'db> SemanticTokenVisitor<'db> { let value_ty = match kind { DefinitionKind::Assignment(assignment) => { - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, definition.python_file(db)).load(db); assignment.value(&parsed).inferred_type(&model) } _ => None, @@ -2112,6 +2115,7 @@ impl SourceOrderVisitor<'_> for SemanticTokenVisitor<'_> { &mut self, interpolated_string_element: &InterpolatedStringElement, ) { + let env = &self.model.program_environment(); match interpolated_string_element { InterpolatedStringElement::Literal(literal) => { // inside a format spec the literal text is a language of its @@ -2140,7 +2144,7 @@ impl SourceOrderVisitor<'_> for SemanticTokenVisitor<'_> { self.in_format_spec = element .expression .inferred_type(self.model) - .and_then(|ty| spec_language(self.model.db(), ty)); + .and_then(|ty| spec_language(self.model.db(), env, ty)); for part in &format_spec.elements { self.visit_interpolated_string_element(part); } @@ -2342,7 +2346,7 @@ mod tests { use insta::assert_snapshot; use ruff_db::{ - files::system_path_to_file, + files::{File, system_path_to_file}, system::{DbWithWritableSystem, SystemPath, SystemPathBuf}, }; use ty_project::ProjectMetadata; @@ -5811,6 +5815,16 @@ def f(): assert_snapshot!(test.to_snapshot(&tokens), @r#""f" @ 5..6: Function [definition]"#); } + #[test] + fn private_builtin_helpers_do_not_receive_semantic_tokens() { + // Private helpers excluded from implicit builtin lookup must remain unresolved for IDE + // highlighting instead of receiving tokens from their typeshed definitions. + let test = SemanticTokenTest::new("_T_co\n_P\n"); + + let tokens = test.highlight_file(); + assert_snapshot!(test.to_snapshot(&tokens), @""); + } + #[test] fn unresolved_attributes_do_not_receive_semantic_tokens() { let test = SemanticTokenTest::new( @@ -7107,8 +7121,8 @@ x = cast(int, "") "#); } - pub(super) struct SemanticTokenTest { - pub(super) db: ty_project::TestDb, + struct SemanticTokenTest { + db: ty_project::TestDb, file: File, } @@ -7126,8 +7140,6 @@ x = cast(int, "") let mut db = ty_project::TestDb::new(ProjectMetadata::new("test", SystemPathBuf::from("/"))); - db.init_program().unwrap(); - let path = SystemPath::new(path); db.write_file(path, ruff_python_trivia::textwrap::dedent(source)) .expect("Write to memory file system to always succeed"); @@ -7139,12 +7151,28 @@ x = cast(int, "") /// Get semantic tokens for the entire file fn highlight_file(&self) -> SemanticTokens { - semantic_tokens(&self.db, self.file, None) + semantic_tokens( + &self.db, + ProgramFile::new( + &self.db, + self.file, + self.db.program_environment().program(&self.db), + ), + None, + ) } /// Get semantic tokens for a specific range in the file fn highlight_range(&self, range: TextRange) -> SemanticTokens { - semantic_tokens(&self.db, self.file, Some(range)) + semantic_tokens( + &self.db, + ProgramFile::new( + &self.db, + self.file, + self.db.program_environment().program(&self.db), + ), + Some(range), + ) } /// Helper function to convert semantic tokens to a snapshot-friendly text format diff --git a/crates/ty_ide/src/signature_help.rs b/crates/ty_ide/src/signature_help.rs index 9141ab0dc5..a602b8c52f 100644 --- a/crates/ty_ide/src/signature_help.rs +++ b/crates/ty_ide/src/signature_help.rs @@ -7,14 +7,15 @@ //! and overloads. use crate::Db; +use crate::FxIndexMap; use crate::docstring::Docstring; use crate::goto::docstring_for_call_definition; -use ruff_db::files::File; use ruff_db::parsed::parsed_module; use ruff_python_ast::find_node::covering_node; use ruff_python_ast::token::TokenKind; use ruff_python_ast::{self as ast, AnyNodeRef}; use ruff_text_size::{Ranged, TextSize}; +use ty_python_core::ProgramFile; use ty_python_semantic::SemanticModel; use ty_python_semantic::types::Type; use ty_python_semantic::types::ide_support::{ @@ -73,8 +74,12 @@ pub struct SignatureHelpInfo<'db> { } /// 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); +pub fn signature_help<'db>( + db: &'db dyn Db, + file: ProgramFile<'db>, + offset: TextSize, +) -> Option> { + let parsed = parsed_module(db, file.python_file(db)).load(db); // Get the call expression at the given position. let (call_expr, current_arg_index) = get_call_expr(&parsed, offset)?; @@ -124,6 +129,8 @@ fn get_call_expr( | TokenKind::Complex | TokenKind::Float | TokenKind::Int => 1, + // Prefer the real token immediately before an empty recovery token at EOF. + TokenKind::Unknown => -1, _ => 0, })?; @@ -158,7 +165,7 @@ fn get_call_expr( return None; }; - // Determine which argument corresponding to the current cursor location. + // Determine which argument corresponds to the current cursor location. let current_arg_index = get_argument_index(call_expr, offset); Some((call_expr, current_arg_index)) @@ -181,7 +188,7 @@ fn get_argument_index(call_expr: &ast::ExprCall, offset: TextSize) -> usize { /// Create signature details from `CallSignatureDetails`. fn create_signature_details_from_call_signature_details<'db>( - db: &dyn crate::Db, + db: &'db dyn Db, details: CallSignatureDetails<'db>, current_arg_index: usize, ) -> SignatureDetails<'db> { @@ -236,7 +243,7 @@ fn create_parameters<'db>( let param_docs = if let Some(docstring) = docstring { docstring.parameter_documentation() } else { - indexmap::IndexMap::new() + FxIndexMap::default() }; parameters @@ -975,7 +982,12 @@ def ab(a: int, *, c: int): // the parameter type should be `str` (not `_KT`). let key_param = &signature.parameters[0]; assert_eq!(key_param.name, "key"); - let type_display = format!("{}", key_param.ty.display(&test.db)); + let type_display = format!( + "{}", + key_param + .ty + .display(&test.db, &test.db.program_environment()) + ); assert_eq!(type_display, "str"); } @@ -996,7 +1008,12 @@ def ab(a: int, *, c: int): // list.append's parameter is typed as `_T`, which should resolve // to `int` for a `list[int]`. let object_param = &signature.parameters[0]; - let type_display = format!("{}", object_param.ty.display(&test.db)); + let type_display = format!( + "{}", + object_param + .ty + .display(&test.db, &test.db.program_environment()) + ); assert_eq!(type_display, "int"); } @@ -1023,12 +1040,18 @@ def ab(a: int, *, c: int): // `T` should be resolved to `str` from the first argument. let a_param = &signature.parameters[0]; assert_eq!(a_param.name, "a"); - let a_type = format!("{}", a_param.ty.display(&test.db)); + let a_type = format!( + "{}", + a_param.ty.display(&test.db, &test.db.program_environment()) + ); assert_eq!(a_type, "str"); let b_param = &signature.parameters[1]; assert_eq!(b_param.name, "b"); - let b_type = format!("{}", b_param.ty.display(&test.db)); + let b_type = format!( + "{}", + b_param.ty.display(&test.db, &test.db.program_environment()) + ); assert_eq!(b_type, "str"); } @@ -1235,6 +1258,32 @@ def ab(a: int, *, c: int): assert_eq!(result.active_signature, Some(0)); } + #[test] + fn signature_help_after_opening_paren_at_end_of_file() { + let test = cursor_test( + r#" + def func(first: int, second: str) -> None: ... + + func("#, + ); + + let result = test.signature_help().expect("Should have signature help"); + assert_eq!(result.signatures[0].active_parameter, Some(0)); + } + + #[test] + fn signature_help_after_comma_at_end_of_file() { + let test = cursor_test( + r#" + def func(first: int, second: str) -> None: ... + + func(1,"#, + ); + + let result = test.signature_help().expect("Should have signature help"); + assert_eq!(result.signatures[0].active_parameter, Some(1)); + } + #[test] fn signature_help_after_closing_paren_at_end_of_file() { let test = cursor_test( @@ -1441,7 +1490,11 @@ def ab(a: int, *, c: int): impl CursorTest { fn signature_help(&self) -> Option> { - crate::signature_help::signature_help(&self.db, self.cursor.file, self.cursor.offset) + crate::signature_help::signature_help( + &self.db, + self.program_file(self.cursor.file), + self.cursor.offset, + ) } fn signature_help_render(&self) -> String { diff --git a/crates/ty_ide/src/snapshots/ty_ide__code_action__tests__add_ignore_trailing_whitespace.snap b/crates/ty_ide/src/snapshots/ty_ide__code_action__tests__add_ignore_trailing_whitespace.snap index 47b7efbbb1..40cf35facd 100644 --- a/crates/ty_ide/src/snapshots/ty_ide__code_action__tests__add_ignore_trailing_whitespace.snap +++ b/crates/ty_ide/src/snapshots/ty_ide__code_action__tests__add_ignore_trailing_whitespace.snap @@ -8,7 +8,6 @@ info[code-action]: Ignore 'unresolved-reference' for this line 1 | b = a / 10 | ^ | - | - b = a / 10 -1 + b = a / 10 # ty:ignore[unresolved-reference] +1 + b = a / 10 # ty: ignore[unresolved-reference] | diff --git a/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_shadowed_numeric_builtin.snap b/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_shadowed_numeric_builtin.snap new file mode 100644 index 0000000000..cc74217ade --- /dev/null +++ b/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_shadowed_numeric_builtin.snap @@ -0,0 +1,17 @@ +--- +source: crates/ty_ide/src/hover.rs +expression: test.hover() +--- +int | builtins.float | main.float +--------------------------------------------- +```python +int | builtins.float | main.float +``` +--------------------------------------------- +info[hover]: Hovered content is + --> main.py:7:5 + | +7 | x + | ^- Cursor offset + | | + | source diff --git a/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_shadowed_numeric_builtin_in_keyword_parameter.snap b/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_shadowed_numeric_builtin_in_keyword_parameter.snap new file mode 100644 index 0000000000..a3924cd5fd --- /dev/null +++ b/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_shadowed_numeric_builtin_in_keyword_parameter.snap @@ -0,0 +1,17 @@ +--- +source: crates/ty_ide/src/hover.rs +expression: test.hover() +--- +(parameter) value: int | builtins.float | main.float +--------------------------------------------- +```python +(parameter) value: int | builtins.float | main.float +``` +--------------------------------------------- +info[hover]: Hovered content is + --> main.py:8:8 + | +8 | choose(value=1.0) + | ^^^^^- Cursor offset + | | + | source diff --git a/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_shadowed_numeric_builtin_in_selected_signature.snap b/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_shadowed_numeric_builtin_in_selected_signature.snap new file mode 100644 index 0000000000..1d2f763862 --- /dev/null +++ b/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_shadowed_numeric_builtin_in_selected_signature.snap @@ -0,0 +1,17 @@ +--- +source: crates/ty_ide/src/hover.rs +expression: test.hover() +--- +def choose(value: int | builtins.float | main.float) +--------------------------------------------- +```python +def choose(value: int | builtins.float | main.float) +``` +--------------------------------------------- +info[hover]: Hovered content is + --> main.py:13:1 + | +13 | choose(1.0) + | ^^^^^^- Cursor offset + | | + | source diff --git a/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_subscript_literal_index_variants.snap b/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_subscript_literal_index_variants.snap index 1547de6380..300ef6b9b1 100644 --- a/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_subscript_literal_index_variants.snap +++ b/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_subscript_literal_index_variants.snap @@ -17,7 +17,6 @@ info[hover]: Hovered content is | | | | | Cursor offset | source - | @@ -36,7 +35,6 @@ info[hover]: Hovered content is | | | | | Cursor offset | source - | @@ -55,7 +53,6 @@ info[hover]: Hovered content is | | | | | Cursor offset | source - | @@ -74,7 +71,6 @@ info[hover]: Hovered content is | | | | | Cursor offset | source - | @@ -93,7 +89,6 @@ info[hover]: Hovered content is | | | | | Cursor offset | source - | @@ -112,7 +107,6 @@ info[hover]: Hovered content is | | | | | Cursor offset | source - | @@ -131,7 +125,6 @@ info[hover]: Hovered content is | | | | | Cursor offset | source - | @@ -150,7 +143,6 @@ info[hover]: Hovered content is | | | | | Cursor offset | source - | @@ -169,4 +161,3 @@ info[hover]: Hovered content is | | | | | Cursor offset | source - | diff --git a/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_subscript_non_literal_index.snap b/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_subscript_non_literal_index.snap index 77c9274dc5..983b0d938a 100644 --- a/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_subscript_non_literal_index.snap +++ b/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_subscript_non_literal_index.snap @@ -16,4 +16,3 @@ info[hover]: Hovered content is | | | source | Cursor offset - | diff --git a/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_subscript_slice_literal_bounds_list_variants.snap b/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_subscript_slice_literal_bounds_list_variants.snap index 4f94fb7b1b..4d84c5ff9e 100644 --- a/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_subscript_slice_literal_bounds_list_variants.snap +++ b/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_subscript_slice_literal_bounds_list_variants.snap @@ -17,7 +17,6 @@ info[hover]: Hovered content is | | | | | Cursor offset | source - | @@ -36,7 +35,6 @@ info[hover]: Hovered content is | | | | | Cursor offset | source - | @@ -55,7 +53,6 @@ info[hover]: Hovered content is | | | | | Cursor offset | source - | @@ -74,7 +71,6 @@ info[hover]: Hovered content is | | | | | Cursor offset | source - | @@ -93,7 +89,6 @@ info[hover]: Hovered content is | | | | | Cursor offset | source - | @@ -112,4 +107,3 @@ info[hover]: Hovered content is | | | | | Cursor offset | source - | diff --git a/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_subscript_slice_literal_bounds_string_variants.snap b/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_subscript_slice_literal_bounds_string_variants.snap index 527c00fc20..0c934022d6 100644 --- a/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_subscript_slice_literal_bounds_string_variants.snap +++ b/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_subscript_slice_literal_bounds_string_variants.snap @@ -17,7 +17,6 @@ info[hover]: Hovered content is | | | | | Cursor offset | source - | @@ -36,7 +35,6 @@ info[hover]: Hovered content is | | | | | Cursor offset | source - | @@ -55,4 +53,3 @@ info[hover]: Hovered content is | | | | | Cursor offset | source - | diff --git a/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_constructor_hint_drops_the_final_modifier.snap b/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_constructor_hint_drops_the_final_modifier.snap index 37879661a9..5096195dff 100644 --- a/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_constructor_hint_drops_the_final_modifier.snap +++ b/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_constructor_hint_drops_the_final_modifier.snap @@ -15,26 +15,22 @@ info[inlay-hint-location]: Inlay Hint Target | 2 | class Wrapper[Element]: | ^^^^^^^ - | info: Source --> main2.py:6:9 | 6 | a[: Wrapper[1]] = Wrapper(1) | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:17 | LL | a[: Wrapper[1]] = Wrapper(1) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits diff --git a/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_declared_variance_beside_an_inferred_parameter.snap b/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_declared_variance_beside_an_inferred_parameter.snap index 301d836cd8..4927d008ec 100644 --- a/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_declared_variance_beside_an_inferred_parameter.snap +++ b/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_declared_variance_beside_an_inferred_parameter.snap @@ -14,65 +14,55 @@ info[inlay-hint-location]: Inlay Hint Target | 2 | class A[out A, B]: | ^ - | info: Source --> main2.py:5:5 | 5 | a[: A[1, 2]] = A[[1, 2]](1, 2) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:7 | LL | a[: A[1, 2]] = A[[1, 2]](1, 2) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:10 | LL | a[: A[1, 2]] = A[[1, 2]](1, 2) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:19 | LL | a[: A[1, 2]] = A[[1, 2]](1, 2) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:22 | LL | a[: A[1, 2]] = A[[1, 2]](1, 2) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits diff --git a/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_implicit_context_arguments.snap b/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_implicit_context_arguments.snap index ef38ca8611..6951e7e263 100644 --- a/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_implicit_context_arguments.snap +++ b/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_implicit_context_arguments.snap @@ -19,36 +19,30 @@ info[inlay-hint-location]: Inlay Hint Target | 5 | context b = 1 | ^ - | info: Source --> main2.py:8:6 | 8 | f([a=b]) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.by:5:9 | 5 | context b = 1 | ^ - | info: Source --> main2.py:10:11 | 10 | g('y'[, a=b, c=d]) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.by:6:9 | 6 | context d = 'x' | ^ - | info: Source --> main2.py:10:16 | 10 | g('y'[, a=b, c=d]) | ^ - | diff --git a/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_implicit_parameters_as_a_value.snap b/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_implicit_parameters_as_a_value.snap index b2ed4dbd2f..097b41a941 100644 --- a/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_implicit_parameters_as_a_value.snap +++ b/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_implicit_parameters_as_a_value.snap @@ -16,13 +16,11 @@ info[inlay-hint-location]: Inlay Hint Target | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:10 | LL | result[: str] = apply:[it: int] | ^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits diff --git a/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_inferred_holes_are_not_hinted.snap b/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_inferred_holes_are_not_hinted.snap index b7f7d3f870..116db83389 100644 --- a/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_inferred_holes_are_not_hinted.snap +++ b/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_inferred_holes_are_not_hinted.snap @@ -18,10 +18,8 @@ info[inlay-hint-location]: Inlay Hint Target | LL | class int: | ^^^ - | info: Source --> main2.py:LL:11 | LL | b = pair[[1]]('lit', 1) | ^ - | diff --git a/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_inferred_override.snap b/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_inferred_override.snap index 88f1784568..1b5c975d72 100644 --- a/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_inferred_override.snap +++ b/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_inferred_override.snap @@ -20,10 +20,8 @@ info[inlay-hint-location]: Inlay Hint Target | 2 | class A: | ^ - | info: Source --> main2.py:8:6 | 8 | [override ]def f(self) -> None: ... | ^^^^^^^^^ - | diff --git a/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_named_type_arguments.snap b/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_named_type_arguments.snap index fb3051c666..3c4aed818d 100644 --- a/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_named_type_arguments.snap +++ b/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_named_type_arguments.snap @@ -19,104 +19,88 @@ info[inlay-hint-location]: Inlay Hint Target | 2 | class Pair[Key, Value]: | ^^^^ - | info: Source --> main2.py:8:5 | 8 | a[: Pair[Key=1, Value="x"]] = Pair[[Key=1, Value="x"]](1, 'x') | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:14 | LL | a[: Pair[Key=1, Value="x"]] = Pair[[Key=1, Value="x"]](1, 'x') | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:23 | LL | a[: Pair[Key=1, Value="x"]] = Pair[[Key=1, Value="x"]](1, 'x') | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:41 | LL | a[: Pair[Key=1, Value="x"]] = Pair[[Key=1, Value="x"]](1, 'x') | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:50 | LL | a[: Pair[Key=1, Value="x"]] = Pair[[Key=1, Value="x"]](1, 'x') | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.by:5:7 | 5 | class One[Element]: | ^^^ - | info: Source --> main2.py:9:5 | 9 | b[: One[1]] = One[[1]](1) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:9 | LL | b[: One[1]] = One[[1]](1) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:20 | LL | b[: One[1]] = One[[1]](1) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits diff --git a/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_some_holes_are_not_hinted.snap b/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_some_holes_are_not_hinted.snap index 4388915e52..d5c193df6e 100644 --- a/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_some_holes_are_not_hinted.snap +++ b/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_some_holes_are_not_hinted.snap @@ -18,10 +18,8 @@ info[inlay-hint-location]: Inlay Hint Target | LL | class int: | ^^^ - | info: Source --> main2.py:LL:11 | LL | b = pair[[1]]('lit', 1) | ^ - | diff --git a/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_string_tag_argument_is_not_hinted.snap b/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_string_tag_argument_is_not_hinted.snap index c2c9c35c99..d6018f5274 100644 --- a/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_string_tag_argument_is_not_hinted.snap +++ b/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_string_tag_argument_is_not_hinted.snap @@ -15,13 +15,11 @@ info[inlay-hint-location]: Inlay Hint Target | 2 | def sql(query: str) -> int: | ^^^^^ - | info: Source --> main2.py:6:10 | 6 | b = sql([query=]"select") | ^^^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits diff --git a/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_type_display.snap b/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_type_display.snap index 589abf0dbd..0526e81b39 100644 --- a/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_type_display.snap +++ b/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__basedpython_type_display.snap @@ -16,26 +16,22 @@ info[inlay-hint-location]: Inlay Hint Target | LL | class int: | ^^^ - | info: Source --> main2.py:LL:5 | LL | a[: 1] = identity[[1]](1) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:20 | LL | a[: 1] = identity[[1]](1) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits diff --git a/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__call_type_arguments.snap b/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__call_type_arguments.snap index 425166a422..7f3b49d7eb 100644 --- a/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__call_type_arguments.snap +++ b/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__call_type_arguments.snap @@ -27,49 +27,41 @@ info[inlay-hint-location]: Inlay Hint Target | LL | class int: | ^^^ - | info: Source --> main2.py:LL:11 | LL | identity[[Literal[1]]](1) | ^^^^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:7 | LL | pair[[Literal["a"], Literal[2]]]('a', 2) | ^^^^^^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:21 | LL | pair[[Literal["a"], Literal[2]]]('a', 2) | ^^^^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:6 | LL | Box[[Literal[1]]](1) | ^^^^^^^^^^ - | diff --git a/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__type_arguments_are_not_named_outside_basedpython.snap b/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__type_arguments_are_not_named_outside_basedpython.snap index af77784cca..33fe1db81d 100644 --- a/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__type_arguments_are_not_named_outside_basedpython.snap +++ b/crates/ty_ide/src/snapshots/ty_ide__inlay_hints__tests__type_arguments_are_not_named_outside_basedpython.snap @@ -14,91 +14,77 @@ info[inlay-hint-location]: Inlay Hint Target | 2 | class Pair[Key, Value]: | ^^^^ - | info: Source --> main2.py:5:5 | 5 | a[: Pair[Literal[1], Literal["x"]]] = Pair[[Literal[1], Literal["x"]]](1, 'x') | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing.byi:LL:1 | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:10 | LL | a[: Pair[Literal[1], Literal["x"]]] = Pair[[Literal[1], Literal["x"]]](1, 'x') | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:18 | LL | a[: Pair[Literal[1], Literal["x"]]] = Pair[[Literal[1], Literal["x"]]](1, 'x') | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing.byi:LL:1 | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:22 | LL | a[: Pair[Literal[1], Literal["x"]]] = Pair[[Literal[1], Literal["x"]]](1, 'x') | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:30 | LL | a[: Pair[Literal[1], Literal["x"]]] = Pair[[Literal[1], Literal["x"]]](1, 'x') | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:45 | LL | a[: Pair[Literal[1], Literal["x"]]] = Pair[[Literal[1], Literal["x"]]](1, 'x') | ^^^^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.byi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:57 | LL | a[: Pair[Literal[1], Literal["x"]]] = Pair[[Literal[1], Literal["x"]]](1, 'x') | ^^^^^^^^^^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits diff --git a/crates/ty_ide/src/stub_mapping.rs b/crates/ty_ide/src/stub_mapping.rs index e69f8b0e77..861aa2028f 100644 --- a/crates/ty_ide/src/stub_mapping.rs +++ b/crates/ty_ide/src/stub_mapping.rs @@ -28,18 +28,23 @@ impl<'db> StubMapper<'db> { /// /// If the definition is in a stub file and a corresponding source file definition exists, /// returns the source file definition(s). Otherwise, returns the original definition. - pub(crate) fn map_definition( + fn map_definition( &self, def: ResolvedDefinition<'db>, ) -> impl Iterator> { - if let Some(definitions) = - map_stub_definition(self.db, &def, self.cached_vendored_root.as_deref()) - { + if let Some(definitions) = self.map_definition_to_source(&def) { return Either::Left(definitions.into_iter()); } Either::Right(std::iter::once(def)) } + pub(crate) fn map_definition_to_source( + &self, + def: &ResolvedDefinition<'db>, + ) -> Option>> { + map_stub_definition(self.db, def, self.cached_vendored_root.as_deref()) + } + /// Map multiple `ResolvedDefinitions`, applying stub-to-source mapping to each. /// /// This is a convenience method that applies `map_definition` to each element diff --git a/crates/ty_ide/src/symbols.rs b/crates/ty_ide/src/symbols.rs index fae38bdec8..31eafa148e 100644 --- a/crates/ty_ide/src/symbols.rs +++ b/crates/ty_ide/src/symbols.rs @@ -6,16 +6,17 @@ use std::ops::Range; use regex::Regex; -use ruff_db::files::File; use ruff_db::parsed::parsed_module; + use ruff_index::{IndexVec, newtype_index}; use ruff_python_ast as ast; use ruff_python_ast::name::{Name, UnqualifiedName}; use ruff_python_ast::visitor::source_order::{self, SourceOrderVisitor}; use ruff_text_size::{Ranged, TextRange}; use rustc_hash::{FxHashMap, FxHashSet}; -use ty_module_resolver::{ModuleName, resolve_module}; +use ty_module_resolver::{ImportingFile, ModuleName, resolve_module}; use ty_project::Db; +use ty_python_core::ProgramFile; use crate::completion::CompletionKind; @@ -31,7 +32,7 @@ pub struct QueryPattern { impl QueryPattern { /// Create a new query pattern from a literal search string given. - pub fn fuzzy(literal_query_string: &str) -> QueryPattern { + pub(crate) fn fuzzy(literal_query_string: &str) -> QueryPattern { let mut pattern = "(?i)".to_string(); for ch in literal_query_string.chars() { pattern.push_str(®ex::escape(ch.encode_utf8(&mut [0; 4]))); @@ -50,7 +51,7 @@ impl QueryPattern { } /// Create a new query - pub fn exactly(symbol: &str) -> QueryPattern { + pub(crate) fn exactly(symbol: &str) -> QueryPattern { QueryPattern { re: None, original: symbol.to_string(), @@ -59,7 +60,7 @@ impl QueryPattern { } /// Create a new query pattern that matches all symbols. - pub fn matches_all_symbols() -> QueryPattern { + pub(crate) fn matches_all_symbols() -> QueryPattern { QueryPattern { re: None, original: String::new(), @@ -71,7 +72,7 @@ impl QueryPattern { self.is_match_symbol_name(&symbol.name) } - pub fn is_match_symbol_name(&self, symbol_name: &str) -> bool { + pub(crate) fn is_match_symbol_name(&self, symbol_name: &str) -> bool { if let Some(ref re) = self.re { re.is_match(symbol_name) } else if self.original_is_exact { @@ -91,7 +92,7 @@ impl QueryPattern { /// This will never return `true` incorrectly, but it may return `false` /// incorrectly. That is, it's possible that this query will match all /// inputs but this still returns `false`. - pub fn will_match_everything(&self) -> bool { + pub(crate) fn will_match_everything(&self) -> bool { self.re.is_none() && self.original.is_empty() } } @@ -149,7 +150,10 @@ impl FlatSymbols { } /// Returns a sequence of symbols that matches the given query. - pub fn search(&self, query: &QueryPattern) -> impl Iterator)> { + pub(crate) fn search( + &self, + query: &QueryPattern, + ) -> impl Iterator)> { self.iter() .filter(|(_, symbol)| query.is_match_symbol(symbol)) } @@ -271,7 +275,7 @@ pub struct SymbolInfo<'a> { } impl SymbolInfo<'_> { - pub fn to_owned(&self) -> SymbolInfo<'static> { + pub(crate) fn to_owned(&self) -> SymbolInfo<'static> { SymbolInfo { name: Cow::Owned(self.name.to_string()), kind: self.kind, @@ -333,7 +337,7 @@ pub enum SymbolKind { } impl SymbolKind { - pub fn function_kind(name: &str, defined_in_class: bool) -> Self { + pub(crate) fn function_kind(name: &str, defined_in_class: bool) -> Self { if !defined_in_class { SymbolKind::Function } else if name == "__init__" { @@ -362,7 +366,7 @@ impl SymbolKind { } /// Maps this to a "completion" kind if a sensible mapping exists. - pub fn to_completion_kind(self) -> Option { + pub(crate) fn to_completion_kind(self) -> Option { Some(match self { SymbolKind::Module => CompletionKind::Module, SymbolKind::Class => CompletionKind::Class, @@ -388,8 +392,8 @@ impl SymbolKind { /// The flattened list includes parent/child information and can be /// converted into a hierarchical collection of symbols. #[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)] -pub(crate) fn symbols_for_file(db: &dyn Db, file: File) -> FlatSymbols { - let parsed = parsed_module(db, file); +pub(crate) fn symbols_for_file(db: &dyn Db, file: ProgramFile<'_>) -> FlatSymbols { + let parsed = parsed_module(db, file.python_file(db)); let module = parsed.load(db); let mut visitor = SymbolVisitor::tree(db, file); @@ -407,14 +411,15 @@ pub(crate) fn symbols_for_file(db: &dyn Db, file: File) -> FlatSymbols { cycle_initial=|_, _, _| FlatSymbols::default(), heap_size=ruff_memory_usage::heap_size, )] -pub(crate) fn symbols_for_file_global_only(db: &dyn Db, file: File) -> FlatSymbols { - let parsed = parsed_module(db, file); +pub(crate) fn symbols_for_file_global_only(db: &dyn Db, file: ProgramFile<'_>) -> FlatSymbols { + let source_file = file.file(db); + let parsed = parsed_module(db, file.python_file(db)); let module = parsed.load(db); let mut visitor = SymbolVisitor::globals(db, file); visitor.visit_body(&module.syntax().body); - if file + if source_file .path(db) .as_system_path() .is_none_or(|path| !db.project().is_file_included(db, path).is_included()) @@ -450,7 +455,7 @@ impl ImportedFrom { fn import_from( db: &dyn Db, - importing_file: File, + importing_file: ImportingFile<'_>, ast: &ast::StmtImportFrom, kind: ImportKind, ) -> Option { @@ -591,16 +596,22 @@ impl<'db> Imports<'db> { fn get_module_symbols( &self, db: &'db dyn Db, - importing_file: File, + program_file: ProgramFile<'db>, name: &ModuleName, ) -> Option<&'db FlatSymbols> { - let module_name = match self.module_names.get(name.as_str())? { + let module_kind = self.module_names.get(name.as_str())?; + let importing_file = + ImportingFile::File(program_file.file(db), program_file.resolver_environment(db)); + let module_name = match module_kind { ImportModuleKind::Definitive(name) | ImportModuleKind::Possible(name) => { name.to_module_name(db, importing_file)? } }; let module = resolve_module(db, importing_file, &module_name)?; - Some(symbols_for_file_global_only(db, module.file(db)?)) + Some(symbols_for_file_global_only( + db, + ProgramFile::new(db, module.file(db)?, program_file.program(db)), + )) } } @@ -649,7 +660,11 @@ enum ImportModuleName<'db> { impl<'db> ImportModuleName<'db> { /// Converts the lazy representation of a module name into an /// actual `ModuleName` that can be used for module resolution. - fn to_module_name(self, db: &'db dyn Db, importing_file: File) -> Option { + fn to_module_name( + self, + db: &'db dyn Db, + importing_file: ImportingFile<'db>, + ) -> Option { match self { ImportModuleName::Import(name) => ModuleName::new(name), ImportModuleName::ImportFrom { parent, child } => { @@ -685,7 +700,7 @@ impl Ranged for AstImport<'_> { #[expect(clippy::struct_excessive_bools)] struct SymbolVisitor<'db> { db: &'db dyn Db, - file: File, + file: ProgramFile<'db>, symbols: IndexVec, symbol_stack: Vec, /// Track if we're currently inside a function at any point. @@ -701,6 +716,12 @@ struct SymbolVisitor<'db> { /// basedpython: the span of the property construct being walked, whose /// synthesized members all stand for source the getter already covers. property_construct: Option, + /// The statement whose expressions are currently being visited. + current_stmt: Option<&'db ast::Stmt>, + /// The binding declared directly by the pattern currently being visited. + pattern_binding: Option<&'db ast::Identifier>, + /// Whether store-context names should be excluded from the enclosing scope. + suppress_store_symbols: bool, /// When enabled, the visitor should only try to extract /// symbols from a module that we believed form the "exported" /// interface for that module. i.e., `__all__` is only respected @@ -722,7 +743,7 @@ struct SymbolVisitor<'db> { } impl<'db> SymbolVisitor<'db> { - fn tree(db: &'db dyn Db, file: File) -> Self { + fn tree(db: &'db dyn Db, file: ProgramFile<'db>) -> Self { Self { db, file, @@ -731,6 +752,9 @@ impl<'db> SymbolVisitor<'db> { in_function: false, in_class: false, property_construct: None, + current_stmt: None, + pattern_binding: None, + suppress_store_symbols: false, exports_only: false, all_origin: None, all_names: FxHashSet::default(), @@ -739,7 +763,7 @@ impl<'db> SymbolVisitor<'db> { } } - fn globals(db: &'db dyn Db, file: File) -> Self { + fn globals(db: &'db dyn Db, file: ProgramFile<'db>) -> Self { Self { exports_only: true, ..Self::tree(db, file) @@ -750,7 +774,10 @@ impl<'db> SymbolVisitor<'db> { // If `__all__` was found but wasn't recognized, // then we emit a diagnostic message indicating as such. if self.all_invalid { - tracing::debug!("Invalid `__all__` in `{}`", self.file.path(self.db)); + tracing::debug!( + "Invalid `__all__` in `{}`", + self.file.file(self.db).path(self.db) + ); } // We want to filter out some of the symbols we collected. // Specifically, to respect conventions around library @@ -824,6 +851,12 @@ impl<'db> SymbolVisitor<'db> { } } + fn visit_nonbinding_target(&mut self, target: &'db ast::Expr) { + let previous = std::mem::replace(&mut self.suppress_store_symbols, true); + self.visit_expr(target); + self.suppress_store_symbols = previous; + } + /// Add a new symbol and return its ID. fn add_symbol(&mut self, mut symbol: SymbolTree) -> SymbolId { if let Some(&parent_id) = self.symbol_stack.last() { @@ -839,27 +872,45 @@ impl<'db> SymbolVisitor<'db> { } /// Adds a symbol for a name definition. - fn add_name_symbol(&mut self, stmt: &ast::Stmt, name: &ast::ExprName, kind: SymbolKind) { + fn add_name_symbol( + &mut self, + stmt: &ast::Stmt, + name: &Name, + name_range: TextRange, + kind: SymbolKind, + ) { let symbol = SymbolTree { parent: None, - name: name.id.to_string(), + name: name.to_string(), kind, deprecated: false, - name_range: name.range(), + name_range, full_range: stmt.range(), imported_from: None, }; self.add_symbol(symbol); } + fn add_pattern_binding(&mut self, stmt: &ast::Stmt, name: &ast::Identifier) { + if self.in_function || !name.is_valid() || name.id == "_" { + return; + } + + self.add_assignment(stmt, &name.id, name.range()); + + if self.exports_only && self.all_origin.is_some() && name.id == "__all__" { + self.all_invalid = true; + } + } + /// Adds a symbol introduced via an assignment. - fn add_assignment(&mut self, stmt: &ast::Stmt, name: &ast::ExprName) { + fn add_assignment(&mut self, stmt: &ast::Stmt, name: &Name, name_range: TextRange) { // Include assignments only when we're in global or class scope. if self.in_function { return; } - let kind = if Self::is_constant_name(name.id.as_str()) { + let kind = if Self::is_constant_name(name.as_str()) { SymbolKind::Constant } else if self .iter_symbol_stack() @@ -869,7 +920,7 @@ impl<'db> SymbolVisitor<'db> { } else { SymbolKind::Variable }; - self.add_name_symbol(stmt, name, kind); + self.add_name_symbol(stmt, name, name_range, kind); } /// Adds a symbol introduced via an import `stmt`. @@ -886,9 +937,15 @@ impl<'db> SymbolVisitor<'db> { let full_range = import.range(); let Some(imported_from) = (match import { AstImport::Import(_) => ImportedFrom::import(alias, import_kind), - AstImport::ImportFrom(ast) => { - ImportedFrom::import_from(self.db, self.file, ast, import_kind) - } + AstImport::ImportFrom(ast) => ImportedFrom::import_from( + self.db, + ImportingFile::File( + self.file.file(self.db), + self.file.resolver_environment(self.db), + ), + ast, + import_kind, + ), }) else { tracing::debug!( "Dropping imported symbol {name} since its module name could not be discovered", @@ -1053,6 +1110,10 @@ impl<'db> SymbolVisitor<'db> { .iter() .find(|alias| &alias.name == "*") .map(Ranged::range); + let importing_file = ImportingFile::File( + self.file.file(self.db), + self.file.resolver_environment(self.db), + ); self.symbols .extend(symbols.symbols.iter().filter_map(|symbol| { // If there's no `__all__`, then names with an underscore @@ -1068,7 +1129,7 @@ impl<'db> SymbolVisitor<'db> { } let Some(imported_from) = ImportedFrom::import_from( self.db, - self.file, + importing_file, import_from, ImportKind::Wildcard, ) else { @@ -1113,10 +1174,17 @@ impl<'db> SymbolVisitor<'db> { &self, import_from: &ast::StmtImportFrom, ) -> Option<&'db FlatSymbols> { + let importing_file = ImportingFile::File( + self.file.file(self.db), + self.file.resolver_environment(self.db), + ); let module_name = - ModuleName::from_import_statement(self.db, self.file, import_from).ok()?; - let module = resolve_module(self.db, self.file, &module_name)?; - Some(symbols_for_file_global_only(self.db, module.file(self.db)?)) + ModuleName::from_import_statement(self.db, importing_file, import_from).ok()?; + let module = resolve_module(self.db, importing_file, &module_name)?; + Some(symbols_for_file_global_only( + self.db, + ProgramFile::new(self.db, module.file(self.db)?, self.file.program(self.db)), + )) } /// Add valid names from `__all__` to the set of existing `__all__` @@ -1163,11 +1231,6 @@ impl<'db> SymbolVisitor<'db> { self.all_origin = Some(origin); } - fn push_symbol(&mut self, symbol: SymbolTree) { - let symbol_id = self.add_symbol(symbol); - self.symbol_stack.push(symbol_id); - } - fn pop_symbol(&mut self) { self.symbol_stack.pop().unwrap(); } @@ -1243,34 +1306,8 @@ impl<'db> SymbolVisitor<'db> { // ... otherwise, it's exported! true } -} - -/// basedpython: the range of the property construct `func` was synthesized -/// from, if it is that construct's getter. -/// -/// The parser lowers `var x: int` plus its accessor blocks into a getter, an -/// optional backing declaration and an optional setter, and marks the getter -/// with a synthetic decorator spanning the whole construct. All three are -/// ranged inside that span, and the source spells one member. -fn property_construct(func: &ast::StmtFunctionDef) -> Option { - func.decorator_list.iter().find_map(|decorator| { - let ast::Expr::Name(marker) = &decorator.expression else { - return None; - }; - matches!(marker.id.as_str(), "__property__" | "__static_property__").then(|| marker.range()) - }) -} -impl<'db> SourceOrderVisitor<'db> for SymbolVisitor<'db> { - fn visit_stmt(&mut self, stmt: &'db ast::Stmt) { - // basedpython: everything else the property construct was lowered into - // stands for source the getter already accounts for - if self - .property_construct - .is_some_and(|construct| construct.contains_range(stmt.range())) - { - return; - } + fn visit_stmt_impl(&mut self, stmt: &'db ast::Stmt) { match stmt { ast::Stmt::FunctionDef(func_def) => { if let Some(construct) = property_construct(func_def) { @@ -1303,19 +1340,32 @@ impl<'db> SourceOrderVisitor<'db> for SymbolVisitor<'db> { imported_from: None, }; + for decorator in &func_def.decorator_list { + self.visit_decorator(decorator); + } + + let symbol_id = self.add_symbol(symbol); + + if let Some(type_params) = &func_def.type_params { + self.visit_type_params(type_params); + } + self.visit_parameters(&func_def.parameters); + if let Some(returns) = &func_def.returns { + self.visit_annotation(returns); + } + if self.exports_only { - self.add_symbol(symbol); // If global_only, don't walk function bodies return; } - self.push_symbol(symbol); + self.symbol_stack.push(symbol_id); // Mark that we're entering a function scope let was_in_function = self.in_function; self.in_function = true; - source_order::walk_stmt(self, stmt); + self.visit_body(&func_def.body); // Restore the previous function scope state self.in_function = was_in_function; @@ -1333,8 +1383,20 @@ impl<'db> SourceOrderVisitor<'db> for SymbolVisitor<'db> { imported_from: None, }; + for decorator in &class_def.decorator_list { + self.visit_decorator(decorator); + } + + let symbol_id = self.add_symbol(symbol); + + if let Some(type_params) = &class_def.type_params { + self.visit_type_params(type_params); + } + if let Some(arguments) = &class_def.arguments { + self.visit_arguments(arguments); + } + if self.exports_only { - self.add_symbol(symbol); // If global_only, don't walk class bodies return; } @@ -1343,8 +1405,8 @@ impl<'db> SourceOrderVisitor<'db> for SymbolVisitor<'db> { let was_in_class = self.in_class; self.in_class = true; - self.push_symbol(symbol); - source_order::walk_stmt(self, stmt); + self.symbol_stack.push(symbol_id); + self.visit_body(&class_def.body); self.pop_symbol(); // Restore the previous class scope state @@ -1358,32 +1420,27 @@ impl<'db> SourceOrderVisitor<'db> for SymbolVisitor<'db> { let ast::Expr::Name(name) = &*type_alias.name else { return; }; - self.add_name_symbol(stmt, name, SymbolKind::Variable); + self.add_name_symbol(stmt, &name.id, name.range(), SymbolKind::Variable); } ast::Stmt::Assign(assign) => { self.add_all_assignment(&assign.targets, Some(&assign.value)); - - for target in &assign.targets { - let ast::Expr::Name(name) = target else { - continue; - }; - self.add_assignment(stmt, name); - } + source_order::walk_stmt(self, stmt); } ast::Stmt::AnnAssign(ann_assign) => { self.add_all_assignment( std::slice::from_ref(&ann_assign.target), ann_assign.value.as_deref(), ); - - let ast::Expr::Name(name) = &*ann_assign.target else { - return; - }; - self.add_assignment(stmt, name); + source_order::walk_stmt(self, stmt); } ast::Stmt::AugAssign(ast::StmtAugAssign { target, op, value, .. }) => { + if !target.is_name_expr() { + self.visit_expr(target); + } + self.visit_expr(value); + // We don't care about `__all__` unless we're // specifically looking for exported symbols. if !self.exports_only { @@ -1408,6 +1465,8 @@ impl<'db> SourceOrderVisitor<'db> for SymbolVisitor<'db> { } } ast::Stmt::Expr(expr) => { + source_order::walk_stmt(self, stmt); + // We don't care about `__all__` unless we're // specifically looking for exported symbols. if !self.exports_only { @@ -1439,8 +1498,6 @@ impl<'db> SourceOrderVisitor<'db> for SymbolVisitor<'db> { if !self.update_all_by_call_idiom(attr, arguments) { self.all_invalid = true; } - - source_order::walk_stmt(self, stmt); } ast::Stmt::Import(import) => { // We ignore any names introduced by imports @@ -1490,15 +1547,123 @@ impl<'db> SourceOrderVisitor<'db> for SymbolVisitor<'db> { // statements. We just assume that all `if` statements are // always `True`. This applies to symbols in general but // also `__all__`. - _ => { - source_order::walk_stmt(self, stmt); + _ => source_order::walk_stmt(self, stmt), + } + } +} + +/// basedpython: the range of the property construct `func` was synthesized +/// from, if it is that construct's getter. +/// +/// The parser lowers `var x: int` plus its accessor blocks into a getter, an +/// optional backing declaration and an optional setter, and marks the getter +/// with a synthetic decorator spanning the whole construct. All three are +/// ranged inside that span, and the source spells one member. +fn property_construct(func: &ast::StmtFunctionDef) -> Option { + func.decorator_list.iter().find_map(|decorator| { + let ast::Expr::Name(marker) = &decorator.expression else { + return None; + }; + matches!(marker.id.as_str(), "__property__" | "__static_property__").then(|| marker.range()) + }) +} + +impl<'db> SourceOrderVisitor<'db> for SymbolVisitor<'db> { + fn visit_stmt(&mut self, stmt: &'db ast::Stmt) { + // basedpython: everything else the property construct was lowered into + // stands for source the getter already accounts for + if self + .property_construct + .is_some_and(|construct| construct.contains_range(stmt.range())) + { + return; + } + + let previous_stmt = self.current_stmt.replace(stmt); + self.visit_stmt_impl(stmt); + self.current_stmt = previous_stmt; + } + + fn visit_expr(&mut self, expr: &'db ast::Expr) { + if self.in_function { + return; + } + + match expr { + ast::Expr::Name(name) + if name.ctx.is_store() + && !self.suppress_store_symbols + && let Some(stmt) = self.current_stmt => + { + self.add_assignment(stmt, &name.id, name.range()); + + if name.id != "__all__" { + return; + } + + // We don't care about `__all__` unless we're + // specifically looking for exported symbols. + if !self.exports_only { + return; + } + + // We can't update `__all__` if it doesn't already exist. + if self.all_origin.is_none() { + return; + } + + if !is_recognized_all_assignment(stmt, name) { + self.all_invalid = true; + } + } + ast::Expr::Lambda(lambda) => { + if let Some(parameters) = &lambda.parameters { + self.visit_parameters(parameters); + } + + let was_in_function = self.in_function; + self.in_function = true; + self.visit_expr(&lambda.body); + self.in_function = was_in_function; } + _ => source_order::walk_expr(self, expr), } } - // TODO: We might consider handling walrus expressions - // here, since they can be used to introduce new names. - fn visit_expr(&mut self, _expr: &ast::Expr) {} + fn visit_comprehension(&mut self, comprehension: &'db ast::Comprehension) { + self.visit_nonbinding_target(&comprehension.target); + self.visit_expr(&comprehension.iter); + + for condition in &comprehension.ifs { + self.visit_expr(condition); + } + } + + fn visit_pattern(&mut self, pattern: &'db ast::Pattern) { + let binding = match pattern { + ast::Pattern::MatchStar(pattern) => pattern.name.as_ref(), + ast::Pattern::MatchAs(pattern) => pattern.name.as_ref(), + ast::Pattern::MatchMapping(pattern) => pattern.rest.as_ref(), + _ => None, + }; + + let previous_binding = self.pattern_binding; + self.pattern_binding = binding; + source_order::walk_pattern(self, pattern); + self.pattern_binding = previous_binding; + } + + fn visit_identifier(&mut self, identifier: &'db ast::Identifier) { + source_order::walk_identifier(self, identifier); + + if let Some(stmt) = self.current_stmt + && self + .pattern_binding + .is_some_and(|binding| std::ptr::eq(binding, identifier)) + { + self.add_pattern_binding(stmt, identifier); + } + } } /// Represents where an `__all__` has been defined. @@ -1517,6 +1682,19 @@ fn is_dunder_all(expr: &ast::Expr) -> bool { matches!(expr, ast::Expr::Name(ast::ExprName { id, .. }) if id == "__all__") } +fn is_recognized_all_assignment(stmt: &ast::Stmt, name: &ast::ExprName) -> bool { + match stmt { + ast::Stmt::Assign(assign) => assign + .targets + .first() + .is_some_and(|target| is_dunder_all(target) && target.range() == name.range()), + ast::Stmt::AnnAssign(assign) => { + is_dunder_all(&assign.target) && assign.target.range() == name.range() + } + _ => false, + } +} + /// Create and return a string representing a name from the given /// expression, or `None` if it is an invalid expression for a /// `__all__` element. @@ -1535,6 +1713,7 @@ mod tests { use ruff_python_ast::PythonVersion; use ruff_python_trivia::textwrap::dedent; use ty_project::{ProjectMetadata, TestDb}; + use ty_python_core::ProgramFile; use super::symbols_for_file_global_only; @@ -1593,6 +1772,200 @@ def quux(): ); } + #[test] + fn exports_with_statement_targets() { + insta::assert_snapshot!( + public_test("\ +from contextlib import nullcontext + +with nullcontext() as module_target, nullcontext((1, 2)) as (left, right): + body_target = 1 + +class C: + with nullcontext() as class_target: + body_field = 1 + +def function(): + with nullcontext() as local_target: + pass +").exports(), + @" + module_target :: Variable + left :: Variable + right :: Variable + body_target :: Variable + C :: Class + function :: Function + ", + ); + } + + #[test] + fn exports_store_context_targets() { + let test = public_test( + "\ +first, *rest, LAST = values +for loop_left, [loop_right, *loop_rest] in rows: + pass +with manager() as [with_left, *with_rest]: + pass +captured = (walrus := 1) +", + ); + + assert_eq!( + test.exports(), + "first :: Variable\n\ +rest :: Variable\n\ +LAST :: Constant\n\ +loop_left :: Variable\n\ +loop_right :: Variable\n\ +loop_rest :: Variable\n\ +with_left :: Variable\n\ +with_rest :: Variable\n\ +captured :: Variable\n\ +walrus :: Variable" + ); + } + + #[test] + fn exports_match_pattern_bindings() { + let test = public_test( + "\ +match subject: + case [first, *middle, last] as sequence: + body_target = 1 + case {\"key\": mapping_value, **remaining}: + fallback_target = 2 + case Point(positional, named=keyword): + pass + case (0 as alternative) | (1 as alternative): + pass + case _: + wildcard_body = 3 + +match other: + case CONSTANT_CAPTURE: + pass +", + ); + + assert_eq!( + test.exports(), + "first :: Variable\n\ +middle :: Variable\n\ +last :: Variable\n\ +sequence :: Variable\n\ +body_target :: Variable\n\ +mapping_value :: Variable\n\ +remaining :: Variable\n\ +fallback_target :: Variable\n\ +positional :: Variable\n\ +keyword :: Variable\n\ +alternative :: Variable\n\ +wildcard_body :: Variable\n\ +CONSTANT_CAPTURE :: Constant" + ); + } + + #[test] + fn exports_reports_mapping_pattern_bindings_in_source_order() { + let test = public_test( + "\ +match subject: + case {\"a\": before, **between, \"b\": after}: + pass +", + ); + + assert_eq!( + test.exports(), + "before :: Variable\n\ +between :: Variable\n\ +after :: Variable" + ); + } + + #[test] + fn exports_invalidate_all_rebound_by_match_pattern() { + let test = public_test( + "\ +hidden = 1 +visible = 2 +__all__ = ['visible'] +match subject: + case __all__: + pass +", + ); + + assert_eq!( + test.exports(), + "hidden :: Variable\n\ +visible :: Variable\n\ +__all__ :: Variable" + ); + } + + #[test] + fn exports_exclude_comprehension_targets() { + let test = public_test( + "\ +result = [item for item in values if (leaked := item)] +generator = (other for other in values) +lambda_value = lambda: (lambda_local := 1) +", + ); + + assert_eq!( + test.exports(), + "result :: Variable\n\ +leaked :: Variable\n\ +generator :: Variable\n\ +lambda_value :: Variable" + ); + } + + #[test] + fn exports_invalidate_all_rebound_by_with_target() { + let test = public_test( + "\ +hidden = 1 +visible = 2 +__all__ = ['visible'] +with manager() as __all__: + pass +", + ); + + assert_eq!( + test.exports(), + "hidden :: Variable\n\ +visible :: Variable\n\ +__all__ :: Variable" + ); + } + + #[test] + fn exports_invalidate_all_rebound_by_named_expression() { + let test = public_test( + "\ +hidden = 1 +visible = 2 +__all__ = ['visible'] +result = (__all__ := unknown) +", + ); + + assert_eq!( + test.exports(), + "hidden :: Variable\n\ +visible :: Variable\n\ +__all__ :: Variable\n\ +result :: Variable" + ); + } + /// The typing spec says that names beginning with an underscore /// ought to be considered unexported[1]. However, at present, we /// currently include them in completions but rank them lower than @@ -2978,7 +3351,14 @@ class C: ... /// The path given must have been written to this test's salsa DB. fn exported_symbols_for(&self, path: impl AsRef) -> &super::FlatSymbols { let file = system_path_to_file(&self.db, path.as_ref()).unwrap(); - symbols_for_file_global_only(&self.db, file) + symbols_for_file_global_only( + &self.db, + ProgramFile::new( + &self.db, + file, + self.db.program_environment().program(&self.db), + ), + ) } /// Returns the exports from the module at the given path. @@ -3013,12 +3393,11 @@ class C: ... } impl PublicTestBuilder { - pub(super) fn build(&self) -> PublicTest { + fn build(&self) -> PublicTest { let metadata = ProjectMetadata::new("test", SystemPathBuf::from("/")); let mut db = TestDb::new(metadata); - db.init_program_with_python_version(self.python_version.unwrap_or_default()) - .unwrap(); + db.set_python_version(self.python_version.unwrap_or_default()); for Source { path, contents } in &self.sources { db.write_file(path, contents) @@ -3047,7 +3426,7 @@ class C: ... } } - pub(super) fn source( + fn source( &mut self, path: impl Into, contents: impl AsRef, @@ -3058,7 +3437,7 @@ class C: ... self } - pub(super) fn python_version(&mut self, version: PythonVersion) -> &mut PublicTestBuilder { + fn python_version(&mut self, version: PythonVersion) -> &mut PublicTestBuilder { self.python_version = Some(version); self } diff --git a/crates/ty_ide/src/type_hierarchy.rs b/crates/ty_ide/src/type_hierarchy.rs index 55959cac08..e77f73ba74 100644 --- a/crates/ty_ide/src/type_hierarchy.rs +++ b/crates/ty_ide/src/type_hierarchy.rs @@ -6,9 +6,10 @@ use ruff_db::parsed::parsed_module; use ruff_python_ast::name::Name; use ruff_text_size::{TextRange, TextSize}; use ty_project::parallel::ParallelIteratorExt; -use ty_python_semantic::SemanticModel; +use ty_python_core::ProgramFile; use ty_python_semantic::TypeHierarchyClass; use ty_python_semantic::types::Type; +use ty_python_semantic::{ProgramEnvironment, SemanticModel}; /// Represents a type hierarchy item returned by the LSP type hierarchy requests. #[derive(Debug, Clone)] @@ -30,28 +31,30 @@ pub struct TypeHierarchyItem { /// Returns `None` if the position is not on a class definition or class reference. pub fn prepare_type_hierarchy( db: &dyn Db, - file: File, + file: ProgramFile<'_>, offset: TextSize, ) -> Option { - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, file.python_file(db)).load(db); let model = SemanticModel::new(db, file); let goto_target = find_goto_target(&model, &module, offset)?; let ty = goto_target.inferred_type(&model)?; - let hierarchy_class = ty_python_semantic::type_hierarchy_prepare(db, ty)?; + let env = model.program_environment(); + let hierarchy_class = ty_python_semantic::type_hierarchy_prepare(db, &env, ty)?; Some(type_hierarchy_class_to_item(db, hierarchy_class)) } /// Get the supertypes (base classes) of a type hierarchy item. pub fn type_hierarchy_supertypes( db: &dyn Db, - file: File, + file: ProgramFile<'_>, offset: TextSize, ) -> Vec { let Some(ty) = resolve_type_at(db, file, offset) else { return vec![]; }; - ty_python_semantic::type_hierarchy_supertypes(db, ty) + let env = ProgramEnvironment::from_file(file); + ty_python_semantic::type_hierarchy_supertypes(db, &env, ty) .into_iter() .map(|c| type_hierarchy_class_to_item(db, c)) .collect() @@ -62,17 +65,18 @@ pub fn type_hierarchy_supertypes( /// This scans all available modules and can be expensive in large projects. pub fn type_hierarchy_subtypes( db: &dyn Db, - file: File, + file: ProgramFile<'_>, offset: TextSize, ) -> Vec { let Some(ty) = resolve_type_at(db, file, offset) else { return vec![]; }; - ty_module_resolver::all_modules(db) + ty_module_resolver::all_modules(db, file.resolver_environment(db)) .into_par_iter() .map_with_db(db, |db, module| { - ty_python_semantic::type_hierarchy_subtypes(db, ty, &[module]) + let env = ProgramEnvironment::from_file(file); + ty_python_semantic::type_hierarchy_subtypes(db, &env, ty, &[module]) .into_iter() .map(|class| type_hierarchy_class_to_item(db, class)) .collect::>() @@ -85,22 +89,29 @@ pub fn type_hierarchy_subtypes( /// /// If a symbol could not be found at the given offset or its type could /// not be inferred, `None` is returned. -fn resolve_type_at(db: &dyn Db, file: File, offset: TextSize) -> Option> { - let module = parsed_module(db, file).load(db); +fn resolve_type_at<'db>( + db: &'db dyn Db, + file: ProgramFile<'db>, + offset: TextSize, +) -> Option> { + let module = parsed_module(db, file.python_file(db)).load(db); let model = SemanticModel::new(db, file); let goto_target = find_goto_target(&model, &module, offset)?; goto_target.inferred_type(&model) } -fn type_hierarchy_class_to_item(db: &dyn Db, class: TypeHierarchyClass) -> TypeHierarchyItem { +fn type_hierarchy_class_to_item<'db>( + db: &'db dyn Db, + class: TypeHierarchyClass<'db>, +) -> TypeHierarchyItem { let detail = ty_module_resolver::file_to_module(db, class.file) .map(|module| module.name(db).to_string()); TypeHierarchyItem { name: class.name, detail, - file: class.file, + file: class.file.file(db), full_range: class.full_range, selection_range: class.selection_range, } @@ -221,7 +232,7 @@ mod tests { let supertypes = test.supertypes(); insta::assert_snapshot!( snapshot(&test.db, &supertypes), - @"vendored://stdlib/builtins.byi:2290:2296 object :: builtins", + @"vendored://stdlib/builtins.byi:2293:2299 object :: builtins", ); } @@ -335,8 +346,9 @@ mod tests { let subtypes = test.subtypes(); insta::assert_snapshot!(snapshot(&test.db, &subtypes), @" vendored://stdlib/email/headerregistry.byi:698:708 BaseHeader :: email.headerregistry - vendored://stdlib/enum.byi:17985:17992 StrEnum :: enum + vendored://stdlib/enum.byi:17977:17984 StrEnum :: enum vendored://stdlib/pdb.byi:38323:38328 _rstr :: pdb + vendored://stdlib/ty_extensions/__init__.pyi:8238:8247 Character :: ty_extensions vendored://stdlib/xxlimited.byi:98:101 Str :: xxlimited "); } @@ -367,9 +379,10 @@ mod tests { let subtypes = test.subtypes(); insta::assert_snapshot!(snapshot(&test.db, &subtypes), @" vendored://stdlib/email/headerregistry.byi:698:708 BaseHeader :: email.headerregistry - vendored://stdlib/enum.byi:17985:17992 StrEnum :: enum + vendored://stdlib/enum.byi:17977:17984 StrEnum :: enum /main.py:77:89 MyEventTypeA :: main vendored://stdlib/pdb.byi:38323:38328 _rstr :: pdb + vendored://stdlib/ty_extensions/__init__.pyi:8238:8247 Character :: ty_extensions vendored://stdlib/xxlimited.byi:98:101 Str :: xxlimited "); } @@ -435,12 +448,12 @@ mod tests { let item = test.prepare().unwrap(); insta::assert_snapshot!( snapshot(&test.db, &[item]), - @"vendored://stdlib/builtins.byi:7009:7013 type :: builtins", + @"vendored://stdlib/builtins.byi:7012:7016 type :: builtins", ); let supertypes = test.supertypes(); insta::assert_snapshot!( snapshot(&test.db, &supertypes), - @"vendored://stdlib/builtins.byi:2290:2296 object :: builtins", + @"vendored://stdlib/builtins.byi:2293:2299 object :: builtins", ); } @@ -492,7 +505,7 @@ mod tests { let supertypes = test.supertypes(); insta::assert_snapshot!( snapshot(&test.db, &supertypes), - @"vendored://stdlib/builtins.byi:97519:97524 tuple :: builtins", + @"vendored://stdlib/builtins.byi:97307:97312 tuple :: builtins", ); } @@ -720,21 +733,33 @@ Public = _Internal impl CursorTest { fn prepare(&self) -> Option { - prepare_type_hierarchy(&self.db, self.cursor.file, self.cursor.offset) + prepare_type_hierarchy( + &self.db, + self.program_file(self.cursor.file), + self.cursor.offset, + ) } fn supertypes(&self) -> Vec { let Some(item) = self.prepare() else { return vec![]; }; - type_hierarchy_supertypes(&self.db, item.file, item.selection_range.start()) + type_hierarchy_supertypes( + &self.db, + self.program_file(item.file), + item.selection_range.start(), + ) } fn subtypes(&self) -> Vec { let Some(item) = self.prepare() else { return vec![]; }; - type_hierarchy_subtypes(&self.db, item.file, item.selection_range.start()) + type_hierarchy_subtypes( + &self.db, + self.program_file(item.file), + item.selection_range.start(), + ) } } } diff --git a/crates/ty_ide/src/workspace_symbols.rs b/crates/ty_ide/src/workspace_symbols.rs index 8833188ec8..bf2b50ba74 100644 --- a/crates/ty_ide/src/workspace_symbols.rs +++ b/crates/ty_ide/src/workspace_symbols.rs @@ -16,7 +16,6 @@ pub fn workspace_symbols(db: &dyn Db, query: &str) -> Vec { let _span = workspace_symbols_span.enter(); let project = db.project(); - let query = QueryPattern::fuzzy(query); let files = project.files(db); let files: Vec<_> = files.iter().copied().collect(); @@ -31,7 +30,7 @@ pub fn workspace_symbols(db: &dyn Db, query: &str) -> Vec { ); let _entered = symbols_for_file_span.entered(); - symbols_for_file(db, file) + symbols_for_file(db, db.program_file(file)) .search(&query) .map(|(_, symbol)| WorkspaceSymbolInfo { symbol: symbol.to_owned(), @@ -118,7 +117,6 @@ API_BASE_URL = 'https://api.example.com' | 2 | def utility_function(): | ^^^^^^^^^^^^^^^^ - | info: Function utility_function "); @@ -128,7 +126,6 @@ API_BASE_URL = 'https://api.example.com' | 2 | class DataModel: | ^^^^^^^^^ - | info: Class DataModel "); @@ -138,7 +135,6 @@ API_BASE_URL = 'https://api.example.com' | 2 | API_BASE_URL = 'https://api.example.com' | ^^^^^^^^^^^^ - | info: Constant API_BASE_URL "); } @@ -161,7 +157,6 @@ class Test: | 3 | def from_path(): ... | ^^^^^^^^^ - | info: Method from_path "); } @@ -185,7 +180,6 @@ class Test: | 4 | def from_path(): ... | ^^^^^^^^^ - | info: Method from_path "); } @@ -210,7 +204,6 @@ foo = 1 | 5 | foo = 1 | ^^^ - | info: Variable foo "); assert_snapshot!(test.workspace_symbols("re"), @"No symbols found"); diff --git a/crates/ty_module_resolver/Cargo.toml b/crates/ty_module_resolver/Cargo.toml index d75f8c4548..dbd4c2c350 100644 --- a/crates/ty_module_resolver/Cargo.toml +++ b/crates/ty_module_resolver/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_module_resolver" -version = "0.0.5" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } @@ -20,6 +20,7 @@ anyhow = { workspace = true } camino = { workspace = true } compact_str = { workspace = true } get-size2 = { workspace = true } +ordermap = { workspace = true } regex = { workspace = true } regex-syntax = { workspace = true } rustc-hash = { workspace = true } diff --git a/crates/ty_module_resolver/README.md b/crates/ty_module_resolver/README.md index 3c34b69455..1c094d7d90 100644 --- a/crates/ty_module_resolver/README.md +++ b/crates/ty_module_resolver/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ty_module_resolver). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ty_module_resolver). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_module_resolver/src/db.rs b/crates/ty_module_resolver/src/db.rs index 5e9cc24c6f..13950cf15c 100644 --- a/crates/ty_module_resolver/src/db.rs +++ b/crates/ty_module_resolver/src/db.rs @@ -1,12 +1,7 @@ use ruff_db::Db as SourceDb; -use crate::resolve::SearchPaths; - #[salsa::db] -pub trait Db: SourceDb { - /// Returns the search paths for module resolution. - fn search_paths(&self) -> &SearchPaths; -} +pub trait Db: SourceDb {} #[cfg(test)] pub(crate) mod tests { @@ -19,7 +14,7 @@ pub(crate) mod tests { use ruff_python_ast::PythonVersion; use super::Db; - use crate::resolve::SearchPaths; + use crate::{ResolverEnvironment, resolve::SearchPaths}; type Events = Arc>>; @@ -71,6 +66,14 @@ pub(crate) mod tests { self.search_paths = Arc::new(search_paths); } + pub(crate) fn search_paths(&self) -> &SearchPaths { + &self.search_paths + } + + pub(crate) fn resolver_environment(&self) -> ResolverEnvironment<'_> { + ResolverEnvironment::new(self, self.python_version, self.search_paths.as_ref()) + } + /// Takes the salsa events. pub(crate) fn take_salsa_events(&mut self) -> Vec { let mut events = self.events.lock().unwrap(); @@ -106,18 +109,10 @@ pub(crate) mod tests { fn files(&self) -> &Files { &self.files } - - fn python_version(&self) -> PythonVersion { - self.python_version - } } #[salsa::db] - impl Db for TestDb { - fn search_paths(&self) -> &SearchPaths { - &self.search_paths - } - } + impl Db for TestDb {} #[salsa::db] impl salsa::Database for TestDb {} diff --git a/crates/ty_module_resolver/src/distributions.rs b/crates/ty_module_resolver/src/distributions.rs index 684b8a09ba..fd05a5e2f8 100644 --- a/crates/ty_module_resolver/src/distributions.rs +++ b/crates/ty_module_resolver/src/distributions.rs @@ -16,6 +16,7 @@ use ruff_db::system::SystemPath; use rustc_hash::FxHashMap; use crate::db::Db; +use crate::environment::ResolverEnvironment; use crate::module::Module; /// The name of a distribution, as `pyproject.toml` and `site-packages` spell it. @@ -175,12 +176,15 @@ fn top_level_of<'db>(db: &'db dyn Db, module: Module<'db>) -> Option<&'db str> { /// The distributions installed into every `site-packages` directory ty resolved. #[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)] -pub fn distribution_index(db: &dyn Db) -> DistributionIndex { +pub fn distribution_index<'db>( + db: &'db dyn Db, + resolver_environment: ResolverEnvironment<'db>, +) -> DistributionIndex { let _span = tracing::debug_span!("distribution_index").entered(); let mut owners: FxHashMap, Vec> = FxHashMap::default(); - for site_packages in db.search_paths().site_packages_paths() { + for site_packages in resolver_environment.search_paths(db).site_packages_paths() { index_site_packages(db, site_packages, &mut owners); } @@ -471,11 +475,11 @@ types_requests-2.32.0.dist-info/RECORD,, fn owners(db: &TestDb, module: &str) -> Vec { let module_name = ModuleName::new(module).unwrap(); - let module = all_modules(db) + let module = all_modules(db, db.resolver_environment()) .into_iter() .find(|listed| listed.name(db) == &module_name) .unwrap_or_else(|| panic!("`{module}` should resolve")); - distribution_index(db) + distribution_index(db, db.resolver_environment()) .owners_of(db, module) .iter() .map(ToString::to_string) @@ -524,7 +528,7 @@ types_requests-2.32.0.dist-info/RECORD,, ], ); - let index = distribution_index(&db); + let index = distribution_index(&db, db.resolver_environment()); let mut owners: Vec<_> = index .owners_of_top_level("google") .iter() @@ -547,7 +551,7 @@ types_requests-2.32.0.dist-info/RECORD,, fn a_distribution_without_a_record_is_skipped() { let (db, _) = case(&[("orphan/__init__.py", "")], &[]); - assert!(distribution_index(&db).is_empty()); + assert!(distribution_index(&db, db.resolver_environment()).is_empty()); assert!(owners(&db, "orphan").is_empty()); } @@ -559,7 +563,7 @@ types_requests-2.32.0.dist-info/RECORD,, ); assert!( - distribution_index(&db) + distribution_index(&db, db.resolver_environment()) .owners_of_top_level("numpy") .is_empty() ); @@ -583,7 +587,7 @@ types_requests-2.32.0.dist-info/RECORD,, ); assert!( - distribution_index(&db) + distribution_index(&db, db.resolver_environment()) .owners_of_top_level("extra") .is_empty() ); diff --git a/crates/ty_module_resolver/src/environment.rs b/crates/ty_module_resolver/src/environment.rs new file mode 100644 index 0000000000..478a854672 --- /dev/null +++ b/crates/ty_module_resolver/src/environment.rs @@ -0,0 +1,100 @@ +use std::fmt; + +use ruff_db::files::File; +use ruff_python_ast::PythonVersion; + +use crate::{Db, ModuleResolveMode, SearchPaths, search_paths}; + +/// The Python version and search paths used to resolve modules. +#[salsa::interned(debug, heap_size = ruff_memory_usage::heap_size)] +pub struct ResolverEnvironment<'db> { + #[returns(copy)] + pub python_version: PythonVersion, + + #[returns(ref)] + pub search_paths: SearchPaths, +} + +impl get_size2::GetSize for ResolverEnvironment<'_> {} + +impl<'db> ResolverEnvironment<'db> { + pub fn display_search_paths( + self, + db: &'db dyn Db, + mode: ModuleResolveMode, + ) -> DisplaySearchPaths<'db> { + DisplaySearchPaths { + db, + resolver_environment: self, + mode, + } + } +} + +pub struct DisplaySearchPaths<'db> { + db: &'db dyn Db, + resolver_environment: ResolverEnvironment<'db>, + mode: ModuleResolveMode, +} + +impl fmt::Display for DisplaySearchPaths<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut paths = search_paths(self.db, self.resolver_environment, self.mode).peekable(); + + if paths.peek().is_none() { + return f.write_str("[]"); + } + + writeln!(f, "[")?; + for path in paths { + writeln!(f, " {path},")?; + } + f.write_str("]") + } +} + +/// A file interpreted within a particular module-resolution environment. +/// +/// The same file can resolve imports differently depending on the Python version and search paths +/// used to interpret it. +/// +/// For example, consider a file containing: +/// +/// ```python +/// from zipfile._path import Path +/// ``` +/// +/// Typeshed makes `zipfile._path` available only on Python 3.12 and newer: +/// +/// ```text +/// resolve_module(ResolverFile(shared.py, Python 3.11), "zipfile._path") +/// -> unresolved +/// +/// resolve_module(ResolverFile(shared.py, Python 3.12), "zipfile._path") +/// -> zipfile/_path/__init__.pyi +/// ``` +/// +/// Search paths can also change which file an import resolves to, even when the Python version is +/// identical: +/// +/// ```text +/// resolve_module(ResolverFile(shared.py, project environment), "dependency") +/// -> .venv/lib/dependency.py +/// +/// resolve_module(ResolverFile(shared.py, script environment), "dependency") +/// -> .script-venv/lib/dependency.py +/// ``` +/// +/// Including the resolver environment in the file's identity keeps these resolution results +/// separate. Projects and scripts with equivalent resolver environments can still share resolution +/// results. +#[salsa::interned(debug, heap_size = ruff_memory_usage::heap_size)] +pub struct ResolverFile<'db> { + #[returns(copy)] + pub file: File, + + #[returns(copy)] + pub environment: ResolverEnvironment<'db>, +} + +impl get_size2::GetSize for ResolverFile<'_> {} diff --git a/crates/ty_module_resolver/src/lib.rs b/crates/ty_module_resolver/src/lib.rs index 7d7e6c0b19..725c1f8023 100644 --- a/crates/ty_module_resolver/src/lib.rs +++ b/crates/ty_module_resolver/src/lib.rs @@ -1,11 +1,14 @@ +use std::hash::BuildHasherDefault; use std::iter::FusedIterator; use ruff_db::system::SystemPath; +use rustc_hash::FxHasher; pub use db::Db; +pub use environment::{ResolverEnvironment, ResolverFile}; pub use module::KnownModule; pub use module::Module; -pub use module_name::{ModuleName, ModuleNameResolutionError}; +pub use module_name::{ImportingFile, ModuleName, ModuleNameResolutionError}; pub use path::{SearchPath, SearchPathError}; pub use resolve::{ SearchPaths, file_to_module, resolve_module, resolve_module_confident, resolve_real_module, @@ -13,9 +16,7 @@ pub use resolve::{ }; pub use settings::{SearchPathSettings, SearchPathSettingsError}; pub use strategy::{FallibleStrategy, MisconfigurationStrategy, UseDefaultStrategy}; -pub use typeshed::{ - PyVersionRange, TypeshedVersions, TypeshedVersionsParseError, vendored_typeshed_versions, -}; +pub use typeshed::{PyVersionRange, TypeshedVersions, TypeshedVersionsParseError}; pub use distributions::{DistributionIndex, DistributionName, distribution_index}; pub use list::{all_modules, list_modules}; @@ -24,6 +25,7 @@ pub use resolve::{ModuleResolveMode, SearchPathIterator, search_paths}; mod db; mod distributions; +mod environment; mod list; mod module; mod module_glob; @@ -34,15 +36,20 @@ mod settings; mod strategy; mod typeshed; +type FxOrderMap = ordermap::map::OrderMap>; + #[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<'_> { +pub fn system_module_search_paths<'db>( + db: &'db dyn Db, + resolver_environment: ResolverEnvironment<'db>, +) -> SystemModuleSearchPathsIter<'db> { SystemModuleSearchPathsIter { // Always run in `Typing` mode because we want to include as much as possible // and we don't care about the "real" stdlib - inner: search_paths(db, ModuleResolveMode::Typing), + inner: search_paths(db, resolver_environment, ModuleResolveMode::Typing), } } diff --git a/crates/ty_module_resolver/src/list.rs b/crates/ty_module_resolver/src/list.rs index 433c31fef9..ba421da8ae 100644 --- a/crates/ty_module_resolver/src/list.rs +++ b/crates/ty_module_resolver/src/list.rs @@ -2,8 +2,8 @@ use std::borrow::Cow; use std::collections::btree_map::{BTreeMap, Entry}; use ruff_db::files::directory_listing; -use ruff_python_ast::PythonVersion; +use crate::ResolverEnvironment; use crate::db::Db; use crate::module::{Module, ModuleKind}; use crate::module_name::ModuleName; @@ -11,8 +11,11 @@ use crate::path::{ModulePath, SearchPath, SystemOrVendoredPathRef}; use crate::resolve::{ModuleResolveMode, ResolverContext, resolve_file_module, search_paths}; /// List all available modules, including all sub-modules, sorted in lexicographic order. -pub fn all_modules(db: &dyn Db) -> Vec> { - let mut modules = list_modules(db).to_vec(); +pub fn all_modules<'db>( + db: &'db dyn Db, + resolver_environment: ResolverEnvironment<'db>, +) -> Vec> { + let mut modules = list_modules(db, resolver_environment).to_vec(); let mut stack = modules.clone(); while let Some(module) = stack.pop() { for &submodule in module.all_submodules(db) { @@ -25,11 +28,24 @@ pub fn all_modules(db: &dyn Db) -> Vec> { } /// List all available top-level modules. +pub fn list_modules<'db>( + db: &'db dyn Db, + resolver_environment: ResolverEnvironment<'db>, +) -> &'db [Module<'db>] { + list_modules_impl(db, resolver_environment) +} + #[salsa::tracked(returns(deref))] -pub fn list_modules(db: &dyn Db) -> Box<[Module<'_>]> { +fn list_modules_impl<'db>( + db: &'db dyn Db, + resolver_environment: ResolverEnvironment<'db>, +) -> Box<[Module<'db>]> { let mut modules: BTreeMap<&ModuleName, ListedModule<'_>> = BTreeMap::new(); - for search_path in search_paths(db, ModuleResolveMode::Typing) { - for &new in list_modules_in(db, SearchPathIngredient::new(db, search_path.clone())) { + for search_path in search_paths(db, resolver_environment, ModuleResolveMode::Typing) { + for &new in list_modules_in( + db, + SearchPathIngredient::new(db, resolver_environment, search_path.clone()), + ) { match modules.entry(new.module(db).name(db)) { Entry::Vacant(entry) => { entry.insert(new); @@ -65,6 +81,8 @@ pub fn list_modules(db: &dyn Db) -> Box<[Module<'_>]> { #[salsa::tracked(debug, heap_size=ruff_memory_usage::heap_size)] struct SearchPathIngredient<'db> { + #[returns(copy)] + resolver_environment: ResolverEnvironment<'db>, #[returns(ref)] path: SearchPath, } @@ -77,7 +95,7 @@ fn list_modules_in<'db>( ) -> Vec> { let path = search_path.path(db); tracing::debug!("Listing modules in search path '{}'", path); - let mut lister = Lister::new(db, path); + let mut lister = Lister::new(db, search_path.resolver_environment(db), path); match path.as_path() { SystemOrVendoredPathRef::System(system_search_path) => { let Ok(listing) = directory_listing(db, system_search_path) else { @@ -118,16 +136,22 @@ impl get_size2::GetSize for ListedModule<'_> {} struct Lister<'db> { db: &'db dyn Db, search_path: &'db SearchPath, + resolver_environment: ResolverEnvironment<'db>, modules: BTreeMap<&'db ModuleName, ListedModule<'db>>, } impl<'db> Lister<'db> { /// Create new state that can accumulate modules from a list /// of file paths. - fn new(db: &'db dyn Db, search_path: &'db SearchPath) -> Lister<'db> { + fn new( + db: &'db dyn Db, + resolver_environment: ResolverEnvironment<'db>, + search_path: &'db SearchPath, + ) -> Lister<'db> { Lister { db, search_path, + resolver_environment, modules: BTreeMap::new(), } } @@ -179,10 +203,11 @@ impl<'db> Lister<'db> { &module_path, Module::file_module( self.db, + file, + self.resolver_environment, Cow::Owned(module_name), ModuleKind::Package, self.search_path.clone(), - file, ), ); return; @@ -223,7 +248,11 @@ impl<'db> Lister<'db> { if !self.search_path.is_standard_library() { self.add_module( &module_path, - Module::namespace_package(self.db, Cow::Owned(module_name)), + Module::namespace_package( + self.db, + self.resolver_environment, + Cow::Owned(module_name), + ), ); } return; @@ -251,10 +280,11 @@ impl<'db> Lister<'db> { &module_path, Module::file_module( self.db, + file, + self.resolver_environment, Cow::Owned(module_name), ModuleKind::Module, self.search_path.clone(), - file, ), ); } @@ -317,20 +347,17 @@ impl<'db> Lister<'db> { /// Returns true if the given module name cannot be shadowable. fn is_non_shadowable(&self, name: &ModuleName) -> bool { - ModuleResolveMode::Typing.is_non_shadowable(self.python_version().minor, name.as_str()) - } - - /// Returns the Python version we want to perform module resolution - /// with. - fn python_version(&self) -> PythonVersion { - self.db.python_version() + ModuleResolveMode::Typing.is_non_shadowable( + self.resolver_environment.python_version(self.db).minor, + name.as_str(), + ) } /// Constructs a resolver context for use with some APIs that require it. fn context(&self) -> ResolverContext<'db> { ResolverContext { db: self.db, - python_version: self.python_version(), + resolver_environment: self.resolver_environment, // We don't currently support listing modules // in a "no stubs allowed" mode. mode: ModuleResolveMode::Typing, @@ -407,7 +434,9 @@ mod tests { use crate::strategy::FallibleStrategy; use crate::testing::{FileSpec, MockedTypeshed, TestCase, TestCaseBuilder}; - use super::list_modules; + fn list_modules(db: &TestDb) -> &[Module<'_>] { + super::list_modules(db, db.resolver_environment()) + } struct ModuleDebugSnapshot<'db> { db: &'db dyn Db, @@ -457,18 +486,18 @@ mod tests { } } - fn sorted_list(db: &dyn Db) -> Vec> { + fn sorted_list(db: &TestDb) -> Vec> { let mut modules = list_modules(db).to_vec(); modules.sort_by(|m1, m2| m1.name(db).cmp(m2.name(db))); modules } - fn list_snapshot(db: &dyn Db) -> Vec> { + fn list_snapshot(db: &TestDb) -> Vec> { list_snapshot_filter(db, |_| true) } fn list_snapshot_filter<'db>( - db: &'db dyn Db, + db: &'db TestDb, predicate: impl Fn(&Module<'db>) -> bool, ) -> Vec> { sorted_list(db) @@ -596,6 +625,20 @@ mod tests { ); } + #[test] + fn ty_extensions_vendored() { + let TestCase { db, .. } = TestCaseBuilder::new().with_vendored_typeshed().build(); + + insta::assert_debug_snapshot!( + list_snapshot_filter(&db, |module| module.name(&db).as_str() == "ty_extensions"), + @r#" + [ + Module::File("ty_extensions", "std-vendored", "stdlib/ty_extensions/__init__.pyi", Package, Some(TyExtensions)), + ] + "#, + ); + } + #[test] fn builtins_custom() { const TYPESHED: MockedTypeshed = MockedTypeshed { @@ -1436,7 +1479,11 @@ not_a_directory assert_function_query_was_not_run( &db, dynamic_resolution_paths, - ModuleResolveModeIngredient::new(&db, ModuleResolveMode::Typing), + ModuleResolveModeIngredient::new( + &db, + db.resolver_environment(), + ModuleResolveMode::Typing, + ), &events, ); } diff --git a/crates/ty_module_resolver/src/module.rs b/crates/ty_module_resolver/src/module.rs index a9f3e9c921..d2fc4d8b73 100644 --- a/crates/ty_module_resolver/src/module.rs +++ b/crates/ty_module_resolver/src/module.rs @@ -5,12 +5,13 @@ use std::str::FromStr; use ruff_db::files::{File, directory_listing, system_path_to_file, vendored_path_to_file}; use ruff_db::system::SystemPath; use ruff_db::vendored::VendoredPath; +use ruff_python_ast::PythonVersion; use salsa::Database; use salsa::plumbing::AsId; -use crate::Db; use crate::module_name::ModuleName; use crate::path::{SearchPath, SystemOrVendoredPathRef}; +use crate::{Db, ResolverEnvironment}; /// Representation of a Python module. #[derive(Clone, Copy, Eq, Hash, PartialEq, salsa::Supertype, salsa::SalsaValue)] @@ -26,18 +27,39 @@ impl get_size2::GetSize for Module<'_> {} impl<'db> Module<'db> { pub(crate) fn file_module( db: &'db dyn Db, + file: File, + resolver_environment: ResolverEnvironment<'db>, name: Cow<'_, ModuleName>, kind: ModuleKind, search_path: SearchPath, - file: File, ) -> Self { let known = KnownModule::try_from_search_path_and_name(&search_path, &name); - Self::File(FileModule::new(db, name, kind, search_path, file, known)) + Self::File(FileModule::new( + db, + name, + kind, + search_path, + file, + resolver_environment, + known, + )) + } + + pub(crate) fn namespace_package( + db: &'db dyn Db, + resolver_environment: ResolverEnvironment<'db>, + name: Cow<'_, ModuleName>, + ) -> Self { + Self::Namespace(NamespacePackage::new(db, resolver_environment, name)) } - pub(crate) fn namespace_package(db: &'db dyn Db, name: Cow<'_, ModuleName>) -> Self { - Self::Namespace(NamespacePackage::new(db, name)) + /// The resolver environment used to resolve this module. + pub fn resolver_environment(self, db: &'db dyn Database) -> ResolverEnvironment<'db> { + match self { + Module::File(module) => module.resolver_environment(db), + Module::Namespace(module) => module.resolver_environment(db), + } } /// The absolute name of the module (e.g. `foo.bar`) @@ -58,6 +80,11 @@ impl<'db> Module<'db> { } } + /// The Python version used to resolve this module. + pub fn python_version(self, db: &'db dyn Database) -> PythonVersion { + self.resolver_environment(db).python_version(db) + } + /// Is this a module that we special-case somehow? If so, which one? pub fn known(self, db: &'db dyn Database) -> Option { match self { @@ -82,6 +109,19 @@ impl<'db> Module<'db> { } } + /// Returns whether this module resolves to a bundled typing-only stub. + /// + /// A project or installed module with the same name may still exist on a + /// lower-priority search path and be available at runtime. + pub fn is_type_check_only(self, db: &'db dyn Database) -> bool { + self.search_path(db) + .is_some_and(SearchPath::is_standard_library) + && matches!( + self.name(db).first_component(), + "_typeshed" | "typing_extensions" | "ty_extensions" + ) + } + /// Determine whether this module is a single-file module or a package pub fn kind(self, db: &'db dyn Database) -> ModuleKind { match self { @@ -175,6 +215,7 @@ fn all_submodule_names_for_package<'db>( path.file_name(), ); + let resolver_environment = module.resolver_environment(db); Some(match path.parent()? { SystemOrVendoredPathRef::System(parent_directory) => { directory_listing(db, parent_directory) @@ -210,10 +251,11 @@ fn all_submodule_names_for_package<'db>( }; Some(Module::file_module( db, + file, + resolver_environment, Cow::Owned(name), kind, module.search_path(db).clone(), - file, )) }) .collect() @@ -247,17 +289,18 @@ fn all_submodule_names_for_package<'db>( }; Some(Module::file_module( db, + file, + resolver_environment, Cow::Owned(name), kind, module.search_path(db).clone(), - file, )) }) .collect(), }) } -/// A module that resolves to a file (`lib.py` or `package/__init__.py`) +/// A module that resolves to a file (`lib.py` or `package/__init__.py`). #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub struct FileModule<'db> { #[returns(ref)] @@ -269,6 +312,8 @@ pub struct FileModule<'db> { #[returns(copy)] pub(super) file: File, #[returns(copy)] + pub(super) resolver_environment: ResolverEnvironment<'db>, + #[returns(copy)] pub(super) known: Option, } @@ -278,6 +323,8 @@ pub struct FileModule<'db> { /// multiple possible paths and they have no corresponding code file. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub struct NamespacePackage<'db> { + #[returns(copy)] + pub(super) resolver_environment: ResolverEnvironment<'db>, #[returns(ref)] pub(super) name: ModuleName, } @@ -450,7 +497,7 @@ impl KnownModule { let known_module = Self::from_str(name.as_str()).ok()?; let is_expected_search_path = if known_module.is_third_party() { - search_path.is_third_party() + search_path.can_contain_third_party_code() } else { search_path.is_standard_library() }; diff --git a/crates/ty_module_resolver/src/module_name.rs b/crates/ty_module_resolver/src/module_name.rs index 6301ff00f8..642b7512e7 100644 --- a/crates/ty_module_resolver/src/module_name.rs +++ b/crates/ty_module_resolver/src/module_name.rs @@ -5,11 +5,12 @@ use std::ops::Deref; use compact_str::{CompactString, ToCompactString}; use ruff_db::files::File; -use ruff_python_ast as ast; +use ruff_python_ast::{self as ast, PythonVersion}; use ruff_python_stdlib::identifiers::is_identifier; use crate::db::Db; use crate::resolve::file_to_module; +use crate::{ResolverEnvironment, ResolverFile}; /// A module name, e.g. `foo.bar`. /// @@ -305,13 +306,12 @@ impl ModuleName { /// Extracts a module name from the AST of a `from import ...` /// statement. /// - /// `importing_file` must be the [`File`] that contains the import - /// statement. + /// `importing_file` must be the file that contains the import statement. /// /// This handles relative import statements. pub fn from_import_statement<'db>( db: &'db dyn Db, - importing_file: File, + importing_file: ImportingFile<'db>, node: &'db ast::StmtImportFrom, ) -> Result { let ast::StmtImportFrom { @@ -327,14 +327,14 @@ impl ModuleName { } /// Computes the absolute module name from the LHS components of `from LHS import RHS` - pub fn from_identifier_parts( - db: &dyn Db, - importing_file: File, + pub fn from_identifier_parts<'db>( + db: &'db dyn Db, + importing_file: ImportingFile<'db>, module: Option<&str>, level: u32, ) -> Result { if let Some(level) = NonZeroU32::new(level) { - relative_module_name(db, importing_file, module, level) + relative_module_name(db, importing_file.resolver_file(db), module, level) } else { module .and_then(Self::new) @@ -345,9 +345,9 @@ impl ModuleName { /// Computes the absolute module name for the package this file belongs to. /// /// i.e. this resolves `.` - pub fn package_for_file( - db: &dyn Db, - importing_file: File, + pub fn package_for_file<'db>( + db: &'db dyn Db, + importing_file: ImportingFile<'db>, ) -> Result { Self::from_identifier_parts(db, importing_file, None, 1) } @@ -469,6 +469,67 @@ impl std::fmt::Display for ModuleName { } } +/// The file from which an import is resolved. +/// +/// Most absolute imports only need the resolver environment. Creating a [`ResolverFile`] for each +/// such import would unnecessarily intern the file and environment together, even though that +/// combined identity is never used: +/// +/// ```text +/// resolve_module(ImportingFile::File(shared.py, environment), "dependency") +/// -> resolve using environment; no ResolverFile needed +/// ``` +/// +/// Relative imports, on the other hand, need the importing file's module identity and therefore +/// require a [`ResolverFile`]: +/// +/// ```text +/// from .dependency import value +/// -> importing_file.resolver_file(db) +/// -> ResolverFile(shared.py, environment) +/// ``` +/// +/// [`ImportingFile::File`] defers interning until such a code path actually calls +/// [`ImportingFile::resolver_file`]. Callers that already have an interned resolver file can pass +/// [`ImportingFile::ResolverFile`] to reuse it directly. +#[derive(Clone, Copy)] +pub enum ImportingFile<'db> { + /// An already-interned resolver key that can be reused without materialization. + ResolverFile(ResolverFile<'db>), + /// An importing file and resolver environment whose combined key is materialized lazily. + File(File, ResolverEnvironment<'db>), +} + +impl<'db> ImportingFile<'db> { + pub fn file(self, db: &dyn Db) -> File { + match self { + Self::ResolverFile(file) => file.file(db), + Self::File(file, _) => file, + } + } + + pub fn resolver_environment(self, db: &'db dyn Db) -> ResolverEnvironment<'db> { + match self { + Self::ResolverFile(file) => file.environment(db), + Self::File(_, resolver_environment) => resolver_environment, + } + } + + pub fn python_version(self, db: &'db dyn Db) -> PythonVersion { + self.resolver_environment(db).python_version(db) + } + + /// Returns the existing resolver key or materializes one when required. + pub fn resolver_file(self, db: &'db dyn Db) -> ResolverFile<'db> { + match self { + Self::ResolverFile(file) => file, + Self::File(file, resolver_environment) => { + ResolverFile::new(db, file, resolver_environment) + } + } + } +} + /// Given a `from .foo import bar` relative import, resolve the relative module /// we're importing `bar` from into an absolute [`ModuleName`] /// using the name of the module we're currently analyzing. @@ -479,9 +540,9 @@ impl std::fmt::Display for ModuleName { /// - `tail` is the relative module name stripped of all leading dots: /// - `from .foo import bar` => `tail == "foo"` /// - `from ..foo.bar import baz` => `tail == "foo.bar"` -fn relative_module_name( - db: &dyn Db, - importing_file: File, +fn relative_module_name<'db>( + db: &'db dyn Db, + importing_file: ResolverFile<'db>, tail: Option<&str>, level: NonZeroU32, ) -> Result { diff --git a/crates/ty_module_resolver/src/path.rs b/crates/ty_module_resolver/src/path.rs index bc5bae94c6..54829ac86d 100644 --- a/crates/ty_module_resolver/src/path.rs +++ b/crates/ty_module_resolver/src/path.rs @@ -13,7 +13,7 @@ use ruff_db::vendored::{VendoredPath, VendoredPathBuf}; use crate::Db; use crate::module_name::ModuleName; use crate::resolve::{PyTyped, ResolverContext}; -use crate::typeshed::{TypeshedVersionsQueryResult, typeshed_versions}; +use crate::typeshed::TypeshedVersionsQueryResult; /// A path that points to a Python module. /// @@ -31,7 +31,7 @@ pub(crate) struct ModulePath { impl ModulePath { #[must_use] - pub(crate) fn is_standard_library(&self) -> bool { + fn is_standard_library(&self) -> bool { matches!( &*self.search_path.0, SearchPathInner::StandardLibraryCustom(_) | SearchPathInner::StandardLibraryVendored(_) @@ -464,13 +464,14 @@ fn query_stdlib_version( let Some(module_name) = stdlib_path_to_module_name(relative_path) else { return TypeshedVersionsQueryResult::DoesNotExist; }; - let ResolverContext { - db, - python_version, - mode: _, - } = context; - - typeshed_versions(*db).query_module(&module_name, *python_version) + context + .resolver_environment + .search_paths(context.db) + .typeshed_versions() + .query_module( + &module_name, + context.resolver_environment.python_version(context.db), + ) } #[derive(Debug, thiserror::Error)] @@ -647,8 +648,26 @@ impl SearchPath { matches!(&*self.0, SearchPathInner::SitePackages(_)) } - /// Is the module on a search path for installed third-party code? - pub fn is_third_party(&self) -> bool { + /// Is it plausible that this search path contains third-party code? + pub fn can_contain_third_party_code(&self) -> bool { + match &*self.0 { + SearchPathInner::SitePackages(_) + | SearchPathInner::Editable(_) + | SearchPathInner::Extra(_) => true, + SearchPathInner::FirstParty(_) + | SearchPathInner::StandardLibraryCustom(_) + | SearchPathInner::StandardLibraryVendored(_) + | SearchPathInner::StandardLibraryReal(_) => false, + } + } + + /// basedpython: did this search path come from *installing* a distribution? + /// + /// This is the narrow half of [`Self::can_contain_third_party_code`]. An extra search path + /// can hold either an installed package or code the project simply keeps elsewhere, so a + /// diagnostic that talks about what a user has installed — telling them to `pip install` a + /// stubs distribution, say — must not fire on one. + pub fn is_installed_distribution(&self) -> bool { match &*self.0 { SearchPathInner::SitePackages(_) | SearchPathInner::Editable(_) => true, SearchPathInner::Extra(_) @@ -741,12 +760,12 @@ impl SearchPath { } #[must_use] - pub fn as_system_path(&self) -> Option<&SystemPath> { + pub(crate) fn as_system_path(&self) -> Option<&SystemPath> { self.as_path().as_system_path() } #[must_use] - pub(crate) fn as_vendored_path(&self) -> Option<&VendoredPath> { + fn as_vendored_path(&self) -> Option<&VendoredPath> { self.as_path().as_vendored_path() } @@ -929,6 +948,7 @@ mod tests { use ruff_db::Db; use ruff_python_ast::PythonVersion; + use crate::ResolverEnvironment; use crate::db::tests::TestDb; use crate::resolve::ModuleResolveMode; use crate::testing::{FileSpec, MockedTypeshed, TestCase, TestCaseBuilder}; @@ -1195,7 +1215,11 @@ mod tests { }; let (db, stdlib_path) = py38_typeshed_test_case(TYPESHED); - let resolver = ResolverContext::new(&db, PythonVersion::PY38, ModuleResolveMode::Typing); + let resolver = ResolverContext::new( + &db, + ResolverEnvironment::new(&db, PythonVersion::PY38, db.search_paths()), + ModuleResolveMode::Typing, + ); let asyncio_regular_package = stdlib_path.join("asyncio"); assert!(asyncio_regular_package.is_directory(&resolver)); @@ -1225,7 +1249,11 @@ mod tests { }; let (db, stdlib_path) = py38_typeshed_test_case(TYPESHED); - let resolver = ResolverContext::new(&db, PythonVersion::PY38, ModuleResolveMode::Typing); + let resolver = ResolverContext::new( + &db, + ResolverEnvironment::new(&db, PythonVersion::PY38, db.search_paths()), + ModuleResolveMode::Typing, + ); let xml_namespace_package = stdlib_path.join("xml"); assert!(xml_namespace_package.is_directory(&resolver)); @@ -1247,7 +1275,11 @@ mod tests { }; let (db, stdlib_path) = py38_typeshed_test_case(TYPESHED); - let resolver = ResolverContext::new(&db, PythonVersion::PY38, ModuleResolveMode::Typing); + let resolver = ResolverContext::new( + &db, + ResolverEnvironment::new(&db, PythonVersion::PY38, db.search_paths()), + ModuleResolveMode::Typing, + ); let functools_module = stdlib_path.join("functools.pyi"); assert!(functools_module.to_file(&resolver).is_some()); @@ -1263,7 +1295,11 @@ mod tests { }; let (db, stdlib_path) = py38_typeshed_test_case(TYPESHED); - let resolver = ResolverContext::new(&db, PythonVersion::PY38, ModuleResolveMode::Typing); + let resolver = ResolverContext::new( + &db, + ResolverEnvironment::new(&db, PythonVersion::PY38, db.search_paths()), + ModuleResolveMode::Typing, + ); let collections_regular_package = stdlib_path.join("collections"); assert_eq!(collections_regular_package.to_file(&resolver), None); @@ -1279,7 +1315,11 @@ mod tests { }; let (db, stdlib_path) = py38_typeshed_test_case(TYPESHED); - let resolver = ResolverContext::new(&db, PythonVersion::PY38, ModuleResolveMode::Typing); + let resolver = ResolverContext::new( + &db, + ResolverEnvironment::new(&db, PythonVersion::PY38, db.search_paths()), + ModuleResolveMode::Typing, + ); let importlib_namespace_package = stdlib_path.join("importlib"); assert_eq!(importlib_namespace_package.to_file(&resolver), None); @@ -1300,7 +1340,11 @@ mod tests { }; let (db, stdlib_path) = py38_typeshed_test_case(TYPESHED); - let resolver = ResolverContext::new(&db, PythonVersion::PY38, ModuleResolveMode::Typing); + let resolver = ResolverContext::new( + &db, + ResolverEnvironment::new(&db, PythonVersion::PY38, db.search_paths()), + ModuleResolveMode::Typing, + ); let non_existent = stdlib_path.join("doesnt_even_exist"); assert_eq!(non_existent.to_file(&resolver), None); @@ -1328,7 +1372,11 @@ mod tests { }; let (db, stdlib_path) = py39_typeshed_test_case(TYPESHED); - let resolver = ResolverContext::new(&db, PythonVersion::PY39, ModuleResolveMode::Typing); + let resolver = ResolverContext::new( + &db, + ResolverEnvironment::new(&db, PythonVersion::PY39, db.search_paths()), + ModuleResolveMode::Typing, + ); // Since we've set the target version to Py39, // `collections` should now exist as a directory, according to VERSIONS... @@ -1359,7 +1407,11 @@ mod tests { }; let (db, stdlib_path) = py39_typeshed_test_case(TYPESHED); - let resolver = ResolverContext::new(&db, PythonVersion::PY39, ModuleResolveMode::Typing); + let resolver = ResolverContext::new( + &db, + ResolverEnvironment::new(&db, PythonVersion::PY39, db.search_paths()), + ModuleResolveMode::Typing, + ); // The `importlib` directory now also exists let importlib_namespace_package = stdlib_path.join("importlib"); @@ -1383,7 +1435,11 @@ mod tests { }; let (db, stdlib_path) = py39_typeshed_test_case(TYPESHED); - let resolver = ResolverContext::new(&db, PythonVersion::PY39, ModuleResolveMode::Typing); + let resolver = ResolverContext::new( + &db, + ResolverEnvironment::new(&db, PythonVersion::PY39, db.search_paths()), + ModuleResolveMode::Typing, + ); // The `xml` package no longer exists on py39: let xml_namespace_package = stdlib_path.join("xml"); diff --git a/crates/ty_module_resolver/src/resolve.rs b/crates/ty_module_resolver/src/resolve.rs index aa2f9c67ff..60ccf9ef57 100644 --- a/crates/ty_module_resolver/src/resolve.rs +++ b/crates/ty_module_resolver/src/resolve.rs @@ -32,38 +32,44 @@ specifies ty's implementation of Python's import resolution algorithm. */ use std::borrow::Cow; -use std::fmt; use std::iter::FusedIterator; use rustc_hash::{FxBuildHasher, FxHashSet}; +use ruff_db::PythonFile; use ruff_db::files::{File, FilePath, FileRootKind, directory_listing, system_path_to_file}; use ruff_db::source::source_text; use ruff_db::system::{System, SystemPath, SystemPathBuf}; use ruff_db::vendored::VendoredFileSystem; use ruff_python_ast::{ - self as ast, PythonVersion, + self as ast, visitor::{Visitor, walk_body}, }; use crate::db::Db; use crate::module::{Module, ModuleKind}; -use crate::module_name::ModuleName; +use crate::module_name::{ImportingFile, ModuleName}; use crate::path::{ModulePath, SearchPath, SystemOrVendoredPathRef}; use crate::strategy::MisconfigurationStrategy; use crate::typeshed::{TypeshedVersions, vendored_typeshed_versions}; -use crate::{SearchPathSettings, SearchPathSettingsError}; +use crate::{ResolverEnvironment, ResolverFile, SearchPathSettings, SearchPathSettingsError}; /// Resolves a module name to a module. pub fn resolve_module<'db>( db: &'db dyn Db, - importing_file: File, + importing_file: ImportingFile<'db>, module_name: &ModuleName, ) -> Option> { - let interned_name = ModuleNameIngredient::new(db, module_name, ModuleResolveMode::Typing); + let resolver_environment = importing_file.resolver_environment(db); + let interned_name = ModuleNameIngredient::new( + db, + module_name, + ModuleResolveMode::Typing, + resolver_environment, + ); resolve_module_query(db, interned_name) - .or_else(|| desperately_resolve_module(db, importing_file, interned_name)) + .or_else(|| desperately_resolve_module(db, importing_file.file(db), interned_name)) } /// Resolves a module name to a module, without desperate resolution available. @@ -72,9 +78,15 @@ pub fn resolve_module<'db>( /// we don't have a well-defined importing file. pub fn resolve_module_confident<'db>( db: &'db dyn Db, + resolver_environment: ResolverEnvironment<'db>, module_name: &ModuleName, ) -> Option> { - let interned_name = ModuleNameIngredient::new(db, module_name, ModuleResolveMode::Typing); + let interned_name = ModuleNameIngredient::new( + db, + module_name, + ModuleResolveMode::Typing, + resolver_environment, + ); resolve_module_query(db, interned_name) } @@ -82,13 +94,19 @@ pub fn resolve_module_confident<'db>( /// Resolves a module name to a module (stubs not allowed). pub fn resolve_real_module<'db>( db: &'db dyn Db, - importing_file: File, + importing_file: ImportingFile<'db>, module_name: &ModuleName, ) -> Option> { - let interned_name = ModuleNameIngredient::new(db, module_name, ModuleResolveMode::Runtime); + let resolver_environment = importing_file.resolver_environment(db); + let interned_name = ModuleNameIngredient::new( + db, + module_name, + ModuleResolveMode::Runtime, + resolver_environment, + ); resolve_module_query(db, interned_name) - .or_else(|| desperately_resolve_module(db, importing_file, interned_name)) + .or_else(|| desperately_resolve_module(db, importing_file.file(db), interned_name)) } /// Resolves a module name to a module, without desperate resolution available (stubs not allowed). @@ -97,9 +115,15 @@ pub fn resolve_real_module<'db>( /// we don't have a well-defined importing file. pub fn resolve_real_module_confident<'db>( db: &'db dyn Db, + resolver_environment: ResolverEnvironment<'db>, module_name: &ModuleName, ) -> Option> { - let interned_name = ModuleNameIngredient::new(db, module_name, ModuleResolveMode::Runtime); + let interned_name = ModuleNameIngredient::new( + db, + module_name, + ModuleResolveMode::Runtime, + resolver_environment, + ); resolve_module_query(db, interned_name) } @@ -117,17 +141,19 @@ pub fn resolve_real_module_confident<'db>( /// are involved in an import cycle with `builtins`. pub fn resolve_real_shadowable_module<'db>( db: &'db dyn Db, - importing_file: File, + importing_file: ImportingFile<'db>, module_name: &ModuleName, ) -> Option> { + let resolver_environment = importing_file.resolver_environment(db); let interned_name = ModuleNameIngredient::new( db, module_name, ModuleResolveMode::RuntimeSomeShadowingAllowed, + resolver_environment, ); resolve_module_query(db, interned_name) - .or_else(|| desperately_resolve_module(db, importing_file, interned_name)) + .or_else(|| desperately_resolve_module(db, importing_file.file(db), interned_name)) } /// Selects typing or runtime module-resolution semantics. @@ -158,6 +184,8 @@ pub enum ModuleResolveMode { #[salsa::interned(heap_size=ruff_memory_usage::heap_size)] #[derive(Debug)] pub(crate) struct ModuleResolveModeIngredient<'db> { + #[returns(copy)] + resolver_environment: ResolverEnvironment<'db>, #[returns(copy)] mode: ModuleResolveMode, } @@ -212,9 +240,10 @@ fn resolve_module_query<'db>( ) -> Option> { let name = module_name.name(db); let mode = module_name.mode(db); + let resolver_environment = module_name.resolver_environment(db); let _span = tracing::trace_span!("resolve_module", %name).entered(); - let Some(resolved) = resolve_name(db, name, mode) else { + let Some(resolved) = resolve_name(db, resolver_environment, name, mode) else { tracing::debug!("Module `{name}` not found in search paths"); return None; }; @@ -222,7 +251,7 @@ fn resolve_module_query<'db>( resolved .into_iter() .next() - .map(|candidate| candidate.into_module(db, name)) + .map(|candidate| candidate.into_module(db, resolver_environment, name)) } /// Like `resolve_module_query` but for cases where it failed to resolve the module @@ -245,9 +274,12 @@ fn desperately_resolve_module<'db>( ) -> Option> { let name = module_name.name(db); let mode = module_name.mode(db); + let resolver_environment = module_name.resolver_environment(db); let _span = tracing::trace_span!("desperately_resolve_module", %name).entered(); - let Some(resolved) = desperately_resolve_name(db, importing_file, name, mode) else { + let Some(resolved) = + desperately_resolve_name(db, importing_file, resolver_environment, name, mode) + else { let mode = match mode { ModuleResolveMode::Typing => "typing mode", ModuleResolveMode::Runtime => "runtime mode", @@ -262,14 +294,18 @@ fn desperately_resolve_module<'db>( resolved .into_iter() .next() - .map(|candidate| candidate.into_module(db, name)) + .map(|candidate| candidate.into_module(db, resolver_environment, name)) } /// 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>(db: &'db dyn Db, path: &FilePath) -> Option> { +pub(crate) fn path_to_module<'db>( + db: &'db dyn Db, + resolver_environment: ResolverEnvironment<'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. @@ -279,7 +315,7 @@ pub(crate) fn path_to_module<'db>(db: &'db dyn Db, path: &FilePath) -> Option(db: &'db dyn Db, path: &FilePath) -> Option Option> { +pub fn file_to_module<'db>( + db: &'db dyn Db, + resolver_file: ResolverFile<'db>, +) -> Option> { + let resolver_environment = resolver_file.environment(db); + let file = resolver_file.file(db); let _span = tracing::trace_span!("file_to_module", ?file).entered(); let path = SystemOrVendoredPathRef::try_from_file(db, file)?; - file_to_module_impl(db, file, path, search_paths(db, ModuleResolveMode::Typing)).or_else(|| { + file_to_module_impl( + db, + resolver_file, + path, + search_paths(db, resolver_environment, ModuleResolveMode::Typing), + ) + .or_else(|| { file_to_module_impl( db, - file, + resolver_file, path, - relative_desperate_search_paths(db, file).iter(), + relative_desperate_search_paths(db, resolver_file).iter(), ) }) } fn file_to_module_impl<'db, 'a>( db: &'db dyn Db, - file: File, + resolver_file: ResolverFile<'db>, path: SystemOrVendoredPathRef<'a>, mut search_paths: impl Iterator, ) -> Option> { @@ -324,9 +371,10 @@ fn file_to_module_impl<'db, 'a>( // 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, file, &module_name)?; + let module = resolve_module(db, ImportingFile::ResolverFile(resolver_file), &module_name)?; let module_file = module.file(db)?; + let file: File = resolver_file.file(db); let file_path = file.path(db); if file_path == module_file.path(db) { return Some(module); @@ -334,7 +382,8 @@ fn file_to_module_impl<'db, 'a>( // If a .py and .pyi are both defined, the .pyi will be the one returned by `resolve_module().file`, // which would make us erroneously believe the `.py` is *not* also this module (breaking things // like relative imports). So here we try `resolve_real_module().file` to cover both cases. - let module = resolve_real_module(db, file, &module_name)?; + let module = + resolve_real_module(db, ImportingFile::ResolverFile(resolver_file), &module_name)?; let module_file = module.file(db)?; if file_path == module_file.path(db) { return Some(module); @@ -350,8 +399,20 @@ fn file_to_module_impl<'db, 'a>( None } -pub fn search_paths(db: &dyn Db, resolve_mode: ModuleResolveMode) -> SearchPathIterator<'_> { - db.search_paths().iter(db, resolve_mode) +pub fn search_paths<'db>( + db: &'db dyn Db, + resolver_environment: ResolverEnvironment<'db>, + resolve_mode: ModuleResolveMode, +) -> SearchPathIterator<'db> { + let search_paths = resolver_environment.search_paths(db); + + SearchPathIterator { + db, + static_paths: search_paths.static_paths.iter(), + stdlib_path: search_paths.stdlib(resolve_mode), + dynamic_paths: None, + mode: ModuleResolveModeIngredient::new(db, resolver_environment, resolve_mode), + } } #[derive(Debug, Clone, Copy, Default)] @@ -440,8 +501,14 @@ impl StubPackageIndex { /// Returns an index of search paths that may contain a top-level stub package, preserving their /// resolution order relative to stdlib. #[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)] -fn stub_package_index(db: &dyn Db) -> StubPackageIndex { - StubPackageIndex::from_search_paths(db, search_paths(db, ModuleResolveMode::Typing)) +fn stub_package_index( + db: &dyn Db, + resolver_environment: ResolverEnvironment<'_>, +) -> StubPackageIndex { + StubPackageIndex::from_search_paths( + db, + search_paths(db, resolver_environment, ModuleResolveMode::Typing), + ) } fn search_path_may_contain_stub_package(db: &dyn Db, search_path: &SearchPath) -> bool { @@ -463,21 +530,26 @@ fn search_path_may_contain_stub_package(db: &dyn Db, search_path: &SearchPath) - /// /// We exclude `__init__.py(i)` dirs to avoid truncating packages. #[salsa::tracked(returns(as_deref), heap_size=ruff_memory_usage::heap_size)] -fn absolute_desperate_search_paths(db: &dyn Db, importing_file: File) -> Option> { +fn absolute_desperate_search_paths( + db: &dyn Db, + importing_file: ResolverFile<'_>, +) -> Option> { + let resolver_environment = importing_file.environment(db); + let importing_file = importing_file.file(db); let system = db.system(); let importing_path = importing_file.path(db).as_system_path()?; // Only allow this if the importing_file is under the first-party search path - let (base_path, rel_path) = - search_paths(db, ModuleResolveMode::Typing).find_map(|search_path| { - if !search_path.is_first_party() { - return None; - } - Some(( - search_path.as_system_path()?, - search_path.relativize_system_path_only(importing_path)?, - )) - })?; + let (base_path, rel_path) = search_paths(db, resolver_environment, ModuleResolveMode::Typing) + .find_map(|search_path| { + if !search_path.is_first_party() { + return None; + } + Some(( + search_path.as_system_path()?, + search_path.relativize_system_path_only(importing_path)?, + )) + })?; // Only allow searching up to the first-party path's root let mut search_paths = Vec::new(); @@ -529,21 +601,26 @@ fn absolute_desperate_search_paths(db: &dyn Db, importing_file: File) -> Option< /// chaotic things. In particular, all files under a given pyproject.toml will currently /// agree on this being their desperate search-path, which is really nice. #[salsa::tracked(returns(clone), heap_size=ruff_memory_usage::heap_size)] -fn relative_desperate_search_paths(db: &dyn Db, importing_file: File) -> Option { +fn relative_desperate_search_paths( + db: &dyn Db, + importing_file: ResolverFile<'_>, +) -> Option { + let resolver_environment = importing_file.environment(db); + let importing_file = importing_file.file(db); let system = db.system(); let importing_path = importing_file.path(db).as_system_path()?; // Only allow this if the importing_file is under the first-party search path - let (base_path, rel_path) = - search_paths(db, ModuleResolveMode::Typing).find_map(|search_path| { - if !search_path.is_first_party() { - return None; - } - Some(( - search_path.as_system_path()?, - search_path.relativize_system_path_only(importing_path)?, - )) - })?; + let (base_path, rel_path) = search_paths(db, resolver_environment, ModuleResolveMode::Typing) + .find_map(|search_path| { + if !search_path.is_first_party() { + return None; + } + Some(( + search_path.as_system_path()?, + search_path.relativize_system_path_only(importing_path)?, + )) + })?; // Only allow searching up to the first-party path's root for rel_dir in rel_path.ancestors() { @@ -562,7 +639,7 @@ fn relative_desperate_search_paths(db: &dyn Db, importing_file: File) -> Option< None } -#[derive(Clone, Debug, PartialEq, Eq, get_size2::GetSize)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, get_size2::GetSize)] pub struct SearchPaths { /// Search paths that have been statically determined purely from reading /// ty's configuration settings. These shouldn't ever change unless the @@ -596,7 +673,7 @@ impl SearchPaths { /// 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 fn from_settings( + pub(crate) fn from_settings( settings: &SearchPathSettings, system: &dyn System, vendored: &VendoredFileSystem, @@ -759,7 +836,7 @@ impl SearchPaths { /// Returns a new `SearchPaths` with no search paths configured. /// - /// This is primarily useful for testing. + /// The vendored standard library remains available. pub fn empty(vendored: &VendoredFileSystem) -> Self { Self { static_paths: vec![], @@ -801,22 +878,7 @@ impl SearchPaths { } } - pub(super) fn iter<'a>( - &'a self, - db: &'a dyn Db, - mode: ModuleResolveMode, - ) -> SearchPathIterator<'a> { - let stdlib_path = self.stdlib(mode); - SearchPathIterator { - db, - static_paths: self.static_paths.iter(), - stdlib_path, - dynamic_paths: None, - mode: ModuleResolveModeIngredient::new(db, mode), - } - } - - pub(crate) fn stdlib(&self, mode: ModuleResolveMode) -> Option<&SearchPath> { + fn stdlib(&self, mode: ModuleResolveMode) -> Option<&SearchPath> { match mode { ModuleResolveMode::Typing => self.stdlib_path.as_ref(), ModuleResolveMode::Runtime | ModuleResolveMode::RuntimeSomeShadowingAllowed => { @@ -825,18 +887,6 @@ impl SearchPaths { } } - pub fn display<'a>( - &'a self, - db: &'a dyn Db, - mode: ModuleResolveMode, - ) -> DisplaySearchPaths<'a> { - DisplaySearchPaths { - search_paths: self, - db, - mode, - } - } - pub fn custom_stdlib(&self) -> Option<&SystemPath> { self.stdlib_path .as_ref() @@ -848,28 +898,6 @@ impl SearchPaths { } } -pub struct DisplaySearchPaths<'a> { - search_paths: &'a SearchPaths, - db: &'a dyn Db, - mode: ModuleResolveMode, -} - -impl fmt::Display for DisplaySearchPaths<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut paths = self.search_paths.iter(self.db, self.mode).peekable(); - - if paths.peek().is_none() { - return f.write_str("[]"); - } - - writeln!(f, "[")?; - for path in paths { - writeln!(f, " {path},")?; - } - f.write_str("]") - } -} - /// 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 @@ -891,7 +919,7 @@ pub(crate) fn dynamic_resolution_paths<'db>( site_packages, typeshed_versions: _, real_stdlib_path, - } = db.search_paths(); + } = mode.resolver_environment(db).search_paths(db); let mut dynamic_paths = Vec::new(); @@ -1060,7 +1088,8 @@ impl<'db> Iterator for SearchPathIterator<'db> { impl FusedIterator for SearchPathIterator<'_> {} -/// A thin wrapper around `ModuleName` to make it a Salsa ingredient. +/// A thin wrapper around a module name, resolution mode, and resolver environment to make them a Salsa +/// ingredient. /// /// This is needed because Salsa requires that all query arguments are salsa ingredients. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] @@ -1069,17 +1098,26 @@ struct ModuleNameIngredient<'db> { pub(super) name: ModuleName, #[returns(copy)] pub(super) mode: ModuleResolveMode, + #[returns(copy)] + pub(super) resolver_environment: ResolverEnvironment<'db>, } /// 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, mode: ModuleResolveMode) -> Option { - let resolver = NameResolver::new(db, name, mode); +fn resolve_name<'db>( + db: &'db dyn Db, + resolver_environment: ResolverEnvironment<'db>, + name: &ModuleName, + mode: ModuleResolveMode, +) -> Option { + let resolver = NameResolver::new(db, resolver_environment, name, mode); match mode { - ModuleResolveMode::Typing => resolver.resolve_typing(stub_package_index(db)), + ModuleResolveMode::Typing => { + resolver.resolve_typing(stub_package_index(db, resolver_environment)) + } ModuleResolveMode::Runtime | ModuleResolveMode::RuntimeSomeShadowingAllowed => { - resolver.resolve_runtime(search_paths(db, mode)) + resolver.resolve_runtime(search_paths(db, resolver_environment, mode)) } } } @@ -1088,14 +1126,16 @@ fn resolve_name(db: &dyn Db, name: &ModuleName, mode: ModuleResolveMode) -> Opti /// and we are now Getting Desperate and willing to try the ancestor directories of /// the `importing_file` as potential temporary search paths that are private /// to this import. -fn desperately_resolve_name( - db: &dyn Db, +fn desperately_resolve_name<'db>( + db: &'db dyn Db, importing_file: File, + resolver_environment: ResolverEnvironment<'db>, name: &ModuleName, mode: ModuleResolveMode, ) -> Option { + let importing_file = ResolverFile::new(db, importing_file, resolver_environment); let search_paths = absolute_desperate_search_paths(db, importing_file).unwrap_or_default(); - let resolver = NameResolver::new(db, name, mode); + let resolver = NameResolver::new(db, resolver_environment, name, mode); match mode { ModuleResolveMode::Typing => resolver.resolve_desperate_typing(search_paths), @@ -1179,11 +1219,16 @@ impl ModuleResolutionCandidate { } // This is the module we were actually interested in resolving, complete the resolution - fn into_module<'db>(self, db: &'db dyn Db, name: &ModuleName) -> Module<'db> { + fn into_module<'db>( + self, + db: &'db dyn Db, + resolver_environment: ResolverEnvironment<'db>, + name: &ModuleName, + ) -> Module<'db> { match self.module { ResolvedModule::NamespacePackage => { tracing::trace!("Resolve namespace package `{name}`"); - Module::namespace_package(db, Cow::Borrowed(name)) + Module::namespace_package(db, resolver_environment, Cow::Borrowed(name)) } ResolvedModule::LegacyNamespacePackage(file) => { // legacy namespace packages behave like regular packages @@ -1194,10 +1239,11 @@ impl ModuleResolutionCandidate { ); Module::file_module( db, + file, + resolver_environment, Cow::Borrowed(name), ModuleKind::Package, self.path.into_search_path(), - file, ) } ResolvedModule::RegularPackage(file) => { @@ -1207,20 +1253,22 @@ impl ModuleResolutionCandidate { ); Module::file_module( db, + file, + resolver_environment, Cow::Borrowed(name), ModuleKind::Package, self.path.into_search_path(), - file, ) } ResolvedModule::Module(file) => { tracing::trace!("Resolved module `{name}` to `{path}`", path = file.path(db)); Module::file_module( db, + file, + resolver_environment, Cow::Borrowed(name), ModuleKind::Module, self.path.into_search_path(), - file, ) } } @@ -1260,10 +1308,15 @@ struct NameResolver<'db, 'name> { } impl<'db, 'name> NameResolver<'db, 'name> { - fn new(db: &'db dyn Db, name: &'name ModuleName, mode: ModuleResolveMode) -> Self { - let python_version = db.python_version(); + fn new( + db: &'db dyn Db, + resolver_environment: ResolverEnvironment<'db>, + name: &'name ModuleName, + mode: ModuleResolveMode, + ) -> Self { + let python_version = resolver_environment.python_version(db); Self { - context: ResolverContext::new(db, python_version, mode), + context: ResolverContext::new(db, resolver_environment, mode), name, is_non_shadowable: mode.is_non_shadowable(python_version.minor, name.as_str()), } @@ -1275,11 +1328,13 @@ impl<'db, 'name> NameResolver<'db, 'name> { /// a fallback when no stub provides the requested module. A stub overlay may use runtime /// packages as parents, but its final module must come from a stub file. fn resolve_typing(&self, stub_packages: &StubPackageIndex) -> Option { - let search_paths = self.context.db.search_paths(); - if self.name.components().nth(1).is_none() { let candidates = self.discover_roots( - search_paths.iter(self.context.db, ModuleResolveMode::Typing), + search_paths( + self.context.db, + self.context.resolver_environment, + ModuleResolveMode::Typing, + ), stub_packages.all(), ); return self.resolve_remaining(candidates, ComponentFileFilter::ByMode); @@ -1290,9 +1345,12 @@ impl<'db, 'name> NameResolver<'db, 'name> { // normal fallback so that each extra path is probed only once. let (overlay_stub_packages, remaining_stub_packages) = stub_packages.split_overlay(); let mut candidates = self.discover_roots( - search_paths - .iter(self.context.db, ModuleResolveMode::Typing) - .take_while(|search_path| search_path.is_extra()), + search_paths( + self.context.db, + self.context.resolver_environment, + ModuleResolveMode::Typing, + ) + .take_while(|search_path| search_path.is_extra()), overlay_stub_packages, ); if let Some(resolved) = @@ -1302,9 +1360,12 @@ impl<'db, 'name> NameResolver<'db, 'name> { } let remaining_candidates = self.discover_roots( - search_paths - .iter(self.context.db, ModuleResolveMode::Typing) - .skip_while(|search_path| search_path.is_extra()), + search_paths( + self.context.db, + self.context.resolver_environment, + ModuleResolveMode::Typing, + ) + .skip_while(|search_path| search_path.is_extra()), remaining_stub_packages, ); candidates.extend(remaining_candidates); @@ -1721,7 +1782,14 @@ fn is_legacy_namespace_package( // // The downside is if you write slightly different syntax we will fail to detect the idiom, // but hey, this is better than nothing! - let parsed = ruff_db::parsed::parsed_module(context.db, init); + let parsed = ruff_db::parsed::parsed_module( + context.db, + PythonFile::new( + context.db, + init, + context.resolver_environment.python_version(context.db), + ), + ); let mut visitor = LegacyNamespacePackageVisitor::default(); visitor.visit_body(parsed.load(context.db).suite()); @@ -1756,19 +1824,19 @@ impl PyTyped { pub(super) struct ResolverContext<'db> { pub(super) db: &'db dyn Db, - pub(super) python_version: PythonVersion, + pub(super) resolver_environment: ResolverEnvironment<'db>, pub(super) mode: ModuleResolveMode, } impl<'db> ResolverContext<'db> { pub(super) fn new( db: &'db dyn Db, - python_version: PythonVersion, + resolver_environment: ResolverEnvironment<'db>, mode: ModuleResolveMode, ) -> Self { Self { db, - python_version, + resolver_environment, mode, } } @@ -1995,6 +2063,24 @@ mod tests { use super::*; + fn resolve_module_confident<'db>( + db: &'db TestDb, + module_name: &ModuleName, + ) -> Option> { + super::resolve_module_confident(db, db.resolver_environment(), module_name) + } + + fn resolve_real_module_confident<'db>( + db: &'db TestDb, + module_name: &ModuleName, + ) -> Option> { + super::resolve_real_module_confident(db, db.resolver_environment(), module_name) + } + + fn path_to_module<'db>(db: &'db TestDb, path: &FilePath) -> Option> { + super::path_to_module(db, db.resolver_environment(), path) + } + #[test] fn first_party_module() { let TestCase { db, src, .. } = TestCaseBuilder::new() @@ -2067,8 +2153,12 @@ mod tests { .build(); let importing_file = system_path_to_file(&db, src.join("nested/main.py")).unwrap(); - let foo = - resolve_module(&db, importing_file, &ModuleName::new_static("foo").unwrap()).unwrap(); + let foo = resolve_module( + &db, + ImportingFile::File(importing_file, db.resolver_environment()), + &ModuleName::new_static("foo").unwrap(), + ) + .unwrap(); assert_eq!( foo.file(&db).unwrap().path(&db), &src.join("nested/foo-stubs/__init__.pyi") @@ -2265,6 +2355,105 @@ mod tests { .collect() } + #[test] + fn resolve_module_uses_resolver_environment_python_version() { + const TYPESHED: MockedTypeshed = MockedTypeshed { + stdlib_files: &[("_sha256.pyi", ""), ("py312_only.pyi", "")], + versions: "_sha256: 3.11-\npy312_only: 3.12-", + }; + + let TestCase { + db, src, stdlib, .. + } = TestCaseBuilder::new() + .with_src_files(&[ + ("main.py", ""), + ("_sha256.py", ""), + ("namespace/module.py", ""), + ]) + .with_mocked_typeshed(TYPESHED) + .with_python_version(PythonVersion::PY311) + .build(); + let importing_file = system_path_to_file(&db, src.join("main.py")).unwrap(); + let py311 = ResolverEnvironment::new(&db, PythonVersion::PY311, db.search_paths()); + let py312 = ResolverEnvironment::new(&db, PythonVersion::PY312, db.search_paths()); + let sha256 = ModuleName::new_static("_sha256").unwrap(); + let py311_module = + resolve_module(&db, ImportingFile::File(importing_file, py311), &sha256).unwrap(); + let py312_module = + resolve_module(&db, ImportingFile::File(importing_file, py312), &sha256).unwrap(); + assert_eq!( + py311_module.file(&db).unwrap().path(&db), + &stdlib.join("_sha256.pyi") + ); + assert_eq!( + py312_module.file(&db).unwrap().path(&db), + &src.join("_sha256.py") + ); + assert_eq!(py311_module.python_version(&db), PythonVersion::PY311); + assert_eq!(py312_module.python_version(&db), PythonVersion::PY312); + + let namespace = ModuleName::new_static("namespace").unwrap(); + let py311_namespace = + resolve_module(&db, ImportingFile::File(importing_file, py311), &namespace).unwrap(); + let py312_namespace = + resolve_module(&db, ImportingFile::File(importing_file, py312), &namespace).unwrap(); + assert!(matches!(py311_namespace, Module::Namespace(_))); + assert!(matches!(py312_namespace, Module::Namespace(_))); + assert_eq!(py311_namespace.python_version(&db), PythonVersion::PY311); + assert_eq!(py312_namespace.python_version(&db), PythonVersion::PY312); + assert_ne!(py311_namespace, py312_namespace); + + let py312_only = ModuleName::new_static("py312_only").unwrap(); + assert!( + resolve_module(&db, ImportingFile::File(importing_file, py311), &py312_only).is_none() + ); + assert_eq!( + resolve_module(&db, ImportingFile::File(importing_file, py312), &py312_only) + .and_then(|module| module.file(&db)) + .unwrap() + .path(&db), + &stdlib.join("py312_only.pyi") + ); + } + + #[test] + fn resolve_module_uses_resolver_environment_search_paths() { + let TestCase { mut db, src, .. } = TestCaseBuilder::new() + .with_src_files(&[("main.py", ""), ("shared.py", "from_src = True")]) + .with_vendored_typeshed() + .build(); + db.write_file("/alternate/shared.py", "from_alternate = True") + .unwrap(); + + let alternate_paths = SearchPathSettings { + src_roots: vec![SystemPathBuf::from("/alternate")], + ..SearchPathSettings::empty() + } + .to_search_paths(db.system(), db.vendored(), &FallibleStrategy) + .unwrap(); + alternate_paths.try_register_static_roots(&db); + + let primary = db.resolver_environment(); + let alternate = ResolverEnvironment::new(&db, PythonVersion::default(), &alternate_paths); + let importing_file = system_path_to_file(&db, src.join("main.py")).unwrap(); + let name = ModuleName::new_static("shared").unwrap(); + + let primary_module = + resolve_module(&db, ImportingFile::File(importing_file, primary), &name).unwrap(); + let alternate_module = + resolve_module(&db, ImportingFile::File(importing_file, alternate), &name).unwrap(); + + assert_eq!( + primary_module.file(&db).unwrap().path(&db), + &src.join("shared.py") + ); + assert_eq!( + alternate_module.file(&db).unwrap().path(&db), + &SystemPathBuf::from("/alternate/shared.py") + ); + assert_ne!(primary_module, alternate_module); + } + #[test] fn stdlib_resolution_respects_versions_file_py38_existing_modules() { const VERSIONS: &str = "\ @@ -2797,7 +2986,12 @@ mod tests { assert_function_query_was_not_run( &db, resolve_module_query, - ModuleNameIngredient::new(&db, functools_module_name, ModuleResolveMode::Typing), + ModuleNameIngredient::new( + &db, + functools_module_name, + ModuleResolveMode::Typing, + db.resolver_environment(), + ), &events, ); assert_eq!(&functools_search_path, &stdlib); @@ -3055,7 +3249,11 @@ not_a_directory assert_function_query_was_not_run( &db, dynamic_resolution_paths, - ModuleResolveModeIngredient::new(&db, ModuleResolveMode::Typing), + ModuleResolveModeIngredient::new( + &db, + db.resolver_environment(), + ModuleResolveMode::Typing, + ), &events, ); } @@ -3074,7 +3272,11 @@ not_a_directory dynamic_resolution_paths( &db, - ModuleResolveModeIngredient::new(&db, ModuleResolveMode::Typing), + ModuleResolveModeIngredient::new( + &db, + db.resolver_environment(), + ModuleResolveMode::Typing, + ), ); db.clear_salsa_events(); @@ -3082,14 +3284,22 @@ not_a_directory .unwrap(); dynamic_resolution_paths( &db, - ModuleResolveModeIngredient::new(&db, ModuleResolveMode::Typing), + ModuleResolveModeIngredient::new( + &db, + db.resolver_environment(), + ModuleResolveMode::Typing, + ), ); let events = db.take_salsa_events(); assert_function_query_was_not_run( &db, dynamic_resolution_paths, - ModuleResolveModeIngredient::new(&db, ModuleResolveMode::Typing), + ModuleResolveModeIngredient::new( + &db, + db.resolver_environment(), + ModuleResolveMode::Typing, + ), &events, ); } @@ -3186,7 +3396,8 @@ not_a_directory .with_site_packages_files(&[("_foo.pth", "/src")]) .build(); - let search_paths: Vec<&SearchPath> = search_paths(&db, ModuleResolveMode::Typing).collect(); + let search_paths: Vec<&SearchPath> = + search_paths(&db, db.resolver_environment(), ModuleResolveMode::Typing).collect(); assert!(search_paths.contains( &&SearchPath::first_party(db.system(), SystemPathBuf::from("/src")).unwrap() @@ -3332,7 +3543,11 @@ not_a_directory db.set_search_paths(search_paths); let foo_module_file = File::new(&db, FilePath::from(installed_foo_module)); - let module = file_to_module(&db, foo_module_file).unwrap(); + let module = file_to_module( + &db, + ResolverFile::new(&db, foo_module_file, db.resolver_environment()), + ) + .unwrap(); assert_eq!(module.search_path(&db).unwrap(), &site_packages); } } diff --git a/crates/ty_module_resolver/src/testing.rs b/crates/ty_module_resolver/src/testing.rs index b3af169a31..199821f4c7 100644 --- a/crates/ty_module_resolver/src/testing.rs +++ b/crates/ty_module_resolver/src/testing.rs @@ -113,6 +113,16 @@ pub(crate) struct TestCaseBuilder { } impl TestCaseBuilder { + fn with_typeshed(self, typeshed_option: U) -> TestCaseBuilder { + TestCaseBuilder { + typeshed_option, + python_version: self.python_version, + first_party_files: self.first_party_files, + site_packages_files: self.site_packages_files, + roots: self.roots, + } + } + /// Specify files to be created in the `src` mock directory pub(crate) fn with_src_files(mut self, files: &[FileSpec]) -> Self { self.first_party_files.extend(files.iter().copied()); @@ -168,20 +178,7 @@ impl TestCaseBuilder { /// Use the vendored stdlib stubs included in the Ruff binary for this test case pub(crate) fn with_vendored_typeshed(self) -> TestCaseBuilder { - let TestCaseBuilder { - typeshed_option: _, - python_version, - first_party_files, - site_packages_files, - roots, - } = self; - TestCaseBuilder { - typeshed_option: VendoredTypeshed, - python_version, - first_party_files, - site_packages_files, - roots, - } + self.with_typeshed(VendoredTypeshed) } /// Use a mock typeshed directory for this test case @@ -189,21 +186,7 @@ impl TestCaseBuilder { self, typeshed: MockedTypeshed, ) -> TestCaseBuilder { - let TestCaseBuilder { - typeshed_option: _, - python_version, - first_party_files, - site_packages_files, - roots, - } = self; - - TestCaseBuilder { - typeshed_option: typeshed, - python_version, - first_party_files, - site_packages_files, - roots, - } + self.with_typeshed(typeshed) } pub(crate) fn build(self) -> TestCase<()> { diff --git a/crates/ty_module_resolver/src/typeshed.rs b/crates/ty_module_resolver/src/typeshed.rs index 1fcac4050d..b7897cc2e7 100644 --- a/crates/ty_module_resolver/src/typeshed.rs +++ b/crates/ty_module_resolver/src/typeshed.rs @@ -6,12 +6,11 @@ use std::str::FromStr; use ruff_db::vendored::VendoredFileSystem; use ruff_python_ast::{PythonVersion, PythonVersionDeserializationError}; -use rustc_hash::FxHashMap; -use crate::db::Db; +use crate::FxOrderMap; use crate::module_name::ModuleName; -pub fn vendored_typeshed_versions(vendored: &VendoredFileSystem) -> TypeshedVersions { +pub(crate) fn vendored_typeshed_versions(vendored: &VendoredFileSystem) -> TypeshedVersions { TypeshedVersions::from_str( &vendored .read_to_string("stdlib/VERSIONS") @@ -20,10 +19,6 @@ pub fn vendored_typeshed_versions(vendored: &VendoredFileSystem) -> TypeshedVers .expect("The VERSIONS file in the vendored typeshed stubs should be well-formed") } -pub(crate) fn typeshed_versions(db: &dyn Db) -> &TypeshedVersions { - db.search_paths().typeshed_versions() -} - #[derive(Debug, PartialEq, Eq, Clone)] pub struct TypeshedVersionsParseError { line_number: Option, @@ -71,8 +66,8 @@ pub enum TypeshedVersionsParseErrorKind { VersionParseError(#[from] PythonVersionDeserializationError), } -#[derive(Clone, Debug, PartialEq, Eq, get_size2::GetSize)] -pub struct TypeshedVersions(FxHashMap); +#[derive(Clone, Debug, PartialEq, Eq, Hash, get_size2::GetSize)] +pub struct TypeshedVersions(FxOrderMap); impl TypeshedVersions { #[must_use] @@ -164,7 +159,7 @@ impl FromStr for TypeshedVersions { type Err = TypeshedVersionsParseError; fn from_str(s: &str) -> Result { - let mut map = FxHashMap::default(); + let mut map = FxOrderMap::default(); for (line_index, line) in s.lines().enumerate() { // humans expect line numbers to be 1-indexed @@ -375,7 +370,12 @@ mod tests { let relative_path = absolute_path .strip_prefix(&stdlib_stubs_path) - .unwrap_or_else(|_| panic!("Expected path to be a child of {stdlib_stubs_path:?} but found {absolute_path:?}")); + .unwrap_or_else(|_| { + panic!( + "Expected path to be a child of {stdlib_stubs_path:?} \ + but found {absolute_path:?}" + ) + }); let relative_path_str = relative_path.as_os_str().to_str().unwrap_or_else(|| { panic!("Expected all typeshed paths to be valid UTF-8; got {relative_path:?}") @@ -386,15 +386,22 @@ mod tests { let top_level_module = if let Some(extension) = relative_path.extension() { // It was a file; strip off the file extension to get the module name: - let extension = extension - .to_str() - .unwrap_or_else(||panic!("Expected all file extensions to be UTF-8; was not true for {relative_path:?}")); + let extension = extension.to_str().unwrap_or_else(|| { + panic!( + "Expected all file extensions to be UTF-8; \ + was not true for {relative_path:?}" + ) + }); relative_path_str .strip_suffix(extension) - .and_then(|string| string.strip_suffix('.')).unwrap_or_else(|| { - panic!("Expected path {relative_path_str:?} to end with computed extension {extension:?}") - }) + .and_then(|string| string.strip_suffix('.')) + .unwrap_or_else(|| { + panic!( + "Expected path {relative_path_str:?} to end \ + with computed extension {extension:?}" + ) + }) } else { // It was a directory; no need to do anything to get the module name relative_path_str diff --git a/crates/ty_project/Cargo.toml b/crates/ty_project/Cargo.toml index aa3ae6606d..1e2633323a 100644 --- a/crates/ty_project/Cargo.toml +++ b/crates/ty_project/Cargo.toml @@ -40,7 +40,6 @@ compact_str = { workspace = true, features = ["serde"] } crossbeam = { workspace = true } get-size2 = { workspace = true, features = ["ordermap", "parking_lot"] } globset = { workspace = true } -memchr = { workspace = true } notify = { workspace = true } ordermap = { workspace = true, features = ["serde"] } parking_lot = { workspace = true } diff --git a/crates/ty_project/src/db.rs b/crates/ty_project/src/db.rs index fdd5582f90..88b00bc021 100644 --- a/crates/ty_project/src/db.rs +++ b/crates/ty_project/src/db.rs @@ -18,13 +18,11 @@ use ruff_db::system::System; use ruff_db::vendored::VendoredFileSystem; use ruff_ranged_value::ValueSource; use salsa::{Database, Event, Setter}; -use ty_module_resolver::SearchPaths; -use ty_python_core::program::{ - FallibleStrategy, MisconfigurationStrategy, Program, UseDefaultStrategy, -}; +use ty_python_core::ProgramFile; +use ty_python_core::program::{FallibleStrategy, MisconfigurationStrategy, UseDefaultStrategy}; use ty_python_semantic::dependencies::DependencyManifest; use ty_python_semantic::lint::{LintRegistry, RuleSelection}; -use ty_python_semantic::{AnalysisSettings, Db as SemanticDb}; +use ty_python_semantic::{AnalysisSettings, Db as SemanticDb, PythonVersionWithSource}; mod changes; @@ -99,20 +97,16 @@ impl ProjectDatabase { /// Permanently freezes the most heavily read inputs that are immutable during a one-shot check. /// - /// This is intentionally not exhaustive. It includes every [`Program`] input, the most heavily + /// This is intentionally not exhaustive. It includes the program, the most heavily /// read immutable [`Project`] inputs, and every field on files created after this call. Existing /// files retain their durability. This must not be used by incremental consumers or checks that /// apply fixes. pub fn freeze(&mut self) { - let program = Program::try_get(self).expect("the program should be initialized"); - let project = self.project(); - - program.freeze(self); - project.freeze(self); + self.project().freeze(self); self.files.freeze(); } - /// See [`Project::freeze_open_files`]. + /// Permanently marks the project as never having open files. pub fn freeze_open_files(&mut self) { let project = self.project(); project.freeze_open_files(self); @@ -155,28 +149,30 @@ impl ProjectDatabase { let merged_options = project_metadata.to_merged_options(); - // Initialize the `Program` singleton let (program_settings, program_settings_diagnostics) = strategy .to_anyhow(merged_options.to_program_settings(db.system(), db.vendored(), strategy))?; - // This must be called before `from_settings`, or the `SearchPath` root + // This must be called before `from_metadata`, or the `SearchPath` root // will take precedence over the `Project` root, resulting in // all project files having HIGH durability. project_metadata.try_add_project_root(&db); - Program::from_settings(&db, program_settings); - - let (settings, settings_diagnostics) = strategy + let (settings, mut settings_diagnostics) = strategy .map_err(merged_options.to_settings(&db, strategy), |error| { anyhow::anyhow!("{}", error.pretty(&db)) })?; + settings_diagnostics.extend( + program_settings_diagnostics + .into_iter() + .map(|diagnostic| diagnostic.into_diagnostic(&db)), + ); db.project = Some(Project::from_metadata( &db, project_metadata, settings, + program_settings, settings_diagnostics, - program_settings_diagnostics, )); Ok(db) @@ -559,11 +555,7 @@ impl SalsaMemoryDump { } #[salsa::db] -impl ty_module_resolver::Db for ProjectDatabase { - fn search_paths(&self) -> &SearchPaths { - Program::get(self).search_paths(self) - } -} +impl ty_module_resolver::Db for ProjectDatabase {} #[salsa::db] impl SemanticDb for ProjectDatabase { @@ -571,6 +563,14 @@ impl SemanticDb for ProjectDatabase { ProjectDatabase::check_file(self, file) } + fn program_file(&self, file: File) -> ProgramFile<'_> { + self.project().program(self).program_file(self, file) + } + + fn python_version_with_source(&self, _file: File) -> &PythonVersionWithSource { + &self.project().program_settings(self).python_version + } + fn rule_selection(&self, file: File) -> &RuleSelection { let settings = file_settings(self, file); settings.rules(self) @@ -640,10 +640,6 @@ impl SourceDb for ProjectDatabase { fn files(&self) -> &Files { &self.files } - - fn python_version(&self) -> ruff_python_ast::PythonVersion { - Program::get(self).python_version(self) - } } #[salsa::db] @@ -669,12 +665,14 @@ mod format { use crate::ProjectDatabase; use ruff_db::files::File; use ruff_python_formatter::{Db as FormatDb, PyFormatOptions}; + use ty_python_semantic::Db as _; #[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) + .with_target_version(self.program_file(file).python_version(self)) } } } @@ -689,10 +687,15 @@ pub(crate) mod testing { use ruff_db::files::{File, FileRootKind, Files}; use ruff_db::system::{DbWithTestSystem, System, SystemPath, SystemPathBuf, TestSystem}; use ruff_db::vendored::VendoredFileSystem; + #[cfg(any(test, feature = "testing"))] use ruff_python_ast::PythonVersion; use ty_module_resolver::SearchPathSettings; + use ty_python_core::ProgramFile; use ty_python_core::platform::PythonPlatform; - use ty_python_core::program::{FallibleStrategy, Program, ProgramSettings}; + use ty_python_core::program::Program; + use ty_python_core::program::{FallibleStrategy, ProgramSettings}; + #[cfg(any(test, feature = "testing"))] + use ty_python_semantic::ProgramEnvironment; use ty_python_semantic::dependencies::DependencyManifest; use ty_python_semantic::lint::{LintRegistry, RuleSelection}; use ty_python_semantic::{AnalysisSettings, PythonVersionWithSource}; @@ -735,8 +738,29 @@ pub(crate) mod testing { .to_merged_options() .to_settings(&db, &FallibleStrategy) .unwrap(); - let project = - Project::from_metadata(&db, project, settings, settings_diagnostics, Vec::new()); + let root = project.root().to_path_buf(); + db.system + .memory_file_system() + .create_directory_all(&root) + .expect("create project root"); + let search_paths = SearchPathSettings::new(vec![root.clone()]) + .to_search_paths(db.system(), db.vendored(), &FallibleStrategy) + .expect("Valid search path settings"); + + db.files().try_add_root(&db, &root, FileRootKind::Project); + + let program_settings = ProgramSettings { + python_version: PythonVersionWithSource::default(), + python_platform: PythonPlatform::default(), + search_paths, + }; + let project = Project::from_metadata( + &db, + project, + settings, + program_settings, + settings_diagnostics, + ); db.project = Some(project); db } @@ -787,21 +811,42 @@ pub(crate) mod testing { self.files().try_add_root(self, root, FileRootKind::Project); - Program::from_settings( - self, - ProgramSettings { - python_version: PythonVersionWithSource { - source: ty_python_semantic::PythonVersionSource::Default, - version: python_version, - }, - python_platform: PythonPlatform::default(), - search_paths, + let settings = ProgramSettings { + python_version: PythonVersionWithSource { + source: ty_python_semantic::PythonVersionSource::Default, + version: python_version, }, - ); + python_platform: PythonPlatform::default(), + search_paths, + }; + // the project has to carry these too: `Project::program` rebuilds the program + // from the project's own settings, so a query that asks the project rather + // than a file would otherwise see no site-packages at all + Program::from_settings(self, settings.clone()); + self.project().update_program(self, settings); + } + + #[cfg(feature = "testing")] + pub fn set_python_version(&mut self, python_version: PythonVersion) { + let program = self.project().program(self); + let settings = ProgramSettings { + python_version: PythonVersionWithSource { + source: ty_python_semantic::PythonVersionSource::Default, + version: python_version, + }, + python_platform: program.python_platform(self).clone(), + search_paths: program.search_paths(self).clone(), + }; + self.project().update_program(self, settings); } } impl TestDb { + #[cfg(feature = "testing")] + pub fn program_environment(&self) -> ProgramEnvironment<'_> { + ProgramEnvironment::from_program(self.project().program(self)) + } + /// Takes the salsa events. pub fn take_salsa_events(&mut self) -> Vec { let mut events = self.events.lock().unwrap(); @@ -833,18 +878,10 @@ pub(crate) mod testing { fn files(&self) -> &Files { &self.files } - - fn python_version(&self) -> ruff_python_ast::PythonVersion { - Program::get(self).python_version(self) - } } #[salsa::db] - impl ty_module_resolver::Db for TestDb { - fn search_paths(&self) -> &ty_module_resolver::SearchPaths { - Program::get(self).search_paths(self) - } - } + impl ty_module_resolver::Db for TestDb {} #[salsa::db] impl ty_python_core::Db for TestDb { @@ -855,6 +892,14 @@ pub(crate) mod testing { #[salsa::db] impl ty_python_semantic::Db for TestDb { + fn program_file(&self, file: File) -> ProgramFile<'_> { + self.project().program(self).program_file(self, file) + } + + fn python_version_with_source(&self, _file: File) -> &PythonVersionWithSource { + &self.project().program_settings(self).python_version + } + #[inline] fn check_file(&self, file: File) -> Vec { crate::check_file(self, file) @@ -917,7 +962,7 @@ mod tests { use ruff_db::system::{SystemPathBuf, TestSystem}; use ty_module_resolver::list_modules; - use crate::{ProjectDatabase, ProjectMetadata}; + use crate::{Db as _, ProjectDatabase, ProjectMetadata}; #[test] fn frozen_inputs_support_a_one_shot_check() -> anyhow::Result<()> { @@ -961,7 +1006,7 @@ mod tests { let metadata = ProjectMetadata::discover(&project, &system)?; let db = ProjectDatabase::fallible(metadata, system)?; - let modules = list_modules(&db); + let modules = list_modules(&db, db.project().program(&db).resolver_environment(&db)); assert!( modules .iter() diff --git a/crates/ty_project/src/db/changes.rs b/crates/ty_project/src/db/changes.rs index 549378292e..325ad0f515 100644 --- a/crates/ty_project/src/db/changes.rs +++ b/crates/ty_project/src/db/changes.rs @@ -10,7 +10,7 @@ use ruff_db::files::{File, Files, system_path_to_file}; use ruff_db::system::{SystemPath, SystemPathBuf}; use rustc_hash::FxHashSet; use salsa::Setter as _; -use ty_python_core::program::{FallibleStrategy, Program}; +use ty_python_core::program::FallibleStrategy; /// Represents the result of applying changes to the project database. pub struct ChangeResult { @@ -36,7 +36,7 @@ impl ProjectDatabase { let project = self.project(); let project_root = project.root(self).to_path_buf(); let configuration_paths = ConfigurationPaths::from_metadata(project.metadata(self)); - let program = Program::get(self); + let program = self.project().program(self); let custom_stdlib_versions_path = program .custom_stdlib_search_path(self) .map(|path| path.join("VERSIONS")); @@ -212,7 +212,8 @@ impl ProjectDatabase { if configuration_paths.may_contain_configuration(path, &project_root) { tracing::debug!( - "Reload project because a configuration file may have been deleted." + "Reload project because a configuration file \ + may have been deleted." ); reload_project = true; } @@ -258,7 +259,8 @@ impl ProjectDatabase { if let Err(error) = metadata.apply_configuration_files(self.system()) { let error = anyhow::Error::new(error); tracing::error!( - "Failed to apply configuration files, continuing without applying them: {error:#}" + "Failed to apply configuration files, \ + continuing without applying them: {error:#}" ); } @@ -271,38 +273,37 @@ impl ProjectDatabase { &FallibleStrategy, ) { Ok((program_settings, diagnostics)) => { - let program = Program::get(self); - program.update_from_settings(self, program_settings); + project.update_program(self, program_settings); diagnostics } Err(error) => { tracing::error!( - "Failed to convert metadata to program settings, continuing without applying them: {error}" + "Failed to convert metadata to program settings, \ + continuing without applying them: {error}" ); Vec::new() } }; - let (settings, settings_diagnostics) = match merged_options - .to_settings(self, &FallibleStrategy) - { - Ok((settings, diagnostics)) => (Some(settings), diagnostics), - Err(error) => { - tracing::warn!( - "Keeping old project configuration because loading the new settings failed with: {error}" - ); - (None, vec![error.into_diagnostic()]) - } - }; + let (settings, mut settings_diagnostics) = + match merged_options.to_settings(self, &FallibleStrategy) { + Ok((settings, diagnostics)) => (Some(settings), diagnostics), + Err(error) => { + tracing::warn!( + "Keeping old project configuration because loading the new \ + settings failed with: {error}" + ); + (None, vec![error.into_diagnostic()]) + } + }; + settings_diagnostics.extend( + program_settings_diagnostics + .into_iter() + .map(|diagnostic| diagnostic.into_diagnostic(self)), + ); tracing::debug!("Reloading project after structural change"); - match project.reload( - self, - metadata, - settings, - settings_diagnostics, - program_settings_diagnostics, - ) { + match project.reload(self, metadata, settings, settings_diagnostics) { ProjectReloadResult::Unchanged => {} ProjectReloadResult::Changed { files_changed } => { result.project_changed = true; @@ -343,17 +344,18 @@ impl ProjectDatabase { &FallibleStrategy, ) { Ok((program_settings, program_settings_diagnostics)) => { - let settings_diagnostics = + let mut settings_diagnostics = match merged_options.to_settings(self, &FallibleStrategy) { Ok((_, diagnostics)) => diagnostics, Err(error) => vec![error.into_diagnostic()], }; - program.update_from_settings(self, program_settings); - project.update_settings_diagnostics( - self, - settings_diagnostics, - program_settings_diagnostics, + project.update_program(self, program_settings); + settings_diagnostics.extend( + program_settings_diagnostics + .into_iter() + .map(|diagnostic| diagnostic.into_diagnostic(self)), ); + project.update_settings_diagnostics(self, settings_diagnostics); } Err(error) => { tracing::error!("Failed to resolve program settings: {error}"); diff --git a/crates/ty_project/src/lib.rs b/crates/ty_project/src/lib.rs index 21468808cd..b425c2ea4f 100644 --- a/crates/ty_project/src/lib.rs +++ b/crates/ty_project/src/lib.rs @@ -3,7 +3,7 @@ reason = "Prefer System trait methods over std methods in ty crates" )] use crate::glob::{GlobFilterCheckMode, IncludeResult}; -use crate::metadata::options::{OptionDiagnostic, ProgramSettingsDiagnostic}; +use crate::metadata::options::OptionDiagnostic; use crate::parallel::ParallelIteratorExt; use crate::walk::{ProjectFilesFilter, ProjectFilesWalker}; #[cfg(feature = "testing")] @@ -27,6 +27,9 @@ use std::collections::{BTreeSet, hash_set}; use std::iter::FusedIterator; use std::panic::{AssertUnwindSafe, UnwindSafe}; use std::sync::Arc; +use ty_python_core::ProgramFile; +use ty_python_core::program::{Program, ProgramSettings}; +pub use ty_python_semantic::Db as SemanticDb; use ty_python_semantic::lint::RuleSelection; mod db; @@ -42,7 +45,7 @@ pub mod watch; /// ## 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 +/// 1. Program is defined in `ty_python_core` 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 @@ -77,6 +80,10 @@ pub struct Project { #[returns(deref)] pub settings: Box, + /// The settings used to construct the Python program for this project. + #[returns(ref)] + pub program_settings: ProgramSettings, + /// 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 @@ -173,7 +180,7 @@ pub trait ProjectChecker: Send + Sync + std::panic::RefUnwindSafe { /// applied where the type checker applies its own (see /// [`ty_python_semantic::check_file_with`]), so that one `ty: ignore` /// silences either kind of diagnostic and counts as used either way. - fn check_python_file(&self, db: &dyn Db, file: File) -> Vec; + fn check_python_file(&self, db: &dyn Db, file: ProgramFile<'_>) -> Vec; /// The module the project points `DJANGO_SETTINGS_MODULE` at, if it names one. /// @@ -237,35 +244,34 @@ impl ProgressReporter for CollectReporter { #[salsa::tracked] impl Project { /// Create a project from resolved metadata and settings. - /// - /// Program-settings diagnostics are accepted separately so callers do not need to know how to - /// convert and merge them into the stored project settings diagnostics. - pub(crate) fn from_metadata( + fn from_metadata( db: &dyn Db, metadata: ProjectMetadata, settings: Settings, + program_settings: ProgramSettings, settings_diagnostics: Vec, - program_settings_diagnostics: Vec, ) -> Self { - let diagnostics = Self::settings_diagnostics_with_program_diagnostics( - db, - settings_diagnostics, - program_settings_diagnostics, - ); + program_settings.search_paths.try_register_static_roots(db); - Project::builder(Box::new(metadata), Box::new(settings), diagnostics) - .durability(Durability::MEDIUM) - .open_fileset_durability(Durability::LOW) - .file_set_durability(Durability::LOW) - .file_system_revision_durability(Durability::LOW) - .new(db) + Project::builder( + Box::new(metadata), + Box::new(settings), + program_settings, + settings_diagnostics, + ) + .durability(Durability::MEDIUM) + .open_fileset_durability(Durability::LOW) + .file_set_durability(Durability::LOW) + .file_system_revision_durability(Durability::LOW) + .new(db) } - /// Permanently freezes the most heavily read immutable project inputs. + /// Permanently freezes the most heavily read immutable project and program inputs. /// /// This is intentionally not exhaustive. - pub(crate) fn freeze(self, db: &mut dyn Db) { + fn freeze(self, db: &mut dyn Db) { let durability = Durability::NEVER_CHANGE; + let program_settings = self.program_settings(db).clone(); let metadata = Box::new(self.metadata(db).clone()); let settings = Box::new(self.settings(db).clone()); let included_paths = self.included_paths_list(db).to_vec(); @@ -279,6 +285,9 @@ impl Project { self.set_settings(db) .with_durability(durability) .to(settings); + self.set_program_settings(db) + .with_durability(durability) + .to(program_settings); self.set_included_paths_list(db) .with_durability(durability) .to(included_paths); @@ -295,11 +304,23 @@ impl Project { IndexedFiles::freeze(db, self); } + #[salsa::tracked(returns(copy), heap_size=ruff_memory_usage::heap_size)] + pub fn program(self, db: &dyn Db) -> Program<'_> { + Program::from_settings(db, self.program_settings(db).clone()) + } + + pub fn update_program(self, db: &mut dyn Db, settings: ProgramSettings) { + if self.program_settings(db) != &settings { + settings.search_paths.try_register_static_roots(db); + self.set_program_settings(db).to(settings); + } + } + pub fn root(self, db: &dyn Db) -> &SystemPath { self.metadata(db).root() } - pub fn name(self, db: &dyn Db) -> &str { + fn name(self, db: &dyn Db) -> &str { self.metadata(db).name() } @@ -324,7 +345,7 @@ impl Project { .is_file_included(path, GlobFilterCheckMode::Adhoc) } - pub fn is_directory_included(self, db: &dyn Db, path: &SystemPath) -> bool { + fn is_directory_included(self, db: &dyn Db, path: &SystemPath) -> bool { matches!( ProjectFilesFilter::from_project(db, self) .is_directory_included(path, GlobFilterCheckMode::Adhoc), @@ -333,25 +354,15 @@ impl Project { } /// Reload the project after its metadata or settings have changed. - /// - /// Program-settings diagnostics are converted and merged here to keep reload behavior - /// consistent with initial project creation. pub fn reload( self, db: &mut dyn Db, metadata: ProjectMetadata, settings: Option, settings_diagnostics: Vec, - program_settings_diagnostics: Vec, ) -> ProjectReloadResult { tracing::debug!("Reloading project"); let metadata_changed = &metadata != self.metadata(db); - let settings_diagnostics = Self::settings_diagnostics_with_program_diagnostics( - db, - settings_diagnostics, - program_settings_diagnostics, - ); - let root_changed = metadata.root() != self.root(db); let (settings_changed, files_changed) = if let Some(settings) = settings && self.settings(db) != &settings @@ -390,38 +401,18 @@ impl Project { /// /// This is used when a change affects [`ty_python_core::program::ProgramSettings`] without /// reloading the full project. - pub(crate) fn update_settings_diagnostics( + fn update_settings_diagnostics( self, db: &mut dyn Db, settings_diagnostics: Vec, - program_settings_diagnostics: Vec, ) { - let settings_diagnostics = Self::settings_diagnostics_with_program_diagnostics( - db, - settings_diagnostics, - program_settings_diagnostics, - ); - if self.settings_diagnostics(db) != settings_diagnostics { self.set_settings_diagnostics(db).to(settings_diagnostics); } } - fn settings_diagnostics_with_program_diagnostics( - db: &dyn Db, - mut settings_diagnostics: Vec, - program_settings_diagnostics: Vec, - ) -> Vec { - settings_diagnostics.extend( - program_settings_diagnostics - .into_iter() - .map(|diagnostic| diagnostic.into_diagnostic(db)), - ); - settings_diagnostics - } - /// Checks the project and its dependencies according to the project's check mode. - pub(crate) fn check(self, db: &ProjectDatabase, reporter: &mut dyn ProgressReporter) { + fn check(self, db: &ProjectDatabase, reporter: &mut dyn ProgressReporter) { let project_span = tracing::debug_span!("Project::check"); let _span = project_span.enter(); @@ -465,17 +456,19 @@ impl Project { let check_file_span = tracing::debug_span!(parent: &project_span, "check_file", ?file); let _entered = check_file_span.entered(); + let program_file = db.program_file(file); - match check_file_impl(db, file) { + match check_file_impl(db, program_file) { Ok(diagnostics) => { reporter.report_checked_file(db, file, diagnostics); // This is outside `check_file_impl` to avoid that opening or closing // a file invalidates the `check_file_impl` query of every file! if !open_files.contains(&file) { + let python_file = program_file.python_file(db); // The module has already been parsed by `check_file_impl`. // We only retrieve it here so that we can call `clear` on it. - let parsed = parsed_module(db, file); + let parsed = parsed_module(db, python_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 @@ -542,7 +535,7 @@ impl Project { } } - pub fn verbose(self, db: &dyn Db) -> bool { + fn verbose(self, db: &dyn Db) -> bool { self.verbose_flag(db) } @@ -552,7 +545,7 @@ impl Project { } } - pub fn force_exclude(self, db: &dyn Db) -> bool { + fn force_exclude(self, db: &dyn Db) -> bool { self.force_exclude_flag(db) } @@ -574,7 +567,7 @@ impl Project { } /// Returns the open files in the project. - pub fn open_files(self, db: &dyn Db) -> &FxHashSet { + fn open_files(self, db: &dyn Db) -> &FxHashSet { self.open_fileset(db) } @@ -588,7 +581,7 @@ impl Project { /// Permanently marks the project as never having open files, so reads of the /// open-file state record no salsa dependency. Any later write panics. - pub fn freeze_open_files(self, db: &mut dyn Db) { + fn freeze_open_files(self, db: &mut dyn Db) { self.set_open_fileset(db) .with_durability(Durability::NEVER_CHANGE) .to(FxHashSet::default()); @@ -622,7 +615,7 @@ impl Project { /// /// This is a no-op if the project files are still lazily indexed. #[tracing::instrument(level = "debug", skip(self, db, paths))] - pub(crate) fn remove_files_under(self, db: &mut dyn Db, paths: I) + fn remove_files_under(self, db: &mut dyn Db, paths: I) where I: IntoIterator, P: AsRef, @@ -671,7 +664,7 @@ impl Project { } } - pub fn add_file(self, db: &mut dyn Db, file: File) { + fn add_file(self, db: &mut dyn Db, file: File) { tracing::debug!( "Adding file `{}` to project `{}`", file.path(db), @@ -688,7 +681,7 @@ impl Project { /// 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) { + fn replace_index_diagnostics(self, db: &mut dyn Db, diagnostics: Vec) { let Some(mut index) = IndexedFiles::indexed_mut(db, self) else { return; }; @@ -721,7 +714,7 @@ impl Project { } } - pub fn reload_files(self, db: &mut dyn Db) { + fn reload_files(self, db: &mut dyn Db) { tracing::debug!("Reloading files for project `{}`", self.name(db)); if !self.file_set(db).is_lazy() { @@ -739,12 +732,12 @@ impl Project { } } -pub(crate) fn check_file(db: &dyn Db, file: File) -> Vec { +fn check_file(db: &dyn Db, file: File) -> Vec { if !db.should_check_file(file) { return Vec::new(); } - check_file_impl(db, file) + check_file_impl(db, db.program_file(file)) .map(<[Diagnostic]>::to_vec) .unwrap_or_else(|diagnostic| vec![diagnostic.clone()]) } @@ -826,10 +819,14 @@ pub enum ProjectReloadResult { } #[salsa::tracked(returns(as_deref), heap_size=ruff_memory_usage::heap_size)] -pub(crate) fn check_file_impl(db: &dyn Db, file: File) -> Result, Diagnostic> { +pub(crate) fn check_file_impl( + db: &dyn Db, + file: ProgramFile<'_>, +) -> Result, Diagnostic> { + let source_file = file.file(db); { let db = AssertUnwindSafe(db); - match catch(&**db, file, || { + match catch(&**db, source_file, || { // what a registered checker has to say about a python file is folded into // the type checker's own pass rather than reported beside it, so that the // file's suppression comments apply to both alike @@ -986,13 +983,13 @@ mod tests { 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 ty_python_semantic::Db as _; use ty_python_semantic::types::check_types; #[test] fn check_file_skips_type_checking_when_file_cant_be_read() -> ruff_db::system::Result<()> { let project = ProjectMetadata::new("test", SystemPathBuf::from("/")); let mut db = TestDb::new(project); - db.init_program().unwrap(); let path = SystemPath::new("test.py"); db.write_file(path, "x = 10")?; @@ -1004,16 +1001,16 @@ mod tests { assert_eq!(source_text(&db, file).as_str(), ""); assert_eq!( - check_file_impl(&db, file) + check_file_impl(&db, db.program_file(file)) .as_ref() .unwrap_err() - .primary_message() + .headline_message() .to_string(), "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); + assert_function_query_was_not_run(&db, check_types, db.program_file(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. @@ -1021,11 +1018,11 @@ mod tests { assert_eq!(source_text(&db, file).as_str(), ""); assert_eq!( - check_file_impl(&db, file) + check_file_impl(&db, db.program_file(file)) .as_ref() .unwrap() .iter() - .map(|diagnostic| diagnostic.primary_message().to_string()) + .map(|diagnostic| diagnostic.headline_message().to_string()) .collect::>(), vec![] as Vec ); diff --git a/crates/ty_project/src/metadata.rs b/crates/ty_project/src/metadata.rs index 820ede29d6..c7ae600448 100644 --- a/crates/ty_project/src/metadata.rs +++ b/crates/ty_project/src/metadata.rs @@ -9,11 +9,15 @@ use std::sync::Arc; use thiserror::Error; use ty_combine::Combine; use ty_python_core::program::{FallibleStrategy, MisconfigurationStrategy, ProgramSettings}; +use ty_static::EnvVars; use crate::Db; -use crate::metadata::options::{OptionDiagnostic, ProgramSettingsDiagnostic, ToSettingsError}; +use crate::metadata::options::{ + EnvironmentOptions, OptionDiagnostic, ProgramSettingsDiagnostic, ToSettingsError, +}; use crate::metadata::pyproject::{Project, PyProject, PyProjectError, ResolveRequiresPythonError}; use crate::metadata::settings::Settings; +use crate::metadata::value::RelativePathBuf; pub use options::Options; use options::TyTomlError; @@ -23,6 +27,7 @@ pub mod pyproject; pub mod python_version; pub(crate) mod script; pub mod settings; +mod uv; pub mod value; #[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)] @@ -40,12 +45,18 @@ pub struct ProjectMetadata { /// When [`Self::config_file_override`] is `None`, then these are the options from the /// project's `basedpython.toml`, `ty.toml`, or `pyproject.toml`. The options come from /// the file specified by [`Self::config_file_override`] if it is `Some` (e.g. when using `--config-file `). - pub(super) options: Options, + options: Options, + + /// The Python version and interpreter path derived from uv workspace metadata. + /// + /// These options have higher precedence than project and user-level configuration. + #[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))] + uv_workspace_options: Option>, /// The user-level configuration path and its options. /// - /// Its options have lower precedence than [`Self::override_options`] and [`Self::options`], - /// but higher precedence than [`Self::fallback_options`]. + /// Its options have lower precedence than [`Self::override_options`], [`Self::options`], and + /// [`Self::uv_workspace_options`], but higher precedence than [`Self::fallback_options`]. #[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))] user_configuration: Option>, @@ -59,6 +70,9 @@ pub struct ProjectMetadata { /// instead of from the project's `pyproject.toml`, `basedpython.toml`, or `ty.toml` file. #[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))] config_file_override: Option, + + #[cfg_attr(test, serde(skip))] + uv_workspace: Option, } impl ProjectMetadata { @@ -68,10 +82,12 @@ impl ProjectMetadata { name: ProjectName::new(name), root, options: Options::default(), + uv_workspace_options: None, override_options: None, user_configuration: None, fallback_options: None, config_file_override: None, + uv_workspace: None, } } @@ -95,15 +111,17 @@ impl ProjectMetadata { name: ProjectName::new(root.file_name().unwrap_or("root")), root: root.to_path_buf(), options, + uv_workspace_options: None, override_options: None, user_configuration: None, fallback_options: None, config_file_override: Some(path), + uv_workspace: None, }) } /// Loads a project from a `pyproject.toml` file. - pub(crate) fn from_pyproject( + fn from_pyproject( pyproject: &PyProject, root: SystemPathBuf, ) -> Result { @@ -153,25 +171,57 @@ impl ProjectMetadata { name, root, options, + uv_workspace_options: None, override_options: None, user_configuration: None, fallback_options: None, config_file_override: None, + uv_workspace: None, }) } /// Discovers the closest project at `path` and returns its metadata. /// /// The algorithm traverses upwards in the `path`'s ancestor chain and uses the following precedence - /// the resolve the project's root. + /// to resolve the project's root. /// /// 1. The closest `basedpython.toml` or `ty.toml`, or `pyproject.toml` with a /// `tool.basedpython` or `tool.ty` section. + /// 1. The uv workspace root, if uv integration is enabled. /// 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 { + let uv_workspace = if matches!(system.env_var(EnvVars::TY_UV).as_deref(), Ok("1" | "true")) + { + match uv::UvWorkspace::discover(path, system) { + Ok(workspace) => Some(workspace), + Err(error) => { + tracing::warn!("{error}"); + None + } + } + } else { + None + }; + + Self::discover_with_uv_workspace(path, system, uv_workspace) + } + + /// Discovers the closest project without considering uv workspace metadata. + pub fn discover_without_uv( + path: &SystemPath, + system: &dyn System, + ) -> Result { + Self::discover_with_uv_workspace(path, system, None) + } + + fn discover_with_uv_workspace( + path: &SystemPath, + system: &dyn System, + uv_workspace: Option, ) -> Result { tracing::debug!("Searching for a project in '{path}'"); @@ -180,115 +230,55 @@ impl ProjectMetadata { } let mut closest_project: Option = None; + let mut uv_project: Option = None; + let uv_workspace_root = uv_workspace.as_ref().map(uv::UvWorkspace::root); for project_root in path.ancestors() { - let pyproject_path = project_root.join("pyproject.toml"); - - let pyproject = if let Ok(pyproject_str) = system.read_to_string(&pyproject_path) { - match PyProject::from_toml_str( - &pyproject_str, - ValueSource::File(Arc::new(pyproject_path.clone())), - ) { - Ok(pyproject) => Some(pyproject), - Err(error) => { - return Err(ProjectMetadataError::InvalidPyProject { - path: pyproject_path, - source: Box::new(error), - }); - } + let is_uv_workspace_root = uv_workspace_root == Some(project_root); + let Some((metadata, has_ty_configuration)) = Self::discover_in(project_root, system)? + else { + if is_uv_workspace_root { + uv_project = Some(Self::new( + project_root.file_name().unwrap_or("root"), + project_root.to_path_buf(), + )); } - } else { - None + continue; }; - // A configuration file takes precedence over a `pyproject.toml`. - let config_file = CONFIG_FILE_NAMES.iter().find_map(|name| { - let path = project_root.join(name); - let content = system.read_to_string(&path).ok()?; - Some((path, content)) - }); - - if let Some((config_path, config_str)) = config_file { - let options = match Options::from_toml_str( - &config_str, - ValueSource::File(Arc::new(config_path.clone())), - ) { - Ok(options) => options, - Err(error) => { - return Err(ProjectMetadataError::InvalidConfigFile { - path: config_path, - source: Box::new(error), - }); - } - }; - - // TODO: Consider using diagnostics for the two warnings below - for ignored in CONFIG_FILE_NAMES - .iter() - .map(|name| project_root.join(name)) - .filter(|path| *path != config_path && system.path_exists(path)) - { - tracing::warn!( - "Ignoring `{ignored}` because `{config_path}` takes precedence." - ); - } - - if let Some(sections) = pyproject - .as_ref() - .filter(|pyproject| pyproject.has_options()) - .map(PyProject::section_names) - { - tracing::warn!( - "Ignoring the {sections} in `{pyproject_path}` because `{config_path}` takes precedence." - ); - } - + if has_ty_configuration { tracing::debug!("Found project at '{}'", project_root); - - let metadata = ProjectMetadata::from_options( - options, - project_root.to_path_buf(), - pyproject - .as_ref() - .and_then(|pyproject| pyproject.project.as_ref()), - &FallibleStrategy, - ) - .map_err(|err| { - ProjectMetadataError::InvalidRequiresPythonConstraint { - source: err, - path: pyproject_path, - } - })?; - - return Ok(metadata); + return Ok(metadata.with_uv_workspace(uv_workspace)); } - if let Some(pyproject) = pyproject { - let has_options = pyproject.has_options(); - let metadata = - ProjectMetadata::from_pyproject(&pyproject, project_root.to_path_buf()) - .map_err( - |err| ProjectMetadataError::InvalidRequiresPythonConstraint { - source: err, - path: pyproject_path, - }, - )?; - - if has_options { - tracing::debug!("Found project at '{}'", project_root); - - return Ok(metadata); - } - - // Not a project itself, keep looking for an enclosing project. - if closest_project.is_none() { - closest_project = Some(metadata); - } + if is_uv_workspace_root { + uv_project = Some(metadata); + } else if closest_project.is_none() { + closest_project = Some(metadata); } } - // No project found, but maybe a pyproject.toml was found. - let metadata = if let Some(closest_project) = closest_project { + // Workspace members can live outside the workspace directory, so their ancestor chain may + // never include the workspace root. + if let Some(workspace_root) = uv_workspace_root + && !path.starts_with(workspace_root) + { + let metadata = Self::discover_in(workspace_root, system)? + .map(|(metadata, _)| metadata) + .unwrap_or_else(|| { + Self::new( + workspace_root.file_name().unwrap_or("root"), + workspace_root.to_path_buf(), + ) + }); + uv_project = Some(metadata); + } + + let metadata = if let Some(uv_project) = uv_project { + tracing::debug!("Using uv workspace at '{}'", uv_project.root()); + + uv_project + } else if let Some(closest_project) = closest_project { tracing::debug!( "Project without a `tool.basedpython` or `tool.ty` section: '{}'", closest_project.root() @@ -304,7 +294,110 @@ impl ProjectMetadata { Self::new(path.file_name().unwrap_or("root"), path.to_path_buf()) }; - Ok(metadata) + Ok(metadata.with_uv_workspace(uv_workspace)) + } + + fn discover_in( + project_root: &SystemPath, + system: &dyn System, + ) -> Result, ProjectMetadataError> { + let pyproject_path = project_root.join("pyproject.toml"); + + let pyproject = if let Ok(pyproject_str) = system.read_to_string(&pyproject_path) { + match PyProject::from_toml_str( + &pyproject_str, + ValueSource::File(Arc::new(pyproject_path.clone())), + ) { + Ok(pyproject) => Some(pyproject), + Err(error) => { + return Err(ProjectMetadataError::InvalidPyProject { + path: pyproject_path, + source: Box::new(error), + }); + } + } + } else { + None + }; + + // A configuration file takes precedence over a `pyproject.toml`. + let config_file = CONFIG_FILE_NAMES.iter().find_map(|name| { + let path = project_root.join(name); + let content = system.read_to_string(&path).ok()?; + Some((path, content)) + }); + + if let Some((config_path, config_str)) = config_file { + let options = match Options::from_toml_str( + &config_str, + ValueSource::File(Arc::new(config_path.clone())), + ) { + Ok(options) => options, + Err(error) => { + return Err(ProjectMetadataError::InvalidConfigFile { + path: config_path, + source: Box::new(error), + }); + } + }; + + // TODO: Consider using diagnostics for the two warnings below + for ignored in CONFIG_FILE_NAMES + .iter() + .map(|name| project_root.join(name)) + .filter(|path| *path != config_path && system.path_exists(path)) + { + tracing::warn!("Ignoring `{ignored}` because `{config_path}` takes precedence."); + } + + if let Some(sections) = pyproject + .as_ref() + .filter(|pyproject| pyproject.has_options()) + .map(PyProject::section_names) + { + tracing::warn!( + "Ignoring the {sections} in `{pyproject_path}` because `{config_path}` takes precedence." + ); + } + + let metadata = ProjectMetadata::from_options( + options, + project_root.to_path_buf(), + pyproject + .as_ref() + .and_then(|pyproject| pyproject.project.as_ref()), + &FallibleStrategy, + ) + .map_err(|source| { + ProjectMetadataError::InvalidRequiresPythonConstraint { + source, + path: pyproject_path, + } + })?; + + return Ok(Some((metadata, true))); + } + + let Some(pyproject) = pyproject else { + return Ok(None); + }; + + let has_ty_configuration = pyproject.has_options(); + let metadata = ProjectMetadata::from_pyproject(&pyproject, project_root.to_path_buf()) + .map_err( + |source| ProjectMetadataError::InvalidRequiresPythonConstraint { + source, + path: pyproject_path, + }, + )?; + + Ok(Some((metadata, has_ty_configuration))) + } + + #[must_use] + fn with_uv_workspace(mut self, uv_workspace: Option) -> Self { + self.uv_workspace = uv_workspace; + self } /// Rediscovers the project, while preserving applied options. @@ -328,11 +421,11 @@ impl ProjectMetadata { Ok(metadata) } - pub fn root(&self) -> &SystemPath { + pub(crate) fn root(&self) -> &SystemPath { &self.root } - pub fn name(&self) -> &str { + pub(crate) fn name(&self) -> &str { self.name.as_str() } @@ -346,7 +439,7 @@ impl ProjectMetadata { } /// Returns configuration paths outside normal project discovery that should be watched. - pub fn extra_configuration_paths(&self) -> impl Iterator { + pub(crate) fn extra_configuration_paths(&self) -> impl Iterator { self.config_file_override().into_iter().chain( self.user_configuration .as_deref() @@ -375,10 +468,14 @@ impl ProjectMetadata { } } + pub fn has_uv_workspace(&self) -> bool { + self.uv_workspace.is_some() + } + /// Applies lower-precedence options to this project. /// /// Options applied later take precedence over options applied earlier, but all fallback options - /// have lower precedence than the raw and user-level options. + /// have lower precedence than the raw, uv workspace, and user-level options. pub fn apply_fallback_options(&mut self, options: Options) { if let Some(existing) = self.fallback_options.as_mut() { let previous = std::mem::replace(existing.as_mut(), options); @@ -390,7 +487,7 @@ impl ProjectMetadata { /// Returns the project's option layers from highest to lowest precedence. /// - /// `options` is used as the raw base layer between the override and user-level options. + /// `options` is used as the raw base layer between the uv workspace and user-level options. /// Layers can be merged by passing them to [`Options::combine_with`] in iterator order: /// /// ```ignore @@ -399,13 +496,14 @@ impl ProjectMetadata { /// merged.combine_with(layer.clone()); /// } /// ``` - pub(crate) fn options_in_precedence_order<'a>( + fn options_in_precedence_order<'a>( &'a self, options: &'a Options, ) -> impl Iterator { self.override_options .as_deref() .into_iter() + .chain(self.uv_workspace_options.as_deref()) .chain(std::iter::once(options)) .chain( self.user_configuration @@ -419,6 +517,7 @@ impl ProjectMetadata { /// /// This includes: /// + /// * The uv workspace configuration /// * The user-level configuration pub fn apply_configuration_files( &mut self, @@ -434,6 +533,19 @@ impl ProjectMetadata { self.user_configuration = Some(Box::new((user.path().to_owned(), user.into_options()))); } + self.uv_workspace_options = self.uv_workspace.as_ref().map(|uv_workspace| { + Box::new(Options { + environment: Some(EnvironmentOptions { + python_version: uv_workspace.python_version().cloned(), + python: uv_workspace + .environment() + .map(|path| RelativePathBuf::new(path, ValueSource::UvWorkspace)), + ..EnvironmentOptions::default() + }), + ..Options::default() + }) + }); + Ok(()) } @@ -541,7 +653,10 @@ mod tests { use insta::assert_ron_snapshot; use ruff_db::system::{SystemPathBuf, TestSystem}; use ruff_python_ast::PythonVersion; + use ruff_ranged_value::ValueSource; + use ty_static::EnvVars; + use crate::metadata::{Options, uv::UvWorkspace, value::RelativePathBuf}; use crate::{ProjectMetadata, ProjectMetadataError}; #[test] @@ -631,7 +746,7 @@ mod tests { name = "backend" [tool.basedpython.src] - root = "src" + respect-ignore-files = false "#, ), (root.join("packages/a/pyproject.toml"), ""), @@ -649,7 +764,7 @@ mod tests { root: "/app", options: Options( src: Some(SrcOptions( - root: Some("src"), + r#respect-ignore-files: Some(false), )), ), ) @@ -673,13 +788,13 @@ mod tests { name = "backend" [tool.ty.src] - root = "ty_src" + respect-ignore-files = true [tool.ty.environment] python-version = "3.10" [tool.basedpython.src] - root = "by_src" + respect-ignore-files = false "#, )]) .context("Failed to write files")?; @@ -687,7 +802,7 @@ mod tests { let project = ProjectMetadata::discover(&root, &system).context("Failed to discover project")?; - // `src.root` comes from `tool.basedpython`, `environment` from the unopposed `tool.ty` + // `src` comes from `tool.basedpython`, `environment` from the unopposed `tool.ty` with_escaped_paths(|| { assert_ron_snapshot!(&project, @r#" ProjectMetadata( @@ -698,7 +813,7 @@ mod tests { r#python-version: Some(r#3.10), )), src: Some(SrcOptions( - root: Some("by_src"), + r#respect-ignore-files: Some(false), )), ), ) @@ -762,8 +877,8 @@ unclosed table, expected `]` [project] name = "project-root" - [tool.ty.src] - root = "src" + [tool.ty.environment] + root = ["src"] "#, ), ( @@ -772,8 +887,8 @@ unclosed table, expected `]` [project] name = "nested-project" - [tool.ty.src] - root = "src" + [tool.ty.environment] + root = ["src"] "#, ), ]) @@ -787,8 +902,10 @@ unclosed table, expected `]` name: ProjectName("nested-project"), root: "/app/packages/a", options: Options( - src: Some(SrcOptions( - root: Some("src"), + environment: Some(EnvironmentOptions( + root: Some([ + "src", + ]), )), ), ) @@ -812,8 +929,8 @@ unclosed table, expected `]` [project] name = "project-root" - [tool.ty.src] - root = "src" + [tool.ty.environment] + root = ["src"] "#, ), ( @@ -822,8 +939,8 @@ unclosed table, expected `]` [project] name = "nested-project" - [tool.ty.src] - root = "src" + [tool.ty.environment] + root = ["src"] "#, ), ]) @@ -837,8 +954,10 @@ unclosed table, expected `]` name: ProjectName("project-root"), root: "/app", options: Options( - src: Some(SrcOptions( - root: Some("src"), + environment: Some(EnvironmentOptions( + root: Some([ + "src", + ]), )), ), ) @@ -888,6 +1007,266 @@ unclosed table, expected `]` Ok(()) } + #[test] + fn uv_workspace_precedes_plain_member_pyproject() -> anyhow::Result<()> { + let system = TestSystem::default(); + let root = SystemPathBuf::from("/app"); + let member = root.join("packages/member"); + + system.memory_file_system().write_files_all([ + (root.join("pyproject.toml"), "[tool.uv.workspace]"), + ( + member.join("pyproject.toml"), + r#" + [project] + name = "member" + "#, + ), + ])?; + + let uv_workspace = uv_workspace(&root, &system)?; + let project = + ProjectMetadata::discover_with_uv_workspace(&member, &system, Some(uv_workspace))?; + + assert_eq!(project.root(), &*root); + + Ok(()) + } + + #[test] + fn external_uv_workspace_precedes_plain_member_pyproject() -> anyhow::Result<()> { + let system = TestSystem::default(); + let root = SystemPathBuf::from("/app/workspace"); + let member = SystemPathBuf::from("/app/external-package"); + + system.memory_file_system().write_files_all([ + ( + root.join("pyproject.toml"), + r#" + [tool.uv.workspace] + members = ["../external-package"] + + [tool.ty.rules] + invalid-assignment = "ignore" + "#, + ), + ( + member.join("pyproject.toml"), + r#" + [project] + name = "external-package" + "#, + ), + ])?; + + let uv_workspace = uv_workspace(&root, &system)?; + let project = + ProjectMetadata::discover_with_uv_workspace(&member, &system, Some(uv_workspace))?; + + assert_eq!(project.root(), &*root); + + Ok(()) + } + + #[test] + fn uv_workspace_discovery_is_system_independent() -> anyhow::Result<()> { + let system = TestSystem::default(); + let root = SystemPathBuf::from("/app"); + let member = root.join("packages/member"); + + system.set_env_var(EnvVars::TY_UV, "1"); + system.set_env_var(EnvVars::UV, "uv"); + system + .memory_file_system() + .write_file_all(member.join("pyproject.toml"), "[project]\nname = 'member'")?; + + let project = ProjectMetadata::discover(&member, &system)?; + + assert_eq!(project.root(), &*member); + + Ok(()) + } + + #[test] + fn member_ty_configuration_selects_project_root() -> anyhow::Result<()> { + let system = TestSystem::default(); + let root = SystemPathBuf::from("/app"); + let member = root.join("packages/member"); + + system.memory_file_system().write_files_all([ + (root.join("uv.toml"), ""), + ( + member.join("pyproject.toml"), + r#" + [project] + name = "member" + + [tool.ty.environment] + python-version = "3.10" + "#, + ), + ])?; + + let uv_workspace = uv_workspace(&root, &system)?; + let mut project = + ProjectMetadata::discover_with_uv_workspace(&member, &system, Some(uv_workspace))?; + project.apply_configuration_files(&system)?; + + assert_eq!(project.root(), &*member); + assert_eq!( + project + .to_merged_options() + .options() + .environment + .as_ref() + .and_then(|environment| environment.python_version.as_deref()) + .copied() + .map(PythonVersion::from), + Some(PythonVersion::PY310) + ); + + Ok(()) + } + + #[test] + fn outer_ty_configuration_precedes_uv_workspace() -> anyhow::Result<()> { + let system = TestSystem::default(); + let root = SystemPathBuf::from("/app"); + let workspace = root.join("workspace"); + let member = workspace.join("packages/member"); + + system.memory_file_system().write_files_all([ + ( + root.join("ty.toml"), + r#" + [environment] + python-version = "3.10" + "#, + ), + (workspace.join("pyproject.toml"), "[tool.uv.workspace]"), + ( + member.join("pyproject.toml"), + r#" + [project] + name = "member" + "#, + ), + ])?; + + let uv_workspace = uv_workspace(&workspace, &system)?; + let project = + ProjectMetadata::discover_with_uv_workspace(&member, &system, Some(uv_workspace))?; + + assert_eq!(project.root(), &*root); + + Ok(()) + } + + #[test] + fn applies_uv_workspace_environment() -> anyhow::Result<()> { + let system = TestSystem::default(); + let root = SystemPathBuf::from("/app"); + let member = root.join("packages/member"); + let environment = root.join("uv-venv"); + + system.memory_file_system().write_files_all([ + ( + root.join("pyproject.toml"), + r#" + [tool.uv.workspace] + + [tool.ty.environment] + python = "/project-venv" + python-version = "3.10" + "#, + ), + (member.join("pyproject.toml"), "[project]\nname = 'member'"), + (environment.join("marker"), ""), + ])?; + + let metadata = serde_json::json!({ + "workspace_root": root, + "environment": { + "root": environment, + "python": { + "version": "3.13.5", + }, + }, + }); + let uv_workspace = UvWorkspace::from_metadata(metadata.to_string().as_bytes(), &system)?; + let mut project = + ProjectMetadata::discover_with_uv_workspace(&member, &system, Some(uv_workspace))?; + project.apply_fallback_options(Options::from_toml_str( + r#" + [environment] + python = "/editor-venv" + python-version = "3.10" + "#, + ValueSource::Editor, + )?); + project.apply_configuration_files(&system)?; + + let merged_options = project.to_merged_options(); + let project_environment = merged_options.options().environment.as_ref(); + assert_eq!( + project_environment + .and_then(|environment| environment.python_version.as_deref()) + .copied() + .map(PythonVersion::from), + Some(PythonVersion::PY313) + ); + assert_eq!( + project_environment + .and_then(|environment| environment.python.as_ref()) + .map(RelativePathBuf::path), + Some(environment.as_path()) + ); + assert!(matches!( + project_environment + .and_then(|environment| environment.python.as_ref()) + .map(RelativePathBuf::source), + Some(ValueSource::UvWorkspace) + )); + assert!(matches!( + project_environment + .and_then(|environment| environment.python_version.as_ref()) + .map(ruff_ranged_value::RangedValue::source), + Some(ValueSource::UvWorkspace) + )); + + let user_config_directory = root.join("config"); + system + .in_memory() + .set_user_configuration_directory(Some(user_config_directory.clone())); + system.memory_file_system().write_file_all( + user_config_directory.join("ty/ty.toml"), + r#" + [environment] + python = "/user-venv" + python-version = "3.12" + "#, + )?; + project.apply_configuration_files(&system)?; + + let merged_options = project.to_merged_options(); + let project_environment = merged_options.options().environment.as_ref(); + assert_eq!( + project_environment + .and_then(|environment| environment.python_version.as_deref()) + .copied() + .map(PythonVersion::from), + Some(PythonVersion::PY313) + ); + assert_eq!( + project_environment + .and_then(|environment| environment.python.as_ref()) + .map(|python| python.path().as_str()), + Some(environment.as_str()) + ); + + Ok(()) + } + #[test] fn nested_projects_with_outer_ty_section() -> anyhow::Result<()> { let system = TestSystem::default(); @@ -954,15 +1333,15 @@ unclosed table, expected `]` name = "super-app" requires-python = ">=3.12" - [tool.ty.src] - root = "this_option_is_ignored" + [tool.ty.environment] + root = ["this_option_is_ignored"] "#, ), ( root.join("ty.toml"), r#" - [src] - root = "src" + [environment] + root = ["src"] "#, ), ]) @@ -977,11 +1356,11 @@ unclosed table, expected `]` root: "/app", options: Options( environment: Some(EnvironmentOptions( + root: Some([ + "src", + ]), r#python-version: Some(r#3.12), )), - src: Some(SrcOptions( - root: Some("src"), - )), ), ) "#); @@ -1008,21 +1387,21 @@ unclosed table, expected `]` requires-python = ">=3.12" [tool.basedpython.src] - root = "this_option_is_ignored" + respect-ignore-files = true "#, ), ( root.join("ty.toml"), r#" [src] - root = "this_option_is_ignored_too" + respect-ignore-files = true "#, ), ( root.join("basedpython.toml"), r#" [src] - root = "src" + respect-ignore-files = false "#, ), ]) @@ -1040,7 +1419,7 @@ unclosed table, expected `]` r#python-version: Some(r#3.12), )), src: Some(SrcOptions( - root: Some("src"), + r#respect-ignore-files: Some(false), )), ), ) @@ -1408,6 +1787,17 @@ unclosed table, expected `]` assert_eq!(format!("{error:#}").replace('\\', "/"), message); } + fn uv_workspace(root: &SystemPathBuf, system: &TestSystem) -> anyhow::Result { + let metadata = serde_json::json!({ + "workspace_root": root, + }); + + Ok(UvWorkspace::from_metadata( + metadata.to_string().as_bytes(), + system, + )?) + } + fn with_escaped_paths(f: impl FnOnce() -> R) -> R { let mut settings = insta::Settings::clone_current(); settings.add_dynamic_redaction(".root", |content, _path| { diff --git a/crates/ty_project/src/metadata/options.rs b/crates/ty_project/src/metadata/options.rs index f08e4c69a2..2db1eb0314 100644 --- a/crates/ty_project/src/metadata/options.rs +++ b/crates/ty_project/src/metadata/options.rs @@ -202,6 +202,7 @@ impl Options { SysPrefixPathOrigin::ConfigFileSetting(path.clone(), python_path.range()) } ValueSource::Editor => SysPrefixPathOrigin::Editor, + ValueSource::UvWorkspace => SysPrefixPathOrigin::UvWorkspace, }; PythonEnvironment::new(python_path.absolute(project_root, system), origin, system) @@ -252,9 +253,15 @@ impl Options { let real_stdlib_path = python_environment.as_ref().and_then(|python_environment| { // For now this is considered non-fatal, we don't Need this for anything. - python_environment.real_stdlib_path(system).map_err(|err| { - tracing::info!("No real stdlib found, stdlib goto-definition may have degraded quality: {err}"); - }).ok() + python_environment + .real_stdlib_path(system) + .map_err(|err| { + tracing::info!( + "No real stdlib found, stdlib goto-definition \ + may have degraded quality: {err}" + ); + }) + .ok() }); let python_version = configured_python_version @@ -311,14 +318,8 @@ impl Options { strategy: &Strategy, ) -> 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()?))) - { + let environment_roots = if let Some(roots) = environment.root.as_deref() { roots .iter() .map(|root| root.absolute(project_root, system)) @@ -336,7 +337,8 @@ impl Options { let src = project_root.join("src"); if system.is_directory(&src) && !is_package(&src) { tracing::debug!( - "Including `./src` in `environment.root` because a `./src` directory exists and is not a package" + "Including `./src` in `environment.root` \ + because a `./src` directory exists and is not a package" ); roots.push(src); } @@ -349,7 +351,9 @@ impl Options { && !roots.contains(&project_name_dir) { tracing::debug!( - "Including `./{project_name}` in `environment.root` because a `./{project_name}/{project_name}` directory exists and `./{project_name}` is not a package" + "Including `./{project_name}` in `environment.root` because a \ + `./{project_name}/{project_name}` directory exists \ + and `./{project_name}` is not a package" ); roots.push(project_name_dir); } @@ -359,7 +363,8 @@ impl Options { let python = project_root.join("python"); if system.is_directory(&python) && !is_package(&python) && !roots.contains(&python) { tracing::debug!( - "Including `./python` in `environment.root` because a `./python` directory exists and is not a package" + "Including `./python` in `environment.root` \ + because a `./python` directory exists and is not a package" ); roots.push(python); } @@ -390,7 +395,8 @@ impl Options { Ok(path) => path, Err(path) => { tracing::debug!( - "Skipping `{path}` listed in `PYTHONPATH` because the path is not valid UTF-8", + "Skipping `{path}` listed in `PYTHONPATH` \ + because the path is not valid UTF-8", path = path.display() ); continue; @@ -401,13 +407,15 @@ impl Options { if !system.is_directory(&abspath) { tracing::debug!( - "Skipping `{abspath}` listed in `PYTHONPATH` because the path doesn't exist or isn't a directory" + "Skipping `{abspath}` listed in `PYTHONPATH` \ + because the path doesn't exist or isn't a directory" ); continue; } tracing::debug!( - "Adding `{abspath}` from the `PYTHONPATH` environment variable to `extra_paths`" + "Adding `{abspath}` from the `PYTHONPATH` environment variable \ + to `extra_paths`" ); extra_paths.push(abspath); @@ -416,7 +424,7 @@ impl Options { let settings = SearchPathSettings { extra_paths, - src_roots, + src_roots: environment_roots, custom_typeshed: environment .typeshed .as_ref() @@ -449,34 +457,6 @@ impl Options { 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( - SubDiagnosticSeverity::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 { @@ -572,6 +552,7 @@ fn python_version_from_config( PythonVersionFileSource::new(path.clone(), ranged_version.range()), ), ValueSource::Editor => PythonVersionSource::Editor, + ValueSource::UvWorkspace => PythonVersionSource::UvWorkspace, }, } } @@ -618,7 +599,7 @@ pub enum ProgramSettingsDiagnostic { impl ProgramSettingsDiagnostic { /// Convert this program-settings diagnostic into a diagnostic that can be stored on a project. - pub(crate) fn into_diagnostic(self, db: &dyn Db) -> OptionDiagnostic { + pub fn into_diagnostic(self, db: &dyn Db) -> OptionDiagnostic { match self { Self::UnsupportedInferredPythonVersion(python_version) => { unsupported_inferred_python_version_diagnostic(db, &python_version) @@ -641,7 +622,8 @@ fn unsupported_inferred_python_version_diagnostic( let mut diagnostic = OptionDiagnostic::new( DiagnosticId::UnsupportedPythonVersion, format!( - "Ignoring unsupported inferred Python version `{}`; ty will use Python {fallback} instead.", + "Ignoring unsupported inferred Python version `{}`; \ + ty will use Python {fallback} instead.", python_version.version ), Severity::Warning, @@ -682,7 +664,8 @@ fn unsupported_inferred_python_version_diagnostic( .sub(SubDiagnostic::new( SubDiagnosticSeverity::Info, format!( - "The version was inferred from the `lib/{site_packages_parent_dir}/site-packages` directory layout.", + "The version was inferred from the \ + `lib/{site_packages_parent_dir}/site-packages` directory layout.", ), )), PythonVersionSource::Cli => diagnostic.sub(SubDiagnostic::new( @@ -693,6 +676,10 @@ fn unsupported_inferred_python_version_diagnostic( SubDiagnosticSeverity::Info, "The version was inferred from your editor.", )), + PythonVersionSource::UvWorkspace => diagnostic.sub(SubDiagnostic::new( + SubDiagnosticSeverity::Info, + "The version was provided by uv workspace metadata.", + )), PythonVersionSource::Default => diagnostic.sub(SubDiagnostic::new( SubDiagnosticSeverity::Info, "ty fell back to its default Python version.", @@ -911,26 +898,6 @@ pub struct EnvironmentOptions { #[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. - /// The project root (`.`) is always included. Additionally, the following directories are included - /// if they exist and are not packages (i.e. they do not contain `__init__.py` or `__init__.pyi` files): - /// - /// * `./src` - /// * `./` (if a `.//` directory exists) - /// * `./python` - #[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. @@ -944,6 +911,18 @@ pub struct SrcOptions { #[serde(skip_serializing_if = "Option::is_none")] pub respect_ignore_files: Option, + /// Whether to exclude files containing PEP 723 inline script metadata unless they are + /// explicitly passed on the command line. + #[option( + default = r#"false"#, + value_type = r#"bool"#, + example = r#" + exclude-scripts = true + "# + )] + #[serde(skip_serializing_if = "Option::is_none")] + pub exclude_scripts: 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) @@ -1063,6 +1042,7 @@ impl SrcOptions { Ok(SrcSettings { respect_ignore_files: self.respect_ignore_files.unwrap_or(true), + exclude_scripts: self.exclude_scripts.unwrap_or(false), files, }) } @@ -1092,7 +1072,7 @@ impl FromIterator<(RangedValue, RangedValue)> for Rules { impl Rules { /// Convert the rules to a `RuleSelection` with diagnostics. - pub fn to_rule_selection( + pub(crate) fn to_rule_selection( &self, db: &dyn Db, diagnostics: &mut Vec, @@ -1108,6 +1088,7 @@ impl Rules { ValueSource::File(_) => LintSource::File, ValueSource::Cli => LintSource::Cli, ValueSource::Editor => LintSource::Editor, + ValueSource::UvWorkspace => LintSource::UvWorkspace, }; let mut set_lint_level = |lint| { @@ -1156,7 +1137,7 @@ impl Rules { selection } - pub(super) fn is_empty(&self) -> bool { + fn is_empty(&self) -> bool { self.inner.is_empty() } } @@ -1208,7 +1189,8 @@ fn build_include_filter( ) .sub(SubDiagnostic::new( SubDiagnosticSeverity::Info, - "Remove the `include` option to match all files or add a pattern to match specific files", + "Remove the `include` option to match all files \ + or add a pattern to match specific files", )); // Add source annotation if we have source information @@ -1258,12 +1240,16 @@ fn build_include_filter( includes.build().map_err(|_| { let diagnostic = OptionDiagnostic::new( DiagnosticId::InvalidGlob, - format!("The `{}` patterns resulted in a regex that is too large", context.include_name()), + format!( + "The `{}` patterns resulted in a regex that is too large", + context.include_name() + ), Severity::Error, ); Box::new(diagnostic.sub(SubDiagnostic::new( SubDiagnosticSeverity::Info, - "Please open an issue on the ty repository and share the patterns that caused the error.", + "Please open an issue on the ty repository \ + and share the patterns that caused the error.", ))) }) } @@ -1316,12 +1302,16 @@ fn build_exclude_filter( excludes.build().map_err(|_| { let diagnostic = OptionDiagnostic::new( DiagnosticId::InvalidGlob, - format!("The `{}` patterns resulted in a regex that is too large", context.exclude_name()), + format!( + "The `{}` patterns resulted in a regex that is too large", + context.exclude_name() + ), Severity::Error, ); Box::new(diagnostic.sub(SubDiagnostic::new( SubDiagnosticSeverity::Info, - "Please open an issue on the ty repository and share the patterns that caused the error.", + "Please open an issue on the ty repository \ + and share the patterns that caused the error.", ))) }) } @@ -1515,6 +1505,32 @@ pub struct RunOptions { #[serde(rename_all = "kebab-case", deny_unknown_fields)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct AnalysisOptions { + /// Whether ty should use strict narrowing for unspecialized generic classes in + /// `isinstance()` and `issubclass()` checks, as well as `match` class patterns. + /// + /// When enabled, ty narrows to the top materialization of the class. For example, + /// `isinstance(value, list)` narrows a value of type `object` to `Top[list[Unknown]]`, + /// representing the (infinite) union of all possible `list` specializations. Iterating + /// over the list would yield values of type `object`. + /// + /// When disabled, ty uses gradual generic narrowing, preserving compatible type + /// arguments from the original type where possible. For example, + /// `isinstance(value, list)` narrows a value of type `Sequence[int]` to `list[int]`. + /// If no specialization is available, the same check narrows a value of type `object` + /// to `list[Unknown]`; items of any type can then be appended to the list. Class + /// patterns such as `case list():` follow the same behavior. + /// + /// Defaults to `false`. + #[option( + default = r#"false"#, + value_type = "bool", + example = r#" + # Use the top materialization when narrowing to an unspecialized generic class + strict-generic-narrowing = true + "# + )] + pub strict_generic_narrowing: Option, + /// Configure ty's behavior regarding type inference and narrowing of equality /// checks. Defaults to `false`. /// @@ -1986,6 +2002,7 @@ impl AnalysisOptions { diagnostics: &mut Vec, ) -> AnalysisSettings { let Self { + strict_generic_narrowing, strict_equality_semantics, respect_type_ignore_comments, allowed_unresolved_imports, @@ -2005,6 +2022,7 @@ impl AnalysisOptions { } = self; let AnalysisSettings { + strict_generic_narrowing: strict_generic_narrowing_default, strict_equality_semantics: strict_equality_semantics_default, respect_type_ignore_comments: respect_type_ignore_default, allowed_unresolved_imports: allowed_unresolved_imports_default, @@ -2047,6 +2065,8 @@ impl AnalysisOptions { }; AnalysisSettings { + strict_generic_narrowing: strict_generic_narrowing + .unwrap_or(strict_generic_narrowing_default), strict_equality_semantics: strict_equality_semantics .unwrap_or(strict_equality_semantics_default), respect_type_ignore_comments: respect_type_ignore_comments @@ -2192,7 +2212,8 @@ fn build_module_glob_set( Box::new(diagnostic.sub(SubDiagnostic::new( SubDiagnosticSeverity::Info, - "Please open an issue on the ty repository and share the patterns that caused the error.", + "Please open an issue on the ty repository \ + and share the patterns that caused the error.", ))) }) } @@ -2292,7 +2313,7 @@ pub struct OverrideOptions { ] "# )] - pub include: Option>>, + include: Option>>, /// A list of file and directory patterns to exclude from this override. /// @@ -2314,7 +2335,7 @@ pub struct OverrideOptions { ] "# )] - pub exclude: Option>>, + exclude: Option>>, /// Rule overrides for files matching the include/exclude patterns. /// @@ -2333,11 +2354,11 @@ pub struct OverrideOptions { possibly-unresolved-reference = "ignore" "# )] - pub rules: Option, + rules: Option, #[serde(skip_serializing_if = "Option::is_none")] #[option_group] - pub analysis: Option, + analysis: Option, } trait ToOverride { @@ -2455,7 +2476,9 @@ impl ToOverride for RangedValue { diagnostic = diagnostic.sub(SubDiagnostic::new( SubDiagnosticSeverity::Info, - "or remove the `[[overrides]]` section and merge the configuration into the root `[rules]` table if the configuration should apply to all files", + "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 @@ -2553,7 +2576,7 @@ pub struct ToSettingsError { } impl ToSettingsError { - pub fn pretty<'a>(&'a self, db: &'a dyn Db) -> impl fmt::Display + use<'a> { + pub(crate) fn pretty<'a>(&'a self, db: &'a dyn Db) -> impl fmt::Display + use<'a> { let db: &dyn ruff_db::Db = db; fmt::from_fn(move |f| { @@ -2571,7 +2594,7 @@ impl ToSettingsError { }) } - pub fn into_diagnostic(self) -> OptionDiagnostic { + pub(crate) fn into_diagnostic(self) -> OptionDiagnostic { *self.diagnostic } } @@ -2636,7 +2659,8 @@ mod schema { all.insert( "description".to_string(), Value::String( - "Configure a default severity level for all rules. Individual rule settings override this default." + "Configure a default severity level for all rules. \ + Individual rule settings override this default." .to_string(), ), ); @@ -2676,7 +2700,7 @@ pub struct OptionDiagnostic { } impl OptionDiagnostic { - pub fn new(id: DiagnosticId, message: String, severity: Severity) -> Self { + fn new(id: DiagnosticId, message: String, severity: Severity) -> Self { Self { id, message, @@ -2746,6 +2770,10 @@ impl OptionDiagnostic { SubDiagnosticSeverity::Info, "The {value_label} was specified in the editor settings.", )), + ValueSource::UvWorkspace => self.sub(SubDiagnostic::new( + SubDiagnosticSeverity::Info, + format!("The {value_label} was provided by uv workspace metadata."), + )), } } diff --git a/crates/ty_project/src/metadata/pyproject.rs b/crates/ty_project/src/metadata/pyproject.rs index 75935264a0..5af6152f59 100644 --- a/crates/ty_project/src/metadata/pyproject.rs +++ b/crates/ty_project/src/metadata/pyproject.rs @@ -260,11 +260,11 @@ pub struct Project { /// /// Note: Intentionally option to be more permissive during deserialization. /// `PackageMetadata::from_pyproject` reports missing names. - pub name: Option>, + pub(crate) name: Option>, /// The version of the project - pub version: Option>, + pub(crate) version: Option>, /// The Python versions this project is compatible with. - pub requires_python: Option>, + pub(crate) requires_python: Option>, /// The requirements installed alongside the project. /// /// Kept as written rather than as parsed requirements: one entry ty cannot @@ -395,7 +395,7 @@ pub struct PackageName(String); impl PackageName { /// Create a validated, normalized package name. - pub(crate) fn new(name: String) -> Result { + fn new(name: String) -> Result { if name.is_empty() { return Err(InvalidPackageNameError::Empty); } @@ -448,7 +448,7 @@ impl PackageName { } /// Returns the underlying package name. - pub(crate) fn as_str(&self) -> &str { + fn as_str(&self) -> &str { &self.0 } } diff --git a/crates/ty_project/src/metadata/python_version.rs b/crates/ty_project/src/metadata/python_version.rs index 8bac1a479f..c2ea830d3b 100644 --- a/crates/ty_project/src/metadata/python_version.rs +++ b/crates/ty_project/src/metadata/python_version.rs @@ -51,7 +51,7 @@ pub enum SupportedPythonVersion { } impl SupportedPythonVersion { - pub const fn as_str(self) -> &'static str { + const fn as_str(self) -> &'static str { match self { Self::Py37 => "3.7", Self::Py38 => "3.8", @@ -65,7 +65,7 @@ impl SupportedPythonVersion { } } - pub const fn to_python_version(self) -> PythonVersion { + pub(crate) const fn to_python_version(self) -> PythonVersion { match self { Self::Py37 => PythonVersion::PY37, Self::Py38 => PythonVersion::PY38, diff --git a/crates/ty_project/src/metadata/script.rs b/crates/ty_project/src/metadata/script.rs index 2ec036a57e..1800ddc9b5 100644 --- a/crates/ty_project/src/metadata/script.rs +++ b/crates/ty_project/src/metadata/script.rs @@ -1,27 +1,15 @@ -use std::sync::{Arc, LazyLock}; +use std::sync::Arc; -use memchr::memmem::Finder; use ruff_db::Db; use ruff_db::files::File; -use ruff_db::parsed::parsed_module; use ruff_db::source::source_text; use ruff_db::system::SystemPathBuf; use ruff_python_ast::script::ScriptTag; -use ruff_python_ast::token::TokenKind; use ruff_ranged_value::ValueSource; -use ruff_text_size::Ranged; use crate::metadata::pyproject::PyProject; -const SCRIPT_TAG: &str = "# /// script"; -static SCRIPT_TAG_FINDER: LazyLock> = - LazyLock::new(|| Finder::new(SCRIPT_TAG.as_bytes())); - /// Returns the PEP 723 metadata embedded in `file`. -/// -/// The byte search keeps the overwhelmingly common non-script path cheap. Parsing is only -/// necessary after finding a possible opening tag at the start of a line, where the token stream -/// disambiguates an actual comment from the same text inside a string literal. #[salsa::tracked(returns(ref))] pub(crate) fn script_metadata(db: &dyn Db, file: File) -> Option> { let path = file.path(db); @@ -34,26 +22,7 @@ pub(crate) fn script_metadata(db: &dyn Db, file: File) -> Option> return None; } - let source_bytes = source.as_bytes(); - let mut candidates = SCRIPT_TAG_FINDER - .find_iter(source_bytes) - .filter(|&offset| offset == 0 || matches!(source_bytes[offset - 1], b'\r' | b'\n')); - let first_candidate = candidates.next()?; - - let parsed = parsed_module(db, file).load(db); - let tokens = parsed.tokens(); - let tag = std::iter::once(first_candidate) - .chain(candidates) - .filter(|&offset| { - let Ok(index) = tokens.binary_search_by_key(&offset, |token| token.start().to_usize()) - else { - return false; - }; - let token = &tokens[index]; - - token.kind() == TokenKind::Comment && &source[token.range()] == SCRIPT_TAG - }) - .find_map(|opening| ScriptTag::parse_at(source_bytes, opening))?; + let tag = ScriptTag::parse(source.as_bytes())?; let value_source = ValueSource::File(Arc::new(SystemPathBuf::from(path.as_str()))); PyProject::from_toml_str_without_spans(tag.metadata(), value_source) diff --git a/crates/ty_project/src/metadata/settings.rs b/crates/ty_project/src/metadata/settings.rs index 4164fe8671..6e82c81d34 100644 --- a/crates/ty_project/src/metadata/settings.rs +++ b/crates/ty_project/src/metadata/settings.rs @@ -38,7 +38,7 @@ pub struct Settings { } impl Settings { - pub fn rules(&self) -> &RuleSelection { + fn rules(&self) -> &RuleSelection { &self.rules } @@ -46,7 +46,7 @@ impl Settings { &self.src } - pub fn to_rules(&self) -> Arc { + pub(crate) fn to_rules(&self) -> Arc { self.rules.clone() } @@ -54,11 +54,11 @@ impl Settings { &self.terminal } - pub fn overrides(&self) -> &[Override] { + fn overrides(&self) -> &[Override] { &self.overrides } - pub fn analysis(&self) -> &AnalysisSettings { + pub(crate) fn analysis(&self) -> &AnalysisSettings { &self.analysis } } @@ -81,12 +81,14 @@ impl Default for TerminalSettings { #[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)] pub struct SrcSettings { pub respect_ignore_files: bool, - pub files: IncludeExcludeFilter, + pub(crate) exclude_scripts: bool, + pub(crate) files: IncludeExcludeFilter, } impl SrcSettings { pub(crate) fn default() -> Self { Self { respect_ignore_files: true, + exclude_scripts: false, files: IncludeExcludeFilter::default(), } } @@ -109,7 +111,7 @@ pub struct Override { impl Override { /// Returns whether this override applies to the given file path. - pub fn matches_file(&self, path: &ruff_db::system::SystemPath) -> bool { + fn matches_file(&self, path: &ruff_db::system::SystemPath) -> bool { use crate::glob::{GlobFilterCheckMode, IncludeResult}; matches!( @@ -256,14 +258,14 @@ pub enum FileSettings { } impl FileSettings { - pub fn rules<'a>(&'a self, db: &'a dyn Db) -> &'a RuleSelection { + pub(crate) 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, } } - pub fn analysis<'a>(&'a self, db: &'a dyn Db) -> &'a AnalysisSettings { + pub(crate) fn analysis<'a>(&'a self, db: &'a dyn Db) -> &'a AnalysisSettings { match self { FileSettings::Global => db.project().settings(db).analysis(), FileSettings::File(override_settings) => &override_settings.analysis, diff --git a/crates/ty_project/src/metadata/uv.rs b/crates/ty_project/src/metadata/uv.rs new file mode 100644 index 0000000000..8bda3071e1 --- /dev/null +++ b/crates/ty_project/src/metadata/uv.rs @@ -0,0 +1,273 @@ +use std::path::PathBuf; + +use pep440_rs::Version; +use ruff_db::system::{Command, System, SystemPath, SystemPathBuf, WhichError}; +use ruff_ranged_value::{RangedValue, ValueSource}; +use serde::Deserialize; +use thiserror::Error; +use ty_static::EnvVars; + +use super::python_version::SupportedPythonVersion; + +#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)] +pub(super) struct UvWorkspace { + root: SystemPathBuf, + environment: Option, + python_version: Option>, +} + +impl UvWorkspace { + pub(super) fn discover( + path: &SystemPath, + system: &dyn System, + ) -> Result { + let uv = match system.env_var(EnvVars::UV) { + Ok(uv) => uv, + Err(_) => system + .which("uv") + .map(SystemPathBuf::into_string) + .map_err(uv_executable_error) + .map_err(UvWorkspaceError::Invocation)?, + }; + + // `uv check` has already selected and synchronized the environment. Keep this query + // read-only so package selection and `--isolated` aren't overwritten by a second sync. + let mut command = Command::new(uv); + command + .args(["workspace", "metadata", "--frozen", "--active"]) + .current_dir(path); + let output = system + .run_command(command) + .map_err(UvWorkspaceError::Invocation)?; + + if !output.status.success() { + return Err(UvWorkspaceError::CommandFailed { + status: output.status, + stderr: String::from_utf8_lossy(&output.stderr).into_owned(), + }); + } + + Self::from_metadata(&output.stdout, system) + } + + pub(super) fn from_metadata( + metadata: &[u8], + system: &dyn System, + ) -> Result { + let metadata = serde_json::from_slice::(metadata) + .map_err(UvWorkspaceError::InvalidMetadata)?; + + let root = existing_directory(metadata.workspace_root, "workspace root", system)?; + + let (environment, python_version) = match metadata.environment { + Some(environment) => ( + Some(existing_directory( + environment.root, + "environment root", + system, + )?), + Some(resolve_python_version(&environment.python.version)?), + ), + None => (None, None), + }; + + Ok(Self { + root, + environment, + python_version, + }) + } + + pub(super) fn root(&self) -> &SystemPath { + &self.root + } + + pub(super) fn environment(&self) -> Option<&SystemPath> { + self.environment.as_deref() + } + + pub(super) fn python_version(&self) -> Option<&RangedValue> { + self.python_version.as_ref() + } +} + +fn uv_executable_error(error: WhichError) -> std::io::Error { + std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("failed to resolve uv executable: {error}"), + ) +} + +fn resolve_python_version( + version: &Version, +) -> Result, UvWorkspaceError> { + let [major, minor, ..] = version.release() else { + return Err(UvWorkspaceError::InvalidPythonVersion(version.clone())); + }; + let version = format!("{major}.{minor}") + .parse::() + .map_err(|_| UvWorkspaceError::InvalidPythonVersion(version.clone()))?; + + Ok(RangedValue::new(version, ValueSource::UvWorkspace)) +} + +fn existing_directory( + path: PathBuf, + description: &'static str, + system: &dyn System, +) -> Result { + let path = match SystemPathBuf::from_path_buf(path) { + Ok(path) => path, + Err(path) => return Err(UvWorkspaceError::NonUnicodePath { description, path }), + }; + + if !system.is_directory(&path) { + return Err(UvWorkspaceError::MissingDirectory { description, path }); + } + + Ok(path) +} + +#[derive(Debug, Error)] +pub(super) enum UvWorkspaceError { + #[error("Failed to invoke `uv workspace metadata`: {0}")] + Invocation(#[source] std::io::Error), + + #[error("`uv workspace metadata` failed with status {status}: {stderr}")] + CommandFailed { + status: std::process::ExitStatus, + stderr: String, + }, + + #[error("invalid `uv workspace metadata` JSON: {0}")] + InvalidMetadata(serde_json::Error), + + #[error("unsupported Python version `{0}` returned by `uv workspace metadata`")] + InvalidPythonVersion(Version), + + #[error("non-Unicode {description} returned by `uv workspace metadata`: `{path}`", path = path.display())] + NonUnicodePath { + description: &'static str, + path: PathBuf, + }, + + #[error("missing {description} returned by `uv workspace metadata`: `{path}`")] + MissingDirectory { + description: &'static str, + path: SystemPathBuf, + }, +} + +#[derive(Deserialize)] +struct WorkspaceMetadata { + workspace_root: PathBuf, + environment: Option, +} + +#[derive(Deserialize)] +struct WorkspaceEnvironment { + root: PathBuf, + python: WorkspacePython, +} + +#[derive(Deserialize)] +struct WorkspacePython { + version: Version, +} + +#[cfg(test)] +mod tests { + use ruff_db::system::{SystemPath, TestSystem}; + use ty_static::EnvVars; + + use super::{UvWorkspace, UvWorkspaceError}; + + #[test] + fn rejects_invalid_metadata() { + let system = TestSystem::default(); + + assert!(matches!( + UvWorkspace::from_metadata(b"{", &system), + Err(UvWorkspaceError::InvalidMetadata(_)) + )); + } + + #[test] + fn explicit_uv_override_skips_path_lookup() { + let system = TestSystem::default(); + system.set_env_var(EnvVars::UV, "/custom/uv"); + + assert!(matches!( + UvWorkspace::discover(SystemPath::new("/app"), &system), + Err(UvWorkspaceError::Invocation(error)) + if error.kind() == std::io::ErrorKind::Unsupported + )); + } + + #[test] + fn environment_can_be_omitted() -> anyhow::Result<()> { + let system = TestSystem::default(); + system + .memory_file_system() + .write_file_all("/app/pyproject.toml", "[tool.uv.workspace]")?; + let metadata = br#"{ + "workspace_root": "/app" + }"#; + + let workspace = UvWorkspace::from_metadata(metadata, &system)?; + + assert!(workspace.environment().is_none()); + assert!(workspace.python_version().is_none()); + + Ok(()) + } + + #[test] + fn uses_environment_python_version() -> anyhow::Result<()> { + let system = TestSystem::default(); + system.memory_file_system().write_files_all([ + ("/app/pyproject.toml", "[tool.uv.workspace]"), + ("/env/marker", ""), + ])?; + let metadata = br#"{ + "workspace_root": "/app", + "environment": { + "root": "/env", + "python": { "version": "3.13.5" } + } + }"#; + + let workspace = UvWorkspace::from_metadata(metadata, &system)?; + + assert_eq!(workspace.environment(), Some(SystemPath::new("/env"))); + assert_eq!( + workspace.python_version().map(ToString::to_string), + Some("3.13".to_string()) + ); + + Ok(()) + } + + #[test] + fn rejects_unsupported_environment_python_version() -> anyhow::Result<()> { + let system = TestSystem::default(); + system.memory_file_system().write_files_all([ + ("/app/pyproject.toml", "[tool.uv.workspace]"), + ("/env/marker", ""), + ])?; + let metadata = br#"{ + "workspace_root": "/app", + "environment": { + "root": "/env", + "python": { "version": "3.16.0" } + } + }"#; + + assert!(matches!( + UvWorkspace::from_metadata(metadata, &system), + Err(UvWorkspaceError::InvalidPythonVersion(_)) + )); + + Ok(()) + } +} diff --git a/crates/ty_project/src/metadata/value.rs b/crates/ty_project/src/metadata/value.rs index 29dc903302..616a118080 100644 --- a/crates/ty_project/src/metadata/value.rs +++ b/crates/ty_project/src/metadata/value.rs @@ -7,7 +7,6 @@ use ruff_macros::Combine; use ruff_ranged_value::{RangedValue, ValueSource}; use ruff_text_size::TextRange; -use crate::Db; use crate::glob::{ AbsolutePortableGlobPattern, PortableGlobError, PortableGlobKind, PortableGlobPattern, }; @@ -37,7 +36,7 @@ use crate::glob::{ pub struct RelativePathBuf(RangedValue); impl RelativePathBuf { - pub fn new(path: impl AsRef, source: ValueSource) -> Self { + pub(crate) fn new(path: impl AsRef, source: ValueSource) -> Self { Self(RangedValue::new(path.as_ref().to_path_buf(), source)) } @@ -54,29 +53,21 @@ impl RelativePathBuf { &self.0 } - pub fn source(&self) -> &ValueSource { + pub(crate) fn source(&self) -> &ValueSource { self.0.source() } - pub fn range(&self) -> Option { + pub(crate) fn range(&self) -> Option { self.0.range() } - /// Returns the owned relative path. - pub fn into_path_buf(self) -> SystemPathBuf { - self.0.into_inner() - } - - /// Resolves the absolute path for `self` based on its origin. - pub fn absolute_with_db(&self, db: &dyn Db) -> SystemPathBuf { - self.absolute(db.project().root(db), db.system()) - } - /// Resolves the absolute path for `self` based on its origin. pub fn absolute(&self, project_root: &SystemPath, system: &dyn System) -> SystemPathBuf { let relative_to = match self.0.source() { ValueSource::File(_) => project_root, - ValueSource::Cli | ValueSource::Editor => system.current_directory(), + ValueSource::Cli | ValueSource::Editor | ValueSource::UvWorkspace => { + system.current_directory() + } }; // Expand tildes and environment variables in the path (e.g. `~/.cache/foo`). @@ -129,7 +120,7 @@ impl fmt::Display for RelativePathBuf { pub struct RelativeGlobPattern(RangedValue); impl RelativeGlobPattern { - pub fn new(pattern: impl AsRef, source: ValueSource) -> Self { + fn new(pattern: impl AsRef, source: ValueSource) -> Self { Self(RangedValue::new(pattern.as_ref().to_string(), source)) } @@ -146,7 +137,9 @@ impl RelativeGlobPattern { ) -> Result { let relative_to = match self.0.source() { ValueSource::File(_) => project_root, - ValueSource::Cli | ValueSource::Editor => system.current_directory(), + ValueSource::Cli | ValueSource::Editor | ValueSource::UvWorkspace => { + system.current_directory() + } }; let pattern = PortableGlobPattern::parse(&self.0, kind)?; diff --git a/crates/ty_project/src/walk.rs b/crates/ty_project/src/walk.rs index 98e4041549..57a89b235b 100644 --- a/crates/ty_project/src/walk.rs +++ b/crates/ty_project/src/walk.rs @@ -1,4 +1,5 @@ use crate::glob::IncludeExcludeFilter; +use crate::metadata::script::script_metadata; use crate::{Db, GlobFilterCheckMode, IncludeResult, Project}; use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity}; use ruff_db::files::{File, system_path_to_file}; @@ -35,7 +36,7 @@ impl<'a> ProjectFilesFilter<'a> { } } - pub(crate) fn force_exclude(&self) -> bool { + fn force_exclude(&self) -> bool { self.force_exclude } @@ -167,6 +168,7 @@ impl ProjectFilesWalker { }; let filter = ProjectFilesFilter::from_project(db, project); + let exclude_scripts = project.settings(db).src().exclude_scripts; let files = std::sync::Mutex::new(Vec::new()); let diagnostics = std::sync::Mutex::new(Vec::new()); @@ -271,6 +273,18 @@ impl ProjectFilesWalker { // If this returns `Err`, then the file was deleted between now and when the walk callback was called. // We can ignore this. if let Ok(file) = system_path_to_file(&*db, entry.path()) { + if entry.depth() > 0 + && exclude_scripts + && script_metadata(&*db, file).is_some() + { + tracing::debug!( + "Ignoring implicitly discovered PEP 723 script `{path}` \ + because `exclude-scripts` is enabled.", + path = entry.path() + ); + return WalkState::Skip; + } + files.lock().unwrap().push(file); } } diff --git a/crates/ty_project/src/watch.rs b/crates/ty_project/src/watch.rs index de4fe6526a..b22596b99a 100644 --- a/crates/ty_project/src/watch.rs +++ b/crates/ty_project/src/watch.rs @@ -74,7 +74,7 @@ impl ChangeEvent { self.system_path().and_then(|path| path.file_name()) } - pub fn system_path(&self) -> Option<&SystemPath> { + pub(crate) fn system_path(&self) -> Option<&SystemPath> { match self { ChangeEvent::Opened(path) | ChangeEvent::Created { path, .. } @@ -151,7 +151,7 @@ impl ExistingPathKind { } } - pub fn from_io_metadata(metadata: &std::io::Result) -> Self { + fn from_io_metadata(metadata: &std::io::Result) -> Self { match metadata { Ok(metadata) if metadata.is_file() => Self::File, Ok(metadata) if metadata.is_dir() => Self::Directory, diff --git a/crates/ty_project/src/watch/project_watcher.rs b/crates/ty_project/src/watch/project_watcher.rs index 92d7451d83..4b5f8afef2 100644 --- a/crates/ty_project/src/watch/project_watcher.rs +++ b/crates/ty_project/src/watch/project_watcher.rs @@ -40,7 +40,8 @@ impl ProjectWatcher { } pub fn update(&mut self, db: &ProjectDatabase) { - let search_paths: Vec<_> = system_module_search_paths(db).collect(); + let environment = db.project().program(db).resolver_environment(db); + let search_paths: Vec<_> = system_module_search_paths(db, environment).collect(); let project_path = db.project().root(db); let new_cache_key = Self::compute_cache_key(project_path, &search_paths); diff --git a/crates/ty_project/src/watch/watcher.rs b/crates/ty_project/src/watch/watcher.rs index 1802de36ae..ba016f59ba 100644 --- a/crates/ty_project/src/watch/watcher.rs +++ b/crates/ty_project/src/watch/watcher.rs @@ -112,24 +112,8 @@ struct WatcherInner { } impl Watcher { - /// Sets up file watching for `path`. - pub fn watch(&mut self, path: &SystemPath) -> notify::Result<()> { - tracing::debug!("Watching path: `{path}`"); - - self.inner_mut() - .watcher - .watch(path.as_std_path(), RecursiveMode::Recursive) - } - - /// Stops file watching for `path`. - pub fn unwatch(&mut self, path: &SystemPath) -> notify::Result<()> { - tracing::debug!("Unwatching path: `{path}`"); - - self.inner_mut().watcher.unwatch(path.as_std_path()) - } - /// Returns a transaction-like view for updating watched paths in one backend operation. - pub fn paths_mut(&mut self) -> WatcherPathsMut<'_> { + pub(crate) fn paths_mut(&mut self) -> WatcherPathsMut<'_> { WatcherPathsMut { inner: self.inner_mut().watcher.paths_mut(), } @@ -140,13 +124,13 @@ impl Watcher { /// Pending events will be discarded. /// /// The call blocks until the watcher has stopped. - pub fn stop(mut self) { + pub(crate) fn stop(mut self) { tracing::debug!("Stop file watcher"); self.set_stop(); } /// Flushes any pending events. - pub fn flush(&self) { + pub(crate) fn flush(&self) { self.inner() .debouncer_sender .send(DebouncerMessage::Flush) @@ -177,22 +161,22 @@ impl Watcher { } } -pub struct WatcherPathsMut<'a> { +pub(crate) struct WatcherPathsMut<'a> { inner: Box, } impl WatcherPathsMut<'_> { - pub fn add(&mut self, path: &SystemPath) -> notify::Result<()> { + pub(crate) fn add(&mut self, path: &SystemPath) -> notify::Result<()> { tracing::debug!("Watching path: `{path}`"); self.inner.add(path.as_std_path(), RecursiveMode::Recursive) } - pub fn remove(&mut self, path: &SystemPath) -> notify::Result<()> { + pub(crate) fn remove(&mut self, path: &SystemPath) -> notify::Result<()> { tracing::debug!("Unwatching path: `{path}`"); self.inner.remove(path.as_std_path()) } - pub fn commit(self) -> notify::Result<()> { + pub(crate) fn commit(self) -> notify::Result<()> { self.inner.commit() } } diff --git a/crates/ty_python_core/Cargo.toml b/crates/ty_python_core/Cargo.toml index 6f6240c877..ca9f0e95fe 100644 --- a/crates/ty_python_core/Cargo.toml +++ b/crates/ty_python_core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_python_core" -version = "0.0.5" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } @@ -48,6 +48,7 @@ anyhow = { workspace = true } [features] serde = ["dep:serde", "dep:ruff_macros"] schemars = ["dep:schemars", "dep:serde_json"] +testing = [] [lints] workspace = true diff --git a/crates/ty_python_core/README.md b/crates/ty_python_core/README.md index 7b9226b17f..c25f0aed08 100644 --- a/crates/ty_python_core/README.md +++ b/crates/ty_python_core/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ty_python_core). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ty_python_core). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_python_core/src/ast_ids.rs b/crates/ty_python_core/src/ast_ids.rs index bcdaa3dd33..4ace4d7678 100644 --- a/crates/ty_python_core/src/ast_ids.rs +++ b/crates/ty_python_core/src/ast_ids.rs @@ -1,11 +1,11 @@ use rustc_hash::FxHashMap; -use ruff_db::files::File; use ruff_index::{IndexVec, newtype_index}; use ruff_python_ast as ast; use ruff_python_ast::ExprRef; use crate::Db; +use crate::ProgramFile; use crate::frozen::FrozenMap; use crate::scope::FileScopeId; use crate::semantic_index; @@ -55,7 +55,7 @@ impl AstIds { } } -fn ast_ids(db: &dyn Db, file: File) -> &AstIds { +fn ast_ids<'db>(db: &'db dyn Db, file: ProgramFile<'db>) -> &'db AstIds { semantic_index(db, file).ast_ids() } @@ -66,46 +66,46 @@ pub struct ScopedUseId; pub trait HasScopedUseId { /// Returns the ID that uniquely identifies the use in its scope. - fn scoped_use_id(&self, db: &dyn Db, file: File) -> ScopedUseId; + fn scoped_use_id(&self, db: &dyn Db, file: ProgramFile<'_>) -> ScopedUseId; } impl HasScopedUseId for ast::Identifier { - fn scoped_use_id(&self, db: &dyn Db, file: File) -> ScopedUseId { + fn scoped_use_id(&self, db: &dyn Db, file: ProgramFile<'_>) -> ScopedUseId { let ast_ids = ast_ids(db, file); ast_ids.use_id(self) } } impl HasScopedUseId for ast::ExprName { - fn scoped_use_id(&self, db: &dyn Db, file: File) -> ScopedUseId { + fn scoped_use_id(&self, db: &dyn Db, file: ProgramFile<'_>) -> ScopedUseId { let expression_ref = ExprRef::from(self); expression_ref.scoped_use_id(db, file) } } impl HasScopedUseId for ast::ExprAttribute { - fn scoped_use_id(&self, db: &dyn Db, file: File) -> ScopedUseId { + fn scoped_use_id(&self, db: &dyn Db, file: ProgramFile<'_>) -> ScopedUseId { let expression_ref = ExprRef::from(self); expression_ref.scoped_use_id(db, file) } } impl HasScopedUseId for ast::ExprSubscript { - fn scoped_use_id(&self, db: &dyn Db, file: File) -> ScopedUseId { + fn scoped_use_id(&self, db: &dyn Db, file: ProgramFile<'_>) -> ScopedUseId { let expression_ref = ExprRef::from(self); expression_ref.scoped_use_id(db, file) } } impl HasScopedUseId for ast::Keyword { - fn scoped_use_id(&self, db: &dyn Db, file: File) -> ScopedUseId { + fn scoped_use_id(&self, db: &dyn Db, file: ProgramFile<'_>) -> ScopedUseId { let ast_ids = ast_ids(db, file); ast_ids.use_id(self) } } impl HasScopedUseId for ast::ExprRef<'_> { - fn scoped_use_id(&self, db: &dyn Db, file: File) -> ScopedUseId { + fn scoped_use_id(&self, db: &dyn Db, file: ProgramFile<'_>) -> ScopedUseId { let ast_ids = ast_ids(db, file); ast_ids.use_id(*self) } diff --git a/crates/ty_python_core/src/ast_node_ref.rs b/crates/ty_python_core/src/ast_node_ref.rs index 81161a5179..d730d15b38 100644 --- a/crates/ty_python_core/src/ast_node_ref.rs +++ b/crates/ty_python_core/src/ast_node_ref.rs @@ -4,6 +4,8 @@ use std::marker::PhantomData; #[cfg(debug_assertions)] use ruff_db::files::File; use ruff_db::parsed::ParsedModuleRef; +#[cfg(debug_assertions)] +use ruff_python_ast::PythonVersion; use ruff_python_ast::{AnyNodeRef, NodeIndex}; use ruff_python_ast::{AnyRootNodeRef, HasNodeIndex}; use ruff_text_size::Ranged; @@ -47,6 +49,8 @@ pub struct AstNodeRef { // AST. #[cfg(debug_assertions)] file: File, + #[cfg(debug_assertions)] + python_version: PythonVersion, _node: PhantomData, } @@ -66,7 +70,7 @@ where /// 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. + /// different file, Python version, 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)); @@ -76,6 +80,8 @@ where #[cfg(debug_assertions)] file: module_ref.module().file(), #[cfg(debug_assertions)] + python_version: module_ref.module().python_version(), + #[cfg(debug_assertions)] kind: AnyNodeRef::from(node).kind(), #[cfg(debug_assertions)] range: node.range(), @@ -86,12 +92,19 @@ where /// 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. + /// different file, Python version, or Salsa revision than the module to which the node belongs. #[track_caller] pub fn node<'ast>(&self, module_ref: &'ast ParsedModuleRef) -> &'ast T { #[cfg(debug_assertions)] - assert_eq!(module_ref.module().file(), self.file); - // The user guarantees that the module is from the same file and Salsa + assert_eq!( + ( + module_ref.module().file(), + module_ref.module().python_version() + ), + (self.file, self.python_version), + "an `AstNodeRef` cannot be used with a module parsed for a different file or Python version" + ); + // The user guarantees that the module is from the same file, Python version, and Salsa // revision, so the file contents cannot have changed. module_ref .get_by_index(self.index) @@ -124,3 +137,35 @@ where } } } + +#[cfg(all(test, debug_assertions))] +mod tests { + use ruff_db::PythonFile; + use ruff_db::files::system_path_to_file; + use ruff_db::parsed::parsed_module; + use ruff_python_ast::PythonVersion; + + use crate::ast_node_ref::AstNodeRef; + use crate::db::tests::TestDbBuilder; + + #[test] + #[should_panic( + expected = "an `AstNodeRef` cannot be used with a module parsed for a different file or Python version" + )] + fn rejects_module_parsed_for_different_python_version() { + let db = TestDbBuilder::new() + .with_file("test.py", "x = 1") + .build() + .unwrap(); + let file = system_path_to_file(&db, "test.py").unwrap(); + + let parsed_py311 = + parsed_module(&db, PythonFile::new(&db, file, PythonVersion::PY311)).load(&db); + let parsed_py312 = + parsed_module(&db, PythonFile::new(&db, file, PythonVersion::PY312)).load(&db); + let assignment = parsed_py311.syntax().body[0].as_assign_stmt().unwrap(); + + let node = AstNodeRef::new(&parsed_py311, assignment); + node.node(&parsed_py312); + } +} diff --git a/crates/ty_python_core/src/builder.rs b/crates/ty_python_core/src/builder.rs index 1006afca66..e61f55389d 100644 --- a/crates/ty_python_core/src/builder.rs +++ b/crates/ty_python_core/src/builder.rs @@ -9,8 +9,8 @@ use ruff_python_ast::helpers::{ }; 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; @@ -24,9 +24,10 @@ use ruff_python_parser::semantic_errors::{ }; use ruff_text_size::{Ranged, TextRange}; use smallvec::SmallVec; -use ty_module_resolver::{ModuleName, resolve_module}; +use ty_module_resolver::{ImportingFile, ModuleName, ResolverEnvironment, resolve_module}; use crate::HasTrackedScope; +use crate::ProgramFile; use crate::ast_ids::node_key::ExpressionNodeKey; use crate::ast_ids::{AstIdsBuilder, ScopedUseId}; use crate::ast_node_ref::AstNodeRef; @@ -37,8 +38,9 @@ use crate::definition::{ ExceptHandlerDefinitionNodeRef, ForStmtDefinitionNodeRef, ImportDefinitionNodeRef, ImportFromDefinitionNodeRef, ImportFromSubmoduleDefinitionNodeRef, LambdaParameterDefinitionNodeRef, LoopHeaderDefinitionNodeRef, LoopStmtRef, - MatchPatternDefinitionNodeRef, NestedBindingsDefinitionKind, ParameterDefinitionNodeRef, - StarImportDefinitionNodeRef, TypeMatchCaptureDefinitionNodeRef, WithItemDefinitionNodeRef, + MatchPatternDefinitionNodeRef, NestedBindingExecution, NestedBindingsDefinitionKind, + ParameterDefinitionNodeRef, StarImportDefinitionNodeRef, TypeMatchCaptureDefinitionNodeRef, + WithItemDefinitionNodeRef, }; use crate::expression::{Expression, ExpressionKind}; use crate::fluid::{FluidUse, FluidUseRole}; @@ -56,7 +58,6 @@ use crate::predicate::{ ScopedPredicateId, SequencePatternPredicateKind, StarImportPlaceholderPredicate, SubjectElementPatternPredicate, }; -use crate::program::Program; use crate::re_exports::exported_names; use crate::reachability_constraints::{ ReachabilityConstraintsBuilder, ScopedReachabilityConstraintId, @@ -69,8 +70,8 @@ use crate::statement::StatementInner; use crate::symbol::{ScopedSymbolId, Symbol}; use crate::unpack::{Unpack, UnpackKind, UnpackPosition, UnpackValue}; use crate::use_def::{ - EnclosingSnapshotKey, FlowSnapshot, FutureDefinitions, LiveBinding, PreviousDefinitions, - ScopedDefinitionId, ScopedEnclosingSnapshotId, UseDefMapBuilder, + EnclosingSnapshotKey, FlowSnapshot, FutureDefinitions, LiveBinding, LiveBindingStatus, + PreviousDefinitions, ScopedDefinitionId, ScopedEnclosingSnapshotId, UseDefMapBuilder, }; use crate::{Db, Statement, StatementNodeKey}; use crate::{ @@ -91,16 +92,6 @@ struct Loop { continue_states: Vec, } -impl Loop { - fn push_break(&mut self, state: FlowSnapshot) { - self.break_states.push(state); - } - - fn push_continue(&mut self, state: FlowSnapshot) { - self.continue_states.push(state); - } -} - /// A narrowing alias: a variable whose RHS is a narrowing expression /// (e.g., `is_none = x is None`). #[derive(Clone, Debug)] @@ -247,7 +238,7 @@ impl ConditionFlowSnapshot { pub(super) struct SemanticIndexBuilder<'db, 'ast> { // Builder state db: &'db dyn Db, - file: File, + file: ProgramFile<'db>, source_type: PySourceType, module: &'ast ParsedModuleRef, scope_stack: Vec>, @@ -273,6 +264,7 @@ pub(super) struct SemanticIndexBuilder<'db, 'ast> { in_type_checking_block: bool, // Used for checking semantic syntax errors + resolver_environment: ResolverEnvironment<'db>, python_version: PythonVersion, source_text: OnceCell, semantic_checker: SemanticSyntaxChecker, @@ -332,11 +324,15 @@ pub(super) struct SemanticIndexBuilder<'db, 'ast> { } impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { - pub(super) fn new(db: &'db dyn Db, file: File, module_ref: &'ast ParsedModuleRef) -> Self { + pub(super) fn new( + db: &'db dyn Db, + file: ProgramFile<'db>, + module_ref: &'ast ParsedModuleRef, + ) -> Self { let mut builder = Self { db, file, - source_type: file.source_type(db), + source_type: file.file(db).source_type(db), module: module_ref, scope_stack: Vec::new(), current_assignments: Vec::new(), @@ -375,7 +371,8 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { enclosing_snapshots: FxHashMap::default(), - python_version: Program::get(db).python_version(db), + resolver_environment: file.resolver_environment(db), + python_version: file.python_version(db), source_text: OnceCell::new(), semantic_checker: SemanticSyntaxChecker::default(), in_try: false, @@ -417,7 +414,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { } } - pub(crate) fn expect_single_definition( + fn expect_single_definition( &self, definition_key: impl Into + std::fmt::Debug + Copy, ) -> Definition<'db> { @@ -1010,7 +1007,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { symbol.name().to_string(), ), range: declaration.range, - python_version: self.python_version, + python_version: self.python_version(), }); } // This `nonlocal` is resolved. @@ -1073,7 +1070,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { self.report_semantic_error(SemanticSyntaxError { kind: SemanticSyntaxErrorKind::NonlocalWithoutBinding(name.to_string()), range: declaration.range, - python_version: self.python_version, + python_version: self.python_version(), }); } } @@ -1211,14 +1208,21 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { self.narrowing_aliases.retain(|name, alias| { // Drop aliases that narrow the reassigned place or any of its members. // e.g. `is_none = x is None and ...; x = 1` - !alias.narrowed_places.contains(&place) - // e.g. `is_none = a.x is None; a = A()` - && !associated_members - .iter() - .any(|m| alias.narrowed_places.contains(&(*m).into())) - // Drop the alias whose own variable is the reassigned place. - // e.g. `is_none = x is None; is_none = False` - && reassigned_alias_name != Some(name) + if alias.narrowed_places.contains(&place) { + return false; + } + + // e.g. `is_none = a.x is None; a = A()` + if associated_members + .iter() + .any(|m| alias.narrowed_places.contains(&(*m).into())) + { + return false; + } + + // Drop the alias whose own variable is the reassigned place. + // e.g. `is_none = x is None; is_none = False` + reassigned_alias_name != Some(name) }); } @@ -1557,9 +1561,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { ); } 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.mark_comprehension_named_target(place_id, named.target.range()); self.add_definition(place_id, named); } Some(CurrentAssignment::Comprehension { @@ -1676,13 +1678,23 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { definitions.len() }; - self.record_definition(place, definition); + self.record_definition(place, definition, None); (definition, num_definitions) } /// Records an already-created definition in the current scope. - fn record_definition(&mut self, place: ScopedPlaceId, definition: Definition<'db>) { + /// + /// `previous_definitions` controls whether a new binding replaces earlier bindings. By + /// default, ordinary assignments replace them and loop headers keep them. Comprehension + /// bindings choose explicitly because an assignment that only runs on some paths must keep + /// the earlier binding. + fn record_definition( + &mut self, + place: ScopedPlaceId, + definition: Definition<'db>, + previous_definitions: Option, + ) { let kind = definition.kind(self.db); let is_loop_header = kind.is_loop_header(); let category = kind.category(self.source_type.is_stub(), self.module); @@ -1712,16 +1724,15 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { } DefinitionCategory::Declaration => use_def.record_declaration(place, definition), DefinitionCategory::Binding => { - // Loop-header bindings don't shadow prior bindings. - let previous_definitions = if is_loop_header { + let previous = previous_definitions.unwrap_or(if is_loop_header { PreviousDefinitions::AreKept } else { PreviousDefinitions::AreShadowed - }; + }); use_def.record_binding( place, definition, - previous_definitions, + previous, FutureDefinitions::ShadowThisOne, ); if !is_loop_header { @@ -1927,6 +1938,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { place, DefinitionKind::NestedBindings(Box::new(NestedBindingsDefinitionKind { name, + execution: NestedBindingExecution::Lazy, nested_declarations: declarations, })), false, @@ -2192,6 +2204,13 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { place, DefinitionKind::NestedBindings(Box::new(NestedBindingsDefinitionKind { name, + // `Eager` models a comprehension, whose binding is one element of an + // iteration and is promoted for that reason. A block's write is an + // ordinary assignment — after `a = 2` the enclosing `a` is `2`, not + // `int` — so both kinds are `Lazy` here. Whether a `once` block's + // write shadows the prior value or unions with it is decided by the + // writeback synthesis, not by this flag. + execution: NestedBindingExecution::Lazy, nested_declarations: std::iter::once(NestedDeclaration { kind, file_scope_id: block_scope, @@ -2224,6 +2243,209 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { } } + /// Records assignment-expression bindings from a comprehension in its containing scope. + /// + /// The value expression still belongs to the comprehension scope, so the real definition + /// stays there. The synthetic definition lets the containing scope observe that binding while + /// retaining the comprehension's scope for type inference. + /// + /// ```python + /// [(last := item) for item in items] + /// print(last) # `last` is owned by this containing scope. + /// ``` + fn synthesize_comprehension_binding_definitions( + &mut self, + nested_bindings: NestedGlobalOrNonlocalDeclarations, + ) { + let mut nested_bindings = nested_bindings.into_iter().collect::>(); + nested_bindings.sort_unstable_by(|(left, _), (right, _)| left.cmp(right)); + + for (name, mut declarations) in nested_bindings { + // Ignore declarations used only to validate `nonlocal` syntax. + declarations.retain(|d| d.is_bound); + declarations.shrink_to_fit(); + let Some(first_declaration) = declarations.first().copied() else { + continue; + }; + + let binding_status = self.comprehension_binding_status(&name, &declarations); + + let symbol = self.add_symbol(name.clone()); + debug_assert!( + declarations + .iter() + .all(|declaration| declaration.is_global() == first_declaration.is_global()) + ); + self.forward_comprehension_binding(&name, first_declaration, symbol); + + let place: ScopedPlaceId = symbol.into(); + if binding_status == LiveBindingStatus::Unbound { + self.mark_place_bound(place); + continue; + } + + let definition = Definition::new( + self.db, + self.current_scope_id(), + place, + DefinitionKind::NestedBindings(Box::new(NestedBindingsDefinitionKind { + name, + execution: NestedBindingExecution::Eager, + nested_declarations: declarations, + })), + false, + ); + let previous = if binding_status == LiveBindingStatus::Bound { + PreviousDefinitions::AreShadowed + } else { + PreviousDefinitions::AreKept + }; + self.record_definition(place, definition, Some(previous)); + } + } + + /// Summarizes whether the comprehension's live exit paths bind `name`. + /// + /// For example, `value` is only possibly bound after this comprehension because the walrus is + /// skipped when `flag` is false: + /// + /// ```python + /// [(value := item) if flag else None for item in items] + /// ``` + fn comprehension_binding_status( + &mut self, + name: &str, + declarations: &[NestedDeclaration], + ) -> LiveBindingStatus { + let mut status = LiveBindingStatus::Unbound; + for declaration in declarations { + let scope_id = declaration.file_scope_id; + let Some(symbol) = self.place_tables[scope_id].symbol_id(name) else { + continue; + }; + match self.use_def_maps[scope_id].symbol_live_binding_status(symbol) { + LiveBindingStatus::Bound => return LiveBindingStatus::Bound, + LiveBindingStatus::PossiblyBound => status = LiveBindingStatus::PossiblyBound, + LiveBindingStatus::Unbound => {} + } + } + status + } + + /// Passes a walrus binding out through nested comprehensions. + /// + /// ```python + /// [[(last := item) for item in row] for row in rows] + /// print(last) # `last` belongs to the scope outside both comprehensions. + /// ``` + /// + /// Each comprehension passes the binding out one level. This preserves the order and + /// conditions under which the assignment is evaluated. + fn forward_comprehension_binding( + &mut self, + name: &Name, + first_declaration: NestedDeclaration, + symbol: ScopedSymbolId, + ) { + if self.scopes[self.current_scope()].kind() != ScopeKind::Comprehension { + return; + } + + self.current_scope_info_mut() + .nested_global_or_nonlocal_declarations + .remove(name); + + if first_declaration.is_global() { + self.current_place_table_mut() + .symbol_mut(symbol) + .mark_global(); + } else { + self.current_place_table_mut() + .symbol_mut(symbol) + .mark_nonlocal(); + } + self.current_scope_info_mut() + .this_scope_global_or_nonlocal_declarations + .entry(name.clone()) + .or_insert(first_declaration.range); + } + + /// Marks a comprehension walrus target as a write to the containing Python scope. + /// + /// The iteration variable remains local to the comprehension, while the walrus target does + /// not: + /// + /// ```python + /// [(result := item) for item in items] + /// print(result) # valid + /// print(item) # `item` is not defined here + /// ``` + fn mark_comprehension_named_target(&mut self, place: ScopedPlaceId, range: TextRange) { + if self.scopes[self.current_scope()].kind() != ScopeKind::Comprehension { + return; + } + if self.semantic_syntax_errors.borrow().iter().any(|error| { + matches!( + error.kind, + SemanticSyntaxErrorKind::ReboundComprehensionVariable + | SemanticSyntaxErrorKind::NamedExpressionInComprehensionIterable + ) && error.range.contains_range(range) + }) { + return; + } + + let Some(symbol) = place.as_symbol() else { + return; + }; + let name = self.current_place_table().symbol(symbol).name().clone(); + let Some(containing_scope) = self.scope_stack.iter().rev().find(|scope_info| { + self.scopes[scope_info.file_scope_id].kind() != ScopeKind::Comprehension + }) else { + return; + }; + + let containing_scope_id = containing_scope.file_scope_id; + let is_global = match self.scopes[containing_scope_id].kind() { + ScopeKind::Module => true, + ScopeKind::Function | ScopeKind::Lambda => self.place_tables[containing_scope_id] + .symbol_id(&name) + .is_some_and(|symbol| { + self.place_tables[containing_scope_id] + .symbol(symbol) + .is_global() + }), + // Assignment expressions are invalid in comprehensions directly contained by these + // scopes. Leave the recovered target local to the comprehension. + ScopeKind::Class | ScopeKind::TypeAlias | ScopeKind::TypeParams => return, + ScopeKind::Comprehension => return, + }; + + if is_global { + self.current_place_table_mut() + .symbol_mut(symbol) + .mark_global(); + } else { + let (containing_symbol, added) = + self.place_tables[containing_scope_id].add_symbol(Symbol::new(name.clone())); + if added { + self.use_def_maps[containing_scope_id].add_place(containing_symbol.into()); + } + + let containing_symbol = + self.place_tables[containing_scope_id].symbol_mut(containing_symbol); + if !containing_symbol.is_nonlocal() && !containing_symbol.is_bound() { + containing_symbol.mark_bound(); + } + + self.current_place_table_mut() + .symbol_mut(symbol) + .mark_nonlocal(); + } + self.current_scope_info_mut() + .this_scope_global_or_nonlocal_declarations + .insert(name, range); + } + fn record_expression_narrowing_constraint( &mut self, predicate_node: &'ast ast::Expr, @@ -2436,6 +2658,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { PredicateNode::SubjectElementPattern(_) | PredicateNode::IsNonTerminalCall(_) | PredicateNode::IsNonEmptyIterable(_) + | PredicateNode::OrPatternAlternative(_) | PredicateNode::StarImportPlaceholder(_) => { // These predicates don't narrow any places PossiblyNarrowedPlaces::default() @@ -2555,6 +2778,41 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { self.current_statements.last_mut() } + /// Return whether a pattern contains any capture that changes the current flow state. + fn pattern_has_bindings(pattern: &ast::Pattern) -> bool { + match pattern { + ast::Pattern::MatchValue(_) | ast::Pattern::MatchSingleton(_) => false, + ast::Pattern::MatchSequence(ast::PatternMatchSequence { patterns, .. }) + | ast::Pattern::MatchOr(ast::PatternMatchOr { patterns, .. }) + | ast::Pattern::MatchAnd(ast::PatternMatchAnd { patterns, .. }) => { + patterns.iter().any(Self::pattern_has_bindings) + } + ast::Pattern::MatchMapping(pattern) => { + pattern.rest.is_some() || pattern.patterns.iter().any(Self::pattern_has_bindings) + } + ast::Pattern::MatchClass(pattern) => pattern + .arguments + .patterns + .iter() + .chain( + pattern + .arguments + .keywords + .iter() + .map(|keyword| &keyword.pattern), + ) + .any(Self::pattern_has_bindings), + ast::Pattern::MatchStar(pattern) => pattern.name.is_some(), + ast::Pattern::MatchAs(pattern) => { + pattern.name.is_some() + || pattern + .pattern + .as_deref() + .is_some_and(Self::pattern_has_bindings) + } + } + } + fn predicate_kind(&mut self, pattern: &ast::Pattern) -> PatternPredicateKind<'db> { match pattern { ast::Pattern::MatchValue(pattern) => { @@ -3201,8 +3459,9 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { value, ); + let mut filtered_out_paths = Vec::new(); for if_expr in &generator.ifs { - self.visit_comprehension_filter(if_expr); + filtered_out_paths.push(self.visit_comprehension_filter(if_expr)); } for generator in generators_iter { @@ -3219,25 +3478,52 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { ); for if_expr in &generator.ifs { - self.visit_comprehension_filter(if_expr); + filtered_out_paths.push(self.visit_comprehension_filter(if_expr)); } } visit_outer_elt(self); - self.pop_scope(); + for filtered_out_path in filtered_out_paths { + self.flow_merge(filtered_out_path); + } + let nested_bindings = self.pop_scope(); + self.synthesize_comprehension_binding_definitions(nested_bindings); self.current_assignments = saved_assignments; comprehension_scope } - fn visit_comprehension_filter(&mut self, if_expr: &'ast ast::Expr) { + /// Visits a comprehension filter on its truthy path and returns the filtered-out path. + /// + /// A false filter skips the rest of the current iteration, but assignments performed while + /// evaluating the filter remain observable: + /// + /// ```python + /// [item for item in items if (last := item)] + /// print(last) + /// ``` + fn visit_comprehension_filter(&mut self, if_expr: &'ast ast::Expr) -> FlowSnapshot { self.visit_expr(if_expr); let condition_flow_snapshot = self.flow_snapshot_for_condition(if_expr); - if let Some(truthy) = condition_flow_snapshot.into_truthy() { - self.flow_restore(truthy); - } - let _ = self.record_expression_narrowing_constraint(if_expr); + let filtered_out = if let Some(snapshots) = condition_flow_snapshot.into_branches() { + self.flow_restore(snapshots.truthy); + snapshots.falsy + } else { + self.flow_snapshot() + }; + + let (predicate, narrowing_id) = self.record_expression_narrowing_constraint(if_expr); + let reachability_constraint = self.record_reachability_constraint(predicate); + let included_path = self.flow_snapshot(); + + self.flow_restore(filtered_out); + self.record_negated_narrowing_constraint(predicate, narrowing_id); + self.record_negated_reachability_constraint(reachability_constraint); + let filtered_out = self.flow_snapshot(); + + self.flow_restore(included_path); + filtered_out } fn declare_parameters(&mut self, parameters: &'ast ast::Parameters) { @@ -3483,7 +3769,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { fn source_text(&self) -> &SourceText { self.source_text - .get_or_init(|| source_text(self.db, self.file)) + .get_or_init(|| source_text(self.db, self.file.file(self.db))) } fn visit_stmt_impl(&mut self, stmt: &'ast ast::Stmt) { @@ -3771,14 +4057,19 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { // that `x` can be freely overwritten, and that we don't assume that an import // in one function is visible in another function. let mut is_self_import = false; - if self.file.is_package(self.db) + let source_file = self.file.file(self.db); + let resolver_environment = self.resolver_environment; + if source_file.is_package(self.db) && let Ok(module_name) = ModuleName::from_identifier_parts( self.db, - self.file, + ImportingFile::File(source_file, resolver_environment), node.module.as_deref(), node.level, ) - && let Ok(thispackage) = ModuleName::package_for_file(self.db, self.file) + && let Ok(thispackage) = ModuleName::package_for_file( + self.db, + ImportingFile::File(source_file, resolver_environment), + ) { // Record whether this is equivalent to `from . import ...` is_self_import = module_name == thispackage; @@ -3851,20 +4142,27 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { continue; } - let Ok(module_name) = - ModuleName::from_import_statement(self.db, self.file, node) - else { + let Ok(module_name) = ModuleName::from_import_statement( + self.db, + ImportingFile::File(source_file, resolver_environment), + node, + ) else { continue; }; - let Some(module) = resolve_module(self.db, self.file, &module_name) else { + let Some(module) = resolve_module( + self.db, + ImportingFile::File(source_file, resolver_environment), + &module_name, + ) else { continue; }; - let Some(referenced_module) = module.file(self.db) else { + let Some(referenced_file) = module.file(self.db) else { continue; }; - + let referenced_program_file = + ProgramFile::new(self.db, referenced_file, self.file.program(self.db)); // 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` @@ -3879,14 +4177,14 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { // ``` // // For more details, see the doc-comment on `StarImportPlaceholderPredicate`. - for export in exported_names(self.db, referenced_module) { + for export in exported_names(self.db, referenced_program_file) { let symbol_id = 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, + referenced_program_file, ); let star_import_predicate = self.add_predicate(star_import.into()); @@ -4080,7 +4378,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { self.report_semantic_error(SemanticSyntaxError { kind: SemanticSyntaxErrorKind::AnnotatedGlobal(name.id.as_str().into()), range: name.range, - python_version: self.python_version, + python_version: self.python_version(), }); } // Check whether the variable has been declared nonlocal. @@ -4090,7 +4388,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { name.id.as_str().into(), ), range: name.range, - python_version: self.python_version, + python_version: self.python_version(), }); } } @@ -4100,7 +4398,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { *node.target, ast::Expr::Attribute(_) | ast::Expr::Subscript(_) | ast::Expr::Name(_) ) { - self.push_assignment(node.into()); + self.push_assignment(CurrentAssignment::AnnAssign(node)); self.visit_expr(&node.target); self.pop_assignment(); @@ -4133,12 +4431,12 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { } } - self.push_assignment(aug_assign.into()); + self.push_assignment(CurrentAssignment::AugAssign(aug_assign)); self.visit_expr(target); self.pop_assignment(); } ast::Expr::Name(_) | ast::Expr::Attribute(_) | ast::Expr::Subscript(_) => { - self.push_assignment(aug_assign.into()); + self.push_assignment(CurrentAssignment::AugAssign(aug_assign)); self.visit_expr(target); self.pop_assignment(); } @@ -4828,26 +5126,23 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { self.mark_unreachable(); } - ast::Stmt::Continue(_) => { - let snapshot = self.flow_snapshot(); - if let Some(current_loop) = self.current_loop_mut() { - current_loop.push_continue(snapshot); - } - self.record_terminal_finally_entry(); - // Everything in the current block after a terminal statement is unreachable. - self.mark_unreachable(); - } - - ast::Stmt::Break(ast::StmtBreak { value, .. }) => { + ast::Stmt::Continue(_) | ast::Stmt::Break(_) => { // the value is evaluated before control leaves the loop, so it is // visited before the break's flow effect is recorded - if let Some(value) = value { + if let ast::Stmt::Break(ast::StmtBreak { + value: Some(value), .. + }) = stmt + { self.check_break_value(stmt, value); self.visit_expr(value); } let snapshot = self.flow_snapshot(); if let Some(current_loop) = self.current_loop_mut() { - current_loop.push_break(snapshot); + if stmt.is_continue_stmt() { + current_loop.continue_states.push(snapshot); + } else { + current_loop.break_states.push(snapshot); + } } self.record_terminal_finally_entry(); // Everything in the current block after a terminal statement is unreachable. @@ -4873,7 +5168,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { start: name.range.start(), }, range: name.range, - python_version: self.python_version, + python_version: self.python_version(), }); } // Check whether the variable has also been declared nonlocal. @@ -4881,7 +5176,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { self.report_semantic_error(SemanticSyntaxError { kind: SemanticSyntaxErrorKind::NonlocalAndGlobal(name.to_string()), range: name.range, - python_version: self.python_version, + python_version: self.python_version(), }); // Never mark a symbol both global and nonlocal, even in this error case. continue; @@ -4926,7 +5221,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { start: name.range.start(), }, range: name.range, - python_version: self.python_version, + python_version: self.python_version(), }); } // Check whether the variable has also been declared global. @@ -4934,7 +5229,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { self.report_semantic_error(SemanticSyntaxError { kind: SemanticSyntaxErrorKind::NonlocalAndGlobal(name.to_string()), range: name.range, - python_version: self.python_version, + python_version: self.python_version(), }); // Never mark a symbol both global and nonlocal, even in this error case. continue; @@ -5383,12 +5678,11 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> { self.visit_expr(&node.value); return; } - // 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.push_assignment(CurrentAssignment::Named(node)); self.visit_expr(&node.target); self.pop_assignment(); } else { @@ -5685,6 +5979,40 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> { } fn visit_pattern(&mut self, pattern: &'ast ast::Pattern) { + if let ast::Pattern::MatchOr(ast::PatternMatchOr { patterns, .. }) = pattern + && let Some((last, alternatives)) = patterns.split_last() + && ( + // Capture-free alternatives do not affect bindings and need no flow merge. + patterns.iter().any(Self::pattern_has_bindings) + ) + { + // Start each alternative without earlier captures so repeated names do not shadow one + // another. Complementary predicates preserve possible missing captures while all + // alternatives together recover the incoming reachability. + let mut successful_alternatives = None; + for alternative in alternatives { + let remaining_alternatives = self.flow_snapshot(); + let selected_alternative = + self.record_reachability_constraint(PredicateOrLiteral::Predicate(Predicate { + node: PredicateNode::OrPatternAlternative(self.current_scope_id()), + is_positive: true, + })); + self.visit_pattern(alternative); + if let Some(previous_alternatives) = successful_alternatives.take() { + self.flow_merge(previous_alternatives); + } + successful_alternatives = Some(self.flow_snapshot()); + self.flow_restore(remaining_alternatives); + self.record_negated_reachability_constraint(selected_alternative); + } + + self.visit_pattern(last); + if let Some(successful_alternative) = successful_alternatives { + self.flow_merge(successful_alternative); + } + return; + } + if let ast::Pattern::MatchStar(ast::PatternMatchStar { name: Some(name), range: _, @@ -5890,7 +6218,7 @@ impl SemanticSyntaxContext for SemanticIndexBuilder<'_, '_> { return; } - if self.db.should_check_file(self.file) { + if self.db.should_check_file(self.file.file(self.db)) { self.semantic_syntax_errors.borrow_mut().push(error); } } @@ -5947,24 +6275,6 @@ impl CurrentAssignment<'_, '_> { } } -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(Default)] struct CurrentStatement<'ast, 'db> { /// A list of lambda expressions contained in this statement. diff --git a/crates/ty_python_core/src/builder/loop_bindings_visitor.rs b/crates/ty_python_core/src/builder/loop_bindings_visitor.rs index 8041e074b1..eb52d2b400 100644 --- a/crates/ty_python_core/src/builder/loop_bindings_visitor.rs +++ b/crates/ty_python_core/src/builder/loop_bindings_visitor.rs @@ -34,7 +34,7 @@ pub(crate) struct LoopBindingsVisitor { } impl LoopBindingsVisitor { - pub(crate) fn add_place_from_target(&mut self, target: &ast::Expr) { + fn add_place_from_target(&mut self, target: &ast::Expr) { match target { ast::Expr::Name(name) => { self.bound_places.push(PlaceExpr::from_expr_name(name)); diff --git a/crates/ty_python_core/src/db.rs b/crates/ty_python_core/src/db.rs index 2c447609f1..98bd1ae087 100644 --- a/crates/ty_python_core/src/db.rs +++ b/crates/ty_python_core/src/db.rs @@ -1,6 +1,9 @@ use ruff_db::files::File; use ty_module_resolver::Db as ModuleResolverDb; +#[cfg(any(test, feature = "testing"))] +use crate::program::{Program, ProgramSettings}; + /// Database giving access to semantic information about a Python program. #[salsa::db] pub trait Db: ModuleResolverDb { @@ -8,6 +11,25 @@ pub trait Db: ModuleResolverDb { fn should_check_file(&self, file: File) -> bool; } +#[cfg(any(test, feature = "testing"))] +#[salsa::db] +pub trait TestProgramDb: Db { + fn program_settings(&self) -> &ProgramSettings; + + // Salsa-cached because interning a program requires hashing all search paths. + fn program(&self) -> Program<'_> + where + Self: Sized, + { + #[salsa::tracked(returns(copy), heap_size=ruff_memory_usage::heap_size)] + fn program_inner(db: &dyn TestProgramDb) -> Program<'_> { + Program::from_settings(db, db.program_settings().clone()) + } + + program_inner(self) + } +} + #[cfg(test)] pub(crate) mod tests { use std::sync::{Arc, Mutex}; @@ -21,15 +43,13 @@ pub(crate) mod tests { }; use ruff_db::vendored::VendoredFileSystem; use ruff_python_ast::PythonVersion; - use ty_module_resolver::{ - Db as ModuleResolverDb, FallibleStrategy, SearchPathSettings, SearchPaths, - }; + use ty_module_resolver::{Db as ModuleResolverDb, FallibleStrategy, SearchPathSettings}; use ty_site_packages::{PythonVersionSource, PythonVersionWithSource}; use crate::platform::PythonPlatform; - use crate::program::{Program, ProgramSettings}; + use crate::program::ProgramSettings; - use super::Db; + use super::{Db, TestProgramDb}; type Events = Arc>>; @@ -40,11 +60,14 @@ pub(crate) mod tests { files: Files, system: TestSystem, vendored: VendoredFileSystem, + program_settings: ProgramSettings, } impl TestDb { - pub(crate) fn new() -> Self { + fn new() -> Self { let events = Events::default(); + let vendored = ty_vendored::file_system().clone(); + let program_settings = ProgramSettings::empty(&vendored); Self { storage: salsa::Storage::new(Some(Box::new({ move |event| { @@ -54,8 +77,9 @@ pub(crate) mod tests { } }))), system: TestSystem::default(), - vendored: ty_vendored::file_system().clone(), + vendored, files: Files::default(), + program_settings, } } } @@ -83,10 +107,6 @@ pub(crate) mod tests { fn files(&self) -> &Files { &self.files } - - fn python_version(&self) -> PythonVersion { - Program::get(self).python_version(self) - } } #[salsa::db] @@ -97,9 +117,12 @@ pub(crate) mod tests { } #[salsa::db] - impl ModuleResolverDb for TestDb { - fn search_paths(&self) -> &SearchPaths { - Program::get(self).search_paths(self) + impl ModuleResolverDb for TestDb {} + + #[salsa::db] + impl TestProgramDb for TestDb { + fn program_settings(&self) -> &ProgramSettings { + &self.program_settings } } @@ -142,19 +165,18 @@ pub(crate) mod tests { 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(), &FallibleStrategy) - .context("Invalid search path settings")?, + let program_settings = 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(), &FallibleStrategy) + .context("Invalid search path settings")?, + }; + program_settings.search_paths.try_register_static_roots(&db); + db.program_settings = program_settings; Ok(db) } diff --git a/crates/ty_python_core/src/definition.rs b/crates/ty_python_core/src/definition.rs index df9ed7b5bc..fe52200790 100644 --- a/crates/ty_python_core/src/definition.rs +++ b/crates/ty_python_core/src/definition.rs @@ -1,5 +1,6 @@ use std::ops::Deref; +use ruff_db::PythonFile; use ruff_db::files::{File, FileRange}; use ruff_db::parsed::{ParsedModuleRef, parsed_module}; use ruff_python_ast::find_node::covering_node; @@ -10,8 +11,8 @@ use ruff_python_ast::{self as ast, AnyNodeRef, Expr}; use ruff_text_size::{Ranged, TextRange, TextSize}; use smallvec::SmallVec; -use crate::Db; use crate::LoopHeaderId; +use crate::ProgramFile; use crate::ast_node_ref::AstNodeRef; use crate::member::ScopedMemberId; use crate::node_key::NodeKey; @@ -20,6 +21,8 @@ use crate::predicate::PatternPredicate; use crate::scope::{FileScopeId, ScopeId}; use crate::symbol::ScopedSymbolId; use crate::unpack::{Unpack, UnpackPosition}; +use crate::use_def::BindingWithConstraintsIterator; +use crate::{Db, Program, SemanticIndex}; /// A definition of a place. /// @@ -83,6 +86,18 @@ impl<'db> Definition<'db> { self.scope_id(db).file(db) } + pub fn python_file(self, db: &'db dyn Db) -> PythonFile<'db> { + self.scope_id(db).python_file(db) + } + + pub fn program_file(self, db: &'db dyn Db) -> ProgramFile<'db> { + self.scope_id(db).program_file(db) + } + + pub fn program(self, db: &'db dyn Db) -> Program<'db> { + self.scope_id(db).program(db) + } + pub fn file_scope(self, db: &'db dyn Db) -> FileScopeId { self.scope_id(db).file_scope_id(db) } @@ -105,8 +120,7 @@ impl<'db> Definition<'db> { /// Returns the name of the item being defined, if applicable. pub fn name(self, db: &'db dyn Db) -> Option { - let file = self.file(db); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, self.python_file(db)).load(db); let kind = self.kind(db); match kind { DefinitionKind::Function(def) => { @@ -142,8 +156,7 @@ impl<'db> Definition<'db> { /// This method returns a docstring for function, class, and attribute 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 module = parsed_module(db, self.python_file(db)).load(db); let kind = self.kind(db); match kind { @@ -271,13 +284,7 @@ pub struct Definitions<'db> { } impl<'db> Definitions<'db> { - pub fn single(definition: Definition<'db>) -> Self { - Self { - definitions: smallvec::smallvec_inline![definition], - } - } - - pub fn push(&mut self, definition: Definition<'db>) { + pub(crate) fn push(&mut self, definition: Definition<'db>) { self.definitions.push(definition); } @@ -610,7 +617,7 @@ pub(crate) enum ParameterDefinitionNodeRef<'ast> { } impl ParameterDefinitionNodeRef<'_> { - pub(super) fn into_owned(self, parsed: &ParsedModuleRef) -> ParameterDefinitionNodeKind { + fn into_owned(self, parsed: &ParsedModuleRef) -> ParameterDefinitionNodeKind { match self { Self::VariadicPositionalParameter(parameter) => { ParameterDefinitionNodeKind::VariadicPositionalParameter(AstNodeRef::new( @@ -628,7 +635,7 @@ impl ParameterDefinitionNodeRef<'_> { } } - pub(super) fn key(self) -> DefinitionNodeKey { + fn key(self) -> DefinitionNodeKey { match self { Self::VariadicPositionalParameter(node) => node.into(), Self::VariadicKeywordParameter(node) => node.into(), @@ -986,7 +993,7 @@ pub enum DefinitionKind<'db> { } impl<'db> DefinitionKind<'db> { - pub fn is_reexported(&self) -> bool { + pub(crate) fn is_reexported(&self) -> bool { match self { DefinitionKind::Import(import) => import.is_reexported(), DefinitionKind::ImportFrom(import) => import.is_reexported(), @@ -1374,7 +1381,7 @@ pub enum ParameterDefinitionNodeKind { } impl ParameterDefinitionNodeKind { - pub(crate) fn target_range(&self, module: &ParsedModuleRef) -> TextRange { + fn target_range(&self, module: &ParsedModuleRef) -> TextRange { match self { Self::VariadicPositionalParameter(parameter) => parameter.node(module).name.range(), Self::VariadicKeywordParameter(parameter) => parameter.node(module).name.range(), @@ -1382,7 +1389,7 @@ impl ParameterDefinitionNodeKind { } } - pub(crate) fn full_range(&self, module: &ParsedModuleRef) -> TextRange { + fn full_range(&self, module: &ParsedModuleRef) -> TextRange { match self { Self::VariadicPositionalParameter(parameter) => parameter.node(module).range(), Self::VariadicKeywordParameter(parameter) => parameter.node(module).range(), @@ -1390,7 +1397,7 @@ impl ParameterDefinitionNodeKind { } } - pub(crate) fn category(&self, module: &ParsedModuleRef) -> DefinitionCategory { + fn category(&self, module: &ParsedModuleRef) -> DefinitionCategory { match self { // a parameter always binds a value, but is only a declaration if annotated Self::VariadicPositionalParameter(parameter) @@ -1441,7 +1448,7 @@ impl ImportDefinitionKind { &self.node.node(module).names[self.alias_index as usize] } - pub fn is_reexported(&self) -> bool { + fn is_reexported(&self) -> bool { self.is_reexported } } @@ -1462,7 +1469,7 @@ impl ImportFromDefinitionKind { &self.node.node(module).names[self.alias_index as usize] } - pub fn is_reexported(&self) -> bool { + fn is_reexported(&self) -> bool { self.is_reexported } } @@ -1477,14 +1484,14 @@ impl ImportFromSubmoduleDefinitionKind { self.node.node(module) } - pub fn module<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Identifier { + fn module<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Identifier { self.import(module) .module .as_ref() .expect("import-from submodule definitions should always have a module identifier") } - pub fn target_range(&self, module: &ParsedModuleRef) -> TextRange { + fn target_range(&self, module: &ParsedModuleRef) -> TextRange { let module_ident = self.module(module); let module_str = module_ident.as_str(); @@ -1565,9 +1572,9 @@ impl AnnotatedAssignmentDefinitionKind { #[derive(Clone, Debug, get_size2::GetSize, salsa::SalsaValue)] pub struct DictKeyAssignmentKind<'db> { - pub(crate) key: AstNodeRef, - pub(crate) value: AstNodeRef, - pub(crate) assignment: Definition<'db>, + key: AstNodeRef, + value: AstNodeRef, + assignment: Definition<'db>, } impl<'db> DictKeyAssignmentKind<'db> { @@ -1688,7 +1695,7 @@ impl LoopHeaderDefinitionKind { self.place } - pub fn range(&self, module: &ParsedModuleRef) -> TextRange { + fn range(&self, module: &ParsedModuleRef) -> TextRange { match &self.loop_stmt { LoopStmtKind::While(stmt) => stmt.node(module).range(), LoopStmtKind::For(stmt) => stmt.node(module).range(), @@ -1699,12 +1706,92 @@ impl LoopHeaderDefinitionKind { #[derive(Clone, Debug, get_size2::GetSize)] pub struct NestedBindingsDefinitionKind { pub name: Name, + pub execution: NestedBindingExecution, // Note that in general this can include both `global` and `nonlocal` declarations from // different nested scopes, because we don't necessarily know at synthesis time which of those // kind will be visible in the current scope. pub nested_declarations: SmallVec<[crate::builder::NestedDeclaration; 1]>, } +impl NestedBindingsDefinitionKind { + /// Returns every nested binding source and whether it was declared `global`. + /// + /// Use [`Self::visible_binding_sources`] when resolving the binding in a particular scope. + fn binding_sources<'index, 'db>( + &'index self, + index: &'index SemanticIndex<'db>, + ) -> impl Iterator)> + 'index { + self.nested_declarations.iter().filter_map(|declaration| { + debug_assert!(declaration.is_bound); + let symbol = index + .place_table(declaration.file_scope_id) + .symbol_id(&self.name)?; + let use_def = index.use_def_map(declaration.file_scope_id); + let bindings = match self.execution { + NestedBindingExecution::Lazy => use_def.reachable_bindings(symbol.into()), + NestedBindingExecution::Eager => use_def.end_of_scope_bindings(symbol.into()), + }; + Some((declaration.is_global(), bindings)) + }) + } + + /// Returns nested binding sources that can update the same variable as `scope`. + /// + /// A synthetic binding can collect both `global` and `nonlocal` writes to one name: + /// + /// ```python + /// x = 0 + /// + /// def outer(): + /// x = 1 + /// + /// def change_global(): + /// global x + /// x = 2 + /// + /// def change_nonlocal(): + /// nonlocal x + /// x = 3 + /// ``` + /// + /// Only `change_nonlocal` can update `outer`'s local `x`. Nested functions also cannot + /// capture a class-local variable, so class scopes do not see nonlocal writes to their + /// own bindings. + pub fn visible_binding_sources<'index, 'db>( + &'index self, + index: &'index SemanticIndex<'db>, + scope: FileScopeId, + ) -> impl Iterator> + 'index { + let symbol_id = index.place_table(scope).symbol_id(&self.name); + let sees_global = symbol_id + .is_some_and(|symbol_id| index.symbol_resolves_to_global_scope(symbol_id, scope)); + let sees_nonlocal = !sees_global + && symbol_id.is_some_and(|symbol_id| { + !(index.scope(scope).kind().is_class() + && index.place_table(scope).symbol(symbol_id).is_local()) + }); + + self.binding_sources(index) + .filter_map(move |(is_global, bindings)| { + (if is_global { + sees_global + } else { + sees_nonlocal + }) + .then_some(bindings) + }) + } +} + +/// Describes when writes from a nested scope can affect its containing scope. +#[derive(Copy, Clone, Debug, Eq, PartialEq, get_size2::GetSize)] +pub enum NestedBindingExecution { + /// The nested scope can run later or repeatedly, as with a function body. + Lazy, + /// The nested scope is modeled as running while evaluating the containing expression. + Eager, +} + #[derive( Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, get_size2::GetSize, salsa::SalsaValue, )] diff --git a/crates/ty_python_core/src/expression.rs b/crates/ty_python_core/src/expression.rs index dec30584ac..93edd77421 100644 --- a/crates/ty_python_core/src/expression.rs +++ b/crates/ty_python_core/src/expression.rs @@ -1,6 +1,8 @@ use crate::ast_node_ref::AstNodeRef; use crate::db::Db; use crate::scope::ScopeId; +use crate::{Program, ProgramFile}; +use ruff_db::PythonFile; use ruff_db::files::File; use ruff_python_ast as ast; use salsa; @@ -73,4 +75,16 @@ impl<'db> Expression<'db> { pub fn file(self, db: &'db dyn Db) -> File { self.scope_id(db).file(db) } + + pub fn python_file(self, db: &'db dyn Db) -> PythonFile<'db> { + self.scope_id(db).python_file(db) + } + + pub fn program_file(self, db: &'db dyn Db) -> ProgramFile<'db> { + self.scope_id(db).program_file(db) + } + + pub fn program(self, db: &'db dyn Db) -> Program<'db> { + self.scope_id(db).program(db) + } } diff --git a/crates/ty_python_core/src/frozen.rs b/crates/ty_python_core/src/frozen.rs index 6df174dd19..23f91a65f3 100644 --- a/crates/ty_python_core/src/frozen.rs +++ b/crates/ty_python_core/src/frozen.rs @@ -22,7 +22,7 @@ impl FrozenMap { self.into_iter() } - pub fn keys(&self) -> impl DoubleEndedIterator + ExactSizeIterator { + pub(crate) fn keys(&self) -> impl DoubleEndedIterator + ExactSizeIterator { self.0.iter().map(|(key, _)| key) } diff --git a/crates/ty_python_core/src/lib.rs b/crates/ty_python_core/src/lib.rs index 3206c21798..a98e836d72 100644 --- a/crates/ty_python_core/src/lib.rs +++ b/crates/ty_python_core/src/lib.rs @@ -6,8 +6,8 @@ use ruff_python_ast as ast; use std::iter::{FusedIterator, once}; use std::sync::Arc; -use ruff_db::files::File; use ruff_db::parsed::parsed_module; + use ruff_index::{FrozenIndexVec, IndexSlice}; use ruff_python_ast::NodeIndex; use ruff_python_parser::semantic_errors::SemanticSyntaxError; @@ -64,16 +64,21 @@ pub mod symbol; pub mod unpack; mod use_def; pub use db::Db; +#[cfg(any(test, feature = "testing"))] +pub use db::TestProgramDb; pub mod program; +pub mod program_file; +pub use program::Program; +pub use program_file::ProgramFile; /// 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=ruff_memory_usage::heap_size)] -pub fn semantic_index(db: &dyn Db, file: File) -> SemanticIndex<'_> { +pub fn semantic_index<'db>(db: &'db dyn Db, file: ProgramFile<'db>) -> SemanticIndex<'db> { let _span = tracing::trace_span!("semantic_index", ?file).entered(); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, file.python_file(db)).load(db); SemanticIndexBuilder::new(db, file, &module).build() } @@ -85,9 +90,9 @@ pub fn semantic_index(db: &dyn Db, file: File) -> SemanticIndex<'_> { /// is unchanged. #[salsa::tracked(returns(deref), heap_size=ruff_memory_usage::heap_size)] pub 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); + let program_file = scope.program_file(db); + let _span = tracing::trace_span!("place_table", scope=?scope.as_id(), ?program_file).entered(); + let index = semantic_index(db, program_file); Arc::clone(&index.place_tables[scope.file_scope_id(db)]) } @@ -98,9 +103,9 @@ pub fn place_table<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc /// is unchanged. #[salsa::tracked(returns(deref), heap_size=ruff_memory_usage::heap_size)] pub 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); + let program_file = scope.program_file(db); + let _span = tracing::trace_span!("use_def_map", scope=?scope.as_id(), ?program_file).entered(); + let index = semantic_index(db, program_file); Arc::clone(&index.use_def_maps[scope.file_scope_id(db)]) } @@ -145,13 +150,13 @@ pub struct LoopHeader { } impl LoopHeader { - pub fn new() -> Self { + fn new() -> Self { Self { bindings: FxHashMap::default(), } } - pub fn add_binding(&mut self, place: ScopedPlaceId, binding: LiveBinding) { + fn add_binding(&mut self, place: ScopedPlaceId, binding: LiveBinding) { self.bindings.entry(place).or_default().push(binding); } @@ -175,8 +180,7 @@ pub fn attribute_scopes<'db>( db: &'db dyn Db, class_body_scope: ScopeId<'db>, ) -> impl Iterator + 'db { - let file = class_body_scope.file(db); - let index = semantic_index(db, file); + let index = semantic_index(db, class_body_scope.program_file(db)); let class_scope_id = class_body_scope.file_scope_id(db); ChildrenIter::new(&index.scopes, class_scope_id) .filter_map(move |(child_scope_id, scope)| { @@ -225,7 +229,7 @@ pub fn attribute_scopes<'db>( /// Returns the module global scope of `file`. #[salsa::tracked(returns(copy), heap_size=ruff_memory_usage::heap_size)] -pub fn global_scope(db: &dyn Db, file: File) -> ScopeId<'_> { +pub fn global_scope<'db>(db: &'db dyn Db, file: ProgramFile<'db>) -> ScopeId<'db> { let _span = tracing::trace_span!("global_scope", ?file).entered(); FileScopeId::global().to_scope_id(db, file) @@ -430,7 +434,7 @@ impl<'db> SemanticIndex<'db> { } #[track_caller] - pub(crate) fn ast_ids(&self) -> &AstIds { + fn ast_ids(&self) -> &AstIds { &self.ast_ids } @@ -473,10 +477,6 @@ impl<'db> SemanticIndex<'db> { self.place_table(scope).symbol(symbol).is_global() } - pub fn symbol_is_nonlocal_in_scope(&self, symbol: ScopedSymbolId, scope: FileScopeId) -> bool { - self.place_table(scope).symbol(symbol).is_nonlocal() - } - /// Returns `true` if the given symbol in the given scope resolves to the global scope, either /// because: /// @@ -592,7 +592,7 @@ impl<'db> SemanticIndex<'db> { } /// Returns an iterator over the descendent scopes of `scope`. - pub(crate) fn descendent_scopes(&self, scope: FileScopeId) -> DescendantsIter<'_> { + fn descendent_scopes(&self, scope: FileScopeId) -> DescendantsIter<'_> { DescendantsIter::new(&self.scopes, scope) } @@ -916,7 +916,7 @@ pub struct ChildrenIter<'a> { } impl<'a> ChildrenIter<'a> { - pub fn new(scopes: &'a IndexSlice, parent: FileScopeId) -> Self { + fn new(scopes: &'a IndexSlice, parent: FileScopeId) -> Self { let descendants = DescendantsIter::new(scopes, parent); Self { @@ -1107,7 +1107,10 @@ impl HasTrackedScope for ast::Identifier {} #[cfg(test)] mod tests { - use ruff_db::{files::system_path_to_file, parsed::ParsedModuleRef}; + use ruff_db::{ + files::{File, system_path_to_file}, + parsed::ParsedModuleRef, + }; use ruff_python_ast as ast; use ruff_text_size::{Ranged, TextRange}; @@ -1119,6 +1122,7 @@ mod tests { definition::{ DefinitionKind, LambdaParameterDefinitionNodeKind, ParameterDefinitionNodeKind, }, + program::Program, }; impl UseDefMap<'_> { @@ -1158,6 +1162,10 @@ mod tests { TestCase { db, file } } + fn program_file(db: &TestDb, file: File) -> ProgramFile<'_> { + db.program().program_file(db, file) + } + fn names(table: &PlaceTable) -> Vec { table .symbols() @@ -1168,7 +1176,7 @@ mod tests { #[test] fn empty() { let TestCase { db, file } = test_case(""); - let global_table = place_table(&db, global_scope(&db, file)); + let global_table = place_table(&db, global_scope(&db, program_file(&db, file))); let global_names = names(global_table); @@ -1178,7 +1186,7 @@ mod tests { #[test] fn simple() { let TestCase { db, file } = test_case("x"); - let global_table = place_table(&db, global_scope(&db, file)); + let global_table = place_table(&db, global_scope(&db, program_file(&db, file))); assert_eq!(names(global_table), vec!["x"]); } @@ -1186,7 +1194,7 @@ mod tests { #[test] fn annotation_only() { let TestCase { db, file } = test_case("x: int"); - let scope = global_scope(&db, file); + let scope = global_scope(&db, program_file(&db, file)); let global_table = place_table(&db, scope); assert_eq!(names(global_table), vec!["int", "x"]); @@ -1204,7 +1212,7 @@ mod tests { #[test] fn import() { let TestCase { db, file } = test_case("import foo"); - let scope = global_scope(&db, file); + let scope = global_scope(&db, program_file(&db, file)); let global_table = place_table(&db, scope); assert_eq!(names(global_table), vec!["foo"]); @@ -1218,7 +1226,7 @@ mod tests { #[test] fn import_sub() { let TestCase { db, file } = test_case("import foo.bar"); - let global_table = place_table(&db, global_scope(&db, file)); + let global_table = place_table(&db, global_scope(&db, program_file(&db, file))); assert_eq!(names(global_table), vec!["foo"]); } @@ -1226,7 +1234,7 @@ mod tests { #[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)); + let global_table = place_table(&db, global_scope(&db, program_file(&db, file))); assert_eq!(names(global_table), vec!["baz"]); } @@ -1234,7 +1242,7 @@ mod tests { #[test] fn import_from() { let TestCase { db, file } = test_case("from bar import foo"); - let scope = global_scope(&db, file); + let scope = global_scope(&db, program_file(&db, file)); let global_table = place_table(&db, scope); assert_eq!(names(global_table), vec!["foo"]); @@ -1255,7 +1263,7 @@ mod tests { #[test] fn assign() { let TestCase { db, file } = test_case("x = foo"); - let scope = global_scope(&db, file); + let scope = global_scope(&db, program_file(&db, file)); let global_table = place_table(&db, scope); assert_eq!(names(global_table), vec!["foo", "x"]); @@ -1275,7 +1283,7 @@ mod tests { #[test] fn augmented_assignment() { let TestCase { db, file } = test_case("x += 1"); - let scope = global_scope(&db, file); + let scope = global_scope(&db, program_file(&db, file)); let global_table = place_table(&db, scope); assert_eq!(names(global_table), vec!["x"]); @@ -1300,12 +1308,12 @@ class C: y = 2 ", ); - let global_table = place_table(&db, global_scope(&db, file)); + let global_table = place_table(&db, global_scope(&db, program_file(&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 module = parsed_module(&db, program_file(&db, file).python_file(&db)).load(&db); + let index = semantic_index(&db, program_file(&db, file)); let [(class_scope_id, class_scope)] = index .child_scopes(FileScopeId::global()) @@ -1315,7 +1323,9 @@ y = 2 }; assert_eq!(class_scope.kind(), ScopeKind::Class); assert_eq!( - class_scope_id.to_scope_id(&db, file).name(&db, &module), + class_scope_id + .to_scope_id(&db, program_file(&db, file)) + .name(&db, &module), "C" ); @@ -1338,8 +1348,8 @@ def func(): y = 2 ", ); - let module = parsed_module(&db, file).load(&db); - let index = semantic_index(&db, file); + let module = parsed_module(&db, program_file(&db, file).python_file(&db)).load(&db); + let index = semantic_index(&db, program_file(&db, file)); let global_table = index.place_table(FileScopeId::global()); assert_eq!(names(global_table), vec!["func", "y"]); @@ -1352,7 +1362,9 @@ y = 2 }; assert_eq!(function_scope.kind(), ScopeKind::Function); assert_eq!( - function_scope_id.to_scope_id(&db, file).name(&db, &module), + function_scope_id + .to_scope_id(&db, program_file(&db, file)) + .name(&db, &module), "func" ); @@ -1375,8 +1387,8 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): ", ); - let index = semantic_index(&db, file); - let global_table = place_table(&db, global_scope(&db, file)); + let index = semantic_index(&db, program_file(&db, file)); + let global_table = place_table(&db, global_scope(&db, program_file(&db, file))); assert_eq!(names(global_table), vec!["str", "int", "f"]); @@ -1420,8 +1432,8 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): 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)); + let index = semantic_index(&db, program_file(&db, file)); + let global_table = place_table(&db, global_scope(&db, program_file(&db, file))); assert!(names(global_table).is_empty()); @@ -1486,8 +1498,8 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): ", ); - let module = parsed_module(&db, file).load(&db); - let index = semantic_index(&db, file); + let module = parsed_module(&db, program_file(&db, file).python_file(&db)).load(&db); + let index = semantic_index(&db, program_file(&db, file)); let global_table = index.place_table(FileScopeId::global()); assert_eq!(names(global_table), vec!["iter1"]); @@ -1502,7 +1514,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): assert_eq!(comprehension_scope.kind(), ScopeKind::Comprehension); assert_eq!( comprehension_scope_id - .to_scope_id(&db, file) + .to_scope_id(&db, program_file(&db, file)) .name(&db, &module), "" ); @@ -1537,7 +1549,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): ", ); - let index = semantic_index(&db, file); + let index = semantic_index(&db, program_file(&db, file)); let [(comprehension_scope_id, _)] = index .child_scopes(FileScopeId::global()) .collect::>()[..] @@ -1547,7 +1559,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): let use_def = index.use_def_map(comprehension_scope_id); - let module = parsed_module(&db, file).load(&db); + let module = parsed_module(&db, program_file(&db, file).python_file(&db)).load(&db); let syntax = module.syntax(); let element = syntax.body[0] .as_expr_stmt() @@ -1558,7 +1570,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): .elt .as_name_expr() .unwrap(); - let element_use_id = element.scoped_use_id(&db, file); + let element_use_id = element.scoped_use_id(&db, program_file(&db, file)); let binding = use_def.first_binding_at_use(element_use_id).unwrap(); let DefinitionKind::Comprehension(comprehension) = binding.kind(&db) else { @@ -1582,8 +1594,8 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): ", ); - let module = parsed_module(&db, file).load(&db); - let index = semantic_index(&db, file); + let module = parsed_module(&db, program_file(&db, file).python_file(&db)).load(&db); + let index = semantic_index(&db, program_file(&db, file)); let global_table = index.place_table(FileScopeId::global()); assert_eq!(names(global_table), vec!["iter1"]); @@ -1598,7 +1610,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): assert_eq!(comprehension_scope.kind(), ScopeKind::Comprehension); assert_eq!( comprehension_scope_id - .to_scope_id(&db, file) + .to_scope_id(&db, program_file(&db, file)) .name(&db, &module), "" ); @@ -1617,7 +1629,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): assert_eq!(inner_comprehension_scope.kind(), ScopeKind::Comprehension); assert_eq!( inner_comprehension_scope_id - .to_scope_id(&db, file) + .to_scope_id(&db, program_file(&db, file)) .name(&db, &module), "" ); @@ -1636,7 +1648,7 @@ with item1 as x, item2 as y: ", ); - let index = semantic_index(&db, file); + let index = semantic_index(&db, program_file(&db, file)); let global_table = index.place_table(FileScopeId::global()); assert_eq!(names(global_table), vec!["item1", "x", "item2", "y"]); @@ -1659,7 +1671,7 @@ with context() as (x, y): ", ); - let index = semantic_index(&db, file); + let index = semantic_index(&db, program_file(&db, file)); let global_table = index.place_table(FileScopeId::global()); assert_eq!(names(global_table), vec!["context", "x", "y"]); @@ -1683,8 +1695,8 @@ def func(): y = 2 ", ); - let module = parsed_module(&db, file).load(&db); - let index = semantic_index(&db, file); + let module = parsed_module(&db, program_file(&db, file).python_file(&db)).load(&db); + let index = semantic_index(&db, program_file(&db, file)); let global_table = index.place_table(FileScopeId::global()); assert_eq!(names(global_table), vec!["func"]); @@ -1701,12 +1713,16 @@ def func(): assert_eq!(func_scope_1.kind(), ScopeKind::Function); assert_eq!( - func_scope1_id.to_scope_id(&db, file).name(&db, &module), + func_scope1_id + .to_scope_id(&db, program_file(&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_scope2_id + .to_scope_id(&db, program_file(&db, file)) + .name(&db, &module), "func" ); @@ -1731,8 +1747,8 @@ def func[T](): ", ); - let module = parsed_module(&db, file).load(&db); - let index = semantic_index(&db, file); + let module = parsed_module(&db, program_file(&db, file).python_file(&db)).load(&db); + let index = semantic_index(&db, program_file(&db, file)); let global_table = index.place_table(FileScopeId::global()); assert_eq!(names(global_table), vec!["func"]); @@ -1746,7 +1762,9 @@ def func[T](): assert_eq!(ann_scope.kind(), ScopeKind::TypeParams); assert_eq!( - ann_scope_id.to_scope_id(&db, file).name(&db, &module), + ann_scope_id + .to_scope_id(&db, program_file(&db, file)) + .name(&db, &module), "func" ); let ann_table = index.place_table(ann_scope_id); @@ -1759,7 +1777,9 @@ def func[T](): }; assert_eq!(func_scope.kind(), ScopeKind::Function); assert_eq!( - func_scope_id.to_scope_id(&db, file).name(&db, &module), + func_scope_id + .to_scope_id(&db, program_file(&db, file)) + .name(&db, &module), "func" ); let func_table = index.place_table(func_scope_id); @@ -1775,8 +1795,8 @@ class C[T]: ", ); - let module = parsed_module(&db, file).load(&db); - let index = semantic_index(&db, file); + let module = parsed_module(&db, program_file(&db, file).python_file(&db)).load(&db); + let index = semantic_index(&db, program_file(&db, file)); let global_table = index.place_table(FileScopeId::global()); assert_eq!(names(global_table), vec!["C"]); @@ -1789,7 +1809,12 @@ class C[T]: }; assert_eq!(ann_scope.kind(), ScopeKind::TypeParams); - assert_eq!(ann_scope_id.to_scope_id(&db, file).name(&db, &module), "C"); + assert_eq!( + ann_scope_id + .to_scope_id(&db, program_file(&db, file)) + .name(&db, &module), + "C" + ); let ann_table = index.place_table(ann_scope_id); assert_eq!(names(ann_table), vec!["T"]); assert!( @@ -1807,7 +1832,9 @@ class C[T]: assert_eq!(class_scope.kind(), ScopeKind::Class); assert_eq!( - class_scope_id.to_scope_id(&db, file).name(&db, &module), + class_scope_id + .to_scope_id(&db, program_file(&db, file)) + .name(&db, &module), "C" ); assert_eq!(names(index.place_table(class_scope_id)), vec!["x"]); @@ -1816,8 +1843,8 @@ class C[T]: #[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 module = parsed_module(&db, program_file(&db, file).python_file(&db)).load(&db); + let scope = global_scope(&db, program_file(&db, file)); let ast = module.syntax(); let ast::Stmt::Expr(ast::StmtExpr { value: x_use_expr, .. @@ -1828,7 +1855,7 @@ class C[T]: 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, file); + let x_use_id = x_use_expr_name.scoped_use_id(&db, program_file(&db, file)); 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 { @@ -1848,8 +1875,8 @@ class C[T]: 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 index = semantic_index(&db, program_file(&db, file)); + let module = parsed_module(&db, program_file(&db, file).python_file(&db)).load(&db); let ast = module.syntax(); let x_stmt = ast.body[0].as_assign_stmt().unwrap(); @@ -1871,11 +1898,16 @@ class C[T]: scopes: impl Iterator, db: &'db dyn Db, file: File, + program: Program<'db>, module: &'a ParsedModuleRef, ) -> Vec<&'a str> { scopes .into_iter() - .map(|(scope_id, _)| scope_id.to_scope_id(db, file).name(db, module)) + .map(|(scope_id, _)| { + scope_id + .to_scope_id(db, program.program_file(db, file)) + .name(db, module) + }) .collect() } @@ -1892,22 +1924,25 @@ def x(): pass", ); - let module = parsed_module(&db, file).load(&db); - let index = semantic_index(&db, file); + let module = parsed_module(&db, program_file(&db, file).python_file(&db)).load(&db); + let index = semantic_index(&db, program_file(&db, file)); let descendants = index.descendent_scopes(FileScopeId::global()); assert_eq!( - scope_names(descendants, &db, file, &module), + scope_names(descendants, &db, file, db.program(), &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"]); + assert_eq!( + scope_names(children, &db, file, db.program(), &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), + scope_names(test_child_scopes, &db, file, db.program(), &module), vec!["foo", "baz"] ); @@ -1919,7 +1954,7 @@ def x(): let ancestors = index.ancestor_scopes(bar_scope); assert_eq!( - scope_names(ancestors, &db, file, &module), + scope_names(ancestors, &db, file, db.program(), &module), vec!["bar", "foo", "Test", ""] ); } @@ -1939,7 +1974,7 @@ match subject: ", ); - let global_scope_id = global_scope(&db, file); + let global_scope_id = global_scope(&db, program_file(&db, file)); let global_table = place_table(&db, global_scope_id); assert!(global_table.symbol_by_name("Foo").unwrap().is_used()); @@ -1971,7 +2006,7 @@ match 1: ", ); - let global_scope_id = global_scope(&db, file); + let global_scope_id = global_scope(&db, program_file(&db, file)); let global_table = place_table(&db, global_scope_id); assert_eq!(names(global_table), vec!["first", "second"]); @@ -1988,7 +2023,7 @@ match 1: #[test] fn for_loops_single_assignment() { let TestCase { db, file } = test_case("for x in a: pass"); - let scope = global_scope(&db, file); + let scope = global_scope(&db, program_file(&db, file)); let global_table = place_table(&db, scope); assert_eq!(&names(global_table), &["a", "x"]); @@ -2004,7 +2039,7 @@ match 1: #[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 scope = global_scope(&db, program_file(&db, file)); let global_table = place_table(&db, scope); assert_eq!(&names(global_table), &["a", "x", "y"]); @@ -2024,7 +2059,7 @@ match 1: #[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 scope = global_scope(&db, program_file(&db, file)); let global_table = place_table(&db, scope); assert_eq!(&names(global_table), &["e", "a", "b", "c", "d"]); diff --git a/crates/ty_python_core/src/member.rs b/crates/ty_python_core/src/member.rs index 3834c1419b..ab12898442 100644 --- a/crates/ty_python_core/src/member.rs +++ b/crates/ty_python_core/src/member.rs @@ -81,7 +81,7 @@ impl Member { /// 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<&str> { + fn as_instance_attribute_candidate(&self) -> Option<&str> { let mut segments = self.expression().segments(); let first_segment = segments.next()?; @@ -106,7 +106,7 @@ impl Member { } /// 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 { + fn is_instance_attribute_named(&self, name: &str) -> bool { self.as_instance_attribute() == Some(name) } @@ -165,7 +165,7 @@ pub(crate) struct MemberExpr { impl MemberExpr { #[cfg(test)] - pub(super) fn try_from_expr(expression: ast::ExprRef<'_>) -> Option { + fn try_from_expr(expression: ast::ExprRef<'_>) -> Option { MemberExprBuilder::visit_expr(expression).and_then(Self::try_from_builder) } @@ -191,7 +191,7 @@ impl MemberExpr { /// Returns the left most part of the member expression, e.g. `x` in `x.y.z`. /// /// This is the symbol on which the member access is performed. - pub(crate) fn symbol_name(&self) -> &str { + fn symbol_name(&self) -> &str { self.as_ref().symbol_name() } diff --git a/crates/ty_python_core/src/narrowing_constraints.rs b/crates/ty_python_core/src/narrowing_constraints.rs index 5740ec075d..9042a93e03 100644 --- a/crates/ty_python_core/src/narrowing_constraints.rs +++ b/crates/ty_python_core/src/narrowing_constraints.rs @@ -303,19 +303,13 @@ impl NarrowingConstraintsBuilder { if_false, }) } - Ordering::Less => { - let node = self.interiors[a]; - let if_uncertain = self.add_or_constraint(node.if_uncertain, b); - self.add_interior(InteriorNode { - atom: node.atom, - if_true: node.if_true, - if_uncertain, - if_false: node.if_false, - }) - } - Ordering::Greater => { - let node = self.interiors[b]; - let if_uncertain = self.add_or_constraint(a, node.if_uncertain); + ordering @ (Ordering::Less | Ordering::Greater) => { + let (node, other) = if ordering == Ordering::Less { + (self.interiors[a], b) + } else { + (self.interiors[b], a) + }; + let if_uncertain = self.add_or_constraint(node.if_uncertain, other); self.add_interior(InteriorNode { atom: node.atom, if_true: node.if_true, @@ -380,23 +374,15 @@ impl NarrowingConstraintsBuilder { if_false, }) } - Ordering::Less => { - let node = self.interiors[a]; - let if_true = self.add_and_constraint(node.if_true, b); - let if_uncertain = self.add_and_constraint(node.if_uncertain, b); - let if_false = self.add_and_constraint(node.if_false, b); - self.add_interior(InteriorNode { - atom: node.atom, - if_true, - if_uncertain, - if_false, - }) - } - Ordering::Greater => { - let node = self.interiors[b]; - let if_true = self.add_and_constraint(a, node.if_true); - let if_uncertain = self.add_and_constraint(a, node.if_uncertain); - let if_false = self.add_and_constraint(a, node.if_false); + ordering @ (Ordering::Less | Ordering::Greater) => { + let (node, other) = if ordering == Ordering::Less { + (self.interiors[a], b) + } else { + (self.interiors[b], a) + }; + let if_true = self.add_and_constraint(node.if_true, other); + let if_uncertain = self.add_and_constraint(node.if_uncertain, other); + let if_false = self.add_and_constraint(node.if_false, other); self.add_interior(InteriorNode { atom: node.atom, if_true, diff --git a/crates/ty_python_core/src/place.rs b/crates/ty_python_core/src/place.rs index 19de8971d7..4c75f65f51 100644 --- a/crates/ty_python_core/src/place.rs +++ b/crates/ty_python_core/src/place.rs @@ -4,7 +4,6 @@ use crate::member::{ ScopedMemberId, }; use crate::predicate::{PatternPredicate, PatternSubject}; -use crate::scope::FileScopeId; use crate::symbol::{ScopedSymbolId, Symbol, SymbolTable, SymbolTableBuilder}; use crate::{Db, PossiblyNarrowedPlaces}; use ruff_db::parsed::ParsedModuleRef; @@ -319,7 +318,7 @@ pub struct PlaceTableBuilder { impl PlaceTableBuilder { /// Looks up a place ID by its expression. - pub fn place_id(&self, expression: PlaceExprRef) -> Option { + pub(crate) fn place_id(&self, expression: PlaceExprRef) -> Option { match expression { PlaceExprRef::Symbol(symbol) => self.symbols.symbol_id(symbol.name()).map(Into::into), PlaceExprRef::Member(member) => { @@ -347,12 +346,12 @@ impl PlaceTableBuilder { } #[track_caller] - pub(super) fn member_mut(&mut self, id: ScopedMemberId) -> &mut Member { + fn member_mut(&mut self, id: ScopedMemberId) -> &mut Member { self.member.member_mut(id) } #[track_caller] - pub fn place(&self, place_id: impl Into) -> PlaceExprRef<'_> { + pub(crate) fn place(&self, place_id: impl Into) -> PlaceExprRef<'_> { match place_id.into() { ScopedPlaceId::Symbol(id) => PlaceExprRef::Symbol(self.symbols.symbol(id)), ScopedPlaceId::Member(id) => PlaceExprRef::Member(self.member.member(id)), @@ -366,18 +365,18 @@ impl PlaceTableBuilder { } } - pub fn iter(&self) -> impl Iterator> { + pub(crate) fn iter(&self) -> impl Iterator> { self.symbols .iter() .map(Into::into) .chain(self.member.iter().map(PlaceExprRef::Member)) } - pub fn symbols(&self) -> impl Iterator { + pub(crate) fn symbols(&self) -> impl Iterator { self.symbols.iter() } - pub fn add_symbol(&mut self, symbol: Symbol) -> (ScopedSymbolId, bool) { + pub(crate) fn add_symbol(&mut self, symbol: Symbol) -> (ScopedSymbolId, bool) { let (id, is_new) = self.symbols.add(symbol); if is_new { @@ -388,7 +387,7 @@ impl PlaceTableBuilder { (id, is_new) } - pub fn add_member(&mut self, member: Member) -> (ScopedMemberId, bool) { + fn add_member(&mut self, member: Member) -> (ScopedMemberId, bool) { let (id, is_new) = self.member.add(member); if is_new { @@ -452,7 +451,7 @@ impl PlaceTableBuilder { } } - pub fn finish(self) -> PlaceTable { + pub(crate) fn finish(self) -> PlaceTable { PlaceTable { symbols: self.symbols.build(), members: self.member.build(), @@ -485,14 +484,6 @@ impl ScopedPlaceId { } } } - - pub const fn as_member(self) -> Option { - if let ScopedPlaceId::Member(id) = self { - Some(id) - } else { - None - } - } } impl std::ops::Index for Vec { @@ -518,29 +509,6 @@ impl From for ScopedPlaceId { } } -/// 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() - } -} - pub struct ParentPlaceIter<'a> { state: Option>, } @@ -578,11 +546,11 @@ impl<'a> ParentPlaceIterState<'a> { } impl<'a> ParentPlaceIter<'a> { - pub(super) fn for_symbol() -> Self { + fn for_symbol() -> Self { ParentPlaceIter { state: None } } - pub(super) fn for_member( + fn for_member( expression: &'a MemberExpr, symbol_table: &'a SymbolTable, member_table: &'a MemberTable, @@ -728,24 +696,21 @@ impl<'db, 'a> PossiblyNarrowedPlacesBuilder<'db, 'a> { self.add_narrowing_target(comparator, &mut places); } - let can_narrow_attribute_base = - matches!(&*expr_compare.ops, [ast::CmpOp::Eq | ast::CmpOp::NotEq]); - let can_narrow_subscript_base = matches!( + let can_narrow_tagged_union_base = matches!( &*expr_compare.ops, [ast::CmpOp::Eq | ast::CmpOp::NotEq | ast::CmpOp::Is | ast::CmpOp::IsNot] ); - // For subscript expressions on either side, the subscript base can also be narrowed. - // (TypedDict and tuple discriminated union narrowing.) + // Tagged-union checks can also narrow the base of a subscript or attribute on either side. for expr in std::iter::once(&*expr_compare.left).chain(&expr_compare.comparators) { - if can_narrow_subscript_base + if can_narrow_tagged_union_base && let ast::Expr::Subscript(subscript) = expr.expression_value() && let Some(place_expr) = PlaceExpr::try_from_expr(&subscript.value) && let Some(place) = self.places.place_id((&place_expr).into()) { places.insert(place); } - if can_narrow_attribute_base + if can_narrow_tagged_union_base && let ast::Expr::Attribute(attribute) = expr && let Some(place_expr) = PlaceExpr::try_from_expr(&attribute.value) && let Some(place) = self.places.place_id((&place_expr).into()) diff --git a/crates/ty_python_core/src/platform.rs b/crates/ty_python_core/src/platform.rs index a575baba47..8da3b45817 100644 --- a/crates/ty_python_core/src/platform.rs +++ b/crates/ty_python_core/src/platform.rs @@ -2,7 +2,7 @@ use std::fmt::{Display, Formatter}; use ty_combine::Combine; /// The target platform to assume when resolving types. -#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)] #[cfg_attr( feature = "serde", derive(serde::Serialize, serde::Deserialize, ruff_macros::RustDoc), diff --git a/crates/ty_python_core/src/predicate.rs b/crates/ty_python_core/src/predicate.rs index fd45582e34..78f66e9aba 100644 --- a/crates/ty_python_core/src/predicate.rs +++ b/crates/ty_python_core/src/predicate.rs @@ -7,10 +7,13 @@ //! - [_Reachability constraints_][crate::reachability_constraints] determine the //! static reachability of a binding, and the reachability of a statement or expression. +use crate::Program; +use ruff_db::PythonFile; use ruff_db::files::File; use ruff_index::{FrozenIndexVec, Idx, IndexVec}; use ruff_python_ast::{Singleton, name::Name}; +use crate::ProgramFile; use crate::ast_ids::ExpressionNodeKey; use crate::db::Db; use crate::definition::Definition; @@ -145,6 +148,10 @@ pub enum PredicateNode<'db> { /// semantically during type checking, so calls to a shadowed `range` remain ambiguous. IsNonEmptyIterable(Expression<'db>), Pattern(PatternPredicate<'db>), + /// Whether control flow takes one branch of an OR pattern instead of its remaining + /// alternatives. The selected branch is unknown, but recording a predicate and its negation + /// preserves the fact that exactly one branch is taken. + OrPatternAlternative(ScopeId<'db>), SubjectElementPattern(SubjectElementPatternPredicate<'db>), StarImportPlaceholder(StarImportPlaceholderPredicate<'db>), } @@ -243,7 +250,7 @@ pub enum PatternPredicateKind<'db> { #[salsa::tracked(debug, heap_size=ruff_memory_usage::heap_size)] pub struct PatternPredicate<'db> { #[returns(copy)] - pub file: File, + pub program_file: ProgramFile<'db>, #[returns(copy)] pub file_scope: FileScopeId, @@ -279,8 +286,20 @@ pub enum PatternSubject<'db> { impl get_size2::GetSize for PatternPredicate<'_> {} impl<'db> PatternPredicate<'db> { + pub fn file(self, db: &'db dyn Db) -> File { + self.program_file(db).file(db) + } + + pub fn python_file(self, db: &'db dyn Db) -> PythonFile<'db> { + self.program_file(db).python_file(db) + } + pub fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { - self.file_scope(db).to_scope_id(db, self.file(db)) + self.file_scope(db).to_scope_id(db, self.program_file(db)) + } + + pub fn program(self, db: &'db dyn Db) -> Program<'db> { + self.scope(db).program(db) } } @@ -327,7 +346,7 @@ impl<'db> PatternPredicate<'db> { #[salsa::tracked(debug, heap_size=ruff_memory_usage::heap_size)] pub struct StarImportPlaceholderPredicate<'db> { #[returns(copy)] - pub importing_file: File, + pub importing_file: ProgramFile<'db>, /// Each symbol imported by a `*` import has a separate predicate associated with it: /// this field identifies which symbol that is. @@ -342,7 +361,7 @@ pub struct StarImportPlaceholderPredicate<'db> { pub symbol_id: ScopedSymbolId, #[returns(copy)] - pub referenced_file: File, + pub referenced_file: ProgramFile<'db>, } // The Salsa heap is tracked separately. diff --git a/crates/ty_python_core/src/program.rs b/crates/ty_python_core/src/program.rs index a4c9660685..613bd8d634 100644 --- a/crates/ty_python_core/src/program.rs +++ b/crates/ty_python_core/src/program.rs @@ -1,109 +1,72 @@ use crate::{Db, platform::PythonPlatform}; +use ruff_db::files::File; use ruff_db::system::SystemPath; +use ruff_db::vendored::VendoredFileSystem; use ruff_python_ast::PythonVersion; -use salsa::Durability; -use salsa::Setter; -use ty_module_resolver::SearchPaths; +use ty_module_resolver::{ResolverEnvironment, SearchPaths}; use ty_site_packages::PythonVersionWithSource; +use crate::ProgramFile; + // Re-export the misconfiguration strategy types from ty_module_resolver. pub use ty_module_resolver::{FallibleStrategy, MisconfigurationStrategy, UseDefaultStrategy}; -#[salsa::input(singleton, heap_size=ruff_memory_usage::heap_size)] -pub struct Program { - #[returns(ref)] - pub python_version_with_source: PythonVersionWithSource, - +#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +pub struct Program<'db> { #[returns(ref)] pub python_platform: PythonPlatform, - #[returns(ref)] - pub search_paths: SearchPaths, + #[returns(copy)] + pub resolver_environment: ResolverEnvironment<'db>, } -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), - } - } +impl get_size2::GetSize for Program<'_> {} - pub fn from_settings(db: &dyn Db, settings: ProgramSettings) -> Self { +impl<'db> Program<'db> { + /// Creates a program from settings whose search roots have already been registered. + pub fn from_settings(db: &'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) + let resolver_environment = + ResolverEnvironment::new(db, python_version.version, &search_paths); + Program::new(db, python_platform, resolver_environment) } - pub fn python_version(self, db: &dyn Db) -> PythonVersion { - self.python_version_with_source(db).version + pub fn python_version(self, db: &'db dyn Db) -> PythonVersion { + self.resolver_environment(db).python_version(db) } - 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 search_paths(self, db: &'db dyn Db) -> &'db SearchPaths { + self.resolver_environment(db).search_paths(db) } - /// Permanently freezes all program inputs. - pub fn freeze(self, db: &mut dyn Db) { - let durability = Durability::NEVER_CHANGE; - let python_version = self.python_version_with_source(db).clone(); - let python_platform = self.python_platform(db).clone(); - let search_paths = self.search_paths(db).clone(); - - self.set_python_version_with_source(db) - .with_durability(durability) - .to(python_version); - self.set_python_platform(db) - .with_durability(durability) - .to(python_platform); - self.set_search_paths(db) - .with_durability(durability) - .to(search_paths); + pub fn program_file(self, db: &'db dyn Db, file: File) -> ProgramFile<'db> { + ProgramFile::new(db, file, self) } - pub fn custom_stdlib_search_path(self, db: &dyn Db) -> Option<&SystemPath> { + pub fn custom_stdlib_search_path(self, db: &'db dyn Db) -> Option<&'db SystemPath> { self.search_paths(db).custom_stdlib() } } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, get_size2::GetSize)] pub struct ProgramSettings { pub python_version: PythonVersionWithSource, pub python_platform: PythonPlatform, pub search_paths: SearchPaths, } + +impl ProgramSettings { + pub fn empty(vendored: &VendoredFileSystem) -> Self { + Self { + python_version: PythonVersionWithSource::default(), + python_platform: PythonPlatform::default(), + search_paths: SearchPaths::empty(vendored), + } + } +} diff --git a/crates/ty_python_core/src/program_file.rs b/crates/ty_python_core/src/program_file.rs new file mode 100644 index 0000000000..bc6548a6d8 --- /dev/null +++ b/crates/ty_python_core/src/program_file.rs @@ -0,0 +1,93 @@ +use ruff_db::PythonFile; +use ruff_db::files::File; +use ruff_python_ast::PythonVersion; +use ty_module_resolver::{ResolverEnvironment, ResolverFile}; + +use crate::{Db, program::Program}; + +/// A file interpreted within a particular Python program. +/// +/// The same file can participate in multiple programs, each with different Python versions, search +/// paths, or other settings that affect type inference. +/// +/// For example: +/// +/// ```text +/// project/ +/// ├── app.py # Project program: Python 3.11 +/// ├── generate.py # Script program: Python 3.12 +/// └── shared.py # Imported by both +/// ``` +/// +/// In `shared.py`, version-dependent code can produce different types: +/// +/// ```python +/// import sys +/// +/// if sys.version_info >= (3, 12): +/// value = 1 +/// else: +/// value = "one" +/// ``` +/// +/// The two interpretations therefore need separate semantic identities: +/// +/// ```text +/// ProgramFile(shared.py, project program) -> value: str +/// ProgramFile(shared.py, script program) -> value: int +/// ``` +/// +/// Semantic queries, such as `semantic_index`, use `ProgramFile` to avoid sharing results between +/// incompatible programs. Lower-level operations use narrower identities where possible: +/// +/// ```text +/// program_file.python_file(db) -> File + Python version +/// program_file.resolver_file(db) -> File + resolver environment +/// ``` +/// +/// This allows programs with the same Python version to share parsed syntax, and programs with +/// equivalent resolver environments to share module resolution, while keeping type inference +/// isolated. +#[salsa::interned( + debug, + constructor = new_internal, + heap_size = ruff_memory_usage::heap_size +)] +pub struct ProgramFile<'db> { + /// Cache the parser key even though its Python version is redundant with `program`: + /// program files are created infrequently, but their parser keys are looked up extensively. + #[returns(copy)] + pub python_file: PythonFile<'db>, + + #[returns(copy)] + pub program: Program<'db>, +} + +impl get_size2::GetSize for ProgramFile<'_> {} + +impl<'db> ProgramFile<'db> { + pub fn new(db: &'db dyn Db, file: File, program: Program<'db>) -> Self { + let python_file = PythonFile::new(db, file, program.python_version(db)); + Self::new_internal(db, python_file, program) + } + + /// Returns the physical file represented by this program file. + pub fn file(self, db: &'db dyn Db) -> File { + self.python_file(db).file(db) + } + + /// Returns the module-resolution environment for this program file. + pub fn resolver_environment(self, db: &'db dyn Db) -> ResolverEnvironment<'db> { + self.program(db).resolver_environment(db) + } + + /// Returns the resolver key for this file. + pub fn resolver_file(self, db: &'db dyn Db) -> ResolverFile<'db> { + ResolverFile::new(db, self.file(db), self.resolver_environment(db)) + } + + /// Returns the Python version associated with this file's program. + pub fn python_version(self, db: &'db dyn Db) -> PythonVersion { + self.program(db).python_version(db) + } +} diff --git a/crates/ty_python_core/src/re_exports.rs b/crates/ty_python_core/src/re_exports.rs index 52093c3578..53031b4711 100644 --- a/crates/ty_python_core/src/re_exports.rs +++ b/crates/ty_python_core/src/re_exports.rs @@ -20,24 +20,25 @@ //! to handle cycles. We do this using fixpoint iteration; adding fixpoint iteration to the //! whole [`super::semantic_index()`] query would probably be prohibitively expensive. -use ruff_db::{files::File, parsed::parsed_module}; +use ruff_db::parsed::parsed_module; + use ruff_python_ast::{ self as ast, name::Name, visitor::{Visitor, walk_expr, walk_pattern, walk_stmt}, }; use rustc_hash::FxHashMap; -use ty_module_resolver::{ModuleName, resolve_module}; +use ty_module_resolver::{ImportingFile, ModuleName, resolve_module}; -use crate::Db; +use crate::{Db, ProgramFile}; #[salsa::tracked( returns(deref), cycle_initial=|_, _, _| Box::default(), heap_size=ruff_memory_usage::heap_size) ] -pub(super) fn exported_names(db: &dyn Db, file: File) -> Box<[Name]> { - let module = parsed_module(db, file).load(db); +pub(super) fn exported_names(db: &dyn Db, file: ProgramFile<'_>) -> Box<[Name]> { + let module = parsed_module(db, file.python_file(db)).load(db); let mut finder = ExportFinder::new(db, file); finder.visit_body(module.suite()); @@ -51,18 +52,18 @@ pub(super) fn exported_names(db: &dyn Db, file: File) -> Box<[Name]> { struct ExportFinder<'db> { db: &'db dyn Db, - file: File, + program_file: ProgramFile<'db>, visiting_stub_file: bool, exports: FxHashMap<&'db Name, PossibleExportKind>, dunder_all: DunderAll, } impl<'db> ExportFinder<'db> { - fn new(db: &'db dyn Db, file: File) -> Self { + fn new(db: &'db dyn Db, file: ProgramFile<'db>) -> Self { Self { db, - file, - visiting_stub_file: file.is_stub(db), + program_file: file, + visiting_stub_file: file.file(db).is_stub(db), exports: FxHashMap::default(), dunder_all: DunderAll::NotPresent, } @@ -257,20 +258,35 @@ impl<'db> Visitor<'db> for ExportFinder<'db> { if &name.name.id == "*" { if !found_star { found_star = true; - for export in - ModuleName::from_import_statement(self.db, self.file, node) - .ok() - .and_then(|module_name| { - resolve_module(self.db, self.file, &module_name) - }) - .iter() - .flat_map(|module| { - module - .file(self.db) - .map(|file| exported_names(self.db, file)) - .unwrap_or_default() + let db = self.db; + let program_file = self.program_file; + let file = program_file.file(db); + let resolver_environment = program_file.resolver_environment(db); + for export in ModuleName::from_import_statement( + db, + ImportingFile::File(file, resolver_environment), + node, + ) + .ok() + .and_then(|module_name| { + resolve_module( + db, + ImportingFile::File(file, resolver_environment), + &module_name, + ) + }) + .iter() + .flat_map(|module| { + module + .file(db) + .map(|file| { + exported_names( + db, + ProgramFile::new(db, file, program_file.program(db)), + ) }) - { + .unwrap_or_default() + }) { self.possibly_add_export(export, PossibleExportKind::Normal); } } diff --git a/crates/ty_python_core/src/reachability_constraints.rs b/crates/ty_python_core/src/reachability_constraints.rs index 04625ecbe2..ad188fec73 100644 --- a/crates/ty_python_core/src/reachability_constraints.rs +++ b/crates/ty_python_core/src/reachability_constraints.rs @@ -100,11 +100,11 @@ impl ScopedReachabilityConstraintId { pub const ALWAYS_FALSE: ScopedReachabilityConstraintId = ScopedReachabilityConstraintId(0xffff_fffd); - pub fn is_terminal(self) -> bool { + pub(crate) fn is_terminal(self) -> bool { self.0 >= SMALLEST_TERMINAL.0 } - pub fn as_u32(self) -> u32 { + fn as_u32(self) -> u32 { self.0 } } diff --git a/crates/ty_python_core/src/scope.rs b/crates/ty_python_core/src/scope.rs index 46c82bf56e..728ee428e1 100644 --- a/crates/ty_python_core/src/scope.rs +++ b/crates/ty_python_core/src/scope.rs @@ -1,19 +1,19 @@ use std::ops::Range; -use ruff_db::{files::File, parsed::ParsedModuleRef}; +use ruff_db::{PythonFile, files::File, parsed::ParsedModuleRef}; use ruff_index::newtype_index; use ruff_python_ast::{self as ast, NodeIndex}; use crate::{ - Db, SemanticIndex, ast_node_ref::AstNodeRef, definition::Definition, node_key::NodeKey, - semantic_index, + Db, Program, ProgramFile, SemanticIndex, ast_node_ref::AstNodeRef, definition::Definition, + node_key::NodeKey, semantic_index, }; /// A cross-module identifier of a scope that can be used as a salsa query parameter. #[salsa::tracked(debug, heap_size=ruff_memory_usage::heap_size)] pub struct ScopeId<'db> { #[returns(copy)] - pub file: File, + pub program_file: ProgramFile<'db>, #[returns(copy)] pub file_scope_id: FileScopeId, @@ -23,16 +23,28 @@ pub struct ScopeId<'db> { impl get_size2::GetSize for ScopeId<'_> {} impl<'db> ScopeId<'db> { + pub fn file(self, db: &dyn Db) -> File { + self.program_file(db).file(db) + } + + pub fn python_file(self, db: &'db dyn Db) -> PythonFile<'db> { + self.program_file(db).python_file(db) + } + + pub fn program(self, db: &'db dyn Db) -> Program<'db> { + self.program_file(db).program(db) + } + pub fn is_annotation(self, db: &'db dyn Db) -> bool { self.node(db).scope_kind().is_annotation() } - pub fn node(self, db: &dyn Db) -> &NodeWithScopeKind { + pub fn node(self, db: &'db dyn Db) -> &'db NodeWithScopeKind { self.scope(db).node() } /// Returns `true` if this scope may require type context from its parent scope. - pub fn accepts_type_context(self, db: &dyn Db) -> bool { + pub fn accepts_type_context(self, db: &'db dyn Db) -> bool { matches!( self.node(db), NodeWithScopeKind::Lambda(_) @@ -43,13 +55,13 @@ impl<'db> ScopeId<'db> { ) } - pub fn scope(self, db: &dyn Db) -> &Scope { - semantic_index(db, self.file(db)).scope(self.file_scope_id(db)) + pub fn scope(self, db: &'db dyn Db) -> &'db Scope { + semantic_index(db, self.program_file(db)).scope(self.file_scope_id(db)) } /// Returns the class definition for the enclosing class if this scope is a method body. pub fn class_definition_of_method(self, db: &'db dyn Db) -> Option> { - semantic_index(db, self.file(db)).class_definition_of_method(self.file_scope_id(db)) + semantic_index(db, self.program_file(db)).class_definition_of_method(self.file_scope_id(db)) } pub fn is_method_scope(self, db: &'db dyn Db) -> bool { @@ -97,7 +109,7 @@ impl FileScopeId { self == FileScopeId::global() } - pub fn to_scope_id(self, db: &dyn Db, file: File) -> ScopeId<'_> { + pub fn to_scope_id<'db>(self, db: &'db dyn Db, file: ProgramFile<'db>) -> ScopeId<'db> { let index = semantic_index(db, file); index.scope_ids_by_scope[self] } @@ -152,7 +164,7 @@ impl Scope { self.kind().visibility() } - pub fn descendants(&self) -> Range { + pub(crate) fn descendants(&self) -> Range { self.descendants.clone() } @@ -227,7 +239,7 @@ impl ScopeKind { } } - pub(crate) const fn visibility(self) -> ScopeVisibility { + const fn visibility(self) -> ScopeVisibility { match self { ScopeKind::Module | ScopeKind::Class => ScopeVisibility::Public, ScopeKind::TypeParams @@ -259,7 +271,7 @@ impl ScopeKind { matches!(self, ScopeKind::Module) } - pub const fn is_annotation(self) -> bool { + pub(crate) const fn is_annotation(self) -> bool { matches!(self, ScopeKind::TypeParams | ScopeKind::TypeAlias) } @@ -328,7 +340,7 @@ impl NodeWithScopeRef<'_> { } } - pub fn node_key(self) -> NodeWithScopeKey { + pub(crate) fn node_key(self) -> NodeWithScopeKey { match self { NodeWithScopeRef::Module => NodeWithScopeKey::Module, NodeWithScopeRef::Class(class) => NodeWithScopeKey::Class(NodeKey::from_node(class)), @@ -433,7 +445,7 @@ impl NodeWithScopeKind { self.as_function().expect("expected function") } - pub fn as_type_alias(&self) -> Option<&AstNodeRef> { + fn as_type_alias(&self) -> Option<&AstNodeRef> { match self { Self::TypeAlias(type_alias) => Some(type_alias), _ => None, diff --git a/crates/ty_python_core/src/statement.rs b/crates/ty_python_core/src/statement.rs index 9f431884f3..eccdfc3c65 100644 --- a/crates/ty_python_core/src/statement.rs +++ b/crates/ty_python_core/src/statement.rs @@ -4,6 +4,8 @@ use crate::definition::Definition; use crate::expression::Expression; use crate::node_key::NodeKey; use crate::scope::{FileScopeId, ScopeId}; +use crate::{Program, ProgramFile}; +use ruff_db::PythonFile; use ruff_db::files::File; use ruff_python_ast as ast; use salsa; @@ -38,7 +40,7 @@ pub enum Statement<'db> { pub struct StatementInner<'db> { /// The file in which the statement occurs. #[returns(copy)] - pub file: File, + pub program_file: ProgramFile<'db>, /// The scope in which the statement occurs. #[returns(copy)] @@ -55,8 +57,20 @@ pub struct StatementInner<'db> { impl get_size2::GetSize for StatementInner<'_> {} impl<'db> StatementInner<'db> { + pub fn file(self, db: &'db dyn Db) -> File { + self.program_file(db).file(db) + } + + pub fn python_file(self, db: &'db dyn Db) -> PythonFile<'db> { + self.program_file(db).python_file(db) + } + pub fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { - self.file_scope(db).to_scope_id(db, self.file(db)) + self.file_scope(db).to_scope_id(db, self.program_file(db)) + } + + pub fn program(self, db: &'db dyn Db) -> Program<'db> { + self.scope(db).program(db) } } diff --git a/crates/ty_python_core/src/symbol.rs b/crates/ty_python_core/src/symbol.rs index b54f482ae9..8245f99a46 100644 --- a/crates/ty_python_core/src/symbol.rs +++ b/crates/ty_python_core/src/symbol.rs @@ -50,7 +50,7 @@ bitflags! { impl get_size2::GetSize for SymbolFlags {} impl Symbol { - pub const fn new(name: Name) -> Self { + pub(crate) const fn new(name: Name) -> Self { Self { name, flags: SymbolFlags::empty(), @@ -122,7 +122,7 @@ impl Symbol { self.flags.contains(SymbolFlags::IS_REASSIGNED) } - pub fn is_parameter(&self) -> bool { + pub(crate) fn is_parameter(&self) -> bool { self.flags.contains(SymbolFlags::IS_PARAMETER) } diff --git a/crates/ty_python_core/src/unpack.rs b/crates/ty_python_core/src/unpack.rs index 2037dede53..bb7ce5437f 100644 --- a/crates/ty_python_core/src/unpack.rs +++ b/crates/ty_python_core/src/unpack.rs @@ -1,3 +1,5 @@ +use crate::Program; +use ruff_db::PythonFile; use ruff_db::files::File; use ruff_db::parsed::ParsedModuleRef; use ruff_python_ast::{self as ast, AnyNodeRef}; @@ -5,6 +7,7 @@ use ruff_text_size::{Ranged, TextRange}; use crate::Db; use crate::EvaluationMode; +use crate::ProgramFile; use crate::ast_node_ref::AstNodeRef; use crate::expression::Expression; use crate::scope::{FileScopeId, ScopeId}; @@ -30,7 +33,7 @@ use crate::scope::{FileScopeId, ScopeId}; #[salsa::tracked(debug, heap_size=ruff_memory_usage::heap_size)] pub struct Unpack<'db> { #[returns(copy)] - pub file: File, + pub program_file: ProgramFile<'db>, #[returns(copy)] pub(crate) value_file_scope: FileScopeId, @@ -55,13 +58,26 @@ pub struct Unpack<'db> { impl get_size2::GetSize for Unpack<'_> {} impl<'db> Unpack<'db> { + pub fn file(self, db: &'db dyn Db) -> File { + self.program_file(db).file(db) + } + + pub fn python_file(self, db: &'db dyn Db) -> PythonFile<'db> { + self.program_file(db).python_file(db) + } + pub 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 fn target_scope(self, db: &'db dyn Db) -> ScopeId<'db> { - self.target_file_scope(db).to_scope_id(db, self.file(db)) + self.target_file_scope(db) + .to_scope_id(db, self.program_file(db)) + } + + pub fn program(self, db: &'db dyn Db) -> Program<'db> { + self.target_scope(db).program(db) } /// Returns the range of the unpack target expression. @@ -80,7 +96,7 @@ pub struct UnpackValue<'db> { } impl<'db> UnpackValue<'db> { - pub fn new(kind: UnpackKind, expression: Expression<'db>) -> Self { + pub(crate) fn new(kind: UnpackKind, expression: Expression<'db>) -> Self { Self { kind, expression } } diff --git a/crates/ty_python_core/src/use_def.rs b/crates/ty_python_core/src/use_def.rs index 3caac2daa5..fc57fb9142 100644 --- a/crates/ty_python_core/src/use_def.rs +++ b/crates/ty_python_core/src/use_def.rs @@ -278,6 +278,17 @@ pub use place_state::LiveBinding; pub use place_state::ScopedDefinitionId; pub(super) use place_state::{FutureDefinitions, PreviousDefinitions}; +/// Summarizes whether the live control-flow paths leave a symbol bound. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub(super) enum LiveBindingStatus { + /// No live path contains a binding. + Unbound, + /// Some live paths contain a binding and others leave the symbol unbound. + PossiblyBound, + /// Every live path contains a binding. + Bound, +} + /// Identifies a [`LoopHeader`] within a single scope's [`UseDefMap`]. #[newtype_index] #[derive(get_size2::GetSize)] @@ -858,10 +869,6 @@ impl<'db> UseDefMap<'db> { &self.constraint_tables().reachability_constraints } - pub fn narrowing_constraints(&self) -> &NarrowingConstraints { - &self.constraint_tables().narrowing_constraints - } - pub fn predicates(&self) -> &Predicates<'db> { &self.constraint_tables().predicates } @@ -986,7 +993,7 @@ impl<'db> UseDefMap<'db> { ) } - pub(crate) fn end_of_scope_member_bindings( + fn end_of_scope_member_bindings( &self, member: ScopedMemberId, ) -> BindingWithConstraintsIterator<'_, 'db> { @@ -1102,7 +1109,7 @@ impl<'db> UseDefMap<'db> { self.declarations_iterator(declarations, BoundnessAnalysis::BasedOnUnboundVisibility) } - pub(crate) fn end_of_scope_member_declarations<'map>( + fn end_of_scope_member_declarations<'map>( &'map self, member: ScopedMemberId, ) -> DeclarationsIterator<'map, 'db> { @@ -1761,7 +1768,7 @@ pub(super) struct UseDefMapBuilder<'db> { used_bindings: IndexVec, /// Builder of predicates. - pub(super) predicates: PredicatesBuilder<'db>, + predicates: PredicatesBuilder<'db>, /// Builder of reachability constraints. pub(super) reachability_constraints: ReachabilityConstraintsBuilder, @@ -2475,6 +2482,37 @@ impl<'db> UseDefMapBuilder<'db> { .map(LiveBinding::binding) } + /// Returns the current boundness of `symbol` after applying pending reachability constraints. + /// + /// Bindings on statically unreachable paths do not contribute to the result. This is stricter + /// than [`Symbol::is_bound`](crate::symbol::Symbol::is_bound), which records whether the symbol + /// is bound anywhere in the scope without considering control flow. + pub(super) fn symbol_live_binding_status( + &mut self, + symbol: ScopedSymbolId, + ) -> LiveBindingStatus { + let mut has_binding = false; + let mut has_unbound = false; + + for binding in self.current_bindings(symbol.into()) { + if binding.reachability_constraint() == ScopedReachabilityConstraintId::ALWAYS_FALSE { + continue; + } + + if binding.binding().is_unbound() { + has_unbound = true; + } else { + has_binding = true; + } + } + + match (has_binding, has_unbound) { + (true, true) => LiveBindingStatus::PossiblyBound, + (true, false) => LiveBindingStatus::Bound, + (false, _) => LiveBindingStatus::Unbound, + } + } + pub(super) fn mark_binding_definitions_used( &mut self, binding_definition_ids: impl IntoIterator, diff --git a/crates/ty_python_core/src/use_def/place_state.rs b/crates/ty_python_core/src/use_def/place_state.rs index 4467d91958..01d0494bdc 100644 --- a/crates/ty_python_core/src/use_def/place_state.rs +++ b/crates/ty_python_core/src/use_def/place_state.rs @@ -105,7 +105,7 @@ pub(crate) enum FutureDefinitions { } impl PreviousDefinitions { - pub(super) fn are_shadowed(self) -> bool { + fn are_shadowed(self) -> bool { matches!(self, PreviousDefinitions::AreShadowed) } } @@ -159,7 +159,7 @@ impl Declarations { } /// Add given reachability constraint to all live declarations. - pub(super) fn record_reachability_constraint( + fn record_reachability_constraint( &mut self, reachability_constraints: &mut ReachabilityConstraintsBuilder, constraint: ScopedReachabilityConstraintId, @@ -386,7 +386,7 @@ impl Bindings { } /// Add given constraint to all live bindings. - pub(super) fn record_narrowing_constraint( + fn record_narrowing_constraint( &mut self, narrowing_constraints: &mut NarrowingConstraintsBuilder, constraint: ScopedNarrowingConstraint, @@ -398,7 +398,7 @@ impl Bindings { } /// Add given reachability constraint to all live bindings. - pub(super) fn record_reachability_constraint( + fn record_reachability_constraint( &mut self, reachability_constraints: &mut ReachabilityConstraintsBuilder, constraint: ScopedReachabilityConstraintId, @@ -621,7 +621,7 @@ mod tests { } #[track_caller] - pub(crate) fn assert_declarations(place: &PlaceState, expected: &[&str]) { + fn assert_declarations(place: &PlaceState, expected: &[&str]) { let actual = place .declarations() .iter() diff --git a/crates/ty_python_semantic/Cargo.toml b/crates/ty_python_semantic/Cargo.toml index c619752737..3ccdeef737 100644 --- a/crates/ty_python_semantic/Cargo.toml +++ b/crates/ty_python_semantic/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_python_semantic" -version = "0.0.5" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } @@ -74,7 +74,7 @@ serde = [ "ruff_python_ast/serde", "ty_python_core/serde", ] -testing = [] +testing = ["ty_python_core/testing"] [[test]] name = "mdtest" diff --git a/crates/ty_python_semantic/README.md b/crates/ty_python_semantic/README.md index 0e52fd63d6..96f36cc39e 100644 --- a/crates/ty_python_semantic/README.md +++ b/crates/ty_python_semantic/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ty_python_semantic). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ty_python_semantic). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_python_semantic/resources/corpus/recursive_bound_method_source_order.py b/crates/ty_python_semantic/resources/corpus/recursive_bound_method_source_order.py new file mode 100644 index 0000000000..42ac7392bb --- /dev/null +++ b/crates/ty_python_semantic/resources/corpus/recursive_bound_method_source_order.py @@ -0,0 +1,28 @@ +# Regression test for the steam.py ecosystem failure in +# https://github.com/astral-sh/ruff/pull/27176. + +from __future__ import annotations + +from typing import Protocol, TypeVar + + +class PartialApp: + pass + + +AppT = TypeVar("AppT", bound=PartialApp, covariant=True) + + +class BaseOwnedBadge(Protocol[AppT]): + app: AppT + + def __init__(self, app: AppT) -> None: + pass + + async def progress(self: BaseOwnedBadge[PartialApp]) -> None: + pass + + +class FavouriteBadge(BaseOwnedBadge[AppT]): + def __init__(self, app: AppT) -> None: + super().__init__(app) diff --git a/crates/ty_python_semantic/resources/corpus/ty_4080_fuzzed_reachability_cycle.py b/crates/ty_python_semantic/resources/corpus/ty_4080_fuzzed_reachability_cycle.py new file mode 100644 index 0000000000..b7f6806056 --- /dev/null +++ b/crates/ty_python_semantic/resources/corpus/ty_4080_fuzzed_reachability_cycle.py @@ -0,0 +1,47 @@ +# Regression test for https://github.com/astral-sh/ty/issues/4080 +# Minimized from py-fuzzer seed 945. Prefix warming must not cause this cycle to diverge. + +lambda: name_3 + +for name_0 in {lambda: name_0: 0}: + pass +else: + try: + while name_0: + pass + unique_name_0() + except* 0: + pass + finally: + with 0 as name_0: + pass + +try: + assert lambda: name_0 + unique_name_1() +except: + while unique_name_2: + pass +finally: + import name_3 + +match 0: + case {**name_0}: + pass + +# Together with the two calls above, keep this scope just above the prefix-warming threshold. +extra_00() +extra_01() +extra_02() +extra_03() +extra_04() +extra_05() +extra_06() +extra_07() +extra_08() +extra_09() +extra_10() +extra_11() +extra_12() +extra_13() +extra_14() diff --git a/crates/ty_python_semantic/resources/lint_docs/abstract-and-final-method.md b/crates/ty_python_semantic/resources/lint_docs/abstract-and-final-method.md new file mode 100644 index 0000000000..390c976af7 --- /dev/null +++ b/crates/ty_python_semantic/resources/lint_docs/abstract-and-final-method.md @@ -0,0 +1,22 @@ +## What it does + +Checks for methods decorated with both `@abstractmethod` and `@final`. + +## Why is this bad? + +An abstract method must be overridden for a subclass to become concrete, but a final +method cannot be overridden. Combining the decorators therefore makes it impossible +for a subclass to provide a concrete implementation. + +## Example + +```python +from abc import ABC, abstractmethod +from typing import final + + +class Base(ABC): + @final + @abstractmethod + def method(self) -> None: ... # error +``` diff --git a/crates/ty_python_semantic/resources/lint_docs/invalid-named-tuple.md b/crates/ty_python_semantic/resources/lint_docs/invalid-named-tuple.md index 59507e46f0..690ed3c1fe 100644 --- a/crates/ty_python_semantic/resources/lint_docs/invalid-named-tuple.md +++ b/crates/ty_python_semantic/resources/lint_docs/invalid-named-tuple.md @@ -41,3 +41,13 @@ without a type annotation will raise an `AttributeError` at runtime. ... _asdict = 42 AttributeError: Cannot overwrite NamedTuple attribute _asdict ``` + +Finally, `NamedTuple` field annotations cannot use the `ClassVar` or `Final` type +qualifiers. These qualifiers also cause a runtime error when annotations are evaluated eagerly: + +```pycon +>>> from typing import ClassVar, NamedTuple +>>> class Foo(NamedTuple): +... x: ClassVar[int] +TypeError: typing.ClassVar[int] is not valid as type argument +``` diff --git a/crates/ty_python_semantic/resources/lint_docs/invalid-type-alias-type.md b/crates/ty_python_semantic/resources/lint_docs/invalid-type-alias-type.md index 005be90ce8..57d44faebf 100644 --- a/crates/ty_python_semantic/resources/lint_docs/invalid-type-alias-type.md +++ b/crates/ty_python_semantic/resources/lint_docs/invalid-type-alias-type.md @@ -14,7 +14,7 @@ python-version = "3.12" ``` ```python -from typing import TypeAliasType +from typing import TypeAliasType, TypeVar def get_name() -> str: @@ -24,4 +24,9 @@ def get_name() -> str: IntOrStr = TypeAliasType("IntOrStr", int | str) # okay # TypeAliasType name must be a string literal NewAlias = TypeAliasType(get_name(), int) # error + +T = TypeVar("T") +GenericAlias = TypeAliasType("GenericAlias", list[T], type_params=(T,)) # okay +# TypeAliasType type parameters must be type variables +InvalidAlias = TypeAliasType("InvalidAlias", list[T], type_params=(list[T],)) # error ``` diff --git a/crates/ty_python_semantic/resources/lint_docs/unsound-return-statement.md b/crates/ty_python_semantic/resources/lint_docs/unsound-return-statement.md new file mode 100644 index 0000000000..85e5cf5fc8 --- /dev/null +++ b/crates/ty_python_semantic/resources/lint_docs/unsound-return-statement.md @@ -0,0 +1,127 @@ +## What it does + +Detects `return` statements that unsoundly return a type that is not a [subtype] of the function's +annotated return type. + +This lint is a stricter version of `invalid-return-type`. + +## Why is this bad? + +By default, type checkers consider a `return` statement valid if the inferred type of the object +being returned is [assignable] to the annotated return type of the function it's in. However, this +makes it easy for incorrect types to percolate through your code unexpectedly due to a single +expression being inferred as `Any`. This can easily lead to runtime errors that are not caught by +the type checker: + +```py +from typing import Any + + +def returns_any() -> Any: + return "foo" + + +def returns_int() -> int: + # error: "Unsound return statement: `Any` is not a subtype of `int`" + return returns_any() + + +# fails at runtime, even though the type checker infers both operands as being of type `int`! +returns_int() + 42 +``` + +This rule allows you to use ["fully static"][fully-static] return types as "typed boundaries" for +your code. With this rule enabled, ty would emit an error on the `return returns_any()` statement +in `returns_int`, since the `returns_any()` call is inferred as having type `Any`, and `Any` is not +a subtype of `int`. This helps prevent the unsoundness from spreading far from its original source +(in this case, the return type of the `returns_any` function). + +Note that this rule is only applied to functions annotated as returning +[fully static][fully-static] types. It will not trigger if `Any` or `Unknown` appear anywhere in +your return type, either implicitly or explicitly: + +```py +from typing import Any + + +def returns_any() -> Any: + return "foo" + + +# error: [missing-type-argument] +def returns_unparameterized_tuple() -> tuple: + # no error, since the return type is implicitly `tuple[Unknown, ...]` + # (which is what the `missing-type-argument` error is complaining about on the line above!) + return returns_any() + + +def returns_list_of_any() -> list[Any]: + # no error, since the return type is explicitly `list[Any]` + return returns_any() +``` + +This rule works especially well when combined with ty's +`missing-type-argument` rule, and the Ruff rules [`ANN201`][ann201], +[`ANN202`][ann202], [`ANN204`][ann204], [`ANN205`][ann205], and [`ANN206`][ann206]. Enabling all +these rules at once effectively makes it much less likely that a `return` statement can lead to +unsoundness "leaking" out of a function unless that function has been *explicitly* annotated with +a dynamic type in some way (`-> Any` or `-> tuple[Any]`, for example). + +This rule is analogous to mypy's [`no-any-return`][no-any-return] error code, which is enabled by +mypy’s [`--strict`][mypy-strict] mode and can also be enabled on its own using mypy’s +[`--warn-return-any`][warn-return-any] option. + +## Examples + +```py +from typing import Any + + +def returns_any() -> Any: + return 42 + + +def returns_int() -> int: + # error: "Unsound return statement: `Any` is not a subtype of `int`" + return returns_any() +``` + +Narrow the type to a subtype of `int` to fix the diagnostic: + +```py +from typing import Any +from typing_extensions import reveal_type + + +def returns_any() -> Any: + return 42 + + +def returns_int() -> int: + my_int = returns_any() + assert isinstance(my_int, int) + reveal_type(my_int) # revealed: Any & int + return my_int # no error: `Any & int` is a subtype of `int` +``` + +## Default level + +This rule is disabled by default. It is intended for advanced users wanting additional soundness +checks from their type checker, not for users who have just started to use type checkers on their +Python code. + +## See also + +- `unsound-yield` is a similar rule that triggers on unsound `yield` expressions rather than unsound `return` statements + +[ann201]: https://docs.astral.sh/ruff/rules/missing-return-type-undocumented-public-function/ +[ann202]: https://docs.astral.sh/ruff/rules/missing-return-type-private-function/ +[ann204]: https://docs.astral.sh/ruff/rules/missing-return-type-special-method/ +[ann205]: https://docs.astral.sh/ruff/rules/missing-return-type-static-method/ +[ann206]: https://docs.astral.sh/ruff/rules/missing-return-type-class-method/ +[assignable]: https://typing.python.org/en/latest/spec/glossary.html#term-assignable +[fully-static]: https://typing.python.org/en/latest/spec/glossary.html#term-fully-static-type +[mypy-strict]: https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-strict +[no-any-return]: https://mypy.readthedocs.io/en/stable/error_code_list2.html#code-no-any-return +[subtype]: https://typing.python.org/en/latest/spec/glossary.html#term-subtype +[warn-return-any]: https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-warn-return-any diff --git a/crates/ty_python_semantic/resources/lint_docs/unsound-yield.md b/crates/ty_python_semantic/resources/lint_docs/unsound-yield.md new file mode 100644 index 0000000000..0944ad3e51 --- /dev/null +++ b/crates/ty_python_semantic/resources/lint_docs/unsound-yield.md @@ -0,0 +1,127 @@ +## What it does + +Detects `yield` and `yield from` expressions that unsoundly yield a type that is not a [subtype] of +the generator function's annotated yield type. + +This lint is a stricter version of `invalid-yield`. + +## Why is this bad? + +By default, type checkers consider a yielded value valid if its inferred type is [assignable] to the +generator's annotated yield type. However, this +makes it easy for incorrect types to percolate through your code unexpectedly due to a single +expression being inferred as `Any`. This can easily lead to runtime errors that are not caught by +the type checker: + +```py +from typing import Any, Generator + + +def returns_any() -> Any: + return "not an integer" + + +def integers() -> Generator[int]: + # error: "Unsound `yield`: `Any` is not a subtype of `int`" + yield returns_any() + + +# Fails at runtime, even though the type checker infers `integers` as yielding only `int`s! +sum(integers()) +``` + +This rule treats [fully static][fully-static] yield types as "typed boundaries" for your code. With this rule enabled, ty would emit an error on the `yield returns_any()` statement +in `integers`, since the `returns_any()` call is inferred as having type `Any`, and `Any` is not +a subtype of `int`. This helps prevent the unsoundness from spreading far from its original source +(in this case, the return type of the `returns_any` function). + +Note that this rule is only applied to functions annotated as yielding +[fully static][fully-static] types. It will not trigger if `Any` or `Unknown` appear anywhere in +your function's yield type, either implicitly or explicitly. It will still trigger on functions that have non-fully-static send and/or return types, however: + +```py +from typing import Any, Generator + + +def returns_any() -> Any: + return "not an integer" + + +def dynamic_yield_type() -> Generator[Any]: + yield returns_any() + + +def static_yield_type() -> Generator[int, Any, Any]: + # error: "Unsound `yield`: `Any` is not a subtype of `int`" + yield returns_any() +``` + +This rule works especially well when combined with ty's +`missing-type-argument` rule, and the Ruff rules [`ANN201`][ann201], +[`ANN202`][ann202], [`ANN204`][ann204], [`ANN205`][ann205], and [`ANN206`][ann206]. Enabling all +these rules at once effectively makes it much less likely that a `yield` expression can lead to +unsoundness "leaking" out of a function unless that function has been *explicitly* annotated with +a dynamic type in some way (`-> Generator[Any]` or `-> Generator[tuple[Any]]`, for example). + +## Examples + +```py +from typing import Any, Iterator + + +def returns_any() -> Any: + return "foo" + + +def any_iterator() -> Iterator[Any]: + yield "foo" + + +def integers() -> Iterator[int]: + # error: "Unsound `yield`: `Any` is not a subtype of `int`" + yield returns_any() + # error: "Unsound `yield from`: `Any` is not a subtype of `int`" + yield from any_iterator() +``` + +Narrow the value before yielding it to fix the diagnostics: + +```py +from typing import Any, Iterator + + +def returns_any() -> Any: + return 42 + + +def any_iterator() -> Iterator[Any]: + yield "foo" + + +def integers() -> Iterator[int]: + value = returns_any() + assert isinstance(value, int) + yield value + + for value in any_iterator(): + assert isinstance(value, int) + yield value +``` + +## Default level + +This rule is disabled by default. It is intended for users who want stricter soundness checks at +generator boundaries. + +## See also + +- `unsound-return-statement` is a similar rule that triggers on unsound `return` statements rather than unsound `yield` expressions + +[ann201]: https://docs.astral.sh/ruff/rules/missing-return-type-undocumented-public-function/ +[ann202]: https://docs.astral.sh/ruff/rules/missing-return-type-private-function/ +[ann204]: https://docs.astral.sh/ruff/rules/missing-return-type-special-method/ +[ann205]: https://docs.astral.sh/ruff/rules/missing-return-type-static-method/ +[ann206]: https://docs.astral.sh/ruff/rules/missing-return-type-class-method/ +[assignable]: https://typing.python.org/en/latest/spec/glossary.html#term-assignable +[fully-static]: https://typing.python.org/en/latest/spec/glossary.html#term-fully-static-type +[subtype]: https://typing.python.org/en/latest/spec/glossary.html#term-subtype diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/annotated.md b/crates/ty_python_semantic/resources/mdtest/annotations/annotated.md index 74cd97d162..f5ddf9639b 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/annotated.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/annotated.md @@ -22,6 +22,45 @@ def _(x: Annotated[tuple[str, int], bytes]): reveal_type(x) # revealed: tuple[str, int] ``` +## Inside `type[...]` + +`Annotated` can wrap a class or specialized generic class inside `type[...]` without changing the +resulting class object type. + +```py +from typing_extensions import Annotated + +def _( + simple: type[Annotated[int, "metadata"]], + generic: type[Annotated[list[str], "metadata"]], +): + reveal_type(simple) # revealed: type[int] + reveal_type(generic) # revealed: type[list[str]] +``` + +This also works for unions of classes and nested `Annotated` forms. + +```py +def _( + union: type[Annotated[int | str, "metadata"]], + nested: type[Annotated[Annotated[int, "inner"], "outer"]], +): + reveal_type(union) # revealed: type[int | str] + reveal_type(nested) # revealed: type[int] +``` + +Wrapping a non-class type in `Annotated` does not make it a valid argument to `type[...]`. + +```py +from typing import Callable + +def _( + # error: [invalid-type-form] "The argument to `type[]` must be a class object type" + invalid: type[Annotated[Callable[[], int], "metadata"]], +): + reveal_type(invalid) # revealed: type[Unknown] +``` + ## Parameterization It is invalid to parameterize `Annotated` with less than two arguments. diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/any.md b/crates/ty_python_semantic/resources/mdtest/annotations/any.md index a8ca54c892..b4b9a3576e 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/any.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/any.md @@ -244,13 +244,12 @@ def check_callable_union(value1: CallableSubclassOfAny | IncompatibleCallable): ```snapshot error[invalid-assignment]: Object of type `CallableSubclassOfAny | IncompatibleCallable` is not assignable to `(int, /) -> int` - --> src/mdtest_snippet.py:141:14 + --> src/mdtest_snippet.py:141:37 | 141 | target1: Callable[[int], int] = value1 # snapshot | -------------------- ^^^^^^ Incompatible value of type `CallableSubclassOfAny | IncompatibleCallable` | | | Declared type - | info: element `IncompatibleCallable` of union `CallableSubclassOfAny | IncompatibleCallable` is not assignable to `(int, /) -> int` info: └── type `IncompatibleCallable` has inferred callable type `(x: int) -> bytes` info: └── incompatible return types: `bytes` is not assignable to `int` diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/int_float_complex.md b/crates/ty_python_semantic/resources/mdtest/annotations/int_float_complex.md index fada88bd9b..bb0317c867 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/int_float_complex.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/int_float_complex.md @@ -40,12 +40,20 @@ def assigns_float_to_int(x: float): y: int = x ``` -Unlike other type checkers, we choose not to obfuscate this special case by displaying `int | float` -as just `float`; we display the actual type: +basedpython always displays a numeric-tower union by its members, so a `float` annotation reads as +`int | float` and a `complex` one as `int | float | complex`. A type that is exactly the runtime +class is displayed as plain `float` or `complex`, so the two are still told apart without a marker +on the name. Use `ty_extensions.JustFloat` or `JustComplex` to write the exact types in annotations. ```py def f(x: float): reveal_type(x) # revealed: int | float + +def returns_float() -> float: + return 1 + +reveal_type(returns_float()) # revealed: int | float +reveal_type(1.0) # revealed: float ``` ## complex @@ -87,6 +95,31 @@ def assigns_complex(x: complex): def f(x: complex): reveal_type(x) # revealed: int | float | complex + +reveal_type(1j) # revealed: complex +``` + +## Shadowed numeric builtins + +Canonical numeric names remain qualified when a module defines a class with the same name: + +```py +import builtins + +class float: ... +class complex: ... + +def reveal_shadowed_names( + x: builtins.float | float, + y: builtins.complex | complex, +): + reveal_type(x) # revealed: int | builtins.float | mdtest_snippet.float + reveal_type(y) # revealed: int | float | builtins.complex | mdtest_snippet.complex + +def takes_custom_float(x: float): ... +def pass_builtin_float(x: builtins.float): + # error: [invalid-argument-type] "Argument to function `takes_custom_float` is incorrect: Expected `mdtest_snippet.float`, found `int | builtins.float`" + takes_custom_float(x) ``` ## Narrowing diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/literal_string.md b/crates/ty_python_semantic/resources/mdtest/annotations/literal_string.md index 5b951562e7..4d36fdf892 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/literal_string.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/literal_string.md @@ -46,7 +46,6 @@ error[invalid-type-form]: `LiteralString` expects no type parameter | 4 | a: LiteralString[str] | ^^^^^^^^^^^^^^^^^^ - | ``` ```py @@ -62,7 +61,6 @@ error[invalid-type-form]: `LiteralString` expects no type parameter | -------------^^^^^^^ | | | Did you mean `Literal`? - | ``` ### As a base class diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md b/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md index 1b83147bf8..49eecdd294 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md @@ -13,6 +13,15 @@ def _(user_id: UserId): reveal_type(user_id) # revealed: UserId ``` +A `NewType` constructor preserves its argument's runtime identity but gives the result its own +static tag. Applying an unrelated `NewType` constructor replaces the previous tag. + +```py +MediaId = NewType("MediaId", int) + +reveal_type(MediaId(UserId(1))) # revealed: MediaId +``` + ## Subtyping The basic purpose of `NewType` is that it acts like a subtype of its base, but not the exact same @@ -202,7 +211,6 @@ warning[mismatched-type-name]: The name passed to `NewType` must match the varia | 5 | UserId = NewType("Id", int) | ^^^^ Expected "UserId", got "Id" - | ``` ```py @@ -218,7 +226,6 @@ warning[mismatched-type-name]: The name passed to `NewType` must match the varia | 10 | UsesExistingId = NewType("Id", "Id") | ^^^^ Expected "UsesExistingId", got "Id" - | ``` ## The base must be a class type or another newtype @@ -567,24 +574,21 @@ E(["foo"]) # error: [invalid-argument-type] E(E(E(["foo"]))) # error: [invalid-argument-type] ``` -## `NewType` wrapping preserves singleton-ness and single-valued-ness +## `NewType` wrapping preserves singleton-ness ```py from typing_extensions import NewType from ty_extensions import static_assert -from ty_extensions._internal import is_singleton, is_single_valued +from ty_extensions._internal import is_singleton from types import EllipsisType A = NewType("A", EllipsisType) static_assert(is_singleton(A)) -static_assert(is_single_valued(A)) reveal_type(type(A(...)) is EllipsisType) # revealed: Literal[True] -# TODO: This should be `Literal[True]` also. -reveal_type(A(...) is ...) # revealed: bool +reveal_type(A(...) is ...) # revealed: Literal[True] B = NewType("B", int) static_assert(not is_singleton(B)) -static_assert(not is_single_valued(B)) ``` ## `NewType`s of tuples can be iterated/unpacked @@ -646,14 +650,15 @@ error[invalid-base]: Cannot subclass an instance of NewType | 6 | class Foo(X): ... | ^ - | info: Perhaps you were looking for: `Foo = NewType('Foo', X)` info: Definition of class `Foo` will raise `TypeError` at runtime ``` -## Don't narrow `NewType`-wrapped `Enum`s inside of match arms +## `NewType`-wrapped enums match their members -`Literal[Foo.X]` is actually disjoint from `N` here: +A `NewType` constructor returns its argument unchanged at runtime, and an ordinary literal does not +restrict `NewType` tags. An enum member can therefore inhabit both its literal type and a `NewType` +based on the enum. Each arm retains both types, and matching every enum member is exhaustive. ```py from enum import Enum @@ -668,11 +673,11 @@ N = NewType("N", Foo) def f(x: N): match x: case Foo.X: - reveal_type(x) # revealed: N + reveal_type(x) # revealed: N & Literal[Foo.X] case Foo.Y: - reveal_type(x) # revealed: N + reveal_type(x) # revealed: N & Literal[Foo.Y] case _: - reveal_type(x) # revealed: N + reveal_type(x) # revealed: Never ``` ## The base of a `NewType` can't be a protocol class or a `TypedDict` @@ -693,7 +698,6 @@ error[invalid-newtype]: invalid base for `typing.NewType` | 7 | UserId = NewType("UserId", Id) | ^^ type `Id` - | info: The base of a `NewType` is not allowed to be a protocol class. ``` @@ -711,7 +715,6 @@ error[invalid-newtype]: invalid base for `typing.NewType` | 12 | Bar = NewType("Bar", Foo) | ^^^ type `Foo` - | info: The base of a `NewType` is not allowed to be a `TypedDict`. ``` diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/self.md b/crates/ty_python_semantic/resources/mdtest/annotations/self.md index 016eb6fa17..8f66d5fb2f 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/self.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/self.md @@ -413,6 +413,7 @@ class GenericShape[T]: @classmethod def baz[U](cls, u: U) -> "GenericShape[U]": reveal_type(cls) # revealed: type[Self@baz] + # error: [invalid-return-type] return cls() class GenericCircle[T](GenericShape[T]): ... @@ -488,6 +489,36 @@ class Child(Parent): assert_type(self.create(), Self) ``` +Truthiness narrowing must also preserve `Self` when an instance accesses a class method. + +```py +from typing import Self, assert_type + +class MaybeEmpty: + @classmethod + def create(cls, other: Self) -> Self: + return cls() + + def copy_if_empty(self, other: Self) -> Self: + if not self: + assert_type(self.create(other), Self) + return self.create(other) + return self +``` + +A mixin narrowed to an unrelated class can also call that class's class methods. + +```py +class Base: + @classmethod + def warn(cls) -> None: ... + +class Mixin: + def method(self) -> None: + assert isinstance(self, Base) + self.warn() +``` + ## Attributes ```py @@ -798,12 +829,7 @@ 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: This `self: T` annotation should be rejected because `T` is not `Self` - def has_existing_self_annotation(self: T) -> Self: - return self # error: [invalid-return-type] - def return_concrete_type(self) -> Self: # TODO: We could emit a hint that suggests annotating with `Foo` instead of `Self` # error: [invalid-return-type] @@ -820,6 +846,261 @@ class Bar(Generic[T]): ... class Baz(Bar[Self]): ... ``` +## Explicit instance-method receivers with `Self` + +An instance method can use `Self` when its first parameter is unannotated or annotated as `Self`: + +```py +from __future__ import annotations + +from typing import Self, TypeVar + +T = TypeVar("T") + +class Valid: + def implicit(self) -> Self: + return self + + def explicit(self: Self) -> Self: + return self +``` + +A different receiver annotation is valid when the method's signature does not use `Self`: + +```py +class WithoutSelf: + def method(self: T) -> T: + return self +``` + +Any other annotation for the first parameter is incompatible with `Self`, even an annotation that +names the class itself: + +```py +class Invalid: + def type_variable(self: T) -> Self: # error: [invalid-type-form] + raise NotImplementedError + + def concrete(self: Invalid) -> Self: # error: [invalid-type-form] + raise NotImplementedError + + def union(self: T | None) -> Self: # error: [invalid-type-form] + raise NotImplementedError + + def class_object(self: type[Self]) -> Self: # error: [invalid-type-form] + raise NotImplementedError +``` + +The invalid receiver does not change the inferred return type of the bound method: + +```py +reveal_type(Invalid().concrete) # revealed: bound method Invalid.concrete() -> Invalid +``` + +## Explicit classmethod receivers with `Self` + +A class method receives the class as its first argument. When the method uses `Self`, that argument +can be unannotated or annotated as `type[Self]`: + +```py +from __future__ import annotations + +from typing import Self, TypeVar + +T = TypeVar("T") + +class Valid: + @classmethod + def implicit(cls) -> Self: + return cls() + + @classmethod + def explicit(cls: type[Self]) -> Self: + return cls() +``` + +A class method can also use a different receiver annotation when its signature does not use `Self`: + +```py +class WithoutSelf: + @classmethod + def method(cls: type[T]) -> T: + return cls() +``` + +Other annotations are incompatible with `Self`, including `Self` without the enclosing `type`: + +```py +class Invalid: + @classmethod + def type_variable(cls: type[T]) -> Self: # error: [invalid-type-form] + raise NotImplementedError + + @classmethod + def concrete(cls: type[Invalid]) -> Self: # error: [invalid-type-form] + raise NotImplementedError + + @classmethod + def instance(cls: Self) -> Self: # error: [invalid-type-form] + raise NotImplementedError +``` + +## `Self` in unions with explicit receivers + +An incompatible receiver makes `Self` invalid even when the surrounding union simplifies to +`object`. This applies to both return and parameter annotations: + +```py +from typing import Self + +class Example: + def return_type(self: object) -> Self | object: ... # error: [invalid-type-form] + def parameter(self: object, value: Self | object) -> None: ... # error: [invalid-type-form] +``` + +## `Self` in type aliases with explicit receivers + +An incompatible receiver also makes `Self` invalid when it appears as an argument to a generic type +alias, whether the alias is used in a return or parameter annotation: + +```py +from typing import Self + +type Identity[T] = T + +class Example: + def return_type(self: object) -> Identity[Self]: # error: [invalid-type-form] + raise NotImplementedError + + def parameter(self: object, value: Identity[Self]) -> None: ... # error: [invalid-type-form] +``` + +## Multiple `Self` annotations with explicit receivers + +An incompatible receiver produces a separate error for each `Self` annotation: + +```py +from typing import Self, Union + +class Multiple: + def method( + self: object, + other: Self, # error: [invalid-type-form] + ) -> Self: # error: [invalid-type-form] + raise NotImplementedError +``` + +Two occurrences in the same annotation also produce separate errors, each pointing at its own +`Self`: + +```py +class Repeated: + # snapshot: invalid-type-form + # snapshot: invalid-type-form + def method(self: object, other: Union[Self, Self]) -> None: ... +``` + +```snapshot +error[invalid-type-form]: `Self` requires `self: Self` or `cls: type[Self]` for annotated receivers + --> src/mdtest_snippet.py:12:43 + | +12 | def method(self: object, other: Union[Self, Self]) -> None: ... + | ^^^^ + + +error[invalid-type-form]: `Self` requires `self: Self` or `cls: type[Self]` for annotated receivers + --> src/mdtest_snippet.py:12:49 + | +12 | def method(self: object, other: Union[Self, Self]) -> None: ... + | ^^^^ +``` + +Suppressing the error on the return annotation does not suppress the error on a parameter +annotation: + +```py +class SuppressedReturn: + def method( + self: object, + other: Self, # error: [invalid-type-form] + ) -> Self: # ty: ignore[invalid-type-form] + raise NotImplementedError +``` + +## Generic methods with explicit receiver annotations + +Methods with their own type parameters follow the same rules for `Self` in both instance methods and +class methods: + +```py +from typing import Self + +class Valid: + def instance[T](self: Self, value: T) -> Self: + return self + + @classmethod + def class_method[T](cls: type[Self], value: T) -> Self: + return cls() +``` + +A method's own type parameter cannot replace `Self` in its receiver annotation: + +```py +class Invalid: + def instance[T](self: T) -> Self: # error: [invalid-type-form] + raise NotImplementedError + + @classmethod + def class_method[T](cls: type[T]) -> Self: # error: [invalid-type-form] + raise NotImplementedError +``` + +## Quoted `Self` with explicit receiver annotations + +A receiver annotation and a `Self` return annotation can both be quoted: + +```py +from typing import Self + +class Valid: + def instance(self: "Self") -> "Self": + return self + + @classmethod + def class_method(cls: "type[Self]") -> "Self": + return cls() +``` + +An incompatible receiver makes a quoted `Self` invalid, including when the quoted union simplifies +to `object`: + +```py +class InvalidReturn: + def simple(self: object) -> "Self": # error: [invalid-type-form] + raise NotImplementedError + + # snapshot: invalid-type-form + def union(self: object) -> "Self | object": ... +``` + +```snapshot +error[invalid-type-form]: `Self` requires `self: Self` or `cls: type[Self]` for annotated receivers + --> src/mdtest_snippet.py:15:33 + | +15 | def union(self: object) -> "Self | object": ... + | ^^^^ +``` + +A quoted parameter annotation is also invalid when it passes `Self` to a type alias: + +```py +type Identity[T] = T + +class InvalidParameter: + def method(self: object, value: "Identity[Self]") -> None: ... # error: [invalid-type-form] +``` + ## Self usage in static methods `Self` cannot be used anywhere in a static method, including parameters, return types, nested @@ -1091,7 +1372,7 @@ class ExplicitGeneric[T]: ExplicitGeneric[int]().special() -# TODO: this should be an `invalid-argument-type` error +# error: [invalid-argument-type] "Argument to bound method `ExplicitGeneric.special` is incorrect: Expected `ExplicitGeneric[int]`, found `ExplicitGeneric[str]`" ExplicitGeneric[str]().special() ``` diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/starred.md b/crates/ty_python_semantic/resources/mdtest/annotations/starred.md index 94c8935f4b..2d5e4dd108 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/starred.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/starred.md @@ -25,6 +25,6 @@ reveal_type(append_int()) # revealed: tuple[*tuple[Unknown, ...], int] def first_arg_int(*args: *tuple[int, *tuple[str, ...]]): ... first_arg_int(42, "42", "42") # fine -first_arg_int("not an int", "42", "42") # TODO: should error -first_arg_int(56, "42", 56) # TODO: should error +first_arg_int("not an int", "42", "42") # error: [invalid-argument-type] +first_arg_int(56, "42", 56) # error: [invalid-argument-type] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/string.md b/crates/ty_python_semantic/resources/mdtest/annotations/string.md index 1d927a8eb8..6fed825953 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/string.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/string.md @@ -307,7 +307,6 @@ error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation | 4 | c: """'"int"'""" = 1 | ^^^^^ Too many levels of nested string annotations; remove the redundant nested quotes - | error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation @@ -315,7 +314,6 @@ error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation | 9 | f: "'str | int | bool | Foo | Bar'" = 1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Nested string annotation is too long; remove the redundant nested quotes - | ``` ## Parameter @@ -393,7 +391,6 @@ error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation | 43 | m: "yield 1" | ^^^^^^^ Yield expression cannot be used here - | help: Did you mean `typing.Literal["yield 1"]`? @@ -402,29 +399,26 @@ error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation | 45 | n: "yield from 1" | ^^^^^^^^^^^^ Yield expression cannot be used here - | help: Did you mean `typing.Literal["yield from 1"]`? error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation - --> src/mdtest_snippet.py:55:5 + --> src/mdtest_snippet.py:55:10 | 55 | t: "list[yield from 1]" | -----^^^^^^^^^^^^- | | | Yield expression cannot be used here - | help: Did you mean `typing.Literal["list[yield from 1]"]`? error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation - --> src/mdtest_snippet.py:57:5 + --> src/mdtest_snippet.py:57:9 | 57 | u: "type]" | ----^ | | | Unexpected token at the end of an expression - | help: Did you mean `typing.Literal["type]"]`? ``` @@ -467,7 +461,7 @@ str ```snapshot error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation - --> src/mdtest_snippet.py:17:12 + --> src/mdtest_snippet.py:19:4 | 17 | a1: """ | ____________- @@ -476,11 +470,10 @@ error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation | |____^- | | | Unexpected token at the end of an expression - | error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation - --> src/mdtest_snippet.py:22:12 + --> src/mdtest_snippet.py:23:6 | 22 | a2: """ | ____________- @@ -488,11 +481,10 @@ error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation | | ^ Unexpected token at the end of an expression 24 | | str | |____- - | error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation - --> src/mdtest_snippet.py:27:12 + --> src/mdtest_snippet.py:28:12 | 27 | a3: """ | ____________- @@ -500,5 +492,4 @@ error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation | |____________^- | | | Unexpected token at the end of an expression - | ``` 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 index 46bf83727f..8dc805ed6f 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md @@ -45,8 +45,8 @@ def ex3(msg: str): def first_arg_int(*args: Unpack[tuple[int, Unpack[tuple[str, ...]]]]): ... first_arg_int(42, "42", "42") # fine -first_arg_int("not an int", "42", "42") # TODO: should error -first_arg_int(56, "42", 56) # TODO: should error +first_arg_int("not an int", "42", "42") # error: [invalid-argument-type] +first_arg_int(56, "42", 56) # error: [invalid-argument-type] ``` ## Allowed `Unpack` contexts diff --git a/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md b/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md index 535b6844e3..3687f181ee 100644 --- a/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md +++ b/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md @@ -27,13 +27,12 @@ a: Number = 1 ```snapshot error[invalid-assignment]: Object of type `Literal[1]` is not assignable to `Number` - --> src/mdtest_snippet.py:4:4 + --> src/mdtest_snippet.py:4:13 | 4 | a: Number = 1 | ------ ^ Incompatible value of type `Literal[1]` | | | Declared type - | info: Types from the `numbers` module aren't supported for static type checking help: Consider using a protocol instead, such as `typing.SupportsFloat` ``` diff --git a/crates/ty_python_semantic/resources/mdtest/assignment/augmented.md b/crates/ty_python_semantic/resources/mdtest/assignment/augmented.md index f46bb1242a..d71db28642 100644 --- a/crates/ty_python_semantic/resources/mdtest/assignment/augmented.md +++ b/crates/ty_python_semantic/resources/mdtest/assignment/augmented.md @@ -68,7 +68,6 @@ error[unsupported-operator]: Unsupported `-=` operation | | | | | Has type `Literal[1]` | Has type `C` - | ``` ## Method union @@ -188,6 +187,435 @@ def f(flag: bool, flag2: bool): reveal_type(f) # revealed: int | str | float ``` +## Declared attributes with in-place operators + +`+=` assigns the value returned by `__iadd__` back to its target. That value must be compatible with +the attribute's declared type. + +```py +class Value: + def __iadd__(self, other: int) -> str: + return "updated" + +class Holder: + value: Value + +holder = Holder() +# error: [invalid-assignment] +holder.value += 1 +reveal_type(holder.value) # revealed: Value +``` + +## Declared attributes without in-place operators + +When an object does not define `__iadd__`, `+=` falls back to `__add__`. Its result must still be +compatible with the attribute's declared type. + +```py +class Value: + def __add__(self, other: int) -> str: + return "updated" + +class Holder: + value: Value + +holder = Holder() +# error: [invalid-assignment] +holder.value += 1 +``` + +## Inferred attributes in loops + +An unannotated instance attribute may change type. After its initial `None` value is replaced, an +augmented assignment inside a loop must also contribute its result to the inferred attribute type. + +```py +class Counter: + def update(self) -> None: + self.value = None + self.value = 0 + for _ in range(1): + self.value += 1.0 + +reveal_type(Counter().value) # revealed: None | int | float +``` + +## Inferred class attributes + +An unannotated class attribute still has an inferred type that restricts assignments through an +instance. + +```py +class Holder: + value = 1 + +holder = Holder() +# error: [invalid-assignment] +holder.value += 0.5 +``` + +## Read-only properties + +`+=` writes its result back to the attribute. A property without a setter therefore cannot be the +target of an augmented assignment. + +```py +class ReadOnly: + @property + def value(self) -> int: + return 1 + +read_only = ReadOnly() +# error: [invalid-assignment] +read_only.value += 1 +``` + +## Properties with different getter and setter types + +A property can accept a wider type in its setter than it returns from its getter. The result of `/=` +is checked against the setter, while subsequent reads still use the getter's return type. + +```py +class Counter: + @property + def value(self) -> int: + return 1 + + @value.setter + def value(self, value: float) -> None: + pass + +counter = Counter() +counter.value /= 2 +reveal_type(counter.value) # revealed: int +``` + +## Attributes defined by descriptors + +When an unannotated class attribute is a data descriptor, its `__set__` method determines which +values may be assigned. + +```py +class Descriptor: + def __get__(self, instance: object, owner: type[object] | None = None) -> int: + return 1 + + def __set__(self, instance: object, value: str) -> None: + pass + +class Holder: + value = Descriptor() + +holder = Holder() +# error: [invalid-assignment] +holder.value += 1 +``` + +## Custom subscript assignments + +`/=` first reads an item, then writes the result back through `__setitem__`. The assigned value is +the result of the operation, not the right-hand operand. + +```py +class Container: + def __getitem__(self, key: int) -> int: + return 1 + + def __setitem__(self, key: int, value: int) -> None: + pass + +container = Container() +# error: [invalid-assignment] +container[0] /= 2 +reveal_type(container[0]) # revealed: int +``` + +## Subscript setters with different value types + +A collection can accept a wider type in `__setitem__` than `__getitem__` returns. After a valid +assignment, subsequent reads still use the return type of `__getitem__`. + +```py +class Container: + def __getitem__(self, key: int) -> int: + return 1 + + def __setitem__(self, key: int, value: float) -> None: + pass + +container = Container() +container[0] /= 2 +reveal_type(container[0]) # revealed: int +``` + +## Annotated collection entries + +An annotation fixes the element type of a list, so `/=` cannot write a `float` into a `list[int]`. + +```py +values: list[int] = [1] +# error: [invalid-assignment] +values[0] /= 2 +``` + +The same rule applies to the value type of an annotated dictionary. + +```py +mapping: dict[str, int] = {"value": 1} +# error: [invalid-assignment] +mapping["value"] /= 2 +``` + +An annotated collection remains constrained when it is accessed through an attribute. + +```py +class Holder: + values: list[int] + +holder = Holder() +# error: [invalid-assignment] +holder.values[0] /= 2 +``` + +## Typed dictionary entries + +A `TypedDict` field can only be assigned a value compatible with its declared type. + +```py +from typing import TypedDict + +class Payload(TypedDict): + value: int + +payload: Payload = {"value": 1} +# error: [invalid-assignment] +payload["value"] /= 2 +``` + +## Read-only subscripts + +A readable item cannot be reassigned when its container does not implement `__setitem__`. + +```py +values: tuple[int] = (1,) +# error: [invalid-assignment] +values[0] += 1 +``` + +## Missing attributes + +If an augmented assignment cannot read its target, it must report that failure only once; no +assignment is attempted. + +```py +class Missing: ... + +missing = Missing() +# error: [unresolved-attribute] +missing.value += 1 +``` + +The same applies when an attribute is missing from one member of a union. + +```py +class Counter: + count: int + +def update(counter: Counter | None) -> None: + # error: [unresolved-attribute] + counter.count += 1 +``` + +An augmented assignment should not define an otherwise missing instance attribute, because it must +read an existing value before writing its result. We currently treat it like an ordinary +self-referential assignment instead. + +```py +class UninitializedCounter: + def increment(self) -> None: + # TODO: Report an unresolved-attribute error instead of implicitly defining the attribute. + self.value += 1 + +reveal_type(UninitializedCounter().value) # revealed: Divergent +``` + +## Dynamically provided attributes + +A dynamic attribute hook can provide the initial value read by an augmented assignment. The +assignment currently infers a divergent attribute type instead of preserving the hook's return type. + +```py +class DynamicCounter: + def __getattr__(self, name: str) -> int: + return 0 + + def increment(self) -> None: + self.value += 1 + +# TODO: Infer `int` from the dynamic attribute hook. +reveal_type(DynamicCounter().value) # revealed: Divergent +``` + +The same behavior applies when the attribute is provided by `__getattribute__`. + +```py +class InterceptedCounter: + def __getattribute__(self, name: str) -> int: + return 0 + + def increment(self) -> None: + self.value += 1 + +reveal_type(InterceptedCounter().value) # revealed: Divergent +``` + +## Class-level defaults in diamond inheritance + +An overriding class-level default supplies the initial value even when another branch of the +inheritance hierarchy declares a wider instance attribute. + +```py +class Base: + value: int | None = None + +class First(Base): ... + +class Second(Base): + value: int | None + +class Child(First, Second): + value: int = 1 + + def update(self) -> None: + self.value |= 2 +``` + +## Invalid subscript reads + +An invalid key prevents an item from being read, so the failed assignment must not produce a second +error. + +```py +mapping: dict[str, int] = {} +# error: [invalid-argument-type] +mapping[1] += 1 +``` + +A value without `__getitem__` also fails before assignment can be attempted. + +```py +value = 1 +# error: [not-subscriptable] +value[0] += 1 +``` + +## Right-hand-side errors after failed reads + +Even when an attribute cannot be read, the right-hand side must still be checked for unrelated +errors. + +```py +class Missing: ... + +missing = Missing() +# error: [unresolved-attribute] +# error: [unresolved-reference] +missing.value += missing_attribute_operand +``` + +The same rule applies when a subscript cannot be read. + +```py +mapping: dict[str, int] = {} +# error: [invalid-argument-type] +# error: [unresolved-reference] +mapping[1] += missing_subscript_operand +``` + +## Failed in-place operations + +If `__iadd__` rejects its operand, its return type must not be treated as a value to assign. + +```py +class Value: + def __iadd__(self, other: int) -> str: + return "updated" + +class Holder: + value: Value + +holder = Holder() +# error: [unsupported-operator] +holder.value += "invalid" +``` + +## Union attribute assignments + +When objects in a union have different attribute types, each operator result should be checked +against the attribute from the same object. Ordinary assignments already lose this relationship, so +augmented assignments currently report the same false positive. + +```py +class AValue: + def __iadd__(self, other: int) -> "AValue": + return self + +class BValue: + def __iadd__(self, other: int) -> "BValue": + return self + +class A: + value: AValue + +class B: + value: BValue + +def update(value: A | B) -> None: + # TODO: Check each result against the attribute it came from. + # error: [invalid-assignment] + value.value += 1 +``` + +## Collections that may be read-only + +When a collection could be a writable list or a read-only tuple, an item assignment is invalid +because it cannot be performed on every possible value. + +```py +def update(value: list[int] | tuple[int, ...]) -> None: + # error: [invalid-assignment] + value[0] += 1 +``` + +## Typed dictionary assignments with multiple possible keys + +A key that can select fields with different value types must only be assigned a value accepted by +every possible field. + +```py +from typing import Literal, TypedDict + +class Payload(TypedDict): + whole: int + fractional: float + +def update(value: Payload, key: Literal["whole", "fractional"]) -> None: + # error: [invalid-assignment] + value[key] /= 2 +``` + +## Inferred collection entries + +Augmented assignments are not yet included when inferring the element type of an unannotated +collection. + +```py +values = [1] +# TODO: Infer `list[float]` instead of rejecting the assignment. +# error: [invalid-assignment] +values[0] /= 2 +``` + ## Implicit dunder calls on class objects ```py diff --git a/crates/ty_python_semantic/resources/mdtest/async.md b/crates/ty_python_semantic/resources/mdtest/async.md index 396b08f355..cf58b332c9 100644 --- a/crates/ty_python_semantic/resources/mdtest/async.md +++ b/crates/ty_python_semantic/resources/mdtest/async.md @@ -239,10 +239,10 @@ def is_async_callable(x: object) -> TypeIs[Top[Callable[..., Awaitable[object]]] async def f(fn: Callable[[int], int | Awaitable[int]]) -> None: if is_async_callable(fn): - reveal_type(fn) # revealed: ((int, /) -> int | Awaitable[int]) & Top[(...) -> Awaitable[object]] + reveal_type(fn) # revealed: ((int, /) -> int | Awaitable[int]) & Top[(...) -> Top[Awaitable[object]]] result = fn(1) - # This includes `int & Awaitable[object]`: an `int` subtype could define `__await__`. - reveal_type(result) # revealed: (int & Awaitable[object]) | Awaitable[int] + # This includes `int & Top[Awaitable[object]]`: an `int` subtype could define `__await__`. + reveal_type(result) # revealed: (int & Top[Awaitable[object]]) | Awaitable[int] reveal_type(await result) # revealed: object ``` diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md index b5b590318f..0185f3ad60 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -259,6 +259,9 @@ reveal_type(c_instance.b) # revealed: int #### Augmented assignments +An augmented assignment contributes its result to the inferred type of an unannotated instance +attribute. + ```py class Weird: def __iadd__(self, other: None) -> str: @@ -269,9 +272,8 @@ class C: self.w = Weird() self.w += None -# TODO: Mypy and pyright do not support this, but it would be great if we could -# infer `str` here (`Weird` is not a possible type for the `w` attribute). -reveal_type(C().w) # revealed: Weird +# TODO: Infer only `str`, since the initial `Weird` value has been overwritten. +reveal_type(C().w) # revealed: Weird | str ``` #### Nested augmented assignments after narrowing @@ -1899,7 +1901,6 @@ error[unresolved-reference]: Name `x` used when not defined | 5 | y = x # snapshot | ^ - | info: An attribute `x` is available: consider using `self.x` ``` @@ -1917,7 +1918,6 @@ error[unresolved-reference]: Name `x` used when not defined | 10 | y = x # snapshot | ^ - | info: An attribute `x` is available: consider using `self.x` ``` @@ -2709,6 +2709,51 @@ accessed on the class itself: CustomGetAttr.whatever ``` +### Invalid `__getattr__` calls + +If `__getattr__` cannot accept the attribute name that Python passes to it, the access is invalid. +The method's return type remains available for error recovery, while defined attributes do not +invoke the fallback. + +```py +class InvalidGetAttr: + defined: bool = True + + def __getattr__(self) -> str: + return "fallback" + +InvalidGetAttr().missing # snapshot: invalid-attribute-access + +# error: [invalid-attribute-access] "Invalid access to attribute `missing` on type `InvalidGetAttr`" +reveal_type(InvalidGetAttr().missing) # revealed: str +reveal_type(InvalidGetAttr().defined) # revealed: bool +``` + +```snapshot +error[invalid-attribute-access]: Invalid access to attribute `missing` on type `InvalidGetAttr` + --> src/mdtest_snippet.py:7:1 + | +7 | InvalidGetAttr().missing # snapshot: invalid-attribute-access + | ^^^^^^^^^^^^^^^^^^^^^^^^ Too many positional arguments to bound method `InvalidGetAttr.__getattr__`: expected 0, got 1 +info: This access implicitly calls `__getattr__` +info: Method signature here + --> src/mdtest_snippet.py:4:9 + | +4 | def __getattr__(self) -> str: + | ^^^^^^^^^^^^^^^^^^^^^^^^ +``` + +An incompatible type for the attribute name is also an invalid fallback call. + +```py +class InvalidNameType: + def __getattr__(self, name: int) -> bytes: + return b"fallback" + +# error: [invalid-attribute-access] "Invalid access to attribute `missing` on type `InvalidNameType`" +reveal_type(InvalidNameType().missing) # revealed: bytes +``` + ### Type of the `name` parameter If the `name` parameter of the `__getattr__` method is annotated with a (union of) literal type(s), @@ -2727,8 +2772,8 @@ reveal_type(date.day) # revealed: int reveal_type(date.month) # revealed: int reveal_type(date.year) # revealed: int -# error: [unresolved-attribute] "Object of type `Date` has no attribute `century`" -reveal_type(date.century) # revealed: Unknown +# error: [invalid-attribute-access] "Invalid access to attribute `century` on type `Date`" +reveal_type(date.century) # revealed: int ``` ### `argparse.Namespace` @@ -2744,6 +2789,8 @@ def _(ns: argparse.Namespace): ## Classes with custom `__getattribute__` methods +### Basic + 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 @@ -2802,6 +2849,113 @@ class ThisFails: ThisFails().x ``` +### Invalid `__getattribute__` calls + +An invalid `__getattribute__` call fails before Python can look up either a defined or missing +attribute. A defined member retains its declared type, while a missing member uses the method's +return type for error recovery. + +```py +class InvalidGetAttribute: + defined: bool = True + + # error: [invalid-method-override] + def __getattribute__(self) -> str: + return "fallback" + +InvalidGetAttribute().missing # snapshot: invalid-attribute-access + +# error: [invalid-attribute-access] "Invalid access to attribute `missing` on type `InvalidGetAttribute`" +reveal_type(InvalidGetAttribute().missing) # revealed: str + +# error: [invalid-attribute-access] "Invalid access to attribute `defined` on type `InvalidGetAttribute`" +reveal_type(InvalidGetAttribute().defined) # revealed: bool + +# error: [invalid-attribute-access] "Invalid access to attribute `__getattribute__` on type `InvalidGetAttribute`" +InvalidGetAttribute().__getattribute__ +``` + +```snapshot +error[invalid-attribute-access]: Invalid access to attribute `missing` on type `InvalidGetAttribute` + --> src/mdtest_snippet.py:8:1 + | +8 | InvalidGetAttribute().missing # snapshot: invalid-attribute-access + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Too many positional arguments to bound method `InvalidGetAttribute.__getattribute__`: expected 0, got 1 +info: This access implicitly calls `__getattribute__` +info: Method signature here + --> src/mdtest_snippet.py:5:9 + | +5 | def __getattribute__(self) -> str: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +``` + +An incompatible type for the attribute name also makes the implicit call invalid. + +```py +class InvalidNameType: + # error: [invalid-method-override] + def __getattribute__(self, name: int) -> bytes: + return b"fallback" + +# error: [invalid-attribute-access] "Invalid access to attribute `missing` on type `InvalidNameType`" +reveal_type(InvalidNameType().missing) # revealed: bytes +``` + +### Inherited invalid `__getattribute__` calls + +An invalid interceptor inherited from a base class also prevents access to attributes declared on +the subclass. + +```py +class InvalidBase: + # error: [invalid-method-override] + def __getattribute__(self) -> int: + return 1 + +class Child(InvalidBase): + defined: str = "hello" + +# error: [invalid-attribute-access] "Invalid access to attribute `defined` on type `Child`" +reveal_type(Child().defined) # revealed: str +``` + +### Invalid `__getattribute__` installed by a metaclass + +A metaclass can install an invalid interceptor in the namespace of each class it creates. + +```py +def invalid_getattribute(self) -> int: + return 1 + +class Meta(type): + def __init__(cls, name: str, bases: tuple[type, ...], namespace: dict[str, object]) -> None: + # error: [invalid-assignment] + cls.__getattribute__ = invalid_getattribute + +class Example(metaclass=Meta): + defined: str = "hello" + +# error: [invalid-attribute-access] "Invalid access to attribute `defined` on type `Example`" +reveal_type(Example().defined) # revealed: str +``` + +### Invalid `__getattribute__` takes precedence over `__getattr__` + +An invalid `__getattribute__` raises before Python can call an otherwise valid `__getattr__` method. + +```py +class CustomAccess: + # error: [invalid-method-override] + def __getattribute__(self) -> int: + return 1 + + def __getattr__(self, name: str) -> str: + return "fallback" + +# error: [invalid-attribute-access] "Invalid access to attribute `missing` on type `CustomAccess`" +reveal_type(CustomAccess().missing) # revealed: int +``` + ## Metaclasses with custom `__getattr__` methods A class is an instance of its metaclass. When attribute lookup on a class fails, Python falls back @@ -2820,6 +2974,22 @@ class Foo(metaclass=Meta): ... reveal_type(Foo.whatever) # revealed: int ``` +### Invalid `__getattr__` calls + +Invalid metaclass `__getattr__` calls are reported on class attribute access while preserving the +method's return type for error recovery. + +```py +class Meta(type): + def __getattr__(cls) -> int: + return 1 + +class Foo(metaclass=Meta): ... + +# error: [invalid-attribute-access] "Invalid access to attribute `missing` on type ``" +reveal_type(Foo.missing) # revealed: int +``` + ### Class attributes take precedence If the class defines the attribute directly, it takes precedence over the metaclass `__getattr__`: @@ -2910,6 +3080,50 @@ class Foo(metaclass=Meta): ... reveal_type(Foo.whatever) # revealed: int ``` +### Invalid `__getattribute__` calls + +A malformed metaclass `__getattribute__` prevents access to both defined and missing class +attributes. Their original types remain available for error recovery. + +```py +class Meta(type): + # error: [invalid-method-override] + def __getattribute__(cls) -> int: + return 1 + +class Foo(metaclass=Meta): + defined: str = "hello" + +# error: [invalid-attribute-access] "Invalid access to attribute `missing` on type ``" +reveal_type(Foo.missing) # revealed: int + +# error: [invalid-attribute-access] "Invalid access to attribute `defined` on type ``" +reveal_type(Foo.defined) # revealed: str + +# error: [invalid-attribute-access] "Invalid access to attribute `__getattribute__` on type ``" +Foo.__getattribute__ +``` + +### Inherited invalid `__getattribute__` calls + +A malformed interceptor inherited by a metaclass still runs before looking up attributes declared on +the class object. + +```py +class InvalidBaseMeta(type): + # error: [invalid-method-override] + def __getattribute__(cls) -> int: + return 1 + +class Meta(InvalidBaseMeta): ... + +class Foo(metaclass=Meta): + defined: str = "hello" + +# error: [invalid-attribute-access] "Invalid access to attribute `defined` on type ``" +reveal_type(Foo.defined) # revealed: str +``` + ### Class attributes take precedence ```py @@ -2981,6 +3195,71 @@ instance.callback = lambda number: ( instance.payload = {"value": 1} ``` +### Nested argument type + +```py +class C: + def __setattr__(self, name: str, value: tuple[int, str]): ... + +c = C() +c.x = (1, b"") # snapshot: invalid-assignment +``` + +```snapshot +error[invalid-assignment]: Cannot assign object of type `tuple[Literal[1], Literal[b""]]` to attribute `x` on type `C` + --> src/mdtest_snippet.py:5:7 + | +5 | c.x = (1, b"") # snapshot: invalid-assignment + | ^^^^^^^^ Expected `tuple[int, str]`, found `tuple[Literal[1], Literal[b""]]` +info: Argument to bound method `C.__setattr__` is incorrect +info: This assignment implicitly calls a custom `__setattr__` method +info: the second tuple element is not compatible: `Literal[b""]` is not assignable to `str` +info: Method defined here + --> src/mdtest_snippet.py:2:9 + | +2 | def __setattr__(self, name: str, value: tuple[int, str]): ... + | ^^^^^^^^^^^ ---------------------- Parameter declared here +``` + +### Overloaded `__setattr__` + +```py +from typing import overload + +class D: + @overload + def __setattr__(self, name: str, value: tuple[int, str]): ... + @overload + def __setattr__(self, name: str, value: int): ... + def __setattr__(self, name: str, value: tuple[int, str] | int): ... + +d = D() +d.x = (1, b"") # snapshot: invalid-assignment +``` + +```snapshot +error[invalid-assignment]: Cannot assign object of type `tuple[Literal[1], Literal[b""]]` to attribute `x` on type `D` + --> src/mdtest_snippet.py:11:1 + | +11 | d.x = (1, b"") # snapshot: invalid-assignment + | ^^^ No overload of bound method `D.__setattr__` matches arguments +info: This assignment implicitly calls a custom `__setattr__` method +info: First overload defined here + --> src/mdtest_snippet.py:4:5 + | +4 | / @overload +5 | | def __setattr__(self, name: str, value: tuple[int, str]): ... + | |_________________________________________________________________^ First overload defined here +info: Possible overloads for bound method `__setattr__`: +info: (self, name: str, value: tuple[int, str]) -> None +info: (self, name: str, value: int) -> None +info: Overload implementation defined here + --> src/mdtest_snippet.py:8:9 + | +8 | def __setattr__(self, name: str, value: tuple[int, str] | int): ... + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +``` + ### Type of the `name` parameter If the `name` parameter of the `__setattr__` method is annotated with a (union of) literal type(s), @@ -2999,10 +3278,55 @@ date.day = 8 date.month = 4 date.year = 2025 -# error: [unresolved-attribute] "Cannot assign object of type `Literal["UTC"]` to attribute `tz` on type `Date` with custom `__setattr__` method." +date.month = "May" # snapshot: invalid-assignment +# snapshot: invalid-assignment +# snapshot: invalid-assignment date.tz = "UTC" ``` +```snapshot +error[invalid-assignment]: Cannot assign object of type `Literal["May"]` to attribute `month` on type `Date` + --> src/mdtest_snippet.py:13:14 + | +13 | date.month = "May" # snapshot: invalid-assignment + | ^^^^^ Expected `int`, found `Literal["May"]` +info: Argument to bound method `Date.__setattr__` is incorrect +info: This assignment implicitly calls a custom `__setattr__` method +info: Method defined here + --> src/mdtest_snippet.py:5:9 + | +5 | def __setattr__(self, name: Literal["day", "month", "year"], value: int) -> None: + | ^^^^^^^^^^^ ---------- Parameter declared here + + +error[invalid-assignment]: Cannot assign object of type `Literal["UTC"]` to attribute `tz` on type `Date` + --> src/mdtest_snippet.py:16:1 + | +16 | date.tz = "UTC" + | ^^^^^^^ Expected `Literal["day", "month", "year"]`, found `Literal["tz"]` +info: Argument to bound method `Date.__setattr__` is incorrect +info: This assignment implicitly calls a custom `__setattr__` method +info: Method defined here + --> src/mdtest_snippet.py:5:9 + | +5 | def __setattr__(self, name: Literal["day", "month", "year"], value: int) -> None: + | ^^^^^^^^^^^ ------------------------------------- Parameter declared here + + +error[invalid-assignment]: Cannot assign object of type `Literal["UTC"]` to attribute `tz` on type `Date` + --> src/mdtest_snippet.py:16:11 + | +16 | date.tz = "UTC" + | ^^^^^ Expected `int`, found `Literal["UTC"]` +info: Argument to bound method `Date.__setattr__` is incorrect +info: This assignment implicitly calls a custom `__setattr__` method +info: Method defined here + --> src/mdtest_snippet.py:5:9 + | +5 | def __setattr__(self, name: Literal["day", "month", "year"], value: int) -> None: + | ^^^^^^^^^^^ ---------- Parameter declared here +``` + ### Return type of `__setattr__` If the return type of the `__setattr__` method is `Never`, we do not allow any attribute assignments @@ -3118,7 +3442,7 @@ def use_module(m: MyModule, param: int) -> None: # But assigning to an attribute that's not explicitly defined will still # use `__setattr__` for validation. - # error: [unresolved-attribute] "Cannot assign object of type `int` to attribute `undefined_param` on type `MyModule` with custom `__setattr__` method." + # error: [invalid-assignment] "Cannot assign object of type `int` to attribute `undefined_param` on type `MyModule`" m.undefined_param = param ``` @@ -3159,7 +3483,7 @@ class Meta(type): class Foo(metaclass=Meta): ... Foo.whatever = 42 -Foo.whatever = "invalid" # error: [unresolved-attribute] "with custom `__setattr__` method" +Foo.whatever = "invalid" # error: [invalid-assignment] ``` If both the metaclass and class define `__setattr__`, class-object assignments use the metaclass @@ -3170,11 +3494,11 @@ class WithSetAttr(metaclass=Meta): def __setattr__(self, name: str, value: str) -> None: ... WithSetAttr.class_attribute = 42 -WithSetAttr.class_attribute = "invalid" # error: [unresolved-attribute] "with custom `__setattr__` method" +WithSetAttr.class_attribute = "invalid" # error: [invalid-assignment] instance = WithSetAttr() instance.instance_attribute = "valid" -instance.instance_attribute = 42 # error: [unresolved-attribute] "with custom `__setattr__` method" +instance.instance_attribute = 42 # error: [invalid-assignment] ``` The same applies when the class object is annotated as `type[Foo]`: @@ -3182,7 +3506,7 @@ The same applies when the class object is annotated as `type[Foo]`: ```py def set_on_subclass(cls: type[Foo]) -> None: cls.whatever = 42 - cls.whatever = "invalid" # error: [unresolved-attribute] "with custom `__setattr__` method" + cls.whatever = "invalid" # error: [invalid-assignment] ``` The setter also provides the expected type when inferring the assigned value: @@ -3226,7 +3550,7 @@ OverloadedClass.callback = lambda number: ( number.missing ) OverloadedClass.payload = {"value": 1} -OverloadedClass.callback = {"value": 1} # error: [unresolved-attribute] "with custom `__setattr__` method" +OverloadedClass.callback = {"value": 1} # error: [invalid-assignment] ``` A metaclass `__setattr__` method returning `Never` prevents writes to undefined attributes: @@ -3849,7 +4173,7 @@ class NestedMixed: def g(self: "NestedMixed"): self.x = {self.x} -reveal_type(NestedMixed().x) # revealed: list[Divergent] | set[Divergent] +reveal_type(NestedMixed().x) # revealed: list[Divergent] | set[Unknown] ``` And cases where the types originate from annotations: @@ -4115,7 +4439,6 @@ error[unresolved-attribute]: Module `datetime` has no member `UTC` | 4 | reveal_type(datetime.UTC) # revealed: Unknown | ^^^^^^^^^^^^ - | info: The member may be available on other Python versions or platforms info: Python 3.10 was assumed when resolving the `UTC` attribute because it was specified on the command line ``` @@ -4137,7 +4460,6 @@ error[unresolved-attribute]: Module `datetime` has no member `fakenotreal` | 4 | reveal_type(datetime.fakenotreal) # revealed: Unknown | ^^^^^^^^^^^^^^^^^^^^ - | ``` ## Unimported submodule incorrectly accessed as attribute @@ -4174,7 +4496,6 @@ warning[possibly-missing-submodule]: Submodule `bar` might not have been importe | 4 | reveal_type(foo.bar) # revealed: Unknown | ^^^^^^^ - | help: Consider explicitly importing `foo.bar` ``` @@ -4193,7 +4514,6 @@ warning[possibly-missing-submodule]: Submodule `bar` might not have been importe | 4 | reveal_type(baz.bar) # revealed: Unknown | ^^^^^^^ - | help: Consider explicitly importing `baz.bar` ``` @@ -4219,7 +4539,6 @@ error[unresolved-attribute]: Object of type `(...) -> Any` has no attribute `__n | 4 | x.__name__ # snapshot: unresolved-attribute | ^^^^^^^^^^ - | help: Function objects have a `__name__` attribute, but not all callable objects are functions help: See this FAQ for more information: ``` @@ -4235,7 +4554,6 @@ error[unresolved-attribute]: Object of type `(...) -> Any` has no attribute `__a | 6 | x.__annotate__ # snapshot: unresolved-attribute | ^^^^^^^^^^^^^^ - | help: Function objects have an `__annotate__` attribute, but not all callable objects are functions help: See this FAQ for more information: ``` diff --git a/crates/ty_python_semantic/resources/mdtest/basedpython_context_sensitive.md b/crates/ty_python_semantic/resources/mdtest/basedpython_context_sensitive.md index 3c32838f93..b639c511b9 100644 --- a/crates/ty_python_semantic/resources/mdtest/basedpython_context_sensitive.md +++ b/crates/ty_python_semantic/resources/mdtest/basedpython_context_sensitive.md @@ -128,7 +128,6 @@ error[unresolved-reference]: Name `Red` used when not defined | 7 | a: Color | Paint = Red # snapshot | ^^^ - | info: `Color` and `Paint` both declare `Red`: write it qualified ``` @@ -148,7 +147,6 @@ error[unresolved-reference]: Name `Green` used when not defined | 4 | b: Color = Green # snapshot | ^^^^^ - | info: `Color` declares `Green`, but this scope binds `Green` itself: write `Color.Green` ``` @@ -277,7 +275,6 @@ error[unresolved-reference]: Name `Red` used when not defined | 3 | a: C = Red # snapshot | ^^^ - | info: `Red` is a member of `Color`, which is not in scope here under that name ``` diff --git a/crates/ty_python_semantic/resources/mdtest/basedpython_conversions.md b/crates/ty_python_semantic/resources/mdtest/basedpython_conversions.md index 1fb17dc2c1..11a691286d 100644 --- a/crates/ty_python_semantic/resources/mdtest/basedpython_conversions.md +++ b/crates/ty_python_semantic/resources/mdtest/basedpython_conversions.md @@ -794,6 +794,8 @@ reveal_type(d) # revealed: dict[str, Show] ```by protocol Show: def show(self) -> str + # a `frozenset` element has to be `Hashable` + def __hash__(self) -> int extension str(Show): override def show(self) -> str: diff --git a/crates/ty_python_semantic/resources/mdtest/basedpython_deferred_type_ops.md b/crates/ty_python_semantic/resources/mdtest/basedpython_deferred_type_ops.md index ec36091302..d7309d76e9 100644 --- a/crates/ty_python_semantic/resources/mdtest/basedpython_deferred_type_ops.md +++ b/crates/ty_python_semantic/resources/mdtest/basedpython_deferred_type_ops.md @@ -12,7 +12,7 @@ re-evaluated when the parameter is specialized at a call site. class Array[Dim: int] def extend[Dim: int](a: Array[Dim]) -> Array[Dim + 1]: - return a + raise NotImplementedError def foo(data: Array[5]): data2 = extend(data) @@ -36,10 +36,10 @@ class Array[Dim: int]: pass def extend[Dim: int](a: Array[Dim]) -> Array[Dim + 1]: - return a + raise NotImplementedError def shrink[Dim: int](a: Array[Dim]) -> Array[Dim - 2]: - return a + raise NotImplementedError def foo(data: Array[5]): reveal_type(extend(extend(data))) # revealed: Array[7] @@ -147,7 +147,7 @@ class Array[Dim: int]: pass def extend[Dim: int](a: Array[Dim]) -> Array[Dim + 1]: - return a + raise NotImplementedError def foo(data: Array[int]): reveal_type(extend(data)) # revealed: Array[int] diff --git a/crates/ty_python_semantic/resources/mdtest/basedpython_keyword_variadic.md b/crates/ty_python_semantic/resources/mdtest/basedpython_keyword_variadic.md index 5bae01cc98..3afbbce0c0 100644 --- a/crates/ty_python_semantic/resources/mdtest/basedpython_keyword_variadic.md +++ b/crates/ty_python_semantic/resources/mdtest/basedpython_keyword_variadic.md @@ -315,7 +315,6 @@ error[invalid-type-form]: Bare keyword-variadic pack `Kwargs` is not valid in th | 2 | def get(self) -> Kwargs: ... # snapshot | ^^^^^^ - | info: A keyword-variadic pack is only valid: info: - unpacked with `**` in a callable parameter list info: - as the default for another keyword-variadic pack diff --git a/crates/ty_python_semantic/resources/mdtest/basedpython_literal_annotations.md b/crates/ty_python_semantic/resources/mdtest/basedpython_literal_annotations.md index cac9f66009..3e972035d9 100644 --- a/crates/ty_python_semantic/resources/mdtest/basedpython_literal_annotations.md +++ b/crates/ty_python_semantic/resources/mdtest/basedpython_literal_annotations.md @@ -58,8 +58,8 @@ reveal_type(b) # revealed: 2j ```by class A[T]: ... -a: A[T=int] = A() -reveal_type(a) # revealed: A[int] +def f(a: A[T=int]): + reveal_type(a) # revealed: A[int] ``` ## keyword type-arg binding reorders by name @@ -67,8 +67,8 @@ reveal_type(a) # revealed: A[int] ```by class B[T, R]: ... -a: B[R=str, T=int] = B() -reveal_type(a) # revealed: B[int, str] +def f(a: B[R=str, T=int]): + reveal_type(a) # revealed: B[int, str] ``` ## keyword type-arg binding falls back to typevar default @@ -81,8 +81,8 @@ python-version = "3.13" ```by class C[T = int, R = str]: ... -a: C[R=int] = C() -reveal_type(a) # revealed: C[int, int] +def f(a: C[R=int]): + reveal_type(a) # revealed: C[int, int] ``` ## forward self-reference works without quotes diff --git a/crates/ty_python_semantic/resources/mdtest/basedpython_trailing_lambda.md b/crates/ty_python_semantic/resources/mdtest/basedpython_trailing_lambda.md index 1f90de8ee2..8904961b17 100644 --- a/crates/ty_python_semantic/resources/mdtest/basedpython_trailing_lambda.md +++ b/crates/ty_python_semantic/resources/mdtest/basedpython_trailing_lambda.md @@ -653,7 +653,6 @@ def f(a): ... f: # error: [invalid-syntax] # error: [invalid-syntax] "Unexpected indentation" # error: [unresolved-reference] "Name `it` used when not defined" -# error: [invalid-syntax] "Expected a statement" print(it) ``` @@ -731,7 +730,6 @@ def f(a: (int) -> None) -> int: x = y = f: # error: [invalid-syntax] "Unexpected indentation" # error: [unresolved-reference] "Name `it` used when not defined" - # error: [invalid-syntax] "Expected a statement" print(it) ``` @@ -748,6 +746,5 @@ def f(a: (int) -> None) -> tuple[int, int]: p, q = f: # error: [invalid-syntax] "Unexpected indentation" # error: [unresolved-reference] "Name `it` used when not defined" - # error: [invalid-syntax] "Expected a statement" print(it) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/bidirectional.md b/crates/ty_python_semantic/resources/mdtest/bidirectional.md index 10a752a0ae..1781a4158a 100644 --- a/crates/ty_python_semantic/resources/mdtest/bidirectional.md +++ b/crates/ty_python_semantic/resources/mdtest/bidirectional.md @@ -67,6 +67,117 @@ def f() -> list[Literal[1]]: return [1] ``` +## Loop-carried assignment context + +A declaration inside a loop provides context to assignments that reach it from an earlier iteration. + +### While loops + +A declaration inside a `while` loop applies to list literals assigned in each iteration. + +```py +while True: + values: list[object] + values = [1] + reveal_type(values) # revealed: list[object] +``` + +### For loops + +The same declaration context applies to assignments in a `for` loop. + +```py +for _ in range(2): + values: list[object] + values = [1] + reveal_type(values) # revealed: list[object] +``` + +### Nested dictionary values + +A declaration inside a loop also provides context for values nested within a dictionary literal. + +```py +from typing import TypedDict + +class Record(TypedDict): + values: list[float] + +while True: + record: Record + record = {"values": [1]} + reveal_type(record) # revealed: Record +``` + +### Invalid dictionary values + +An incompatible dictionary item is reported at the assignment, not at the declaration. + +```py +from typing import TypedDict + +class Record(TypedDict): + value: int + +while True: + record: Record + record = {"value": "invalid"} # error: [invalid-argument-type] + reveal_type(record) # revealed: Record +``` + +### Stringified annotations + +String annotations provide their resolved type when a loop-carried assignment needs context. + +```py +while True: + values: "list[object]" + values = [1] + reveal_type(values) # revealed: list[object] +``` + +### Deferred forward references + +Deferred annotations resolve a `TypedDict` defined after the loop before inferring its dictionary +assignments. + +```py +from __future__ import annotations +from typing import TypedDict + +for _ in range(2): + record: Record + record = {"value": 1} + reveal_type(record) # revealed: Record + + invalid: Record + invalid = {"value": "invalid"} # error: [invalid-argument-type] + +class Record(TypedDict): + value: int +``` + +### Deferred forward references on Python 3.14 + +Annotations are deferred by default in Python 3.14 and later. + +```toml +[environment] +python-version = "3.14" +``` + +```py +from typing import TypedDict + +for _ in range(2): + record: Record + record = {"value": 1} + reveal_type(record) # revealed: Record + +class Record(TypedDict): + value: int +``` + ## Collection literals ### Basic @@ -137,6 +248,95 @@ reveal_type(s) # revealed: dict[int | str, int | str] reveal_type(s) # revealed: dict[int | str, int | str] ``` +### Exact float types in covariant contexts + +A covariant collection context must preserve an exact float when numeric promotion would introduce +an `int` that the expected element type rejects. + +```py +from collections.abc import Iterable, Sequence +from ty_extensions import JustFloat + +def takes_exact_sequence(values: Sequence[JustFloat]) -> None: ... +def takes_exact_iterable(values: Iterable[JustFloat]) -> None: ... +def takes_exact_list(values: list[JustFloat]) -> None: ... + +takes_exact_sequence([1.0]) +takes_exact_sequence((1.0,)) +takes_exact_sequence([1]) # error: [invalid-argument-type] + +takes_exact_iterable([1.0]) +takes_exact_iterable((1.0,)) +takes_exact_iterable([1]) # error: [invalid-argument-type] + +takes_exact_list([1.0]) + +annotated: list[JustFloat] = [1.0] +takes_exact_sequence(annotated) +``` + +Ordinary `float` contexts and unannotated mutable lists must retain numeric promotion. + +```py +def takes_float_sequence(values: Sequence[float]) -> None: ... + +takes_float_sequence([1.0]) +takes_float_sequence([1]) + +mutable_floats = [1.0] +mutable_floats.append(1) +reveal_type(mutable_floats) # revealed: list[int | float] +``` + +### Exact complex types in covariant contexts + +The same contextual restriction applies when promoting an exact complex number would introduce `int` +and `float`. + +```py +from collections.abc import Sequence +from ty_extensions import JustComplex + +def takes_exact_complexes(values: Sequence[JustComplex]) -> None: ... + +takes_exact_complexes([1j]) +takes_exact_complexes((1j,)) +takes_exact_complexes([1]) # error: [invalid-argument-type] +takes_exact_complexes([1.0]) # error: [invalid-argument-type] +``` + +### Exact-type protocols in covariant contexts + +A writable `__class__` property allows an invariant protocol to distinguish a runtime float from an +integer. A covariant sequence of a union containing this protocol must preserve that distinction. + +```py +from collections.abc import Sequence +from typing import Generic, Protocol, TypeVar + +T = TypeVar("T") + +class Just(Protocol, Generic[T]): + @property + def __class__(self, /) -> type[T]: ... + @__class__.setter + def __class__(self, value: type[T], /) -> None: ... + +def takes_exact_float(value: Just[float]) -> None: ... +def takes_exact_values(values: Sequence[str | Just[float]]) -> None: ... + +takes_exact_float(1.0) +takes_exact_float(1) # error: [invalid-argument-type] + +takes_exact_values(["1", 1.0]) +takes_exact_values(["1", float("nan")]) +takes_exact_values(("1", 1.0)) +takes_exact_values(["1", 1]) # error: [invalid-argument-type] + +annotated: list[str | Just[float]] = ["1", 1.0] +takes_exact_values(annotated) +``` + ### Optional unions ```py @@ -710,13 +910,14 @@ def _(): ## Prefer the declared type of generic classes and callables When inferring a generic call, we only use the declared type as type context if it is in -non-covariant position. The final annotated assignment binding still uses the declared type if the -inferred and declared types are mutually assignable: +non-covariant position. Unused type parameters are inferred as covariant. The final annotated +assignment binding still uses the declared type if the inferred and declared types are mutually +assignable: ```py from typing import Any -class Bivariant[T]: +class UnusedTypeParameter[T]: pass class Covariant[T]: @@ -730,8 +931,8 @@ class Contravariant[T]: class Invariant[T]: x: T -def bivariant[T](x: T) -> Bivariant[T]: - return Bivariant() +def unused_type_parameter[T](x: T) -> UnusedTypeParameter[T]: + return UnusedTypeParameter() def covariant[T](x: T) -> Covariant[T]: return Covariant() @@ -742,32 +943,32 @@ def contravariant[T](x: T) -> Contravariant[T]: def invariant[T](x: T) -> Invariant[T]: return Invariant() -x1 = bivariant(1) +x1 = unused_type_parameter(1) x2 = covariant(1) x3 = contravariant(1) x4 = invariant(1) -reveal_type(x1) # revealed: Bivariant[Literal[1]] +reveal_type(x1) # revealed: UnusedTypeParameter[Literal[1]] reveal_type(x2) # revealed: Covariant[Literal[1]] reveal_type(x3) # revealed: Contravariant[int] reveal_type(x4) # revealed: Invariant[int] -x5: Bivariant[int | None] = bivariant(1) +x5: UnusedTypeParameter[int | None] = unused_type_parameter(1) x6: Covariant[int | None] = covariant(1) x7: Contravariant[int | None] = contravariant(1) x8: Invariant[int | None] = invariant(1) -reveal_type(x5) # revealed: Bivariant[int | None] +reveal_type(x5) # revealed: UnusedTypeParameter[Literal[1]] reveal_type(x6) # revealed: Covariant[Literal[1]] reveal_type(x7) # revealed: Contravariant[int | None] reveal_type(x8) # revealed: Invariant[int | None] -x9: Bivariant[Any] = bivariant(1) +x9: UnusedTypeParameter[Any] = unused_type_parameter(1) x10: Covariant[Any] = covariant(1) x11: Contravariant[Any] = contravariant(1) x12: Invariant[Any] = invariant(1) -reveal_type(x9) # revealed: Bivariant[Any] +reveal_type(x9) # revealed: UnusedTypeParameter[Any] reveal_type(x10) # revealed: Covariant[Any] reveal_type(x11) # revealed: Contravariant[Any] reveal_type(x12) # revealed: Invariant[Any] @@ -1626,11 +1827,10 @@ reveal_type(f7) # revealed: (*args) -> None f8: Callable[[int], None] = lambda *, x=1: None reveal_type(f8) # revealed: (int, /) -> None -# `Callable` annotations only describe positional parameters, so the keyword-only `x` is not -# compatible with the positional suffix in the annotation. -# error: [invalid-assignment] +# An optional keyword-only parameter does not prevent `*args` from accepting the positional +# suffix in a `Callable` annotation. f9: Callable[[*tuple[int, ...], int], None] = lambda *args, x=1: None -reveal_type(f9) # revealed: (*tuple[int, ...], int) -> None +reveal_type(f9) # revealed: (*args, *, x: int = 1) -> None f10: Callable[[str, int, str], tuple[str, int, str]] = lambda x, y, z: reveal_type((x, y, z)) # revealed: tuple[str, int, str] reveal_type(f10) # revealed: (x: str, y: int, z: str) -> tuple[str, int, str] diff --git a/crates/ty_python_semantic/resources/mdtest/binary/custom.md b/crates/ty_python_semantic/resources/mdtest/binary/custom.md index 92f60d0977..1b63bc0158 100644 --- a/crates/ty_python_semantic/resources/mdtest/binary/custom.md +++ b/crates/ty_python_semantic/resources/mdtest/binary/custom.md @@ -320,7 +320,6 @@ error[unsupported-operator]: Unsupported `+` operation | ---^^^--- | | | Both operands have type `` - | ``` ```py @@ -336,7 +335,6 @@ error[unsupported-operator]: Unsupported `+` operation | ---^^^--- | | | Both operands have type `` - | ``` ```py @@ -352,7 +350,6 @@ error[unsupported-operator]: Unsupported `+` operation | --^^^-- | | | Both operands have type `` - | ``` ## Subclass @@ -449,5 +446,4 @@ error[unsupported-operator]: Unsupported `+` operation | | | | | Has type `mod1.A` | Has type `mod2.A` - | ``` diff --git a/crates/ty_python_semantic/resources/mdtest/binary/instances.md b/crates/ty_python_semantic/resources/mdtest/binary/instances.md index f88a1d3006..6782dbf908 100644 --- a/crates/ty_python_semantic/resources/mdtest/binary/instances.md +++ b/crates/ty_python_semantic/resources/mdtest/binary/instances.md @@ -494,7 +494,6 @@ error[unsupported-bool-conversion]: Boolean conversion is not supported for type | 7 | 10 and a and True | ^ - | info: `__bool__` on `NotBoolable` must be callable ``` diff --git a/crates/ty_python_semantic/resources/mdtest/call/abstract_method.md b/crates/ty_python_semantic/resources/mdtest/call/abstract_method.md index 6509b63edf..e0552d43b4 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/abstract_method.md +++ b/crates/ty_python_semantic/resources/mdtest/call/abstract_method.md @@ -18,7 +18,7 @@ Foo.method() ```snapshot error[call-abstract-method]: Cannot call `method` on class object - --> src/mdtest_snippet.py:4:5 + --> src/mdtest_snippet.py:9:1 | 4 | / @classmethod 5 | | @abstractmethod @@ -28,7 +28,6 @@ error[call-abstract-method]: Cannot call `method` on class object 8 | # snapshot: call-abstract-method 9 | Foo.method() | ^^^^^^^^^^^^ `method` is an abstract classmethod with a trivial body - | ``` ## Abstract staticmethod with trivial body on class literal diff --git a/crates/ty_python_semantic/resources/mdtest/call/builtins.md b/crates/ty_python_semantic/resources/mdtest/call/builtins.md index cc3dbd8e9a..45062b7996 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/builtins.md +++ b/crates/ty_python_semantic/resources/mdtest/call/builtins.md @@ -412,7 +412,7 @@ result to `Sized` or `object`; ideally the element type would remain `Unknown`, return type would still be used where possible. ```py -from ty_extensions import Unknown +from ty_extensions._internal import Unknown def _(xs: Unknown): # TODO: should be `list[Unknown]` @@ -466,7 +466,6 @@ error[call-non-callable]: `NotImplemented` is not callable | --------------^^ | | | Did you mean `NotImplementedError`? - | ``` ```py @@ -483,13 +482,12 @@ error[call-non-callable]: `NotImplemented` is not callable | --------------^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | | | Did you mean `NotImplementedError`? - | ``` ## `map` with generic callbacks ```py -from ty_extensions import Unknown +from ty_extensions._internal import Unknown import re def _(s: Unknown | str): @@ -503,3 +501,73 @@ def _(xs: Unknown | list[str]): tokens: list[Unknown | str] = [] tokens.extend(escaped) ``` + +## Failed `map` calls retain their result type + +When the argument count identifies a single `map` overload, an incompatible callback still produces +its usual error. The mapped values retain the callback's return type and do not produce an +additional error when called. + +```py +class Function: + def __init__(self, value: str) -> None: ... + def __call__(self) -> None: ... + +# error: [invalid-argument-type] +for function in map(Function, [object()]): + function() +``` + +## `dict` calls do not expose internal type variables + +Several `dict` overloads accept one positional argument. Whichever is selected, the constructed type +is reported in terms of the argument, never in terms of `dict`'s own type variables. + +```toml +[analysis] +strict-generic-narrowing = true +``` + +```py +from collections.abc import Mapping + +def copy(value: object) -> dict[str, str]: + if isinstance(value, Mapping): + # error: [invalid-return-type] "Return type does not match returned value: expected `dict[str, str]`, found `dict[object, object]`" + return dict(value) + return {} +``` + +## Failed `dict` calls preserve narrowed mapping types + +An invalid `dict` call must not invalidate an assignment inside a branch where the original value +has already been narrowed to a mapping. + +```toml +[analysis] +strict-generic-narrowing = true +``` + +```py +from collections.abc import Mapping + +def clean(value: dict[str, int] | str | None) -> None: + if isinstance(value, Mapping): + value = dict(value, 1) # error: [no-matching-overload] + reveal_type(value) # revealed: dict[str, int] + for key, item in value.items(): + value[key] = item +``` + +## Failed inner `OrderedDict` calls do not invalidate outer constructors + +Constructing an `OrderedDict` from a list containing both strings and floats is already rejected. +That failure must not cause a second error when the resulting value is passed to another +`OrderedDict` constructor. + +```py +from collections import OrderedDict + +items = [OrderedDict([["key", 1.0]])] # error: [no-matching-overload] +OrderedDict(zip(["name"], items)) +``` diff --git a/crates/ty_python_semantic/resources/mdtest/call/callables_as_descriptors.md b/crates/ty_python_semantic/resources/mdtest/call/callables_as_descriptors.md index 6078706837..68dce9a5f0 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/callables_as_descriptors.md +++ b/crates/ty_python_semantic/resources/mdtest/call/callables_as_descriptors.md @@ -143,6 +143,45 @@ class C2: C2().method_decorated(1) ``` +A generic decorator must preserve a type variable bound by the method's enclosing class, even when +the decorator uses an ellipsis instead of a `ParamSpec`: + +```py +def preserve_return[R](function: Callable[..., R]) -> Callable[..., R]: + return function + +class DecoratedBox[T]: + @preserve_return + def value(self) -> T: + raise NotImplementedError + + @preserve_return + def values(self) -> list[T]: + raise NotImplementedError + +reveal_type(DecoratedBox[int]().value()) # revealed: int +reveal_type(DecoratedBox[int]().values()) # revealed: list[int] +``` + +The same behavior applies to decorators and classes using legacy type variables: + +```py +from typing import Generic, TypeVar + +LegacyT = TypeVar("LegacyT") +LegacyR = TypeVar("LegacyR") + +def legacy_preserve_return(function: Callable[..., LegacyR]) -> Callable[..., LegacyR]: + return function + +class LegacyDecoratedBox(Generic[LegacyT]): + @legacy_preserve_return + def value(self) -> LegacyT: + raise NotImplementedError + +reveal_type(LegacyDecoratedBox[int]().value()) # revealed: int +``` + And if the callable-typed decorator leaves some generic parameters unconstrained, we should keep those parameters unspecialized rather than collapsing them to `Never`: diff --git a/crates/ty_python_semantic/resources/mdtest/call/constructor.md b/crates/ty_python_semantic/resources/mdtest/call/constructor.md index d7dda6eef3..40eae772ba 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/constructor.md +++ b/crates/ty_python_semantic/resources/mdtest/call/constructor.md @@ -277,6 +277,103 @@ class Foo: reveal_type(Foo(1)) # revealed: Foo ``` +## Implicit `__new__` receivers + +An unannotated `cls` parameter on `__new__` is inferred as `type[Self]`. Constructor calls must be +accepted when a generic callback determines the class's type argument. Here, the correlated callback +overloads, covariant `frozenset`, and fully dynamic `values` exercise a path-merged specialization. +Reapplying that specialization to the synthetic `cls` would introduce an extra `frozenset` layer and +incorrectly reject both ordinary and signature-preserving `Callable` constructors. + +```pyi +from collections.abc import Callable +from typing import Any, Generic, ParamSpec, Protocol, TypeVar, overload +from typing_extensions import Self + +P = ParamSpec("P") +R = TypeVar("R") +R_co = TypeVar("R_co", covariant=True) +T = TypeVar("T") + +@overload +def callback(value: frozenset[T]) -> T: ... +@overload +def callback(value: T) -> T: ... + +class Mapper(Generic[R]): + def __new__(cls, callback: Callable[[T], R], values: list[T], /) -> Self: ... + +values: Any + +# TODO: Preserve correlated overload solutions so dynamic values do not infer an extra +# `frozenset` layer or an element type of `Never`. +reveal_type(Mapper(callback, values)) # revealed: Mapper[frozenset[frozenset[Never]]] + +def wrap(function: Callable[P, R]) -> Callable[P, R]: ... + +class Wrapped(Generic[R]): + @wrap + def __new__(cls, callback: Callable[[T], R], values: list[T]) -> Self: ... + +# TODO: Preserve the callback's correlated overload solutions through the decorator. +reveal_type(Wrapped(callback, values)) # revealed: Wrapped[frozenset[frozenset[Never]]] +``` + +A decorator can preserve `cls` explicitly with `Concatenate`, re-expressing the receiver with its +own type variable. Constraint inference checks the synthetic receiver; the later assignability pass +must not reject it merely because that decorator-scoped type variable remains unsolved: + +```pyi +from typing_extensions import Concatenate + +def wrap_cls(function: Callable[Concatenate[type[T], P], R]) -> Callable[Concatenate[type[T], P], R]: ... + +class WrappedCls: + @wrap_cls + def __new__(cls) -> Self: ... + +reveal_type(WrappedCls()) # revealed: WrappedCls +``` + +A decorator can also return a callback protocol instead of `Callable`. Its inferred `type[Self]` +receiver must likewise not be rejected by the later assignability pass: + +```pyi +class CallableObject(Protocol[P, R_co]): + def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R_co: ... + +def wrap_object(function: Callable[P, R_co]) -> CallableObject[P, R_co]: ... + +class WrappedObject: + @wrap_object + def __new__(cls) -> Self: ... + +reveal_type(WrappedObject()) # revealed: WrappedObject +``` + +The explicit `cls` type in a signature-preserving decorator can also be expressed with a generic +type alias. The alias must be resolved when identifying the constructor receiver, or the later +assignability pass rejects the synthetic argument against the decorator's unsolved receiver type: + +```toml +[environment] +python-version = "3.12" +``` + +```pyi +type Receiver[X] = type[X] + +def preserve[U, **P, R]( + function: Callable[Concatenate[Receiver[U], P], R], +) -> Callable[Concatenate[Receiver[U], P], R]: ... + +class Simple: + @preserve + def __new__(cls) -> Self: ... + +reveal_type(Simple()) # revealed: Simple +``` + ## `__new__` defined as a classmethod Marking it as a classmethod, on the other hand, breaks at runtime. @@ -762,7 +859,7 @@ class C[T]: x: T def __new__[S](cls, x: S) -> "C[tuple[S, S]]": - return object.__new__(cls) + raise NotImplementedError() reveal_type(C(1)) # revealed: C[tuple[int, int]] reveal_type(C("hello")) # revealed: C[tuple[str, str]] @@ -894,6 +991,76 @@ reveal_type(SimpleMixed(1)) # revealed: int reveal_type(SimpleMixed("foo")) # revealed: SimpleMixed ``` +### Overlapping generic `__new__` overloads preserve first-match selection + +A synthetic constructor receiver can still contain inferable class type variables, even though each +overload specializes `cls` differently. Step 5 of the overload evaluation algorithm must preserve +the overload's inferable variables when checking whether the argument types are covered; otherwise a +concrete constructor call appears ambiguous. In particular, both overloads below accept `list[int]`, +but the first one must win. The non-instance return case verifies that this is not specific to +`Self` or to returning the constructed class. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from __future__ import annotations + +from typing import Self, overload + +class MixedSelf[T]: + @overload + def __new__(cls, value: list[T]) -> Self: ... + @overload + def __new__(cls, value: T) -> T: ... + def __new__(cls, value: object) -> object: + return object.__new__(cls) + +reveal_type(MixedSelf([1])) # revealed: MixedSelf[int] +reveal_type(MixedSelf(1)) # revealed: Literal[1] + +class DistinctNonInstanceReturns[T]: + @overload + def __new__(cls, value: list[T]) -> str: ... + @overload + def __new__(cls, value: T) -> T: ... + def __new__(cls, value: object) -> object: + return object.__new__(cls) + +reveal_type(DistinctNonInstanceReturns([1])) # revealed: str +``` + +### A gradual constructor receiver participates in overload filtering + +The synthetic `cls` argument must participate in overload filtering because it can be the only +gradual argument. A concrete class specialization selects its matching receiver overload, but an +`Any` specialization can match receivers with different return types and must remain ambiguous. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from __future__ import annotations + +from typing import Any, overload + +class Foo[T]: + @overload + def __new__(cls: type[Foo[int]]) -> int: ... + @overload + def __new__(cls: type[Foo[str]]) -> str: ... + def __new__(cls) -> object: ... + +reveal_type(Foo[int]()) # revealed: int +reveal_type(Foo[str]()) # revealed: str +# `Any` matches both overloads, so the result is either of their return types +reveal_type(Foo[Any]()) # revealed: UnsafeUnion[int, str] +``` + ### Multiple matching `__new__` overloads If overload resolution for `__new__` stays ambiguous because the argument is `Any` or `Unknown`, so @@ -1347,6 +1514,193 @@ def f(cls: type[T]): reveal_type(cls(1, "foo")) # revealed: T@f ``` +## Intersection constructors + +```toml +[environment] +python-version = "3.12" +``` + +### Narrowed bound type variables + +Narrowing a bounded class type with `issubclass` must use the subclass constructor while preserving +both the original type variable and the narrowed subclass in the return type. + +```py +class Base: + def __init__(self, value: str) -> None: ... + +class IntConstructor(Base): + def __init__(self, value: int) -> None: ... + +def valid[T: Base](cls: type[T]) -> T: + if issubclass(cls, IntConstructor): + reveal_type(cls) # revealed: type[T@valid] & type[IntConstructor] + reveal_type(cls(1)) # revealed: T@valid & IntConstructor + return cls(1) + return cls("ok") +``` + +An argument accepted by the bound's constructor must still be rejected by the narrowed subclass. +Arguments rejected by both constructors produce only the subclass constructor's diagnostic. + +```py +def invalid_arguments[T: Base](cls: type[T]) -> None: + if issubclass(cls, IntConstructor): + # error: [invalid-argument-type] "Argument to `IntConstructor.__init__` is incorrect: Expected `int`, found `Literal["wrong"]`" + cls("wrong") + # error: [invalid-argument-type] "Argument to `IntConstructor.__init__` is incorrect: Expected `int`, found `None`" + cls(None) +``` + +### Narrowed constrained type variables + +Narrowing a constrained type variable selects the matching constructor without losing the original +type variable in the return type. + +```py +class StringConstructor: + def __init__(self, value: str) -> None: ... + +class IntConstructor: + def __init__(self, value: int) -> None: ... + +def construct[T: (StringConstructor, IntConstructor)](cls: type[T]) -> None: + if issubclass(cls, IntConstructor): + reveal_type(cls) # revealed: type[T@construct] & type[IntConstructor] + reveal_type(cls(1)) # revealed: T@construct & IntConstructor + # error: [invalid-argument-type] "Argument to `IntConstructor.__init__` is incorrect: Expected `int`, found `Literal["wrong"]`" + cls("wrong") +``` + +### Narrowed unbounded type variables + +The narrowed subclass constructor also determines which arguments are valid when the original type +variable has no upper bound. + +```py +class IntConstructor: + def __init__(self, value: int) -> None: ... + +def construct[T](cls: type[T]) -> None: + if issubclass(cls, IntConstructor): + reveal_type(cls) # revealed: type[T@construct] & type[IntConstructor] + reveal_type(cls(1)) # revealed: T@construct & IntConstructor + # error: [invalid-argument-type] "Argument to `IntConstructor.__init__` is incorrect: Expected `int`, found `Literal["wrong"]`" + cls("wrong") +``` + +### Specialized generic constructors + +A generic constructor provider must retain its explicit specialization when validating arguments and +preserve the original type variable in its return type. + +```py +from ty_extensions import Intersection + +class Box[S]: + def __init__(self, value: S) -> None: ... + +def construct[T](cls: Intersection[type[T], type[Box[int]]]) -> None: + reveal_type(cls(1)) # revealed: T@construct & Box[int] + # error: [invalid-argument-type] "Argument to `Box.__init__` is incorrect: Expected `int`, found `Literal["wrong"]`" + cls("wrong") +``` + +### Built-in constructor behavior + +Narrowing to a final built-in class must retain its specialized constructor behavior and the +original type variable. + +```py +def construct[T](cls: type[T]) -> None: + if issubclass(cls, bool): + reveal_type(cls) # revealed: type[T@construct] & + reveal_type(cls(1)) # revealed: T@construct & Literal[True] +``` + +### `Self` constructor returns + +An explicit `Self` return still represents the constructed instance, so narrowing must preserve the +original type variable and validate the subclass initializer. + +```py +from typing import Self + +class Base: + def __init__(self, value: str) -> None: ... + +class NewChild(Base): + def __new__(cls, value: object) -> Self: + return object.__new__(cls) + + def __init__(self, value: int) -> None: ... + +def construct[T: Base](cls: type[T]) -> None: + if issubclass(cls, NewChild): + reveal_type(cls(1)) # revealed: T@construct & NewChild + # error: [invalid-argument-type] "Argument to `NewChild.__init__` is incorrect: Expected `int`, found `Literal["wrong"]`" + cls("wrong") +``` + +### Non-instance `__new__` returns + +A constructor explicitly returning a non-instance type must retain that return type instead of +intersecting it with the original type variable. + +```py +class Base: + def __init__(self, value: str) -> None: ... + +class ReturnsString(Base): + def __new__(cls, value: int) -> str: + return str(value) + +def construct[T: Base](cls: type[T]) -> None: + if issubclass(cls, ReturnsString): + reveal_type(cls(1)) # revealed: str +``` + +### Non-instance metaclass `__call__` returns + +A custom metaclass's explicit non-instance return similarly takes precedence over the original type +variable. + +```py +class StringFactory(type): + def __call__(cls, value: int) -> str: + return str(value) + +class Base: + def __init__(self, value: str) -> None: ... + +class Factory(Base, metaclass=StringFactory): ... + +def construct[T: Base](cls: type[T]) -> None: + if issubclass(cls, Factory): + reveal_type(cls(1)) # revealed: str +``` + +### Independent metaclass callables + +An intersection of a class-object type and a metaclass instance retains both independent callables. +Arguments accepted by only one callable use that callable's return type. + +```py +from ty_extensions import Intersection + +class StringBase: + def __init__(self, value: str) -> None: ... + +class IntMeta(type): + def __call__(cls, value: int) -> str: + return str(value) + +def construct(cls: Intersection[type[StringBase], IntMeta]) -> None: + reveal_type(cls(1)) # revealed: str + reveal_type(cls("ok")) # revealed: StringBase +``` + ## Union of constructors ```py @@ -1586,3 +1940,40 @@ reveal_type(C()) # revealed: C # Meta.__lt__ is implicitly called here: reveal_type(C < C) # revealed: Literal[True] ``` + +## A constructor call that matches no overload + +When no overload of an overloaded constructor accepts the arguments, nothing is inferred for the +class's type parameters. The constructed type says `Unknown` for them rather than naming the type +variables themselves, which belong to the class's declaration and mean nothing at the call site. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import overload + +class Pair[T]: + @overload + def __init__(self, value: list[T]) -> None: ... + @overload + def __init__(self, value: set[T]) -> None: ... + def __init__(self, value) -> None: ... + +reveal_type(Pair([1])) # revealed: Pair[int] +# error: [no-matching-overload] +reveal_type(Pair(1)) # revealed: Pair[Unknown] +``` + +A constructor with a single signature keeps the ordinary treatment of an unsolved type parameter, +because there is no ambiguity about which signature was meant: + +```py +class Boxed[T]: + def __init__(self, values: list[T]) -> None: ... + +# error: [invalid-argument-type] +reveal_type(Boxed(1)) # revealed: Boxed[Never] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/call/function.md b/crates/ty_python_semantic/resources/mdtest/call/function.md index c6af60e735..3db35d754a 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/function.md +++ b/crates/ty_python_semantic/resources/mdtest/call/function.md @@ -1213,6 +1213,179 @@ def f(*args: int) -> int: reveal_type(f()) # revealed: int ``` +### Unpacked variadic arguments can require positional arguments + +An unpacked tuple can require arguments even though an ordinary variadic parameter can be empty. +Fixed tuples also reject positional arguments beyond their declared length. + +```toml +[environment] +python-version = "3.13" +``` + +```py +def at_least_one(*args: *tuple[*tuple[int, ...], int]) -> None: ... +def exactly_two(*args: *tuple[int, str]) -> None: ... +def exactly_zero(*args: *tuple[()]) -> None: ... + +at_least_one() # error: [missing-argument] +at_least_one(1) +at_least_one(1, 2) +at_least_one("wrong") # error: [invalid-argument-type] +at_least_one(1, "wrong") # error: [invalid-argument-type] + +exactly_two() # error: [missing-argument] +exactly_two(1) # error: [missing-argument] +exactly_two(1, "two") +exactly_two("one", "two") # error: [invalid-argument-type] +exactly_two(1, 2) # error: [invalid-argument-type] +exactly_two(1, "two", 3) # error: [too-many-positional-arguments] + +exactly_zero() +exactly_zero(1) # error: [too-many-positional-arguments] +``` + +### Unpacked variadic arity errors preserve element diagnostics + +Matched tuple elements should still be checked when a call has the wrong arity. + +```toml +[environment] +python-version = "3.13" +``` + +```py +def exactly_two(*args: *tuple[int, str]) -> None: ... + +exactly_two(1, "valid") +exactly_two("wrong", "valid") # error: [invalid-argument-type] + +# TODO: error: [invalid-argument-type] +# error: [missing-argument] +exactly_two("wrong") + +# TODO: error: [invalid-argument-type] +# error: [too-many-positional-arguments] +exactly_two("wrong", "valid", 3) + +# TODO: error: [invalid-argument-type] +# TODO: error: [invalid-argument-type] +# error: [too-many-positional-arguments] +exactly_two("wrong", 2, 3) +``` + +The same recovery should validate fixed prefixes when a required suffix is missing. + +```py +def with_suffix(*args: *tuple[int, *tuple[str, ...], bytes]) -> None: ... + +# TODO: error: [invalid-argument-type] +# error: [missing-argument] +with_suffix("wrong") +``` + +Forwarding a fixed-length tuple should preserve the same element and arity diagnostics. + +```py +def forward(values: tuple[str]) -> None: + # TODO: error: [invalid-argument-type] + # error: [missing-argument] + exactly_two(*values) +``` + +Callable protocols should use the same recovery as ordinary functions. + +```py +from typing import Protocol + +class ExactlyTwo(Protocol): + def __call__(self, *args: *tuple[int, str]) -> None: ... + +def call(callback: ExactlyTwo) -> None: + # TODO: error: [invalid-argument-type] + # error: [missing-argument] + callback("wrong") +``` + +### Unpacked variadic arguments preserve element positions + +Fixed prefixes, a homogeneous variadic segment, and fixed suffixes each retain their own argument +types. A required suffix also requires any preceding defaulted positional parameter to be filled. + +```toml +[environment] +python-version = "3.13" +``` + +```py +def mixed(*args: *tuple[int, *tuple[str, ...], bytes]) -> None: ... +def with_default(first: int = 0, *args: *tuple[*tuple[int, ...], int]) -> None: ... + +mixed(1, b"last") +mixed(1, "middle", b"last") +mixed("first", b"last") # error: [invalid-argument-type] +mixed(1, 2, b"last") # error: [invalid-argument-type] +mixed(1, "middle", "last") # error: [invalid-argument-type] + +with_default() # error: [missing-argument] +with_default(first=1) # error: [missing-argument] +with_default(1) # error: [missing-argument] +with_default(1, 2) +``` + +### Unpacked variadic elements preserve generic bounds + +Ordinary type variables are inferred from individual unpacked elements, even beside an unresolved +type-variable tuple. Their upper bounds remain enforced. + +```toml +[environment] +python-version = "3.13" +``` + +```py +def fixed[T: str](*args: *tuple[T]) -> T: + return args[0] + +def suffix[T: str, *Ts](*args: *tuple[*Ts, T]) -> T: + return args[-1] + +reveal_type(fixed("valid")) # revealed: Literal["valid"] +fixed(1) # error: [invalid-argument-type] + +reveal_type(suffix("prefix", "valid")) # revealed: Literal["valid"] +suffix("prefix", 1) # error: [invalid-argument-type] +``` + +### Callable protocols enforce unpacked variadic requirements + +Calling a callable protocol uses the same tuple element types and argument-count bounds as calling +an ordinary function. + +```toml +[environment] +python-version = "3.13" +``` + +```py +from typing import Protocol + +class AtLeastOne(Protocol): + def __call__(self, *args: *tuple[*tuple[int, ...], int]) -> None: ... + +class ExactlyOne(Protocol): + def __call__(self, *args: *tuple[int]) -> None: ... + +def call(at_least_one: AtLeastOne, exactly_one: ExactlyOne) -> None: + at_least_one() # error: [missing-argument] + at_least_one(1) + at_least_one("wrong") # error: [invalid-argument-type] + + exactly_one(1) + exactly_one() # error: [missing-argument] + exactly_one(1, 2) # error: [too-many-positional-arguments] +``` + ### Keywords argument is not required ```py @@ -1471,13 +1644,11 @@ error[invalid-argument-type]: Argument to function `f` is incorrect | 15 | f(**Foo1(a=1, b="b")) | ^^^^^^^^^^^^^^^^^^ Expected `int`, found `str` - | info: Function defined here --> src/mdtest_snippet.py:11:5 | 11 | def f(**kwargs: int) -> None: ... | ^ ------------- Parameter declared here - | error[invalid-argument-type]: Argument to function `f` is incorrect @@ -1485,13 +1656,11 @@ error[invalid-argument-type]: Argument to function `f` is incorrect | 15 | f(**Foo1(a=1, b="b")) | ^^^^^^^^^^^^^^^^^^ Possible extra items in unpacked open `TypedDict` have type `object`, expected `int` - | info: Function defined here --> src/mdtest_snippet.py:11:5 | 11 | def f(**kwargs: int) -> None: ... | ^ ------------- Parameter declared here - | ``` ### TypedDict union @@ -1577,7 +1746,7 @@ Or, it can be a type that is assignable to `str`. ```py from typing import Any -from ty_extensions import Unknown +from ty_extensions._internal import Unknown def _(kwargs1: dict[Any, int], kwargs2: dict[Unknown, int]) -> None: f(**kwargs1) @@ -1620,7 +1789,7 @@ def _(kwargs: dict[str, int]) -> None: ### `Unknown` type ```py -from ty_extensions import Unknown +from ty_extensions._internal import Unknown def f(**kwargs: int) -> None: ... def _(kwargs: Unknown): @@ -1764,7 +1933,7 @@ variadic expansion should not greedily consume optional positional parameters th as explicit keyword arguments. ```py -from ty_extensions import Unknown +from ty_extensions._internal import Unknown def f(a: int = 0, b: int = 0, c: int = 0, fmt: str | None = None) -> None: ... def _(args: "Unknown | tuple[int, int, int]"): diff --git a/crates/ty_python_semantic/resources/mdtest/call/methods.md b/crates/ty_python_semantic/resources/mdtest/call/methods.md index a9440cff93..742a07462f 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/methods.md +++ b/crates/ty_python_semantic/resources/mdtest/call/methods.md @@ -616,13 +616,11 @@ error[missing-argument]: No argument provided for required parameter `arg` of fu | 18 | class MissingArg(RequiresArg): ... | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | info: Parameter declared here --> src/mdtest_snippet.py:13:32 | 13 | def __init_subclass__(cls, arg: int): ... | ^^^^^^^^ - | ``` ```py @@ -637,13 +635,11 @@ error[invalid-argument-type]: Argument to function `RequiresArg.__init_subclass_ | 20 | class InvalidType(RequiresArg, arg="foo"): ... | ^^^^^^^^^ Expected `int`, found `Literal["foo"]` - | info: Function defined here --> src/mdtest_snippet.py:13:9 | 13 | def __init_subclass__(cls, arg: int): ... | ^^^^^^^^^^^^^^^^^ -------- Parameter declared here - | ``` ```py @@ -668,13 +664,11 @@ error[missing-argument]: No argument provided for required parameter `arg` of fu | 24 | class IncorrectArg(RequiresArg, not_arg="foo"): | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | info: Parameter declared here --> src/mdtest_snippet.py:13:32 | 13 | def __init_subclass__(cls, arg: int): ... | ^^^^^^^^ - | error[unknown-argument]: Argument `not_arg` does not match any known parameter of function `RequiresArg.__init_subclass__` @@ -682,13 +676,11 @@ error[unknown-argument]: Argument `not_arg` does not match any known parameter o | 24 | class IncorrectArg(RequiresArg, not_arg="foo"): | ^^^^^^^^^^^^^ - | info: Function signature here --> src/mdtest_snippet.py:13:9 | 13 | def __init_subclass__(cls, arg: int): ... | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | ``` ```py @@ -704,7 +696,7 @@ class Bad(NotCallableInitSubclass): ```snapshot error[non-callable-init-subclass]: Invalid definition of class `Bad` - --> src/mdtest_snippet.py:36:5 + --> src/mdtest_snippet.py:39:7 | 36 | __init_subclass__ = None | ----------------- `NotCallableInitSubclass.__init_subclass__` has type `None | Unknown`, which may not be callable @@ -712,7 +704,6 @@ error[non-callable-init-subclass]: Invalid definition of class `Bad` 38 | # snapshot: non-callable-init-subclass 39 | class Bad(NotCallableInitSubclass): | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Superclass `NotCallableInitSubclass` cannot be subclassed - | info: `__init_subclass__` on a superclass is implicitly called during creation of a class object info: See https://docs.python.org/3/reference/datamodel.html#customizing-class-creation ``` @@ -1019,6 +1010,48 @@ class X: return self.__new__(type(self)) ``` +Calling `object.__new__` from an overriding `__new__` method preserves `Self`, so an invalid +attribute access on the result is reported: + +```py +class Item: + def __new__(cls) -> Self: + result = object.__new__(cls) + reveal_type(result) # revealed: Self@__new__ + # error: [unresolved-attribute] + result.nonexistent() + return result +``` + +Explicitly marking `__new__` as a static method does not change the inferred result: + +```py +class StaticItem: + @staticmethod + def __new__(cls) -> Self: + result = object.__new__(cls) + reveal_type(result) # revealed: Self@__new__ + return result +``` + +`Self` is also preserved through a chain of inherited `__new__` calls: + +```py +class Foo: ... + +class Bar(Foo): + def __new__(cls) -> Self: + return Foo.__new__(cls) + +class Baz(Bar): + def __new__(cls) -> Self: + result = Bar.__new__(cls) + reveal_type(result) # revealed: Self@__new__ + # error: [unresolved-attribute] + result.nonexistent() + return result +``` + ## Bound-method attribute fallback Bound-method attributes are resolved first on `types.MethodType`, then, if absent, on the underlying diff --git a/crates/ty_python_semantic/resources/mdtest/call/overloads.md b/crates/ty_python_semantic/resources/mdtest/call/overloads.md index 835338afc4..1050121c28 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/overloads.md +++ b/crates/ty_python_semantic/resources/mdtest/call/overloads.md @@ -1030,7 +1030,6 @@ error[no-matching-overload]: No overload of function `f` matches arguments 39 | | a30=a, 40 | | ) | |_________^ - | info: Limit of argument type expansion reached at argument 9 info: First overload defined here --> src/overloaded.pyi:7:1 @@ -1038,7 +1037,6 @@ info: First overload defined here 7 | / @overload 8 | | def f() -> None: ... | |____________________^ First overload defined here - | info: Possible overloads for function `f`: info: () -> None info: (**kwargs: int) -> C @@ -1489,8 +1487,7 @@ def _(int_str: tuple[int, str], int_any: tuple[int, Any], any_any: tuple[Any, An ```pyi from typing_extensions import Iterable, overload, LiteralString, Protocol -from ty_extensions import Unknown -from ty_extensions._internal import is_assignable_to +from ty_extensions._internal import Unknown, is_assignable_to class Foo: @overload diff --git a/crates/ty_python_semantic/resources/mdtest/call/replace.md b/crates/ty_python_semantic/resources/mdtest/call/replace.md index 26ec7f1749..3b962a9ed8 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/replace.md +++ b/crates/ty_python_semantic/resources/mdtest/call/replace.md @@ -69,6 +69,41 @@ e = a.__replace__(x="wrong") # error: [invalid-argument-type] e = replace(a, x="wrong") ``` +### Dataclass transforms + +Classes transformed through a base class or metaclass also support the `__replace__` protocol. + +```py +from copy import replace +from typing import dataclass_transform + +@dataclass_transform() +class ModelBase: ... + +class BaseModel(ModelBase): + value: int + +# revealed: (self: BaseModel, *, value: int = ...) -> BaseModel +reveal_type(BaseModel.__replace__) + +base_model = BaseModel(value=1) +reveal_type(base_model.__replace__(value=2)) # revealed: BaseModel +reveal_type(replace(base_model, value=2)) # revealed: BaseModel + +@dataclass_transform() +class ModelMetaclass(type): ... + +class MetaclassModel(metaclass=ModelMetaclass): + value: int + +# revealed: (self: MetaclassModel, *, value: int = ...) -> MetaclassModel +reveal_type(MetaclassModel.__replace__) + +metaclass_model = MetaclassModel(value=1) +reveal_type(metaclass_model.__replace__(value=2)) # revealed: MetaclassModel +reveal_type(replace(metaclass_model, value=2)) # revealed: MetaclassModel +``` + ### NamedTuples NamedTuples also support the `__replace__` protocol: @@ -102,3 +137,25 @@ Invalid calls to `__replace__` will raise an error: # error: [unknown-argument] "Argument `z` does not match any known parameter" a.__replace__(z=42) ``` + +## Before Python 3.13 + +Dataclass transforms do not synthesize `__replace__` before the replacement protocol exists. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import dataclass_transform + +@dataclass_transform() +class ModelBase: ... + +class Model(ModelBase): + value: int + +Model.__replace__ # error: [unresolved-attribute] +Model(value=1).__replace__ # error: [unresolved-attribute] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/call/subclass_of.md b/crates/ty_python_semantic/resources/mdtest/call/subclass_of.md index 4bea16d3c4..f0f217c22e 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/subclass_of.md +++ b/crates/ty_python_semantic/resources/mdtest/call/subclass_of.md @@ -32,7 +32,7 @@ def _(subclass_of_c: type[C]): ```py from typing import Any -from ty_extensions import Unknown +from ty_extensions._internal 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/type.md b/crates/ty_python_semantic/resources/mdtest/call/type.md index 59401bc33f..90ab1c3f81 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/type.md +++ b/crates/ty_python_semantic/resources/mdtest/call/type.md @@ -518,7 +518,7 @@ them: ```py from typing import Any -from ty_extensions import Unknown +from ty_extensions._internal import Unknown def f(a: type[Any], b: type[Unknown]): reveal_type(a.__mro__) # revealed: tuple[type, ...] & Any @@ -691,7 +691,6 @@ error[inconsistent-mro]: Cannot create a consistent method resolution order (MRO | 7 | class Foo1(Generic[K, V], dict): ... # snapshot: inconsistent-mro | ^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Move `Generic[K, V]` to the end of the bases list | 6 | # error: [missing-type-argument] @@ -729,7 +728,6 @@ error[inconsistent-mro]: Cannot create a consistent method resolution order (MRO 16 | | # comment5 17 | | ): ... | |_^ - | help: Move `Generic[K, V]` to the end of the bases list | 11 | # comment1 @@ -754,7 +752,6 @@ error[inconsistent-mro]: Cannot create a consistent method resolution order (MRO | 19 | class Foo3(Generic[K, V], dict, metaclass=type): ... # snapshot: inconsistent-mro | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Move `Generic[K, V]` to the end of the bases list | 18 | # error: [missing-type-argument] @@ -796,7 +793,6 @@ error[inconsistent-mro]: Cannot create a consistent method resolution order (MRO 28 | | # comment7 29 | | ): ... | |_^ - | help: Move `Generic[K, V]` to the end of the bases list | 21 | # comment1 @@ -828,7 +824,6 @@ error[duplicate-base]: Duplicate base class in class `Dup` | 4 | Dup = type("Dup", (A, A), {}) | ^^^^^^^^^^^^^^^^^^^^^^^ - | ``` ## Metaclass conflicts @@ -956,7 +951,6 @@ error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to | 8 | X = type("X", (A, B), {}) | ^^^^^^^^^^^^^^^^^^^^^ 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:16 | @@ -964,7 +958,6 @@ info: Two classes cannot coexist in a class's MRO if their instances have incomp | - - `B` instances have a distinct memory layout because `B` defines non-empty `__slots__` | | | `A` instances have a distinct memory layout because `A` defines non-empty `__slots__` - | ``` When the bases are not a tuple literal (e.g., a variable), the diagnostic is emitted without diff --git a/crates/ty_python_semantic/resources/mdtest/call/union.md b/crates/ty_python_semantic/resources/mdtest/call/union.md index c8691a5c7a..a6af3d6293 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/union.md +++ b/crates/ty_python_semantic/resources/mdtest/call/union.md @@ -966,13 +966,11 @@ error[invalid-argument-type]: Argument to bound method `BytesCaller.__call__` is | 21 | f(None) | ^^^^ Expected `bytes`, found `None` - | info: Method defined here --> src/mdtest_snippet.py:13:9 | 13 | def __call__(self, x: bytes) -> bytes: | ^^^^^^^^ -------- Parameter declared here - | info: Union variant `BytesCaller` is incompatible with this call site info: Attempted to call union type `(IntCaller & StrCaller) | BytesCaller` @@ -982,13 +980,11 @@ error[invalid-argument-type]: Argument to bound method `IntCaller.__call__` is i | 21 | f(None) | ^^^^ Expected `int`, found `None` - | info: Method defined here --> src/mdtest_snippet.py:5:9 | 5 | def __call__(self, x: int) -> int: | ^^^^^^^^ ------ Parameter declared here - | info: Intersection element `IntCaller` is incompatible with this call site info: Attempted to call intersection type `IntCaller & StrCaller` info: Attempted to call union type `(IntCaller & StrCaller) | BytesCaller` @@ -999,18 +995,133 @@ error[invalid-argument-type]: Argument to bound method `StrCaller.__call__` is i | 21 | f(None) | ^^^^ Expected `str`, found `None` - | info: Method defined here --> src/mdtest_snippet.py:9:9 | 9 | def __call__(self, x: str) -> str: | ^^^^^^^^ ------ Parameter declared here - | info: Intersection element `StrCaller` is incompatible with this call site info: Attempted to call intersection type `IntCaller & StrCaller` info: Attempted to call union type `(IntCaller & StrCaller) | BytesCaller` ``` +## Union of intersected constructors retains the called class types + +When one union variant is an intersection of class objects, its constructor diagnostics should +describe that original intersection instead of intersecting the underlying constructor methods. + +```py +from typing_extensions import Self + +class UsesInit: + def __init__(self, value: int) -> None: ... + +class UsesNew: + def __new__(cls, value: str) -> Self: + return object.__new__(cls) + +class UsesBytes: + def __init__(self, value: bytes) -> None: ... + +def _(cls: type[UsesInit], other: type[UsesBytes], condition: bool) -> None: + if issubclass(cls, UsesNew): + constructor = cls if condition else other + reveal_type(constructor) # revealed: (type[UsesInit] & type[UsesNew]) | type[UsesBytes] + # error: [invalid-argument-type] "class `UsesInit`" + # error: [invalid-argument-type] "class `UsesNew`" + # snapshot: invalid-argument-type + constructor(None) +``` + +```snapshot +error[invalid-argument-type]: Argument to class `UsesBytes` is incorrect + --> src/mdtest_snippet.py:20:21 + | +20 | constructor(None) + | ^^^^ Expected `bytes`, found `None` +info: Method defined here + --> src/mdtest_snippet.py:11:9 + | +11 | def __init__(self, value: bytes) -> None: ... + | ^^^^^^^^ ------------ Parameter declared here +info: Union variant `bound method UsesBytes.__init__(value: bytes)` is incompatible with this call site +info: Attempted to call union type `(type[UsesInit] & type[UsesNew]) | type[UsesBytes]` +``` + +## Union intersection diagnostics retain excluded types + +A failing intersection inside a union should keep its excluded type in the intersection-specific +diagnostic, even though the exclusion does not contribute a callable binding. + +```py +from ty_extensions import Intersection, Not + +class IntCaller: + def __call__(self, value: int) -> None: ... + +class Required: ... +class Excluded: ... + +class AcceptsNone: + def __call__(self, value: None) -> None: ... + +def _(value: Intersection[IntCaller, Required, Not[Excluded]] | AcceptsNone) -> None: + # snapshot: invalid-argument-type + value(None) +``` + +```snapshot +error[invalid-argument-type]: Argument to bound method `IntCaller.__call__` is incorrect + --> src/mdtest_snippet.py:14:11 + | +14 | value(None) + | ^^^^ Expected `int`, found `None` +info: Method defined here + --> src/mdtest_snippet.py:4:9 + | +4 | def __call__(self, value: int) -> None: ... + | ^^^^^^^^ ---------- Parameter declared here +info: Intersection element `IntCaller` is incompatible with this call site +info: Attempted to call intersection type `IntCaller & Required & ~Excluded` +info: Attempted to call union type `(IntCaller & Required & ~Excluded) | AcceptsNone` +``` + +## Union variants retain excluded types with one callable + +An intersection with only one positive callable is still a distinct union variant, so its excluded +type should remain visible in the variant-specific diagnostic. + +```py +from ty_extensions import Intersection, Not + +class IntCaller: + def __call__(self, value: int) -> None: ... + +class Excluded: ... + +class AcceptsNone: + def __call__(self, value: None) -> None: ... + +def _(value: Intersection[IntCaller, Not[Excluded]] | AcceptsNone) -> None: + # snapshot: invalid-argument-type + value(None) +``` + +```snapshot +error[invalid-argument-type]: Argument to bound method `IntCaller.__call__` is incorrect + --> src/mdtest_snippet.py:13:11 + | +13 | value(None) + | ^^^^ Expected `int`, found `None` +info: Method defined here + --> src/mdtest_snippet.py:4:9 + | +4 | def __call__(self, value: int) -> None: ... + | ^^^^^^^^ ---------- Parameter declared here +info: Union variant `IntCaller & ~Excluded` is incompatible with this call site +info: Attempted to call union type `(IntCaller & ~Excluded) | AcceptsNone` +``` + ## Union semantics with constrained callable typevars ```toml diff --git a/crates/ty_python_semantic/resources/mdtest/comparison/identity.md b/crates/ty_python_semantic/resources/mdtest/comparison/identity.md index 51e75fd529..04962fa8a2 100644 --- a/crates/ty_python_semantic/resources/mdtest/comparison/identity.md +++ b/crates/ty_python_semantic/resources/mdtest/comparison/identity.md @@ -1,8 +1,23 @@ -# Identity tests +# Identity comparisons + +## Basic comparisons ```py from typing_extensions import TypeAliasType +reveal_type(False is False) # revealed: Literal[True] +reveal_type(False is True) # revealed: Literal[False] +reveal_type(1 is True) # revealed: Literal[False] +reveal_type(... is ...) # revealed: Literal[True] +reveal_type(NotImplemented is NotImplemented) # revealed: Literal[True] + +# two occurences of the same literal `1` do not necessarily share the +# same memory address, as `1` is not a singleton (but they also *might*!) +reveal_type(1 is 1) # revealed: bool + +# but two different integer literals definitely don't share the same memory address +reveal_type(1 is 2) # revealed: Literal[False] + class A: ... def _(a1: A, a2: A, o: object): @@ -40,3 +55,119 @@ def _(a1: TypeAliasType, a2: TypeAliasType): reveal_type(list[int] is list[int]) # revealed: bool reveal_type(list[int] is not list[int]) # revealed: bool ``` + +## Identity comparisons with NewTypes + +Two variables cannot share the same memory address if they have disjoint nominal-instance backing +types: + +```py +def f(x: str, y: int): + reveal_type(x is y) # revealed: Literal[False] + reveal_type(x is not y) # revealed: Literal[True] +``` + +Distinct `NewType` tags are mutually exclusive, so their types are disjoint. Their constructors +still return their arguments unchanged: `B(True)` and `C(True)` have different tags but share the +same memory address, so an identity comparison can succeed. + +```py +from typing import NewType, Literal +from ty_extensions._internal import is_disjoint_from + +B = NewType("B", bool) +C = NewType("C", bool) + +reveal_type(is_disjoint_from(B, C)) # revealed: ConstraintSet[Literal[True]] +reveal_type(is_disjoint_from(B, Literal[True])) # revealed: ConstraintSet[Literal[False]] + +def f(x: B, y: C): + reveal_type(x is y) # revealed: bool + reveal_type(x is not y) # revealed: bool + # error: [redundant-boolean-comparison] "Comparison of a `bool` with `True` is redundant" + reveal_type(x is True) # revealed: bool + # error: [redundant-boolean-comparison] "Comparison of a `bool` with `False` is redundant" + reveal_type(x is False) # revealed: bool + # error: [redundant-boolean-comparison] "Comparison of a `bool` with `True` is redundant" + reveal_type(x is not True) # revealed: bool + # error: [redundant-boolean-comparison] "Comparison of a `bool` with `False` is redundant" + reveal_type(x is not False) # revealed: bool +``` + +Nonetheless, if the NewType's nominal backing type is disjoint from another type, `Literal` boolean +types can still be inferred as a result: + +```py +from typing import NewType, Literal + +N = NewType("N", str) +O = NewType("O", int) + +def f(x: N, y: int, z: O): + reveal_type(x is y) # revealed: Literal[False] + reveal_type(x is not y) # revealed: Literal[True] + reveal_type(x is z) # revealed: Literal[False] + reveal_type(x is not z) # revealed: Literal[True] +``` + +## Identity comparisons see through type aliases + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Literal + +type SoTrue = Literal[True] +type SoFalse = Literal[False] + +def f(x: SoTrue, y: SoFalse): + # error: [redundant-boolean-comparison] "Comparison of a `bool` with `True` is redundant" + reveal_type(x is True) # revealed: Literal[True] + # error: [redundant-boolean-comparison] "Comparison of a `bool` with `False` is redundant" + reveal_type(x is False) # revealed: Literal[False] + reveal_type(x is y) # revealed: Literal[False] + reveal_type(x is not y) # revealed: Literal[True] +``` + +## Repeated identity comparisons after narrowing `Unknown` + +Once `value is None` has succeeded, the value can only be the `None` singleton even when its +original type is `Unknown`. + +```py +from ty_extensions._internal import Unknown + +def f(value: Unknown) -> None: + if value is None: + reveal_type(value) # revealed: Unknown & None + reveal_type(value is not None) # revealed: Literal[False] +``` + +## Identity comparisons for the same constrained `TypeVar` + +All occurrences of the same constrained `TypeVar` use the same constraint. Here, each constraint +contains only one object, so two values with that `TypeVar` must be identical. This remains true +when one occurrence appears through a type alias. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from types import EllipsisType +from typing import TypeVar + +T = TypeVar("T", None, EllipsisType) + +def f(left: T, right: T) -> None: + reveal_type(left is right) # revealed: Literal[True] + +type Alias[X] = X + +def aliased(left: Alias[T], right: T) -> None: + reveal_type(left is right) # revealed: Literal[True] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/comparison/instances/membership_test.md b/crates/ty_python_semantic/resources/mdtest/comparison/instances/membership_test.md index f75a458a26..d101b57fde 100644 --- a/crates/ty_python_semantic/resources/mdtest/comparison/instances/membership_test.md +++ b/crates/ty_python_semantic/resources/mdtest/comparison/instances/membership_test.md @@ -122,6 +122,111 @@ reveal_type(42 in AlwaysFalse()) # revealed: Literal[False] reveal_type(42 not in AlwaysFalse()) # revealed: Literal[True] ``` +## Required and optional `TypedDict` keys + +A required key is always present, while an optional key may or may not be present. + +```py +from typing_extensions import NotRequired, TypedDict + +class Items(TypedDict): + required: int + optional: NotRequired[int] + +def membership(items: Items) -> None: + reveal_type("required" in items) # revealed: Literal[True] + reveal_type("required" not in items) # revealed: Literal[False] + reveal_type("optional" in items) # revealed: bool + reveal_type("optional" not in items) # revealed: bool +``` + +## Absent keys in closed `TypedDict`s + +A closed `TypedDict` cannot contain an undeclared key or an optional key whose value type is +uninhabited. Declaring `extra_items=Never` closes a `TypedDict` in the same way as `closed=True`. + +```py +from typing_extensions import Never, NotRequired, TypedDict + +class Closed(TypedDict, closed=True): + present: int + impossible: NotRequired[Never] + +class ClosedByExtraItems(TypedDict, extra_items=Never): + present: int + +def closed_membership(closed: Closed, closed_by_extra_items: ClosedByExtraItems) -> None: + reveal_type("missing" in closed) # revealed: Literal[False] + reveal_type("missing" not in closed) # revealed: Literal[True] + reveal_type("impossible" in closed) # revealed: Literal[False] + reveal_type("impossible" not in closed) # revealed: Literal[True] + reveal_type("missing" in closed_by_extra_items) # revealed: Literal[False] + reveal_type("missing" not in closed_by_extra_items) # revealed: Literal[True] +``` + +## Undeclared keys in open `TypedDict`s + +Open `TypedDict`s and `TypedDict`s with nonempty extra items may contain keys that their schemas do +not declare. + +```py +from typing_extensions import TypedDict + +class Open(TypedDict): + present: int + +class ExtraItems(TypedDict, extra_items=int): + present: int + +def open_membership(open_items: Open, extra_items: ExtraItems) -> None: + reveal_type("missing" in open_items) # revealed: bool + reveal_type("missing" not in open_items) # revealed: bool + reveal_type("missing" in extra_items) # revealed: bool + reveal_type("missing" not in extra_items) # revealed: bool +``` + +## `TypedDict` membership with unions and non-literal keys + +Membership remains ambiguous when either the key or the `TypedDict` can vary between a present and +an absent alternative. A key missing from every closed alternative is always absent. + +```py +from typing_extensions import Literal, TypedDict + +class Left(TypedDict, closed=True): + left: int + +class Right(TypedDict, closed=True): + right: int + +def union_membership( + left: Left, + either: Left | Right, + literal_key: Literal["left", "missing"], + unknown_key: str, +) -> None: + reveal_type("missing" in either) # revealed: Literal[False] + reveal_type("missing" not in either) # revealed: Literal[True] + reveal_type("left" in either) # revealed: bool + reveal_type(literal_key in left) # revealed: bool + reveal_type(unknown_key in left) # revealed: bool +``` + +## Functional closed `TypedDict` membership + +Functional `TypedDict` definitions expose the same key-presence information as class-based +definitions. + +```py +from typing_extensions import TypedDict + +Closed = TypedDict("Closed", {"present": int}, closed=True) + +def functional_membership(closed: Closed) -> None: + reveal_type("present" in closed) # revealed: Literal[True] + reveal_type("missing" in closed) # revealed: Literal[False] +``` + ## No Fallback for `__contains__` If `__contains__` is implemented, checking membership of a type it doesn't accept is an error; it @@ -226,7 +331,6 @@ error[unsupported-bool-conversion]: Boolean conversion is not supported for type | 9 | 10 in WithContains() | ^^^^^^^^^^^^^^^^^^^^ - | info: `__bool__` on `NotBoolable` must be callable ``` @@ -241,6 +345,5 @@ error[unsupported-bool-conversion]: Boolean conversion is not supported for type | 11 | 10 not in WithContains() | ^^^^^^^^^^^^^^^^^^^^^^^^ - | info: `__bool__` on `NotBoolable` must be callable ``` diff --git a/crates/ty_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md b/crates/ty_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md index f880106c0f..837592fb10 100644 --- a/crates/ty_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md +++ b/crates/ty_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md @@ -165,7 +165,9 @@ reveal_type(C() <= C()) # revealed: NeReturnType When subclasses override comparison methods, these overridden methods take precedence over those in the parent class. Class `B` inherits from `A` and redefines comparison methods to return types other -than `A`. +than `A`. However, because `A` is not final, its instances could have runtime classes other than +`A`. The reflected method therefore has priority only for some possible operands, so both methods' +return types are included in the result. ```py from __future__ import annotations @@ -215,14 +217,14 @@ class B(A): def __ge__(self, other: A) -> GeReturnType: # error: [invalid-method-override] return GeReturnType() -reveal_type(A() == B()) # revealed: EqReturnType -reveal_type(A() != B()) # revealed: NeReturnType +reveal_type(A() == B()) # revealed: A | EqReturnType +reveal_type(A() != B()) # revealed: A | NeReturnType -reveal_type(A() < B()) # revealed: GtReturnType -reveal_type(A() <= B()) # revealed: GeReturnType +reveal_type(A() < B()) # revealed: A | GtReturnType +reveal_type(A() <= B()) # revealed: A | GeReturnType -reveal_type(A() > B()) # revealed: LtReturnType -reveal_type(A() >= B()) # revealed: LeReturnType +reveal_type(A() > B()) # revealed: A | LtReturnType +reveal_type(A() >= B()) # revealed: A | LeReturnType ``` ## Reflected Comparisons with Subclass But Falls Back to LHS @@ -369,7 +371,6 @@ error[unsupported-bool-conversion]: Boolean conversion is not supported for type | 12 | 10 < Comparable() < 20 | ^^^^^^^^^^^^^^^^^ - | info: `__bool__` on `NotBoolable` must be callable ``` @@ -386,7 +387,6 @@ error[unsupported-bool-conversion]: Boolean conversion is not supported for type | 14 | 10 < Comparable() < Comparable() | ^^^^^^^^^^^^^^^^^ - | info: `__bool__` on `NotBoolable` must be callable ``` diff --git a/crates/ty_python_semantic/resources/mdtest/comparison/intersections.md b/crates/ty_python_semantic/resources/mdtest/comparison/intersections.md index bd85637b08..f485531b04 100644 --- a/crates/ty_python_semantic/resources/mdtest/comparison/intersections.md +++ b/crates/ty_python_semantic/resources/mdtest/comparison/intersections.md @@ -50,17 +50,13 @@ 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 == "abc") # revealed: Literal[False] + reveal_type("abc" == x) # revealed: Literal[False] 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 != "abc") # revealed: Literal[True] + reveal_type("abc" != x) # revealed: Literal[True] reveal_type(x != "something else") # revealed: bool reveal_type("something else" != x) # revealed: bool @@ -76,6 +72,34 @@ if x != "abc": reveal_type("abc" in x) # revealed: bool ``` +A negative literal-string constraint does not exclude a runtime string with that value unless the +candidate already has known literal origin. + +```py +from typing import Literal +from typing_extensions import LiteralString +from ty_extensions import Intersection, Not + +def without_literal_origin(value: Intersection[str, Not[LiteralString]]) -> None: + reveal_type(value == "hello") # revealed: bool + reveal_type("hello" == value) # revealed: bool +``` + +A negative string-literal constraint likewise leaves the same runtime value possible, with or +without an explicit `str` constraint. + +```py +def excluded_string_literal(value: Intersection[str, Not[Literal["hello"]]]) -> None: + reveal_type(value == "hello") # revealed: bool + reveal_type("hello" == value) # revealed: bool + reveal_type(value != "hello") # revealed: bool + +def excluded_literal(value: Not[Literal["hello"]]) -> None: + reveal_type(value == "hello") # revealed: bool + reveal_type("hello" == value) # revealed: bool + reveal_type(value != "hello") # revealed: bool +``` + #### Integers ```py @@ -92,19 +116,61 @@ def _(x: int): ### Identity comparisons -```py -class A: ... +The type `~None` excludes the `None` object, so its identity comparisons with `None` have definite +results. +```py def _(o: object): - a = A() n = None if o is not None: - reveal_type(o) # revealed: ~None + reveal_type(o) # revealed: ~None reveal_type(o is n) # revealed: Literal[False] reveal_type(o is not n) # revealed: Literal[True] ``` +A single-member enum contains only one object. A value excluded from `E` cannot be `E.ONLY`, so the +branch below is unreachable and must not emit an attribute error. + +```py +from enum import Enum +from ty_extensions import Not + +class E(Enum): + ONLY = 1 + +def f(value: Not[E]) -> None: + if value is E.ONLY: + reveal_type(value) # revealed: Never + value.does_not_exist # no error (unreachable branch) +``` + +A `NewType` negation removes its static tag, not the runtime objects of its base: an integer without +that tag can still be identical to the integer passed into the `NewType` constructor. + +```py +from typing import NewType + +UserId = NewType("UserId", int) + +def f(value: Not[UserId]) -> None: + reveal_type(value is 1) # revealed: bool +``` + +After `not isinstance(value, B)`, `value` cannot be identical to a `B` instance. This remains true +when `value` has also been narrowed to `A`, so the inner branch is unreachable. + +```py +class A: ... +class B: ... + +def f(value: object, other_b: B) -> None: + if isinstance(value, A) and not isinstance(value, B): + if value is other_b: + reveal_type(value) # revealed: Never + value.does_not_exist # no error (unreachable branch) +``` + ## Diagnostics ### Unsupported operators for positive contributions @@ -134,7 +200,6 @@ error[unsupported-operator]: Unsupported `in` operation | | | | | Has type `NonContainer1 & NonContainer2` | Has type `Literal[2]` - | ``` Do not raise an error if at least one of the positive contributions to the intersection type support @@ -176,7 +241,6 @@ error[unsupported-operator]: Unsupported `in` operation | | | | | Has type `~NonContainer1` | Has type `Literal[2]` - | ``` ### Unsupported operators for negative contributions diff --git a/crates/ty_python_semantic/resources/mdtest/comparison/tuples.md b/crates/ty_python_semantic/resources/mdtest/comparison/tuples.md index 6a70f1e5ea..5196cbfba9 100644 --- a/crates/ty_python_semantic/resources/mdtest/comparison/tuples.md +++ b/crates/ty_python_semantic/resources/mdtest/comparison/tuples.md @@ -149,6 +149,7 @@ of the dunder methods.) ```py from __future__ import annotations +from typing import Literal class EqReturnType: ... class NeReturnType: ... @@ -203,6 +204,18 @@ class B: return LtReturnTypeOnB() reveal_type((A(), B()) < (A(), B())) # revealed: LtReturnType | LtReturnTypeOnB | Literal[False] + +class LaterLtReturnType: ... + +class CustomEq: + def __eq__(self, other: object) -> Literal[True]: + return True + +class Later: + def __lt__(self, other: Later) -> LaterLtReturnType: + return LaterLtReturnType() + +reveal_type((CustomEq(), Later()) < (CustomEq(), Later())) # revealed: LaterLtReturnType | Literal[False] ``` #### Special Handling of Eq and NotEq in Lexicographic Comparisons @@ -311,6 +324,96 @@ def _(n: int): reveal_type(a not in d) # revealed: bool ``` +Membership in a fixed-length tuple compares each element with the needle, regardless of whether the +needle is itself a tuple: + +```py +from typing import Literal + +def scalar_membership(value: int, values: tuple[Literal[1], Literal[2]]): + reveal_type(1 in values) # revealed: Literal[True] + reveal_type(3 in values) # revealed: Literal[False] + reveal_type(value in values) # revealed: bool + + reveal_type(1 not in values) # revealed: Literal[False] + reveal_type(3 not in values) # revealed: Literal[True] + reveal_type(value not in values) # revealed: bool + +def empty_tuple(value: object, values: tuple[()]): + reveal_type(value in values) # revealed: Literal[False] + reveal_type(value not in values) # revealed: Literal[True] +``` + +A variable-length tuple might be empty, even if all its possible elements would compare equal to the +needle: + +```py +from typing import Literal + +def variable_length( + nested: tuple[tuple[()], ...], + values: tuple[Literal[1], ...], +): + reveal_type(() in nested) # revealed: bool + reveal_type(() not in nested) # revealed: bool + + reveal_type(1 in values) # revealed: bool + reveal_type(1 not in values) # revealed: bool +``` + +Tuple membership checks whether the needle is the same object as an element before comparing the +objects for equality. A non-reflexive equality method therefore cannot establish that membership is +always false: + +```py +from typing import Literal + +class NeverEqual: + def __eq__(self, other: object) -> Literal[False]: + return False + +class AlwaysEqual: + def __eq__(self, other: object) -> Literal[True]: + return True + +def identity_before_equality(value: NeverEqual): + reveal_type(value == value) # revealed: Literal[False] + reveal_type(value in (value,)) # revealed: bool + reveal_type(value not in (value,)) # revealed: bool + +def custom_equality(value: AlwaysEqual): + reveal_type(value in (1,)) # revealed: bool + reveal_type(value not in (1,)) # revealed: bool + reveal_type((value,) == (1,)) # revealed: Literal[True] + reveal_type((value,) != (1,)) # revealed: Literal[False] + +def custom_equality_union(value: AlwaysEqual | None): + reveal_type(value in (1,)) # revealed: bool + reveal_type(value not in (1,)) # revealed: bool + +def custom_equality_union_member(value: AlwaysEqual | None, member: AlwaysEqual): + reveal_type(value.__eq__(member)) # revealed: bool + reveal_type(member.__eq__(value)) # revealed: Literal[True] + reveal_type(value in (member,)) # revealed: Literal[True] + reveal_type(value not in (member,)) # revealed: Literal[False] + reveal_type((value,) == (member,)) # revealed: bool + reveal_type((value,) != (member,)) # revealed: bool + +class Base: + def __eq__(self, other: object) -> bool: + return False + +class AlwaysEqualChild(Base): + def __eq__(self, other: object) -> Literal[True]: + return True + +def reflected_custom_equality(value: Base, child: AlwaysEqualChild): + reveal_type(value == child) # revealed: bool + reveal_type(child == value) # revealed: Literal[True] + reveal_type(value in (child,)) # revealed: Literal[True] + reveal_type(value not in (child,)) # revealed: Literal[False] +``` + ### Identity Comparisons "Identity Comparisons" refers to `is` and `is not`. @@ -496,7 +599,20 @@ class A: return NotBoolable() # error: [unsupported-bool-conversion] -(A(),) == (A(),) +reveal_type((A(),) == (A(),)) # revealed: bool +# error: [unsupported-bool-conversion] +reveal_type((A(), "x") == (A(), "y")) # revealed: Literal[False] +# error: [unsupported-bool-conversion] +reveal_type((A(),) != (A(), 0)) # revealed: Literal[True] +``` + +Tuple identity comparisons do not compare elements and therefore do not coerce their equality +results to `bool`: + +```py +def tuple_identity(left: tuple[A], right: tuple[A]) -> None: + reveal_type(left is right) # revealed: bool + reveal_type(left is not right) # revealed: bool ``` ## Recursive NamedTuple diff --git a/crates/ty_python_semantic/resources/mdtest/comparison/unions.md b/crates/ty_python_semantic/resources/mdtest/comparison/unions.md index 92afb6587b..4c287f9947 100644 --- a/crates/ty_python_semantic/resources/mdtest/comparison/unions.md +++ b/crates/ty_python_semantic/resources/mdtest/comparison/unions.md @@ -92,7 +92,6 @@ error[unsupported-operator]: Unsupported `in` operation | | | | | Has type `list[int] | Literal[1]` | Has type `Literal[1]` - | info: Operation fails because operator `in` is not supported between two objects of type `Literal[1]` ``` @@ -109,7 +108,6 @@ error[unsupported-operator]: Unsupported `in` operation | -^^^^- | | | Both operands have type `list[int] | Literal[1]` - | ``` ```py @@ -125,7 +123,6 @@ error[unsupported-operator]: Unsupported `<` operation | | | | | Has type `tuple[str] | tuple[str, str]` | Has type `tuple[int]` - | info: Operation fails because operator `<` is not supported between objects of type `int` and `str` ``` @@ -142,7 +139,6 @@ error[unsupported-operator]: Unsupported `<` operation | | | | | Has type `tuple[int]` | Has type `tuple[str] | tuple[str, str]` - | info: Operation fails because operator `<` is not supported between objects of type `str` and `int` ``` @@ -159,6 +155,5 @@ error[unsupported-operator]: Unsupported `<` operation | | | | | Has type `tuple[str] | tuple[str, str]` | Has type `tuple[int] | tuple[int, int]` - | info: Operation fails because operator `<` is not supported between objects of type `int` and `str` ``` diff --git a/crates/ty_python_semantic/resources/mdtest/comparison/unsupported.md b/crates/ty_python_semantic/resources/mdtest/comparison/unsupported.md index 8e4fd03486..7e55fc27b6 100644 --- a/crates/ty_python_semantic/resources/mdtest/comparison/unsupported.md +++ b/crates/ty_python_semantic/resources/mdtest/comparison/unsupported.md @@ -17,7 +17,6 @@ error[unsupported-operator]: Unsupported `in` operation | | | | | Has type `Literal[7]` | Has type `Literal[1]` - | ``` ```py @@ -35,7 +34,6 @@ error[unsupported-operator]: Unsupported `not in` operation | | | | | Has type `Literal[10]` | Has type `Literal[0]` - | ``` ```py @@ -53,7 +51,6 @@ error[unsupported-operator]: Unsupported `<` operation | | | | | Has type `Literal[5]` | Has type `object` - | ``` ```py @@ -71,7 +68,6 @@ error[unsupported-operator]: Unsupported `<` operation | | | | | Has type `object` | Has type `Literal[5]` - | ``` ```py @@ -90,7 +86,6 @@ error[unsupported-operator]: Unsupported `in` operation | | | | | Has type `Literal[1, "foo"]` | Has type `Literal[42]` - | info: Operation fails because operator `in` is not supported between objects of type `Literal[42]` and `Literal[1]` ``` @@ -109,7 +104,6 @@ error[unsupported-operator]: Unsupported `<` operation | | | | | Has type `tuple[Literal[1], Literal["hello"]]` | Has type `tuple[Literal[1], Literal[2]]` - | info: Operation fails because operator `<` is not supported between the tuple elements at index 2 (of type `Literal[2]` and `Literal["hello"]`) ``` @@ -127,6 +121,5 @@ error[unsupported-operator]: Unsupported `<` operation | ------------^^^------------ | | | Both operands have type `tuple[bool, A]` - | info: Operation fails because operator `<` is not supported between the tuple elements at index 2 (both of type `A`) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md b/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md index c99289b60e..2281639935 100644 --- a/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md +++ b/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md @@ -24,6 +24,55 @@ class Table: {0: reveal_type(x) for x in range(3)} ``` +## Invalid comprehension filters + +A filter in any comprehension form must support boolean conversion: + +```py +class NotBoolable: + __bool__ = None + +[x for x in range(3) if NotBoolable()] # error: [unsupported-bool-conversion] +{x for x in range(3) if NotBoolable()} # error: [unsupported-bool-conversion] +{x: x for x in range(3) if NotBoolable()} # error: [unsupported-bool-conversion] +(x for x in range(3) if NotBoolable()) # error: [unsupported-bool-conversion] +``` + +Every filter is checked, including filters on subsequent `for` clauses: + +```py +[ + x + for x in range(3) + if NotBoolable() # error: [unsupported-bool-conversion] + if NotBoolable() # error: [unsupported-bool-conversion] +] + +[ + x + for x in range(3) + if NotBoolable() # error: [unsupported-bool-conversion] + for y in range(3) + if NotBoolable() # error: [unsupported-bool-conversion] +] +``` + +The final operand of a boolean expression is converted when it becomes the filter condition: + +```py +[x for x in range(3) if True and NotBoolable()] # error: [unsupported-bool-conversion] +``` + +Filter validation also rejects a `__bool__` method with an invalid return type: + +```py +class InvalidBoolReturn: + def __bool__(self) -> str: + return "invalid" + +[x for x in range(3) if InvalidBoolReturn()] # error: [unsupported-bool-conversion] +``` + ## Nested comprehension ```py @@ -31,6 +80,234 @@ class Table: [[reveal_type((x, y)) for x in range(3)] for y in range(3)] ``` +## Assignment expressions in comprehensions + +[PEP 572] specifies that an assignment expression in a comprehension binds its target in the scope +containing the outermost comprehension. + +ty currently assumes that a comprehension runs at least once and that a generator expression is +consumed immediately. + +### Basic forms + +Assignment expressions can appear in the element of a list comprehension and in the key or value of +a dictionary comprehension: + +```py +[(list_value := item) for item in [1]] +{(dict_key := item): (dict_value := item) for item in [1]} + +reveal_type(list_value) # revealed: int +reveal_type(dict_key) # revealed: int +reveal_type(dict_value) # revealed: int +``` + +### Generator expressions + +The target also binds in the containing scope when the assignment is in a generator expression. PEP +572 uses this `any` pattern as a motivating example: + +```py +def find_comment(lines: list[str]): + if any((comment := line).startswith("#") for line in lines): + reveal_type(comment) # revealed: str +``` + +### Assignment order + +If an iteration assigns the same target more than once, the last assignment determines its value +after the comprehension: + +```py +[(ordered := item, ordered := "") for item in [1]] +reveal_type(ordered) # revealed: str +``` + +### Branches that do not assign + +A target in a branch known not to run remains unbound, while the other target is available after the +comprehension: + +```py +[(dead := 1) if False else (live := 2) for _ in [0]] + +dead # error: [unresolved-reference] +reveal_type(live) # revealed: int +``` + +### Assignments on only some paths + +When the assignment only runs on one possible path, an earlier value remains possible: + +```py +def conditional_with_previous_value(flag: bool): + value = "old" + [(value := 1) if flag else 0 for _ in [0]] + reveal_type(value) # revealed: Literal["old"] | int +``` + +Without an earlier value, the target may be unbound: + +```py +def conditional_without_previous_value(flag: bool): + [(value := 1) if flag else 0 for _ in [0]] + # error: [possibly-unresolved-reference] + reveal_type(value) # revealed: int +``` + +ty conservatively keeps the type of an assignment that is unreachable on the first iteration, since +a later iteration may take a different branch. Even though `0 == 1` is always false, the target is +therefore possibly unbound, and checking must continue after the read: + +```py +def statically_false_condition(): + [(value := 1) if 0 == 1 else 0 for _ in [0]] + # error: [possibly-unresolved-reference] + reveal_type(value) # revealed: int + still_reachable # error: [unresolved-reference] +``` + +### Comprehension filters + +A false filter skips the element, but an assignment made while evaluating that filter still takes +effect: + +```py +[value for value in [True, False] if (last_value := value)] +reveal_type(last_value) # revealed: bool +``` + +If short-circuit evaluation skips the assignment, the target may be unbound: + +```py +def conditional_filter(flag: bool): + [0 for _ in [0] if flag and (value := 1)] + # error: [possibly-unresolved-reference] + reveal_type(value) # revealed: int +``` + +An assignment in the element only runs when every preceding filter succeeds: + +```py +def assignment_after_filter(flag: bool): + [(value := 1) for _ in [0] if flag] + # error: [possibly-unresolved-reference] + reveal_type(value) # revealed: int +``` + +### Assignments that depend on earlier iterations + +An assignment can read the value left by an earlier iteration. In this example, the final value is +`3`, so retaining only the first iteration's literal values would be incorrect: + +```py +def partial_sum(): + total = 0 + [total := total + value for value in [1, 2]] + reveal_type(total) # revealed: int +``` + +ty does not yet account for a type that changes between iterations. The second iteration below +assigns `int`, so the final type should be `str | int` and `value.upper()` should report an error: + +```py +def type_changes_across_iterations(): + value = 0 + [value := "" if isinstance(value, int) else 0 for _ in [0, 1]] + reveal_type(value) # revealed: str + value.upper() +``` + +The same applies when two targets depend on values from earlier iterations: + +```py +def two_dependent_targets(): + x = 0 + y = 0 + [(y := x, x := y + 1) for _ in [1, 2]] + reveal_type(x) # revealed: int + reveal_type(y) # revealed: int +``` + +A guard can also depend on a value changed by a later assignment in the same iteration. The first +iteration below sets `flag`, so the second iteration assigns `value`: + +```py +def loop_carried_guard(): + flag = False + # basedpython reports the guard as redundant on the first iteration, where `flag` + # is still the `False` it was just assigned + # error: [redundant-condition] + [((value := 1) if flag else 0, (flag := True)) for _ in [0, 1]] + # error: [possibly-unresolved-reference] + reveal_type(value) # revealed: int +``` + +### Function-local targets + +An assignment in a branch known not to run still makes its target local to the containing function. +A read must not fall back to a global variable with the same name: + +```py +local_target = "global" + +def read_local_target(): + [(local_target := 1) if False else 0 for _ in [0]] + local_target # error: [unresolved-reference] +``` + +A walrus also makes its target local before the first iteration. Its first assignment must not read +a global with the same name. Explicit `global` and `nonlocal` declarations still refer to the +existing outer variable: + +```py +total = 0 + +def sums(values: list[int]) -> list[int]: + return [total := total + value for value in values] # error: [unresolved-reference] + +def sums_global(values: list[int]) -> list[int]: + global total + return [total := total + value for value in values] + +def sums_nonlocal(values: list[int]) -> list[int]: + total = 0 + + def add_values() -> list[int]: + nonlocal total + return [total := total + value for value in values] + + return add_values() +``` + +### Nested comprehensions + +An assignment in an inner comprehension still binds outside the outermost comprehension. A later +assignment in the outer comprehension replaces the inner value: + +```py +[([nested_order := 1 for _ in [0]], (nested_order := "")) for _ in [0]] +reveal_type(nested_order) # revealed: str +``` + +These are controls for an inner comprehension that is never evaluated. It must not replace an +earlier value: + +```py +def unreachable_nested_assignment_with_previous_value(): + value = "old" + [[value := 1 for _ in [0]] if False else [] for _ in [0]] + reveal_type(value) # revealed: Literal["old"] +``` + +Nor should it create a new value: + +```py +def unreachable_nested_assignment_without_previous_value(): + [[value := 1 for _ in [0]] if False else [] for _ in [0]] + value # error: [unresolved-reference] +``` + ## Comprehension referencing outer comprehension ```py @@ -122,6 +399,26 @@ async def _(): [reveal_type(x) async for x in range(3)] ``` +### Invalid async comprehension filters + +Filters in asynchronous comprehensions also require valid boolean conversion: + +```py +from collections.abc import AsyncIterator + +class NotBoolable: + __bool__ = None + +async def items() -> AsyncIterator[int]: + yield 1 + +async def invalid_filters() -> None: + [x async for x in items() if NotBoolable()] # error: [unsupported-bool-conversion] + {x async for x in items() if NotBoolable()} # error: [unsupported-bool-conversion] + {x: x async for x in items() if NotBoolable()} # error: [unsupported-bool-conversion] + (x async for x in items() if NotBoolable()) # error: [unsupported-bool-conversion] +``` + ## Comprehension value type The type of the expression being iterated over is immutable, and so should not be widened with @@ -262,3 +559,5 @@ reveal_type(dict_with_literal_values) # revealed: dict[str, Literal[1, 2, 3]] set_with_literals: set[Literal[1, 2, 3]] = {k for k in (1, 2, 3)} reveal_type(set_with_literals) # revealed: set[Literal[1, 2, 3]] ``` + +[pep 572]: https://peps.python.org/pep-0572/#scope-of-the-target diff --git a/crates/ty_python_semantic/resources/mdtest/conditions.md b/crates/ty_python_semantic/resources/mdtest/conditions.md index 052b264035..ac562e269f 100644 --- a/crates/ty_python_semantic/resources/mdtest/conditions.md +++ b/crates/ty_python_semantic/resources/mdtest/conditions.md @@ -521,7 +521,6 @@ warning[overlapping-condition]: This condition does not distinguish between `Lit | 5 | if not a: ... | ^^^^^ - | info: `bool | None` is tested for falsiness help: Compare against the specific value instead of testing truthiness @@ -531,7 +530,6 @@ warning[redundant-condition]: This condition is always true | 9 | if a: ... | ^ - | info: `Literal[True]` is always truthy @@ -540,7 +538,6 @@ warning[redundant-boolean-comparison]: Comparison of a `bool` with `False` is re | 13 | if a == False: ... | ^^^^^^^^^^ - | info: `bool` already is the value this comparison produces help: Negate the operand with `not` instead ``` diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md index 6cda27020c..0434c3bbe5 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md @@ -847,6 +847,30 @@ class NotOrderedWithOverrides: return False ``` +### Unrecognized parameters + +`dataclass_transform` rejects unrecognized parameters: + +```py +from typing import dataclass_transform + +# error: [unknown-argument] "Argument `unsupported` does not match any known parameter" +@dataclass_transform(unsupported=True) +def my_model[T](cls: type[T]) -> type[T]: + return cls +``` + +This also works for the variant from `typing_extensions`: + +```py +from typing_extensions import dataclass_transform + +# error: [unknown-argument] "Argument `unsupported` does not match any known parameter" +@dataclass_transform(unsupported=True) +def my_model[T](cls: type[T]) -> type[T]: + return cls +``` + ## Other `dataclass` parameters Other parameters from normal dataclasses can also be set on models created using @@ -1418,6 +1442,10 @@ class InvalidModel: x: int = 1 y: str # error: [dataclass-field-order] +@create_model +class InvalidInheritedModel(ValidModel): + z: bytes # error: [dataclass-field-order] + @dataclass_transform(field_specifiers=(field,), kw_only_default=True) def create_kwonly_default_model[T](cls: type[T]) -> type[T]: ... @@ -1564,6 +1592,28 @@ reveal_type(t.key) # revealed: int reveal_type(t.name) # revealed: str ``` +Dataclass-transform defaults remain attached to inherited fields even when a subclass is explicitly +decorated with `@dataclass`. + +```py +@dataclass_transform(kw_only_default=True) +class KeywordOnlyModelMeta(type): + pass + +class RequiredModel(metaclass=KeywordOnlyModelMeta): + required: int + +class OptionalModel(metaclass=KeywordOnlyModelMeta): + optional: int = 1 + +@dataclass(kw_only=True) +class Child(RequiredModel, OptionalModel): + pass + +reveal_type(Child.__init__) # revealed: (self: Child, *, optional: int = 1, required: int) -> None +Child(required=1) +``` + ## `__dataclass_fields__` and `DataclassInstance` protocol Classes created via `dataclass_transform` should have `__dataclass_fields__` and diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md index f355ed10c9..b44d6744f1 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md @@ -169,10 +169,8 @@ class GoodWithClassInitFalse: GoodWithClassInitFalse("value") -# Re-enabling `init` makes the inherited default-before-required ordering invalid at runtime. -# TODO: error: [dataclass-field-order] @dataclass -class BadWithReenabledInit(GoodWithClassInitFalse): +class BadWithReenabledInit(GoodWithClassInitFalse): # error: [dataclass-field-order] pass ``` @@ -659,7 +657,7 @@ reveal_type(WithUnsafeHash.__hash__) # revealed: (self: WithUnsafeHash) -> int ### `frozen` -If true (the default is False), assigning to fields will generate a diagnostic. +When `frozen=True`, a dataclass does not allow its fields to be assigned or deleted. ```py from dataclasses import dataclass @@ -670,6 +668,11 @@ class MyFrozenClass: frozen_instance = MyFrozenClass(1) frozen_instance.x = 2 # error: [invalid-assignment] + +reveal_type(frozen_instance.__delattr__) # revealed: (name) -> Never + +# error: [invalid-assignment] "Cannot delete attribute `x` on type `MyFrozenClass` whose `__delattr__` method returns `Never`/`NoReturn`" +del frozen_instance.x ``` If `__setattr__()` or `__delattr__()` is defined in the class, a diagnostic is emitted. @@ -810,11 +813,224 @@ grandchild.z = 2 grandchild.unknown = 2 ``` +When another base class rejects assignment, a frozen dataclass must not hide its `__setattr__` +method: + +```py +from dataclasses import dataclass +from typing import NoReturn + +@dataclass(frozen=True) +class Frozen: + x: int = 1 + +class RejectsAssignment: + y: int = 1 + + def __setattr__(self, name: str, value: object) -> NoReturn: + raise AttributeError(name) + +class ChildWithRejectingAssignmentBase(Frozen, RejectsAssignment): ... + +# error: [invalid-assignment] "Cannot assign to attribute `y` on type `ChildWithRejectingAssignmentBase` whose `__setattr__` method returns `Never`/`NoReturn`" +ChildWithRejectingAssignmentBase().y = 2 +``` + +A later base class can customize assignment to an ordinary attribute. The value must satisfy both +the later `__setattr__` and the attribute declaration: + +```py +class AllowsAssignment: + y: object = 1 + + def __setattr__(self, name: str, value: int) -> None: ... + +class ChildWithAllowingAssignmentBase(Frozen, AllowsAssignment): ... + +allowed = ChildWithAllowingAssignmentBase() +allowed.y = 2 + +# error: [invalid-assignment] "Cannot assign object of type" +allowed.y = "invalid" +``` + +A later `__setattr__` can forward to `object.__setattr__`, which still invokes data descriptors: + +```py +class ForwardsAssignment: + def __setattr__(self, name: str, value: object) -> None: + super().__setattr__(name, value) +``` + +A read-only property therefore remains read-only: + +```py +class ReadOnlyPropertyBase(ForwardsAssignment): + @property + def y(self) -> int: + return 1 + +class ChildWithReadOnlyProperty(Frozen, ReadOnlyPropertyBase): ... + +# error: [invalid-assignment] "Cannot assign to read-only property `y` on object of type `ChildWithReadOnlyProperty`" +ChildWithReadOnlyProperty().y = 2 +``` + +The property's setter still determines which values it accepts: + +```py +class TypedPropertyBase(ForwardsAssignment): + @property + def y(self) -> int: + return 1 + + @y.setter + def y(self, value: int) -> None: ... + +class ChildWithTypedProperty(Frozen, TypedPropertyBase): ... + +# error: [invalid-assignment] "Expected `int`, found `Literal["invalid"]`" +ChildWithTypedProperty().y = "invalid" +``` + +A property setter that never returns prevents assignment: + +```py +class TerminalPropertyBase(ForwardsAssignment): + @property + def y(self) -> int: + return 1 + + @y.setter + def y(self, value: int) -> NoReturn: + raise AttributeError + +class ChildWithTerminalProperty(Frozen, TerminalPropertyBase): ... + +# error: [invalid-assignment] "Cannot assign to attribute `y` on type `ChildWithTerminalProperty` whose `__set__` method returns `Never`/`NoReturn`" +ChildWithTerminalProperty().y = 2 +``` + +The same rule applies to a custom descriptor whose setter never returns: + +```py +class TerminalDescriptor: + def __get__(self, instance: object, owner: type | None = None) -> int: + return 1 + + def __set__(self, instance: object, value: int) -> NoReturn: + raise AttributeError + +class TerminalDescriptorBase(ForwardsAssignment): + y: TerminalDescriptor = TerminalDescriptor() + +class ChildWithTerminalDescriptor(Frozen, TerminalDescriptorBase): ... + +# error: [invalid-assignment] "Cannot assign to attribute `y` on type `ChildWithTerminalDescriptor` whose `__set__` method returns `Never`/`NoReturn`" +ChildWithTerminalDescriptor().y = 2 +``` + +A later `__setattr__` does not make the declared type of an ordinary attribute disappear: + +```py +class AllowsUntypedAssignment: + y: int = 1 + + def __setattr__(self, name: str, value: object) -> None: ... + +class ChildWithUntypedAssignmentBase(Frozen, AllowsUntypedAssignment): ... + +# error: [invalid-assignment] +ChildWithUntypedAssignmentBase().y = "invalid" +``` + +A specialized `Generic[T]` frozen base must also preserve the next `__setattr__` in the MRO: + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") + +@dataclass(frozen=True) +class GenericFrozen(Generic[T]): + value: T + +class RejectingGenericAssignmentChild(GenericFrozen[int], RejectsAssignment): ... + +# error: [invalid-assignment] "Cannot assign to attribute `y` on type `RejectingGenericAssignmentChild` whose `__setattr__` method returns `Never`/`NoReturn`" +RejectingGenericAssignmentChild(1).y = 2 +``` + +The same behavior applies to Python 3.12 type-parameter syntax: + +```py +@dataclass(frozen=True) +class TypeParameterFrozen[T]: + value: T + +class RejectingTypeParameterAssignmentChild(TypeParameterFrozen[int], RejectsAssignment): ... + +# error: [invalid-assignment] "Cannot assign to attribute `y` on type `RejectingTypeParameterAssignmentChild` whose `__setattr__` method returns `Never`/`NoReturn`" +RejectingTypeParameterAssignmentChild(1).y = 2 +``` + +When a subclass inherits from two frozen dataclasses, fields from both bases remain frozen: + +```py +@dataclass(frozen=True) +class FirstFrozen: + first: int = 1 + +@dataclass(frozen=True) +class SecondFrozen: + second: int = 1 + +class ChildWithTwoFrozenBases(FirstFrozen, SecondFrozen): ... + +multiple = ChildWithTwoFrozenBases() +# revealed: Overload[(name: Literal["first"], value) -> Never, (name: Literal["second"], value) -> Never, (name: str, value) -> None] +reveal_type(multiple.__setattr__) +# revealed: Overload[(name: Literal["first"]) -> Never, (name: Literal["second"]) -> Never, (name: str) -> None] +reveal_type(multiple.__delattr__) + +multiple.second = 2 # error: [invalid-assignment] +del multiple.second # error: [invalid-assignment] +``` + +An `InitVar` is a constructor argument, not a frozen field. A subclass can assign and delete an +attribute with the same name: + +```py +from dataclasses import InitVar + +@dataclass(frozen=True) +class FrozenWithInitVar: + temporary: InitVar[int] = 0 + +class ChildWithInitVar(FrozenWithInitVar): + temporary: int = 1 + +init_var_child = ChildWithInitVar() +init_var_child.temporary = 4 +del init_var_child.temporary +``` + +The same rule applies when the `InitVar` belongs to a second frozen base: + +```py +class ChildWithSecondBaseInitVar(Frozen, FrozenWithInitVar): + temporary: int = 1 + +second_init_var_child = ChildWithSecondBaseInitVar() +second_init_var_child.temporary = 4 +del second_init_var_child.temporary +``` + Non-field attributes on subclasses of slotted frozen dataclasses are still rejected. This correctly models the runtime behavior, but is somewhat surprising and may be a CPython bug, as subclasses of slotted classes usually allow arbitrary attributes to be set on them unless the subclass also explicitly declares `__slots__`. We should change our behavior here to follow CPython, if they "fix" -it. +it. The same limitation applies when deleting an attribute. ```py from dataclasses import dataclass @@ -834,6 +1050,9 @@ frozen.x = 2 # error: [invalid-assignment] frozen.y = 2 # error: [invalid-assignment] frozen.z = 2 # error: [invalid-assignment] +del frozen.x # error: [invalid-assignment] +del frozen.y # error: [invalid-assignment] + grandchild = MySlottedFrozenGrandchildClass() grandchild.x = 2 # error: [invalid-assignment] grandchild.y = 2 # error: [invalid-assignment] @@ -841,8 +1060,7 @@ grandchild.z = 2 # error: [invalid-assignment] grandchild.unknown = 2 # error: [invalid-assignment] ``` -The same diagnostic is emitted if a frozen dataclass is inherited, and an attempt is made to delete -an attribute: +A frozen dataclass also prevents an ordinary subclass from deleting an inherited field: ```py from dataclasses import dataclass @@ -854,7 +1072,183 @@ class MyFrozenClass: class MyFrozenChildClass(MyFrozenClass): ... frozen = MyFrozenChildClass() -del frozen.x # TODO this should emit an [invalid-assignment] + +# revealed: Overload[(name: Literal["x"]) -> Never, (name: str) -> None] +reveal_type(frozen.__delattr__) + +del frozen.x # error: [invalid-assignment] +``` + +A frozen dataclass does not make a subclass's read-only property safe to delete: + +```py +from dataclasses import dataclass + +@dataclass(frozen=True) +class Frozen: + x: int = 1 + +class ReadOnlyChild(Frozen): + @property + def y(self) -> int: + return 1 + +# error: [invalid-assignment] "Cannot delete read-only property `y` on object of type `ReadOnlyChild`" +del ReadOnlyChild().y +``` + +Deleting a property is also invalid when its deleter never returns: + +```py +from typing import NoReturn + +class RejectingPropertyChild(Frozen): + @property + def y(self) -> int: + return 1 + + @y.deleter + def y(self) -> NoReturn: + raise AttributeError("y") + +# error: [invalid-assignment] "Cannot delete attribute `y` on type `RejectingPropertyChild` whose `__delete__` method returns `Never`/`NoReturn`" +del RejectingPropertyChild().y +``` + +When another base class rejects deletion, the frozen dataclass must not hide its `__delattr__` +method: + +```py +class RejectsDeletion: + y: int = 1 + + def __delattr__(self, name: str) -> NoReturn: + raise AttributeError(name) + +class ChildWithRejectingBase(Frozen, RejectsDeletion): ... + +# error: [invalid-assignment] "Cannot delete attribute `y` on type `ChildWithRejectingBase` whose `__delattr__` method returns `Never`/`NoReturn`" +del ChildWithRejectingBase().y +``` + +A second base class can customize deletion of an ordinary attribute: + +```py +class AllowsDeletion: + y: int = 1 + + def __delattr__(self, name: str) -> None: ... + +class ChildWithAllowingBase(Frozen, AllowsDeletion): ... + +del ChildWithAllowingBase().y +``` + +A later `__delattr__` can forward to `object.__delattr__`, which still invokes data descriptors: + +```py +class ForwardsDeletion: + def __delattr__(self, name: str) -> None: + super().__delattr__(name) +``` + +A read-only property therefore remains read-only: + +```py +class ReadOnlyDeletionBase(ForwardsDeletion): + @property + def y(self) -> int: + return 1 + +class ChildWithReadOnlyDeletion(Frozen, ReadOnlyDeletionBase): ... + +# error: [invalid-assignment] "Cannot delete read-only property `y` on object of type `ChildWithReadOnlyDeletion`" +del ChildWithReadOnlyDeletion().y +``` + +A property deleter that never returns also prevents deletion: + +```py +class TerminalDeletionBase(ForwardsDeletion): + @property + def y(self) -> int: + return 1 + + @y.deleter + def y(self) -> NoReturn: + raise AttributeError + +class ChildWithTerminalDeletion(Frozen, TerminalDeletionBase): ... + +# error: [invalid-assignment] "Cannot delete attribute `y` on type `ChildWithTerminalDeletion` whose `__delete__` method returns `Never`/`NoReturn`" +del ChildWithTerminalDeletion().y +``` + +The same rule applies to a custom descriptor whose deleter never returns: + +```py +class TerminalDeleteDescriptor: + def __get__(self, instance: object, owner: type | None = None) -> int: + return 1 + + def __delete__(self, instance: object) -> NoReturn: + raise AttributeError + +class TerminalDescriptorDeletionBase(ForwardsDeletion): + y: TerminalDeleteDescriptor = TerminalDeleteDescriptor() + +class ChildWithTerminalDescriptorDeletion(Frozen, TerminalDescriptorDeletionBase): ... + +# error: [invalid-assignment] "Cannot delete attribute `y` on type `ChildWithTerminalDescriptorDeletion` whose `__delete__` method returns `Never`/`NoReturn`" +del ChildWithTerminalDescriptorDeletion().y +``` + +An ordinary attribute defined on a subclass can also be deleted: + +```py +class ChildWithOwnAttribute(Frozen): + y: int = 1 + +deletable = ChildWithOwnAttribute() +deletable.y = 2 +del deletable.y +``` + +A subclass can replace the inherited `__delattr__`, but a method that returns `None` is an invalid +override of the frozen base's method, which returns `Never`. The overriding method still controls +deletion on both that subclass and its subclasses: + +```py +class ChildWithDeletionOverride(Frozen): + # error: [invalid-method-override] + def __delattr__(self, name: str) -> None: ... + +class GrandchildWithDeletionOverride(ChildWithDeletionOverride): ... + +del ChildWithDeletionOverride().x +del GrandchildWithDeletionOverride().x +``` + +A read-only property remains protected when the frozen base is a specialized `Generic[T]` dataclass: + +```py +class ReadOnlyGenericChild(GenericFrozen[int]): + @property + def y(self) -> int: + return 1 + +# error: [invalid-assignment] "Cannot delete read-only property `y` on object of type `ReadOnlyGenericChild`" +del ReadOnlyGenericChild(1).y +``` + +The Python 3.12 type-parameter syntax must also preserve a `__delattr__` method defined by another +base class: + +```py +class RejectingTypeParameterChild(TypeParameterFrozen[int], RejectsDeletion): ... + +# error: [invalid-assignment] "Cannot delete attribute `y` on type `RejectingTypeParameterChild` whose `__delattr__` method returns `Never`/`NoReturn`" +del RejectingTypeParameterChild(1).y ``` ### frozen/non-frozen inheritance @@ -879,23 +1273,21 @@ class Child(FrozenBase): ```snapshot error[invalid-frozen-dataclass-subclass]: Non-frozen dataclass cannot inherit from frozen dataclass - --> src/foo.py:7:1 + --> src/foo.py:9:7 | 7 | @dataclass | ---------- `Child` dataclass parameters 8 | # snapshot: invalid-frozen-dataclass-subclass 9 | class Child(FrozenBase): | ^^^^^^----------^ Subclass `Child` is not frozen but base class `FrozenBase` is - | info: This causes the class creation to fail info: Base class definition - --> src/foo.py:3:1 + --> src/foo.py:4:7 | 3 | @dataclass(frozen=True) | ----------------------- `FrozenBase` dataclass parameters 4 | class FrozenBase: | ^^^^^^^^^^ `FrozenBase` definition - | ``` Frozen dataclasses inheriting from non-frozen dataclasses are also illegal: @@ -1476,6 +1868,256 @@ Derived(1, "a") Derived(True) ``` +### Required fields after inherited defaults + +A required positional field cannot follow a positional field with a default inherited from a +dataclass base. + +```toml +[environment] +python-version = "3.10" +``` + +```py +from dataclasses import dataclass, field + +@dataclass +class DefaultedBase: + x: int = 1 + +@dataclass +class InvalidChild(DefaultedBase): + # error: [dataclass-field-order] "Required field `y` cannot be defined after fields with default values" + y: int +``` + +A default factory also makes an inherited field optional. + +```py +@dataclass +class DefaultFactoryBase: + x: list[int] = field(default_factory=list) + +@dataclass +class InvalidDefaultFactoryChild(DefaultFactoryBase): + # error: [dataclass-field-order] + y: int +``` + +An ordering violation already present in an ancestor is not reported again on its descendants. + +```py +@dataclass +class InvalidAncestor: + optional: int = 1 + required: int # error: [dataclass-field-order] + +@dataclass +class ChildOfInvalidAncestor(InvalidAncestor): + pass + +@dataclass +class GrandchildOfInvalidAncestor(ChildOfInvalidAncestor): + pass +``` + +Suppressing the original diagnostic also suppresses that inherited violation throughout the +hierarchy. + +```py +@dataclass +class IgnoredInvalidAncestor: + optional: int = 1 + required: int # ty: ignore[dataclass-field-order] + +@dataclass +class ChildOfIgnoredAncestor(IgnoredInvalidAncestor): + pass + +@dataclass +class GrandchildOfIgnoredAncestor(ChildOfIgnoredAncestor): + pass +``` + +Redeclaring fields can introduce a new violation even when the same required field had an ignored +violation in an ancestor. + +```py +@dataclass +class IgnoredViolationsBase: + first: int = 1 + second: int # ty: ignore[dataclass-field-order] + third: int # ty: ignore[dataclass-field-order] + +@dataclass +class NewlyInvalidOverride(IgnoredViolationsBase): + first: int = field() + second: int = 1 + third: int = field() # error: [dataclass-field-order] +``` + +Combining independently valid bases can introduce a new ordering violation even when the child +declares no fields. + +```py +@dataclass +class DefaultOnlyBase: + optional: int = 1 + +@dataclass +class RequiredOnlyBase: + required: int + +@dataclass +class InvalidMergedBases(RequiredOnlyBase, DefaultOnlyBase): # error: [dataclass-field-order] + pass + +@dataclass +class ValidMergedBases(DefaultOnlyBase, RequiredOnlyBase): + pass +``` + +Inherited fields that are keyword-only or excluded from `__init__` do not affect positional field +ordering, and a required child field can itself be keyword-only. + +```py +@dataclass +class KeywordOnlyBase: + x: int = field(default=1, kw_only=True) + +@dataclass +class ValidKeywordOnlyBaseChild(KeywordOnlyBase): + y: int + +@dataclass +class NonInitBase: + x: int = field(default=1, init=False) + +@dataclass +class ValidNonInitBaseChild(NonInitBase): + y: int + +@dataclass +class ValidKeywordOnlyChild(DefaultedBase): + y: int = field(kw_only=True) +``` + +Overriding a field preserves its original position in the inherited field order. Removing its +default permits later required fields, while introducing a default before another inherited required +field is invalid. + +```py +@dataclass +class ValidRequiredOverride(DefaultedBase): + x: int = field() + y: int + +@dataclass +class RequiredBase: + first: int + second: int + +@dataclass +class InvalidDefaultOverride(RequiredBase): # error: [dataclass-field-order] + first: int = 1 +``` + +### Class variables overriding inherited fields + +Redeclaring an inherited instance field as a class variable removes it from the generated +constructor and positional ordering checks. The override itself remains invalid. + +```py +from dataclasses import InitVar, dataclass, field +from typing import ClassVar + +@dataclass +class DefaultedFieldBase: + x: int = 1 + +@dataclass +class ClassVariableOverride(DefaultedFieldBase): + x: ClassVar[int] = 1 # error: [invalid-attribute-override] + y: int + +reveal_type(ClassVariableOverride.__init__) # revealed: (self: ClassVariableOverride, y: int) -> None + +@dataclass +class InheritedClassVariableOverride(ClassVariableOverride): + z: int + +reveal_type(InheritedClassVariableOverride.__init__) # revealed: (self: InheritedClassVariableOverride, y: int, z: int) -> None +``` + +A class variable declared by an undecorated intermediate class does not remove the inherited +dataclass field. + +```py +class OrdinaryClassVariableOverride(DefaultedFieldBase): + x: ClassVar[int] = 1 # error: [invalid-attribute-override] + +@dataclass +class DataclassAfterOrdinaryOverride(OrdinaryClassVariableOverride): + y: int # error: [dataclass-field-order] +``` + +An annotation-only class variable also masks the inherited instance field. + +```py +@dataclass +class AnnotationOnlyClassVariableOverride(DefaultedFieldBase): + x: ClassVar[int] # error: [invalid-attribute-override] + y: int + +reveal_type(AnnotationOnlyClassVariableOverride.__init__) # revealed: (self: AnnotationOnlyClassVariableOverride, y: int) -> None +``` + +Restoring an instance field in a later subclass preserves the field's original inherited position. + +```py +@dataclass +class RestoredInstanceField(ClassVariableOverride): + x: int = field() # error: [invalid-attribute-override] + +reveal_type(RestoredInstanceField.__init__) # revealed: (self: RestoredInstanceField, x: int, y: int) -> None +``` + +An initialization-only field overrides an inherited class variable and remains a constructor +parameter. + +```py +@dataclass +class ClassVariableBase: + value: ClassVar[int] + +@dataclass +class InitializationVariableOverride(ClassVariableBase): + value: InitVar[int] + +reveal_type(InitializationVariableOverride.__init__) # revealed: (self: InitializationVariableOverride, value: int) -> None +InitializationVariableOverride(1) +``` + +### Fields named after generated dataclass attributes + +Fields named after generated dataclass attributes are still ordinary constructor parameters. + +```py +from dataclasses import dataclass + +@dataclass +class DataclassFieldsConstructorField: + __dataclass_fields__: int + +DataclassFieldsConstructorField(1) + +@dataclass +class DataclassParamsConstructorField: + __dataclass_params__: int + +DataclassParamsConstructorField(1) +``` + ### Overwriting attributes from base class The following example comes from the @@ -1826,6 +2468,31 @@ But calling `asdict` on the class object is not allowed: asdict(Foo) ``` +## `dataclasses.is_dataclass` + +`is_dataclass` recognizes both dataclass instances and dataclass classes. A concrete dataclass +instance always satisfies the `DataclassInstance` protocol: + +```py +from dataclasses import dataclass, is_dataclass + +@dataclass +class Event: + x: int + +def check(event: Event) -> None: + if not is_dataclass(event): + reveal_type(event) # revealed: Never +``` + +This also works for class objects: + +```py +def check_class(event_type: type[Event]) -> None: + if not is_dataclass(event_type): + reveal_type(event_type) # revealed: Never +``` + ## `dataclasses.KW_ONLY` If an attribute is annotated with `dataclasses.KW_ONLY`, it is not added to the synthesized @@ -1862,7 +2529,6 @@ error[missing-argument]: No argument provided for required parameter `y` of clas | 13 | C(3, "") | ^^^^^^^^ - | error[too-many-positional-arguments]: Too many positional arguments to class `C`: expected 1, got 2 @@ -1870,7 +2536,6 @@ error[too-many-positional-arguments]: Too many positional arguments to class `C` | 13 | C(3, "") | ^^ - | ``` Declaration order still controls `KW_ONLY` when a later field name was already referenced by an diff --git a/crates/ty_python_semantic/resources/mdtest/declaration/error.md b/crates/ty_python_semantic/resources/mdtest/declaration/error.md index 6633ba562d..b003938e13 100644 --- a/crates/ty_python_semantic/resources/mdtest/declaration/error.md +++ b/crates/ty_python_semantic/resources/mdtest/declaration/error.md @@ -7,6 +7,18 @@ x = 1 x: str # error: [invalid-declaration] "Cannot declare type `str` for inferred type `Literal[1]`" ``` +## Declarations in loops reject incompatible earlier bindings + +An incompatible binding that predates the loop must still invalidate a declaration inside it. + +```py +values = [1] + +while True: + values: list[str] # error: [invalid-declaration] + values = ["a"] +``` + ## Incompatible declarations ```py diff --git a/crates/ty_python_semantic/resources/mdtest/decorators.md b/crates/ty_python_semantic/resources/mdtest/decorators.md index 4c45d22e69..e783acfaa3 100644 --- a/crates/ty_python_semantic/resources/mdtest/decorators.md +++ b/crates/ty_python_semantic/resources/mdtest/decorators.md @@ -200,6 +200,30 @@ class Foo: reveal_type(Foo().foo) # revealed: str ``` +### `functools.cached_property` on a generic class + +A cached property must preserve the type variable bound by its enclosing generic class, including +when the return type is a union: + +```py +from functools import cached_property +from typing import Generic, TypeVar + +T = TypeVar("T") + +class Box(Generic[T]): + @cached_property + def value(self) -> T: + raise NotImplementedError + + @cached_property + def values(self) -> list[T] | None: + raise NotImplementedError + +reveal_type(Box[int]().value) # revealed: int +reveal_type(Box[int]().values) # revealed: list[int] | None +``` + ## Lambdas as decorators ```py diff --git a/crates/ty_python_semantic/resources/mdtest/del.md b/crates/ty_python_semantic/resources/mdtest/del.md index 6ac52f4138..6fad0dad95 100644 --- a/crates/ty_python_semantic/resources/mdtest/del.md +++ b/crates/ty_python_semantic/resources/mdtest/del.md @@ -448,7 +448,6 @@ error[invalid-argument-type]: Cannot delete required key "name" from TypedDict ` | 19 | del m["name"] | ^^^^^^ - | info: Field defined here --> src/mdtest_snippet.py:3:7 | @@ -459,7 +458,6 @@ info: Field defined here | | | `name` declared as required here | Consider making it `NotRequired` - | info: Only keys marked as `NotRequired` (or in a TypedDict with `total=False`) can be deleted ``` @@ -488,7 +486,6 @@ error[invalid-argument-type]: Cannot delete required key "name" from TypedDict ` | 23 | del mixed["name"] | ^^^^^^ - | info: Field defined here --> src/mdtest_snippet.py:11:7 | @@ -499,7 +496,6 @@ info: Field defined here | | | `name` declared as required here | Consider making it `NotRequired` - | info: Only keys marked as `NotRequired` (or in a TypedDict with `total=False`) can be deleted ``` @@ -516,5 +512,4 @@ error[invalid-argument-type]: Cannot delete unknown key "non_existent" from Type | 25 | del mixed["non_existent"] | ^^^^^^^^^^^^^^ - | ``` diff --git a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md index b3b92fc7b3..3dd52380d8 100644 --- a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md +++ b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md @@ -35,7 +35,7 @@ reveal_type(C.ten) # revealed: Literal[10] # This is fine: c.ten = 10 -# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `ten` on type `C` with custom `__set__` method" +# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `ten` on type `C`" c.ten = 11 ``` @@ -78,7 +78,7 @@ c.flexible_int = "42" # also okay! reveal_type(c.flexible_int) # revealed: int | None -# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `flexible_int` on type `C` with custom `__set__` method" +# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `flexible_int` on type `C`" c.flexible_int = None # not okay reveal_type(c.flexible_int) # revealed: int | None @@ -215,7 +215,7 @@ def f1(flag: bool): attr = DataDescriptor() def f(self): - # error: [invalid-assignment] "Invalid assignment to data descriptor attribute `attr` on type `Self@f` with custom `__set__` method" + # error: [invalid-assignment] "Invalid assignment to data descriptor attribute `attr` on type `Self@f`" self.attr = b"foo" reveal_type(C1().attr) # revealed: Literal["data"] | bytes @@ -376,6 +376,157 @@ class UnionC(metaclass=UnionMeta): reveal_type(UnionC.attribute) # revealed: Any | Literal["descriptor"] ``` +### `TypeForm` metaclass attributes + +A `TypeForm` argument describes the instances produced by a type form, not the runtime type form +value itself. A metaclass attribute typed as `TypeForm[Descriptor]` can therefore be a class whose +own metaclass makes it a data descriptor, and must continue to take precedence over a class +attribute with the same name when assigning to the attribute: + +```py +from typing_extensions import TypeForm + +class DescriptorMeta(type): + def __set__(self, instance: object, value: str) -> None: + pass + +class Descriptor(metaclass=DescriptorMeta): ... + +class Meta(type): + attribute: TypeForm[Descriptor] = Descriptor + +class C(metaclass=Meta): + attribute: int = 1 + +C.attribute = 1 # error: [invalid-assignment] +# error: [invalid-assignment] +C.attribute = Descriptor # error: [invalid-assignment] +``` + +A quoted type expression remains valid when both possible write targets accept the same runtime +string: + +```py +class StringC(metaclass=Meta): + attribute: str = "" + +StringC.attribute = "valid" +StringC.attribute = "Descriptor" +``` + +The descriptor setter still rejects a class object even when the fallback attribute accepts that +same class object: + +```py +class TypeFormC(metaclass=Meta): + attribute: TypeForm[Descriptor] = Descriptor + +TypeFormC.attribute = Descriptor # error: [invalid-assignment] +``` + +The same contextual check applies when the metaclass attribute can also hold an ordinary string: + +```py +class UnionMeta(type): + attribute: TypeForm[Descriptor] | str = Descriptor + +class UnionC(metaclass=UnionMeta): + attribute: int = 1 + +UnionC.attribute = 1 # error: [invalid-assignment] +``` + +### Bounded class-object metaclass attributes + +An inexact `type[Base]` attribute can hold a subclass whose custom metaclass makes the class object +a data descriptor. It must therefore continue to take precedence over a class attribute with the +same name when assigning to the attribute: + +```py +class Base: ... + +class DescriptorMeta(type): + def __set__(self, instance: object, value: str) -> None: + pass + +class Descriptor(Base, metaclass=DescriptorMeta): ... + +class Meta(type): + attribute: type[Base] = Descriptor + +class C(metaclass=Meta): + attribute: int = 1 + +C.attribute = 1 # error: [invalid-assignment] +# error: [invalid-assignment] +C.attribute = Descriptor # error: [invalid-assignment] +``` + +An assignment succeeds when both the possible descriptor setter and class attribute accept the +assigned string: + +```py +class StringC(metaclass=Meta): + attribute: str = "" + +StringC.attribute = "valid" +``` + +An assignment fails when the class attribute accepts the assigned class but the descriptor setter +does not: + +```py +class ClassC(metaclass=Meta): + attribute: type[Base] = Base + +ClassC.attribute = Base # error: [invalid-assignment] +``` + +### Broad class-object metaclass attributes + +Both `type[object]` and bare `type` can contain a class whose metaclass implements `__set__`. Their +possible descriptor setters must therefore be checked independently of the class-attribute fallback: + +```py +class Base: ... + +class DescriptorMeta(type): + def __set__(self, instance: object, value: str) -> None: + pass + +class Descriptor(Base, metaclass=DescriptorMeta): ... + +class ObjectMeta(type): + attribute: type[object] = Descriptor + +class ObjectStringC(metaclass=ObjectMeta): + attribute: str = "" + +ObjectStringC.attribute = "valid" + +class ObjectClassC(metaclass=ObjectMeta): + attribute: type[Base] = Base + +ObjectClassC.attribute = Base # error: [invalid-assignment] +``` + +The unparameterized spelling follows the same descriptor and class-attribute paths: + +```py +class BareMeta(type): + attribute: type = Descriptor + +class BareStringC(metaclass=BareMeta): + attribute: str = "" + +BareStringC.attribute = "valid" + +class BareClassC(metaclass=BareMeta): + attribute: type[Base] = Base + +BareClassC.attribute = Base # error: [invalid-assignment] +``` + ### Class objects with unknown metaclasses A `type[Any]` value could contain a class whose metaclass implements the descriptor protocol. We @@ -480,7 +631,7 @@ on the metaclass: ```py C1.meta_data_descriptor = 1 -# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `meta_data_descriptor` on type `` with custom `__set__` method" +# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `meta_data_descriptor` on type ``" C1.meta_data_descriptor = "invalid" ``` @@ -586,7 +737,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 `` with custom `__set__` method" + # error: [invalid-assignment] "Invalid assignment to data descriptor attribute `meta_data_descriptor1` on type ``" # 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 @@ -735,7 +886,7 @@ reveal_type(C.name) # revealed: property c.name = "new" c.name = None -# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `name` on type `C` with custom `__set__` method" +# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `name` on type `C`" c.name = 42 ``` @@ -791,7 +942,7 @@ DontAssignToMe().immutable = "the properties, they are a-changing" ```snapshot error[invalid-assignment]: Cannot assign to read-only property `immutable` on object of type `DontAssignToMe` - --> src/mdtest_snippet.py:3:9 + --> src/mdtest_snippet.py:6:1 | 3 | def immutable(self): ... | --------- Property `DontAssignToMe.immutable` defined here with no setter @@ -799,7 +950,6 @@ error[invalid-assignment]: Cannot assign to read-only property `immutable` on ob 5 | # snapshot: invalid-assignment 6 | DontAssignToMe().immutable = "the properties, they are a-changing" | ^^^^^^^^^^^^^^^^^^^^^^^^^^ Attempted assignment to `DontAssignToMe.immutable` here - | ``` ### Built-in `classmethod` descriptor @@ -929,7 +1079,9 @@ wrapper_descriptor(f, None, type(f), "one too many") ### `__get__` is called with correct arguments -This test makes sure that we call `__get__` with the right argument types for various scenarios: +Python passes the instance and its class to a descriptor on an instance access. On a class access, +it passes `None` and the class instead. A descriptor on a metaclass receives the class and its +metaclass. ```py from __future__ import annotations @@ -956,21 +1108,52 @@ class C(metaclass=Meta): reveal_type(C.class_object_access) # revealed: int reveal_type(C().instance_access) # revealed: str reveal_type(C.metaclass_access) # revealed: bytes +``` + +An invalid descriptor access is reported, but we still use the declared return type of `__get__` to +avoid cascading errors. -# TODO: These should emit a diagnostic -# -# However, we use the return-type of `__get__` as the inferred type anyway: -# the way to specify that the descriptor object itself is returned when the -# attribute is accessed on the instance or the class is by overloading `__get__`. -# -# Using the return type of `__get__` even for `__get__` calls that have invalid -# arguments passed to them avoids false positives in situations where there are -# `__get__` calls that we don't sufficiently understand. +```py +# snapshot: invalid-attribute-access reveal_type(C().class_object_access) # revealed: int + +# snapshot: invalid-attribute-access reveal_type(C.instance_access) # revealed: str ``` -### Descriptors with incorrect `__get__` signature +```snapshot +error[invalid-attribute-access]: Invalid access to descriptor attribute `class_object_access` on type `C` + --> src/mdtest_snippet.py:26:13 + | +26 | reveal_type(C().class_object_access) # revealed: int + | ^^^ Expected `None`, found `C` +info: Argument to function `TailoredForClassObjectAccess.__get__` is incorrect +info: This access implicitly calls `__get__` on a descriptor of type `TailoredForClassObjectAccess` +info: Function defined here + --> src/mdtest_snippet.py:4:9 + | +4 | def __get__(self, instance: None, owner: type[C]) -> int: + | ^^^^^^^ -------------- Parameter declared here + + +error[invalid-attribute-access]: Invalid access to descriptor attribute `instance_access` on type `` + --> src/mdtest_snippet.py:29:13 + | +29 | reveal_type(C.instance_access) # revealed: str + | ^ Expected `C`, found `None` +info: Argument to function `TailoredForInstanceAccess.__get__` is incorrect +info: This access implicitly calls `__get__` on a descriptor of type `TailoredForInstanceAccess` +info: Function defined here + --> src/mdtest_snippet.py:8:9 + | +8 | def __get__(self, instance: C, owner: type[C] | None = None) -> str: + | ^^^^^^^ ----------- Parameter declared here +``` + +### Descriptors with an incorrect `__get__` signature + +Python calls `__get__` with the descriptor, an instance or `None`, and the owner class. A method +that accepts only the descriptor cannot handle that call. ```py class Descriptor: @@ -981,29 +1164,545 @@ class Descriptor: class C: descriptor: Descriptor = Descriptor() -# TODO: This should be an error +C().descriptor # snapshot: invalid-attribute-access + +# error: [invalid-attribute-access] "Invalid access to descriptor attribute `descriptor` on type ``" reveal_type(C.descriptor) # revealed: int +``` -# TODO: This should be an error -reveal_type(C().descriptor) # revealed: int +```snapshot +error[invalid-attribute-access]: Invalid access to descriptor attribute `descriptor` on type `C` + --> src/mdtest_snippet.py:9:1 + | +9 | C().descriptor # snapshot: invalid-attribute-access + | ^^^ Too many positional arguments to function `Descriptor.__get__`: expected 1, got 3 +info: This access implicitly calls `__get__` on a descriptor of type `Descriptor` +info: Function signature here + --> src/mdtest_snippet.py:3:9 + | +3 | def __get__(self) -> int: + | ^^^^^^^^^^^^^^^^^^^^ +``` + +### Recursive descriptor aliases terminate + +Inspecting a recursive attribute must not recurse forever. The recursive alternative also cannot +prove that the access will invoke an invalid descriptor. + +```toml +[environment] +python-version = "3.12" +``` + +```py +type Recursive = int | Recursive + +class C: + value: Recursive = 1 + +C().value +``` + +### Property getters reject invalid receiver specializations + +A property getter checks the same specialized receiver as an ordinary method. A generic alias with +alternatives that impose different type-variable bounds can produce an invalid property access. + +```py +from collections.abc import Callable +from typing import Generic, TypeVar + +AItem = TypeVar("AItem", bound=Callable[[int], str]) +BItem = TypeVar("BItem", bound=Callable[[str], str]) + +class A(Generic[AItem]): + @property + def callback(self) -> AItem: + raise NotImplementedError + +class B(Generic[BItem]): + @property + def callback(self) -> BItem: + raise NotImplementedError + +AnyCallback = TypeVar("AnyCallback", bound=Callable[..., str]) +Command = A[AnyCallback] | B[AnyCallback] +Callback = TypeVar("Callback", bound=Callable[[int], str]) + +def access(value: Callback | Command[Callback]) -> None: + if isinstance(value, A | B): + # error: [invalid-attribute-access] + value.callback ``` -### "Descriptors" with non-callable `__get__` attributes +### Property getter failures preserve their underlying error and return type -If `__get__` is not callable at all, the interpreter will still attempt to call the method at -runtime, and this will raise an exception. As such, even for `__get__ = None`, we still "attempt to -call `__get__`" on the descriptor object (leading us to infer `Unknown`): +A property inherited from an unrelated class rejects the instance passed to its getter. The +diagnostic reports the getter's actual receiver mismatch and preserves its return type. + +```py +class Owner: + @property + def value(self) -> int: + return 1 + +class Other: + value = Owner.value + +# error: [invalid-attribute-access] "Expected `Owner`, found `Other`" +reveal_type(Other().value) # revealed: int +``` + +### Every descriptor alternative must accept the call + +As with other operations on a union, an attribute access is invalid if any possible descriptor +cannot accept the implicit call. ```py class BrokenDescriptor: + def __get__(self) -> bytes: + return b"" + +class ValidDescriptor: + def __get__(self, instance: object, owner: type | None = None) -> str: + return "" + +def descriptor() -> BrokenDescriptor | ValidDescriptor: + raise NotImplementedError + +class C: + value = descriptor() + +# error: [invalid-attribute-access] "Invalid access to descriptor attribute `value` on type `C`" +reveal_type(C().value) # revealed: bytes | str +``` + +### Descriptor diagnostics are reported through `super()` + +Accessing an inherited descriptor through `super()` still invokes its `__get__` method. + +```py +class Descriptor: + def __get__(self) -> int: + return 1 + +class Base: + value = Descriptor() + +class Derived(Base): + def access(self) -> None: + # error: [invalid-attribute-access] + super().value +``` + +### Type variables preserve invalid descriptor calls + +A type variable's bound does not prevent its receiver or descriptor value from reaching an invalid +`__get__` method. The same applies when accessing an attribute on `type[T]`. + +```py +from typing import TypeVar + +class Descriptor: + def __get__(self) -> int: + return 1 + +class Owner: + value = Descriptor() + +OwnerT = TypeVar("OwnerT", bound=Owner) +DescriptorT = TypeVar("DescriptorT", bound=Descriptor) + +def instance(owner: OwnerT) -> None: + # error: [invalid-attribute-access] + owner.value + +def class_object(owner: type[OwnerT]) -> None: + # error: [invalid-attribute-access] + owner.value + +def descriptor_value(descriptor: DescriptorT) -> None: + class C: + value = descriptor + + # error: [invalid-attribute-access] + C().value +``` + +### Intersections preserve invalid descriptor calls + +Intersecting a receiver or descriptor value with another type does not make its invalid `__get__` +method callable. + +```py +class Descriptor: + def __get__(self) -> int: + return 1 + +class Owner: + value = Descriptor() + +class Marker: ... + +def receiver(owner: Owner) -> None: + if isinstance(owner, Marker): + # error: [invalid-attribute-access] + owner.value + +def descriptor_value(descriptor: Descriptor) -> None: + if isinstance(descriptor, Marker): + class C: + value = descriptor + + # error: [invalid-attribute-access] + C().value +``` + +### Every `__get__` definition must accept the call + +A conditionally defined method can have several callable signatures. The access is invalid if any +possible definition rejects the call. + +```py +def access(flag: bool) -> None: + class Descriptor: + if flag: + def __get__(self, instance: object, owner: type | None = None) -> int: + return 1 + + else: + def __get__(self) -> str: + return "" + + class C: + value = Descriptor() + + # error: [invalid-attribute-access] "Invalid access to descriptor attribute `value` on type `C`" + reveal_type(C().value) # revealed: int | str +``` + +### A possible `__getattr__` fallback does not hide an invalid descriptor + +When a descriptor is only conditionally present, `__getattr__` handles the path where it is absent. +The other path still invokes the invalid descriptor and must produce a diagnostic. + +```py +def access(flag: bool) -> None: + class Descriptor: + def __get__(self) -> int: + return 1 + + class C: + if flag: + value = Descriptor() + + def __getattr__(self, name: str) -> str: + return name + + # error: [invalid-attribute-access] + reveal_type(C().value) # revealed: int | str +``` + +### A class-object lookup uses its declared member type + +Class-object member lookup uses the declared attribute type even when the declaration has no value. + +```py +class Descriptor: + def __get__(self) -> int: + return 1 + +class C: + value: Descriptor + +# error: [invalid-attribute-access] "Invalid access to descriptor attribute `value` on type ``" +C.value +``` + +### An instance `__getattribute__` can bypass descriptors + +A custom `__getattribute__` can return without invoking the malformed descriptor. The ordinary +member type remains unchanged, even when the override has the same return type as the descriptor. + +```py +class Descriptor: + def __get__(self) -> int: + return 1 + +class C: + value = Descriptor() + + def __getattribute__(self, name: str) -> int: + return 42 + +reveal_type(C().value) # revealed: int +``` + +### An unknown `__getattribute__` can bypass descriptors + +A dynamic base may provide an attribute interceptor that avoids a malformed descriptor, so the +descriptor access cannot be guaranteed to fail. + +```py +from typing import Any + +class Descriptor: + def __get__(self) -> int: + return 1 + +class C(Any): + value = Descriptor() + +reveal_type(C().value) # revealed: int +``` + +### An instance `__getattribute__` may delegate to descriptor lookup + +The return annotation of an override does not establish whether it delegates to the default +attribute lookup. Since ty does not inspect the implementation, it cannot conclude that the +descriptor is invoked. + +```py +class Descriptor: + def __get__(self) -> int: + return 1 + +class C: + value = Descriptor() + + def __getattribute__(self, name: str) -> str: + return super().__getattribute__(name) + +C().value +``` + +### An invalid `__getattribute__` runs before descriptors + +A malformed `__getattribute__` fails before it can invoke a malformed descriptor. The diagnostic +therefore describes the `__getattribute__` call while preserving the descriptor's return type. + +```py +class Descriptor: + def __get__(self) -> int: + return 1 + +class C: + value = Descriptor() + + # error: [invalid-method-override] + def __getattribute__(self) -> str: + return "fallback" + +# error: [invalid-attribute-access] "Invalid access to attribute `value` on type `C`" +reveal_type(C().value) # revealed: int +``` + +### An assigned instance attribute shadows a non-data descriptor + +An instance attribute takes precedence over a non-data descriptor. After the assignment, reading the +attribute does not call the descriptor. + +```py +from typing import Literal + +class Descriptor: + def __get__(self) -> str: + return "" + +class C: + value = Descriptor() + + def replace(self) -> None: + self.value: int = 1 + reveal_type(self.value) # revealed: Literal[1] +``` + +### An instance assignment does not shadow a data descriptor + +Assigning to a data descriptor invokes its `__set__` method. A subsequent read still invokes its +`__get__` method, even though the attribute has a known assigned type. + +```py +class Descriptor: + def __get__(self) -> int: + return 1 + + def __set__(self, instance: object, value: int) -> None: + pass + +class C: + value = Descriptor() + + def access(self) -> None: + self.value = 1 + # error: [invalid-attribute-access] + self.value +``` + +### A conditional assignment does not hide an invalid descriptor call + +The assignment shadows the non-data descriptor on one path, but the other path still invokes its +invalid `__get__` method. + +```py +class Descriptor: + def __get__(self) -> str: + return "" + +class C: + value = Descriptor() + +def access(c: C, flag: bool) -> None: + if flag: + c.value = Descriptor() + + # error: [invalid-attribute-access] "Invalid access to descriptor attribute `value` on type `C`" + c.value +``` + +### Augmented assignment reads before writing + +An augmented assignment reads the descriptor before writing the operation's result. The malformed +`__get__` call is therefore reported even though `__set__` accepts the result. + +```py +class Descriptor: + def __get__(self) -> int: + return 1 + + def __set__(self, instance: object, value: int) -> None: + pass + +class C: + value = Descriptor() + +c = C() +# error: [invalid-attribute-access] "Invalid access to descriptor attribute `value` on type `C`" +c.value += 1 +``` + +### Deletion does not read a descriptor + +Deleting a descriptor calls `__delete__` without first calling `__get__`. + +```py +class Descriptor: + def __get__(self) -> int: + return 1 + + def __delete__(self, instance: object) -> None: + pass + +class C: + value = Descriptor() + +c = C() +del c.value +``` + +### A class attribute can shadow a metaclass non-data descriptor + +The class attribute takes precedence, so the malformed metaclass descriptor is not invoked. + +```py +class Descriptor: + def __get__(self) -> str: + return "" + +class Meta(type): + value = Descriptor() + +class C(metaclass=Meta): + value = 1 + +reveal_type(C.value) # revealed: int +``` + +### A possible class attribute does not shadow a metaclass descriptor + +A conditionally defined class attribute shadows a metaclass descriptor only when it exists. The +other path invokes the invalid descriptor. + +```py +class Descriptor: + def __get__(self) -> str: + return "" + +class Meta(type): + value = Descriptor() + +def access(flag: bool) -> None: + class C(metaclass=Meta): + if flag: + value = 1 + + # error: [invalid-attribute-access] + reveal_type(C.value) # revealed: str | int +``` + +### A metaclass data descriptor takes precedence over a class attribute + +A data descriptor on the metaclass runs even when the class defines an attribute with the same name, +so an invalid descriptor call must be reported. + +```py +class Descriptor: + def __get__(self) -> str: + return "" + + def __set__(self, instance: object, value: int) -> None: + pass + +class Meta(type): + value = Descriptor() + +class C(metaclass=Meta): + value = 1 + +# error: [invalid-attribute-access] +reveal_type(C.value) # revealed: str +``` + +### A metaclass data descriptor shadows an invalid class descriptor + +A data descriptor on the metaclass has priority over a descriptor stored on the class. The class +descriptor is never called, so its invalid signature does not affect the access. + +```py +class DataDescriptor: + def __get__(self, instance: object, owner: type | None = None) -> int: + return 1 + + def __set__(self, instance: object, value: int) -> None: + pass + +class InvalidDescriptor: + def __get__(self) -> str: + return "" + +class Meta(type): + value = DataDescriptor() + +class C(metaclass=Meta): + value = InvalidDescriptor() + +reveal_type(C.value) # revealed: int +``` + +### `__get__` is not callable + +Python still attempts to call a non-callable `__get__` attribute, so the access fails and its type +is unknown. + +```py +class Descriptor: __get__: None = None -class Foo: - desc: BrokenDescriptor = BrokenDescriptor() +class C: + value: Descriptor = Descriptor() -# TODO: this raises `TypeError` at runtime due to the implicit call to `__get__`; -# we should emit a diagnostic -reveal_type(Foo().desc) # revealed: Unknown +# error: [invalid-attribute-access] "Invalid access to descriptor attribute `value` on type `C`" +reveal_type(C().value) # revealed: Unknown ``` ### Undeclared descriptor arguments @@ -1078,6 +1777,25 @@ def _(flag: bool): reveal_type(C().descriptor) # revealed: int | MaybeDescriptor ``` +### A possibly-unbound invalid `__get__` method still fails when present + +When a descriptor method is only conditionally defined, the branch where it exists must still accept +the implicit descriptor arguments. + +```py +def access(flag: bool) -> None: + class Descriptor: + if flag: + def __get__(self) -> int: + return 1 + + class C: + value = Descriptor() + + # error: [invalid-attribute-access] + reveal_type(C().value) # revealed: int | Descriptor +``` + ### Descriptors with non-function `__get__` callables that are descriptors themselves The descriptor protocol is recursive, i.e. looking up `__get__` can involve triggering the diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md index de60429349..e94d43dab8 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md @@ -29,7 +29,6 @@ error[invalid-assignment]: Object of type `Literal["wrong"]` is not assignable t | 8 | instance.attr = "wrong" # snapshot: invalid-assignment | ^^^^^^^^^^^^^ - | ``` And on the class object: @@ -44,7 +43,6 @@ error[invalid-assignment]: Object of type `Literal["wrong"]` is not assignable t | 9 | C.attr = "wrong" # snapshot: invalid-assignment | ^^^^^^ - | ``` ## Pure instance attributes @@ -74,7 +72,6 @@ error[invalid-attribute-access]: Cannot assign to instance attribute `attr` from | 8 | C.attr = 1 # snapshot: invalid-attribute-access | ^^^^^^ - | ``` ## Invalid annotated assignment to attribute @@ -96,23 +93,21 @@ class C: ```snapshot error[invalid-assignment]: Object of type `None` is not assignable to `str` - --> src/mdtest_snippet.py:3:20 + --> src/mdtest_snippet.py:3:26 | 3 | self.attr: str = None # snapshot: invalid-assignment | --- ^^^^ Incompatible value of type `None` | | | Declared type - | error[invalid-assignment]: Object of type `None` is not assignable to `str` - --> src/mdtest_snippet.py:8:26 + --> src/mdtest_snippet.py:8:32 | 8 | cls.class_attr1: str = None # snapshot: invalid-assignment | --- ^^^^ Incompatible value of type `None` | | | Declared type - | ``` Annotations on other attribute targets are ignored, and the assignment is checked against the @@ -180,7 +175,6 @@ error[invalid-attribute-access]: Cannot assign to ClassVar `attr` from an instan | 9 | instance.attr = 1 # snapshot: invalid-attribute-access | ^^^^^^^^^^^^^ - | ``` ## Unknown attributes @@ -199,7 +193,6 @@ error[unresolved-attribute]: Unresolved attribute `non_existent` on type ` src/mdtest_snippet.py:12:1 - | -12 | instance.attr = "wrong" # snapshot: invalid-assignment - | ^^^^^^^^^^^^^ +error[invalid-assignment]: Invalid assignment to data descriptor attribute `attr` on type `C` + --> src/mdtest_snippet.py:11:17 | +11 | instance.attr = "wrong" # snapshot: invalid-assignment + | ^^^^^^^ Expected `int`, found `Literal["wrong"]` +info: Argument to function `Descriptor.__set__` is incorrect +info: This assignment implicitly calls `__set__` on a descriptor of type `Descriptor` +info: Function defined here + --> src/mdtest_snippet.py:2:9 + | +2 | def __set__(self, instance: object, value: int) -> None: + | ^^^^^^^ ---------- Parameter declared here ``` ### Invalid `__set__` method signature @@ -299,17 +298,85 @@ class C: instance = C() -# TODO: ideally, we would mention why this is an invalid assignment (wrong number of arguments for `__set__`) instance.attr = 1 # snapshot: invalid-assignment ``` ```snapshot -error[invalid-assignment]: Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method - --> src/mdtest_snippet.py:11:1 +error[invalid-assignment]: Invalid assignment to data descriptor attribute `attr` on type `C` + --> src/mdtest_snippet.py:10:1 | -11 | instance.attr = 1 # snapshot: invalid-assignment - | ^^^^^^^^^^^^^ +10 | instance.attr = 1 # snapshot: invalid-assignment + | ^^^^^^^^^^^^^ No argument provided for required parameter `extra` of function `WrongDescriptor.__set__` +info: This assignment implicitly calls `__set__` on a descriptor of type `WrongDescriptor` +info: Parameter declared here + --> src/mdtest_snippet.py:2:53 + | +2 | def __set__(self, instance: object, value: int, extra: int) -> None: + | ^^^^^^^^^^ +``` + +### Invalid property setter argument type + +```py +class Document: ... + +class HasDocumentRef: + @property + def document(self) -> Document | None: ... + @document.setter + def document(self, document: Document) -> None: ... + +class Model(HasDocumentRef): + def detach(self) -> None: + self.document = None # snapshot: invalid-assignment + + # Check that the concise diagnostic identifies the actual setter argument mismatch. + # error: [invalid-assignment] "Expected `Document`, found `None`" + self.document = None +``` + +```snapshot +error[invalid-assignment]: Invalid assignment to data descriptor attribute `document` on type `Self@detach` + --> src/mdtest_snippet.py:11:25 | +11 | self.document = None # snapshot: invalid-assignment + | ^^^^ Expected `Document`, found `None` +info: Argument to function `HasDocumentRef.document` is incorrect +info: This assignment implicitly calls `__set__` on a descriptor of type `property` +info: Function defined here + --> src/mdtest_snippet.py:7:9 + | +7 | def document(self, document: Document) -> None: ... + | ^^^^^^^^ ------------------ Parameter declared here +``` + +### Nested argument type + +```py +class Descriptor: + def __set__(self, instance, value: tuple[int, str]) -> None: ... + +class C: + x = Descriptor() + +c = C() +c.x = (1, b"") # snapshot: invalid-assignment +``` + +```snapshot +error[invalid-assignment]: Invalid assignment to data descriptor attribute `x` on type `C` + --> src/mdtest_snippet.py:8:7 + | +8 | c.x = (1, b"") # snapshot: invalid-assignment + | ^^^^^^^^ Expected `tuple[int, str]`, found `tuple[Literal[1], Literal[b""]]` +info: Argument to function `Descriptor.__set__` is incorrect +info: This assignment implicitly calls `__set__` on a descriptor of type `Descriptor` +info: the second tuple element is not compatible: `Literal[b""]` is not assignable to `str` +info: Function defined here + --> src/mdtest_snippet.py:2:9 + | +2 | def __set__(self, instance, value: tuple[int, str]) -> None: ... + | ^^^^^^^ ---------------------- Parameter declared here ``` ## Setting attributes on union types @@ -344,5 +411,4 @@ error[invalid-assignment]: Object of type `Literal[1]` is not assignable to attr | 10 | C1.attr = 1 # snapshot: invalid-assignment | ^^^^^^^ - | ``` diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/error_context.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/error_context.md index b3629b4179..b2ff146490 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/error_context.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/error_context.md @@ -21,13 +21,12 @@ def _(source: str): ```snapshot error[invalid-assignment]: Object of type `str` is not assignable to `bytes` - --> src/mdtest_snippet.py:2:13 + --> src/mdtest_snippet.py:2:21 | 2 | target: bytes = source # snapshot | ----- ^^^^^^ Incompatible value of type `str` | | | Declared type - | ``` ## Unions @@ -41,13 +40,12 @@ def _(source: str | None): ```snapshot error[invalid-assignment]: Object of type `str | None` is not assignable to `str` - --> src/mdtest_snippet.py:2:13 + --> src/mdtest_snippet.py:2:19 | 2 | target: str = source # snapshot | --- ^^^^^^ Incompatible value of type `str | None` | | | Declared type - | info: element `None` of union `str | None` is not assignable to `str` ``` @@ -60,13 +58,12 @@ def _(source: int): ```snapshot error[invalid-assignment]: Object of type `int` is not assignable to `str | None` - --> src/mdtest_snippet.py:4:13 + --> src/mdtest_snippet.py:4:26 | 4 | target: str | None = source # snapshot | ---------- ^^^^^^ Incompatible value of type `int` | | | Declared type - | ``` Assigning a union to a union: @@ -78,13 +75,12 @@ def _(source: str | None): ```snapshot error[invalid-assignment]: Object of type `str | None` is not assignable to `bytes | None` - --> src/mdtest_snippet.py:6:13 + --> src/mdtest_snippet.py:6:28 | 6 | target: bytes | None = source # snapshot | ------------ ^^^^^^ Incompatible value of type `str | None` | | | Declared type - | info: element `str` of union `str | None` is not assignable to `bytes | None` ``` @@ -120,13 +116,12 @@ def _(source: Intersection[HasBar, HasNeither]): ```snapshot error[invalid-assignment]: Object of type `HasBar & HasNeither` is not assignable to `SupportsFooAndBar` - --> src/mdtest_snippet.py:23:13 + --> src/mdtest_snippet.py:23:33 | 23 | target: SupportsFooAndBar = source # snapshot | ----------------- ^^^^^^ Incompatible value of type `HasBar & HasNeither` | | | Declared type - | info: no element of intersection `HasBar & HasNeither` is assignable to `SupportsFooAndBar` info: ├── type `HasBar` is not assignable to protocol `SupportsFooAndBar` info: │ └── protocol member `foo` is not defined on type `HasBar` @@ -143,13 +138,12 @@ def _(source: HasFoo): ```snapshot error[invalid-assignment]: Object of type `HasFoo` is not assignable to `SupportsFoo & SupportsBar` - --> src/mdtest_snippet.py:25:13 + --> src/mdtest_snippet.py:25:54 | 25 | target: Intersection[SupportsFoo, SupportsBar] = source # snapshot | -------------------------------------- ^^^^^^ Incompatible value of type `HasFoo` | | | Declared type - | info: type `HasFoo` is not assignable to element `SupportsBar` of intersection `SupportsFoo & SupportsBar` info: └── type `HasFoo` is not assignable to protocol `SupportsBar` info: └── protocol member `bar` is not defined on type `HasFoo` @@ -164,13 +158,12 @@ def _(source: Intersection[HasFoo, HasNeither]): ```snapshot error[invalid-assignment]: Object of type `HasFoo & HasNeither` is not assignable to `SupportsFoo & SupportsBar` - --> src/mdtest_snippet.py:27:13 + --> src/mdtest_snippet.py:27:54 | 27 | target: Intersection[SupportsFoo, SupportsBar] = source # snapshot | -------------------------------------- ^^^^^^ Incompatible value of type `HasFoo & HasNeither` | | | Declared type - | info: type `HasFoo & HasNeither` is not assignable to element `SupportsBar` of intersection `SupportsFoo & SupportsBar` info: └── no element of intersection `HasFoo & HasNeither` is assignable to `SupportsBar` info: ├── type `HasFoo` is not assignable to protocol `SupportsBar` @@ -190,13 +183,12 @@ def _(source: tuple[int, str, bool]): ```snapshot error[invalid-assignment]: Object of type `tuple[int, str, bool]` is not assignable to `tuple[int, bytes, bool]` - --> src/mdtest_snippet.py:2:13 + --> src/mdtest_snippet.py:2:39 | 2 | target: tuple[int, bytes, bool] = source # snapshot | ----------------------- ^^^^^^ Incompatible value of type `tuple[int, str, bool]` | | | Declared type - | info: the second tuple element is not compatible: `str` is not assignable to `bytes` ``` @@ -209,13 +201,12 @@ def _(source: tuple[int, str]): ```snapshot error[invalid-assignment]: Object of type `tuple[int, str]` is not assignable to `tuple[int, str, bool]` - --> src/mdtest_snippet.py:4:13 + --> src/mdtest_snippet.py:4:37 | 4 | target: tuple[int, str, bool] = source # snapshot | --------------------- ^^^^^^ Incompatible value of type `tuple[int, str]` | | | Declared type - | info: a tuple of length 2 is not assignable to a tuple of length 3 ``` @@ -234,13 +225,12 @@ target: Callable[[int, bytes], bool] = source # snapshot ```snapshot error[invalid-assignment]: Object of type `def source(x: int, y: str)` is not assignable to `(int, bytes, /) -> bool` - --> src/mdtest_snippet.py:6:9 + --> src/mdtest_snippet.py:6:40 | 6 | target: Callable[[int, bytes], bool] = source # snapshot | ---------------------------- ^^^^^^ Incompatible value of type `def source(x: int, y: str)` | | | Declared type - | info: incompatible return types: `None` is not assignable to `bool` ``` @@ -253,13 +243,12 @@ def _(source: Callable[[int, str], bool]): ```snapshot error[invalid-assignment]: Object of type `(int, str, /) -> bool` is not assignable to `(int, bytes, /) -> bool` - --> src/mdtest_snippet.py:8:13 + --> src/mdtest_snippet.py:8:44 | 8 | target: Callable[[int, bytes], bool] = source # snapshot | ---------------------------- ^^^^^^ Incompatible value of type `(int, str, /) -> bool` | | | Declared type - | info: the second parameter has an incompatible type: `bytes` is not assignable to `str` ``` @@ -272,13 +261,12 @@ def _(source: Callable[[int, bytes], None]): ```snapshot error[invalid-assignment]: Object of type `(int, bytes, /) -> None` is not assignable to `(int, bytes, /) -> bool` - --> src/mdtest_snippet.py:10:13 + --> src/mdtest_snippet.py:10:44 | 10 | target: Callable[[int, bytes], bool] = source # snapshot | ---------------------------- ^^^^^^ Incompatible value of type `(int, bytes, /) -> None` | | | Declared type - | info: incompatible return types: `None` is not assignable to `bool` ``` @@ -291,14 +279,14 @@ def _(source: Callable[[int, str], bool]): ```snapshot error[invalid-assignment]: Object of type `(int, str, /) -> bool` is not assignable to `(int, /) -> bool` - --> src/mdtest_snippet.py:12:13 + --> src/mdtest_snippet.py:12:37 | 12 | target: Callable[[int], bool] = source # snapshot | --------------------- ^^^^^^ Incompatible value of type `(int, str, /) -> bool` | | | Declared type - | info: unexpected extra parameter +help: The parameter must have a default value ``` Assigning a function with an extra required parameter to a `Callable`: @@ -312,14 +300,14 @@ target: Callable[[int], bool] = source # snapshot ```snapshot error[invalid-assignment]: Object of type `def source(x: int, extra: str) -> bool` is not assignable to `(int, /) -> bool` - --> src/mdtest_snippet.py:16:9 + --> src/mdtest_snippet.py:16:33 | 16 | target: Callable[[int], bool] = source # snapshot | --------------------- ^^^^^^ Incompatible value of type `def source(x: int, extra: str) -> bool` | | | Declared type - | info: unexpected extra parameter `extra` +help: Parameter `extra` must have a default value ``` Assigning a class to a `Callable` @@ -333,13 +321,12 @@ target: Callable[[str], Any] = Number # snapshot ```snapshot error[invalid-assignment]: Object of type `` is not assignable to `(str, /) -> Any` - --> src/mdtest_snippet.py:20:9 + --> src/mdtest_snippet.py:20:32 | 20 | target: Callable[[str], Any] = Number # snapshot | -------------------- ^^^^^^ Incompatible value of type `` | | | Declared type - | info: type `` has inferred callable type `(value: int) -> Number` info: └── the first parameter has an incompatible type: `str` is not assignable to `int` ``` @@ -363,15 +350,14 @@ error[invalid-argument-type]: Argument to function `accepts_callable` is incorre | 28 | accepts_callable(Foo) # snapshot | ^^^ Expected `(Any, /) -> Any`, found `` - | info: type `` has inferred callable type `(x: Any, y: Any) -> Foo` info: └── unexpected extra parameter `y` +help: Parameter `y` must have a default value info: Function defined here --> src/mdtest_snippet.py:23:5 | 23 | def accepts_callable(callback: Callable[[Any], Any]) -> None: ... | ^^^^^^^^^^^^^^^^ ------------------------------ Parameter declared here - | ``` Assigning a bound method to a `Callable`: @@ -387,13 +373,12 @@ bound_method_target: Callable[[int], str] = greeter.greet # snapshot ```snapshot error[invalid-assignment]: Object of type `bound method Greeter.greet(name: str, greeting: str = "Hello") -> str` is not assignable to `(int, /) -> str` - --> src/mdtest_snippet.py:34:22 + --> src/mdtest_snippet.py:34:45 | 34 | bound_method_target: Callable[[int], str] = greeter.greet # snapshot | -------------------- ^^^^^^^^^^^^^ Incompatible value of type `bound method Greeter.greet(name: str, greeting: str = "Hello") -> str` | | | Declared type - | info: the first parameter has an incompatible type: `int` is not assignable to `str` ``` @@ -408,13 +393,12 @@ known_bound_method_target: Callable[[str], bool] = callable_base.__call__ # sna ```snapshot error[invalid-assignment]: Object of type `` is not assignable to `(str, /) -> bool` - --> src/mdtest_snippet.py:38:28 + --> src/mdtest_snippet.py:38:52 | 38 | known_bound_method_target: Callable[[str], bool] = callable_base.__call__ # snapshot | --------------------- ^^^^^^^^^^^^^^^^^^^^^^ Incompatible value of type `` | | | Declared type - | info: type `` has inferred callable type `(x: int) -> bool` info: └── the first parameter has an incompatible type: `str` is not assignable to `int` ``` @@ -433,16 +417,143 @@ partial_target: Callable[[bytes], bool] = partial_predicate # snapshot ```snapshot error[invalid-assignment]: Object of type `partial[(y: str) -> bool]` is not assignable to `(bytes, /) -> bool` - --> src/mdtest_snippet.py:45:17 + --> src/mdtest_snippet.py:45:43 | 45 | partial_target: Callable[[bytes], bool] = partial_predicate # snapshot | ----------------------- ^^^^^^^^^^^^^^^^^ Incompatible value of type `partial[(y: str) -> bool]` | | | Declared type - | info: the first parameter has an incompatible type: `bytes` is not assignable to `str` ``` +## Missing unnamed callable parameters + +Parameters in a `Callable` type do not have names, so a missing parameter is identified by its +position. + +```py +from typing import Callable + +def assign(source: Callable[[], None]) -> None: + target: Callable[[int], None] = source # snapshot: invalid-assignment +``` + +```snapshot +error[invalid-assignment]: Object of type `() -> None` is not assignable to `(int, /) -> None` + --> src/mdtest_snippet.py:4:37 + | +4 | target: Callable[[int], None] = source # snapshot: invalid-assignment + | --------------------- ^^^^^^ Incompatible value of type `() -> None` + | | + | Declared type +info: the first parameter is missing +``` + +## Missing parameters in nested generic calls involving `TypeVarTuple`s and `ParamSpec`s + +In the following example, the signature of the `callback` function does not satisfy the `fn` +parameter of `wrapper` in the `accept()` call, because the arguments provided to `accept()` +following `fn` indicate that it must accept the value `1` as a positional argument, and it does not. + +We don't currently add error context in this code path, but we could add it in the future: + +```py +from collections.abc import Callable + +def wrapper1[**P](fn: Callable[P, None]) -> Callable[P, None]: + return fn + +def accept1[**P](fn: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... +def callback1() -> None: ... + +accept1(wrapper1(callback1), 1) # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper1` is incorrect + --> src/mdtest_snippet.py:9:18 + | +9 | accept1(wrapper1(callback1), 1) # snapshot: invalid-argument-type + | ^^^^^^^^^ Expected `(**P@accept1) -> None`, found `def callback1()` +info: Function defined here + --> src/mdtest_snippet.py:3:5 + | +3 | def wrapper1[**P](fn: Callable[P, None]) -> Callable[P, None]: + | ^^^^^^^^ --------------------- Parameter declared here +``` + +The following case is similar, but exercises a different code path. Here, we could also add error +context to improve the diagnostic in the future: + +```py +def wrapper2[**P](fn: Callable[P, None]) -> Callable[P, None]: + return fn + +def accept2[**P](fn: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... +def callback2(**kwargs: int) -> None: ... + +accept2(wrapper2(callback2), 1) # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper2` is incorrect + --> src/mdtest_snippet.py:16:18 + | +16 | accept2(wrapper2(callback2), 1) # snapshot: invalid-argument-type + | ^^^^^^^^^ Expected `(**P@accept2) -> None`, found `def callback2(**kwargs: int)` +info: Function defined here + --> src/mdtest_snippet.py:10:5 + | +10 | def wrapper2[**P](fn: Callable[P, None]) -> Callable[P, None]: + | ^^^^^^^^ --------------------- Parameter declared here +``` + +And the same applies to the following two examples too, which both use a `TypeVarTuple` instead of a +`ParamSpec`: + +```py +def wrapper3[*Ts](fn: Callable[[*Ts], None]) -> Callable[[*Ts], None]: + return fn + +def accept3[*Ts](fn: Callable[[*Ts], None], *args: *Ts) -> None: ... +def callback3(value: int) -> None: ... + +accept3(wrapper3(callback3)) # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper3` is incorrect + --> src/mdtest_snippet.py:23:18 + | +23 | accept3(wrapper3(callback3)) # snapshot: invalid-argument-type + | ^^^^^^^^^ Expected `(*int) -> None`, found `def callback3(value: int)` +info: Function defined here + --> src/mdtest_snippet.py:17:5 + | +17 | def wrapper3[*Ts](fn: Callable[[*Ts], None]) -> Callable[[*Ts], None]: + | ^^^^^^^^ ------------------------- Parameter declared here +``` + +```py +def accepts4[*Ts](fn: Callable[[*Ts, int], None]) -> None: ... +def callback4() -> None: ... + +accepts4(callback4) # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `accepts4` is incorrect + --> src/mdtest_snippet.py:27:10 + | +27 | accepts4(callback4) # snapshot: invalid-argument-type + | ^^^^^^^^^ Expected `(*args: Unknown, int, /) -> None`, found `def callback4()` +info: Function defined here + --> src/mdtest_snippet.py:24:5 + | +24 | def accepts4[*Ts](fn: Callable[[*Ts, int], None]) -> None: ... + | ^^^^^^^^ ------------------------------ Parameter declared here +``` + ## Function assignability and overrides Liskov checks use function-to-function assignability. @@ -471,7 +582,6 @@ error[invalid-method-override]: Invalid override of method `method` | 2 | def method(self, x: str) -> bool: | ---------------------------- `Parent.method` defined here - | info: parameter `x` has an incompatible type: `str` is not assignable to `bytes` info: This violates the Liskov Substitution Principle ``` @@ -500,7 +610,6 @@ error[invalid-method-override]: Invalid override of method `method` | 10 | def method(self, *, x: str, y: int) -> bool: | --------------------------------------- `ParentXY.method` defined here - | info: parameter `x` has an incompatible type: `str` is not assignable to `bytes` info: This violates the Liskov Substitution Principle ``` @@ -525,7 +634,6 @@ error[invalid-method-override]: Invalid override of method `method` | 2 | def method(self, x: str) -> bool: | ---------------------------- `Parent.method` defined here - | info: incompatible return types: `None` is not assignable to `bool` info: This violates the Liskov Substitution Principle ``` @@ -550,11 +658,35 @@ error[invalid-method-override]: Invalid override of method `method` | 2 | def method(self, x: str) -> bool: | ---------------------------- `Parent.method` defined here - | info: the parameter named `y` does not match `x` (and can be used as a keyword parameter) info: This violates the Liskov Substitution Principle ``` +## Uncallable top signatures + +A top callable represents every possible callable signature, so no specific call is guaranteed to be +accepted. It therefore cannot be assigned to a callable that promises to accept an integer. + +```py +from typing import Callable +from ty_extensions import Top + +def assign(source: Top[Callable[..., int]]) -> None: + target: Callable[[int], int] = source # snapshot: invalid-assignment +``` + +```snapshot +error[invalid-assignment]: Object of type `Top[(...) -> int]` is not assignable to `(int, /) -> int` + --> src/mdtest_snippet.py:5:36 + | +5 | target: Callable[[int], int] = source # snapshot: invalid-assignment + | -------------------- ^^^^^^ Incompatible value of type `Top[(...) -> int]` + | | + | Declared type +info: Object of type `Top[(...) -> int]` is not safe to call; its signature is not known +help: This type includes all possible parameter sets, so it cannot safely be called because there is no valid set of arguments for it +``` + ## `TypedDict` Incompatible field types: @@ -574,13 +706,12 @@ def _(source: Person): ```snapshot error[invalid-assignment]: Object of type `Person` is not assignable to `Other` - --> src/mdtest_snippet.py:10:13 + --> src/mdtest_snippet.py:10:21 | 10 | target: Other = source # snapshot | ----- ^^^^^^ Incompatible value of type `Person` | | | Declared type - | info: field "name" on TypedDict `Person` has type `str` which is not assignable to type `bytes` expected by TypedDict `Other` ``` @@ -597,13 +728,12 @@ def _(source: Person): ```snapshot error[invalid-assignment]: Object of type `Person` is not assignable to `PersonWithAge` - --> src/mdtest_snippet.py:16:13 + --> src/mdtest_snippet.py:16:29 | 16 | target: PersonWithAge = source # snapshot | ------------- ^^^^^^ Incompatible value of type `Person` | | | Declared type - | info: required field "age" is not present in source TypedDict `Person` ``` @@ -620,13 +750,12 @@ def _(source: PersonWithOptionalAge): ```snapshot error[invalid-assignment]: Object of type `PersonWithOptionalAge` is not assignable to `PersonWithAge` - --> src/mdtest_snippet.py:22:13 + --> src/mdtest_snippet.py:22:29 | 22 | target: PersonWithAge = source # snapshot | ------------- ^^^^^^ Incompatible value of type `PersonWithOptionalAge` | | | Declared type - | info: field "age" is required in TypedDict `PersonWithAge` but not required in TypedDict `PersonWithOptionalAge` ``` @@ -642,13 +771,12 @@ def _(source: PersonWithReadOnlyName): ```snapshot error[invalid-assignment]: Object of type `PersonWithReadOnlyName` is not assignable to `Person` - --> src/mdtest_snippet.py:27:13 + --> src/mdtest_snippet.py:27:22 | 27 | target: Person = source # snapshot | ------ ^^^^^^ Incompatible value of type `PersonWithReadOnlyName` | | | Declared type - | info: field "name" is read-only in TypedDict `PersonWithReadOnlyName` but mutable in TypedDict `Person` ``` @@ -661,13 +789,12 @@ def _(source: PersonWithAge): ```snapshot error[invalid-assignment]: Object of type `PersonWithAge` is not assignable to `PersonWithOptionalAge` - --> src/mdtest_snippet.py:29:13 + --> src/mdtest_snippet.py:29:37 | 29 | target: PersonWithOptionalAge = source # snapshot | --------------------- ^^^^^^ Incompatible value of type `PersonWithAge` | | | Declared type - | info: field "age" is required in TypedDict `PersonWithAge` but not required and mutable in TypedDict `PersonWithOptionalAge` help: The required field could be removed through a destructive operation like `del` on the target. ``` @@ -681,18 +808,131 @@ def _(source: Person): ```snapshot error[invalid-assignment]: Object of type `Person` is not assignable to `dict[str, Any]` - --> src/mdtest_snippet.py:31:13 + --> src/mdtest_snippet.py:31:30 | 31 | target: dict[str, Any] = source # snapshot | -------------- ^^^^^^ Incompatible value of type `Person` | | | Declared type - | info: TypedDict `Person` is not assignable to `dict` help: A TypedDict is not usually assignable to any `dict[..]` type; `dict` types allow destructive operations like `clear()`. help: Consider using `Mapping[..]` instead of `dict[..]`. ``` +Assigning an open `TypedDict` to a specialized `Mapping`: + +```py +from collections.abc import Mapping +from typing import TypedDict + +class D(TypedDict): + a: int + b: int + +def f(d: D) -> Mapping[str, int]: + return d # snapshot +``` + +```snapshot +error[invalid-return-type]: Return type does not match returned value + --> src/mdtest_snippet.py:40:12 + | +39 | def f(d: D) -> Mapping[str, int]: + | ----------------- Expected `Mapping[str, int]` because of return type +40 | return d # snapshot + | ^ expected `Mapping[str, int]`, found `D` +info: TypedDict `D` is not assignable to `Mapping[str, int]` +help: `D` would be assignable to this `Mapping` type if it were declared with `closed=True`, but TypedDicts are open by default. +help: A subclass of `D` could validly add a new field of an arbitrary type, violating subtyping with the `Mapping` type +``` + +## Generic `TypedDict` field conflicts in overload diagnostics + +A generic `TypedDict` relation can be unsatisfiable without being the `never` terminal. The +resulting overload diagnostic should still explain which field introduced the conflicting +constraints. + +```py +from typing import Generic, Self, TypeVar, TypedDict, overload + +T = TypeVar("T") + +class Pair(TypedDict, Generic[T]): + first: T + second: T + +class Fixed(TypedDict): + first: int + second: str + +class OverloadedSelf: + @overload + def method(self, value: Fixed) -> None: ... # snapshot: invalid-overload + @overload + def method(self, value: str) -> None: ... + def method(self, value: Pair[Self] | str) -> None: ... +``` + +```snapshot +error[invalid-overload]: Implementation does not accept all arguments of this overload + --> src/mdtest_snippet.py:15:9 + | +15 | def method(self, value: Fixed) -> None: ... # snapshot: invalid-overload + | ^^^^^^ +16 | @overload +17 | def method(self, value: str) -> None: ... +18 | def method(self, value: Pair[Self] | str) -> None: ... + | ------ Implementation defined here +info: Implementation signature `(self, value: Pair[Self@method] | str) -> None` is not assignable to overload signature `(self, value: Fixed) -> None` +info: parameter `value` has an incompatible type: `Fixed` is not assignable to `Pair[Self@method] | str` +info: └── type `Fixed` is not assignable to any element of the union `Pair[Self@method] | str` +info: ├── field "second" on TypedDict `Fixed` has type `str` which is not assignable to type `Self@method` expected by TypedDict `Pair` +info: └── ... omitted 1 union element without additional context +``` + +## Stop checking callable parameters after incompatible generic constraints + +Once earlier parameters produce an unsatisfiable nonterminal constraint set, continuing to a later +parameter must not replace the diagnostic context that explains the original incompatibility. + +```py +from typing import Generic, Self, TypeVar, TypedDict, overload + +T = TypeVar("T") + +class Pair(TypedDict, Generic[T]): + first: T + second: T + +class Fixed(TypedDict): + first: int + second: str + +class OverloadedSelf: + @overload + def method(self, value: Fixed, later: int) -> None: ... # snapshot: invalid-overload + @overload + def method(self, value: str, later: str) -> None: ... + def method(self, value: Pair[Self] | str, later: str) -> None: ... +``` + +```snapshot +error[invalid-overload]: Implementation does not accept all arguments of this overload + --> src/mdtest_snippet.py:15:9 + | +15 | def method(self, value: Fixed, later: int) -> None: ... # snapshot: invalid-overload + | ^^^^^^ +16 | @overload +17 | def method(self, value: str, later: str) -> None: ... +18 | def method(self, value: Pair[Self] | str, later: str) -> None: ... + | ------ Implementation defined here +info: Implementation signature `(self, value: Pair[Self@method] | str, later: str) -> None` is not assignable to overload signature `(self, value: Fixed, later: int) -> None` +info: parameter `value` has an incompatible type: `Fixed` is not assignable to `Pair[Self@method] | str` +info: └── type `Fixed` is not assignable to any element of the union `Pair[Self@method] | str` +info: ├── field "second" on TypedDict `Fixed` has type `str` which is not assignable to type `Self@method` expected by TypedDict `Pair` +info: └── ... omitted 1 union element without additional context +``` + ## Type variable upper bounds Assignability context is included when an explicit type argument does not satisfy a type variable's @@ -710,7 +950,7 @@ bad: Box[tuple[int, str, bool]] # snapshot: invalid-type-arguments ```snapshot error[invalid-type-arguments]: Type `tuple[int, str, bool]` is not assignable to upper bound `tuple[int, bytes, bool]` of type variable `T@Box` - --> src/mdtest_snippet.py:3:1 + --> src/mdtest_snippet.py:7:10 | 3 | T = TypeVar("T", bound=tuple[int, bytes, bool]) | - Type variable defined here @@ -719,7 +959,6 @@ error[invalid-type-arguments]: Type `tuple[int, str, bool]` is not assignable to 6 | 7 | bad: Box[tuple[int, str, bool]] # snapshot: invalid-type-arguments | ^^^^^^^^^^^^^^^^^^^^^ - | info: the second tuple element is not compatible: `str` is not assignable to `bytes` ``` @@ -741,13 +980,12 @@ def _(source: DoesNotHaveCheck): ```snapshot error[invalid-assignment]: Object of type `DoesNotHaveCheck` is not assignable to `SupportsCheck` - --> src/mdtest_snippet.py:9:13 + --> src/mdtest_snippet.py:9:29 | 9 | target: SupportsCheck = source # snapshot | ------------- ^^^^^^ Incompatible value of type `DoesNotHaveCheck` | | | Declared type - | info: type `DoesNotHaveCheck` is not assignable to protocol `SupportsCheck` info: └── protocol member `check` is not defined on type `DoesNotHaveCheck` ``` @@ -765,13 +1003,12 @@ def _(source: CheckWithWrongSignature): ```snapshot error[invalid-assignment]: Object of type `CheckWithWrongSignature` is not assignable to `SupportsCheck` - --> src/mdtest_snippet.py:15:13 + --> src/mdtest_snippet.py:15:29 | 15 | target: SupportsCheck = source # snapshot | ------------- ^^^^^^ Incompatible value of type `CheckWithWrongSignature` | | | Declared type - | info: type `CheckWithWrongSignature` is not assignable to protocol `SupportsCheck` info: └── protocol member `check` is incompatible info: └── parameter `y` has an incompatible type: `str` is not assignable to `bytes` @@ -792,13 +1029,12 @@ def _(source: DoesNotHaveName): ```snapshot error[invalid-assignment]: Object of type `DoesNotHaveName` is not assignable to `SupportsName` - --> src/mdtest_snippet.py:23:13 + --> src/mdtest_snippet.py:23:28 | 23 | target: SupportsName = source # snapshot | ------------ ^^^^^^ Incompatible value of type `DoesNotHaveName` | | | Declared type - | info: type `DoesNotHaveName` is not assignable to protocol `SupportsName` info: └── protocol member `name` is not defined on type `DoesNotHaveName` ``` @@ -815,13 +1051,12 @@ def _(source: SupportsSomethingElse): ```snapshot error[invalid-assignment]: Object of type `SupportsSomethingElse` is not assignable to `SupportsCheck` - --> src/mdtest_snippet.py:28:13 + --> src/mdtest_snippet.py:28:29 | 28 | target: SupportsCheck = source # snapshot | ------------- ^^^^^^ Incompatible value of type `SupportsSomethingElse` | | | Declared type - | info: protocol `SupportsSomethingElse` is not assignable to protocol `SupportsCheck` info: └── protocol member `check` is not defined on type `SupportsSomethingElse` ``` @@ -862,13 +1097,12 @@ def _(source: BytesName): ```snapshot error[invalid-assignment]: Object of type `BytesName` is not assignable to `ReadableName` - --> src/mdtest_snippet.py:54:13 + --> src/mdtest_snippet.py:54:28 | 54 | target: ReadableName = source # snapshot | ------------ ^^^^^^ Incompatible value of type `BytesName` | | | Declared type - | info: type `BytesName` is not assignable to protocol `ReadableName` info: └── protocol member `name` is incompatible info: └── read type `bytes` is not assignable to `str` @@ -881,13 +1115,12 @@ def _(source: ReadOnlyName): ```snapshot error[invalid-assignment]: Object of type `ReadOnlyName` is not assignable to `WritableName` - --> src/mdtest_snippet.py:56:13 + --> src/mdtest_snippet.py:56:28 | 56 | target: WritableName = source # snapshot | ------------ ^^^^^^ Incompatible value of type `ReadOnlyName` | | | Declared type - | info: type `ReadOnlyName` is not assignable to protocol `WritableName` info: └── protocol member `name` is incompatible info: └── the member does not accept writes of type `str` @@ -900,13 +1133,12 @@ def _(source: BytesSetterName): ```snapshot error[invalid-assignment]: Object of type `BytesSetterName` is not assignable to `WritableName` - --> src/mdtest_snippet.py:58:13 + --> src/mdtest_snippet.py:58:28 | 58 | target: WritableName = source # snapshot | ------------ ^^^^^^ Incompatible value of type `BytesSetterName` | | | Declared type - | info: type `BytesSetterName` is not assignable to protocol `WritableName` info: └── protocol member `name` is incompatible info: └── the member does not accept writes of type `str` @@ -936,13 +1168,12 @@ def _(source: ReadOnlyNameProtocol): ```snapshot error[invalid-assignment]: Object of type `ReadOnlyNameProtocol` is not assignable to `WritableName` - --> src/mdtest_snippet.py:72:13 + --> src/mdtest_snippet.py:72:28 | 72 | target: WritableName = source # snapshot | ------------ ^^^^^^ Incompatible value of type `ReadOnlyNameProtocol` | | | Declared type - | info: protocol `ReadOnlyNameProtocol` is not assignable to protocol `WritableName` info: └── protocol member `name` is incompatible info: └── the member is not writable @@ -955,13 +1186,12 @@ def _(source: BytesNameProtocol): ```snapshot error[invalid-assignment]: Object of type `BytesNameProtocol` is not assignable to `WritableName` - --> src/mdtest_snippet.py:74:13 + --> src/mdtest_snippet.py:74:28 | 74 | target: WritableName = source # snapshot | ------------ ^^^^^^ Incompatible value of type `BytesNameProtocol` | | | Declared type - | info: protocol `BytesNameProtocol` is not assignable to protocol `WritableName` info: └── protocol member `name` is incompatible info: └── read type `bytes` is not assignable to `str` @@ -974,13 +1204,12 @@ def _(source: BytesSetterNameProtocol): ```snapshot error[invalid-assignment]: Object of type `BytesSetterNameProtocol` is not assignable to `WritableName` - --> src/mdtest_snippet.py:76:13 + --> src/mdtest_snippet.py:76:28 | 76 | target: WritableName = source # snapshot | ------------ ^^^^^^ Incompatible value of type `BytesSetterNameProtocol` | | | Declared type - | info: protocol `BytesSetterNameProtocol` is not assignable to protocol `WritableName` info: └── protocol member `name` is incompatible info: └── the member does not accept writes of type `str` @@ -998,13 +1227,12 @@ def _(source: SupportsCheckWithOtherSignature): ```snapshot error[invalid-assignment]: Object of type `SupportsCheckWithOtherSignature` is not assignable to `SupportsCheck` - --> src/mdtest_snippet.py:81:13 + --> src/mdtest_snippet.py:81:29 | 81 | target: SupportsCheck = source # snapshot | ------------- ^^^^^^ Incompatible value of type `SupportsCheckWithOtherSignature` | | | Declared type - | info: protocol `SupportsCheckWithOtherSignature` is not assignable to protocol `SupportsCheck` info: └── protocol member `check` is incompatible info: └── parameter `y` has an incompatible type: `str` is not assignable to `bytes` @@ -1032,13 +1260,12 @@ def _(source: HasName): ```snapshot error[invalid-assignment]: Object of type `HasName` is not assignable to `StringOrName` - --> src/mdtest_snippet.py:13:13 + --> src/mdtest_snippet.py:13:28 | 13 | target: StringOrName = source # snapshot | ------------ ^^^^^^ Incompatible value of type `HasName` | | | Declared type - | info: type `HasName` is not assignable to any element of the union `str | SupportsName` info: ├── type `HasName` is not assignable to protocol `SupportsName` info: │ └── protocol member `name` is incompatible @@ -1059,13 +1286,12 @@ target: Callable[[tuple[int, bytes]], bool] = source # snapshot ```snapshot error[invalid-assignment]: Object of type `def source(x: tuple[int, str]) -> bool` is not assignable to `(tuple[int, bytes], /) -> bool` - --> src/mdtest_snippet.py:6:9 + --> src/mdtest_snippet.py:6:47 | 6 | target: Callable[[tuple[int, bytes]], bool] = source # snapshot | ----------------------------------- ^^^^^^ Incompatible value of type `def source(x: tuple[int, str]) -> bool` | | | Declared type - | info: the first parameter has an incompatible type: `tuple[int, bytes]` is not assignable to `tuple[int, str]` info: └── the second tuple element is not compatible: `bytes` is not assignable to `str` ``` @@ -1089,13 +1315,12 @@ def _(source: Incompatible): ```snapshot error[invalid-assignment]: Object of type `Incompatible` is not assignable to `SupportsCheck` - --> src/mdtest_snippet.py:12:13 + --> src/mdtest_snippet.py:12:29 | 12 | target: SupportsCheck = source # snapshot | ------------- ^^^^^^ Incompatible value of type `Incompatible` | | | Declared type - | info: type `Incompatible` is not assignable to protocol `SupportsCheck` info: └── protocol member `check1` is incompatible info: └── parameter `x` has an incompatible type: `str` is not assignable to `bytes` @@ -1120,13 +1345,12 @@ def _(source: HasNeither): ```snapshot error[invalid-assignment]: Object of type `HasNeither` is not assignable to `SupportsFoo | SupportsBar` - --> src/mdtest_snippet.py:12:13 + --> src/mdtest_snippet.py:12:41 | 12 | target: SupportsFoo | SupportsBar = source # snapshot | ------------------------- ^^^^^^ Incompatible value of type `HasNeither` | | | Declared type - | info: type `HasNeither` is not assignable to any element of the union `SupportsFoo | SupportsBar` info: ├── type `HasNeither` is not assignable to protocol `SupportsFoo` info: │ └── protocol member `foo` is not defined on type `HasNeither` @@ -1143,13 +1367,12 @@ def _(source: int): ```snapshot error[invalid-assignment]: Object of type `int` is not assignable to `str | bytes | bool | None` - --> src/mdtest_snippet.py:2:13 + --> src/mdtest_snippet.py:2:41 | 2 | target: str | bytes | bool | None = source # snapshot | ------------------------- ^^^^^^ Incompatible value of type `int` | | | Declared type - | ``` ## Failures for multiple intersection elements @@ -1170,13 +1393,12 @@ def _(source: Intersection[DoesNotSupportFoo1, DoesNotSupportFoo2]): ```snapshot error[invalid-assignment]: Object of type `DoesNotSupportFoo1 & DoesNotSupportFoo2` is not assignable to `SupportsFoo` - --> src/mdtest_snippet.py:11:13 + --> src/mdtest_snippet.py:11:27 | 11 | target: SupportsFoo = source # snapshot | ----------- ^^^^^^ Incompatible value of type `DoesNotSupportFoo1 & DoesNotSupportFoo2` | | | Declared type - | info: no element of intersection `DoesNotSupportFoo1 & DoesNotSupportFoo2` is assignable to `SupportsFoo` info: ├── type `DoesNotSupportFoo1` is not assignable to protocol `SupportsFoo` info: │ └── protocol member `foo` is not defined on type `DoesNotSupportFoo1` @@ -1210,13 +1432,12 @@ def _(source: IncompatibleFoo): ```snapshot error[invalid-assignment]: Object of type `IncompatibleFoo` is not assignable to `SupportsFooAndBar` - --> src/mdtest_snippet.py:16:13 + --> src/mdtest_snippet.py:16:33 | 16 | target: SupportsFooAndBar = source # snapshot | ----------------- ^^^^^^ Incompatible value of type `IncompatibleFoo` | | | Declared type - | info: type `IncompatibleFoo` is not assignable to protocol `SupportsFooAndBar` info: └── protocol member `foo` is incompatible info: └── the parameter named `name_` does not match `name` (and can be used as a keyword parameter) @@ -1233,13 +1454,12 @@ def _(source: list[str]): ```snapshot error[invalid-assignment]: Object of type `list[str]` is not assignable to `Iterable[bytes]` - --> src/mdtest_snippet.py:4:13 + --> src/mdtest_snippet.py:4:31 | 4 | target: Iterable[bytes] = source # snapshot | --------------- ^^^^^^ Incompatible value of type `list[str]` | | | Declared type - | info: type `list[str]` is not assignable to protocol `Iterable[bytes]` info: └── protocol member `__iter__` is incompatible info: └── incompatible return types: `Iterator[str]` is not assignable to `Iterator[bytes]` @@ -1260,13 +1480,12 @@ def _(source: list[bool]): ```snapshot error[invalid-assignment]: Object of type `list[bool]` is not assignable to `list[int]` - --> src/mdtest_snippet.py:2:13 + --> src/mdtest_snippet.py:2:25 | 2 | target: list[int] = source # snapshot | --------- ^^^^^^ Incompatible value of type `list[bool]` | | | Declared type - | info: `list` is invariant in its type parameter info: Consider using the covariant supertype `collections.abc.Sequence` info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics @@ -1320,163 +1539,150 @@ def _(source: MutableSequence[bool]): ```snapshot error[invalid-assignment]: Object of type `set[bool]` is not assignable to `set[int]` - --> src/mdtest_snippet.py:7:13 + --> src/mdtest_snippet.py:7:24 | 7 | target: set[int] = source # snapshot | -------- ^^^^^^ Incompatible value of type `set[bool]` | | | Declared type - | info: `set` is invariant in its type parameter info: Consider using the covariant supertype `collections.abc.Set` info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics error[invalid-assignment]: Object of type `dict[str, bool]` is not assignable to `dict[str, int]` - --> src/mdtest_snippet.py:10:13 + --> src/mdtest_snippet.py:10:30 | 10 | target: dict[str, int] = source # snapshot | -------------- ^^^^^^ Incompatible value of type `dict[str, bool]` | | | Declared type - | info: `dict` is invariant in its second type parameter info: Consider using the supertype `collections.abc.Mapping`, which is covariant in its value type info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics error[invalid-assignment]: Object of type `dict[bool, str]` is not assignable to `dict[int, str]` - --> src/mdtest_snippet.py:13:13 + --> src/mdtest_snippet.py:13:30 | 13 | target: dict[int, str] = source # snapshot | -------------- ^^^^^^ Incompatible value of type `dict[bool, str]` | | | Declared type - | info: `dict` is invariant in its first type parameter info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics error[invalid-assignment]: Object of type `dict[bool, bool]` is not assignable to `dict[int, int]` - --> src/mdtest_snippet.py:16:13 + --> src/mdtest_snippet.py:16:30 | 16 | target: dict[int, int] = source # snapshot | -------------- ^^^^^^ Incompatible value of type `dict[bool, bool]` | | | Declared type - | info: `dict` is invariant in its first and second type parameters info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics error[invalid-assignment]: Object of type `defaultdict[str, bool]` is not assignable to `defaultdict[str, int]` - --> src/mdtest_snippet.py:19:13 + --> src/mdtest_snippet.py:19:37 | 19 | target: defaultdict[str, int] = source # snapshot | --------------------- ^^^^^^ Incompatible value of type `defaultdict[str, bool]` | | | Declared type - | info: `defaultdict` is invariant in its second type parameter info: Consider using the supertype `collections.abc.Mapping`, which is covariant in its value type info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics error[invalid-assignment]: Object of type `defaultdict[bool, str]` is not assignable to `defaultdict[int, str]` - --> src/mdtest_snippet.py:22:13 + --> src/mdtest_snippet.py:22:37 | 22 | target: defaultdict[int, str] = source # snapshot | --------------------- ^^^^^^ Incompatible value of type `defaultdict[bool, str]` | | | Declared type - | info: `defaultdict` is invariant in its first type parameter info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics error[invalid-assignment]: Object of type `OrderedDict[str, bool]` is not assignable to `OrderedDict[str, int]` - --> src/mdtest_snippet.py:25:13 + --> src/mdtest_snippet.py:25:37 | 25 | target: OrderedDict[str, int] = source # snapshot | --------------------- ^^^^^^ Incompatible value of type `OrderedDict[str, bool]` | | | Declared type - | info: `OrderedDict` is invariant in its second type parameter info: Consider using the supertype `collections.abc.Mapping`, which is covariant in its value type info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics error[invalid-assignment]: Object of type `OrderedDict[bool, str]` is not assignable to `OrderedDict[int, str]` - --> src/mdtest_snippet.py:28:13 + --> src/mdtest_snippet.py:28:37 | 28 | target: OrderedDict[int, str] = source # snapshot | --------------------- ^^^^^^ Incompatible value of type `OrderedDict[bool, str]` | | | Declared type - | info: `OrderedDict` is invariant in its first type parameter info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics error[invalid-assignment]: Object of type `ChainMap[str, bool]` is not assignable to `ChainMap[str, int]` - --> src/mdtest_snippet.py:31:13 + --> src/mdtest_snippet.py:31:34 | 31 | target: ChainMap[str, int] = source # snapshot | ------------------ ^^^^^^ Incompatible value of type `ChainMap[str, bool]` | | | Declared type - | info: `ChainMap` is invariant in its second type parameter info: Consider using the supertype `collections.abc.Mapping`, which is covariant in its value type info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics error[invalid-assignment]: Object of type `ChainMap[bool, str]` is not assignable to `ChainMap[int, str]` - --> src/mdtest_snippet.py:34:13 + --> src/mdtest_snippet.py:34:34 | 34 | target: ChainMap[int, str] = source # snapshot | ------------------ ^^^^^^ Incompatible value of type `ChainMap[bool, str]` | | | Declared type - | info: `ChainMap` is invariant in its first type parameter info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics error[invalid-assignment]: Object of type `deque[bool]` is not assignable to `deque[int]` - --> src/mdtest_snippet.py:37:13 + --> src/mdtest_snippet.py:37:26 | 37 | target: deque[int] = source # snapshot | ---------- ^^^^^^ Incompatible value of type `deque[bool]` | | | Declared type - | info: `deque` is invariant in its type parameter info: Consider using the covariant supertype `collections.abc.Sequence` info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics error[invalid-assignment]: Object of type `Counter[bool]` is not assignable to `Counter[int]` - --> src/mdtest_snippet.py:40:13 + --> src/mdtest_snippet.py:40:28 | 40 | target: Counter[int] = source # snapshot | ------------ ^^^^^^ Incompatible value of type `Counter[bool]` | | | Declared type - | info: `Counter` is invariant in its type parameter info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics error[invalid-assignment]: Object of type `MutableSequence[bool]` is not assignable to `MutableSequence[int]` - --> src/mdtest_snippet.py:43:13 + --> src/mdtest_snippet.py:43:36 | 43 | target: MutableSequence[int] = source # snapshot | -------------------- ^^^^^^ Incompatible value of type `MutableSequence[bool]` | | | Declared type - | info: `MutableSequence` is invariant in its type parameter info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics ``` @@ -1497,13 +1703,12 @@ def _(source: MyContainer[bool]): ```snapshot error[invalid-assignment]: Object of type `MyContainer[bool]` is not assignable to `MyContainer[int]` - --> src/mdtest_snippet.py:52:13 + --> src/mdtest_snippet.py:52:32 | 52 | target: MyContainer[int] = source # snapshot | ---------------- ^^^^^^ Incompatible value of type `MyContainer[bool]` | | | Declared type - | info: `MyContainer` is invariant in its type parameter info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics ``` @@ -1517,13 +1722,12 @@ def _(source: list[int]): ```snapshot error[invalid-assignment]: Object of type `list[int]` is not assignable to `list[str]` - --> src/mdtest_snippet.py:54:13 + --> src/mdtest_snippet.py:54:25 | 54 | target: list[str] = source # snapshot | --------- ^^^^^^ Incompatible value of type `list[int]` | | | Declared type - | ``` We do not emit any error if the collection types are covariant: @@ -1552,13 +1756,12 @@ def f() -> tuple[int, str]: ```snapshot error[invalid-return-type]: Return type does not match returned value - --> src/mdtest_snippet.py:1:12 + --> src/mdtest_snippet.py:2:12 | 1 | def f() -> tuple[int, str]: | --------------- Expected `tuple[int, str]` because of return type 2 | return 1, b"" # snapshot: invalid-return-type | ^^^^^^ expected `tuple[int, str]`, found `tuple[Literal[1], Literal[b""]]` - | info: the second tuple element is not compatible: `Literal[b""]` is not assignable to `str` ``` @@ -1578,7 +1781,6 @@ error[invalid-assignment]: Object of type `tuple[Literal[1], Literal[b""]]` is n | 5 | c.x = (1, b"") # snapshot | ^^^ - | info: the second tuple element is not compatible: `Literal[b""]` is not assignable to `str` ``` @@ -1593,13 +1795,12 @@ def f() -> Generator[tuple[int, str], None, None]: ```snapshot error[invalid-yield]: Yield expression type does not match annotation - --> src/mdtest_snippet.py:3:12 + --> src/mdtest_snippet.py:4:11 | 3 | def f() -> Generator[tuple[int, str], None, None]: | -------------------------------------- Function annotated with yield type `tuple[int, str]` here 4 | yield (1, b"") # snapshot: invalid-yield | ^^^^^^^^ expression of type `tuple[Literal[1], Literal[b""]]`, expected `tuple[int, str]` - | info: the second tuple element is not compatible: `Literal[b""]` is not assignable to `str` ``` @@ -1627,7 +1828,6 @@ error[not-iterable]: Object of type `WrongIterable` is not iterable | 12 | for _ in WrongIterable(): | ^^^^^^^^^^^^^^^ - | info: Its `__iter__` method returns an object of type `WrongIterator`, which has an invalid `__next__` method info: type `WrongIterable` is not assignable to protocol `Iterable[Unknown]` info: └── protocol member `__iter__` is incompatible @@ -1635,5 +1835,6 @@ info: └── incompatible return types: `WrongIterator` is not assignable info: └── type `WrongIterator` is not assignable to protocol `Iterator[Unknown]` info: └── protocol member `__next__` is incompatible info: └── unexpected extra parameter `wrong` +help: Parameter `wrong` must have a default value info: Expected signature for `__next__` is `def __next__(self): ...` ``` diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md index 5a4c780461..12621eda18 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md @@ -18,13 +18,11 @@ error[invalid-argument-type]: Argument to function `foo` is incorrect | 4 | foo("hello") # snapshot: 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 - | ``` ## Different source order @@ -45,13 +43,11 @@ error[invalid-argument-type]: Argument to function `foo` is incorrect | 2 | foo("hello") # snapshot: invalid-argument-type | ^^^^^^^ Expected `int`, found `Literal["hello"]` - | info: Function defined here --> src/mdtest_snippet.py:4:5 | 4 | def foo(x: int) -> int: | ^^^ ------ Parameter declared here - | ``` ## Different files @@ -78,13 +74,11 @@ error[invalid-argument-type]: Argument to function `foo` is incorrect | 3 | package.foo("hello") # snapshot: 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 - | ``` ## Many parameters @@ -104,13 +98,11 @@ error[invalid-argument-type]: Argument to function `foo` is incorrect | 4 | foo(1, "hello", 3) # snapshot: 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 - | ``` ## Many parameters across multiple lines @@ -135,7 +127,6 @@ error[invalid-argument-type]: Argument to function `foo` is incorrect | 8 | foo(1, "hello", 3) # snapshot: invalid-argument-type | ^^^^^^^ Expected `int`, found `Literal["hello"]` - | info: Function defined here --> src/mdtest_snippet.py:1:5 | @@ -144,7 +135,6 @@ info: Function defined here 2 | x: int, 3 | y: int, | ------ Parameter declared here - | ``` ## Many parameters with multiple invalid arguments @@ -172,13 +162,11 @@ error[invalid-argument-type]: Argument to function `foo` is incorrect | 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 - | error[invalid-argument-type]: Argument to function `foo` is incorrect @@ -186,13 +174,11 @@ error[invalid-argument-type]: Argument to function `foo` is incorrect | 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 - | error[invalid-argument-type]: Argument to function `foo` is incorrect @@ -200,13 +186,11 @@ error[invalid-argument-type]: Argument to function `foo` is incorrect | 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 - | ``` ## Test calling a function whose type is vendored from `typeshed` @@ -226,7 +210,6 @@ error[invalid-argument-type]: Argument to function `loads` is incorrect | 3 | json.loads(5) # snapshot: invalid-argument-type | ^ Expected `str | bytes | bytearray`, found `Literal[5]` - | info: Function defined here --> stdlib/json/__init__.byi:320:9 | @@ -234,7 +217,6 @@ info: Function defined here | ^^^^^ 321 | s: str | bytes | bytearray, | -------------------------- Parameter declared here - | ``` ## Tests for a variety of argument types @@ -259,13 +241,11 @@ error[invalid-argument-type]: Argument to function `foo` is incorrect | 4 | foo(1, "hello", 3) # snapshot: 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 - | ``` ### Variadic arguments @@ -285,13 +265,11 @@ error[invalid-argument-type]: Argument to function `foo` is incorrect | 4 | foo(1, 2, 3, "hello", 5) # snapshot: 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 - | ``` ### Keyword only arguments @@ -311,13 +289,11 @@ error[invalid-argument-type]: Argument to function `foo` is incorrect | 4 | foo(1, 2, z="hello") # snapshot: 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 - | ``` ### One keyword argument @@ -337,13 +313,11 @@ error[invalid-argument-type]: Argument to function `foo` is incorrect | 4 | foo(1, 2, "hello") # snapshot: 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 - | ``` ### Variadic keyword arguments @@ -361,13 +335,11 @@ error[invalid-argument-type]: Argument to function `foo` is incorrect | 4 | foo(a=1, b=2, c=3, d="hello", e=5) # snapshot: 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 - | ``` ### Mix of arguments @@ -387,13 +359,11 @@ error[invalid-argument-type]: Argument to function `foo` is incorrect | 4 | foo(1, 2, z="hello") # snapshot: 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 - | ``` ### Synthetic arguments @@ -415,13 +385,11 @@ error[invalid-argument-type]: Argument to bound method `C.__call__` is incorrect | 6 | c("wrong") # snapshot: invalid-argument-type | ^^^^^^^ Expected `int`, found `Literal["wrong"]` - | info: Method defined here --> src/mdtest_snippet.py:2:9 | 2 | def __call__(self, x: int) -> int: | ^^^^^^^^ ------ Parameter declared here - | ``` ## Calls to methods @@ -443,13 +411,11 @@ error[invalid-argument-type]: Argument to bound method `C.square` is incorrect | 6 | c.square("hello") # snapshot: invalid-argument-type | ^^^^^^^ Expected `int`, found `Literal["hello"]` - | info: Method defined here --> src/mdtest_snippet.py:2:9 | 2 | def square(self, x: int) -> int: | ^^^^^^ ------ Parameter declared here - | ``` ## Calls to protocol methods @@ -470,13 +436,11 @@ error[invalid-argument-type]: Argument to bound method `P.method` is incorrect | 7 | p.method("bad") # snapshot: invalid-argument-type | ^^^^^ Expected `int`, found `Literal["bad"]` - | info: Method defined here --> src/mdtest_snippet.py:4:9 | 4 | def method(self, value: int) -> None: ... | ^^^^^^ ---------- Parameter declared here - | ``` ## Calls to overloaded protocol methods @@ -500,13 +464,11 @@ error[invalid-argument-type]: Argument to bound method `P.method` is incorrect | 10 | p.method("bad") # snapshot: invalid-argument-type | ^^^^^ Expected `int`, found `Literal["bad"]` - | info: Matching overload defined here --> src/mdtest_snippet.py:5:9 | 5 | def method(self, value: int) -> None: ... | ^^^^^^ ---------- Parameter declared here - | info: Non-matching overloads for bound method `method`: info: (self, /, value: int, extra: int) -> None ``` @@ -537,13 +499,11 @@ error[invalid-argument-type]: Argument to function `needs_a_foo` is incorrect | 5 | needs_a_foo(Foo()) # snapshot: invalid-argument-type | ^^^^^ Expected `module.Foo`, found `main.Foo` - | info: Function defined here --> src/module.py:3:5 | 3 | def needs_a_foo(x: Foo): ... | ^^^^^^^^^^^ ------ Parameter declared here - | ``` ## TypeVars with bounds that have the same name but are from different files @@ -581,13 +541,11 @@ error[invalid-argument-type]: Argument to function `needs_a_foo` is incorrect | 6 | needs_a_foo(x) # snapshot: invalid-argument-type | ^ Expected `Foo`, found `T@f` - | info: Function defined here --> src/module.py:3:5 | 3 | def needs_a_foo(x: Foo): ... | ^^^^^^^^^^^ ------ Parameter declared here - | ``` ## Numbers special case @@ -609,13 +567,11 @@ error[invalid-argument-type]: Argument to function `f` is incorrect | 5 | f(5) # snapshot: invalid-argument-type | ^ Expected `Number`, found `Literal[5]` - | info: Function defined here --> src/mdtest_snippet.py:3:5 | 3 | def f(x: Number): ... | ^ --------- Parameter declared here - | info: Types from the `numbers` module aren't supported for static type checking help: Consider using a protocol instead, such as `typing.SupportsFloat` @@ -625,14 +581,12 @@ error[invalid-argument-type]: Argument to function `f` is incorrect | 8 | f(x) # snapshot: invalid-argument-type | ^ Expected `Number`, found `int | float` - | info: element `int` of union `int | float` is not assignable to `Number` info: Function defined here --> src/mdtest_snippet.py:3:5 | 3 | def f(x: Number): ... | ^ --------- Parameter declared here - | info: Types from the `numbers` module aren't supported for static type checking help: Consider using a protocol instead, such as `typing.SupportsFloat` ``` @@ -656,13 +610,11 @@ error[invalid-argument-type]: Argument to function `modify` is incorrect | 5 | modify(xs) # snapshot: invalid-argument-type | ^^ Expected `list[int]`, found `list[bool]` - | info: Function defined here --> src/mdtest_snippet.py:1:5 | 1 | def modify(xs: list[int]): | ^^^^^^ ------------- Parameter declared here - | info: `list` is invariant in its type parameter info: Consider using the covariant supertype `collections.abc.Sequence` info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_syntactic_variants.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_syntactic_variants.md index 6e0d318842..ee3be82f45 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_syntactic_variants.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_syntactic_variants.md @@ -13,13 +13,12 @@ Here, we point to the type annotation directly: ```snapshot error[invalid-assignment]: Object of type `Literal["three"]` is not assignable to `int` - --> src/mdtest_snippet.py:1:4 + --> src/mdtest_snippet.py:1:10 | 1 | x: int = "three" # snapshot: invalid-assignment | --- ^^^^^^^ Incompatible value of type `Literal["three"]` | | | Declared type - | ``` ## Unannotated assignment @@ -34,13 +33,12 @@ type in an annotation on the variable name: ```snapshot error[invalid-assignment]: Object of type `Literal["three"]` is not assignable to `int` - --> src/mdtest_snippet.py:2:1 + --> src/mdtest_snippet.py:2:5 | 2 | x = "three" # snapshot: invalid-assignment | - ^^^^^^^ Incompatible value of type `Literal["three"]` | | | Declared type `int` - | ``` ## Named expression @@ -55,13 +53,12 @@ Similar here, we could ideally point to the type annotation: ```snapshot error[invalid-assignment]: Object of type `Literal["three"]` is not assignable to `int` - --> src/mdtest_snippet.py:3:2 + --> src/mdtest_snippet.py:3:7 | 3 | (x := "three") # snapshot: invalid-assignment | - ^^^^^^^ Incompatible value of type `Literal["three"]` | | | Declared type `int` - | ``` ## Multiline expressions @@ -79,7 +76,7 @@ x: str = ( ```snapshot error[invalid-assignment]: Object of type `Literal[15]` is not assignable to `str` - --> src/mdtest_snippet.py:4:4 + --> src/mdtest_snippet.py:4:10 | 4 | x: str = ( | ____---___^ @@ -90,7 +87,6 @@ error[invalid-assignment]: Object of type `Literal[15]` is not assignable to `st 7 | | ) 8 | | ) | |_^ Incompatible value of type `Literal[15]` - | ``` ## Multiple targets @@ -109,23 +105,21 @@ tuple: ```snapshot error[invalid-assignment]: Object of type `Literal["a"]` is not assignable to `int` - --> src/mdtest_snippet.py:4:1 + --> src/mdtest_snippet.py:4:8 | 4 | x, y = ("a", "b") # snapshot: invalid-assignment | - ^^^^^^^^^^ Incompatible value of type `Literal["a"]` | | | Declared type `int` - | error[invalid-assignment]: Object of type `Literal[0]` is not assignable to `str` - --> src/mdtest_snippet.py:6:4 + --> src/mdtest_snippet.py:6:8 | 6 | x, y = (0, 0) # snapshot: invalid-assignment | - ^^^^^^ Incompatible value of type `Literal[0]` | | | Declared type `str` - | ``` ## Shadowing of classes and functions diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md index 843073b5b7..afd1d818b7 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md @@ -2,26 +2,40 @@ The full tests for these features are in `generics/legacy/variables.md`. - - ## Must have a name ```py from typing import TypeVar -# error: [invalid-legacy-type-variable] +# snapshot: invalid-legacy-type-variable T = TypeVar() ``` +```snapshot +error[invalid-legacy-type-variable]: The `name` parameter of `TypeVar` is required. + --> src/mdtest_snippet.py:4:5 + | +4 | T = TypeVar() + | ^^^^^^^^^ +``` + ## Name can't be given more than once ```py from typing import TypeVar -# error: [invalid-legacy-type-variable] +# snapshot: invalid-legacy-type-variable T = TypeVar("T", name="T") ``` +```snapshot +error[invalid-legacy-type-variable]: The `name` parameter of `TypeVar` can only be provided once. + --> src/mdtest_snippet.py:4:18 + | +4 | T = TypeVar("T", name="T") + | ^^^^^^^^ +``` + ## Must be directly assigned to a variable > A `TypeVar()` expression must always directly be assigned to a variable (it should not be used as @@ -31,13 +45,28 @@ T = TypeVar("T", name="T") from typing import TypeVar T = TypeVar("T") -# error: [invalid-legacy-type-variable] +# snapshot: invalid-legacy-type-variable U: TypeVar = TypeVar("U") -# error: [invalid-legacy-type-variable] +# snapshot: invalid-legacy-type-variable tuple_with_typevar = ("foo", TypeVar("W")) ``` +```snapshot +error[invalid-legacy-type-variable]: A `TypeVar` definition must be a simple variable assignment + --> src/mdtest_snippet.py:5:14 + | +5 | U: TypeVar = TypeVar("U") + | ^^^^^^^^^^^^ + + +error[invalid-legacy-type-variable]: A `TypeVar` definition must be a simple variable assignment + --> src/mdtest_snippet.py:8:30 + | +8 | tuple_with_typevar = ("foo", 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. @@ -45,10 +74,18 @@ tuple_with_typevar = ("foo", TypeVar("W")) ```py from typing import TypeVar -# error: [mismatched-type-name] +# snapshot: mismatched-type-name T = TypeVar("Q") ``` +```snapshot +warning[mismatched-type-name]: The name passed to `TypeVar` must match the variable it is assigned to + --> src/mdtest_snippet.py:4:13 + | +4 | T = TypeVar("Q") + | ^^^ Expected "T", got "Q" +``` + ## Must not be redefined ```py @@ -56,10 +93,22 @@ from typing import TypeVar T = TypeVar("T") -# error: [invalid-legacy-type-variable] +# snapshot: invalid-legacy-type-variable T = TypeVar("T") ``` +```snapshot +error[invalid-legacy-type-variable]: Cannot redefine `T` as a type variable + --> src/mdtest_snippet.py:6:1 + | +3 | T = TypeVar("T") + | - Previously defined here +4 | +5 | # snapshot: invalid-legacy-type-variable +6 | T = TypeVar("T") + | ^ +``` + ## No variadic arguments ```py @@ -67,63 +116,26 @@ from typing import TypeVar types = (int, str) -# error: [invalid-legacy-type-variable] +# snapshot: invalid-legacy-type-variable T = TypeVar("T", *types) -# error: [invalid-legacy-type-variable] +# snapshot: invalid-legacy-type-variable S = TypeVar("S", **{"bound": int}) ``` -## 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. +```snapshot +error[invalid-legacy-type-variable]: Starred arguments are not supported in `TypeVar` creation + --> src/mdtest_snippet.py:6:18 + | +6 | T = TypeVar("T", *types) + | ^^^^^^ -```py -from typing import TypeVar -# error: [invalid-legacy-type-variable] -T = TypeVar("T", int) -``` - -## Cannot have both bound and constraint - -```py -from typing import TypeVar - -# error: [invalid-legacy-type-variable] -T = TypeVar("T", int, str, bound=bytes) -``` - -## 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) -``` - -## Boolean parameters must be unambiguous - -```py -from typing_extensions 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()) - -# error: [invalid-legacy-type-variable] -V = TypeVar("V", infer_variance=cond()) +error[invalid-legacy-type-variable]: Starred arguments are not supported in `TypeVar` creation + --> src/mdtest_snippet.py:9:18 + | +9 | S = TypeVar("S", **{"bound": int}) + | ^^^^^^^^^^^^^^^^ ``` ## Invalid keyword arguments @@ -131,10 +143,18 @@ V = TypeVar("V", infer_variance=cond()) ```py from typing import TypeVar -# error: [invalid-legacy-type-variable] +# snapshot: invalid-legacy-type-variable T = TypeVar("T", invalid_keyword=True) ``` +```snapshot +error[invalid-legacy-type-variable]: Unknown keyword argument `invalid_keyword` in `TypeVar` creation + --> src/mdtest_snippet.py:4:18 + | +4 | T = TypeVar("T", invalid_keyword=True) + | ^^^^^^^^^^^^^^^^^^^^ +``` + ## Invalid feature for this Python version ```toml @@ -145,6 +165,14 @@ python-version = "3.10" ```py from typing import TypeVar -# error: [invalid-legacy-type-variable] +# snapshot: invalid-legacy-type-variable T = TypeVar("T", default=int) ``` + +```snapshot +error[invalid-legacy-type-variable]: The `default` parameter of `typing.TypeVar` was added in Python 3.13 + --> src/mdtest_snippet.py:4:18 + | +4 | T = TypeVar("T", default=int) + | ^^^^^^^^^^^ +``` diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/missing_argument.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/missing_argument.md index 1d48b9c384..026c08beae 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/missing_argument.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/missing_argument.md @@ -48,13 +48,11 @@ error[missing-argument]: No argument provided for required parameter `a` of func | 3 | f() # snapshot | ^^^ - | info: Parameter declared here --> src/module.py:1:7 | 1 | def f(a, b=42): ... | ^ - | error[missing-argument]: No argument provided for required parameter `a` of function `f` @@ -62,7 +60,6 @@ error[missing-argument]: No argument provided for required parameter `a` of func | 12 | h(b=56) | ^^^^^^^ - | info: Union variant `def f(a, b: some int = 42)` is incompatible with this call site info: Attempted to call union type `(def f(a, b: some int = 42)) | (def g(a, b))` @@ -72,7 +69,6 @@ error[missing-argument]: No argument provided for required parameter `a` of func | 12 | h(b=56) | ^^^^^^^ - | info: Union variant `def g(a, b)` is incompatible with this call site info: Attempted to call union type `(def f(a, b: some int = 42)) | (def g(a, b))` @@ -82,13 +78,11 @@ error[missing-argument]: No argument provided for required parameter `a` of boun | 14 | Foo().method() # snapshot: missing-argument | ^^^^^^^^^^^^^^ - | info: Parameter declared here --> src/module.py:5:22 | 5 | def method(self, a): ... | ^ - | error[missing-argument]: No argument provided for required parameter `value` of bound method `P.method` @@ -96,11 +90,9 @@ error[missing-argument]: No argument provided for required parameter `value` of | 22 | p.method() # snapshot: missing-argument | ^^^^^^^^^^ - | info: Parameter declared here --> src/main.py:19:22 | 19 | def method(self, value: int) -> None: ... | ^^^^^^^^^^ - | ``` 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 index eb0a8927fc..c8a23e3150 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md @@ -27,7 +27,6 @@ error[invalid-syntax]: cannot use an asynchronous comprehension inside of a sync | 6 | 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: @@ -44,7 +43,6 @@ error[not-iterable]: Object of type `range` is not async-iterable | 9 | return [[x async for x in elements(n)] async for n in range(3)] | ^^^^^^^^ - | info: It has no `__aiter__` method ``` @@ -267,6 +265,12 @@ def returns_list() -> list[int]: # error: [invalid-syntax] "assignment expression cannot be used in a comprehension iterable expression" [x for x in (z := returns_list()).copy()] +def invalid_later_iterable(): + # error: [invalid-syntax] "assignment expression cannot be used in a comprehension iterable expression" + [item for item in [0] for _ in (escaped := [1])] + # error: [unresolved-reference] + reveal_type(escaped) # revealed: Unknown + # error: [invalid-syntax] "assignment expression cannot be used in a comprehension iterable expression" # error: [invalid-syntax] "assignment expression cannot rebind comprehension variable" [a for a in [(b := 1) for b in [1]]] @@ -495,7 +499,6 @@ error[invalid-syntax]: `break` outside loop | 1 | break # snapshot: invalid-syntax | ^^^^^ - | error[invalid-syntax]: `continue` outside loop @@ -503,7 +506,6 @@ error[invalid-syntax]: `continue` outside loop | 2 | continue # snapshot: invalid-syntax | ^^^^^^^^ - | error[invalid-syntax]: `break` outside loop @@ -511,7 +513,6 @@ error[invalid-syntax]: `break` outside loop | 9 | break # snapshot: invalid-syntax | ^^^^^ - | error[invalid-syntax]: `continue` outside loop @@ -519,7 +520,6 @@ error[invalid-syntax]: `continue` outside loop | 10 | continue # snapshot: invalid-syntax | ^^^^^^^^ - | error[invalid-syntax]: `break` outside loop @@ -527,7 +527,6 @@ error[invalid-syntax]: `break` outside loop | 14 | break # snapshot: invalid-syntax | ^^^^^ - | error[invalid-syntax]: `continue` outside loop @@ -535,7 +534,6 @@ error[invalid-syntax]: `continue` outside loop | 15 | continue # snapshot: invalid-syntax | ^^^^^^^^ - | ``` ## name cannot refer to a parameter and a global variable @@ -580,7 +578,6 @@ error[invalid-syntax]: name `a` cannot refer to a parameter and a global variabl | 4 | global a # snapshot: invalid-syntax | ^ - | error[invalid-syntax]: name `a` cannot refer to a parameter and a global variable @@ -588,7 +585,6 @@ error[invalid-syntax]: name `a` cannot refer to a parameter and a global variabl | 8 | global a # snapshot: invalid-syntax | ^ - | error[invalid-syntax]: name `a` cannot refer to a parameter and a global variable @@ -596,7 +592,6 @@ error[invalid-syntax]: name `a` cannot refer to a parameter and a global variabl | 16 | global a # snapshot: invalid-syntax | ^ - | error[invalid-syntax]: name `a` cannot refer to a parameter and a global variable @@ -604,7 +599,6 @@ error[invalid-syntax]: name `a` cannot refer to a parameter and a global variabl | 22 | global a # snapshot: invalid-syntax | ^ - | error[invalid-syntax]: name `a` cannot refer to a parameter and a global variable @@ -612,5 +606,4 @@ error[invalid-syntax]: name `a` cannot refer to a parameter and a global variabl | 27 | global a # snapshot: invalid-syntax | ^ - | ``` diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/shadowing.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/shadowing.md index f6fa7d78a1..b8d2139af2 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/shadowing.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/shadowing.md @@ -13,13 +13,12 @@ C = 1 # snapshot: invalid-assignment ```snapshot error[invalid-assignment]: Object of type `Literal[1]` is not assignable to `` - --> src/mdtest_snippet.py:3:1 + --> src/mdtest_snippet.py:3:5 | 3 | C = 1 # snapshot: invalid-assignment | - ^ Incompatible value of type `Literal[1]` | | | Declared type `` - | info: Implicit shadowing of class `C`. Add an annotation to make it explicit if this is intentional ``` @@ -33,13 +32,12 @@ f = 1 # snapshot: invalid-assignment ```snapshot error[invalid-assignment]: Object of type `Literal[1]` is not assignable to `def f()` - --> src/mdtest_snippet.py:3:1 + --> src/mdtest_snippet.py:3:5 | 3 | f = 1 # snapshot: invalid-assignment | - ^ Incompatible value of type `Literal[1]` | | | Declared type `def f()` - | info: Implicit shadowing of function `f`. Add an annotation to make it explicit if this is intentional ``` @@ -58,5 +56,4 @@ error[invalid-assignment]: Object of type `` is not assignable to a | 4 | config.optionxform = str # snapshot: invalid-assignment | ^^^^^^^^^^^^^^^^^^ - | ``` diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/too_many_positionals.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/too_many_positionals.md index 6aa37c5b9e..2bce8650f8 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/too_many_positionals.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/too_many_positionals.md @@ -40,13 +40,11 @@ error[too-many-positional-arguments]: Too many positional arguments to function | 3 | f(1, 2, 3) # snapshot: too-many-positional-arguments | ^ - | info: Function signature here --> src/module.py:1:5 | 1 | def f(a, b=42): ... | ^^^^^^^^^^ - | error[too-many-positional-arguments]: Too many positional arguments to function `f`: expected 2, got 3 @@ -54,7 +52,6 @@ error[too-many-positional-arguments]: Too many positional arguments to function | 12 | h(1, 2, 3) | ^ - | info: Union variant `def f(a, b: some int = 42)` is incompatible with this call site info: Attempted to call union type `(def f(a, b: some int = 42)) | (def g(a, b))` @@ -64,7 +61,6 @@ error[too-many-positional-arguments]: Too many positional arguments to function | 12 | h(1, 2, 3) | ^ - | info: Union variant `def g(a, b)` is incompatible with this call site info: Attempted to call union type `(def f(a, b: some int = 42)) | (def g(a, b))` @@ -74,11 +70,9 @@ error[too-many-positional-arguments]: Too many positional arguments to bound met | 14 | Foo().method(1, 2) # snapshot: too-many-positional-arguments | ^ - | info: Method signature here --> src/module.py:5:9 | 5 | def method(self, a): ... | ^^^^^^^^^^^^^^^ - | ``` diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/unpacking.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/unpacking.md index 205aafbd60..c616be1686 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/unpacking.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/unpacking.md @@ -12,7 +12,6 @@ error[not-iterable]: Object of type `Literal[1]` is not iterable | 1 | a, b = 1 # snapshot: not-iterable | ^ - | info: It doesn't have an `__iter__` method or a `__getitem__` method ``` @@ -30,7 +29,6 @@ error[invalid-assignment]: Too many values to unpack | ^^^^ --------- Got 3 | | | Expected 2 - | ``` ## Exactly too few values to unpack @@ -47,7 +45,6 @@ error[invalid-assignment]: Not enough values to unpack | ^^^^ ---- Got 1 | | | Expected 2 - | ``` ## Too few values to unpack @@ -64,5 +61,4 @@ error[invalid-assignment]: Not enough values to unpack | ^^^^^^^^^^^^^ ------ Got 2 | | | Expected at least 3 - | ``` diff --git a/crates/ty_python_semantic/resources/mdtest/directives/assert_never.md b/crates/ty_python_semantic/resources/mdtest/directives/assert_never.md index a7c63452ab..25398e0ce7 100644 --- a/crates/ty_python_semantic/resources/mdtest/directives/assert_never.md +++ b/crates/ty_python_semantic/resources/mdtest/directives/assert_never.md @@ -8,7 +8,7 @@ ```py from typing_extensions import assert_never, Never, Any -from ty_extensions import Unknown +from ty_extensions._internal import Unknown def _(never: Never): assert_never(never) # fine @@ -20,7 +20,7 @@ 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 +from ty_extensions._internal import Unknown def _(): assert_never(0) # snapshot: type-assertion-failure @@ -34,7 +34,6 @@ error[type-assertion-failure]: Argument does not have asserted type `Never` | ^^^^^^^^^^^^^-^ | | | Inferred type of argument is `Literal[0]` - | info: `Never` and `Literal[0]` are not equivalent types ``` @@ -51,7 +50,6 @@ error[type-assertion-failure]: Argument does not have asserted type `Never` | ^^^^^^^^^^^^^--^ | | | Inferred type of argument is `Literal[""]` - | info: `Never` and `Literal[""]` are not equivalent types ``` @@ -68,7 +66,6 @@ error[type-assertion-failure]: Argument does not have asserted type `Never` | ^^^^^^^^^^^^^----^ | | | Inferred type of argument is `None` - | info: `Never` and `None` are not equivalent types ``` @@ -85,7 +82,6 @@ error[type-assertion-failure]: Argument does not have asserted type `Never` | ^^^^^^^^^^^^^--^ | | | Inferred type of argument is `tuple[()]` - | info: `Never` and `tuple[()]` are not equivalent types ``` @@ -102,7 +98,6 @@ error[type-assertion-failure]: Argument does not have asserted type `Never` | ^^^^^^^^^^^^^--------------------^ | | | Inferred type of argument is `Literal[1]` - | info: `Never` and `Literal[1]` are not equivalent types ``` @@ -119,7 +114,6 @@ error[type-assertion-failure]: Argument does not have asserted type `Never` | ^^^^^^^^^^^^^----^ | | | Inferred type of argument is `Any` - | info: `Never` and `Any` are not equivalent types ``` @@ -136,7 +130,6 @@ error[type-assertion-failure]: Argument does not have asserted type `Never` | ^^^^^^^^^^^^^-------^ | | | Inferred type of argument is `Unknown` - | info: `Never` and `Unknown` are not equivalent types ``` diff --git a/crates/ty_python_semantic/resources/mdtest/directives/assert_type.md b/crates/ty_python_semantic/resources/mdtest/directives/assert_type.md index 2b43c89685..88ab9a76b2 100644 --- a/crates/ty_python_semantic/resources/mdtest/directives/assert_type.md +++ b/crates/ty_python_semantic/resources/mdtest/directives/assert_type.md @@ -19,7 +19,6 @@ error[type-assertion-failure]: Argument does not have asserted type `str` | ^^^^^^^^^^^^-^^^^^^ | | | Inferred type is `int` - | info: `str` and `int` are not equivalent types ``` @@ -38,7 +37,6 @@ error[type-assertion-failure]: Argument does not have asserted type `int` | ^^^^^^^^^^^^-^^^^^^ | | | Inferred type is `bool` - | info: `bool` is a subtype of `int`, but they are not equivalent ``` @@ -106,7 +104,6 @@ error[type-assertion-failure]: Argument does not have asserted type `Bar` | ^^^^^^^^^^^^-^^^^^^ | | | Inferred type is `Foo` - | info: `Bar` and `Foo` are not equivalent types ``` @@ -123,7 +120,6 @@ error[assert-type-unspellable-subtype]: Argument does not have asserted type `Ba | ^^^^^^^^^^^^-^^^^^^ | | | Inferred type is `Foo & Bar` - | info: `Foo & Bar` is a subtype of `Bar`, but they are not equivalent ``` @@ -141,7 +137,6 @@ error[type-assertion-failure]: Argument does not have asserted type `Baz` | ^^^^^^^^^^^^-^^^^^^ | | | Inferred type is `Foo & Bar` - | info: `Baz` and `Foo & Bar` are not equivalent types ``` @@ -168,7 +163,7 @@ def _(f: F): from typing import Any from typing_extensions import Literal, assert_type -from ty_extensions import Unknown +from ty_extensions._internal import Unknown # Any and Unknown are considered equivalent def _(a: Unknown, b: Any): @@ -193,7 +188,7 @@ Tuple types with the same elements are the same. ```py from typing_extensions import Any, assert_type -from ty_extensions import Unknown +from ty_extensions._internal import Unknown def _(a: tuple[int, str, bytes]): assert_type(a, tuple[int, str, bytes]) # fine diff --git a/crates/ty_python_semantic/resources/mdtest/directives/cast.md b/crates/ty_python_semantic/resources/mdtest/directives/cast.md index 3a1837f0b2..0311c79766 100644 --- a/crates/ty_python_semantic/resources/mdtest/directives/cast.md +++ b/crates/ty_python_semantic/resources/mdtest/directives/cast.md @@ -63,7 +63,7 @@ the gradual guarantee and leads to cascading errors when an object is inferred a `Unknown` due to a missing import or similar. ```py -from ty_extensions import Unknown +from ty_extensions._internal import Unknown def f(x: Any, y: Unknown, z: Any | str | int): a = cast(dict[str, Any], x) @@ -113,7 +113,6 @@ warning[redundant-cast]: Value is already of type `int` | 5 | cast(int, secrets.randbelow(10)) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove the redundant `cast` | 4 | # snapshot: redundant-cast @@ -134,7 +133,6 @@ warning[redundant-cast]: Value is already of type `int` | 7 | cast(val=secrets.randbelow(10), typ=int) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove the redundant `cast` | 6 | # snapshot: redundant-cast @@ -156,7 +154,6 @@ warning[redundant-cast]: Value is already of type `int` | 10 | return cast(int, x + y) * z | ^^^^^^^^^^^^^^^^ - | help: Remove the redundant `cast` | 9 | # snapshot: redundant-cast @@ -178,7 +175,6 @@ warning[redundant-cast]: Value is already of type `int` | 13 | return -cast(int, x + y) | ^^^^^^^^^^^^^^^^ - | help: Remove the redundant `cast` | 12 | # snapshot: redundant-cast @@ -200,7 +196,6 @@ warning[redundant-cast]: Value is already of type `int` | 16 | print(cast(int, x + y)) | ^^^^^^^^^^^^^^^^ - | help: Remove the redundant `cast` | 15 | # snapshot: redundant-cast diff --git a/crates/ty_python_semantic/resources/mdtest/directives/reveal_type.md b/crates/ty_python_semantic/resources/mdtest/directives/reveal_type.md index 44648443fe..83bc6b0181 100644 --- a/crates/ty_python_semantic/resources/mdtest/directives/reveal_type.md +++ b/crates/ty_python_semantic/resources/mdtest/directives/reveal_type.md @@ -38,6 +38,43 @@ fail at runtime: reveal_type(1) # revealed: Literal[1] ``` +## In type-checking blocks + +An unimported `reveal_type` cannot fail at runtime inside a `TYPE_CHECKING` block because that code +is never executed at runtime. + +Note that this test uses `# error: [revealed-type]` assertions instead of the more common +`# revealed` assertions that we use elsewhere for `reveal_type` calls. `# revealed` assertions +swallow `undefined-reveal` errors as well as asserting the revealed type, but +`# error: [revealed-type]` assertions do not also match `undefined-reveal`. This means that an +unexpected so an unexpected `undefined-reveal` warning would cause these tests to fail. + +```py +from typing import TYPE_CHECKING +import typing + +if TYPE_CHECKING: + reveal_type(1) # error: [revealed-type] "Literal[1]" + + def nested() -> None: + reveal_type("nested") # error: [revealed-type] "nested" + +if typing.TYPE_CHECKING: + reveal_type(True) # error: [revealed-type] "Literal[True]" +``` + +## In stub files + +An unimported `reveal_type` also cannot fail at runtime in a stub file because stub files are never +executed. + +As in the previous section, this test uses `# error: [revealed-type]` rather than `revealed:` +assertions to ensure that an unexpected `undefined-reveal` warning is not silently matched. + +```pyi +reveal_type(1) # error: [revealed-type] "Literal[1]" +``` + ## In unreachable code Make sure that `reveal_type` works even in unreachable code. diff --git a/crates/ty_python_semantic/resources/mdtest/enums.md b/crates/ty_python_semantic/resources/mdtest/enums.md index 173242ef1b..262eb0bd8c 100644 --- a/crates/ty_python_semantic/resources/mdtest/enums.md +++ b/crates/ty_python_semantic/resources/mdtest/enums.md @@ -375,7 +375,8 @@ class Color(Enum): PURPLE = [] # error: [invalid-assignment] ``` -When `_value_` is annotated, `.value` and `._value_` are inferred as the declared type: +When `_value_` is annotated, `.value` and `._value_` are inferred as the declared type on both enum +members and method receivers: ```py from enum import Enum @@ -386,6 +387,11 @@ class Color2(Enum): RED = 1 GREEN = 2 + def read_value(self) -> int: + reveal_type(self._value_) # revealed: int + reveal_type(self.value) # revealed: int + return self.value + reveal_type(Color2.RED.value) # revealed: int reveal_type(Color2.RED._value_) # revealed: int @@ -527,6 +533,8 @@ to `Any`: from enum import Enum class Connector(Enum): + connector_id: int + def __new__(cls, value: str, connector_id: int) -> "Connector": obj = object.__new__(cls) obj._value_ = value @@ -546,6 +554,7 @@ from enum import Enum class AnnotatedConnector(Enum): _value_: str + connector_id: int def __new__(cls, value: str, connector_id: int = 0) -> "AnnotatedConnector": obj = object.__new__(cls) @@ -661,6 +670,8 @@ annotation, subclass member values remain dynamic: from enum import Enum class Base(Enum): + connector_id: int + def __new__(cls, value: str, connector_id: int) -> "Base": obj = object.__new__(cls) obj._value_ = value @@ -680,6 +691,8 @@ An explicit `_value_` annotation on the subclass still takes precedence: from enum import Enum class Base(Enum): + connector_id: int + def __new__(cls, value: str, connector_id: int = 0) -> "Base": obj = object.__new__(cls) obj._value_ = value @@ -702,6 +715,8 @@ explicitly annotated: from enum import Enum class Base(Enum): + connector_id: int + def __new__(cls, value: int, connector_id: int = 0) -> "Base": obj = object.__new__(cls) obj._value_ = value @@ -1178,6 +1193,9 @@ class Choices(Enum): @enum_property def value(self) -> Any: ... + def read_value(self) -> Any: + reveal_type(self.value) # revealed: Any + return self.value reveal_type(Choices.A.value) # revealed: Any @@ -1189,6 +1207,10 @@ class BaseChoices(Enum): class InheritedChoices(BaseChoices): A = 1 + def read_value(self) -> str: + reveal_type(self.value) # revealed: str + return self.value + reveal_type(InheritedChoices.A.value) # revealed: str ``` @@ -1986,6 +2008,50 @@ reveal_type(Answer.name) # revealed: Literal[Answer.name] reveal_type(Answer.value) # revealed: Literal[Answer.value] ``` +## Enum classes as collection protocols + +An enum class is a container because `EnumMeta.__contains__` accepts any object. Consequently, the +class satisfies `Container[T]` for every `T`, including types unrelated to its members. Its +metaclass also provides the iteration, reversal, and length methods required by the corresponding +collection protocols. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from collections.abc import Collection, Container, Iterable, Reversible +from enum import Enum, IntEnum, StrEnum, auto +from typing import Any + +class Color(Enum): + RED = auto() + +# error: [missing-type-argument] +unparameterized_container: Container = Color +any_container: Container[Any] = Color +object_container: Container[object] = Color +member_container: Container[Color] = Color +integer_container: Container[int] = Color +string_container: Container[str] = Color +iterable: Iterable[Color] = Color +reversible: Reversible[Color] = Color +collection: Collection[Color] = Color + +class Number(IntEnum): + ONE = 1 + +integer_enum_container: Container[int] = Number +integer_enum_iterable: Iterable[int] = Number + +class Word(StrEnum): + HELLO = "hello" + +string_enum_container: Container[str] = Word +string_enum_iterable: Iterable[str] = Word +``` + ## Iterating over enum members ```py @@ -2154,6 +2220,106 @@ def _(answer: Answer): reveal_type(answer.value) # revealed: Literal["yes", "no"] ``` +### Special attributes on method receivers + +Implicit receivers and receivers annotated with `Self` retain the special attributes of their enum +bound. Their `Self` type preserves the particular member at call sites. + +```toml +[environment] +python-version = "3.11" + +[rules] +unsound-return-statement = "error" +``` + +```py +from enum import Enum +from typing import Self + +class Answer(Enum): + YES = 1 + NO = 2 + + def implicit(self) -> int: + reveal_type(self) # revealed: Self@implicit + reveal_type(self.name) # revealed: Literal["YES", "NO"] + reveal_type(self._name_) # revealed: Literal["YES", "NO"] + reveal_type(self.value) # revealed: Literal[1, 2] + reveal_type(self._value_) # revealed: Literal[1, 2] + return self.value + + def explicit(self: Self) -> int: + reveal_type(self.value) # revealed: Literal[1, 2] + return self.value + + def concrete(self: "Answer") -> int: + reveal_type(self.value) # revealed: Literal[1, 2] + return self.value + + def identity(self) -> Self: + return self + +reveal_type(Answer.YES.identity()) # revealed: Literal[Answer.YES] +``` + +### Special attributes on bounded type variables + +An ordinary type variable bounded by an enum has the same special attributes as the enum itself. + +```toml +[rules] +unsound-return-statement = "error" +``` + +```py +from enum import Enum +from typing import TypeVar + +class Answer(Enum): + YES = 1 + NO = 2 + +AnswerT = TypeVar("AnswerT", bound=Answer) + +def value(answer: AnswerT) -> int: + reveal_type(answer.name) # revealed: Literal["YES", "NO"] + reveal_type(answer._name_) # revealed: Literal["YES", "NO"] + reveal_type(answer.value) # revealed: Literal[1, 2] + reveal_type(answer._value_) # revealed: Literal[1, 2] + return answer.value +``` + +### Special attributes on constrained type variables + +When a type variable can be one of several enum types, its special attributes include the values +from every possible enum. + +```toml +[rules] +unsound-return-statement = "error" +``` + +```py +from enum import Enum +from typing import TypeVar + +class Number(Enum): + ONE = 1 + TWO = 2 + +class Word(Enum): + LEFT = "left" + RIGHT = "right" + +EnumT = TypeVar("EnumT", Number, Word) + +def value(item: EnumT) -> int | str: + reveal_type(item.name) # revealed: Literal["ONE", "TWO", "LEFT", "RIGHT"] + reveal_type(item.value) # revealed: Literal[1, 2, "left", "right"] + return item.value +``` + ## Properties of enum types ### Implicitly final @@ -2443,7 +2609,6 @@ warning[mismatched-type-name]: The name passed to `Enum` must match the variable | 8 | Mismatch = Enum("WrongName", "A B") | ^^^^^^^^^^^ Expected "Mismatch", got "WrongName" - | ``` If the name is not a string literal, we also emit a diagnostic: @@ -2460,7 +2625,6 @@ warning[mismatched-type-name]: The name passed to `Enum` must match the variable | 11 | DynamicMismatch = Enum(name, "A B") | ^^^^ Expected "DynamicMismatch", got variable of type `str` - | ``` ### List/tuple of tuples @@ -3675,13 +3839,13 @@ dynamic construction of enums using the functional syntax: from enum import Enum, IntEnum, StrEnum from ty_extensions._internal import into_regular_callable -# revealed: Overload[[EnumMemberT](value: Any, names: None = None) -> EnumMemberT, (value: str, names: EnumNames, *, module: str | None = None, qualname: str | None = None, type: type | None = None, start: int = 1, boundary: FlagBoundary | None = None) -> type[Enum]] +# revealed: Overload[(value: Any, names: None = None) -> Enum, (value: str, names: EnumNames, *, module: str | None = None, qualname: str | None = None, type: type | None = None, start: int = 1, boundary: FlagBoundary | None = None) -> type[Enum]] reveal_type(into_regular_callable(Enum)) -# revealed: Overload[[EnumMemberT](value: Any, names: None = None) -> EnumMemberT, (value: str, names: EnumNames, *, module: str | None = None, qualname: str | None = None, type: type | None = None, start: int = 1, boundary: FlagBoundary | None = None) -> type[Enum]] +# revealed: Overload[(value: Any, names: None = None) -> IntEnum, (value: str, names: EnumNames, *, module: str | None = None, qualname: str | None = None, type: type | None = None, start: int = 1, boundary: FlagBoundary | None = None) -> type[Enum]] reveal_type(into_regular_callable(IntEnum)) -# revealed: Overload[[EnumMemberT](value: Any, names: None = None) -> EnumMemberT, (value: str, names: EnumNames, *, module: str | None = None, qualname: str | None = None, type: type | None = None, start: int = 1, boundary: FlagBoundary | None = None) -> type[Enum]] +# revealed: Overload[(value: Any, names: None = None) -> StrEnum, (value: str, names: EnumNames, *, module: str | None = None, qualname: str | None = None, type: type | None = None, start: int = 1, boundary: FlagBoundary | None = None) -> type[Enum]] reveal_type(into_regular_callable(StrEnum)) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/expression/attribute.md b/crates/ty_python_semantic/resources/mdtest/expression/attribute.md index 8c19bca58a..03da8f60df 100644 --- a/crates/ty_python_semantic/resources/mdtest/expression/attribute.md +++ b/crates/ty_python_semantic/resources/mdtest/expression/attribute.md @@ -47,3 +47,86 @@ def f() -> None: box = StrBox() reveal_type(box.attr) # revealed: str ``` + +## Local prefixes block enclosing whole-place bindings + +When a nested function binds the root name of a whole-place access, the local root binding takes +precedence over the root binding from the enclosing scope. + +```py +class IntBox: + attr: int + +class StrBox: + attr: str + +box = IntBox() + +def outer_root() -> None: + box.attr = 1 + + def inner() -> None: + box = StrBox() + reveal_type(box.attr) # revealed: str +``` + +Similarly, a nested rebinding of an intermediate member, rather than the root, takes precedence over +the enclosing binding of that same intermediate member. + +```py +class Holder: + box: IntBox | StrBox + +def outer_member() -> None: + holder = Holder() + holder.box = IntBox() + holder.box.attr = 1 + + def inner() -> None: + holder.box = StrBox() + reveal_type(holder.box.attr) # revealed: str +``` + +Under Python's function name-resolution rules, even a conditional assignment to the root name makes +the local root take precedence over the enclosing binding. When the condition is false, the unbound +local root cannot fall back to the binding in the enclosing scope. + +```py +def with_inner_conditional_root(flag: bool) -> None: + box = IntBox() + box.attr = 1 + + def inner() -> None: + if flag: + box = StrBox() + # error: [possibly-unresolved-reference] "Name `box` used when possibly not defined" + reveal_type(box.attr) # revealed: str +``` + +By contrast, binding an intermediate member does not affect resolution of the root, which still +comes from the enclosing scope. When the intermediate member is conditionally rebound, it can refer +to either object, so both member types remain visible in the whole-place access. + +```py +def with_inner_conditional_member(flag: bool) -> None: + holder = Holder() + holder.box = IntBox() + holder.box.attr = 1 + + def inner() -> None: + if flag: + holder.box = StrBox() + reveal_type(holder.box.attr) # revealed: int | str +``` + +If none of the prefixes are bound in the nested scope, the enclosing whole-place binding remains +visible. + +```py +def outer_fallback() -> None: + box = IntBox() + box.attr = 1 + + def inner() -> None: + reveal_type(box.attr) # revealed: int +``` diff --git a/crates/ty_python_semantic/resources/mdtest/expression/yield_and_yield_from.md b/crates/ty_python_semantic/resources/mdtest/expression/yield_and_yield_from.md index 930cd8b0b3..45091d4971 100644 --- a/crates/ty_python_semantic/resources/mdtest/expression/yield_and_yield_from.md +++ b/crates/ty_python_semantic/resources/mdtest/expression/yield_and_yield_from.md @@ -186,6 +186,59 @@ def iterator_yield_from() -> Generator[int, None, int]: return 1 ``` +## Generator type aliases + +ty "sees through" type aliases used as return annotations when inferring a generator's yield type. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import AsyncGenerator, Generator, Iterator + +type GeneratorAlias[T] = Generator[T] + +def invalid_yield() -> GeneratorAlias[int]: + yield "foo" # error: [invalid-yield] + +def invalid_return() -> GeneratorAlias[int]: + yield 42 + return "foo" # error: [invalid-return-type] + +type NestedGeneratorAlias[T] = GeneratorAlias[T] + +def invalid_nested_yield() -> NestedGeneratorAlias[int]: + yield "foo" # error: [invalid-yield] + +type IteratorAlias[T] = Iterator[T] + +def invalid_iterator_return() -> IteratorAlias[int]: + yield 42 + return "foo" # error: [invalid-return-type] + +type AsyncGeneratorAlias[T] = AsyncGenerator[T] + +async def invalid_async_yield() -> AsyncGeneratorAlias[int]: + yield "foo" # error: [invalid-yield] +``` + +The same applies when inferring a generator's return type and send type: + +```py +type FullGeneratorAlias[YieldT, SendT, ReturnT] = Generator[YieldT, SendT, ReturnT] + +def inner_aliased_generator() -> FullGeneratorAlias[int, bytes, str]: + sent = yield 42 + reveal_type(sent) # revealed: bytes + return "done" + +def outer_aliased_generator() -> FullGeneratorAlias[int, bytes, None]: + result = yield from inner_aliased_generator() + reveal_type(result) # revealed: str +``` + ## Error cases ### Non-iterable type @@ -209,14 +262,13 @@ def invalid_generator() -> Generator[int, None, None]: ```snapshot error[invalid-yield]: Yield expression type does not match annotation - --> src/mdtest_snippet.py:3:28 + --> src/mdtest_snippet.py:5:11 | 3 | def invalid_generator() -> Generator[int, None, None]: | -------------------------- Function annotated with yield type `int` here 4 | # snapshot: invalid-yield 5 | yield "" | ^^ expression of type `Literal[""]`, expected `int` - | ``` ### Invalid annotation @@ -277,14 +329,13 @@ def outer() -> Generator[int, str, None]: ```snapshot error[invalid-yield]: Send type does not match annotation - --> src/mdtest_snippet.py:6:16 + --> src/mdtest_snippet.py:8:16 | 6 | def outer() -> Generator[int, str, None]: | ------------------------- Function annotated with send type `str` here 7 | # snapshot: invalid-yield 8 | yield from inner() | ^^^^^^^ generator with send type `int`, expected `str` - | ``` ### Non generator function with `Generator` annotation @@ -301,14 +352,277 @@ reveal_type(non_gen) # revealed: def non_gen() -> Generator[int, int, None] ```snapshot error[invalid-return-type]: Return type does not match returned value - --> src/mdtest_snippet.py:3:18 + --> src/mdtest_snippet.py:5:12 | 3 | def non_gen() -> Generator[int, int, None]: | ------------------------- Expected `Generator[int, int, None]` because of return type 4 | # snapshot: invalid-return-type 5 | return 1 | ^ expected `Generator[int, int, None]`, found `Literal[1]` - | info: type `Literal[1]` is not assignable to protocol `Generator[int, int, None]` info: └── protocol member `__iter__` is not defined on type `Literal[1]` ``` + +## *Unsound* yield expressions + +In addition to `invalid-yield`, we also offer a disabled-by-default stricter rule `unsound-yield`. +This rule forbids `yield` expressions that yield an instance of a type `A` unless `A` is a *subtype* +of the annotated yield type: + +```toml +[rules] +unsound-yield = "error" +``` + +```py +from typing import Any, Generator, Iterator + +def returns_any() -> Any: + return "not an integer" + +def generator() -> Generator[int]: + # snapshot: unsound-yield + yield returns_any() +``` + +```snapshot +error[unsound-yield]: Unsound `yield` + --> src/mdtest_snippet.py:8:11 + | +6 | def generator() -> Generator[int]: + | -------------- Expected a subtype of `int` because of the yield type +7 | # snapshot: unsound-yield +8 | yield returns_any() + | ^^^^^^^^^^^^^ Inferred as `Any` +info: `Any` is assignable to `int`, but not a subtype of `int` +help: Consider using an `assert` to narrow the type before yielding it +``` + +The same check applies to generators annotated as iterators. Values that are not even assignable to +the annotated yield type still cause us to emit only `invalid-yield`. + +```py +def iterator() -> Iterator[int]: + yield returns_any() # error: [unsound-yield] + +def invalid_generator() -> Generator[int]: + yield "not an integer" # error: [invalid-yield] +``` + +Narrowing a dynamic value before yielding it makes the yield sound. + +```py +def narrowed_generator() -> Generator[int]: + value = returns_any() + assert isinstance(value, int) + yield value + +def unannotated_generator(): + yield returns_any() +``` + +An example with nested error context: + +```py +def nested_generator() -> Generator[tuple[tuple[int, int]]]: + # snapshot: unsound-yield + yield ((42, returns_any()),) +``` + +```snapshot +error[unsound-yield]: Unsound `yield` + --> src/mdtest_snippet.py:23:11 + | +21 | def nested_generator() -> Generator[tuple[tuple[int, int]]]: + | --------------------------------- Expected a subtype of `tuple[tuple[int, int]]` because of the yield type +22 | # snapshot: unsound-yield +23 | yield ((42, returns_any()),) + | ^^^^^^^^^^^^^^^^^^^^^^ Inferred as `tuple[tuple[Literal[42], Any]]` +info: `tuple[tuple[Literal[42], Any]]` is assignable to `tuple[tuple[int, int]]`, but not a subtype of `tuple[tuple[int, int]]` +info: the first tuple element is not compatible: `tuple[Literal[42], Any]` is not a subtype of `tuple[int, int]` +info: └── the second tuple element is not compatible: `Any` is not a subtype of `int` +help: Consider using an `assert` to narrow the type before yielding it +``` + +## Unsound yield statements with gradual yield types + +The rule applies only when the annotated yield type is fully static. An explicit `Any`, an alias of +`Any`, or an `Any` nested inside the yield type disables the strict check. + +```toml +[rules] +unsound-yield = "error" +``` + +```py +from typing import Any, Generator, Iterator +from typing_extensions import Never, TypeAliasType + +AnyAlias = TypeAliasType("AnyAlias", Any) + +def returns_any() -> Any: + return "not an integer" + +def dynamic_yield_type() -> Generator[Any]: + yield returns_any() + +def aliased_dynamic_yield_type() -> Generator[AnyAlias]: + yield returns_any() + +def nested_dynamic_yield_type() -> Iterator[tuple[int, Any]]: + yield returns_any() + +# error: [missing-type-argument] +def unknown_yield_type() -> Iterator: + yield returns_any() +``` + +Only the yield type determines whether the boundary is fully static; dynamic send and return types +do not disable the check. `Never` is also a fully static yield type. + +```py +def dynamic_send_and_return_types() -> Generator[int, Any, Any]: + yield returns_any() # error: [unsound-yield] + +def never_yields() -> Generator[Never]: + yield returns_any() # error: [unsound-yield] +``` + +## Unsound delegated yield expressions + +`yield from` exposes every value produced by the delegated iterator, so its element type must also +be a subtype of the outer generator's fully static yield type. + +```toml +[rules] +unsound-yield = "error" +``` + +```py +from typing import Any, Generator, Iterator + +def dynamic_values() -> Generator[Any]: + yield "not an integer" + +def delegated_generator() -> Generator[int]: + # snapshot: unsound-yield + yield from dynamic_values() +``` + +```snapshot +error[unsound-yield]: Unsound `yield from` + --> src/mdtest_snippet.py:8:16 + | +6 | def delegated_generator() -> Generator[int]: + | -------------- Expected a subtype of `int` because of the yield type +7 | # snapshot: unsound-yield +8 | yield from dynamic_values() + | ^^^^^^^^^^^^^^^^ Yielded elements inferred as `Any` +info: `Any` is assignable to `int`, but not a subtype of `int` +help: Consider using `assert`s to narrow the types of the elements before yielding them +``` + +Nested dynamic values are rejected too, while genuinely incompatible iterators cause us to emit +`invalid-yield` instead. + +```py +def nested_dynamic_values() -> Iterator[tuple[int, Any]]: + yield (1, "not an integer") + +def nested_delegated_generator() -> Iterator[tuple[int, int]]: + yield from nested_dynamic_values() # error: [unsound-yield] + +def invalid_delegated_generator() -> Iterator[int]: + yield from ["not an integer"] # error: [invalid-yield] + +def valid_delegated_generator() -> Iterator[int]: + yield from [1, 2] +``` + +## Edge case: `unsound-yield` combined with `yield from` expressions that are not iterable + +```toml +[rules] +unsound-yield = "error" +``` + +In the following situation, we only emit `not-iterable`, even though the inferred `yield` type here +is `Unknown` (not a subtype of `int`). Also emitting `unsound-yield` here would just add confusing +noise to our diagnostics: `Unknown` is just a fallback type here that we "spun out of thin air" +because `42` has no `__iter__` method to tell us any better. + +```py +from typing import Iterable, Iterator, Any + +def non_iterable_delegated_generator() -> Iterator[int]: + # Here we only emit `not-iterable`, even though the inferred yield type here + # is `Unknown`: also emitting `unsound-yield` would just add noise + yield from 42 # error: [not-iterable] +``` + +But the following situation is different: here we emit both `not-iterable` *and* `unsound-yield`, +because `Any` was not simply a fallback here that we "invented out of thin air". It's the annotated +iterable type of `BrokenIterable`'s `__iter__` method: + +```py +class BrokenIterable: + def __iter__(self, oh_no) -> Iterator[Any]: + raise NotImplementedError + +def broken_iterable_delegated_generator() -> Iterator[int]: + # snapshot: not-iterable + # snapshot: unsound-yield + yield from BrokenIterable() +``` + +```snapshot +error[not-iterable]: Object of type `BrokenIterable` is not iterable + --> src/mdtest_snippet.py:14:16 + | +14 | yield from BrokenIterable() + | ^^^^^^^^^^^^^^^^ +info: Its `__iter__` method has an invalid signature +info: type `BrokenIterable` is not assignable to protocol `Iterable[Unknown]` +info: └── protocol member `__iter__` is incompatible +info: └── unexpected extra parameter `oh_no` +help: Parameter `oh_no` must have a default value +info: Expected signature `def __iter__(self): ...` + + +error[unsound-yield]: Unsound `yield from` + --> src/mdtest_snippet.py:14:16 + | +11 | def broken_iterable_delegated_generator() -> Iterator[int]: + | ------------- Expected a subtype of `int` because of the yield type +12 | # snapshot: not-iterable +13 | # snapshot: unsound-yield +14 | yield from BrokenIterable() + | ^^^^^^^^^^^^^^^^ Yielded elements inferred as `Any` +info: `Any` is assignable to `int`, but not a subtype of `int` +help: Consider using `assert`s to narrow the types of the elements before yielding them +``` + +## Unsound asynchronous yield statements + +The strict yield check also applies to asynchronous generators and asynchronous iterators. + +```toml +[rules] +unsound-yield = "error" +``` + +```py +from typing import Any, AsyncGenerator, AsyncIterator + +def returns_any() -> Any: + return "not an integer" + +async def asynchronous_generator() -> AsyncGenerator[int]: + yield returns_any() # error: [unsound-yield] + +async def asynchronous_iterator() -> AsyncIterator[int]: + yield returns_any() # error: [unsound-yield] + +async def dynamic_asynchronous_generator() -> AsyncGenerator[Any]: + yield returns_any() +``` diff --git a/crates/ty_python_semantic/resources/mdtest/external/numpy.md b/crates/ty_python_semantic/resources/mdtest/external/numpy.md index 39bfa6d110..caa987e517 100644 --- a/crates/ty_python_semantic/resources/mdtest/external/numpy.md +++ b/crates/ty_python_semantic/resources/mdtest/external/numpy.md @@ -18,6 +18,32 @@ xs = np.array([1, 2, 3]) reveal_type(xs) # revealed: ndarray[tuple[Any, ...], dtype[Any]] xs = np.array([1.0, 2.0, 3.0], dtype=np.float64) -# TODO: should be `ndarray[tuple[Any, ...], dtype[float64]]` -reveal_type(xs) # revealed: ndarray[tuple[Any, ...], dtype[Unknown]] +reveal_type(xs) # revealed: ndarray[tuple[Any, ...], dtype[float64]] +``` + +Explicit dtypes remain distinct when checking an array against a parameter annotation. This is a +regression test for : + +```py +def takes_float16(values: np.ndarray[tuple[int, ...], np.dtype[np.float16]]) -> None: ... + +float32_values = np.array([1, 2, 3], dtype=np.float32) +reveal_type(float32_values) # revealed: ndarray[tuple[Any, ...], dtype[floating[_32Bit]]] + +float16_values = np.array([1, 2, 3], dtype=np.float16) +reveal_type(float16_values) # revealed: ndarray[tuple[Any, ...], dtype[floating[_16Bit]]] + +takes_float16(float32_values) # error: [invalid-argument-type] +takes_float16(float16_values) +``` + +An explicit integer dtype is also preserved through `array`, allowing `interp` to select its array +overload. This is a regression test for : + +```py +values = np.array([0, 1, 2], dtype=np.int64) +reveal_type(values) # revealed: ndarray[tuple[Any, ...], dtype[signedinteger[_64Bit]]] + +interpolated = np.interp(values, values, values) +reveal_type(interpolated) # revealed: ndarray[tuple[Any, ...], dtype[float64]] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/external/pydantic.lock b/crates/ty_python_semantic/resources/mdtest/external/pydantic.lock index 87f6bd7e46..fb7a5c2fb8 100644 --- a/crates/ty_python_semantic/resources/mdtest/external/pydantic.lock +++ b/crates/ty_python_semantic/resources/mdtest/external/pydantic.lock @@ -1,14 +1,14 @@ version = 1 revision = 3 -requires-python = "==3.12.*" +requires-python = "==3.13.*" [[package]] name = "annotated-types" -version = "0.7.0" +version = "0.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, ] [[package]] @@ -50,25 +50,21 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, - { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, - { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, - { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, - { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, - { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, - { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, - { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, - { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, - { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, - { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, - { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, - { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, - { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, - { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, - { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, - { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, ] [[package]] @@ -96,11 +92,11 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] [[package]] diff --git a/crates/ty_python_semantic/resources/mdtest/external/pydantic.md b/crates/ty_python_semantic/resources/mdtest/external/pydantic.md index b62c841b9f..9a0cbda628 100644 --- a/crates/ty_python_semantic/resources/mdtest/external/pydantic.md +++ b/crates/ty_python_semantic/resources/mdtest/external/pydantic.md @@ -2,7 +2,7 @@ ```toml [environment] -python-version = "3.12" +python-version = "3.13" python-platform = "linux" [project] @@ -1134,7 +1134,7 @@ There are various ways to make a field immutable. A model can be globally frozen parameter: ```py -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, Field, PrivateAttr class PersonFrozenName1(BaseModel, frozen=True): name: str @@ -1331,6 +1331,23 @@ static_assert(not is_subtype_of(PartiallyFrozen[A, B], PartiallyFrozen[A, A])) static_assert(not is_subtype_of(PartiallyFrozen[A, A], PartiallyFrozen[A, B])) ``` +Private attributes on models with `frozen=True` can be mutated: + +```py +class FrozenPerson(BaseModel): + model_config = ConfigDict(frozen=True) + + _implicit_private: int + _private_with_default: int = 1 + _explicit_private: int = PrivateAttr(default=0) + +person = FrozenPerson() + +person._implicit_private = 2 +person._private_with_default = 2 +person._explicit_private = 2 +``` + ## Validation of default values At runtime, default values are *not* validated against the field type annotation, unless @@ -1672,6 +1689,187 @@ class InvalidFieldQualifiers(BaseModel): required: Required[int] ``` +## Replacement + +Pydantic models support `copy.replace` and expose a synthesized `__replace__` method on Python 3.13 +and later. + +### Frozen models + +```py +from copy import replace + +from pydantic import BaseModel + +class Model(BaseModel, frozen=True): + value: int + +model = Model(value=1) + +# revealed: (self: Model, *, value: int = ...) -> Model +reveal_type(Model.__replace__) + +reveal_type(model.__replace__(value=2)) # revealed: Model +reveal_type(replace(model, value=2)) # revealed: Model +``` + +### Mutable models + +Replacement is available on mutable models and accepts only real model fields. + +```py +from copy import replace + +from pydantic import BaseModel + +class Model(BaseModel): + value: int + _private: int = 0 + +model = Model(value=1) + +# revealed: (self: Model, *, value: int = ...) -> Model +reveal_type(Model.__replace__) + +reveal_type(model.__replace__(value=2)) # revealed: Model +reveal_type(replace(model, value=2)) # revealed: Model + +model.__replace__(value="two") # error: [invalid-argument-type] +model.__replace__(_private=2) # error: [unknown-argument] +model.__replace__(missing=2) # error: [unknown-argument] +``` + +### Field aliases + +Replacement updates model fields by name, even when initialization uses an alias. + +```py +from copy import replace + +from pydantic import BaseModel, Field + +class Model(BaseModel): + value: int = Field(alias="external_value") + +model = Model(external_value=1) + +# revealed: (self: Model, *, value: int = ...) -> Model +reveal_type(Model.__replace__) + +reveal_type(model.__replace__(value=2)) # revealed: Model +reveal_type(replace(model, value=2)) # revealed: Model + +model.__replace__(external_value=2) # error: [unknown-argument] +``` + +### Member discovery + +The synthesized method is available in completions for both a model class and its instances. Models +do not expose attributes that belong only to standard-library dataclasses. + +```py +from pydantic import BaseModel +from ty_extensions import static_assert +from ty_extensions._internal import has_member + +class Model(BaseModel): + value: int + +model = Model(value=1) + +static_assert(has_member(Model, "__replace__")) +static_assert(has_member(model, "__replace__")) +static_assert(not has_member(Model, "__dataclass_fields__")) +static_assert(not has_member(Model, "__dataclass_params__")) +static_assert(not has_member(Model, "__match_args__")) +``` + +### Inherited fields + +```py +from copy import replace + +from pydantic import BaseModel + +class Parent(BaseModel): + inherited: int + +class Child(Parent): + own: str + +model = Child(inherited=1, own="first") + +# revealed: (self: Child, *, inherited: int = ..., own: str = ...) -> Child +reveal_type(Child.__replace__) + +reveal_type(model.__replace__(inherited=2)) # revealed: Child +reveal_type(model.__replace__(own="second")) # revealed: Child +reveal_type(replace(model, inherited=2, own="second")) # revealed: Child + +model.__replace__(inherited="two") # error: [invalid-argument-type] +model.__replace__(own=2) # error: [invalid-argument-type] +``` + +### Generic models + +```py +from copy import replace + +from pydantic import BaseModel + +class Model[T](BaseModel): + value: T + +model = Model[int](value=1) + +reveal_type(model.__replace__(value=2)) # revealed: Model[int] +reveal_type(replace(model, value=2)) # revealed: Model[int] + +model.__replace__(value="two") # error: [invalid-argument-type] +``` + +### Root models + +```py +from copy import replace + +from pydantic import RootModel + +class Model(RootModel[int]): ... + +model = Model(1) + +# revealed: (self: Model, *, root: int = ...) -> Model +reveal_type(Model.__replace__) + +reveal_type(model.__replace__(root=2)) # revealed: Model +reveal_type(replace(model, root=2)) # revealed: Model + +model.__replace__(root="two") # error: [invalid-argument-type] +``` + +### Settings models + +```py +from copy import replace + +from pydantic_settings import BaseSettings + +class Model(BaseSettings): + value: int + +model = Model(value=1) + +# revealed: (self: Model, *, value: int = ...) -> Model +reveal_type(Model.__replace__) + +reveal_type(model.__replace__(value=2)) # revealed: Model +reveal_type(replace(model, value=2)) # revealed: Model + +model.__replace__(value="two") # error: [invalid-argument-type] +model.__replace__(_secrets_dir=".") # error: [unknown-argument] +``` + ## Pydantic dataclasses Pydantic's dataclasses are similar to the standard library dataclasses: diff --git a/crates/ty_python_semantic/resources/mdtest/external/pydantic_extra_search_paths.lock b/crates/ty_python_semantic/resources/mdtest/external/pydantic_extra_search_paths.lock new file mode 100644 index 0000000000..8f9c35e475 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/external/pydantic_extra_search_paths.lock @@ -0,0 +1,97 @@ +version = 1 +revision = 3 +requires-python = "==3.11.*" + +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, +] + +[[package]] +name = "mdtest-deps" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "pydantic" }, +] + +[package.metadata] +requires-dist = [{ name = "pydantic", specifier = "==2.13.4" }] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] diff --git a/crates/ty_python_semantic/resources/mdtest/external/pydantic_extra_search_paths.md b/crates/ty_python_semantic/resources/mdtest/external/pydantic_extra_search_paths.md new file mode 100644 index 0000000000..59a41d8a16 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/external/pydantic_extra_search_paths.md @@ -0,0 +1,23 @@ +# Pydantic on extra search paths + +Pydantic-specific behavior still applies when the installed package is resolved from an extra search +path, such as when its `site-packages` directory is included in `PYTHONPATH`. + +```toml +[environment] +python-version = "3.11" +python-platform = "linux" +extra-paths = ["/.venv/"] + +[project] +dependencies = ["pydantic==2.13.4"] +``` + +```py +from pydantic import BaseModel, ConfigDict + +class Model(BaseModel): + model_config = ConfigDict(extra="allow") + +Model(a=1) +``` diff --git a/crates/ty_python_semantic/resources/mdtest/final.md b/crates/ty_python_semantic/resources/mdtest/final.md index 8990faad86..deb8f3c5a8 100644 --- a/crates/ty_python_semantic/resources/mdtest/final.md +++ b/crates/ty_python_semantic/resources/mdtest/final.md @@ -452,6 +452,65 @@ class F: def not_a_method(): ... ``` +## A method cannot be both abstract and final + +An abstract method must be overridden for a subclass to become concrete, but a final method cannot +be overridden. + +```py +from abc import abstractmethod +from typing import final + +class A: + @final + @abstractmethod + def first(self) -> None: ... # error: [abstract-and-final-method] + + # Decorator order does not matter. + @abstractmethod + @final + def second(self) -> None: ... # error: [abstract-and-final-method] + @abstractmethod + def abstract(self) -> None: ... + @final + def final(self) -> None: ... +``` + +## An overloaded method cannot be both abstract and final + +`runtime.py`: + +```py +from abc import ABC, abstractmethod +from typing import final, overload + +class A(ABC): + @overload + def method(self, value: int) -> int: ... + @overload + def method(self, value: str) -> str: ... + @final + @abstractmethod + def method(self, value: int | str) -> int | str: # error: [abstract-and-final-method] + raise NotImplementedError +``` + +`stub.pyi`: + +```pyi +from abc import abstractmethod +from typing import final, overload + +class A: + @overload + @final + @abstractmethod + def method(self, value: int) -> int: ... # error: [abstract-and-final-method] + @overload + @abstractmethod + def method(self, value: str) -> str: ... +``` + ## An `@final` method is overridden by an implicit instance attribute ```py @@ -1553,7 +1612,6 @@ error[final-on-variable]: `final` on variable `a` has no effect; use `let` inste | 2 | final a = 1 | ^^^^^^ - | info: a final variable is declared with `let`, which lowers to `Final` ``` diff --git a/crates/ty_python_semantic/resources/mdtest/function/parameters.md b/crates/ty_python_semantic/resources/mdtest/function/parameters.md index 9a8d4650c0..457df1b0e0 100644 --- a/crates/ty_python_semantic/resources/mdtest/function/parameters.md +++ b/crates/ty_python_semantic/resources/mdtest/function/parameters.md @@ -1,12 +1,59 @@ # Function parameter types +## Basic + Within a function scope, the declared type of each parameter is its annotated type (or Unknown if -not annotated). The initial inferred type is the annotated type of the parameter, if any. If there -is no annotation, it is the union of `Unknown` with the type of the default value expression (if -any). +not annotated). The initial inferred type is the annotated type of the parameter, if any: + +```py +def f(declared: int, unannotated): + reveal_type(declared) # revealed: int + reveal_type(unannotated) # revealed: unannotated@f +``` + +basedpython infers a signature for the unannotated parameter rather than leaving it `Unknown`, so +the parameter reads as the type variable that stands for whatever callers pass. The variadic parameter is a variadic tuple of its annotated type; the variadic-keywords parameter is -a dictionary from strings to its annotated type. +a dictionary from strings to its annotated type: + +```py +def g(*args: int, **kwargs: int): + reveal_type(args) # revealed: tuple[int, ...] + reveal_type(kwargs) # revealed: dict[str, int] +``` + +## Unannotated parameters with defaults + +If there is no annotation but there is a default value, the parameter is still inferred, and the +default value's type bounds the inferred type variable: + +```py +def f(a="foo", b=0, c=True, d=None): + reveal_type(a) # revealed: a@f + reveal_type(b) # revealed: b@f + reveal_type(c) # revealed: c@f + reveal_type(d) # revealed: d@f +``` + +The body is checked against that bound, so an operation the default value does not support is +rejected: + +```py +def g(x=0): + print(x + 1) + + # error: [unsupported-operator] "Operator `+` is not supported between objects of type `x@g` and `Literal["foo"]`" + x + "foo" +``` + +The bound also constrains callers, so an argument wider than the default value's type is rejected at +the call site: + +```py +# error: [invalid-argument-type] "Argument to function `g` is incorrect: Argument type `float` does not satisfy `int`, inferred for parameter `x`" +g(1.5) +``` ## Parameter kinds diff --git a/crates/ty_python_semantic/resources/mdtest/function/return_type.md b/crates/ty_python_semantic/resources/mdtest/function/return_type.md index 9dc5277394..4151f5af90 100644 --- a/crates/ty_python_semantic/resources/mdtest/function/return_type.md +++ b/crates/ty_python_semantic/resources/mdtest/function/return_type.md @@ -630,3 +630,356 @@ from typing import Never, Any def f(func: Any) -> Never: # error: [invalid-return-type] func() ``` + +## `unsound-return-statement` + +In addition to `invalid-return-type`, we also offer a disabled-by-default stricter rule +`unsound-return-statement`. This rule forbids `return` statements that return an instance of a type +`A` unless `A` is a *subtype* of the annotated return type: + +```toml +[rules] +unsound-return-statement = "error" +``` + +```py +from typing import Any + +# no error, even though `str` is not a subtype of `Any`: +# the lint only applies to a function if its return annotation is not a dynamic +# type such as `Any` +def returns_any() -> Any: + return "foo" + +def g() -> int: + # snapshot: unsound-return-statement + return returns_any() +``` + +```snapshot +error[unsound-return-statement]: Unsound return statement + --> src/mdtest_snippet.py:11:12 + | + 9 | def g() -> int: + | --- Expected a subtype of `int` because of the return type +10 | # snapshot: unsound-return-statement +11 | return returns_any() + | ^^^^^^^^^^^^^ Inferred as `Any` +info: `Any` is assignable to `int`, but not a subtype of `int` +help: Consider using an `assert` to narrow the type prior to the `return` statement +``` + +An example with nested error context: + +```py +def h() -> tuple[tuple[int, int]]: + # snapshot: unsound-return-statement + return ((42, returns_any()),) +``` + +```snapshot +error[unsound-return-statement]: Unsound return statement + --> src/mdtest_snippet.py:14:12 + | +12 | def h() -> tuple[tuple[int, int]]: + | ---------------------- Expected a subtype of `tuple[tuple[int, int]]` because of the return type +13 | # snapshot: unsound-return-statement +14 | return ((42, returns_any()),) + | ^^^^^^^^^^^^^^^^^^^^^^ Inferred as `tuple[tuple[Literal[42], Any]]` +info: `tuple[tuple[Literal[42], Any]]` is assignable to `tuple[tuple[int, int]]`, but not a subtype of `tuple[tuple[int, int]]` +info: the first tuple element is not compatible: `tuple[Literal[42], Any]` is not a subtype of `tuple[int, int]` +info: └── the second tuple element is not compatible: `Any` is not a subtype of `int` +help: Consider using an `assert` to narrow the type prior to the `return` statement +``` + +The rule is also applied to generator functions: + +```py +from typing import Generator + +def f() -> Generator[None, None, int]: + yield + # snapshot: unsound-return-statement + return returns_any() +``` + +```snapshot +error[unsound-return-statement]: Unsound return statement + --> src/mdtest_snippet.py:20:12 + | +17 | def f() -> Generator[None, None, int]: + | -------------------------- Expected a subtype of `int` because of the return type +18 | yield +19 | # snapshot: unsound-return-statement +20 | return returns_any() + | ^^^^^^^^^^^^^ Inferred as `Any` +info: `Any` is assignable to `int`, but not a subtype of `int` +help: Consider using an `assert` to narrow the type prior to the `return` statement +``` + +Aliases of `Any` are also dynamic return annotations and must not trigger the rule: + +```py +from typing_extensions import TypeAliasType + +AnyAlias = TypeAliasType("AnyAlias", Any) + +def returns_any_alias() -> AnyAlias: + return "foo" +``` + +The same applies when an alias of `Any` is the return type of a generator: + +```py +def generator_returns_any_alias() -> Generator[None, None, AnyAlias]: + yield + return "foo" +``` + +The rule in fact will not trigger if `Any` appears anywhere in your return type, either implicitly +or explicitly: + +```py +from typing import Any + +# error: [missing-type-argument] +def returns_unparameterized_tuple() -> tuple: + # no error, since the return type is implicitly `tuple[Any, ...]` + # (which is what the `missing-type-argument` error is complaining about on the line above!) + return returns_any() + +def returns_tuple_of_any() -> tuple[Any, Any]: + # no error, since the return type is explicitly `tuple[Any, Any]` + return returns_any() +``` + +Edge case: for `TypeIs`-annotated functions, we want the error message to say "not a subtype of +`bool`" rather than "not a subtype of `TypeIs`": + +```py +from typing_extensions import TypeIs + +def f(x: object) -> TypeIs[int]: + # snapshot: unsound-return-statement + return returns_any() +``` + +```snapshot +error[unsound-return-statement]: Unsound return statement + --> src/mdtest_snippet.py:45:12 + | +43 | def f(x: object) -> TypeIs[int]: + | ----------- Expected a subtype of `bool` because of the return type +44 | # snapshot: unsound-return-statement +45 | return returns_any() + | ^^^^^^^^^^^^^ Inferred as `Any` +info: `Any` is assignable to `bool`, but not a subtype of `bool` +help: Consider using an `assert` to narrow the type prior to the `return` statement +``` + +Aliases of `TypeIs` still return `bool`, so diagnostics must mention `bool` rather than the alias: + +```py +TypeIsAlias = TypeAliasType("TypeIsAlias", TypeIs[int]) + +def returns_type_is_alias(value: object) -> TypeIsAlias: + # error: "Unsound return statement: `Any` is not a subtype of `bool`" + return returns_any() +``` + +Detailed error context for aliases of `TypeIs` must also compare each union member against `bool`, +rather than against the original `TypeIs` annotation: + +```py +def returns_type_is_alias_union(value: object, result: bool | Any) -> TypeIsAlias: + # snapshot: unsound-return-statement + return result +``` + +```snapshot +error[unsound-return-statement]: Unsound return statement + --> src/mdtest_snippet.py:53:12 + | +51 | def returns_type_is_alias_union(value: object, result: bool | Any) -> TypeIsAlias: + | ----------- Expected a subtype of `bool` because of the return type +52 | # snapshot: unsound-return-statement +53 | return result + | ^^^^^^ Inferred as `bool | Any` +info: `bool | Any` is assignable to `bool`, but not a subtype of `bool` +info: element `Any` of union `bool | Any` is not a subtype of `bool` +help: Consider using an `assert` to narrow the type prior to the `return` statement +``` + +A `Never` return annotation is still a typed boundary, so returning `Any` must trigger the rule: + +```py +from typing_extensions import Never + +def never_returns() -> Never: + return returns_any() # error: [unsound-return-statement] +``` + +The same applies when `Never` is the return type of a generator: + +```py +def generator_never_returns() -> Generator[None, None, Never]: + yield + return returns_any() # error: [unsound-return-statement] +``` + +There is currently a limitation in how this rule interacts with contextual inference for collection +literals. When a function is annotated as returning `list[int]`, the annotation is used as context +while inferring the type of a list literal in a `return` statement. As a result, a list literal +containing an `Any` value is inferred as `list[int]` rather than `list[Any]`. The rule therefore +does not emit a diagnostic for the following unsound return statement. Mypy's `--warn-return-any` +option has the same limitation. In fact, mypy only rejects return expressions whose entire type is +`Any`, whereas this rule also rejects an independently inferred `list[Any]` when the annotated +return type is `list[int]`: + +```py +def returns_list_containing_any() -> list[int]: + return [returns_any()] +``` + +## Regression test: `unsound-return-statement` uses "pure redundancy" + +Internally, the rule uses "pure redundancy" rather than "impure redundancy". The following example +is a regression test that shows why this internal implementation detail is important. As an +optimisation as of 06 August 2026, `Phantom[str]` is not currently considered "impurely redundant" +with `Phantom[int]` (we do not simplify the union `Phantom[str] | Phantom[int]`). But the two +protocols are considered equivalent, are considered mutual subtypes of each other, and are +considered mutually redundant, meaning that no `unsound-return-statement` error is reported on this +snippet: + +```toml +[rules] +unsound-return-statement = "error" +``` + +```py +from typing import Generator, Protocol, TypeVar + +T = TypeVar("T") + +class Phantom(Protocol[T]): + def ping(self) -> int: ... + +def returns_protocol(value: Phantom[int]) -> Phantom[str]: + return value + +def generator_returns_protocol(value: Phantom[int]) -> Generator[None, None, Phantom[str]]: + yield + return value +``` + +## Regression test: `unsound-return-statement` with non-fully-static `TypedDict`s + +A `TypedDict` with a field or explicit extra items of type `Any` is not fully static, even when the +dictionary is defined as a class or inherits its fields from another `TypedDict`. The rule is not +applied to `TypedDict`s like this that are not fully static: + +```toml +[rules] +unsound-return-statement = "error" +``` + +```py +from typing_extensions import Any, Generator, TypedDict + +class StaticPayload(TypedDict): + value: int + +class DynamicPayload(TypedDict): + value: Any + +class InheritedDynamicPayload(DynamicPayload): ... +class DynamicExtraPayload(TypedDict, extra_items=Any): ... + +FunctionalDynamicPayload = TypedDict("FunctionalDynamicPayload", {"value": Any}) + +def returns_dynamic_typed_dict(value: StaticPayload) -> DynamicPayload: + return value + +def returns_inherited_dynamic_typed_dict(value: StaticPayload) -> InheritedDynamicPayload: + return value + +def returns_functional_dynamic_typed_dict(value: StaticPayload) -> FunctionalDynamicPayload: + return value + +def returns_dynamic_extra_typed_dict(value: Any) -> DynamicExtraPayload: + return value + +def generator_returns_dynamic_typed_dict( + value: StaticPayload, +) -> Generator[None, None, DynamicPayload]: + yield + return value + +def returns_static_typed_dict(value: Any) -> StaticPayload: + return value # error: [unsound-return-statement] +``` + +## Regression test: `unsound-return-statement` + recursive structural types + +Recursively specializing a protocol can produce infinitely many distinct types. Checking whether +such a return annotation is fully static must recognize the recurring protocol definition and +terminate instead of expanding the recursive member indefinitely, which would lead to a stack +overflow: + +```toml +[rules] +unsound-return-statement = "error" +``` + +```py +from typing import Any, Protocol, TypeVar + +T = TypeVar("T") + +class Growing(Protocol[T]): + @property + def next(self) -> "Growing[list[T]]": ... + +def returns_recursive_protocol(value: Any) -> Growing[int]: + return value +``` + +The same protection is needed for class-based `TypedDict` fields that recursively specialize their +containing dictionary. + +```py +from typing import Generic, TypedDict + +class GrowingPayload(TypedDict, Generic[T]): + child: "GrowingPayload[list[T]]" + +def returns_recursive_typed_dict(value: Any) -> GrowingPayload[int]: + return value +``` + +## Regression test: `unsound-return-statement` + recursive type aliases + +Recursively specializing a generic type alias can also produce infinitely many distinct types. The +check for whether a type is fully static must recognize repeated visits to the same alias +definition, including when `Any` appears elsewhere in the recursive alias. + +```toml +[environment] +python-version = "3.12" + +[rules] +unsound-return-statement = "error" +``` + +```py +from typing import Any + +type GrowingAlias[T] = list[GrowingAlias[list[T]]] +type GrowingAliasWithAny[T] = list[GrowingAliasWithAny[list[T]] | Any] + +def returns_recursive_alias(value: Any) -> GrowingAlias[int]: + return value + +def returns_recursive_alias_with_any(value: Any) -> GrowingAliasWithAny[int]: + return value +``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/callables.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/callables.md index 911f87d723..06eab72da0 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/callables.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/callables.md @@ -93,6 +93,8 @@ def decorator_factory() -> IdentityCallable[T]: return fn # revealed: ty_extensions._internal.GenericContext[T@decorator] reveal_type(generic_context(decorator)) + # revealed: Literal[1] + reveal_type(decorator(1)) return decorator @@ -232,6 +234,20 @@ reveal_type(decorator_factory()(identity)) reveal_type(decorator_factory()(identity)(1)) ``` +A legacy factory's return statements are checked against the lexical form of its return type. This +also applies when the returned callable accepts and returns another callable: + +```py +from typing import NoReturn + +class WrappedCallable: + def __call__(self, *args: object, **kwargs: object) -> NoReturn: + raise NotImplementedError + +def nested_callable_factory() -> Callable[[Callable[P, T]], Callable[P, T]]: + return lambda callback: WrappedCallable() +``` + If the typevar also appears in a parameter, it is the function that is generic, and the returned `Callable` is not: diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md index a75580cb52..0469cec067 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md @@ -117,14 +117,23 @@ class ParamSpecOuterClass(Generic[P]): ```snapshot error[shadowed-type-variable]: Generic class `InnerClass` uses ParamSpec `P` already bound by an enclosing scope - --> src/mdtest_snippet.py:70:7 + --> src/mdtest_snippet.py:72:11 | 70 | class ParamSpecOuterClass(Generic[P]): | ------------------------------- ParamSpec `P` is bound in this enclosing scope 71 | # snapshot: shadowed-type-variable 72 | class InnerClass(SingleParamSpec[P]): ... | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `P` used in class definition here - | +``` + +A `TypeVarTuple` must be unpacked when used as an argument to `Generic`. Even though the base is +invalid, ty still treats the `TypeVarTuple` as a type parameter of the class during error recovery, +so correctly unpacked uses within the class do not produce cascading errors. + +```py +# error: [invalid-generic-class] "`TypeVarTuple` must be unpacked" +class BareTypeVarTuple(Generic[Ts]): + values: tuple[*Ts] ``` If you don't specialize a generic base class, we use the default specialization, which maps each @@ -159,6 +168,116 @@ reveal_type(generic_context(ExplicitInheritedGenericPartiallySpecialized)) reveal_type(generic_context(ExplicitInheritedGenericPartiallySpecializedExtraTypevar)) ``` +## Specializing classes with unavailable generic context + +When an earlier error prevents ty from determining a class's generic context, specializing the class +can emit a cascading `not-subscriptable` diagnostic. + +### Conditional typing compatibility imports + +Libraries support multiple Python versions by importing generic machinery from either +`typing_extensions` or `typing`. ty does not yet recognize the resulting union as the corresponding +typing special form. + +```py +try: + import typing_extensions as typing +except ImportError: + import typing + +T = typing.TypeVar("T") + +# TODO: Fix the conditional typing import in https://github.com/astral-sh/ty/issues/1585. +# error: [invalid-argument-type] "`typing_extensions.TypeVar | typing.TypeVar` is not a valid argument to `Generic`" +class Parser(typing.Generic[T]): ... + +# TODO: Remove this cascading error when https://github.com/astral-sh/ty/issues/1585 is fixed. +parser: Parser[int] # error: [not-subscriptable] "Cannot subscript non-generic type ``" +``` + +### Decorated generic bases + +A decorator that ty cannot fully understand can obscure the generic context of a base class. A +subclass that forwards type variables to that base remains possibly generic. + +```py +import collections.abc +from typing import Generic, TypeVar +from ty_extensions._internal import generic_context + +K = TypeVar("K") +V = TypeVar("V") + +# error: [unresolved-attribute] "Class `Mapping` has no attribute `register`" +@collections.abc.Mapping.register +class Mapping(Generic[K, V]): ... + +# TODO: Invalid decorator causes us to lose the generic context from the class... +reveal_type(generic_context(Mapping)) # revealed: None + +class FrozenDict(Mapping[K, V]): ... + +# TODO: ...which then causes us to emit this +# error: [not-subscriptable] "Cannot subscript non-generic type ``" +mapping: FrozenDict[str, int] +``` + +### Unresolved generic bases + +```py +from typing import TypeVar + +from missing import Base # error: [unresolved-import] + +reveal_type(Base) # revealed: Unknown + +T = TypeVar("T") + +class Child(Base[T]): ... + +# error: [not-subscriptable] "Cannot subscript non-generic type ``" +child: Child[int] +``` + +### Conditional generic bases + +`base1.py`: + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") + +class Base(Generic[T]): ... +``` + +`base2.py`: + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") + +class Base(Generic[T]): ... +``` + +```py +from typing import TypeVar + +try: + from base1 import Base +except ImportError: + from base2 import Base + +T = TypeVar("T") + +# error: [unsupported-base] +class Child(Base[T]): ... + +# error: [not-subscriptable] "Cannot subscript non-generic type ``" +child: Child[int] +``` + ## Errors for inconsistent type arguments @@ -353,6 +472,28 @@ Stop2T = TypeVar("Stop2T", default=int) class Bad(Generic[Start2T, Stop2T, StepT]): ... ``` +## A subclass of a fully specialized generic is not generic + +A subclass is generic only if its bases leave at least one type variable unspecialized. Omitting a +type variable that has a default fully specializes the base, so the subclass cannot be specialized +again. + +```py +from typing_extensions import Generic, TypeVar + +T = TypeVar("T") +DefaultT = TypeVar("DefaultT", default=str) + +class Base(Generic[T, DefaultT]): ... +class GenericSubclass(Base[int, DefaultT]): ... +class NonGenericSubclass(Base[int]): ... + +reveal_type(GenericSubclass[bytes]()) # revealed: GenericSubclass[bytes] + +# error: [not-subscriptable] "Cannot specialize non-generic class `NonGenericSubclass`" +NonGenericSubclass[bytes] +``` + ## Diagnostics for bad specializations We show the user where the type variable was defined if a specialization is given that doesn't @@ -474,6 +615,53 @@ reveal_type(C(1)) # revealed: C[int] wrong_innards: C[int] = C("five") ``` +### Constructing the class from its own type variable + +A constructor call inside a generic class can use a value whose type is one of the class's type +variables. The constructed instance keeps that type variable instead of falling back to `Unknown`, +so an incompatible type context is rejected. + +```py +from typing_extensions import Generic, TypeVar + +T = TypeVar("T") + +class C(Generic[T]): + def __init__(self, value: T) -> None: + reveal_type(C(value)) # revealed: C[T@C] + + # error: [invalid-assignment] "Object of type `C[T@C]` is not assignable to `C[int]`" + invalid: C[int] = C(value) +``` + +### Constructing through a classmethod receiver + +A constructor call through a classmethod receiver keeps an enclosing `TypeVarTuple` when checking +the constructor arguments. In particular, freshening the constructor must not replace the +`TypeVarTuple` in the receiver with `Unknown`. + +```toml +[environment] +python-version = "3.11" +``` + +```py +from __future__ import annotations + +from typing import Generic, TypeVarTuple + +Ts = TypeVarTuple("Ts") + +class Thunk(Generic[*Ts]): + def __init__(self, state: Unresolved[*Ts] | None) -> None: ... + @classmethod + def make(cls, *values: *Ts) -> Thunk[*Ts]: + return cls(Unresolved(values)) + +class Unresolved(Generic[*Ts]): + def __init__(self, values: tuple[*Ts]) -> None: ... +``` + ### Many invariant parameters with dynamic bounds Treating unrelated classes with `Any` in their MRO as transitive pivots caused inference time to diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md index e367b55b2f..fa8e9b134d 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md @@ -229,13 +229,11 @@ error[invalid-argument-type]: Argument to function `f` is incorrect | 11 | reveal_type(f("string")) # revealed: Unknown | ^^^^^^^^ Argument type `Literal["string"]` does not satisfy upper bound `int` of type variable `T` - | info: Type variable defined here --> src/mdtest_snippet.py:3:1 | 3 | T = TypeVar("T", bound=int) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | ``` A bound can also be a union of protocols. If inference produces a union for the type variable, each @@ -282,13 +280,11 @@ error[invalid-argument-type]: Argument to function `f` is incorrect | 12 | reveal_type(f("string")) # revealed: Unknown | ^^^^^^^^ Argument type `Literal["string"]` does not satisfy constraints (`int`, `None`) of type variable `T` - | info: Type variable defined here --> src/mdtest_snippet.py:3:1 | 3 | T = TypeVar("T", int, None) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | ``` ## Typevar constraints @@ -440,10 +436,37 @@ def consume_callback(callback: Callable[[Row], None]) -> Row: reveal_type(consume_callback(callback)) # revealed: tuple[Any, ...] ``` -## Gradual constraints can obscure a more specific constraint +## Incompatible invariant protocol members + +When the same inferred type variable appears in multiple invariant protocol members, those members +must agree on one exact specialization. Gradual consistency between their types is not sufficient. + +```py +from typing import Any, Generic, Protocol, TypeVar + +T = TypeVar("T") +U = TypeVar("U") + +class Pair(Protocol[T]): + first: T + second: T + +class GradualPair(Generic[U]): + first: tuple[U, Any] + second: tuple[U, int] + +def infer_pair(value: Pair[T]) -> T: + raise NotImplementedError + +def check_pair(value: GradualPair[U]) -> None: + # TODO: error: [invalid-argument-type] "Argument to function `infer_pair` is incorrect" + reveal_type(infer_pair(value)) # revealed: Unknown +``` + +## Prefer specific compatible constraints over gradual constraints -A gradual constraint that is compatible with a concrete argument can be selected before a more -specific constraint. This makes inference depend on the order in which the constraints are declared. +A gradual constraint can be compatible with a concrete argument and a more specific declared +constraint. We prefer the more specific constraint regardless of declaration order. ```py from typing import Any, TypeVar @@ -454,6 +477,8 @@ class Row(tuple[Any, ...]): GradualFirst = TypeVar("GradualFirst", list[Any], tuple[Any, ...], Row) RowFirst = TypeVar("RowFirst", Row, tuple[Any, ...], list[Any]) +AnyFirst = TypeVar("AnyFirst", Any, int) +IntFirst = TypeVar("IntFirst", int, Any) def gradual_first(row: GradualFirst) -> GradualFirst: return row @@ -461,15 +486,22 @@ def gradual_first(row: GradualFirst) -> GradualFirst: def row_first(row: RowFirst) -> RowFirst: return row +def any_first(value: AnyFirst) -> AnyFirst: + return value + +def int_first(value: IntFirst) -> IntFirst: + return value + gradual = gradual_first(Row()) -# TODO: revealed: Row -reveal_type(gradual) # revealed: tuple[Any, ...] -# error: [unresolved-attribute] "Object of type `tuple[Any, ...]` has no attribute `asDict`" +reveal_type(gradual) # revealed: Row gradual.asDict() specific = row_first(Row()) reveal_type(specific) # revealed: Row specific.asDict() + +reveal_type(any_first(1)) # revealed: int +reveal_type(int_first(1)) # revealed: int ``` ## Typevar inference is a unification problem @@ -901,6 +933,30 @@ def union_bound(cls: U) -> None: reveal_type(cls.attr) # revealed: str | int ``` +## Attribute access on TypeVars constrained to instances and class objects + +A constrained type variable can contain both ordinary instances and class objects. Accessing a +shared attribute must inspect each constraint without treating the entire type variable as a class. + +```py +from typing import TypeVar + +class Instance: + @staticmethod + def keys() -> list[str]: + return [] + +class ClassObject: + @staticmethod + def keys() -> list[str]: + return [] + +T = TypeVar("T", Instance, type[ClassObject]) + +def read(value: T) -> list[str]: + return value.keys() +``` + ## Solving TypeVars with upper bounds in unions ```py @@ -1190,6 +1246,70 @@ class MyCallable: reveal_type(call(MyCallable())) # revealed: int ``` +## Callable return union order does not affect inference + +```py +from typing import Callable, Generic, TypeVar + +T = TypeVar("T") +T_co = TypeVar("T_co", covariant=True) + +class Box(Generic[T_co]): ... + +def ensure_tuple(func: Callable[[], tuple[T, ...] | T]) -> tuple[T, ...]: + raise NotImplementedError + +def ensure_tuple_reversed(func: Callable[[], T | tuple[T, ...]]) -> tuple[T, ...]: + raise NotImplementedError + +def ensure_box(func: Callable[[], Box[T] | T]) -> Box[T]: + raise NotImplementedError + +def ensure_box_reversed(func: Callable[[], T | Box[T]]) -> Box[T]: + raise NotImplementedError + +def check( + scalar_first: Callable[[], str | tuple[str, ...]], + tuple_first: Callable[[], tuple[str, ...] | str], + nested_member_first: Callable[[], Box[str] | tuple[Box[str], ...]], + nested_tuple_first: Callable[[], tuple[Box[str], ...] | Box[str]], + box_scalar_first: Callable[[], str | Box[str]], + box_first: Callable[[], Box[str] | str], +) -> None: + reveal_type(ensure_tuple(scalar_first)) # revealed: tuple[str, ...] + reveal_type(ensure_tuple(tuple_first)) # revealed: tuple[str, ...] + reveal_type(ensure_tuple_reversed(scalar_first)) # revealed: tuple[str, ...] + reveal_type(ensure_tuple_reversed(tuple_first)) # revealed: tuple[str, ...] + reveal_type(ensure_tuple(nested_member_first)) # revealed: tuple[Box[str], ...] + reveal_type(ensure_tuple(nested_tuple_first)) # revealed: tuple[Box[str], ...] + reveal_type(ensure_tuple_reversed(nested_member_first)) # revealed: tuple[Box[str], ...] + reveal_type(ensure_tuple_reversed(nested_tuple_first)) # revealed: tuple[Box[str], ...] + reveal_type(ensure_box(box_scalar_first)) # revealed: Box[str] + reveal_type(ensure_box(box_first)) # revealed: Box[str] + reveal_type(ensure_box_reversed(box_scalar_first)) # revealed: Box[str] + reveal_type(ensure_box_reversed(box_first)) # revealed: Box[str] +``` + +## Container constraints infer the element type + +basedpython's typeshed declares `Collection`'s parameter covariant, so it can inherit +`Container[Element]` rather than the `Container[Any]` upstream needs to keep covariance. Inferring a +type variable from a collection passed to a `Container` therefore lands on the element type itself, +with no gradual constraint left to preserve. + +```py +from collections.abc import Container +from typing import Any, TypeVar + +T = TypeVar("T") + +def value(items: Container[T]) -> T: + raise NotImplementedError + +items: list[str] = [] +reveal_type(value(items)) # revealed: str +``` + ## Passing a constrained TypeVar to a function expecting a compatible constrained TypeVar A constrained TypeVar should be assignable to a different constrained TypeVar if each constraint of @@ -1233,6 +1353,73 @@ reveal_type(narrow(1)) # revealed: int reveal_type(narrow("hello")) # revealed: str ``` +## Redundant callback bounds preserve constrained type-variable relationships + +A contravariant callback can contribute both another constrained type variable and a redundant +`object` upper bound. The inferred result must retain the other type variable in either callback +order. + +```py +from collections.abc import Callable +from typing import TypeVar + +T = TypeVar("T", int, str) +S = TypeVar("S", int, str) + +def select(first: Callable[[T], None], second: Callable[[T], None]) -> T: + raise NotImplementedError + +def forward_object(specific: Callable[[S], None], redundant: Callable[[object], None]) -> S: + result = select(specific, redundant) + reveal_type(result) # revealed: S@forward_object + return result + +def forward_object_reversed(specific: Callable[[S], None], redundant: Callable[[object], None]) -> S: + result = select(redundant, specific) + reveal_type(result) # revealed: S@forward_object_reversed + return result +``` + +A union of the type variable's constraints is also a redundant upper bound, even though it is not +`object`. + +```py +def forward_union(specific: Callable[[S], None], redundant: Callable[[int | str], None]) -> S: + result = select(specific, redundant) + reveal_type(result) # revealed: S@forward_union + return result + +def forward_union_reversed(specific: Callable[[S], None], redundant: Callable[[int | str], None]) -> S: + result = select(redundant, specific) + reveal_type(result) # revealed: S@forward_union_reversed + return result +``` + +The same relationship must survive a redundant, non-`object` nominal superclass shared by both +constraints. + +```py +class Base: ... +class Left(Base): ... +class Right(Base): ... + +TNominal = TypeVar("TNominal", Left, Right) +SNominal = TypeVar("SNominal", Left, Right) + +def select_nominal(first: Callable[[TNominal], None], second: Callable[[TNominal], None]) -> TNominal: + raise NotImplementedError + +def forward_nominal(specific: Callable[[SNominal], None], redundant: Callable[[Base], None]) -> SNominal: + result = select_nominal(specific, redundant) + reveal_type(result) # revealed: SNominal@forward_nominal + return result + +def forward_nominal_reversed(specific: Callable[[SNominal], None], redundant: Callable[[Base], None]) -> SNominal: + result = select_nominal(redundant, specific) + reveal_type(result) # revealed: SNominal@forward_nominal_reversed + return result +``` + ## Incompatible constraint sets But a constrained TypeVar with constraints not satisfied by the formal TypeVar should still error: diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md index 158d0c83f7..769951e064 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md @@ -369,6 +369,7 @@ annotated types of `*args` and `**kwargs` respectively. ```py from typing import Generic, Callable, ParamSpec +from ty_extensions._internal import generic_context P = ParamSpec("P") @@ -397,14 +398,83 @@ def foo1(c: Callable[P, int]) -> None: # error: [invalid-paramspec] "`*args: P.args` must be accompanied by `**kwargs: P.kwargs`" **kwargs: int, ) -> None: ... +``` -# TODO: error +`P.args` and `P.kwargs` do not bind `P` themselves. They must refer to a `ParamSpec` bound by +another parameter annotation or a visible enclosing generic context. A return annotation on the same +function is not sufficient. A generic outer class does not make its `ParamSpec` visible across a +nested class boundary. + +```py +# snapshot: unbound-type-variable def bar1(*args: P.args, **kwargs: P.kwargs) -> None: pass +# error: [unbound-type-variable] "ParamSpec `P` is not in scope" +def return_only(*args: P.args, **kwargs: P.kwargs) -> Callable[P, int]: + raise NotImplementedError + class Foo1: - # TODO: error + # error: [unbound-type-variable] "ParamSpec `P` is not in scope" def method(self, *args: P.args, **kwargs: P.kwargs) -> None: ... + +class Outer(Generic[P]): + def method(self, *args: P.args, **kwargs: P.kwargs) -> None: ... + + class Inner: + # error: [unbound-type-variable] "ParamSpec `P` is not in scope" + def method(self, *args: P.args, **kwargs: P.kwargs) -> None: ... + + def method_with_nested_class(self, callback: Callable[P, int]) -> None: + class Inner: + # error: [unbound-type-variable] "ParamSpec `P` is not in scope" + def method(self, *args: P.args, **kwargs: P.kwargs) -> None: ... + + def method_with_components(self, *outer_args: P.args, **outer_kwargs: P.kwargs) -> None: + class Inner: + # error: [unbound-type-variable] "ParamSpec `P` is not in scope" + def method(self, *args: P.args, **kwargs: P.kwargs) -> None: ... +``` + +```snapshot +error[unbound-type-variable]: ParamSpec `P` is not in scope + --> src/mdtest_snippet.py:32:17 + | +32 | def bar1(*args: P.args, **kwargs: P.kwargs) -> None: + | ^^^^^^ -------- This component uses the same out-of-scope ParamSpec +``` + +A `ParamSpec` moved to an enclosing factory's returned callable remains lexically visible within the +factory's body. A nested function referring to it through its components owns its own binding, +consistently with an ordinary return-only `TypeVar`: + +```py +def callable_factory() -> Callable[P, int]: + def nested(*args: P.args, **kwargs: P.kwargs) -> int: + return 1 + + def nested_with_parameter(callback: Callable[P, int], *args: P.args, **kwargs: P.kwargs) -> int: + return callback(*args, **kwargs) + + # revealed: ty_extensions._internal.GenericContext[P@nested_with_parameter] + reveal_type(generic_context(nested_with_parameter)) + return nested + +def repeated_paramspec_factory() -> Callable[P, Callable[P, int]]: + def nested(*args: P.args, **kwargs: P.kwargs) -> Callable[P, int]: + callback: Callable[P, int] + raise NotImplementedError + + return nested + +def nested_components_factory() -> Callable[P, int]: + def outer(*args: P.args, **kwargs: P.kwargs) -> int: + def inner(*inner_args: P.args, **inner_kwargs: P.kwargs) -> int: + return 1 + + return inner(*args, **kwargs) + + return outer ``` And, they need to be used together. @@ -457,6 +527,56 @@ def bar(c: Callable[P, int]) -> None: def f4(*a: P.args, x: int, **kw: P.kwargs) -> None: ... ``` +## Return-only ParamSpecs own nested callables + +A legacy `ParamSpec` appearing only in a factory's returned callable belongs to the callable, just +as a return-only `TypeVar` does. The nested callable should therefore own its `ParamSpec`, making it +possible to infer concrete arguments when the callable is used inside the factory. + +```py +from typing import Callable, ParamSpec, TypeVar +from ty_extensions._internal import generic_context + +P = ParamSpec("P") +T = TypeVar("T") + +def takes_int(value: int) -> int: + return value + +def paramspec_factory() -> Callable[P, int]: + def nested(*args: P.args, **kwargs: P.kwargs) -> int: + reveal_type(args) # revealed: P@nested.args + return 1 + + def with_callback(callback: Callable[P, int], *args: P.args, **kwargs: P.kwargs) -> int: + reveal_type(callback) # revealed: (**P@with_callback) -> int + return callback(*args, **kwargs) + + reveal_type(generic_context(nested)) # revealed: ty_extensions._internal.GenericContext[P@nested] + reveal_type(generic_context(with_callback)) # revealed: ty_extensions._internal.GenericContext[P@with_callback] + reveal_type(with_callback(takes_int, 1)) # revealed: int + return nested + +def typevar_factory() -> Callable[[T], int]: + def nested(value: T) -> int: + reveal_type(value) # revealed: T@nested + return 1 + + reveal_type(generic_context(nested)) # revealed: ty_extensions._internal.GenericContext[T@nested] + return nested +``` + +A genuinely generic enclosing function still owns the `ParamSpec` captured by its nested callable. + +```py +def public_paramspec(callback: Callable[P, int]) -> None: + def nested(*args: P.args, **kwargs: P.kwargs) -> int: + reveal_type(args) # revealed: P@public_paramspec.args + return callback(*args, **kwargs) + + reveal_type(generic_context(nested)) # revealed: None +``` + ## Specializing generic classes explicitly ```py @@ -593,16 +713,13 @@ def _(concrete: Command[[str]], gradual: Command[...]) -> None: This avoids rejecting wrappers around callbacks that are safe to use with a positional-only callback protocol. -```toml -[environment] -python-version = "3.12" -``` - ```py from collections.abc import Callable -from typing import Final +from typing import Final, Generic, ParamSpec + +P = ParamSpec("P", contravariant=True) -class Job[**P]: +class Job(Generic[P]): target: Final[Callable[P, None]] def __init__(self, target: Callable[P, None]) -> None: diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/typevartuple.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/typevartuple.md index 8aac051fd1..832cb3dda7 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/typevartuple.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/typevartuple.md @@ -614,13 +614,7 @@ def fn0(a: int) -> None: ... def fn1(a: int, b: str) -> None: ... def fn2(a: int, b: str, c: bytes) -> None: ... -# TODO: Should reveal `tuple[()]` without an error. -# error: [invalid-argument-type] "Argument to function `test` is incorrect: Expected `(int, /, *args: tuple[Unknown, ...]) -> None`, found `def fn0(a: int)`" -reveal_type(test(fn0)) # revealed: tuple[Unknown, ...] -# TODO: Should reveal `tuple[str]` without an error. -# error: [invalid-argument-type] "Argument to function `test` is incorrect: Expected `(int, /, *args: tuple[Unknown, ...]) -> None`, found `def fn1(a: int, b: str)`" -reveal_type(test(fn1)) # revealed: tuple[Unknown, ...] -# TODO: Should reveal `tuple[str, bytes]` without an error. -# error: [invalid-argument-type] "Argument to function `test` is incorrect: Expected `(int, /, *args: tuple[Unknown, ...]) -> None`, found `def fn2(a: int, b: str, c: bytes)`" -reveal_type(test(fn2)) # revealed: tuple[Unknown, ...] +reveal_type(test(fn0)) # revealed: tuple[()] +reveal_type(test(fn1)) # revealed: tuple[str] +reveal_type(test(fn2)) # revealed: tuple[str, bytes] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/unpack.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/unpack.md index 8547a24329..435ca08fe7 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/unpack.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/unpack.md @@ -92,8 +92,9 @@ def f( ## Unsupported union unpacking -Unpacking a type variable tuple into `Union` is currently not supported. Both the rejected union and -runtime element access recover to `object`. +Unpacking a type variable tuple into `Union` is currently not supported. The rejected union recovers +to `object` both on its own and inside another generic specialization. Runtime element access also +recovers to `object`. ```py from typing import TypeVarTuple, Union, Unpack @@ -106,6 +107,10 @@ def reject_union(value: Union[Unpack[Ts]]) -> None: # TODO: should reveal `Union[*Ts]` representation reveal_type(value) # revealed: object +# error: [invalid-type-form] "Unpacking a `TypeVarTuple` in `Union` is not supported" +def reject_nested_union(value: list[Union[Unpack[Ts], None]]) -> None: + reveal_type(value) # revealed: list[object] + def element_types(values: tuple[Unpack[Ts]]) -> None: # TODO: should reveal `Union[*Ts]` representation reveal_type(values[0]) # revealed: object @@ -115,6 +120,35 @@ def element_types(values: tuple[Unpack[Ts]]) -> None: reveal_type(value) # revealed: object ``` +## Invalid unpack operand nested in a union + +Although `Unpack[int]` is valid Python syntax, its non-tuple operand should report an ordinary +diagnostic when the union appears inside a generic specialization. + +```py +from typing import Union, Unpack + +# error: [invalid-type-form] "`Unpack` can only unpack a tuple type or `TypeVarTuple`" +def invalid_operand(value: list[Union[Unpack[int], None]]) -> None: + reveal_type(value) # revealed: list[tuple[Unknown, ...] | None] +``` + +## Invalid unpack contexts still infer the operand + +An invalid unpack context should not suppress runtime errors from its operand. String annotations do +not execute their contents, so unresolved names inside an invalid string annotation remain silent. + +```py +from typing import Unpack + +# error: [invalid-type-form] "`Unpack` is not allowed in parameter annotations" +# error: [unresolved-reference] "Name `Missing` used when not defined" +def invalid_context(value: Unpack[Missing]) -> None: ... + +# error: [invalid-type-form] "`Unpack` is not allowed in parameter annotations" +def invalid_stringified_context(value: "Unpack[Missing]") -> None: ... +``` + ## Concrete and nested tuple unpacking `Unpack` can expand a concrete tuple annotation for `*args`, including a nested unbounded tuple. @@ -128,8 +162,7 @@ def accept( accept(True, "phase", "status", b"ok") accept(True, b"ok") -# TODO: error: [invalid-argument-type] "Argument to function `accept` is incorrect: Expected `tuple[bool, *tuple[str, ...], bytes]`" -accept(True, 1, b"bad") +accept(True, 1, b"bad") # error: [invalid-argument-type] ``` ## Defaults diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md index 2353ff190f..1b5a77b414 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md @@ -6,7 +6,8 @@ for both type variable syntaxes. Unless otherwise specified, all quotations come from the [Generics] section of the typing spec. -Diagnostics for invalid type variables are snapshotted in `diagnostics/legacy_typevars.md`. +Additional diagnostics for invalid type variables are snapshotted in +`diagnostics/legacy_typevars.md`. ## Type variables @@ -612,19 +613,35 @@ reveal_type(S.__constraints__) # revealed: tuple[int | float, str] ```py from typing import TypeVar -# error: [invalid-legacy-type-variable] +# snapshot: invalid-legacy-type-variable T = TypeVar("T", int) ``` +```snapshot +error[invalid-legacy-type-variable]: A `TypeVar` cannot have exactly one constraint + --> src/mdtest_snippet.py:4:18 + | +4 | T = TypeVar("T", int) + | ^^^ +``` + ### Cannot have both bound and constraint ```py from typing import TypeVar -# error: [invalid-legacy-type-variable] +# snapshot: invalid-legacy-type-variable T = TypeVar("T", int, str, bound=bytes) ``` +```snapshot +error[invalid-legacy-type-variable]: A `TypeVar` cannot have both a bound and constraints + --> src/mdtest_snippet.py:4:5 + | +4 | T = TypeVar("T", int, str, bound=bytes) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +``` + ### Cannot be both covariant and contravariant > To facilitate the declaration of container types where covariant or contravariant type checking is @@ -634,10 +651,18 @@ T = TypeVar("T", int, str, bound=bytes) ```py from typing import TypeVar -# error: [invalid-legacy-type-variable] +# snapshot: invalid-legacy-type-variable T = TypeVar("T", covariant=True, contravariant=True) ``` +```snapshot +error[invalid-legacy-type-variable]: A `TypeVar` cannot be both covariant and contravariant + --> src/mdtest_snippet.py:4:5 + | +4 | T = TypeVar("T", covariant=True, contravariant=True) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +``` + ### Infer variance For a `TypeVar` with `infer_variance=True`, we infer covariance when the type variable only appears @@ -722,7 +747,6 @@ error[invalid-legacy-type-variable]: A `TypeVar` cannot specify variance when `i | 48 | CovariantAndInferred = TypeVar("CovariantAndInferred", covariant=True, infer_variance=True) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | ``` ### Boolean parameters must be unambiguous @@ -733,16 +757,38 @@ from typing_extensions import TypeVar def cond() -> bool: return True -# error: [invalid-legacy-type-variable] +# snapshot: invalid-legacy-type-variable T = TypeVar("T", covariant=cond()) -# error: [invalid-legacy-type-variable] +# snapshot: invalid-legacy-type-variable U = TypeVar("U", contravariant=cond()) -# error: [invalid-legacy-type-variable] +# snapshot: invalid-legacy-type-variable V = TypeVar("V", infer_variance=cond()) ``` +```snapshot +error[invalid-legacy-type-variable]: The `covariant` parameter of `TypeVar` cannot have an ambiguous truthiness + --> src/mdtest_snippet.py:7:28 + | +7 | T = TypeVar("T", covariant=cond()) + | ^^^^^^ + + +error[invalid-legacy-type-variable]: The `contravariant` parameter of `TypeVar` cannot have an ambiguous truthiness + --> src/mdtest_snippet.py:10:32 + | +10 | U = TypeVar("U", contravariant=cond()) + | ^^^^^^ + + +error[invalid-legacy-type-variable]: The `infer_variance` parameter of `TypeVar` cannot have an ambiguous truthiness + --> src/mdtest_snippet.py:13:33 + | +13 | V = TypeVar("V", infer_variance=cond()) + | ^^^^^^ +``` + ### Invalid keyword arguments ```py @@ -999,6 +1045,73 @@ reveal_type(D().x) # revealed: Unknown ## Regression +### Specialization cycle recovery preserves concrete defaults + +When a generic call uses a type variable's default, cycle recovery must allow the initial `Unknown` +specialization to resolve to the concrete default. + +```toml +[environment] +python-version = "3.14" +``` + +```py +class C: + pass + +def f(a: T | None = None) -> T: + raise NotImplementedError + +if f(): + pass + +if f(): + sum() # error: [no-matching-overload] +else: + sum() # error: [no-matching-overload] + +from typing import TypeVar + +T = TypeVar("T", default=C) + +reveal_type(f()) # revealed: C +``` + +### Specialization cycle recovery prevents oscillating defaults + +A type variable's default can depend on an overloaded call that itself uses the same type variable. +Specialization must converge even when overload selection changes between cycle iterations. + +```toml +[environment] +python-version = "3.14" +``` + +```py +from typing import TypeVar, overload + +@overload +def choose(value: int) -> type[int]: ... +@overload +def choose(value: object) -> type[str]: ... +def choose(value: object) -> type[int] | type[str]: + return str + +def f() -> T: + raise NotImplementedError + +if f(): + Default = str +else: + Default = choose(f()) + +# one branch binds `Default` to a class object rather than to a type, which a default has to be +# error: [invalid-type-form] "Variable of type `type[str]` is not allowed in a type expression" +T = TypeVar("T", default=Default) + +reveal_type(f()) # revealed: Unknown | str +``` + ### Use of typevar with default inside a function body that binds it ```toml diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variance.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variance.md index cd8ad49d95..72005757ad 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variance.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variance.md @@ -22,8 +22,8 @@ Types that "produce" data on demand are covariant in their typevar. If you expec get from the sequence is a valid `int`. ```py -from ty_extensions import static_assert, Unknown -from ty_extensions._internal import is_assignable_to, is_equivalent_to, is_subtype_of +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_assignable_to, is_equivalent_to, is_subtype_of from typing import Any, Generic, TypeVar class A: ... @@ -104,8 +104,8 @@ Types that "consume" data are contravariant in their typevar. If you expect a co that you pass into the consumer is a valid `int`. ```py -from ty_extensions import static_assert, Unknown -from ty_extensions._internal import is_assignable_to, is_equivalent_to, is_subtype_of +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_assignable_to, is_equivalent_to, is_subtype_of from typing import Any, Generic, TypeVar class A: ... @@ -217,8 +217,8 @@ In the end, if you expect a mutable list, you must always be given a list of exa since we can't know in advance which of the allowed methods you'll want to use. ```py -from ty_extensions import static_assert, Unknown -from ty_extensions._internal import is_assignable_to, is_equivalent_to, is_subtype_of +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_assignable_to, is_equivalent_to, is_subtype_of from typing import Any, Generic, TypeVar class A: ... @@ -339,7 +339,6 @@ error[invalid-generic-class]: Variance of type variable `T_co` is incompatible w | 18 | class BadCovariantParameter(Generic[T_co]): | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Type variable `T_co` is declared as covariant, but `BadCovariantParameter` uses it contravariantly ``` @@ -412,7 +411,6 @@ error[invalid-generic-class]: Variance of type variable `T_co` is incompatible w | 18 | class BadInvariantCo(Invariant[T_co]): ... | ^^^^^^^^^^^^^^^ - | help: Type variable `T_co` is declared as covariant, but base class `Invariant` requires it to be invariant @@ -421,7 +419,6 @@ error[invalid-generic-class]: Variance of type variable `T_contra` is incompatib | 21 | class BadInvariantContra(Invariant[T_contra]): ... | ^^^^^^^^^^^^^^^^^^^ - | help: Type variable `T_contra` is declared as contravariant, but base class `Invariant` requires it to be invariant @@ -430,7 +427,6 @@ error[invalid-generic-class]: Variance of type variable `T_contra` is incompatib | 24 | class BadCovariant(Covariant[T_contra]): ... | ^^^^^^^^^^^^^^^^^^^ - | help: Type variable `T_contra` is declared as contravariant, but base class `Covariant` requires it to be covariant @@ -439,7 +435,6 @@ error[invalid-generic-class]: Variance of type variable `T_co` is incompatible w | 27 | class BadContravariant(Contravariant[T_co]): ... | ^^^^^^^^^^^^^^^^^^^ - | help: Type variable `T_co` is declared as covariant, but base class `Contravariant` requires it to be contravariant ``` @@ -474,4 +469,57 @@ static_assert(not is_assignable_to(GoodInferredInvariant[B], GoodInferredInvaria static_assert(not is_assignable_to(GoodInferredInvariant[A], GoodInferredInvariant[B])) ``` +## Inferred variance for writable subclass-type attributes + +A writable public `type[T]` attribute makes a legacy type variable with inferred variance invariant. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Generic, TypeVar +from ty_extensions import static_assert +from ty_extensions._internal import is_assignable_to, is_subtype_of + +T = TypeVar("T", infer_variance=True) + +class ClassContainer(Generic[T]): + cls: type[T] + +static_assert(not is_subtype_of(ClassContainer[int], ClassContainer[object])) +static_assert(not is_subtype_of(ClassContainer[object], ClassContainer[int])) + +static_assert(not is_assignable_to(ClassContainer[int], ClassContainer[object])) +static_assert(not is_assignable_to(ClassContainer[object], ClassContainer[int])) +``` + +## Inferred variance for subclass-type method parameters + +A method parameter annotated as `type[T]` makes a legacy type variable with inferred variance +contravariant. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Generic, TypeVar +from ty_extensions import static_assert +from ty_extensions._internal import is_assignable_to, is_subtype_of + +T = TypeVar("T", infer_variance=True) + +class ClassContainer(Generic[T]): + def put(self, cls: type[T]) -> None: ... + +static_assert(is_subtype_of(ClassContainer[object], ClassContainer[int])) +static_assert(not is_subtype_of(ClassContainer[int], ClassContainer[object])) + +static_assert(is_assignable_to(ClassContainer[object], ClassContainer[int])) +static_assert(not is_assignable_to(ClassContainer[int], ClassContainer[object])) +``` + [spec]: https://typing.python.org/en/latest/spec/generics.html#variance diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md index 169ebff7d7..22e205f270 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md @@ -152,6 +152,7 @@ class LegacyDict(TypedDict[T]): # error: [unbound-type-variable] x: T +# error: [not-subscriptable] "Cannot subscript non-generic type ``" type LegacyDictInt = LegacyDict[int] # error: [not-subscriptable] "Cannot specialize non-generic type alias `LegacyDictInt`" @@ -372,7 +373,6 @@ error[not-subscriptable]: Cannot specialize non-generic type alias `AliasA` | ------^^^^^ | | | Alias to `A`, which is not generic - | ``` ```py @@ -388,7 +388,6 @@ error[not-subscriptable]: Cannot specialize non-generic type alias `AliasB` | ------^^^^^ | | | Alias to `B[int]`, which is already specialized - | ``` ## Aliases are not callable @@ -706,13 +705,12 @@ type Alias1[*Ts, T = int] = tuple[*Ts, T] ```snapshot error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter - --> src/mdtest_snippet.py:2:13 + --> src/mdtest_snippet.py:2:18 | 2 | type Alias1[*Ts, T = int] = tuple[*Ts, T] | --- ^^^^^^^ `T` has a default | | | `Ts` is a TypeVarTuple - | info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple ``` @@ -723,13 +721,12 @@ type Alias2[T1, *Ts, T2 = int] = tuple[T1, *Ts, T2] ```snapshot error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter - --> src/mdtest_snippet.py:4:17 + --> src/mdtest_snippet.py:4:22 | 4 | type Alias2[T1, *Ts, T2 = int] = tuple[T1, *Ts, T2] | --- ^^^^^^^^ `T2` has a default | | | `Ts` is a TypeVarTuple - | info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple ``` @@ -740,14 +737,13 @@ type Alias3[*Ts, T1 = int, T2 = str] = tuple[*Ts, T1, T2] ```snapshot error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter - --> src/mdtest_snippet.py:6:13 + --> src/mdtest_snippet.py:6:18 | 6 | type Alias3[*Ts, T1 = int, T2 = str] = tuple[*Ts, T1, T2] | --- ^^^^^^^^ -------- `T2` also has a default | | | | | `T1` has a default | `Ts` is a TypeVarTuple - | info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple ``` @@ -760,13 +756,12 @@ type Alias4[*Us, *Ts = *tuple[int, str]] = tuple[*Us, *Ts] ```snapshot error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter - --> src/mdtest_snippet.py:10:13 + --> src/mdtest_snippet.py:10:18 | 10 | type Alias4[*Us, *Ts = *tuple[int, str]] = tuple[*Us, *Ts] | --- ^^^^^^^^^^^^^^^^^^^^^^ `Ts` has a default | | | `Us` is a TypeVarTuple - | info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple ``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md index 4fed5ad9df..29c73ba71e 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md @@ -307,23 +307,47 @@ If a typevar does not provide a default, we use `Unknown`: reveal_type(C()) # revealed: C[Unknown] ``` +## Calls within the generic class + +A call to a generic class from one of its own methods creates an independent generic occurrence. The +enclosing class's type variable does not constrain the new instance. + +```py +class C[T]: + def __init__(self) -> None: ... + def method(self) -> None: + reveal_type(C()) # revealed: C[Never] + contextual: C[int] = C() +``` + +The same applies when an explicit `__new__` is followed by a downstream `__init__`. Both bound +receivers refer to the new generic occurrence. + +```py +from typing import Self + +class D[T]: + def __new__(cls) -> Self: + return super().__new__(cls) + + def __init__(self) -> None: ... + def method(self) -> None: + reveal_type(D()) # revealed: D[Never] + contextual: D[int] = D() +``` + ## 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. -We have to add `x: T` to the classes to ensure they're not bivariant in `T` (__new__ and __init__ -signatures don't count towards variance). - ### `__new__` only ```py from ty_extensions._internal import generic_context, into_regular_callable class C[T]: - x: T - def __new__(cls, x: T) -> "C[T]": return object.__new__(cls) @@ -332,9 +356,9 @@ reveal_type(generic_context(C)) # revealed: ty_extensions._internal.GenericContext[T@C] reveal_type(generic_context(into_regular_callable(C))) -reveal_type(C(1)) # revealed: C[int] +reveal_type(C(1)) # revealed: C[Literal[1]] -# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`" +# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`" wrong_innards: C[int] = C("five") ``` @@ -344,8 +368,6 @@ wrong_innards: C[int] = C("five") from ty_extensions._internal import generic_context, into_regular_callable class C[T]: - x: T - def __init__(self, x: T) -> None: ... # revealed: ty_extensions._internal.GenericContext[T@C] @@ -353,12 +375,63 @@ reveal_type(generic_context(C)) # revealed: ty_extensions._internal.GenericContext[T@C] reveal_type(generic_context(into_regular_callable(C))) -reveal_type(C(1)) # revealed: C[int] +reveal_type(C(1)) # revealed: C[Literal[1]] -# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`" +# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`" wrong_innards: C[int] = C("five") ``` +### Failed constructor inference + +A failed constructor call reports its argument error without exposing an unsolved class type +parameter or producing an additional assignment error. + +```py +from collections.abc import Callable + +class Animal: ... +class Dog(Animal): ... + +class Consumer[T]: + def __init__(self, callback: Callable[[T], None]) -> None: + self.callback = callback + +def accepts_dog(value: Dog) -> None: ... + +consumer: Consumer[Animal] = Consumer(accepts_dog) # error: [invalid-argument-type] +``` + +### Constructing the class from its own type variable + +A constructor call inside a generic class can use a value whose type is one of the class's type +variables. The constructed instance keeps that type variable instead of falling back to `Unknown`, +so an incompatible type context is rejected. + +```py +class C[T]: + def __init__(self, value: T) -> None: + reveal_type(C(value)) # revealed: C[T@C] + + # error: [invalid-assignment] "Object of type `C[T@C]` is not assignable to `C[int]`" + invalid: C[int] = C(value) + + def from_union(self, value: T | list[T]) -> None: + reveal_type(C(value)) # revealed: C[T@C | list[T@C]] + + # error: [invalid-assignment] "Object of type `C[T@C | list[T@C]]` is not assignable to `C[list[T@C]]`" + invalid_union: C[list[T]] = C(value) +``` + +A method's own type variable is independent of the class type variable and is preserved in the same +way. + +```py +class D[T]: + def __init__(self, value: T) -> None: ... + def method[S](self, value: S) -> None: + reveal_type(D(value)) # revealed: D[S@method] +``` + ### Identical `__new__` and `__init__` signatures ```py @@ -553,10 +626,6 @@ from typing import overload from ty_extensions._internal import generic_context, into_regular_callable class C[T]: - # we need to use the type variable or else the class is bivariant in T, and - # specializations become meaningless - x: T - @overload def __init__(self: C[str], x: str) -> None: ... @overload @@ -574,7 +643,7 @@ reveal_type(generic_context(into_regular_callable(C))) reveal_type(C("string")) # revealed: C[str] reveal_type(C(b"bytes")) # revealed: C[bytes] -reveal_type(C(12)) # revealed: C[Unknown] +reveal_type(C(12)) # revealed: C[Never] C[str]("string") C[str](b"bytes") # error: [no-matching-overload] @@ -593,10 +662,6 @@ C[None](b"bytes") # error: [no-matching-overload] C[None](12) class D[T, U]: - # we need to use the type variable or else the class is bivariant in T, and - # specializations become meaningless - x: T - @overload def __init__(self: "D[str, U]", u: U) -> None: ... @overload @@ -610,7 +675,7 @@ reveal_type(generic_context(into_regular_callable(D))) reveal_type(D("string")) # revealed: D[str, Literal["string"]] reveal_type(D(1)) # revealed: D[str, Literal[1]] -reveal_type(D(1, "string")) # revealed: D[int, Literal["string"]] +reveal_type(D(1, "string")) # revealed: D[Literal[1], Literal["string"]] ``` ### Synthesized methods with dataclasses @@ -934,6 +999,44 @@ reveal_type(generic_context(A.merge)) # revealed: ty_extensions._internal.Gener reveal_type(generic_context(Impl.foo)) # revealed: ty_extensions._internal.GenericContext[Self@foo] ``` +## Subscripting non-generic classes + +Subscripting a non-generic class in a type expression is an error. The invalid type expression +recovers to `Unknown`. + +```py +class NonGeneric: ... + +# error: [not-subscriptable] "Cannot subscript non-generic type ``" +def direct(value: NonGeneric[int]) -> None: + reveal_type(value) # revealed: Unknown +``` + +The same diagnostic applies when the specialization is nested inside `type[...]`. + +```py +# error: [not-subscriptable] "Cannot subscript non-generic type ``" +def nested(value: type[NonGeneric[int]]) -> None: + reveal_type(value) # revealed: Unknown +``` + +Inheriting from a non-generic class, or from a specialization of a generic class, does not make the +subclass generic. + +```py +class Child(NonGeneric): ... +class Generic[T, U = str]: ... +class SpecializedChild(Generic[int]): ... + +# error: [not-subscriptable] "Cannot subscript non-generic type ``" +def child(value: Child[str]) -> None: + reveal_type(value) # revealed: Unknown + +# error: [not-subscriptable] "Cannot subscript non-generic type ``" +def specialized_child(value: SpecializedChild[bytes]) -> None: + reveal_type(value) # revealed: Unknown +``` + ## Tuple as a PEP-695 generic class Our special handling for `tuple` does not break if `tuple` is defined as a PEP-695 generic class in diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/concatenate.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/concatenate.md index 57f8fcd55e..1a68e8d03d 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/concatenate.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/concatenate.md @@ -649,12 +649,11 @@ def remove_param[**P, R](func: Callable[Concatenate[int, P], R]) -> Callable[P, def f1(x: int, y: str) -> str: ... @overload def f1(x: int, y: int) -> int: ... -@remove_param def f1(x: int, y: str | int) -> str | int: return y # TODO: Should reveal `Overloaded[(y: str) -> str, (y: int) -> int]` -reveal_type(f1) # revealed: (y: str) -> str | int +reveal_type(remove_param(f1)) # revealed: (y: str) -> str | int ``` But, it's not possible to _add_ a parameter to an overloaded function using `Concatenate` because @@ -666,18 +665,17 @@ def add_param[**P, R](func: Callable[P, R]) -> Callable[Concatenate[int, P], R]: return func(*args, **kwargs) return wrapper -# TODO: Raise a diagnostic stating that the signature of the implementation doesn't match the -# overloads because the overloads don't have the extra `int` parameter. @overload +# error: [invalid-overload] "Implementation does not accept all arguments of this overload" def f2(y: str) -> str: ... @overload +# error: [invalid-overload] "Implementation does not accept all arguments of this overload" def f2(y: int) -> int: ... @add_param def f2(y: str | int) -> str | int: return y -# TODO: Should this reveal `Overloaded[(int, /, y: str) -> str, (int, /, y: int) -> int]` ? -reveal_type(f2) # revealed: Overload[(int, /, y: str) -> str | int, (int, /, y: int) -> str | int] +reveal_type(f2) # revealed: Overload[(y: str) -> str, (y: int) -> int] ``` But, it's possible to add the additional parameter just to the overload signatures and not the @@ -692,8 +690,7 @@ def f3(x: int, /, y: int) -> int: ... def f3(y: str | int) -> str | int: return y -# TODO: Should reveal `Overloaded[(int, /, y: str) -> str, (int, /, y: int) -> int]` -reveal_type(f3) # revealed: Overload[(int, x: int, /, y: str) -> str | int, (int, x: int, /, y: int) -> str | int] +reveal_type(f3) # revealed: Overload[(x: int, /, y: str) -> str, (x: int, /, y: int) -> int] ``` ## `Concatenate` with protocol classes diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md index 55a1e7545b..5c781b7b89 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md @@ -271,13 +271,11 @@ error[invalid-argument-type]: Argument to function `f` is incorrect | 9 | reveal_type(f("string")) # revealed: Unknown | ^^^^^^^^ Argument type `Literal["string"]` does not satisfy upper bound `int` of type variable `T` - | info: Type variable defined here --> src/mdtest_snippet.py:3:7 | 3 | def f[T: int](x: T) -> T: | ^^^^^^ - | ``` ## Inferring a constrained typevar @@ -301,13 +299,11 @@ error[invalid-argument-type]: Argument to function `f` is incorrect | 10 | reveal_type(f("string")) # revealed: Unknown | ^^^^^^^^ Argument type `Literal["string"]` does not satisfy constraints (`int`, `None`) of type variable `T` - | info: Type variable defined here --> src/mdtest_snippet.py:3:7 | 3 | def f[T: (int, None)](x: T) -> T: | ^^^^^^^^^^^^^^ - | ``` ## Typevar constraints @@ -744,6 +740,35 @@ reveal_type(invoke(head_invariant, Invariant[int]())) reveal_type(invoke(lift_invariant, 1)) ``` +## Passing unbound generic methods to generic functions + +An unbound method of a generic class can be passed to a generic higher-order function. The class +type parameter must still be inferred from the concrete receiver expected by that function. + +```py +from __future__ import annotations + +from collections.abc import Callable + +class Box[T]: + def merge(self, other: Box[T]) -> Box[T]: + return self + +def fold[T](function: Callable[[T, T], T], values: list[T]) -> T: + return values[0] + +def merge_boxes(values: list[Box[str]]) -> Box[str]: + return fold(Box.merge, values) +``` + +The same applies to the standard-library `set.union` method passed to `functools.reduce`. + +```py +from functools import reduce + +reveal_type(reduce(set.union, [set[str]()])) # revealed: set[str] +``` + ## Protocols as TypeVar bounds Protocol types can be used as TypeVar bounds, just like nominal types. @@ -1044,6 +1069,63 @@ def f[T](x: T, y: Not[T]) -> T: ## `Callable` parameters +### Return type inference from object-variadic callbacks + +Object-variadic callbacks must preserve `Callable[..., T]` return constraints. + +```py +from collections.abc import Callable + +def call[T](callback: Callable[..., T]) -> T: + return callback() + +def bounded[T: int](callback: Callable[..., T]) -> T: + return callback() + +def callback(*args: object, **kwargs: object) -> int: + return 1 + +reveal_type(call(callback)) # revealed: int +reveal_type(bounded(callback)) # revealed: int +``` + +### Return type inference from top callables + +Top callable parameters must preserve return-type constraints. + +```py +from collections.abc import Callable +from ty_extensions import Top + +def accept_top[T](callback: Top[Callable[..., T]]) -> T: + raise NotImplementedError + +def ordinary() -> int: + return 1 + +reveal_type(accept_top(ordinary)) # revealed: int +``` + +### Gradual callable parameters with a required prefix + +```py +from collections.abc import Callable +from typing import Concatenate + +def invoke[T](callback: Callable[Concatenate[int, ...], T]) -> T: + return callback(1) + +def accepts_int(value: int, *args: object, **kwargs: object) -> int: + return value + +def needs_str(value: str, *args: object, **kwargs: object) -> int: + return len(value) + +reveal_type(invoke(accepts_int)) # revealed: int +# error: [invalid-argument-type] +reveal_type(invoke(needs_str)) # revealed: int +``` + ### Class constructors We can recurse into the parameters and return values of `Callable` parameters to infer @@ -1226,6 +1308,67 @@ def get_int() -> int | None: ... reveal_type(my_iter(get_int)) # revealed: Box[int] ``` +### Callable return union order does not affect inference + +```py +from typing import Callable + +class Box[T]: + def get(self) -> T: + raise NotImplementedError + +def ensure_tuple[T](func: Callable[[], tuple[T, ...] | T]) -> tuple[T, ...]: + raise NotImplementedError + +def ensure_tuple_reversed[T](func: Callable[[], T | tuple[T, ...]]) -> tuple[T, ...]: + raise NotImplementedError + +def ensure_box[T](func: Callable[[], Box[T] | T]) -> Box[T]: + raise NotImplementedError + +def ensure_box_reversed[T](func: Callable[[], T | Box[T]]) -> Box[T]: + raise NotImplementedError + +def check( + scalar_first: Callable[[], str | tuple[str, ...]], + tuple_first: Callable[[], tuple[str, ...] | str], + nested_member_first: Callable[[], Box[str] | tuple[Box[str], ...]], + nested_tuple_first: Callable[[], tuple[Box[str], ...] | Box[str]], + box_scalar_first: Callable[[], str | Box[str]], + box_first: Callable[[], Box[str] | str], +) -> None: + reveal_type(ensure_tuple(scalar_first)) # revealed: tuple[str, ...] + reveal_type(ensure_tuple(tuple_first)) # revealed: tuple[str, ...] + reveal_type(ensure_tuple_reversed(scalar_first)) # revealed: tuple[str, ...] + reveal_type(ensure_tuple_reversed(tuple_first)) # revealed: tuple[str, ...] + reveal_type(ensure_tuple(nested_member_first)) # revealed: tuple[Box[str], ...] + reveal_type(ensure_tuple(nested_tuple_first)) # revealed: tuple[Box[str], ...] + reveal_type(ensure_tuple_reversed(nested_member_first)) # revealed: tuple[Box[str], ...] + reveal_type(ensure_tuple_reversed(nested_tuple_first)) # revealed: tuple[Box[str], ...] + reveal_type(ensure_box(box_scalar_first)) # revealed: Box[str] + reveal_type(ensure_box(box_first)) # revealed: Box[str] + reveal_type(ensure_box_reversed(box_scalar_first)) # revealed: Box[str] + reveal_type(ensure_box_reversed(box_first)) # revealed: Box[str] +``` + +### Container constraints infer the element type + +basedpython's typeshed declares `Collection`'s parameter covariant, so it can inherit +`Container[Element]` rather than the `Container[Any]` upstream needs to keep covariance. Inferring a +type variable from a collection passed to a `Container` therefore lands on the element type itself, +with no gradual constraint left to preserve. + +```py +from collections.abc import Container +from typing import Any + +def value[T](items: Container[T]) -> T: + raise NotImplementedError + +items: list[str] = [] +reveal_type(value(items)) # revealed: str +``` + ### Don't include identical lower/upper bounds in type mapping multiple times This is was a performance regression reported in @@ -1350,6 +1493,90 @@ def g[S: (bool, str)](x: S) -> S: return f(x) # error: [invalid-argument-type] ``` +## Redundant callback bounds preserve constrained type-variable relationships + +A contravariant callback can contribute both another constrained type variable and a redundant +`object` upper bound. The inferred result must retain the other type variable in either callback +order. + +```py +from collections.abc import Callable + +def select[T: (int, str)]( + first: Callable[[T], None], + second: Callable[[T], None], +) -> T: + raise NotImplementedError + +def forward_object[S: (int, str)]( + specific: Callable[[S], None], + redundant: Callable[[object], None], +) -> S: + result = select(specific, redundant) + reveal_type(result) # revealed: S@forward_object + return result + +def forward_object_reversed[S: (int, str)]( + specific: Callable[[S], None], + redundant: Callable[[object], None], +) -> S: + result = select(redundant, specific) + reveal_type(result) # revealed: S@forward_object_reversed + return result +``` + +A union of the type variable's constraints is also a redundant upper bound, even though it is not +`object`. + +```py +def forward_union[S: (int, str)]( + specific: Callable[[S], None], + redundant: Callable[[int | str], None], +) -> S: + result = select(specific, redundant) + reveal_type(result) # revealed: S@forward_union + return result + +def forward_union_reversed[S: (int, str)]( + specific: Callable[[S], None], + redundant: Callable[[int | str], None], +) -> S: + result = select(redundant, specific) + reveal_type(result) # revealed: S@forward_union_reversed + return result +``` + +The same relationship must survive a redundant, non-`object` nominal superclass shared by both +constraints. + +```py +class Base: ... +class Left(Base): ... +class Right(Base): ... + +def select_nominal[T: (Left, Right)]( + first: Callable[[T], None], + second: Callable[[T], None], +) -> T: + raise NotImplementedError + +def forward_nominal[S: (Left, Right)]( + specific: Callable[[S], None], + redundant: Callable[[Base], None], +) -> S: + result = select_nominal(specific, redundant) + reveal_type(result) # revealed: S@forward_nominal + return result + +def forward_nominal_reversed[S: (Left, Right)]( + specific: Callable[[S], None], + redundant: Callable[[Base], None], +) -> S: + result = select_nominal(redundant, specific) + reveal_type(result) # revealed: S@forward_nominal_reversed + return result +``` + ## Display ordering Where possible, we want the types that appear in inferred specializations to line up with the types diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md index e616f78934..0dee7b22b2 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md @@ -1092,11 +1092,10 @@ def unwrap_awaitable(function: Callable[P, Awaitable[R]], /) -> Callable[P, R]: async def unwrapped(value: int) -> int: ... @overload async def unwrapped(value: str) -> str: ... -@unwrap_awaitable async def unwrapped(value: int | str) -> int | str: raise NotImplementedError -reveal_type(unwrapped(1)) # revealed: int | str +reveal_type(unwrap_awaitable(unwrapped)(1)) # revealed: int | str ``` The selected decorator overload can use an `Awaitable` return type. diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/typevartuple.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/typevartuple.md index 3a8dcd012a..b0184ee64e 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/typevartuple.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/typevartuple.md @@ -546,12 +546,27 @@ def expect_nested( def pass_flattened( callback: Callable[[int, *tuple[str, ...], bytes, str], None], ) -> None: - # TODO: This should be assignable because the nested unpacking is equivalent to the flattened - # form. - # error: [invalid-argument-type] expect_nested(callback) ``` +### Nested unpacked `TypeVarTuple` callable parameters + +A `TypeVarTuple` nested inside an unpacked tuple remains inferable after the surrounding tuple is +expanded into its fixed prefix and suffix. + +```py +from typing import Callable + +def infer_nested[*Ts](callback: Callable[[int, *tuple[*Ts, bytes]], None]) -> tuple[*Ts]: + raise NotImplementedError + +def fixed_middle(prefix: int, middle: str, suffix: bytes, /) -> None: ... +def empty_middle(prefix: int, suffix: bytes, /) -> None: ... + +reveal_type(infer_nested(fixed_middle)) # revealed: tuple[str] +reveal_type(infer_nested(empty_middle)) # revealed: tuple[()] +``` + ### Callable inference with additional keyword parameters Additional keyword-only or variadic keyword parameters do not contribute to a `TypeVarTuple` @@ -595,18 +610,12 @@ def positional_only_with_keyword(x: int, y: str, /, *, flag: bool) -> None: ... def positional_or_keyword(x: int, y: str, flag: bool) -> None: ... def keyword_catch_all(x: int, y: str, **kwargs: object) -> None: ... -# TODO: Should reveal `tuple[int, str]`. -# error: [invalid-argument-type] "Argument to function `infer_keyword_only` is incorrect: Expected `KeywordOnlyCallback[*tuple[Unknown, ...]]`, found `def explicit_keyword_only(x: int, y: str, *, flag: bool)`" -reveal_type(infer_keyword_only(explicit_keyword_only)) # revealed: tuple[Unknown, ...] -# TODO: Should reveal `tuple[int, str]`. -# error: [invalid-argument-type] "Argument to function `infer_keyword_only` is incorrect: Expected `KeywordOnlyCallback[*tuple[Unknown, ...]]`, found `def positional_only_with_keyword(x: int, y: str, /, *, flag: bool)`" -reveal_type(infer_keyword_only(positional_only_with_keyword)) # revealed: tuple[Unknown, ...] +reveal_type(infer_keyword_only(explicit_keyword_only)) # revealed: tuple[int, str] +reveal_type(infer_keyword_only(positional_only_with_keyword)) # revealed: tuple[int, str] # TODO: Should reveal `tuple[int, str]`. # error: [invalid-argument-type] "Argument to function `infer_keyword_only` is incorrect: Expected `KeywordOnlyCallback[*tuple[Unknown, ...]]`, found `def positional_or_keyword(x: int, y: str, flag: bool)`" reveal_type(infer_keyword_only(positional_or_keyword)) # revealed: tuple[Unknown, ...] -# TODO: Should reveal `tuple[int, str]`. -# error: [invalid-argument-type] "Argument to function `infer_keyword_only` is incorrect: Expected `KeywordOnlyCallback[*tuple[Unknown, ...]]`, found `def keyword_catch_all(x: int, y: str, **kwargs: object)`" -reveal_type(infer_keyword_only(keyword_catch_all)) # revealed: tuple[Unknown, ...] +reveal_type(infer_keyword_only(keyword_catch_all)) # revealed: tuple[int, str] class OptionalKeywordCallback[*Ts](Protocol): def __call__(self, *args: *Ts, flag: bool = False) -> None: ... @@ -616,9 +625,7 @@ def infer_optional_keyword[*Ts](callback: OptionalKeywordCallback[*Ts]) -> tuple def optional_keyword_callback(x: int, y: str, *, flag: bool = False) -> None: ... -# TODO: Should reveal `tuple[int, str]`. -# error: [invalid-argument-type] "Argument to function `infer_optional_keyword` is incorrect: Expected `OptionalKeywordCallback[*tuple[Unknown, ...]]`, found `def optional_keyword_callback(x: int, y: str, *, flag: bool = False)`" -reveal_type(infer_optional_keyword(optional_keyword_callback)) # revealed: tuple[Unknown, ...] +reveal_type(infer_optional_keyword(optional_keyword_callback)) # revealed: tuple[int, str] class PrefixedKeywordCallback[*Ts](Protocol): def __call__(self, prefix: bytes, *args: *Ts, flag: bool) -> None: ... @@ -629,9 +636,7 @@ def infer_prefixed[*Ts](callback: PrefixedKeywordCallback[*Ts]) -> tuple[*Ts]: def prefixed(prefix: bytes, x: int, y: str, *, flag: bool) -> None: ... def prefixed_variadic(prefix: bytes, *args: str, flag: bool) -> None: ... -# TODO: Should reveal `tuple[int, str]`. -# error: [invalid-argument-type] "Argument to function `infer_prefixed` is incorrect: Expected `PrefixedKeywordCallback[*tuple[Unknown, ...]]`, found `def prefixed(prefix: bytes, x: int, y: str, *, flag: bool)`" -reveal_type(infer_prefixed(prefixed)) # revealed: tuple[Unknown, ...] +reveal_type(infer_prefixed(prefixed)) # revealed: tuple[int, str] # An open-ended positional parameter can be inferred in an otherwise mixed signature. reveal_type(infer_prefixed(prefixed_variadic)) # revealed: tuple[str, ...] @@ -653,9 +658,7 @@ def infer_keyword_variadic[*Ts](callback: KeywordVariadicCallback[*Ts]) -> tuple def keyword_variadic(x: int, y: str, **kwargs: int) -> None: ... -# TODO: Should reveal `tuple[int, str]`. -# error: [invalid-argument-type] "Argument to function `infer_keyword_variadic` is incorrect: Expected `KeywordVariadicCallback[*tuple[Unknown, ...]]`, found `def keyword_variadic(x: int, y: str, **kwargs: int)`" -reveal_type(infer_keyword_variadic(keyword_variadic)) # revealed: tuple[Unknown, ...] +reveal_type(infer_keyword_variadic(keyword_variadic)) # revealed: tuple[int, str] class KeywordOnlyAndVariadicCallback[*Ts](Protocol): def __call__(self, *args: *Ts, flag: bool, **kwargs: int) -> None: ... @@ -667,9 +670,7 @@ def infer_keyword_only_and_variadic[*Ts]( def keyword_only_and_variadic(x: int, y: str, *, flag: bool, **kwargs: int) -> None: ... -# TODO: Should reveal `tuple[int, str]`. -# error: [invalid-argument-type] "Argument to function `infer_keyword_only_and_variadic` is incorrect: Expected `KeywordOnlyAndVariadicCallback[*tuple[Unknown, ...]]`, found `def keyword_only_and_variadic(x: int, y: str, *, flag: bool, **kwargs: int)`" -reveal_type(infer_keyword_only_and_variadic(keyword_only_and_variadic)) # revealed: tuple[Unknown, ...] +reveal_type(infer_keyword_only_and_variadic(keyword_only_and_variadic)) # revealed: tuple[int, str] class MultipleKeywordCallback[*Ts](Protocol): def __call__(self, *args: *Ts, first: int, second: str) -> None: ... @@ -679,9 +680,7 @@ def infer_multiple_keywords[*Ts](callback: MultipleKeywordCallback[*Ts]) -> tupl def multiple_keyword_catch_all(x: int, y: str, **kwargs: object) -> None: ... -# TODO: Should reveal `tuple[int, str]`. -# error: [invalid-argument-type] "Argument to function `infer_multiple_keywords` is incorrect: Expected `MultipleKeywordCallback[*tuple[Unknown, ...]]`, found `def multiple_keyword_catch_all(x: int, y: str, **kwargs: object)`" -reveal_type(infer_multiple_keywords(multiple_keyword_catch_all)) # revealed: tuple[Unknown, ...] +reveal_type(infer_multiple_keywords(multiple_keyword_catch_all)) # revealed: tuple[int, str] ``` ### Length-sensitive inference @@ -729,11 +728,11 @@ reveal_type(add_letters(Array[B, D]())) # revealed: Array[A, B, D, C] reveal_type(add_letter_a(Array[B, C]())) # revealed: Array[A, B, C] reveal_type(del_letter_a(Array[A, B]())) # revealed: Array[B] -# TODO: error: [invalid-argument-type] +# error: [invalid-argument-type] "Argument to function `del_letter_a` is incorrect: Expected `Array[A, C]`, found `Array[B, C]`" reveal_type(del_letter_a(Array[B, C]())) # revealed: Array[C] reveal_type(del_letter_c(Array[A, B, C]())) # revealed: Array[A, B] -# TODO: error: [invalid-argument-type] +# error: [invalid-argument-type] "Argument to function `del_letter_c` is incorrect: Expected `Array[A, C]`, found `Array[A, B]`" reveal_type(del_letter_c(Array[A, B]())) # revealed: Array[A] reveal_type(generic(A(), Array[B, D]())) # revealed: Array[A, B, D] @@ -792,8 +791,7 @@ def remove_bytes[*Prefix](*args: *tuple[*Prefix, bytes]) -> tuple[*Prefix]: accept_str_in_between(True, "phase", "status", b"ok") accept_str_in_between(True, b"ok") -# TODO: error: [invalid-argument-type] "Argument to function `accept_str_in_between` is incorrect: Expected `tuple[bool, *tuple[str, ...], bytes]`" -accept_str_in_between(True, 1, b"bad") +accept_str_in_between(True, 1, b"bad") # error: [invalid-argument-type] # TODO: Infer the `TypeVarTuple` from arguments matched to the variadic parameter. reveal_type(remove_bytes(1, "record", b"sum")) # revealed: tuple[Unknown, ...] @@ -937,15 +935,9 @@ def fn0(a: int) -> None: ... def fn1(a: int, b: str) -> None: ... def fn2(a: int, b: str, c: bytes) -> None: ... -# TODO: Should reveal `tuple[()]` without an error. -# error: [invalid-argument-type] "Argument to function `test` is incorrect: Expected `Alias[*tuple[int, *tuple[Unknown, ...]]]`, found `def fn0(a: int)`" -reveal_type(test(fn0)) # revealed: tuple[Unknown, ...] -# TODO: Should reveal `tuple[str]` without an error. -# error: [invalid-argument-type] "Argument to function `test` is incorrect: Expected `Alias[*tuple[int, *tuple[Unknown, ...]]]`, found `def fn1(a: int, b: str)`" -reveal_type(test(fn1)) # revealed: tuple[Unknown, ...] -# TODO: Should reveal `tuple[str, bytes]` without an error. -# error: [invalid-argument-type] "Argument to function `test` is incorrect: Expected `Alias[*tuple[int, *tuple[Unknown, ...]]]`, found `def fn2(a: int, b: str, c: bytes)`" -reveal_type(test(fn2)) # revealed: tuple[Unknown, ...] +reveal_type(test(fn0)) # revealed: tuple[()] +reveal_type(test(fn1)) # revealed: tuple[str] +reveal_type(test(fn2)) # revealed: tuple[str, bytes] ``` ### Indexing and iteration @@ -1026,8 +1018,7 @@ class Row[*Cells]: def f(pair: Row[int, str], triple: Row[int, str, bytes]) -> None: reveal_type(pair.get()) # revealed: Row[str, int] - # TODO: Should reveal `Row[str, bytes, int]`. - reveal_type(triple.get()) # revealed: Row[Never] + reveal_type(triple.get()) # revealed: Row[str, bytes, int] ``` ## Invalid Forms diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md index c494b36348..05d49f09b2 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md @@ -604,21 +604,17 @@ def f[ # 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.) +## Singletons An unbounded, unconstrained typevar is not a singleton, because it can be specialized to a non-singleton type. ```py from ty_extensions import static_assert -from ty_extensions._internal import is_singleton, is_single_valued +from ty_extensions._internal import is_singleton 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 @@ -627,7 +623,6 @@ 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 @@ -638,13 +633,9 @@ 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 diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md index 0dee561e4f..5808327c72 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md @@ -6,9 +6,11 @@ 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.) +relations. Much more detail can be found in the [spec]. PEP 695 defines inferred variance as +**covariant**, **contravariant**, or **invariant**. We also represent **bivariance** internally, for +cases where varying a type parameter does not change the type. For PEP 695 parameters, we report +these cases as covariant, matching the spec's inference algorithm when assignment is valid in both +directions. 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`. @@ -27,8 +29,8 @@ Types that "produce" data on demand are covariant in their typevar. If you expec get from the sequence is a valid `int`. ```py -from ty_extensions import static_assert, Unknown -from ty_extensions._internal import is_assignable_to, is_equivalent_to, is_subtype_of +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_assignable_to, is_equivalent_to, is_subtype_of from typing import Any, Never class A: ... @@ -106,8 +108,8 @@ Types that "consume" data are contravariant in their typevar. If you expect a co that you pass into the consumer is a valid `int`. ```py -from ty_extensions import static_assert, Unknown -from ty_extensions._internal import is_assignable_to, is_equivalent_to, is_subtype_of +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_assignable_to, is_equivalent_to, is_subtype_of from typing import Any, Never class A: ... @@ -214,8 +216,8 @@ In the end, if you expect a mutable list, you must always be given a list of exa since we can't know in advance which of the allowed methods you'll want to use. ```py -from ty_extensions import static_assert, Unknown -from ty_extensions._internal import is_assignable_to, is_equivalent_to, is_subtype_of +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_assignable_to, is_equivalent_to, is_subtype_of from typing import Any, Never class A: ... @@ -284,19 +286,14 @@ static_assert(not is_equivalent_to(D[Any], C[Any])) static_assert(not is_equivalent_to(D[Any], C[Unknown])) ``` -## Bivariance +## Bivariant Fallback -With a bivariant typevar, _all_ specializations of the generic class are assignable to (and in fact, -gradually equivalent to) each other, and all 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.) +If inference for a PEP 695 type parameter would otherwise conclude bivariance because the type +parameter is unused, we fall back to covariance instead. ```py -from ty_extensions import static_assert, Unknown -from ty_extensions._internal import is_assignable_to, is_equivalent_to, is_subtype_of +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_assignable_to, is_equivalent_to, is_subtype_of from typing import Any, Never class A: ... @@ -309,7 +306,7 @@ class D[U](C[U]): pass static_assert(is_assignable_to(C[B], C[A])) -static_assert(is_assignable_to(C[A], C[B])) +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])) @@ -317,37 +314,37 @@ static_assert(is_assignable_to(C[Any], C[B])) static_assert(is_assignable_to(D[B], C[A])) static_assert(is_subtype_of(C[A], C[A])) -static_assert(is_assignable_to(D[A], C[B])) +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[B])) -static_assert(is_subtype_of(C[A], C[Any])) -static_assert(is_subtype_of(C[B], C[Any])) -static_assert(is_subtype_of(C[Any], C[A])) -static_assert(is_subtype_of(C[Any], C[B])) -static_assert(is_subtype_of(C[Any], C[Any])) -static_assert(is_subtype_of(C[object], C[Any])) -static_assert(is_subtype_of(C[Any], C[Never])) +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(not is_subtype_of(C[object], C[Any])) +static_assert(not is_subtype_of(C[Any], C[Never])) static_assert(is_subtype_of(D[B], C[A])) -static_assert(is_subtype_of(D[A], C[B])) -static_assert(is_subtype_of(D[A], C[Any])) -static_assert(is_subtype_of(D[B], C[Any])) -static_assert(is_subtype_of(D[Any], C[A])) -static_assert(is_subtype_of(D[Any], C[B])) +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(is_equivalent_to(C[B], C[A])) -static_assert(is_equivalent_to(C[A], C[B])) -static_assert(is_equivalent_to(C[A], C[Any])) -static_assert(is_equivalent_to(C[B], C[Any])) -static_assert(is_equivalent_to(C[Any], C[A])) -static_assert(is_equivalent_to(C[Any], 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])) @@ -405,11 +402,11 @@ of that instance affect its variance. from ty_extensions import static_assert from ty_extensions._internal import is_subtype_of -class Bivariant[T]: - def takes_int_self(self, value: Bivariant[int]): ... +class WouldBeBivariant[T]: + def takes_int_self(self, value: WouldBeBivariant[int]): ... -static_assert(is_subtype_of(Bivariant[int], Bivariant[object])) -static_assert(is_subtype_of(Bivariant[object], Bivariant[int])) +static_assert(is_subtype_of(WouldBeBivariant[int], WouldBeBivariant[object])) +static_assert(not is_subtype_of(WouldBeBivariant[object], WouldBeBivariant[int])) class Covariant[T]: def get(self) -> T: @@ -1013,10 +1010,11 @@ class C[T]: def __new__(self, x: T): ... static_assert(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[B])) ``` -This example is then bivariant because it doesn't use `T` outside of the two exempted methods. +This example would otherwise be bivariant because it doesn't use `T` outside of the two exempted +methods, so we fall back to covariance. This holds likewise for dataclasses with synthesized `__init__`: @@ -1095,7 +1093,8 @@ static_assert(not is_assignable_to(Intersection[C, Not[B]], Intersection[C, Not[ ## Subclass Types (type[T]) The `type[T]` construct represents the type of classes that are subclasses of `T`. It is covariant -in `T` because if `A <: B`, then `type[A] <: type[B]` holds. +in `T` because if `A <: B`, then `type[A] <: type[B]` holds. A public, writable `type[T]` attribute +still makes its enclosing class invariant, while a private attribute can remain covariant. ```py from ty_extensions import static_assert @@ -1114,10 +1113,10 @@ static_assert(not is_assignable_to(type[A], type[B])) # With generic classes using type[T] class ClassContainer[T]: def __init__(self, cls: type[T]) -> None: - self.cls = cls + self._cls = cls def create_instance(self) -> T: - return self.cls() + return self._cls() # ClassContainer is covariant in T due to type[T] static_assert(is_subtype_of(ClassContainer[B], ClassContainer[A])) @@ -1135,6 +1134,64 @@ b_container = ClassContainer[B](B) a_instance: A = use_a_class_container(b_container) # This should work ``` +## Subclass types in writable attributes + +A writable public `type[T]` attribute makes its enclosing class invariant in `T`. + +```py +from ty_extensions import static_assert +from ty_extensions._internal import is_assignable_to, is_subtype_of + +class ClassContainer[T]: + cls: type[T] + +static_assert(not is_subtype_of(ClassContainer[int], ClassContainer[object])) +static_assert(not is_subtype_of(ClassContainer[object], ClassContainer[int])) + +static_assert(not is_assignable_to(ClassContainer[int], ClassContainer[object])) +static_assert(not is_assignable_to(ClassContainer[object], ClassContainer[int])) +``` + +## Subclass types in return positions + +A `type[T]` return contributes covariance for `T`. Combining it with a method that accepts `T` +therefore makes the enclosing class invariant. + +```py +from ty_extensions import static_assert +from ty_extensions._internal import is_assignable_to, is_subtype_of + +class ClassContainer[T]: + def get(self) -> type[T]: + raise NotImplementedError + + def put(self, value: T) -> None: ... + +static_assert(not is_subtype_of(ClassContainer[int], ClassContainer[object])) +static_assert(not is_subtype_of(ClassContainer[object], ClassContainer[int])) + +static_assert(not is_assignable_to(ClassContainer[int], ClassContainer[object])) +static_assert(not is_assignable_to(ClassContainer[object], ClassContainer[int])) +``` + +## Subclass types in parameter positions + +A method parameter annotated as `type[T]` makes the enclosing class contravariant in `T`. + +```py +from ty_extensions import static_assert +from ty_extensions._internal import is_assignable_to, is_subtype_of + +class ClassContainer[T]: + def put(self, cls: type[T]) -> None: ... + +static_assert(is_subtype_of(ClassContainer[object], ClassContainer[int])) +static_assert(not is_subtype_of(ClassContainer[int], ClassContainer[object])) + +static_assert(is_assignable_to(ClassContainer[object], ClassContainer[int])) +static_assert(not is_assignable_to(ClassContainer[int], ClassContainer[object])) +``` + ## TypeIs ```toml @@ -1252,17 +1309,17 @@ static_assert(not is_subtype_of(InvariantLiteral1, InvariantInt)) static_assert(not is_subtype_of(MyInvariant[Literal[1]], MyInvariant[int])) static_assert(not is_subtype_of(MyInvariant[int], MyInvariant[Literal[1]])) -class Bivariant[T]: +class WouldBeBivariant[T]: pass -type BivariantLiteral1 = Bivariant[Literal[1]] -type BivariantInt = Bivariant[int] -type MyBivariant[T] = Bivariant[T] +type WouldBeBivariantLiteral1 = WouldBeBivariant[Literal[1]] +type WouldBeBivariantInt = WouldBeBivariant[int] +type MyWouldBeBivariant[T] = WouldBeBivariant[T] -static_assert(is_subtype_of(BivariantInt, BivariantLiteral1)) -static_assert(is_subtype_of(BivariantLiteral1, BivariantInt)) -static_assert(is_subtype_of(MyBivariant[Literal[1]], MyBivariant[int])) -static_assert(is_subtype_of(MyBivariant[int], MyBivariant[Literal[1]])) +static_assert(not is_subtype_of(WouldBeBivariantInt, WouldBeBivariantLiteral1)) +static_assert(is_subtype_of(WouldBeBivariantLiteral1, WouldBeBivariantInt)) +static_assert(is_subtype_of(MyWouldBeBivariant[Literal[1]], MyWouldBeBivariant[int])) +static_assert(not is_subtype_of(MyWouldBeBivariant[int], MyWouldBeBivariant[Literal[1]])) ``` ## Inheriting from generic classes with inferred variance @@ -1507,7 +1564,6 @@ error[invalid-generic-class]: Variance of type variable `T` is incompatible with | 2 | class BadProducer[out T]: | ^^^^^ - | help: Type variable `T` is declared as covariant, but `BadProducer` uses it contravariantly ``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/scoping.md b/crates/ty_python_semantic/resources/mdtest/generics/scoping.md index 814dc4af70..927cd71d32 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/scoping.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/scoping.md @@ -310,7 +310,6 @@ error[shadowed-type-variable]: Generic function `bad` uses TypeVarTuple `Ts` alr | 1 | def outer[*Ts](*args: *Ts) -> None: | ------------------------------ TypeVarTuple `Ts` is bound in this enclosing scope - | ``` ### Generic method within generic class 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 index 43c5eccf57..038af00f21 100644 --- a/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md +++ b/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md @@ -675,6 +675,32 @@ static_assert(has_member(module, "evaluate")) static_assert(not has_member(module, "Optional")) ``` +### Private typing-only stub members + +Typing-only helpers in stubs remain available as module members for autocomplete. + +`module.pyi`: + +```pyi +from typing import TypeAlias, TypeVar + +_Alias: TypeAlias = int +_T = TypeVar("_T") +_runtime: int +``` + +`main.py`: + +```py +import module +from ty_extensions import static_assert +from ty_extensions._internal import has_member + +static_assert(has_member(module, "_runtime")) +static_assert(has_member(module, "_Alias")) +static_assert(has_member(module, "_T")) +``` + ## Conditionally available members Some members are only conditionally available. For example, `bytearray.take_bytes` was only diff --git a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md index 6170ff29d5..e6469d5393 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -34,7 +34,7 @@ We also support unions in type aliases: ```py from typing_extensions import Any, Never, Literal, LiteralString, Tuple, Annotated, Optional, Union, Callable, TypeVar -from ty_extensions import Unknown +from ty_extensions._internal import Unknown T = TypeVar("T") @@ -881,7 +881,6 @@ error[not-subscriptable]: Cannot subscript non-generic type alias `ListOfInts2` | -----------^^^^^ | | | Alias to `list[int]`, which is already specialized - | ``` ```py @@ -899,7 +898,6 @@ error[not-subscriptable]: Cannot subscript non-generic type `` | ---------^^^^^ | | | Type is already specialized - | ``` ### Multiple definitions @@ -1357,6 +1354,220 @@ def _( reveal_type(invalid_subclass_of_literal) # revealed: ``` +### Subscripted generic alias inside `type[…]` + +A generic alias can also be specialized inside a `type[…]` annotation. + +#### Valid specializations + +The PEP 613 spelling and `typing.Type[…]` take the same path: + +```py +from typing import Generic, Type, TypeAlias, TypeVar + +T = TypeVar("T") +U = TypeVar("U") + +class Pair(Generic[T, U]): ... + +PairAlias = Pair[T, U] +PairAliasExplicit: TypeAlias = Pair[T, U] + +def implicit(x: type[PairAlias[int, str]]): + reveal_type(x) # revealed: type[Pair[int, str]] + +def pep_613(x: type[PairAliasExplicit[int, str]]): + reveal_type(x) # revealed: type[Pair[int, str]] + +def uppercase_type(x: Type[PairAlias[int, str]]): + reveal_type(x) # revealed: type[Pair[int, str]] + +def partially_specialized(x: type[PairAlias[int, T]]): + reveal_type(x) # revealed: type[Pair[int, T@partially_specialized]] +``` + +#### Incorrect type-argument counts + +A generic alias specialized inside `type[…]` must receive the correct number of type arguments: + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") +U = TypeVar("U") + +class Pair(Generic[T, U]): ... + +PairAlias = Pair[T, U] + +def _( + # error: [invalid-type-arguments] "No type argument provided for required type variable `U`" + too_few: type[PairAlias[int]], + # error: [invalid-type-arguments] "Too many type arguments: expected 2, got 3" + too_many: type[PairAlias[int, str, bool]], +): + reveal_type(too_few) # revealed: type[Pair[Unknown, Unknown]] + reveal_type(too_many) # revealed: type[Pair[Unknown, Unknown]] +``` + +#### Type-variable bounds + +Specializing an alias inside `type[…]` enforces the upper bound of its type variable: + +```py +from typing import Generic, TypeVar + +Bounded = TypeVar("Bounded", bound=int) + +class BoundedBox(Generic[Bounded]): ... + +BoundedAlias = BoundedBox[Bounded] + +def _( + # error: [invalid-type-arguments] "Type `str` is not assignable to upper bound `int` of type variable `Bounded@BoundedAlias`" + violated_bound: type[BoundedAlias[str]], +): + reveal_type(violated_bound) # revealed: type[BoundedBox[Unknown]] +``` + +#### Type-variable constraints + +Specializing an alias inside `type[…]` also enforces constraints on its type variable: + +```py +from typing import Generic, TypeVar + +Constrained = TypeVar("Constrained", int, str) + +class ConstrainedBox(Generic[Constrained]): ... + +ConstrainedAlias = ConstrainedBox[Constrained] + +def _( + # error: [invalid-type-arguments] "Type `bytes` does not satisfy constraints `int`, `str` of type variable `Constrained@ConstrainedAlias`" + violated_constraint: type[ConstrainedAlias[bytes]], +): + reveal_type(violated_constraint) # revealed: type[ConstrainedBox[Unknown]] +``` + +#### Bounds on union-valued aliases + +The upper bound of a type variable is enforced even when its alias resolves to a union: + +```py +from typing import TypeVar + +Bounded = TypeVar("Bounded", bound=int) +BoundedUnionAlias = list[Bounded] | set[Bounded] + +def _( + # error: [invalid-type-arguments] "Type `str` is not assignable to upper bound `int` of type variable `Bounded@BoundedUnionAlias`" + union_violated_bound: type[BoundedUnionAlias[str]], +): + reveal_type(union_violated_bound) # revealed: type[list[Unknown] | set[Unknown]] +``` + +#### Invalid nested subscripts + +Subscripting an already-subscripted alias inside `type[…]` is invalid, just as it is outside it: + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") +U = TypeVar("U") + +class Pair(Generic[T, U]): ... + +PairAlias = Pair[T, U] + +def _( + # error: [invalid-type-form] "Only simple names and dotted names can be subscripted in parameter annotations" + double_subscript: type[PairAlias[T, U][int, str]], +): + reveal_type(double_subscript) # revealed: type[Unknown] +``` + +#### Assignments to class-backed aliases + +An object assigned to a specialized alias inside `type[…]` must match the class it represents: + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") +U = TypeVar("U") + +class Pair(Generic[T, U]): ... + +PairAlias = Pair[T, U] + +# error: [invalid-assignment] "Object of type `` is not assignable to `type[Pair[int, str]]`" +assigned: type[PairAlias[int, str]] = int +``` + +#### Assignments to union-valued aliases + +An object assigned to a union-valued alias inside `type[…]` must match one of the union elements: + +```py +from typing import TypeVar + +T = TypeVar("T") +UnionAlias = list[T] | set[T] + +# error: [invalid-assignment] "Object of type `` is not assignable to `type[list[int] | set[int]]`" +assigned_union: type[UnionAlias[int]] = str +``` + +#### Other alias representations + +An alias does not have to be backed by a class. Stringified, transparent, `Annotated` and +union-valued aliases all specialize inside `type[…]` the same way they do outside it: + +```py +from __future__ import annotations + +from typing import Annotated, TypeAlias, TypeVar + +T = TypeVar("T") + +StringAlias: TypeAlias = "list[T]" +TransparentAlias: TypeAlias = T +AnnotatedAlias = Annotated[list[T], "metadata"] +UnionAlias = list[T] | set[T] + +def _( + string: type[StringAlias[int]], + transparent: type[TransparentAlias[int]], + annotated: type[AnnotatedAlias[int]], + union: type[UnionAlias[int]], +): + reveal_type(string) # revealed: type[list[int]] + reveal_type(transparent) # revealed: type[int] + reveal_type(annotated) # revealed: type[list[int]] + reveal_type(union) # revealed: type[list[int] | set[int]] +``` + +#### Callable aliases + +A callable is not a class object, so specializing a `Callable` alias inside `type[…]` is rejected, +just as a directly spelled callable is: + +```py +from typing import Callable, TypeVar + +T = TypeVar("T") + +CallableAlias = Callable[[T], T] + +def _( + # error: [invalid-type-form] "The argument to `type[]` must be a class object type" + callable_: type[CallableAlias[int]], +): + reveal_type(callable_) # revealed: type[Unknown] +``` + ### `Type[…]` The same also works for `typing.Type[…]`: @@ -1744,7 +1955,8 @@ RecursiveList1 = list["RecursiveList1 | None"] RecursiveList2 = List["RecursiveList2 | None"] RecursiveDict1 = dict[str, "RecursiveDict1 | None"] RecursiveDict2 = Dict[str, "RecursiveDict2 | None"] -RecursiveDict3 = dict["RecursiveDict3", int] +# a dict is not `Hashable`, so a dict keyed by itself violates `dict`'s key bound +RecursiveDict3 = dict["RecursiveDict3", int] # error: [invalid-type-arguments] RecursiveDict4 = Dict["RecursiveDict4", int] def _( @@ -1759,7 +1971,7 @@ def _( reveal_type(recursive_list2) # revealed: list[Divergent] reveal_type(recursive_dict1) # revealed: dict[str, Divergent] reveal_type(recursive_dict2) # revealed: dict[str, Divergent] - reveal_type(recursive_dict3) # revealed: dict[Divergent, int] + reveal_type(recursive_dict3) # revealed: dict[Unknown, int] reveal_type(recursive_dict4) # revealed: dict[Divergent, int] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/import/builtins.md b/crates/ty_python_semantic/resources/mdtest/import/builtins.md index ab0eba432f..5669117976 100644 --- a/crates/ty_python_semantic/resources/mdtest/import/builtins.md +++ b/crates/ty_python_semantic/resources/mdtest/import/builtins.md @@ -19,6 +19,209 @@ reveal_type(chr) # revealed: def chr(i: SupportsIndex, /) -> str reveal_type(str) # revealed: ``` +## Private type-checking-only builtin helpers are not implicit builtins + +Private type variables, type aliases, and type-checking-only definitions in a `builtins` stub are +implementation details. They must not be available without an explicit import. + +```toml +[environment] +typeshed = "/typeshed" +``` + +`/typeshed/stdlib/typing.pyi`: + +```pyi +class TypeVar: + def __new__(cls, name): ... + +class ParamSpec: + def __new__(cls, name): ... + +class Protocol: ... +class _SpecialForm: ... + +TypeAlias: _SpecialForm + +def type_check_only(obj): ... +``` + +`/typeshed/stdlib/builtins.pyi`: + +```pyi +from typing import ParamSpec, Protocol, TypeAlias, TypeVar, type_check_only + +class object: ... +class int: ... + +_T = TypeVar("_T") +_P = ParamSpec("_P") +_PrivateAlias: TypeAlias = int + +@type_check_only +class _PrivateProtocol(Protocol): ... + +@type_check_only +class PublicTypeOnlyClass: ... + +@type_check_only +def public_type_only_function(): ... +``` + +`module.py`: + +```py +_T # error: [unresolved-reference] +_P # error: [unresolved-reference] +_PrivateAlias # error: [unresolved-reference] +_PrivateProtocol # error: [unresolved-reference] +PublicTypeOnlyClass # error: [unresolved-reference] +public_type_only_function # error: [unresolved-reference] +``` + +## Explicitly importing private builtin helpers + +We still allow users to explicitly import implementation details from the `builtins` module. + +```toml +[environment] +typeshed = "/typeshed" +``` + +`/typeshed/stdlib/typing.pyi`: + +```pyi +class TypeVar: + def __new__(cls, name): ... + +class Protocol: ... +class _SpecialForm: ... + +TypeAlias: _SpecialForm + +def type_check_only(obj): ... +``` + +`/typeshed/stdlib/builtins.pyi`: + +```pyi +from typing import Protocol, TypeAlias, TypeVar, type_check_only + +class object: ... +class int: ... + +_T = TypeVar("_T") +_PrivateAlias: TypeAlias = int + +@type_check_only +class _PrivateProtocol(Protocol): ... + +@type_check_only +class PublicTypeOnlyClass: ... +``` + +`module.py`: + +```py +from builtins import PublicTypeOnlyClass, _PrivateAlias, _PrivateProtocol, _T + +_T +_PrivateAlias +_PrivateProtocol +PublicTypeOnlyClass +``` + +## Private project-level builtins + +A project-level `__builtins__.pyi` can deliberately provide private runtime names, including names +that overlap with private helpers in the standard `builtins` stub. + +```py +reveal_type(_private_value) # revealed: int +reveal_type(_T_co) # revealed: int + +_PrivateTypeVar # error: [unresolved-reference] +_PrivateAlias # error: [unresolved-reference] +_PrivateTypeOnlyProtocol # error: [unresolved-reference] +_PrivateTypeCheckingProtocol # error: [unresolved-reference] + +_RuntimeProtocol +_runtime_typevar +``` + +`__builtins__.pyi`: + +```pyi +from typing import TYPE_CHECKING, Protocol, TypeAlias, TypeVar, type_check_only + +_private_value: int +_T_co: int + +_PrivateTypeVar = TypeVar("_PrivateTypeVar") +_PrivateAlias: TypeAlias = int + +@type_check_only +class _PrivateTypeOnlyProtocol(Protocol): ... + +if TYPE_CHECKING: + class _PrivateTypeCheckingProtocol(Protocol): ... + +class _RuntimeProtocol(Protocol): ... + +def make_typevar() -> TypeVar: ... + +_runtime_typevar = make_typevar() +``` + +## Private type-checking-only builtins with stacked decorators + +An outer decorator can change the inferred type of a private function or class, but it does not make +an inner `@type_check_only` definition available at runtime. + +```py +_PrivateFunction # error: [unresolved-reference] +_PrivateClass # error: [unresolved-reference] +``` + +`__builtins__.pyi`: + +```pyi +from typing import Callable, type_check_only + +def decorate_function(callback: Callable[[int], int]) -> Callable[[int], int]: ... +def decorate_class(cls: type[object]) -> type[object]: ... +@decorate_function +@type_check_only +def _PrivateFunction(value: int) -> int: ... + +@decorate_class +@type_check_only +class _PrivateClass: ... +``` + +## Private runtime standard builtins + +A private class declared by the standard `builtins` stub remains available when it represents a real +runtime builtin, rather than a type-checking-only helper. + +```toml +[environment] +typeshed = "/typeshed" +``` + +`/typeshed/stdlib/builtins.pyi`: + +```pyi +class object: ... +class _IncompleteInputError: ... +``` + +`module.py`: + +```py +_IncompleteInputError +``` + ## Builtin symbol from custom typeshed If we specify a custom typeshed, we can use the builtin symbol from it, and no longer access the diff --git a/crates/ty_python_semantic/resources/mdtest/import/star.md b/crates/ty_python_semantic/resources/mdtest/import/star.md index 518c28f59a..af61878a35 100644 --- a/crates/ty_python_semantic/resources/mdtest/import/star.md +++ b/crates/ty_python_semantic/resources/mdtest/import/star.md @@ -425,40 +425,51 @@ print(K) print(L) ``` -### Definitions in function-like scopes are not global definitions +### Comprehension and lambda locals are not global definitions -Except for some cases involving walrus expressions inside comprehension scopes. +Comprehension iteration variables, lambda parameters, and assignments inside lambdas are not module +globals and are therefore not available to a wildcard import. `exporter.py`: ```py -class Iterator: - def __next__(self) -> int: - return 42 +[a for a in [1]] +{b for b in [1]} +{c: c for c in [1]} +(d for d in [1]) +lambda e: (f := 42) +[(lambda s=s: (t := 42))() for s in [1]] +``` -class Iterable: - def __iter__(self) -> Iterator: - return Iterator() +`importer.py`: -[a for a in Iterable()] -{b for b in Iterable()} -{c: c for c in Iterable()} -(d for d in Iterable()) -lambda e: (f := 42) +```py +from exporter import * + +a # error: [unresolved-reference] +b # error: [unresolved-reference] +c # error: [unresolved-reference] +d # error: [unresolved-reference] +e # error: [unresolved-reference] +f # error: [unresolved-reference] +s # error: [unresolved-reference] +t # error: [unresolved-reference] +``` -# Definitions created by walruses in a comprehension scope are unique; -# they "leak out" of the scope and are stored in the surrounding scope -[(g := h * 2) for h in Iterable()] -[i for j in Iterable() if (i := j - 10) > 0] -{(k := l * 2): (m := l * 3) for l in Iterable()} -list(((o := p * 2) for p in Iterable())) +### Assignment-expression targets in comprehensions are global definitions -# A walrus expression nested inside several scopes *still* leaks out -# to the global scope: -[[[[(q := r) for r in Iterable()]] for _ in range(42)] for _ in range(42)] +Assignment-expression targets bind in the scope containing the comprehension. At module level, +targets in an element, filter, dictionary key or value, generator expression, or nested +comprehension are all available to a wildcard import. -# A walrus inside a lambda inside a comprehension does not leak out -[(lambda s=s: (t := 42))() for s in Iterable()] +`exporter.py`: + +```py +[(list_value := item) for item in [1]] +[item for item in [1] if (filtered_value := item - 10) > 0] +{(dict_key := item * 2): (dict_value := item * 3) for item in [1]} +list((generator_value := item * 2) for item in [1]) +[[[[(nested_value := item) for item in [1]]] for _ in [1]] for _ in [1]] ``` `importer.py`: @@ -466,47 +477,12 @@ list(((o := p * 2) for p in Iterable())) ```py from exporter import * -# error: [unresolved-reference] -reveal_type(a) # revealed: Unknown -# error: [unresolved-reference] -reveal_type(b) # revealed: Unknown -# error: [unresolved-reference] -reveal_type(c) # revealed: Unknown -# error: [unresolved-reference] -reveal_type(d) # revealed: Unknown -# error: [unresolved-reference] -reveal_type(e) # revealed: Unknown -# error: [unresolved-reference] -reveal_type(f) # revealed: Unknown -# error: [unresolved-reference] -reveal_type(h) # revealed: Unknown -# error: [unresolved-reference] -reveal_type(j) # revealed: Unknown -# error: [unresolved-reference] -reveal_type(p) # revealed: Unknown -# error: [unresolved-reference] -reveal_type(r) # revealed: Unknown -# error: [unresolved-reference] -reveal_type(s) # revealed: Unknown -# error: [unresolved-reference] -reveal_type(t) # revealed: Unknown - -# TODO: these should all reveal `Unknown | int` and should not emit errors. -# (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] -reveal_type(g) # revealed: Unknown -# error: [unresolved-reference] -reveal_type(i) # revealed: Unknown -# error: [unresolved-reference] -reveal_type(k) # revealed: Unknown -# error: [unresolved-reference] -reveal_type(m) # revealed: Unknown -# error: [unresolved-reference] -reveal_type(o) # revealed: Unknown -# error: [unresolved-reference] -reveal_type(q) # revealed: Unknown +reveal_type(list_value) # revealed: int +reveal_type(filtered_value) # revealed: int +reveal_type(dict_key) # revealed: int +reveal_type(dict_value) # revealed: int +reveal_type(generator_value) # revealed: int +reveal_type(nested_value) # revealed: int ``` ### An annotation without a value is a definition in a stub but not a `.py` file diff --git a/crates/ty_python_semantic/resources/mdtest/intersection_types.md b/crates/ty_python_semantic/resources/mdtest/intersection_types.md index e0f35c42bf..65f70cdfeb 100644 --- a/crates/ty_python_semantic/resources/mdtest/intersection_types.md +++ b/crates/ty_python_semantic/resources/mdtest/intersection_types.md @@ -449,6 +449,27 @@ def example_type_bool_type_str( reveal_type(i) # revealed: Never ``` +Ordinary types accept values with any `NewType` tag, so an integer-based `NewType` can overlap +`bool`. Distinct `NewType` tags are mutually exclusive even when their runtime values overlap; +nested `NewType`s retain their relationship with their parent. + +```py +from typing import NewType + +UserId = NewType("UserId", int) +OtherUserId = NewType("OtherUserId", int) +NestedUserId = NewType("NestedUserId", UserId) + +def newtype_intersections( + user_bool: UserId & bool, + user_nested: UserId & NestedUserId, + user_other: UserId & OtherUserId, +) -> None: + reveal_type(user_bool) # revealed: UserId & bool + reveal_type(user_nested) # revealed: NestedUserId + reveal_type(user_other) # revealed: Never +``` + #### Positive and negative contributions If we intersect a type `X` with the negation `~Y` of a disjoint type `Y`, we can remove the negative @@ -727,7 +748,8 @@ simplified, due to the fact that a `LiteralString` inhabitant is known to have ` exactly `str` (and not a subclass of `str`): ```py -from ty_extensions import AlwaysTruthy, AlwaysFalsy, Unknown +from ty_extensions import AlwaysTruthy, AlwaysFalsy +from ty_extensions._internal import Unknown from typing_extensions import LiteralString def f( @@ -820,13 +842,60 @@ def _(e: (Single | int) & ~Single) -> None: reveal_type(e) # revealed: int ``` +A `NewType` is preserved when all but one member of its underlying enum are excluded. The resulting +intersection is also assignable to the remaining member. + +```pyi +from typing import NewType +from ty_extensions import static_assert +from ty_extensions._internal import is_assignable_to, is_equivalent_to + +ColorId = NewType("ColorId", Color) +NestedColorId = NewType("NestedColorId", ColorId) +type NestedAlias = NestedColorId + +def enum_newtype(value: ColorId & ~(Red | Green), nested: NestedAlias & ~Red & ~Green) -> None: + reveal_type(value) # revealed: ColorId & Literal[Color.BLUE] + reveal_type(nested) # revealed: NestedColorId & Literal[Color.BLUE] + +static_assert(is_assignable_to(ColorId & ~(Red | Green), ColorId)) +static_assert(is_assignable_to(ColorId & ~(Red | Green), Blue)) +static_assert(is_equivalent_to(ColorId & ~(Red | Green), ColorId & Blue)) +``` + +Aliases name the same enum member, while `Flag` members are not exhaustive. + +```pyi +from enum import Flag + +class Aliased(Enum): + FIRST = 1 + FIRST_ALIAS = 1 + LAST = 2 + +AliasedId = NewType("AliasedId", Aliased) + +def aliased_member(value: AliasedId & ~Literal[Aliased.FIRST_ALIAS]) -> None: + reveal_type(value) # revealed: AliasedId & Literal[Aliased.LAST] + +class Permission(Flag): + READ = 1 + WRITE = 2 + +PermissionId = NewType("PermissionId", Permission) + +def non_exhaustive(value: PermissionId & ~Literal[Permission.READ]) -> None: + reveal_type(value) # revealed: PermissionId & ~Literal[Permission.READ] +``` + ## Addition of a type to an intersection with many non-disjoint types This slightly strange-looking test is a regression test for a mistake that was nearly made in a PR: . ```py -from ty_extensions import AlwaysFalsy, Unknown +from ty_extensions import AlwaysFalsy +from ty_extensions._internal import Unknown from typing_extensions import Literal def _(x: str & Unknown & AlwaysFalsy & Literal[""]): @@ -842,7 +911,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 ty_extensions import Unknown +from ty_extensions._internal import Unknown from typing_extensions import Any, Never class P: ... @@ -872,7 +941,7 @@ The intersection of an unknown set of runtime values with (another) unknown set still an unknown set of runtime values: ```py -from ty_extensions import Unknown +from ty_extensions._internal import Unknown from typing_extensions import Any class P: ... @@ -907,7 +976,7 @@ of another unknown set of values is not necessarily empty, so we keep the positi ```py from typing import Any -from ty_extensions import Unknown +from ty_extensions._internal import Unknown def any( i1: Any & ~Any, @@ -930,7 +999,7 @@ Gradually-equivalent types can be simplified out of intersections: ```py from typing import Any -from ty_extensions import Unknown +from ty_extensions._internal import Unknown def mixed( i1: Any & Unknown, @@ -1005,6 +1074,45 @@ def _( x(1.0) ``` +### Constructor intersection diagnostics retain the called class types + +When an intersection of class objects rejects a constructor call, the diagnostic should describe the +original class types instead of reconstructing an intersection from their `__init__` and `__new__` +methods. + +```py +from typing import Self + +class UsesInit: + def __init__(self, value: int) -> None: ... + +class UsesNew: + def __new__(cls, value: str) -> Self: + return object.__new__(cls) + +def _(cls: type[UsesInit]) -> None: + if issubclass(cls, UsesNew): + reveal_type(cls) # revealed: type[UsesInit] & type[UsesNew] + # error: [invalid-argument-type] "class `UsesNew`" + # snapshot: invalid-argument-type + cls(None) +``` + +```snapshot +error[invalid-argument-type]: Argument to class `UsesInit` is incorrect + --> src/mdtest_snippet.py:15:13 + | +15 | cls(None) + | ^^^^ Expected `int`, found `None` +info: Method defined here + --> src/mdtest_snippet.py:4:9 + | +4 | def __init__(self, value: int) -> None: ... + | ^^^^^^^^ ---------- Parameter declared here +info: Intersection element `bound method UsesInit.__init__(value: int)` is incompatible with this call site +info: Attempted to call intersection type `type[UsesInit] & type[UsesNew]` +``` + ### Error priority: binding error over top-callable When intersection elements fail with different error types, we use a priority hierarchy to determine @@ -1349,7 +1457,7 @@ For any gradual type `G`, `Invariant[G] & Invariant[Any] = Invariant[G]`. ```py from typing import Any -from ty_extensions import Unknown +from ty_extensions._internal import Unknown class P: ... class Q: ... diff --git a/crates/ty_python_semantic/resources/mdtest/invalid_syntax.md b/crates/ty_python_semantic/resources/mdtest/invalid_syntax.md index 53acc4fbe3..b11b9c7e9e 100644 --- a/crates/ty_python_semantic/resources/mdtest/invalid_syntax.md +++ b/crates/ty_python_semantic/resources/mdtest/invalid_syntax.md @@ -40,7 +40,6 @@ type pass = 1 # error: [invalid-syntax] # error: [invalid-syntax] def True(for): - # error: [invalid-syntax] # error: [invalid-syntax] pass ``` @@ -76,7 +75,6 @@ match while: # error: [invalid-syntax] # error: [unresolved-reference] "Name `case` used when not defined" case in: - # error: [invalid-syntax] # error: [invalid-syntax] pass ``` @@ -108,6 +106,112 @@ out = (obj.attr := obj).attr out = (obj[0] := obj).attr ``` +## Match-pattern alternatives binding different names + +A capture present in only one invalid `or` alternative is possibly undefined. + +```py +match 0: + case first | second: # error: [invalid-syntax] "alternative patterns bind different names" + first # error: [possibly-unresolved-reference] + second # error: [possibly-unresolved-reference] +``` + +## Match-pattern alternative without a binding + +A capture missing from one alternative is possibly undefined, regardless of alternative order. + +```py +match (0,): + # error: [invalid-syntax] "alternative patterns bind different names" + case [first_value] | []: + first_value # error: [possibly-unresolved-reference] +``` + +An alternative without a capture can also occur first. + +```py +match (0,): + # error: [invalid-syntax] "alternative patterns bind different names" + case [] | [last_value]: + last_value # error: [possibly-unresolved-reference] +``` + +A capture limited to the middle of three alternatives also remains possibly undefined. + +```py +match (0,): + # error: [invalid-syntax] "alternative patterns bind different names" + case [] | [middle_value] | []: + middle_value # error: [possibly-unresolved-reference] +``` + +## Previously bound match-pattern captures + +A prior binding remains visible on alternatives that do not capture the name. + +```py +value = "previous" + +match (0,): + case [value] | []: # error: [invalid-syntax] "alternative patterns bind different names" + value + +value +``` + +## Partially overlapping match-pattern bindings + +Shared captures remain definitely bound; branch-specific captures are possibly undefined. + +```py +match (0, 1): + # error: [invalid-syntax] "alternative patterns bind different names" + case [first, shared] | [second, shared]: + first # error: [possibly-unresolved-reference] + second # error: [possibly-unresolved-reference] + shared +``` + +## Nested mismatched match-pattern bindings + +Syntax checking stops after an outer mismatch, but unchecked nested alternatives must still be +modeled safely. + +```py +match (0,): + # error: [invalid-syntax] "alternative patterns bind different names" + case [first] | [second] | [third | fourth]: + third # error: [possibly-unresolved-reference] + fourth # error: [possibly-unresolved-reference] +``` + +## Partially bound match-pattern capture in a guard + +A guard can observe a name that is bound by only one invalid alternative. + +```py +match (0,): + # error: [invalid-syntax] "alternative patterns bind different names" + # error: [possibly-unresolved-reference] + # error: [redundant-condition] "This condition is always false" + case [value] | [] if value: + pass +``` + +## Malformed match-case recovery + +Parser recovery treats the trailing name as an annotation-only statement, whose binding lookup must +not panic. + +```py +match 0: + # error: [invalid-syntax] "alternative patterns bind different names" + # error: [invalid-syntax] "Expected `:`, found name" + # error: [invalid-syntax] "Expected an expression" + case first | second first: +``` + ## Invalid annotation ### `typing.Callable` @@ -145,6 +249,23 @@ def _(u: InvalidEmptyUnion): reveal_type(u) # revealed: Unknown ``` +### `typing.Unpack` + +```toml +[environment] +python-version = "3.11" +``` + +An empty `Unpack` nested inside a union and a generic specialization should report its syntax error +without panicking. + +```py +from typing import Union, Unpack + +# error: [invalid-syntax] "Expected index or slice expression" +list[Union[Unpack[], None]] +``` + ### `typing.Annotated` ```py diff --git a/crates/ty_python_semantic/resources/mdtest/libraries/numpy.md b/crates/ty_python_semantic/resources/mdtest/libraries/numpy.md index 2b657a34f7..ce9b5895c1 100644 --- a/crates/ty_python_semantic/resources/mdtest/libraries/numpy.md +++ b/crates/ty_python_semantic/resources/mdtest/libraries/numpy.md @@ -49,9 +49,13 @@ _DTypeLike: TypeAlias = type[_ScalarT] | dtype[_ScalarT] | _SupportsDType[dtype[ DTypeLike: TypeAlias = _DTypeLike[Any] | str | None ``` -Now we can make sure that a function which accepts `DTypeLike | None` works as expected: +Now we can make sure that a function which accepts `DTypeLike | None` works as expected. A generic +function accepting `_DTypeLike[_ScalarT]` should also infer the scalar type from a scalar class. The +protocol union element describes instances with a `dtype` property, not the class object whose class +access exposes that property descriptor: ```py +from typing import TypeVar import mini_numpy as np def accepts_dtype(dtype: np.DTypeLike | None) -> None: ... @@ -61,4 +65,11 @@ accepts_dtype(dtype=np.dtype[np.bool]) accepts_dtype(dtype=object) accepts_dtype(dtype=np.object_) accepts_dtype(dtype="U") + +_ScalarT = TypeVar("_ScalarT", bound=np.generic) + +def from_dtype_like(value: np._DTypeLike[_ScalarT]) -> np.dtype[_ScalarT]: + raise NotImplementedError + +reveal_type(from_dtype_like(np.bool)) # revealed: dtype[mini_numpy.bool[builtins.bool]] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/liskov.md b/crates/ty_python_semantic/resources/mdtest/liskov.md index c05eac2415..e97328a671 100644 --- a/crates/ty_python_semantic/resources/mdtest/liskov.md +++ b/crates/ty_python_semantic/resources/mdtest/liskov.md @@ -53,7 +53,6 @@ error[invalid-method-override]: Invalid override of method `method` | 2 | def method(self) -> int: ... | ------------------- `Super.method` defined here - | info: incompatible return types: `object` is not assignable to `int` info: This violates the Liskov Substitution Principle ``` @@ -76,7 +75,6 @@ error[invalid-method-override]: Invalid override of method `method` | 2 | def method(self) -> int: ... | ------------------- `Super.method` defined here - | info: incompatible return types: `str` is not assignable to `int` info: This violates the Liskov Substitution Principle ``` @@ -171,7 +169,7 @@ error[invalid-method-override]: Invalid override of method `method` | 2 | def method(self, x: int, /): ... | ----------------------- `Super.method` defined here - | +info: parameter `x` is missing info: This violates the Liskov Substitution Principle ``` @@ -193,8 +191,8 @@ error[invalid-method-override]: Invalid override of method `method` | 2 | def method(self, x: int, /): ... | ----------------------- `Super.method` defined here - | info: unexpected extra parameter `y` +help: Parameter `y` must have a default value info: This violates the Liskov Substitution Principle ``` @@ -216,7 +214,6 @@ error[invalid-method-override]: Invalid override of method `method` | 2 | def method(self, x: int, /): ... | ----------------------- `Super.method` defined here - | info: parameter `x` is keyword-only but must also accept positional arguments info: This violates the Liskov Substitution Principle ``` @@ -239,7 +236,6 @@ error[invalid-method-override]: Invalid override of method `method` | 2 | def method(self, x: int, /): ... | ----------------------- `Super.method` defined here - | info: parameter `x` has an incompatible type: `int` is not assignable to `bool` info: This violates the Liskov Substitution Principle ``` @@ -256,7 +252,7 @@ class Sub16(Super2): ```snapshot error[invalid-method-override]: Invalid override of method `method2` - --> src/mdtest_snippet.pyi:43:9 + --> src/mdtest_snippet.pyi:46:9 | 43 | def method2(self, x): ... | ---------------- `Super2.method2` defined here @@ -264,7 +260,6 @@ error[invalid-method-override]: Invalid override of method `method2` 45 | class Sub16(Super2): 46 | def method2(self, x, /): ... # snapshot: invalid-method-override | ^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super2.method2` - | info: parameter `x` is positional-only but must also accept keyword arguments info: This violates the Liskov Substitution Principle ``` @@ -278,7 +273,7 @@ class Sub17(Super2): ```snapshot error[invalid-method-override]: Invalid override of method `method2` - --> src/mdtest_snippet.pyi:43:9 + --> src/mdtest_snippet.pyi:48:9 | 43 | def method2(self, x): ... | ---------------- `Super2.method2` defined here @@ -288,7 +283,6 @@ error[invalid-method-override]: Invalid override of method `method2` 47 | class Sub17(Super2): 48 | def method2(self, *, x): ... # snapshot: invalid-method-override | ^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super2.method2` - | info: parameter `x` is keyword-only but must also accept positional arguments info: This violates the Liskov Substitution Principle ``` @@ -312,7 +306,7 @@ class Sub19(Super3): ```snapshot error[invalid-method-override]: Invalid override of method `method3` - --> src/mdtest_snippet.pyi:50:9 + --> src/mdtest_snippet.pyi:55:9 | 50 | def method3(self, *, x): ... | ------------------- `Super3.method3` defined here @@ -322,7 +316,7 @@ error[invalid-method-override]: Invalid override of method `method3` 54 | class Sub19(Super3): 55 | def method3(self, x, /): ... # snapshot: invalid-method-override | ^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super3.method3` - | +info: parameter `x` is positional-only but must also accept keyword arguments info: This violates the Liskov Substitution Principle ``` @@ -345,7 +339,7 @@ class Sub21(Super4): ```snapshot error[invalid-method-override]: Invalid override of method `method` - --> src/mdtest_snippet.pyi:57:9 + --> src/mdtest_snippet.pyi:62:9 | 57 | def method(self, *args: int, **kwargs: str): ... | --------------------------------------- `Super4.method` defined here @@ -355,7 +349,7 @@ error[invalid-method-override]: Invalid override of method `method` 61 | class Sub21(Super4): 62 | def method(self, *args): ... # snapshot: invalid-method-override | ^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super4.method` - | +info: the signature must accept arbitrary keyword arguments info: This violates the Liskov Substitution Principle ``` @@ -377,7 +371,7 @@ error[invalid-method-override]: Invalid override of method `method` | 57 | def method(self, *args: int, **kwargs: str): ... | --------------------------------------- `Super4.method` defined here - | +info: the signature must accept arbitrary positional arguments info: This violates the Liskov Substitution Principle ``` @@ -390,6 +384,274 @@ class Sub23(Super4): def method(self, x, *args, y, **kwargs): ... ``` +## Variadic keyword parameters cannot replace positional parameters + +A method that accepts only keyword arguments cannot accept a positional argument required by the +superclass method. + +```pyi +class Parent: + def method(self, value: int, /) -> None: ... + +class Child(Parent): + def method(self, **kwargs: int) -> None: ... # snapshot: invalid-method-override +``` + +```snapshot +error[invalid-method-override]: Invalid override of method `method` + --> src/mdtest_snippet.pyi:5:9 + | +2 | def method(self, value: int, /) -> None: ... + | ----------------------------------- `Parent.method` defined here +3 | +4 | class Child(Parent): +5 | def method(self, **kwargs: int) -> None: ... # snapshot: invalid-method-override + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.method` +info: parameter `value` is missing +info: This violates the Liskov Substitution Principle +``` + +## Signatures with variadic positional arguments cannot add additional required arguments + +A method that accepts any number of positional arguments can be called with no arguments. An +override must not introduce a required positional argument before its variadic parameter. + +```pyi +class Parent: + def method(self, *args: int) -> None: ... + +class Child(Parent): + def method(self, first: int, *args: int) -> None: ... # snapshot: invalid-method-override +``` + +```snapshot +error[invalid-method-override]: Invalid override of method `method` + --> src/mdtest_snippet.pyi:5:9 + | +2 | def method(self, *args: int) -> None: ... + | -------------------------------- `Parent.method` defined here +3 | +4 | class Child(Parent): +5 | def method(self, first: int, *args: int) -> None: ... # snapshot: invalid-method-override + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.method` +info: unexpected extra parameter `first` +help: Parameter `first` must have a default value +info: This violates the Liskov Substitution Principle +``` + +Adding a positional parameter with a default is valid because callers can omit it. + +```pyi +class OptionalChild(Parent): + # TODO: this is a false-positive error that should be fixed. + def method(self, first: int = 0, *args: int) -> None: ... # error: [invalid-method-override] +``` + +## Variadic keyword parameters cannot be overridden with a limited set of keyword-only parameters + +A method that accepts arbitrary keyword arguments cannot be overridden by a method that accepts only +one named keyword argument, even when that argument is optional. + +```pyi +class Parent: + def method(self, **kwargs: int) -> None: ... + +class Child(Parent): + def method(self, *, value: int = 0) -> None: ... # snapshot: invalid-method-override +``` + +```snapshot +error[invalid-method-override]: Invalid override of method `method` + --> src/mdtest_snippet.pyi:5:9 + | +2 | def method(self, **kwargs: int) -> None: ... + | ----------------------------------- `Parent.method` defined here +3 | +4 | class Child(Parent): +5 | def method(self, *, value: int = 0) -> None: ... # snapshot: invalid-method-override + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.method` +info: the signature must accept arbitrary keyword arguments +info: This violates the Liskov Substitution Principle +``` + +## Optional parameters must remain optional on subclass overrides + +A positional-only parameter that callers may omit on the superclass cannot become required on the +subclass. + +```pyi +class ParentPositionalOnly: + def method(self, parent_value: int = 0, /) -> None: ... + +class ChildPositionalOnly(ParentPositionalOnly): + def method(self, child_value: int, /) -> None: ... # snapshot: invalid-method-override +``` + +```snapshot +error[invalid-method-override]: Invalid override of method `method` + --> src/mdtest_snippet.pyi:5:9 + | +2 | def method(self, parent_value: int = 0, /) -> None: ... + | ---------------------------------------------- `ParentPositionalOnly.method` defined here +3 | +4 | class ChildPositionalOnly(ParentPositionalOnly): +5 | def method(self, child_value: int, /) -> None: ... # snapshot: invalid-method-override + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `ParentPositionalOnly.method` +info: parameter `child_value` must have a default value +info: This violates the Liskov Substitution Principle +``` + +The same rule applies when the optional parameter is positional-or-keyword: + +```pyi +class ParentPositionalOrKeyword: + def method(self, value: int = 0) -> None: ... + +class ChildPositionalOrKeyword(ParentPositionalOrKeyword): + def method(self, value: int) -> None: ... # snapshot: invalid-method-override +``` + +```snapshot +error[invalid-method-override]: Invalid override of method `method` + --> src/mdtest_snippet.pyi:10:9 + | + 7 | def method(self, value: int = 0) -> None: ... + | ------------------------------------ `ParentPositionalOrKeyword.method` defined here + 8 | + 9 | class ChildPositionalOrKeyword(ParentPositionalOrKeyword): +10 | def method(self, value: int) -> None: ... # snapshot: invalid-method-override + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `ParentPositionalOrKeyword.method` +info: parameter `value` must have a default value +info: This violates the Liskov Substitution Principle +``` + +And if the parameter is keyword-only: + +```pyi +class ParentKeywordOnly: + def method(self, *, value: int = 0) -> None: ... + +class ChildKeywordOnly(ParentKeywordOnly): + def method(self, *, value: int) -> None: ... # snapshot: invalid-method-override +``` + +```snapshot +error[invalid-method-override]: Invalid override of method `method` + --> src/mdtest_snippet.pyi:15:9 + | +12 | def method(self, *, value: int = 0) -> None: ... + | --------------------------------------- `ParentKeywordOnly.method` defined here +13 | +14 | class ChildKeywordOnly(ParentKeywordOnly): +15 | def method(self, *, value: int) -> None: ... # snapshot: invalid-method-override + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `ParentKeywordOnly.method` +info: parameter `value` must have a default value +info: This violates the Liskov Substitution Principle +``` + +## Subclass overrides may not add additional positional-only parameters without default values + +This is true if the new parameter is positional-only: + +```pyi +class PositionalOnlyParent: + def method(self, *, value: int) -> None: ... + +class PositionalOnlyChild(PositionalOnlyParent): + def method(self, extra: int, /, *, value: int) -> None: ... # snapshot: invalid-method-override +``` + +```snapshot +error[invalid-method-override]: Invalid override of method `method` + --> src/mdtest_snippet.pyi:5:9 + | +2 | def method(self, *, value: int) -> None: ... + | ----------------------------------- `PositionalOnlyParent.method` defined here +3 | +4 | class PositionalOnlyChild(PositionalOnlyParent): +5 | def method(self, extra: int, /, *, value: int) -> None: ... # snapshot: invalid-method-override + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `PositionalOnlyParent.method` +info: unexpected extra parameter `extra` +help: Parameter `extra` must have a default value +info: This violates the Liskov Substitution Principle +``` + +And if the new parameter is keyword-only: + +```pyi +class KeywordOnlyParent: + def method(self, *, value: int) -> None: ... + +class KeywordOnlyChild(KeywordOnlyParent): + def method(self, *, value: int, extra: int) -> None: ... # snapshot: invalid-method-override +``` + +```snapshot +error[invalid-method-override]: Invalid override of method `method` + --> src/mdtest_snippet.pyi:10:9 + | + 7 | def method(self, *, value: int) -> None: ... + | ----------------------------------- `KeywordOnlyParent.method` defined here + 8 | + 9 | class KeywordOnlyChild(KeywordOnlyParent): +10 | def method(self, *, value: int, extra: int) -> None: ... # snapshot: invalid-method-override + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `KeywordOnlyParent.method` +info: unexpected extra parameter `extra` +help: Parameter `extra` must have a default value +info: This violates the Liskov Substitution Principle +``` + +## Keyword-only parameters cannot be removed + +Removing a keyword-only parameter means that the overriding method no longer accepts the +corresponding keyword argument. + +```pyi +class Parent: + def method(self, *, value: int) -> None: ... + +class Child(Parent): + def method(self) -> None: ... # snapshot: invalid-method-override +``` + +```snapshot +error[invalid-method-override]: Invalid override of method `method` + --> src/mdtest_snippet.pyi:5:9 + | +2 | def method(self, *, value: int) -> None: ... + | ----------------------------------- `Parent.method` defined here +3 | +4 | class Child(Parent): +5 | def method(self) -> None: ... # snapshot: invalid-method-override + | ^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.method` +info: parameter `value` is missing +info: This violates the Liskov Substitution Principle +``` + +Replacing the parameter with a differently named optional keyword also prevents callers from +providing the original argument. + +```pyi +class ChildWithDifferentKeyword(Parent): + def method(self, *, other: int = 0) -> None: ... # snapshot: invalid-method-override +``` + +```snapshot +error[invalid-method-override]: Invalid override of method `method` + --> src/mdtest_snippet.pyi:7:9 + | +2 | def method(self, *, value: int) -> None: ... + | ----------------------------------- `Parent.method` defined here +3 | +4 | class Child(Parent): +5 | def method(self) -> None: ... # snapshot: invalid-method-override +6 | class ChildWithDifferentKeyword(Parent): +7 | def method(self, *, other: int = 0) -> None: ... # snapshot: invalid-method-override + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.method` +info: parameter `value` is missing +info: This violates the Liskov Substitution Principle +``` + ## `ClassVar` and instance variables A pure class variable cannot override an inherited instance variable, and an instance variable @@ -661,7 +923,7 @@ class Compatible(ReturnsBool, ReturnsInt): ... ```snapshot error[invalid-method-override]: Base classes for class `BasicConflict` define method `method` incompatibly - --> src/mdtest_snippet.pyi:2:9 + --> src/mdtest_snippet.pyi:10:7 | 2 | def method(self) -> str: ... | ------ `ReturnsStr.method` defined here @@ -675,7 +937,6 @@ error[invalid-method-override]: Base classes for class `BasicConflict` define me 9 | 10 | class BasicConflict(ReturnsStr, ReturnsInt): ... # snapshot: invalid-method-override | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `ReturnsStr.method` is incompatible with `ReturnsInt.method` - | info: incompatible return types: `str` is not assignable to `int` info: This violates the Liskov Substitution Principle ``` @@ -914,7 +1175,7 @@ class StaticClassConflict(StaticMethod, ClassMethod): ... # error: [invalid-met ```snapshot error[invalid-method-override]: Base classes for class `ClassInstanceConflict` define method `kind` incompatibly - --> src/mdtest_snippet.pyi:10:9 + --> src/mdtest_snippet.pyi:13:7 | 10 | def kind(cls, value: int) -> int: ... | ---- `ClassMethod.kind` defined here @@ -927,7 +1188,6 @@ error[invalid-method-override]: Base classes for class `ClassInstanceConflict` d | 2 | def kind(self, value: int) -> int: ... | ---- `InstanceMethod.kind` defined here - | info: `ClassMethod.kind` is a classmethod but `InstanceMethod.kind` is an instance method info: This violates the Liskov Substitution Principle ``` @@ -1068,7 +1328,6 @@ error[invalid-method-override]: Base classes for class `Combined` define method | 2 | def method(self) -> int: ... | ------ `right.Base.method` defined here - | info: incompatible return types: `str` is not assignable to `int` info: This violates the Liskov Substitution Principle ``` @@ -1156,7 +1415,7 @@ class ThirdChild(GradualParent): ```snapshot error[invalid-method-override]: Invalid override of method `method` - --> src/stub.pyi:4:9 + --> src/stub.pyi:7:9 | 4 | def method(self, x: int) -> None: ... | ---------------------------- `Grandparent.method` defined here @@ -1164,7 +1423,6 @@ error[invalid-method-override]: Invalid override of method `method` 6 | class Parent(Grandparent): 7 | def method(self, x: str) -> None: ... # snapshot: invalid-method-override | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Grandparent.method` - | info: parameter `x` has an incompatible type: `int` is not assignable to `str` info: This violates the Liskov Substitution Principle @@ -1179,7 +1437,6 @@ error[invalid-method-override]: Invalid override of method `method` | 7 | def method(self, x: str) -> None: ... # snapshot: invalid-method-override | ---------------------------- `Parent.method` defined here - | info: parameter `x` has an incompatible type: `str` is not assignable to `int` info: This violates the Liskov Substitution Principle @@ -1194,13 +1451,12 @@ error[invalid-method-override]: Invalid override of method `method` | 7 | def method(self, x: str) -> None: ... # snapshot: invalid-method-override | ---------------------------- `Parent.method` defined here - | info: parameter `x` has an incompatible type: `str` is not assignable to `bytes` info: This violates the Liskov Substitution Principle error[invalid-method-override]: Invalid override of method `method` - --> src/stub.pyi:25:9 + --> src/stub.pyi:28:9 | 25 | def method(self) -> int: ... | ------------------- `GrandparentWithReturnType.method` defined here @@ -1208,13 +1464,12 @@ error[invalid-method-override]: Invalid override of method `method` 27 | class ParentWithReturnType(GrandparentWithReturnType): 28 | def method(self) -> str: ... # snapshot: invalid-method-override | ^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `GrandparentWithReturnType.method` - | info: incompatible return types: `str` is not assignable to `int` info: This violates the Liskov Substitution Principle error[invalid-method-override]: Invalid override of method `method` - --> src/stub.pyi:28:9 + --> src/stub.pyi:33:9 | 28 | def method(self) -> str: ... # snapshot: invalid-method-override | ------------------- `ParentWithReturnType.method` defined here @@ -1224,7 +1479,6 @@ error[invalid-method-override]: Invalid override of method `method` 32 | # but not with `ParentWithReturnType.method`. We report against the immediate parent. 33 | def method(self) -> int: ... # snapshot: invalid-method-override | ^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `ParentWithReturnType.method` - | info: incompatible return types: `int` is not assignable to `str` info: This violates the Liskov Substitution Principle @@ -1239,7 +1493,6 @@ error[invalid-method-override]: Invalid override of method `method` | 4 | def method(self, x: int) -> None: ... | ---------------------------- `Grandparent.method` defined here - | info: parameter `x` has an incompatible type: `int` is not assignable to `str` info: This violates the Liskov Substitution Principle ``` @@ -1268,7 +1521,7 @@ class D(C): ```snapshot error[invalid-method-override]: Invalid override of method `get` - --> src/other_stub.pyi:2:9 + --> src/other_stub.pyi:5:9 | 2 | def get(self, default): ... | ------------------ `A.get` defined here @@ -1276,7 +1529,6 @@ error[invalid-method-override]: Invalid override of method `get` 4 | class B(A): 5 | def get(self, default, /): ... # snapshot: invalid-method-override | ^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `A.get` - | info: parameter `default` is positional-only but must also accept keyword arguments info: This violates the Liskov Substitution Principle ``` @@ -1390,22 +1642,26 @@ class C3(A3): class D3(A3): def method(self: Self) -> Self: ... # fine +# These overrides would otherwise be valid, but a method returning `Self` must leave `self` +# unannotated or annotate it as `Self`. class E3(A3): - def method(self: E3) -> Self: ... # fine + def method(self: E3) -> Self: ... # error: [invalid-type-form] class F3(A3): - def method(self: A3) -> Self: ... # fine + def method(self: A3) -> Self: ... # error: [invalid-type-form] class G3(A3): - def method(self: object) -> Self: ... # fine + def method(self: object) -> Self: ... # error: [invalid-type-form] class H3(A3): # `A3.method()` can be called on any subtype of `A3`, but `H3.method()` can only be called on # objects that are subtypes of `str`. + # error: [invalid-type-form] def method(self: str) -> Self: ... # error: [invalid-method-override] class I3(A3): # `I3.method()` cannot be called with any inhabited type. + # error: [invalid-type-form] def method(self: Never) -> Self: ... # error: [invalid-method-override] class A4: @@ -1477,7 +1733,6 @@ error[invalid-method-override]: Invalid override of method `method` | 7 | def method(self: HasValue, argument: int) -> None: ... | --------------------------------------------- `Mixin.method` defined here - | info: parameter `argument` has an incompatible type: `int` is not assignable to `str` info: This violates the Liskov Substitution Principle ``` @@ -1613,7 +1868,6 @@ error[invalid-method-override]: Invalid override of method `foo` | 2 | def foo(self, x): ... | ------------ `one.A.foo` defined here - | info: the parameter named `y` does not match `x` (and can be used as a keyword parameter) info: This violates the Liskov Substitution Principle ``` @@ -1706,7 +1960,7 @@ class D(C): ```snapshot error[invalid-method-override]: Invalid override of method `x` - --> src/bar.pyi:4:9 + --> src/bar.pyi:7:5 | 4 | def x(self, y: int): ... | --------------- `A.x` defined here @@ -1719,13 +1973,12 @@ error[invalid-method-override]: Invalid override of method `x` | 1 | def x(self, y: str): ... | --------------- Signature of `B.x` - | info: parameter `y` has an incompatible type: `int` is not assignable to `str` info: This violates the Liskov Substitution Principle error[invalid-method-override]: Invalid override of method `x` - --> src/bar.pyi:10:5 + --> src/bar.pyi:13:9 | 10 | x = foo.x | --------- `C.x` defined here @@ -1738,7 +1991,6 @@ error[invalid-method-override]: Invalid override of method `x` | 1 | def x(self, y: str): ... | --------------- Signature of `C.x` - | info: parameter `y` has an incompatible type: `str` is not assignable to `int` info: This violates the Liskov Substitution Principle ``` @@ -1759,20 +2011,19 @@ error[invalid-method-override]: Invalid override of method `__eq__` 3 | def __eq__(self, other: "Bad") -> bool: # snapshot: invalid-method-override | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `object.__eq__` | - ::: stdlib/builtins.byi:89:9 + ::: stdlib/builtins.byi:90:9 | -89 | def __eq__(self, value: object, /) -> bool +90 | def __eq__(self, value: object, /) -> bool | -------------------------------------- `object.__eq__` defined here - | info: parameter `value` has an incompatible type: `object` is not assignable to `Bad` info: This violates the Liskov Substitution Principle help: It is recommended for `__eq__` to work with arbitrary objects, for example: -help +help: help: def __eq__(self, other: object) -> bool: help: if not isinstance(other, Bad): help: return False help: return -help +help: ``` ## Class-private names do not override @@ -1847,7 +2098,6 @@ error[invalid-method-override]: Invalid override of method `_asdict` | 41 | def _asdict(self) -> tuple[int, ...]: ... # snapshot: invalid-method-override | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Baz._asdict` - | info: incompatible return types: `tuple[int, ...]` is not assignable to `dict[str, Any]` info: This violates the Liskov Substitution Principle info: `Baz._asdict` is a generated method created because `Baz` inherits from `typing.NamedTuple` @@ -1855,7 +2105,6 @@ info: `Baz._asdict` is a generated method created because `Baz` inherits from `t | 37 | class Baz(NamedTuple): | ^^^^^^^^^^^^^^^ Definition of `Baz` - | ``` ## Staticmethods and classmethods @@ -1903,7 +2152,6 @@ error[invalid-method-override]: Invalid override of method `class_method` | 4 | def class_method(cls, x: int) -> int: ... | -------------------------------- `Parent.class_method` defined here - | info: incompatible return types: `object` is not assignable to `int` info: This violates the Liskov Substitution Principle ``` @@ -1925,7 +2173,6 @@ error[invalid-method-override]: Invalid override of method `static_method` | 6 | def static_method(x: int) -> int: ... | ---------------------------- `Parent.static_method` defined here - | info: incompatible return types: `object` is not assignable to `int` info: This violates the Liskov Substitution Principle ``` @@ -1949,7 +2196,6 @@ error[invalid-method-override]: Invalid override of method `instance_method` | 2 | def instance_method(self, x: int) -> int: ... | ------------------------------------ `Parent.instance_method` defined here - | info: `BadChild1A.instance_method` is a staticmethod but `Parent.instance_method` is an instance method info: This violates the Liskov Substitution Principle ``` @@ -1970,7 +2216,6 @@ error[invalid-method-override]: Invalid override of method `static_method` | 6 | def static_method(x: int) -> int: ... | ---------------------------- `Parent.static_method` defined here - | info: `BadChild1B.static_method` is an instance method but `Parent.static_method` is a staticmethod info: This violates the Liskov Substitution Principle ``` @@ -2019,7 +2264,6 @@ error[invalid-method-override]: Invalid override of method `class_method` | 4 | def class_method(cls, x: int) -> int: ... | -------------------------------- `Parent.class_method` defined here - | info: `BadChild3A.class_method` is a staticmethod but `Parent.class_method` is a classmethod info: This violates the Liskov Substitution Principle ``` @@ -2041,7 +2285,6 @@ error[invalid-method-override]: Invalid override of method `static_method` | 6 | def static_method(x: int) -> int: ... | ---------------------------- `Parent.static_method` defined here - | info: `BadChild3B.static_method` is a classmethod but `Parent.static_method` is a staticmethod info: This violates the Liskov Substitution Principle ``` @@ -2159,3 +2402,63 @@ class MaybeEqWhile: def __eq__(self, other: MaybeEqWhile) -> bool: return True ``` + +## Overloaded generic receivers remain visible to override checks + +An override must still be checked against every applicable receiver-specialized overload when the +subclass retains a covariant type parameter. A `str` receiver matches both the `str` and `object` +overloads, so accepting only `str` is invalid. A two-item receiver excludes the one-item overload, +so matching only that excluded overload is also invalid. + +```toml +[environment] +python-version = "3.12" +``` + +```pyi +from typing import Any, Generic, TypeVar, overload + +ValueCo = TypeVar("ValueCo", covariant=True) +ShapeCo = TypeVar("ShapeCo", covariant=True) + +class Receiver(Generic[ValueCo, ShapeCo]): + @overload + def by_value(self: "Receiver[str, Any]", value: str) -> None: ... + @overload + def by_value(self: "Receiver[object, Any]", value: object) -> None: ... + @overload + def by_shape(self: "Receiver[Any, tuple[str]]", value: str) -> None: ... + @overload + def by_shape(self: "Receiver[Any, tuple[str, str]]", value: bytes) -> None: ... + +class NarrowValueOverride(Receiver[str, ShapeCo], Generic[ShapeCo]): + def by_value(self, value: str) -> None: ... # error: [invalid-method-override] + +class WrongShapeOverride(Receiver[ValueCo, tuple[str, str]], Generic[ValueCo]): + def by_shape(self, value: str) -> None: ... # error: [invalid-method-override] +``` + +## Equivalent overloaded protocol receivers are valid overrides + +A generic implementation can restate a protocol's receiver-specialized overload set using its own +receiver type without changing the method contract. + +```py +from typing import Generic, Protocol, TypeVar, overload + +T = TypeVar("T") +TContra = TypeVar("TContra", contravariant=True) + +class TaskStatus(Protocol[TContra]): + @overload + def started(self: "TaskStatus[None]") -> None: ... + @overload + def started(self, value: TContra) -> None: ... + +class ConcreteStatus(Generic[T], TaskStatus[T]): + @overload + def started(self: "ConcreteStatus[None]") -> None: ... + @overload + def started(self: "ConcreteStatus[T]", value: T) -> None: ... + def started(self, value: T | None = None) -> None: ... +``` diff --git a/crates/ty_python_semantic/resources/mdtest/loops/async_for.md b/crates/ty_python_semantic/resources/mdtest/loops/async_for.md index 22ae4e977e..13a4ef25a4 100644 --- a/crates/ty_python_semantic/resources/mdtest/loops/async_for.md +++ b/crates/ty_python_semantic/resources/mdtest/loops/async_for.md @@ -80,7 +80,6 @@ error[not-iterable]: Object of type `NotAsyncIterable` is not async-iterable | 5 | async for x in NotAsyncIterable(): | ^^^^^^^^^^^^^^^^^^ - | info: It has no `__aiter__` method ``` @@ -107,7 +106,6 @@ error[not-iterable]: Object of type `Iterator` is not async-iterable | 11 | async for x in Iterator(): | ^^^^^^^^^^ - | info: It has no `__aiter__` method ``` @@ -132,7 +130,6 @@ error[not-iterable]: Object of type `AsyncIterable` is not async-iterable | 9 | async for x in AsyncIterable(): | ^^^^^^^^^^^^^^^ - | info: Its `__aiter__` method returns an object of type `NoAnext`, which has no `__anext__` method ``` @@ -160,7 +157,6 @@ error[not-iterable]: Object of type `AsyncIterable` may not be async-iterable | 12 | async for x in AsyncIterable(): | ^^^^^^^^^^^^^^^ - | info: Its `__aiter__` method returns an object of type `PossiblyUnboundAnext`, which may not have a `__anext__` method info: type `AsyncIterable` is not assignable to protocol `AsyncIterable[Unknown]` info: └── protocol member `__aiter__` is incompatible @@ -193,7 +189,6 @@ error[not-iterable]: Object of type `PossiblyUnboundAiter` may not be async-iter | 12 | async for x in PossiblyUnboundAiter(): | ^^^^^^^^^^^^^^^^^^^^^^ - | info: Its `__aiter__` attribute (with type `bound method PossiblyUnboundAiter.__aiter__() -> AsyncIterable`) may not be callable ``` @@ -220,11 +215,11 @@ error[not-iterable]: Object of type `AsyncIterable` is not async-iterable | 11 | async for x in AsyncIterable(): | ^^^^^^^^^^^^^^^ - | info: Its `__aiter__` method has an invalid signature info: type `AsyncIterable` is not assignable to protocol `AsyncIterable[Unknown]` info: └── protocol member `__aiter__` is incompatible info: └── unexpected extra parameter `arg` +help: Parameter `arg` must have a default value info: Expected signature `def __aiter__(self): ...` ``` @@ -251,7 +246,6 @@ error[not-iterable]: Object of type `AsyncIterable` is not async-iterable | 11 | async for x in AsyncIterable(): | ^^^^^^^^^^^^^^^ - | info: Its `__aiter__` method returns an object of type `AsyncIterator`, which has an invalid `__anext__` method info: type `AsyncIterable` is not assignable to protocol `AsyncIterable[Unknown]` info: └── protocol member `__aiter__` is incompatible @@ -259,5 +253,6 @@ info: └── incompatible return types: `AsyncIterator` is not assignable info: └── type `AsyncIterator` is not assignable to protocol `AsyncIterator[Unknown]` info: └── protocol member `__anext__` is incompatible info: └── unexpected extra parameter `arg` +help: Parameter `arg` must have a default value info: Expected signature for `__anext__` is `def __anext__(self): ...` ``` diff --git a/crates/ty_python_semantic/resources/mdtest/loops/for.md b/crates/ty_python_semantic/resources/mdtest/loops/for.md index 6564d135d0..ac995e8bf0 100644 --- a/crates/ty_python_semantic/resources/mdtest/loops/for.md +++ b/crates/ty_python_semantic/resources/mdtest/loops/for.md @@ -86,27 +86,6 @@ def non_empty_first(flag: bool) -> None: reveal_type(value) # revealed: range ``` -Empty ranges all compare equal, but non-empty ranges with the same type refinement may contain -different values: - -```py -empty_left = range(0) -empty_right = range(1, 1) - -if empty_left == empty_right: - reveal_type(empty_left) # revealed: range -else: - reveal_type(empty_left) # revealed: Never - -non_empty_left = range(1) -non_empty_right = range(2) - -if non_empty_left == non_empty_right: - reveal_type(non_empty_left) # revealed: range -else: - reveal_type(non_empty_left) # revealed: range -``` - ## With shadowed `range` ```py @@ -369,7 +348,6 @@ error[not-iterable]: Object of type `NotIterable` is not iterable | 9 | for x in NotIterable(): | ^^^^^^^^^^^^^ - | info: Its `__iter__` attribute has type `int | None`, which is not callable ``` @@ -387,7 +365,6 @@ error[not-iterable]: Object of type `Literal[123]` is not iterable | 2 | for x in nonsense: # snapshot: not-iterable | ^^^^^^^^ - | info: It doesn't have an `__iter__` method or a `__getitem__` method ``` @@ -409,7 +386,6 @@ error[not-iterable]: Object of type `NotIterable` is not iterable | 6 | for x in NotIterable(): # snapshot: not-iterable | ^^^^^^^^^^^^^ - | info: Its `__iter__` attribute has type `None`, which is not callable ``` @@ -598,7 +574,6 @@ error[not-iterable]: Object of type `Test | Literal[42]` may not be iterable | 13 | for x in iterable: | ^^^^^^^^ - | info: It may not have an `__iter__` method and it doesn't have a `__getitem__` method info: `Literal[42]` does not implement `__iter__` ``` @@ -631,7 +606,6 @@ error[not-iterable]: Object of type `Test | Test2` may not be iterable | 16 | for x in iterable: | ^^^^^^^^ - | info: Its `__iter__` method returns an object of type `TestIter | int`, which may not have a `__next__` method info: element `Test2` of union `Test | Test2` is not assignable to `Iterable[Unknown]` info: └── type `Test2` is not assignable to protocol `Iterable[Unknown]` @@ -672,7 +646,6 @@ error[not-iterable]: Object of type `Test | NotIter` may not be iterable | 15 | for x in iterable: | ^^^^^^^^ - | info: Its `__iter__` attribute (with type `(bound method Test.__iter__() -> TestIter) | int`) may not be callable ``` @@ -706,14 +679,13 @@ def _(x: Sequence[int], y: object): reveal_type(item) # revealed: int if isinstance(y, list): - reveal_type(y) # revealed: Top[list[Unknown]] + reveal_type(y) # revealed: list[Unknown] for item in y: - reveal_type(item) # revealed: object + reveal_type(item) # revealed: Unknown if isinstance(x, list): - reveal_type(x) # revealed: Sequence[int] & Top[list[Unknown]] + reveal_type(x) # revealed: list[int] for item in x: - # int & object simplifies to int reveal_type(item) # revealed: int ``` @@ -915,11 +887,11 @@ error[not-iterable]: Object of type `Iterable` is not iterable | 10 | for x in Iterable(): | ^^^^^^^^^^ - | info: Its `__iter__` method has an invalid signature info: type `Iterable` is not assignable to protocol `Iterable[Unknown]` info: └── protocol member `__iter__` is incompatible info: └── unexpected extra parameter `extra_arg` +help: Parameter `extra_arg` must have a default value info: Expected signature `def __iter__(self): ...` ``` @@ -941,7 +913,6 @@ error[not-iterable]: Object of type `Bad` is not iterable | 6 | for x in Bad(): | ^^^^^ - | info: Its `__iter__` method returns an object of type `int`, which has no `__next__` method ``` @@ -992,7 +963,6 @@ error[not-iterable]: Object of type `Iterable1` is not iterable | 17 | for x in Iterable1(): | ^^^^^^^^^^^ - | info: Its `__iter__` method returns an object of type `Iterator1`, which has an invalid `__next__` method info: type `Iterable1` is not assignable to protocol `Iterable[Unknown]` info: └── protocol member `__iter__` is incompatible @@ -1000,6 +970,7 @@ info: └── incompatible return types: `Iterator1` is not assignable to info: └── type `Iterator1` is not assignable to protocol `Iterator[Unknown]` info: └── protocol member `__next__` is incompatible info: └── unexpected extra parameter `extra_arg` +help: Parameter `extra_arg` must have a default value info: Expected signature for `__next__` is `def __next__(self): ...` ``` @@ -1015,7 +986,6 @@ error[not-iterable]: Object of type `Iterable2` is not iterable | 20 | for y in Iterable2(): | ^^^^^^^^^^^ - | info: Its `__iter__` method returns an object of type `Iterator2`, which has a `__next__` attribute that is not callable ``` @@ -1047,7 +1017,6 @@ error[not-iterable]: Object of type `Iterable` may not be iterable | 16 | for x in Iterable(): | ^^^^^^^^^^ - | 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 ``` @@ -1107,7 +1076,6 @@ error[not-iterable]: Object of type `Iterable` may not be iterable | 15 | for x in Iterable(): | ^^^^^^^^^^ - | info: It may not have an `__iter__` method or a `__getitem__` method ``` @@ -1128,7 +1096,6 @@ error[not-iterable]: Object of type `Bad` is not iterable | 5 | for x in Bad(): | ^^^^^ - | info: It has no `__iter__` method and its `__getitem__` attribute has type `None`, which is not callable ``` @@ -1167,7 +1134,6 @@ error[not-iterable]: Object of type `Iterable1` may not be iterable | 22 | for x in Iterable1(): | ^^^^^^^^^^^ - | info: It has no `__iter__` method and its `__getitem__` attribute is invalid info: `__getitem__` has type `CustomCallable`, which is not callable ``` @@ -1185,7 +1151,6 @@ error[not-iterable]: Object of type `Iterable2` may not be iterable | 26 | for y in Iterable2(): | ^^^^^^^^^^^ - | 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 ``` @@ -1210,7 +1175,6 @@ error[not-iterable]: Object of type `Iterable` is not iterable | 8 | for x in Iterable(): | ^^^^^^^^^^ - | 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 ``` @@ -1266,11 +1230,11 @@ error[not-iterable]: Object of type `Iterable1` may not be iterable | 16 | for x in Iterable1(): | ^^^^^^^^^^^ - | info: Its `__iter__` method may have an invalid signature info: type `Iterable1` is not assignable to protocol `Iterable[Unknown]` info: └── protocol member `__iter__` is incompatible info: └── unexpected extra parameter `invalid_extra_arg` +help: Parameter `invalid_extra_arg` must have a default value 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): ...` ``` @@ -1296,7 +1260,6 @@ error[not-iterable]: Object of type `Iterable2` may not be iterable | 27 | for x in Iterable2(): | ^^^^^^^^^^^ - | info: Its `__iter__` attribute (with type `(bound method Iterable2.__iter__() -> Iterator) | None`) may not be callable ``` @@ -1340,7 +1303,6 @@ error[not-iterable]: Object of type `Iterable1` may not be iterable | 28 | for x in Iterable1(): | ^^^^^^^^^^^ - | info: Its `__iter__` method returns an object of type `Iterator1`, which may have an invalid `__next__` method info: type `Iterable1` is not assignable to protocol `Iterable[Unknown]` info: └── protocol member `__iter__` is incompatible @@ -1348,6 +1310,7 @@ info: └── incompatible return types: `Iterator1` is not assignable to info: └── type `Iterator1` is not assignable to protocol `Iterator[Unknown]` info: └── protocol member `__next__` is incompatible info: └── unexpected extra parameter `invalid_extra_arg` +help: Parameter `invalid_extra_arg` must have a default value info: Expected signature for `__next__` is `def __next__(self): ...` ``` @@ -1364,7 +1327,6 @@ error[not-iterable]: Object of type `Iterable2` may not be iterable | 31 | for y in Iterable2(): | ^^^^^^^^^^^ - | info: Its `__iter__` method returns an object of type `Iterator2`, which has a `__next__` attribute that may not be callable info: type `Iterable2` is not assignable to protocol `Iterable[Unknown]` info: └── protocol member `__iter__` is incompatible @@ -1406,7 +1368,6 @@ error[not-iterable]: Object of type `Iterable1` may not be iterable | 20 | for x in Iterable1(): | ^^^^^^^^^^^ - | 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 ``` @@ -1423,7 +1384,6 @@ error[not-iterable]: Object of type `Iterable2` may not be iterable | 24 | for y in Iterable2(): | ^^^^^^^^^^^ - | 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 ``` @@ -1472,7 +1432,6 @@ error[not-iterable]: Object of type `Iterable1` may not be iterable | 31 | for x in Iterable1(): | ^^^^^^^^^^^ - | 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 ``` @@ -1488,7 +1447,6 @@ error[not-iterable]: Object of type `Iterable2` may not be iterable | 35 | for y in Iterable2(): | ^^^^^^^^^^^ - | 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 ``` @@ -1531,8 +1489,8 @@ A class literal can be iterated over if it has `Any` or `Unknown` in its MRO, si ```py from unresolved_module import SomethingUnknown # error: [unresolved-import] from typing import Any, Iterable -from ty_extensions import static_assert, Unknown -from ty_extensions._internal import TypeOf, is_assignable_to, reveal_mro +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, TypeOf, is_assignable_to, reveal_mro class Foo(SomethingUnknown): ... @@ -1582,12 +1540,10 @@ simplify to `Never`, leaving only the iterable parts. ```py def f[T: tuple[int, ...] | int](x: T): if isinstance(x, tuple): - reveal_type(x) # revealed: T@f & tuple[object, ...] + reveal_type(x) # revealed: T@f & tuple[int, ...] for item in x: - # The intersection `(tuple[int, ...] | int) & tuple[object, ...]` distributes to: - # `(tuple[int, ...] & tuple[object, ...]) | (int & tuple[object, ...])` - # which simplifies to `tuple[int, ...] | Never` = `tuple[int, ...]` - # so iterating gives `int`. + # The `int` alternative in the TypeVar bound is disjoint from `tuple`. The + # remaining `tuple[int, ...]` alternative supplies the narrowed specialization. reveal_type(item) # revealed: int ``` @@ -1599,13 +1555,10 @@ constraint, those parts should also simplify to `Never`. ```py def g[T: tuple[int, ...] | list[str]](x: T): if isinstance(x, tuple): - reveal_type(x) # revealed: T@g & tuple[object, ...] + reveal_type(x) # revealed: T@g & tuple[int, ...] for item in x: - # The intersection `(tuple[int, ...] | list[str]) & tuple[object, ...]` distributes to: - # `(tuple[int, ...] & tuple[object, ...]) | (list[str] & tuple[object, ...])` - # Since `list[str]` is disjoint from `tuple[object, ...]`, this simplifies to: - # `tuple[int, ...] | Never` = `tuple[int, ...]` - # so iterating gives `int`, NOT `int | str`. + # The `list[str]` alternative in the TypeVar bound is disjoint from `tuple`. The + # remaining `tuple[int, ...]` alternative supplies the narrowed specialization. reveal_type(item) # revealed: int ``` diff --git a/crates/ty_python_semantic/resources/mdtest/metaclass.md b/crates/ty_python_semantic/resources/mdtest/metaclass.md index 2b5fb98b62..6393a9560a 100644 --- a/crates/ty_python_semantic/resources/mdtest/metaclass.md +++ b/crates/ty_python_semantic/resources/mdtest/metaclass.md @@ -788,7 +788,6 @@ error[invalid-metaclass]: Metaclass type `int` is not callable | 3 | class B(metaclass=n): | ^^^^^^^^^^^ - | ``` ## Cyclic diff --git a/crates/ty_python_semantic/resources/mdtest/mro.md b/crates/ty_python_semantic/resources/mdtest/mro.md index fe78f421f0..4f123c4abe 100644 --- a/crates/ty_python_semantic/resources/mdtest/mro.md +++ b/crates/ty_python_semantic/resources/mdtest/mro.md @@ -227,8 +227,8 @@ guarantee: ```py from typing import Any -from ty_extensions import Unknown, Intersection -from ty_extensions._internal import reveal_mro +from ty_extensions import Intersection +from ty_extensions._internal import Unknown, reveal_mro def f(x: type[Any], y: Intersection[Unknown, type[Any]]): class Foo(x): ... diff --git a/crates/ty_python_semantic/resources/mdtest/named_tuple.md b/crates/ty_python_semantic/resources/mdtest/named_tuple.md index 8e932642a5..a917450c4b 100644 --- a/crates/ty_python_semantic/resources/mdtest/named_tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/named_tuple.md @@ -1651,6 +1651,150 @@ Invalid = NamedTuple("Invalid", [("not valid", int), ("ok", str)]) reveal_type(Invalid) # revealed: ``` +## NamedTuple fields cannot be qualified with `ClassVar` or `Final` + +Type checkers reject `ClassVar` and `Final` qualifiers on `NamedTuple` fields. When annotations are +evaluated eagerly, passing these qualifiers to `typing._type_check` also raises `TypeError` while +the class is defined. + +```py +from typing import ClassVar, Final, NamedTuple + +class Foo(NamedTuple): + # error: [invalid-named-tuple] "Type qualifier `ClassVar` is not allowed on NamedTuple field `a`" + a: ClassVar[int] + # error: [invalid-named-tuple] "Type qualifier `Final` is not allowed on NamedTuple field `b`" + b: Final[str] = "foo" + # error: [invalid-named-tuple] "Type qualifier `ClassVar` is not allowed on NamedTuple field `c`" + # error: [invalid-named-tuple] "Type qualifier `Final` is not allowed on NamedTuple field `c`" + # error: [redundant-final-classvar] "Combining `ClassVar` and `Final` is redundant" + c: ClassVar[Final[int]] +``` + +An unsubscripted qualifier is rejected for the same reason: + +```py +from typing import ClassVar, NamedTuple + +class Bare(NamedTuple): + # error: [invalid-named-tuple] "Type qualifier `ClassVar` is not allowed on NamedTuple field `x`" + x: ClassVar +``` + +A class that inherits from a `NamedTuple` class is an ordinary class at runtime, so it may use both +qualifiers freely: + +```py +from typing import ClassVar, Final, NamedTuple + +class Base(NamedTuple): + x: int + +class Sub(Base): + y: ClassVar[int] = 1 + z: Final[str] = "z" +``` + +The full diagnostic points at the offending field: + +```py +from typing import ClassVar, NamedTuple + +class Snapshot(NamedTuple): + # snapshot + a: ClassVar[int] +``` + +```snapshot +error[invalid-named-tuple]: Type qualifier `ClassVar` is not allowed in a NamedTuple field + --> src/mdtest_snippet.py:29:5 + | +29 | a: ClassVar[int] + | ^^^^^^^^^^^^^^^^ +``` + +## NamedTuple qualifiers and redeclared symbols + +A later method declaration does not change the field annotation processed by `NamedTuple`. + +```py +from typing import Final, NamedTuple + +class Redeclared(NamedTuple): + # error: [invalid-named-tuple] "Type qualifier `Final` is not allowed on NamedTuple field `x`" + x: Final[int] + + def x(self) -> int: + return 1 +``` + +## NamedTuple qualifiers in conditional declarations + +When only one branch qualifies a field, the diagnostic points to the declaration in that branch. + +```py +from typing import Final, NamedTuple + +def condition() -> bool: + return True + +class Conditional(NamedTuple): + if condition(): + y: int + else: + # error: [invalid-named-tuple] "Type qualifier `Final` is not allowed on NamedTuple field `y`" + y: Final[int] +``` + +Statically unreachable qualified declarations are ignored: + +```py +class Unreachable(NamedTuple): + if False: + hidden: Final[int] + visible: int +``` + +## NamedTuple qualifiers in deferred annotations + +The restriction still applies when annotation evaluation is postponed. The diagnostic does not claim +that defining the class will fail at runtime, because Python stores a forward reference in this +case. + +```py +from __future__ import annotations + +from typing import Final, NamedTuple + +class Deferred(NamedTuple): + # snapshot + x: Final[int] +``` + +```snapshot +error[invalid-named-tuple]: Type qualifier `Final` is not allowed in a NamedTuple field + --> src/mdtest_snippet.py:7:5 + | +7 | x: Final[int] + | ^^^^^^^^^^^^^ +``` + +## NamedTuple qualifiers in quoted and wrapped annotations + +Explicitly quoted and `Annotated` field annotations are rejected for the same static reason: + +```py +from typing import Annotated, Final, NamedTuple + +class Quoted(NamedTuple): + # error: [invalid-named-tuple] "Type qualifier `Final` is not allowed on NamedTuple field `x`" + x: "Final[int]" + +class Wrapped(NamedTuple): + # error: [invalid-named-tuple] "Type qualifier `Final` is not allowed on NamedTuple field `x`" + x: Annotated[Final[int], "metadata"] +``` + ## Prohibited NamedTuple attributes `NamedTuple` classes have certain synthesized attributes that cannot be overwritten. Attempting to diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/callable.md b/crates/ty_python_semantic/resources/mdtest/narrow/callable.md index 25e35e90bf..66f5795d93 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/callable.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/callable.md @@ -2,7 +2,7 @@ ## Basic narrowing -The `callable()` builtin returns `TypeIs[Callable[..., object]]`, which narrows the type to the +The `callable()` builtin returns `TypeIs[Top[Callable[..., object]]]`, which narrows the type to the intersection with `Top[Callable[..., object]]`. The `Top[...]` wrapper indicates this is a fully static type representing the top materialization of a gradual callable. @@ -54,7 +54,15 @@ def f(x: object): ## Calling narrowed callables -The narrowed type `Top[Callable[..., object]]` represents the set of all possible callable types +### Strict generic narrowing mode + +```toml +[analysis] +strict-generic-narrowing = true +``` + +In strict generic narrowing mode, an `isinstance(.., Callable)` check intersects the type with +`Top[Callable[..., object]]`. This type represents the set of all possible callable types (including, e.g., functions that take no arguments and functions that require arguments). While such objects *are* callable (they pass `callable()`), no specific set of arguments can be guaranteed to be valid. @@ -80,6 +88,36 @@ def resolve(value: str): reveal_type(value()) # revealed: object ``` +### Gradual generic narrowing mode + +```toml +[analysis] +strict-generic-narrowing = false +``` + +In gradual generic narrowing mode, an `isinstance(.., Callable)` check narrows to a gradual +callable. Its parameters accept arbitrary arguments, and its return type is `Unknown`: + +```py +from typing import Callable + +def call_with_args(y: object): + if isinstance(y, Callable): + reveal_type(y) # revealed: (...) -> Unknown + + reveal_type(y()) # revealed: Unknown + reveal_type(y(1, "foo")) # revealed: Unknown + reveal_type(y(1, "foo", keyword_arg="bar")) # revealed: Unknown +``` + +An already-specialized callable retains its known parameter and return types: + +```py +def preserve_callable_signature(fn: Callable[[int], str]) -> None: + if isinstance(fn, Callable): + reveal_type(fn) # revealed: (int, /) -> str +``` + ## Narrowing with named expressions (walrus operator) When `callable()` is used with a named expression, the target of the named expression should be @@ -139,9 +177,14 @@ import collections.abc def f(x: object): if isinstance(x, typing.Callable): - reveal_type(x) # revealed: Top[(...) -> object] + reveal_type(x) # revealed: (...) -> Unknown + else: + reveal_type(x) # revealed: ~Top[(...) -> object] + if isinstance(x, collections.abc.Callable): - reveal_type(x) # revealed: Top[(...) -> object] + reveal_type(x) # revealed: (...) -> Unknown + else: + reveal_type(x) # revealed: ~Top[(...) -> object] ``` ## `Callable` special-form identity diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/complex_target.md b/crates/ty_python_semantic/resources/mdtest/narrow/complex_target.md index f46e0bb784..4ead907a2e 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/complex_target.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/complex_target.md @@ -7,7 +7,7 @@ We support type narrowing for attributes and subscripts. ### Basic ```py -from ty_extensions import Unknown +from ty_extensions._internal import Unknown class C: x: int | None = 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 index 83b4b4b8b6..688b350a10 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md @@ -172,6 +172,31 @@ def compare_non_overlapping_literal_unions( reveal_type(left == right) # revealed: Literal[False] ``` +Adding `None` to either side must not change which enum values can match: + +```py +def compare_optional_left(left: Choice | None, right: Choice): + if left == right: + reveal_type(left) # revealed: Choice + else: + reveal_type(left) # revealed: Choice | None + +def compare_optional_right(left: Choice, right: Choice | None): + if left == right: + reveal_type(right) # revealed: Choice +``` + +With ty's default builtin-equality assumptions, neither an integer nor `None` matches a +string-valued enum member: + +```py +def compare_enum_with_integer(left: Choice | int | None, right: Choice): + if left == right: + reveal_type(left) # revealed: Choice + else: + reveal_type(left) # revealed: Choice | int | None +``` + Members with the same known value are aliases, even when one value comes from a function call. Comparisons between their canonical members are always true: @@ -404,7 +429,8 @@ member. Exact member comparisons are true or false when both values are known: ```py from enum import StrEnum -from typing import Literal +from typing import Any, Literal +from typing_extensions import assert_type class Left(StrEnum): A = "a" @@ -450,12 +476,117 @@ def compare_subsets( reveal_type(right) # revealed: Literal[Right.SHARED] ``` +When only one side can be `None`, equality still narrows both enums to their shared value: + +```py +def compare_optional_cross_enum_left(left: Left | None, right: Right): + if left == right: + reveal_type(left) # revealed: Literal[Left.SHARED] + reveal_type(right) # revealed: Literal[Right.SHARED] + +def compare_optional_cross_enum_right(left: Left, right: Right | None): + if left == right: + reveal_type(left) # revealed: Literal[Left.SHARED] + reveal_type(right) # revealed: Literal[Right.SHARED] +``` + +When both sides can be `None`, equality can match `None` or the shared string: + +```py +def compare_both_optional_cross_enums(left: Left | None, right: Right | None): + if left == right: + reveal_type(left) # revealed: Literal[Left.SHARED] | None + reveal_type(right) # revealed: Literal[Right.SHARED] | None +``` + +Under the same assumptions, an unrelated integer does not change which enum members match, whether +the condition uses `==` or `!=`: + +```py +def compare_cross_enums_with_integer(left: Left | None, right: Right | int): + if left == right: + reveal_type(left) # revealed: Literal[Left.SHARED] + reveal_type(right) # revealed: Literal[Right.SHARED] + + if left != right: + reveal_type(left) # revealed: Left | None + reveal_type(right) # revealed: Right | int + else: + reveal_type(left) # revealed: Literal[Left.SHARED] + reveal_type(right) # revealed: Literal[Right.SHARED] +``` + +A plain string can also match a member of the other enum. The string and every matching enum member +must remain possible: + +```py +def compare_left_string_against_enum_members(left: Left | Literal["b"], right: Right): + if left == right: + reveal_type(left) # revealed: Literal[Left.SHARED, "b"] + reveal_type(right) # revealed: Literal[Right.SHARED, Right.B] + +def compare_right_string_against_enum_members(left: Left, right: Right | Literal["a"]): + if left == right: + reveal_type(left) # revealed: Literal[Left.SHARED, Left.A] + assert_type(right, Literal[Right.SHARED, "a"]) +``` + +A `dict[str, Any]` is treated as having dictionary equality, so it cannot match a string-valued enum +member: + +```py +def compare_cross_enum_with_dictionary(left: Left | dict[str, Any], right: Right | None): + if left == right: + reveal_type(left) # revealed: Literal[Left.SHARED] + reveal_type(right) # revealed: Literal[Right.SHARED] +``` + +By contrast, `Any` can match any enum member. It must not exclude `None` from the other side: + +```py +def compare_optional_enum_against_any(left: Left | None, right: Right | Any): + if left == right: + reveal_type(left) # revealed: Left | None + reveal_type(right) # revealed: Literal[Right.SHARED] | Any + +def compare_any_against_optional_enum(left: Left | Any, right: Right | None): + if left == right: + reveal_type(left) # revealed: Literal[Left.SHARED] | Any + reveal_type(right) # revealed: Right | None +``` + +If the two sides have no matching values, `==` is always false and `!=` is always true. A shared +`None` makes `==` uncertain: + +```py +def compare_disjoint_cross_enum_alternatives( + left: Literal[Left.A] | None, + disjoint: Literal[Right.B] | Literal[1], + overlapping: Literal[Right.B] | None, +): + reveal_type(left == disjoint) # revealed: Literal[False] + reveal_type(left != disjoint) # revealed: Literal[True] + reveal_type(left == overlapping) # revealed: bool +``` + +When all possible values match, `==` is always true: + +```py +def compare_matching_cross_enum_alternatives( + left: Literal[Left.SHARED] | Literal["shared"], + right: Literal[Right.SHARED], +): + reveal_type(left == right) # revealed: Literal[True] + reveal_type(left != right) # revealed: Literal[False] +``` + The same comparison-key projection applies when each operand spans several enum classes. This example represents 18 possible values on each side, which would otherwise require 324 pairwise comparisons: ```py from enum import IntEnum +from typing import Literal class MixedLeft0(IntEnum): A = 0 @@ -510,6 +641,29 @@ def compare_mixed_domains( reveal_type(right) # revealed: MixedRight0 ``` +Treating `str` as having builtin equality, adding `None` or `str` does not prevent matches between +integer-valued enum classes: + +```py +def compare_multiple_integer_enums_with_other_values( + left: MixedLeft0 | MixedLeft1 | None, + right: MixedRight0 | MixedRight1 | str, +): + if left == right: + reveal_type(left) # revealed: MixedLeft0 + reveal_type(right) # revealed: MixedRight0 +``` + +Python considers `False` equal to `0`, so a `False` alternative can match an integer-valued enum +member even when the other enum has no matching members: + +```py +def compare_false_to_integer_enum(left: MixedLeft1 | Literal[False], right: MixedRight0): + if left == right: + reveal_type(left) # revealed: Literal[False] + reveal_type(right) # revealed: Literal[MixedRight0.A] +``` + An open identity-comparing enum can still be narrowed to all of its declared members. Undeclared runtime members are not retained merely because every declared member matches: @@ -606,6 +760,23 @@ def compare_open(left: OpenLeft, right: CustomRight): reveal_type(left) # revealed: OpenLeft ``` +A custom equality method must still determine the result when the enum is combined with `None`: + +```py +def compare_optional_custom(left: CustomLeft | None, right: CustomRight): + if left == right: + reveal_type(left) # revealed: CustomLeft +``` + +An enum with `_missing_` may have members that do not appear in its definition. Adding `None` must +not cause the comparison to assume that its declared member is the only possible match: + +```py +def compare_optional_open(left: OpenLeft | None, right: CustomRight): + if left == right: + reveal_type(left) # revealed: OpenLeft +``` + The same narrowing applies when comparing enum members directly with their inherited integer or string values. The negative constraint excludes both the builtin literal and every enum member known to compare equal to it: @@ -949,7 +1120,8 @@ def _(answer: CoupledInequality): ## Recursive aliases containing enum domains -Enum domains nested in a recursive alias fall back to general comparison inference: +Comparisons involving recursive enum aliases remain valid. Comparing against a specific enum member +narrows both branches to their remaining members while preserving any `NewType` tag. ```toml [environment] @@ -958,6 +1130,7 @@ python-version = "3.12" ```py from enum import Enum +from typing import NewType class EnumValue(Enum): VALUE = 1 @@ -967,6 +1140,90 @@ type Recursive = EnumValue | Recursive def _(left: Recursive, right: EnumValue): reveal_type(left == right) # revealed: bool + +BrandedEnumValue = NewType("BrandedEnumValue", EnumValue) +type RecursiveBrand = BrandedEnumValue | RecursiveBrand + +def compare_recursive_brand_to_member(left: RecursiveBrand) -> None: + if left == EnumValue.VALUE: + reveal_type(left) # revealed: BrandedEnumValue & Literal[EnumValue.VALUE] + else: + reveal_type(left) # revealed: BrandedEnumValue & Literal[EnumValue.OTHER] + + if left != EnumValue.VALUE: + reveal_type(left) # revealed: BrandedEnumValue & Literal[EnumValue.OTHER] + else: + reveal_type(left) # revealed: BrandedEnumValue & Literal[EnumValue.VALUE] +``` + +A recursive alias with changing type arguments may introduce values outside its original enum +domain. Here, `True` compares equal to the integer-valued enum member, so the `bool` alternative +must remain reachable. + +```py +from enum import IntEnum + +class Number(IntEnum): + ONE = 1 + TWO = 2 + +BrandedNumber = NewType("BrandedNumber", Number) +type Changing[T] = T | Changing[bool] + +def compare_changing_specialization(value: Changing[BrandedNumber]) -> None: + if value == Number.ONE: + reveal_type(value) # revealed: (BrandedNumber & Literal[Number.ONE]) | bool + else: + reveal_type(value) # revealed: (BrandedNumber & Literal[Number.TWO]) | bool +``` + +Mutually recursive aliases can likewise admit values outside their enum domain. Intersecting the +aliases does not remove their shared `bool` alternative. + +```py +from ty_extensions import Intersection + +type RecursiveWithBool = RecursiveWithBrand | bool +type RecursiveWithBrand = RecursiveWithBool | BrandedNumber + +def compare_mutually_recursive_intersection( + value: Intersection[RecursiveWithBool, RecursiveWithBrand], +) -> None: + if value == Number.ONE: + reveal_type(value) # revealed: bool | BrandedNumber + else: + reveal_type(value) # revealed: bool | BrandedNumber +``` + +## Recursive aliases containing gradual generic branches + +Equality narrowing must terminate when a recursive sequence alias contains a mapping with a gradual +key. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from collections.abc import Mapping, Sequence +from typing import Any + +type RecursiveMappingKey = Sequence[RecursiveMappingKey] | Mapping[Any, int] + +def narrow_recursive_mapping_key(value: RecursiveMappingKey) -> None: + assert value == 0 + _ = value +``` + +A gradual mapping value also must not cause recursive materialization to unfold indefinitely. + +```py +type RecursiveMappingValue = Sequence[RecursiveMappingValue] | Mapping[int, Any] + +def narrow_recursive_mapping_value(value: RecursiveMappingValue) -> None: + assert value == 0 + _ = value ``` ## Known built-in equality behavior @@ -1037,6 +1294,101 @@ def narrow_different_equality_implementations(value: FinalObject | FinalInt, oth reveal_type(value) # revealed: FinalObject ``` +## Sentinels + +Sentinels always compare equal to themselves, since they are singletons: + +```py +from typing_extensions import Sentinel + +MISSING = Sentinel("MISSING") + +reveal_type(MISSING == MISSING) # revealed: Literal[True] +``` + +## Known typing-object equality behavior + +Certain typing APIs are heavily special-cased by ty, which makes it tempting to special case +equality inference for these symbols. This, however, is error-prone: for example, ty currently +infers the same type for `typing_extensions.Literal` as it does for `typing.Literal`, even though +these may not be the same runtime object and may not compare equal. There's also no known use case +for precisely inferring equality comparisons between these objects. + +For most special-cased typing APIs, therefore, we simply fallback to the nominal instance that the +typing symbol is known to be an instance of: + +```toml +[environment] +python-version = "3.12" +``` + +```py +from functools import partial +from typing import Literal, NamedTuple +from typing_extensions import NamedTuple as ExtensionsNamedTuple +from ty_extensions._internal import generic_context + +type Alias = int + +class GenericClass[T]: ... + +reveal_type(Alias == Alias) # revealed: bool +reveal_type(generic_context(GenericClass) == generic_context(GenericClass)) # revealed: bool +reveal_type((int | str) == (int | str)) # revealed: bool +reveal_type(Literal[1] == Literal[1]) # revealed: bool + +def target(value: int) -> int: + return value + +# The bound `__call__` methods belong to distinct `partial` objects. +reveal_type(partial(target, 1).__call__ == partial(target, 1).__call__) # revealed: bool + +reveal_type(NamedTuple == ExtensionsNamedTuple) # revealed: bool +reveal_type(NamedTuple != ExtensionsNamedTuple) # revealed: bool +``` + +Repeated construction of `dataclasses.Field` and `typing_extensions.deprecated` produces distinct +objects that will compare unequal, even when their inferred payloads are identical: + +```py +from dataclasses import dataclass, field +from typing_extensions import deprecated + +@dataclass +class FieldComparisons: + # False at runtime! + equals: bool = reveal_type(field(default=1) == field(default=1)) # revealed: bool + # True at runtime! + not_equals: bool = reveal_type(field(default=1) != field(default=1)) # revealed: bool + +# False at runtime! +reveal_type(deprecated("gone") == deprecated("gone")) # revealed: bool +# True at runtime! +reveal_type(deprecated("gone") != deprecated("gone")) # revealed: bool +``` + +Runtime-significant metadata, spelling, and origin can be erased from the types that ty records for +many of these APIs. Just because ty infers two of these objects as being of the same type does not +therefore mean that they are equal: + +```py +import builtins +from collections.abc import Callable as AbcCallable +from typing import Annotated, Callable, List, Type, TypeAlias + +A: TypeAlias = "int" +B: TypeAlias = "builtins.int" + +# The `Annotated[]` metadata is discarded and ignored by ty, so these are inferred +# as having the same type, but they will compare unequal at runtime +reveal_type(Annotated[int, "a"] == Annotated[int, "b"]) # revealed: bool + +reveal_type(A == B) # revealed: bool +reveal_type(Callable[[int], str] == AbcCallable[[int], str]) # revealed: bool +reveal_type(List[int] == list[int]) # revealed: bool +reveal_type(Type[int] == type[int]) # revealed: bool +``` + ## Constrained type variables Equality analysis expands the constraints of a constrained type variable in either operand position. @@ -1044,7 +1396,8 @@ The resulting constraint is intersected with the type variable, preserving its i ```py from enum import Enum -from typing import Literal, TypeVar, final +from typing import Any, Generic, Literal, TypeVar, final +from ty_extensions import Intersection, Top @final class ConstraintA: ... @@ -1083,6 +1436,32 @@ def correlated_typevar_ne(value: E, other: EnumT) -> EnumT: return other reveal_type(value) # revealed: EnumT@correlated_typevar_ne return value + +LiteralT = TypeVar("LiteralT", Literal[1], Literal[2]) + +def correlated_literal_typevar_eq(value: Literal[1, 2], other: LiteralT) -> LiteralT: + if value == other: + return value + return other + +def correlated_literal_typevar_ne(value: Literal[1, 2], other: LiteralT) -> LiteralT: + if value != other: + return other + return value + +MaterializedT = TypeVar("MaterializedT", Literal[1], Intersection[Literal[2], Any]) + +HolderT = TypeVar("HolderT") + +class Holder(Generic[HolderT]): + def __init__(self, value: HolderT) -> None: + self.value = value + +def correlated_materialized_pattern(left: Top[MaterializedT], right: MaterializedT) -> int: + holder = Holder(right) + match left: + case holder.value: + return 1 ``` ## `LiteralString` and string-valued enums @@ -1304,6 +1683,59 @@ def preserve_custom_comparison(value: str | AlwaysEqual): reveal_type(value) # revealed: Literal["a"] | AlwaysEqual ``` +## String-literal origin and exclusions + +A string without literal origin can equal a string literal without acquiring the literal's origin. +The successful branch remains reachable and preserves the original exclusion. + +```py +from typing import Literal +from typing_extensions import LiteralString +from ty_extensions import Intersection, Not + +def without_literal_origin(value: Intersection[str, Not[LiteralString]]) -> None: + if value == "hello": + reveal_type(value) # revealed: str & ~LiteralString + value.definitely_missing_attribute # error: [unresolved-attribute] + + if "hello" == value: + reveal_type(value) # revealed: str & ~LiteralString + + if value != "hello": + reveal_type(value) # revealed: str & ~LiteralString + else: + reveal_type(value) # revealed: str & ~LiteralString +``` + +Excluding a particular string literal also leaves its runtime value possible when literal origin is +not known. A different literal can still narrow the string normally. + +```py +def without_literal_value(value: Intersection[str, Not[Literal["hello"]]]) -> None: + if value == "hello": + reveal_type(value) # revealed: str & ~Literal["hello"] + + if value == "goodbye": + reveal_type(value) # revealed: Literal["goodbye"] +``` + +Optional alternatives that cannot compare equal are still removed without discarding the possible +string value. + +```py +def optional_without_literal_origin(value: Intersection[str, Not[LiteralString]] | None) -> None: + if value == "hello": + reveal_type(value) # revealed: str & ~LiteralString +``` + +Once literal origin is known, excluding a string literal really does exclude its runtime value. + +```py +def trusted_value_is_excluded(value: Intersection[LiteralString, Not[Literal["hello"]]]) -> None: + if value == "hello": + reveal_type(value) # revealed: Never +``` + ## `x != y` where `y` is of literal type ```py @@ -1314,7 +1746,7 @@ def _(x: Literal[1, 2]): reveal_type(x) # revealed: Literal[2] ``` -## `x != y` where `y` is a single-valued type +## `x != y` where `y` is a class literal ```py def _(flag: bool): @@ -1328,7 +1760,7 @@ def _(flag: bool): reveal_type(C) # revealed: ``` -## `x != y` where `y` has multiple single-valued options +## `x != y` where `y` has multiple literal options ```py from typing import Literal @@ -1359,9 +1791,9 @@ def _(x: Literal[1, 2], y: Y): reveal_type(x) # revealed: Literal[1, 2] ``` -## `!=` for non-single-valued types +## `!=` for broad types -Only single-valued types should narrow the type: +A broad right-hand type cannot narrow `x`: ```py def _(x: int | None, y: int): @@ -1369,7 +1801,7 @@ def _(x: int | None, y: int): reveal_type(x) # revealed: int | None ``` -## Mix of single-valued and non-single-valued types +## Mix of literal and broad types ```py from typing import Literal @@ -1432,6 +1864,28 @@ def overwritten_tagged_union(value: A | B | bool): reveal_type(value) # revealed: Literal[True] else: reveal_type(value) # revealed: Literal[False] + +def overwritten_tagged_union_attribute(value: A | B | str): + if isinstance(value, (A, B)): + if (value := value.tag) == "a": + reveal_type(value) # revealed: Literal["a"] + else: + reveal_type(value) # revealed: Literal["b"] + +def tagged_union_rebound_by_comparator(value: A | B | str): + if isinstance(value, (A, B)): + if value.tag == (value := "a"): + reveal_type(value) # revealed: Literal["a"] + else: + reveal_type(value) # revealed: Literal["a"] + +def tagged_union_with_unrelated_assignment(value: A | B): + if value.tag == (tag := "a"): + reveal_type(value) # revealed: A + reveal_type(tag) # revealed: Literal["a"] + else: + reveal_type(value) # revealed: B + reveal_type(tag) # revealed: Literal["a"] ``` ## Union with `Any` @@ -1439,7 +1893,10 @@ def overwritten_tagged_union(value: A | B | bool): ```py import sys from enum import Enum, IntEnum -from typing import Any, Literal, TypeVar +from typing import Any, Literal, TypeAlias, TypeVar + +from ty_extensions._internal import Unknown +from typing_extensions import assert_never, assert_type T = TypeVar("T", bound=object) U = TypeVar("U") @@ -1450,6 +1907,9 @@ class Color(Enum): RED = 1 BLUE = 2 +class OtherColor(Enum): + RED = 1 + class NonReflexive(Enum): VALUE = 1 @@ -1520,7 +1980,7 @@ def _(x: Any, y: Any | str): def _(x: Any): if x != list[Any]: - reveal_type(x) # revealed: Any & ~ + reveal_type(x) # revealed: Any def _(x: Any, y: SingleIntEnum): if x == y: @@ -1537,7 +1997,148 @@ def _(x: Any): if x == RUNTIME_TYPE_VAR: pass else: - reveal_type(x) # revealed: Any & ~TypeVar + reveal_type(x) # revealed: Any +``` + +`Any` must stay `Any` when compared with an enum, on either side of the comparison: + +```py +def enum_against_any(value: Color, other: Any): + if value != other: + reveal_type(other) # revealed: Any + +def any_against_enum(value: Any, other: Color): + if value != other: + reveal_type(value) # revealed: Any +``` + +`Any` must also stay `Any` when the enum can be `None`: + +```py +def optional_enum_against_any(value: Color | None, other: Any): + if value != other: + reveal_type(other) # revealed: Any + +def any_against_optional_enum(value: Any, other: Color | None): + if value != other: + reveal_type(value) # revealed: Any +``` + +`Any` must also stay `Any` when compared with `bool | None`: + +```py +def optional_bool_against_any(value: bool | None, other: Any): + if value != other: + reveal_type(other) # revealed: Any +``` + +Comparing `Color | Any` with `Color | None` must keep both `Color` and `Any`: + +```py +def gradual_enum_union(value: Color | Any, other: Color | None): + if value != other: + reveal_type(value) # revealed: Color | Any +``` + +`Color | Any` must stay unchanged when the other value can be an enum member or `None`. This applies +to `!=` and the false branch of `==`: + +```py +def any_union_against_optional_enum_member(value: Color | Any, other: Literal[Color.RED] | None): + if value != other: + reveal_type(value) # revealed: Color | Any + assert_type(value, Color | Any) + +def any_union_against_optional_enum_member_equality_else(value: Color | Any, other: Literal[Color.RED] | None): + if value == other: + return + reveal_type(value) # revealed: Color | Any +``` + +An alias for `Any` must preserve the same result: + +```py +AnyAlias: TypeAlias = Any + +def any_alias_union_against_optional_enum_member(value: Color | AnyAlias, other: Literal[Color.RED] | None): + if value != other: + reveal_type(value) # revealed: Color | Any +``` + +The same comparisons also preserve `Unknown`: + +```py +def unknown_union_against_optional_enum_member(value: Color | Unknown, other: Literal[Color.RED] | None): + if value != other: + reveal_type(value) # revealed: Color | Unknown + assert_type(value, Color | Unknown) + +def unknown_union_against_optional_enum_member_equality_else(value: Color | Unknown, other: Literal[Color.RED] | None): + if value == other: + return + reveal_type(value) # revealed: Color | Unknown +``` + +When an enum check and a comparison are combined with `and`, either condition can be false. The +original union must therefore be preserved: + +```py +def any_union_after_enum_check(value: Color | Any, other: Color | Any): + if isinstance(value, Color) and value == other: + return + reveal_type(value) # revealed: Color | Any + assert_type(value, Color | Any) + +def unknown_union_after_enum_check(value: Color | Unknown, other: Color | Unknown): + if isinstance(value, Color) and value == other: + return + reveal_type(value) # revealed: Color | Unknown + assert_type(value, Color | Unknown) +``` + +The second comparison can fail even when the first one matches, so both possible types must remain: + +```py +def any_union_after_failed_comparisons(value: Color | Any, other: OtherColor | None): + if value == Color.RED and value == other: + return + reveal_type(value) # revealed: Color | Any + assert_type(value, Color | Any) + +def unknown_union_after_failed_comparisons(value: Color | Unknown, other: OtherColor | None): + if value == Color.RED and value == other: + return + reveal_type(value) # revealed: Color | Unknown + assert_type(value, Color | Unknown) +``` + +These `Enum` classes compare by identity, so their members are not equal even when their underlying +values match. Comparing with `OtherColor.RED` must therefore exclude every `Color` member: + +```py +def any_comparison_with_other_enum(value: Color | OtherColor | Any): + if value == OtherColor.RED: + reveal_type(value) # revealed: OtherColor | (Any & ~Color) + if isinstance(value, Color): + assert_never(value) + +def unknown_comparison_with_other_enum(value: Color | OtherColor | Unknown): + if value == OtherColor.RED: + reveal_type(value) # revealed: OtherColor | (Unknown & ~Color) + if isinstance(value, Color): + assert_never(value) +``` + +`Color | Any` must also stay unchanged after either `==` or `!=`: + +```py +def gradual_enum_union_against_enum(value: Color | Any, other: Color): + if value == other: + reveal_type(value) # revealed: Color | Any + +def gradual_enum_union_inequality(value: Color | Any, other: Color): + if value != other: + reveal_type(value) # revealed: Color | Any ``` ## Booleans and integers @@ -1638,7 +2239,7 @@ We assume that tuple subclasses don't override `tuple.__eq__`, which only return tuples. So they are excluded from the narrowed type when comparing to non-tuple values. ```py -from typing import Literal +from typing import Literal, cast def _(x: Literal["a", "b"] | tuple[int, int]): if x == "a": @@ -1647,21 +2248,227 @@ def _(x: Literal["a", "b"] | tuple[int, int]): else: # tuple type remains in the else branch reveal_type(x) # revealed: Literal["b"] | tuple[int, int] + +class OpenTupleSubclass(tuple[int, int]): ... + +def _(x: Literal["a", "b"] | OpenTupleSubclass): + if x == "a": + reveal_type(x) # revealed: Literal["a"] + else: + reveal_type(x) # revealed: Literal["b"] | OpenTupleSubclass + +def inequality_else(value: str | tuple[str | None, str | None, str] | None) -> None: + if value == "files": + pass + elif value != "response": + return + + reveal_type(value) # revealed: Literal["files", "response"] + cast(Literal["files", "response"], value) # error: [redundant-cast] ``` -## Narrowing tagged unions of nominal classes by attribute +Fixed-length tuples compare corresponding elements using identity before equality, so distinct +inferred element types can still make the result definite. Different lengths cannot compare equal: ```py -from typing import Literal +from enum import Enum +from typing import Final, Literal, NewType -class A: +class TupleValues: + TRUE: Final = (True,) + LONGER: Final = (True, 0) + +def equivalent_tuple_pattern(value: tuple[Literal[1]]) -> int: + match value: + case TupleValues.TRUE: + return 1 + +def different_length_tuple_pattern(value: tuple[Literal[1]]) -> None: + match value: + case TupleValues.LONGER: + reveal_type(value) # revealed: Never + +class NeverEqualTupleElement(Enum): + A = 1 + B = 2 + + def __eq__(self, other: object) -> Literal[False]: + return False + +reveal_type((NeverEqualTupleElement.A,) == (NeverEqualTupleElement.A,)) # revealed: Literal[True] +reveal_type((NeverEqualTupleElement.A,) != (NeverEqualTupleElement.A,)) # revealed: Literal[False] + +def tuple_with_non_reflexive_elements(left: NeverEqualTupleElement, right: NeverEqualTupleElement) -> None: + reveal_type((left,) == (right,)) # revealed: bool + reveal_type((left,) != (right,)) # revealed: bool + +LeftElement = NewType("LeftElement", NeverEqualTupleElement) +RightElement = NewType("RightElement", NeverEqualTupleElement) + +def tuple_with_erased_element_identity(value: NeverEqualTupleElement) -> None: + reveal_type((LeftElement(value),) == (RightElement(value),)) # revealed: bool + reveal_type((LeftElement(value),) != (RightElement(value),)) # revealed: bool +``` + +## Narrowing with NewTypes + +A `NewType` constructor returns its argument unchanged at runtime. A `WrappedIdentityEnum` value can +therefore be either `IdentityEnum.A` or `IdentityEnum.B`, so comparing it with `IdentityEnum.A` has +an unknown result: + +```py +from enum import Enum +from typing import NewType + +class IdentityEnum(Enum): + A = 1 + B = 2 + +WrappedIdentityEnum = NewType("WrappedIdentityEnum", IdentityEnum) + +def literal_with_erased_identity(value: WrappedIdentityEnum) -> None: + reveal_type(IdentityEnum.A == value) # revealed: bool + reveal_type(IdentityEnum.A != value) # revealed: bool +``` + +When a `WrappedIdentityEnum` value is `IdentityEnum.B`, equality narrows another `IdentityEnum` +value to the same member. The first value keeps its `WrappedIdentityEnum` type, and both operands +can be passed to a function accepting `Literal[IdentityEnum.B]`. + +```py +from typing import Literal, TypeAlias +from ty_extensions import Intersection + +def accepts_b(value: Literal[IdentityEnum.B]) -> None: ... +def compare_branded_member( + branded: Intersection[WrappedIdentityEnum, Literal[IdentityEnum.B]], + other: IdentityEnum, +) -> None: + if branded == other: + reveal_type(branded) # revealed: WrappedIdentityEnum & Literal[IdentityEnum.B] + reveal_type(other) # revealed: Literal[IdentityEnum.B] + accepts_b(branded) + accepts_b(other) + else: + reveal_type(other) # revealed: Literal[IdentityEnum.A] + +NestedIdentityEnum = NewType("NestedIdentityEnum", WrappedIdentityEnum) +NestedAlias: TypeAlias = NestedIdentityEnum + +def compare_nested_brand(value: NestedAlias, other: Literal[IdentityEnum.A]) -> None: + if value == other: + reveal_type(value) # revealed: NestedIdentityEnum & Literal[IdentityEnum.A] + else: + reveal_type(value) # revealed: NestedIdentityEnum & Literal[IdentityEnum.B] +``` + +`NewType` does not change how an `IntEnum` compares: values from different `IntEnum` classes still +compare by their integer values. A custom enum `__eq__` method likewise still determines the result +after its value is passed through a `NewType` constructor. + +```py +from enum import IntEnum + +class FirstNumber(IntEnum): + ONE = 1 + TWO = 2 + +class SecondNumber(IntEnum): + ONE = 1 + THREE = 3 + +BrandedFirstNumber = NewType("BrandedFirstNumber", FirstNumber) +BrandedSecondNumber = NewType("BrandedSecondNumber", SecondNumber) + +def compare_branded_int_enums(left: BrandedFirstNumber, right: BrandedSecondNumber) -> None: + if left == right: + reveal_type(left) # revealed: BrandedFirstNumber & Literal[FirstNumber.ONE] + reveal_type(right) # revealed: BrandedSecondNumber & Literal[SecondNumber.ONE] + +class NeverEqualEnum(Enum): + A = 1 + B = 2 + + def __eq__(self, other: object) -> Literal[False]: + return False + +BrandedNeverEqual = NewType("BrandedNeverEqual", NeverEqualEnum) + +def branded_custom_equality(value: BrandedNeverEqual, other: NeverEqualEnum) -> None: + reveal_type(value == other) # revealed: Literal[False] + reveal_type(value != other) # revealed: bool +``` + +## Narrowing with enums that have custom `__eq__` methods + +Custom enum comparison methods with definite return types determine equality and inequality +independently: + +```py +from enum import Enum +from typing import Any, Literal + +class AlwaysEqualEnum(Enum): + A = 1 + B = 2 + + def __eq__(self, other: object) -> Literal[True]: + return True + +class NeverUnequalEnum(Enum): + A = 1 + B = 2 + + def __ne__(self, other: object) -> Literal[False]: + return False + +reveal_type(AlwaysEqualEnum.A == AlwaysEqualEnum.B) # revealed: Literal[True] +reveal_type(NeverUnequalEnum.A != NeverUnequalEnum.B) # revealed: Literal[False] + +def tuple_with_custom_equality(left: AlwaysEqualEnum, right: AlwaysEqualEnum) -> None: + reveal_type((left,) == (right,)) # revealed: Literal[True] + reveal_type((left,) != (right,)) # revealed: Literal[False] + +def never_unequal_narrowing(x: Any, value: Literal[NeverUnequalEnum.A]) -> None: + if x != value: + reveal_type(x) # revealed: Any & ~Literal[NeverUnequalEnum.A] +``` + +## Narrowing tagged unions by attribute + +```py +from typing import Literal, Protocol + +from ty_extensions import Intersection + +class BaseA: tag: Literal["a"] + +class A(BaseA): field_a: int class B: tag: Literal["b"] field_b: str +class Marker(Protocol): + marked: bool + +class TaggedA(Protocol): + field_a: int + + @property + def tag(self) -> Literal["a"]: ... + +class TaggedB(Protocol): + field_b: str + + @property + def tag(self) -> Literal["b"]: ... + +class Container: + value: A | B | None + def _(x: A | B): if x.tag == "a": reveal_type(x) # revealed: A @@ -1679,6 +2486,46 @@ def _(x: A | B): reveal_type(x) # revealed: B else: reveal_type(x) # revealed: A + +def truthiness_guard(value: A | B | None): + # error: [overlapping-condition] "This condition does not distinguish between `A & ~AlwaysTruthy`, `B & ~AlwaysTruthy` and `None`" + if not value: + return + + reveal_type(value) # revealed: (A & ~AlwaysFalsy) | (B & ~AlwaysFalsy) + + if value.tag == "a": + reveal_type(value) # revealed: A & ~AlwaysFalsy + reveal_type(value.field_a) # revealed: int + else: + reveal_type(value) # revealed: B & ~AlwaysFalsy + reveal_type(value.field_b) # revealed: str + +def nested_attribute_after_truthiness_guard(container: Container): + # error: [overlapping-condition] "This condition does not distinguish between `A & ~AlwaysTruthy`, `B & ~AlwaysTruthy` and `None`" + if not container.value: + return + + if container.value.tag == "a": + reveal_type(container.value) # revealed: A & ~AlwaysFalsy + reveal_type(container.value.field_a) # revealed: int + else: + reveal_type(container.value) # revealed: B & ~AlwaysFalsy + reveal_type(container.value.field_b) # revealed: str + +def positive_intersection(value: Intersection[A, Marker] | Intersection[B, Marker]): + if value.tag == "a": + reveal_type(value) # revealed: A & Marker + else: + reveal_type(value) # revealed: B & Marker + +def protocol_union(value: TaggedA | TaggedB): + if value.tag == "a": + reveal_type(value) # revealed: TaggedA + reveal_type(value.field_a) # revealed: int + else: + reveal_type(value) # revealed: TaggedB + reveal_type(value.field_b) # revealed: str ``` Enum literals are also supported as attribute tags: @@ -1752,17 +2599,23 @@ def _(x: A | B): ## Enabling strict equality narrowing -The `strict-equality-semantics` option can be enabled to preserve broad builtin types and union -members that a subclass could compare equal to. Narrowing types that are already literal unions -remains safe and is unaffected. This also applies to tuples, whose subclasses can override equality. +Enabling `strict-equality-semantics` accounts for builtin subclasses that override `__eq__` or +compare equal to a literal without belonging to its `Literal` type. It preserves broad builtin types +and union alternatives that could compare equal, including tuples. Literal unions and enum members +are still narrowed when it is safe. ```toml +[environment] +python-version = "3.11" + [analysis] strict-equality-semantics = true ``` ```py -from typing import Literal +from enum import IntEnum, StrEnum +from typing import Any, Literal, LiteralString +from ty_extensions import Intersection, Not def broad(value: str): if value == "a": @@ -1776,10 +2629,75 @@ def inequality(value: str): else: reveal_type(value) # revealed: str +def without_literal_origin(value: Intersection[str, Not[LiteralString]]): + if value == "a": + reveal_type(value) # revealed: str & ~LiteralString + +def trusted_value_is_excluded(value: Intersection[LiteralString, Not[Literal["a"]]]): + reveal_type(value == "a") # revealed: Literal[False] + def literal(value: Literal["a", "b"]): if value == "a": reveal_type(value) # revealed: Literal["a"] +class Left(StrEnum): + A = "a" + SHARED = "shared" + +class Right(StrEnum): + SHARED = "shared" + B = "b" + +def compare_enum_with_integer(left: Left | int | None, right: Left): + if left == right: + reveal_type(left) # revealed: Left | int + +def compare_cross_enums_with_integer(left: Left | None, right: Right | int): + if left == right: + reveal_type(left) # revealed: Left | None + reveal_type(right) # revealed: Literal[Right.SHARED] | int + + if left != right: + reveal_type(left) # revealed: Left | None + reveal_type(right) # revealed: Right | int + else: + reveal_type(left) # revealed: Left | None + reveal_type(right) # revealed: Literal[Right.SHARED] | int + +def compare_cross_enum_with_dictionary(left: Left | dict[str, Any], right: Right | None): + if left == right: + reveal_type(left) # revealed: Literal[Left.SHARED] | dict[str, Any] + reveal_type(right) # revealed: Right | None + +def compare_both_optional_cross_enums(left: Left | None, right: Right | None): + if left == right: + reveal_type(left) # revealed: Literal[Left.SHARED] | None + reveal_type(right) # revealed: Literal[Right.SHARED] | None + +class MixedLeft0(IntEnum): + ZERO = 0 + ONE = 1 + +class MixedLeft1(IntEnum): + TWO = 2 + THREE = 3 + +class MixedRight0(IntEnum): + ZERO = 0 + ONE = 1 + +class MixedRight1(IntEnum): + FOUR = 4 + FIVE = 5 + +def compare_multiple_integer_enums_with_other_values( + left: MixedLeft0 | MixedLeft1 | None, + right: MixedRight0 | MixedRight1 | str, +): + if left == right: + reveal_type(left) # revealed: MixedLeft0 | MixedLeft1 | None + reveal_type(right) # revealed: MixedRight0 | str + class Foo: ... def union(value: Foo | None, other: Foo): diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md index 3869e5c6d7..60e40c5a27 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md @@ -70,7 +70,6 @@ def _(x: Literal[1, 2, "a", "b", False, b"abc"]): reveal_type(x) # revealed: Literal[2, "a"] elif x in (b"abc",): reveal_type(x) # revealed: Literal[b"abc"] - # error: [unsupported-operator] elif x not in (3,): reveal_type(x) # revealed: Literal["b", False] else: @@ -544,7 +543,8 @@ def unrelated_typevar(x: AlwaysEqual, y: U) -> U: ## Direct `not in` conditional ```py -from typing import Any, Literal, TypeVar +from enum import Enum +from typing import Any, Literal, NewType, TypeVar T = TypeVar("T", Literal[1], Literal[2]) @@ -596,6 +596,44 @@ def correlated_typevar(x: T | None, y: T) -> None: if x not in (y,): reveal_type(x) # revealed: None +def empty_tuple_slot(x: tuple[()] | None) -> None: + if x not in ((),): + reveal_type(x) # revealed: None + +def fixed_tuple_slot(x: tuple[Literal[1], Literal["x"]] | None) -> None: + if x not in ((1, "x"),): + reveal_type(x) # revealed: None + +# We optimistically assume that an unseen runtime subclass does not override `tuple.__eq__`. +class OpenTupleSubclass(tuple[Literal[1], Literal["x"]]): ... + +def tuple_subclass_slot(x: OpenTupleSubclass | None, value: OpenTupleSubclass) -> None: + if x not in (value,): + reveal_type(x) # revealed: None + +WrappedTuple = NewType("WrappedTuple", tuple[Literal[1], Literal["x"]]) + +def newtype_tuple_slot(x: WrappedTuple | None, value: WrappedTuple) -> None: + if x not in (value,): + reveal_type(x) # revealed: None + +class ReflexiveEnum(Enum): + A = 1 + B = 2 + + def __eq__(self, other: object) -> Literal[True]: + return True + +E = TypeVar("E", Literal[ReflexiveEnum.A], Literal[ReflexiveEnum.B]) + +def reflexive_enum_literal_slot(x: Literal[ReflexiveEnum.A] | None, value: Literal[ReflexiveEnum.A]) -> None: + if x not in (value,): + reveal_type(x) # revealed: Never + +def reflexive_enum_typevar_slot(x: E | None, value: E) -> None: + if x not in (value,): + reveal_type(x) # revealed: Never + def tuple_with_any_slot(x: str | None, missing: Any) -> None: if x not in (missing, None): reveal_type(x) # revealed: str @@ -618,6 +656,21 @@ def mutable_global_rhs(x: str | None, unavailable: set[str | None]) -> None: reveal_type(x) # revealed: str | None ``` +## Recursive tuple slots + +```toml +[environment] +python-version = "3.12" +``` + +```py +type Recursive = tuple[Recursive, int] + +def recursive_tuple_slot(x: Recursive | None, value: Recursive) -> None: + if x not in (value,): + reveal_type(x) # revealed: tuple[Recursive, int] | None +``` + ## Membership and equality When containment is known to compare items using equality, we can remove a union member that cannot @@ -671,10 +724,15 @@ def builtin_equality_and_membership(x: str | None, y: str, values: list[str]): class C: ... def broad_union_membership(origin: C | int): - # the fork types `__contains__` with `Overlapping[Element]`, so a membership test - # that can never hold is an error rather than a silently-false comparison - if origin in ("x",): # error: [unsupported-operator] + # membership in a literal tuple is folded element by element, so a test that can never + # hold is simply false. the `Overlapping[Element]` rejection below applies to every other + # container, where there is no fold to give an exact answer + if origin in ("x",): reveal_type(origin) # revealed: Never + +def disjoint_membership_in_a_list(value: int, values: list[str]): + if value in values: # error: [unsupported-operator] + reveal_type(value) # revealed: Never ``` ```py @@ -710,6 +768,16 @@ def custom_equality(x: AlwaysEqual | Literal[1]): def empty_tuple(x: Payload | Literal["missing"], values: tuple[()]): if x in values: reveal_type(x) # revealed: Never + +def incompatible_tuple_key( + key: tuple[str, bool, bool], + values: dict[tuple[str, bool], int], +) -> int | None: + # a `tuple[str, bool, bool]` can never be a `tuple[str, bool]` key + if key in values: # error: [unsupported-operator] + reveal_type(key) # revealed: Never + return values[key] + return None ``` ## Custom containment methods @@ -1098,6 +1166,11 @@ After the `isinstance` check, `values` has type `Iterable[Literal[1]] & tuple[ob semantics were checked: the `tuple` component establishes that membership compares against its elements, while the `Iterable` component constrains those elements to `Literal[1]`. +```toml +[analysis] +strict-generic-narrowing = true +``` + ```py from collections.abc import Iterable from typing import Literal, final @@ -1217,8 +1290,12 @@ def _(x: bool | str): ## LiteralString +Known literal-origin strings can safely narrow to the matching members of a literal tuple. + ```py +from typing import Literal from typing_extensions import LiteralString +from ty_extensions import Intersection, Not def _(x: LiteralString): if x in ("a", "b", "c"): @@ -1233,6 +1310,24 @@ def _(x: LiteralString | int): reveal_type(x) # revealed: (LiteralString & ~Literal["a"] & ~Literal["b"] & ~Literal["c"]) | int ``` +A string without literal origin can match a tuple member without gaining that member's origin. + +```py +def without_literal_origin(value: Intersection[str, Not[LiteralString]]) -> None: + if value in ("hello",): + reveal_type(value) # revealed: str & ~LiteralString +``` + +An excluded value cannot appear in a tuple when the candidate already has known literal origin. + +```py +def trusted_value_is_excluded(value: Intersection[LiteralString, Not[Literal["hello"]]]) -> None: + reveal_type(value in ("hello",)) # revealed: Literal[False] + + if value in ("hello",): + reveal_type(value) # revealed: Never +``` + ## enums ```py diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md index 6661ca5db0..57ef6e7d45 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md @@ -28,6 +28,268 @@ def _(x: A, y: A | None): reveal_type(y) # revealed: A | None ``` +Identity also transfers facts about the shared object, such as whether a string is truthy. + +```py +def truthy_string(value: object, text: str) -> None: + if text: + if value is text: + reveal_type(value) # revealed: str & ~AlwaysFalsy +``` + +## `is` with invariant generic types + +A `list[int]` guarantees that values read from the list are integers. That guarantee must hold for +every reference to the same mutable list: if another reference could treat it as `list[str]`, it +could append a string that the first reference would then incorrectly read as an integer. An +identity comparison can therefore transfer the invariant type argument. + +```py +def generic_type(value: object, items: list[int]) -> None: + if value is items: + reveal_type(value) # revealed: list[int] +``` + +Incompatible invariant specializations cannot describe the same object in soundly typed code. + +```py +def incompatible_generic_types(integers: list[int], strings: list[str]) -> None: + reveal_type(integers is strings) # revealed: Literal[False] + if integers is strings: + reveal_type(integers) # revealed: Never + reveal_type(strings) # revealed: Never +``` + +## `is` with covariant generic types + +Covariant specializations can describe the same object: an empty tuple belongs to both +`tuple[int, ...]` and `tuple[str, ...]`. Identity therefore remains possible and preserves both sets +of type arguments. + +```py +def covariant_generic_type(value: object, items: tuple[int, ...]) -> None: + if value is items: + reveal_type(value) # revealed: tuple[int, ...] + +def overlapping_generic_types(integers: tuple[int, ...], strings: tuple[str, ...]) -> None: + reveal_type(integers is strings) # revealed: bool + if integers is strings: + # TODO: Ideally, these intersections would simplify to tuple[()]. + reveal_type(integers) # revealed: tuple[int, ...] & tuple[str, ...] + reveal_type(strings) # revealed: tuple[str, ...] & tuple[int, ...] +``` + +## `is` with a `NewType` + +A `NewType` constructor returns its argument unchanged, so its tag belongs to one static view rather +than the shared object. Identity can establish the underlying type without transferring that tag. + +```py +from typing import NewType + +UserId = NewType("UserId", int) + +def discard_newtype_tag(value: object, user_id: UserId) -> None: + if value is user_id: + reveal_type(value) # revealed: int + reveal_type(user_id) # revealed: UserId +``` + +## `is` with unconstrained type variables + +An unconstrained type variable can hold a `NewType`. Identity therefore cannot transfer the type +variable, since doing so would also transfer the `NewType` tag. + +```py +from typing import NewType, TypeVar + +T = TypeVar("T") +UserId = NewType("UserId", int) + +def type_variable(value: object, other: T) -> T: + if value is other: + reveal_type(value) # revealed: object + reveal_type(other) # revealed: T@type_variable + return other + +reveal_type(type_variable(1, UserId(1))) # revealed: UserId +``` + +## `is` with bounded type variables + +A type variable bounded by `int` can still hold an integer `NewType`. Identity transfers its `int` +bound without transferring the type variable or its possible `NewType` tag. + +```py +from typing import NewType, TypeVar + +BoundedT = TypeVar("BoundedT", bound=int) +UserId = NewType("UserId", int) + +def bounded_type_variable(value: object, other: BoundedT) -> BoundedT: + if value is other: + reveal_type(value) # revealed: int + reveal_type(other) # revealed: BoundedT@bounded_type_variable + return other + +reveal_type(bounded_type_variable(1, UserId(1))) # revealed: UserId +``` + +## `is` with constrained type variables + +Identity transfers a constrained type variable's possible runtime types without transferring any +`NewType` tags in its constraints. + +```py +from typing import NewType, TypeVar + +UserId = NewType("UserId", int) +TaggedChoice = TypeVar("TaggedChoice", UserId, str) + +def constrained_type_variable(value: object, other: TaggedChoice) -> None: + if value is other: + reveal_type(value) # revealed: int | str + reveal_type(other) # revealed: TaggedChoice@constrained_type_variable +``` + +## Narrowing tagged unions of nominal classes by attribute identity + +```py +from dataclasses import dataclass +from enum import Enum +from typing import Literal, NewType + +@dataclass +class Foo: + tag: Literal[False] + +@dataclass +class Bar: + tag: Literal[True] + +@dataclass +class UnknownTag: + tag: bool + +def boolean_tags(value: Foo | Bar): + # error: [redundant-boolean-comparison] "Comparison of a `bool` with `True` is redundant" + if value.tag is True: + reveal_type(value) # revealed: Bar + else: + reveal_type(value) # revealed: Foo + + # error: [redundant-boolean-comparison] "Comparison of a `bool` with `True` is redundant" + if value.tag is not True: + reveal_type(value) # revealed: Foo + else: + reveal_type(value) # revealed: Bar + + # error: [redundant-boolean-comparison] "Comparison of a `bool` with `True` is redundant" + if True is value.tag: + reveal_type(value) # revealed: Bar + else: + reveal_type(value) # revealed: Foo + + # error: [redundant-boolean-comparison] "Comparison of a `bool` with `True` is redundant" + if True is not value.tag: + reveal_type(value) # revealed: Foo + else: + reveal_type(value) # revealed: Bar + +def ambiguous_tag(value: Foo | Bar | UnknownTag): + # error: [redundant-boolean-comparison] "Comparison of a `bool` with `True` is redundant" + if value.tag is True: + reveal_type(value) # revealed: Bar | UnknownTag + else: + reveal_type(value) # revealed: Foo | UnknownTag + +def nonsingleton_tag(value: Foo | Bar, tag: bool): + if value.tag is tag: + reveal_type(value) # revealed: Foo | Bar + else: + reveal_type(value) # revealed: Foo | Bar + +def overwritten_tagged_union(value: Foo | Bar | bool): + if isinstance(value, (Foo, Bar)): + # error: [redundant-boolean-comparison] "Comparison of a `bool` with `True` is redundant" + if (value := value.tag) is True: + reveal_type(value) # revealed: Literal[True] + else: + reveal_type(value) # revealed: Literal[False] + +def tagged_union_rebound_by_comparator(value: Foo | Bar | bool): + if isinstance(value, (Foo, Bar)): + if value.tag is (value := True): + reveal_type(value) # revealed: Literal[True] + else: + reveal_type(value) # revealed: Literal[True] + +def tagged_union_with_unrelated_assignment(value: Foo | Bar): + if value.tag is (tag := True): + reveal_type(value) # revealed: Bar + reveal_type(tag) # revealed: Literal[True] + else: + reveal_type(value) # revealed: Foo + reveal_type(tag) # revealed: Literal[True] + +class MissingTag: + tag: None + +class PresentTag: + tag: str + +def optional_tags(value: MissingTag | PresentTag): + if value.tag is None: + reveal_type(value) # revealed: MissingTag + else: + reveal_type(value) # revealed: PresentTag + +class Tag(Enum): + FOO = 1 + BAR = 2 + +class EnumFoo: + tag: Literal[Tag.FOO] + +class EnumBar: + tag: Literal[Tag.BAR] + +def enum_tags(value: EnumFoo | EnumBar): + if value.tag is Tag.FOO: + reveal_type(value) # revealed: EnumFoo + else: + reveal_type(value) # revealed: EnumBar + +BoolTag = NewType("BoolTag", bool) + +class NewTypeTag: + tag: BoolTag + +def newtype_tags(value: Foo | Bar | NewTypeTag): + # error: [redundant-boolean-comparison] "Comparison of a `bool` with `True` is redundant" + if value.tag is True: + reveal_type(value) # revealed: Bar | NewTypeTag + else: + reveal_type(value) # revealed: Foo | NewTypeTag + +def nonsingleton_newtype_tag(value: Foo | Bar, tag: BoolTag): + if value.tag is tag: + reveal_type(value) # revealed: Foo | Bar + else: + reveal_type(value) # revealed: Foo | Bar + +def boolean_tags_after_truthiness(value: Foo | Bar | None): + # error: [overlapping-condition] "This condition does not distinguish between `Foo & ~AlwaysTruthy`, `Bar & ~AlwaysTruthy` and `None`" + if not value: + return + + # error: [redundant-boolean-comparison] "Comparison of a `bool` with `True` is redundant" + if value.tag is True: + reveal_type(value) # revealed: Bar & ~AlwaysFalsy + else: + reveal_type(value) # revealed: Foo & ~AlwaysFalsy +``` + ## `is` in chained comparisons ```py @@ -190,6 +452,318 @@ def narrow_generic_alias[T: (Generic[int], Specialized)](klass: type[T]) -> None reveal_type(Generic[int]) # revealed: ``` +## Narrowing with a constrained `TypeVar` + +The `is` check below can discard `int` because it cannot be `None` or `...`. The `is not` check +cannot discard either remaining type: depending on the current constraint, either value could differ +from `other`. + +```py +from types import EllipsisType +from typing import TypeVar + +T = TypeVar("T", None, EllipsisType) + +def takes_singleton(value: None | EllipsisType) -> None: ... +def f(value: int | None | EllipsisType, other: T) -> None: + if value is other: + takes_singleton(value) + if value is not other: + reveal_type(value) # revealed: int | (None & ~T@f) | (EllipsisType & ~T@f) +``` + +## `is` with a negated `NewType` + +Excluding a `NewType` removes its invisible tag, not the runtime objects accepted by its +constructor. An identity comparison preserves that negation without making a reachable branch +disappear. + +```py +from typing import Literal, NewType, TypeVar +from ty_extensions import Intersection, Not + +UserId = NewType("UserId", int) + +def excluded_newtype(value: Not[UserId], other: UserId) -> None: + if value is other: + reveal_type(value) # revealed: int & ~UserId + reveal_type(other) # revealed: UserId + + if other is value: + reveal_type(value) # revealed: int & ~UserId +``` + +A type variable can hide the same static negation in its upper bound. The reachable branch must +preserve that type variable. + +```py +ExcludedBound = TypeVar("ExcludedBound", bound=Intersection[int, Not[UserId]]) + +def excluded_newtype_in_bound( + value: ExcludedBound, + other: UserId, + without_one: Intersection[ExcludedBound, Not[Literal[1]]], +) -> None: + if value is other: + reveal_type(value) # revealed: ExcludedBound@excluded_newtype_in_bound + + if without_one is other: + reveal_type(without_one) # revealed: ExcludedBound@excluded_newtype_in_bound & ~Literal[1] +``` + +The same runtime overlap remains reachable when both operands are unions, while genuinely +incompatible alternatives are removed. + +```py +def excluded_newtype_in_unions( + value: Intersection[int, Not[UserId]] | None, + other: UserId | bytes, +) -> None: + if value is other: + reveal_type(value) # revealed: int & ~UserId + reveal_type(other) # revealed: UserId +``` + +Unlike a negated `NewType`, a negated runtime class genuinely rules out identity with its instances. + +```py +def excluded_runtime_class(not_int: Not[int], other: UserId) -> None: + if not_int is other: + reveal_type(not_int) # revealed: Never + reveal_type(other) # revealed: Never +``` + +## `is` with string types + +Identity transfers known literal-string origin when the other operand already proves it. + +```py +from typing import Literal +from typing_extensions import LiteralString +from ty_extensions import Intersection, Not + +def literal_string(value: object, text: LiteralString) -> None: + if value is text: + reveal_type(value) # revealed: LiteralString +``` + +The same string object (same memory address) can be referenced by multiple different expressions +(due to aliasing or interning). Some of those expressions may be validly typed as having literal +origin and others may not. Checking string identity does not assume this is impossible: + +```py +def negated_string_literal(value: Not[Literal["hello"]]) -> None: + if value is "hello": + reveal_type(value) # revealed: ~Literal["hello"] + +def negated_literal_string(value: Intersection[str, Not[LiteralString]]) -> None: + reveal_type(value is "hello") # revealed: bool + + if value is "hello": + reveal_type(value) # revealed: str & ~LiteralString + + if "hello" is value: + reveal_type(value) # revealed: str & ~LiteralString +``` + +When literal origin is already known, excluding a literal string also excludes that runtime value. + +```py +def trusted_value_is_excluded(value: Intersection[LiteralString, Not[Literal["hello"]]]) -> None: + reveal_type(value is "hello") # revealed: Literal[False] + reveal_type("hello" is value) # revealed: Literal[False] + + if value is "hello": + reveal_type(value) # revealed: Never +``` + +## `is` with `NewType`s + +### Distinct `NewType`s with the same base + +Distinct `NewType` tags are mutually exclusive, so their types are disjoint even when they have the +same concrete base. Their constructors still return their arguments unchanged: an identity +comparison can succeed, but each operand retains only its own tag. + +```py +from typing import NewType +from ty_extensions import Intersection + +class Foo: ... +class FooSub(Foo): ... + +FooNewType1 = NewType("FooNewType1", Foo) +FooNewType2 = NewType("FooNewType2", Foo) + +def same_base(foo1: FooNewType1, foo2: FooNewType2) -> None: + reveal_type(foo1 is foo2) # revealed: bool + if foo1 is foo2: + reveal_type(foo1) # revealed: FooNewType1 + reveal_type(foo2) # revealed: FooNewType2 + +def union(value: FooNewType1 | None, other: FooNewType2) -> None: + if value is other: + reveal_type(value) # revealed: FooNewType1 + +def intersection(left: Intersection[FooNewType1, FooSub], right: FooNewType2) -> None: + if left is right: + reveal_type(right) # revealed: FooNewType2 & FooSub +``` + +### `NewType`s in `TypeVar` bounds and constraints + +`NewType`s inside `TypeVar` bounds and constraints can likewise refer to the same runtime object. +Comparing distinct type variables is not always false, but a successful comparison preserves each +operand's own type variable and tag. + +```py +from typing import NewType, TypeVar + +class Foo: ... + +FooNewType1 = NewType("FooNewType1", Foo) +FooNewType2 = NewType("FooNewType2", Foo) +FooNewType3 = NewType("FooNewType3", Foo) +FooNewType4 = NewType("FooNewType4", Foo) + +BoundedT = TypeVar("BoundedT", bound=FooNewType1) +BoundedU = TypeVar("BoundedU", bound=FooNewType2) + +def bounded_typevars(left: BoundedT, right: BoundedU) -> None: + reveal_type(left is right) # revealed: bool + if left is right: + # These are the same object, so substituting `left` for `right` in a return would be + # sound. But `BoundedT & BoundedU` is still empty because their `NewType` tags differ; + # inferring that intersection could incorrectly make reachable code disappear. + reveal_type(left) # revealed: BoundedT@bounded_typevars + reveal_type(right) # revealed: BoundedU@bounded_typevars + +ConstrainedT = TypeVar("ConstrainedT", FooNewType1, FooNewType2) +ConstrainedU = TypeVar("ConstrainedU", FooNewType3, FooNewType4) + +def constrained_typevars(left: ConstrainedT, right: ConstrainedU) -> None: + reveal_type(left is right) # revealed: bool + if left is right: + reveal_type(left) # revealed: ConstrainedT@constrained_typevars + reveal_type(right) # revealed: ConstrainedU@constrained_typevars +``` + +A type variable bounded by a `NewType` also carries that `NewType` tag. Identity cannot transfer the +type variable to an untagged value, but can establish the underlying runtime class. + +```py +def object_with_bounded_newtype(value: object, tagged: BoundedT) -> None: + if value is tagged: + reveal_type(value) # revealed: Foo + reveal_type(tagged) # revealed: BoundedT@object_with_bounded_newtype +``` + +Every constraint below is a `NewType` based on `EllipsisType`. Although their tags are mutually +exclusive, all of these values refer to the same `...` object. An `is not` check therefore removes +the singleton alternative, making a subsequent `is` check unreachable. + +```py +from types import EllipsisType +from typing import NewType, TypeVar +from typing_extensions import assert_never + +SingletonA = NewType("SingletonA", EllipsisType) +SingletonB = NewType("SingletonB", EllipsisType) +SingletonC = NewType("SingletonC", EllipsisType) + +SingletonT = TypeVar("SingletonT", SingletonA, SingletonB) + +def same_singleton(first: SingletonA, second: SingletonB) -> None: + reveal_type(first is second) # revealed: Literal[True] + if first is second: + reveal_type(first) # revealed: SingletonA + reveal_type(second) # revealed: SingletonB + +def contradictory_singleton_comparisons(value: SingletonC | int, other: SingletonT) -> None: + if value is not other: + reveal_type(value) # revealed: int + if value is other: + assert_never(value) +``` + +### Narrowing an object to the generic base of a `NewType` + +Identity does not transfer a `NewType` tag, but it preserves the invariant type arguments of the +underlying generic type. + +```py +from typing import NewType + +UserIds = NewType("UserIds", list[int]) + +def preserve_generic_base(value: object, user_ids: UserIds) -> None: + if value is user_ids: + reveal_type(value) # revealed: list[int] + reveal_type(user_ids) # revealed: UserIds +``` + +### Comparing `NewType`s with literals + +Calls to `NewType` return their arguments unchanged. Comparisons with `bool` and `int` literals can +therefore succeed. Identity transfers the literal value to the tagged operand, but does not transfer +its `NewType` tag back to the literal. + +```py +from typing import Literal, NewType + +BoolNewType = NewType("BoolNewType", bool) +IntNewType = NewType("IntNewType", int) + +def literals(true: Literal[True], b: BoolNewType, forty_two: Literal[42], i: IntNewType) -> None: + if b is true: + reveal_type(true) # revealed: Literal[True] + reveal_type(b) # revealed: BoolNewType & Literal[True] + if i is forty_two: + reveal_type(forty_two) # revealed: Literal[42] + reveal_type(i) # revealed: IntNewType & Literal[42] +``` + +### `is not` with singleton `NewType`s + +Both `NewType`s below are based on `EllipsisType`, which contains only the `...` object. The +`is not` branch therefore removes the `NewType` alternative. + +```py +from types import EllipsisType +from typing import NewType + +SingletonA = NewType("SingletonA", EllipsisType) +SingletonB = NewType("SingletonB", EllipsisType) + +def singleton_is_not(value: SingletonA | int, other: SingletonB) -> None: + if value is not other: + reveal_type(value) # revealed: int + + if value is other: + reveal_type(value) # revealed: SingletonA + reveal_type(other) # revealed: SingletonB +``` + +### Comparisons that are always false + +An identity comparison is still always false when the two runtime types are distinct final classes. + +```py +from typing import NewType, final + +@final +class A: ... + +@final +class B: ... + +ANewType = NewType("ANewType", A) +BNewType = NewType("BNewType", B) + +def disjoint_bases(a: ANewType, b: BNewType) -> None: + reveal_type(a is b) # revealed: Literal[False] +``` + ## `is` where the other operand is a call expression ```py diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md index cdbb225810..5946f625d3 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md @@ -125,7 +125,6 @@ error[invalid-argument-type]: Invalid second argument to `isinstance` | ^^^^^^^^^^^^^^---------------^ | | | This `UnionType` instance contains non-class elements - | info: A `UnionType` instance can only be used as the second argument to `isinstance` if all elements are class objects info: Element `` in the union is not a class object ``` @@ -144,7 +143,6 @@ error[invalid-argument-type]: Invalid second argument to `isinstance` | ^^^^^^^^^^^^^^-------------------------------^ | | | This `UnionType` instance contains non-class elements - | info: A `UnionType` instance can only be used as the second argument to `isinstance` if all elements are class objects info: Elements `` and `` in the union are not class objects ``` @@ -163,7 +161,6 @@ error[invalid-argument-type]: Invalid second argument to `isinstance` | ^^^^^^^^^^^^^^----------------------------^ | | | This `UnionType` instance contains non-class elements - | info: A `UnionType` instance can only be used as the second argument to `isinstance` if all elements are class objects info: Element `` in the union, and 2 more elements, are not class objects ``` @@ -192,7 +189,6 @@ error[invalid-argument-type]: Invalid second argument to `isinstance` | ^^^^^^^^^^^^^^^^^^^^-----------------^^ | | | This `UnionType` instance contains non-class elements - | info: A `UnionType` instance can only be used as the second argument to `isinstance` if all elements are class objects info: Element `` in the union is not a class object ``` @@ -216,7 +212,6 @@ error[invalid-argument-type]: Invalid second argument to `isinstance` | ^^^^^^^^^^^^^^^^^^^^^^^^^^-----------------^^^ | | | This `UnionType` instance contains non-class elements - | info: A `UnionType` instance can only be used as the second argument to `isinstance` if all elements are class objects info: Element `` in the union is not a class object ``` @@ -240,7 +235,6 @@ error[invalid-argument-type]: Invalid second argument to `isinstance` | 31 | if isinstance(x, classes): | ^^^^^^^^^^^^^^^^^^^^^^ - | info: A `UnionType` instance can only be used as the second argument to `isinstance` if all elements are class objects info: Element `` in the union `list[int] | bytes` is not a class object ``` @@ -311,7 +305,7 @@ def f(x: dict[str, int] | list[str], y: object): reveal_type(x) # revealed: list[str] if isinstance(y, t.Callable): - reveal_type(y) # revealed: Top[(...) -> object] + reveal_type(y) # revealed: (...) -> Unknown ``` ## Class types @@ -338,6 +332,37 @@ else: reveal_type(x) # revealed: ~A & ~B & ~C ``` +## `NewType` instances and concrete-base subclasses + +A `NewType` constructor returns its argument unchanged at runtime, and runtime class checks ignore +its static tag. The resulting value can therefore still be an instance of a subclass of its concrete +base. For example, `UserId(True)` is valid because `bool` is a subtype of `int`, and the returned +value remains a `bool`. + +```py +from typing import NewType + +class Base: ... +class Child(Base): ... + +BrandedBase = NewType("BrandedBase", Base) +UserId = NewType("UserId", int) + +UserId(True) # error: [bool-as-int] + +def narrow_branded_subclass(value: BrandedBase) -> None: + if isinstance(value, Child): + reveal_type(value) # revealed: BrandedBase & Child + else: + reveal_type(value) # revealed: BrandedBase & ~Child + +def narrow_branded_boolean(value: UserId) -> None: + if isinstance(value, bool): + reveal_type(value) # revealed: UserId & bool + else: + reveal_type(value) # revealed: UserId & ~bool +``` + ## No narrowing for instances of `builtins.type` ```py @@ -609,14 +634,19 @@ def f(x: Foo, y: Intersection[type[Bar], type[list[int]]]): ## Narrowing with generics +### Strict mode + ```toml [environment] python-version = "3.12" + +[analysis] +strict-generic-narrowing = true ``` -Narrowing to a generic class using `isinstance()` uses the top materialization of the generic. With -a covariant generic, this is equivalent to using the upper bound of the type parameter (by default, -`object`): +In strict mode, narrowing to a generic class using `isinstance()` uses the top materialization of +the generic. With a covariant generic, this is equivalent to using the upper bound of the type +parameter (by default, `object`): ```py from typing import Self @@ -631,6 +661,73 @@ def _(x: object): reveal_type(x.get()) # revealed: object ``` +A bounded covariant generic uses its declared upper bound rather than `object`: + +```py +class BoundedCovariant[T: int]: + def get(self) -> T: + raise NotImplementedError + +def _(x: object): + if isinstance(x, BoundedCovariant): + reveal_type(x) # revealed: BoundedCovariant[int] + reveal_type(x.get()) # revealed: int +``` + +Negative narrowing must exclude every specialization of a bounded generic, including a gradual one. + +```py +from typing import Any + +def excludes_bounded_generic(value: BoundedCovariant[Any] | bool) -> bool: + if isinstance(value, BoundedCovariant): + reveal_type(value) # revealed: BoundedCovariant[Any] + return False + + reveal_type(value) # revealed: bool + return value +``` + +The same exclusion applies when the generic appears in a tuple of runtime classes. + +```py +def excludes_bounded_generic_tuple( + value: BoundedCovariant[Any] | bool | bytes, +) -> bool: + if isinstance(value, (BoundedCovariant, bytes)): + reveal_type(value) # revealed: BoundedCovariant[Any] | bytes + return False + + reveal_type(value) # revealed: bool + return value +``` + +Constrained type parameters preserve the materialization of the generic class while making the union +of valid constraints available when reading a covariant attribute: + +```py +class ConstrainedCovariant[T: (int, str)]: + def get(self) -> T: + raise NotImplementedError + +def _(x: object): + if isinstance(x, ConstrainedCovariant): + reveal_type(x) # revealed: Top[ConstrainedCovariant[Unknown]] + reveal_type(x.get()) # revealed: int | str +``` + +Constrained generics must also be excluded by negative narrowing. + +```py +def excludes_constrained_generic(value: ConstrainedCovariant[Any] | bool) -> bool: + if isinstance(value, ConstrainedCovariant): + reveal_type(value) # revealed: ConstrainedCovariant[Any] + return False + + reveal_type(value) # revealed: bool + return value +``` + Similarly, contravariant type parameters use their lower bound of `Never`: ```py @@ -692,7 +789,7 @@ class InvariantWithAny[T: int]: def _(x: object): if isinstance(x, InvariantWithAny): reveal_type(x) # revealed: Top[InvariantWithAny[Unknown]] - reveal_type(x.a) # revealed: object + reveal_type(x.a) # revealed: int reveal_type(x.b) # revealed: Any ``` @@ -732,6 +829,28 @@ def _(x: Invariant[int] | Covariant[str]): reveal_type(x) # revealed: Covariant[str] & ~Top[Invariant[Unknown]] ``` +The built-in `tuple` stores its variable-length shape separately from its generic type argument. +Narrowing must preserve and materialize that shape. + +```py +def narrow_tuple(value: object) -> None: + if isinstance(value, tuple): + reveal_type(value) # revealed: tuple[object, ...] +``` + +A tuple subclass retains its nominal type and inherits its tuple shape from its specialized base. +The subclass's own type parameter is still materialized using its declared bound. + +```py +class BoundedTuple[T: int](tuple[T, str]): ... + +def narrow_tuple_subclass(value: object) -> None: + if isinstance(value, BoundedTuple): + reveal_type(value) # revealed: BoundedTuple[int] + reveal_type(value[0]) # revealed: int + reveal_type(value[1]) # revealed: str +``` + The behavior of `issubclass()` is similar. ```py @@ -744,6 +863,551 @@ def _(x: type[object], y: type[object], z: type[object]): reveal_type(z) # revealed: type[Top[Invariant[Unknown]]] ``` +Negative `issubclass()` narrowing also excludes every specialization of a bounded generic. + +```py +def excludes_bounded_generic_subclass( + cls: type[BoundedCovariant[Any]] | type[bool], +) -> type[bool]: + if issubclass(cls, BoundedCovariant): + reveal_type(cls) # revealed: type[BoundedCovariant[Any]] + return bool + + reveal_type(cls) # revealed: + return cls +``` + +### Gradual mode + +```toml +[environment] +python-version = "3.12" + +[analysis] +strict-generic-narrowing = false +``` + +In gradual mode, narrowing to a generic class using `isinstance()` preserves any compatible +specialization from the original type. If the original type does not provide a specialization, we +intersect with the `Unknown` specialization. The negative branch still excludes the top +materialization because a failed `isinstance()` check rules out every specialization of the class. + +```py +class Covariant[T]: + def get(self) -> T: + raise NotImplementedError + +def _(x: object): + if isinstance(x, Covariant): + # `object & Covariant[Unknown]` simplifies to `Covariant[Unknown]`. + reveal_type(x) # revealed: Covariant[Unknown] + reveal_type(x.get()) # revealed: Unknown + else: + reveal_type(x) # revealed: ~Covariant[object] +``` + +For contravariant generics, we similarly intersect with the `Unknown` specialization: + +```py +class Contravariant[T]: + def push(self, x: T) -> None: ... + +def _(x: object): + if isinstance(x, Contravariant): + reveal_type(x) # revealed: Contravariant[Unknown] + x.push(42) + x.push("foo") + else: + reveal_type(x) # revealed: ~Contravariant[Never] +``` + +Similarly, for invariant generics we intersect with the `Unknown` specialization. Reading produces +`Unknown`, while writing accepts arguments of any type: + +```py +class Invariant[T]: + def push(self, x: T) -> None: ... + def get(self) -> T: + raise NotImplementedError + +def _(x: object): + if isinstance(x, Invariant): + reveal_type(x) # revealed: Invariant[Unknown] + reveal_type(x.get) # revealed: bound method Invariant[Unknown].get() -> Unknown + reveal_type(x.get()) # revealed: Unknown + reveal_type(x.push) # revealed: bound method Invariant[Unknown].push(x: Unknown) + x.push(42) + x.push("foo") + else: + reveal_type(x) # revealed: ~Top[Invariant[Unknown]] +``` + +Narrowing already specialized generics preserves their concrete type arguments: + +```py +class P: ... + +def _(x: Covariant[P], y: Contravariant[P], z: Invariant[P]): + if isinstance(x, Covariant): + reveal_type(x) # revealed: Covariant[P] + if isinstance(y, Contravariant): + reveal_type(y) # revealed: Contravariant[P] + if isinstance(z, Invariant): + reveal_type(z) # revealed: Invariant[P] +``` + +Specialized base classes also determine the type arguments of matching subclasses, including +subclasses with a stricter variance: + +```py +class SubOfCovariant[T](Covariant[T]): ... +class SubOfContravariant[T](Contravariant[T]): ... +class SubOfInvariant[T](Invariant[T]): ... + +class InvariantSubOfCovariant[T](Covariant[T]): + def push(self, value: T) -> None: ... + +class InvariantSubOfContravariant[T](Contravariant[T]): + def get(self) -> T: + raise NotImplementedError + +def narrow_generic_subclasses(covariant: Covariant[P], contravariant: Contravariant[P], invariant: Invariant[P]) -> None: + if isinstance(covariant, SubOfCovariant): + reveal_type(covariant) # revealed: SubOfCovariant[P] + + if isinstance(contravariant, SubOfContravariant): + reveal_type(contravariant) # revealed: SubOfContravariant[P] + + if isinstance(invariant, SubOfInvariant): + reveal_type(invariant) # revealed: SubOfInvariant[P] + + if isinstance(covariant, InvariantSubOfCovariant): + reveal_type(covariant) # revealed: InvariantSubOfCovariant[P] + + if isinstance(contravariant, InvariantSubOfContravariant): + reveal_type(contravariant) # revealed: InvariantSubOfContravariant[P] +``` + +Narrowing unions and intersections preserves unrelated types when they can overlap with the checked +class, while excluding unrelated final classes: + +```py +from typing import Sequence, final +from ty_extensions import Intersection + +@final +class Item: ... + +class OpenItem: ... + +def _(value: Item | OpenItem | Sequence[int]) -> None: + if isinstance(value, list): + reveal_type(value) # revealed: (OpenItem & list[Unknown]) | list[int] + +def _( + value: Intersection[OpenItem, Sequence[int]], +) -> None: + if isinstance(value, list): + reveal_type(value) # revealed: OpenItem & list[int] +``` + +When an intersection contains multiple specialized bases, each base contributes its known type +arguments to a matching subclass: + +```py +class Left[L]: ... +class Right[R]: ... + +class Both[L, R](Left[L], Right[R]): + left: L + right: R + +def _(value: Intersection[Left[int], Right[str]]) -> None: + if isinstance(value, Both): + reveal_type(value) # revealed: Both[int, str] + reveal_type(value.left) # revealed: int + reveal_type(value.right) # revealed: str +``` + +Subclass type arguments are inferred through their actual inheritance relationship, so this also +works correctly if type parameters change position: + +```py +class Base[A, B]: ... +class Child[X, Y](Base[Y, X]): ... + +def _(value: Base[int, str]) -> None: + if isinstance(value, Child): + reveal_type(value) # revealed: Child[str, int] +``` + +A subclass type parameter that cannot be inferred from its base remains `Unknown`: + +```py +class PartiallyInferredChild[Extra1, T, Extra2](Sequence[T]): ... + +def _(value: Sequence[int]) -> None: + if isinstance(value, PartiallyInferredChild): + reveal_type(value) # revealed: PartiallyInferredChild[Unknown, int, Unknown] +``` + +If we're "narrowing" in the opposite direction, we retain the existing subclass specialization: + +```py +def _(covariant: SubOfCovariant[P], contravariant: SubOfContravariant[P], invariant: SubOfInvariant[P]) -> None: + if isinstance(covariant, Covariant): + reveal_type(covariant) # revealed: SubOfCovariant[P] + + if isinstance(contravariant, Contravariant): + reveal_type(contravariant) # revealed: SubOfContravariant[P] + + if isinstance(invariant, Invariant): + reveal_type(invariant) # revealed: SubOfInvariant[P] +``` + +This also works for runtime-checkable protocols: + +```py +from typing import Protocol, runtime_checkable + +@runtime_checkable +class Reader[T](Protocol): + def read(self) -> T: ... + +class Concrete[T]: + def read(self) -> T: + raise NotImplementedError + +def _(value: Concrete[int]) -> None: + if isinstance(value, Reader): + reveal_type(value) # revealed: Concrete[int] + reveal_type(value.read()) # revealed: int +``` + +## Use cases: `isinstance` narrowing and generics + +### Strict mode + +```toml +[analysis] +strict-generic-narrowing = true +``` + +#### Covariance + +Narrowing from `object` via `isinstance(.., Sequence)`: + +```py +from typing import Sequence, final + +def _(xs: object): + if isinstance(xs, Sequence): + reveal_type(xs) # revealed: Sequence[object] + for x in xs: + reveal_type(x) # revealed: object + else: + reveal_type(xs) # revealed: ~Sequence[object] +``` + +Narrowing from `Item | Sequence[Item]` via `isinstance(.., Sequence)`: + +```py +@final +class Item: ... + +def _(xs: Item | Sequence[Item]): + if isinstance(xs, Sequence): + reveal_type(xs) # revealed: Sequence[Item] + for x in xs: + reveal_type(x) # revealed: Item + else: + reveal_type(xs) # revealed: Item +``` + +Narrowing from (non-final) `OpenItem | Sequence[OpenItem]` via `isinstance(.., Sequence)`: + +```py +class OpenItem: ... + +def _(xs: OpenItem | Sequence[OpenItem]): + if isinstance(xs, Sequence): + reveal_type(xs) # revealed: (OpenItem & Sequence[object]) | Sequence[OpenItem] + for x in xs: + reveal_type(x) # revealed: object + else: + reveal_type(xs) # revealed: OpenItem & ~Sequence[object] +``` + +#### Invariance + +Narrowing from `object` via `isinstance(.., list)`: + +```py +def _(xs: object): + if isinstance(xs, list): + reveal_type(xs) # revealed: Top[list[Unknown]] + for x in xs: + reveal_type(x) # revealed: object + + # This is an error in strict mode: + # error: [invalid-argument-type] "Expected `Never`, found `Literal[1]`" + xs.append(1) + + else: + reveal_type(xs) # revealed: ~Top[list[Unknown]] +``` + +Narrowing from `Item | list[Item]` via `isinstance(.., list)`: + +```py +from typing import final + +@final +class Item: ... + +def _(xs: Item | list[Item]): + if isinstance(xs, list): + reveal_type(xs) # revealed: list[Item] + for x in xs: + reveal_type(x) # revealed: Item + else: + reveal_type(xs) # revealed: Item +``` + +Narrowing from (non-final) `OpenItem | list[OpenItem]` via `isinstance(.., list)`: + +```py +class OpenItem: ... + +def _(xs: OpenItem | list[OpenItem]): + if isinstance(xs, list): + reveal_type(xs) # revealed: (OpenItem & Top[list[Unknown]]) | list[OpenItem] + for x in xs: + reveal_type(x) # revealed: object + else: + reveal_type(xs) # revealed: OpenItem & ~Top[list[Unknown]] +``` + +#### Exhaustiveness checking + +```py +def _(xs: list[str] | set[str]) -> str: + if isinstance(xs, list): + return "it's a list!" + elif isinstance(xs, set): + return "it's a set!" +``` + +### Gradual mode + +```toml +[environment] +python-version = "3.12" + +[analysis] +strict-generic-narrowing = false +``` + +#### Covariance + +Narrowing from `object` via `isinstance(.., Sequence)`: + +```py +from typing import Sequence, final + +def _(xs: object): + if isinstance(xs, Sequence): + reveal_type(xs) # revealed: Sequence[Unknown] + for x in xs: + reveal_type(x) # revealed: Unknown + else: + reveal_type(xs) # revealed: ~Sequence[object] +``` + +Narrowing from `Item | Sequence[Item]` via `isinstance(.., Sequence)`: + +```py +@final +class Item: ... + +def _(xs: Item | Sequence[Item]): + if isinstance(xs, Sequence): + reveal_type(xs) # revealed: Sequence[Item] + for x in xs: + reveal_type(x) # revealed: Item + else: + reveal_type(xs) # revealed: Item +``` + +Narrowing from (non-final) `OpenItem | Sequence[OpenItem]` via `isinstance(.., Sequence)`: + +```py +class OpenItem: ... + +def _(xs: OpenItem | Sequence[OpenItem]): + if isinstance(xs, Sequence): + reveal_type(xs) # revealed: (OpenItem & Sequence[Unknown]) | Sequence[OpenItem] + for x in xs: + reveal_type(x) # revealed: Unknown | OpenItem + else: + reveal_type(xs) # revealed: OpenItem & ~Sequence[object] +``` + +#### Invariance + +Narrowing from `object` via `isinstance(.., list)`: + +```py +def _(xs: object): + if isinstance(xs, list): + reveal_type(xs) # revealed: list[Unknown] + for x in xs: + reveal_type(x) # revealed: Unknown + + xs.append(1) + xs.append("foo") + + else: + reveal_type(xs) # revealed: ~Top[list[Unknown]] +``` + +Narrowing from `Item | list[Item]` via `isinstance(.., list)`: + +```py +from typing import final + +@final +class Item: ... + +def _(xs: Item | list[Item]): + if isinstance(xs, list): + reveal_type(xs) # revealed: list[Item] + for x in xs: + reveal_type(x) # revealed: Item + else: + reveal_type(xs) # revealed: Item +``` + +Narrowing from (non-final) `OpenItem | list[OpenItem]` via `isinstance(.., list)`: + +```py +class OpenItem: ... + +def _(xs: OpenItem | list[OpenItem]): + if isinstance(xs, list): + reveal_type(xs) # revealed: (OpenItem & list[Unknown]) | list[OpenItem] + for x in xs: + reveal_type(x) # revealed: Unknown | OpenItem + else: + reveal_type(xs) # revealed: OpenItem & ~Top[list[Unknown]] +``` + +#### Exhaustiveness checking + +```py +def _(xs: list[str] | set[str]) -> str: + if isinstance(xs, list): + return "it's a list!" + elif isinstance(xs, set): + return "it's a set!" +``` + +## Narrowing recursively bounded generics (strict mode) + +An `isinstance()` check must not recurse indefinitely when a generic bound refers to its own class. + +```toml +[environment] +python-version = "3.12" + +[analysis] +strict-generic-narrowing = true +``` + +```py +from typing import Any + +class Recursive[T: "Recursive[Any]"]: ... + +def narrow(value: object) -> None: + if isinstance(value, Recursive): + reveal_type(value) # revealed: Recursive[object] +``` + +A self-referential bound must also be safe when its recursion is hidden behind a type alias. + +```py +class AliasedRecursive[T: "RecursiveAlias"]: ... + +type RecursiveAlias = AliasedRecursive[Any] + +def narrow_alias(value: object) -> None: + if isinstance(value, AliasedRecursive): + reveal_type(value) # revealed: AliasedRecursive[object] +``` + +The same cycle recovery must handle bounds shared by mutually recursive generic classes. + +```py +class Left[T: "Right[Any]"]: ... +class Right[U: Left[Any]]: ... + +def narrow_mutual(value: object) -> None: + if isinstance(value, Left): + reveal_type(value) # revealed: Left[object] + + if isinstance(value, Right): + reveal_type(value) # revealed: Right[object] +``` + +## Narrowing recursively bounded generics (gradual mode) + +An `isinstance()` check must not recurse indefinitely when a generic bound refers to its own class. + +```toml +[environment] +python-version = "3.12" + +[analysis] +strict-generic-narrowing = false +``` + +```py +from typing import Any + +class Recursive[T: "Recursive[Any]"]: ... + +def narrow(value: object) -> None: + if isinstance(value, Recursive): + reveal_type(value) # revealed: Recursive[Unknown] +``` + +A self-referential bound must also be safe when its recursion is hidden behind a type alias. + +```py +class AliasedRecursive[T: "RecursiveAlias"]: ... + +type RecursiveAlias = AliasedRecursive[Any] + +def narrow_alias(value: object) -> None: + if isinstance(value, AliasedRecursive): + reveal_type(value) # revealed: AliasedRecursive[Unknown] +``` + +The same cycle recovery must handle bounds shared by mutually recursive generic classes. + +```py +class Left[T: "Right[Any]"]: ... +class Right[U: Left[Any]]: ... + +def narrow_mutual(value: object) -> None: + if isinstance(value, Left): + reveal_type(value) # revealed: Left[Unknown] + + if isinstance(value, Right): + reveal_type(value) # revealed: Right[Unknown] +``` + ## Narrowing generic defaults in Python 3.13 When a type parameter has a bare `Any` default, narrowing still materializes the substituted @@ -753,6 +1417,9 @@ instead), so the default value is irrelevant here: ```toml [environment] python-version = "3.13" + +[analysis] +strict-generic-narrowing = true ``` ```py @@ -781,6 +1448,81 @@ def _(x: object): reveal_type(x.y) # revealed: tuple[A, object] ``` +`isinstance(value, Box)` checks the runtime class, not the type argument used to specialize it. +Narrowing must therefore preserve the original type argument instead of substituting `Box`'s +default. + +```py +from typing import assert_never + +class Box[T: str = str]: + value: T + + def __init__(self, value: T) -> None: ... + +def box_with_default[T: str = str](value: Box[T] | T) -> Box[T]: + if isinstance(value, Box): + reveal_type(value) # revealed: Box[T@box_with_default] + return value + + if not isinstance(value, Box): + reveal_type(value) # revealed: T@box_with_default & ~Top[Box[Unknown]] + return Box[T](value) + + assert_never(value) +``` + +When `isinstance()` narrows a value of type `object` to a tuple subclass, its type argument comes +from the declared upper bound, not the default. Its element types are inherited from the specialized +base. + +```py +class DefaultedTuple[T: int = bool](tuple[T, str]): ... + +def narrow_defaulted_tuple(value: object) -> None: + if isinstance(value, DefaultedTuple): + reveal_type(value) # revealed: DefaultedTuple[int] + reveal_type(value[0]) # revealed: int + reveal_type(value[1]) # revealed: str +``` + +Negative narrowing also excludes gradual specializations of the defaulted tuple subclass. + +```py +def excludes_defaulted_tuple(value: DefaultedTuple[Any] | bool) -> bool: + if isinstance(value, DefaultedTuple): + reveal_type(value) # revealed: DefaultedTuple[Any] + reveal_type(value[0]) # revealed: Any + reveal_type(value[1]) # revealed: str + return False + + reveal_type(value) # revealed: bool + return value +``` + +## Narrowing bounded generic defaults in gradual mode + +In gradual mode, narrowing a value of type `object` to a tuple subclass leaves its type argument +`Unknown`. + +```toml +[environment] +python-version = "3.13" + +[analysis] +strict-generic-narrowing = false +``` + +```py +class DefaultedTuple[T: int = bool](tuple[T, str]): ... + +def narrow_defaulted_tuple(value: object) -> None: + if isinstance(value, DefaultedTuple): + reveal_type(value) # revealed: DefaultedTuple[Unknown] + reveal_type(value[0]) # revealed: Unknown + reveal_type(value[1]) # revealed: str +``` + ## Narrowing generic `classmethod` After an `isinstance(..., classmethod)` branch unwraps and replaces a generic `classmethod`, the @@ -858,3 +1600,75 @@ def f(): reveal_type(value) # revealed: str reveal_type(result) # revealed: Literal[False] ``` + +## Preserving TypedDict interfaces when narrowing mappings + +A `TypedDict` is always a dictionary at runtime, but its static interface deliberately disallows +operations that could remove required keys or introduce undeclared ones. Narrowing to `dict`, +`Mapping`, or `MutableMapping` must not discard these restrictions. + +Use a `TypedDict` with one required key and one optional key to distinguish safe operations from +those that could invalidate its declared shape. + +```py +from typing import TypedDict, Mapping, MutableMapping +from typing_extensions import NotRequired + +class Payload(TypedDict): + key: int + optional: NotRequired[str] +``` + +Narrowing directly to `dict` preserves both the required-key restrictions and the optional key's +known type. + +```py +def narrow_typed_dict_to_dict(value: int | Payload) -> None: + if isinstance(value, dict): + reveal_type(value) # revealed: Payload + reveal_type(value["key"]) # revealed: int + value["key"] = 1 + value["optional"] = "present" + reveal_type(value.pop("optional")) # revealed: str + + # error: [unresolved-attribute] + value.clear() + # error: [invalid-argument-type] "Cannot pop required field 'key' from TypedDict `Payload`" + value.pop("key") + # error: [invalid-key] "Unknown key "unexpected" for TypedDict `Payload`" + value["unexpected"] = 1 + # error: [invalid-argument-type] "Cannot delete required key "key" from TypedDict `Payload`" + del value["key"] +``` + +Same for `MutableMapping`: + +```py +def narrow_typed_dict_to_mutable_mapping(value: Payload) -> None: + if isinstance(value, MutableMapping): + reveal_type(value) # revealed: Payload + # error: [unresolved-attribute] + value.clear() +``` + +And for `Mapping`: + +```py +def narrow_typed_dict_to_mapping(value: Payload) -> None: + if isinstance(value, Mapping): + reveal_type(value) # revealed: Payload + # error: [unresolved-attribute] + value.clear() +``` + +A type alias must retain the same `TypedDict` interface. + +```py +PayloadAlias = Payload + +def narrow_aliased_typed_dict_to_dict(value: PayloadAlias) -> None: + if isinstance(value, dict): + reveal_type(value) # revealed: Payload + # error: [unresolved-attribute] + value.clear() +``` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md b/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md index 02818b2cbd..afb3c312c4 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md @@ -187,7 +187,6 @@ error[invalid-argument-type]: Invalid second argument to `issubclass` | ^^^^^^^^^^^^^^---------------^ | | | This `UnionType` instance contains non-class elements - | info: A `UnionType` instance can only be used as the second argument to `issubclass` if all elements are class objects info: Element `` in the union is not a class object ``` @@ -211,7 +210,6 @@ error[invalid-argument-type]: Invalid second argument to `issubclass` | ^^^^^^^^^^^^^^^^^^^^-----------------^^ | | | This `UnionType` instance contains non-class elements - | info: A `UnionType` instance can only be used as the second argument to `issubclass` if all elements are class objects info: Element `` in the union is not a class object ``` @@ -235,7 +233,6 @@ error[invalid-argument-type]: Invalid second argument to `issubclass` | ^^^^^^^^^^^^^^^^^^^^^^^^^^-----------------^^^ | | | This `UnionType` instance contains non-class elements - | info: A `UnionType` instance can only be used as the second argument to `issubclass` if all elements are class objects info: Element `` in the union is not a class object ``` @@ -259,7 +256,6 @@ error[invalid-argument-type]: Invalid second argument to `issubclass` | 23 | if issubclass(x, classes): | ^^^^^^^^^^^^^^^^^^^^^^ - | info: A `UnionType` instance can only be used as the second argument to `issubclass` if all elements are class objects info: Element `` in the union `list[int] | bytes` is not a class object ``` @@ -284,6 +280,63 @@ def f(x: type[int | str | bytes | range]): reveal_type(x) # revealed: ``` +## Narrowing with generic classes + +### Strict mode + +```toml +[analysis] +strict-generic-narrowing = true +``` + +Without a known specialization, narrowing to a generic class uses the top materialization: + +```py +def _(cls: type) -> None: + if issubclass(cls, list): + reveal_type(cls) # revealed: type[Top[list[Unknown]]] + reveal_type(cls()) # revealed: Top[list[Unknown]] +``` + +When narrowing from a generic superclass to a generic subclass, we intersect with the top +materialization of the subclass: + +```py +from typing import Sequence + +def narrow_sequence_to_list(cls: type[Sequence[int]]) -> None: + if issubclass(cls, list): + reveal_type(cls) # revealed: type[Sequence[int]] & type[Top[list[Unknown]]] + reveal_type(cls()) # revealed: Sequence[int] & Top[list[Unknown]] +``` + +### Gradual mode + +```toml +[analysis] +strict-generic-narrowing = false +``` + +Without a known specialization, narrowing to a generic class leaves its type argument unknown. + +```py +def _(cls: type) -> None: + if issubclass(cls, list): + reveal_type(cls) # revealed: type[list[Unknown]] + reveal_type(cls()) # revealed: list[Unknown] +``` + +Narrowing to a generic subclass preserves the specialized base class's type argument. + +```py +from typing import Sequence + +def _(cls: type[Sequence[int]]) -> None: + if issubclass(cls, list): + reveal_type(cls) # revealed: type[list[int]] + reveal_type(cls()) # revealed: list[int] +``` + ## `classinfo` is a generic final class ```toml diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/match.md b/crates/ty_python_semantic/resources/mdtest/narrow/match.md index c5e10820df..b659c1bfa9 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/match.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/match.md @@ -44,6 +44,24 @@ match x: reveal_type(x) # revealed: object ``` +## Class patterns on `NewType` instances + +A `NewType` does not change its argument's runtime class, so an integer-based `NewType` can match a +`bool` class pattern. + +```py +from typing import NewType + +UserId = NewType("UserId", int) + +def match_newtype_boolean(value: UserId) -> None: + match value: + case bool(): + reveal_type(value) # revealed: UserId & bool + case _: + reveal_type(value) # revealed: UserId & ~bool +``` + ## Class pattern with guard ```py @@ -91,9 +109,16 @@ def exhaustive_pattern_with_guard(x: A, flag: bool) -> None: ## Class patterns with generic classes +### Gradual mode + +Generic class patterns follow the same gradual filtering as `isinstance()` checks. + ```toml [environment] python-version = "3.12" + +[analysis] +strict-generic-narrowing = false ``` ```py @@ -112,6 +137,104 @@ def f(x: Covariant[int]): assert_never(x) ``` +A `list()` pattern preserves the type argument inherited from a specialized `Sequence`. + +```py +from typing import Sequence + +def narrow_sequence_to_list(value: Sequence[int]) -> None: + match value: + case list(): + reveal_type(value) # revealed: list[int] + case _: + reveal_type(value) # revealed: Sequence[int] & ~Top[list[Unknown]] +``` + +### Strict mode + +With strict generic narrowing enabled, class patterns retain their top materializations. + +```toml +[environment] +python-version = "3.12" + +[analysis] +strict-generic-narrowing = true +``` + +A `list()` pattern retains the original `Sequence` alongside the top-materialized list. + +```py +from typing import Sequence + +def narrow_sequence_to_list(value: Sequence[int]) -> None: + match value: + case list(): + reveal_type(value) # revealed: Sequence[int] & Top[list[Unknown]] + case _: + reveal_type(value) # revealed: Sequence[int] & ~Top[list[Unknown]] +``` + +## Generic patterns ignore type parameter defaults + +A generic class pattern matches every runtime specialization, not only the specialization described +by its type parameter's default. + +```toml +[environment] +python-version = "3.13" + +[analysis] +strict-generic-narrowing = true +``` + +```py +from typing import Any + +class Box[T: str = str]: + value: T + + def __init__(self, value: T) -> None: ... + +def box_with_default[T: str = str](value: Box[T] | T) -> Box[T]: + match value: + case Box(): + reveal_type(value) # revealed: Box[T@box_with_default] + return value + case remaining: + reveal_type(remaining) # revealed: T@box_with_default & ~Top[Box[Unknown]] + return Box[T](remaining) +``` + +When a class pattern matches a tuple subclass, its type argument comes from the declared upper +bound, not the default. Its element types are inherited from the specialized base. + +```py +class DefaultedTuple[T: int = bool](tuple[T, str]): ... + +def match_defaulted_tuple(value: object) -> None: + match value: + case DefaultedTuple(): + reveal_type(value) # revealed: DefaultedTuple[int] + reveal_type(value[0]) # revealed: int + reveal_type(value[1]) # revealed: str +``` + +The same pattern excludes gradual specializations from the remaining match arms. + +```py +def excludes_defaulted_tuple(value: DefaultedTuple[Any] | bool) -> bool: + match value: + case DefaultedTuple(): + reveal_type(value) # revealed: DefaultedTuple[Any] + reveal_type(value[0]) # revealed: Any + reveal_type(value[1]) # revealed: str + return False + case remaining: + reveal_type(remaining) # revealed: bool + return remaining +``` + ## Class patterns with generic `@final` classes These work the same as non-`@final` classes. @@ -146,7 +269,7 @@ from typing import Any def test_isinstance(x: dict[Any, Any] | int) -> None: if isinstance(x, Mapping): - reveal_type(x) # revealed: dict[Any, Any] | (int & Mapping[object, object]) + reveal_type(x) # revealed: dict[Any, Any] | (int & Mapping[Unknown, Unknown]) else: reveal_type(x) # revealed: int & ~Mapping[object, object] @@ -308,7 +431,7 @@ a fixed-length tuple, we can determine exactly which elements appear in that lis ```py from typing import Any, Literal, TypeVar -from ty_extensions import Unknown +from ty_extensions._internal import Unknown BoundTupleT = TypeVar("BoundTupleT", bound=tuple[int] | tuple[str]) @@ -919,12 +1042,178 @@ def test_incompatible_declared_class_capture(value: PatternBox[int]) -> None: ## Generic subclass captures -When a generic pattern class inherits from the subject's class through an invariant base, the -subject specialization determines the pattern class's type arguments. This applies to annotated -attributes and properties. Every pattern-class type parameter must have an exact solution; variant -bases and unconstrained parameters retain the existing conservative fallback. When the subject does -not provide type arguments, members declared by the pattern class use `Unknown`; a type parameter -default does not restrict which instances match at runtime. +### Gradual mode + +When a generic pattern class inherits from the subject's class, the subject specialization +determines any inferable pattern-class type arguments. This applies to annotated attributes and +properties, including classes with unconstrained type parameters or variant bases. When the subject +does not provide type arguments, members declared by the pattern class use `Unknown`; a type +parameter default does not restrict which instances match at runtime. + +```toml +[analysis] +strict-generic-narrowing = false +``` + +```py +from typing import final, Generic +from typing_extensions import TypeVar + +GenericPatternT = TypeVar("GenericPatternT") +ExtraGenericPatternT = TypeVar("ExtraGenericPatternT") +CovariantGenericPatternT = TypeVar("CovariantGenericPatternT", covariant=True) +DefaultGenericPatternT = TypeVar("DefaultGenericPatternT", default=str) + +class GenericPatternBase(Generic[GenericPatternT]): ... + +OptionalGenericPatternT = TypeVar( + "OptionalGenericPatternT", + bound=GenericPatternBase[int] | None, +) +UnionBoundGenericPatternT = TypeVar( + "UnionBoundGenericPatternT", + bound=GenericPatternBase[int] | GenericPatternBase[str], +) + +class GenericPatternChild(GenericPatternBase[GenericPatternT]): + item: GenericPatternT + items: list[GenericPatternT] + +class PartiallySpecializedGenericPatternChild( + GenericPatternBase[GenericPatternT], + Generic[GenericPatternT, ExtraGenericPatternT], +): + item: GenericPatternT + +class CovariantGenericPatternBase(Generic[CovariantGenericPatternT]): ... + +# `item` is a mutable public attribute, so the class cannot be covariant in its type +# error: [invalid-generic-class] +class CovariantGenericPatternChild(CovariantGenericPatternBase[CovariantGenericPatternT]): + item: CovariantGenericPatternT + +class GenericMemberBase(Generic[GenericPatternT]): + item: GenericPatternT + +class GenericMemberChild(GenericMemberBase[GenericPatternT]): ... +class IntGenericMemberChild(GenericMemberBase[int]): ... + +@final +class FinalGenericPatternBox(Generic[GenericPatternT]): + value: list[GenericPatternT] + +class DefaultGenericPatternBox(Generic[DefaultGenericPatternT]): + value: DefaultGenericPatternT + +ResultValueT = TypeVar("ResultValueT") +ResultErrorT = TypeVar("ResultErrorT") + +class MatchResult(Generic[ResultValueT, ResultErrorT]): ... + +class MatchOk(MatchResult[ResultValueT, ResultErrorT]): + __match_args__ = ("value",) + + @property + def value(self) -> ResultValueT: + raise NotImplementedError + +class MatchErr(MatchResult[ResultValueT, ResultErrorT]): + __match_args__ = ("error",) + + @property + def error(self) -> ResultErrorT: + raise NotImplementedError + +def test_match_generic_subclass_property_capture( + result: MatchResult[int, str], +) -> int: + match result: + case MatchOk(value): + reveal_type(value) # revealed: int + return value + case MatchErr(error): + reveal_type(error) # revealed: str + raise ValueError(error) + raise AssertionError + +def test_match_generic_subclass_capture(value: GenericPatternBase[int]) -> None: + match value: + case GenericPatternChild(item=item): + reveal_type(item) # revealed: int + +def test_match_generic_subclass_capture_from_optional_typevar_bound( + value: OptionalGenericPatternT, +) -> None: + match value: + case GenericPatternChild(item=item): + reveal_type(item) # revealed: int + +def test_match_generic_subclass_capture_from_union_typevar_bound( + value: UnionBoundGenericPatternT, +) -> None: + match value: + case GenericPatternChild(item=item): + reveal_type(item) # revealed: int | str + +def test_match_nested_generic_subclass_capture(value: GenericPatternBase[int]) -> list[int]: + match value: + case GenericPatternChild(items=items): + reveal_type(items) # revealed: list[int] + return items + return [] + +def test_match_partially_specialized_generic_subclass( + value: GenericPatternBase[int], +) -> None: + match value: + case PartiallySpecializedGenericPatternChild(item=item): + reveal_type(item) # revealed: int + +def test_match_covariant_generic_subclass( + value: CovariantGenericPatternBase[int], +) -> None: + match value: + case CovariantGenericPatternChild(item=item): + reveal_type(item) # revealed: int + +def test_match_inherited_generic_subclass_capture( + value: GenericMemberBase[GenericPatternT], +) -> GenericPatternT: + match value: + case GenericMemberChild(item=item): + # revealed: GenericPatternT@test_match_inherited_generic_subclass_capture + reveal_type(item) + return item + case _: + raise ValueError + +def test_match_generic_base_capture_preserves_subject_specialization( + value: IntGenericMemberChild, +) -> None: + match value: + case GenericMemberBase(item=item): + reveal_type(item) # revealed: int + +def test_match_direct_generic_pattern_preserves_declared_member(value: object) -> None: + match value: + case FinalGenericPatternBox(value=int() as item): + reveal_type(item) # revealed: Never + +def test_match_generic_pattern_ignores_typevar_default(value: object) -> None: + match value: + case DefaultGenericPatternBox(value=int() as item): + reveal_type(item) # revealed: Unknown & int +``` + +### Strict mode + +An invariant generic base determines its subclass's type arguments only when every argument has one +exact solution. Unconstrained arguments and variant bases retain conservative member types. + +```toml +[analysis] +strict-generic-narrowing = true +``` ```py from typing import final, Generic @@ -1038,8 +1327,6 @@ def test_match_partially_specialized_generic_subclass( ) -> None: match value: case PartiallySpecializedGenericPatternChild(item=item): - # `ExtraGenericPatternT` is not constrained by the subject, so the pattern class does - # not have one exact specialization. reveal_type(item) # revealed: Unknown def test_match_covariant_generic_subclass( @@ -1047,7 +1334,6 @@ def test_match_covariant_generic_subclass( ) -> None: match value: case CovariantGenericPatternChild(item=item): - # The subject constrains only one end of the possible pattern-class specializations. reveal_type(item) # revealed: Unknown def test_match_inherited_generic_subclass_capture( @@ -1283,7 +1569,8 @@ Two unrelated non-final classes can have a common subclass through multiple inhe successful pattern therefore preserves both class types. Attributes defined on both classes use the intersection of their declared types, consistent with ordinary attribute access on an intersection. For a generic pattern class whose type arguments are not known from the subject, its attributes use -`Unknown`. +`Unknown`. Iterating over a generic attribute likewise produces an unknown element type in gradual +mode. ```py from typing import Generic, TypeVar @@ -1379,7 +1666,7 @@ def test_match_generic_container_member_keeps_loop_reachable( match value: case GenericListOverlapB(values=items): for item in items: - reveal_type(item) # revealed: object + reveal_type(item) # revealed: Unknown ``` ## Class pattern captures from `Any` and `Unknown` @@ -1389,7 +1676,7 @@ declared by the pattern class. ```py from typing import Any -from ty_extensions import Unknown +from ty_extensions._internal import Unknown class GradualPatternBox: value: int @@ -1558,7 +1845,7 @@ keep the same uncertainty as the subject. ```py from typing import Any -from ty_extensions import Unknown +from ty_extensions._internal import Unknown def test_match_gradual_mapping_captures(any_value: Any, unknown_value: Unknown) -> None: match any_value: @@ -1681,7 +1968,7 @@ also keeps the uncertainty of an `Any` or `Unknown` subject. ```py from typing import Any, Generic, Literal, TypeVar, final from typing_extensions import TypedDict -from ty_extensions import Unknown +from ty_extensions._internal import Unknown TagT = TypeVar("TagT") PayloadT = TypeVar("PayloadT") @@ -1805,7 +2092,9 @@ def builtin_positional_patterns_are_exhaustive( bytes, dict[object, object], float, - frozenset[object], + # basedpython's typeshed bounds a `frozenset`'s element by `Hashable`, which `object` is + # not, so the widest `frozenset` here is one over a hashable element + frozenset[str], int, list[object], set[object], @@ -1893,7 +2182,8 @@ exercise three separate checks: an optional field, an unknown key, and a non-str ```py from typing import Any, Literal, Protocol, TypeVar, TypedDict -from ty_extensions import Intersection, Unknown +from ty_extensions import Intersection +from ty_extensions._internal import Unknown class RequiredPayload(TypedDict): tag: Literal["int"] @@ -2934,31 +3224,57 @@ def _(value: FinalPatternInt): reveal_type(value) # revealed: FinalPatternInt ``` -Some precisely modeled objects compare equal to themselves, so an equivalent value pattern is -exhaustive: +We don't attempt to precisely model equality behaviour between special-cased typing-API objects. As +described in `narrow/conditionals/eq.md`, doing so would be possible in some cases, but it would be +error-prone, and there are few known use cases for doing this. ```py +from functools import partial from types import FunctionType -from typing import NewType, TypeVar +from typing import List, Literal, NewType, Optional, TypeVar +from typing_extensions import Literal as ExtensionsLiteral T = TypeVar("T") UserId = NewType("UserId", int) class ReflexivePatternValues: LIST_INT = list[int] + LEGACY_LIST_INT = List[int] + EXTENSIONS_LITERAL = ExtensionsLiteral + OPTIONAL = Optional TYPE_VAR = T NEW_TYPE = UserId +# error: [invalid-return-type] def generic_alias_value_pattern() -> int: match list[int]: case ReflexivePatternValues.LIST_INT: return 1 +# error: [invalid-return-type] +def cross_origin_generic_alias_value_pattern() -> int: + match list[int]: + case ReflexivePatternValues.LEGACY_LIST_INT: + return 1 + +# error: [invalid-return-type] +def cross_origin_special_form_value_pattern() -> int: + match Literal: + case ReflexivePatternValues.EXTENSIONS_LITERAL: + return 1 + +def singleton_special_form_value_pattern() -> int: + match Optional: + case ReflexivePatternValues.OPTIONAL: + return 1 + +# error: [invalid-return-type] def type_var_value_pattern() -> int: match T: case ReflexivePatternValues.TYPE_VAR: return 1 +# error: [invalid-return-type] def new_type_value_pattern() -> int: match UserId: case ReflexivePatternValues.NEW_TYPE: @@ -2974,13 +3290,6 @@ def bound_method_value_pattern() -> int: match helper.__get__: case helper.__get__: return 1 -``` - -Two calls that construct equivalent objects need not produce equal values. For example, separate -`partial` objects do not compare equal, so this match is not exhaustive: - -```py -from functools import partial def target(value: int) -> int: return value @@ -3027,6 +3336,30 @@ def test_match_value_sequence(value: object) -> None: reveal_type(value[0]) # revealed: object ``` +## String-literal origin in value patterns + +A string without literal origin can match a literal value pattern without gaining literal origin. + +```py +from typing import Literal +from typing_extensions import LiteralString +from ty_extensions import Intersection, Not + +def without_literal_origin(value: Intersection[str, Not[LiteralString]]) -> None: + match value: + case "hello": + reveal_type(value) # revealed: str & ~LiteralString +``` + +For a known literal-origin string, excluding the same literal makes the value pattern impossible. + +```py +def trusted_value_is_excluded(value: Intersection[LiteralString, Not[Literal["hello"]]]) -> None: + match value: + case "hello": + reveal_type(value) # revealed: Never +``` + ## Enum equality semantics Enum value patterns use the enum class's actual `__eq__` implementation. Members of an enum whose @@ -3041,8 +3374,9 @@ python-version = "3.11" ```py from enum import Enum, IntEnum, StrEnum, auto -from typing import Literal, assert_never -from ty_extensions import Unknown +from typing import Literal, NewType, assert_never +from ty_extensions import Intersection +from ty_extensions._internal import Unknown class Color(StrEnum): RED = "r" @@ -3096,6 +3430,37 @@ class Second(IntEnum): ONE = 1 TWO = 2 +BrandedFirst = NewType("BrandedFirst", First) +BrandedSecond = NewType("BrandedSecond", Second) + +def branded_int_enum_literal_pattern_is_exhaustive(value: Intersection[BrandedFirst, Literal[First.ONE]]) -> int: + match value: + case 1: + return 1 + +def branded_int_enum_integer_patterns_are_exhaustive(value: BrandedFirst) -> int: + match value: + case 1: + reveal_type(value) # revealed: BrandedFirst & Literal[First.ONE] + return 1 + case 2: + reveal_type(value) # revealed: BrandedFirst & Literal[First.TWO] + return 2 + +def branded_int_enum_member_patterns_are_exhaustive(value: BrandedFirst) -> int: + match value: + case First.ONE: + return 1 + case First.TWO: + return 2 + +def branded_cross_int_enum_member_patterns(value: BrandedFirst | BrandedSecond) -> None: + match value: + case First.ONE: + reveal_type(value) # revealed: (BrandedFirst & Literal[First.ONE]) | (BrandedSecond & Literal[Second.ONE]) + case _: + reveal_type(value) # revealed: (BrandedFirst & Literal[First.TWO]) | (BrandedSecond & Literal[Second.TWO]) + def cross_int_enum_members(value: First | Second) -> None: match value: case First.ONE: @@ -3103,6 +3468,13 @@ def cross_int_enum_members(value: First | Second) -> None: case _: reveal_type(value) # revealed: Literal[First.TWO, Second.TWO] +def optional_cross_int_enum_members(value: First | Second | None) -> None: + match value: + case First.ONE: + reveal_type(value) # revealed: Literal[First.ONE, Second.ONE] + case _: + reveal_type(value) # revealed: Literal[First.TWO, Second.TWO] | None + class Warning(Enum): W1 = auto() @@ -3324,6 +3696,49 @@ def test_match_alias_ignores_custom_ne(flag: bool) -> str: return item ``` +## Recursive enum aliases in value patterns + +An enum value pattern narrows a recursive alias to the matching member while preserving its +`NewType` tag. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from enum import IntEnum +from typing import NewType + +class Number(IntEnum): + ONE = 1 + TWO = 2 + +BrandedNumber = NewType("BrandedNumber", Number) +type RecursiveNumber = BrandedNumber | RecursiveNumber + +def match_recursive_branded_enum(value: RecursiveNumber) -> None: + match value: + case Number.ONE: + reveal_type(value) # revealed: BrandedNumber & Literal[Number.ONE] + case Number.TWO: + reveal_type(value) # revealed: BrandedNumber & Literal[Number.TWO] +``` + +A recursive alias that changes its specialization can also contain values outside the enum. Since +`True` compares equal to `Number.ONE`, both branches preserve the possible boolean values. + +```py +type Changing[T] = T | Changing[bool] + +def match_changing_specialization(value: Changing[BrandedNumber]) -> None: + match value: + case Number.ONE: + reveal_type(value) # revealed: (BrandedNumber & Literal[Number.ONE]) | bool + case _: + reveal_type(value) # revealed: (BrandedNumber & Literal[Number.TWO]) | bool +``` + ## Value patterns with guard ```py diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md b/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md index 1847075f4c..6d7492dd01 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md @@ -122,6 +122,18 @@ def _(response: Success | Failure | TruthyIntTag | FalsyIntTag | AmbiguousTag): reveal_type(response) # revealed: Success | TruthyIntTag | AmbiguousTag else: reveal_type(response) # revealed: Failure | FalsyIntTag | AmbiguousTag + +def truthiness_after_value_guard(response: Success | Failure | None): + # error: [overlapping-condition] "This condition does not distinguish between `Success & ~AlwaysTruthy`, `Failure & ~AlwaysTruthy` and `None`" + if not response: + return + + if response.success: + reveal_type(response) # revealed: Success & ~AlwaysFalsy + reveal_type(response.result) # revealed: int + else: + reveal_type(response) # revealed: Failure & ~AlwaysFalsy + reveal_type(response.errors) # revealed: list[str] ``` ## Function Literals diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/type.md b/crates/ty_python_semantic/resources/mdtest/narrow/type.md index 6bfbaf9359..b222d2d1fd 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/type.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/type.md @@ -180,14 +180,14 @@ def h(x: object): if type(x) is list: reveal_type(x) # revealed: Top[list[Unknown]] elif type(x) is frozenset: - reveal_type(x) # revealed: frozenset[object] + reveal_type(x) # revealed: frozenset[Hashable] else: reveal_type(x) # revealed: object if type(x) is not list and type(x) is not frozenset: reveal_type(x) # revealed: object else: - reveal_type(x) # revealed: Top[list[Unknown]] | frozenset[object] + reveal_type(x) # revealed: Top[list[Unknown]] | frozenset[Hashable] ``` ## No narrowing for `type(x) is C[int]` @@ -212,7 +212,7 @@ def f(x: A[int] | B): reveal_type(x) # revealed: A[int] | B if type(x) is A: - reveal_type(x) # revealed: A[int] + reveal_type(x) # revealed: A[int] | (B & A[object]) else: reveal_type(x) # revealed: A[int] | B @@ -230,7 +230,7 @@ def f(x: A[int] | B): if type(x) is not A: reveal_type(x) # revealed: A[int] | B else: - reveal_type(x) # revealed: A[int] + reveal_type(x) # revealed: A[int] | (B & A[object]) if type(x) is not B: reveal_type(x) # revealed: A[int] | B diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md index f09caedac3..95ec7d9816 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md @@ -259,6 +259,22 @@ def g(a: Literal["foo", "bar"]) -> TypeIs[Literal["foo"]]: return False ``` +A valid boolean return must also be accepted when the predicate's return annotation is an alias of +`TypeIs` or `TypeGuard`, rather than incorrectly producing an `invalid-return-type` diagnostic. + +```py +from typing_extensions import TypeAliasType + +TypeIsAlias = TypeAliasType("TypeIsAlias", TypeIs[int]) +TypeGuardAlias = TypeAliasType("TypeGuardAlias", TypeGuard[int]) + +def aliased_type_is(value: object) -> TypeIsAlias: + return True + +def aliased_type_guard(value: object) -> TypeGuardAlias: + return True +``` + ## Calls ```py @@ -458,17 +474,10 @@ def _(x: Foo | Bar, is_bar: Callable[[object], TypeIs[Bar]]): reveal_type(x) # revealed: Foo & ~Bar ``` -For generics, we transform the argument passed into `TypeIs[]` from `X` to `Top[X]`. This helps -especially when using various functions from typeshed that are annotated as returning -`TypeIs[SomeCovariantGeneric[Any]]` to avoid false positives in other type checkers. For ty's -purposes, it would usually lead to more intuitive results if `object` was used as the specialization -for a covariant generic inside the `TypeIs` special form, but this is mitigated by our implicit -transformation from `TypeIs[SomeCovariantGeneric[Any]]` to `TypeIs[Top[SomeCovariantGeneric[Any]]]` -(which just simplifies to `TypeIs[SomeCovariantGeneric[object]]`). +A `TypeIs` function that returns a gradual specialization of a generic class narrows to that generic +type without replacing its gradual type argument: ```py -class Unrelated: ... - class Covariant[T]: def get(self) -> T: raise NotImplementedError @@ -476,6 +485,20 @@ class Covariant[T]: def is_instance_of_covariant(arg: object) -> TypeIs[Covariant[Any]]: return isinstance(arg, Covariant) +def _(x: object): + if is_instance_of_covariant(x): + reveal_type(x) # revealed: Covariant[Any] +``` + +However, intersecting with the declared gradual type does not necessarily exclude every other +specialization in the negative branch: + +```py +from typing import final + +@final +class Unrelated: ... + def needs_instance_of_unrelated(arg: Unrelated): pass @@ -483,11 +506,50 @@ def _(x: Unrelated | Covariant[int]): if is_instance_of_covariant(x): raise RuntimeError("oh no") - reveal_type(x) # revealed: Unrelated & ~Covariant[object] + reveal_type(x) # revealed: Unrelated | (Covariant[int] & ~Covariant[Any]) + + needs_instance_of_unrelated(x) # error: [invalid-argument-type] +``` + +If a user wants to select *all* instances of `Covariant`, they must use `Covariant[object]`, or more +generally, `Top[C[Any]]`, which also works for invariant generic types: + +```py +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ty_extensions import Top + +class Invariant[T]: + value: T # make it invariant in `T` + +def is_instance_of_invariant(arg: object) -> "TypeIs[Top[Invariant[Any]]]": + return isinstance(arg, Invariant) + +def _(x: Unrelated | Invariant[int]): + if is_instance_of_invariant(x): + reveal_type(x) # revealed: Invariant[int] + else: + reveal_type(x) # revealed: Unrelated +``` + +## `TypeIs` narrowing of `NewType` instances + +`NewType` constructors return their arguments unchanged, so an integer-based `NewType` can contain a +`bool`. A `TypeIs[bool]` guard preserves both the `NewType` and its runtime class. + +```py +from typing import NewType +from typing_extensions import TypeIs + +UserId = NewType("UserId", int) + +def is_bool(value: object) -> TypeIs[bool]: + return isinstance(value, bool) - # We would emit a false-positive diagnostic here if we didn't implicitly transform - # `TypeIs[Covariant[Any]]` to `TypeIs[Covariant[object]]` - needs_instance_of_unrelated(x) +def _(value: UserId): + if is_bool(value): + reveal_type(value) # revealed: UserId & bool ``` ## `TypeGuard` special cases diff --git a/crates/ty_python_semantic/resources/mdtest/notebook.md b/crates/ty_python_semantic/resources/mdtest/notebook.md index 7d9f840af0..cd3dcee5cd 100644 --- a/crates/ty_python_semantic/resources/mdtest/notebook.md +++ b/crates/ty_python_semantic/resources/mdtest/notebook.md @@ -40,5 +40,4 @@ error[invalid-syntax]: Expected class, function definition or async function def | 2 | @staticmethod | ^ - | ``` diff --git a/crates/ty_python_semantic/resources/mdtest/overloads.md b/crates/ty_python_semantic/resources/mdtest/overloads.md index 2ffff5fc82..f8bd5f6f41 100644 --- a/crates/ty_python_semantic/resources/mdtest/overloads.md +++ b/crates/ty_python_semantic/resources/mdtest/overloads.md @@ -275,9 +275,7 @@ def union_receiver(reader: Reader[int | str]): ## Method type variables inferred from `self` Binding an overload whose explicit receiver introduces a method type variable should infer that -variable from the concrete receiver and apply it to the remainder of the signature. At present, -receiver matching retains the overload, but does not yet apply the inferred `S = str` -specialization. +variable from the concrete receiver and apply it to the remainder of the signature. ```toml [environment] @@ -285,9 +283,11 @@ python-version = "3.12" ``` ```py -from typing import overload +from typing import Any, Callable, overload class ReceiverGeneric[T]: + value: T + @overload def method[S](self: "ReceiverGeneric[S]", value: S) -> S: ... @overload @@ -295,18 +295,147 @@ class ReceiverGeneric[T]: def method(self, value: object) -> object: return value -# Receiver constraints are preserved for later relation checks, but are not yet solved into the -# displayed bound signature. -# TODO: revealed: Overload[(value: str) -> str, (value: bytes) -> bytes] -reveal_type(ReceiverGeneric[str]().method) # revealed: Overload[[S](value: S) -> S, (value: bytes) -> bytes] +reveal_type(ReceiverGeneric[str]().method) # revealed: Overload[(value: str) -> str, (value: bytes) -> bytes] + +def takes_callable(fn: Callable[..., Any]) -> None: ... +def use_generic_receiver[T](value: ReceiverGeneric[T]) -> None: + # revealed: Overload[(value: T@use_generic_receiver) -> T@use_generic_receiver, (value: bytes) -> bytes] + reveal_type(value.method) + takes_callable(value.method) +``` + +## Constrained method type variables inferred from `self` + +Matching a receiver against a value-constrained method type variable must reject values outside that +variable's constraints. A subclass of an allowed value must be promoted to the declared constraint +rather than appearing as the specialized return type. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Callable, Generic, TypeVar, overload + +BoxT = TypeVar("BoxT", covariant=True) +Constrained = TypeVar("Constrained", str, bytes) + +class ConstrainedReceiverBox(Generic[BoxT]): + @overload + def method(self: "ConstrainedReceiverBox[Constrained]", value: Constrained) -> Constrained: ... + @overload + def method(self: "ConstrainedReceiverBox[Constrained]", value: Constrained, repeat: int = ...) -> Constrained: ... + def method(self, value: str | bytes, repeat: int = 1) -> str | bytes: + return value + +invalid_receiver = ConstrainedReceiverBox[int]() +invalid_method = invalid_receiver.method +reveal_type(invalid_method) # revealed: Overload[] + +# error: [no-matching-overload] +reveal_type(invalid_method(1)) # revealed: Unknown + +# error: [invalid-assignment] +invalid_callback: Callable[[int], int] = invalid_method + +class SubStr(str): ... + +subclass_receiver = ConstrainedReceiverBox[SubStr]() +reveal_type(subclass_receiver.method(SubStr())) # revealed: str +promoted_callback: Callable[[SubStr], str] = subclass_receiver.method +``` + +## Disjunctive generic receivers + +A receiver may satisfy both sides of a union without constraining both sides' type variables on the +same path. In particular, matching `Left[int]` leaves the `Right` type variable available to +specialize from the method arguments. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Callable, overload + +class Left[T]: + left: T + +class Right[T]: + right: T + +class BaseWithDisjunctiveReceiver: + @overload + def method[S, U](self: "Left[S] | Right[U]", first: S, second: U) -> tuple[S, U]: ... + @overload + def method(self, first: bytes, second: bytes) -> tuple[bytes, bytes]: ... + def method(self, first: object, second: object) -> tuple[object, object]: + return first, second + +class Both(BaseWithDisjunctiveReceiver, Left[int], Right[str]): ... + +receiver = Both() +receiver.method(1, b"value") +valid_callback: Callable[[int, bytes], tuple[int, bytes]] = receiver.method +``` + +## Receiver type variables alongside variadic type parameters + +A method's `ParamSpec` or `TypeVarTuple` must not prevent an ordinary type variable from being +specialized by its receiver. The variadic parameters remain available for argument inference; +neither method can be converted into a callback with a return type incompatible with the receiver. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Callable, overload + +class VariadicReceiverBox[T]: + value: T + + @overload + def with_paramspec[**P, S](self: "VariadicReceiverBox[S]", callback: Callable[P, object]) -> S: ... + @overload + def with_paramspec(self, callback: bytes) -> bytes: ... + def with_paramspec(self, callback: object) -> object: + return callback + + @overload + def with_typevartuple[*Ts, S](self: "VariadicReceiverBox[S]", first: int, *values: *Ts) -> S: ... + @overload + def with_typevartuple(self, first: bytes) -> bytes: ... + def with_typevartuple(self, first: object, *values: object) -> object: + return first + +def accepts_int(value: int) -> object: + return value + +receiver = VariadicReceiverBox[str]() + +# revealed: Overload[[**P](callback: (**P) -> object) -> str, (callback: bytes) -> bytes] +reveal_type(receiver.with_paramspec) +reveal_type(receiver.with_paramspec(accepts_int)) # revealed: str + +# error: [invalid-assignment] +bad_paramspec_callback: Callable[[Callable[[int], object]], int] = receiver.with_paramspec + +reveal_type(receiver.with_typevartuple(1, b"value")) # revealed: str +typevartuple_callback: Callable[[int, bytes], str] = receiver.with_typevartuple + +# error: [invalid-assignment] +bad_typevartuple_callback: Callable[[int, bytes], int] = receiver.with_typevartuple ``` ## Structural protocol receivers Checking a generic protocol receiver requires solving all uses of its type variable together. Here `get()` would require `int` to be assignable to `T`, while `put()` would require `T` to be -assignable to `str`, so no `T` can satisfy `ProtocolSelf[T]`. At present, the incompatible overload -is retained because structural receiver specialization is not yet supported. +assignable to `str`, so no `T` can satisfy `ProtocolSelf[T]`. ```py from typing import Callable, Protocol, TypeVar, overload @@ -331,14 +460,51 @@ class ProtocolSelfImplementation(BaseWithProtocolSelf): def put(self, x: str) -> None: ... -# TODO: The first overload should be eliminated, leaving `bound method -# BaseWithProtocolSelf.method() -> bytes`. -reveal_type(ProtocolSelfImplementation().method) # revealed: Overload[[ProtocolSelfT]() -> ProtocolSelfT, () -> bytes] +reveal_type(ProtocolSelfImplementation().method) # revealed: bound method ProtocolSelfImplementation.method() -> bytes good_protocol_receiver: Callable[[], bytes] = ProtocolSelfImplementation().method bad_protocol_receiver: Callable[[], int] = ProtocolSelfImplementation().method # error: [invalid-assignment] ``` +## One-sided constraints from protocol receivers + +An explicit protocol receiver can constrain a method type variable without determining an exact +specialization. Keep that type variable generic so that compatible callbacks remain valid while the +receiver constraint rejects incompatible ones. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Callable, Protocol, overload + +class Producer[T](Protocol): + def get(self) -> T: ... + +class BaseWithProducer: + @overload + def method[S](self: Producer[S], value: S) -> S: ... + @overload + def method(self, value: bytes) -> bytes: ... + def method(self, value: object) -> object: + return value + +class ProducerImplementation(BaseWithProducer): + def get(self) -> str: + return "" + +# `Producer` is covariant, so binding records `str <: S` without specializing `S` to `str`. +reveal_type(ProducerImplementation().method) # revealed: Overload[[S](value: S) -> S, (value: bytes) -> bytes] +# `S = object` satisfies the receiver constraint. +producer_callback: Callable[[object], object] = ProducerImplementation().method +# `S = int` violates the receiver constraint, and the `bytes` overload is also incompatible. +bad_producer_callback: Callable[[int], int] = ProducerImplementation().method # error: [invalid-assignment] +# The argument adds `Literal[1] <: S`, so the combined lower bound is `str | Literal[1]`. +reveal_type(ProducerImplementation().method(1)) # revealed: str | Literal[1] +``` + ## Constructor ```py @@ -807,6 +973,264 @@ def generic_parameter_type(x: int) -> int | str: return x ``` +A method that refers to a type variable from its enclosing class is not itself generic. In +particular, overload consistency must still account for keyword names that may be included in an +enclosing `ParamSpec`: + +```py +from typing import Generic, ParamSpec, overload + +P = ParamSpec("P") + +class Task(Generic[P]): + @overload + # error: [invalid-overload] "Implementation does not accept all arguments of this overload" + def submit(self: "Task[P]", *args: P.args, **kwargs: P.kwargs) -> int: ... + @overload + def submit(self: "Task[P]", value: int) -> int: ... + def submit( + self: "Task[P]", + *args: object, + return_state: bool = False, + **kwargs: object, + ) -> int: + return 1 +``` + +### Decorated implementation consistency + +Decorators on an overload implementation apply only to the implementation signature. The decorated +signature is checked against the overloads, while callers continue to see only the overloads. + +```py +from typing import Callable, overload + +def widen_return(func: Callable[[int | str], int]) -> Callable[[int | str], int | str]: + raise NotImplementedError + +@overload +def widened(x: int, /) -> int: ... +@overload +def widened(x: str, /) -> str: ... +@widen_return +def widened(x: int | str) -> int: + return 1 + +reveal_type(widened) # revealed: Overload[(x: int, /) -> int, (x: str, /) -> str] +reveal_type(widened(1)) # revealed: int +reveal_type(widened("one")) # revealed: str + +def narrow_parameter(func: Callable[[int | str], int | str]) -> Callable[[int], int | str]: + raise NotImplementedError + +@overload +def narrowed(x: int, /) -> int: ... +@overload +# error: [invalid-overload] "Implementation does not accept all arguments of this overload" +def narrowed(x: str, /) -> str: ... +@narrow_parameter +def narrowed(x: int | str) -> int | str: + return x + +reveal_type(narrowed) # revealed: Overload[(x: int, /) -> int, (x: str, /) -> str] +reveal_type(narrowed(1)) # revealed: int +reveal_type(narrowed("one")) # revealed: str +``` + +### Decorated overload consistency + +Decorators on individual overloads transform those overload signatures before implementation +consistency is checked. The transformed signatures remain visible to callers. + +```py +from typing import Callable, overload + +def decorate_overload(func: Callable[..., object]) -> Callable[[int], int]: + raise NotImplementedError + +def decorate_implementation(func: Callable[..., object]) -> Callable[[int | str], int | str]: + raise NotImplementedError + +@overload +@decorate_overload +# basedpython also checks each overload against the implementation, and a decorator that +# rewrites one side's signature but not the other's makes them disagree +# error: [invalid-overload] +def decorated() -> None: ... +@overload +def decorated(x: str, /) -> str: ... +@decorate_implementation +def decorated(y: bytes, z: bytes) -> bytes: + raise NotImplementedError + +reveal_type(decorated) # revealed: Overload[(int, /) -> int, (x: str, /) -> str] +reveal_type(decorated(1)) # revealed: int +reveal_type(decorated("one")) # revealed: str +``` + +### Decorated overloads with `Concatenate` + +Each decorated overload applies its decorator to its own signature, without including any preceding +overloads in the decorator call. + +```py +from collections.abc import Callable +from typing import Any, Concatenate, ParamSpec, TypeVar, overload + +P = ParamSpec("P") +A = TypeVar("A") +R = TypeVar("R") + +def curry1(func: Callable[Concatenate[A, P], R]) -> Callable[[A], Callable[P, R]]: + raise NotImplementedError + +@curry1 +@overload +# error: [invalid-overload] +def starmap(mapper: Callable[[int, int], int], parser: int) -> int: ... +@curry1 +@overload +# error: [invalid-overload] +def starmap(mapper: Callable[[str, str, str], str], parser: str) -> str: ... +@curry1 +def starmap(mapper: Callable[..., Any], parser: Any) -> Any: + raise NotImplementedError + +def add(x: int, y: int) -> int: + return x + y + +# revealed: Overload[((int, int, /) -> int, /) -> ((parser: int) -> int), ((str, str, str, /) -> str, /) -> ((parser: str) -> str)] +reveal_type(starmap) +reveal_type(starmap(add)) # revealed: (parser: int) -> int +``` + +### Decorated implementation replaced by a function + +A decorator can replace an overload implementation with another function. The overload set remains +visible to callers, the replacement signature is checked for consistency, and an outer `@deprecated` +decorator still applies to the overload set. + +```py +from collections.abc import Callable +from typing import Any, TypeVar, overload +from typing_extensions import deprecated + +R = TypeVar("R") + +def replacement(x: int, /) -> int: + return x + +def replace_with(value: R) -> Callable[[Callable[..., Any]], R]: + def decorator(_function: Callable[..., Any]) -> R: + return value + return decorator + +@overload +def replaced(x: int, /) -> int: ... +@overload +# error: [invalid-overload] "Overload signature is not consistent with implementation" +def replaced(x: str, /) -> str: ... +@deprecated("use replacement directly") +@replace_with(replacement) +def replaced(x: int | str) -> int | str: + return x + +# error: [deprecated] "use replacement directly" +reveal_type(replaced) # revealed: Overload[(x: int, /) -> int, (x: str, /) -> str] +# error: [deprecated] "use replacement directly" +reveal_type(replaced("one")) # revealed: str +``` + +### Decorated implementation with multiple callable signatures + +An overloaded callback protocol can provide one implementation signature for each overload. Every +callable in a union must support every overload. A decorator that returns a non-callable cannot +implement any overload. + +```py +from typing import Callable, Protocol, overload + +class ValidCallback(Protocol): + @overload + def __call__(self, x: int, /) -> int: ... + @overload + def __call__(self, x: str, /) -> str: ... + +class NarrowCallback(Protocol): + @overload + def __call__(self, x: int, /) -> int: ... + @overload + def __call__(self, x: bytes, /) -> bytes: ... + +def valid_callback(func: Callable[[int | str], int | str]) -> ValidCallback: + raise NotImplementedError + +def narrow_callback(func: Callable[[int | str], int | str]) -> NarrowCallback: + raise NotImplementedError + +def valid_union( + func: Callable[[int | str], int | str], +) -> Callable[[int | str], int | str] | Callable[[object], object]: + raise NotImplementedError + +def narrow_union( + func: Callable[[int | str], int | str], +) -> Callable[[int | str], int | str] | Callable[[int], int]: + raise NotImplementedError + +def noncallable(func: Callable[[int | str], int | str]) -> int: + raise NotImplementedError + +@overload +def callback_valid(x: int, /) -> int: ... +@overload +def callback_valid(x: str, /) -> str: ... +@valid_callback +def callback_valid(x: int | str) -> int | str: + return x + +@overload +def callback_narrowed(x: int, /) -> int: ... +@overload +# error: [invalid-overload] "Overload signature is not consistent with implementation" +def callback_narrowed(x: str, /) -> str: ... +@narrow_callback +def callback_narrowed(x: int | str) -> int | str: + return x + +@overload +def union_valid(x: int, /) -> int: ... +@overload +def union_valid(x: str, /) -> str: ... +@valid_union +def union_valid(x: int | str) -> int | str: + return x + +@overload +def union_narrowed(x: int, /) -> int: ... +@overload +# error: [invalid-overload] "Overload signature is not consistent with implementation" +def union_narrowed(x: str, /) -> str: ... +@narrow_union +def union_narrowed(x: int | str) -> int | str: + return x + +@overload +def not_callable(x: int, /) -> int: ... +@overload +def not_callable(x: str, /) -> str: ... +@noncallable +# error: [invalid-overload] "Overload implementation is not callable after applying decorators" +def not_callable(x: int | str) -> int | str: + return x + +reveal_type(callback_valid) # revealed: Overload[(x: int, /) -> int, (x: str, /) -> str] +reveal_type(callback_narrowed) # revealed: Overload[(x: int, /) -> int, (x: str, /) -> str] +reveal_type(union_valid) # revealed: Overload[(x: int, /) -> int, (x: str, /) -> str] +reveal_type(union_narrowed) # revealed: Overload[(x: int, /) -> int, (x: str, /) -> str] +reveal_type(not_callable) # revealed: Overload[(x: int, /) -> int, (x: str, /) -> str] +``` + ### Implementation consistency parameter mismatch diagnostics Non-generic implementation checks require parameter names and positional-only forms to line up with @@ -848,7 +1272,6 @@ error[invalid-overload]: Implementation does not accept all arguments of this ov | ^^^^^^^^ 10 | def _extract(self, row_key: int | None = None, column_key: int | None = None) -> object: | -------- Implementation defined here - | info: Implementation signature `(self, row_key: int | None = None, column_key: int | None = None) -> object` is not assignable to overload signature `(self, column_key: int) -> object` info: the parameter named `row_key` does not match `column_key` (and can be used as a keyword parameter) @@ -860,7 +1283,6 @@ error[invalid-overload]: Implementation does not accept all arguments of this ov | ^^^^^^ 19 | def update(self, params=(), /, **kwds) -> None: | ------ Implementation defined here - | info: Implementation signature `(self, params: Iterable[tuple[str, Iterable[str]]] = ..., /, **kwds: Iterable[str]) -> None` is not assignable to overload signature `(self, **kwds: Iterable[str]) -> None` info: parameter `self` is positional-only but must also accept keyword arguments ``` @@ -897,7 +1319,6 @@ error[invalid-overload]: Overload return type is not assignable to implementatio 7 | def return_tuple(x: str) -> tuple[int]: ... 8 | def return_tuple(x: int | str) -> tuple[int]: | ------------ Implementation defined here - | info: Overload returns `tuple[str]`, which is not assignable to implementation return type `tuple[int]` info: the first tuple element is not compatible: `str` is not assignable to `int` ``` @@ -1351,3 +1772,51 @@ def baz(x, y, z=None) -> bytes | list[str]: # revealed: Overload[(x, y) -> bytes, (x, y, z) -> list[str]] reveal_type(baz) ``` + +## Generic overloaded protocol members preserve receiver relationships + +An overloaded method used to satisfy a protocol receiver can relate a method-scoped type variable to +a concrete generic receiver. Binding that member must retain the scalar return type. + +```toml +[environment] +python-version = "3.12" +``` + +```pyi +from typing import Any, Generic, Protocol, TypeVar, assert_type, overload, reveal_type + +class ScalarBase: ... +class Scalar(ScalarBase): ... + +ScalarCo = TypeVar("ScalarCo", bound=ScalarBase, covariant=True, default=ScalarBase) +ShapeCo = TypeVar("ShapeCo", bound=tuple[int, ...], covariant=True, default=tuple[Any, ...]) + +class HasPhantom[T](Protocol): + def phantom(self) -> T: ... + +class Phantom(Generic[ShapeCo, ScalarCo]): + # An empty shape selects the scalar overload and relates its return type to the receiver. + @overload + def phantom[T: ScalarBase](self: "Phantom[tuple[()], T]") -> T: ... + # A non-empty shape selects the list-valued overload instead. + @overload + def phantom[Shape: tuple[int, *tuple[int, ...]], T: ScalarBase]( + self: "Phantom[Shape, T]", + ) -> list[T]: ... + +class Normal(Phantom[ShapeCo, ScalarCo], Generic[ShapeCo, ScalarCo]): + # Matching this protocol receiver requires binding the inherited `phantom` overloads. + @property + def value[T](self: "HasPhantom[T]") -> T: ... + +# The empty shape selects `phantom() -> Scalar`, so the protocol and property type is `Scalar`. +normal: Normal[tuple[()], Scalar] +assert_type(normal.value, Scalar) + +# A non-empty shape selects `phantom() -> list[Scalar]`, so the property type is `list[Scalar]`. +shaped: Normal[tuple[int], Scalar] +assert_type(shaped.phantom(), list[Scalar]) +# TODO: The receiver constraint `Scalar <: T` should propagate through invariant `list[T]`. +reveal_type(shaped.value) # revealed: Unknown +``` diff --git a/crates/ty_python_semantic/resources/mdtest/override.md b/crates/ty_python_semantic/resources/mdtest/override.md index 144d36fad4..2556808303 100644 --- a/crates/ty_python_semantic/resources/mdtest/override.md +++ b/crates/ty_python_semantic/resources/mdtest/override.md @@ -558,6 +558,80 @@ class StubAbstractImplementation(StubAbstractInterface): def method(self) -> int: ... # error: [missing-override-decorator] ``` +## Missing `@override` decorator on Python 3.11 + +```toml +[environment] +python-version = "3.11" + +[rules] +missing-override-decorator = "error" +``` + +```py +from typing_extensions import override + +class Parent: + def method(self) -> None: ... + +class Child(Parent): + def method(self) -> None: ... # snapshot: missing-override-decorator + +class ExplicitChild(Parent): + @override + def method(self) -> None: ... +``` + +```snapshot +error[missing-override-decorator]: Method `method` overrides `Parent.method` but is not decorated with `@override` + --> src/mdtest_snippet.py:7:9 + | +4 | def method(self) -> None: ... + | ------ `Parent.method` defined here +5 | +6 | class Child(Parent): +7 | def method(self) -> None: ... # snapshot: missing-override-decorator + | ^^^^^^ +info: Decorate the method with `@typing_extensions.override` to make the override explicit +``` + +## Missing `@override` decorator on Python 3.12 + +```toml +[environment] +python-version = "3.12" + +[rules] +missing-override-decorator = "error" +``` + +```py +from typing import override + +class Parent: + def method(self) -> None: ... + +class Child(Parent): + def method(self) -> None: ... # snapshot: missing-override-decorator + +class ExplicitChild(Parent): + @override + def method(self) -> None: ... +``` + +```snapshot +error[missing-override-decorator]: Method `method` overrides `Parent.method` but is not decorated with `@override` + --> src/mdtest_snippet.py:7:9 + | +4 | def method(self) -> None: ... + | ------ `Parent.method` defined here +5 | +6 | class Child(Parent): +7 | def method(self) -> None: ... # snapshot: missing-override-decorator + | ^^^^^^ +info: Decorate the method with `@typing.override` to make the override explicit +``` + ## Possibly-unbound definitions ```py @@ -783,6 +857,7 @@ class Spam: @overload @override + # error: [invalid-overload] "`@override` decorator should be applied only to the overload implementation" def quux(self, x: str) -> str: ... @overload def quux(self, x: int) -> int: ... diff --git a/crates/ty_python_semantic/resources/mdtest/paramspec_subcall_error_location.md b/crates/ty_python_semantic/resources/mdtest/paramspec_subcall_error_location.md index f5b35cc3f6..20623b2a97 100644 --- a/crates/ty_python_semantic/resources/mdtest/paramspec_subcall_error_location.md +++ b/crates/ty_python_semantic/resources/mdtest/paramspec_subcall_error_location.md @@ -1,9 +1,9 @@ # `ParamSpec` error locations -When a free `ParamSpec` is available in a parameter before the ones representing it's components -(`P.args` and `P.kwargs`), ty invokes a sub-call logic where it performs a separate call to the -function with the arguments that are resolved from the `ParamSpec`. In this case, the diagnostic -location need to be offset based on the position of the `ParamSpec` components. +A callable can accept another callable and forward its positional and keyword arguments using a +`ParamSpec`. These tests check that argument errors identify the callback parameter that rejected +the argument, or fall back to the forwarding function's `*args` or `**kwargs` when that parameter +cannot be identified. ```toml [environment] @@ -30,13 +30,11 @@ error[invalid-argument-type]: Argument to function `foo` is incorrect | 9 | foo(fn1, "a", 2, c="c", unknown=1) | ^^^ Expected `int`, found `Literal["a"]` - | info: Function defined here - --> src/mdtest_snippet.py:3:5 - | -3 | def foo[**P, T](fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs): ... - | ^^^ ------------------ Parameter declared here + --> src/mdtest_snippet.py:4:5 | +4 | def fn1(a: int, b: int, c: int) -> None: ... + | ^^^ ------ Parameter declared here error[invalid-argument-type]: Argument to function `foo` is incorrect @@ -44,13 +42,11 @@ error[invalid-argument-type]: Argument to function `foo` is incorrect | 9 | foo(fn1, "a", 2, c="c", unknown=1) | ^^^^^ Expected `int`, found `Literal["c"]` - | info: Function defined here - --> src/mdtest_snippet.py:3:5 - | -3 | def foo[**P, T](fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs): ... - | ^^^ ------------------ Parameter declared here + --> src/mdtest_snippet.py:4:5 | +4 | def fn1(a: int, b: int, c: int) -> None: ... + | ^^^ ------ Parameter declared here error[unknown-argument]: Argument `unknown` does not match any known parameter of function `foo` @@ -58,13 +54,11 @@ error[unknown-argument]: Argument `unknown` does not match any known parameter o | 9 | foo(fn1, "a", 2, c="c", unknown=1) | ^^^^^^^^^ - | info: Function signature here --> src/mdtest_snippet.py:3:5 | 3 | def foo[**P, T](fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs): ... | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | ``` ```py @@ -80,13 +74,11 @@ error[too-many-positional-arguments]: Too many positional arguments to function | 13 | foo(fn2, 1, 2, 3) | ^ - | info: Function signature here --> src/mdtest_snippet.py:3:5 | 3 | def foo[**P, T](fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs): ... | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | ``` ```py @@ -102,13 +94,11 @@ error[positional-only-parameter-as-kwarg]: Positional-only parameter 1 (`a`) pas | 17 | foo(fn3, a=1) | ^^^ - | info: Function signature here --> src/mdtest_snippet.py:3:5 | 3 | def foo[**P, T](fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs): ... | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | ``` ```py @@ -128,13 +118,11 @@ error[missing-argument]: No argument provided for required parameter `b` of func | 22 | foo(fn4, 1, a=2) | ^^^^^^^^^^^^^^^^ - | info: Parameter declared here --> src/mdtest_snippet.py:3:37 | 3 | def foo[**P, T](fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs): ... | ^^^^^^^^^^^^^ - | error[parameter-already-assigned]: Multiple values provided for parameter `a` of function `foo` @@ -142,7 +130,6 @@ error[parameter-already-assigned]: Multiple values provided for parameter `a` of | 22 | foo(fn4, 1, a=2) | ^^^ - | error[missing-argument]: No arguments provided for required parameters `a`, `b` of function `foo` @@ -150,13 +137,11 @@ error[missing-argument]: No arguments provided for required parameters `a`, `b` | 25 | foo(fn4) | ^^^^^^^^ - | info: Parameters declared here --> src/mdtest_snippet.py:3:16 | 3 | def foo[**P, T](fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs): ... | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | ``` ## Methods @@ -179,3 +164,800 @@ foo = Foo() # error: [unknown-argument] foo.method(fn1, "a", 2, c="c", unknown=1) ``` + +## Forwarded keyword arguments + +A forwarded keyword argument should identify the matching callback parameter, not the parameter that +accepts the callback. + +```py +from typing import Callable + +def wrapper[**P](callback: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... +def callback(*, value: int) -> None: ... + +wrapper(callback, value="incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:6:19 + | +6 | wrapper(callback, value="incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Function defined here + --> src/mdtest_snippet.py:4:5 + | +4 | def callback(*, value: int) -> None: ... + | ^^^^^^^^ ---------- Parameter declared here +``` + +## Forwarded bound methods + +A bound method still includes `self` in its source signature. The diagnostic should skip that +parameter and identify the argument that actually failed. + +```py +from typing import Callable + +def wrapper[**P](callback: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... + +class Handler: + def callback(self, *, value: int) -> None: ... + +def run(handler: Handler) -> None: + wrapper(handler.callback, value="incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:9:31 + | +9 | wrapper(handler.callback, value="incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Method defined here + --> src/mdtest_snippet.py:6:9 + | +6 | def callback(self, *, value: int) -> None: ... + | ^^^^^^^^ ---------- Parameter declared here +``` + +## Callbacks without a source definition + +A `Callable` annotation describes the accepted arguments but does not identify the function that +declared them. In that case, point to the forwarding function's `*args` parameter. The expanded +keyword parameters below cover the corresponding `**kwargs` fallback. + +```py +from typing import Callable + +def wrapper[**P](callback: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... +def run(callback: Callable[[int], None]) -> None: + wrapper(callback, "incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:5:23 + | +5 | wrapper(callback, "incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Function defined here + --> src/mdtest_snippet.py:3:5 + | +3 | def wrapper[**P](callback: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... + | ^^^^^^^ ------------- Parameter declared here +``` + +## Parameters consumed by Concatenate + +`Concatenate` lets a forwarding function provide the first argument itself. The diagnostic still +needs to account for that argument when locating the callback's remaining parameter. + +```py +from typing import Callable, Concatenate + +def wrapper[**P](callback: Callable[Concatenate[int, P], None], *args: P.args, **kwargs: P.kwargs) -> None: + callback(0, *args, **kwargs) + +def callback(prefix: int, *, value: int) -> None: ... + +wrapper(callback, value="incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:8:19 + | +8 | wrapper(callback, value="incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Function defined here + --> src/mdtest_snippet.py:6:5 + | +6 | def callback(prefix: int, *, value: int) -> None: ... + | ^^^^^^^^ ---------- Parameter declared here +``` + +## Overloaded callbacks + +When a callback has multiple overloads, the diagnostic should identify the parameter on the overload +that accepted the other arguments. + +```py +from typing import Callable, overload + +def wrapper[**P](callback: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... +@overload +def callback(value: int) -> None: ... +@overload +def callback(value: str, *, flag: str) -> None: ... +def callback(value: int | str, *, flag: str | None = None) -> None: ... + +wrapper(callback, "value", flag=1) # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:10:28 + | +10 | wrapper(callback, "value", flag=1) # snapshot: invalid-argument-type + | ^^^^^^ Expected `str`, found `Literal[1]` +info: Function defined here + --> src/mdtest_snippet.py:7:5 + | +7 | def callback(value: str, *, flag: str) -> None: ... + | ^^^^^^^^ --------- Parameter declared here +``` + +## Overloads selected by Concatenate + +The first callback overload accepts a `str` prefix, so it cannot match a forwarding function that +always supplies an `int`. An error in the remaining arguments should point to the second overload. + +```py +from typing import Callable, Concatenate, overload + +def wrapper[**P](callback: Callable[Concatenate[int, P], None], *args: P.args, **kwargs: P.kwargs) -> None: + callback(1, *args, **kwargs) + +@overload +def callback(prefix: str, value: str) -> None: ... +@overload +def callback(prefix: int, value: int) -> None: ... +def callback(prefix: str | int, value: str | int) -> None: ... + +wrapper(callback, "incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:12:19 + | +12 | wrapper(callback, "incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Function defined here + --> src/mdtest_snippet.py:9:5 + | +9 | def callback(prefix: int, value: int) -> None: ... + | ^^^^^^^^ ---------- Parameter declared here +``` + +## Overloads selected by a bound receiver + +A generic method can have separate overloads for different receiver types. A method on +`Receiver[int]` should point to the overload declared for `Receiver[int]`. + +```py +from typing import Callable, overload + +def wrapper[**P](callback: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... + +class Receiver[T]: + value: T + + @overload + def method(self: "Receiver[str]", value: str) -> None: ... + @overload + def method(self: "Receiver[int]", value: int) -> None: ... + def method(self, value: str | int) -> None: ... + +def run(receiver: Receiver[int]) -> None: + wrapper(receiver.method, "incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:15:30 + | +15 | wrapper(receiver.method, "incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Method defined here + --> src/mdtest_snippet.py:11:9 + | +11 | def method(self: "Receiver[int]", value: int) -> None: ... + | ^^^^^^ ---------- Parameter declared here +``` + +## Callback annotations with multiple callable alternatives + +The callback below matches the second union alternative, which does not consume a leading argument. +The diagnostic should therefore identify `first`, not `second`. + +```py +from typing import Callable, Concatenate + +def wrapper[**P](callback: Callable[Concatenate[int, P], None] | Callable[P, str], *args: P.args, **kwargs: P.kwargs) -> None: ... +def callback(first: int, second: str) -> str: + return second + +wrapper(callback, "incorrect", "valid") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:7:19 + | +7 | wrapper(callback, "incorrect", "valid") # snapshot: invalid-argument-type + | ^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Function defined here + --> src/mdtest_snippet.py:4:5 + | +4 | def callback(first: int, second: str) -> str: + | ^^^^^^^^ ---------- Parameter declared here +``` + +## Optional callbacks + +A union may also contain a value that is not callable. The presence of `None` should not prevent the +diagnostic from identifying the callback's parameter. + +```py +from typing import Callable + +def wrapper[**P](callback: Callable[P, None] | None, *args: P.args, **kwargs: P.kwargs) -> None: ... +def callback(value: int) -> None: ... + +wrapper(callback, "incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:6:19 + | +6 | wrapper(callback, "incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Function defined here + --> src/mdtest_snippet.py:4:5 + | +4 | def callback(value: int) -> None: ... + | ^^^^^^^^ ---------- Parameter declared here +``` + +## Overloaded forwarding functions + +An overloaded forwarding function should retain the note identifying its matching overload as well +as the note identifying the callback parameter. + +```py +from typing import Callable, overload + +@overload +def wrap[**P](callback: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... +@overload +def wrap(value: int, first: int, second: int) -> None: ... +def wrap(callback: Callable[..., None] | int, *args: object, **kwargs: object) -> None: ... +def keyword_callback(*, value: int) -> None: ... +def positional_callback(*values: int) -> None: ... +``` + +A keyword argument belongs to the forwarding function's `**kwargs` parameter. + +```py +wrap(keyword_callback, value="incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrap` is incorrect + --> src/mdtest_snippet.py:10:24 + | +10 | wrap(keyword_callback, value="incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Function defined here + --> src/mdtest_snippet.py:8:5 + | +8 | def keyword_callback(*, value: int) -> None: ... + | ^^^^^^^^^^^^^^^^ ---------- Parameter declared here +info: Matching overload defined here + --> src/mdtest_snippet.py:4:5 + | +4 | def wrap[**P](callback: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... + | ^^^^ ------------------ Parameter declared here +info: Non-matching overloads for function `wrap`: +info: (value: int, first: int, second: int) -> None +``` + +A positional argument belongs to `*args`, even when the callback accepts it through `*values`. + +```py +wrap(positional_callback, "incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrap` is incorrect + --> src/mdtest_snippet.py:11:27 + | +11 | wrap(positional_callback, "incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Function defined here + --> src/mdtest_snippet.py:9:5 + | +9 | def positional_callback(*values: int) -> None: ... + | ^^^^^^^^^^^^^^^^^^^ ------------ Parameter declared here +info: Matching overload defined here + --> src/mdtest_snippet.py:4:5 + | +4 | def wrap[**P](callback: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... + | ^^^^ ------------- Parameter declared here +info: Non-matching overloads for function `wrap`: +info: (value: int, first: int, second: int) -> None +``` + +## Forwarding through a callable object + +The forwarding object's own `self` parameter is not the callback. The diagnostic should point to the +argument accepted by `callback`. + +```py +from typing import Callable + +class Wrapper: + def __call__[**P](self, callback: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... + +def callback(value: int) -> None: ... + +Wrapper()(callback, "incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to bound method `Wrapper.__call__` is incorrect + --> src/mdtest_snippet.py:8:21 + | +8 | Wrapper()(callback, "incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Function defined here + --> src/mdtest_snippet.py:6:5 + | +6 | def callback(value: int) -> None: ... + | ^^^^^^^^ ---------- Parameter declared here +``` + +## Overloads with expanded positional parameters + +Expanding `Unpack[tuple[int]]` must preserve the link to the callback's `*values` declaration. The +diagnostic can then identify the matching overload instead of falling back to the forwarding +function's `*args` parameter. + +```py +from typing import Callable, Concatenate, Unpack, overload + +def wrapper[**P](callback: Callable[Concatenate[int, P], None], *args: P.args, **kwargs: P.kwargs) -> None: ... +@overload +def callback(prefix: str, *values: Unpack[tuple[str]]) -> None: ... +@overload +def callback(prefix: int, *values: Unpack[tuple[int]]) -> None: ... +def callback(prefix: str | int, *values: Unpack[tuple[str | int]]) -> None: ... + +wrapper(callback, "incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:10:19 + | +10 | wrapper(callback, "incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Function defined here + --> src/mdtest_snippet.py:7:5 + | +7 | def callback(prefix: int, *values: Unpack[tuple[int]]) -> None: ... + | ^^^^^^^^ --------------------------- Parameter declared here +``` + +## Expanded keyword parameters + +`Unpack[Config]` creates separate keyword parameters for `alpha` and `beta`, even though the +callback declares only `**options`. An error for `beta` should point to that `**options` +declaration. + +```py +from typing import Callable, TypedDict, Unpack + +def wrapper[**P](callback: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... + +class Config(TypedDict): + alpha: int + beta: int + +def callback(**options: Unpack[Config]) -> None: ... + +wrapper(callback, alpha=1, beta="incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:11:28 + | +11 | wrapper(callback, alpha=1, beta="incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Function defined here + --> src/mdtest_snippet.py:9:5 + | +9 | def callback(**options: Unpack[Config]) -> None: ... + | ^^^^^^^^ ------------------------- Parameter declared here +``` + +## Overloads with expanded keyword parameters + +Both callback overloads unpack the same `TypedDict`, so their expanded parameters refer to the same +field declarations. The diagnostic should still identify the overload selected by its `int` prefix. + +```py +from typing import Callable, Concatenate, TypedDict, Unpack, overload + +class Config(TypedDict): + value: int + +def wrapper[**P](callback: Callable[Concatenate[int, P], None], *args: P.args, **kwargs: P.kwargs) -> None: ... +@overload +def callback(prefix: str, **options: Unpack[Config]) -> None: ... +@overload +def callback(prefix: int, **options: Unpack[Config]) -> None: ... +def callback(prefix: str | int, **options: Unpack[Config]) -> None: ... + +wrapper(callback, value="incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:13:19 + | +13 | wrapper(callback, value="incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Function defined here + --> src/mdtest_snippet.py:10:5 + | +10 | def callback(prefix: int, **options: Unpack[Config]) -> None: ... + | ^^^^^^^^ ------------------------- Parameter declared here +``` + +The same overload identity must survive `functools.partial`, which removes the bound prefix before +forwarding the remaining arguments. + +```py +from functools import partial + +def forward[**P](callback: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... + +forward(partial(callback, 1), value="incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `forward` is incorrect + --> src/mdtest_snippet.py:18:31 + | +18 | forward(partial(callback, 1), value="incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Function defined here + --> src/mdtest_snippet.py:10:5 + | +10 | def callback(prefix: int, **options: Unpack[Config]) -> None: ... + | ^^^^^^^^ ------------------------- Parameter declared here +``` + +## Expanded positional parameters + +`Unpack[tuple[int, str]]` creates two positional parameters from one `*values` declaration. An error +in the second argument should point to that declaration. + +```py +from typing import Callable, Unpack + +def wrapper[**P](callback: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... +def callback(*values: Unpack[tuple[int, str]]) -> None: ... + +wrapper(callback, 1, 2) # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:6:22 + | +6 | wrapper(callback, 1, 2) # snapshot: invalid-argument-type + | ^ Expected `str`, found `Literal[2]` +info: Function defined here + --> src/mdtest_snippet.py:4:5 + | +4 | def callback(*values: Unpack[tuple[int, str]]) -> None: ... + | ^^^^^^^^ -------------------------------- Parameter declared here +``` + +## Callback protocols + +Unlike a plain `Callable` annotation, a callback protocol includes a declaration for `__call__`. The +diagnostic should point to the parameter on that method. + +```py +from typing import Callable, Protocol + +def wrapper[**P](callback: Callable[P, object], *args: P.args, **kwargs: P.kwargs) -> None: ... + +class Callback(Protocol): + def __call__(self, *, value: int) -> None: ... + +def run(callback: Callback) -> None: + wrapper(callback, value="incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:9:23 + | +9 | wrapper(callback, value="incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Method defined here + --> src/mdtest_snippet.py:6:9 + | +6 | def __call__(self, *, value: int) -> None: ... + | ^^^^^^^^ ---------- Parameter declared here +``` + +## Callable objects + +A callable object declares its accepted arguments on `__call__`. Point to that method when the +object is passed to a forwarding function. + +```py +from typing import Callable + +def wrapper[**P](callback: Callable[P, object], *args: P.args, **kwargs: P.kwargs) -> None: ... + +class Callback: + def __call__(self, *, value: int) -> None: ... + +wrapper(Callback(), value="incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:8:21 + | +8 | wrapper(Callback(), value="incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Method defined here + --> src/mdtest_snippet.py:6:9 + | +6 | def __call__(self, *, value: int) -> None: ... + | ^^^^^^^^ ---------- Parameter declared here +``` + +## Constructors defined by __init__ + +A class passed as the callback receives the forwarded arguments in its constructor. A class that +declares `__init__` should identify the matching constructor parameter. + +```py +from typing import Callable + +def wrapper[**P](callback: Callable[P, object], *args: P.args, **kwargs: P.kwargs) -> None: ... + +class Factory: + def __init__(self, value: int) -> None: ... + +wrapper(Factory, "incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:8:18 + | +8 | wrapper(Factory, "incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Method defined here + --> src/mdtest_snippet.py:6:9 + | +6 | def __init__(self, value: int) -> None: ... + | ^^^^^^^^ ---------- Parameter declared here +``` + +## Overloaded constructors defined by __init__ + +Synthesized constructor signatures should preserve the overload selected by `Concatenate`, even when +both overloads unpack the same `TypedDict` fields. + +```py +from typing import Callable, Concatenate, TypedDict, Unpack, overload + +class Options(TypedDict): + value: int + +def wrapper[**P, T](callback: Callable[Concatenate[int, P], T], *args: P.args, **kwargs: P.kwargs) -> T: + return callback(1, *args, **kwargs) + +class Factory: + @overload + def __init__(self, prefix: str, **options: Unpack[Options]) -> None: ... + @overload + def __init__(self, prefix: int, **options: Unpack[Options]) -> None: ... + def __init__(self, prefix: str | int, **options: Unpack[Options]) -> None: ... + +wrapper(Factory, value="incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:16:18 + | +16 | wrapper(Factory, value="incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Method defined here + --> src/mdtest_snippet.py:13:9 + | +13 | def __init__(self, prefix: int, **options: Unpack[Options]) -> None: ... + | ^^^^^^^^ -------------------------- Parameter declared here +``` + +## Constructors defined by a metaclass + +A custom metaclass can determine the accepted constructor arguments through its own `__call__`. That +declaration takes precedence over the class's `__init__` method. + +```py +from typing import Callable + +def wrapper[**P](callback: Callable[P, object], *args: P.args, **kwargs: P.kwargs) -> None: ... + +class Meta(type): + def __call__(cls, value: int) -> object: + return object() + +class Factory(metaclass=Meta): + def __init__(self, value: str) -> None: ... + +wrapper(Factory, "incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:12:18 + | +12 | wrapper(Factory, "incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Method defined here + --> src/mdtest_snippet.py:6:9 + | +6 | def __call__(cls, value: int) -> object: + | ^^^^^^^^ ---------- Parameter declared here +``` + +## Constructors defined by __new__ + +A constructor defined by `__new__` consumes `cls` before checking the forwarded arguments. The +diagnostic should point to `value`, not `cls`. + +```py +from typing import Callable, Self + +def wrapper[**P](callback: Callable[P, object], *args: P.args, **kwargs: P.kwargs) -> None: ... + +class Factory: + def __new__(cls, value: int) -> Self: + return super().__new__(cls) + +wrapper(Factory, "incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:9:18 + | +9 | wrapper(Factory, "incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Function defined here + --> src/mdtest_snippet.py:6:9 + | +6 | def __new__(cls, value: int) -> Self: + | ^^^^^^^ ---------- Parameter declared here +``` + +## Overloaded constructors defined by __new__ + +When `__new__` is overloaded, the diagnostic must both select the matching overload and account for +its `cls` parameter. + +```py +from typing import Callable, Self, overload + +def wrapper[**P](callback: Callable[P, object], *args: P.args, **kwargs: P.kwargs) -> None: ... + +class Factory: + @overload + def __new__(cls, value: str) -> Self: ... + @overload + def __new__(cls, value: int, flag: int) -> Self: ... + def __new__(cls, value: str | int, flag: int | None = None) -> Self: + return super().__new__(cls) + +wrapper(Factory, 1, "incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:13:21 + | +13 | wrapper(Factory, 1, "incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Function defined here + --> src/mdtest_snippet.py:9:9 + | +9 | def __new__(cls, value: int, flag: int) -> Self: ... + | ^^^^^^^ --------- Parameter declared here +``` + +## Functions wrapped by functools.partial + +`functools.partial` supplies the first argument before the callback is passed to the forwarding +function. An invalid forwarded argument should point to the next parameter on the original function. + +```py +from functools import partial +from typing import Callable + +def wrapper[**P](callback: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... +def callback(prefix: int, value: int) -> None: ... + +wrapper(partial(callback, 1), "incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:7:31 + | +7 | wrapper(partial(callback, 1), "incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Function defined here + --> src/mdtest_snippet.py:5:5 + | +5 | def callback(prefix: int, value: int) -> None: ... + | ^^^^^^^^ ---------- Parameter declared here +``` + +## Bound methods wrapped by functools.partial + +When `functools.partial` wraps a bound method, both `self` and the argument supplied by `partial` +come before the forwarded argument. + +```py +from functools import partial +from typing import Callable + +def wrapper[**P](callback: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... + +class Handler: + def callback(self, prefix: int, value: int) -> None: ... + +def run(handler: Handler) -> None: + wrapper(partial(handler.callback, 1), "incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:10:43 + | +10 | wrapper(partial(handler.callback, 1), "incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Method defined here + --> src/mdtest_snippet.py:7:9 + | +7 | def callback(self, prefix: int, value: int) -> None: ... + | ^^^^^^^^ ---------- Parameter declared here +``` diff --git a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md index edfee69d75..b7bc652d2f 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md @@ -271,8 +271,7 @@ MyList = TypeAliasType("MyList", list[T], type_params=(T,)) MyAlias5 = Callable[[MyList[T]], int] def _(c: MyAlias5[int]): - # TODO: should be (list[int], /) -> int - reveal_type(c) # revealed: (Unknown, /) -> int + reveal_type(c) # revealed: (MyList[int], /) -> int K = TypeVar("K") V = TypeVar("V") @@ -282,14 +281,12 @@ MyDict = TypeAliasType("MyDict", dict[K, V], type_params=(K, V)) MyAlias6 = Callable[[MyDict[K, V]], int] def _(c: MyAlias6[str, bytes]): - # TODO: should be (dict[str, bytes], /) -> int - reveal_type(c) # revealed: (Unknown, /) -> int + reveal_type(c) # revealed: (MyDict[str, bytes], /) -> int ListOrDict: TypeAlias = MyList[T] | dict[str, T] def _(x: ListOrDict[int]): - # TODO: should be list[int] | dict[str, int] - reveal_type(x) # revealed: Unknown | dict[str, int] + reveal_type(x) # revealed: list[int] | dict[str, int] MyAlias7: TypeAlias = Callable[Concatenate[T, ...], None] @@ -536,7 +533,6 @@ error[invalid-type-form]: `Unpack` is not allowed in type alias values | 14 | differently_bad: TypeAlias = Unpack[tuple[int, ...]] # snapshot: invalid-type-form | ^^^^^^^^^^^^^^^^^^^^^^^ - | 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 ``` diff --git a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md index 1537c77a57..eea9140984 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md @@ -305,7 +305,6 @@ error[unsupported-operator]: Unsupported `|` operation | | | | | Has type `` | Has type `Literal["int"]` - | info: A type alias scope is lazy but will be executed at runtime if the `__value__` property is accessed ``` @@ -463,16 +462,296 @@ def f(x: IntOrStr) -> None: ### Generic example +Manual aliases can be specialized in annotations and value positions, including when they are used +in `type[...]` or nested inside another alias. + ```py -from typing_extensions import TypeAliasType, TypeVar +from typing import Callable, Concatenate, Generic +from typing_extensions import ParamSpec, TypeAliasType, TypeVar, TypeVarTuple, Union, Unpack T = TypeVar("T") IntAndT = TypeAliasType("IntAndT", tuple[int, T], type_params=(T,)) def f(x: IntAndT[str]) -> None: - # TODO: This should be `tuple[int, str]` - reveal_type(x) # revealed: Unknown + reveal_type(x) # revealed: tuple[int, str] + +reveal_type(IntAndT[str]) # revealed: + +def generic_meta(value: type[IntAndT[str]]) -> None: + reveal_type(value) # revealed: type[tuple[int, str]] + +Nested = TypeAliasType("Nested", list[IntAndT[T]], type_params=(T,)) + +def nested(value: Nested[str]) -> None: + reveal_type(value) # revealed: list[IntAndT[str]] +``` + +Defaults apply to unspecialized aliases, and the order of `type_params` determines how type +arguments are mapped even if the parameters appear in a different order in the alias value. + +```py +U = TypeVar("U", default=str) + +ListOrSet = TypeAliasType("ListOrSet", Union[list[U], set[U]], type_params=(U,)) +MyDict = TypeAliasType("MyDict", dict[T, U], type_params=(T, U)) +Reordered = TypeAliasType("Reordered", tuple[U, T], type_params=(T, U)) + +def g( + list_or_set_of_int: ListOrSet[int], + list_or_set_of_str: ListOrSet, + dict_int_str: MyDict[int, str], + dict_unknown_str: MyDict, + reordered: Reordered[int, str], +) -> None: + reveal_type(list_or_set_of_int) # revealed: list[int] | set[int] + reveal_type(list_or_set_of_str) # revealed: list[str] | set[str] + reveal_type(dict_int_str) # revealed: dict[int, str] + reveal_type(dict_unknown_str) # revealed: dict[Unknown, str] + reveal_type(reordered) # revealed: tuple[str, int] +``` + +Constructor inference sees through the specialized `ModelAlias[T]`: passing `Model` infers `T` as +`Model` in `ViaAlias[T]`. + +```py +ModelAlias = TypeAliasType("ModelAlias", type[T], type_params=(T,)) + +class Model: ... + +class ViaAlias(Generic[T]): + def __init__(self, value: ModelAlias[T]) -> None: ... + +reveal_type(ViaAlias(Model)) # revealed: ViaAlias[Model] +``` + +`ParamSpec` parameters can be specialized alongside regular type variables and are preserved when a +callable alias is used as a decorator return type. + +```py +P = ParamSpec("P") +R = TypeVar("R") +WrappedMethod = TypeAliasType("WrappedMethod", Callable[Concatenate[T, P], R], type_params=(T, P, R)) + +def wrapped_method(value: WrappedMethod[int, P, str]) -> None: + reveal_type(value) # revealed: (int, /, *args: P@wrapped_method.args, **kwargs: P@wrapped_method.kwargs) -> str + +WrapsMethod = TypeAliasType("WrapsMethod", Callable[Concatenate[T, ...], R], type_params=(T, R)) + +def decorate(value: WrapsMethod[T, R], /) -> WrappedMethod[T, P, R]: + return value + +@decorate +def decorated(value: int) -> int: + return value + +reveal_type(decorated) # revealed: [**P'return](int, /, *args: P'return.args, **kwargs: P'return.kwargs) -> int +``` + +`TypeVarTuple` parameters accept multiple type arguments when specializing a variadic alias. + +```py +Ts = TypeVarTuple("Ts") +Variadic = TypeAliasType("Variadic", tuple[Unpack[Ts]], type_params=(Ts,)) + +def variadic(value: Variadic[int, str]) -> None: + reveal_type(value) # revealed: tuple[int, str] +``` + +### Recursive generic example + +```py +from typing import Callable +from typing_extensions import TypeAliasType, TypeVar, Union + +T = TypeVar("T") +Recursive = TypeAliasType("Recursive", Union[T, list["Recursive[T]"]], type_params=(T,)) +RecursiveCallable = Callable[[Recursive[T]], None] + +def recursive(value: Recursive[int]) -> None: + reveal_type(value) # revealed: int | list[Recursive[int]] + +def recursive_callable(value: RecursiveCallable[int]) -> None: + reveal_type(value) # revealed: (Recursive[int], /) -> None +``` + +### Generic specialization errors + +```py +from typing_extensions import TypeAliasType, TypeVar + +T = TypeVar("T") +BoundedT = TypeVar("BoundedT", bound=int) + +GenericAlias = TypeAliasType("GenericAlias", list[T], type_params=(T,)) +BoundedAlias = TypeAliasType("BoundedAlias", list[BoundedT], type_params=(BoundedT,)) +NonGenericAlias = TypeAliasType("NonGenericAlias", list[int]) +DefaultedT = TypeVar("DefaultedT", default=str) + +# error: [invalid-type-variable-default] "Type parameter `T` without a default cannot follow earlier parameter `DefaultedT` with a default" +InvalidOrder = TypeAliasType("InvalidOrder", tuple[DefaultedT, T], type_params=(DefaultedT, T)) + +# error: [invalid-type-arguments] "Too many type arguments: expected 1, got 2" +reveal_type(GenericAlias[int, str]) # revealed: + +# error: [invalid-type-arguments] "Type `str` is not assignable to upper bound `int` of type variable `BoundedT@BoundedAlias`" +reveal_type(BoundedAlias[str]) # revealed: + +# error: [not-subscriptable] "Cannot subscript non-generic type alias `NonGenericAlias`" +reveal_type(NonGenericAlias[int]) # revealed: Unknown + +# error: [not-subscriptable] "Cannot specialize non-generic type alias `NonGenericAlias`" +def non_generic(value: NonGenericAlias[int]) -> None: + reveal_type(value) # revealed: Unknown +``` + +### Invalid type parameters + +```py +from typing_extensions import TypeAliasType, TypeVar, TypeVarTuple, Union, Unpack + +T = TypeVar("T") +U = TypeVar("U") +Ts = TypeVarTuple("Ts") +Us = TypeVarTuple("Us") + +# error: [invalid-type-alias-type] "The `type_params` argument to `TypeAliasType` must be a tuple literal" +InvalidList = TypeAliasType("InvalidList", list[T], type_params=[T]) + +# error: [invalid-type-alias-type] "The `type_params` argument to `TypeAliasType` must be a tuple literal" +InvalidBare = TypeAliasType("InvalidBare", list[T], type_params=T) + +params = (T,) +# error: [invalid-type-alias-type] "The `type_params` argument to `TypeAliasType` must be a tuple literal" +InvalidTupleVariable = TypeAliasType("InvalidTupleVariable", list[T], type_params=params) + +# error: [invalid-type-alias-type] "Each `type_params` entry for `TypeAliasType` must be a type variable" +InvalidUnpack = TypeAliasType("InvalidUnpack", list[T], type_params=(*params,)) + +# error: [invalid-type-alias-type] "Each `type_params` entry for `TypeAliasType` must be a type variable" +InvalidNested = TypeAliasType("InvalidNested", list[T], type_params=(list[T],)) + +# error: [invalid-type-alias-type] "Each `type_params` entry for `TypeAliasType` must be a type variable" +InvalidMixed = TypeAliasType("InvalidMixed", list[T], type_params=(int, T)) + +# error: [invalid-type-alias-type] "Type parameter `U` used in the alias value must be included in `type_params`" +Missing = TypeAliasType("Missing", dict[T, U], type_params=(T,)) + +# error: [invalid-type-alias-type] "Type parameter `T` used in the alias value must be included in `type_params`" +MissingAll = TypeAliasType("MissingAll", list[T]) + +# error: [invalid-type-alias-type] "Type parameter `T` is duplicated in `type_params`" +Duplicate = TypeAliasType("Duplicate", tuple[T, U], type_params=(T, U, T)) + +MultipleTypeVarTuples = TypeAliasType( + "MultipleTypeVarTuples", + Union[tuple[Unpack[Ts]], tuple[Unpack[Us]]], + # error: [invalid-type-alias-type] "Only one `TypeVarTuple` parameter is allowed in `type_params`" + type_params=(Ts, Us), +) + +DefaultedT = TypeVar("DefaultedT", default=int) + +DefaultAfterTypeVarTuple = TypeAliasType( + "DefaultAfterTypeVarTuple", + tuple[Unpack[Ts], DefaultedT], + # error: [invalid-type-variable-default] "Type parameter `DefaultedT` with a default follows TypeVarTuple `Ts`" + type_params=(Ts, DefaultedT), +) + +InvalidOrderAndEntries = TypeAliasType( + "InvalidOrderAndEntries", + tuple[DefaultedT, T], + # error: [invalid-type-variable-default] "Type parameter `T` without a default cannot follow earlier parameter `DefaultedT` with a default" + # error: [invalid-type-alias-type] "Type parameter `T` is duplicated in `type_params`" + # error: [invalid-type-alias-type] "Each `type_params` entry for `TypeAliasType` must be a type variable" + type_params=(DefaultedT, T, T, "V"), +) +``` + +### Scoped type parameters + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Callable +from typing_extensions import TypeAliasType, TypeVar + +LegacyT = TypeVar("LegacyT") + +def pep695_outer[T]() -> None: + # error: [invalid-type-alias-type] "Type parameter `T` is bound in an outer scope and cannot be used in `type_params`" + Pep695Alias = TypeAliasType("Pep695Alias", list[T], type_params=(T,)) + # error: [not-subscriptable] "Cannot specialize non-generic type alias `Pep695Alias`" + def check(value: Pep695Alias[int]) -> None: ... + +def legacy_outer(value: LegacyT) -> None: + # error: [invalid-type-alias-type] "Type parameter `LegacyT` is bound in an outer scope and cannot be used in `type_params`" + LegacyAlias = TypeAliasType("LegacyAlias", list[LegacyT], type_params=(LegacyT,)) + # error: [not-subscriptable] "Cannot specialize non-generic type alias `LegacyAlias`" + def check(value: LegacyAlias[int]) -> None: ... + +class Pep695Outer[T]: + # error: [invalid-type-alias-type] "Type parameter `T` is bound in an outer scope and cannot be used in `type_params`" + ClassAlias = TypeAliasType("ClassAlias", list[T], type_params=(T,)) + +def paramspec_outer[**P]() -> None: + # error: [invalid-type-alias-type] "Type parameter `P` is bound in an outer scope and cannot be used in `type_params`" + ParamSpecAlias = TypeAliasType("ParamSpecAlias", Callable[P, int], type_params=(P,)) + +def variadic_outer[*Ts]() -> None: + # error: [invalid-type-alias-type] "Type parameter `Ts` is bound in an outer scope and cannot be used in `type_params`" + VariadicAlias = TypeAliasType("VariadicAlias", tuple[*Ts], type_params=(Ts,)) +``` + +### Generic alias from `typing` + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import TypeAliasType, TypeVar + +K = TypeVar("K") +V = TypeVar("V") +MyDict = TypeAliasType("MyDict", dict[K, V], type_params=(K, V)) + +def generic_from_typing(value: MyDict[str, int]) -> None: + reveal_type(value) # revealed: dict[str, int] +``` + +### PEP 695 aliases in `Callable` + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Callable, TypeVar + +T = TypeVar("T") + +type Pep695List[A] = list[A] +Pep695ConcreteCallable = Callable[[Pep695List[int]], None] +Pep695GenericCallable = Callable[[Pep695List[T]], None] + +type Recursive[A] = A | list[Recursive[A]] +RecursiveCallable = Callable[[Recursive[int]], None] + +def _( + concrete: Pep695ConcreteCallable, + generic: Pep695GenericCallable[str], + recursive: RecursiveCallable, +) -> None: + reveal_type(concrete) # revealed: (Pep695List[int], /) -> None + reveal_type(generic) # revealed: (Pep695List[str], /) -> None + reveal_type(recursive) # revealed: (Recursive[int], /) -> None ``` ### Generic value binds type variables to alias definition diff --git a/crates/ty_python_semantic/resources/mdtest/promotion.md b/crates/ty_python_semantic/resources/mdtest/promotion.md index f219500cc9..716ea14885 100644 --- a/crates/ty_python_semantic/resources/mdtest/promotion.md +++ b/crates/ty_python_semantic/resources/mdtest/promotion.md @@ -515,8 +515,8 @@ reveal_type(x4) # revealed: list[LiteralString] x5: list[list[Literal[1]]] = [[1]] reveal_type(x5) # revealed: list[list[Literal[1]]] -x6: dict[list[Literal[1]], list[Literal[Color.RED]]] = {[1]: [Color.RED, Color.RED]} -reveal_type(x6) # revealed: dict[list[Literal[1]], list[Literal[Color.RED]]] +x6: dict[tuple[Literal[1]], list[Literal[Color.RED]]] = {(1,): [Color.RED, Color.RED]} +reveal_type(x6) # revealed: dict[tuple[Literal[1]], list[Literal[Color.RED]]] x7: X[Literal[1]] = X([1]) reveal_type(x7) # revealed: X[Literal[1]] @@ -524,8 +524,8 @@ reveal_type(x7) # revealed: X[Literal[1]] x8: X[int] = X([1]) reveal_type(x8) # revealed: X[int] -x9: dict[list[X[Literal[1]]], set[Literal[b"a"]]] = {[X([1])]: {b"a"}} -reveal_type(x9) # revealed: dict[list[X[Literal[1]]], set[Literal[b"a"]]] +x9: dict[tuple[X[Literal[1]]], set[Literal[b"a"]]] = {(X([1]),): {b"a"}} +reveal_type(x9) # revealed: dict[tuple[X[Literal[1]]], set[Literal[b"a"]]] x10: list[Literal[1, 2, 3]] = [1, 2, 3] reveal_type(x10) # revealed: list[Literal[1, 2, 3]] diff --git a/crates/ty_python_semantic/resources/mdtest/properties.md b/crates/ty_python_semantic/resources/mdtest/properties.md index 50b54a483b..03ca9d17fc 100644 --- a/crates/ty_python_semantic/resources/mdtest/properties.md +++ b/crates/ty_python_semantic/resources/mdtest/properties.md @@ -243,11 +243,33 @@ class C: c = C() c.attr = 1 -# TODO: An error should be emitted here. -# See https://github.com/astral-sh/ruff/issues/16298 for more details. +# error: [call-non-callable] "property has no getter" +C.attr.__get__(c, C) +# error: [call-non-callable] "property has no getter" +type(C.attr).__get__(C.attr, c, C) + +# error: [invalid-attribute-access] "Cannot read property `attr` on object of type `C` because it has no getter" reveal_type(c.attr) # revealed: Never ``` +### Attempting to call a getter with an incompatible instance + +Explicit bound and unbound `property.__get__` calls preserve the getter's receiver error and return +type. For the unbound call, the reported argument is the instance rather than the property itself. + +```py +class C: + @property + def attr(self) -> int: + return 1 + +# error: [invalid-argument-type] "Argument to function `C.attr` is incorrect: Expected `C`" +reveal_type(C.attr.__get__("wrong", C)) # revealed: int + +# error: [invalid-argument-type] "Argument to function `C.attr` is incorrect: Expected `C`" +reveal_type(property.__get__(C.attr, "wrong", C)) # revealed: int +``` + ### Non-returning setter ```py @@ -313,7 +335,6 @@ error[invalid-assignment]: Cannot delete read-only property `attr` on object of | 3 | def attr(self) -> int: | ---- Property `C.attr` defined here with no deleter - | ``` ## Limitations @@ -449,19 +470,32 @@ This attribute access desugars to ```py type(attr_property).__set__(attr_property, c, "a") -# error: [call-non-callable] "Call of wrapper descriptor `property.__set__` failed: calling the setter failed" +# snapshot: invalid-argument-type type(attr_property).__set__(attr_property, c, 1) ``` +```snapshot +error[invalid-argument-type]: Argument to function `C.attr` is incorrect + --> src/mdtest_snippet.py:31:47 + | +31 | type(attr_property).__set__(attr_property, c, 1) + | ^ Expected `str`, found `Literal[1]` +info: Function defined here + --> src/mdtest_snippet.py:10:9 + | +10 | def attr(self, value: str) -> None: + | ^^^^ ---------- Parameter declared here +``` + which is also equivalent to the following expressions: ```py attr_property.__set__(c, "a") -# error: [call-non-callable] +# error: [invalid-argument-type] attr_property.__set__(c, 1) C.attr.__set__(c, "a") -# error: [call-non-callable] +# error: [invalid-argument-type] C.attr.__set__(c, 1) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index 2c83808541..aab0e5cb8e 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -776,12 +776,12 @@ static_assert(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)) +static_assert(not is_subtype_of(FooWithZero, HasClassVarX)) +static_assert(not 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] +# An instance declaration does not become a class variable without an explicit qualifier. +static_assert(not is_subtype_of(Foo, HasClassVarX)) +static_assert(not is_assignable_to(Foo, HasClassVarX)) static_assert(not is_subtype_of(Qux, HasClassVarX)) static_assert(not is_assignable_to(Qux, HasClassVarX)) @@ -1066,13 +1066,11 @@ warning[ambiguous-protocol-member]: Cannot assign to an undeclared attribute in | 326 | self.augmented += 1 # snapshot: ambiguous-protocol-member | ^^^^^^^^^^^^^^ `augmented` is not declared as a protocol member - | info: Assigning to an undeclared attribute in a protocol method leads to an ambiguous interface --> src/mdtest_snippet.py:318:7 | 318 | class AssignmentForms(Protocol): | ^^^^^^^^^^^^^^^^^^^^^^^^^ `AssignmentForms` declared as a protocol here - | info: No declarations found for `augmented` in the body of `AssignmentForms` or any of its superclasses ``` @@ -1244,9 +1242,11 @@ static_assert(not is_assignable_to(HasX, Foo)) static_assert(not is_subtype_of(HasX, Foo)) ``` -Since `object` defines a `__hash__` method, this means that the standard-library `Hashable` protocol -is currently understood by ty as being equivalent to `object`, much like `SupportsStr` and -`UniversalSet` above: +Although `object` defines a `__hash__` method, its subclasses can disable hashing by replacing that +method with `None`. The standard-library `Hashable` protocol is therefore not equivalent to +`object`, unlike `SupportsStr` and `UniversalSet` above. Modeling this distinction violates normal +subtyping rules and is therefore unsound, but it is widely relied on throughout the Python +ecosystem: ```py from typing import Hashable, Protocol @@ -1254,9 +1254,9 @@ from typing import Hashable, Protocol class SupportsHash(Protocol): def __hash__(self) -> int: ... -static_assert(is_equivalent_to(object, Hashable)) +static_assert(not is_equivalent_to(object, Hashable)) static_assert(is_assignable_to(object, Hashable)) -static_assert(is_subtype_of(object, Hashable)) +static_assert(not is_subtype_of(object, Hashable)) def check_object_or_hashable(x: object | Hashable): reveal_type(x) # revealed: object @@ -1268,14 +1268,13 @@ def check_hashable_or_supports_hash(x: Hashable | SupportsHash): reveal_type(x) # revealed: Hashable def check_hashable_or_universal(x: Hashable | UniversalSet): - reveal_type(x) # revealed: Hashable + reveal_type(x) # revealed: UniversalSet ``` -This means that any type considered assignable to `object` (which is all types) is considered by ty -to be assignable to `Hashable`. However, ty preserves a non-final nominal type in a union with -`Hashable` instead of discarding it as redundant. A non-final class can have unhashable subclasses, -so keeping the corresponding union element retains the annotation's more precise description of -those subclasses. For example, `list[str]` is unhashable but is a subtype of `Sequence[Hashable]`: +ty checks whether a type actually provides a callable `__hash__` method instead of assuming all +subtypes of `object` are hashable. It also preserves a non-final nominal type in a union with +`Hashable` instead of discarding it as redundant, since a subclass can disable hashing. For example, +`list[str]` is unhashable but is a subtype of `Sequence[Hashable]`: ```py from collections.abc import Hashable as AbcHashable @@ -1299,8 +1298,9 @@ static_assert(is_subtype_of(list[Hashable], Sequence[Hashable])) static_assert(is_subtype_of(list[str], Sequence[Hashable])) ``` -The additional union element is still simplified if it is a final class, because instances of the -class cannot override their inherited hashability: +The additional union element is still simplified if it is a final hashable class, because instances +of that class cannot override their inherited hashability. Final classes with `__hash__ = None` must +remain in the union: ```py from dataclasses import dataclass @@ -1332,9 +1332,8 @@ class UnhashableDataclass: ... def check_hashable_or_final(x: Hashable | C): reveal_type(x) # revealed: Hashable -# TODO: Preserve final classes that are known to be unhashable. def check_hashable_or_unhashable_final(x: Hashable | Unhashable): - reveal_type(x) # revealed: Hashable + reveal_type(x) # revealed: Hashable | Unhashable def check_hashable_or_eq_only(x: Hashable | EqOnly): reveal_type(x) # revealed: Hashable @@ -1343,10 +1342,10 @@ def check_hashable_or_eq_only_child(x: Hashable | EqOnlyChild): reveal_type(x) # revealed: Hashable def check_hashable_or_unhashable_dataclass(x: Hashable | UnhashableDataclass): - reveal_type(x) # revealed: Hashable + reveal_type(x) # revealed: Hashable | UnhashableDataclass ``` -The special case is currently limited to nominal instance types: +Type variables and protocols that can contain unhashable values also remain in the union: ```py from typing import TypeVar, TypedDict @@ -1356,24 +1355,24 @@ T = TypeVar("T") class Payload(TypedDict): value: int -# TODO: Preserve non-nominal types that can contain unhashable values. def check_hashable_or_typevar(x: Hashable | T): - reveal_type(x) # revealed: Hashable + reveal_type(x) # revealed: Hashable | T@check_hashable_or_typevar +# TODO: Preserve TypedDict types, which are unhashable at runtime. def check_hashable_or_typed_dict(x: Hashable | Payload): reveal_type(x) # revealed: Hashable def check_hashable_or_protocol(x: Hashable | HasX): - reveal_type(x) # revealed: Hashable + reveal_type(x) # revealed: Hashable | HasX ``` -We do not detect errors in cases like the following, which are flagged by other type checkers: +A list does not satisfy `Hashable`, even though it inherits from `object`: ```py def needs_something_hashable(x: Hashable): hash(x) -needs_something_hashable([]) +needs_something_hashable([]) # error: [invalid-argument-type] ``` ## Diagnostics for protocols with invalid attribute members @@ -1437,6 +1436,148 @@ class C(A, Protocol): x = 42 # fine, due to declaration in the base class ``` +## Hashable protocol assignability + +An explicitly disabled `__hash__` method makes an object incompatible with the standard-library +`Hashable` protocol, even though `object` itself defines a valid `__hash__` method. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from collections.abc import Hashable +from typing import ClassVar, Hashable as TypingHashable, final +from ty_extensions import static_assert +from ty_extensions._internal import is_assignable_to, is_subtype_of + +def accepts_hashable(value: Hashable) -> None: ... + +class AnnotationOnly: + __hash__: ClassVar[None] + +class ExplicitNone: + __hash__: ClassVar[None] = None + +accepts_hashable(AnnotationOnly()) # error: [invalid-argument-type] +accepts_hashable(ExplicitNone()) # error: [invalid-argument-type] +``` + +Disabling `__hash__` also applies to subclasses, and declaring an unhashable class final does not +make it hashable. + +```py +class InheritedNone(ExplicitNone): ... + +@final +class FinalExplicitNone: + __hash__: ClassVar[None] = None + +accepts_hashable(InheritedNone()) # error: [invalid-argument-type] +accepts_hashable(FinalExplicitNone()) # error: [invalid-argument-type] +``` + +The standard-library stubs mark mutable built-in containers as unhashable. + +```py +accepts_hashable([]) # error: [invalid-argument-type] +accepts_hashable({}) # error: [invalid-argument-type] +accepts_hashable(set()) # error: [invalid-argument-type] +``` + +The `typing` alias imposes the same requirements as `collections.abc.Hashable`. + +```py +def accepts_typing_hashable(value: TypingHashable) -> None: ... + +accepts_typing_hashable(ExplicitNone()) # error: [invalid-argument-type] +accepts_typing_hashable([]) # error: [invalid-argument-type] +``` + +Explicitly hashable classes and immutable built-in values remain valid. + +```py +class ExplicitHash: + def __hash__(self) -> int: + return 1 + +accepts_hashable(ExplicitHash()) +accepts_hashable(1) +accepts_hashable("value") +accepts_hashable(("value",)) +``` + +Concrete `object()` instances are hashable and commonly used as sentinel values, even though the +`object` type can also include unhashable subclasses. + +```py +SENTINEL = object() + +def accepts_hashable_default(value: Hashable = SENTINEL) -> None: ... + +accepts_hashable(object()) +accepts_hashable(SENTINEL) +``` + +The same distinction applies to both assignability and subtyping checks. + +```py +static_assert(not is_assignable_to(ExplicitNone, Hashable)) +static_assert(not is_subtype_of(ExplicitNone, Hashable)) +static_assert(not is_assignable_to(list[str], Hashable)) +static_assert(not is_subtype_of(list[str], Hashable)) +``` + +## User-defined hash protocols + +A user-defined protocol that explicitly requires a callable `__hash__` method imposes the same +requirement as the standard-library `Hashable` protocol. + +```py +from typing import ClassVar, Protocol + +class SupportsHash(Protocol): + def __hash__(self) -> int: ... + +class ExplicitNone: + __hash__: ClassVar[None] = None + +class ExplicitHash: + def __hash__(self) -> int: + return 1 + +def accepts_hashable(value: SupportsHash) -> None: ... + +accepts_hashable(ExplicitNone()) # error: [invalid-argument-type] +accepts_hashable([]) # error: [invalid-argument-type] +accepts_hashable(ExplicitHash()) +accepts_hashable(object()) +``` + +## Hashability of dataclasses + +Mutable dataclasses disable hashing by default, while frozen dataclasses synthesize a callable +`__hash__` method. + +```py +from collections.abc import Hashable +from dataclasses import dataclass + +@dataclass +class Mutable: + value: int + +@dataclass(frozen=True) +class Frozen: + value: int + +def accepts_hashable(value: Hashable) -> None: ... + +accepts_hashable(Mutable(1)) # error: [invalid-argument-type] +accepts_hashable(Frozen(1)) +``` + ## Equivalence of protocols ```toml @@ -1909,14 +2050,16 @@ static_assert(is_assignable_to(UsesMeta, HasX)) If a protocol `ClassVarX` has a `ClassVar` attribute member `x` with type `int`, this indicates that the non-callable attribute must be readable with the same type through both an inhabitant of -`ClassVarX` and the type of that inhabitant: +`ClassVarX` and the type of that inhabitant. An implementing class must declare the member as a +`ClassVar`; an instance attribute does not satisfy the requirement merely because it has a default +value in the class body: `classvars.py`: ```py -from typing import Any, ClassVar, Protocol -from ty_extensions import static_assert -from ty_extensions._internal import is_subtype_of, is_assignable_to +from typing import Any, ClassVar, Protocol, final +from ty_extensions import Intersection, static_assert +from ty_extensions._internal import TypeOf, is_assignable_to, is_disjoint_from, is_subtype_of class ClassVarXProto(Protocol): x: ClassVar[int] @@ -1929,9 +2072,14 @@ def f(obj: 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] +static_assert(not is_assignable_to(InstanceAttrX, ClassVarXProto)) +static_assert(not is_subtype_of(InstanceAttrX, ClassVarXProto)) + +class InstanceAttrXWithDefault: + x: int = 42 + +static_assert(not is_assignable_to(InstanceAttrXWithDefault, ClassVarXProto)) +static_assert(not is_subtype_of(InstanceAttrXWithDefault, ClassVarXProto)) class PropertyX: @property @@ -1947,6 +2095,14 @@ class ClassVarX: static_assert(is_assignable_to(ClassVarX, ClassVarXProto)) static_assert(is_subtype_of(ClassVarX, ClassVarXProto)) +class InheritedClassVarX(ClassVarX): + x = 1 + +static_assert(is_assignable_to(InheritedClassVarX, ClassVarXProto)) +static_assert(is_subtype_of(InheritedClassVarX, ClassVarXProto)) +static_assert(is_assignable_to(TypeOf[InheritedClassVarX], type[ClassVarXProto])) +static_assert(is_subtype_of(TypeOf[InheritedClassVarX], type[ClassVarXProto])) + class XMeta(type): def x(cls) -> str: return "" @@ -1976,6 +2132,51 @@ class NotHashable: static_assert(is_assignable_to(NotHashable, NotHashableProto)) static_assert(is_subtype_of(NotHashable, NotHashableProto)) + +class Descriptor: + def __get__(self, instance: object, owner: type) -> "Descriptor": + return self + + def __set__(self, instance: object, value: "Descriptor") -> None: ... + +class HasClassDescriptor(Protocol): + descriptor: ClassVar[Descriptor] + +class DescriptorImplementation: + descriptor: ClassVar[Descriptor] = Descriptor() + +static_assert(is_assignable_to(DescriptorImplementation, HasClassDescriptor)) +static_assert(is_subtype_of(DescriptorImplementation, HasClassDescriptor)) + +@final +class FinalInstanceAttrX: + x: int = 42 + +@final +class FinalClassVarX: + x: ClassVar[int] = 42 + +static_assert(is_disjoint_from(FinalInstanceAttrX, ClassVarXProto)) +static_assert(not is_disjoint_from(InstanceAttrXWithDefault, ClassVarXProto)) +static_assert(not is_disjoint_from(FinalClassVarX, ClassVarXProto)) + +def impossible(value: Intersection[FinalInstanceAttrX, ClassVarXProto]) -> None: + reveal_type(value) # revealed: Never + +implementation: ClassVarXProto = InstanceAttrX() # snapshot: invalid-assignment +``` + +```snapshot +error[invalid-assignment]: Object of type `InstanceAttrX` is not assignable to `ClassVarXProto` + --> src/classvars.py:107:34 + | +107 | implementation: ClassVarXProto = InstanceAttrX() # snapshot: invalid-assignment + | -------------- ^^^^^^^^^^^^^^^ Incompatible value of type `InstanceAttrX` + | | + | Declared type +info: type `InstanceAttrX` is not assignable to protocol `ClassVarXProto` +info: └── protocol member `x` is incompatible +info: └── protocol member `x` is an instance variable on type `InstanceAttrX`, but a class variable is required ``` This is mentioned by the @@ -2017,7 +2218,7 @@ read/write property, a `Final` attribute, or a `ClassVar` attribute: ```py from typing import ClassVar, Final, Protocol, final from ty_extensions import static_assert -from ty_extensions._internal import is_subtype_of, is_assignable_to, is_disjoint_from +from ty_extensions._internal import TypeOf, is_subtype_of, is_assignable_to, is_disjoint_from class HasXProperty(Protocol): @property @@ -2088,6 +2289,18 @@ static_assert(not is_assignable_to(HasStrXProperty, HasXProperty)) static_assert(not is_assignable_to(HasXProperty, HasStrXProperty)) ``` +Accessing an instance property on the class object exposes the property descriptor, not the value +returned by its getter. A class object with only an instance property is therefore disjoint from the +protocol: + +```py +static_assert(not is_subtype_of(TypeOf[XReadProperty], HasXProperty)) +static_assert(not is_assignable_to(TypeOf[XReadProperty], HasXProperty)) +static_assert(is_disjoint_from(TypeOf[XReadProperty], HasXProperty)) + +x_class: HasXProperty = XReadProperty # error: [invalid-assignment] +``` + 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`. @@ -2758,9 +2971,8 @@ has_name: HasCachedName = WithCachedName() ### Generic descriptor result types -Applying a generic descriptor decorator to a generic protocol method currently loses the protocol's -type variable and produces `cached_property[Unknown]`. The protocol must preserve that descriptor -type instead of reducing it to a bare `Unknown`, which would allow an incompatible implementation. +Applying a generic descriptor decorator to a generic protocol method must preserve the protocol's +type variable and expose the specialized descriptor's readable and writable member types. ```py from functools import cached_property @@ -2781,10 +2993,7 @@ class StrValue: static_assert(not is_assignable_to(StrValue, HasValue[int])) -# TODO: The read type should be `int` once decorator calls preserve enclosing type variables. The -# `cached_property` call leaves its own type variable unsolved, and an unsolved type variable is -# solved to `Never`. -# revealed: {"value": PropertyMember { read: `Never`, write: `Never` }} +# revealed: {"value": PropertyMember { read: `int`, write: `int` }} reveal_protocol_interface(HasValue[int]) ``` @@ -3774,6 +3983,65 @@ static_assert(is_subtype_of(Text, ConsoleRenderable)) static_assert(is_assignable_to(Text, ConsoleRenderable)) ``` +## Recursive protocol receiver binding + +A classmethod on a generic protocol can cause receiver binding for another method to depend on +itself. The cached receiver-binding query must reach a fixed point and report the ordinary +return-type error instead of panicking. + +```toml +[environment] +python-version = "3.11" +``` + +```py +from __future__ import annotations + +from datetime import datetime, timedelta, tzinfo +from typing import ClassVar, Optional, Protocol, TypeVar + +T = TypeVar("T", bound=Optional[tzinfo], covariant=True) + +class DateTime(Protocol[T]): + resolution: ClassVar[timedelta] + + def __sub__(self: DateTime[tzinfo], other: DateTime[tzinfo]) -> timedelta: ... + @classmethod + def now(cls, tz: Optional[tzinfo] = None) -> DateTime[Optional[tzinfo]]: + return datetime.now(tz) # error: [invalid-return-type] +``` + +## Recursive protocol receiver binding with an incompatible override + +An incompatible override of a covariant generic protocol method can recursively compare the +protocol's explicitly annotated receiver with the implementing class. The receiver-binding and +assignability queries must converge and report the incompatible override instead of panicking. + +```toml +[environment] +python-version = "3.11" +``` + +```py +from typing import Generic, Protocol, TypeVar + +T = TypeVar("T") +T_co = TypeVar("T_co", covariant=True) + +class Result(Generic[T_co]): ... + +class SupportsMethod(Protocol[T_co]): + def method(self: "SupportsMethod[T]") -> Result[T]: ... + +class Compatible(SupportsMethod[T_co]): + def method(self: "Compatible[T]") -> Result[T]: + raise NotImplementedError + +class Incompatible(SupportsMethod[T_co]): + def method(self: "Incompatible[T]", value: int) -> Result[T]: # error: [invalid-method-override] + raise NotImplementedError +``` + ## Subtyping of protocols with generic method members Protocol method members can be generic. They can have generic contexts scoped to the class: @@ -4236,13 +4504,12 @@ iterable: Iterable[int] = DirectIterable # snapshot ```snapshot error[invalid-assignment]: Object of type `` is not assignable to `Iterable[int]` - --> src/mdtest_snippet.py:20:11 + --> src/mdtest_snippet.py:20:27 | 20 | iterable: Iterable[int] = DirectIterable # snapshot | ------------- ^^^^^^^^^^^^^^ Incompatible value of type `` | | | Declared type - | info: type `` is not assignable to protocol `Iterable[int]` info: └── protocol member `__iter__` is not defined on type `` info: └── special methods must be defined on the meta-type when matching a protocol @@ -4267,6 +4534,75 @@ class Custom: static_assert(is_assignable_to(TypeOf[Custom], CustomProtocol)) ``` +## Class objects with explicitly typed special-method receivers + +A special method defined on a metaclass receives the class object, not an instance of that class. An +explicitly annotated metaclass receiver must therefore be checked against the class object when +matching a collection protocol. Special-method lookup must also ignore conflicting methods defined +on the class itself. + +```py +from collections.abc import Collection, Container, Iterable, Iterator, Reversible +from typing import Any, Protocol +from ty_extensions import static_assert +from ty_extensions._internal import TypeOf, is_assignable_to, is_subtype_of + +class Membership(Protocol): + def __contains__(self, value: int, /) -> bool: ... + +class CollectionMeta(type): + def __contains__(self: type[Any], value: object, /) -> bool: + return True + + def __iter__(self: type[Any]) -> Iterator[int]: + return iter((1,)) + + def __reversed__(self: type[Any]) -> Iterator[int]: + return iter((1,)) + + def __len__(self: type[Any]) -> int: + return 1 + +class ClassCollection(metaclass=CollectionMeta): + def __contains__(self, value: str, /) -> bool: + return True + + def __iter__(self) -> Iterator[str]: + return iter(("member",)) + + def __reversed__(self) -> Iterator[str]: + return iter(("member",)) + +static_assert(is_assignable_to(TypeOf[ClassCollection], Membership)) +static_assert(is_assignable_to(TypeOf[ClassCollection], Container[int])) +static_assert(is_assignable_to(TypeOf[ClassCollection], Container[str])) +static_assert(is_subtype_of(TypeOf[ClassCollection], Container[int])) +static_assert(is_assignable_to(TypeOf[ClassCollection], Iterable[int])) +static_assert(is_assignable_to(TypeOf[ClassCollection], Reversible[int])) +static_assert(is_assignable_to(TypeOf[ClassCollection], Collection[int])) +``` + +The explicit receiver must not hide an incompatible membership parameter or return type. + +```py +class StringMembershipMeta(type): + def __contains__(self: type[Any], value: str, /) -> bool: + return True + +class StringMembership(metaclass=StringMembershipMeta): + pass + +class NonBooleanMembershipMeta(type): + def __contains__(self: type[Any], value: object, /) -> int: + return 1 + +class NonBooleanMembership(metaclass=NonBooleanMembershipMeta): + pass + +static_assert(not is_assignable_to(TypeOf[StringMembership], Container[int])) +static_assert(not is_assignable_to(TypeOf[NonBooleanMembership], Container[int])) +``` + ## Subtyping of protocols with `@classmethod` or `@staticmethod` members The typing spec states that protocols may have `@classmethod` or `@staticmethod` method members. @@ -4610,6 +4946,53 @@ static_assert(not is_assignable_to(MethodPSuper, MethodPUnrelated)) static_assert(not is_assignable_to(MethodPSuper, MethodPSub)) ``` +## Object members in protocol-to-protocol comparisons + +A protocol inherits ordinary `object` members even when they are not part of its declared interface. +Those inherited members can satisfy compatible requirements on another protocol. + +```py +from collections.abc import Hashable, Iterator +from typing import Literal, Protocol +from ty_extensions import static_assert +from ty_extensions._internal import is_assignable_to, is_subtype_of + +class HasValue(Protocol): + value: int + +class HasValueAndRepr(Protocol): + value: int + + def __repr__(self) -> str: ... + +static_assert(is_assignable_to(HasValue, HasValueAndRepr)) +static_assert(is_subtype_of(HasValue, HasValueAndRepr)) +``` + +An inherited method must still have a compatible signature. + +```py +class HasValueAndPreciseRepr(Protocol): + value: int + + def __repr__(self) -> Literal["precise"]: ... + +static_assert(not is_assignable_to(HasValue, HasValueAndPreciseRepr)) +``` + +Unlike other inherited `object` methods, `__hash__` can be disabled by a subclass. Protocols must +explicitly require a callable `__hash__` before they can satisfy a hashability requirement. + +```py +class HasValueAndHash(Protocol): + value: int + + def __hash__(self) -> int: ... + +static_assert(not is_assignable_to(HasValue, HasValueAndHash)) +static_assert(not is_assignable_to(Iterator[int], Hashable)) +``` + ## Subtyping between protocols with method members and protocols with non-method members A protocol with a method member can be considered a subtype of a protocol with a read-only @@ -5164,7 +5547,7 @@ def _(x: Foo): pass ``` -## Protocols are never singleton types, and are never single-valued types +## Protocols are never singleton types It *might* be possible to have a singleton protocol-instance type...? @@ -5174,14 +5557,13 @@ worth it. Such cases should anyway be exceedingly rare and/or contrived. ```py from typing import Protocol, Callable -from ty_extensions._internal import is_singleton, is_single_valued +from ty_extensions._internal import is_singleton 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` @@ -5594,7 +5976,7 @@ python-version = "3.12" ```py from typing import Protocol, cast -from ty_extensions import Unknown +from ty_extensions._internal import Unknown class UnknownMethod[T](Protocol): def method(self) -> Unknown: ... @@ -5609,7 +5991,7 @@ checked. ```py from typing import Protocol, cast -from ty_extensions import Unknown +from ty_extensions._internal import Unknown class IntProperty[T](Protocol): @property @@ -5632,7 +6014,7 @@ has been replaced by `int`, so the cast is redundant. ```py from typing import Protocol, TypeVar, cast -from ty_extensions import Unknown +from ty_extensions._internal import Unknown T = TypeVar("T", bound=Unknown) @@ -5685,7 +6067,7 @@ example, descriptor overload resolution exposes `Unknown` only through the neste ```py from typing import Protocol, cast, overload -from ty_extensions import Unknown +from ty_extensions._internal import Unknown class Descriptor: @overload @@ -5768,7 +6150,7 @@ def f(c: C[int]) -> None: # The key thing is that we don't stack overflow while checking this. # The cycle detection assumes compatibility when it detects potential # infinite recursion between protocol specializations. - takes_c(c) + takes_c(c) # error: [invalid-argument-type] class Left[T](Protocol): @property @@ -6472,7 +6854,7 @@ class B1(A1[T3], Protocol[T3]): ... class B2(A2[T4], Protocol[T4]): ... # TODO should just be `B2[Any]` -reveal_type(T3.__bound__) # revealed: B2[Any] | @Todo(specialized non-generic class) +reveal_type(T3.__bound__) # revealed: B2[Any] | Unknown # TODO error: [invalid-type-arguments] def f(x: B1[int]): diff --git a/crates/ty_python_semantic/resources/mdtest/regression/3525_character_split_notebook.md b/crates/ty_python_semantic/resources/mdtest/regression/3525_character_split_notebook.md index b7afa71b24..e3909af220 100644 --- a/crates/ty_python_semantic/resources/mdtest/regression/3525_character_split_notebook.md +++ b/crates/ty_python_semantic/resources/mdtest/regression/3525_character_split_notebook.md @@ -36,7 +36,6 @@ warning[unused-ignore-comment]: Unused `ty: ignore` directive | 2 | x = 1 # ty: ignore[unresolved-reference] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove the unused suppression comment ::: cell 2 | diff --git a/crates/ty_python_semantic/resources/mdtest/regression/3593_function_known_decorators_cycle.md b/crates/ty_python_semantic/resources/mdtest/regression/3593_function_known_decorators_cycle.md index 06e50b3ba6..6501a7ed5e 100644 --- a/crates/ty_python_semantic/resources/mdtest/regression/3593_function_known_decorators_cycle.md +++ b/crates/ty_python_semantic/resources/mdtest/regression/3593_function_known_decorators_cycle.md @@ -13,7 +13,9 @@ from typing import Self, overload, reveal_type class C: a: D +# error: [invalid-attribute-access] C.a +# error: [invalid-attribute-access] reveal_type(C().a) # revealed: D class D: diff --git a/crates/ty_python_semantic/resources/mdtest/regression/constraint_set_ordering.md b/crates/ty_python_semantic/resources/mdtest/regression/constraint_set_ordering.md index 1b402f6a1d..1ae6e2a0ef 100644 --- a/crates/ty_python_semantic/resources/mdtest/regression/constraint_set_ordering.md +++ b/crates/ty_python_semantic/resources/mdtest/regression/constraint_set_ordering.md @@ -23,6 +23,25 @@ the `wobbling-ty-constraint-order` agent skill to automate this process. python-version = "3.13" ``` +## Constraint absorption is independent of source order + +```py +from ty_extensions._internal import ConstraintSet + +def absorption[T]() -> None: + scalar = ConstraintSet.lower_bound(str, T) + tuple_ = ConstraintSet.lower_bound(tuple[str, ...], T) + + # revealed: tuple[Solution[T=str]] + reveal_type((scalar & (scalar | tuple_)).solutions_for(T, inferable=tuple[T])) + # revealed: tuple[Solution[T=str]] + reveal_type(((scalar | tuple_) & scalar).solutions_for(T, inferable=tuple[T])) + + # A genuine alternative still produces both solutions; absorption does not prefer one match. + # revealed: tuple[Solution[T=str], Solution[T=tuple[str, ...]]] + reveal_type((scalar | tuple_).solutions_for(T, inferable=tuple[T])) +``` + ## Solution binding order follows constraint source order The order of bindings within a path must follow the first constraint that introduced each typevar. @@ -34,21 +53,31 @@ from ty_extensions._internal import ConstraintSet def bindings_tuv[T, U, V]() -> None: # (T = int) ∧ (U = str) ∧ (V = bytes) - constraints = ConstraintSet.range(int, T, int) & ConstraintSet.range(str, U, str) & ConstraintSet.range(bytes, V, bytes) + constraints = ConstraintSet.equality(T, int) & ConstraintSet.equality(U, str) & ConstraintSet.equality(V, bytes) # revealed: tuple[Solution[T=int, U=str, V=bytes]] reveal_type(constraints.solutions(inferable=tuple[T, U, V])) def bindings_vtu[V, T, U]() -> None: # (T = int) ∧ (U = str) ∧ (V = bytes) - constraints = ConstraintSet.range(int, T, int) & ConstraintSet.range(str, U, str) & ConstraintSet.range(bytes, V, bytes) + constraints = ConstraintSet.equality(T, int) & ConstraintSet.equality(U, str) & ConstraintSet.equality(V, bytes) # revealed: tuple[Solution[T=int, U=str, V=bytes]] reveal_type(constraints.solutions(inferable=tuple[T, U, V])) def bindings_reverse_source[T, U, V]() -> None: # (V = bytes) ∧ (U = str) ∧ (T = int) - constraints = ConstraintSet.range(bytes, V, bytes) & ConstraintSet.range(str, U, str) & ConstraintSet.range(int, T, int) + constraints = ConstraintSet.equality(V, bytes) & ConstraintSet.equality(U, str) & ConstraintSet.equality(T, int) # revealed: tuple[Solution[V=bytes, U=str, T=int]] reveal_type(constraints.solutions(inferable=tuple[T, U, V])) + +def bindings_absorbed[T, U, X]() -> None: + t = ConstraintSet.lower_bound(str, T) + u = ConstraintSet.lower_bound(bytes, U) + x = ConstraintSet.lower_bound(int, X) + + # ((X ≥ int) ∧ (T ≥ str) ∧ (U ≥ bytes)) | ((U ≥ bytes) ∧ (T ≥ str)) + constraints = (x & t & u) | (u & t) + # revealed: tuple[Solution[T=str, U=bytes]] + reveal_type(constraints.solutions(inferable=tuple[T, U, X])) ``` ## Nested transitive constraints and an unrelated alternative @@ -59,14 +88,13 @@ and vice versa. Because we combine them with union, we are allowed to _either_ f `T` and `U`, _or_ find a solution for `V`. We are not _obligated_ to find a solution for all three. ```py -from typing import Never from ty_extensions._internal import ConstraintSet def nested_transitive[T, U, V]() -> None: # ((T ≤ list[U]) ∧ (U ≤ int) ∧ (list[int] ≤ T)) | (bytes ≤ V) constraints = ( - ConstraintSet.range(Never, T, list[U]) & ConstraintSet.range(Never, U, int) & ConstraintSet.range(list[int], T, object) - ) | ConstraintSet.range(bytes, V, object) + ConstraintSet.upper_bound(T, list[U]) & ConstraintSet.upper_bound(U, int) & ConstraintSet.lower_bound(list[int], T) + ) | ConstraintSet.lower_bound(bytes, V) # TODO: sometimes: revealed tuple[Solution[T=list[int]], Solution[T=Never], Solution[]] # TODO: sometimes: revealed tuple[Solution[T=list[int]], Solution[T=list[int]], Solution[]] @@ -98,14 +126,11 @@ includes both sides of the union, so any solution that includes `bytes ≤ U` sh solution for `T`. ```py -from typing import Never from ty_extensions._internal import ConstraintSet def negated_alternative[T, U]() -> None: # ¬((T ≤ int) ∨ (T ≤ str)) | (bytes ≤ U) - constraints = ~(ConstraintSet.range(Never, T, int) | ConstraintSet.range(Never, T, str)) | ConstraintSet.range( - bytes, U, object - ) + constraints = ~(ConstraintSet.upper_bound(T, int) | ConstraintSet.upper_bound(T, str)) | ConstraintSet.lower_bound(bytes, U) # TODO: sometimes: revealed tuple[Solution[], Solution[T=Never], Solution[]] # revealed: tuple[Solution[], Solution[]] @@ -126,15 +151,14 @@ Constructing the constraints in the opposite source order makes the derived unio elements should not be reordered merely because the TDD-variable order changes. ```py -from typing import Never from ty_extensions._internal import ConstraintSet def derived_solution[U, T]() -> None: # (U ≤ int) ∧ (int ≤ T) ∧ ((T ≤ int) | (T ≤ str)) constraints = ( - ConstraintSet.range(Never, U, int) - & ConstraintSet.range(int, T, object) - & (ConstraintSet.range(Never, T, int) | ConstraintSet.range(Never, T, str)) + ConstraintSet.upper_bound(U, int) + & ConstraintSet.lower_bound(int, T) + & (ConstraintSet.upper_bound(T, int) | ConstraintSet.upper_bound(T, str)) ) # TODO: The derived relationship should not leave an inferable `U` in the solution for `T`. @@ -145,7 +169,7 @@ def derived_solution[U, T]() -> None: # TODO: The derived relationship should not leave an inferable `T` in the solution for `U`. # TODO: revealed: tuple[Solution[U=int]] - # revealed: tuple[Solution[U=Never]] + # revealed: tuple[Solution[U=int & T@derived_solution]] reveal_type(constraints.solutions_for(U, inferable=tuple[T, U])) ``` @@ -156,60 +180,59 @@ range or two linked constraints. Logical equivalence and solution-element order in both declaration orders. ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet def orientation_st[S, T]() -> None: - lower = ConstraintSet.range(Never, S, T) - upper = ConstraintSet.range(S, T, object) + lower = ConstraintSet.upper_bound(S, T) + upper = ConstraintSet.lower_bound(S, T) # TODO: sometimes: error [static-assert-error] "Static assertion error: argument evaluates to `False`" static_assert(lower == upper) - equality_st = ConstraintSet.range(T, S, T) - equality_ts = ConstraintSet.range(S, T, S) + equality_st = ConstraintSet.equality(S, T) + equality_ts = ConstraintSet.equality(T, S) static_assert(equality_st == equality_ts) def orientation_ts[T, S]() -> None: - lower = ConstraintSet.range(Never, S, T) - upper = ConstraintSet.range(S, T, object) + lower = ConstraintSet.upper_bound(S, T) + upper = ConstraintSet.lower_bound(S, T) # TODO: sometimes: error [static-assert-error] "Static assertion error: argument evaluates to `False`" static_assert(lower == upper) - equality_st = ConstraintSet.range(T, S, T) - equality_ts = ConstraintSet.range(S, T, S) + equality_st = ConstraintSet.equality(S, T) + equality_ts = ConstraintSet.equality(T, S) static_assert(equality_st == equality_ts) def chain_stu[S, T, U]() -> None: chain = ConstraintSet.range(S, T, U) - linked = ConstraintSet.range(Never, S, T) & ConstraintSet.range(Never, T, U) + linked = ConstraintSet.upper_bound(S, T) & ConstraintSet.upper_bound(T, U) # TODO: sometimes: error [static-assert-error] "Static assertion error: argument evaluates to `False`" static_assert(chain == linked) - constraints = chain & ConstraintSet.range(int, S, object) & ConstraintSet.range(Never, U, int) + constraints = chain & ConstraintSet.lower_bound(int, S) & ConstraintSet.upper_bound(U, int) # TODO: inferable typevars should not remain in these concrete solutions. # TODO: sometimes: revealed tuple[Solution[S=int | U@chain_stu | T@chain_stu]] - # revealed: tuple[Solution[S=T@chain_stu | int | U@chain_stu]] + # revealed: tuple[Solution[S=int | T@chain_stu | U@chain_stu]] reveal_type(constraints.solutions_for(S, inferable=tuple[S, T, U])) # revealed: tuple[Solution[T=S@chain_stu | int | U@chain_stu]] reveal_type(constraints.solutions_for(T, inferable=tuple[S, T, U])) - # revealed: tuple[Solution[U=T@chain_stu | S@chain_stu | int]] + # revealed: tuple[Solution[U=S@chain_stu | int | T@chain_stu]] reveal_type(constraints.solutions_for(U, inferable=tuple[S, T, U])) def chain_uts[U, T, S]() -> None: chain = ConstraintSet.range(S, T, U) - linked = ConstraintSet.range(Never, S, T) & ConstraintSet.range(Never, T, U) + linked = ConstraintSet.upper_bound(S, T) & ConstraintSet.upper_bound(T, U) # TODO: sometimes: error [static-assert-error] "Static assertion error: argument evaluates to `False`" static_assert(chain == linked) - constraints = chain & ConstraintSet.range(int, S, object) & ConstraintSet.range(Never, U, int) + constraints = chain & ConstraintSet.lower_bound(int, S) & ConstraintSet.upper_bound(U, int) # TODO: inferable typevars should not remain in these concrete solutions. # TODO: sometimes: revealed tuple[Solution[S=int | U@chain_uts | T@chain_uts]] - # revealed: tuple[Solution[S=T@chain_uts | int | U@chain_uts]] + # revealed: tuple[Solution[S=int | T@chain_uts | U@chain_uts]] reveal_type(constraints.solutions_for(S, inferable=tuple[S, T, U])) # revealed: tuple[Solution[T=S@chain_uts | int | U@chain_uts]] reveal_type(constraints.solutions_for(T, inferable=tuple[S, T, U])) - # revealed: tuple[Solution[U=T@chain_uts | S@chain_uts | int]] + # revealed: tuple[Solution[U=S@chain_uts | int | T@chain_uts]] reveal_type(constraints.solutions_for(U, inferable=tuple[S, T, U])) ``` @@ -220,14 +243,13 @@ leak onto the surviving paths. Universal abstraction of an alternative must like unrelated branch. ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet def noninferable_nested[T, U, V]() -> None: constraints = ( - ConstraintSet.range(Never, T, list[U]) & ConstraintSet.range(Never, U, int) & ConstraintSet.range(list[int], T, object) - ) | ConstraintSet.range(bytes, V, object) + ConstraintSet.upper_bound(T, list[U]) & ConstraintSet.upper_bound(U, int) & ConstraintSet.lower_bound(list[int], T) + ) | ConstraintSet.lower_bound(bytes, V) # `U` is deliberately non-inferable here. # TODO: We should not include a solution for non-inferable U. @@ -244,18 +266,16 @@ def noninferable_nested[T, U, V]() -> None: reveal_type(constraints.solutions_for(V, inferable=tuple[T, V])) quantified = constraints.for_all(tuple[T, U]) - expected = ConstraintSet.range(bytes, V, object) + expected = ConstraintSet.lower_bound(bytes, V) static_assert(quantified == expected) # revealed: tuple[Solution[V=bytes]] reveal_type(quantified.solutions_for(V, inferable=tuple[V])) def noninferable_negated[T, U]() -> None: - constraints = ~(ConstraintSet.range(Never, T, int) | ConstraintSet.range(Never, T, str)) | ConstraintSet.range( - bytes, U, object - ) + constraints = ~(ConstraintSet.upper_bound(T, int) | ConstraintSet.upper_bound(T, str)) | ConstraintSet.lower_bound(bytes, U) quantified = constraints.for_all(tuple[T]) - expected = ConstraintSet.range(bytes, U, object) + expected = ConstraintSet.lower_bound(bytes, U) static_assert(quantified == expected) # revealed: tuple[Solution[U=bytes]] reveal_type(quantified.solutions_for(U, inferable=tuple[U])) @@ -303,7 +323,7 @@ def listify[T](value: T) -> list[T]: return [value] def invariant_callable[U, V]() -> None: - constraints = ConstraintSet.range(bool, U, int) & ConstraintSet.range(int, V, int) + constraints = ConstraintSet.range(bool, U, int) & ConstraintSet.equality(V, int) # TODO: no error. Existential reduction of the callable's fresh typevar is currently lossy. # TODO: sometimes: no error # error: [static-assert-error] @@ -368,7 +388,7 @@ sequent fuel budget. The remaining solution, its element order, and the elements truncated diagnostic display must not depend on which implications were encountered first. ```py -from typing import Literal, Never +from typing import Literal from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet @@ -414,18 +434,18 @@ def high_fanout[ & ConstraintSet.range(Literal[11], L11, P) ) upper = ( - ConstraintSet.range(Never, P, R0) - & ConstraintSet.range(Never, P, R1) - & ConstraintSet.range(Never, P, R2) - & ConstraintSet.range(Never, P, R3) - & ConstraintSet.range(Never, P, R4) - & ConstraintSet.range(Never, P, R5) - & ConstraintSet.range(Never, P, R6) - & ConstraintSet.range(Never, P, R7) - & ConstraintSet.range(Never, P, R8) - & ConstraintSet.range(Never, P, R9) - & ConstraintSet.range(Never, P, R10) - & ConstraintSet.range(Never, P, R11) + ConstraintSet.upper_bound(P, R0) + & ConstraintSet.upper_bound(P, R1) + & ConstraintSet.upper_bound(P, R2) + & ConstraintSet.upper_bound(P, R3) + & ConstraintSet.upper_bound(P, R4) + & ConstraintSet.upper_bound(P, R5) + & ConstraintSet.upper_bound(P, R6) + & ConstraintSet.upper_bound(P, R7) + & ConstraintSet.upper_bound(P, R8) + & ConstraintSet.upper_bound(P, R9) + & ConstraintSet.upper_bound(P, R10) + & ConstraintSet.upper_bound(P, R11) ) inferable = tuple[ P, @@ -465,7 +485,7 @@ def high_fanout[ # TODO: sometimes: revealed tuple[Solution[P=L0@high_fanout | Literal[0, 1, 2, 5, 6, 7, 8, 9, 10, 11] | L1@high_fanout | L2@high_fanout | L3@high_fanout | L4@high_fanout | L5@high_fanout | L6@high_fanout | L7@high_fanout | L8@high_fanout | L9@high_fanout | L10@high_fanout | L11@high_fanout]] # TODO: sometimes: revealed tuple[Solution[P=L0@high_fanout | Literal[0, 1, 2, 3, 6, 7, 8, 9, 10, 11] | L1@high_fanout | L2@high_fanout | L3@high_fanout | L4@high_fanout | L5@high_fanout | L6@high_fanout | L7@high_fanout | L8@high_fanout | L9@high_fanout | L10@high_fanout | L11@high_fanout]] # TODO: sometimes: revealed tuple[Solution[P=L0@high_fanout | Literal[0, 1, 2, 3, 4, 5, 6, 9, 10, 11] | L1@high_fanout | L2@high_fanout | L3@high_fanout | L4@high_fanout | L5@high_fanout | L6@high_fanout | L7@high_fanout | L8@high_fanout | L9@high_fanout | L10@high_fanout | L11@high_fanout]] - # revealed: tuple[Solution[P=L0@high_fanout | L1@high_fanout | L2@high_fanout | Literal[2, 3, 4, 5, 6, 7, 8, 9, 10, 11] | L3@high_fanout | L4@high_fanout | L5@high_fanout | L6@high_fanout | L7@high_fanout | L8@high_fanout | L9@high_fanout | L10@high_fanout | L11@high_fanout]] + # revealed: tuple[Solution[P=L0@high_fanout | L1@high_fanout | Literal[2, 3, 4, 5, 6, 7, 8, 9, 10, 11] | L2@high_fanout | L3@high_fanout | L4@high_fanout | L5@high_fanout | L6@high_fanout | L7@high_fanout | L8@high_fanout | L9@high_fanout | L10@high_fanout | L11@high_fanout]] reveal_type(pivot) # TODO: sometimes: revealed tuple[Solution[R11=P@high_fanout]] @@ -475,10 +495,10 @@ def high_fanout[ # TODO: sometimes: revealed tuple[Solution[R11=L0@high_fanout | Literal[0, 1, 5, 6, 7, 8, 9, 10, 11] | L1@high_fanout | L2@high_fanout | L4@high_fanout | L5@high_fanout | L6@high_fanout | L7@high_fanout | L8@high_fanout | L9@high_fanout | L10@high_fanout | L11@high_fanout | P@high_fanout]] # TODO: sometimes: revealed tuple[Solution[R11=L0@high_fanout | Literal[0, 1, 2, 3, 6, 7, 8, 9, 10, 11] | L1@high_fanout | L2@high_fanout | L3@high_fanout | L5@high_fanout | L6@high_fanout | L7@high_fanout | L8@high_fanout | L9@high_fanout | L10@high_fanout | L11@high_fanout | P@high_fanout]] # TODO: sometimes: revealed tuple[Solution[R11=L0@high_fanout | Literal[0, 1, 2, 3, 4, 5, 6, 9, 10, 11] | L1@high_fanout | L2@high_fanout | L3@high_fanout | L4@high_fanout | L5@high_fanout | L6@high_fanout | L8@high_fanout | L9@high_fanout | L10@high_fanout | L11@high_fanout | P@high_fanout]] - # revealed: tuple[Solution[R11=L1@high_fanout | L2@high_fanout | Literal[2, 3, 4, 5, 6, 7, 8, 9, 10, 11] | L3@high_fanout | L4@high_fanout | L5@high_fanout | L6@high_fanout | L7@high_fanout | L8@high_fanout | L9@high_fanout | L10@high_fanout | L11@high_fanout | P@high_fanout]] + # revealed: tuple[Solution[R11=L1@high_fanout | Literal[2, 3, 4, 5, 6, 7, 8, 9, 10, 11] | L2@high_fanout | L3@high_fanout | L4@high_fanout | L5@high_fanout | L6@high_fanout | L7@high_fanout | L8@high_fanout | L9@high_fanout | L10@high_fanout | L11@high_fanout | P@high_fanout]] reveal_type(result) - impossible = constraints & ConstraintSet.range(Never, R11, Literal[0]) + impossible = constraints & ConstraintSet.upper_bound(R11, Literal[0]) # TODO: sometimes: error [static-assert-error] "Static assertion error: argument evaluates to `False`" static_assert(not impossible.satisfied_by_all_typevars(inferable=inferable)) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/regression/derived_constraint_cycles.md b/crates/ty_python_semantic/resources/mdtest/regression/derived_constraint_cycles.md index 86ec59c3f7..fc4f2052c3 100644 --- a/crates/ty_python_semantic/resources/mdtest/regression/derived_constraint_cycles.md +++ b/crates/ty_python_semantic/resources/mdtest/regression/derived_constraint_cycles.md @@ -171,14 +171,13 @@ Structural fuel is charged only for depth introduced by a derivation. Propagatin concrete bound through a typevar therefore remains cheap. ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet type Deep = tuple[tuple[tuple[tuple[tuple[tuple[tuple[tuple[tuple[tuple[int]]]]]]]]]] def check_deep_bound[T, U](): - constraints = ConstraintSet.range(Never, T, U) & ConstraintSet.range(Never, U, Deep) + constraints = ConstraintSet.upper_bound(T, U) & ConstraintSet.upper_bound(U, Deep) static_assert(constraints.implies_subtype_of(T, Deep)) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/scopes/builtin.md b/crates/ty_python_semantic/resources/mdtest/scopes/builtin.md index b6bae824a5..a7eb07d429 100644 --- a/crates/ty_python_semantic/resources/mdtest/scopes/builtin.md +++ b/crates/ty_python_semantic/resources/mdtest/scopes/builtin.md @@ -2,8 +2,8 @@ ## 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: +If a builtin name is conditionally shadowed by a local variable, the function's binding scope +terminates name resolution. The name can be unbound, but it cannot refer to the builtin: ```py def _(flag: bool) -> None: @@ -11,8 +11,10 @@ def _(flag: bool) -> None: abs = 1 chr: int = 1 - reveal_type(abs) # revealed: Literal[1] | (def abs[Element](x: SupportsAbs[Element], /) -> Element) - reveal_type(chr) # revealed: Literal[1] | (def chr(i: SupportsIndex, /) -> str) + # error: [possibly-unresolved-reference] + reveal_type(abs) # revealed: Literal[1] + # error: [possibly-unresolved-reference] + reveal_type(chr) # revealed: Literal[1] ``` ## Conditionally global override of builtin diff --git a/crates/ty_python_semantic/resources/mdtest/scopes/class_implicit_attrs.md b/crates/ty_python_semantic/resources/mdtest/scopes/class_implicit_attrs.md index 7130538acf..e0281fcbb2 100644 --- a/crates/ty_python_semantic/resources/mdtest/scopes/class_implicit_attrs.md +++ b/crates/ty_python_semantic/resources/mdtest/scopes/class_implicit_attrs.md @@ -72,6 +72,18 @@ reveal_type(__qualname__) # revealed: Literal[42] reveal_type(__module__) # revealed: Literal[42] ``` +They also take priority over a possibly-bound snapshot from an enclosing `global` declaration: + +```py +def enclosing(flag: bool) -> None: + global __module__ + if flag: + __module__ = 1 + + class Foo: + reveal_type(__module__) # revealed: str +``` + ## `__firstlineno__` has priority over globals (Python 3.13+) The same applies to `__firstlineno__` on Python 3.13+: diff --git a/crates/ty_python_semantic/resources/mdtest/scopes/global.md b/crates/ty_python_semantic/resources/mdtest/scopes/global.md index 7ff2d573bb..8772480298 100644 --- a/crates/ty_python_semantic/resources/mdtest/scopes/global.md +++ b/crates/ty_python_semantic/resources/mdtest/scopes/global.md @@ -299,6 +299,30 @@ def factory(): reveal_type(x) # revealed: Literal[1] ``` +If the rebinding is conditional, an unbound enclosing snapshot continues to the implicit global: + +```py +def conditional_factory(flag: bool): + global __file__ + if flag: + __file__ = "shadow" + + class C: + reveal_type(__file__) # revealed: str +``` + +An unbound snapshot can also continue through the module scope to a builtin. + +```py +def conditional_builtin_factory(flag: bool): + global len # error: [unresolved-global] "Invalid global declaration of `len`: `len` has no declarations or bindings in the global scope" + if flag: + len = 1 + + class C: + reveal_type(len) # revealed: Literal[1] | (def len(obj: Sized, /) -> int) +``` + ## References to variables before they are defined within a class scope are considered global If we try to access a variable in a class before it has been defined, the lookup will fall back to diff --git a/crates/ty_python_semantic/resources/mdtest/scopes/unbound.md b/crates/ty_python_semantic/resources/mdtest/scopes/unbound.md index c697d41de6..98acc365a0 100644 --- a/crates/ty_python_semantic/resources/mdtest/scopes/unbound.md +++ b/crates/ty_python_semantic/resources/mdtest/scopes/unbound.md @@ -71,3 +71,14 @@ def f(): # revealed: Literal[2] reveal_type(x) ``` + +## Unbound function local named `reveal_type` + +The convenience fallback for an unimported `reveal_type` only applies when name resolution does not +find the name. It does not replace an unbound local. + +```py +def f(): + reveal_type(1) # error: [unresolved-reference] + reveal_type = lambda value: value +``` diff --git a/crates/ty_python_semantic/resources/mdtest/scripts.md b/crates/ty_python_semantic/resources/mdtest/scripts.md index 66f9ade3d1..abffa906eb 100644 --- a/crates/ty_python_semantic/resources/mdtest/scripts.md +++ b/crates/ty_python_semantic/resources/mdtest/scripts.md @@ -126,3 +126,44 @@ print(missing) # error: [unresolved-reference] print(missing) ``` + +# Valid blocks after invalid opening tags + +Invalid opening tags do not prevent a later valid metadata block from being recognized. + +```py +value = 1 # /// script +# [tool.ty.rules] +# unresolved-reference = "error" +# /// + +# /// script invalid +# [tool.ty.rules] +# unresolved-reference = "error" +# /// + +# /// script +# [tool.ty.rules] +# unresolved-reference = "ignore" +# /// + +print(missing) +``` + +# Valid blocks after unclosed blocks + +An earlier unclosed block does not prevent a later valid metadata block from being recognized. + +```py +# /// script +# [tool.ty.rules] +# unresolved-reference = "error" +value = 1 + +# /// script +# [tool.ty.rules] +# unresolved-reference = "ignore" +# /// + +print(missing) +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Annotated_subscript_\342\200\246_(98082f2161ea366f).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Annotated_subscript_\342\200\246_(98082f2161ea366f).snap" index bba81a9627..5b6dd364dd 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Annotated_subscript_\342\200\246_(98082f2161ea366f).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Annotated_subscript_\342\200\246_(98082f2161ea366f).snap" @@ -27,7 +27,6 @@ error[invalid-assignment]: Invalid subscript assignment with key of type `Litera | 4 | numbers[0]: str = "three" | ^^^^^^^^^^^^^^^^^^^^^^^^^ - | ``` @@ -37,6 +36,5 @@ error[invalid-type-form]: Type annotations are not allowed on subscripted expres | 4 | numbers[0]: str = "three" | ^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_key_type_-_For_a_`dict`_(4aa9d1d82d07fcf1).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_key_type_-_For_a_`dict`_(4aa9d1d82d07fcf1).snap" index 7305b144c3..d5facda9bf 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_key_type_-_For_a_`dict`_(4aa9d1d82d07fcf1).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_key_type_-_For_a_`dict`_(4aa9d1d82d07fcf1).snap" @@ -27,6 +27,5 @@ error[invalid-assignment]: Invalid subscript assignment with key of type `Litera | ^^^^^^^-^^^^^ | | | Expected key of type `str`, got `Literal[0]` - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_key_type_-_For_a_`list`_(752cfa73fb34c1c).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_key_type_-_For_a_`list`_(752cfa73fb34c1c).snap" index 0b0e770975..fd77ebe502 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_key_type_-_For_a_`list`_(752cfa73fb34c1c).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_key_type_-_For_a_`list`_(752cfa73fb34c1c).snap" @@ -25,6 +25,5 @@ error[invalid-assignment]: Invalid subscript assignment with key of type `Litera | 2 | numbers["zero"] = 3 # error: [invalid-assignment] | ^^^^^^^^^^^^^^^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_key_type_for\342\200\246_(815dae276e2fd2b7).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_key_type_for\342\200\246_(815dae276e2fd2b7).snap" index ba4a84aedf..f6b047e6fe 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_key_type_for\342\200\246_(815dae276e2fd2b7).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_key_type_for\342\200\246_(815dae276e2fd2b7).snap" @@ -30,6 +30,5 @@ error[invalid-key]: TypedDict `Config` can only be subscripted with a string lit | 7 | config[0] = 3 # error: [invalid-key] | ^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_value_type_-_For_a_`dict`_(177872afa1956fef).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_value_type_-_For_a_`dict`_(177872afa1956fef).snap" index e7464a9ce4..9670e1f60f 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_value_type_-_For_a_`dict`_(177872afa1956fef).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_value_type_-_For_a_`dict`_(177872afa1956fef).snap" @@ -27,6 +27,5 @@ error[invalid-assignment]: Invalid subscript assignment with key of type `Litera | ^^^^^^^^^^^^^^^^^^^^------- | | | Expected value of type `int`, got `Literal["three"]` - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_value_type_-_For_a_`list`_(e7ebbd4af387837c).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_value_type_-_For_a_`list`_(e7ebbd4af387837c).snap" index a0362842b3..2f085bcce7 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_value_type_-_For_a_`list`_(e7ebbd4af387837c).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_value_type_-_For_a_`list`_(e7ebbd4af387837c).snap" @@ -25,6 +25,5 @@ error[invalid-assignment]: Invalid subscript assignment with key of type `Litera | 2 | numbers[0] = "three" # error: [invalid-assignment] | ^^^^^^^^^^^^^^^^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_value_type_f\342\200\246_(155d53762388f9ad).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_value_type_f\342\200\246_(155d53762388f9ad).snap" index 7b7a1ba07f..f5b7c6efe4 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_value_type_f\342\200\246_(155d53762388f9ad).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_value_type_f\342\200\246_(155d53762388f9ad).snap" @@ -26,19 +26,17 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_dia ``` error[invalid-assignment]: Invalid assignment to key "retries" with declared type `int` on TypedDict `Config` - --> src/mdtest_snippet.py:7:5 + --> src/mdtest_snippet.py:7:25 | 7 | config["retries"] = "three" # error: [invalid-assignment] | ------ --------- ^^^^^^^ value of type `Literal["three"]` | | | | | key has declared type `int` | TypedDict `Config` - | info: Item declaration --> src/mdtest_snippet.py:4:5 | 4 | retries: int | ------------ Item declared here - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Misspelled_key_for_`\342\200\246_(7cf0fa634e2a2d59).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Misspelled_key_for_`\342\200\246_(7cf0fa634e2a2d59).snap" index bc656372fa..b902d9d89b 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Misspelled_key_for_`\342\200\246_(7cf0fa634e2a2d59).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Misspelled_key_for_`\342\200\246_(7cf0fa634e2a2d59).snap" @@ -26,14 +26,13 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_dia ``` error[invalid-key]: Unknown key "Retries" for TypedDict `Config` - --> src/mdtest_snippet.py:7:5 + --> src/mdtest_snippet.py:7:12 | 7 | config["Retries"] = 30.0 # error: [invalid-key] | ------ ^^^^^^^^^ Did you mean "retries"? | | | TypedDict `Config` | - | 6 | def _(config: Config) -> None: - config["Retries"] = 30.0 # error: [invalid-key] 7 + config["retries"] = 30.0 # error: [invalid-key] diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_No_`__setitem__`_met\342\200\246_(468f62a3bdd1d60c).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_No_`__setitem__`_met\342\200\246_(468f62a3bdd1d60c).snap" index 28293ef4ab..0304377faf 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_No_`__setitem__`_met\342\200\246_(468f62a3bdd1d60c).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_No_`__setitem__`_met\342\200\246_(468f62a3bdd1d60c).snap" @@ -29,7 +29,6 @@ error[invalid-assignment]: Cannot assign to a subscript on an object of type `Re | 6 | config["retries"] = 3 # error: [invalid-assignment] | ^^^^^^^^^^^^^^^^^ - | help: Consider adding a `__setitem__` method to `ReadOnlyDict`. ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Possibly_missing_`__\342\200\246_(efd3f0c02e9b89e9).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Possibly_missing_`__\342\200\246_(efd3f0c02e9b89e9).snap" index e7bc46be47..890d507ff6 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Possibly_missing_`__\342\200\246_(efd3f0c02e9b89e9).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Possibly_missing_`__\342\200\246_(efd3f0c02e9b89e9).snap" @@ -25,7 +25,6 @@ error[invalid-assignment]: Cannot assign to a subscript on an object of type `No | 2 | config["retries"] = 3 # error: [invalid-assignment] | ^^^^^^^^^^^^^^^^^ - | info: The full type of the subscripted object is `dict[str, int] | None` info: `None` does not have a `__setitem__` method. diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Unknown_key_for_all_\342\200\246_(8a0f0e8ceccc51b2).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Unknown_key_for_all_\342\200\246_(8a0f0e8ceccc51b2).snap" index e05945c548..d831b9bcbe 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Unknown_key_for_all_\342\200\246_(8a0f0e8ceccc51b2).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Unknown_key_for_all_\342\200\246_(8a0f0e8ceccc51b2).snap" @@ -33,24 +33,22 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_dia ``` error[invalid-key]: Unknown key "nane" for TypedDict `Animal` - --> src/mdtest_snippet.py:14:5 + --> src/mdtest_snippet.py:14:11 | 14 | being["nane"] = "unknown" | ----- ^^^^^^ Unknown key "nane" | | | TypedDict `Animal` in union type `Person | Animal` - | ``` ``` error[invalid-key]: Unknown key "nane" for TypedDict `Person` - --> src/mdtest_snippet.py:14:5 + --> src/mdtest_snippet.py:14:11 | 14 | being["nane"] = "unknown" | ----- ^^^^^^ Unknown key "nane" | | | TypedDict `Person` in union type `Person | Animal` - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Unknown_key_for_one_\342\200\246_(b515711c0a451a86).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Unknown_key_for_one_\342\200\246_(b515711c0a451a86).snap" index 6444938508..f79d5e454b 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Unknown_key_for_one_\342\200\246_(b515711c0a451a86).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Unknown_key_for_one_\342\200\246_(b515711c0a451a86).snap" @@ -31,12 +31,11 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_dia ``` error[invalid-key]: Unknown key "legs" for TypedDict `Person` - --> src/mdtest_snippet.py:12:5 + --> src/mdtest_snippet.py:12:11 | 12 | being["legs"] = 4 # error: [invalid-key] | ----- ^^^^^^ Unknown key "legs" | | | TypedDict `Person` in union type `Person | Animal` - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Wrong_value_type_for\342\200\246_(57372b65e30392a8).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Wrong_value_type_for\342\200\246_(57372b65e30392a8).snap" index 09ccb445d4..5ab94cc973 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Wrong_value_type_for\342\200\246_(57372b65e30392a8).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Wrong_value_type_for\342\200\246_(57372b65e30392a8).snap" @@ -27,7 +27,6 @@ error[invalid-assignment]: Invalid subscript assignment with key of type `Litera | ^^^^^^^^^^^^^^^^^^^^- | | | Expected value of type `str`, got `Literal[3]` - | info: The full type of the subscripted object is `dict[str, int] | dict[str, str]` ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Wrong_value_type_for\342\200\246_(ffe39a3bae68cfe4).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Wrong_value_type_for\342\200\246_(ffe39a3bae68cfe4).snap" index dd6482e433..60fc839161 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Wrong_value_type_for\342\200\246_(ffe39a3bae68cfe4).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Wrong_value_type_for\342\200\246_(ffe39a3bae68cfe4).snap" @@ -29,7 +29,6 @@ error[invalid-assignment]: Invalid subscript assignment with key of type `Litera | ^^^^^^^^^^^^^^^^^^^^--- | | | Expected value of type `int`, got `float` - | info: The full type of the subscripted object is `dict[str, int] | dict[str, str]` ``` @@ -42,7 +41,6 @@ error[invalid-assignment]: Invalid subscript assignment with key of type `Litera | ^^^^^^^^^^^^^^^^^^^^--- | | | Expected value of type `str`, got `float` - | info: The full type of the subscripted object is `dict[str, int] | dict[str, str]` ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/async.md_-_Async_with_statement\342\200\246_-_Context_expression_w\342\200\246_(28ef812089a32e6a).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/async.md_-_Async_with_statement\342\200\246_-_Context_expression_w\342\200\246_(28ef812089a32e6a).snap" index 55ca624cf8..3e94e3a0db 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/async.md_-_Async_with_statement\342\200\246_-_Context_expression_w\342\200\246_(28ef812089a32e6a).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/async.md_-_Async_with_statement\342\200\246_-_Context_expression_w\342\200\246_(28ef812089a32e6a).snap" @@ -14,10 +14,10 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/with/async.md ``` 1 | class Manager1: - 2 | def __aenter__(self) -> str: + 2 | async def __aenter__(self) -> str: 3 | return "foo" 4 | - 5 | def __aexit__(self, exc_type, exc_value, traceback): ... + 5 | async def __aexit__(self, exc_type, exc_value, traceback): ... 6 | 7 | class NotAContextManager: ... 8 | @@ -35,7 +35,6 @@ error[invalid-context-manager]: Object of type `Manager1 | NotAContextManager` c | 11 | async with context_expr as f: | ^^^^^^^^^^^^ - | info: `NotAContextManager` does not implement `__aenter__` or `__aexit__` ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/basedpython_type_mod\342\200\246_-_basedpython___`litera\342\200\246_-_a_`Callable`_paramet\342\200\246_(1d80f3ff7d077e34).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/basedpython_type_mod\342\200\246_-_basedpython___`litera\342\200\246_-_a_`Callable`_paramet\342\200\246_(1d80f3ff7d077e34).snap" index 30d4197f52..a2b3328a01 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/basedpython_type_mod\342\200\246_-_basedpython___`litera\342\200\246_-_a_`Callable`_paramet\342\200\246_(1d80f3ff7d077e34).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/basedpython_type_mod\342\200\246_-_basedpython___`litera\342\200\246_-_a_`Callable`_paramet\342\200\246_(1d80f3ff7d077e34).snap" @@ -27,6 +27,5 @@ error[invalid-syntax]: the `literal` type modifier is not valid in .py files | 4 | a: Callable[[literal str], None] | ^^^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/basedpython_type_mod\342\200\246_-_basedpython___`litera\342\200\246_-_the_modifiers_are_ba\342\200\246_(6d9965b144910b9f).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/basedpython_type_mod\342\200\246_-_basedpython___`litera\342\200\246_-_the_modifiers_are_ba\342\200\246_(6d9965b144910b9f).snap" index ce235213cb..6a8b17930b 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/basedpython_type_mod\342\200\246_-_basedpython___`litera\342\200\246_-_the_modifiers_are_ba\342\200\246_(6d9965b144910b9f).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/basedpython_type_mod\342\200\246_-_basedpython___`litera\342\200\246_-_the_modifiers_are_ba\342\200\246_(6d9965b144910b9f).snap" @@ -25,6 +25,5 @@ error[invalid-syntax]: the `literal` type modifier is not valid in .py files | 2 | a: literal str = "x" | ^^^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Exception_Handling_-_Invalid_exception_ha\342\200\246_(d394c561bdd35078).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Exception_Handling_-_Invalid_exception_ha\342\200\246_(d394c561bdd35078).snap" index 3619f8434e..2f433a830b 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Exception_Handling_-_Invalid_exception_ha\342\200\246_(d394c561bdd35078).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Exception_Handling_-_Invalid_exception_ha\342\200\246_(d394c561bdd35078).snap" @@ -57,7 +57,6 @@ error[invalid-exception-caught]: Invalid object caught in an exception handler | 4 | except 3 as e: | ^ Object has type `Literal[3]` - | info: Can only catch a subclass of `BaseException` or tuple of `BaseException` subclasses ``` @@ -71,7 +70,6 @@ error[invalid-exception-caught]: Invalid tuple caught in an exception handler | | | | | Invalid element of type `Literal[b"bar"]` | Invalid element of type `Literal["foo"]` - | info: Can only catch a subclass of `BaseException` or tuple of `BaseException` subclasses ``` @@ -82,7 +80,6 @@ error[invalid-exception-caught]: Invalid object caught in an exception handler | 21 | except x as e: | ^ Object has type `type[str]` - | info: Can only catch a subclass of `BaseException` or tuple of `BaseException` subclasses ``` @@ -93,7 +90,6 @@ error[invalid-exception-caught]: Invalid tuple caught in an exception handler | 24 | except y as f: | ^ Object has type `tuple[type[OSError], type[RuntimeError], int]` - | info: Can only catch a subclass of `BaseException` or tuple of `BaseException` subclasses ``` @@ -104,7 +100,6 @@ error[invalid-exception-caught]: Invalid tuple caught in an exception handler | 27 | except z as g: | ^ Object has type `tuple[type[str], ...]` - | info: Can only catch a subclass of `BaseException` or tuple of `BaseException` subclasses ``` @@ -115,7 +110,6 @@ error[invalid-exception-caught]: Invalid object caught in an exception handler | 33 | except int: | ^^^ Object has type `` - | info: Can only catch a subclass of `BaseException` or tuple of `BaseException` subclasses ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Exception_Handling_-_Special-cased_diagno\342\200\246_(a97274530a7f61c1).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Exception_Handling_-_Special-cased_diagno\342\200\246_(a97274530a7f61c1).snap" index cb14d8e188..3984c55bae 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Exception_Handling_-_Special-cased_diagno\342\200\246_(a97274530a7f61c1).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Exception_Handling_-_Special-cased_diagno\342\200\246_(a97274530a7f61c1).snap" @@ -33,7 +33,6 @@ error[invalid-raise]: Cannot raise `NotImplemented` | 4 | raise NotImplemented from NotImplemented | ^^^^^^^^^^^^^^ Did you mean `NotImplementedError`? - | info: Can only raise an instance or subclass of `BaseException` ``` @@ -44,7 +43,6 @@ error[invalid-raise]: Cannot use `NotImplemented` as an exception cause | 4 | raise NotImplemented from NotImplemented | ^^^^^^^^^^^^^^ Did you mean `NotImplementedError`? - | info: An exception cause must be an instance of `BaseException`, subclass of `BaseException`, or `None` ``` @@ -55,7 +53,6 @@ error[invalid-exception-caught]: Cannot catch `NotImplemented` in an exception h | 6 | except NotImplemented: | ^^^^^^^^^^^^^^ Did you mean `NotImplementedError`? - | info: Can only catch a subclass of `BaseException` or tuple of `BaseException` subclasses ``` @@ -69,7 +66,6 @@ error[invalid-exception-caught]: Invalid tuple caught in an exception handler | | | Invalid element of type `NotImplementedType` | Did you mean `NotImplementedError`? - | info: Can only catch a subclass of `BaseException` or tuple of `BaseException` subclasses ``` 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" index ba7d471ac4..3da3fe923c 100644 --- "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" @@ -26,7 +26,6 @@ error[unresolved-import]: Cannot resolve imported module `tomllib` | 1 | import tomllib # 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 @@ -38,7 +37,6 @@ error[unresolved-import]: Cannot resolve imported module `string.templatelib` | 2 | from string.templatelib import Template # 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 @@ -50,7 +48,6 @@ error[unresolved-import]: Module `importlib.resources` has no member `abc` | 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 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" index 2155b6dcf1..1c0e66e13e 100644 --- "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" @@ -25,7 +25,6 @@ error[unresolved-import]: Cannot resolve imported module `aifc` | 1 | import aifc # 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 @@ -37,7 +36,6 @@ error[unresolved-import]: Cannot resolve imported module `distutils` | 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 diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Attempting_to_import\342\200\246_(dba22bd97137ee38).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Attempting_to_import\342\200\246_(dba22bd97137ee38).snap" index 068a1b4124..da2e29279d 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Attempting_to_import\342\200\246_(dba22bd97137ee38).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Attempting_to_import\342\200\246_(dba22bd97137ee38).snap" @@ -27,7 +27,6 @@ error[unresolved-import]: Cannot resolve imported module `compression.zstd` | 1 | import compression.zstd # error: [unresolved-import] | ^^^^^^^^^^^^^^^^ - | info: The stdlib module `compression` is only available on Python 3.14+ info: Python 3.10 was assumed when resolving modules because it was specified on the command line @@ -39,7 +38,6 @@ error[unresolved-import]: Cannot resolve imported module `compression` | 2 | from compression import zstd # error: [unresolved-import] | ^^^^^^^^^^^ - | info: The stdlib module `compression` is only available on Python 3.14+ info: Python 3.10 was assumed when resolving modules because it was specified on the command line @@ -51,7 +49,6 @@ error[unresolved-import]: Cannot resolve imported module `compression.fakebutwho | 3 | import compression.fakebutwhocansay # error: [unresolved-import] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | info: The stdlib module `compression` is only available on Python 3.14+ info: Python 3.10 was assumed when resolving modules because it was specified on the command line @@ -63,7 +60,6 @@ error[unresolved-import]: Cannot resolve imported module `compression` | 4 | from compression import fakebutwhocansay # error: [unresolved-import] | ^^^^^^^^^^^ - | info: The stdlib module `compression` is only available on Python 3.14+ info: Python 3.10 was assumed when resolving modules because it was specified on the command line 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" index 47fdfa10bb..a2d0cc2dbc 100644 --- "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" @@ -25,7 +25,6 @@ error[unresolved-import]: Cannot resolve imported module `does_not_exist` | 2 | from does_not_exist import foo, bar, baz | ^^^^^^^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. /src (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) 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" index 58dd439bb8..1894f0f5ac 100644 --- "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" @@ -24,7 +24,6 @@ error[unresolved-import]: Cannot resolve imported module `zqzqzqzqzqzqzq` | 1 | import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve imported module `zqzqzqzqzqzqzq`" | ^^^^^^^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. /src (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) 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" index 63d9b19ef1..f607150f14 100644 --- "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" @@ -33,7 +33,6 @@ error[unresolved-import]: Cannot resolve imported module `a.foo` | 2 | import a.foo # error: [unresolved-import] "Cannot resolve imported module `a.foo`" | ^^^^^ - | info: Searched in the following paths during module resolution: info: 1. /src (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -47,7 +46,6 @@ error[unresolved-import]: Cannot resolve imported module `b.foo` | 5 | import b.foo # error: [unresolved-import] "Cannot resolve imported module `b.foo`" | ^^^^^ - | info: Searched in the following paths during module resolution: info: 1. /src (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___Leg\342\200\246_-_Diagnostics_for_bad_\342\200\246_(2ceba7b720e21b8b).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___Leg\342\200\246_-_Diagnostics_for_bad_\342\200\246_(2ceba7b720e21b8b).snap" index 47ae403330..32cdd45d55 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___Leg\342\200\246_-_Diagnostics_for_bad_\342\200\246_(2ceba7b720e21b8b).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___Leg\342\200\246_-_Diagnostics_for_bad_\342\200\246_(2ceba7b720e21b8b).snap" @@ -47,7 +47,6 @@ error[invalid-type-arguments]: Type `int` is not assignable to upper bound `str` | 3 | T = TypeVar("T", bound=str) | - Type variable defined here - | ``` @@ -62,6 +61,5 @@ error[invalid-type-arguments]: Type `str` does not satisfy constraints `int`, `b | 4 | U = TypeVar("U", int, bytes) | - Type variable defined here - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___Leg\342\200\246_-_Errors_for_inconsist\342\200\246_(557742f3cd2464b2).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___Leg\342\200\246_-_Errors_for_inconsist\342\200\246_(557742f3cd2464b2).snap" index a9dead94b7..2169103cd6 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___Leg\342\200\246_-_Errors_for_inconsist\342\200\246_(557742f3cd2464b2).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___Leg\342\200\246_-_Errors_for_inconsist\342\200\246_(557742f3cd2464b2).snap" @@ -85,7 +85,6 @@ error[invalid-generic-class]: Inconsistent type arguments for `Grandparent` amon | | | | | Later class base is `Grandparent[T2@BadChild, T1@BadChild]` | Earlier class base inherits from `Grandparent[T1@BadChild, T2@BadChild]` - | ``` @@ -98,7 +97,6 @@ error[invalid-generic-class]: Inconsistent type arguments for `Grandparent` amon | | | | | Later class base is `Grandparent[T2@BadChild2, int]` | Earlier class base inherits from `Grandparent[T1@BadChild2, T2@BadChild2]` - | ``` @@ -111,7 +109,6 @@ error[invalid-generic-class]: Inconsistent type arguments for `Grandparent` amon | | | | | Later class base inherits from `Grandparent[T2@BadChild3, T1@BadChild3]` | Earlier class base inherits from `Grandparent[T1@BadChild3, T2@BadChild3]` - | ``` @@ -121,7 +118,6 @@ info[missing-type-argument]: Missing type arguments for generic class `Parent` ( | 29 | class Fine(Parent, Grandparent[T1, T2]): ... # error: [missing-type-argument] | ^^^^^^ - | ``` @@ -131,7 +127,6 @@ info[missing-type-argument]: Missing type arguments for generic class `Parent3` | 30 | class AlsoFine(Parent3, Parent4[T1, T2]): ... # error: [missing-type-argument] | ^^^^^^^ - | ``` @@ -141,7 +136,6 @@ info[missing-type-argument]: Missing type arguments for generic class `Parent` ( | 35 | class Dandy(Parent, Parent3, Parent4): ... | ^^^^^^ - | ``` @@ -151,7 +145,6 @@ info[missing-type-argument]: Missing type arguments for generic class `Parent3` | 35 | class Dandy(Parent, Parent3, Parent4): ... | ^^^^^^^ - | ``` @@ -161,7 +154,6 @@ info[missing-type-argument]: Missing type arguments for generic class `Parent4` | 35 | class Dandy(Parent, Parent3, Parent4): ... | ^^^^^^^ - | ``` @@ -174,7 +166,6 @@ error[invalid-generic-class]: Inconsistent type arguments for `Grandparent` amon | | | | | Later class base inherits from `Grandparent[T2@BadChild4, T1@BadChild4]` | Earlier class base inherits from `Grandparent[T1@BadChild4, T2@BadChild4]` - | ``` @@ -184,7 +175,6 @@ info[missing-type-argument]: Missing type arguments for generic class `Parent` ( | 42 | class BadChild4(Parent, Parent3[T1, T2], Parent4[T2, T1]): ... | ^^^^^^ - | ``` @@ -197,7 +187,6 @@ error[invalid-generic-class]: Inconsistent type arguments for `Grandparent` amon | | | | | Later class base inherits from `Grandparent[T2@BadChild5, T1@BadChild5]` | Earlier class base inherits from `Grandparent[T1@BadChild5, T2@BadChild5]` - | ``` @@ -210,7 +199,6 @@ error[invalid-generic-class]: Inconsistent type arguments for `Grandparent` amon | | | | | Later class base inherits from `Grandparent[T2@BadChild6, T1@BadChild6]` | Earlier class base inherits from `Grandparent[T1@BadChild6, T2@BadChild6]` - | ``` @@ -220,7 +208,6 @@ info[missing-type-argument]: Missing type arguments for generic class `Parent3` | 49 | class BadChild6(Parent[T1, T2], Parent3, Parent4[T2, T1]): ... | ^^^^^^^ - | ``` @@ -233,7 +220,6 @@ error[invalid-generic-class]: Inconsistent type arguments for `Grandparent` amon | | | | | Later class base inherits from `Grandparent[T2@BadChild7, T1@BadChild7]` | Earlier class base inherits from `Grandparent[T1@BadChild7, T2@BadChild7]` - | ``` @@ -246,7 +232,6 @@ error[invalid-generic-class]: Inconsistent type arguments for `Grandparent` amon | | | | | Later class base inherits from `Grandparent[T2@BadChild8, T1@BadChild8]` | Earlier class base inherits from `Grandparent[T1@BadChild8, T2@BadChild8]` - | ``` @@ -256,7 +241,6 @@ info[missing-type-argument]: Missing type arguments for generic class `Parent4` | 56 | class BadChild8(Parent[T1, T2], Parent3[T2, T1], Parent4): ... | ^^^^^^^ - | ``` @@ -269,6 +253,5 @@ error[invalid-generic-class]: Inconsistent type arguments for `Grandparent` amon | | | | | Later class base inherits from `Grandparent[T2@BadChild9, T1@BadChild9]` | Earlier class base inherits from `Grandparent[T1@BadChild9, T2@BadChild9]` - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___Leg\342\200\246_-_Specializing_generic\342\200\246_(5a066394f338af48).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___Leg\342\200\246_-_Specializing_generic\342\200\246_(5a066394f338af48).snap" index 5d28b0b431..627aa2e7b9 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___Leg\342\200\246_-_Specializing_generic\342\200\246_(5a066394f338af48).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___Leg\342\200\246_-_Specializing_generic\342\200\246_(5a066394f338af48).snap" @@ -108,7 +108,6 @@ error[invalid-type-arguments]: Too many type arguments to class `C`: expected 1, | 11 | reveal_type(C[int, int]()) # revealed: C[Unknown] | ^^^ - | ``` @@ -123,7 +122,6 @@ error[invalid-type-arguments]: Type `str` is not assignable to upper bound `int` | 14 | BoundedT = TypeVar("BoundedT", bound=int) | -------- Type variable defined here - | ``` @@ -138,7 +136,6 @@ error[invalid-type-arguments]: Type `int | str` is not assignable to upper bound | 14 | BoundedT = TypeVar("BoundedT", bound=int) | -------- Type variable defined here - | info: element `str` of union `int | str` is not assignable to `int` ``` @@ -154,7 +151,6 @@ error[invalid-type-arguments]: Type `object` does not satisfy constraints `int`, | 34 | ConstrainedT = TypeVar("ConstrainedT", int, str) | ------------ Type variable defined here - | ``` @@ -164,7 +160,6 @@ error[invalid-type-arguments]: Too many type arguments to class `WithDefault`: e | 60 | reveal_type(WithDefault[str, str, str]()) # revealed: WithDefault[Unknown, Unknown] | ^^^ - | ``` @@ -181,7 +176,6 @@ error[invalid-generic-class]: Default of `WithDefaultT2` cannot reference later | ----------------------------------------------------- `WithDefaultT1` defined here 64 | WithDefaultT2 = TypeVar("WithDefaultT2", default=WithDefaultT1) | --------------------------------------------------------------- `WithDefaultT2` defined here - | ``` @@ -198,7 +192,6 @@ error[invalid-generic-class]: Default of `WithDefaultT2` cannot reference later | ----------------------------------------------------- `WithDefaultT1` defined here 64 | WithDefaultT2 = TypeVar("WithDefaultT2", default=WithDefaultT1) | --------------------------------------------------------------- `WithDefaultT2` defined here - | ``` @@ -213,6 +206,5 @@ error[invalid-generic-class]: Default of `Start2T` cannot reference out-of-scope | 81 | Start2T = TypeVar("Start2T", default="StopT") | --------------------------------------------- `Start2T` defined here - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___PEP\342\200\246_-_Default_type_paramet\342\200\246_(6bb09b09c131074).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___PEP\342\200\246_-_Default_type_paramet\342\200\246_(6bb09b09c131074).snap" index c77a605eb2..281636a2c5 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___PEP\342\200\246_-_Default_type_paramet\342\200\246_(6bb09b09c131074).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___PEP\342\200\246_-_Default_type_paramet\342\200\246_(6bb09b09c131074).snap" @@ -48,74 +48,69 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/pep695/classes. ``` error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter - --> src/mdtest_snippet.py:2:11 + --> src/mdtest_snippet.py:2:16 | 2 | class Foo[*Ts, T = int]: ... | --- ^^^^^^^ `T` has a default | | | `Ts` is a TypeVarTuple - | info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple ``` ``` error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter - --> src/mdtest_snippet.py:5:15 + --> src/mdtest_snippet.py:5:20 | 5 | class Bar[T1, *Ts, T2 = int]: ... | --- ^^^^^^^^ `T2` has a default | | | `Ts` is a TypeVarTuple - | info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple ``` ``` error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter - --> src/mdtest_snippet.py:8:11 + --> src/mdtest_snippet.py:8:16 | 8 | class Baz[*Ts, T1 = int, T2 = str]: ... | --- ^^^^^^^^ -------- `T2` also has a default | | | | | `T1` has a default | `Ts` is a TypeVarTuple - | info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple ``` ``` error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter - --> src/mdtest_snippet.py:15:11 + --> src/mdtest_snippet.py:15:16 | 15 | class Qux[*Ts, **P = [int, str]]: ... | --- ^^^^^^^^^^^^^^^^ `P` has a default | | | `Ts` is a TypeVarTuple - | info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple ``` ``` error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter - --> src/mdtest_snippet.py:18:12 + --> src/mdtest_snippet.py:18:17 | 18 | class Quux[*Ts, T1 = int, **P = [int, str]]: ... | --- ^^^^^^^^ ---------------- `P` also has a default | | | | | `T1` has a default | `Ts` is a TypeVarTuple - | info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple ``` ``` error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter - --> src/mdtest_snippet.py:21:13 + --> src/mdtest_snippet.py:21:18 | 21 | class Corge[*Ts, T1 = int, T2 = str, **P = [int, str]]: ... | --- ^^^^^^^^ -------- ---------------- `P` also has a default @@ -123,33 +118,30 @@ error[invalid-type-variable-default]: Type parameters with defaults cannot follo | | | `T2` also has a default | | `T1` has a default | `Ts` is a TypeVarTuple - | info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple ``` ``` error[invalid-type-form]: Generic class `Grault` cannot have multiple `TypeVarTuple` type parameters - --> src/mdtest_snippet.py:25:14 + --> src/mdtest_snippet.py:25:19 | 25 | class Grault[*Us, *Ts = *tuple[int, str]]: ... | --- ^^^^^^^^^^^^^^^^^^^^^^ `Ts` is an additional TypeVarTuple | | | `Us` is the first TypeVarTuple - | info: See https://typing.python.org/en/latest/spec/generics.html#multiple-type-variable-tuples-not-allowed ``` ``` error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter - --> src/mdtest_snippet.py:25:14 + --> src/mdtest_snippet.py:25:19 | 25 | class Grault[*Us, *Ts = *tuple[int, str]]: ... | --- ^^^^^^^^^^^^^^^^^^^^^^ `Ts` has a default | | | `Us` is a TypeVarTuple - | info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___PEP\342\200\246_-_Diagnostics_for_bad_\342\200\246_(cf706b07cf0ec31f).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___PEP\342\200\246_-_Diagnostics_for_bad_\342\200\246_(cf706b07cf0ec31f).snap" index 8f16be3264..65e0a3b234 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___PEP\342\200\246_-_Diagnostics_for_bad_\342\200\246_(cf706b07cf0ec31f).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___PEP\342\200\246_-_Diagnostics_for_bad_\342\200\246_(cf706b07cf0ec31f).snap" @@ -42,7 +42,6 @@ error[invalid-type-arguments]: Type `int` is not assignable to upper bound `str` | 1 | class Bounded[T: str]: | - Type variable defined here - | ``` @@ -57,6 +56,5 @@ error[invalid-type-arguments]: Type `str` does not satisfy constraints `int`, `b | 4 | class Constrained[U: (int, bytes)]: | - Type variable defined here - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___PEP\342\200\246_-_Scoping_of_typevars_-_No_back-references_(9051beb16a623d36).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___PEP\342\200\246_-_Scoping_of_typevars_-_No_back-references_(9051beb16a623d36).snap" index 7841c6b034..a3573d2ea0 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___PEP\342\200\246_-_Scoping_of_typevars_-_No_back-references_(9051beb16a623d36).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___PEP\342\200\246_-_Scoping_of_typevars_-_No_back-references_(9051beb16a623d36).snap" @@ -47,7 +47,6 @@ error[invalid-type-variable-bound]: TypeVar upper bound cannot be generic | 2 | class C[S: T, T]: | ^ - | ``` @@ -57,7 +56,6 @@ error[invalid-type-variable-bound]: TypeVar upper bound cannot be generic | 6 | class D[S, T: S]: | ^ - | ``` @@ -67,7 +65,6 @@ error[invalid-type-variable-constraints]: TypeVar constraint cannot be generic | 10 | class E[S: (int, T), T]: | ^ - | ``` @@ -79,7 +76,6 @@ error[invalid-generic-class]: Default of `S` cannot reference later type paramet | ^^^ ----- ------- `T` defined here | | | `S` defined here - | ``` @@ -91,6 +87,5 @@ error[invalid-generic-class]: Default of `S` cannot reference later type paramet | ^^^^^^^ ----------- ------- `T` defined here | | | `S` defined here - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/concatenate.md_-_`typing.Concatenate`_-_Invalid_uses_of_`Con\342\200\246_-_Last_argument_must_b\342\200\246_(dc429fc3e8c18eaf).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/concatenate.md_-_`typing.Concatenate`_-_Invalid_uses_of_`Con\342\200\246_-_Last_argument_must_b\342\200\246_(dc429fc3e8c18eaf).snap" index a1382a8091..9b98036efb 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/concatenate.md_-_`typing.Concatenate`_-_Invalid_uses_of_`Con\342\200\246_-_Last_argument_must_b\342\200\246_(dc429fc3e8c18eaf).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/concatenate.md_-_`typing.Concatenate`_-_Invalid_uses_of_`Con\342\200\246_-_Last_argument_must_b\342\200\246_(dc429fc3e8c18eaf).snap" @@ -45,7 +45,6 @@ error[invalid-type-arguments]: The last argument to `typing.Concatenate` must be | 7 | def _(c: Callable[Concatenate[int, str], bool]): ... | ^^^ Got `str` - | ``` @@ -55,7 +54,6 @@ error[invalid-type-arguments]: The last argument to `typing.Concatenate` must be | 10 | reveal_type(Foo[Concatenate[int, str]].attr) # revealed: (...) -> None | ^^^ Got `str` - | ``` @@ -65,7 +63,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 13 | reveal_type(Foo[Concatenate[int, Concatenate]].attr) # revealed: (...) -> None | ^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter @@ -78,7 +75,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 16 | reveal_type(Foo[Concatenate[int, Concatenate[()]]].attr) # revealed: (...) -> None | ^^^^^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter @@ -91,7 +87,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 19 | reveal_type(Foo[Concatenate[int, Concatenate[int]]].attr) # revealed: (...) -> None | ^^^^^^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter @@ -104,7 +99,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 22 | reveal_type(Foo[Concatenate[int, Concatenate[int, str]]].attr) # revealed: (...) -> None | ^^^^^^^^^^^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/concatenate.md_-_`typing.Concatenate`_-_Invalid_uses_of_`Con\342\200\246_-_Nested_`Concatenate`_(86093b62e6e6874c).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/concatenate.md_-_`typing.Concatenate`_-_Invalid_uses_of_`Con\342\200\246_-_Nested_`Concatenate`_(86093b62e6e6874c).snap" index 078e61c274..ea11c86add 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/concatenate.md_-_`typing.Concatenate`_-_Invalid_uses_of_`Con\342\200\246_-_Nested_`Concatenate`_(86093b62e6e6874c).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/concatenate.md_-_`typing.Concatenate`_-_Invalid_uses_of_`Con\342\200\246_-_Nested_`Concatenate`_(86093b62e6e6874c).snap" @@ -34,7 +34,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 5 | c: Callable[Concatenate[Concatenate[int, ...], P], None], | ^^^^^^^^^^^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter @@ -47,7 +46,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 7 | d: Callable[Concatenate[Concatenate, P], int], | ^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter @@ -60,7 +58,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 9 | e: Callable[Concatenate[int, Concatenate[int, ...]], None], | ^^^^^^^^^^^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/concatenate.md_-_`typing.Concatenate`_-_Invalid_uses_of_`Con\342\200\246_-_Standalone_annotatio\342\200\246_(bb5fe70ded875e4).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/concatenate.md_-_`typing.Concatenate`_-_Invalid_uses_of_`Con\342\200\246_-_Standalone_annotatio\342\200\246_(bb5fe70ded875e4).snap" index 51a5e5ae30..fd0155652e 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/concatenate.md_-_`typing.Concatenate`_-_Invalid_uses_of_`Con\342\200\246_-_Standalone_annotatio\342\200\246_(bb5fe70ded875e4).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/concatenate.md_-_`typing.Concatenate`_-_Invalid_uses_of_`Con\342\200\246_-_Standalone_annotatio\342\200\246_(bb5fe70ded875e4).snap" @@ -51,7 +51,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 6 | def invalid0(x: Concatenate): ... | ^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter @@ -64,7 +63,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 9 | def invalid1(x: Concatenate[int]): ... | ^^^^^^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter @@ -77,7 +75,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 12 | def invalid2(x: Concatenate[int, ...]) -> None: ... | ^^^^^^^^^^^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter @@ -90,7 +87,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 15 | def invalid3() -> Concatenate[int, ...]: ... | ^^^^^^^^^^^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter @@ -103,7 +99,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 18 | def invalid4() -> Concatenate[()]: ... | ^^^^^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter @@ -116,7 +111,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 21 | a: Concatenate | ^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter @@ -129,7 +123,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 25 | b: Concatenate[int, P] | ^^^^^^^^^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter @@ -142,7 +135,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 28 | def invalid5[**P](x: Foo[Concatenate[P, ...]]) -> None: ... | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/concatenate.md_-_`typing.Concatenate`_-_Invalid_uses_of_`Con\342\200\246_-_Too_few_arguments_(efcf77cdbde3ff86).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/concatenate.md_-_`typing.Concatenate`_-_Invalid_uses_of_`Con\342\200\246_-_Too_few_arguments_(efcf77cdbde3ff86).snap" index ee82397533..b39470307c 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/concatenate.md_-_`typing.Concatenate`_-_Invalid_uses_of_`Con\342\200\246_-_Too_few_arguments_(efcf77cdbde3ff86).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/concatenate.md_-_`typing.Concatenate`_-_Invalid_uses_of_`Con\342\200\246_-_Too_few_arguments_(efcf77cdbde3ff86).snap" @@ -74,7 +74,6 @@ error[invalid-type-form]: `typing.Concatenate` requires at least 2 arguments whe | 8 | a: Callable[Concatenate[()], int], | ^^^^^^^^^^^^^^^ - | ``` @@ -84,7 +83,6 @@ error[invalid-type-form]: `typing.Concatenate` requires at least 2 arguments whe | 10 | b: Callable[Concatenate[int], int], | ^^^^^^^^^^^^^^^^ - | ``` @@ -94,7 +92,6 @@ error[invalid-type-form]: `typing.Concatenate` requires at least 2 arguments whe | 12 | c: Callable[Concatenate[(int,)], int], | ^^^^^^^^^^^^^^^^^^^ - | ``` @@ -104,7 +101,6 @@ error[invalid-type-form]: `typing.Concatenate` requires at least two arguments w | 14 | d: Callable[Concatenate, int], | ^^^^^^^^^^^ - | ``` @@ -114,7 +110,6 @@ error[invalid-type-form]: `typing.Concatenate` requires at least 2 arguments whe | 21 | reveal_type(Foo[Concatenate[()]].attr) # revealed: (...) -> None | ^^^^^^^^^^^^^^^ - | ``` @@ -124,7 +119,6 @@ error[invalid-type-form]: `typing.Concatenate` requires at least 2 arguments whe | 23 | reveal_type(Foo[Concatenate[int]].attr) # revealed: (...) -> None | ^^^^^^^^^^^^^^^^ - | ``` @@ -134,7 +128,6 @@ error[invalid-type-form]: `typing.Concatenate` requires at least 2 arguments whe | 25 | reveal_type(Foo[Concatenate[(int,)]].attr) # revealed: (...) -> None | ^^^^^^^^^^^^^^^^^^^ - | ``` @@ -144,7 +137,6 @@ error[invalid-type-form]: `typing.Concatenate` requires at least two arguments w | 27 | reveal_type(Foo[Concatenate].attr) # revealed: (...) -> None | ^^^^^^^^^^^ - | ``` @@ -154,7 +146,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 29 | reveal_type(Foo[[Concatenate]].attr) # revealed: (Unknown, /) -> None | ^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter @@ -167,7 +158,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 31 | reveal_type(Foo[[Concatenate, int]].attr) # revealed: (Unknown, int, /) -> None | ^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter @@ -180,7 +170,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 34 | reveal_type(Foo[[Concatenate[int], str]].attr) # revealed: (Unknown, str, /) -> None | ^^^^^^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter @@ -193,7 +182,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 36 | reveal_type(Foo[[Concatenate[int, str], str]].attr) # revealed: (Unknown, str, /) -> None | ^^^^^^^^^^^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter @@ -206,7 +194,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 38 | reveal_type(Foo[[Concatenate[()], str]].attr) # revealed: (Unknown, str, /) -> None | ^^^^^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter @@ -219,7 +206,6 @@ error[invalid-type-form]: `typing.Concatenate` requires at least two arguments w | 48 | reveal_type(Bar[Concatenate, Concatenate].a) # revealed: (...) -> int | ^^^^^^^^^^^ - | ``` @@ -229,7 +215,6 @@ error[invalid-type-form]: `typing.Concatenate` requires at least two arguments w | 48 | reveal_type(Bar[Concatenate, Concatenate].a) # revealed: (...) -> int | ^^^^^^^^^^^ - | ``` @@ -239,7 +224,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 51 | reveal_type(Bar[[Concatenate], [Concatenate]].a) # revealed: (Unknown, /) -> int | ^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter @@ -252,7 +236,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 51 | reveal_type(Bar[[Concatenate], [Concatenate]].a) # revealed: (Unknown, /) -> int | ^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/deprecated.md_-_Tests_for_the_`@depr\342\200\246_-_Introduction_(cff2724f4c9d28c4).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/deprecated.md_-_Tests_for_the_`@depr\342\200\246_-_Introduction_(cff2724f4c9d28c4).snap" index 45e9ea1687..4564dd5749 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/deprecated.md_-_Tests_for_the_`@depr\342\200\246_-_Introduction_(cff2724f4c9d28c4).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/deprecated.md_-_Tests_for_the_`@depr\342\200\246_-_Introduction_(cff2724f4c9d28c4).snap" @@ -45,7 +45,6 @@ warning[deprecated]: The function `myfunc` is deprecated | 6 | myfunc(1) # error: [deprecated] "use OtherClass" | ^^^^^^ use OtherClass - | ``` @@ -55,7 +54,6 @@ warning[deprecated]: The class `MyClass` is deprecated | 12 | MyClass() # error: [deprecated] "use BetterClass" | ^^^^^^^ use BetterClass - | ``` @@ -65,7 +63,6 @@ warning[deprecated]: The function `afunc` is deprecated | 21 | MyClass.afunc() # error: [deprecated] "use something else" | ^^^^^ use something else - | ``` @@ -75,6 +72,5 @@ warning[deprecated]: The function `amethod` is deprecated | 22 | MyClass().amethod() # error: [deprecated] "don't use this!" | ^^^^^^^ don't use this! - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/deprecated.md_-_Tests_for_the_`@depr\342\200\246_-_Syntax_(142fa2948c3c6cf1).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/deprecated.md_-_Tests_for_the_`@depr\342\200\246_-_Syntax_(142fa2948c3c6cf1).snap" index ed263f9d3e..ff761dd22c 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/deprecated.md_-_Tests_for_the_`@depr\342\200\246_-_Syntax_(142fa2948c3c6cf1).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/deprecated.md_-_Tests_for_the_`@depr\342\200\246_-_Syntax_(142fa2948c3c6cf1).snap" @@ -73,7 +73,6 @@ error[invalid-argument-type]: Argument to class `deprecated` is incorrect | 3 | @deprecated # error: [invalid-argument-type] "LiteralString" | ^^^^^^^^^^^ Expected `LiteralString`, found `def invalid_deco()` - | ``` @@ -83,13 +82,11 @@ error[missing-argument]: No argument provided for required parameter `arg` of bo | 6 | invalid_deco() # error: [missing-argument] | ^^^^^^^^^^^^^^ - | info: Parameter declared here - --> stdlib/typing_extensions.byi:1205:28 + --> stdlib/typing_extensions.byi:1204:28 | -1205 | def __call__(self, arg: _T, /) -> _T +1204 | def __call__(self, arg: _T, /) -> _T | ^^^^^^^ - | ``` @@ -99,7 +96,6 @@ error[missing-argument]: No argument provided for required parameter `message` o | 9 | @deprecated() # error: [missing-argument] "message" | ^^^^^^^^^^^^ - | ``` @@ -109,7 +105,6 @@ warning[deprecated]: The function `invalid_deco` is deprecated | 20 | invalid_deco() # error: [deprecated] "message" | ^^^^^^^^^^^^ message - | ``` @@ -119,7 +114,6 @@ warning[deprecated]: The function `valid_deco` is deprecated | 29 | valid_deco() # error: [deprecated] | ^^^^^^^^^^ - | ``` @@ -129,7 +123,6 @@ error[invalid-argument-type]: Argument to class `deprecated` is incorrect | 35 | @deprecated(opaque()) # error: [invalid-argument-type] "LiteralString" | ^^^^^^^^ Expected `LiteralString`, found `str` - | ``` @@ -139,7 +132,6 @@ error[unknown-argument]: Argument `dsfsdf` does not match any known parameter of | 41 | @deprecated("some message", dsfsdf="whatever") # error: [unknown-argument] "dsfsdf" | ^^^^^^^^^^^^^^^^^ - | ``` @@ -149,6 +141,5 @@ warning[deprecated]: The function `valid_deco` is deprecated | 50 | valid_deco() # error: [deprecated] "some message" | ^^^^^^^^^^ some message - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Abstract_method_in_g\342\200\246_(6d8b024dda7ced11).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Abstract_method_in_g\342\200\246_(6d8b024dda7ced11).snap" index 97d474c324..d5efd8ded3 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Abstract_method_in_g\342\200\246_(6d8b024dda7ced11).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Abstract_method_in_g\342\200\246_(6d8b024dda7ced11).snap" @@ -32,7 +32,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/final.md ``` error[abstract-method-in-final-class]: Final class `Child` has unimplemented abstract methods - --> src/mdtest_snippet.py:5:5 + --> src/mdtest_snippet.py:12:7 | 5 | / @abstractmethod 6 | | def method(self) -> int: ... @@ -45,6 +45,5 @@ error[abstract-method-in-final-class]: Final class `Child` has unimplemented abs | ------ 12 | class Child(Parent): # error: [abstract-method-in-final-class] | ^^^^^ `method` is unimplemented - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Basic_case_with_ABC_(21e412599c45972a).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Basic_case_with_ABC_(21e412599c45972a).snap" index 4a35aa96b9..7252403011 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Basic_case_with_ABC_(21e412599c45972a).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Basic_case_with_ABC_(21e412599c45972a).snap" @@ -30,7 +30,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/final.md ``` error[abstract-method-in-final-class]: Final class `Derived` has unimplemented abstract methods - --> src/mdtest_snippet.py:5:5 + --> src/mdtest_snippet.py:10:7 | 5 | / @abstractmethod 6 | | def foo(self) -> int: @@ -41,6 +41,5 @@ error[abstract-method-in-final-class]: Final class `Derived` has unimplemented a | ------ 10 | class Derived(Base): # error: [abstract-method-in-final-class] "Final class `Derived` has unimplemented abstract method `foo`" | ^^^^^^^ `foo` is unimplemented - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Diagnostic_when_ther\342\200\246_(ecae0f4510696c95).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Diagnostic_when_ther\342\200\246_(ecae0f4510696c95).snap" index 6d678dfab6..3ecf3bac7c 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Diagnostic_when_ther\342\200\246_(ecae0f4510696c95).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Diagnostic_when_ther\342\200\246_(ecae0f4510696c95).snap" @@ -45,17 +45,16 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/final.md ``` error[abstract-method-in-final-class]: Final class `Abstract` has unimplemented abstract methods - --> src/mdtest_snippet.py:4:1 + --> src/mdtest_snippet.py:6:7 | 4 | @final | ------ -5 | # error: [abstract-method-in-final-class] "Final class `Abstract` has unimplemented abstract methods `aaaaaaaaaa`, `bbbbbbbb`, `ccccccc… +5 | # error: [abstract-method-in-final-class] "Final class `Abstract` has unimplemented abstract methods `aaaaaaaaaa`, `bbbbbbbb`, `ccccc… 6 | class Abstract(ABC): | ^^^^^^^^ Abstract methods `aaaaaaaaaa`, `bbbbbbbb`, `cccccccc`, `ddddddddd`, `eeeeeeeee`, `ffffffff`, `ggggggg`, `hhhhhhhh`, `iiiiiiiii` and `kkkkkkkkkk` are unimplemented 7 | / @abstractmethod 8 | | def aaaaaaaaaa(self) -> int: ... | |____________________________________- `aaaaaaaaaa` declared as abstract - | info: rule `abstract-method-in-final-class` is enabled by default ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Diagnostic_when_ther\342\200\246_(f807ff3716d8ab0d).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Diagnostic_when_ther\342\200\246_(f807ff3716d8ab0d).snap" index 68cb8fa1f5..f1323f512d 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Diagnostic_when_ther\342\200\246_(f807ff3716d8ab0d).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Diagnostic_when_ther\342\200\246_(f807ff3716d8ab0d).snap" @@ -45,17 +45,16 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/final.md ``` error[abstract-method-in-final-class]: Final class `Abstract` has unimplemented abstract methods - --> src/mdtest_snippet.py:4:1 + --> src/mdtest_snippet.py:6:7 | 4 | @final | ------ -5 | # error: [abstract-method-in-final-class] "Final class `Abstract` has 10 unimplemented abstract methods, including `aaaaaaaaaa`, `bbbbb… +5 | # error: [abstract-method-in-final-class] "Final class `Abstract` has 10 unimplemented abstract methods, including `aaaaaaaaaa`, `bbb… 6 | class Abstract(ABC): | ^^^^^^^^ 10 abstract methods are unimplemented, including `aaaaaaaaaa`, `bbbbbbbb` and `cccccccc` 7 | / @abstractmethod 8 | | def aaaaaaaaaa(self) -> int: ... | |____________________________________- `aaaaaaaaaa` declared as abstract - | info: Use `--verbose` to see all 10 unimplemented abstract methods ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Multiple_abstract_me\342\200\246_(feafee9a4abbe8d1).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Multiple_abstract_me\342\200\246_(feafee9a4abbe8d1).snap" index a48f5f60c8..05eaf2da1e 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Multiple_abstract_me\342\200\246_(feafee9a4abbe8d1).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Multiple_abstract_me\342\200\246_(feafee9a4abbe8d1).snap" @@ -41,7 +41,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/final.md ``` error[abstract-method-in-final-class]: Final class `MissingAll` has unimplemented abstract methods - --> src/mdtest_snippet.py:12:1 + --> src/mdtest_snippet.py:13:7 | 12 | @final | ------ @@ -53,13 +53,12 @@ error[abstract-method-in-final-class]: Final class `MissingAll` has unimplemente 5 | / @abstractmethod 6 | | def foo(self) -> int: ... | |_____________________________- `foo` declared as abstract on superclass `Base` - | ``` ``` error[abstract-method-in-final-class]: Final class `PartiallyImplemented` has unimplemented abstract methods - --> src/mdtest_snippet.py:16:1 + --> src/mdtest_snippet.py:17:7 | 16 | @final | ------ @@ -71,6 +70,5 @@ error[abstract-method-in-final-class]: Final class `PartiallyImplemented` has un 9 | / @abstractmethod 10 | | def baz(self) -> None: ... | |______________________________- `baz` declared as abstract on superclass `Base` - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Protocol_with_implic\342\200\246_(e373f31c7a7d88e7).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Protocol_with_implic\342\200\246_(e373f31c7a7d88e7).snap" index 599aeabe42..e1896646dd 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Protocol_with_implic\342\200\246_(e373f31c7a7d88e7).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Protocol_with_implic\342\200\246_(e373f31c7a7d88e7).snap" @@ -169,7 +169,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/final.md ``` error[abstract-method-in-final-class]: Final class `Q` has unimplemented abstract methods - --> src/mdtest_snippet.py:11:5 + --> src/mdtest_snippet.py:14:7 | 11 | def still_abstractmethod(self): ... | ----------------------------------- `still_abstractmethod` declared as abstract on superclass `P` @@ -178,20 +178,18 @@ error[abstract-method-in-final-class]: Final class `Q` has unimplemented abstrac | ------ 14 | class Q(P): ... # error: [abstract-method-in-final-class] | ^ `still_abstractmethod` is unimplemented - | info: `P.still_abstractmethod` is implicitly abstract because `P` is a `Protocol` class and `still_abstractmethod` lacks an implementation --> src/mdtest_snippet.py:3:7 | 3 | class P(Protocol): | ----------- `P` declared here - | help: Change the body of `still_abstractmethod` to `return` or `return None` if it was not intended to be abstract ``` ``` error[abstract-method-in-final-class]: Final class `S` has unimplemented abstract methods - --> src/mdtest_snippet.py:18:5 + --> src/mdtest_snippet.py:21:7 | 18 | def also_still_abstractmethod(self) -> None: ... | ------------------------------------------------ `also_still_abstractmethod` declared as abstract on superclass `R` @@ -200,20 +198,18 @@ error[abstract-method-in-final-class]: Final class `S` has unimplemented abstrac | ------ 21 | class S(R): ... # error: [abstract-method-in-final-class] | ^ `also_still_abstractmethod` is unimplemented - | info: `R.also_still_abstractmethod` is implicitly abstract because `R` is a `Protocol` class and `also_still_abstractmethod` lacks an implementation --> src/mdtest_snippet.py:16:7 | 16 | class R(Protocol): | ----------- `R` declared here - | help: Change the body of `also_still_abstractmethod` to `return` or `return None` if it was not intended to be abstract ``` ``` error[abstract-method-in-final-class]: Final class `RaisesSub` has unimplemented abstract methods - --> src/mdtest_snippet.py:24:5 + --> src/mdtest_snippet.py:28:7 | 24 | / def even_this_is_abstract(self): 25 | | raise NotImplementedError @@ -223,19 +219,17 @@ error[abstract-method-in-final-class]: Final class `RaisesSub` has unimplemented | ------ 28 | class RaisesSub(Raises): ... # error: [abstract-method-in-final-class] | ^^^^^^^^^ `even_this_is_abstract` is unimplemented - | info: `Raises.even_this_is_abstract` is implicitly abstract because `Raises` is a `Protocol` class and `even_this_is_abstract` lacks an implementation --> src/mdtest_snippet.py:23:7 | 23 | class Raises(Protocol): | ---------------- `Raises` declared here - | ``` ``` error[abstract-method-in-final-class]: Final class `AlsoRaisesSub` has unimplemented abstract methods - --> src/mdtest_snippet.py:31:5 + --> src/mdtest_snippet.py:35:7 | 31 | / def also_abstractmethod(self) -> Never: 32 | | raise NotImplementedError @@ -245,19 +239,17 @@ error[abstract-method-in-final-class]: Final class `AlsoRaisesSub` has unimpleme | ------ 35 | class AlsoRaisesSub(AlsoRaises): ... # error: [abstract-method-in-final-class] | ^^^^^^^^^^^^^ `also_abstractmethod` is unimplemented - | info: `AlsoRaises.also_abstractmethod` is implicitly abstract because `AlsoRaises` is a `Protocol` class and `also_abstractmethod` lacks an implementation --> src/mdtest_snippet.py:30:7 | 30 | class AlsoRaises(Protocol): | -------------------- `AlsoRaises` declared here - | ``` ``` error[abstract-method-in-final-class]: Final class `StrangeSub` has unimplemented abstract methods - --> src/mdtest_snippet.py:41:9 + --> src/mdtest_snippet.py:45:11 | 41 | / def weird_abstractmethod(self): 42 | | raise x @@ -267,19 +259,17 @@ error[abstract-method-in-final-class]: Final class `StrangeSub` has unimplemente | ------ 45 | class StrangeSub(Strange): ... # error: [abstract-method-in-final-class] | ^^^^^^^^^^ `weird_abstractmethod` is unimplemented - | info: `Strange.weird_abstractmethod` is implicitly abstract because `Strange` is a `Protocol` class and `weird_abstractmethod` lacks an implementation --> src/mdtest_snippet.py:40:11 | 40 | class Strange(Protocol): | ----------------- `Strange` declared here - | ``` ``` error[abstract-method-in-final-class]: Final class `HasOverloadSub` has unimplemented abstract methods - --> src/mdtest_snippet.py:51:9 + --> src/mdtest_snippet.py:54:7 | 51 | def foo(self, x: int) -> str: ... | --- `foo` declared as abstract on superclass `HasOverloads` @@ -288,19 +278,17 @@ error[abstract-method-in-final-class]: Final class `HasOverloadSub` has unimplem | ------ 54 | class HasOverloadSub(HasOverloads): ... # error: [abstract-method-in-final-class] | ^^^^^^^^^^^^^^ `foo` is unimplemented - | info: `HasOverloads.foo` is implicitly abstract because `HasOverloads` is a `Protocol` class and `foo` lacks an implementation --> src/mdtest_snippet.py:47:7 | 47 | class HasOverloads(Protocol): | ---------------------- `HasOverloads` declared here - | ``` ``` error[abstract-method-in-final-class]: Final class `HasAbstractSub` has unimplemented abstract methods - --> src/mdtest_snippet.py:122:1 + --> src/mdtest_snippet.py:123:7 | 122 | @final | ------ @@ -311,19 +299,17 @@ error[abstract-method-in-final-class]: Final class `HasAbstractSub` has unimplem | 72 | def a(self) -> int: ... | ----------------------- `a` declared as abstract on superclass `HasAbstract` - | info: `HasAbstract.a` is implicitly abstract because `HasAbstract` is a `Protocol` class and `a` lacks an implementation --> src/mdtest_snippet.py:71:7 | 71 | class HasAbstract(Protocol): | --------------------- `HasAbstract` declared here - | ``` ``` error[abstract-method-in-final-class]: Final class `HasAbstract2Sub` has unimplemented abstract methods - --> src/mdtest_snippet.py:125:1 + --> src/mdtest_snippet.py:126:7 | 125 | @final | ------ @@ -335,19 +321,17 @@ error[abstract-method-in-final-class]: Final class `HasAbstract2Sub` has unimple 75 | / def a(self) -> int: 76 | | pass | |____________- `a` declared as abstract on superclass `HasAbstract2` - | info: `HasAbstract2.a` is implicitly abstract because `HasAbstract2` is a `Protocol` class and `a` lacks an implementation --> src/mdtest_snippet.py:74:7 | 74 | class HasAbstract2(Protocol): | ---------------------- `HasAbstract2` declared here - | ``` ``` error[abstract-method-in-final-class]: Final class `HasAbstract3Sub` has unimplemented abstract methods - --> src/mdtest_snippet.py:128:1 + --> src/mdtest_snippet.py:129:7 | 128 | @final | ------ @@ -358,19 +342,17 @@ error[abstract-method-in-final-class]: Final class `HasAbstract3Sub` has unimple | 83 | def a(self) -> int: | ------------------ `a` declared as abstract on superclass `HasAbstract4` - | info: `HasAbstract4.a` is implicitly abstract because `HasAbstract4` is a `Protocol` class and `a` lacks an implementation --> src/mdtest_snippet.py:82:7 | 82 | class HasAbstract4(Protocol): | ---------------------- `HasAbstract4` declared here - | ``` ``` error[abstract-method-in-final-class]: Final class `HasAbstract4Sub` has unimplemented abstract methods - --> src/mdtest_snippet.py:131:1 + --> src/mdtest_snippet.py:132:7 | 131 | @final | ------ @@ -381,19 +363,17 @@ error[abstract-method-in-final-class]: Final class `HasAbstract4Sub` has unimple | 83 | def a(self) -> int: | ------------------ `a` declared as abstract on superclass `HasAbstract4` - | info: `HasAbstract4.a` is implicitly abstract because `HasAbstract4` is a `Protocol` class and `a` lacks an implementation --> src/mdtest_snippet.py:82:7 | 82 | class HasAbstract4(Protocol): | ---------------------- `HasAbstract4` declared here - | ``` ``` error[abstract-method-in-final-class]: Final class `HasAbstract5Sub` has unimplemented abstract methods - --> src/mdtest_snippet.py:134:1 + --> src/mdtest_snippet.py:135:7 | 134 | @final | ------ @@ -404,19 +384,17 @@ error[abstract-method-in-final-class]: Final class `HasAbstract5Sub` has unimple | 88 | def a(self) -> int: | ------------------ `a` declared as abstract on superclass `HasAbstract5` - | info: `HasAbstract5.a` is implicitly abstract because `HasAbstract5` is a `Protocol` class and `a` lacks an implementation --> src/mdtest_snippet.py:87:7 | 87 | class HasAbstract5(Protocol): | ---------------------- `HasAbstract5` declared here - | ``` ``` error[abstract-method-in-final-class]: Final class `HasAbstract6Sub` has unimplemented abstract methods - --> src/mdtest_snippet.py:137:1 + --> src/mdtest_snippet.py:138:7 | 137 | @final | ------ @@ -427,19 +405,17 @@ error[abstract-method-in-final-class]: Final class `HasAbstract6Sub` has unimple | 93 | def a(self) -> int: | ------------------ `a` declared as abstract on superclass `HasAbstract6` - | info: `HasAbstract6.a` is implicitly abstract because `HasAbstract6` is a `Protocol` class and `a` lacks an implementation --> src/mdtest_snippet.py:92:7 | 92 | class HasAbstract6(Protocol): | ---------------------- `HasAbstract6` declared here - | ``` ``` error[abstract-method-in-final-class]: Final class `HasAbstract7Sub` has unimplemented abstract methods - --> src/mdtest_snippet.py:140:1 + --> src/mdtest_snippet.py:141:7 | 140 | @final | ------ @@ -451,19 +427,17 @@ error[abstract-method-in-final-class]: Final class `HasAbstract7Sub` has unimple 105 | / def a(self) -> int: 106 | | raise NotImplementedError | |_________________________________- `a` declared as abstract on superclass `HasAbstract7` - | info: `HasAbstract7.a` is implicitly abstract because `HasAbstract7` is a `Protocol` class and `a` lacks an implementation --> src/mdtest_snippet.py:104:7 | 104 | class HasAbstract7(Protocol): | ---------------------- `HasAbstract7` declared here - | ``` ``` error[abstract-method-in-final-class]: Final class `HasAbstract8Sub` has unimplemented abstract methods - --> src/mdtest_snippet.py:143:1 + --> src/mdtest_snippet.py:144:7 | 143 | @final | ------ @@ -475,19 +449,17 @@ error[abstract-method-in-final-class]: Final class `HasAbstract8Sub` has unimple 109 | / def a(self) -> int: 110 | | raise NotImplementedError() | |___________________________________- `a` declared as abstract on superclass `HasAbstract8` - | info: `HasAbstract8.a` is implicitly abstract because `HasAbstract8` is a `Protocol` class and `a` lacks an implementation --> src/mdtest_snippet.py:108:7 | 108 | class HasAbstract8(Protocol): | ---------------------- `HasAbstract8` declared here - | ``` ``` error[abstract-method-in-final-class]: Final class `HasAbstract9Sub` has unimplemented abstract methods - --> src/mdtest_snippet.py:146:1 + --> src/mdtest_snippet.py:147:7 | 146 | @final | ------ @@ -498,19 +470,17 @@ error[abstract-method-in-final-class]: Final class `HasAbstract9Sub` has unimple | 113 | def a(self) -> int: | ------------------ `a` declared as abstract on superclass `HasAbstract9` - | info: `HasAbstract9.a` is implicitly abstract because `HasAbstract9` is a `Protocol` class and `a` lacks an implementation --> src/mdtest_snippet.py:112:7 | 112 | class HasAbstract9(Protocol): | ---------------------- `HasAbstract9` declared here - | ``` ``` error[abstract-method-in-final-class]: Final class `HasAbstract10Sub` has unimplemented abstract methods - --> src/mdtest_snippet.py:149:1 + --> src/mdtest_snippet.py:150:7 | 149 | @final | ------ @@ -521,12 +491,10 @@ error[abstract-method-in-final-class]: Final class `HasAbstract10Sub` has unimpl | 118 | def a(self) -> int: | ------------------ `a` declared as abstract on superclass `HasAbstract10` - | info: `HasAbstract10.a` is implicitly abstract because `HasAbstract10` is a `Protocol` class and `a` lacks an implementation --> src/mdtest_snippet.py:117:7 | 117 | class HasAbstract10(Protocol): | ----------------------- `HasAbstract10` declared here - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_possibly-undefined\342\200\246_(fc7b496fd1986deb).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_possibly-undefined\342\200\246_(fc7b496fd1986deb).snap" index fe1a18dfe3..bde456cf82 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_possibly-undefined\342\200\246_(fc7b496fd1986deb).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_possibly-undefined\342\200\246_(fc7b496fd1986deb).snap" @@ -96,7 +96,6 @@ error[override-of-final-method]: Cannot override `A.method1` | 40 | def method1(self) -> None: ... # error: [override-of-final-method] | ^^^^^^^ Overrides a definition from superclass `A` - | info: `A.method1` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.py:8:9 | @@ -104,7 +103,6 @@ info: `A.method1` is decorated with `@final`, forbidding overrides | ------ 9 | def method1(self) -> None: ... | ------- `A.method1` defined here - | help: Remove the override of `method1` | 39 | class B(A): @@ -122,7 +120,6 @@ error[override-of-final-method]: Cannot override `A.method2` | 41 | def method2(self) -> None: ... # error: [override-of-final-method] | ^^^^^^^ Overrides a definition from superclass `A` - | info: `A.method2` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.py:18:9 | @@ -130,7 +127,6 @@ info: `A.method2` is decorated with `@final`, forbidding overrides | ------ 19 | def method2(self) -> None: ... | ------- `A.method2` defined here - | help: Remove the override of `method2` | 40 | def method1(self) -> None: ... # error: [override-of-final-method] @@ -148,7 +144,6 @@ error[override-of-final-method]: Cannot override `A.method3` | 42 | def method3(self) -> None: ... # error: [override-of-final-method] | ^^^^^^^ Overrides a definition from superclass `A` - | info: `A.method3` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.py:22:9 | @@ -156,7 +151,6 @@ info: `A.method3` is decorated with `@final`, forbidding overrides | ------ 23 | def method3(self) -> None: ... | ------- `A.method3` defined here - | help: Remove the override of `method3` | 41 | def method2(self) -> None: ... # error: [override-of-final-method] @@ -174,7 +168,6 @@ error[override-of-final-method]: Cannot override `A.method4` | 49 | method4 = 42 | ^^^^^^^ Overrides a definition from superclass `A` - | info: `A.method4` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.py:33:9 | @@ -182,7 +175,6 @@ info: `A.method4` is decorated with `@final`, forbidding overrides | ------ 34 | def method4(self) -> None: ... | ------- `A.method4` defined here - | help: Remove the override of `method4` ``` @@ -193,7 +185,6 @@ error[override-of-final-method]: Cannot override `A.method1` | 55 | def method1(self) -> None: ... # error: [override-of-final-method] | ^^^^^^^ Overrides a definition from superclass `A` - | info: `A.method1` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.py:8:9 | @@ -201,7 +192,6 @@ info: `A.method1` is decorated with `@final`, forbidding overrides | ------ 9 | def method1(self) -> None: ... | ------- `A.method1` defined here - | help: Remove the override of `method1` ``` @@ -212,7 +202,6 @@ error[override-of-final-method]: Cannot override `A.method2` | 61 | def method2(self) -> None: ... # error: [override-of-final-method] | ^^^^^^^ Overrides a definition from superclass `A` - | info: `A.method2` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.py:18:9 | @@ -220,7 +209,6 @@ info: `A.method2` is decorated with `@final`, forbidding overrides | ------ 19 | def method2(self) -> None: ... | ------- `A.method2` defined here - | help: Remove the override of `method2` ``` @@ -231,7 +219,6 @@ error[override-of-final-method]: Cannot override `A.method3` | 67 | def method3(self) -> None: ... # error: [override-of-final-method] | ^^^^^^^ Overrides a definition from superclass `A` - | info: `A.method3` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.py:22:9 | @@ -239,7 +226,6 @@ info: `A.method3` is decorated with `@final`, forbidding overrides | ------ 23 | def method3(self) -> None: ... | ------- `A.method3` defined here - | help: Remove the override of `method3` ``` @@ -250,7 +236,6 @@ error[override-of-final-method]: Cannot override `A.method4` | 71 | method4 = 42 # error: [override-of-final-method] | ^^^^^^^ Overrides a definition from superclass `A` - | info: `A.method4` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.py:33:9 | @@ -258,7 +243,6 @@ info: `A.method4` is decorated with `@final`, forbidding overrides | ------ 34 | def method4(self) -> None: ... | ------- `A.method4` defined here - | help: Remove the override of `method4` ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Cannot_override_a_me\342\200\246_(338615109711a91b).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Cannot_override_a_me\342\200\246_(338615109711a91b).snap" index b744a5683d..4e28e306ed 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Cannot_override_a_me\342\200\246_(338615109711a91b).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Cannot_override_a_me\342\200\246_(338615109711a91b).snap" @@ -136,7 +136,6 @@ error[override-of-final-method]: Cannot override `Parent.foo` | 42 | def foo(self): ... | ^^^ Overrides a definition from superclass `Parent` - | info: `Parent.foo` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.pyi:7:5 | @@ -144,7 +143,6 @@ info: `Parent.foo` is decorated with `@final`, forbidding overrides | ------ 8 | def foo(self): ... | --- `Parent.foo` defined here - | help: Remove the override of `foo` | 41 | # error: [override-of-final-method] "Cannot override final member `foo` from superclass `Parent`" @@ -162,7 +160,6 @@ error[override-of-final-method]: Cannot override `Parent.my_property1` | 44 | def my_property1(self) -> int: ... # error: [override-of-final-method] | ^^^^^^^^^^^^ Overrides a definition from superclass `Parent` - | info: `Parent.my_property1` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.pyi:9:5 | @@ -171,7 +168,6 @@ info: `Parent.my_property1` is decorated with `@final`, forbidding overrides 10 | @property 11 | def my_property1(self) -> int: ... | ------------ `Parent.my_property1` defined here - | help: Remove the override of `my_property1` ``` @@ -182,7 +178,6 @@ error[override-of-final-method]: Cannot override `Parent.my_property2` | 46 | def my_property2(self) -> int: ... # error: [override-of-final-method] | ^^^^^^^^^^^^ Overrides a definition from superclass `Parent` - | info: `Parent.my_property2` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.pyi:13:5 | @@ -190,7 +185,6 @@ info: `Parent.my_property2` is decorated with `@final`, forbidding overrides | ------ 14 | def my_property2(self) -> int: ... | ------------ `Parent.my_property2` defined here - | help: Remove the getter and setter for `my_property2` ``` @@ -201,7 +195,6 @@ error[override-of-final-method]: Cannot override `Parent.my_property3` | 50 | def my_property3(self) -> int: ... # error: [override-of-final-method] | ^^^^^^^^^^^^ Overrides a definition from superclass `Parent` - | info: `Parent.my_property3` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.pyi:16:5 | @@ -209,7 +202,6 @@ info: `Parent.my_property3` is decorated with `@final`, forbidding overrides | ------ 17 | def my_property3(self) -> int: ... | ------------ `Parent.my_property3` defined here - | help: Remove the override of `my_property3` ``` @@ -220,7 +212,6 @@ error[override-of-final-method]: Cannot override `Parent.class_method1` | 54 | def class_method1(cls) -> int: ... # error: [override-of-final-method] | ^^^^^^^^^^^^^ Overrides a definition from superclass `Parent` - | info: `Parent.class_method1` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.pyi:18:5 | @@ -229,7 +220,6 @@ info: `Parent.class_method1` is decorated with `@final`, forbidding overrides 19 | @classmethod 20 | def class_method1(cls) -> int: ... | ------------- `Parent.class_method1` defined here - | help: Remove the override of `class_method1` | 52 | def my_property3(self) -> None: ... @@ -248,7 +238,6 @@ error[override-of-final-method]: Cannot override `Parent.static_method1` | 56 | def static_method1() -> int: ... # error: [override-of-final-method] | ^^^^^^^^^^^^^^ Overrides a definition from superclass `Parent` - | info: `Parent.static_method1` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.pyi:24:5 | @@ -257,7 +246,6 @@ info: `Parent.static_method1` is decorated with `@final`, forbidding overrides 25 | @staticmethod 26 | def static_method1() -> int: ... | -------------- `Parent.static_method1` defined here - | help: Remove the override of `static_method1` | 54 | def class_method1(cls) -> int: ... # error: [override-of-final-method] @@ -276,7 +264,6 @@ error[override-of-final-method]: Cannot override `Parent.class_method2` | 58 | def class_method2(cls) -> int: ... # error: [override-of-final-method] | ^^^^^^^^^^^^^ Overrides a definition from superclass `Parent` - | info: `Parent.class_method2` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.pyi:22:5 | @@ -284,7 +271,6 @@ info: `Parent.class_method2` is decorated with `@final`, forbidding overrides | ------ 23 | def class_method2(cls) -> int: ... | ------------- `Parent.class_method2` defined here - | help: Remove the override of `class_method2` | 56 | def static_method1() -> int: ... # error: [override-of-final-method] @@ -303,7 +289,6 @@ error[override-of-final-method]: Cannot override `Parent.static_method2` | 60 | def static_method2() -> int: ... # error: [override-of-final-method] | ^^^^^^^^^^^^^^ Overrides a definition from superclass `Parent` - | info: `Parent.static_method2` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.pyi:28:5 | @@ -311,7 +296,6 @@ info: `Parent.static_method2` is decorated with `@final`, forbidding overrides | ------ 29 | def static_method2() -> int: ... | -------------- `Parent.static_method2` defined here - | help: Remove the override of `static_method2` | 58 | def class_method2(cls) -> int: ... # error: [override-of-final-method] @@ -335,7 +319,6 @@ error[invalid-method-override]: Invalid override of method `foo` | 8 | def foo(self): ... | --------- `Parent.foo` defined here - | info: `Grandchild.foo` is a staticmethod but `Parent.foo` is an instance method info: This violates the Liskov Substitution Principle @@ -347,7 +330,6 @@ error[override-of-final-method]: Cannot override `Parent.foo` | 75 | def foo(): ... | ^^^ Overrides a definition from superclass `Parent` - | info: `Parent.foo` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.pyi:7:5 | @@ -355,7 +337,6 @@ info: `Parent.foo` is decorated with `@final`, forbidding overrides | ------ 8 | def foo(self): ... | --- `Parent.foo` defined here - | help: Remove the override of `foo` | 71 | # concern of Liskov. @@ -376,7 +357,6 @@ error[override-of-final-method]: Cannot override `Parent.my_property1` | 79 | def my_property1(self) -> str: ... | ^^^^^^^^^^^^ Overrides a definition from superclass `Parent` - | info: `Parent.my_property1` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.pyi:9:5 | @@ -385,7 +365,6 @@ info: `Parent.my_property1` is decorated with `@final`, forbidding overrides 10 | @property 11 | def my_property1(self) -> int: ... | ------------ `Parent.my_property1` defined here - | help: Remove the override of `my_property1` ``` @@ -396,7 +375,6 @@ error[override-of-final-method]: Cannot override `Parent.class_method1` | 82 | class_method1 = None | ^^^^^^^^^^^^^ Overrides a definition from superclass `Parent` - | info: `Parent.class_method1` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.pyi:18:5 | @@ -405,7 +383,6 @@ info: `Parent.class_method1` is decorated with `@final`, forbidding overrides 19 | @classmethod 20 | def class_method1(cls) -> int: ... | ------------- `Parent.class_method1` defined here - | help: Remove the override of `class_method1` ``` @@ -416,7 +393,6 @@ error[override-of-final-method]: Cannot override `Foo.bar` | 113 | def bar(self): ... # error: [override-of-final-method] | ^^^ Overrides a definition from superclass `Foo` - | info: `Foo.bar` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.pyi:91:5 | @@ -427,7 +403,6 @@ info: `Foo.bar` is decorated with `@final`, forbidding overrides | 110 | def bar(self): ... | --- `Foo.bar` defined here - | help: Remove the override of `bar` | 112 | class Baz(Foo): diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Diagnostic_edge_case\342\200\246_(2389d52c5ecfa2bd).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Diagnostic_edge_case\342\200\246_(2389d52c5ecfa2bd).snap" index 118991dde6..3e3f8481ac 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Diagnostic_edge_case\342\200\246_(2389d52c5ecfa2bd).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Diagnostic_edge_case\342\200\246_(2389d52c5ecfa2bd).snap" @@ -37,7 +37,6 @@ error[override-of-final-method]: Cannot override `module1.Foo.f` | 4 | def f(self): ... # error: [override-of-final-method] | ^ Overrides a definition from superclass `module1.Foo` - | info: `module1.Foo.f` is decorated with `@final`, forbidding overrides --> src/module1.py:4:5 | @@ -45,7 +44,6 @@ info: `module1.Foo.f` is decorated with `@final`, forbidding overrides | ------ 5 | def f(self): ... | - `module1.Foo.f` defined here - | help: Remove the override of `f` | 3 | class Foo(module1.Foo): diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Only_the_first_`@fin\342\200\246_(9863b583f4c651c5).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Only_the_first_`@fin\342\200\246_(9863b583f4c651c5).snap" index 74f25111d2..47bd4c9ad4 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Only_the_first_`@fin\342\200\246_(9863b583f4c651c5).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Only_the_first_`@fin\342\200\246_(9863b583f4c651c5).snap" @@ -37,7 +37,6 @@ error[override-of-final-method]: Cannot override `A.f` | 9 | def f(self): ... # error: [override-of-final-method] | ^ Overrides a definition from superclass `A` - | info: `A.f` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.py:4:5 | @@ -45,7 +44,6 @@ info: `A.f` is decorated with `@final`, forbidding overrides | ------ 5 | def f(self): ... | - `A.f` defined here - | help: Remove the override of `f` | 7 | class B(A): @@ -64,7 +62,6 @@ error[override-of-final-method]: Cannot override `B.f` | 14 | def f(self): ... # error: [override-of-final-method] | ^ Overrides a definition from superclass `B` - | info: `B.f` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.py:8:5 | @@ -72,7 +69,6 @@ info: `B.f` is decorated with `@final`, forbidding overrides | ------ 9 | def f(self): ... # error: [override-of-final-method] | - `B.f` defined here - | help: Remove the override of `f` | 11 | class C(B): diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Overloaded_methods_d\342\200\246_(861757f48340ed92).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Overloaded_methods_d\342\200\246_(861757f48340ed92).snap" index 5bfde0fbfa..9fa15ee430 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Overloaded_methods_d\342\200\246_(861757f48340ed92).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Overloaded_methods_d\342\200\246_(861757f48340ed92).snap" @@ -135,7 +135,6 @@ error[override-of-final-method]: Cannot override `Good.bar` | 19 | def bar(self, x: int) -> int: ... # error: [override-of-final-method] | ^^^ Overrides a definition from superclass `Good` - | info: `Good.bar` is decorated with `@final`, forbidding overrides --> src/stub.pyi:5:5 | @@ -143,7 +142,6 @@ info: `Good.bar` is decorated with `@final`, forbidding overrides | ------ 6 | def bar(self, x: str) -> str: ... | --- `Good.bar` defined here - | help: Remove all overloads for `bar` | 15 | class ChildOfGood(Good): @@ -165,7 +163,6 @@ error[override-of-final-method]: Cannot override `Good.baz` | 23 | def baz(self, x: int) -> int: ... # error: [override-of-final-method] | ^^^ Overrides a definition from superclass `Good` - | info: `Good.baz` is decorated with `@final`, forbidding overrides --> src/stub.pyi:9:5 | @@ -174,7 +171,6 @@ info: `Good.baz` is decorated with `@final`, forbidding overrides 10 | @overload 11 | def baz(self, x: str) -> str: ... | --- `Good.baz` defined here - | help: Remove all overloads for `baz` | 19 | def bar(self, x: int) -> int: ... # error: [override-of-final-method] @@ -192,7 +188,7 @@ note: This is an unsafe fix and may change runtime behavior ``` error[invalid-overload]: `@final` decorator should be applied only to the first overload - --> src/stub.pyi:26:5 + --> src/stub.pyi:31:9 | 26 | / @overload 27 | | def bar(self, x: str) -> str: ... @@ -203,13 +199,12 @@ error[invalid-overload]: `@final` decorator should be applied only to the first 30 | # error: [invalid-overload] 31 | def bar(self, x: int) -> int: ... | ^^^ - | ``` ``` error[invalid-overload]: `@final` decorator should be applied only to the first overload - --> src/stub.pyi:32:5 + --> src/stub.pyi:37:9 | 32 | / @overload 33 | | def baz(self, x: str) -> str: ... @@ -220,7 +215,6 @@ error[invalid-overload]: `@final` decorator should be applied only to the first 36 | # error: [invalid-overload] 37 | def baz(self, x: int) -> int: ... | ^^^ - | ``` @@ -230,13 +224,11 @@ error[override-of-final-method]: Cannot override `Bad.bar` | 43 | def bar(self, x: int) -> int: ... # error: [override-of-final-method] | ^^^ Overrides a definition from superclass `Bad` - | info: `Bad.bar` is decorated with `@final`, forbidding overrides --> src/stub.pyi:27:9 | 27 | def bar(self, x: str) -> str: ... | --- `Bad.bar` defined here - | help: Remove all overloads for `bar` | 39 | class ChildOfBad(Bad): @@ -258,13 +250,11 @@ error[override-of-final-method]: Cannot override `Bad.baz` | 47 | def baz(self, x: int) -> int: ... # error: [override-of-final-method] | ^^^ Overrides a definition from superclass `Bad` - | info: `Bad.baz` is decorated with `@final`, forbidding overrides --> src/stub.pyi:33:9 | 33 | def baz(self, x: str) -> str: ... | --- `Bad.baz` defined here - | help: Remove all overloads for `baz` | 43 | def bar(self, x: int) -> int: ... # error: [override-of-final-method] @@ -285,7 +275,6 @@ error[override-of-final-method]: Cannot override `Good.f` | 19 | def f(self, x: int | str) -> int | str: | ^ Overrides a definition from superclass `Good` - | info: `Good.f` is decorated with `@final`, forbidding overrides --> src/main.py:8:5 | @@ -293,7 +282,6 @@ info: `Good.f` is decorated with `@final`, forbidding overrides | ------ 9 | def f(self, x: int | str) -> int | str: | - `Good.f` defined here - | help: Remove all overloads and the implementation for `f` | 12 | class ChildOfGood(Good): @@ -316,7 +304,7 @@ note: This is an unsafe fix and may change runtime behavior ``` error[invalid-overload]: `@final` decorator should be applied only to the overload implementation - --> src/main.py:23:5 + --> src/main.py:25:9 | 23 | @overload | --------- @@ -328,13 +316,12 @@ error[invalid-overload]: `@final` decorator should be applied only to the overlo 27 | def f(self, x: int) -> int: ... 28 | def f(self, x: int | str) -> int | str: | - Implementation defined here - | ``` ``` error[invalid-overload]: `@final` decorator should be applied only to the overload implementation - --> src/main.py:31:5 + --> src/main.py:33:9 | 31 | @final | ------ @@ -346,13 +333,12 @@ error[invalid-overload]: `@final` decorator should be applied only to the overlo 35 | def g(self, x: int) -> int: ... 36 | def g(self, x: int | str) -> int | str: | - Implementation defined here - | ``` ``` error[invalid-overload]: `@final` decorator should be applied only to the overload implementation - --> src/main.py:41:5 + --> src/main.py:43:9 | 41 | @overload | --------- @@ -362,13 +348,12 @@ error[invalid-overload]: `@final` decorator should be applied only to the overlo | ^ 44 | def h(self, x: int | str) -> int | str: | - Implementation defined here - | ``` ``` error[invalid-overload]: `@final` decorator should be applied only to the overload implementation - --> src/main.py:49:5 + --> src/main.py:51:9 | 49 | @final | ------ @@ -378,7 +363,6 @@ error[invalid-overload]: `@final` decorator should be applied only to the overlo | ^ 52 | def i(self, x: int | str) -> int | str: | - Implementation defined here - | ``` @@ -388,13 +372,11 @@ error[override-of-final-method]: Cannot override `Bad.f` | 57 | f = None # error: [override-of-final-method] | ^ Overrides a definition from superclass `Bad` - | info: `Bad.f` is decorated with `@final`, forbidding overrides --> src/main.py:28:9 | 28 | def f(self, x: int | str) -> int | str: | - `Bad.f` defined here - | help: Remove the override of `f` ``` @@ -405,13 +387,11 @@ error[override-of-final-method]: Cannot override `Bad.g` | 58 | g = None # error: [override-of-final-method] | ^ Overrides a definition from superclass `Bad` - | info: `Bad.g` is decorated with `@final`, forbidding overrides --> src/main.py:36:9 | 36 | def g(self, x: int | str) -> int | str: | - `Bad.g` defined here - | help: Remove the override of `g` ``` @@ -422,13 +402,11 @@ error[override-of-final-method]: Cannot override `Bad.h` | 59 | h = None # error: [override-of-final-method] | ^ Overrides a definition from superclass `Bad` - | info: `Bad.h` is decorated with `@final`, forbidding overrides --> src/main.py:44:9 | 44 | def h(self, x: int | str) -> int | str: | - `Bad.h` defined here - | help: Remove the override of `h` ``` @@ -439,13 +417,11 @@ error[override-of-final-method]: Cannot override `Bad.i` | 60 | i = None # error: [override-of-final-method] | ^ Overrides a definition from superclass `Bad` - | info: `Bad.i` is decorated with `@final`, forbidding overrides --> src/main.py:52:9 | 52 | def i(self, x: int | str) -> int | str: | - `Bad.i` defined here - | help: Remove the override of `i` ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Overloads_in_statica\342\200\246_(29a698d9deaf7318).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Overloads_in_statica\342\200\246_(29a698d9deaf7318).snap" index 48b157a182..4df9b8fbff 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Overloads_in_statica\342\200\246_(29a698d9deaf7318).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Overloads_in_statica\342\200\246_(29a698d9deaf7318).snap" @@ -61,7 +61,6 @@ error[override-of-final-method]: Cannot override `Foo.method` | 31 | def method(self, x: str) -> str: ... # error: [override-of-final-method] | ^^^^^^ Overrides a definition from superclass `Foo` - | info: `Foo.method` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.pyi:7:9 | @@ -69,7 +68,6 @@ info: `Foo.method` is decorated with `@final`, forbidding overrides | ------ 8 | def method(self, x: int) -> int: ... | ------ `Foo.method` defined here - | help: Remove all overloads for `method` | 27 | class Bar(Foo): diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Overriding_a_`@final\342\200\246_(c004aaab38745318).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Overriding_a_`@final\342\200\246_(c004aaab38745318).snap" index ba4dbf7231..d4f5078b9d 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Overriding_a_`@final\342\200\246_(c004aaab38745318).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Overriding_a_`@final\342\200\246_(c004aaab38745318).snap" @@ -44,7 +44,6 @@ error[override-of-final-method]: Cannot override `Base.method` | 5 | method = replacement_method # error: [override-of-final-method] | ^^^^^^ Overrides a definition from superclass `Base` - | info: `Base.method` is decorated with `@final`, forbidding overrides --> src/base.py:4:5 | @@ -52,7 +51,6 @@ info: `Base.method` is decorated with `@final`, forbidding overrides | ------ 5 | def method(self) -> None: ... | ------ `Base.method` defined here - | help: Remove the override of `method` ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/function.md_-_Call_expression_-_PEP-484_convention_f\342\200\246_(ee99fadd6476677e).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/function.md_-_Call_expression_-_PEP-484_convention_f\342\200\246_(ee99fadd6476677e).snap" index 420e29e2d4..5aa525fd6c 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/function.md_-_Call_expression_-_PEP-484_convention_f\342\200\246_(ee99fadd6476677e).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/function.md_-_Call_expression_-_PEP-484_convention_f\342\200\246_(ee99fadd6476677e).snap" @@ -89,90 +89,82 @@ error[positional-only-parameter-as-kwarg]: Positional-only parameter 1 (`__x`) p | 5 | f(__x=1) | ^^^^^ - | info: Function signature here --> src/mdtest_snippet.py:1:5 | 1 | def f(__x: int): ... | ^^^^^^^^^^^ - | ``` ``` warning[invalid-legacy-positional-parameter]: Invalid use of the legacy convention for positional-only parameters - --> src/mdtest_snippet.py:9:7 + --> src/mdtest_snippet.py:9:15 | 9 | def g(x: int, __y: str): ... | - ^^^ Parameter name begins with `__` but will not be treated as positional-only | | | Prior parameter here was positional-or-keyword - | info: A parameter can only be positional-only if it precedes all positional-or-keyword parameters ``` ``` warning[invalid-legacy-positional-parameter]: Invalid use of the legacy convention for positional-only parameters - --> src/mdtest_snippet.py:20:8 + --> src/mdtest_snippet.py:20:16 | 20 | def g2(x: int, __y: str): ... # error: [invalid-legacy-positional-parameter] | - ^^^ Parameter name begins with `__` but will not be treated as positional-only | | | Prior parameter here was positional-or-keyword - | info: A parameter can only be positional-only if it precedes all positional-or-keyword parameters ``` ``` warning[invalid-legacy-positional-parameter]: Invalid use of the legacy convention for positional-only parameters - --> src/mdtest_snippet.py:22:8 + --> src/mdtest_snippet.py:22:16 | 22 | def g2(x: str, __y: int): ... # error: [invalid-legacy-positional-parameter] | - ^^^ Parameter name begins with `__` but will not be treated as positional-only | | | Prior parameter here was positional-or-keyword - | info: A parameter can only be positional-only if it precedes all positional-or-keyword parameters ``` ``` warning[invalid-legacy-positional-parameter]: Invalid use of the legacy convention for positional-only parameters - --> src/mdtest_snippet.py:23:8 + --> src/mdtest_snippet.py:23:22 | 23 | def g2(x: str | int, __y: int | str): ... # error: [invalid-legacy-positional-parameter] | - ^^^ Parameter name begins with `__` but will not be treated as positional-only | | | Prior parameter here was positional-or-keyword - | info: A parameter can only be positional-only if it precedes all positional-or-keyword parameters ``` ``` warning[invalid-legacy-positional-parameter]: Invalid use of the legacy convention for positional-only parameters - --> src/mdtest_snippet.py:41:8 + --> src/mdtest_snippet.py:41:11 | 41 | def g4(a, __b): ... # error: [invalid-legacy-positional-parameter] | - ^^^ Parameter name begins with `__` but will not be treated as positional-only | | | Prior parameter here was positional-or-keyword - | info: A parameter can only be positional-only if it precedes all positional-or-keyword parameters ``` ``` warning[invalid-legacy-positional-parameter]: Invalid use of the legacy convention for positional-only parameters - --> src/mdtest_snippet.py:55:23 + --> src/mdtest_snippet.py:55:29 | 55 | def static_method(self, __x: int): ... # error: [invalid-legacy-positional-parameter] | ---- ^^^ Parameter name begins with `__` but will not be treated as positional-only | | | Prior parameter here was positional-or-keyword - | info: A parameter can only be positional-only if it precedes all positional-or-keyword parameters ``` @@ -183,13 +175,11 @@ error[positional-only-parameter-as-kwarg]: Positional-only parameter 2 (`__x`) p | 63 | C(42).method(__x=1) | ^^^^^ - | info: Method signature here --> src/mdtest_snippet.py:49:9 | 49 | def method(self, __x: int): ... | ^^^^^^^^^^^^^^^^^^^^^^ - | ``` @@ -199,12 +189,10 @@ error[positional-only-parameter-as-kwarg]: Positional-only parameter 2 (`__x`) p | 65 | C.class_method(__x="1") | ^^^^^^^ - | info: Method signature here --> src/mdtest_snippet.py:51:9 | 51 | def class_method(cls, __x: str): ... | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/function.md_-_Call_expression_-_Wrong_argument_type_-_Diagnostics_for_unio\342\200\246_(5396a8f9e7f88f71).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/function.md_-_Call_expression_-_Wrong_argument_type_-_Diagnostics_for_unio\342\200\246_(5396a8f9e7f88f71).snap" index dd637fa1cf..3163e5ba02 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/function.md_-_Call_expression_-_Wrong_argument_type_-_Diagnostics_for_unio\342\200\246_(5396a8f9e7f88f71).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/function.md_-_Call_expression_-_Wrong_argument_type_-_Diagnostics_for_unio\342\200\246_(5396a8f9e7f88f71).snap" @@ -40,7 +40,6 @@ error[invalid-argument-type]: Argument to function `f` is incorrect | 14 | f(a) # error: [invalid-argument-type] | ^ Expected `Sized`, found `str | Foo` - | info: element `Foo` of union `str | Foo` is not assignable to `Sized` info: └── type `Foo` is not assignable to protocol `Sized` info: └── protocol member `__len__` is not defined on type `Foo` @@ -49,7 +48,6 @@ info: Function defined here | 7 | def f(x: Sized): ... | ^ -------- Parameter declared here - | ``` @@ -59,7 +57,6 @@ error[invalid-argument-type]: Argument to function `f` is incorrect | 15 | f(b) # error: [invalid-argument-type] | ^ Expected `Sized`, found `list[str] | str | dict[str, str] | ... omitted 5 union elements` - | info: element `Foo` of union `list[str] | str | dict[str, str] | ... omitted 5 union elements` is not assignable to `Sized` info: └── type `Foo` is not assignable to protocol `Sized` info: └── protocol member `__len__` is not defined on type `Foo` @@ -68,7 +65,6 @@ info: Function defined here | 7 | def f(x: Sized): ... | ^ -------- Parameter declared here - | ``` @@ -78,7 +74,6 @@ error[invalid-argument-type]: Argument to function `f` is incorrect | 16 | f(c) # error: [invalid-argument-type] | ^ Expected `Sized`, found `list[str] | str | dict[str, str] | ... omitted 6 union elements` - | info: element `Foo` of union `list[str] | str | dict[str, str] | ... omitted 6 union elements` is not assignable to `Sized` info: └── type `Foo` is not assignable to protocol `Sized` info: └── protocol member `__len__` is not defined on type `Foo` @@ -87,7 +82,6 @@ info: Function defined here | 7 | def f(x: Sized): ... | ^ -------- Parameter declared here - | ``` @@ -97,7 +91,6 @@ error[invalid-argument-type]: Argument to function `f` is incorrect | 17 | f(d) # error: [invalid-argument-type] | ^ Expected `Sized`, found `list[str] | str | dict[str, str] | ... omitted 7 union elements` - | info: element `Foo` of union `list[str] | str | dict[str, str] | ... omitted 7 union elements` is not assignable to `Sized` info: └── type `Foo` is not assignable to protocol `Sized` info: └── protocol member `__len__` is not defined on type `Foo` @@ -106,6 +99,5 @@ info: Function defined here | 7 | def f(x: Sized): ... | ^ -------- Parameter declared here - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf\342\200\246_-_Tests_for_ty's_`inst\342\200\246_-_Builtins_with_implic\342\200\246_(4c3d127986a58f11).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf\342\200\246_-_Tests_for_ty's_`inst\342\200\246_-_Builtins_with_implic\342\200\246_(4c3d127986a58f11).snap" index 41ab66ec38..6e920e9688 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf\342\200\246_-_Tests_for_ty's_`inst\342\200\246_-_Builtins_with_implic\342\200\246_(4c3d127986a58f11).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf\342\200\246_-_Tests_for_ty's_`inst\342\200\246_-_Builtins_with_implic\342\200\246_(4c3d127986a58f11).snap" @@ -70,7 +70,6 @@ error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to 7 | | str 8 | | ): ... | |_^ Bases `int` and `str` 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:6:5 | @@ -78,7 +77,6 @@ info: Two classes cannot coexist in a class's MRO if their instances have incomp | --- `int` instances have a distinct memory layout because of the way `int` is implemented in a C extension 7 | str | --- `str` instances have a distinct memory layout because of the way `str` is implemented in a C extension - | ``` @@ -92,7 +90,6 @@ error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to 15 | | B, 16 | | ): ... | |_^ Bases `int` 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:14:5 | @@ -100,7 +97,6 @@ info: Two classes cannot coexist in a class's MRO if their instances have incomp | --- `int` instances have a distinct memory layout because of the way `int` is implemented in a C extension 15 | B, | - `B` instances have a distinct memory layout because `B` defines non-empty `__slots__` - | ``` @@ -114,7 +110,6 @@ error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to 21 | | str 22 | | ): ... | |_^ Bases `D` and `str` 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:20:5 | @@ -125,7 +120,6 @@ info: Two classes cannot coexist in a class's MRO if their instances have incomp | `int` instances have a distinct memory layout because of the way `int` is implemented in a C extension 21 | str | --- `str` instances have a distinct memory layout because of the way `str` is implemented in a C extension - | ``` @@ -135,7 +129,6 @@ error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to | 24 | class F(int, bytes, bytearray): ... # error: [instance-layout-conflict] | ^^^^^^^^^^^^^^^^^^^^^^^^ Bases `int`, `bytes` and `bytearray` 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:24:9 | @@ -144,7 +137,6 @@ info: Two classes cannot coexist in a class's MRO if their instances have incomp | | | | | `bytes` instances have a distinct memory layout because of the way `bytes` is implemented in a C extension | `int` instances have a distinct memory layout because of the way `int` is implemented in a C extension - | ``` @@ -154,7 +146,6 @@ error[invalid-typed-dict-header]: `@disjoint_base` cannot be used with `TypedDic | 31 | @disjoint_base # error: [invalid-typed-dict-header] "`@disjoint_base` cannot be used with `TypedDict` class `Movie`" | ^^^^^^^^^^^^^^ - | ``` @@ -164,7 +155,6 @@ error[invalid-protocol]: `@disjoint_base` cannot be used with protocol class `Su | 34 | @disjoint_base # error: [invalid-protocol] "`@disjoint_base` cannot be used with protocol class `SupportsClose`" | ^^^^^^^^^^^^^^ - | ``` @@ -178,7 +168,6 @@ error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to 39 | | H 40 | | ): ... | |_^ Bases `G` and `H` 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:38:5 | @@ -186,7 +175,6 @@ info: Two classes cannot coexist in a class's MRO if their instances have incomp | - `G` instances have a distinct memory layout because of the way `G` is implemented in a C extension 39 | H | - `H` instances have a distinct memory layout because of the way `H` is implemented in a C extension - | ``` @@ -199,7 +187,6 @@ error[invalid-generic-class]: Inconsistent type arguments for `Sequence` among c | | | | | Later class base inherits from `Sequence[str]` | Earlier class base inherits from `Sequence[int]` - | ``` @@ -209,6 +196,5 @@ error[subclass-of-final-class]: Class `Foo` cannot inherit from final class `ran | 43 | class Foo(range, str): ... # error: [subclass-of-final-class] | ^^^^^ - | ``` 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" index 16814de0fe..d1c6853a6c 100644 --- "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" @@ -37,7 +37,6 @@ error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to 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 | @@ -45,6 +44,5 @@ info: Two classes cannot coexist in a class's MRO if their instances have incomp | - `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__` - | ``` 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_-_AST_nodes_that_are_o\342\200\246_(58a3839a9bc7026d).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_AST_nodes_that_are_o\342\200\246_(58a3839a9bc7026d).snap" index 10f3407bcf..39dff0c499 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_AST_nodes_that_are_o\342\200\246_(58a3839a9bc7026d).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_AST_nodes_that_are_o\342\200\246_(58a3839a9bc7026d).snap" @@ -33,7 +33,6 @@ error[invalid-type-form]: Int literals are not allowed in this context in a para | 3 | a: 42, | ^^ Did you mean `typing.Literal[42]`? - | 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 @@ -45,7 +44,6 @@ error[invalid-type-form]: Bytes literals are not allowed in this context in a pa | 5 | b: b"42", | ^^^^^ Did you mean `typing.Literal[b"42"]`? - | 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 @@ -57,7 +55,6 @@ error[invalid-type-form]: Boolean literals are not allowed in this context in a | 7 | c: True, | ^^^^ Did you mean `typing.Literal[True]`? - | 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 @@ -65,13 +62,12 @@ info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotat ``` error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation - --> src/mdtest_snippet.py:9:9 + --> src/mdtest_snippet.py:9:17 | 9 | d: "invalid syntax", | --------^^^^^^ | | | Unexpected token at the end of an expression - | help: Did you mean `typing.Literal["invalid syntax"]`? ``` 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_-_Dict-literal_or_set-\342\200\246_(15737b0beb194b0e).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_Dict-literal_or_set-\342\200\246_(15737b0beb194b0e).snap" index 1227652f2d..b0bbb1b6f8 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_Dict-literal_or_set-\342\200\246_(15737b0beb194b0e).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_Dict-literal_or_set-\342\200\246_(15737b0beb194b0e).snap" @@ -27,7 +27,6 @@ error[invalid-type-form]: Dict literals are not allowed in parameter annotations | 2 | x: {int: str}, # error: [invalid-type-form] | ^^^^^^^^^^ Did you mean `dict[int, str]`? - | 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 @@ -39,7 +38,6 @@ error[invalid-type-form]: Set literals are not allowed in parameter annotations | 3 | y: {str}, # error: [invalid-type-form] | ^^^^^ Did you mean `set[str]`? - | 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 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_(ba5cb09eaa3715d8).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_(ba5cb09eaa3715d8).snap" index 77eeb58124..db6a0ecbd5 100644 --- "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_(ba5cb09eaa3715d8).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_(ba5cb09eaa3715d8).snap" @@ -33,7 +33,6 @@ error[invalid-type-form]: List literals are not allowed in this context in a par | 2 | x: [int], # error: [invalid-type-form] | ^^^^^ Did you mean `list[int]`? - | 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 @@ -45,7 +44,6 @@ error[invalid-type-form]: List literals are not allowed in this context in a ret | 3 | ) -> [int]: # error: [invalid-type-form] | ^^^^^ Did you mean `list[int]`? - | 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 @@ -57,7 +55,6 @@ error[invalid-type-form]: List literals are not allowed in this context in a par | 8 | x: [int, str], # error: [invalid-type-form] | ^^^^^^^^^^ - | 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 @@ -69,7 +66,6 @@ error[invalid-type-form]: List literals are not allowed in this context in a ret | 9 | ) -> [int, str]: # error: [invalid-type-form] | ^^^^^^^^^^ - | 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 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" index 9ecce90a24..da667d81c7 100644 --- "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" @@ -41,7 +41,6 @@ error[invalid-type-form]: Module `datetime` is not valid in a parameter annotati 3 | def f(x: datetime): ... # error: [invalid-type-form] | ^^^^^^^^ Did you mean to use the module's member `datetime.datetime`? | - | 2 | - def f(x: datetime): ... # error: [invalid-type-form] 3 + def f(x: datetime.datetime): ... # error: [invalid-type-form] @@ -57,7 +56,6 @@ error[invalid-type-form]: Module `PIL.Image` is not valid in a parameter annotat 3 | def g(x: Image): ... # error: [invalid-type-form] | ^^^^^ Did you mean to use the module's member `Image.Image`? | - | 2 | - def g(x: Image): ... # error: [invalid-type-form] 3 + def g(x: Image.Image): ... # error: [invalid-type-form] 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_-_Special-cased_diagno\342\200\246_(a4b698196d337a3f).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_Special-cased_diagno\342\200\246_(a4b698196d337a3f).snap" index d575a1c0f9..06b07d4704 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_Special-cased_diagno\342\200\246_(a4b698196d337a3f).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_Special-cased_diagno\342\200\246_(a4b698196d337a3f).snap" @@ -27,7 +27,6 @@ error[invalid-type-form]: Function `callable` is not valid in a parameter annota | 3 | def decorator(fn: callable) -> callable: | ^^^^^^^^ Did you mean `collections.abc.Callable`? - | ``` @@ -37,6 +36,5 @@ error[invalid-type-form]: Function `callable` is not valid in a return type anno | 3 | def decorator(fn: callable) -> callable: | ^^^^^^^^ Did you mean `collections.abc.Callable`? - | ``` 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" index 4a6890c747..68d261cc83 100644 --- "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" @@ -35,7 +35,6 @@ error[invalid-type-form]: Tuple literals are not allowed in this context in a pa | 2 | x: (), # error: [invalid-type-form] | ^^ Did you mean `tuple[()]`? - | 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 @@ -47,7 +46,6 @@ error[invalid-type-form]: Tuple literals are not allowed in this context in a re | 3 | ) -> (): # error: [invalid-type-form] | ^^ Did you mean `tuple[()]`? - | 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 @@ -59,7 +57,6 @@ error[invalid-type-form]: Tuple literals are not allowed in this context in a pa | 6 | x: (int,), # error: [invalid-type-form] | ^^^^^^ Did you mean `tuple[int]`? - | 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 @@ -71,7 +68,6 @@ error[invalid-type-form]: Tuple literals are not allowed in this context in a re | 7 | ) -> (int,): # error: [invalid-type-form] | ^^^^^^ Did you mean `tuple[int]`? - | 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 @@ -83,7 +79,6 @@ error[invalid-type-form]: Tuple literals are not allowed in this context in a pa | 10 | x: (int, str), # error: [invalid-type-form] | ^^^^^^^^^^ Did you mean `tuple[int, str]`? - | 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 @@ -95,7 +90,6 @@ error[invalid-type-form]: Tuple literals are not allowed in this context in a re | 11 | ) -> (int, str): # error: [invalid-type-form] | ^^^^^^^^^^ Did you mean `tuple[int, str]`? - | 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 diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Multiple_starred_exp\342\200\246_(3fbab22ead236138).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Multiple_starred_exp\342\200\246_(3fbab22ead236138).snap" index 374334e324..2ad9d772e7 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Multiple_starred_exp\342\200\246_(3fbab22ead236138).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Multiple_starred_exp\342\200\246_(3fbab22ead236138).snap" @@ -53,7 +53,6 @@ error[invalid-type-form]: Multiple unpacked variadic tuples are not allowed in a | | | | | Later unpacked variadic tuple | First unpacked variadic tuple - | ``` @@ -66,7 +65,6 @@ error[invalid-type-form]: Multiple unpacked variadic tuples are not allowed in a | | | | | Later unpacked variadic tuple | First unpacked variadic tuple - | ``` @@ -79,7 +77,6 @@ error[invalid-type-form]: Multiple unpacked variadic tuples are not allowed in a | | | | | Later unpacked variadic tuple | First unpacked variadic tuple - | ``` @@ -92,7 +89,6 @@ error[invalid-type-form]: Multiple unpacked variadic tuples are not allowed in a | | | | | Later unpacked variadic tuple | First unpacked variadic tuple - | ``` @@ -105,7 +101,6 @@ error[invalid-type-form]: Multiple unpacked variadic tuples are not allowed in a | | | | | Later unpacked variadic tuple | First unpacked variadic tuple - | ``` @@ -118,6 +113,5 @@ error[invalid-type-form]: Multiple unpacked variadic tuples are not allowed in a | | | | | Later unpacked variadic tuple | First unpacked variadic tuple - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Basic_(f15db7dc447d0795).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Basic_(f15db7dc447d0795).snap" index 407e440f14..965944e476 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Basic_(f15db7dc447d0795).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Basic_(f15db7dc447d0795).snap" @@ -26,11 +26,10 @@ error[invalid-await]: `Literal[1]` is not awaitable 2 | await 1 # error: [invalid-await] | ^ | - ::: stdlib/builtins.byi:278:7 + ::: stdlib/builtins.byi:279:7 | -278 | class int: +279 | class int: | --- type defined here - | info: `__await__` is missing ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Custom_type_with_mis\342\200\246_(9ce1ee3cd1c9c8d1).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Custom_type_with_mis\342\200\246_(9ce1ee3cd1c9c8d1).snap" index a806e2605a..7e1ab9e6ec 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Custom_type_with_mis\342\200\246_(9ce1ee3cd1c9c8d1).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Custom_type_with_mis\342\200\246_(9ce1ee3cd1c9c8d1).snap" @@ -33,7 +33,6 @@ error[invalid-await]: `MissingAwait` is not awaitable | 1 | class MissingAwait: | ------------ type defined here - | info: `__await__` is missing ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Custom_type_with_pos\342\200\246_(a028edbafe180ca).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Custom_type_with_pos\342\200\246_(a028edbafe180ca).snap" index ade20c220f..dddc76738f 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Custom_type_with_pos\342\200\246_(a028edbafe180ca).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Custom_type_with_pos\342\200\246_(a028edbafe180ca).snap" @@ -37,7 +37,6 @@ error[invalid-await]: `PossiblyUnbound` is not awaitable | 5 | def __await__(self): | --------------- method defined here - | info: `__await__` may be missing ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Invalid_union_return\342\200\246_(fedf62ffaca0f2d7).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Invalid_union_return\342\200\246_(fedf62ffaca0f2d7).snap" index ea978d87ed..3bddee84b7 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Invalid_union_return\342\200\246_(fedf62ffaca0f2d7).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Invalid_union_return\342\200\246_(fedf62ffaca0f2d7).snap" @@ -37,7 +37,6 @@ error[invalid-await]: `UnawaitableUnion` is not awaitable | 14 | await UnawaitableUnion() # error: [invalid-await] | ^^^^^^^^^^^^^^^^^^ - | info: `__await__` returns `Generator[Any, None, None] | int`, which is not a valid iterator ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Non-callable_`__awai\342\200\246_(9db6c457a98cde25).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Non-callable_`__awai\342\200\246_(9db6c457a98cde25).snap" index c339d15aa5..f9e68e6075 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Non-callable_`__awai\342\200\246_(9db6c457a98cde25).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Non-callable_`__awai\342\200\246_(9db6c457a98cde25).snap" @@ -39,7 +39,6 @@ error[invalid-await]: `HasBadAwait` is not awaitable | 2 | __await__ = 42 | --------- attribute defined here - | info: `__await__` is not callable ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Non-callable_`__awai\342\200\246_(d78580fb6720e4ea).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Non-callable_`__awai\342\200\246_(d78580fb6720e4ea).snap" index d05b772572..0dcb38a298 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Non-callable_`__awai\342\200\246_(d78580fb6720e4ea).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Non-callable_`__awai\342\200\246_(d78580fb6720e4ea).snap" @@ -42,7 +42,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_awai ``` error[invalid-await]: `NonCallableAwait` is not awaitable - --> src/mdtest_snippet.py:2:5 + --> src/mdtest_snippet.py:5:11 | 2 | __await__ = 42 | --------- attribute defined here @@ -50,7 +50,6 @@ error[invalid-await]: `NonCallableAwait` is not awaitable 4 | async def main() -> None: 5 | await NonCallableAwait() # error: [invalid-await] | ^^^^^^^^^^^^^^^^^^ - | info: `__await__` is not callable ``` @@ -66,7 +65,6 @@ error[invalid-await]: `DeepInheritedNonCallableAwait` is not awaitable | 7 | __await__ = 42 | --------- attribute defined here - | info: `__await__` is not callable ``` @@ -77,7 +75,6 @@ error[invalid-await]: `A | B` is not awaitable | 23 | await x # error: [invalid-await] | ^ - | info: `__await__` is not callable ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Union_type_where_one\342\200\246_(ef7c2c0c8d9b1f0).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Union_type_where_one\342\200\246_(ef7c2c0c8d9b1f0).snap" index 763a73ac3c..2b52cc1ef6 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Union_type_where_one\342\200\246_(ef7c2c0c8d9b1f0).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Union_type_where_one\342\200\246_(ef7c2c0c8d9b1f0).snap" @@ -36,7 +36,6 @@ error[invalid-await]: `Awaitable | NotAwaitable` is not awaitable | 2 | def __await__(self): | --------------- method defined here - | info: `__await__` may be missing info: `NotAwaitable` does not implement `__await__` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_`__await__`_definiti\342\200\246_(15b05c126b6ae968).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_`__await__`_definiti\342\200\246_(15b05c126b6ae968).snap" index b59b837165..0a8188f067 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_`__await__`_definiti\342\200\246_(15b05c126b6ae968).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_`__await__`_definiti\342\200\246_(15b05c126b6ae968).snap" @@ -34,7 +34,6 @@ error[invalid-await]: `InvalidAwaitArgs` is not awaitable | 2 | def __await__(self, value: int): | ------------------ parameters here - | info: `__await__` requires arguments and cannot be called implicitly ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_`__await__`_definiti\342\200\246_(ccb69f512135dd61).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_`__await__`_definiti\342\200\246_(ccb69f512135dd61).snap" index 69d17b266a..66008853dd 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_`__await__`_definiti\342\200\246_(ccb69f512135dd61).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_`__await__`_definiti\342\200\246_(ccb69f512135dd61).snap" @@ -34,7 +34,6 @@ error[invalid-await]: `InvalidAwaitReturn` is not awaitable | 2 | def __await__(self) -> int: | ---------------------- method defined here - | info: `__await__` returns `int`, which is not a valid iterator ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_paramet\342\200\246_-_Invalid_Order_of_Leg\342\200\246_(eaa359e8d6b3031d).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_paramet\342\200\246_-_Invalid_Order_of_Leg\342\200\246_(eaa359e8d6b3031d).snap" index f8e178fed2..99ad2532ab 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_paramet\342\200\246_-_Invalid_Order_of_Leg\342\200\246_(eaa359e8d6b3031d).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_paramet\342\200\246_-_Invalid_Order_of_Leg\342\200\246_(eaa359e8d6b3031d).snap" @@ -68,7 +68,6 @@ error[invalid-generic-class]: Type parameters without defaults cannot follow typ 4 | 5 | T2 = TypeVar("T2") | ------------------ `T2` defined here - | ``` @@ -90,7 +89,6 @@ error[invalid-generic-class]: Type parameters without defaults cannot follow typ 5 | T2 = TypeVar("T2") 6 | T3 = TypeVar("T3") | ------------------ `T3` defined here - | ``` @@ -111,7 +109,6 @@ error[invalid-generic-class]: Type parameters without defaults cannot follow typ 4 | 5 | T2 = TypeVar("T2") | ------------------ `T2` defined here - | ``` @@ -132,7 +129,6 @@ error[invalid-generic-class]: Type parameters without defaults cannot follow typ 4 | 5 | T2 = TypeVar("T2") | ------------------ `T2` defined here - | ``` @@ -142,7 +138,6 @@ error[invalid-generic-class]: Cannot both inherit from subscripted `Protocol` an | 32 | Protocol[T1, T2, DefaultStrT, T3], | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove the type parameters from the `Protocol` base | 31 | # error: [invalid-generic-class] @@ -171,6 +166,5 @@ error[invalid-generic-class]: Type parameters without defaults cannot follow typ 4 | 5 | T2 = TypeVar("T2") | ------------------ `T2` defined here - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Boolean_parameters_m\342\200\246_(3edf97b20f58fa11).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Boolean_parameters_m\342\200\246_(3edf97b20f58fa11).snap" deleted file mode 100644 index f6d155f7e1..0000000000 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Boolean_parameters_m\342\200\246_(3edf97b20f58fa11).snap" +++ /dev/null @@ -1,61 +0,0 @@ ---- -source: crates/mdtest/src/lib.rs -expression: snapshot ---- - ---- -mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Boolean parameters must be unambiguous -mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md ---- - -# Python source files - -## mdtest_snippet.py - -``` - 1 | from typing_extensions import TypeVar - 2 | - 3 | def cond() -> bool: - 4 | return True - 5 | - 6 | # error: [invalid-legacy-type-variable] - 7 | T = TypeVar("T", covariant=cond()) - 8 | - 9 | # error: [invalid-legacy-type-variable] -10 | U = TypeVar("U", contravariant=cond()) -11 | -12 | # error: [invalid-legacy-type-variable] -13 | V = TypeVar("V", infer_variance=cond()) -``` - -# Diagnostics - -``` -error[invalid-legacy-type-variable]: The `covariant` parameter of `TypeVar` cannot have an ambiguous truthiness - --> src/mdtest_snippet.py:7:28 - | -7 | T = TypeVar("T", covariant=cond()) - | ^^^^^^ - | - -``` - -``` -error[invalid-legacy-type-variable]: The `contravariant` parameter of `TypeVar` cannot have an ambiguous truthiness - --> src/mdtest_snippet.py:10:32 - | -10 | U = TypeVar("U", contravariant=cond()) - | ^^^^^^ - | - -``` - -``` -error[invalid-legacy-type-variable]: The `infer_variance` parameter of `TypeVar` cannot have an ambiguous truthiness - --> src/mdtest_snippet.py:13:33 - | -13 | V = TypeVar("V", infer_variance=cond()) - | ^^^^^^ - | - -``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Cannot_be_both_covar\342\200\246_(b7b0976739681470).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Cannot_be_both_covar\342\200\246_(b7b0976739681470).snap" deleted file mode 100644 index 95f919e749..0000000000 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Cannot_be_both_covar\342\200\246_(b7b0976739681470).snap" +++ /dev/null @@ -1,32 +0,0 @@ ---- -source: crates/mdtest/src/lib.rs -expression: snapshot ---- - ---- -mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Cannot be both covariant and contravariant -mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | from typing import TypeVar -2 | -3 | # error: [invalid-legacy-type-variable] -4 | T = TypeVar("T", covariant=True, contravariant=True) -``` - -# Diagnostics - -``` -error[invalid-legacy-type-variable]: A `TypeVar` cannot be both covariant and contravariant - --> src/mdtest_snippet.py:4:5 - | -4 | T = TypeVar("T", covariant=True, contravariant=True) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - -``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Cannot_have_both_bou\342\200\246_(4ca5f13621915554).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Cannot_have_both_bou\342\200\246_(4ca5f13621915554).snap" deleted file mode 100644 index d225dc5f27..0000000000 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Cannot_have_both_bou\342\200\246_(4ca5f13621915554).snap" +++ /dev/null @@ -1,32 +0,0 @@ ---- -source: crates/mdtest/src/lib.rs -expression: snapshot ---- - ---- -mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Cannot have both bound and constraint -mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | from typing import TypeVar -2 | -3 | # error: [invalid-legacy-type-variable] -4 | T = TypeVar("T", int, str, bound=bytes) -``` - -# Diagnostics - -``` -error[invalid-legacy-type-variable]: A `TypeVar` cannot have both a bound and constraints - --> src/mdtest_snippet.py:4:5 - | -4 | T = TypeVar("T", int, str, bound=bytes) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - -``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Cannot_have_only_one\342\200\246_(8b0258f5188209c6).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Cannot_have_only_one\342\200\246_(8b0258f5188209c6).snap" deleted file mode 100644 index 1d57404085..0000000000 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Cannot_have_only_one\342\200\246_(8b0258f5188209c6).snap" +++ /dev/null @@ -1,32 +0,0 @@ ---- -source: crates/mdtest/src/lib.rs -expression: snapshot ---- - ---- -mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Cannot have only one constraint -mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | from typing import TypeVar -2 | -3 | # error: [invalid-legacy-type-variable] -4 | T = TypeVar("T", int) -``` - -# Diagnostics - -``` -error[invalid-legacy-type-variable]: A `TypeVar` cannot have exactly one constraint - --> src/mdtest_snippet.py:4:18 - | -4 | T = TypeVar("T", int) - | ^^^ - | - -``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Invalid_feature_for_\342\200\246_(72827c64b5c73d05).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Invalid_feature_for_\342\200\246_(72827c64b5c73d05).snap" deleted file mode 100644 index c9c31ef57f..0000000000 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Invalid_feature_for_\342\200\246_(72827c64b5c73d05).snap" +++ /dev/null @@ -1,32 +0,0 @@ ---- -source: crates/mdtest/src/lib.rs -expression: snapshot ---- - ---- -mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Invalid feature for this Python version -mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | from typing import TypeVar -2 | -3 | # error: [invalid-legacy-type-variable] -4 | T = TypeVar("T", default=int) -``` - -# Diagnostics - -``` -error[invalid-legacy-type-variable]: The `default` parameter of `typing.TypeVar` was added in Python 3.13 - --> src/mdtest_snippet.py:4:18 - | -4 | T = TypeVar("T", default=int) - | ^^^^^^^^^^^ - | - -``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Invalid_keyword_argu\342\200\246_(39164266ada3dc2f).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Invalid_keyword_argu\342\200\246_(39164266ada3dc2f).snap" deleted file mode 100644 index fb335e4fae..0000000000 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Invalid_keyword_argu\342\200\246_(39164266ada3dc2f).snap" +++ /dev/null @@ -1,32 +0,0 @@ ---- -source: crates/mdtest/src/lib.rs -expression: snapshot ---- - ---- -mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Invalid keyword arguments -mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | from typing import TypeVar -2 | -3 | # error: [invalid-legacy-type-variable] -4 | T = TypeVar("T", invalid_keyword=True) -``` - -# Diagnostics - -``` -error[invalid-legacy-type-variable]: Unknown keyword argument `invalid_keyword` in `TypeVar` creation - --> src/mdtest_snippet.py:4:18 - | -4 | T = TypeVar("T", invalid_keyword=True) - | ^^^^^^^^^^^^^^^^^^^^ - | - -``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Must_be_directly_ass\342\200\246_(c2e3e46852bb268f).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Must_be_directly_ass\342\200\246_(c2e3e46852bb268f).snap" deleted file mode 100644 index 359c8f9eda..0000000000 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Must_be_directly_ass\342\200\246_(c2e3e46852bb268f).snap" +++ /dev/null @@ -1,46 +0,0 @@ ---- -source: crates/mdtest/src/lib.rs -expression: snapshot ---- - ---- -mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Must be directly assigned to a variable -mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | from typing import TypeVar -2 | -3 | T = TypeVar("T") -4 | # error: [invalid-legacy-type-variable] -5 | U: TypeVar = TypeVar("U") -6 | -7 | # error: [invalid-legacy-type-variable] -8 | tuple_with_typevar = ("foo", TypeVar("W")) -``` - -# Diagnostics - -``` -error[invalid-legacy-type-variable]: A `TypeVar` definition must be a simple variable assignment - --> src/mdtest_snippet.py:5:14 - | -5 | U: TypeVar = TypeVar("U") - | ^^^^^^^^^^^^ - | - -``` - -``` -error[invalid-legacy-type-variable]: A `TypeVar` definition must be a simple variable assignment - --> src/mdtest_snippet.py:8:30 - | -8 | tuple_with_typevar = ("foo", TypeVar("W")) - | ^^^^^^^^^^^^ - | - -``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Must_have_a_name_(79a4ce09338e666b).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Must_have_a_name_(79a4ce09338e666b).snap" deleted file mode 100644 index d24e221540..0000000000 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Must_have_a_name_(79a4ce09338e666b).snap" +++ /dev/null @@ -1,32 +0,0 @@ ---- -source: crates/mdtest/src/lib.rs -expression: snapshot ---- - ---- -mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Must have a name -mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | from typing import TypeVar -2 | -3 | # error: [invalid-legacy-type-variable] -4 | T = TypeVar() -``` - -# Diagnostics - -``` -error[invalid-legacy-type-variable]: The `name` parameter of `TypeVar` is required. - --> src/mdtest_snippet.py:4:5 - | -4 | T = TypeVar() - | ^^^^^^^^^ - | - -``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Must_not_be_redefine\342\200\246_(b1be57970f924722).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Must_not_be_redefine\342\200\246_(b1be57970f924722).snap" deleted file mode 100644 index 91c84bbc35..0000000000 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Must_not_be_redefine\342\200\246_(b1be57970f924722).snap" +++ /dev/null @@ -1,38 +0,0 @@ ---- -source: crates/mdtest/src/lib.rs -expression: snapshot ---- - ---- -mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Must not be redefined -mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | from typing import TypeVar -2 | -3 | T = TypeVar("T") -4 | -5 | # error: [invalid-legacy-type-variable] -6 | T = TypeVar("T") -``` - -# Diagnostics - -``` -error[invalid-legacy-type-variable]: Cannot redefine `T` as a type variable - --> src/mdtest_snippet.py:3:1 - | -3 | T = TypeVar("T") - | - Previously defined here -4 | -5 | # error: [invalid-legacy-type-variable] -6 | T = TypeVar("T") - | ^ - | - -``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Name_can't_be_given_\342\200\246_(8f6aed0dba79e995).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Name_can't_be_given_\342\200\246_(8f6aed0dba79e995).snap" deleted file mode 100644 index 340c9ce49b..0000000000 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Name_can't_be_given_\342\200\246_(8f6aed0dba79e995).snap" +++ /dev/null @@ -1,32 +0,0 @@ ---- -source: crates/mdtest/src/lib.rs -expression: snapshot ---- - ---- -mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Name can't be given more than once -mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | from typing import TypeVar -2 | -3 | # error: [invalid-legacy-type-variable] -4 | T = TypeVar("T", name="T") -``` - -# Diagnostics - -``` -error[invalid-legacy-type-variable]: The `name` parameter of `TypeVar` can only be provided once. - --> src/mdtest_snippet.py:4:18 - | -4 | T = TypeVar("T", name="T") - | ^^^^^^^^ - | - -``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_No_variadic_argument\342\200\246_(9d57505425233fd8).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_No_variadic_argument\342\200\246_(9d57505425233fd8).snap" deleted file mode 100644 index e998cbb5e7..0000000000 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_No_variadic_argument\342\200\246_(9d57505425233fd8).snap" +++ /dev/null @@ -1,47 +0,0 @@ ---- -source: crates/mdtest/src/lib.rs -expression: snapshot ---- - ---- -mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - No variadic arguments -mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | from typing import TypeVar -2 | -3 | types = (int, str) -4 | -5 | # error: [invalid-legacy-type-variable] -6 | T = TypeVar("T", *types) -7 | -8 | # error: [invalid-legacy-type-variable] -9 | S = TypeVar("S", **{"bound": int}) -``` - -# Diagnostics - -``` -error[invalid-legacy-type-variable]: Starred arguments are not supported in `TypeVar` creation - --> src/mdtest_snippet.py:6:18 - | -6 | T = TypeVar("T", *types) - | ^^^^^^ - | - -``` - -``` -error[invalid-legacy-type-variable]: Starred arguments are not supported in `TypeVar` creation - --> src/mdtest_snippet.py:9:18 - | -9 | S = TypeVar("S", **{"bound": int}) - | ^^^^^^^^^^^^^^^^ - | - -``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_`TypeVar`_parameter_\342\200\246_(8424f2b8bc4351f9).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_`TypeVar`_parameter_\342\200\246_(8424f2b8bc4351f9).snap" deleted file mode 100644 index 6e440a5f14..0000000000 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_`TypeVar`_parameter_\342\200\246_(8424f2b8bc4351f9).snap" +++ /dev/null @@ -1,32 +0,0 @@ ---- -source: crates/mdtest/src/lib.rs -expression: snapshot ---- - ---- -mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - `TypeVar` parameter must match variable name -mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | from typing import TypeVar -2 | -3 | # error: [mismatched-type-name] -4 | T = TypeVar("Q") -``` - -# Diagnostics - -``` -warning[mismatched-type-name]: The name passed to `TypeVar` must match the variable it is assigned to - --> src/mdtest_snippet.py:4:13 - | -4 | T = TypeVar("Q") - | ^^^ Expected "T", got "Q" - | - -``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/missing_argument_par\342\200\246_-_Missing_argument_for\342\200\246_(b632d61c1d75f9fb).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/missing_argument_par\342\200\246_-_Missing_argument_for\342\200\246_(b632d61c1d75f9fb).snap" index c24f7e5519..5d14c8d9d8 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/missing_argument_par\342\200\246_-_Missing_argument_for\342\200\246_(b632d61c1d75f9fb).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/missing_argument_par\342\200\246_-_Missing_argument_for\342\200\246_(b632d61c1d75f9fb).snap" @@ -31,7 +31,6 @@ error[missing-argument]: No arguments provided for required parameters `*args`, | 5 | func() # error: [missing-argument] | ^^^^^^ - | info: These arguments are required because `ParamSpec` `P` could represent any set of parameters at runtime ``` @@ -42,7 +41,6 @@ error[missing-argument]: No argument provided for required parameter `**kwargs` | 6 | func(*args) # error: [missing-argument] | ^^^^^^^^^^^ - | info: These arguments are required because `ParamSpec` `P` could represent any set of parameters at runtime ``` @@ -53,7 +51,6 @@ error[missing-argument]: No argument provided for required parameter `*args` | 7 | func(**kwargs) # error: [missing-argument] | ^^^^^^^^^^^^^^ - | info: These arguments are required because `ParamSpec` `P` could represent any set of parameters at runtime ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or\342\200\246_-_Inline_tuple-literal\342\200\246_(4ee237f49e7ac736).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or\342\200\246_-_Inline_tuple-literal\342\200\246_(4ee237f49e7ac736).snap" index dc192bc259..12071a81a8 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or\342\200\246_-_Inline_tuple-literal\342\200\246_(4ee237f49e7ac736).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or\342\200\246_-_Inline_tuple-literal\342\200\246_(4ee237f49e7ac736).snap" @@ -30,7 +30,6 @@ error[duplicate-base]: Duplicate base class `int` | ^^^^^^^^^^^^^^^^^^^^^^^^^ --- ^^^ Class `int` later repeated here | | | Class `int` first included in bases list here - | info: Definition of class `InlineTupleDuplicateBases` will raise `TypeError` at runtime ``` @@ -41,7 +40,6 @@ error[invalid-base]: Invalid class base with type `Literal[1]` | 5 | class InlineTupleInvalidBases(*(int, 1)): ... | ^ - | info: Definition of class `InlineTupleInvalidBases` will raise `TypeError` at runtime ``` 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" index c3824309d7..353c2c160a 100644 --- "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" @@ -30,6 +30,5 @@ error[inconsistent-mro]: Cannot create a consistent method resolution order (MRO | 7 | class Baz(Protocol[T], Foo, Bar[T]): ... # error: [inconsistent-mro] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | ``` 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" index fae0fef2f0..3577953f28 100644 --- "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" @@ -51,7 +51,6 @@ warning[unsupported-base]: Unsupported class base | 17 | class Foo(x): ... | ^ Has type ` | ` - | info: ty cannot resolve a consistent method resolution order (MRO) for class `Foo` due to this base info: Only class objects or `Any` are supported as class bases @@ -63,7 +62,6 @@ warning[unsupported-base]: Unsupported class base | 28 | class D(C): ... # error: [unsupported-base] | ^ Has type `.C @ src/mdtest_snippet.py:23:15'> | .C @ src/mdtest_snippet.py:26:15'>` - | info: ty cannot resolve a consistent method resolution order (MRO) for class `D` due to this base info: Only class objects or `Any` are supported as class bases 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" index 2f460e3e2c..b9469efc22 100644 --- "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" @@ -47,7 +47,6 @@ error[invalid-base]: Invalid class base with type `Literal[2]` | 1 | class Foo(2): ... # error: [invalid-base] | ^ - | info: Definition of class `Foo` will raise `TypeError` at runtime ``` @@ -58,7 +57,6 @@ warning[unsupported-base]: Unsupported class base | 6 | class Bar(Foo()): ... # error: [unsupported-base] | ^^^^^ Has type `Foo` - | info: ty cannot resolve a consistent method resolution order (MRO) for class `Bar` due to this base info: Only class objects or `Any` are supported as class bases @@ -70,7 +68,6 @@ error[invalid-base]: Invalid class base with type `Bad1` | 15 | class BadSub1(Bad1()): ... # 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 @@ -84,7 +81,6 @@ error[invalid-base]: Invalid class base with type `Bad2` | 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 @@ -97,7 +93,6 @@ error[invalid-base]: Invalid class base with type `HasMroEntries | NoMroEntries` | 24 | class Foo(base): ... # error: [invalid-base] | ^^^^ - | info: Definition of class `Foo` 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 `HasMroEntries | NoMroEntries` may have an `__mro_entries__` attribute, but it may be missing 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" index 6ed04adbfb..4cdfd9adc0 100644 --- "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" @@ -91,7 +91,6 @@ error[duplicate-base]: Duplicate base class `str` | ^^^ --- ^^^ Class `str` later repeated here | | | Class `str` first included in bases list here - | info: Definition of class `Foo` will raise `TypeError` at runtime ``` @@ -110,7 +109,6 @@ error[duplicate-base]: Duplicate base class `Eggs` 21 | Spam, 22 | Eggs, | ^^^^ Class `Eggs` later repeated here - | info: Definition of class `Ham` will raise `TypeError` at runtime ``` @@ -128,7 +126,6 @@ error[duplicate-base]: Duplicate base class `Spam` 20 | Baz, 21 | Spam, | ^^^^ Class `Spam` later repeated here - | info: Definition of class `Ham` will raise `TypeError` at runtime ``` @@ -141,7 +138,6 @@ error[duplicate-base]: Duplicate base class `Mushrooms` | ^^^^^^^^ --------- ^^^^^^^^^ Class `Mushrooms` later repeated here | | | Class `Mushrooms` first included in bases list here - | info: Definition of class `Omelette` will raise `TypeError` at runtime ``` @@ -165,7 +161,6 @@ error[duplicate-base]: Duplicate base class `Eggs` 45 | Baz, 46 | Eggs, | ^^^^ Class `Eggs` later repeated here - | info: Definition of class `VeryEggyOmelette` will raise `TypeError` at runtime ``` @@ -180,7 +175,6 @@ error[duplicate-base]: Duplicate base class `A` | - Class `A` first included in bases list here 61 | A | ^ Class `A` later repeated here - | info: Definition of class `C` will raise `TypeError` at runtime ``` @@ -191,7 +185,6 @@ warning[unused-type-ignore-comment]: Unused `type: ignore` directive | 63 | ): # type: ignore[ty:duplicate-base] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove the unused suppression comment | 62 | # error: [unused-type-ignore-comment] diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_Edge_case___multiple_\342\200\246_(f30babd05c89dce9).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_Edge_case___multiple_\342\200\246_(f30babd05c89dce9).snap" index 1dce9a6fbc..1c4e2074c6 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_Edge_case___multiple_\342\200\246_(f30babd05c89dce9).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_Edge_case___multiple_\342\200\246_(f30babd05c89dce9).snap" @@ -37,7 +37,6 @@ error[invalid-named-tuple]: NamedTuple field name cannot start with an underscor | 8 | _asdict: bool # error: [invalid-named-tuple] "NamedTuple field `_asdict` cannot start with an underscore" | ^^^^^^^^^^^^^ Class definition will raise `TypeError` at runtime due to this field - | ``` @@ -47,7 +46,6 @@ error[invalid-named-tuple]: Cannot overwrite NamedTuple attribute `_asdict` | 14 | _asdict = True | ^^^^^^^ - | info: This will cause the class creation to fail at runtime ``` @@ -58,7 +56,6 @@ error[invalid-named-tuple]: Cannot overwrite NamedTuple attribute `_asdict` | 14 | _asdict = True | ^^^^^^^ - | info: This will cause the class creation to fail at runtime ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_NamedTuples_cannot_h\342\200\246_(e2ed186fe2b2fc35).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_NamedTuples_cannot_h\342\200\246_(e2ed186fe2b2fc35).snap" index 00c621984e..84df168350 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_NamedTuples_cannot_h\342\200\246_(e2ed186fe2b2fc35).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_NamedTuples_cannot_h\342\200\246_(e2ed186fe2b2fc35).snap" @@ -51,7 +51,6 @@ error[invalid-named-tuple]: NamedTuple field name cannot start with an underscor | 5 | _bar: int | ^^^^^^^^^ Class definition will raise `TypeError` at runtime due to this field - | ``` @@ -61,7 +60,6 @@ error[invalid-named-tuple]: Field name `_x` in `NamedTuple()` cannot start with | 15 | Underscore = NamedTuple("Underscore", [("_x", int), ("y", str)]) | ^^^^^^^^^^^^^^^^^^^^^^^^^ Will raise `ValueError` at runtime - | ``` @@ -71,7 +69,6 @@ error[invalid-named-tuple]: Field name `class` in `NamedTuple()` cannot be a Pyt | 19 | Keyword = NamedTuple("Keyword", [("x", int), ("class", str)]) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Will raise `ValueError` at runtime - | ``` @@ -81,7 +78,6 @@ error[invalid-named-tuple]: Duplicate field name `x` in `NamedTuple()` | 23 | Duplicate = NamedTuple("Duplicate", [("x", int), ("y", str), ("x", float)]) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Field `x` already defined; will raise `ValueError` at runtime - | ``` @@ -91,6 +87,5 @@ error[invalid-named-tuple]: Field name `not valid` in `NamedTuple()` is not a va | 27 | Invalid = NamedTuple("Invalid", [("not valid", int), ("ok", str)]) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Will raise `ValueError` at runtime - | ``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_`typing.NamedTuple`_-_Definition_(bbf79630502e65e9).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_`typing.NamedTuple`_-_Definition_(bbf79630502e65e9).snap index 441d9f1d73..57be141c45 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_`typing.NamedTuple`_-_Definition_(bbf79630502e65e9).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_`typing.NamedTuple`_-_Definition_(bbf79630502e65e9).snap @@ -41,20 +41,19 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/named_tuple.md ``` error[invalid-named-tuple]: NamedTuple field without default value cannot follow field(s) with default value(s) - --> src/mdtest_snippet.py:4:5 + --> src/mdtest_snippet.py:6:5 | 4 | altitude: float = 0.0 | --------------------- Earlier field `altitude` defined here with a default value 5 | # error: [invalid-named-tuple] "NamedTuple field without default value cannot follow field(s) with default value(s): Field `latitud… 6 | latitude: float | ^^^^^^^^^^^^^^^ Field `latitude` defined here without a default value - | ``` ``` error[invalid-named-tuple]: NamedTuple field without default value cannot follow field(s) with default value(s) - --> src/mdtest_snippet.py:4:5 + --> src/mdtest_snippet.py:8:5 | 4 | altitude: float = 0.0 | --------------------- Earlier field `altitude` defined here with a default value @@ -63,32 +62,29 @@ error[invalid-named-tuple]: NamedTuple field without default value cannot follow 7 | # error: [invalid-named-tuple] "NamedTuple field without default value cannot follow field(s) with default value(s): Field `longitu… 8 | longitude: float | ^^^^^^^^^^^^^^^^ Field `longitude` defined here without a default value - | ``` ``` error[invalid-named-tuple]: NamedTuple field without default value cannot follow field(s) with default value(s) - --> src/mdtest_snippet.py:14:5 + --> src/mdtest_snippet.py:15:5 | 14 | altitude: float = 0.0 | --------------------- Earlier field `altitude` defined here with a default value 15 | latitude: float # error: [invalid-named-tuple] | ^^^^^^^^^^^^^^^ Field `latitude` defined here without a default value - | ``` ``` error[invalid-named-tuple]: NamedTuple field without default value cannot follow field(s) with default value(s) - --> src/mdtest_snippet.py:14:5 + --> src/mdtest_snippet.py:16:5 | 14 | altitude: float = 0.0 | --------------------- Earlier field `altitude` defined here with a default value 15 | latitude: float # error: [invalid-named-tuple] 16 | longitude: float # error: [invalid-named-tuple] | ^^^^^^^^^^^^^^^^ Field `longitude` defined here without a default value - | ``` @@ -98,7 +94,6 @@ error[invalid-named-tuple]: NamedTuple field without default value cannot follow | 20 | latitude: float # error: [invalid-named-tuple] | ^^^^^^^^^^^^^^^ Field `latitude` defined here without a default value - | info: Earlier field `altitude` was defined with a default value ``` @@ -109,7 +104,6 @@ error[invalid-named-tuple]: NamedTuple field without default value cannot follow | 21 | longitude: float # error: [invalid-named-tuple] | ^^^^^^^^^^^^^^^^ Field `longitude` defined here without a default value - | info: Earlier field `altitude` was defined with a default value ``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_`typing.NamedTuple`_-_Multiple_Inheritance_(82ed33d1b3b433d8).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_`typing.NamedTuple`_-_Multiple_Inheritance_(82ed33d1b3b433d8).snap index e184e4e373..89fb704a2a 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_`typing.NamedTuple`_-_Multiple_Inheritance_(82ed33d1b3b433d8).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_`typing.NamedTuple`_-_Multiple_Inheritance_(82ed33d1b3b433d8).snap @@ -57,7 +57,6 @@ error[invalid-named-tuple]: NamedTuple class `C` cannot use multiple inheritance | 4 | class C(NamedTuple, object): | ^^^^^^ - | ``` @@ -67,7 +66,6 @@ error[invalid-named-tuple]: NamedTuple class `D` cannot use multiple inheritance | 10 | int, # error: [invalid-named-tuple] | ^^^ - | ``` @@ -77,6 +75,5 @@ error[invalid-named-tuple]: NamedTuple class `E` cannot use multiple inheritance | 17 | class E(NamedTuple, Protocol): ... | ^^^^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_`typing.NamedTuple`_-_Name_mismatch_diagno\342\200\246_(8ca723b970e370d0).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_`typing.NamedTuple`_-_Name_mismatch_diagno\342\200\246_(8ca723b970e370d0).snap" index b40529677c..3912a521a6 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_`typing.NamedTuple`_-_Name_mismatch_diagno\342\200\246_(8ca723b970e370d0).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_`typing.NamedTuple`_-_Name_mismatch_diagno\342\200\246_(8ca723b970e370d0).snap" @@ -30,6 +30,5 @@ warning[mismatched-type-name]: The name passed to `NamedTuple` must match the va | 5 | Mismatch = NamedTuple("WrongName", [("x", int)]) | ^^^^^^^^^^^ Expected "Mismatch", got "WrongName" - | ``` 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" index 4657987302..914ada6c79 100644 --- "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" @@ -24,7 +24,6 @@ error[no-matching-overload]: No overload of class `type` matches arguments | 1 | type() # error: [no-matching-overload] | ^^^^^^ - | help: `builtins.type()` can either be called with one or three positional arguments (got 0) ``` 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" index d9ec2d4d0a..892d856520 100644 --- "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" @@ -35,14 +35,12 @@ error[no-matching-overload]: No overload of bound method `Foo.bar` matches argum | 12 | foo.bar(b"wat") # error: [no-matching-overload] | ^^^^^^^^^^^^^^^ - | info: First overload defined here --> src/mdtest_snippet.py:4:5 | 4 | / @overload 5 | | def bar(self, x: int) -> int: ... | |_____________________________________^ First overload defined here - | info: Possible overloads for bound method `bar`: info: (self, x: int) -> int info: (self, x: str) -> str @@ -51,6 +49,5 @@ info: Overload implementation defined here | 8 | def bar(self, x: int | str) -> int | str: | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_An_explicit_`__get__\342\200\246_(9ecd21d0927ee1ff).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_An_explicit_`__get__\342\200\246_(9ecd21d0927ee1ff).snap" index 27e1354f70..d7aab53c57 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_An_explicit_`__get__\342\200\246_(9ecd21d0927ee1ff).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_An_explicit_`__get__\342\200\246_(9ecd21d0927ee1ff).snap" @@ -35,14 +35,12 @@ error[no-matching-overload]: No overload of method wrapper `__get__` of function | 12 | f.__get__() # error: [no-matching-overload] | ^^^^^^^^^^^ - | info: First overload defined here --> src/mdtest_snippet.py:3:1 | 3 | / @overload 4 | | def f(x: int) -> int: ... | |_________________________^ First overload defined here - | info: Possible overloads for method wrapper `__get__` of function `f`: info: (x: int) -> int info: (x: str) -> str @@ -52,6 +50,5 @@ info: Overload implementation defined here | 9 | def f(x: int | str | bytes) -> int | str | bytes: | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | ``` 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" index 7321331144..ca6062fd07 100644 --- "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" @@ -72,14 +72,12 @@ error[no-matching-overload]: No overload of function `foo` matches arguments | 49 | foo(Foo(), Foo()) # error: [no-matching-overload] | ^^^^^^^^^^^^^^^^^ - | info: First overload defined here --> src/mdtest_snippet.py:5:1 | 5 | / @overload 6 | | def foo(a: int, b: int, c: int): ... | |____________________________________^ First overload defined here - | info: Possible overloads for function `foo`: info: (a: int, b: int, c: int) -> None info: (a: str, b: int, c: int) -> None @@ -107,6 +105,5 @@ info: Overload implementation defined here | 47 | def foo(a, b, c): ... | ^^^^^^^^^^^^ - | ``` 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" index 64c0a1f010..35e2827c8e 100644 --- "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" @@ -152,14 +152,12 @@ error[no-matching-overload]: No overload of function `foo` matches arguments | 129 | foo(Foo(), Foo()) # error: [no-matching-overload] | ^^^^^^^^^^^^^^^^^ - | info: First overload defined here --> src/mdtest_snippet.py:5:1 | 5 | / @overload 6 | | def foo(a: int, b: int, c: int): ... | |____________________________________^ First overload defined here - | info: Possible overloads for function `foo`: info: (a: int, b: int, c: int) -> None info: (a: str, b: int, c: int) -> None @@ -217,6 +215,5 @@ info: Overload implementation defined here | 127 | def foo(a, b, c): ... | ^^^^^^^^^^^^ - | ``` 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" index 63d8843741..91fc4c1e35 100644 --- "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" @@ -33,14 +33,12 @@ error[no-matching-overload]: No overload of function `f` matches arguments | 10 | f(b"foo") # error: [no-matching-overload] | ^^^^^^^^^ - | info: First overload defined here --> src/mdtest_snippet.py:3:1 | 3 | / @overload 4 | | def f(x: int) -> int: ... | |_________________________^ First overload defined here - | info: Possible overloads for function `f`: info: (x: int) -> int info: (x: str) -> str @@ -49,6 +47,5 @@ info: Overload implementation defined here | 7 | def f(x: int | str) -> int | str: | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | ``` 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" index e641681188..ac0bde43f0 100644 --- "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" @@ -84,7 +84,6 @@ error[no-matching-overload]: No overload of function `f` matches arguments | 61 | f(b"foo") # error: [no-matching-overload] | ^^^^^^^^^ - | info: First overload defined here --> src/mdtest_snippet.py:3:1 | @@ -108,7 +107,6 @@ info: First overload defined here 20 | | hyena: int, 21 | | ) -> int: ... | |_____________^ First overload defined here - | 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 @@ -135,6 +133,5 @@ info: Overload implementation defined here 57 | | hyena: int | str, 58 | | ) -> int | str: | |______________^ - | ``` 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" index f5cac57e3e..be70aba4af 100644 --- "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" @@ -36,26 +36,24 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/overloads.md ``` error[invalid-overload]: Overloaded function `func` requires at least two overloads - --> src/mdtest_snippet.py:3:1 + --> src/mdtest_snippet.py:5:5 | 3 | @overload | --------- 4 | # error: [invalid-overload] 5 | def func(x: int) -> int: ... | ^^^^ Only one overload defined here - | ``` ``` error[invalid-overload]: Overloaded function `func` requires at least two overloads - --> src/mdtest_snippet.pyi:3:1 + --> src/mdtest_snippet.pyi:5:5 | 3 | @overload | --------- 4 | # error: [invalid-overload] 5 | def func(x: int) -> int: ... | ^^^^ Only one overload defined here - | ``` 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" index 5d1efa29cd..6d5aa76712 100644 --- "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" @@ -95,7 +95,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/overloads.md ``` error[invalid-overload]: Overloaded function `try_from1` does not use the `@classmethod` decorator consistently - --> src/mdtest_snippet.py:12:5 + --> src/mdtest_snippet.py:16:9 | 12 | @overload | --------- @@ -105,7 +105,6 @@ error[invalid-overload]: Overloaded function `try_from1` does not use the `@clas 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: | ^^^^^^^^^ - | ``` @@ -122,7 +121,6 @@ error[invalid-overload]: Overloaded function `try_from2` does not use the `@clas | --------- 22 | def try_from2(cls, x: int) -> CheckClassMethod: ... | --------- Missing here - | ``` @@ -131,10 +129,9 @@ error[invalid-overload]: Overloaded function `try_from3` does not use the `@clas --> src/mdtest_snippet.py:40:9 | 40 | def try_from3(cls, x: int | str) -> CheckClassMethod | None: - | --------- + | ^^^^^^^^^ | | | Missing here - | ``` @@ -144,19 +141,17 @@ error[call-non-callable]: Object of type `CheckClassMethod` is not callable | 43 | return cls(x) | ^^^^^^ - | ``` ``` error[invalid-assignment]: Object of type `bound method .from_value(x: int) -> int` is not assignable to `(str, /) -> str` - --> src/mdtest_snippet.py:76:6 + --> src/mdtest_snippet.py:76:29 | 76 | bad: Callable[[str], str] = Base.from_value | -------------------- ^^^^^^^^^^^^^^^ Incompatible value of type `bound method .from_value(x: int) -> int` | | | Declared type - | info: incompatible return types: `int` is not assignable to `str` ``` 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" index bcade81649..91268b403d 100644 --- "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" @@ -76,7 +76,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/overloads.md ``` error[invalid-overload]: `@final` decorator should be applied only to the overload implementation - --> src/mdtest_snippet.py:12:5 + --> src/mdtest_snippet.py:15:9 | 12 | @overload | --------- @@ -89,13 +89,12 @@ error[invalid-overload]: `@final` decorator should be applied only to the overlo 17 | def method2(self, x: str) -> str: ... 18 | def method2(self, x: int | str) -> int | str: | ------- Implementation defined here - | ``` ``` error[invalid-overload]: `@final` decorator should be applied only to the overload implementation - --> src/mdtest_snippet.py:23:5 + --> src/mdtest_snippet.py:26:9 | 23 | @overload | --------- @@ -106,13 +105,12 @@ error[invalid-overload]: `@final` decorator should be applied only to the overlo | ^^^^^^^ 27 | def method3(self, x: int | str) -> int | str: | ------- Implementation defined here - | ``` ``` error[invalid-overload]: `@final` decorator should be applied only to the first overload - --> src/mdtest_snippet.pyi:9:5 + --> src/mdtest_snippet.pyi:14:9 | 9 | / @overload 10 | | def method2(self, x: int) -> int: ... @@ -123,13 +121,12 @@ error[invalid-overload]: `@final` decorator should be applied only to the first 13 | # error: [invalid-overload] 14 | def method2(self, x: str) -> str: ... | ^^^^^^^ - | ``` ``` error[invalid-overload]: `@final` decorator should be applied only to the first overload - --> src/mdtest_snippet.pyi:15:5 + --> src/mdtest_snippet.pyi:19:9 | 15 | / @overload 16 | | def method3(self, x: int) -> int: ... @@ -139,13 +136,12 @@ error[invalid-overload]: `@final` decorator should be applied only to the first 18 | @overload 19 | def method3(self, x: str) -> int: ... # error: [invalid-overload] | ^^^^^^^ - | ``` ``` error[invalid-overload]: `@final` decorator should be applied only to the first overload - --> src/mdtest_snippet.pyi:15:5 + --> src/mdtest_snippet.pyi:22:9 | 15 | / @overload 16 | | def method3(self, x: int) -> int: ... @@ -158,6 +154,5 @@ error[invalid-overload]: `@final` decorator should be applied only to the first | ------ 22 | def method3(self, x: bytes) -> bytes: ... # error: [invalid-overload] | ^^^^^^^ - | ``` 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" index a6ac3a452e..349cfd46e3 100644 --- "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" @@ -84,7 +84,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/overloads.md ``` error[invalid-overload]: `@override` decorator should be applied only to the overload implementation - --> src/mdtest_snippet.py:23:5 + --> src/mdtest_snippet.py:26:9 | 23 | @overload | --------- @@ -95,13 +95,12 @@ error[invalid-overload]: `@override` decorator should be applied only to the ove | ^^^^^^ 27 | def method(self, x: int | str) -> int | str: | ------ Implementation defined here - | ``` ``` error[invalid-overload]: `@override` decorator should be applied only to the overload implementation - --> src/mdtest_snippet.py:31:5 + --> src/mdtest_snippet.py:34:9 | 31 | @overload | --------- @@ -114,13 +113,12 @@ error[invalid-overload]: `@override` decorator should be applied only to the ove 36 | def method(self, x: str) -> str: ... 37 | def method(self, x: int | str) -> int | str: | ------ Implementation defined here - | ``` ``` error[invalid-overload]: `@override` decorator should be applied only to the first overload - --> src/mdtest_snippet.pyi:17:5 + --> src/mdtest_snippet.pyi:22:9 | 17 | / @overload 18 | | def method(self, x: int) -> int: ... @@ -131,6 +129,5 @@ error[invalid-overload]: `@override` decorator should be applied only to the fir 21 | # error: [invalid-overload] 22 | def method(self, x: str) -> str: ... | ^^^^^^ - | ``` 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" index 7901a7572e..693a5564d6 100644 --- "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" @@ -37,7 +37,6 @@ error[invalid-overload]: Overloads for function `func` must be followed by a non | 5 | def func(x: int) -> int: ... | ^^^^ - | info: Attempting to call `func` will raise `TypeError` at runtime info: Overloaded functions without implementations are only permitted: info: - in stub files @@ -54,7 +53,6 @@ error[invalid-overload]: Overloads for function `method` must be followed by a n | 12 | def method(self, x: int) -> int: ... | ^^^^^^ - | info: Attempting to call `method` will raise `TypeError` at runtime info: Overloaded functions without implementations are only permitted: info: - in stub files diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_`@overload`-decorate\342\200\246_(d17a1580f99a6402).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_`@overload`-decorate\342\200\246_(d17a1580f99a6402).snap" index fa62b81262..e141cb5d17 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_`@overload`-decorate\342\200\246_(d17a1580f99a6402).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_`@overload`-decorate\342\200\246_(d17a1580f99a6402).snap" @@ -56,7 +56,6 @@ warning[useless-overload-body]: Useless body for `@overload`-decorated function | 23 | return x # error: [useless-overload-body] | ^^^^^^^^ This statement will never be executed - | info: `@overload`-decorated functions are solely for type checkers and must be overwritten at runtime by a non-`@overload`-decorated implementation help: Consider replacing this function body with `...` or `pass` @@ -68,7 +67,6 @@ warning[useless-overload-body]: Useless body for `@overload`-decorated function | 29 | print("oh no, a string") # error: [useless-overload-body] | ^^^^^^^^^^^^^^^^^^^^^^^^ This statement will never be executed - | info: `@overload`-decorated functions are solely for type checkers and must be overwritten at runtime by a non-`@overload`-decorated implementation help: Consider replacing this function body with `...` or `pass` @@ -76,14 +74,13 @@ help: Consider replacing this function body with `...` or `pass` ``` error[invalid-return-type]: Return type does not match returned value - --> src/mdtest_snippet.py:31:5 + --> src/mdtest_snippet.py:33:12 | 31 | def foo(x): | --- Expected `int | None` because of return type 32 | # error: [invalid-return-type] "Return type does not match returned value: expected `int | None`, found `int | str`" 33 | return x | ^ expected `int | None`, found `int | str` - | info: element `str` of union `int | str` is not assignable to `int | None` ``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/override.md_-_`typing.override`_-_Basics_(b7c220f8171f11f0).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/override.md_-_`typing.override`_-_Basics_(b7c220f8171f11f0).snap index 2fb6d32fdf..bfd766091c 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/override.md_-_`typing.override`_-_Basics_(b7c220f8171f11f0).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/override.md_-_`typing.override`_-_Basics_(b7c220f8171f11f0).snap @@ -194,134 +194,124 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/override.md ``` error[invalid-explicit-override]: Method `___reprrr__` is decorated with `@override` but does not override anything - --> src/mdtest_snippet.pyi:98:5 + --> src/mdtest_snippet.pyi:99:9 | 98 | @override | --------- 99 | def ___reprrr__(self): ... # error: [invalid-explicit-override] | ^^^^^^^^^^^ - | info: No `___reprrr__` definitions were found on any superclasses of `Invalid` ``` ``` error[invalid-explicit-override]: Method `foo` is decorated with `@override` but does not override anything - --> src/mdtest_snippet.pyi:100:5 + --> src/mdtest_snippet.pyi:102:9 | 100 | @override | --------- 101 | @classmethod 102 | def foo(self): ... # error: [invalid-explicit-override] | ^^^ - | info: No `foo` definitions were found on any superclasses of `Invalid` ``` ``` error[invalid-explicit-override]: Method `bar` is decorated with `@override` but does not override anything - --> src/mdtest_snippet.pyi:104:5 + --> src/mdtest_snippet.pyi:105:9 | 104 | @override | --------- 105 | def bar(self): ... # error: [invalid-explicit-override] | ^^^ - | info: No `bar` definitions were found on any superclasses of `Invalid` ``` ``` error[invalid-explicit-override]: Method `baz` is decorated with `@override` but does not override anything - --> src/mdtest_snippet.pyi:107:5 + --> src/mdtest_snippet.pyi:108:9 | 107 | @override | --------- 108 | def baz(): ... # error: [invalid-explicit-override] | ^^^ - | info: No `baz` definitions were found on any superclasses of `Invalid` ``` ``` error[invalid-explicit-override]: Method `eggs` is decorated with `@override` but does not override anything - --> src/mdtest_snippet.pyi:109:5 + --> src/mdtest_snippet.pyi:111:9 | 109 | @override | --------- 110 | @staticmethod 111 | def eggs(): ... # error: [invalid-explicit-override] | ^^^^ - | info: No `eggs` definitions were found on any superclasses of `Invalid` ``` ``` error[invalid-explicit-override]: Method `bad_property1` is decorated with `@override` but does not override anything - --> src/mdtest_snippet.pyi:113:5 + --> src/mdtest_snippet.pyi:114:9 | 113 | @override | --------- 114 | def bad_property1(self) -> int: ... # error: [invalid-explicit-override] | ^^^^^^^^^^^^^ - | info: No `bad_property1` definitions were found on any superclasses of `Invalid` ``` ``` error[invalid-explicit-override]: Method `bad_property2` is decorated with `@override` but does not override anything - --> src/mdtest_snippet.pyi:115:5 + --> src/mdtest_snippet.pyi:117:9 | 115 | @override | --------- 116 | @property 117 | def bad_property2(self) -> int: ... # error: [invalid-explicit-override] | ^^^^^^^^^^^^^ - | info: No `bad_property2` definitions were found on any superclasses of `Invalid` ``` ``` error[invalid-explicit-override]: Method `bad_settable_property` is decorated with `@override` but does not override anything - --> src/mdtest_snippet.pyi:119:5 + --> src/mdtest_snippet.pyi:120:9 | 119 | @override | --------- 120 | def bad_settable_property(self) -> int: ... # error: [invalid-explicit-override] | ^^^^^^^^^^^^^^^^^^^^^ - | info: No `bad_settable_property` definitions were found on any superclasses of `Invalid` ``` ``` error[invalid-explicit-override]: Method `lossy` is decorated with `@override` but does not override anything - --> src/mdtest_snippet.pyi:124:5 + --> src/mdtest_snippet.pyi:125:9 | 124 | @override | --------- 125 | def lossy(self): ... # error: [invalid-explicit-override] | ^^^^^ - | info: No `lossy` definitions were found on any superclasses of `Invalid` ``` ``` error[invalid-explicit-override]: Method `lossy2` is decorated with `@override` but does not override anything - --> src/mdtest_snippet.pyi:126:5 + --> src/mdtest_snippet.pyi:128:9 | 126 | @override | --------- 127 | @lossy_decorator 128 | def lossy2(self): ... # error: [invalid-explicit-override] | ^^^^^^ - | info: No `lossy2` definitions were found on any superclasses of `Invalid` ``` @@ -337,7 +327,6 @@ error[invalid-method-override]: Invalid override of method `class_method1` | 20 | def class_method1(cls) -> int: ... | ------------------------- `Parent.class_method1` defined here - | info: `LiskovViolatingButNotOverrideViolating.class_method1` is a staticmethod but `Parent.class_method1` is a classmethod info: This violates the Liskov Substitution Principle @@ -354,7 +343,6 @@ error[invalid-explicit-override]: Method `bar` is decorated with `@override` but | 156 | @override | --------- - | info: No `bar` definitions were found on any superclasses of `Foo` ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_Legacy_`ParamSpec`_-_Validating_`ParamSpe\342\200\246_(648be2a43987ffd8).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_Legacy_`ParamSpec`_-_Validating_`ParamSpe\342\200\246_(648be2a43987ffd8).snap" index 1b284285a5..34060323b9 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_Legacy_`ParamSpec`_-_Validating_`ParamSpe\342\200\246_(648be2a43987ffd8).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_Legacy_`ParamSpec`_-_Validating_`ParamSpe\342\200\246_(648be2a43987ffd8).snap" @@ -108,7 +108,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 24 | a1: P, | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -124,7 +123,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 26 | a3: Callable[[P], int], | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -140,7 +138,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 28 | a4: Callable[..., P], | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -156,7 +153,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 30 | a5: Callable[Concatenate[P, ...], int], | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -172,7 +168,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 32 | a6: P | int, | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -188,7 +183,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 34 | a7: Union[P, int], | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -204,7 +198,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 36 | a8: Optional[P], | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -220,7 +213,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 38 | a9: Annotated[P, "metadata"], | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -236,7 +228,6 @@ error[invalid-type-form]: The first argument to `Callable` must be either a list | 40 | a10: Callable["[int, str]", str], | ^^^^^^^^^^^^ - | 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 @@ -248,7 +239,6 @@ error[invalid-type-form]: The first argument to `Callable` must be either a list | 42 | a11: Callable["...", int], | ^^^^^ - | 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 @@ -260,7 +250,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a r | 46 | def invalid_return() -> P: | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -276,7 +265,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a t | 51 | x: P = y | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -292,7 +280,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a t | 55 | x: Final[P] = y | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -308,7 +295,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a r | 58 | def invalid_stringified_return() -> "P": | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -324,7 +310,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 63 | a: "P", | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -340,7 +325,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a t | 67 | x: "P" = y | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -356,7 +340,6 @@ error[invalid-type-form]: Bare ParamSpec `Q` is not valid in this context in a p | 74 | a: InvalidSpecializationTarget[[Q]], | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -372,7 +355,6 @@ error[invalid-type-form]: Bare ParamSpec `Q` is not valid in this context in a p | 76 | b: InvalidSpecializationTarget[Q,], | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_Legacy_`ParamSpec`_-_`ParamSpec`_cannot_s\342\200\246_(c9dbdc7b13b704a4).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_Legacy_`ParamSpec`_-_`ParamSpec`_cannot_s\342\200\246_(c9dbdc7b13b704a4).snap" index 30d66b02d2..31e331a2c6 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_Legacy_`ParamSpec`_-_`ParamSpec`_cannot_s\342\200\246_(c9dbdc7b13b704a4).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_Legacy_`ParamSpec`_-_`ParamSpec`_cannot_s\342\200\246_(c9dbdc7b13b704a4).snap" @@ -56,7 +56,6 @@ error[invalid-type-arguments]: ParamSpec `P` cannot be used to specialize type v | - Type variable `T` defined here 4 | P = ParamSpec("P") | - ParamSpec `P` defined here - | ``` @@ -66,6 +65,5 @@ error[invalid-type-arguments]: Type argument for `ParamSpec` must be either a li | 26 | def func3(c: ParamSpecAndTypeVar[T, int], other: T): ... | ^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_PEP_695_`ParamSpec`_-_Validating_`ParamSpe\342\200\246_(327594c6dacd8ad).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_PEP_695_`ParamSpec`_-_Validating_`ParamSpe\342\200\246_(327594c6dacd8ad).snap" index 99303e6579..170d8af1e6 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_PEP_695_`ParamSpec`_-_Validating_`ParamSpe\342\200\246_(327594c6dacd8ad).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_PEP_695_`ParamSpec`_-_Validating_`ParamSpe\342\200\246_(327594c6dacd8ad).snap" @@ -86,7 +86,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 11 | a1: P, | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -102,7 +101,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 13 | a3: Callable[[P], int], | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -118,7 +116,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 15 | a4: Callable[..., P], | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -134,7 +131,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 17 | a5: Callable[Concatenate[P, ...], int], | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -150,7 +146,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 19 | a6: P | int, | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -166,7 +161,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 21 | a7: Union[P, int], | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -182,7 +176,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 23 | a8: Optional[P], | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -198,7 +191,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 25 | a9: Annotated[P, "metadata"], | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -214,7 +206,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a r | 29 | def invalid_return[**P]() -> P: | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -230,7 +221,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a t | 33 | type Alias[**P] = P | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -246,7 +236,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a t | 37 | x: P = y | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -262,7 +251,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a t | 41 | x: Final[P] = y | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -278,7 +266,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a r | 44 | def invalid_stringified_return[**P]() -> "P": | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -294,7 +281,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 49 | a: "P", | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -310,7 +296,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a t | 53 | x: "P" = y | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -326,7 +311,6 @@ error[invalid-type-form]: Bare ParamSpec `Q` is not valid in this context in a p | 60 | a: InvalidSpecializationTarget[[Q]], | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -342,7 +326,6 @@ error[invalid-type-form]: Bare ParamSpec `Q` is not valid in this context in a p | 62 | b: InvalidSpecializationTarget[Q,], | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_PEP_695_`ParamSpec`_-_`ParamSpec`_cannot_s\342\200\246_(8243f67799c93e3c).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_PEP_695_`ParamSpec`_-_`ParamSpec`_cannot_s\342\200\246_(8243f67799c93e3c).snap" index 7c51e0f626..4216032909 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_PEP_695_`ParamSpec`_-_`ParamSpec`_cannot_s\342\200\246_(8243f67799c93e3c).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_PEP_695_`ParamSpec`_-_`ParamSpec`_cannot_s\342\200\246_(8243f67799c93e3c).snap" @@ -48,7 +48,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspe ``` error[invalid-type-arguments]: ParamSpec `P` cannot be used to specialize type variable `T` - --> src/mdtest_snippet.py:9:9 + --> src/mdtest_snippet.py:11:20 | 9 | def f[**P, T](): | - ParamSpec `P` defined here @@ -60,7 +60,6 @@ error[invalid-type-arguments]: ParamSpec `P` cannot be used to specialize type v | 3 | class OnlyTypeVar[T]: | - Type variable `T` defined here - | ``` @@ -79,7 +78,6 @@ error[invalid-type-arguments]: ParamSpec `P` cannot be used to specialize type v 8 | 9 | def f[**P, T](): | - ParamSpec `P` defined here - | ``` @@ -89,6 +87,5 @@ error[invalid-type-arguments]: Type argument for `ParamSpec` must be either a li | 29 | def func3[T](c: ParamSpecAndTypeVar[T, int], other: T): ... | ^ - | ``` 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" index 83895eae63..413c1927b8 100644 --- "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" @@ -48,7 +48,6 @@ error[call-non-callable]: Object of type `` is n | 4 | reveal_type(Protocol()) # revealed: Unknown | ^^^^^^^^^^ - | ``` @@ -58,13 +57,11 @@ error[call-non-callable]: Cannot instantiate class `MyProtocol` | 10 | reveal_type(MyProtocol()) # revealed: MyProtocol | ^^^^^^^^^^^^ This call will raise `TypeError` at runtime - | info: Protocol classes cannot be instantiated --> src/mdtest_snippet.py:6:7 | 6 | class MyProtocol(Protocol): | ^^^^^^^^^^^^^^^^^^^^ `MyProtocol` declared as a protocol here - | ``` @@ -74,12 +71,10 @@ error[call-non-callable]: Cannot instantiate class `GenericProtocol` | 16 | reveal_type(GenericProtocol[int]()) # revealed: GenericProtocol[int] | ^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime - | info: Protocol classes cannot be instantiated --> src/mdtest_snippet.py:12:7 | 12 | class GenericProtocol[T](Protocol): | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `GenericProtocol` declared as a protocol here - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Diagnostics_and_auto\342\200\246_(310665856cfe2424).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Diagnostics_and_auto\342\200\246_(310665856cfe2424).snap" index fb5b8f5be2..b04e1b5ef8 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Diagnostics_and_auto\342\200\246_(310665856cfe2424).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Diagnostics_and_auto\342\200\246_(310665856cfe2424).snap" @@ -55,7 +55,6 @@ error[invalid-generic-class]: Cannot both inherit from subscripted `Protocol` an | 5 | class Foo(Protocol[T], Generic[T]): ... # error: [invalid-generic-class] | ^^^^^^^^^^^ - | help: Remove the type parameters from the `Protocol` base | 4 | @@ -76,7 +75,6 @@ error[invalid-generic-class]: Cannot both inherit from subscripted `Protocol` an 11 | | T, 12 | | ], Generic[T]): ... | |_^ - | help: Remove the type parameters from the `Protocol` base | 9 | # error: [invalid-generic-class] @@ -100,7 +98,6 @@ error[invalid-generic-class]: Cannot both inherit from subscripted `Protocol` an 19 | | # very well documented code 20 | | ], # important comma! | |_^ - | help: Remove the type parameters from the `Protocol` base | 15 | # error: [invalid-generic-class] @@ -122,7 +119,6 @@ error[invalid-generic-class]: Cannot both inherit from subscripted `Protocol` an | 32 | class Foo[T](Protocol[T]): ... # error: [invalid-generic-class] | ^^^^^^^^^^^ - | help: Remove the type parameters from the `Protocol` base | 31 | diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Diagnostics_for_prot\342\200\246_(585a3e9545d41b64).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Diagnostics_for_prot\342\200\246_(585a3e9545d41b64).snap" index c62928847e..50abe4a3a5 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Diagnostics_for_prot\342\200\246_(585a3e9545d41b64).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Diagnostics_for_prot\342\200\246_(585a3e9545d41b64).snap" @@ -68,13 +68,11 @@ warning[ambiguous-protocol-member]: Cannot assign to undeclared variable in the | 12 | a = None # type: int | ^^^^^^^^ Consider adding an annotation for `a` - | info: Assigning to an undeclared variable in a protocol class leads to an ambiguous interface --> src/a.py:6:7 | 6 | class A(Protocol): | ^^^^^^^^^^^ `A` declared as a protocol here - | info: No declarations found for `a` in the body of `A` or any of its superclasses ``` @@ -85,13 +83,11 @@ warning[ambiguous-protocol-member]: Cannot assign to undeclared variable in the | 14 | b = ... # type: str | ^^^^^^^ Consider adding an annotation for `b` - | info: Assigning to an undeclared variable in a protocol class leads to an ambiguous interface --> src/a.py:6:7 | 6 | class A(Protocol): | ^^^^^^^^^^^ `A` declared as a protocol here - | info: No declarations found for `b` in the body of `A` or any of its superclasses ``` @@ -102,13 +98,11 @@ warning[ambiguous-protocol-member]: Cannot assign to undeclared variable in the | 17 | c = 1 # error: [ambiguous-protocol-member] | ^^^^^ Consider adding an annotation, e.g. `c: int = ...` - | info: Assigning to an undeclared variable in a protocol class leads to an ambiguous interface --> src/a.py:6:7 | 6 | class A(Protocol): | ^^^^^^^^^^^ `A` declared as a protocol here - | info: No declarations found for `c` in the body of `A` or any of its superclasses ``` @@ -119,13 +113,11 @@ warning[ambiguous-protocol-member]: Cannot assign to undeclared variable in the | 22 | for d in range(42): | ^ `d` is not declared as a protocol member - | info: Assigning to an undeclared variable in a protocol class leads to an ambiguous interface --> src/a.py:6:7 | 6 | class A(Protocol): | ^^^^^^^^^^^ `A` declared as a protocol here - | info: No declarations found for `d` in the body of `A` or any of its superclasses ``` 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" index 4b8f21004d..cfc81b9343 100644 --- "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" @@ -35,14 +35,12 @@ error[invalid-argument-type]: Invalid argument to `get_protocol_members` | 5 | get_protocol_members(NotAProtocol) # error: [invalid-argument-type] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime - | 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 | 3 | class NotAProtocol: ... | ^^^^^^^^^^^^ - | 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# @@ -54,14 +52,12 @@ error[invalid-argument-type]: Invalid argument to `get_protocol_members` | 9 | get_protocol_members(AlsoNotAProtocol) # error: [invalid-argument-type] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime - | 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 | 7 | class AlsoNotAProtocol(NotAProtocol, object): ... | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | 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# diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Match_class_patterns\342\200\246_(8ae0e231033b78e).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Match_class_patterns\342\200\246_(8ae0e231033b78e).snap" index 017fdee477..62f4a2a204 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Match_class_patterns\342\200\246_(8ae0e231033b78e).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Match_class_patterns\342\200\246_(8ae0e231033b78e).snap" @@ -52,13 +52,11 @@ error[isinstance-against-protocol]: Class `HasX` cannot be used in a class patte | 12 | case HasX(): # error: [isinstance-against-protocol] | ^^^^ This will raise `TypeError` at runtime - | info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable --> src/mdtest_snippet.py:3:7 | 3 | class HasX(Protocol): | ^^^^^^^^^^^^^^ `HasX` declared here - | info: A protocol class can only be used in a match class pattern 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 @@ -70,13 +68,11 @@ error[isinstance-against-protocol]: Class `HasX` cannot be used in a class patte | 28 | case Wrapper(inner=HasX()): # error: [isinstance-against-protocol] | ^^^^ This will raise `TypeError` at runtime - | info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable --> src/mdtest_snippet.py:3:7 | 3 | class HasX(Protocol): | ^^^^^^^^^^^^^^ `HasX` declared here - | info: A protocol class can only be used in a match class pattern 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 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" index 13a0b9d177..0c5ec01140 100644 --- "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" @@ -103,13 +103,11 @@ error[isinstance-against-protocol]: Class `HasX` cannot be used as the second ar | 7 | if isinstance(arg, HasX): # error: [isinstance-against-protocol] | ^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime - | info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable --> src/mdtest_snippet.py:3:7 | 3 | class HasX(Protocol): | ^^^^^^^^^^^^^^ `HasX` declared here - | 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 @@ -121,13 +119,11 @@ error[isinstance-against-protocol]: Class `HasX` cannot be used as the second ar | 12 | if issubclass(arg2, HasX): # error: [isinstance-against-protocol] | ^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime - | info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable --> src/mdtest_snippet.py:3:7 | 3 | class HasX(Protocol): | ^^^^^^^^^^^^^^ `HasX` declared here - | 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 @@ -139,13 +135,11 @@ error[isinstance-against-protocol]: Class `RuntimeCheckableHasX` cannot be used | 43 | if issubclass(arg1, RuntimeCheckableHasX): | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime - | info: A protocol class cannot be used in `issubclass` checks if it has non-method members --> src/mdtest_snippet.py:20:5 | 20 | x: int | ^ Non-method member `x` declared here - | ``` @@ -155,14 +149,12 @@ error[isinstance-against-protocol]: Class `MultipleNonMethodMembers` cannot be u | 48 | if issubclass(arg1, MultipleNonMethodMembers): # error: [isinstance-against-protocol] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime - | info: A protocol class cannot be used in `issubclass` checks if it has non-method members info: `MultipleNonMethodMembers` has non-method members `a` and `b` --> src/mdtest_snippet.py:39:5 | 39 | a: int | ^ Non-method member `a` declared here - | ``` @@ -172,13 +164,11 @@ error[isinstance-against-protocol]: Class `HasX` cannot be used as the second ar | 63 | isinstance(arg, (HasX, RuntimeCheckableHasX)) # error: [isinstance-against-protocol] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime - | info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable --> src/mdtest_snippet.py:3:7 | 3 | class HasX(Protocol): | ^^^^^^^^^^^^^^ `HasX` declared here - | 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 @@ -190,13 +180,11 @@ error[isinstance-against-protocol]: Class `HasX` cannot be used as the second ar | 64 | isinstance(arg, (HasX, int)) # error: [isinstance-against-protocol] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime - | info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable --> src/mdtest_snippet.py:3:7 | 3 | class HasX(Protocol): | ^^^^^^^^^^^^^^ `HasX` declared here - | 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 @@ -208,13 +196,11 @@ error[isinstance-against-protocol]: Class `HasX` cannot be used as the second ar | 68 | issubclass(arg2, (HasX, RuntimeCheckableHasX)) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime - | info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable --> src/mdtest_snippet.py:3:7 | 3 | class HasX(Protocol): | ^^^^^^^^^^^^^^ `HasX` declared here - | 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 @@ -226,13 +212,11 @@ error[isinstance-against-protocol]: Class `RuntimeCheckableHasX` cannot be used | 68 | issubclass(arg2, (HasX, RuntimeCheckableHasX)) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime - | info: A protocol class cannot be used in `issubclass` checks if it has non-method members --> src/mdtest_snippet.py:20:5 | 20 | x: int | ^ Non-method member `x` declared here - | ``` @@ -242,13 +226,11 @@ error[isinstance-against-protocol]: Class `HasX` cannot be used as the second ar | 70 | issubclass(arg2, (HasX, OnlyMethodMembers)) # error: [isinstance-against-protocol] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime - | info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable --> src/mdtest_snippet.py:3:7 | 3 | class HasX(Protocol): | ^^^^^^^^^^^^^^ `HasX` declared here - | 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 @@ -260,13 +242,11 @@ error[isinstance-against-protocol]: Class `HasX` cannot be used as the second ar | 72 | isinstance(arg, (int, (HasX, str))) # error: [isinstance-against-protocol] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime - | info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable --> src/mdtest_snippet.py:3:7 | 3 | class HasX(Protocol): | ^^^^^^^^^^^^^^ `HasX` declared here - | 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 @@ -278,13 +258,11 @@ error[isinstance-against-protocol]: Class `HasX` cannot be used as the second ar | 76 | issubclass(arg2, (int, (HasX, RuntimeCheckableHasX))) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime - | info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable --> src/mdtest_snippet.py:3:7 | 3 | class HasX(Protocol): | ^^^^^^^^^^^^^^ `HasX` declared here - | 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 @@ -296,13 +274,11 @@ error[isinstance-against-protocol]: Class `RuntimeCheckableHasX` cannot be used | 76 | issubclass(arg2, (int, (HasX, RuntimeCheckableHasX))) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime - | info: A protocol class cannot be used in `issubclass` checks if it has non-method members --> src/mdtest_snippet.py:20:5 | 20 | x: int | ^ Non-method member `x` declared here - | ``` @@ -312,13 +288,11 @@ error[isinstance-against-protocol]: Class `HasX` cannot be used as the second ar | 80 | isinstance(arg, classes) # error: [isinstance-against-protocol] | ^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime - | info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable --> src/mdtest_snippet.py:3:7 | 3 | class HasX(Protocol): | ^^^^^^^^^^^^^^ `HasX` declared here - | 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 diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Protocol_members_in_\342\200\246_(21be5d9bdab1c844).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Protocol_members_in_\342\200\246_(21be5d9bdab1c844).snap" index 7596d930a6..88ad76c836 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Protocol_members_in_\342\200\246_(21be5d9bdab1c844).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Protocol_members_in_\342\200\246_(21be5d9bdab1c844).snap" @@ -38,13 +38,11 @@ warning[ambiguous-protocol-member]: Cannot assign to undeclared variable in the | 12 | e = 56 # error: [ambiguous-protocol-member] | ^^^^^^ Consider adding an annotation, e.g. `e: int = ...` - | info: Assigning to an undeclared variable in a protocol class leads to an ambiguous interface --> src/mdtest_snippet.py:4:7 | 4 | class Foo(Protocol): | ^^^^^^^^^^^^^ `Foo` declared as a protocol here - | info: No declarations found for `e` in the body of `Foo` or any of its superclasses ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Diagnostics_for_`emp\342\200\246_(f44e56404a51ca26).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Diagnostics_for_`emp\342\200\246_(f44e56404a51ca26).snap" index 3b2a930a99..064faaad29 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Diagnostics_for_`emp\342\200\246_(f44e56404a51ca26).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Diagnostics_for_`emp\342\200\246_(f44e56404a51ca26).snap" @@ -30,7 +30,6 @@ error[empty-body]: Function always implicitly returns `None`, which is not assig | 7 | def method(self) -> str: ... # error: [empty-body] | ^^^ - | info: Consider changing the return annotation to `-> None` or adding a `return` statement info: Functions with empty bodies and non-`None` return types are only permitted: info: - in stub files @@ -43,7 +42,6 @@ info: Only classes that directly inherit from `typing.Protocol` or `typing_exten | 6 | class Concrete(Abstract): | ^^^^^^^^^^^^^^^^^^ `Protocol` not present in `Concrete`'s immediate bases - | info: See https://typing.python.org/en/latest/spec/protocol.html# ``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Generator_functions_-_Asynchronous_(408134055c24a538).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Generator_functions_-_Asynchronous_(408134055c24a538).snap index c930f1b4a3..5f4aa0fd81 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Generator_functions_-_Asynchronous_(408134055c24a538).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Generator_functions_-_Asynchronous_(408134055c24a538).snap @@ -44,7 +44,6 @@ error[invalid-return-type]: Return type does not match returned value | 16 | async def j() -> str: # error: [invalid-return-type] | ^^^ expected `str`, found `types.AsyncGeneratorType` - | 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 @@ -56,6 +55,5 @@ error[invalid-syntax]: `return` with value in async generator | 21 | return 2 # error: [invalid-syntax] "`return` with value in async generator" | ^^^^^^^^ - | ``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Generator_functions_-_Synchronous_(6a32ec69d15117b8).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Generator_functions_-_Synchronous_(6a32ec69d15117b8).snap index d684e31c9d..563566ebf3 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Generator_functions_-_Synchronous_(6a32ec69d15117b8).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Generator_functions_-_Synchronous_(6a32ec69d15117b8).snap @@ -59,7 +59,6 @@ error[invalid-return-type]: Return type does not match returned value | 19 | def j() -> str: # error: [invalid-return-type] | ^^^ expected `str`, found `types.GeneratorType` - | 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 @@ -67,27 +66,25 @@ info: See https://docs.python.org/3/glossary.html#term-generator for more detail ``` error[invalid-return-type]: Return type does not match returned value - --> src/mdtest_snippet.py:22:30 + --> src/mdtest_snippet.py:24:12 | 22 | def invalid_return_type() -> typing.Generator[None, None, None]: | ---------------------------------- Expected `None` because of return type 23 | yield 24 | return "" # error: [invalid-return-type] | ^^ expected `None`, found `Literal[""]` - | ``` ``` error[invalid-return-type]: Return type does not match returned value - --> src/mdtest_snippet.py:25:23 + --> src/mdtest_snippet.py:27:12 | 25 | def wrong_return() -> typing.Generator[int, int, int]: | ------------------------------- Expected `int` because of return type 26 | yield 1 27 | return "" # error: [invalid-return-type] | ^^ expected `int`, found `Literal[""]` - | ``` @@ -97,14 +94,13 @@ error[invalid-return-type]: Function always implicitly returns `None`, which is | 31 | def missing_return() -> typing.Generator[int, int, int]: # error: [invalid-return-type] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | info: Consider changing the return annotation to `-> None` or adding a `return` statement ``` ``` error[invalid-return-type]: Return type does not match returned value - --> src/mdtest_snippet.py:33:35 + --> src/mdtest_snippet.py:36:12 | 33 | def iterator_must_not_return() -> typing.Iterator[int]: | -------------------- Expected `None` because of return type @@ -112,6 +108,5 @@ error[invalid-return-type]: Return type does not match returned value 35 | # error: [invalid-return-type] 36 | return "foo" | ^^^^^ expected `None`, found `Literal["foo"]` - | ``` 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" index 16e9f47210..6588600c7e 100644 --- "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" @@ -33,7 +33,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/function/return_type.md ``` error[invalid-return-type]: Return type does not match returned value - --> src/mdtest_snippet.py:1:22 + --> src/mdtest_snippet.py:6:16 | 1 | def f(cond: bool) -> str: | --- Expected `str` because of return type @@ -43,13 +43,12 @@ error[invalid-return-type]: Return type does not match returned value 5 | # error: [invalid-return-type] 6 | return 1 | ^ expected `str`, found `Literal[1]` - | ``` ``` error[invalid-return-type]: Return type does not match returned value - --> src/mdtest_snippet.py:8:22 + --> src/mdtest_snippet.py:11:16 | 8 | def f(cond: bool) -> str: | --- Expected `str` because of return type @@ -57,7 +56,6 @@ error[invalid-return-type]: Return type does not match returned value 10 | # error: [invalid-return-type] 11 | return 1 | ^ expected `str`, found `Literal[1]` - | ``` @@ -72,6 +70,5 @@ error[invalid-return-type]: Return type does not match returned value | 8 | def f(cond: bool) -> str: | --- Expected `str` because of return type - | ``` 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" index e2883a0638..ad275ccb2c 100644 --- "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" @@ -46,7 +46,6 @@ error[invalid-return-type]: Function can implicitly return `None`, which is not | 6 | def f(cond: bool) -> int: | ^^^ - | ``` @@ -56,7 +55,6 @@ error[invalid-return-type]: Function always implicitly returns `None`, which is | 11 | def f(cond: bool) -> int: | ^^^ - | info: Consider changing the return annotation to `-> None` or adding a `return` statement ``` @@ -67,7 +65,6 @@ error[invalid-return-type]: Function can implicitly return `None`, which is not | 16 | def f(cond: bool) -> int: | ^^^ - | ``` @@ -77,7 +74,6 @@ warning[redundant-condition]: This condition is always false | 22 | if cond: | ^^^^ - | info: `Literal[False]` is always falsy ``` 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" index 778f3b10d0..48783ffacb 100644 --- "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" @@ -26,7 +26,6 @@ error[invalid-return-type]: Function always implicitly returns `None`, which is | 2 | def f() -> int: | ^^^ - | info: Consider changing the return annotation to `-> None` or adding a `return` statement ``` 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 index 2edd69296d..fe4187e42b 100644 --- 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 @@ -53,34 +53,31 @@ error[invalid-return-type]: Function always implicitly returns `None`, which is | 2 | def f() -> int: | ^^^ - | info: Consider changing the return annotation to `-> None` or adding a `return` statement ``` ``` error[invalid-return-type]: Return type does not match returned value - --> src/mdtest_snippet.py:5:12 + --> src/mdtest_snippet.py:7:12 | 5 | def f() -> str: | --- Expected `str` because of return type 6 | # error: [invalid-return-type] 7 | return 1 | ^ expected `str`, found `Literal[1]` - | ``` ``` error[invalid-return-type]: Return type does not match returned value - --> src/mdtest_snippet.py:9:12 + --> src/mdtest_snippet.py:11:5 | 9 | def f() -> int: | --- Expected `int` because of return type 10 | # error: [invalid-return-type] 11 | return | ^^^^^^ expected `int`, found `None` - | ``` @@ -90,7 +87,6 @@ error[empty-body]: Function always implicitly returns `None`, which is not assig | 18 | def m(x: T) -> T: ... | ^ - | info: Consider changing the return annotation to `-> None` or adding a `return` statement info: Functions with empty bodies and non-`None` return types are only permitted: info: - in stub files @@ -102,26 +98,24 @@ info: - or as `@abstractmethod`-decorated methods on abstract classes ``` error[invalid-return-type]: Return type does not match returned value - --> src/mdtest_snippet.py:22:12 + --> src/mdtest_snippet.py:24:12 | 22 | def f() -> A[int]: | ------ Expected `mdtest_snippet.A[int]` because of return type 23 | class A[T]: ... 24 | return A[int]() # error: [invalid-return-type] | ^^^^^^^^ expected `mdtest_snippet.A[int]`, found `mdtest_snippet..A[int]` - | ``` ``` error[invalid-return-type]: Return type does not match returned value - --> src/mdtest_snippet.py:28:12 + --> src/mdtest_snippet.py:30:12 | 28 | def g() -> B: | - Expected `mdtest_snippet.B` because of return type 29 | class B: ... 30 | return B() # error: [invalid-return-type] | ^^^ expected `mdtest_snippet.B`, found `mdtest_snippet..B` - | ``` 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" index 1c43d901aa..68ae2d8b0a 100644 --- "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" @@ -32,14 +32,13 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/function/return_type.md ``` error[invalid-return-type]: Return type does not match returned value - --> src/mdtest_snippet.pyi:1:12 + --> src/mdtest_snippet.pyi:3:12 | 1 | def f() -> int: | --- Expected `int` because of return type 2 | # error: [invalid-return-type] 3 | return ... | ^^^ expected `int`, found `EllipsisType` - | ``` @@ -49,7 +48,6 @@ error[invalid-return-type]: Function always implicitly returns `None`, which is | 6 | def foo() -> int: | ^^^ - | info: Consider changing the return annotation to `-> None` or adding a `return` statement ``` @@ -60,7 +58,6 @@ error[invalid-return-type]: Function always implicitly returns `None`, which is | 11 | def foo() -> int: | ^^^ - | info: Consider changing the return annotation to `-> None` or adding a `return` statement ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_class_within\342\200\246_(3259718bf20b45a2).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_class_within\342\200\246_(3259718bf20b45a2).snap" index 4e584caec4..6f2552df1a 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_class_within\342\200\246_(3259718bf20b45a2).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_class_within\342\200\246_(3259718bf20b45a2).snap" @@ -27,7 +27,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md ``` error[shadowed-type-variable]: Generic class `Bad1` uses type variable `T` already bound by an enclosing scope - --> src/mdtest_snippet.py:3:5 + --> src/mdtest_snippet.py:6:11 | 3 | def f[T](x: T, y: T) -> None: | ------------------------ Type variable `T` is bound in this enclosing scope @@ -35,13 +35,12 @@ error[shadowed-type-variable]: Generic class `Bad1` uses type variable `T` alrea 5 | # error: [shadowed-type-variable] 6 | class Bad1[T]: ... | ^^^^ `T` used in class definition here - | ``` ``` error[shadowed-type-variable]: Generic class `Bad2` uses type variable `T` already bound by an enclosing scope - --> src/mdtest_snippet.py:3:5 + --> src/mdtest_snippet.py:8:11 | 3 | def f[T](x: T, y: T) -> None: | ------------------------ Type variable `T` is bound in this enclosing scope @@ -51,6 +50,5 @@ error[shadowed-type-variable]: Generic class `Bad2` uses type variable `T` alrea 7 | # error: [shadowed-type-variable] 8 | class Bad2(Iterable[T]): ... | ^^^^^^^^^^^^^^^^^ `T` used in class definition here - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_class_within\342\200\246_(711fb86287c4d87b).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_class_within\342\200\246_(711fb86287c4d87b).snap" index 55db2ec578..702a9a71ed 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_class_within\342\200\246_(711fb86287c4d87b).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_class_within\342\200\246_(711fb86287c4d87b).snap" @@ -27,7 +27,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md ``` error[shadowed-type-variable]: Generic class `Bad1` uses type variable `T` already bound by an enclosing scope - --> src/mdtest_snippet.py:3:7 + --> src/mdtest_snippet.py:6:11 | 3 | class C[T]: | - Type variable `T` is bound in this enclosing scope @@ -35,13 +35,12 @@ error[shadowed-type-variable]: Generic class `Bad1` uses type variable `T` alrea 5 | # error: [shadowed-type-variable] 6 | class Bad1[T]: ... | ^^^^ `T` used in class definition here - | ``` ``` error[shadowed-type-variable]: Generic class `Bad2` uses type variable `T` already bound by an enclosing scope - --> src/mdtest_snippet.py:3:7 + --> src/mdtest_snippet.py:8:11 | 3 | class C[T]: | - Type variable `T` is bound in this enclosing scope @@ -51,6 +50,5 @@ error[shadowed-type-variable]: Generic class `Bad2` uses type variable `T` alrea 7 | # error: [shadowed-type-variable] 8 | class Bad2(Iterable[T]): ... | ^^^^^^^^^^^^^^^^^ `T` used in class definition here - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_function_wit\342\200\246_(f58a51442a16371e).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_function_wit\342\200\246_(f58a51442a16371e).snap" index a2a37ae67a..85cd276233 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_function_wit\342\200\246_(f58a51442a16371e).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_function_wit\342\200\246_(f58a51442a16371e).snap" @@ -33,6 +33,5 @@ error[shadowed-type-variable]: Generic function `bad` uses type variable `T` alr | 1 | def f[T](x: T, y: T) -> None: | ------------------------ Type variable `T` is bound in this enclosing scope - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_method_withi\342\200\246_(c19e9277cf9fafb5).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_method_withi\342\200\246_(c19e9277cf9fafb5).snap" index 29725bea77..1b5754cbb5 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_method_withi\342\200\246_(c19e9277cf9fafb5).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_method_withi\342\200\246_(c19e9277cf9fafb5).snap" @@ -33,6 +33,5 @@ error[shadowed-type-variable]: Generic function `bad` uses type variable `T` alr | 1 | class C[T]: | - Type variable `T` is bound in this enclosing scope - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Function_nested_in_c\342\200\246_(1a50b4ccb10b95dd).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Function_nested_in_c\342\200\246_(1a50b4ccb10b95dd).snap" index 9f7d87d5b2..4cd746fe2a 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Function_nested_in_c\342\200\246_(1a50b4ccb10b95dd).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Function_nested_in_c\342\200\246_(1a50b4ccb10b95dd).snap" @@ -23,14 +23,13 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md ``` error[invalid-type-variable-default]: Invalid default for type parameter `U` - --> src/mdtest_snippet.py:1:9 + --> src/mdtest_snippet.py:3:15 | 1 | class C[T]: | - `T` defined here 2 | # error: [invalid-type-variable-default] 3 | def f[U = T](self): ... | ^ `T` is a type parameter bound in an outer scope - | info: See https://typing.python.org/en/latest/spec/generics.html#scoping-rules ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_in_me\342\200\246_(2ed4c18a38ed9090).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_in_me\342\200\246_(2ed4c18a38ed9090).snap" index 82324be658..2a31bc02c2 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_in_me\342\200\246_(2ed4c18a38ed9090).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_in_me\342\200\246_(2ed4c18a38ed9090).snap" @@ -28,7 +28,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md ``` error[invalid-type-variable-default]: Invalid use of type variable `T2` - --> src/mdtest_snippet.py:4:1 + --> src/mdtest_snippet.py:8:25 | 4 | T2 = TypeVar("T2", default=T1) | ------------------------------ `T2` defined here @@ -37,7 +37,6 @@ error[invalid-type-variable-default]: Invalid use of type variable `T2` 7 | # error: [invalid-type-variable-default] "Invalid use of type variable `T2`: default of `T2` refers to out-of-scope type variable `… 8 | def method(self, x: T2) -> T2: | ^^ Default of `T2` references out-of-scope type variable `T1` - | info: See https://typing.python.org/en/latest/spec/generics.html#scoping-rules ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_in_ne\342\200\246_(a1aca17ea750ffdd).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_in_ne\342\200\246_(a1aca17ea750ffdd).snap" index 37b3cf576e..8e30747b70 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_in_ne\342\200\246_(a1aca17ea750ffdd).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_in_ne\342\200\246_(a1aca17ea750ffdd).snap" @@ -29,7 +29,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md ``` error[invalid-type-variable-default]: Invalid use of type variable `U` - --> src/mdtest_snippet.py:4:1 + --> src/mdtest_snippet.py:8:18 | 4 | U = TypeVar("U", default=T) | --------------------------- `U` defined here @@ -38,7 +38,6 @@ error[invalid-type-variable-default]: Invalid use of type variable `U` 7 | # error: [invalid-type-variable-default] 8 | def inner(y: U) -> U: | ^ Default of `U` references out-of-scope type variable `T` - | info: See https://typing.python.org/en/latest/spec/generics.html#scoping-rules ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_order\342\200\246_(d075a45828c9dbc5).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_order\342\200\246_(d075a45828c9dbc5).snap" index 946a5aee3d..b188fdd233 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_order\342\200\246_(d075a45828c9dbc5).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_order\342\200\246_(d075a45828c9dbc5).snap" @@ -43,7 +43,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md ``` error[invalid-type-variable-default]: Type parameters without defaults cannot follow type parameters with defaults - --> src/mdtest_snippet.py:9:10 + --> src/mdtest_snippet.py:9:17 | 9 | def f(x: T1, y: T2) -> tuple[T1, T2]: | -- ^^ Type variable `T2` does not have a default @@ -56,13 +56,12 @@ error[invalid-type-variable-default]: Type parameters without defaults cannot fo | ------------------------------- `T1` defined here 4 | T2 = TypeVar("T2") | ------------------ `T2` defined here - | ``` ``` error[invalid-type-variable-default]: Type parameters without defaults cannot follow type parameters with defaults - --> src/mdtest_snippet.py:13:17 + --> src/mdtest_snippet.py:13:24 | 13 | def g(x: T2, y: T1, z: T3) -> tuple[T2, T1, T3]: | -- ^^ Type variable `T3` does not have a default @@ -76,13 +75,12 @@ error[invalid-type-variable-default]: Type parameters without defaults cannot fo 4 | T2 = TypeVar("T2") 5 | T3 = TypeVar("T3") | ------------------ `T3` defined here - | ``` ``` error[invalid-type-variable-default]: Type parameters without defaults cannot follow type parameters with defaults - --> src/mdtest_snippet.py:17:10 + --> src/mdtest_snippet.py:17:17 | 17 | def h(x: T1, y: T2, z: DefaultStrT, w: T3) -> tuple[T1, T2, DefaultStrT, T3]: | -- ^^ Type variables `T2` and `T3` do not have defaults @@ -95,6 +93,5 @@ error[invalid-type-variable-default]: Type parameters without defaults cannot fo | ------------------------------- `T1` defined here 4 | T2 = TypeVar("T2") | ------------------ `T2` defined here - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_with_\342\200\246_(ce8defbeaf54e06c).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_with_\342\200\246_(ce8defbeaf54e06c).snap" index 98aa8d2eef..8e2a4102a5 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_with_\342\200\246_(ce8defbeaf54e06c).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_with_\342\200\246_(ce8defbeaf54e06c).snap" @@ -31,7 +31,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md ``` error[invalid-type-variable-default]: Invalid use of type variable `U` - --> src/mdtest_snippet.py:4:1 + --> src/mdtest_snippet.py:7:12 | 4 | U = TypeVar("U", default=T) | --------------------------- `U` defined here @@ -39,7 +39,6 @@ error[invalid-type-variable-default]: Invalid use of type variable `U` 6 | # error: [invalid-type-variable-default] 7 | def bad(y: U, z: T) -> tuple[U, T]: | ^ Default of `U` references later type parameter `T` - | info: See https://typing.python.org/en/latest/spec/generics.html#scoping-rules ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Nested_functions_(3f2ee9fa81da0177).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Nested_functions_(3f2ee9fa81da0177).snap" index 62bd9c1a5c..d461955cf6 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Nested_functions_(3f2ee9fa81da0177).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Nested_functions_(3f2ee9fa81da0177).snap" @@ -23,14 +23,13 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md ``` error[invalid-type-variable-default]: Invalid default for type parameter `U` - --> src/mdtest_snippet.py:1:11 + --> src/mdtest_snippet.py:3:19 | 1 | def outer[T](): | - `T` defined here 2 | # error: [invalid-type-variable-default] "Type parameter `U` cannot use outer-scope type parameter `T` as its default" 3 | def inner[U = T](): ... | ^ `T` is a type parameter bound in an outer scope - | info: See https://typing.python.org/en/latest/spec/generics.html#scoping-rules ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Type_alias_nested_in\342\200\246_(de027dcc5360f252).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Type_alias_nested_in\342\200\246_(de027dcc5360f252).snap" index 55e25a7604..7b56d2abb1 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Type_alias_nested_in\342\200\246_(de027dcc5360f252).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Type_alias_nested_in\342\200\246_(de027dcc5360f252).snap" @@ -24,14 +24,13 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md ``` error[invalid-type-variable-default]: Invalid default for type parameter `U` - --> src/mdtest_snippet.py:1:9 + --> src/mdtest_snippet.py:3:20 | 1 | class C[T]: | - `T` defined here 2 | # error: [invalid-type-variable-default] 3 | type Alias[U = T] = list[U] | ^ `T` is a type parameter bound in an outer scope - | info: See https://typing.python.org/en/latest/spec/generics.html#scoping-rules ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/single_matching_over\342\200\246_-_Single_matching_over\342\200\246_-_Bound_method_overloa\342\200\246_(39e892ccb644ee63).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/single_matching_over\342\200\246_-_Single_matching_over\342\200\246_-_Bound_method_overloa\342\200\246_(39e892ccb644ee63).snap" index 6af0555664..3c7bc0e068 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/single_matching_over\342\200\246_-_Single_matching_over\342\200\246_-_Bound_method_overloa\342\200\246_(39e892ccb644ee63).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/single_matching_over\342\200\246_-_Single_matching_over\342\200\246_-_Bound_method_overloa\342\200\246_(39e892ccb644ee63).snap" @@ -49,7 +49,6 @@ error[invalid-overload]: Implementation does not accept all arguments of this ov 12 | def f(self, x: str, y: int) -> str: ... 13 | def f( | - Implementation defined here - | info: Implementation signature `(self, x: bytes | int | str, y: int = 0) -> bytes | int | str` is not assignable to overload signature `(self: Other, x: bytes) -> bytes` info: parameter `self` has an incompatible type: `Other` is not assignable to `Base` @@ -61,13 +60,11 @@ error[invalid-argument-type]: Argument to bound method `Base.f` is incorrect | 20 | Base().f("ok", "bad") # error: [invalid-argument-type] | ^^^^^ Expected `int`, found `Literal["bad"]` - | info: Matching overload defined here --> src/mdtest_snippet.py:12:9 | 12 | def f(self, x: str, y: int) -> str: ... | ^ ------ Parameter declared here - | info: Non-matching overloads for bound method `f`: info: (self: Other, x: bytes) -> bytes info: (self, x: int) -> int 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" index c4c28369e1..e0c89ab03b 100644 --- "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" @@ -159,13 +159,11 @@ error[invalid-argument-type]: Argument to function `foo` is incorrect | 5 | foo("foo") # error: [invalid-argument-type] | ^^^^^ Expected `int`, found `Literal["foo"]` - | info: Matching overload defined here --> src/overloaded.pyi:4:5 | 4 | def foo(a: int): ... | ^^^ ------ Parameter declared here - | info: Non-matching overloads for function `foo`: info: (a: int, b: int, c: int) -> None info: (a: str, b: int, c: int) -> None 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" index c2f43cfdf0..2b6bf4efe4 100644 --- "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" @@ -39,13 +39,11 @@ error[invalid-argument-type]: Argument to function `f` is incorrect | 3 | f("a") # error: [invalid-argument-type] | ^^^ Expected `int`, found `Literal["a"]` - | info: Matching overload defined here --> src/overloaded.pyi:6:5 | 6 | def f(x: int) -> int: ... | ^ ------ Parameter declared here - | info: Non-matching overloads for function `f`: info: () -> None info: (x: int, y: int) -> int diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/special_form_attribu\342\200\246_-_Diagnostics_for_inva\342\200\246_(249d635e74a41c9e).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/special_form_attribu\342\200\246_-_Diagnostics_for_inva\342\200\246_(249d635e74a41c9e).snap" index 653c37cc17..bc70f34a72 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/special_form_attribu\342\200\246_-_Diagnostics_for_inva\342\200\246_(249d635e74a41c9e).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/special_form_attribu\342\200\246_-_Diagnostics_for_inva\342\200\246_(249d635e74a41c9e).snap" @@ -43,7 +43,6 @@ error[unresolved-attribute]: Special form `typing.Any` has no attribute `foo` | 14 | X.foo # error: [unresolved-attribute] | ^^^^^ - | help: Objects with type `Any` have a `foo` attribute, but the symbol `typing.Any` does not itself inhabit the type `Any` help: This error may indicate that `X` was defined as `X = typing.Any` when `X: typing.Any` was intended @@ -55,7 +54,6 @@ error[unresolved-attribute]: Special form `typing.Any` has no attribute `aaaaooo | 15 | X.aaaaooooooo # error: [unresolved-attribute] | ^^^^^^^^^^^^^ - | help: Objects with type `Any` have an `aaaaooooooo` attribute, but the symbol `typing.Any` does not itself inhabit the type `Any` help: This error may indicate that `X` was defined as `X = typing.Any` when `X: typing.Any` was intended @@ -67,7 +65,6 @@ error[unresolved-attribute]: Special form `typing.LiteralString` has no attribut | 16 | Foo.X.startswith # error: [unresolved-attribute] | ^^^^^^^^^^^^^^^^ - | help: Objects with type `LiteralString` have a `startswith` attribute, but the symbol `typing.LiteralString` does not itself inhabit the type `LiteralString` help: This error may indicate that `Foo.X` was defined as `Foo.X = typing.LiteralString` when `Foo.X: typing.LiteralString` was intended @@ -79,7 +76,6 @@ error[unresolved-attribute]: Special form `typing.LiteralString` has no attribut | 17 | Foo.Bar().y.startswith # error: [unresolved-attribute] | ^^^^^^^^^^^^^^^^^^^^^^ - | help: Objects with type `LiteralString` have a `startswith` attribute, but the symbol `typing.LiteralString` does not itself inhabit the type `LiteralString` ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/string.md_-_String_annotations_-_Partially_deferred_a\342\200\246_-_Python_less_than_3.1\342\200\246_(5e6477d05ddea33f).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/string.md_-_String_annotations_-_Partially_deferred_a\342\200\246_-_Python_less_than_3.1\342\200\246_(5e6477d05ddea33f).snap" index f784c93ac8..ad9488ac5f 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/string.md_-_String_annotations_-_Partially_deferred_a\342\200\246_-_Python_less_than_3.1\342\200\246_(5e6477d05ddea33f).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/string.md_-_String_annotations_-_Partially_deferred_a\342\200\246_-_Python_less_than_3.1\342\200\246_(5e6477d05ddea33f).snap" @@ -91,7 +91,6 @@ error[unsupported-operator]: Unsupported `|` operation | | | | | Has type `Literal["Foo"]` | Has type `` - | info: All parameter annotations are evaluated at runtime by default on Python <3.14 info: Python 3.13 was assumed when inferring types because it was specified on the command line help: Put quotes around the whole union rather than just certain elements @@ -107,7 +106,6 @@ error[unsupported-operator]: Unsupported `|` operation | | | | | Has type `Literal["memoryview"]` | Has type `` - | info: All parameter annotations are evaluated at runtime by default on Python <3.14 info: Python 3.13 was assumed when inferring types because it was specified on the command line help: Put quotes around the whole union rather than just certain elements @@ -123,7 +121,6 @@ error[unsupported-operator]: Unsupported `|` operation | | | | | Has type `None` | Has type `Literal["TD"]` - | info: All parameter annotations are evaluated at runtime by default on Python <3.14 info: Python 3.13 was assumed when inferring types because it was specified on the command line help: Put quotes around the whole union rather than just certain elements @@ -139,7 +136,6 @@ error[unsupported-operator]: Unsupported `|` operation | | | | | Has type `None` | Has type `Literal["P"]` - | info: All parameter annotations are evaluated at runtime by default on Python <3.14 info: Python 3.13 was assumed when inferring types because it was specified on the command line help: Put quotes around the whole union rather than just certain elements @@ -152,7 +148,6 @@ error[unsupported-operator]: Unsupported `|` operation | 33 | h: None | None, | ^^^^^^^^^^^ Both operands have type `None` - | info: All parameter annotations are evaluated at runtime by default on Python <3.14 info: Python 3.13 was assumed when inferring types because it was specified on the command line @@ -164,7 +159,6 @@ error[unresolved-reference]: Name `SomethingUndefined` used when not defined | 36 | i: SomethingUndefined | SomethingAlsoUndefined, | ^^^^^^^^^^^^^^^^^^ - | ``` @@ -174,7 +168,6 @@ error[unresolved-reference]: Name `SomethingAlsoUndefined` used when not defined | 36 | i: SomethingUndefined | SomethingAlsoUndefined, | ^^^^^^^^^^^^^^^^^^^^^^ - | ``` @@ -187,7 +180,6 @@ error[unsupported-operator]: Unsupported `|` operation | | | | | Has type `Literal["bytes"]` | Has type `` - | info: All parameter annotations are evaluated at runtime by default on Python <3.14 info: Python 3.13 was assumed when inferring types because it was specified on the command line help: Put quotes around the whole union rather than just certain elements @@ -203,7 +195,6 @@ error[unsupported-operator]: Unsupported `|` operation | | | | | Has type `None` | Has type `Literal["int"]` - | info: All parameter annotations are evaluated at runtime by default on Python <3.14 info: Python 3.13 was assumed when inferring types because it was specified on the command line help: Put quotes around the whole union rather than just certain elements @@ -219,7 +210,6 @@ error[unsupported-operator]: Unsupported `|` operation | | | | | Has type `None` | Has type `Literal["int"]` - | info: All type expressions are evaluated at runtime by default on Python <3.14 info: Python 3.13 was assumed when inferring types because it was specified on the command line help: Put quotes around the whole union rather than just certain elements diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Explicit_Super_Objec\342\200\246_(b753048091f275c0).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Explicit_Super_Objec\342\200\246_(b753048091f275c0).snap" index 3a985f7b41..d854223c8b 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Explicit_Super_Objec\342\200\246_(b753048091f275c0).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Explicit_Super_Objec\342\200\246_(b753048091f275c0).snap" @@ -129,7 +129,6 @@ error[unresolved-attribute]: Object of type `, C>` has no att | 20 | super(C, C()).c # error: [unresolved-attribute] | ^^^^^^^^^^^^^^^ - | ``` @@ -139,7 +138,6 @@ error[unresolved-attribute]: Object of type `, C>` has no att | 23 | super(B, C()).b # error: [unresolved-attribute] | ^^^^^^^^^^^^^^^ - | ``` @@ -149,7 +147,6 @@ error[unresolved-attribute]: Object of type `, C>` has no att | 24 | super(B, C()).c # error: [unresolved-attribute] | ^^^^^^^^^^^^^^^ - | ``` @@ -159,7 +156,6 @@ error[unresolved-attribute]: Object of type `, C>` has no att | 26 | super(A, C()).a # error: [unresolved-attribute] | ^^^^^^^^^^^^^^^ - | ``` @@ -169,7 +165,6 @@ error[unresolved-attribute]: Object of type `, C>` has no att | 27 | super(A, C()).b # error: [unresolved-attribute] | ^^^^^^^^^^^^^^^ - | ``` @@ -179,7 +174,6 @@ error[unresolved-attribute]: Object of type `, C>` has no att | 28 | super(A, C()).c # error: [unresolved-attribute] | ^^^^^^^^^^^^^^^ - | ``` @@ -189,7 +183,6 @@ error[invalid-super-argument]: `` is an abstract/st | 78 | reveal_type(super(object, x)) | ^^^^^^^^^^^^^^^^ - | ``` @@ -199,7 +192,6 @@ error[invalid-super-argument]: `(int, str, /) -> bool` is an abstract/structural | 82 | reveal_type(super(object, z)) | ^^^^^^^^^^^^^^^^ - | ``` @@ -209,6 +201,5 @@ error[invalid-super-argument]: `types.GenericAlias` instance `list[int]` is not | 98 | reveal_type(super(list[int], [])) | ^^^^^^^^^^^^^^^^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Implicit_Super_Objec\342\200\246_(f9e5e48e3a4a4c12).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Implicit_Super_Objec\342\200\246_(f9e5e48e3a4a4c12).snap" index 675b898a76..b4eb19178b 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Implicit_Super_Objec\342\200\246_(f9e5e48e3a4a4c12).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Implicit_Super_Objec\342\200\246_(f9e5e48e3a4a4c12).snap" @@ -166,7 +166,6 @@ info[missing-type-argument]: Missing type argument for generic class `Foo` (expe | 61 | def method3(self: Foo): # error: [missing-type-argument] | ^^^ - | ``` @@ -176,7 +175,6 @@ error[invalid-super-argument]: `S@method7` is not an instance or subclass of `` help: Consider adding an upper bound to type variable `S` @@ -189,7 +187,6 @@ error[invalid-super-argument]: `S@method8` is not an instance or subclass of `` @@ -201,7 +198,6 @@ error[invalid-super-argument]: `S@method9` is not an instance or subclass of `` @@ -213,7 +209,6 @@ error[invalid-super-argument]: `S@method10` is a type variable with an abstract/ | 100 | reveal_type(super()) | ^^^^^^^ - | info: Type variable `S` has upper bound `(...) -> str` ``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Metaclasses_(faeb52a8cd1533b3).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Metaclasses_(faeb52a8cd1533b3).snap index d4c42f028e..7b1b9425eb 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Metaclasses_(faeb52a8cd1533b3).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Metaclasses_(faeb52a8cd1533b3).snap @@ -63,7 +63,6 @@ error[invalid-super-argument]: `` is not an instance or subcl | 34 | super(Meta, OtherBase) # error: [invalid-super-argument] | ^^^^^^^^^^^^^^^^^^^^^^ - | ``` @@ -73,7 +72,6 @@ error[invalid-super-argument]: `type[T@__call__]` is not an instance or subclass | 40 | return super(BoundIntMeta, cls).__call__() # error: [invalid-super-argument] | ^^^^^^^^^^^^^^^^^^^^^^^^ - | info: Type variable `T` has upper bound `int` info: `type[int]` is not an instance or subclass of `` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Invalid_Usages_-_Diagnostic_when_the_\342\200\246_(93e8ab913ead83b2).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Invalid_Usages_-_Diagnostic_when_the_\342\200\246_(93e8ab913ead83b2).snap" index 456d3f18be..094e39d4ef 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Invalid_Usages_-_Diagnostic_when_the_\342\200\246_(93e8ab913ead83b2).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Invalid_Usages_-_Diagnostic_when_the_\342\200\246_(93e8ab913ead83b2).snap" @@ -34,6 +34,5 @@ error[invalid-super-argument]: Argument is not a valid class | 11 | super(A, A()) # error: [invalid-super-argument] | ^^^^^^^^^^^^^ Argument has type `.A @ src/mdtest_snippet.py:6:15'> | .A @ src/mdtest_snippet.py:9:15'>` - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/sync.md_-_With_statements_-_Context_expression_w\342\200\246_(3d4f2229d00f8d86).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/sync.md_-_With_statements_-_Context_expression_w\342\200\246_(3d4f2229d00f8d86).snap" index 036133bd83..1a5b7bf330 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/sync.md_-_With_statements_-_Context_expression_w\342\200\246_(3d4f2229d00f8d86).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/sync.md_-_With_statements_-_Context_expression_w\342\200\246_(3d4f2229d00f8d86).snap" @@ -40,6 +40,5 @@ error[invalid-context-manager]: Object of type `GoodManager | BadManager` cannot | 16 | with context_expr as f: | ^^^^^^^^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/sync.md_-_With_statements_-_Context_expression_w\342\200\246_(718dcfd7e6ed9829).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/sync.md_-_With_statements_-_Context_expression_w\342\200\246_(718dcfd7e6ed9829).snap" index f845368fbe..1e75bdf2b2 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/sync.md_-_With_statements_-_Context_expression_w\342\200\246_(718dcfd7e6ed9829).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/sync.md_-_With_statements_-_Context_expression_w\342\200\246_(718dcfd7e6ed9829).snap" @@ -39,7 +39,6 @@ error[invalid-context-manager]: Object of type `GoodManager | MissingExitManager | 15 | with context_expr as f: | ^^^^^^^^^^^^ - | info: `NotAContextManager` does not implement `__enter__` or `__exit__` info: `MissingExitManager` does not implement `__exit__` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/sync.md_-_With_statements_-_Context_expression_w\342\200\246_(8686e7748a7c975).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/sync.md_-_With_statements_-_Context_expression_w\342\200\246_(8686e7748a7c975).snap" index 02cebd2c08..c53e9081f6 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/sync.md_-_With_statements_-_Context_expression_w\342\200\246_(8686e7748a7c975).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/sync.md_-_With_statements_-_Context_expression_w\342\200\246_(8686e7748a7c975).snap" @@ -35,7 +35,6 @@ error[invalid-context-manager]: Object of type `Manager1 | NotAContextManager` c | 11 | with context_expr as f: | ^^^^^^^^^^^^ - | info: `NotAContextManager` does not implement `__enter__` or `__exit__` ``` 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" index 4929923292..8fff86797c 100644 --- "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" @@ -40,7 +40,6 @@ error[unsupported-bool-conversion]: Boolean conversion is not supported for type | 15 | a < b < b | ^^^^^ - | info: `__bool__` on `NotBoolable | Literal[False]` must be callable ``` 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" index d6aec2e9e4..ec8901fcd3 100644 --- "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" @@ -22,7 +22,14 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/comparison/tuples.md 7 | return NotBoolable() 8 | 9 | # error: [unsupported-bool-conversion] -10 | (A(),) == (A(),) +10 | reveal_type((A(),) == (A(),)) # revealed: bool +11 | # error: [unsupported-bool-conversion] +12 | reveal_type((A(), "x") == (A(), "y")) # revealed: Literal[False] +13 | # error: [unsupported-bool-conversion] +14 | reveal_type((A(),) != (A(), 0)) # revealed: Literal[True] +15 | def tuple_identity(left: tuple[A], right: tuple[A]) -> None: +16 | reveal_type(left is right) # revealed: bool +17 | reveal_type(left is not right) # revealed: bool ``` # Diagnostics @@ -34,30 +41,48 @@ error[invalid-method-override]: Invalid override of method `__eq__` 6 | def __eq__(self, other) -> NotBoolable: | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `object.__eq__` | - ::: stdlib/builtins.byi:89:9 + ::: stdlib/builtins.byi:90:9 | -89 | def __eq__(self, value: object, /) -> bool +90 | def __eq__(self, value: object, /) -> bool | -------------------------------------- `object.__eq__` defined here - | info: incompatible return types: `NotBoolable` is not assignable to `bool` info: This violates the Liskov Substitution Principle help: It is recommended for `__eq__` to work with arbitrary objects, for example: -help +help: help: def __eq__(self, other: object) -> bool: help: if not isinstance(other, A): help: return False help: return -help +help: + +``` + +``` +error[unsupported-bool-conversion]: Boolean conversion is not supported for type `NotBoolable` + --> src/mdtest_snippet.py:10:13 + | +10 | reveal_type((A(),) == (A(),)) # revealed: bool + | ^^^^^^^^^^^^^^^^ +info: `__bool__` on `NotBoolable` must be callable ``` ``` error[unsupported-bool-conversion]: Boolean conversion is not supported for type `NotBoolable` - --> src/mdtest_snippet.py:10:1 + --> src/mdtest_snippet.py:12:13 | -10 | (A(),) == (A(),) - | ^^^^^^^^^^^^^^^^ +12 | reveal_type((A(), "x") == (A(), "y")) # revealed: Literal[False] + | ^^^^^^^^^^^^^^^^^^^^^^^^ +info: `__bool__` on `NotBoolable` must be callable + +``` + +``` +error[unsupported-bool-conversion]: Boolean conversion is not supported for type `NotBoolable` + --> src/mdtest_snippet.py:14:13 | +14 | reveal_type((A(),) != (A(), 0)) # revealed: Literal[True] + | ^^^^^^^^^^^^^^^^^^ info: `__bool__` on `NotBoolable` must be callable ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Heterogeneous_-_Value_Comparisons_-_Comparison_Unsupport\342\200\246_(966dd82bd3668d0e).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Heterogeneous_-_Value_Comparisons_-_Comparison_Unsupport\342\200\246_(966dd82bd3668d0e).snap" index 4c94dacd08..379e13c47c 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Heterogeneous_-_Value_Comparisons_-_Comparison_Unsupport\342\200\246_(966dd82bd3668d0e).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Heterogeneous_-_Value_Comparisons_-_Comparison_Unsupport\342\200\246_(966dd82bd3668d0e).snap" @@ -53,7 +53,6 @@ error[unsupported-operator]: Unsupported `<` operation | | | | | Has type `tuple[Literal[1], Literal["hello"]]` | Has type `tuple[Literal[1], Literal[2]]` - | info: Operation fails because operator `<` is not supported between the tuple elements at index 2 (of type `Literal[2]` and `Literal["hello"]`) ``` @@ -67,7 +66,6 @@ error[unsupported-operator]: Unsupported `<=` operation | | | | | Has type `tuple[Literal[1], Literal["hello"]]` | Has type `tuple[Literal[1], Literal[2]]` - | info: Operation fails because operator `<=` is not supported between the tuple elements at index 2 (of type `Literal[2]` and `Literal["hello"]`) ``` @@ -81,7 +79,6 @@ error[unsupported-operator]: Unsupported `>` operation | | | | | Has type `tuple[Literal[1], Literal["hello"]]` | Has type `tuple[Literal[1], Literal[2]]` - | info: Operation fails because operator `>` is not supported between the tuple elements at index 2 (of type `Literal[2]` and `Literal["hello"]`) ``` @@ -95,7 +92,6 @@ error[unsupported-operator]: Unsupported `>=` operation | | | | | Has type `tuple[Literal[1], Literal["hello"]]` | Has type `tuple[Literal[1], Literal[2]]` - | info: Operation fails because operator `>=` is not supported between the tuple elements at index 2 (of type `Literal[2]` and `Literal["hello"]`) ``` @@ -108,7 +104,6 @@ error[unsupported-operator]: Unsupported `<` operation | -----------^^^----------- | | | Both operands have type `tuple[object]` - | info: Operation fails because operator `<` is not supported between the tuple elements at index 1 (both of type `object`) ``` @@ -121,7 +116,6 @@ error[unsupported-operator]: Unsupported `<` operation | -----------^^^----------- | | | Both operands have type `tuple[object]` - | info: Operation fails because operator `<` is not supported between the tuple elements at index 1 (both of type `object`) ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Homogeneous_-_Tuples_with_Prefixes\342\200\246_(c25079c01f6d8eb3).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Homogeneous_-_Tuples_with_Prefixes\342\200\246_(c25079c01f6d8eb3).snap" index 254cb309ea..0eae1e0107 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Homogeneous_-_Tuples_with_Prefixes\342\200\246_(c25079c01f6d8eb3).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Homogeneous_-_Tuples_with_Prefixes\342\200\246_(c25079c01f6d8eb3).snap" @@ -39,7 +39,6 @@ error[unsupported-operator]: Unsupported `<` operation | | | | | Has type `tuple[str, *tuple[int, ...]]` | Has type `tuple[int, *tuple[str, ...]]` - | info: Operation fails because operator `<` is not supported between objects of type `int` and `str` ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Homogeneous_-_Unsupported_Comparis\342\200\246_(400a427b33d53e00).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Homogeneous_-_Unsupported_Comparis\342\200\246_(400a427b33d53e00).snap" index 6d81e32027..144450cf1e 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Homogeneous_-_Unsupported_Comparis\342\200\246_(400a427b33d53e00).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Homogeneous_-_Unsupported_Comparis\342\200\246_(400a427b33d53e00).snap" @@ -65,7 +65,6 @@ error[unsupported-operator]: Unsupported `<` operation | | | | | Has type `tuple[str, ...]` | Has type `tuple[int, ...]` - | info: Operation fails because operator `<` is not supported between objects of type `int` and `str` ``` @@ -79,7 +78,6 @@ error[unsupported-operator]: Unsupported `<` operation | | | | | Has type `tuple[int, ...]` | Has type `tuple[str, ...]` - | info: Operation fails because operator `<` is not supported between objects of type `str` and `int` ``` @@ -93,7 +91,6 @@ error[unsupported-operator]: Unsupported `<` operation | | | | | Has type `tuple[str]` | Has type `tuple[int, ...]` - | info: Operation fails because operator `<` is not supported between objects of type `int` and `str` ``` @@ -107,7 +104,6 @@ error[unsupported-operator]: Unsupported `<` operation | | | | | Has type `tuple[int, ...]` | Has type `tuple[str]` - | info: Operation fails because operator `<` is not supported between objects of type `str` and `int` ``` @@ -121,7 +117,6 @@ error[unsupported-operator]: Unsupported `<` operation | | | | | Has type `tuple[int, ...]` | Has type `tuple[int, str]` - | info: Operation fails because operator `<` is not supported between objects of type `str` and `int` ``` @@ -135,7 +130,6 @@ error[unsupported-operator]: Unsupported `<` operation | | | | | Has type `tuple[int, str]` | Has type `tuple[int, ...]` - | info: Operation fails because operator `<` is not supported between objects of type `int` and `str` ``` @@ -149,7 +143,6 @@ error[unsupported-operator]: Unsupported `<` operation | | | | | Has type `tuple[int, str]` | Has type `tuple[str, ...]` - | info: Operation fails because operator `<` is not supported between objects of type `str` and `int` ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Class_header_validat\342\200\246_(25381f371caa1401).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Class_header_validat\342\200\246_(25381f371caa1401).snap" index 1764875e58..584fb74e78 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Class_header_validat\342\200\246_(25381f371caa1401).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Class_header_validat\342\200\246_(25381f371caa1401).snap" @@ -46,11 +46,10 @@ error[invalid-typed-dict-header]: TypedDict class `Foo` can only inherit from Ty 3 | class Foo(TypedDict, int): ... # error: [invalid-typed-dict-header] | ^^^ `int` is not a `TypedDict` class | - ::: stdlib/builtins.byi:278:7 + ::: stdlib/builtins.byi:279:7 | -278 | class int: +279 | class int: | --- `int` defined here - | ``` @@ -61,11 +60,10 @@ error[invalid-typed-dict-header]: TypedDict class `Foo2` can only inherit from T 6 | class Foo2(TypedDict, object): ... # error: [invalid-typed-dict-header] | ^^^^^^ `object` is not a `TypedDict` class | - ::: stdlib/builtins.byi:66:7 + ::: stdlib/builtins.byi:67:7 | -66 | class object: +67 | class object: | ------ `object` defined here - | ``` @@ -75,7 +73,6 @@ error[invalid-argument-type]: Invalid argument to parameter `total` in `TypedDic | 7 | class Bar(TypedDict, total=42): ... # error: [invalid-argument-type] | ^^^^^^^^ Expected either `True` or `False`, got object of type `Literal[42]` - | ``` @@ -85,7 +82,6 @@ error[invalid-argument-type]: Invalid argument to parameter `closed` in `TypedDi | 8 | class Baz(TypedDict, closed=None): ... # error: [invalid-argument-type] | ^^^^^^^^^^^ Expected either `True` or `False`, got object of type `None` - | ``` @@ -95,7 +91,6 @@ error[invalid-argument-type]: Invalid argument to parameter `total` in `TypedDic | 10 | class VeryDynamic(TypedDict, total=is_total): ... # error: [invalid-argument-type] | ^^^^^^^^^^^^^^ Expected either `True` or `False`, got object of type `bool` - | ``` @@ -105,7 +100,6 @@ error[unknown-argument]: Unknown keyword argument `weird` in `TypedDict` definit | 11 | class Bazzzz(TypedDict, weird=56): ... # error: [unknown-argument] | ^^^^^^^^ - | ``` @@ -115,7 +109,6 @@ error[invalid-typed-dict-header]: Custom metaclasses are not supported in `Typed | 14 | class Spam(TypedDict, metaclass=ABCMeta): ... # error: [invalid-typed-dict-header] | ^^^^^^^^^^^^^^^^^ - | ``` @@ -125,7 +118,6 @@ error[invalid-typed-dict-header]: Custom metaclasses are not supported in `Typed | 18 | class Ham(TypedDict, metaclass=type): ... # error: [invalid-typed-dict-header] | ^^^^^^^^^^^^^^ - | ``` @@ -135,7 +127,6 @@ error[invalid-typed-dict-header]: Keyword-variadic arguments are not supported i | 20 | class Eggs(TypedDict, **kwargs): ... # error: [invalid-typed-dict-header] | ^^^^^^^^ - | ``` @@ -145,7 +136,6 @@ error[invalid-argument-type]: Invalid argument to parameter `total` in `TypedDic | 21 | class Qux(TypedDict, total=1 == 1): ... # error: [invalid-argument-type] | ^^^^^^^^^^^^ Expected either `True` or `False`, got object of type `Literal[True]` - | ``` @@ -155,6 +145,5 @@ error[invalid-argument-type]: Invalid argument to parameter `closed` in `TypedDi | 22 | class Quux(TypedDict, closed=1 == 1): ... # error: [invalid-argument-type] | ^^^^^^^^^^^^^ Expected either `True` or `False`, got object of type `Literal[True]` - | ``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap index 33c4c3732b..aedd64b990 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap @@ -76,14 +76,13 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/typed_dict.md ``` error[invalid-key]: Unknown key "nane" for TypedDict `Person` - --> src/mdtest_snippet.py:8:5 + --> src/mdtest_snippet.py:8:12 | 8 | person["nane"] # error: [invalid-key] | ------ ^^^^^^ Did you mean "name"? | | | TypedDict `Person` | - | 7 | def access_invalid_literal_string_key(person: Person): - person["nane"] # error: [invalid-key] 8 + person["name"] # error: [invalid-key] @@ -95,13 +94,12 @@ note: This is an unsafe fix and may change runtime behavior ``` error[invalid-key]: Unknown key "nane" for TypedDict `Person` - --> src/mdtest_snippet.py:13:5 + --> src/mdtest_snippet.py:13:12 | 13 | person[NAME_KEY] # error: [invalid-key] | ------ ^^^^^^^^ Unknown key "nane" - did you mean "name"? | | | TypedDict `Person` - | ``` @@ -111,39 +109,35 @@ error[invalid-key]: TypedDict `Person` can only be subscripted with a string lit | 16 | person[str_key] # error: [invalid-key] | ^^^^^^^ - | ``` ``` error[invalid-assignment]: Invalid assignment to key "age" with declared type `int | None` on TypedDict `Person` - --> src/mdtest_snippet.py:19:5 + --> src/mdtest_snippet.py:19:21 | 19 | person["age"] = "42" # error: [invalid-assignment] | ------ ----- ^^^^ value of type `Literal["42"]` | | | | | key has declared type `int | None` | TypedDict `Person` - | info: Item declaration --> src/mdtest_snippet.py:5:5 | 5 | age: int | None | --------------- Item declared here - | ``` ``` error[invalid-key]: Unknown key "nane" for TypedDict `Person` - --> src/mdtest_snippet.py:22:5 + --> src/mdtest_snippet.py:22:12 | 22 | person["nane"] = "Alice" # error: [invalid-key] | ------ ^^^^^^ Did you mean "name"? | | | TypedDict `Person` | - | 21 | def write_to_non_existing_key(person: Person): - person["nane"] = "Alice" # error: [invalid-key] 22 + person["name"] = "Alice" # error: [invalid-key] @@ -159,61 +153,55 @@ error[invalid-key]: TypedDict `Person` can only be subscripted with a string lit | 25 | person[str_key] = "Alice" # error: [invalid-key] | ^^^^^^^ - | ``` ``` error[invalid-key]: Unknown key "unknown" for TypedDict `Person` - --> src/mdtest_snippet.py:29:21 + --> src/mdtest_snippet.py:29:50 | 29 | alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"} | -----------------------------^^^^^^^^^-------- | | | | | Unknown key "unknown" | TypedDict `Person` - | ``` ``` error[invalid-key]: Unknown key "unknown" for TypedDict `Person` - --> src/mdtest_snippet.py:32:11 + --> src/mdtest_snippet.py:32:38 | 32 | bob = Person(name="Bob", age=25, unknown="Bar") | ------ TypedDict `Person` ^^^^^^^^^^^^^ Unknown key "unknown" - | ``` ``` error[invalid-assignment]: Cannot assign to key "id" on TypedDict `Employee` - --> src/mdtest_snippet.py:40:5 + --> src/mdtest_snippet.py:40:14 | 40 | employee["id"] = 42 # error: [invalid-assignment] | -------- ^^^^ key is marked read-only | | | TypedDict `Employee` - | info: Item declaration --> src/mdtest_snippet.py:36:5 | 36 | id: ReadOnly[int] | ----------------- Read-only item declared here - | ``` ``` error[invalid-key]: Unknown key "nane" for TypedDict `Person` - --> src/mdtest_snippet.py:43:5 + --> src/mdtest_snippet.py:43:12 | 43 | person['nane'] = "Alice" # fmt: skip | ------ ^^^^^^ Did you mean 'name'? | | | TypedDict `Person` | - | 42 | # error: [invalid-key] - person['nane'] = "Alice" # fmt: skip 43 + person['name'] = "Alice" # fmt: skip @@ -229,13 +217,11 @@ error[invalid-typed-dict-field]: Cannot overwrite TypedDict field `name` | 48 | name: int # error: [invalid-typed-dict-field] | ^^^^^^^^^ Inherited mutable field type `str` is incompatible with `int` - | info: Field declaration --> src/mdtest_snippet.py:45:5 | 45 | name: str | --------- Inherited field `name` declared here on base `MovieBase` - | ``` @@ -245,18 +231,15 @@ error[invalid-typed-dict-field]: Cannot overwrite TypedDict field `value` while | 56 | class BadMerge(LeftBase, RightBase): # error: [invalid-typed-dict-field] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Inherited mutable field type `str` is incompatible with `int` - | info: Field declaration --> src/mdtest_snippet.py:51:5 | 51 | value: int | ---------- Field `value` already inherited from another base here - | info: Field declaration --> src/mdtest_snippet.py:54:5 | 54 | value: str | ---------- Inherited field `value` declared here on base `RightBase` - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Error_cases_-_`typing.TypedDict`_i\342\200\246_(9df67eb93e3df341).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Error_cases_-_`typing.TypedDict`_i\342\200\246_(9df67eb93e3df341).snap" index 75b6c4ca3f..647fabb729 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Error_cases_-_`typing.TypedDict`_i\342\200\246_(9df67eb93e3df341).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Error_cases_-_`typing.TypedDict`_i\342\200\246_(9df67eb93e3df341).snap" @@ -27,7 +27,6 @@ error[invalid-type-form]: The special form `typing.TypedDict` is not allowed in | 4 | x: TypedDict = {"name": "Alice"} | ^^^^^^^^^ - | help: You might have meant to use a concrete TypedDict or `collections.abc.Mapping[str, object]` ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Function_syntax_with\342\200\246_(4b18755412dfaff1).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Function_syntax_with\342\200\246_(4b18755412dfaff1).snap" index 11ff93b1a4..35fa56551c 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Function_syntax_with\342\200\246_(4b18755412dfaff1).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Function_syntax_with\342\200\246_(4b18755412dfaff1).snap" @@ -119,7 +119,6 @@ error[too-many-positional-arguments]: Too many positional arguments to function | 4 | TypedDict("Foo", {}, {}) | ^^ - | ``` @@ -129,7 +128,6 @@ error[missing-argument]: No arguments provided for required parameters `typename | 6 | TypedDict() | ^^^^^^^^^^^ - | ``` @@ -139,7 +137,6 @@ error[missing-argument]: No argument provided for required parameter `fields` of | 8 | TypedDict("Foo") | ^^^^^^^^^^^^^^^^ - | ``` @@ -149,7 +146,6 @@ error[invalid-argument-type]: Invalid argument to parameter `typename` of `Typed | 11 | Bad1 = TypedDict(123, {"name": str}) | ^^^ Expected `str`, found `Literal[123]` - | ``` @@ -159,7 +155,6 @@ warning[mismatched-type-name]: The name passed to `TypedDict` must match the var | 14 | BadTypedDict3 = TypedDict("WrongName", {"name": str}) | ^^^^^^^^^^^ Expected "BadTypedDict3", got "WrongName" - | ``` @@ -169,7 +164,6 @@ warning[mismatched-type-name]: The name passed to `TypedDict` must match the var | 19 | Y = TypedDict(x, {}) | ^ Expected "Y", got variable of type `str` - | ``` @@ -179,7 +173,6 @@ error[invalid-argument-type]: Expected a dict literal for parameter `fields` of | 28 | Bad2 = TypedDict("Bad2", "not a dict") | ^^^^^^^^^^^^ - | ``` @@ -189,7 +182,6 @@ error[invalid-argument-type]: Expected a dict literal for parameter `fields` of | 30 | TypedDict("Bad2", "not a dict") | ^^^^^^^^^^^^ - | ``` @@ -199,7 +191,6 @@ error[invalid-argument-type]: Expected a dict literal for parameter `fields` of | 36 | Bad2b = TypedDict("Bad2b", get_fields()) | ^^^^^^^^^^^^ - | ``` @@ -209,7 +200,6 @@ error[invalid-argument-type]: Invalid argument to parameter `total` of `TypedDic | 39 | Bad3 = TypedDict("Bad3", {"name": str}, total="not a bool") | ^^^^^^^^^^^^ Expected either `True` or `False`, got object of type `Literal["not a bool"]` - | ``` @@ -219,7 +209,6 @@ error[invalid-argument-type]: Invalid argument to parameter `closed` of `TypedDi | 42 | Bad4 = TypedDict("Bad4", {"name": str}, closed=123) | ^^^ Expected either `True` or `False`, got object of type `Literal[123]` - | ``` @@ -229,7 +218,6 @@ error[invalid-argument-type]: Variadic positional arguments are not supported in | 48 | Bad5 = TypedDict(*tup) | ^^^^ - | ``` @@ -239,7 +227,6 @@ error[invalid-argument-type]: Variadic keyword arguments are not supported in `T | 51 | Bad6 = TypedDict("Bad6", {"name": str}, **kw) | ^^^^ - | ``` @@ -249,7 +236,6 @@ error[invalid-argument-type]: Variadic positional and keyword arguments are not | 54 | Bad7 = TypedDict(*tup, "foo", "bar", **kw) | ^^^^ ---- - | ``` @@ -259,7 +245,6 @@ error[invalid-argument-type]: Variadic keyword arguments are not supported in `T | 58 | Bad7b = TypedDict("Bad7b", **kw, random_other_arg=56) | ^^^^ - | ``` @@ -269,7 +254,6 @@ error[unknown-argument]: Argument `random_other_arg` does not match any known pa | 58 | Bad7b = TypedDict("Bad7b", **kw, random_other_arg=56) | ^^^^^^^^^^^^^^^^^^^ - | ``` @@ -279,7 +263,6 @@ error[invalid-argument-type]: Keyword splats are not allowed in the `fields` par | 63 | Bad8 = TypedDict("Bad8", {**kwargs}) | ^^^^^^ - | ``` @@ -289,7 +272,6 @@ error[invalid-argument-type]: Keyword splats are not allowed in the `fields` par | 65 | TypedDict("Bad8", {**kwargs}) | ^^^^^^ - | ``` @@ -299,7 +281,6 @@ error[invalid-argument-type]: Keyword splats are not allowed in the `fields` par | 68 | Bad81 = TypedDict("Bad81", {**kwargs, **kwargs}) | ^^^^^^ - | ``` @@ -309,7 +290,6 @@ error[invalid-argument-type]: Keyword splats are not allowed in the `fields` par | 68 | Bad81 = TypedDict("Bad81", {**kwargs, **kwargs}) | ^^^^^^ - | ``` @@ -319,7 +299,6 @@ error[invalid-argument-type]: Keyword splats are not allowed in the `fields` par | 71 | TypedDict("Bad81", {**kwargs, **kwargs}) | ^^^^^^ - | ``` @@ -329,7 +308,6 @@ error[invalid-argument-type]: Keyword splats are not allowed in the `fields` par | 71 | TypedDict("Bad81", {**kwargs, **kwargs}) | ^^^^^^ - | ``` @@ -339,7 +317,6 @@ error[invalid-argument-type]: Keyword splats are not allowed in the `fields` par | 74 | Bad82 = TypedDict("Bad82", {**kwargs, "foo": []}) | ^^^^^^ - | ``` @@ -349,7 +326,6 @@ error[invalid-type-form]: List literals are not allowed in this context in a typ | 74 | Bad82 = TypedDict("Bad82", {**kwargs, "foo": []}) | ^^ - | 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 @@ -361,7 +337,6 @@ error[invalid-argument-type]: Keyword splats are not allowed in the `fields` par | 77 | TypedDict("Bad82", {**kwargs, "foo": []}) | ^^^^^^ - | ``` @@ -371,7 +346,6 @@ error[invalid-type-form]: List literals are not allowed in this context in a typ | 77 | TypedDict("Bad82", {**kwargs, "foo": []}) | ^^ - | 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 @@ -383,7 +357,6 @@ error[invalid-argument-type]: Expected a string-literal key in the `fields` dict | 85 | Bad9 = TypedDict("Bad9", {name: int}) | ^^^^ Found `str` - | ``` @@ -393,7 +366,6 @@ error[invalid-argument-type]: Expected a string-literal key in the `fields` dict | 89 | Bad10 = TypedDict("Bad10", {name: 42}) | ^^^^ Found `str` - | ``` @@ -403,7 +375,6 @@ error[invalid-type-form]: Int literals are not allowed in this context in a type | 89 | Bad10 = TypedDict("Bad10", {name: 42}) | ^^ Did you mean `typing.Literal[42]`? - | 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 @@ -415,7 +386,6 @@ error[invalid-argument-type]: Expected a string-literal key in the `fields` dict | 93 | class Bad11(TypedDict("Bad11", {name: 42})): ... | ^^^^ Found `str` - | ``` @@ -425,7 +395,6 @@ error[invalid-type-form]: Int literals are not allowed in this context in a type | 93 | class Bad11(TypedDict("Bad11", {name: 42})): ... | ^^ Did you mean `typing.Literal[42]`? - | 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 @@ -437,6 +406,5 @@ error[invalid-argument-type]: Invalid argument to parameter `typename` of `Typed | 96 | class Bad12(TypedDict(123, {"field": int})): ... | ^^^ Expected `str`, found `Literal[123]` - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Only_annotated_decla\342\200\246_(bef70731cae5b8af).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Only_annotated_decla\342\200\246_(bef70731cae5b8af).snap" index 664164e876..73aa2e5512 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Only_annotated_decla\342\200\246_(bef70731cae5b8af).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Only_annotated_decla\342\200\246_(bef70731cae5b8af).snap" @@ -48,7 +48,6 @@ error[invalid-typed-dict-statement]: invalid statement in TypedDict class body | 17 | 42 | ^^ - | info: Only annotated declarations (`: `) are allowed. ``` @@ -59,7 +58,6 @@ error[invalid-typed-dict-statement]: TypedDict item cannot have a value | 19 | b: str = "hello" | ^^^^^^^ - | ``` @@ -69,7 +67,6 @@ error[invalid-typed-dict-statement]: TypedDict class cannot have methods | 21 | def bar(self): ... | ^^^^^^^^^^^^^^^^^^ - | ``` @@ -80,6 +77,5 @@ error[invalid-typed-dict-statement]: TypedDict class cannot have methods 24 | / def baz(self): 25 | | pass | |____________^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Redundant_cast_warni\342\200\246_(75ac240a2d1f7108).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Redundant_cast_warni\342\200\246_(75ac240a2d1f7108).snap" index 4dee562cbc..11b404c26e 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Redundant_cast_warni\342\200\246_(75ac240a2d1f7108).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Redundant_cast_warni\342\200\246_(75ac240a2d1f7108).snap" @@ -34,7 +34,6 @@ warning[redundant-cast]: Value is already of type `Foo2` | 10 | _ = cast(Foo2, foo) # error: [redundant-cast] | ^^^^^^^^^^^^^^^ - | help: Remove the redundant `cast` | 9 | foo: Foo2 = {"x": 1} @@ -51,7 +50,6 @@ warning[redundant-cast]: Value is already of type `Bar2` | 11 | _ = cast(Bar2, foo) # error: [redundant-cast] | ^^^^^^^^^^^^^^^ - | info: `Bar2` is equivalent to `Foo2` help: Remove the redundant `cast` | 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" index d3cd4f9d36..3c6c138089 100644 --- "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" @@ -37,13 +37,11 @@ error[invalid-argument-type]: Argument to function `f2` is incorrect | 14 | x = f(3) | ^ Expected `str`, found `Literal[3]` - | info: Function defined here --> src/mdtest_snippet.py:4:5 | 4 | def f2(name: str) -> int: | ^^ --------- Parameter declared here - | 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)` @@ -55,7 +53,6 @@ error[too-many-positional-arguments]: Too many positional arguments to function | 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)` 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" index afc0cac027..ab68afe402 100644 --- "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" @@ -36,13 +36,11 @@ error[invalid-argument-type]: Argument to function `f2` is incorrect | 13 | x = f(3) | ^ Expected `str`, found `Literal[3]` - | info: Function defined here --> src/mdtest_snippet.py:4:5 | 4 | def f2(name: str) -> int: | ^^ --------- Parameter declared here - | 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)` 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_-_Attribute_access_on_\342\200\246_(7bdb97302c27c412).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_-_Attribute_access_on_\342\200\246_(7bdb97302c27c412).snap" index c42fc43dc8..7d4a0a1fe8 100644 --- "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_-_Attribute_access_on_\342\200\246_(7bdb97302c27c412).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_-_Attribute_access_on_\342\200\246_(7bdb97302c27c412).snap" @@ -40,7 +40,6 @@ error[invalid-argument-type]: Argument to bound method `A.foo` is incorrect | 17 | return x.foo(y) | ^^^^^^^^ Argument type `T@_` does not satisfy upper bound `A` of type variable `Self` - | info: Union variant `bound method T@_.foo(x: int) -> T@_` is incompatible with this call site info: Attempted to call union type `(bound method T@_.foo(x: int) -> T@_) | (bound method T@_.foo(x: str) -> T@_)` @@ -52,7 +51,6 @@ error[invalid-argument-type]: Argument to bound method `B.foo` is incorrect | 17 | return x.foo(y) | ^^^^^^^^ Argument type `T@_` does not satisfy upper bound `B` of type variable `Self` - | info: Union variant `bound method T@_.foo(x: str) -> T@_` is incompatible with this call site info: Attempted to call union type `(bound method T@_.foo(x: int) -> T@_) | (bound method T@_.foo(x: str) -> T@_)` @@ -64,13 +62,11 @@ error[invalid-argument-type]: Argument to bound method `B.foo` is incorrect | 17 | return x.foo(y) | ^ Expected `str`, found `int` - | info: Method defined here --> src/mdtest_snippet.py:8:9 | 8 | def foo(self, x: str) -> Self: | ^^^ ------ Parameter declared here - | info: Union variant `bound method T@_.foo(x: str) -> T@_` is incompatible with this call site info: Attempted to call union type `(bound method T@_.foo(x: int) -> T@_) | (bound method T@_.foo(x: str) -> T@_)` 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" index 43cc83213d..f5fd256433 100644 --- "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" @@ -37,7 +37,6 @@ error[parameter-already-assigned]: Multiple values provided for parameter `name` | 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)` @@ -49,7 +48,6 @@ error[unknown-argument]: Argument `unknown` does not match any known parameter o | 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)` 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" index fc22cf876d..1081975618 100644 --- "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" @@ -83,7 +83,6 @@ error[call-non-callable]: Object of type `Literal[5]` is not callable | 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) | ... omitted 5 union elements` @@ -95,7 +94,6 @@ error[call-non-callable]: Object of type `PossiblyNotCallable` is not callable ( | 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) | ... omitted 5 union elements` @@ -107,7 +105,6 @@ error[missing-argument]: No argument provided for required parameter `b` of func | 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) | ... omitted 5 union elements` @@ -119,14 +116,12 @@ error[no-matching-overload]: No overload of function `f6` matches arguments | 60 | x = f(3) | ^^^^ - | info: First overload defined here --> src/mdtest_snippet.py:23:1 | 23 | / @overload 24 | | def f6() -> None: ... | |_____________________^ First overload defined here - | info: Possible overloads for function `f6`: info: () -> None info: (x: str, y: str) -> str @@ -135,7 +130,6 @@ info: Overload implementation defined here | 27 | def f6(x: str | None = None, y: str | None = None) -> str | 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) | ... omitted 5 union elements` @@ -147,13 +141,11 @@ error[invalid-argument-type]: Argument to function `f2` is incorrect | 60 | x = f(3) | ^ Expected `str`, found `Literal[3]` - | info: Function defined here --> src/mdtest_snippet.py:7:5 | 7 | def f2(name: str) -> int: | ^^ --------- Parameter declared here - | 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) | ... omitted 5 union elements` @@ -165,13 +157,11 @@ error[invalid-argument-type]: Argument to function `f4` is incorrect | 60 | x = f(3) | ^ Argument type `Literal[3]` does not satisfy upper bound `str` of type variable `T` - | info: Type variable defined here --> src/mdtest_snippet.py:13:8 | 13 | def f4[T: str](x: T) -> int: | ^^^^^^ - | info: Union variant `def f4[T](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) | ... omitted 5 union elements` @@ -183,13 +173,11 @@ error[invalid-argument-type]: Argument to function `f5` is incorrect | 60 | x = f(3) | ^ Expected `str`, found `Literal[3]` - | info: Matching overload defined here --> src/mdtest_snippet.py:19:5 | 19 | def f5(x: str) -> str: ... | ^^ ------ Parameter declared here - | info: Non-matching overloads for function `f5`: info: () -> None info: Union variant `Overload[() -> None, (x: str) -> str]` is incompatible with this call site @@ -203,7 +191,6 @@ error[too-many-positional-arguments]: Too many positional arguments to function | 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) | ... omitted 5 union elements` 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_-_Truncation_for_long_\342\200\246_(ec94b5e857284ef3).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_-_Truncation_for_long_\342\200\246_(ec94b5e857284ef3).snap" index 1f809315b5..50eecabcce 100644 --- "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_-_Truncation_for_long_\342\200\246_(ec94b5e857284ef3).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_-_Truncation_for_long_\342\200\246_(ec94b5e857284ef3).snap" @@ -39,12 +39,10 @@ error[invalid-argument-type]: Argument to function `f1` is incorrect | 16 | f1(x) | ^ Expected `Literal[1, 2, 3, 4, 5, ... omitted 3 literals] | A | B | ... omitted 4 union elements`, found `int` - | info: Function defined here --> src/mdtest_snippet.py:10:5 | 10 | def f1(x: Union[Literal[1, 2, 3, 4, 5, 6, 7, 8], A, B, C, D, E, F]) -> int: | ^^ ----------------------------------------------------------- Parameter declared here - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Union_with_overloade\342\200\246_(4408ade1316b97c0).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Union_with_overloade\342\200\246_(4408ade1316b97c0).snap" index 42937c9389..ea10526b19 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Union_with_overloade\342\200\246_(4408ade1316b97c0).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Union_with_overloade\342\200\246_(4408ade1316b97c0).snap" @@ -27,7 +27,6 @@ error[unresolved-attribute]: Attribute `split` is not defined on `int` in union | 4 | x.split(" ") | ^^^^^^^ - | ``` @@ -37,17 +36,15 @@ error[invalid-argument-type]: Argument to bound method `bytes.split` is incorrec | 4 | x.split(" ") | ^^^ Expected `ReadableBuffer | None`, found `Literal[" "]` - | info: type `Literal[" "]` is not assignable to any element of the union `Buffer | None` info: ├── type `Literal[" "]` is not assignable to protocol `Buffer` info: │ └── protocol member `__buffer__` is not defined on type `Literal[" "]` info: └── ... omitted 1 union element without additional context info: Method defined here - --> stdlib/builtins.byi:1663:9 + --> stdlib/builtins.byi:1664:9 | -1663 | def split(self, sep: ReadableBuffer | None = None, maxsplit: SupportsIndex = -1) -> list[bytes]: +1664 | def split(self, sep: ReadableBuffer | None = None, maxsplit: SupportsIndex = -1) -> list[bytes]: | ^^^^^ --------------------------------- Parameter declared here - | info: Union variant `bound method bytes.split(sep: ReadableBuffer | None = None, maxsplit: SupportsIndex = -1) -> list[bytes]` is incompatible with this call site info: Attempted to call union type `(bound method bytes.split(sep: ReadableBuffer | None = None, maxsplit: SupportsIndex = -1) -> list[bytes]) | (bound method str.split(sep: str | None = None, maxsplit: SupportsIndex = -1) -> list[str])` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unknown_argument.md_-_Unknown_argument_dia\342\200\246_(f419c2a8e2ce2412).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unknown_argument.md_-_Unknown_argument_dia\342\200\246_(f419c2a8e2ce2412).snap" index 431289fc19..a196a02bae 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/unknown_argument.md_-_Unknown_argument_dia\342\200\246_(f419c2a8e2ce2412).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unknown_argument.md_-_Unknown_argument_dia\342\200\246_(f419c2a8e2ce2412).snap" @@ -47,13 +47,11 @@ error[unknown-argument]: Argument `d` does not match any known parameter of func | 3 | f(a=1, b=2, c=3, d=42) # error: [unknown-argument] | ^^^^ - | info: Function signature here --> src/module.py:1:5 | 1 | def f(a, b, c=42): ... | ^^^^^^^^^^^^^ - | ``` @@ -63,7 +61,6 @@ error[unknown-argument]: Argument `d` does not match any known parameter of func | 12 | h(a=1, b=2, d=42) | ^^^^ - | info: Union variant `def f(a, b, c: some int = 42)` is incompatible with this call site info: Attempted to call union type `(def f(a, b, c: some int = 42)) | (def g(a, b))` @@ -75,7 +72,6 @@ error[unknown-argument]: Argument `d` does not match any known parameter of func | 12 | h(a=1, b=2, d=42) | ^^^^ - | info: Union variant `def g(a, b)` is incompatible with this call site info: Attempted to call union type `(def f(a, b, c: some int = 42)) | (def g(a, b))` @@ -87,12 +83,10 @@ error[unknown-argument]: Argument `c` does not match any known parameter of boun | 14 | Foo().method(a=1, b=2, c=3) # error: [unknown-argument] | ^^^ - | info: Method signature here --> src/module.py:5:9 | 5 | def method(self, a, b): ... | ^^^^^^^^^^^^^^^^^^ - | ``` 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" index 561207fe58..51d6e37be9 100644 --- "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" @@ -26,7 +26,6 @@ error[unresolved-import]: Cannot resolve imported module `does_not_exist` | 1 | import does_not_exist # error: [unresolved-import] | ^^^^^^^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. /src (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) 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" index 8aa1e73adc..1d2b3fe720 100644 --- "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" @@ -31,6 +31,5 @@ error[unresolved-import]: Module `a` has no member `does_not_exist` | 1 | from a import does_exist1, does_not_exist, does_exist2 # error: [unresolved-import] | ^^^^^^^^^^^^^^ - | ``` 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" index 7ff5d67069..3675b71844 100644 --- "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" @@ -26,7 +26,6 @@ error[unresolved-import]: Cannot resolve imported module `.does_not_exist.foo.ba | 1 | from .does_not_exist.foo.bar import add # error: [unresolved-import] | ^^^^^^^^^^^^^^^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. /src (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) 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" index 308a61eedc..8ac259aae2 100644 --- "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" @@ -26,7 +26,6 @@ error[unresolved-import]: Cannot resolve imported module `.does_not_exist` | 1 | from .does_not_exist import add # error: [unresolved-import] | ^^^^^^^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. /src (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) 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" index d8cc9b5c31..5a7816e521 100644 --- "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" @@ -26,7 +26,6 @@ error[unresolved-import]: Cannot resolve imported module `does_not_exist` | 1 | from does_not_exist import add # error: [unresolved-import] | ^^^^^^^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. /src (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) 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" index b6eb52cd63..31c9ed5c0c 100644 --- "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" @@ -38,7 +38,6 @@ error[unresolved-import]: Cannot resolve imported module `....foo` | 1 | from ....foo import add # error: [unresolved-import] | ^^^ - | help: The module can be resolved if the number of leading dots is reduced help: Did you mean `...foo`? info: Searched in the following paths during module resolution: 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" index 703f7b3257..d6e34c335c 100644 --- "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" @@ -24,7 +24,6 @@ error[unresolved-reference]: Name `PythonFinalizationError` used when not define | 1 | PythonFinalizationError # error: [unresolved-reference] | ^^^^^^^^^^^^^^^^^^^^^^^ - | info: `PythonFinalizationError` was added as a builtin in Python 3.13 info: Python 3.12 was assumed when resolving types because it was specified on the command line diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_reference\342\200\246_-_Diagnostics_for_unre\342\200\246_-_Typing_builtin_has_I\342\200\246_-_Info_not_present_bef\342\200\246_(41702a6f6d20b082).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_reference\342\200\246_-_Diagnostics_for_unre\342\200\246_-_Typing_builtin_has_I\342\200\246_-_Info_not_present_bef\342\200\246_(41702a6f6d20b082).snap" index a1d39ac44b..0fcc75f48a 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_reference\342\200\246_-_Diagnostics_for_unre\342\200\246_-_Typing_builtin_has_I\342\200\246_-_Info_not_present_bef\342\200\246_(41702a6f6d20b082).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_reference\342\200\246_-_Diagnostics_for_unre\342\200\246_-_Typing_builtin_has_I\342\200\246_-_Info_not_present_bef\342\200\246_(41702a6f6d20b082).snap" @@ -25,7 +25,6 @@ error[unresolved-reference]: Name `List` used when not defined | 1 | foo: List[int] # error: [unresolved-reference] | ^^^^ - | ``` @@ -35,6 +34,5 @@ error[unresolved-reference]: Name `Type` used when not defined | 2 | bar: Type # error: [unresolved-reference] | ^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_reference\342\200\246_-_Diagnostics_for_unre\342\200\246_-_Typing_builtin_has_I\342\200\246_-_Info_present_in_Pyth\342\200\246_(1028a80959504fc9).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_reference\342\200\246_-_Diagnostics_for_unre\342\200\246_-_Typing_builtin_has_I\342\200\246_-_Info_present_in_Pyth\342\200\246_(1028a80959504fc9).snap" index ff80a77106..cc2b3758fd 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_reference\342\200\246_-_Diagnostics_for_unre\342\200\246_-_Typing_builtin_has_I\342\200\246_-_Info_present_in_Pyth\342\200\246_(1028a80959504fc9).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_reference\342\200\246_-_Diagnostics_for_unre\342\200\246_-_Typing_builtin_has_I\342\200\246_-_Info_present_in_Pyth\342\200\246_(1028a80959504fc9).snap" @@ -25,7 +25,6 @@ error[unresolved-reference]: Name `List` used when not defined | 1 | foo: List[int] # error: [unresolved-reference] | ^^^^ Did you mean `list`? - | ``` @@ -35,6 +34,5 @@ error[unresolved-reference]: Name `Type` used when not defined | 2 | bar: Type # error: [unresolved-reference] | ^^^^ Did you mean `type`? - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_Enum_base_(4873196c8b48364).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_Enum_base_(4873196c8b48364).snap" index 16ebf6b6af..31ef1a61df 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_Enum_base_(4873196c8b48364).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_Enum_base_(4873196c8b48364).snap" @@ -29,7 +29,6 @@ error[invalid-base]: Invalid base for class created via `type()` | 6 | X = type("X", (MyEnum,), {}) # error: [invalid-base] | ^^^^^^ Has type `` - | info: Creating an enum class via `type()` is not supported info: Consider using `Enum("X", [])` instead diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_Enum_with_members_(81bef9a8e1230854).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_Enum_with_members_(81bef9a8e1230854).snap" index f971b932d3..8485e0f5ba 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_Enum_with_members_(81bef9a8e1230854).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_Enum_with_members_(81bef9a8e1230854).snap" @@ -30,6 +30,5 @@ error[subclass-of-final-class]: Class `X` cannot inherit from final class `Color | 7 | X = type("X", (Color,), {}) # error: [subclass-of-final-class] | ^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_`@final`_class_(ea69d237256b3762).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_`@final`_class_(ea69d237256b3762).snap" index 9ab5159345..e87d71ccbb 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_`@final`_class_(ea69d237256b3762).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_`@final`_class_(ea69d237256b3762).snap" @@ -30,6 +30,5 @@ error[subclass-of-final-class]: Class `X` cannot inherit from final class `Final | 7 | X = type("X", (FinalClass,), {}) # error: [subclass-of-final-class] | ^^^^^^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_`Generic`_base_(d455f46a27cec685).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_`Generic`_base_(d455f46a27cec685).snap" index 2ad2cc4044..c6ef194401 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_`Generic`_base_(d455f46a27cec685).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_`Generic`_base_(d455f46a27cec685).snap" @@ -28,7 +28,6 @@ error[invalid-base]: Invalid base for class created via `type()` | 5 | X = type("X", (Generic[T],), {}) # error: [invalid-base] | ^^^^^^^^^^ Has type `` - | info: Classes created via `type()` cannot be generic info: Consider using `class X(Generic[...]): ...` instead diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_`Protocol`_base_(99c9bde73664dd51).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_`Protocol`_base_(99c9bde73664dd51).snap" index a10d89d6a8..395f228b5f 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_`Protocol`_base_(99c9bde73664dd51).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_`Protocol`_base_(99c9bde73664dd51).snap" @@ -26,7 +26,6 @@ info[unsupported-dynamic-base]: Unsupported base for class created via `type()` | 3 | X = type("X", (Protocol,), {}) # error: [unsupported-dynamic-base] | ^^^^^^^^ Has type `` - | info: Classes created via `type()` cannot be protocols info: Consider using `class X(Protocol): ...` instead diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_`TypedDict`_base_(6f76171c88fc8760).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_`TypedDict`_base_(6f76171c88fc8760).snap" index 67179b87da..03d01d0604 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_`TypedDict`_base_(6f76171c88fc8760).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_`TypedDict`_base_(6f76171c88fc8760).snap" @@ -26,7 +26,6 @@ error[invalid-base]: Invalid base for class created via `type()` | 3 | X = type("X", (TypedDict,), {}) # error: [invalid-base] | ^^^^^^^^^ Has type `` - | info: Classes created via `type()` cannot be TypedDicts info: Consider using `TypedDict("X", {})` instead 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" index b26e706834..97865ae1a4 100644 --- "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" @@ -30,7 +30,6 @@ error[unsupported-bool-conversion]: Boolean conversion is not supported for type | 7 | 10 and a and True | ^ - | info: `__bool__` on `NotBoolable` must be callable ``` 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" index 2f05042fde..787dad66d8 100644 --- "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" @@ -31,14 +31,12 @@ error[unsupported-bool-conversion]: Boolean conversion is not supported for type | 8 | 10 and a and True | ^ - | info: `str` is not assignable to `bool` - --> src/mdtest_snippet.py:2:9 + --> src/mdtest_snippet.py:2:27 | 2 | def __bool__(self) -> str: | -------- ^^^ Incorrect return type | | | Method defined here - | ``` 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" index 44e0c7eb04..aea2c2c523 100644 --- "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" @@ -31,14 +31,12 @@ error[unsupported-bool-conversion]: Boolean conversion is not supported for type | 8 | 10 and a and True | ^ - | info: `__bool__` methods must only have a `self` parameter - --> src/mdtest_snippet.py:2:9 + --> src/mdtest_snippet.py:2:17 | 2 | def __bool__(self, foo): | --------^^^^^^^^^^^ Incorrect parameters | | | Method defined here - | ``` 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" index 90026b8d2e..991e95d41b 100644 --- "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" @@ -38,6 +38,5 @@ error[unsupported-bool-conversion]: Boolean conversion is not supported for unio | 15 | 10 and get() and True | ^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_A_constrained_defaul\342\200\246_(b62ed1f409042cc).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_A_constrained_defaul\342\200\246_(b62ed1f409042cc).snap" index f40a34c7b4..77083e0d90 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_A_constrained_defaul\342\200\246_(b62ed1f409042cc).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_A_constrained_defaul\342\200\246_(b62ed1f409042cc).snap" @@ -30,7 +30,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/legacy/variable ``` error[invalid-type-variable-default]: TypeVar default is inconsistent with the TypeVar's constraints - --> src/mdtest_snippet.py:11:18 + --> src/mdtest_snippet.py:11:41 | 11 | U = TypeVar("U", bool, complex, default=T1) | ------------- ^^ Constraint `int` of default `T1` is not one of the constraints of `U` @@ -41,6 +41,5 @@ error[invalid-type-variable-default]: TypeVar default is inconsistent with the T | 3 | T1 = TypeVar("T1", int, str) | ---------------------------- `T1` defined here - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_A_constrained_defaul\342\200\246_(d9ffda7fd9cdf840).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_A_constrained_defaul\342\200\246_(d9ffda7fd9cdf840).snap" index b4ed239ad5..6d6f1e60b5 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_A_constrained_defaul\342\200\246_(d9ffda7fd9cdf840).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_A_constrained_defaul\342\200\246_(d9ffda7fd9cdf840).snap" @@ -45,6 +45,5 @@ error[invalid-type-variable-default]: TypeVar default is not assignable to the T | 3 | T1 = TypeVar("T1", int, str) | ---------------------------- `T1` defined here - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_A_non-constrained_de\342\200\246_(ff24930259abfb3).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_A_non-constrained_de\342\200\246_(ff24930259abfb3).snap" index 82a9b1532d..4b0d93355b 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_A_non-constrained_de\342\200\246_(ff24930259abfb3).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_A_non-constrained_de\342\200\246_(ff24930259abfb3).snap" @@ -29,7 +29,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/legacy/variable ``` error[invalid-type-variable-default]: TypeVar default is inconsistent with the TypeVar's constraints - --> src/mdtest_snippet.py:7:18 + --> src/mdtest_snippet.py:7:38 | 7 | S = TypeVar("S", float, str, default=T1) | ---------- ^^ Bounded TypeVar cannot be used as the default for a constrained TypeVar @@ -40,14 +40,13 @@ error[invalid-type-variable-default]: TypeVar default is inconsistent with the T | 3 | T1 = TypeVar("T1", bound=int) | ----------------------------- `T1` defined here - | info: `T1` has bound `int` but is not constrained ``` ``` error[invalid-type-variable-default]: TypeVar default is inconsistent with the TypeVar's constraints - --> src/mdtest_snippet.py:10:18 + --> src/mdtest_snippet.py:10:38 | 10 | U = TypeVar("U", str, bytes, default=T2) | ---------- ^^ Unbounded TypeVar cannot be used as the default for a constrained TypeVar @@ -58,7 +57,6 @@ error[invalid-type-variable-default]: TypeVar default is inconsistent with the T | 4 | T2 = TypeVar("T2") | ------------------ `T2` defined here - | info: `T2` has no bound or constraints ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_An_unbounded_default\342\200\246_(a2759fd9d2731a7d).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_An_unbounded_default\342\200\246_(a2759fd9d2731a7d).snap" index 6543b7f86a..52ce5e3590 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_An_unbounded_default\342\200\246_(a2759fd9d2731a7d).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_An_unbounded_default\342\200\246_(a2759fd9d2731a7d).snap" @@ -25,7 +25,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/legacy/variable ``` error[invalid-type-variable-default]: TypeVar default is not assignable to the TypeVar's upper bound - --> src/mdtest_snippet.py:3:1 + --> src/mdtest_snippet.py:6:26 | 3 | T1 = TypeVar("T1") | ------------------ `T1` defined here @@ -35,6 +35,5 @@ error[invalid-type-variable-default]: TypeVar default is not assignable to the T | ^^ --- Upper bound of `S` | | | Upper bound `object` of default `T1` is not assignable to upper bound of `S` - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_Concrete_default_wit\342\200\246_(30284a6490652e58).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_Concrete_default_wit\342\200\246_(30284a6490652e58).snap" index 71aaa269d4..3e0cff3a6b 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_Concrete_default_wit\342\200\246_(30284a6490652e58).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_Concrete_default_wit\342\200\246_(30284a6490652e58).snap" @@ -32,24 +32,22 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/legacy/variable ``` error[invalid-type-variable-default]: TypeVar default is inconsistent with the TypeVar's constraints - --> src/mdtest_snippet.py:4:18 + --> src/mdtest_snippet.py:4:36 | 4 | T = TypeVar("T", int, str, default=bytes) | -------- ^^^^^ `bytes` is not one of the constraints of `T` | | | Constraints of `T` - | ``` ``` error[invalid-type-variable-default]: TypeVar default is inconsistent with the TypeVar's constraints - --> src/mdtest_snippet.py:10:18 + --> src/mdtest_snippet.py:10:36 | 10 | U = TypeVar("U", int, str, default=bool) | -------- ^^^^ `bool` is not one of the constraints of `U` | | | Constraints of `U` - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_Concrete_default_wit\342\200\246_(37f9b6583c0633f5).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_Concrete_default_wit\342\200\246_(37f9b6583c0633f5).snap" index 4e7343b87d..b46e4d503a 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_Concrete_default_wit\342\200\246_(37f9b6583c0633f5).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_Concrete_default_wit\342\200\246_(37f9b6583c0633f5).snap" @@ -25,12 +25,11 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/legacy/variable ``` error[invalid-type-variable-default]: TypeVar default is not assignable to the TypeVar's upper bound - --> src/mdtest_snippet.py:4:24 + --> src/mdtest_snippet.py:4:37 | 4 | T = TypeVar("T", bound=str, default=int) | --- ^^^ Default of `T` | | | Upper bound of `T` - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_Default_TypeVar's_bo\342\200\246_(fcd7ad5416c91629).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_Default_TypeVar's_bo\342\200\246_(fcd7ad5416c91629).snap" index 909fb658f0..4e0073ed49 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_Default_TypeVar's_bo\342\200\246_(fcd7ad5416c91629).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_Default_TypeVar's_bo\342\200\246_(fcd7ad5416c91629).snap" @@ -42,6 +42,5 @@ error[invalid-type-variable-default]: TypeVar default is not assignable to the T | 5 | T3 = TypeVar("T3", bound=str) | ----------------------------- `T3` defined here - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Shadowing_checks_use\342\200\246_(7e6bb178099059fe).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Shadowing_checks_use\342\200\246_(7e6bb178099059fe).snap" index 26499c34cd..e254c1e653 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Shadowing_checks_use\342\200\246_(7e6bb178099059fe).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Shadowing_checks_use\342\200\246_(7e6bb178099059fe).snap" @@ -37,13 +37,12 @@ warning[mismatched-type-name]: The name passed to `TypeVar` must match the varia | 8 | Q = TypeVar("T") | ^^^ Expected "Q", got "T" - | ``` ``` error[shadowed-type-variable]: Generic class `Bad` uses type variable `Q` already bound by an enclosing scope - --> src/mdtest_snippet.py:10:7 + --> src/mdtest_snippet.py:14:11 | 10 | class Outer(Generic[Q]): | ----------------- Type variable `Q` is bound in this enclosing scope @@ -52,13 +51,12 @@ error[shadowed-type-variable]: Generic class `Bad` uses type variable `Q` alread 13 | # error: [shadowed-type-variable] 14 | class Bad(Generic[Q]): ... | ^^^^^^^^^^^^^^^ `Q` used in class definition here - | ``` ``` error[shadowed-type-variable]: Generic class `Bad` uses type variable `Q` already bound by an enclosing scope - --> src/mdtest_snippet.py:10:7 + --> src/mdtest_snippet.py:14:11 | 10 | class Outer(Generic[Q]): | ----------------- Type variable `Q` is bound in this enclosing scope @@ -67,6 +65,5 @@ error[shadowed-type-variable]: Generic class `Bad` uses type variable `Q` alread 13 | # error: [shadowed-type-variable] 14 | class Bad(Generic[Q]): ... | ^^^^^^^^^^^^^^^ `Q` used in class definition here - | ``` 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" index dc72f19035..36b35780ec 100644 --- "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" @@ -26,7 +26,6 @@ error[invalid-syntax]: Cannot use `match` statement on Python 3.9 (syntax was ad | 1 | match 2: # error: 1 [invalid-syntax] "Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)" | ^^^^^ - | info: Python 3.9 was assumed when parsing syntax because it was specified on the command line ``` diff --git a/crates/ty_python_semantic/resources/mdtest/statically_known_branches.md b/crates/ty_python_semantic/resources/mdtest/statically_known_branches.md index 25ecc6b03e..56eaf37ae8 100644 --- a/crates/ty_python_semantic/resources/mdtest/statically_known_branches.md +++ b/crates/ty_python_semantic/resources/mdtest/statically_known_branches.md @@ -1034,7 +1034,7 @@ reveal_type(c) # revealed: Literal[1] python-version = "3.10" ``` -### Single-valued types, always true +### Literal subject, always true ```py x = 1 @@ -1048,7 +1048,7 @@ match "a": reveal_type(x) # revealed: Literal[2] ``` -### Single-valued types, always true, with wildcard pattern +### Literal subject, always true, with wildcard pattern ```py x = 1 @@ -1064,7 +1064,7 @@ match "a": reveal_type(x) # revealed: Literal[2] ``` -### Single-valued types, always true, with guard +### Literal subject, always true, with guard Make sure we don't infer a static truthiness in case there is a case guard: @@ -1085,7 +1085,7 @@ match "a": reveal_type(x) # revealed: Literal[1, 2] ``` -### Single-valued types, always false +### Literal subject, always false ```py x = 1 @@ -1099,7 +1099,7 @@ match "something else": reveal_type(x) # revealed: Literal[1] ``` -### Single-valued types, always false, with wildcard pattern +### Literal subject, always false, with wildcard pattern ```py x = 1 @@ -1115,7 +1115,7 @@ match "something else": reveal_type(x) # revealed: Literal[1] ``` -### Single-valued types, always false, with guard +### Literal subject, always false, with guard For definitely-false cases, the presence of a guard has no influence: @@ -1136,7 +1136,7 @@ match "something else": reveal_type(x) # revealed: Literal[1] ``` -### Non-single-valued types +### Broad subject type ```py def _(s: str): diff --git a/crates/ty_python_semantic/resources/mdtest/subscript/instance.md b/crates/ty_python_semantic/resources/mdtest/subscript/instance.md index f0490f831e..abe0367ade 100644 --- a/crates/ty_python_semantic/resources/mdtest/subscript/instance.md +++ b/crates/ty_python_semantic/resources/mdtest/subscript/instance.md @@ -15,7 +15,6 @@ error[not-subscriptable]: Cannot subscript object of type `NotSubscriptable` wit | 4 | a = NotSubscriptable()[0] | ^^^^^^^^^^^^^^^^^^^^^ - | ``` ## `__getitem__` not callable diff --git a/crates/ty_python_semantic/resources/mdtest/subscript/typevar.md b/crates/ty_python_semantic/resources/mdtest/subscript/typevar.md index e2917c141c..82428d0f2a 100644 --- a/crates/ty_python_semantic/resources/mdtest/subscript/typevar.md +++ b/crates/ty_python_semantic/resources/mdtest/subscript/typevar.md @@ -1,14 +1,14 @@ # Subscripts involving type variables -## TypeVar bound/constrained to a tuple/int-literal/bool-literal - -The upper bounds of type variables are considered when analysing subscripts. - ```toml [environment] python-version = "3.12" ``` +## TypeVar bound/constrained to a tuple/int-literal/bool-literal + +The upper bounds of type variables are considered when analysing subscripts. + ```py from typing_extensions import TypeAlias, Literal @@ -41,38 +41,128 @@ def f[ # but it's hard to do that without introducing false positives elsewhere reveal_type(tuple_1[some_integer]) # revealed: str | int | bytes - # TODO: would ideally be `tuple[str, int] | tuple[int, bytes]` - reveal_type(tuple_2[:2]) # revealed: tuple[str | int | bytes, ...] + reveal_type(tuple_2[:2]) # revealed: tuple[str, int] | tuple[int, bytes] reveal_type(tuple_2[zero]) # revealed: str | int reveal_type(tuple_2[some_integer]) # revealed: str | int | bytes # fmt: on ``` -## TypeVars +## Slicing overlapping constrained sequence types + +A value-constrained type variable selects one declared constraint for the entire function call. +Slicing a `list` or a `Sequence` preserves the selected constraint, even though `list` is also a +subtype of `Sequence`. + +```py +from collections.abc import Sequence + +def slice_sequence[T: (list[int], Sequence[int])](value: T) -> T: + reveal_type(value[:2]) # revealed: T@slice_sequence + return value[:2] +``` + +## Slicing constrained types with distinct implementations + +Each constraint's `__getitem__` method must be called with its own receiver. When both methods +return their corresponding constraint, the result preserves the original type variable. + +```py +class First: + def __getitem__(self, index: slice) -> "First": + return self + +class Second: + def __getitem__(self, index: slice) -> "Second": + return self + +def slice_value[T: (First, Second)](value: T) -> T: + return value[:2] +``` + +## Slicing a legacy constrained type variable + +Legacy `TypeVar` declarations preserve the selected constraint in the same way as PEP 695 type +parameters. ```toml [environment] -python-version = "3.12" +python-version = "3.10" ``` +```py +from collections.abc import Sequence +from typing import TypeVar + +T = TypeVar("T", list[int], Sequence[int]) + +def slice_sequence(value: T) -> T: + return value[:2] +``` + +## Slicing a constrained type can change its type + +A result cannot retain the constrained type variable when one constraint's slice returns a different +type. + +```py +class First: + def __getitem__(self, index: slice) -> "First": + return self + +class ChangesType: + def __getitem__(self, index: slice) -> First: + return First() + +def slice_value[T: (First, ChangesType)](value: T) -> T: + reveal_type(value[:2]) # revealed: First + # error: [invalid-return-type] + return value[:2] +``` + +## Slicing an upper-bounded type variable + +An upper-bounded type variable can specialize to a `Sequence` subclass whose slice returns a +different sequence type, so the slice is not guaranteed to preserve the type variable. + +```py +from collections.abc import Sequence + +def slice_sequence[T: Sequence[int]](value: T) -> T: + # error: [invalid-return-type] + return value[:2] +``` + +## Subscripting an unsupported constrained type + +A subscript remains invalid when any declared constraint does not support it. + +```py +class Sliceable: + def __getitem__(self, index: slice) -> "Sliceable": + return self + +def slice_value[T: (Sliceable, int)](value: T) -> None: + # error: [not-subscriptable] + value[:2] +``` + +## TypeVars + ```py from typing import Protocol class SupportsLessThan(Protocol): def __lt__(self, other, /) -> bool: ... +# `SupportsLessThan` says nothing about hashing, and a `dict` key must be `Hashable` +# error: [invalid-type-arguments] def f[K: SupportsLessThan](dictionary: dict[K, int], key: K): reveal_type(dictionary[key]) # revealed: int ``` ## ParamSpecs -```toml -[environment] -python-version = "3.12" -``` - ```py from typing import Callable diff --git a/crates/ty_python_semantic/resources/mdtest/suppressions/ty_ignore.md b/crates/ty_python_semantic/resources/mdtest/suppressions/ty_ignore.md index 7faafc6698..29b8829eaa 100644 --- a/crates/ty_python_semantic/resources/mdtest/suppressions/ty_ignore.md +++ b/crates/ty_python_semantic/resources/mdtest/suppressions/ty_ignore.md @@ -126,7 +126,6 @@ warning[unused-ignore-comment]: Unused `ty: ignore` directive | 3 | a = test + 3 # ty: ignore[possibly-unresolved-reference] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove the unused suppression comment | 2 | # snapshot @@ -150,7 +149,6 @@ warning[unused-ignore-comment]: Unused `ty: ignore` directive | 3 | a = test + 3 # ty: ignore[possibly-unresolved-reference] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove the unused suppression comment | 2 | # error: [unresolved-reference] @@ -184,7 +182,6 @@ warning[unused-ignore-comment]: Unused `ty: ignore` directive: 'unused-ignore-co | 2 | a = 10 / 0 # ty: ignore[division-by-zero, unused-ignore-comment] | ^^^^^^^^^^^^^^^^^^^^^ - | help: Remove the unused suppression code | 1 | # snapshot @@ -208,7 +205,6 @@ warning[unused-ignore-comment]: Unused `ty: ignore` directive | 2 | a = 10 / 2 # ty: ignore[division-by-zero, unresolved-reference] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove the unused suppression comment | 1 | # snapshot @@ -230,7 +226,6 @@ warning[unused-ignore-comment]: Unused `ty: ignore` directive: 'invalid-assignme | 5 | a = 10 / 0 # ty: ignore[invalid-assignment, division-by-zero, unresolved-reference] | ^^^^^^^^^^^^^^^^^^ - | help: Remove the unused suppression code | 4 | # snapshot @@ -245,7 +240,6 @@ warning[unused-ignore-comment]: Unused `ty: ignore` directive: 'unresolved-refer | 5 | a = 10 / 0 # ty: ignore[invalid-assignment, division-by-zero, unresolved-reference] | ^^^^^^^^^^^^^^^^^^^^ - | help: Remove the unused suppression code | 4 | # snapshot @@ -266,7 +260,6 @@ warning[unused-ignore-comment]: Unused `ty: ignore` directive: 'invalid-assignme | 7 | a = 10 / 0 # ty: ignore[invalid-assignment, unresolved-reference, division-by-zero] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove the unused suppression codes | 6 | # snapshot @@ -312,7 +305,6 @@ warning[unused-ignore-comment]: Unused `ty: ignore` directive | 9 | # fmt: off # ty: ignore[division-by-zero] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove the unused suppression comment | 8 | # snapshot @@ -337,7 +329,6 @@ warning[unused-ignore-comment]: Unused `ty: ignore` directive | 15 | # ty: ignore[division-by-zero] # fmt: off | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove the unused suppression comment | 14 | # snapshot @@ -475,7 +466,6 @@ warning[ignore-comment-unknown-rule]: Unknown rule `division-by-zer`. Did you me | 2 | a = 10 + 4 # ty: ignore[division-by-zer] | ^^^^^^^^^^^^^^^ - | ``` ## Code with `lint:` prefix diff --git a/crates/ty_python_semantic/resources/mdtest/suppressions/type_ignore.md b/crates/ty_python_semantic/resources/mdtest/suppressions/type_ignore.md index fdf33f588a..6ba5bdbc6f 100644 --- a/crates/ty_python_semantic/resources/mdtest/suppressions/type_ignore.md +++ b/crates/ty_python_semantic/resources/mdtest/suppressions/type_ignore.md @@ -170,7 +170,6 @@ warning[unused-ignore-comment]: Unused `ty: ignore` directive | 9 | + 2) # ty:ignore[division-by-zero] # fmt: skip | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove the unused suppression comment | 8 | # snapshot @@ -192,7 +191,6 @@ warning[unused-ignore-comment]: Unused `ty: ignore` directive | 12 | + 2) # fmt: skip # ty:ignore[division-by-zero] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove the unused suppression comment | 11 | # snapshot @@ -319,7 +317,6 @@ warning[unused-type-ignore-comment]: Unused `type: ignore` directive: 'division- | 2 | a = 10 / 2 # type: ignore[mypy-code, ty:division-by-zero] | ^^^^^^^^^^^^^^^^^^^ - | help: Remove the unused suppression code | 1 | # snapshot @@ -341,7 +338,6 @@ warning[unused-type-ignore-comment]: Unused `type: ignore` directive | 2 | a = 10 / 2 # type: ignore[ty:division-by-zero] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove the unused suppression comment | 1 | # snapshot @@ -363,5 +359,4 @@ warning[ignore-comment-unknown-rule]: Unknown rule `division-by`. Did you mean ` | 2 | a = 10 / 2 # type: ignore[ty:division-by] | ^^^^^^^^^^^^^^ - | ``` diff --git a/crates/ty_python_semantic/resources/mdtest/ty_extensions.md b/crates/ty_python_semantic/resources/mdtest/ty_extensions.md index 8777079674..bf7e637a13 100644 --- a/crates/ty_python_semantic/resources/mdtest/ty_extensions.md +++ b/crates/ty_python_semantic/resources/mdtest/ty_extensions.md @@ -31,11 +31,9 @@ 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]` - # respectively, since all possible runtime objects that are created by the literal syntax `1` - # are members of the type `Literal[1]` - reveal_type(not_one is not 1) # revealed: bool - reveal_type(not_one is 1) # revealed: bool + # Negating a literal rules out every literal with that value. + reveal_type(not_one is not 1) # revealed: Literal[True] + reveal_type(not_one is 1) # revealed: Literal[False] # But these are both `bool`, rather than `Literal[True]` or `Literal[False]` # as there are many runtime objects that inhabit the type `~Literal[1]` @@ -92,8 +90,8 @@ 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 ty_extensions import Unknown, static_assert -from ty_extensions._internal import is_assignable_to, reveal_mro +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_assignable_to, reveal_mro static_assert(is_assignable_to(Unknown, int)) static_assert(is_assignable_to(int, Unknown)) @@ -112,7 +110,7 @@ class C(Unknown): ... # revealed: (, Unknown, ) reveal_mro(C) -# error: "Special form `ty_extensions.Unknown` expected no type parameter" +# error: "Special form `ty_extensions._internal.Unknown` expected no type parameter" u: Unknown[str] ``` @@ -294,7 +292,6 @@ error[static-assert-error]: Static assertion error: argument evaluates to `False | ^^^^^^^^^^^^^^-----^ | | | Inferred type of argument is `Literal[False]` - | ``` With a custom message: @@ -312,7 +309,6 @@ error[static-assert-error]: Static assertion error: with a message | ^^^^^^^^^^^^^^-----^^^^^^^^^^^^^^^^^^^ | | | Inferred type of argument is `Literal[False]` - | ``` When it evaluates to something falsy: @@ -330,7 +326,6 @@ error[static-assert-error]: Static assertion error: argument of type `Literal["" | ^^^^^^^^^^^^^^--^ | | | Inferred type of argument is `Literal[""]` - | ``` When it evaluates to something that is not statically known to be truthy or falsy: @@ -348,7 +343,6 @@ error[static-assert-error]: Static assertion error: argument of type `int` has a | ^^^^^^^^^^^^^^--------------------^ | | | Inferred type of argument is `int` - | ``` ## Type predicates @@ -434,21 +428,6 @@ static_assert(not is_singleton(int)) static_assert(not is_singleton(Literal["a"])) ``` -### Single-valued types - -```py -from ty_extensions import static_assert -from ty_extensions._internal import is_single_valued -from typing import Literal - -static_assert(is_single_valued(None)) -static_assert(is_single_valued(Literal[True])) -static_assert(is_single_valued(Literal["a"])) - -static_assert(not is_single_valued(int)) -static_assert(not is_single_valued(Literal["a"] | Literal["b"])) -``` - ## `TypeOf` We use `TypeOf` to get the inferred type of an expression. This is useful when we want to refer to 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 index 98af644f8f..396d7dff34 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_compendium/integer_literals.md +++ b/crates/ty_python_semantic/resources/mdtest/type_compendium/integer_literals.md @@ -50,22 +50,9 @@ def f(x: int): reveal_type(x) # revealed: int ``` -## Integer `Literal`s are single-valued types +## Equality narrowing for integer `Literal`s -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 -from ty_extensions._internal import 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 equality comparisons: +Integer literals can narrow types in equality comparisons: ```py def f(x: int): diff --git a/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md b/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md index c1fbaff3e2..a80fcfe227 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md @@ -506,8 +506,8 @@ An unspecialized tuple is equivalent to `tuple[Any, ...]` and `tuple[Unknown, .. ```py from typing_extensions import Any, assert_type -from ty_extensions import Unknown, static_assert -from ty_extensions._internal import is_equivalent_to +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_equivalent_to static_assert(is_equivalent_to(tuple[Any, ...], tuple[Unknown, ...])) @@ -555,10 +555,10 @@ y = 1, 2 reveal_type(("foo", *y)) # revealed: tuple[Literal["foo"], Literal[1], Literal[2]] -aa: tuple[list[int], ...] = ([42], *{[56], [78]}, [100]) +aa: tuple[list[int], ...] = ([42], *[[56], [78]], [100]) reveal_type(aa) # revealed: tuple[list[int], list[int], list[int], list[int]] -bb: tuple[list[Literal[42, 56]], ...] = ([42], *{[56, 42], [42]}, [42, 42, 56]) +bb: tuple[list[Literal[42, 56]], ...] = ([42], *[[56, 42], [42]], [42, 42, 56]) reveal_type(bb) # revealed: tuple[list[Literal[42, 56]], list[Literal[42, 56]], list[Literal[42, 56]], list[Literal[42, 56]]] reveal_type((*[],)) # revealed: tuple[()] diff --git a/crates/ty_python_semantic/resources/mdtest/type_of/basic.md b/crates/ty_python_semantic/resources/mdtest/type_of/basic.md index 00c38b31b5..ef443eb97e 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_of/basic.md +++ b/crates/ty_python_semantic/resources/mdtest/type_of/basic.md @@ -319,8 +319,6 @@ from typing import final, Any from ty_extensions import static_assert from ty_extensions._internal import is_assignable_to, is_subtype_of, is_disjoint_from -class Biv[T]: ... - class Cov[T]: def pop(self) -> T: raise NotImplementedError @@ -332,9 +330,6 @@ class Contra[T]: class Inv[T]: x: T -@final -class BivSub[T](Biv[T]): ... - @final class CovSub[T](Cov[T]): ... @@ -345,9 +340,6 @@ class ContraSub[T](Contra[T]): ... class InvSub[T](Inv[T]): ... def _[T, U](): - static_assert(is_subtype_of(type[BivSub[T]], type[BivSub[U]])) - static_assert(not is_disjoint_from(type[BivSub[U]], type[BivSub[T]])) - # `T` and `U` could specialize to the same type. static_assert(not is_subtype_of(type[CovSub[T]], type[CovSub[U]])) static_assert(not is_disjoint_from(type[CovSub[U]], type[CovSub[T]])) @@ -359,12 +351,6 @@ def _[T, U](): static_assert(not is_disjoint_from(type[InvSub[U]], type[InvSub[T]])) def _(): - static_assert(is_subtype_of(type[BivSub[bool]], type[BivSub[int]])) - static_assert(is_subtype_of(type[BivSub[int]], type[BivSub[bool]])) - static_assert(not is_disjoint_from(type[BivSub[bool]], type[BivSub[int]])) - # `BivSub[int]` and `BivSub[str]` are mutual subtypes. - static_assert(not is_disjoint_from(type[BivSub[int]], type[BivSub[str]])) - static_assert(is_subtype_of(type[CovSub[bool]], type[CovSub[int]])) static_assert(not is_subtype_of(type[CovSub[int]], type[CovSub[bool]])) static_assert(not is_disjoint_from(type[CovSub[bool]], type[CovSub[int]])) @@ -383,12 +369,6 @@ def _(): static_assert(is_disjoint_from(type[InvSub[bool]], type[InvSub[int]])) def _[T](): - static_assert(is_subtype_of(type[BivSub[T]], type[BivSub[Any]])) - static_assert(is_subtype_of(type[BivSub[Any]], type[BivSub[T]])) - static_assert(is_assignable_to(type[BivSub[T]], type[BivSub[Any]])) - static_assert(is_assignable_to(type[BivSub[Any]], type[BivSub[T]])) - static_assert(not is_disjoint_from(type[BivSub[T]], type[BivSub[Any]])) - static_assert(not is_subtype_of(type[CovSub[T]], type[CovSub[Any]])) static_assert(not is_subtype_of(type[CovSub[Any]], type[CovSub[T]])) static_assert(is_assignable_to(type[CovSub[T]], type[CovSub[Any]])) @@ -408,12 +388,6 @@ def _[T](): static_assert(not is_disjoint_from(type[InvSub[T]], type[InvSub[Any]])) def _[T, U](): - static_assert(is_subtype_of(type[BivSub[T]], type[Biv[T]])) - static_assert(not is_subtype_of(type[Biv[T]], type[BivSub[T]])) - static_assert(not is_disjoint_from(type[BivSub[T]], type[Biv[T]])) - static_assert(not is_disjoint_from(type[BivSub[U]], type[Biv[T]])) - static_assert(not is_disjoint_from(type[BivSub[U]], type[Biv[U]])) - static_assert(is_subtype_of(type[CovSub[T]], type[Cov[T]])) static_assert(not is_subtype_of(type[Cov[T]], type[CovSub[T]])) static_assert(not is_disjoint_from(type[CovSub[T]], type[Cov[T]])) @@ -433,11 +407,6 @@ def _[T, U](): static_assert(not is_disjoint_from(type[InvSub[U]], type[Inv[U]])) def _(): - static_assert(is_subtype_of(type[BivSub[bool]], type[Biv[int]])) - static_assert(is_subtype_of(type[BivSub[int]], type[Biv[bool]])) - static_assert(not is_disjoint_from(type[BivSub[bool]], type[Biv[int]])) - static_assert(not is_disjoint_from(type[BivSub[int]], type[Biv[bool]])) - static_assert(is_subtype_of(type[CovSub[bool]], type[Cov[int]])) static_assert(not is_subtype_of(type[CovSub[int]], type[Cov[bool]])) static_assert(not is_disjoint_from(type[CovSub[bool]], type[Cov[int]])) @@ -454,12 +423,6 @@ def _(): static_assert(is_disjoint_from(type[InvSub[int]], type[Inv[bool]])) def _[T](): - static_assert(is_subtype_of(type[BivSub[T]], type[Biv[Any]])) - static_assert(is_subtype_of(type[BivSub[Any]], type[Biv[T]])) - static_assert(is_assignable_to(type[BivSub[T]], type[Biv[Any]])) - static_assert(is_assignable_to(type[BivSub[Any]], type[Biv[T]])) - static_assert(not is_disjoint_from(type[BivSub[T]], type[Biv[Any]])) - static_assert(not is_subtype_of(type[CovSub[T]], type[Cov[Any]])) static_assert(not is_subtype_of(type[CovSub[Any]], type[Cov[T]])) static_assert(is_assignable_to(type[CovSub[T]], type[Cov[Any]])) diff --git a/crates/ty_python_semantic/resources/mdtest/type_of/generics.md b/crates/ty_python_semantic/resources/mdtest/type_of/generics.md index b746548b83..2de8042120 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_of/generics.md +++ b/crates/ty_python_semantic/resources/mdtest/type_of/generics.md @@ -16,7 +16,8 @@ def _[T](x: T): reveal_type(type(x)) # revealed: type[T@_] ``` -`type[T]` with an unbounded type variable represents any subclass of `object`. +`type[T]` with an unbounded type variable represents any subclass of `object`. Constructor calls are +checked against `object.__init__`. ```py def unbounded[T](x: type[T]) -> T: @@ -25,10 +26,54 @@ def unbounded[T](x: type[T]) -> T: reveal_type(x.__init__) # revealed: def __init__(self) reveal_type(x.__qualname__) # revealed: str reveal_type(x()) # revealed: T@unbounded + # error: [too-many-positional-arguments] "Too many positional arguments to `type[T]`: expected 0, got 1" + x(1) + # error: [unknown-argument] "Argument `value` does not match any known parameter of `type[T]`" + x(value=1) return x() ``` +An explicit `object` upper bound has the same constructor signature as an implicit `object` bound, +including for legacy type variables: + +```py +from typing import TypeVar + +LegacyObjectT = TypeVar("LegacyObjectT", bound=object) + +def explicit_object_bound[T: object](x: type[T]) -> T: + reveal_type(x()) # revealed: T@explicit_object_bound + x(1) # error: [too-many-positional-arguments] + return x() + +def legacy_object_bound(x: type[LegacyObjectT]) -> LegacyObjectT: + reveal_type(x()) # revealed: LegacyObjectT@legacy_object_bound + x(1) # error: [too-many-positional-arguments] + return x() +``` + +Aliases of `object` have the same constructor and callable signature as a direct `object` bound, +even when the bound contains more than one alias: + +```py +from collections.abc import Callable + +type ObjectAlias = object +type ChainedObjectAlias = ObjectAlias + +def aliased_object_bound[T: ChainedObjectAlias](cls: type[T]) -> T: + # error: [too-many-positional-arguments] "Too many positional arguments to `type[T]`: expected 0, got 1" + cls(1) + # error: [unknown-argument] "Argument `value` does not match any known parameter of `type[T]`" + cls(value=1) + + zero_argument: Callable[[], T] = cls + # error: [invalid-assignment] + one_argument: Callable[[int], T] = cls + return cls() +``` + `type[T]` with an upper bound of `T: A` represents any subclass of `A`. ```py @@ -97,7 +142,8 @@ reveal_type(union_bound(Multiply)) # revealed: Multiply ## Union ```py -from ty_extensions import Intersection, Unknown +from ty_extensions import Intersection +from ty_extensions._internal import Unknown def _[T: int](x: type | type[T]): reveal_type(x()) # revealed: Any @@ -161,6 +207,45 @@ class Holder(Generic[T]): reveal_type(self.value) # revealed: type[T@Holder] | ((() -> type[T@Holder]) & type) ``` +## Narrowing constructor calls + +Narrowing a class object with `issubclass` uses the narrowed class's constructor without losing its +original type variable: + +```py +class IntConstructor: + def __init__(self, value: int) -> None: ... + +def narrowed_subclass[T](cls: type[T]) -> T: + if issubclass(cls, IntConstructor): + reveal_type(cls(1)) # revealed: T@narrowed_subclass & IntConstructor + return cls(1) + return cls() +``` + +Invalid positional and keyword arguments each produce only the narrowed subclass constructor's +diagnostic: + +```py +def narrowed_invalid[T](cls: type[T]) -> None: + if issubclass(cls, IntConstructor): + # error: [invalid-argument-type] "Argument to `IntConstructor.__init__` is incorrect: Expected `int`, found `Literal["wrong"]`" + cls("wrong") + + # error: [invalid-argument-type] "Argument to `IntConstructor.__init__` is incorrect: Expected `int`, found `Literal["wrong"]`" + cls(value="wrong") +``` + +Checking a class object's identity preserves the original type variable in the same way: + +```py +def narrowed_identity[T](cls: type[T]) -> T: + if cls is IntConstructor: + reveal_type(cls(1)) # revealed: T@narrowed_identity & IntConstructor + return cls(1) + return cls() +``` + ## `__class__` ```py @@ -601,9 +686,11 @@ expects_type_c_of_int_and_str(C) # Also OK, the specialized `C[int, str]` is assignable to `type[C[int, str]]` expects_type_c_of_int_and_str(C[int, str]) -# TODO: these should be errors +# error: [invalid-argument-type] expects_type_c_of_int_and_str(C[str]) +# error: [invalid-argument-type] expects_type_c_of_int_and_str(C[int, str, bytes]) +# error: [invalid-argument-type] expects_type_c_of_int_and_str(C[str, int]) ``` @@ -619,24 +706,28 @@ def expects_type_c_default_of_int_str(f: type[C[int, str]]): ... expects_type_c_default(C) expects_type_c_default(C[int, str]) -expects_type_c_default_of_int(C) expects_type_c_default_of_int(C[int]) expects_type_c_default_of_int_str(C) expects_type_c_default_of_int_str(C[int, str]) -# TODO: these should be errors +# error: [invalid-argument-type] expects_type_c_default(C[int]) +# error: [invalid-argument-type] +expects_type_c_default_of_int(C) +# error: [invalid-argument-type] expects_type_c_default_of_int(C[str]) +# error: [invalid-argument-type] expects_type_c_default_of_int_str(C[str, int]) ``` ## Upcasting a `type[]` type to a `Callable` type -`type[T]` accepts the same parameters as `object.__init__` if `T` does not have an upper bound. If -`T` is bound to a nominal-instance type, `type[T]` accepts the same parameters as the constructor of -the class that the instance-type refers to. +`type[T]` accepts the same parameters as `object.__init__` if `T` has an implicit or explicit +`object` upper bound. If `T` has a more specific upper bound, `type[T]` accepts the same parameters +as that bound's constructor. Bare `type` retains its permissive constructor signature. ```py +from collections.abc import Callable from ty_extensions._internal import RegularCallableTypeOf class TakesStrInConstructor: @@ -672,13 +763,21 @@ def f[ reveal_type(type_object("")) # revealed: Any reveal_type(type_t_unbound()) # revealed: T@f - # TODO: we could consider emitting an error here as well + # error: [too-many-positional-arguments] reveal_type(type_t_unbound("")) # revealed: T@f + zero_argument_unbound: Callable[[], T] = type_t_unbound + # error: [invalid-assignment] + one_argument_unbound: Callable[[int], T] = type_t_unbound + reveal_type(type_t_object_bound()) # revealed: T1@f - # TODO: we could consider emitting an error here as well + # error: [too-many-positional-arguments] reveal_type(type_t_object_bound("")) # revealed: T1@f + zero_argument_object_bound: Callable[[], T1] = type_t_object_bound + # error: [invalid-assignment] + one_argument_object_bound: Callable[[int], T1] = type_t_object_bound + reveal_type(type_int()) # revealed: int reveal_type(type_int("1")) # revealed: int # error: [invalid-argument-type] @@ -720,10 +819,8 @@ def f[ reveal_type(bare_type_upcast) # revealed: (...) -> Any reveal_type(type_object_upcast) # revealed: (...) -> Any - # TODO: if we did decide to override typeshed's `type.__call__` annotations (see above), - # we should also turn these two into `() -> T@f` / `() -> T1@f` - reveal_type(type_t_unbound_upcast) # revealed: (...) -> T@f - reveal_type(type_t_object_bound_upcast) # revealed: (...) -> T1@f + reveal_type(type_t_unbound_upcast) # revealed: () -> T@f + reveal_type(type_t_object_bound_upcast) # revealed: () -> T1@f # revealed: Overload[(x: ConvertibleToInt = 0, /) -> int, (x: str | bytes | bytearray, /, base: SupportsIndex) -> int] reveal_type(type_int_upcast) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md b/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md index c4e8cd5c9e..eca4a0a144 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md @@ -49,22 +49,20 @@ def _[T]() -> None: ConstraintSet.range(Sub, T, Super) ``` -Every type is a supertype of `Never`, so a lower bound of `Never` is the same as having no lower -bound. +Every type is a supertype of `Never`, so `upper_bound` can omit the lower bound. ```py def _[T]() -> None: # (T@_ ≤ Base) - ConstraintSet.range(Never, T, Base) + ConstraintSet.upper_bound(T, Base) ``` -Similarly, every type is a subtype of `object`, so an upper bound of `object` is the same as having -no upper bound. +Similarly, every type is a subtype of `object`, so `lower_bound` can omit the upper bound. ```py def _[T]() -> None: # (Base ≤ T@_) - ConstraintSet.range(Base, T, object) + ConstraintSet.lower_bound(Base, T) ``` And a range constraint with a lower bound of `Never` and an upper bound of `object` allows the @@ -88,13 +86,13 @@ def _[T]() -> None: static_assert(not ConstraintSet.range(Base, T, Unrelated)) ``` -The lower and upper bound can be the same type, in which case the typevar can only be specialized to +When the lower and upper bounds are the same type, `equality` requires the typevar to specialize to that specific type. ```py def _[T]() -> None: # (T@_ = Base) - ConstraintSet.range(Base, T, Base) + ConstraintSet.equality(T, Base) ``` Constraints can only refer to fully static types, so the lower and upper bounds are transformed into @@ -103,7 +101,7 @@ their bottom and top materializations, respectively. ```py def _[T]() -> None: constraints = ConstraintSet.range(Base, T, Any) - expected = ConstraintSet.range(Base, T, object) + expected = ConstraintSet.lower_bound(Base, T) static_assert(constraints == expected) constraints = ConstraintSet.range(Sequence[Base], T, Sequence[Any]) @@ -111,7 +109,7 @@ def _[T]() -> None: static_assert(constraints == expected) constraints = ConstraintSet.range(Any, T, Base) - expected = ConstraintSet.range(Never, T, Base) + expected = ConstraintSet.upper_bound(T, Base) static_assert(constraints == expected) constraints = ConstraintSet.range(Sequence[Any], T, Sequence[Base]) @@ -119,6 +117,65 @@ def _[T]() -> None: static_assert(constraints == expected) ``` +### Lower bound + +A lower-bound constraint requires the type variable to be a supertype of its bound without providing +upper-bound evidence. + +```py +from ty_extensions import static_assert +from ty_extensions._internal import ConstraintSet, is_constraint_set_assignable_to + +def _[T]() -> None: + expected = is_constraint_set_assignable_to(int, T) + static_assert(ConstraintSet.lower_bound(int, T) == expected) +``` + +### Upper bound + +An upper-bound constraint requires the type variable to be a subtype of its bound without providing +lower-bound evidence. + +```py +from ty_extensions import static_assert +from ty_extensions._internal import ConstraintSet, is_constraint_set_assignable_to + +def _[T]() -> None: + expected = is_constraint_set_assignable_to(T, int) + static_assert(ConstraintSet.upper_bound(T, int) == expected) +``` + +Unlike an explicit two-sided range, an upper-bound constraint does not supply `Never` as lower-bound +inference evidence. + +```py +from typing import Never + +def inferred_solution[T]() -> None: + # revealed: tuple[Solution[T=int]] + reveal_type(ConstraintSet.upper_bound(T, int).solutions_for(T, inferable=tuple[T])) + + # revealed: tuple[Solution[T=Never]] + reveal_type(ConstraintSet.range(Never, T, int).solutions_for(T, inferable=tuple[T])) +``` + +### Equality + +An equality constraint requires the type variable to specialize exactly to the specified type. It is +equivalent to an explicit range with that type as both bounds. + +```py +from ty_extensions import static_assert +from ty_extensions._internal import ConstraintSet + +def _[T]() -> None: + equality = ConstraintSet.equality(T, int) + static_assert(equality == ConstraintSet.range(int, T, int)) + + # revealed: tuple[Solution[T=int]] + reveal_type(equality.solutions_for(T, inferable=tuple[T])) +``` + ### Negated range A _negated range_ constraint is the opposite of a range constraint: it requires the typevar to _not_ @@ -142,22 +199,20 @@ def _[T]() -> None: ~ConstraintSet.range(Sub, T, Super) ``` -Every type is a supertype of `Never`, so a lower bound of `Never` is the same as having no lower -bound. +Every type is a supertype of `Never`, so `upper_bound` can omit the lower bound. ```pyi def _[T]() -> None: # ¬(T@_ ≤ Base) - ~ConstraintSet.range(Never, T, Base) + ~ConstraintSet.upper_bound(T, Base) ``` -Similarly, every type is a subtype of `object`, so an upper bound of `object` is the same as having -no upper bound. +Similarly, every type is a subtype of `object`, so `lower_bound` can omit the upper bound. ```pyi def _[T]() -> None: # ¬(Base ≤ T@_) - ~ConstraintSet.range(Base, T, object) + ~ConstraintSet.lower_bound(Base, T) ``` And a negated range constraint with _both_ a lower bound of `Never` and an upper bound of `object` @@ -184,7 +239,7 @@ type other than that specific type. ```pyi def _[T]() -> None: # (T@_ ≠ Base) - ~ConstraintSet.range(Base, T, Base) + ~ConstraintSet.equality(T, Base) ``` Constraints can only refer to fully static types, so the lower and upper bounds are transformed into @@ -193,7 +248,7 @@ their bottom and top materializations, respectively. ```pyi def _[T]() -> None: constraints = ~ConstraintSet.range(Base, T, Any) - expected = ~ConstraintSet.range(Base, T, object) + expected = ~ConstraintSet.lower_bound(Base, T) static_assert(constraints == expected) constraints = ~ConstraintSet.range(Sequence[Base], T, Sequence[Any]) @@ -201,7 +256,7 @@ def _[T]() -> None: static_assert(constraints == expected) constraints = ~ConstraintSet.range(Any, T, Base) - expected = ~ConstraintSet.range(Never, T, Base) + expected = ~ConstraintSet.upper_bound(T, Base) static_assert(constraints == expected) constraints = ~ConstraintSet.range(Sequence[Any], T, Sequence[Base]) @@ -213,8 +268,8 @@ A negated _type_ is not the same thing as a negated _range_. ```pyi def _[T]() -> None: - negated_type = ConstraintSet.range(Never, T, ~int) - negated_constraint = ~ConstraintSet.range(Never, T, int) + negated_type = ConstraintSet.upper_bound(T, ~int) + negated_constraint = ~ConstraintSet.upper_bound(T, int) static_assert(negated_type != negated_constraint) ``` @@ -270,7 +325,7 @@ def _[T]() -> None: static_assert(constraints == expected) constraints = ConstraintSet.range(Sub, T, Base) & ConstraintSet.range(Base, T, Super) - expected = ConstraintSet.range(Base, T, Base) + expected = ConstraintSet.equality(T, Base) static_assert(constraints == expected) constraints = ConstraintSet.range(Sub, T, Super) & ConstraintSet.range(Sub, T, Super) @@ -283,7 +338,7 @@ If they don't overlap, the intersection is empty. ```pyi def _[T]() -> None: static_assert(not ConstraintSet.range(SubSub, T, Sub) & ConstraintSet.range(Base, T, Super)) - static_assert(not ConstraintSet.range(SubSub, T, Sub) & ConstraintSet.range(Unrelated, T, object)) + static_assert(not ConstraintSet.range(SubSub, T, Sub) & ConstraintSet.lower_bound(Unrelated, T)) ``` Expanding on this, when intersecting two upper bounds constraints (`(T ≤ Base) ∧ (T ≤ Other)`), we @@ -291,16 +346,14 @@ intersect the upper bounds. Any type that satisfies both `T ≤ Base` and `T ≤ satisfy their intersection `T ≤ Base & Other`, and vice versa. ```pyi -from typing import Never - # This is not final, so it's possible for a subclass to inherit from both Base and Other. class Other: ... def upper_bounds[T](): # (T@upper_bounds ≤ Base & Other) - intersection_type = ConstraintSet.range(Never, T, Base & Other) + intersection_type = ConstraintSet.upper_bound(T, Base & Other) # (T@upper_bounds ≤ Base) ∧ (T@upper_bounds ≤ Other) - intersection_constraint = ConstraintSet.range(Never, T, Base) & ConstraintSet.range(Never, T, Other) + intersection_constraint = ConstraintSet.upper_bound(T, Base) & ConstraintSet.upper_bound(T, Other) static_assert(intersection_type == intersection_constraint) ``` @@ -311,12 +364,53 @@ bounds. Any type that satisfies both `Base ≤ T` and `Other ≤ T` must necessa ```pyi def lower_bounds[T](): # (Base | Other ≤ T@lower_bounds) - union_type = ConstraintSet.range(Base | Other, T, object) + union_type = ConstraintSet.lower_bound(Base | Other, T) # (Base ≤ T@upper_bounds) ∧ (Other ≤ T@upper_bounds) - intersection_constraint = ConstraintSet.range(Base, T, object) & ConstraintSet.range(Other, T, object) + intersection_constraint = ConstraintSet.lower_bound(Base, T) & ConstraintSet.lower_bound(Other, T) static_assert(union_type == intersection_constraint) ``` +### Intersection of two equality constraints + +A type variable cannot be exactly equal to two non-equivalent types. This is stronger than checking +whether the types are disjoint: two classes can have a common subclass, which makes their +upper-bound constraints compatible, but that subclass is not exactly equal to either class. + +```py +from typing import Any +from ty_extensions import static_assert +from ty_extensions._internal import ConstraintSet + +class Row: ... +class RowTuple(Row, tuple[Any, ...]): ... + +def _[T, U, V]() -> None: + row = ConstraintSet.equality(T, Row) + tuple_ = ConstraintSet.equality(T, tuple[Any, ...]) + static_assert(~(row & tuple_)) + + equivalent = row & row + static_assert(equivalent == row) + + upper_bounds = ConstraintSet.upper_bound(T, Row) & ConstraintSet.upper_bound(T, tuple[Any, ...]) + static_assert(not ~upper_bounds) + + row_tuple = ConstraintSet.equality(T, RowTuple) + static_assert(row_tuple & upper_bounds == row_tuple) + + gradual_mismatch = ConstraintSet.equality(T, list[Any]) & ConstraintSet.equality(T, list[int]) + static_assert(~gradual_mismatch) + + any_mismatch = ConstraintSet.equality(T, Any) & ConstraintSet.equality(T, int) + static_assert(~any_mismatch) + + symbolic_mismatch = ConstraintSet.equality(T, tuple[U, Any]) & ConstraintSet.equality(T, tuple[U, int]) + static_assert(~symbolic_mismatch) + + symbolic_match = ConstraintSet.equality(T, list[U]) & ConstraintSet.equality(T, list[V]) + static_assert(not ~symbolic_match) +``` + ### Intersection of a range and a negated range The bounds of the range constraint provide a range of types that should be included; the bounds of @@ -324,7 +418,7 @@ the negated range constraint provide a "hole" of types that should not be includ the intersection as removing the hole from the range constraint. ```py -from typing import final, Never +from typing import final from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet @@ -350,7 +444,7 @@ anything; the intersection is the positive range. ```py def _[T]() -> None: - constraints = ConstraintSet.range(Sub, T, Base) & ~ConstraintSet.range(Never, T, Unrelated) + constraints = ConstraintSet.range(Sub, T, Base) & ~ConstraintSet.upper_bound(T, Unrelated) expected = ConstraintSet.range(Sub, T, Base) static_assert(constraints == expected) @@ -410,7 +504,7 @@ def _[T]() -> None: # ¬(Base ≤ T@_ ≤ Super) ∧ ¬(SubSub ≤ T@_ ≤ Sub)) ~ConstraintSet.range(SubSub, T, Sub) & ~ConstraintSet.range(Base, T, Super) # ¬(SubSub ≤ T@_ ≤ Sub) ∧ ¬(Unrelated ≤ T@_) - ~ConstraintSet.range(SubSub, T, Sub) & ~ConstraintSet.range(Unrelated, T, object) + ~ConstraintSet.range(SubSub, T, Sub) & ~ConstraintSet.lower_bound(Unrelated, T) ``` In particular, the following does not simplify, even though it seems like it could simplify to @@ -493,7 +587,7 @@ def _[T]() -> None: # (Base ≤ T@_ ≤ Super) ∨ (SubSub ≤ T@_ ≤ Sub) ConstraintSet.range(SubSub, T, Sub) | ConstraintSet.range(Base, T, Super) # (SubSub ≤ T@_ ≤ Sub) ∨ (Unrelated ≤ T@_) - ConstraintSet.range(SubSub, T, Sub) | ConstraintSet.range(Unrelated, T, object) + ConstraintSet.range(SubSub, T, Sub) | ConstraintSet.lower_bound(Unrelated, T) ``` In particular, the following does not simplify, even though it seems like it could simplify to @@ -518,19 +612,17 @@ as `T = Base | Other`) that satisfy the union type, but not the union constraint that satisfies the union constraint satisfies the union type. ```py -from typing import Never - # This is not final, so it's possible for a subclass to inherit from both Base and Other. class Other: ... def union[T](): # (T@union ≤ Base | Other) - union_type = ConstraintSet.range(Never, T, Base | Other) + union_type = ConstraintSet.upper_bound(T, Base | Other) # (T@union ≤ Base) ∨ (T@union ≤ Other) - union_constraint = ConstraintSet.range(Never, T, Base) | ConstraintSet.range(Never, T, Other) + union_constraint = ConstraintSet.upper_bound(T, Base) | ConstraintSet.upper_bound(T, Other) # (T = Base | Other) satisfies (T ≤ Base | Other) but not (T ≤ Base ∨ T ≤ Other) - specialization = ConstraintSet.range(Base | Other, T, Base | Other) + specialization = ConstraintSet.equality(T, Base | Other) static_assert(specialization.satisfies(union_type)) static_assert(not specialization.satisfies(union_constraint)) @@ -546,12 +638,12 @@ satisfies the union constraint (`(Base ≤ T) ∨ (Other ≤ T)`) but not the un ```py def union[T](): # (Base | Other ≤ T@union) - union_type = ConstraintSet.range(Base | Other, T, object) + union_type = ConstraintSet.lower_bound(Base | Other, T) # (Base ≤ T@union) ∨ (Other ≤ T@union) - union_constraint = ConstraintSet.range(Base, T, object) | ConstraintSet.range(Other, T, object) + union_constraint = ConstraintSet.lower_bound(Base, T) | ConstraintSet.lower_bound(Other, T) # (T = Base) satisfies (Base ≤ T ∨ Other ≤ T) but not (Base | Other ≤ T) - specialization = ConstraintSet.range(Base, T, Base) + specialization = ConstraintSet.equality(T, Base) static_assert(not specialization.satisfies(union_type)) static_assert(specialization.satisfies(union_constraint)) @@ -567,7 +659,7 @@ the negated range constraint provide a "hole" of types that should not be includ the union as filling part of the hole with the types from the range constraint. ```py -from typing import final, Never +from typing import final from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet @@ -593,7 +685,7 @@ the union is the negative range. ```py def _[T]() -> None: - constraints = ~ConstraintSet.range(Sub, T, Base) | ConstraintSet.range(Never, T, Unrelated) + constraints = ~ConstraintSet.range(Sub, T, Base) | ConstraintSet.upper_bound(T, Unrelated) expected = ~ConstraintSet.range(Sub, T, Base) static_assert(constraints == expected) @@ -643,7 +735,7 @@ def _[T]() -> None: static_assert(constraints == expected) constraints = ~ConstraintSet.range(Sub, T, Base) | ~ConstraintSet.range(Base, T, Super) - expected = ~ConstraintSet.range(Base, T, Base) + expected = ~ConstraintSet.equality(T, Base) static_assert(constraints == expected) constraints = ~ConstraintSet.range(Sub, T, Super) | ~ConstraintSet.range(Sub, T, Super) @@ -656,7 +748,7 @@ If the holes don't overlap, the union is always satisfied. ```py def _[T]() -> None: static_assert(~ConstraintSet.range(SubSub, T, Sub) | ~ConstraintSet.range(Base, T, Super)) - static_assert(~ConstraintSet.range(SubSub, T, Sub) | ~ConstraintSet.range(Unrelated, T, object)) + static_assert(~ConstraintSet.range(SubSub, T, Sub) | ~ConstraintSet.lower_bound(Unrelated, T)) ``` ## Negation @@ -676,9 +768,9 @@ def _[T]() -> None: # ¬(Sub ≤ T@_ ≤ Base) ~ConstraintSet.range(Sub, T, Base) # ¬(T@_ ≤ Base) - ~ConstraintSet.range(Never, T, Base) + ~ConstraintSet.upper_bound(T, Base) # ¬(Sub ≤ T@_) - ~ConstraintSet.range(Sub, T, object) + ~ConstraintSet.lower_bound(Sub, T) # (T@_ ≠ *) ~ConstraintSet.range(Never, T, object) ``` @@ -694,7 +786,7 @@ def _[T]() -> None: ### Negation of constraints involving two variables ```py -from typing import final, Never +from typing import final from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet @@ -705,18 +797,18 @@ class Unrelated: ... def _[T, U]() -> None: # ¬(T@_ ≤ Base) ∨ ¬(U@_ ≤ Base) - ~(ConstraintSet.range(Never, T, Base) & ConstraintSet.range(Never, U, Base)) + ~(ConstraintSet.upper_bound(T, Base) & ConstraintSet.upper_bound(U, Base)) ``` The union of a constraint and its negation should always be satisfiable. ```py def _[T, U]() -> None: - c1 = ConstraintSet.range(Never, T, Base) & ConstraintSet.range(Never, U, Base) + c1 = ConstraintSet.upper_bound(T, Base) & ConstraintSet.upper_bound(U, Base) static_assert(c1 | ~c1) static_assert(~c1 | c1) - c2 = ConstraintSet.range(Unrelated, T, object) & ConstraintSet.range(Unrelated, U, object) + c2 = ConstraintSet.lower_bound(Unrelated, T) & ConstraintSet.lower_bound(Unrelated, U) static_assert(c2 | ~c2) static_assert(~c2 | c2) @@ -733,20 +825,19 @@ being constrained. The other is then the lower or upper bound of the constraint. enforce an arbitrary ordering on typevars, and always place the constraint on the "earlier" typevar. ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet def f[S, T](): # (S@f ≤ T@f) - c1 = ConstraintSet.range(Never, S, T) - c2 = ConstraintSet.range(S, T, object) + c1 = ConstraintSet.upper_bound(S, T) + c2 = ConstraintSet.lower_bound(S, T) static_assert(c1 == c2) def f[T, S](): # (S@f ≤ T@f) - c1 = ConstraintSet.range(Never, S, T) - c2 = ConstraintSet.range(S, T, object) + c1 = ConstraintSet.upper_bound(S, T) + c2 = ConstraintSet.lower_bound(S, T) static_assert(c1 == c2) ``` @@ -756,14 +847,14 @@ the constraint, and the other the bound. ```py def f[S, T](): # (S@f = T@f) - c1 = ConstraintSet.range(T, S, T) - c2 = ConstraintSet.range(S, T, S) + c1 = ConstraintSet.equality(S, T) + c2 = ConstraintSet.equality(T, S) static_assert(c1 == c2) def f[T, S](): # (S@f = T@f) - c1 = ConstraintSet.range(T, S, T) - c2 = ConstraintSet.range(S, T, S) + c1 = ConstraintSet.equality(S, T) + c2 = ConstraintSet.equality(T, S) static_assert(c1 == c2) ``` @@ -788,17 +879,16 @@ The ordering of elements in a union or intersection do not affect what types sat set. ```pyi -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet def f[T](): - c1 = ConstraintSet.range(Never, T, str | int) - c2 = ConstraintSet.range(Never, T, int | str) + c1 = ConstraintSet.upper_bound(T, str | int) + c2 = ConstraintSet.upper_bound(T, int | str) static_assert(c1 == c2) - c1 = ConstraintSet.range(Never, T, str & int) - c2 = ConstraintSet.range(Never, T, int & str) + c1 = ConstraintSet.upper_bound(T, str & int) + c2 = ConstraintSet.upper_bound(T, int & str) static_assert(c1 == c2) ``` @@ -815,15 +905,15 @@ from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet def same_typevar[T](): - constraints = ConstraintSet.range(Never, T, T) + constraints = ConstraintSet.upper_bound(T, T) expected = ConstraintSet.range(Never, T, object) static_assert(constraints == expected) - constraints = ConstraintSet.range(T, T, object) + constraints = ConstraintSet.lower_bound(T, T) expected = ConstraintSet.range(Never, T, object) static_assert(constraints == expected) - constraints = ConstraintSet.range(T, T, T) + constraints = ConstraintSet.equality(T, T) expected = ConstraintSet.range(Never, T, object) static_assert(constraints == expected) ``` @@ -834,11 +924,11 @@ as shown above.) ```pyi def same_typevar[T](): - constraints = ConstraintSet.range(Never, T, T | None) + constraints = ConstraintSet.upper_bound(T, T | None) expected = ConstraintSet.range(Never, T, object) static_assert(constraints == expected) - constraints = ConstraintSet.range(T & None, T, object) + constraints = ConstraintSet.lower_bound(T & None, T) expected = ConstraintSet.range(Never, T, object) static_assert(constraints == expected) @@ -852,39 +942,66 @@ constraint set can never be satisfied, since every type is disjoint with its neg ```pyi def same_typevar[T](): - constraints = ConstraintSet.range(~T & None, T, object) + constraints = ConstraintSet.lower_bound(~T & None, T) expected = ~ConstraintSet.range(Never, T, object) static_assert(constraints == expected) - constraints = ConstraintSet.range(~T, T, object) + constraints = ConstraintSet.lower_bound(~T, T) expected = ~ConstraintSet.range(Never, T, object) static_assert(constraints == expected) ``` +## Existential quantification + +Existential quantification removes the listed typevars from a constraint set. Any constraints that +do not involve those typevars must remain in the result. The result holds whenever _at least one_ +valid assignment to the quantified variables satisfies the expression being quantified over. + +```py +from ty_extensions import static_assert +from ty_extensions._internal import ConstraintSet + +def preserves_remaining_conjunct[T, U]() -> None: + t_int = ConstraintSet.equality(T, int) + u_str = ConstraintSet.equality(U, str) + quantified = (t_int & u_str).exists(tuple[U]) + static_assert(quantified == t_int) + +def satisfies_uncertain_disjunct[T, U]() -> None: + t_int = ConstraintSet.equality(T, int) + u_str = ConstraintSet.equality(U, str) + quantified = (t_int | u_str).exists(tuple[U]) + static_assert(quantified == ConstraintSet.always()) + +def no_typevars_is_identity[T]() -> None: + constraints = ConstraintSet.upper_bound(T, int) + static_assert(constraints.exists(tuple[()]) == constraints) +``` + ## Universal quantification Universal quantification removes the listed typevars from a constraint set. Any constraints that do -not involve those typevars must remain in the result, including constraints in an uncertain branch. +not involve those typevars must remain in the result. The result holds whenever _every_ valid +assignment to the quantified variables satisfies the expression being quantified over. ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet def preserves_uncertain_disjunct[T, U]() -> None: - t_int = ConstraintSet.range(int, T, int) - u_str = ConstraintSet.range(str, U, str) + t_int = ConstraintSet.equality(T, int) + u_str = ConstraintSet.equality(U, str) quantified = (t_int | u_str).for_all(tuple[U]) static_assert(quantified == t_int) def removes_multiple_typevars[T, U]() -> None: - t_int = ConstraintSet.range(int, T, int) - u_str = ConstraintSet.range(str, U, str) + t_int = ConstraintSet.equality(T, int) + u_str = ConstraintSet.equality(U, str) quantified = (t_int | u_str).for_all(tuple[T, U]) static_assert(quantified == ConstraintSet.never()) def no_typevars_is_identity[T]() -> None: - constraints = ConstraintSet.range(Never, T, int) + constraints = ConstraintSet.upper_bound(T, int) static_assert(constraints.for_all(tuple[()]) == constraints) ``` @@ -897,17 +1014,16 @@ from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet def quantifier_order[S, T]() -> None: - source_is_int = ConstraintSet.range(int, S, int) - target_is_int = ConstraintSet.range(int, T, int) + source_is_int = ConstraintSet.equality(S, int) + target_is_int = ConstraintSet.equality(T, int) equal = source_is_int.satisfies(target_is_int) & target_is_int.satisfies(source_is_int) - # ∀T.∃S.equal(S, T) = ∀T.¬∀S.¬equal(S, T) - forall_target_exists_source = (~((~equal).for_all(tuple[S]))).for_all(tuple[T]) + # ∀T.∃S.equal(S, T) + forall_target_exists_source = equal.exists(tuple[S]).for_all(tuple[T]) static_assert(forall_target_exists_source == ConstraintSet.always()) - # ∃S.∀T.equal(S, T) = ¬∀S.¬∀T.equal(S, T) - forall_target = equal.for_all(tuple[T]) - exists_source_forall_target = ~((~forall_target).for_all(tuple[S])) + # ∃S.∀T.equal(S, T) + exists_source_forall_target = equal.for_all(tuple[T]).exists(tuple[S]) static_assert(exists_source_forall_target == ConstraintSet.never()) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/implies_subtype_of.md b/crates/ty_python_semantic/resources/mdtest/type_properties/implies_subtype_of.md index 1cd7451c47..7ee22c47e0 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/implies_subtype_of.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/implies_subtype_of.md @@ -31,11 +31,10 @@ Moreover, for concrete types, the answer does not depend on which constraint set there isn't a valid specialization for the typevars we are considering. ```py -from typing import Never from ty_extensions._internal import ConstraintSet def even_given_constraints[T](): - constraints = ConstraintSet.range(Never, T, int) + constraints = ConstraintSet.upper_bound(T, int) static_assert(constraints.implies_subtype_of(bool, int)) static_assert(not constraints.implies_subtype_of(bool, str)) @@ -62,7 +61,7 @@ def assignability[T](): static_assert(constraints == expected) constraints = is_constraint_set_assignable_to(T, bool) - expected = ConstraintSet.range(Never, T, bool) + expected = ConstraintSet.upper_bound(T, bool) static_assert(constraints == expected) # TODO: is_assignable_to should eventually work the way is_constraint_set_assignable_to does @@ -72,7 +71,7 @@ def assignability[T](): static_assert(constraints == expected) constraints = is_constraint_set_assignable_to(T, int) - expected = ConstraintSet.range(Never, T, int) + expected = ConstraintSet.upper_bound(T, int) static_assert(constraints == expected) constraints = is_assignable_to(T, object) @@ -85,12 +84,12 @@ def assignability[T](): def subtyping[T](): constraints = is_subtype_of(T, bool) - # TODO: expected = ConstraintSet.range(Never, T, bool) + # TODO: expected = ConstraintSet.upper_bound(T, bool) expected = ConstraintSet.never() static_assert(constraints == expected) constraints = is_subtype_of(T, int) - # TODO: expected = ConstraintSet.range(Never, T, int) + # TODO: expected = ConstraintSet.upper_bound(T, int) expected = ConstraintSet.never() static_assert(constraints == expected) @@ -123,53 +122,53 @@ def assignability[T](): static_assert(constraints == expected) constraints = is_assignable_to(T, Covariant[Any]) - # TODO: expected = ConstraintSet.range(Never, T, Covariant[object]) + # TODO: expected = ConstraintSet.upper_bound(T, Covariant[object]) expected = ConstraintSet.never() static_assert(constraints == expected) constraints = is_assignable_to(Covariant[Any], T) - # TODO: expected = ConstraintSet.range(Covariant[Never], T, object) + # TODO: expected = ConstraintSet.lower_bound(Covariant[Never], T) expected = ConstraintSet.never() static_assert(constraints == expected) constraints = is_assignable_to(T, Contravariant[Any]) - # TODO: expected = ConstraintSet.range(Never, T, Contravariant[Never]) + # TODO: expected = ConstraintSet.upper_bound(T, Contravariant[Never]) expected = ConstraintSet.never() static_assert(constraints == expected) constraints = is_assignable_to(Contravariant[Any], T) - # TODO: expected = ConstraintSet.range(Contravariant[object], T, object) + # TODO: expected = ConstraintSet.lower_bound(Contravariant[object], T) expected = ConstraintSet.never() static_assert(constraints == expected) def subtyping[T](): constraints = is_subtype_of(T, Any) - # TODO: expected = ConstraintSet.range(Never, T, Never) + # TODO: expected = ConstraintSet.equality(T, Never) expected = ConstraintSet.never() static_assert(constraints == expected) constraints = is_subtype_of(Any, T) - # TODO: expected = ConstraintSet.range(object, T, object) + # TODO: expected = ConstraintSet.equality(T, object) expected = ConstraintSet.never() static_assert(constraints == expected) constraints = is_subtype_of(T, Covariant[Any]) - # TODO: expected = ConstraintSet.range(Never, T, Covariant[Never]) + # TODO: expected = ConstraintSet.upper_bound(T, Covariant[Never]) expected = ConstraintSet.never() static_assert(constraints == expected) constraints = is_subtype_of(Covariant[Any], T) - # TODO: expected = ConstraintSet.range(Covariant[object], T, object) + # TODO: expected = ConstraintSet.lower_bound(Covariant[object], T) expected = ConstraintSet.never() static_assert(constraints == expected) constraints = is_subtype_of(T, Contravariant[Any]) - # TODO: expected = ConstraintSet.range(Never, T, Contravariant[object]) + # TODO: expected = ConstraintSet.upper_bound(T, Contravariant[object]) expected = ConstraintSet.never() static_assert(constraints == expected) constraints = is_subtype_of(Contravariant[Any], T) - # TODO: expected = ConstraintSet.range(Contravariant[Never], T, object) + # TODO: expected = ConstraintSet.lower_bound(Contravariant[Never], T) expected = ConstraintSet.never() static_assert(constraints == expected) ``` @@ -193,12 +192,12 @@ def given_constraints[T](): static_assert(ConstraintSet.never().implies_subtype_of(T, bool)) static_assert(ConstraintSet.never().implies_subtype_of(T, str)) - given_int = ConstraintSet.range(Never, T, int) + given_int = ConstraintSet.upper_bound(T, int) static_assert(given_int.implies_subtype_of(T, int)) static_assert(not given_int.implies_subtype_of(T, bool)) static_assert(not given_int.implies_subtype_of(T, str)) - given_bool = ConstraintSet.range(Never, T, bool) + given_bool = ConstraintSet.upper_bound(T, bool) static_assert(given_bool.implies_subtype_of(T, int)) static_assert(given_bool.implies_subtype_of(T, bool)) static_assert(not given_bool.implies_subtype_of(T, str)) @@ -208,7 +207,7 @@ def given_constraints[T](): static_assert(given_both.implies_subtype_of(T, bool)) static_assert(not given_both.implies_subtype_of(T, str)) - given_str = ConstraintSet.range(Never, T, str) + given_str = ConstraintSet.upper_bound(T, str) static_assert(not given_str.implies_subtype_of(T, int)) static_assert(not given_str.implies_subtype_of(T, bool)) static_assert(given_str.implies_subtype_of(T, str)) @@ -222,26 +221,26 @@ BDD logic that is dependent on which variable ordering we end up with.) ```py def mutually_constrained[T, U](): # If [T = U ∧ U ≤ int], then [T ≤ int] must be true as well. - given_int = ConstraintSet.range(U, T, U) & ConstraintSet.range(Never, U, int) + given_int = ConstraintSet.equality(T, U) & ConstraintSet.upper_bound(U, int) static_assert(given_int.implies_subtype_of(T, int)) static_assert(not given_int.implies_subtype_of(T, bool)) static_assert(not given_int.implies_subtype_of(T, str)) # If [T ≤ U ∧ U ≤ int], then [T ≤ int] must be true as well. - given_int = ConstraintSet.range(Never, T, U) & ConstraintSet.range(Never, U, int) + given_int = ConstraintSet.upper_bound(T, U) & ConstraintSet.upper_bound(U, int) static_assert(given_int.implies_subtype_of(T, int)) static_assert(not given_int.implies_subtype_of(T, bool)) static_assert(not given_int.implies_subtype_of(T, str)) def mutually_constrained[U, T](): # If [T = U ∧ U ≤ int], then [T ≤ int] must be true as well. - given_int = ConstraintSet.range(U, T, U) & ConstraintSet.range(Never, U, int) + given_int = ConstraintSet.equality(T, U) & ConstraintSet.upper_bound(U, int) static_assert(given_int.implies_subtype_of(T, int)) static_assert(not given_int.implies_subtype_of(T, bool)) static_assert(not given_int.implies_subtype_of(T, str)) # If [T ≤ U ∧ U ≤ int], then [T ≤ int] must be true as well. - given_int = ConstraintSet.range(Never, T, U) & ConstraintSet.range(Never, U, int) + given_int = ConstraintSet.upper_bound(T, U) & ConstraintSet.upper_bound(U, int) static_assert(given_int.implies_subtype_of(T, int)) static_assert(not given_int.implies_subtype_of(T, bool)) static_assert(not given_int.implies_subtype_of(T, str)) @@ -252,7 +251,6 @@ def mutually_constrained[U, T](): All of the relationships in the above section also apply when a typevar appears in a compound type. ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet @@ -271,12 +269,12 @@ def given_constraints[T](): static_assert(ConstraintSet.never().implies_subtype_of(Covariant[T], Covariant[str])) # For a covariant typevar, (T ≤ int) implies that (Covariant[T] ≤ Covariant[int]). - given_int = ConstraintSet.range(Never, T, int) + given_int = ConstraintSet.upper_bound(T, int) static_assert(given_int.implies_subtype_of(Covariant[T], Covariant[int])) static_assert(not given_int.implies_subtype_of(Covariant[T], Covariant[bool])) static_assert(not given_int.implies_subtype_of(Covariant[T], Covariant[str])) - given_bool = ConstraintSet.range(Never, T, bool) + given_bool = ConstraintSet.upper_bound(T, bool) static_assert(given_bool.implies_subtype_of(Covariant[T], Covariant[int])) static_assert(given_bool.implies_subtype_of(Covariant[T], Covariant[bool])) static_assert(not given_bool.implies_subtype_of(Covariant[T], Covariant[str])) @@ -289,14 +287,14 @@ def given_constraints[T](): def mutually_constrained[T, U](): # If (T = U ∧ U ≤ int), then (T ≤ int) must be true as well, and therefore # (Covariant[T] ≤ Covariant[int]). - given_int = ConstraintSet.range(U, T, U) & ConstraintSet.range(Never, U, int) + given_int = ConstraintSet.equality(T, U) & ConstraintSet.upper_bound(U, int) static_assert(given_int.implies_subtype_of(Covariant[T], Covariant[int])) static_assert(not given_int.implies_subtype_of(Covariant[T], Covariant[bool])) static_assert(not given_int.implies_subtype_of(Covariant[T], Covariant[str])) # If (T ≤ U ∧ U ≤ int), then (T ≤ int) must be true as well, and therefore # (Covariant[T] ≤ Covariant[int]). - given_int = ConstraintSet.range(Never, T, U) & ConstraintSet.range(Never, U, int) + given_int = ConstraintSet.upper_bound(T, U) & ConstraintSet.upper_bound(U, int) static_assert(given_int.implies_subtype_of(Covariant[T], Covariant[int])) static_assert(not given_int.implies_subtype_of(Covariant[T], Covariant[bool])) static_assert(not given_int.implies_subtype_of(Covariant[T], Covariant[str])) @@ -305,14 +303,14 @@ def mutually_constrained[T, U](): def mutually_constrained[U, T](): # If (T = U ∧ U ≤ int), then (T ≤ int) must be true as well, and therefore # (Covariant[T] ≤ Covariant[int]). - given_int = ConstraintSet.range(U, T, U) & ConstraintSet.range(Never, U, int) + given_int = ConstraintSet.equality(T, U) & ConstraintSet.upper_bound(U, int) static_assert(given_int.implies_subtype_of(Covariant[T], Covariant[int])) static_assert(not given_int.implies_subtype_of(Covariant[T], Covariant[bool])) static_assert(not given_int.implies_subtype_of(Covariant[T], Covariant[str])) # If (T ≤ U ∧ U ≤ int), then (T ≤ int) must be true as well, and therefore # (Covariant[T] ≤ Covariant[int]). - given_int = ConstraintSet.range(Never, T, U) & ConstraintSet.range(Never, U, int) + given_int = ConstraintSet.upper_bound(T, U) & ConstraintSet.upper_bound(U, int) static_assert(given_int.implies_subtype_of(Covariant[T], Covariant[int])) static_assert(not given_int.implies_subtype_of(Covariant[T], Covariant[bool])) static_assert(not given_int.implies_subtype_of(Covariant[T], Covariant[str])) @@ -337,12 +335,12 @@ def given_constraints[T](): # For a contravariant typevar, (T ≤ int) implies that (Contravariant[int] ≤ Contravariant[T]). # (The order of the comparison is reversed because of contravariance.) - given_int = ConstraintSet.range(Never, T, int) + given_int = ConstraintSet.upper_bound(T, int) static_assert(given_int.implies_subtype_of(Contravariant[int], Contravariant[T])) static_assert(not given_int.implies_subtype_of(Contravariant[bool], Contravariant[T])) static_assert(not given_int.implies_subtype_of(Contravariant[str], Contravariant[T])) - given_bool = ConstraintSet.range(Never, T, int) + given_bool = ConstraintSet.upper_bound(T, int) static_assert(given_bool.implies_subtype_of(Contravariant[int], Contravariant[T])) static_assert(not given_bool.implies_subtype_of(Contravariant[bool], Contravariant[T])) static_assert(not given_bool.implies_subtype_of(Contravariant[str], Contravariant[T])) @@ -350,14 +348,14 @@ def given_constraints[T](): def mutually_constrained[T, U](): # If (T = U ∧ U ≤ int), then (T ≤ int) must be true as well, and therefore # (Contravariant[int] ≤ Contravariant[T]). - given_int = ConstraintSet.range(U, T, U) & ConstraintSet.range(Never, U, int) + given_int = ConstraintSet.equality(T, U) & ConstraintSet.upper_bound(U, int) static_assert(given_int.implies_subtype_of(Contravariant[int], Contravariant[T])) static_assert(not given_int.implies_subtype_of(Contravariant[bool], Contravariant[T])) static_assert(not given_int.implies_subtype_of(Contravariant[str], Contravariant[T])) # If (T ≤ U ∧ U ≤ int), then (T ≤ int) must be true as well, and therefore # (Contravariant[int] ≤ Contravariant[T]). - given_int = ConstraintSet.range(Never, T, U) & ConstraintSet.range(Never, U, int) + given_int = ConstraintSet.upper_bound(T, U) & ConstraintSet.upper_bound(U, int) static_assert(given_int.implies_subtype_of(Contravariant[int], Contravariant[T])) static_assert(not given_int.implies_subtype_of(Contravariant[bool], Contravariant[T])) static_assert(not given_int.implies_subtype_of(Contravariant[str], Contravariant[T])) @@ -366,14 +364,14 @@ def mutually_constrained[T, U](): def mutually_constrained[U, T](): # If (T = U ∧ U ≤ int), then (T ≤ int) must be true as well, and therefore # (Contravariant[int] ≤ Contravariant[T]). - given_int = ConstraintSet.range(U, T, U) & ConstraintSet.range(Never, U, int) + given_int = ConstraintSet.equality(T, U) & ConstraintSet.upper_bound(U, int) static_assert(given_int.implies_subtype_of(Contravariant[int], Contravariant[T])) static_assert(not given_int.implies_subtype_of(Contravariant[bool], Contravariant[T])) static_assert(not given_int.implies_subtype_of(Contravariant[str], Contravariant[T])) # If (T ≤ U ∧ U ≤ int), then (T ≤ int) must be true as well, and therefore # (Contravariant[int] ≤ Contravariant[T]). - given_int = ConstraintSet.range(Never, T, U) & ConstraintSet.range(Never, U, int) + given_int = ConstraintSet.upper_bound(T, U) & ConstraintSet.upper_bound(U, int) static_assert(given_int.implies_subtype_of(Contravariant[int], Contravariant[T])) static_assert(not given_int.implies_subtype_of(Contravariant[bool], Contravariant[T])) static_assert(not given_int.implies_subtype_of(Contravariant[str], Contravariant[T])) @@ -401,7 +399,7 @@ def given_constraints[T](): static_assert(ConstraintSet.never().implies_subtype_of(Invariant[T], Invariant[str])) # For an invariant typevar, (T ≤ int) does not imply that (Invariant[T] ≤ Invariant[int]). - given_int = ConstraintSet.range(Never, T, int) + given_int = ConstraintSet.upper_bound(T, int) static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[int])) static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[bool])) static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[str])) @@ -412,7 +410,7 @@ def given_constraints[T](): static_assert(not given_int.implies_subtype_of(Invariant[str], Invariant[T])) # But (T = int) does imply both. - given_int = ConstraintSet.range(int, T, int) + given_int = ConstraintSet.equality(T, int) static_assert(given_int.implies_subtype_of(Invariant[T], Invariant[int])) static_assert(given_int.implies_subtype_of(Invariant[int], Invariant[T])) static_assert(not given_int.implies_subtype_of(Invariant[bool], Invariant[T])) @@ -423,14 +421,14 @@ def given_constraints[T](): def mutually_constrained[T, U](): # If (T = U ∧ U ≤ int), then (T ≤ int) must be true as well. But because T is invariant, that # does _not_ imply that (Invariant[T] ≤ Invariant[int]). - given_int = ConstraintSet.range(U, T, U) & ConstraintSet.range(Never, U, int) + given_int = ConstraintSet.equality(T, U) & ConstraintSet.upper_bound(U, int) static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[int])) static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[bool])) static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[str])) # If (T = U ∧ U = int), then (T = int) must be true as well. That is an equality constraint, so # even though T is invariant, it does imply that (Invariant[T] ≤ Invariant[int]). - given_int = ConstraintSet.range(U, T, U) & ConstraintSet.range(int, U, int) + given_int = ConstraintSet.equality(T, U) & ConstraintSet.equality(U, int) static_assert(given_int.implies_subtype_of(Invariant[T], Invariant[int])) static_assert(given_int.implies_subtype_of(Invariant[int], Invariant[T])) static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[bool])) @@ -442,14 +440,14 @@ def mutually_constrained[T, U](): def mutually_constrained[U, T](): # If (T = U ∧ U ≤ int), then (T ≤ int) must be true as well. But because T is invariant, that # does _not_ imply that (Invariant[T] ≤ Invariant[int]). - given_int = ConstraintSet.range(U, T, U) & ConstraintSet.range(Never, U, int) + given_int = ConstraintSet.equality(T, U) & ConstraintSet.upper_bound(U, int) static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[int])) static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[bool])) static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[str])) # If (T = U ∧ U = int), then (T = int) must be true as well. That is an equality constraint, so # even though T is invariant, it does imply that (Invariant[T] ≤ Invariant[int]). - given_int = ConstraintSet.range(U, T, U) & ConstraintSet.range(int, U, int) + given_int = ConstraintSet.equality(T, U) & ConstraintSet.equality(U, int) static_assert(given_int.implies_subtype_of(Invariant[T], Invariant[int])) static_assert(given_int.implies_subtype_of(Invariant[int], Invariant[T])) static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[bool])) @@ -571,7 +569,7 @@ def quantifies_callable_typevars_together[V](): raise NotImplementedError actual = ConstraintSet.always().implies_subtype_of(RegularCallableTypeOf[source], RegularCallableTypeOf[target]) - expected = ConstraintSet.range(int, V, object) + expected = ConstraintSet.lower_bound(int, V) static_assert(actual == expected) ``` @@ -588,7 +586,7 @@ def listify[T](t: T) -> list[T]: return [t] def constrained_by_other_typevars[U, V]() -> None: - ok = ConstraintSet.range(bool, U, int) & ConstraintSet.range(int, V, int) + ok = ConstraintSet.range(bool, U, int) & ConstraintSet.equality(V, int) # TODO: no error # This does not depend on combining constraints from multiple call arguments. The callable # relation introduces constraints involving listify's fresh typevar and then existentially @@ -598,7 +596,7 @@ def constrained_by_other_typevars[U, V]() -> None: # error: [static-assert-error] static_assert(ok.implies_subtype_of(TypeOf[listify], Callable[[U], list[V]])) - bad = ConstraintSet.range(str, U, str) & ConstraintSet.range(int, V, int) + bad = ConstraintSet.equality(U, str) & ConstraintSet.equality(V, int) static_assert(not bad.implies_subtype_of(TypeOf[listify], Callable[[U], list[V]])) def recursive_listify[T](t: T) -> list[T]: @@ -616,40 +614,38 @@ def recursive_listify[T](t: T) -> list[T]: ### Transitivity can propagate across typevars ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet def concrete_pivot[T, U](): # If [int ≤ T ∧ T ≤ U], then [int ≤ U] must be true as well. - constraints = ConstraintSet.range(int, T, object) & ConstraintSet.range(T, U, object) + constraints = ConstraintSet.lower_bound(int, T) & ConstraintSet.lower_bound(T, U) static_assert(constraints.implies_subtype_of(int, U)) ``` ### Transitivity can propagate across fully static concrete types ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet def concrete_pivot[T, U](): # If [T ≤ int ∧ int ≤ U], then [T ≤ U] must be true as well. - constraints = ConstraintSet.range(Never, T, int) & ConstraintSet.range(int, U, object) + constraints = ConstraintSet.upper_bound(T, int) & ConstraintSet.lower_bound(int, U) static_assert(constraints.implies_subtype_of(T, U)) ``` ### Transitivity cannot propagate across non-fully-static concrete types ```py -from typing import Any, Never +from typing import Any from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet def concrete_pivot[T, U](): # If [T ≤ Any ∧ Any ≤ U], then the two `Any`s might materialize to different types. That means # [T ≤ U] is NOT necessarily true. - constraints = ConstraintSet.range(Never, T, Any) & ConstraintSet.range(Any, U, object) + constraints = ConstraintSet.upper_bound(T, Any) & ConstraintSet.lower_bound(Any, U) static_assert(not constraints.implies_subtype_of(T, U)) ``` @@ -659,7 +655,6 @@ When a typevar appears nested inside a covariant generic type in another constra propagate the bound "into" the generic type. ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet @@ -670,7 +665,7 @@ class Covariant[T]: def upper_bound[T, U](): # If (T ≤ int) ∧ (U ≤ Covariant[T]), then by covariance, Covariant[T] ≤ Covariant[int], # and by transitivity, U ≤ Covariant[int]. - constraints = ConstraintSet.range(Never, T, int) & ConstraintSet.range(Never, U, Covariant[T]) + constraints = ConstraintSet.upper_bound(T, int) & ConstraintSet.upper_bound(U, Covariant[T]) static_assert(constraints.implies_subtype_of(U, Covariant[int])) static_assert(not constraints.implies_subtype_of(U, Covariant[bool])) static_assert(not constraints.implies_subtype_of(U, Covariant[str])) @@ -678,21 +673,21 @@ def upper_bound[T, U](): def lower_bound[T, U](): # If (int ≤ T ∧ Covariant[T] ≤ U), then by covariance, Covariant[int] ≤ Covariant[T], # and by transitivity, Covariant[int] ≤ U. Since bool ≤ int, Covariant[bool] ≤ U also holds. - constraints = ConstraintSet.range(int, T, object) & ConstraintSet.range(Covariant[T], U, object) + constraints = ConstraintSet.lower_bound(int, T) & ConstraintSet.lower_bound(Covariant[T], U) static_assert(constraints.implies_subtype_of(Covariant[int], U)) static_assert(constraints.implies_subtype_of(Covariant[bool], U)) static_assert(not constraints.implies_subtype_of(Covariant[str], U)) # Repeat with reversed typevar ordering to verify BDD-ordering independence. def upper_bound[U, T](): - constraints = ConstraintSet.range(Never, T, int) & ConstraintSet.range(Never, U, Covariant[T]) + constraints = ConstraintSet.upper_bound(T, int) & ConstraintSet.upper_bound(U, Covariant[T]) static_assert(constraints.implies_subtype_of(U, Covariant[int])) static_assert(not constraints.implies_subtype_of(U, Covariant[bool])) static_assert(not constraints.implies_subtype_of(U, Covariant[str])) def lower_bound[U, T](): # Since bool ≤ int, Covariant[bool] ≤ U also holds. - constraints = ConstraintSet.range(int, T, object) & ConstraintSet.range(Covariant[T], U, object) + constraints = ConstraintSet.lower_bound(int, T) & ConstraintSet.lower_bound(Covariant[T], U) static_assert(constraints.implies_subtype_of(Covariant[int], U)) static_assert(constraints.implies_subtype_of(Covariant[bool], U)) static_assert(not constraints.implies_subtype_of(Covariant[str], U)) @@ -704,7 +699,6 @@ The previous section also works for contravariant generic types, though one of t constraints is flipped. ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet @@ -718,7 +712,7 @@ def upper_bound[T, U](): # Note: we need the *lower* bound on T (not the upper) because contravariance flips. # Since bool ≤ int, Contravariant[int] ≤ Contravariant[bool], so U ≤ Contravariant[bool] # also holds. - constraints = ConstraintSet.range(int, T, object) & ConstraintSet.range(Never, U, Contravariant[T]) + constraints = ConstraintSet.lower_bound(int, T) & ConstraintSet.upper_bound(U, Contravariant[T]) static_assert(constraints.implies_subtype_of(U, Contravariant[int])) static_assert(constraints.implies_subtype_of(U, Contravariant[bool])) static_assert(not constraints.implies_subtype_of(U, Contravariant[str])) @@ -728,20 +722,20 @@ def lower_bound[T, U](): # Contravariant[int] ≤ Contravariant[T], and by transitivity, Contravariant[int] ≤ U. # Contravariant[bool] is a supertype of Contravariant[int] (since bool ≤ int), so # Contravariant[bool] ≤ U does NOT hold. - constraints = ConstraintSet.range(Never, T, int) & ConstraintSet.range(Contravariant[T], U, object) + constraints = ConstraintSet.upper_bound(T, int) & ConstraintSet.lower_bound(Contravariant[T], U) static_assert(constraints.implies_subtype_of(Contravariant[int], U)) static_assert(not constraints.implies_subtype_of(Contravariant[bool], U)) static_assert(not constraints.implies_subtype_of(Contravariant[str], U)) # Repeat with reversed typevar ordering to verify BDD-ordering independence. def upper_bound[U, T](): - constraints = ConstraintSet.range(int, T, object) & ConstraintSet.range(Never, U, Contravariant[T]) + constraints = ConstraintSet.lower_bound(int, T) & ConstraintSet.upper_bound(U, Contravariant[T]) static_assert(constraints.implies_subtype_of(U, Contravariant[int])) static_assert(constraints.implies_subtype_of(U, Contravariant[bool])) static_assert(not constraints.implies_subtype_of(U, Contravariant[str])) def lower_bound[U, T](): - constraints = ConstraintSet.range(Never, T, int) & ConstraintSet.range(Contravariant[T], U, object) + constraints = ConstraintSet.upper_bound(T, int) & ConstraintSet.lower_bound(Contravariant[T], U) static_assert(constraints.implies_subtype_of(Contravariant[int], U)) static_assert(not constraints.implies_subtype_of(Contravariant[bool], U)) static_assert(not constraints.implies_subtype_of(Contravariant[str], U)) @@ -753,7 +747,6 @@ For invariant type parameters, only an equality constraint on the typevar allows one-sided bound (upper or lower only) is not sufficient. ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet @@ -766,7 +759,7 @@ class Invariant[T]: def equality_constraint[T, U](): # (T = int ∧ U ≤ Invariant[T]) should imply U ≤ Invariant[int]. - constraints = ConstraintSet.range(int, T, int) & ConstraintSet.range(Never, U, Invariant[T]) + constraints = ConstraintSet.equality(T, int) & ConstraintSet.upper_bound(U, Invariant[T]) static_assert(constraints.implies_subtype_of(U, Invariant[int])) static_assert(not constraints.implies_subtype_of(U, Invariant[bool])) static_assert(not constraints.implies_subtype_of(U, Invariant[str])) @@ -774,7 +767,7 @@ def equality_constraint[T, U](): def upper_bound_only[T, U](): # (T ≤ int ∧ U ≤ Invariant[T]) should NOT imply U ≤ Invariant[int], because T is invariant # and we only have an upper bound, not equality. - constraints = ConstraintSet.range(Never, T, int) & ConstraintSet.range(Never, U, Invariant[T]) + constraints = ConstraintSet.upper_bound(T, int) & ConstraintSet.upper_bound(U, Invariant[T]) static_assert(not constraints.implies_subtype_of(U, Invariant[int])) static_assert(not constraints.implies_subtype_of(U, Invariant[bool])) static_assert(not constraints.implies_subtype_of(U, Invariant[str])) @@ -782,14 +775,14 @@ def upper_bound_only[T, U](): def lower_bound_only[T, U](): # (int ≤ T ∧ Invariant[T] ≤ U) should NOT imply Invariant[int] ≤ U, because T is invariant # and we only have a lower bound, not equality. - constraints = ConstraintSet.range(int, T, object) & ConstraintSet.range(Invariant[T], U, object) + constraints = ConstraintSet.lower_bound(int, T) & ConstraintSet.lower_bound(Invariant[T], U) static_assert(not constraints.implies_subtype_of(Invariant[int], U)) static_assert(not constraints.implies_subtype_of(Invariant[bool], U)) static_assert(not constraints.implies_subtype_of(Invariant[str], U)) # Repeat with reversed typevar ordering. def equality_constraint[U, T](): - constraints = ConstraintSet.range(int, T, int) & ConstraintSet.range(Never, U, Invariant[T]) + constraints = ConstraintSet.equality(T, int) & ConstraintSet.upper_bound(U, Invariant[T]) static_assert(constraints.implies_subtype_of(U, Invariant[int])) static_assert(not constraints.implies_subtype_of(U, Invariant[bool])) static_assert(not constraints.implies_subtype_of(U, Invariant[str])) @@ -801,7 +794,6 @@ When a typevar is nested inside multiple layers of generics, variances compose. covariant type inside a contravariant type yields contravariant overall. ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet @@ -816,25 +808,25 @@ class Contravariant[T]: def covariant_of_contravariant[T, U](): # Covariant[Contravariant[T]]: T is contravariant overall (covariant × contravariant). # So a lower bound on T should propagate (flipped). - constraints = ConstraintSet.range(int, T, object) & ConstraintSet.range(Never, U, Covariant[Contravariant[T]]) + constraints = ConstraintSet.lower_bound(int, T) & ConstraintSet.upper_bound(U, Covariant[Contravariant[T]]) static_assert(constraints.implies_subtype_of(U, Covariant[Contravariant[int]])) static_assert(not constraints.implies_subtype_of(U, Covariant[Contravariant[str]])) def contravariant_of_covariant[T, U](): # Contravariant[Covariant[T]]: T is contravariant overall (contravariant × covariant). # So a lower bound on T should propagate (flipped). - constraints = ConstraintSet.range(int, T, object) & ConstraintSet.range(Never, U, Contravariant[Covariant[T]]) + constraints = ConstraintSet.lower_bound(int, T) & ConstraintSet.upper_bound(U, Contravariant[Covariant[T]]) static_assert(constraints.implies_subtype_of(U, Contravariant[Covariant[int]])) static_assert(not constraints.implies_subtype_of(U, Contravariant[Covariant[str]])) # Repeat with reversed typevar ordering. def covariant_of_contravariant[U, T](): - constraints = ConstraintSet.range(int, T, object) & ConstraintSet.range(Never, U, Covariant[Contravariant[T]]) + constraints = ConstraintSet.lower_bound(int, T) & ConstraintSet.upper_bound(U, Covariant[Contravariant[T]]) static_assert(constraints.implies_subtype_of(U, Covariant[Contravariant[int]])) static_assert(not constraints.implies_subtype_of(U, Covariant[Contravariant[str]])) def contravariant_of_covariant[U, T](): - constraints = ConstraintSet.range(int, T, object) & ConstraintSet.range(Never, U, Contravariant[Covariant[T]]) + constraints = ConstraintSet.lower_bound(int, T) & ConstraintSet.upper_bound(U, Contravariant[Covariant[T]]) static_assert(constraints.implies_subtype_of(U, Contravariant[Covariant[int]])) static_assert(not constraints.implies_subtype_of(U, Contravariant[Covariant[str]])) ``` @@ -851,7 +843,6 @@ For example, `(Covariant[S] ≤ C) ∧ (S ≤ B)` should imply `Covariant[B] ≤ typevars.) ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet @@ -873,42 +864,42 @@ class Invariant[T]: def covariant_upper_bound_into_lower[S, B, C](): # (Covariant[S] ≤ C) ∧ (B ≤ S) → (Covariant[B] ≤ C) # B ≤ S, so Covariant[B] ≤ Covariant[S], and Covariant[S] ≤ C gives Covariant[B] ≤ C. - constraints = ConstraintSet.range(Covariant[S], C, object) & ConstraintSet.range(Never, B, S) + constraints = ConstraintSet.lower_bound(Covariant[S], C) & ConstraintSet.upper_bound(B, S) static_assert(constraints.implies_subtype_of(Covariant[B], C)) def covariant_lower_bound_into_upper[S, B, C](): # (C ≤ Covariant[S]) ∧ (S ≤ B) → (C ≤ Covariant[B]) # S ≤ B, so Covariant[S] ≤ Covariant[B], and C ≤ Covariant[S] ≤ Covariant[B]. - constraints = ConstraintSet.range(Never, C, Covariant[S]) & ConstraintSet.range(S, B, object) + constraints = ConstraintSet.upper_bound(C, Covariant[S]) & ConstraintSet.lower_bound(S, B) static_assert(constraints.implies_subtype_of(C, Covariant[B])) def contravariant_upper_bound_into_lower[S, B, C](): # (Contravariant[S] ≤ C) ∧ (S ≤ B) → (Contravariant[B] ≤ C) # S ≤ B gives Contravariant[B] ≤ Contravariant[S], so Contravariant[B] ≤ Contravariant[S] ≤ C. - constraints = ConstraintSet.range(Contravariant[S], C, object) & ConstraintSet.range(S, B, object) + constraints = ConstraintSet.lower_bound(Contravariant[S], C) & ConstraintSet.lower_bound(S, B) static_assert(constraints.implies_subtype_of(Contravariant[B], C)) def contravariant_lower_bound_into_upper[S, B, C](): # (C ≤ Contravariant[S]) ∧ (B ≤ S) → (C ≤ Contravariant[B]) # B ≤ S gives Contravariant[S] ≤ Contravariant[B], so C ≤ Contravariant[S] ≤ Contravariant[B]. - constraints = ConstraintSet.range(Never, C, Contravariant[S]) & ConstraintSet.range(Never, B, S) + constraints = ConstraintSet.upper_bound(C, Contravariant[S]) & ConstraintSet.upper_bound(B, S) static_assert(constraints.implies_subtype_of(C, Contravariant[B])) # Repeat with reversed typevar ordering. def covariant_upper_bound_into_lower[C, B, S](): - constraints = ConstraintSet.range(Covariant[S], C, object) & ConstraintSet.range(Never, B, S) + constraints = ConstraintSet.lower_bound(Covariant[S], C) & ConstraintSet.upper_bound(B, S) static_assert(constraints.implies_subtype_of(Covariant[B], C)) def covariant_lower_bound_into_upper[C, B, S](): - constraints = ConstraintSet.range(Never, C, Covariant[S]) & ConstraintSet.range(S, B, object) + constraints = ConstraintSet.upper_bound(C, Covariant[S]) & ConstraintSet.lower_bound(S, B) static_assert(constraints.implies_subtype_of(C, Covariant[B])) def contravariant_upper_bound_into_lower[C, B, S](): - constraints = ConstraintSet.range(Contravariant[S], C, object) & ConstraintSet.range(S, B, object) + constraints = ConstraintSet.lower_bound(Contravariant[S], C) & ConstraintSet.lower_bound(S, B) static_assert(constraints.implies_subtype_of(Contravariant[B], C)) def contravariant_lower_bound_into_upper[C, B, S](): - constraints = ConstraintSet.range(Never, C, Contravariant[S]) & ConstraintSet.range(Never, B, S) + constraints = ConstraintSet.upper_bound(C, Contravariant[S]) & ConstraintSet.upper_bound(B, S) static_assert(constraints.implies_subtype_of(C, Contravariant[B])) ``` @@ -919,7 +910,6 @@ When B's bound _contains_ a typevar (but is not a bare typevar), the same logic TODO: This is not implemented yet, since it requires different detection machinery. ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet @@ -929,14 +919,14 @@ class Covariant[T]: def upper_bound_into_lower[B, C](): # (Covariant[int] ≤ C) ∧ (B ≤ int) → (Covariant[B] ≤ C) - constraints = ConstraintSet.range(Covariant[int], C, object) & ConstraintSet.range(Never, B, int) + constraints = ConstraintSet.lower_bound(Covariant[int], C) & ConstraintSet.upper_bound(B, int) # TODO: no error # error: [static-assert-error] static_assert(constraints.implies_subtype_of(Covariant[B], C)) def lower_bound_into_upper[B, C](): # (C ≤ Covariant[int]) ∧ (int ≤ B) → (C ≤ Covariant[B]) - constraints = ConstraintSet.range(Never, C, Covariant[int]) & ConstraintSet.range(int, B, object) + constraints = ConstraintSet.upper_bound(C, Covariant[int]) & ConstraintSet.lower_bound(int, B) # TODO: no error # error: [static-assert-error] static_assert(constraints.implies_subtype_of(C, Covariant[B])) @@ -945,7 +935,6 @@ def lower_bound_into_upper[B, C](): ### Nested typevar propagation also works when the replacement is a bare typevar ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet @@ -966,39 +955,39 @@ class Invariant[T]: def covariant_upper[B, S, U](): # (B ≤ S) ∧ (U ≤ Covariant[B]) -> (U ≤ Covariant[S]) - constraints = ConstraintSet.range(Never, B, S) & ConstraintSet.range(Never, U, Covariant[B]) + constraints = ConstraintSet.upper_bound(B, S) & ConstraintSet.upper_bound(U, Covariant[B]) static_assert(constraints.implies_subtype_of(U, Covariant[S])) def covariant_lower[B, S, U](): # (S ≤ B) ∧ (Covariant[B] ≤ U) -> (Covariant[S] ≤ U) - constraints = ConstraintSet.range(S, B, object) & ConstraintSet.range(Covariant[B], U, object) + constraints = ConstraintSet.lower_bound(S, B) & ConstraintSet.lower_bound(Covariant[B], U) static_assert(constraints.implies_subtype_of(Covariant[S], U)) def contravariant_upper[B, S, U](): # (S ≤ B) ∧ (U ≤ Contravariant[B]) -> (U ≤ Contravariant[S]) - constraints = ConstraintSet.range(S, B, object) & ConstraintSet.range(Never, U, Contravariant[B]) + constraints = ConstraintSet.lower_bound(S, B) & ConstraintSet.upper_bound(U, Contravariant[B]) static_assert(constraints.implies_subtype_of(U, Contravariant[S])) def contravariant_lower[B, S, U](): # (B ≤ S) ∧ (Contravariant[B] ≤ U) -> (Contravariant[S] ≤ U) - constraints = ConstraintSet.range(Never, B, S) & ConstraintSet.range(Contravariant[B], U, object) + constraints = ConstraintSet.upper_bound(B, S) & ConstraintSet.lower_bound(Contravariant[B], U) static_assert(constraints.implies_subtype_of(Contravariant[S], U)) def invariant_upper_requires_equality[B, S, U](): # Invariant replacement only holds under equality constraints on B. - constraints = ConstraintSet.range(S, B, S) & ConstraintSet.range(Never, U, Invariant[B]) + constraints = ConstraintSet.equality(B, S) & ConstraintSet.upper_bound(U, Invariant[B]) static_assert(constraints.implies_subtype_of(U, Invariant[S])) def invariant_lower_requires_equality[B, S, U](): - constraints = ConstraintSet.range(S, B, S) & ConstraintSet.range(Invariant[B], U, object) + constraints = ConstraintSet.equality(B, S) & ConstraintSet.lower_bound(Invariant[B], U) static_assert(constraints.implies_subtype_of(Invariant[S], U)) def invariant_upper_one_sided_is_not_enough[B, S, U](): - constraints = ConstraintSet.range(Never, B, S) & ConstraintSet.range(Never, U, Invariant[B]) + constraints = ConstraintSet.upper_bound(B, S) & ConstraintSet.upper_bound(U, Invariant[B]) static_assert(not constraints.implies_subtype_of(U, Invariant[S])) def invariant_lower_one_sided_is_not_enough[B, S, U](): - constraints = ConstraintSet.range(S, B, object) & ConstraintSet.range(Invariant[B], U, object) + constraints = ConstraintSet.lower_bound(S, B) & ConstraintSet.lower_bound(Invariant[B], U) static_assert(not constraints.implies_subtype_of(Invariant[S], U)) ``` @@ -1010,7 +999,6 @@ can decompose the bounds to extract constraints on the nested typevar. For insta `Covariant[int] ≤ Covariant[T]` requires `int ≤ T`. ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet @@ -1091,14 +1079,14 @@ def subclass_lower_bound[T, A](): ### Transitivity should not introduce impossible constraints ```py -from typing import Never, TypeVar, Union +from typing import TypeVar, Union from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet def impossible_result[A, T, U](): constraint_a = ConstraintSet.range(int, A, Union[T, U]) - constraint_t = ConstraintSet.range(Never, T, str) - constraint_u = ConstraintSet.range(Never, U, bytes) + constraint_t = ConstraintSet.upper_bound(T, str) + constraint_u = ConstraintSet.upper_bound(U, bytes) # Given (int ≤ A ≤ T | U), we can infer that (int ≤ T) ∨ (int ≤ U). If we intersect that with # (T ≤ str), we get false ∨ (int ≤ U) — that is, there is no valid solution for T. Therefore A 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 index 1f5f3a80ce..061f8285ca 100644 --- 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 @@ -45,8 +45,8 @@ static_assert(not is_assignable_to(Child1, Child2)) The dynamic type is assignable to or from any type. ```py -from ty_extensions import static_assert, Unknown -from ty_extensions._internal import is_assignable_to +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_assignable_to from typing import Any, Literal static_assert(is_assignable_to(Unknown, Literal[1])) @@ -208,8 +208,8 @@ Both `TypeOf[str]` and `type[str]` are subtypes of `type` and `type[object]`, wh is known to be no larger than the set of possible objects represented by `type`. ```py -from ty_extensions import static_assert, Unknown -from ty_extensions._internal import TypeOf, is_assignable_to +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, TypeOf, is_assignable_to from typing import Any static_assert(is_assignable_to(type, type)) @@ -687,8 +687,8 @@ static_assert(not is_assignable_to(tuple[int, *tuple[int, ...], int], tuple[int, ## Union types ```py -from ty_extensions import AlwaysTruthy, AlwaysFalsy, static_assert, Unknown -from ty_extensions._internal import is_assignable_to +from ty_extensions import AlwaysTruthy, AlwaysFalsy, static_assert +from ty_extensions._internal import Unknown, is_assignable_to from typing_extensions import Literal, Any, LiteralString static_assert(is_assignable_to(int, int | str)) @@ -813,8 +813,8 @@ The root cause was that we failed to properly materialize a `Callable[..., Unkno `Unknown` return type originated from a missing annotation. ```pyi -from ty_extensions import static_assert, Unknown -from ty_extensions._internal import RegularCallableTypeOf, is_assignable_to +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, RegularCallableTypeOf, is_assignable_to from typing import Callable # `Callable[..., Unknown]` has explicit Unknown return type @@ -906,8 +906,8 @@ See also: our property tests in `property_tests.rs`. `object` is Python's top type; the set of all possible objects at runtime: ```py -from ty_extensions import static_assert, Unknown -from ty_extensions._internal import is_assignable_to +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_assignable_to from typing import Literal, Any static_assert(is_assignable_to(str, object)) @@ -927,8 +927,8 @@ static_assert(is_assignable_to(type[Any], object)) any type is assignable to them: ```py -from ty_extensions import static_assert, Unknown -from ty_extensions._internal import is_assignable_to +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_assignable_to from typing import Literal, Any static_assert(is_assignable_to(str, Any)) @@ -958,8 +958,8 @@ static_assert(is_assignable_to(type[Any], Unknown)) assignable to any arbitrary type. ```py -from ty_extensions import static_assert, Unknown -from ty_extensions._internal import is_assignable_to +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_assignable_to from typing_extensions import Never, Any, Literal static_assert(is_assignable_to(Never, str)) @@ -979,8 +979,8 @@ static_assert(is_assignable_to(Never, type[Any])) including `Never`. ```pyi -from ty_extensions import static_assert, Unknown -from ty_extensions._internal import is_assignable_to +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_assignable_to from typing_extensions import Never, Any static_assert(is_assignable_to(Any, Never)) @@ -1001,8 +1001,8 @@ are covered in the [subtyping tests](./is_subtype_of.md#callable). ### Return type ```py -from ty_extensions import Unknown, static_assert -from ty_extensions._internal import RegularCallableTypeOf, is_assignable_to +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, RegularCallableTypeOf, is_assignable_to from typing import Any, Callable static_assert(is_assignable_to(Callable[[], Any], Callable[[], int])) @@ -1077,6 +1077,201 @@ static_assert(is_assignable_to(RegularCallableTypeOf[keyword_variadic], Callable static_assert(is_assignable_to(RegularCallableTypeOf[mixed], Callable[..., None])) ``` +### Unpacked positional parameters with a required suffix + +A variadic positional parameter can accept both the unpacked tuple and a required positional +parameter following that tuple. + +```py +from typing import Any, Callable, Never, Unpack, cast +from ty_extensions import static_assert +from ty_extensions._internal import RegularCallableTypeOf, is_assignable_to + +def expects_suffix(callback: Callable[[Unpack[tuple[str, ...]], None], None]) -> None: ... +def accepts_unknown(*args): ... + +expects_suffix(accepts_unknown) +``` + +The variadic parameter's annotation must be compatible with the unpacked elements and the required +suffix. + +```py +def accepts_objects(*args: object) -> None: ... +def accepts_strings_or_none(*args: str | None) -> None: ... +def accepts_strings(*args: str) -> None: ... + +expects_suffix(accepts_objects) +expects_suffix(accepts_strings_or_none) +expects_suffix(accepts_strings) # error: [invalid-argument-type] + +static_assert( + is_assignable_to( + RegularCallableTypeOf[accepts_objects], + Callable[[Unpack[tuple[str, ...]], None], None], + ) +) +static_assert( + is_assignable_to( + RegularCallableTypeOf[accepts_strings_or_none], + Callable[[Unpack[tuple[str, ...]], None], None], + ) +) +static_assert( + not is_assignable_to( + RegularCallableTypeOf[accepts_strings], + Callable[[Unpack[tuple[str, ...]], None], None], + ) +) +``` + +A required keyword-only parameter cannot be supplied by the positional callback signature. + +```py +def requires_keyword(*args: object, value: int) -> None: ... + +expects_suffix(requires_keyword) # error: [invalid-argument-type] +``` + +A required positional prefix does not prevent the source variadic parameter from also accepting the +target's required suffix. + +```py +def expects_prefix_and_suffix( + callback: Callable[[int, Unpack[tuple[str, ...]], None], None], +) -> None: ... +def accepts_prefixed_objects(first: int, *args: object) -> None: ... + +expects_prefix_and_suffix(accepts_prefixed_objects) +``` + +A required suffix can align with a longer suffix or an equivalent positional prefix when all the +unpacked elements have the same type. + +```py +def requires_one_integer(*args: *tuple[*tuple[int, ...], int]) -> None: ... + +longer_suffix: Callable[[*tuple[int, ...], int, int], None] = requires_one_integer +equivalent_prefix: Callable[[int, *tuple[int, ...]], None] = requires_one_integer + +type OneOrMoreIntegers = RegularCallableTypeOf[requires_one_integer] + +static_assert(is_assignable_to(OneOrMoreIntegers, Callable[[*tuple[int, ...], int, int], None])) +static_assert(is_assignable_to(OneOrMoreIntegers, Callable[[int, *tuple[int, ...]], None])) +``` + +A type alias for the variadic element does not prevent the required suffix from matching. + +```py +type Integer = int + +def requires_one_aliased_integer(*args: *tuple[*tuple[Integer, ...], int]) -> None: ... + +type AliasedIntegers = RegularCallableTypeOf[requires_one_aliased_integer] + +static_assert(is_assignable_to(AliasedIntegers, Callable[[int, *tuple[int, ...]], None])) +``` + +A longer suffix is aligned from the end when its other elements fit the source variadic parameter. + +```py +def requires_string_suffix(*args: *tuple[*tuple[object, ...], str]) -> None: ... +def requires_string_after_integers(*args: *tuple[*tuple[int, ...], str]) -> None: ... + +type StringSuffix = RegularCallableTypeOf[requires_string_suffix] +type IntegerStringSuffix = RegularCallableTypeOf[requires_string_after_integers] + +static_assert(is_assignable_to(StringSuffix, Callable[[*tuple[object, ...], int, str], None])) +static_assert(is_assignable_to(IntegerStringSuffix, Callable[[*tuple[int, ...], int, str], None])) +``` + +Gradual variadic elements remain assignable in both directions. + +```py +type GradualSuffix = Callable[[*tuple[Any, ...], int], None] + +static_assert(is_assignable_to(OneOrMoreIntegers, GradualSuffix)) +static_assert(is_assignable_to(GradualSuffix, OneOrMoreIntegers)) +``` + +A positional parameter cannot also be filled by a target keyword argument. + +```py +def occupies_keyword(a: int, *args: int, **kwargs: int) -> None: ... +def accepts_keyword(*args: *tuple[*tuple[int, ...], int], **kwargs: int) -> None: ... + +type OccupiesKeyword = RegularCallableTypeOf[occupies_keyword] +type AcceptsKeyword = RegularCallableTypeOf[accepts_keyword] + +static_assert(not is_assignable_to(OccupiesKeyword, AcceptsKeyword)) +``` + +An uninhabited keyword parameter cannot collide with an occupied positional parameter. + +```py +type Bottom = Never + +def rejects_keywords(*args: *tuple[*tuple[int, ...], int], **kwargs: Bottom) -> None: ... +def rejects_named_keyword(*args: *tuple[*tuple[int, ...], int], a: Never = cast(Never, 0)) -> None: ... + +static_assert(is_assignable_to(OccupiesKeyword, RegularCallableTypeOf[rejects_keywords])) +static_assert(is_assignable_to(OccupiesKeyword, RegularCallableTypeOf[rejects_named_keyword])) +``` + +A suffix cannot be extended with elements that the source variadic parameter rejects. + +```py +# error: [invalid-assignment] +incompatible_suffix: Callable[[*tuple[int, ...], str, str], None] = requires_string_after_integers +``` + +### Fixed-length unpacked positional parameters + +An unpacked fixed-length tuple accepts exactly its declared positional arguments, including when the +tuple is empty. Equivalent unpacked source and target tuples are compatible. + +```py +from typing import Callable, Unpack +from ty_extensions import static_assert +from ty_extensions._internal import RegularCallableTypeOf, is_assignable_to + +def accepts_no_arguments(*args: Unpack[tuple[()]]) -> None: ... +def accepts_one_integer(*args: Unpack[tuple[int]]) -> None: ... +def accepts_strings(*args: str) -> None: ... + +empty_callback: Callable[[Unpack[tuple[()]]], None] = accepts_no_arguments +fixed_callback: Callable[[Unpack[tuple[int]]], None] = accepts_one_integer +fixed_strings: Callable[[Unpack[tuple[str, str]]], None] = accepts_strings +empty_strings: Callable[[Unpack[tuple[()]]], None] = accepts_strings + +static_assert(is_assignable_to(RegularCallableTypeOf[accepts_no_arguments], Callable[[Unpack[tuple[()]]], None])) +static_assert(is_assignable_to(RegularCallableTypeOf[accepts_one_integer], Callable[[Unpack[tuple[int]]], None])) +``` + +Empty and exhausted fixed-length source tuples cannot satisfy a target with additional positional +arguments or an open-ended variadic parameter. + +```py +# error: [invalid-assignment] +empty_with_prefix: Callable[[int, Unpack[tuple[str, ...]], None], None] = accepts_no_arguments + +# error: [invalid-assignment] +empty_with_suffix: Callable[[Unpack[tuple[str, ...]], None], None] = accepts_no_arguments + +# error: [invalid-assignment] +exhausted_with_suffix: Callable[[int, Unpack[tuple[str, ...]], None], None] = accepts_one_integer + +# error: [invalid-assignment] +callback: Callable[[Unpack[tuple[tuple[int], ...]], tuple[int]], None] = accepts_one_integer + +static_assert( + not is_assignable_to( + RegularCallableTypeOf[accepts_one_integer], + Callable[[Unpack[tuple[tuple[int], ...]], tuple[int]], None], + ) +) +``` + ### Function types ```py @@ -1146,6 +1341,32 @@ c: Callable[[Any], str] = A().f c: Callable[[Any], str] = A().g ``` +### Generic method types with gradual class return types + +A generic receiver makes signature comparison lazy without changing whether gradual class types are +assignable. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Any, Callable +from ty_extensions._internal import Unknown + +class C: + def concrete[T](self: T) -> type[int]: + return int + + def gradual[T](self: T) -> type[Any]: + return int + +accepts_any: Callable[[], type[Any]] = C().concrete +accepts_unknown: Callable[[], type[Unknown]] = C().concrete +accepts_concrete: Callable[[], type[int]] = C().gradual +``` + ### Class literal types ```py @@ -1374,7 +1595,13 @@ the generic callable.) ```py from typing import Callable, Self from ty_extensions import static_assert -from ty_extensions._internal import RegularCallableTypeOf, TypeOf, is_assignable_to +from ty_extensions._internal import ( + ConstraintSet, + RegularCallableTypeOf, + TypeOf, + is_assignable_to, + is_constraint_set_assignable_to, +) def identity[T](t: T) -> T: return t @@ -1466,6 +1693,20 @@ static_assert( ) ``` +A constraint-producing comparison must keep an enclosing class variable symbolic while solving the +surrounding callable's return variable: + +```py +class OuterCarrier[A_outer]: + def method(self) -> A_outer: + raise NotImplementedError + + def check[R](self) -> None: + actual = is_constraint_set_assignable_to(RegularCallableTypeOf[OuterCarrier[A_outer].method], Callable[..., R]) + expected = ConstraintSet.lower_bound(A_outer, R) + static_assert(actual == expected) +``` + The reverse is not true — if someone expects a generic function that can be called with any specialization, we cannot hand them a function that only works with one specialization. @@ -1576,8 +1817,8 @@ static_assert(not is_assignable_to(TypeOf[GenericFinalClass[str]], type[GenericF `TypeGuard[...]` and `TypeIs[...]` are always assignable to `bool`. ```py -from ty_extensions import Unknown, static_assert -from ty_extensions._internal import is_assignable_to +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_assignable_to from typing_extensions import Any, TypeGuard, TypeIs static_assert(is_assignable_to(TypeGuard[Unknown], bool)) @@ -1628,8 +1869,8 @@ takes_plugin_predicate(callable) ## `ParamSpec` ```py -from ty_extensions import static_assert, Unknown -from ty_extensions._internal import TypeOf, is_assignable_to +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, TypeOf, is_assignable_to from typing import ParamSpec, Mapping, Callable, Any P = ParamSpec("P") 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 index 3d89c97a54..e7e847615f 100644 --- 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 @@ -1,69 +1,33 @@ # 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. +Two types `S` and `T` are disjoint if they have no overlap; that is, their intersection `S & T` is +empty (equivalent to `Never`). ## Basic builtin types +For basic builtin types, disjointness simply means that no runtime object can inhabit both types. + ```pyi -from typing_extensions import Literal, LiteralString, Any +from typing_extensions import LiteralString from ty_extensions import static_assert -from ty_extensions._internal import TypeOf, is_disjoint_from +from ty_extensions._internal import is_disjoint_from +# No object can be both a `bool` and a `str`. static_assert(is_disjoint_from(bool, str)) + +# But the same object can be a `bool`, an `int`, and an `object`. 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, ~Any)) - static_assert(not is_disjoint_from(LiteralString, LiteralString)) static_assert(not is_disjoint_from(str, LiteralString)) ``` -## Statically empty and non-empty ranges - -```py -from ty_extensions import static_assert -from ty_extensions._internal import TypeOf, is_disjoint_from - -static_assert(is_disjoint_from(TypeOf[range(0)], TypeOf[range(1)])) -static_assert(is_disjoint_from(TypeOf[range(1)], TypeOf[range(0)])) -static_assert(not is_disjoint_from(TypeOf[range(0)], range)) -static_assert(not is_disjoint_from(TypeOf[range(1)], range)) -``` - -## Enum complements - -```pyi -from enum import Enum -from typing import Literal -from ty_extensions import static_assert -from ty_extensions._internal import is_disjoint_from - -class Color(Enum): - RED = 1 - GREEN = 2 - BLUE = 3 - -static_assert( - is_disjoint_from( - Color & ~Literal[Color.RED], - Color & ~Literal[Color.GREEN, Color.BLUE], - ) -) -static_assert( - is_disjoint_from( - Color & ~Literal[Color.GREEN, Color.BLUE], - Color & ~Literal[Color.RED], - ) -) -``` - ## Class hierarchies +Classes overlap through a common subclass unless finality or incompatible metaclasses prevent it. + ```pyi from ty_extensions import static_assert from ty_extensions._internal import is_disjoint_from, is_subtype_of @@ -112,7 +76,7 @@ static_assert(is_disjoint_from(UsesMeta1, UsesMeta2)) ## `@final` builtin types -Some builtins types are declared as `@final`: +Some builtin types are declared as `@final`: ```py from ty_extensions import static_assert @@ -129,123 +93,18 @@ static_assert(is_disjoint_from(memoryview, Foo)) static_assert(is_disjoint_from(type[memoryview], type[Foo])) ``` -## Specialized `@final` types - -```toml -[environment] -python-version = "3.12" -``` - -```py -from typing import Any, final -from ty_extensions import static_assert -from ty_extensions._internal import is_disjoint_from - -@final -class Foo[T]: - def get(self) -> T: - raise NotImplementedError - -class A: ... -class B: ... - -static_assert(not is_disjoint_from(A, B)) -static_assert(not is_disjoint_from(Foo[A], Foo[B])) -static_assert(not is_disjoint_from(Foo[A], Foo[Any])) -static_assert(not is_disjoint_from(Foo[Any], Foo[B])) - -# `Foo[Never]` is a subtype of both `Foo[int]` and `Foo[str]`. -static_assert(not is_disjoint_from(Foo[int], Foo[str])) -``` - -## Invariant generic specializations and bases +## Gradual types -Only incompatible invariant generic arguments imply disjointness. Covariant generic arguments do -not: a covariant container can be inhabited by an empty value. +Gradual types are not disjoint if any possible materialization is not disjoint. ```pyi -from collections.abc import Sequence -from typing import Any, Generic, TypeVar -from ty_extensions import static_assert -from ty_extensions._internal import is_disjoint_from - -T = TypeVar("T") -U = TypeVar("U") -T_co = TypeVar("T_co", covariant=True) - -class A: ... -class B: ... - -class Invariant(Generic[T]): - x: T - -class InvariantPair(Generic[T, U]): - x: T - y: U - -class Covariant(Generic[T_co]): - def get(self) -> T_co: - raise NotImplementedError() - -class InvSubA(Invariant[A]): - pass - -class CoSubB(Covariant[B]): - pass - -static_assert(is_disjoint_from(Invariant[A], Invariant[B])) -static_assert(is_disjoint_from(InvSubA, Invariant[B])) -static_assert(not is_disjoint_from(Invariant[A], Invariant[A])) -static_assert(not is_disjoint_from(Invariant[Any], Invariant[B])) -static_assert(not is_disjoint_from(Invariant[B], Invariant[Any])) -# `A | Any` cannot materialize to be equivalent to `B`. -static_assert(is_disjoint_from(Invariant[A | Any], Invariant[B])) -static_assert(is_disjoint_from(Invariant[B], Invariant[A | Any])) -static_assert(is_disjoint_from(Invariant[A & Any], Invariant[B])) -static_assert(is_disjoint_from(Invariant[B], Invariant[A & Any])) -static_assert(is_disjoint_from(InvariantPair[A, A], InvariantPair[A, B])) -static_assert(not is_disjoint_from(Covariant[A], Covariant[B])) -static_assert(not is_disjoint_from(Covariant[A], CoSubB)) -static_assert(not is_disjoint_from(Sequence[int], Sequence[str])) -``` - -## Type-variable aliases and empty invariant arguments - -```toml -[environment] -python-version = "3.12" -``` - -```py -from typing import Generic, Never, TypeVar +from typing import Any from ty_extensions import static_assert from ty_extensions._internal import is_disjoint_from -T = TypeVar("T") - -class Invariant(Generic[T]): - x: T - -type Id[V] = V - -def _[U](): - static_assert(not is_disjoint_from(Invariant[U], Invariant[int])) - static_assert(not is_disjoint_from(Invariant[Id[U]], Invariant[int])) - -static_assert(not is_disjoint_from(Invariant[Id[int]], Invariant[int])) -static_assert(is_disjoint_from(Invariant[Id[int]], Invariant[str])) - -class Mixed[T, U]: - x: T - -# `Mixed` is bivariant in `U`, so the differing second argument cannot make these disjoint. -static_assert(not is_disjoint_from(Mixed[Never, int], Mixed[Never, str])) - -class Left(Invariant[Never]): ... -class Right(Invariant[Never]): ... -class Both(Left, Right): ... - -static_assert(not is_disjoint_from(Left, Right)) +static_assert(not is_disjoint_from(Any, bool)) +static_assert(not is_disjoint_from(Any, Any)) +static_assert(not is_disjoint_from(Any, ~Any)) ``` ## "Disjoint base" builtin types @@ -337,6 +196,8 @@ static_assert(not is_disjoint_from(D, A)) ## Dataclasses +Dataclasses with incompatible non-empty slots are disjoint; those with empty slots can overlap. + ```py from dataclasses import dataclass from ty_extensions import static_assert @@ -369,6 +230,8 @@ static_assert(is_disjoint_from(I, J)) ## Tuple types +Tuple types are disjoint when their lengths or corresponding element types cannot overlap. + ```py from typing_extensions import Literal, Never from ty_extensions import static_assert @@ -396,6 +259,8 @@ static_assert(is_disjoint_from(tuple[int, int], tuple[None, ...])) # error: [st ## Unions +A union is disjoint from another type when none of its alternatives overlap that type. + ```py from typing_extensions import Literal from ty_extensions import static_assert @@ -410,6 +275,8 @@ static_assert(not is_disjoint_from(Literal[1, 2], Literal[2, 3])) ## Intersections +Positive requirements and negations can make an intersection disjoint from another type. + ```pyi from typing_extensions import Literal, final, Any, LiteralString from ty_extensions import static_assert, AlwaysFalsy @@ -475,6 +342,8 @@ static_assert(is_disjoint_from(AlwaysFalsy, LiteralString & ~Literal[""])) # er ## Special types +Some typing constructs and precisely described runtime values have their own disjointness rules. + ### `Never` `Never` is disjoint from every type, including itself. @@ -492,6 +361,8 @@ static_assert(is_disjoint_from(Never, object)) ### `None` +`None` overlaps only with types that can contain the `None` object. + ```pyi from typing_extensions import Literal, LiteralString from ty_extensions import static_assert @@ -515,6 +386,8 @@ static_assert(is_disjoint_from(None, int & ~str)) ### Literals +Literal types are disjoint when their values or runtime types cannot overlap. + ```pyi from typing_extensions import Literal, LiteralString from ty_extensions import static_assert, AlwaysFalsy, AlwaysTruthy @@ -577,6 +450,8 @@ static_assert(is_disjoint_from(LiteralString & ~AlwaysFalsy, ~LiteralString | Al ### Class, module and function literals +Class, module, and function literal types for distinct runtime objects are disjoint. + ```toml [environment] python-version = "3.12" @@ -624,6 +499,8 @@ static_assert(not is_disjoint_from(TypeOf[f], object)) ### Bound methods +Bound methods are disjoint when their names or possible receiver types cannot overlap. + ```py from typing import final from ty_extensions import static_assert @@ -708,6 +585,8 @@ static_assert(not is_disjoint_from(TypeOf[F().foo], TypeOf[G().foo])) ### `AlwaysTruthy` and `AlwaysFalsy` +`AlwaysTruthy` and `AlwaysFalsy` are disjoint from types with incompatible truthiness. + ```py from ty_extensions import AlwaysFalsy, AlwaysTruthy, static_assert from ty_extensions._internal import is_disjoint_from @@ -783,6 +662,9 @@ static_assert(is_disjoint_from(type[UsesMeta1], type[UsesMeta2])) ### `property` +Property descriptors and property-bearing classes are disjoint from incompatible final classes or +protocol requirements. + ```py from ty_extensions import static_assert from ty_extensions._internal import TypeOf, is_disjoint_from @@ -832,6 +714,8 @@ static_assert(is_disjoint_from(HasReadWriteIntProp, E)) ### `TypeGuard` and `TypeIs` +`TypeGuard` and `TypeIs` represent boolean return values, so they overlap `bool` but not `str`. + ```py from ty_extensions import static_assert from ty_extensions._internal import is_disjoint_from @@ -908,6 +792,8 @@ static_assert(is_disjoint_from(type[Foo], BarNone)) ### `NamedTuple` +`NamedTuple`s overlap matching tuple shapes, but not different lengths or distinct final classes. + ```py from __future__ import annotations @@ -932,49 +818,6 @@ static_assert(is_disjoint_from(Path, tuple[Path | None, str, int])) static_assert(is_disjoint_from(Path, Path2)) ``` -## Generic aliases - -```toml -[environment] -python-version = "3.12" -``` - -```py -from typing import final -from ty_extensions import static_assert -from ty_extensions._internal import TypeOf, is_disjoint_from - -class GenericClass[T]: - x: T # invariant - -static_assert(not is_disjoint_from(TypeOf[GenericClass], type[GenericClass])) # error: [missing-type-argument] -static_assert(not is_disjoint_from(TypeOf[GenericClass[int]], type[GenericClass])) # error: [missing-type-argument] -static_assert(not is_disjoint_from(TypeOf[GenericClass], type[GenericClass[int]])) -static_assert(not is_disjoint_from(TypeOf[GenericClass[int]], type[GenericClass[int]])) -static_assert(is_disjoint_from(TypeOf[GenericClass[str]], type[GenericClass[int]])) - -class GenericClassIntBound[T: int]: - x: T # invariant - -static_assert(not is_disjoint_from(TypeOf[GenericClassIntBound], type[GenericClassIntBound])) # error: [missing-type-argument] -static_assert( - # error: [missing-type-argument] - not is_disjoint_from(TypeOf[GenericClassIntBound[int]], type[GenericClassIntBound]) -) -static_assert(not is_disjoint_from(TypeOf[GenericClassIntBound], type[GenericClassIntBound[int]])) -static_assert(not is_disjoint_from(TypeOf[GenericClassIntBound[int]], type[GenericClassIntBound[int]])) - -@final -class GenericFinalClass[T]: - x: T # invariant - -static_assert(not is_disjoint_from(TypeOf[GenericFinalClass], type[GenericFinalClass])) # error: [missing-type-argument] -static_assert(not is_disjoint_from(TypeOf[GenericFinalClass[int]], type[GenericFinalClass])) # error: [missing-type-argument] -static_assert(not is_disjoint_from(TypeOf[GenericFinalClass], type[GenericFinalClass[int]])) -static_assert(not is_disjoint_from(TypeOf[GenericFinalClass[int]], type[GenericFinalClass[int]])) -static_assert(is_disjoint_from(TypeOf[GenericFinalClass[str]], type[GenericFinalClass[int]])) -``` - ## Callables No two callable types are disjoint because there exists a non-empty callable type @@ -1127,8 +970,24 @@ static_assert(not is_disjoint_from(Callable[..., Any], TypeOf[OrderedDict])) static_assert(not is_disjoint_from(TypeOf[OrderedDict], Callable[..., Any])) ``` +## Statically empty and non-empty ranges + +Empty and non-empty ranges are disjoint, but both overlap the general `range` type. + +```py +from ty_extensions import static_assert +from ty_extensions._internal import TypeOf, is_disjoint_from + +static_assert(is_disjoint_from(TypeOf[range(0)], TypeOf[range(1)])) +static_assert(is_disjoint_from(TypeOf[range(1)], TypeOf[range(0)])) +static_assert(not is_disjoint_from(TypeOf[range(0)], range)) +static_assert(not is_disjoint_from(TypeOf[range(1)], range)) +``` + ## Custom enum classes +Enum members overlap their enum class and its ancestors, but not other members or unrelated classes. + ```py from enum import Enum from ty_extensions import static_assert @@ -1152,3 +1011,457 @@ static_assert(is_disjoint_from(Literal[MyAnswer.NO], UnrelatedClass)) static_assert(not is_disjoint_from(Literal[MyAnswer.NO], MyAnswer)) static_assert(not is_disjoint_from(Literal[MyAnswer.NO], MyEnum)) ``` + +## Enum complements + +Enum types with complementary negations are disjoint when no enum member satisfies both. + +```pyi +from enum import Enum +from typing import Literal +from ty_extensions import static_assert +from ty_extensions._internal import is_disjoint_from + +class Color(Enum): + RED = 1 + GREEN = 2 + BLUE = 3 + +static_assert( + is_disjoint_from( + Color & ~Literal[Color.RED], + Color & ~Literal[Color.GREEN, Color.BLUE], + ) +) +static_assert( + is_disjoint_from( + Color & ~Literal[Color.GREEN, Color.BLUE], + Color & ~Literal[Color.RED], + ) +) +``` + +## Static tags and typed inhabitants + +An inhabitant of a type can be more than a bare runtime object: it can also include static type +information, not present at runtime, which can be understood as an invisible "tag". For example, a +generic tag records the type arguments of a specialization such as `list[int]`, while a `NewType` +tag identifies the `NewType` applied to a value. Neither kind of tag is visible on the runtime +object itself, but both affect which types an inhabitant belongs to. + +Generic tags carry guarantees about how an object can be used. The invariant types `list[int]` and +`list[str]` are disjoint because one reference could append a string that the other would then +incorrectly read as an integer. These incompatible generic tags cannot describe the same object +simultaneously in soundly typed code. + +Unlike incompatible invariant generic tags, distinct `NewType` tags can describe different typed +inhabitants of the same runtime object. If `UserId` and `OrderId` are distinct integer `NewType`s, +both `UserId(value)` and `OrderId(value)` return the same integer unchanged at runtime, but their +tags are incompatible. The two `NewType`s are disjoint even though their values can identify the +same runtime object. Both types still overlap `int`, which does not require either specific tag. + +### Invariant and covariant generic specializations + +Incompatible invariant arguments make generic specializations disjoint. Covariant specializations +can still overlap when a common empty or bottom specialization satisfies both. + +```pyi +from collections.abc import Sequence +from typing import Any, Generic, TypeVar +from ty_extensions import static_assert +from ty_extensions._internal import is_disjoint_from + +static_assert(is_disjoint_from(list[int], list[str])) + +T = TypeVar("T") +U = TypeVar("U") +T_co = TypeVar("T_co", covariant=True) + +class A: ... +class B: ... + +class Invariant(Generic[T]): + x: T + +class InvariantPair(Generic[T, U]): + x: T + y: U + +class Covariant(Generic[T_co]): + def get(self) -> T_co: + raise NotImplementedError() + +class InvSubA(Invariant[A]): + pass + +class CoSubB(Covariant[B]): + pass + +static_assert(is_disjoint_from(Invariant[A], Invariant[B])) +static_assert(is_disjoint_from(InvSubA, Invariant[B])) +static_assert(not is_disjoint_from(Invariant[A], Invariant[A])) +static_assert(not is_disjoint_from(Invariant[Any], Invariant[B])) +static_assert(not is_disjoint_from(Invariant[B], Invariant[Any])) +# `A | Any` cannot materialize to be equivalent to `B`. +static_assert(is_disjoint_from(Invariant[A | Any], Invariant[B])) +static_assert(is_disjoint_from(Invariant[B], Invariant[A | Any])) +static_assert(is_disjoint_from(Invariant[A & Any], Invariant[B])) +static_assert(is_disjoint_from(Invariant[B], Invariant[A & Any])) +static_assert(is_disjoint_from(InvariantPair[A, A], InvariantPair[A, B])) +static_assert(not is_disjoint_from(Covariant[A], Covariant[B])) +static_assert(not is_disjoint_from(Covariant[A], CoSubB)) +static_assert(not is_disjoint_from(Sequence[int], Sequence[str])) +``` + +### Specialized `@final` types + +Final generic specializations can overlap through a shared subtype such as `Foo[Never]`. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Any, final +from ty_extensions import static_assert +from ty_extensions._internal import is_disjoint_from + +@final +class Foo[T]: + def get(self) -> T: + raise NotImplementedError + +class A: ... +class B: ... + +static_assert(not is_disjoint_from(A, B)) +static_assert(not is_disjoint_from(Foo[A], Foo[B])) +static_assert(not is_disjoint_from(Foo[A], Foo[Any])) +static_assert(not is_disjoint_from(Foo[Any], Foo[B])) + +# `Foo[Never]` is inhabited (`get` can raise) and is a subtype of both `Foo[int]` and `Foo[str]`. +static_assert(not is_disjoint_from(Foo[int], Foo[str])) +``` + +### Type-variable aliases and empty invariant arguments + +Type-variable aliases preserve potentially compatible generic arguments. Empty or irrelevant type +arguments do not make otherwise compatible subclasses disjoint. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Generic, Never, TypeVar +from ty_extensions import static_assert +from ty_extensions._internal import is_disjoint_from + +T = TypeVar("T") + +class Invariant(Generic[T]): + x: T + +type Id[V] = V + +def _[U](): + static_assert(not is_disjoint_from(Invariant[U], Invariant[int])) + static_assert(not is_disjoint_from(Invariant[Id[U]], Invariant[int])) + +static_assert(not is_disjoint_from(Invariant[Id[int]], Invariant[int])) +static_assert(is_disjoint_from(Invariant[Id[int]], Invariant[str])) + +class Mixed[T, U]: + x: T + +# `Mixed` is bivariant in `U`, so the differing second argument cannot make these disjoint. +static_assert(not is_disjoint_from(Mixed[Never, int], Mixed[Never, str])) + +class Left(Invariant[Never]): ... +class Right(Invariant[Never]): ... +class Both(Left, Right): ... + +static_assert(not is_disjoint_from(Left, Right)) +``` + +### NewTypes and overlapping types + +A `NewType` overlaps with any nominal or structural type that overlaps its concrete base. This +includes the base itself, its supertypes and subclasses, and protocols satisfied by the base. + +```py +from typing import NewType, Protocol, final +from ty_extensions import static_assert +from ty_extensions._internal import is_disjoint_from + +UserId = NewType("UserId", int) + +@final +class FinalInt(int): ... + +class OrdinaryInt(int): ... + +class SupportsInt(Protocol): + def __int__(self) -> int: ... + +FinalIntId = NewType("FinalIntId", FinalInt) + +static_assert(not is_disjoint_from(UserId, int)) +static_assert(not is_disjoint_from(UserId, object)) +static_assert(not is_disjoint_from(UserId, FinalInt)) +static_assert(not is_disjoint_from(UserId, OrdinaryInt)) +static_assert(not is_disjoint_from(UserId, SupportsInt)) +static_assert(is_disjoint_from(UserId, str)) +static_assert(not is_disjoint_from(FinalIntId, FinalInt)) +static_assert(not is_disjoint_from(FinalIntId, int)) +``` + +### NewTypes and literal types + +The same overlap rule applies to literal types: a `NewType` overlaps with any literal type that +overlaps with its concrete base. Because `bool` is a subtype of `int`, type checkers correctly +accept `UserId(True)`, and an integer-based `NewType` also overlaps boolean literals. + +```py +from typing import Literal, NewType +from ty_extensions import static_assert +from ty_extensions._internal import is_disjoint_from + +UserId = NewType("UserId", int) +StringId = NewType("StringId", str) +BytesId = NewType("BytesId", bytes) + +UserId(True) # error: [bool-as-int] + +static_assert(not is_disjoint_from(UserId, Literal[True])) +static_assert(not is_disjoint_from(Literal[True], UserId)) +static_assert(not is_disjoint_from(UserId, Literal[False])) +static_assert(not is_disjoint_from(UserId, Literal[1])) +static_assert(not is_disjoint_from(UserId, bool)) +static_assert(not is_disjoint_from(bool, UserId)) +static_assert(is_disjoint_from(UserId, Literal["user"])) + +static_assert(not is_disjoint_from(StringId, Literal["user"])) +static_assert(not is_disjoint_from(BytesId, Literal[b"user"])) +``` + +An `IntEnum` and its members also overlap with an integer-based `NewType`. + +```py +from enum import IntEnum + +class Choice(IntEnum): + FIRST = 1 + SECOND = 2 + +static_assert(not is_disjoint_from(UserId, Choice)) +static_assert(not is_disjoint_from(UserId, Literal[Choice.FIRST])) +``` + +Nested NewTypes retain the overlap, and a float-based NewType also accepts `int` and `bool` through +the `int`/`float` special case. + +```py +NestedUserId = NewType("NestedUserId", UserId) +FloatId = NewType("FloatId", float) +BoolId = NewType("BoolId", bool) + +static_assert(not is_disjoint_from(NestedUserId, bool)) +static_assert(not is_disjoint_from(NestedUserId, Literal[True])) +static_assert(not is_disjoint_from(FloatId, bool)) +static_assert(not is_disjoint_from(FloatId, Literal[True])) +static_assert(not is_disjoint_from(FloatId, Literal[1])) +static_assert(not is_disjoint_from(FloatId, int)) +static_assert(not is_disjoint_from(BoolId, bool)) +``` + +### NewTypes and type guards + +`TypeGuard` and `TypeIs` represent boolean return values, so they overlap with `NewType`s whose +concrete bases accept booleans, including `int` and `float`. A `NewType` with an incompatible base +remains disjoint. + +```py +from typing import NewType +from typing_extensions import TypeGuard, TypeIs +from ty_extensions import static_assert +from ty_extensions._internal import is_disjoint_from + +Boolean = NewType("Boolean", bool) +Integer = NewType("Integer", int) +Numeric = NewType("Numeric", float) +Text = NewType("Text", str) + +static_assert(not is_disjoint_from(Boolean, TypeGuard[str])) +static_assert(not is_disjoint_from(TypeIs[str], Boolean)) + +static_assert(not is_disjoint_from(Integer, TypeGuard[str])) + +static_assert(not is_disjoint_from(Numeric, TypeIs[str])) + +static_assert(is_disjoint_from(Text, TypeGuard[str])) +static_assert(is_disjoint_from(TypeIs[str], Text)) +``` + +### Distinct NewTypes + +Unrelated `NewType` tags are mutually exclusive, even when their constructors return the same +runtime object. For the runtime object `True`, `(bool, First)` and `(bool, Second)` are different +(runtime type, tag) pairs. Both inhabit `int`, `bool`, and `Literal[True]`, but only the first +inhabits `First` and only the second inhabits `Second`. No pair inhabits both `NewType`s, so those +types are disjoint even though each overlaps the same ordinary types. + +```py +from typing import Literal, NewType +from ty_extensions import static_assert +from ty_extensions._internal import is_assignable_to, is_disjoint_from, is_subtype_of + +First = NewType("First", int) +Second = NewType("Second", int) +Numeric = NewType("Numeric", float) +Text = NewType("Text", str) + +static_assert(is_disjoint_from(First, Second)) +static_assert(is_disjoint_from(Second, First)) +static_assert(is_disjoint_from(First, Numeric)) +static_assert(is_disjoint_from(First, Text)) + +static_assert(not is_disjoint_from(First, int)) +static_assert(not is_disjoint_from(Second, int)) +static_assert(not is_disjoint_from(First, bool)) +static_assert(not is_disjoint_from(Second, bool)) +static_assert(not is_disjoint_from(First, Literal[True])) +static_assert(not is_disjoint_from(Second, Literal[True])) + +static_assert(not is_subtype_of(First, Second)) +static_assert(not is_assignable_to(First, Second)) +``` + +### Nested NewTypes + +A nested `NewType` remains a subtype of its parent, so their types overlap. Independently nested +`NewType`s remain disjoint, as do a nested `NewType` and an unrelated tag. + +```py +from typing import NewType +from ty_extensions import static_assert +from ty_extensions._internal import is_assignable_to, is_disjoint_from, is_subtype_of + +First = NewType("First", int) +Second = NewType("Second", int) +NestedFirst = NewType("NestedFirst", First) +OtherNestedFirst = NewType("OtherNestedFirst", First) + +static_assert(is_disjoint_from(NestedFirst, Second)) +static_assert(is_disjoint_from(NestedFirst, OtherNestedFirst)) +static_assert(not is_disjoint_from(NestedFirst, First)) +static_assert(not is_disjoint_from(First, NestedFirst)) +static_assert(is_subtype_of(NestedFirst, First)) +static_assert(is_assignable_to(NestedFirst, First)) +static_assert(not is_assignable_to(First, NestedFirst)) +``` + +### NewTypes and generic classes + +A `NewType` based on a covariant generic specialization overlaps with its generic supertypes and +subclasses. Two differently specialized covariant types can also overlap through a common, more +specific specialization. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Any, NewType +from ty_extensions import static_assert +from ty_extensions._internal import is_disjoint_from, is_subtype_of + +class Base[T]: + def get(self) -> T: + raise NotImplementedError + +class Child[T](Base[T]): ... + +BaseId = NewType("BaseId", Base[int]) + +static_assert(not is_disjoint_from(BaseId, Base[int])) +static_assert(not is_disjoint_from(BaseId, Base[object])) +# `Base[Never]` is inhabited (`get` can raise) and is a subtype of both `Base[int]` and `Base[str]`. +static_assert(not is_disjoint_from(BaseId, Base[str])) +static_assert(not is_disjoint_from(BaseId, Child[object])) +``` + +An ordinary gradual specialization can overlap a `NewType` even when strict subtyping does not hold. +Independently defined `NewType`s remain disjoint. + +```py +AnyListId = NewType("AnyListId", list[Any]) +IntListId = NewType("IntListId", list[int]) + +static_assert(not is_subtype_of(AnyListId, list[int])) +static_assert(not is_disjoint_from(AnyListId, list[int])) +static_assert(not is_disjoint_from(IntListId, list[Any])) +static_assert(is_disjoint_from(IntListId, list[str])) +static_assert(is_disjoint_from(IntListId, AnyListId)) +``` + +A generic type variable must not make a potentially compatible specialization appear disjoint. +Compatible constraints and bounds also preserve the overlap. + +```py +def unconstrained[T]() -> None: + static_assert(not is_disjoint_from(IntListId, list[T])) + static_assert(not is_disjoint_from(list[T], IntListId)) + +def compatible_constraints[T: (int, str)]() -> None: + static_assert(not is_disjoint_from(IntListId, list[T])) + +def compatible_bound[T: int]() -> None: + static_assert(not is_disjoint_from(IntListId, list[T])) +``` + +## Generic aliases + +Generic class objects and aliases overlap compatible `type[...]` types; incompatible invariant +specializations make them disjoint. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Any, final +from ty_extensions import static_assert +from ty_extensions._internal import TypeOf, is_disjoint_from + +class GenericClass[T]: + x: T # invariant + +static_assert(not is_disjoint_from(TypeOf[GenericClass], type[GenericClass[Any]])) +static_assert(not is_disjoint_from(TypeOf[GenericClass[int]], type[GenericClass[Any]])) +static_assert(not is_disjoint_from(TypeOf[GenericClass], type[GenericClass[int]])) +static_assert(not is_disjoint_from(TypeOf[GenericClass[int]], type[GenericClass[int]])) +static_assert(is_disjoint_from(TypeOf[GenericClass[str]], type[GenericClass[int]])) + +class GenericClassIntBound[T: int]: + x: T # invariant + +static_assert(not is_disjoint_from(TypeOf[GenericClassIntBound], type[GenericClassIntBound[Any]])) +static_assert(not is_disjoint_from(TypeOf[GenericClassIntBound[int]], type[GenericClassIntBound[Any]])) +static_assert(not is_disjoint_from(TypeOf[GenericClassIntBound], type[GenericClassIntBound[int]])) +static_assert(not is_disjoint_from(TypeOf[GenericClassIntBound[int]], type[GenericClassIntBound[int]])) + +@final +class GenericFinalClass[T]: + x: T # invariant + +static_assert(not is_disjoint_from(TypeOf[GenericFinalClass], type[GenericFinalClass[Any]])) +static_assert(not is_disjoint_from(TypeOf[GenericFinalClass[int]], type[GenericFinalClass[Any]])) +static_assert(not is_disjoint_from(TypeOf[GenericFinalClass], type[GenericFinalClass[int]])) +static_assert(not is_disjoint_from(TypeOf[GenericFinalClass[int]], type[GenericFinalClass[int]])) +static_assert(is_disjoint_from(TypeOf[GenericFinalClass[str]], type[GenericFinalClass[int]])) +``` 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 index 96f125c999..c3e33146bd 100644 --- 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 @@ -14,8 +14,8 @@ materializations of `B`, and all materializations of `B` are also materializatio ```py from typing_extensions import Literal, LiteralString, Protocol, Never -from ty_extensions import Unknown, static_assert, AlwaysTruthy, AlwaysFalsy -from ty_extensions._internal import TypeOf, is_equivalent_to +from ty_extensions import static_assert, AlwaysTruthy, AlwaysFalsy +from ty_extensions._internal import Unknown, TypeOf, is_equivalent_to from enum import Enum class Answer(Enum): @@ -72,8 +72,8 @@ static_assert(is_equivalent_to(type, type[object])) ```py from typing import Any from typing_extensions import Literal, LiteralString, Never -from ty_extensions import Unknown, static_assert -from ty_extensions._internal import is_equivalent_to +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_equivalent_to static_assert(is_equivalent_to(Any, Any)) static_assert(is_equivalent_to(Unknown, Unknown)) @@ -84,12 +84,102 @@ static_assert(not is_equivalent_to(type, type[Any])) static_assert(not is_equivalent_to(type[object], type[Any])) ``` +## Equivalent bounded gradual specializations + +A bounded generic specialized with a gradual type alias is equivalent to the same generic +specialized with the expanded alias. + +```toml +[environment] +python-version = "3.13" +``` + +For a covariant bounded type parameter, this applies to aliases containing either `Any` or +`Unknown`. + +```py +from typing import Any + +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_equivalent_to + +type AnyTuple = tuple[Any, ...] +type UnknownTuple = tuple[Unknown, ...] + +class BoundedCovariant[T: tuple[int, ...]]: + def get(self) -> T: + raise NotImplementedError + +static_assert(is_equivalent_to(BoundedCovariant[AnyTuple], BoundedCovariant[tuple[Any, ...]])) +static_assert(is_equivalent_to(BoundedCovariant[UnknownTuple], BoundedCovariant[AnyTuple])) +``` + +The same gradual tuple alias remains equivalent when the bounded type parameter is invariant. + +```py +class BoundedInvariant[T: tuple[int, ...]]: + value: T + +static_assert(is_equivalent_to(BoundedInvariant[AnyTuple], BoundedInvariant[tuple[Any, ...]])) +``` + +`Outer[int, Inner]` is equivalent to `Outer[int, Inner[Any]]` because `Inner` defaults to `Any`. +`Outer[int]` is equivalent to the same explicit specialization because `Outer` defaults to +`Inner[Any]`. + +```py +class Inner[T: int = Any]: + def get(self) -> T: + raise NotImplementedError + +class Outer[T: int, U: Inner[Any] = Inner[Any]]: + def get(self) -> U: + raise NotImplementedError + +static_assert(is_equivalent_to(Outer[int, Inner[Any]], Outer[int, Inner])) +static_assert(is_equivalent_to(Outer[int, Inner[Any]], Outer[int])) +``` + +## Bounded gradual specializations are distinct from upper bounds + +A generic specialized with a gradual type argument is not equivalent to the same generic specialized +with the type parameter's upper bound. + +```toml +[environment] +python-version = "3.13" +``` + +For a covariant type parameter: + +```py +from typing import Any + +from ty_extensions import static_assert +from ty_extensions._internal import is_equivalent_to + +class BoundedCovariant[T: tuple[int, ...]]: + def get(self) -> T: + raise NotImplementedError + +static_assert(not is_equivalent_to(BoundedCovariant[tuple[Any, ...]], BoundedCovariant[tuple[int, ...]])) +``` + +The same distinction applies to an invariant type parameter. + +```py +class BoundedInvariant[T: tuple[int, ...]]: + value: T + +static_assert(not is_equivalent_to(BoundedInvariant[tuple[Any, ...]], BoundedInvariant[tuple[int, ...]])) +``` + ## Unions and intersections ```pyi from typing import Any, Literal, TypeAlias -from ty_extensions import Unknown, static_assert -from ty_extensions._internal import is_equivalent_to +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_equivalent_to from enum import Enum static_assert(is_equivalent_to(str | int, str | int)) @@ -152,8 +242,8 @@ static_assert(is_equivalent_to(Any, ~None & Unknown | Unknown)) ## Tuples ```py -from ty_extensions import Unknown, static_assert -from ty_extensions._internal import is_equivalent_to +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_equivalent_to from typing import Any static_assert(is_equivalent_to(tuple[str, Any], tuple[str, Unknown])) @@ -384,8 +474,8 @@ infer-unannotated-signatures = false ``` ```py -from ty_extensions import Unknown, static_assert -from ty_extensions._internal import RegularCallableTypeOf, is_equivalent_to +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, RegularCallableTypeOf, is_equivalent_to def f(x): ... def g(x: Unknown): ... @@ -509,8 +599,8 @@ infer-unannotated-signatures = false ``` ```py -from ty_extensions import Unknown, static_assert -from ty_extensions._internal import CallableTypeOf, RegularCallableTypeOf, TypeOf, is_equivalent_to +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, CallableTypeOf, RegularCallableTypeOf, TypeOf, is_equivalent_to from typing import Any, Callable static_assert(is_equivalent_to(Callable[..., int], Callable[..., int])) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_single_valued.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_single_valued.md deleted file mode 100644 index 82dcfb33e2..0000000000 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_single_valued.md +++ /dev/null @@ -1,143 +0,0 @@ -## Single-valued types - -A type is single-valued iff it is not empty and all inhabitants of it compare equal. - -```pyi -import types -from types import UnionType -from typing_extensions import Any, Literal, LiteralString, Never, Callable, TypeAliasType -from ty_extensions import static_assert -from ty_extensions._internal import TypeOf, is_single_valued - -static_assert(is_single_valued(None)) -static_assert(is_single_valued(Literal[True])) -static_assert(is_single_valued(Literal[1])) -static_assert(is_single_valued(Literal["abc"])) -static_assert(is_single_valued(Literal[b"abc"])) - -static_assert(is_single_valued(tuple[()])) -static_assert(is_single_valued(tuple[Literal[True], Literal[1]])) - -class EmptyTupleSubclass(tuple[()]): ... -class HeterogeneousTupleSubclass(tuple[Literal[True], Literal[1]]): ... - -# N.B. this follows from the fact that `EmptyTupleSubclass` is a subtype of `tuple[()]`, -# and any property recognised for `tuple[()]` should therefore also be recognised for -# `EmptyTupleSubclass` since an `EmptyTupleSubclass` instance can be used anywhere where -# `tuple[()]` is accepted. This is only sound, however, if we ban `__eq__` and `__ne__` -# from being overridden on a tuple subclass. This is something we plan to do as part of -# our implementation of the Liskov Substitution Principle -# (https://github.com/astral-sh/ty/issues/166) -static_assert(is_single_valued(EmptyTupleSubclass)) -static_assert(is_single_valued(HeterogeneousTupleSubclass)) - -static_assert(not is_single_valued(str)) -static_assert(not is_single_valued(Never)) -static_assert(not is_single_valued(Any)) - -static_assert(not is_single_valued(Literal[1, 2])) - -static_assert(not is_single_valued(tuple[None, int])) - -class MultiValuedHeterogeneousTupleSubclass(tuple[None, int]): ... - -static_assert(not is_single_valued(MultiValuedHeterogeneousTupleSubclass)) - -static_assert(not is_single_valued(Callable[..., None])) -static_assert(not is_single_valued(Callable[[int, str], None])) - -static_assert(not is_single_valued(TypeAliasType)) -static_assert(not is_single_valued(UnionType)) -static_assert(is_single_valued(TypeOf[list[int]])) - -class A: - def method(self): ... - -# Binding the same method to different instances yields different objects: `[].sort != [].sort` -static_assert(not is_single_valued(TypeOf[A().method])) -static_assert(is_single_valued(TypeOf[types.FunctionType.__get__])) -static_assert(is_single_valued(TypeOf[A.method.__get__])) -``` - -An enum literal is only considered single-valued if it has no custom `__eq__`/`__ne__` method, or if -these methods always return `True`/`False`, respectively. Otherwise, the single member of the enum -literal type might not compare equal to itself. - -```pyi -from ty_extensions import static_assert -from ty_extensions._internal import TypeOf, is_single_valued -from enum import Enum - -class NormalEnum(Enum): - NO = 0 - YES = 1 - -class SingleValuedEnum(Enum): - VALUE = 1 - -class ComparesEqualEnum(Enum): - NO = 0 - YES = 1 - - def __eq__(self, other: object) -> Literal[True]: - return True - -class CustomEqEnum(Enum): - NO = 0 - YES = 1 - - def __eq__(self, other: object) -> bool: - return False - -class CustomNeEnum(Enum): - NO = 0 - YES = 1 - - def __ne__(self, other: object) -> bool: - return False - -class StrEnum(str, Enum): - A = "a" - B = "b" - -class IntEnum(int, Enum): - A = 1 - B = 2 - -static_assert(is_single_valued(Literal[NormalEnum.NO])) -static_assert(is_single_valued(Literal[NormalEnum.YES])) -static_assert(not is_single_valued(NormalEnum)) - -def _(value: NormalEnum) -> None: - if value is NormalEnum.NO: - return - static_assert(is_single_valued(TypeOf[value])) - -def _(value: NormalEnum & Any) -> None: - if value is NormalEnum.NO: - return - static_assert(not is_single_valued(TypeOf[value])) - -static_assert(is_single_valued(Literal[SingleValuedEnum.VALUE])) -static_assert(is_single_valued(SingleValuedEnum)) - -static_assert(is_single_valued(Literal[ComparesEqualEnum.NO])) -static_assert(is_single_valued(Literal[ComparesEqualEnum.YES])) -static_assert(not is_single_valued(ComparesEqualEnum)) - -static_assert(not is_single_valued(Literal[CustomEqEnum.NO])) -static_assert(not is_single_valued(Literal[CustomEqEnum.YES])) -static_assert(not is_single_valued(CustomEqEnum)) - -static_assert(not is_single_valued(Literal[CustomNeEnum.NO])) -static_assert(not is_single_valued(Literal[CustomNeEnum.YES])) -static_assert(not is_single_valued(CustomNeEnum)) - -static_assert(is_single_valued(Literal[StrEnum.A])) -static_assert(is_single_valued(Literal[StrEnum.B])) -static_assert(not is_single_valued(StrEnum)) - -static_assert(is_single_valued(Literal[IntEnum.A])) -static_assert(is_single_valued(Literal[IntEnum.B])) -static_assert(not is_single_valued(IntEnum)) -``` 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 index e128771c27..40b54fab60 100644 --- 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 @@ -890,8 +890,8 @@ of the first type represent sets of values that are a subset of every possible s represented by a materialization of the second type. ```pyi -from ty_extensions import Unknown, static_assert -from ty_extensions._internal import is_subtype_of +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_subtype_of from typing_extensions import Any static_assert(not is_subtype_of(Any, Any)) @@ -931,14 +931,6 @@ static_assert(not is_subtype_of(Invariant[Any], Invariant[int])) static_assert(not is_subtype_of(Invariant[int], Invariant[Any])) static_assert(not is_subtype_of(Invariant[Any], Invariant[object])) static_assert(not is_subtype_of(Invariant[object], Invariant[Any])) - -class Bivariant[T]: ... - -static_assert(is_subtype_of(Bivariant[Any], Bivariant[Any])) -static_assert(is_subtype_of(Bivariant[Any], Bivariant[int])) -static_assert(is_subtype_of(Bivariant[int], Bivariant[Any])) -static_assert(is_subtype_of(Bivariant[Any], Bivariant[object])) -static_assert(is_subtype_of(Bivariant[object], Bivariant[Any])) ``` The same for `Unknown`: @@ -985,6 +977,16 @@ static_assert(not is_subtype_of(type[Any], type[Arbitrary])) static_assert(is_subtype_of(type[Any], type[object])) ``` +A covariant specialization whose argument is a recursive alias remains a subtype of the same +specialization with `object`. A gradual invariant branch must not cause recursive materialization to +unfold indefinitely. + +```pyi +type RecursiveGradual = Covariant[RecursiveGradual] | Invariant[Any] + +static_assert(is_subtype_of(Covariant[RecursiveGradual], Covariant[object])) +``` + ## Callable The general principle is that a callable type is a subtype of another if it's more flexible in what @@ -1353,6 +1355,136 @@ static_assert(is_subtype_of(RegularCallableTypeOf[variadic], RegularCallableType static_assert(is_subtype_of(RegularCallableTypeOf[variadic], RegularCallableTypeOf[positional_variadic])) ``` +#### Variadic with an unpacked positional suffix + +A variadic positional parameter must accept both the unpacked elements and any fixed positional +suffix in the supertype. + +```py +from typing import Callable, Never, Unpack, cast +from ty_extensions import static_assert +from ty_extensions._internal import RegularCallableTypeOf, is_subtype_of + +def accepts_objects(*args: object) -> None: ... +def accepts_strings_or_none(*args: str | None) -> None: ... +def accepts_strings(*args: str) -> None: ... + +static_assert( + is_subtype_of( + RegularCallableTypeOf[accepts_objects], + Callable[[Unpack[tuple[str, ...]], None], None], + ) +) +static_assert( + is_subtype_of( + RegularCallableTypeOf[accepts_strings_or_none], + Callable[[Unpack[tuple[str, ...]], None], None], + ) +) +static_assert( + not is_subtype_of( + RegularCallableTypeOf[accepts_strings], + Callable[[Unpack[tuple[str, ...]], None], None], + ) +) +``` + +A required suffix can align with a longer suffix or an equivalent positional prefix when all the +unpacked elements have the same type. + +```py +def requires_one_integer(*args: *tuple[*tuple[int, ...], int]) -> None: ... + +type OneOrMoreIntegers = RegularCallableTypeOf[requires_one_integer] + +static_assert(is_subtype_of(OneOrMoreIntegers, Callable[[*tuple[int, ...], int, int], None])) +static_assert(is_subtype_of(OneOrMoreIntegers, Callable[[int, *tuple[int, ...]], None])) +``` + +A type alias for the variadic element does not prevent the required suffix from matching. + +```py +type Integer = int + +def requires_one_aliased_integer(*args: *tuple[*tuple[Integer, ...], int]) -> None: ... + +type AliasedIntegers = RegularCallableTypeOf[requires_one_aliased_integer] + +static_assert(is_subtype_of(AliasedIntegers, Callable[[int, *tuple[int, ...]], None])) +``` + +A longer suffix is aligned from the end when its other elements fit the source variadic parameter. + +```py +def requires_string_suffix(*args: *tuple[*tuple[object, ...], str]) -> None: ... +def requires_string_after_integers(*args: *tuple[*tuple[int, ...], str]) -> None: ... + +type StringSuffix = RegularCallableTypeOf[requires_string_suffix] +type IntegerStringSuffix = RegularCallableTypeOf[requires_string_after_integers] + +static_assert(is_subtype_of(StringSuffix, Callable[[*tuple[object, ...], int, str], None])) +static_assert(is_subtype_of(IntegerStringSuffix, Callable[[*tuple[int, ...], int, str], None])) +``` + +A positional parameter cannot also be filled by a target keyword argument. + +```py +def occupies_keyword(a: int, *args: int, **kwargs: int) -> None: ... +def accepts_keyword(*args: *tuple[*tuple[int, ...], int], **kwargs: int) -> None: ... + +type OccupiesKeyword = RegularCallableTypeOf[occupies_keyword] +type AcceptsKeyword = RegularCallableTypeOf[accepts_keyword] + +static_assert(not is_subtype_of(OccupiesKeyword, AcceptsKeyword)) +``` + +An uninhabited keyword parameter cannot collide with an occupied positional parameter. + +```py +type Bottom = Never + +def rejects_keywords(*args: *tuple[*tuple[int, ...], int], **kwargs: Bottom) -> None: ... +def rejects_named_keyword(*args: *tuple[*tuple[int, ...], int], a: Never = cast(Never, 0)) -> None: ... + +static_assert(is_subtype_of(OccupiesKeyword, RegularCallableTypeOf[rejects_keywords])) +static_assert(is_subtype_of(OccupiesKeyword, RegularCallableTypeOf[rejects_named_keyword])) +``` + +Equivalent empty or fixed-length unpacked parameters are compatible, but cannot be reused for +additional positional arguments. + +```py +def accepts_no_arguments(*args: Unpack[tuple[()]]) -> None: ... +def accepts_one_integer(*args: Unpack[tuple[int]]) -> None: ... + +static_assert(is_subtype_of(RegularCallableTypeOf[accepts_no_arguments], Callable[[Unpack[tuple[()]]], None])) +static_assert(is_subtype_of(RegularCallableTypeOf[accepts_one_integer], Callable[[Unpack[tuple[int]]], None])) +static_assert( + not is_subtype_of( + RegularCallableTypeOf[accepts_no_arguments], + Callable[[int, Unpack[tuple[str, ...]], None], None], + ) +) +static_assert( + not is_subtype_of( + RegularCallableTypeOf[accepts_no_arguments], + Callable[[Unpack[tuple[str, ...]], None], None], + ) +) +static_assert( + not is_subtype_of( + RegularCallableTypeOf[accepts_one_integer], + Callable[[int, Unpack[tuple[str, ...]], None], None], + ) +) +static_assert( + not is_subtype_of( + RegularCallableTypeOf[accepts_one_integer], + Callable[[Unpack[tuple[tuple[int], ...]], tuple[int]], None], + ) +) +``` + #### Variadic with other kinds Variadic parameter in a subtype can only be used to match against an unmatched positional-only @@ -1699,6 +1831,120 @@ def f(*args: Any, **kwargs: Any) -> Any: ... static_assert(not is_subtype_of(RegularCallableTypeOf[f], Callable[[], object])) ``` +#### Bottom callables with gradual positional prefixes + +A callable accepting every argument list is a subtype of a gradual callable with any positional +prefix when its return type is compatible. + +```py +from typing import Any, Callable, Concatenate, Never +from ty_extensions import static_assert +from ty_extensions._internal import RegularCallableTypeOf, is_subtype_of + +def bottom(*args: object, **kwargs: object) -> Never: + raise Exception() + +type BottomCallable = RegularCallableTypeOf[bottom] + +static_assert(is_subtype_of(BottomCallable, Callable[Concatenate[int, ...], None])) +static_assert(is_subtype_of(BottomCallable, Callable[Concatenate[int, str, ...], None])) +``` + +A callable with an optional positional-only parameter and a dynamically typed variadic tail is also +a supertype of the bottom callable. + +```py +def gradual_prefix(value: int = 0, /, *args: Any, **kwargs: Any) -> None: ... + +static_assert(is_subtype_of(BottomCallable, RegularCallableTypeOf[gradual_prefix])) +``` + +#### Object-variadic callables with matching gradual prefixes + +Object-variadic callables are subtypes of gradual callables when their required positional +parameters match the gradual callable's prefix. + +```py +from typing import Callable, Concatenate +from ty_extensions import static_assert +from ty_extensions._internal import RegularCallableTypeOf, is_subtype_of + +def positional_or_keyword(value: int, *args: object, **kwargs: object) -> None: ... +def positional_only(value: int, /, *args: object, **kwargs: object) -> None: ... + +type GradualIntCallable = Callable[Concatenate[int, ...], None] + +static_assert(is_subtype_of(RegularCallableTypeOf[positional_or_keyword], GradualIntCallable)) +static_assert(is_subtype_of(RegularCallableTypeOf[positional_only], GradualIntCallable)) +``` + +An unrestricted variadic parameter can satisfy additional positional parameters in the target. + +```py +static_assert( + is_subtype_of( + RegularCallableTypeOf[positional_only], + Callable[Concatenate[int, str, ...], None], + ) +) +``` + +Unpacked positional parameters are normalized before comparing an unrestricted variadic tail. + +```py +def unpacked_prefix(*args: *tuple[int, *tuple[object, ...]], **kwargs: object) -> None: ... + +static_assert(is_subtype_of(RegularCallableTypeOf[unpacked_prefix], GradualIntCallable)) +``` + +#### Object-variadic callables with incompatible gradual prefixes + +A source callable cannot be a subtype when its prefix has an incompatible parameter or requires more +positional arguments than the target's prefix. + +```py +from typing import Callable, Concatenate +from ty_extensions import static_assert +from ty_extensions._internal import RegularCallableTypeOf, is_subtype_of + +type GradualIntCallable = Callable[Concatenate[int, ...], None] + +def wrong_prefix(value: str, *args: object, **kwargs: object) -> None: ... +def extra_required(value: int, another: str, *args: object, **kwargs: object) -> None: ... + +static_assert(not is_subtype_of(RegularCallableTypeOf[wrong_prefix], GradualIntCallable)) +static_assert(not is_subtype_of(RegularCallableTypeOf[extra_required], GradualIntCallable)) +``` + +Both variadic parameters must accept every possible argument from the gradual tail. + +```py +def restricted_args(value: int, *args: int, **kwargs: object) -> None: ... +def restricted_kwargs(value: int, *args: object, **kwargs: int) -> None: ... + +static_assert(not is_subtype_of(RegularCallableTypeOf[restricted_args], GradualIntCallable)) +static_assert(not is_subtype_of(RegularCallableTypeOf[restricted_kwargs], GradualIntCallable)) +``` + +An additional keyword-only parameter also restricts the otherwise unrestricted variadic tail. + +```py +def required_keyword(value: int, *args: object, flag: int, **kwargs: object) -> None: ... +def optional_keyword(value: int, *args: object, flag: int = 0, **kwargs: object) -> None: ... + +static_assert(not is_subtype_of(RegularCallableTypeOf[required_keyword], GradualIntCallable)) +static_assert(not is_subtype_of(RegularCallableTypeOf[optional_keyword], GradualIntCallable)) +``` + +The return type must remain compatible even when the parameters accept every possible call. + +```py +def wrong_return(value: int, *args: object, **kwargs: object) -> int: + return 1 + +static_assert(not is_subtype_of(RegularCallableTypeOf[wrong_return], GradualIntCallable)) +``` + ### Classes with `__call__` ```py @@ -1881,7 +2127,7 @@ class MetaWithIntReturn(type): class F(metaclass=MetaWithIntReturn): def __new__(cls) -> str: - return super().__new__(cls) + return "" class Returns[T](Protocol): def __call__(self) -> T: ... @@ -1960,7 +2206,7 @@ static_assert(not is_subtype_of(TypeOf[A], Returns[A])) class B: def __new__(cls, a: int) -> int: - return super().__new__(cls) + return 0 def __init__(self, a: str) -> None: ... @@ -2031,7 +2277,7 @@ class MetaWithIntReturn(type): class F(metaclass=MetaWithIntReturn): def __new__(cls) -> str: - return super().__new__(cls) + return "" def __init__(self, x: int) -> None: ... diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md b/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md index ac4359a570..5d8a7960de 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md @@ -29,7 +29,8 @@ The dynamic type at the top-level is replaced with `object`. ```py from typing import Any, Callable -from ty_extensions import Unknown, Top +from ty_extensions import Top +from ty_extensions._internal import Unknown def _(top_any: Top[Any], top_unknown: Top[Unknown]): reveal_type(top_any) # revealed: object @@ -56,7 +57,8 @@ The dynamic type at the top-level is replaced with `Never`. ```py from typing import Any, Callable -from ty_extensions import Unknown, Bottom +from ty_extensions import Bottom +from ty_extensions._internal import Unknown def _(bottom_any: Bottom[Any], bottom_unknown: Bottom[Unknown]): reveal_type(bottom_any) # revealed: Never @@ -150,8 +152,8 @@ python-version = "3.12" ```py from typing import Any, Callable -from ty_extensions import Unknown, Bottom, Top -from ty_extensions._internal import TypeOf +from ty_extensions import Bottom, Top +from ty_extensions._internal import Unknown, TypeOf type C1 = Callable[[Any, Unknown], Any] @@ -288,8 +290,8 @@ python-version = "3.12" ```py from typing import Any, Never -from ty_extensions import Unknown, Bottom, Top, static_assert -from ty_extensions._internal import is_equivalent_to +from ty_extensions import Bottom, Top, static_assert +from ty_extensions._internal import Unknown, is_equivalent_to static_assert(is_equivalent_to(Top[tuple[Any, int]], tuple[object, int])) static_assert(is_equivalent_to(Bottom[tuple[Any, int]], Never)) @@ -351,8 +353,8 @@ python-version = "3.12" ```py from typing import Any -from ty_extensions import Unknown, Bottom, Top, static_assert -from ty_extensions._internal import is_equivalent_to +from ty_extensions import Bottom, Top, static_assert +from ty_extensions._internal import Unknown, is_equivalent_to static_assert(is_equivalent_to(Top[Any | int], object)) static_assert(is_equivalent_to(Bottom[Any | int], int)) @@ -404,8 +406,8 @@ All positions in an intersection are covariant. ```pyi from typing import Any from typing_extensions import Never -from ty_extensions import Unknown, Bottom, Top, static_assert -from ty_extensions._internal import is_equivalent_to +from ty_extensions import Bottom, Top, static_assert +from ty_extensions._internal import Unknown, is_equivalent_to static_assert(is_equivalent_to(Top[Any & int], int)) static_assert(is_equivalent_to(Bottom[Any & int], Never)) @@ -460,8 +462,8 @@ All positions in a negation are contravariant. ```pyi from typing import Any from typing_extensions import Never -from ty_extensions import Unknown, Bottom, Top, static_assert -from ty_extensions._internal import is_equivalent_to +from ty_extensions import Bottom, Top, static_assert +from ty_extensions._internal import Unknown, is_equivalent_to # ~Any is still Any, so the top materialization is object static_assert(is_equivalent_to(Top[~Any], object)) @@ -483,8 +485,8 @@ python-version = "3.12" ```py from typing import Any from typing_extensions import Never -from ty_extensions import Unknown, Bottom, Top, static_assert -from ty_extensions._internal import is_equivalent_to +from ty_extensions import Bottom, Top, static_assert +from ty_extensions._internal import Unknown, is_equivalent_to static_assert(is_equivalent_to(Top[type[Any]], type)) static_assert(is_equivalent_to(Bottom[type[Any]], Never)) @@ -501,6 +503,71 @@ def _(top: Top[list[type[Any]]], bottom: Bottom[list[type[Any]]]): reveal_type(bottom) # revealed: Bottom[list[type[Any]]] ``` +## Materialized class annotations and constructors + +A class-object annotation can name either materialization of an invariant generic. Calling the +annotated class produces an instance with the same materialization. + +```py +from typing import Any +from ty_extensions import Bottom, Top + +def materialized_list_classes( + top: type[Top[list[Any]]], + bottom: type[Bottom[list[Any]]], +) -> None: + reveal_type(top) # revealed: type[Top[list[Any]]] + reveal_type(bottom) # revealed: type[Bottom[list[Any]]] + reveal_type(top()) # revealed: Top[list[Any]] + reveal_type(bottom()) # revealed: Bottom[list[Any]] +``` + +## Generic aliases of materialized classes + +A generic class alias can be materialized inside `type[...]`. Aliasing the complete materialized +type also preserves its polarity, and both alias forms resolve to the underlying class. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Any +from ty_extensions import Bottom, Top + +type ListAlias[T] = list[T] +type TopList = Top[ListAlias[Any]] +type BottomList = Bottom[ListAlias[Any]] + +def aliased_materialized_list_classes( + generic_top: type[Top[ListAlias[Any]]], + generic_bottom: type[Bottom[ListAlias[Any]]], + aliased_top: type[TopList], + aliased_bottom: type[BottomList], +) -> None: + reveal_type(generic_top) # revealed: type[Top[list[Any]]] + reveal_type(generic_bottom) # revealed: type[Bottom[list[Any]]] + reveal_type(aliased_top) # revealed: type[Top[list[Any]]] + reveal_type(aliased_bottom) # revealed: type[Bottom[list[Any]]] + reveal_type(aliased_top()) # revealed: Top[list[Any]] + reveal_type(aliased_bottom()) # revealed: Bottom[list[Any]] +``` + +## Invalid materialization arity in class annotations + +`Top` and `Bottom` each require exactly one type argument, even when they are nested inside a +class-object annotation. + +```py +from ty_extensions import Bottom, Top + +def invalid_materialized_list_classes( + top: type[Top[int, str]], # error: [invalid-type-form] + bottom: type[Bottom[int, str]], # error: [invalid-type-form] +) -> None: ... +``` + ## Type variables ```toml @@ -510,8 +577,8 @@ python-version = "3.12" ```py from typing import Any, Never, TypeVar -from ty_extensions import Unknown, Bottom, Top, static_assert -from ty_extensions._internal import is_subtype_of +from ty_extensions import Bottom, Top, static_assert +from ty_extensions._internal import Unknown, is_subtype_of def bounded_by_gradual[T: Any](t: T) -> None: # Top materialization of `T: Any` is `T: object` @@ -616,6 +683,406 @@ def contravariant(top: Top[ContravariantCallable], bottom: Bottom[ContravariantC reveal_type(bottom) # revealed: (GenericContravariant[Never], /) -> None ``` +## Bounded generic type parameters + +Top materialization of a covariant generic uses the type parameter's declared upper bound. Bottom +materialization uses its lower bound, `Never`. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Any, Generic, Never, TypeVar +from ty_extensions import Bottom, Top, static_assert +from ty_extensions._internal import is_equivalent_to, is_subtype_of + +class BoundedCovariant[T: int]: + def get(self) -> T: + raise NotImplementedError + +static_assert(is_equivalent_to(Top[BoundedCovariant[Any]], BoundedCovariant[int])) +static_assert(is_equivalent_to(Bottom[BoundedCovariant[Any]], BoundedCovariant[Never])) +static_assert(is_subtype_of(BoundedCovariant[Any], Top[BoundedCovariant[Any]])) +static_assert(is_subtype_of(BoundedCovariant[Any], BoundedCovariant[int])) +``` + +A type alias can conceal a gradual argument; the same subtype relationships still apply. + +```py +type AliasedAny = Any + +static_assert(is_subtype_of(BoundedCovariant[AliasedAny], Top[BoundedCovariant[AliasedAny]])) +static_assert(is_subtype_of(BoundedCovariant[AliasedAny], BoundedCovariant[int])) +``` + +An alias for a static upper bound remains static. It absorbs a bounded gradual specialization in +either union order. + +```py +type AliasedInt = int + +def aliased_static_bound( + gradual_first: BoundedCovariant[Any] | BoundedCovariant[AliasedInt], + gradual_last: BoundedCovariant[AliasedInt] | BoundedCovariant[Any], +) -> None: + reveal_type(gradual_first) # revealed: BoundedCovariant[AliasedInt] + reveal_type(gradual_last) # revealed: BoundedCovariant[AliasedInt] +``` + +Contravariance reverses which bound is used by top and bottom materialization. + +```py +class BoundedContravariant[T: int]: + def put(self, value: T) -> None: ... + +static_assert(is_equivalent_to(Top[BoundedContravariant[Any]], BoundedContravariant[Never])) +static_assert(is_equivalent_to(Bottom[BoundedContravariant[Any]], BoundedContravariant[int])) +``` + +For an invariant generic, materialize attributes and method parameters according to their own +variance. An unrelated `Any` attribute must remain gradual. + +```py +class BoundedInvariant[T: int]: + value: T + unrelated: Any + + def get(self) -> T: + raise NotImplementedError + + def put(self, value: T) -> None: ... + +def bounded_invariant( + top: Top[BoundedInvariant[Any]], + bottom: Bottom[BoundedInvariant[Any]], +) -> None: + reveal_type(top.value) # revealed: int + reveal_type(top.unrelated) # revealed: Any + reveal_type(top.get) # revealed: bound method Top[BoundedInvariant[Any]].get() -> int + reveal_type(top.put) # revealed: bound method Top[BoundedInvariant[Any]].put(value: Never) + + reveal_type(bottom.unrelated) # revealed: Any + reveal_type(bottom.get) # revealed: bound method Bottom[BoundedInvariant[Any]].get() -> Never + reveal_type(bottom.put) # revealed: bound method Bottom[BoundedInvariant[Any]].put(value: int) + reveal_type(bottom.value) # revealed: Never +``` + +Explicitly covariant and contravariant legacy `TypeVar` declarations obey the same bounded +materialization rules. + +```py +BoundedT_co = TypeVar("BoundedT_co", bound=int, covariant=True) + +class LegacyBoundedCovariant(Generic[BoundedT_co]): ... + +static_assert(is_equivalent_to(Top[LegacyBoundedCovariant[Any]], LegacyBoundedCovariant[int])) +static_assert(is_equivalent_to(Bottom[LegacyBoundedCovariant[Any]], LegacyBoundedCovariant[Never])) + +BoundedT_contra = TypeVar("BoundedT_contra", bound=int, contravariant=True) + +class LegacyBoundedContravariant(Generic[BoundedT_contra]): ... + +static_assert(is_equivalent_to(Top[LegacyBoundedContravariant[Any]], LegacyBoundedContravariant[Never])) +static_assert(is_equivalent_to(Bottom[LegacyBoundedContravariant[Any]], LegacyBoundedContravariant[int])) +``` + +Reading an attribute of a top-materialized legacy invariant generic yields the type parameter's +upper bound; reading the same attribute from its bottom materialization yields the lower bound. + +```py +BoundedT = TypeVar("BoundedT", bound=int) + +class LegacyBoundedInvariant(Generic[BoundedT]): + value: BoundedT + +def legacy_bounded_invariant( + legacy_top: Top[LegacyBoundedInvariant[Any]], + legacy_bottom: Bottom[LegacyBoundedInvariant[Any]], +) -> None: + reveal_type(legacy_top.value) # revealed: int + reveal_type(legacy_bottom.value) # revealed: Never +``` + +## Constrained generic type parameters + +A constrained type parameter cannot generally be replaced by the union of its constraints: the union +need not itself be a valid specialization. Top and bottom materialization must instead retain the +covariant generic and its valid specializations. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Any, Generic, Never, TypeVar +from ty_extensions import Bottom, Intersection, Not, Top, static_assert +from ty_extensions._internal import is_assignable_to, is_equivalent_to, is_subtype_of + +class ConstrainedCovariant[T: (int, str)]: + def get(self) -> T: + raise NotImplementedError + +def constrained_covariant( + top: Top[ConstrainedCovariant[Any]], + bottom: Bottom[ConstrainedCovariant[Any]], +) -> None: + reveal_type(top) # revealed: Top[ConstrainedCovariant[Any]] + reveal_type(bottom) # revealed: Bottom[ConstrainedCovariant[Any]] + +static_assert(is_subtype_of(ConstrainedCovariant[int], Top[ConstrainedCovariant[Any]])) +static_assert(is_subtype_of(ConstrainedCovariant[str], Top[ConstrainedCovariant[Any]])) +static_assert(is_subtype_of(ConstrainedCovariant[Any], Top[ConstrainedCovariant[Any]])) +static_assert(not is_subtype_of(Top[ConstrainedCovariant[Any]], ConstrainedCovariant[int])) +static_assert(not is_subtype_of(Top[ConstrainedCovariant[Any]], ConstrainedCovariant[str])) +static_assert(is_subtype_of(Bottom[ConstrainedCovariant[Any]], ConstrainedCovariant[int])) +static_assert(is_subtype_of(Bottom[ConstrainedCovariant[Any]], ConstrainedCovariant[str])) +static_assert(is_subtype_of(Bottom[ConstrainedCovariant[Any]], Top[ConstrainedCovariant[Any]])) +static_assert(not is_equivalent_to(Intersection[ConstrainedCovariant[str], Not[ConstrainedCovariant[int]]], Never)) + +static_assert(is_assignable_to(ConstrainedCovariant[int], Top[ConstrainedCovariant[Any]])) +static_assert(not is_assignable_to(Top[ConstrainedCovariant[Any]], ConstrainedCovariant[int])) +static_assert(is_assignable_to(Bottom[ConstrainedCovariant[Any]], ConstrainedCovariant[int])) +``` + +Contravariant constrained generics likewise preserve their materializations while reversing the +relationship between input positions and top or bottom types. + +```py +class ConstrainedContravariant[T: (int, str)]: + def put(self, value: T) -> None: ... + +def constrained_contravariant( + top: Top[ConstrainedContravariant[Any]], + bottom: Bottom[ConstrainedContravariant[Any]], +) -> None: + reveal_type(top) # revealed: Top[ConstrainedContravariant[Any]] + reveal_type(bottom) # revealed: Bottom[ConstrainedContravariant[Any]] + +static_assert(is_subtype_of(ConstrainedContravariant[int], Top[ConstrainedContravariant[Any]])) +static_assert(is_subtype_of(ConstrainedContravariant[str], Top[ConstrainedContravariant[Any]])) +static_assert(not is_subtype_of(Top[ConstrainedContravariant[Any]], ConstrainedContravariant[int])) +static_assert(not is_subtype_of(Top[ConstrainedContravariant[Any]], ConstrainedContravariant[str])) +static_assert(is_subtype_of(Bottom[ConstrainedContravariant[Any]], ConstrainedContravariant[int])) +static_assert(is_subtype_of(Bottom[ConstrainedContravariant[Any]], ConstrainedContravariant[str])) +static_assert(is_subtype_of(Bottom[ConstrainedContravariant[Any]], Top[ConstrainedContravariant[Any]])) + +static_assert(is_assignable_to(ConstrainedContravariant[int], Top[ConstrainedContravariant[Any]])) +static_assert(not is_assignable_to(Top[ConstrainedContravariant[Any]], ConstrainedContravariant[int])) +static_assert(is_assignable_to(Bottom[ConstrainedContravariant[Any]], ConstrainedContravariant[int])) +``` + +An invariant constrained parameter materializes readable values to the union of valid constraints +and writable parameters to `Never`. Unrelated gradual attributes remain `Any`. + +```py +class ConstrainedInvariant[T: (int, str)]: + value: T + unrelated: Any + + def get(self) -> T: + raise NotImplementedError + + def put(self, value: T) -> None: ... + +def constrained_invariant( + top: Top[ConstrainedInvariant[Any]], + bottom: Bottom[ConstrainedInvariant[Any]], +) -> None: + reveal_type(top.value) # revealed: int | str + reveal_type(top.unrelated) # revealed: Any + reveal_type(top.get) # revealed: bound method Top[ConstrainedInvariant[Any]].get() -> int | str + reveal_type(top.put) # revealed: bound method Top[ConstrainedInvariant[Any]].put(value: Never) + + reveal_type(bottom.unrelated) # revealed: Any + reveal_type(bottom.get) # revealed: bound method Bottom[ConstrainedInvariant[Any]].get() -> Never + reveal_type(bottom.put) # revealed: bound method Bottom[ConstrainedInvariant[Any]].put(value: int | str) + reveal_type(bottom.value) # revealed: Never +``` + +Direct attribute writes are currently checked against the readable union rather than the safe +`Never` parameter used for setters. + +```py +def constrained_invariant_writes(top: Top[ConstrainedInvariant[Any]]) -> None: + # TODO: Reject these writes; neither value is safe for every specialization. + top.value = 1 + top.value = "value" + top.value = 1.5 # error: [invalid-assignment] +``` + +Legacy constrained type variables preserve the same covariant and contravariant subtype +relationships. + +```py +ConstrainedT_co = TypeVar("ConstrainedT_co", int, str, covariant=True) + +class LegacyConstrainedCovariant(Generic[ConstrainedT_co]): ... + +static_assert(is_subtype_of(LegacyConstrainedCovariant[int], Top[LegacyConstrainedCovariant[Any]])) +static_assert(is_subtype_of(Bottom[LegacyConstrainedCovariant[Any]], LegacyConstrainedCovariant[str])) + +ConstrainedT_contra = TypeVar("ConstrainedT_contra", int, str, contravariant=True) + +class LegacyConstrainedContravariant(Generic[ConstrainedT_contra]): ... + +static_assert(is_subtype_of(LegacyConstrainedContravariant[int], Top[LegacyConstrainedContravariant[Any]])) +static_assert(is_subtype_of(Bottom[LegacyConstrainedContravariant[Any]], LegacyConstrainedContravariant[str])) +``` + +A partially gradual type argument filters out constraints incompatible with its static `int` arm. + +```py +static_assert(is_equivalent_to(Bottom[ConstrainedCovariant[Any | int]], ConstrainedCovariant[int])) +static_assert(not is_subtype_of(ConstrainedCovariant[str], Top[ConstrainedCovariant[Any | int]])) +static_assert(is_equivalent_to(Top[ConstrainedContravariant[Any | int]], ConstrainedContravariant[int])) +static_assert(not is_subtype_of(Bottom[ConstrainedContravariant[Any | int]], ConstrainedContravariant[str])) +``` + +An intersection of `int` and `Any` likewise retains only the compatible `int` constraint for both +variances. + +```py +type GradualInt = Intersection[int, Any] + +static_assert(is_subtype_of(ConstrainedCovariant[int], Top[ConstrainedCovariant[GradualInt]])) +static_assert(not is_subtype_of(ConstrainedCovariant[str], Top[ConstrainedCovariant[GradualInt]])) +static_assert(is_subtype_of(Bottom[ConstrainedCovariant[GradualInt]], ConstrainedCovariant[int])) +static_assert(not is_subtype_of(Bottom[ConstrainedCovariant[GradualInt]], ConstrainedCovariant[str])) +static_assert(is_subtype_of(ConstrainedContravariant[int], Top[ConstrainedContravariant[GradualInt]])) +static_assert(not is_subtype_of(ConstrainedContravariant[str], Top[ConstrainedContravariant[GradualInt]])) +static_assert(is_subtype_of(Bottom[ConstrainedContravariant[GradualInt]], ConstrainedContravariant[int])) +static_assert(not is_subtype_of(Bottom[ConstrainedContravariant[GradualInt]], ConstrainedContravariant[str])) +``` + +## Gradual generic constraints + +When `Any` is itself a constraint, static specializations outside the other constraint must remain +valid. Reading a top-materialized covariant value produces `object`. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Any +from ty_extensions import Bottom, Top, static_assert +from ty_extensions._internal import is_subtype_of + +class GradualConstrainedCovariant[T: (int, Any)]: + def get(self) -> T: + raise NotImplementedError + +class GradualConstrainedContravariant[T: (int, Any)]: + def put(self, value: T) -> None: ... + +def gradual_constraints(value: Top[GradualConstrainedCovariant[Any]]) -> None: + reveal_type(value) # revealed: Top[GradualConstrainedCovariant[Any]] + reveal_type(value.get()) # revealed: object + +static_assert(is_subtype_of(GradualConstrainedCovariant[int], Top[GradualConstrainedCovariant[Any]])) +static_assert(is_subtype_of(GradualConstrainedCovariant[str], Top[GradualConstrainedCovariant[Any]])) +static_assert(is_subtype_of(Bottom[GradualConstrainedCovariant[Any]], GradualConstrainedCovariant[int])) +static_assert(is_subtype_of(Bottom[GradualConstrainedCovariant[Any]], GradualConstrainedCovariant[str])) +static_assert(is_subtype_of(GradualConstrainedContravariant[int], Top[GradualConstrainedContravariant[Any]])) +static_assert(is_subtype_of(GradualConstrainedContravariant[str], Top[GradualConstrainedContravariant[Any]])) +static_assert(is_subtype_of(Bottom[GradualConstrainedContravariant[Any]], GradualConstrainedContravariant[int])) +static_assert(is_subtype_of(Bottom[GradualConstrainedContravariant[Any]], GradualConstrainedContravariant[str])) +``` + +## Overlapping generic constraints + +When one valid constraint is a subtype of another, the broader constraint supplies the upper bound +and the narrower constraint supplies the lower bound. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Any +from ty_extensions import Bottom, Top, static_assert +from ty_extensions._internal import is_equivalent_to + +class OverlappingCovariant[T: (int, bool)]: + def get(self) -> T: + raise NotImplementedError + +class OverlappingContravariant[T: (int, bool)]: + def put(self, value: T) -> None: ... + +static_assert(is_equivalent_to(Top[OverlappingCovariant[Any]], OverlappingCovariant[int])) +static_assert(is_equivalent_to(Bottom[OverlappingCovariant[Any]], OverlappingCovariant[bool])) +static_assert(is_equivalent_to(Top[OverlappingContravariant[Any]], OverlappingContravariant[bool])) +static_assert(is_equivalent_to(Bottom[OverlappingContravariant[Any]], OverlappingContravariant[int])) +``` + +## Mixed constrained and unconstrained type parameters + +A generic with both constrained and unconstrained parameters materializes each parameter +independently. Filtering the constrained parameter must not change the unconstrained parameter. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Any +from ty_extensions import Bottom, Intersection, Top, static_assert +from ty_extensions._internal import is_assignable_to, is_subtype_of + +type GradualInt = Intersection[int, Any] + +class MixedConstrained[T: (int, str), U]: + value: T + items: list[U] + +def mixed_constrained( + top: Top[MixedConstrained[Any, Any]], + bottom: Bottom[MixedConstrained[Any, Any]], +) -> None: + reveal_type(top) # revealed: Top[MixedConstrained[Any, Any]] + reveal_type(bottom) # revealed: Bottom[MixedConstrained[Any, Any]] + reveal_type(top.value) # revealed: int | str + reveal_type(top.items) # revealed: Top[list[Any]] + reveal_type(bottom.items) # revealed: Bottom[list[Any]] + reveal_type(bottom.value) # revealed: Never + +static_assert(is_subtype_of(MixedConstrained[int, int], Top[MixedConstrained[Any, Any]])) +static_assert(is_subtype_of(MixedConstrained[str, int], Top[MixedConstrained[Any, Any]])) +static_assert(is_subtype_of(Bottom[MixedConstrained[Any, Any]], MixedConstrained[int, int])) +static_assert(is_subtype_of(Bottom[MixedConstrained[Any, Any]], MixedConstrained[str, int])) +static_assert(is_subtype_of(MixedConstrained[int, int], Top[MixedConstrained[Any, int]])) +static_assert(is_assignable_to(MixedConstrained[int, str], Top[MixedConstrained[GradualInt, Any]])) +static_assert(not is_assignable_to(MixedConstrained[str, str], Top[MixedConstrained[GradualInt, Any]])) +static_assert(is_assignable_to(Bottom[MixedConstrained[GradualInt, Any]], MixedConstrained[int, int])) +static_assert(not is_assignable_to(Bottom[MixedConstrained[GradualInt, Any]], MixedConstrained[str, int])) +``` + +## Materialization does not force invalid recursive specializations + +An invalid self-referential bound must produce the expected diagnostics without forcing recursive +materialization. Invalid specializations recover as `Unknown`. + +```toml +[environment] +python-version = "3.12" +``` + +```py +# error: [invalid-type-arguments] +class RecursiveSpecialization[T: "RecursiveSpecialization[int]"]: ... + +# error: [invalid-type-arguments] +def recursive_specialization(value: RecursiveSpecialization[str]) -> None: + reveal_type(value) # revealed: RecursiveSpecialization[Unknown] +``` + ## Invalid use `Top[]` and `Bottom[]` are special forms that take a single argument. @@ -654,6 +1121,11 @@ def _( `Top[T]` and `Bottom[T]` are always fully static types. Therefore, they have only one materialization (themselves) and applying `Top` or `Bottom` again does nothing. +```toml +[environment] +python-version = "3.12" +``` + ```py from typing import Any from ty_extensions import Top, Bottom, static_assert @@ -666,6 +1138,66 @@ static_assert(is_equivalent_to(Bottom[Bottom[list[Any]]], Bottom[list[Any]])) static_assert(is_equivalent_to(Top[Bottom[list[Any]]], Bottom[list[Any]])) ``` +The same is true when a covariant specialization contains a recursive alias with a gradual invariant +branch. Materializing the recursive branch again must not unfold another layer. + +```py +class Covariant[T]: + def get(self) -> T: + raise NotImplementedError + +class Invariant[T]: + value: T + +type Recursive = Covariant[Recursive] | Invariant[Any] + +static_assert(is_equivalent_to(Top[Covariant[Recursive]], Top[Top[Covariant[Recursive]]])) +static_assert(is_equivalent_to(Bottom[Covariant[Recursive]], Bottom[Bottom[Covariant[Recursive]]])) +static_assert(is_equivalent_to(Top[Covariant[Recursive]], Bottom[Top[Covariant[Recursive]]])) +static_assert(is_equivalent_to(Bottom[Covariant[Recursive]], Top[Bottom[Covariant[Recursive]]])) +``` + +Both branches retain the requested materialization polarity. + +```py +def recursive_materializations(top: Top[Recursive], bottom: Bottom[Recursive]) -> None: + reveal_type(top) # revealed: Covariant[Top[Recursive]] | Top[Invariant[Any]] + reveal_type(bottom) # revealed: Covariant[Bottom[Recursive]] | Bottom[Invariant[Any]] +``` + +Nested recursive aliases preserve their materialization polarity in displays and diagnostics. + +```py +def nested_recursive_materializations(top: Top[Covariant[Recursive]], bottom: Bottom[Covariant[Recursive]]) -> None: + reveal_type(top) # revealed: Covariant[Top[Recursive]] + reveal_type(bottom) # revealed: Covariant[Bottom[Recursive]] + + # error: [invalid-assignment] "Object of type `Covariant[Top[Recursive]]` is not assignable to `Covariant[Bottom[Recursive]]`" + bottom = top +``` + +Explicitly constructed recursive aliases preserve the same materialized identity. + +```py +from typing_extensions import TypeAliasType + +ManualRecursive = TypeAliasType("ManualRecursive", "Covariant[ManualRecursive] | Invariant[Any]") + +static_assert(is_equivalent_to(Top[Covariant[ManualRecursive]], Top[Top[Covariant[ManualRecursive]]])) +``` + +Materialization also preserves the specialization of a recursive generic alias. + +```py +type GenericRecursive[T] = Covariant[GenericRecursive[T]] | Invariant[Any] | T + +static_assert(is_equivalent_to(Top[GenericRecursive[int]], Top[Top[GenericRecursive[int]]])) +static_assert(not is_equivalent_to(Top[GenericRecursive[int]], Top[GenericRecursive[str]])) + +def generic_recursive_materialization(value: Top[Covariant[GenericRecursive[int]]]) -> None: + reveal_type(value) # revealed: Covariant[Top[GenericRecursive[int]]] +``` + ## Subtyping Any `list[T]` is a subtype of `Top[list[Any]]`, but with more restrictive gradual types, not all @@ -936,3 +1468,1014 @@ def _(top: Top[FunctionHolder[Any]], bottom: Bottom[FunctionHolder[Any]]) -> Non # revealed: (def shared(self, value: Never) -> object, /) -> def shared(self, value: object) -> Never reveal_type(bottom.nested) ``` + +## Protocols + +Materializing a protocol maps each member according to how it is used. Reads are covariant and +writes are contravariant. + +```toml +[environment] +python-version = "3.12" +``` + +### Instance attributes + +For a mutable `Any` attribute, `Top` reads `object` and writes `Never`; `Bottom` does the reverse: + +```py +from typing import Any, Protocol +from ty_extensions import Bottom, Top + +class MutableAny(Protocol): + value: Any + +def mutable_top_attributes(top: Top[MutableAny]) -> None: + reveal_type(top) # revealed: Top[MutableAny] + reveal_type(top.value) # revealed: object + top.value = 1 # error: [invalid-assignment] + +def mutable_bottom_attributes(bottom: Bottom[MutableAny]) -> None: + reveal_type(bottom) # revealed: Bottom[MutableAny] + bottom.value = object() + reveal_type(bottom.value) # revealed: Never +``` + +The class object of a materialized protocol preserves its instance type when called directly or +passed through a generic callable: + +```py +from typing import Callable + +def invoke[T](factory: Callable[[], T]) -> T: + return factory() + +def constructors(top: Top[MutableAny]) -> None: + reveal_type(type(top)) # revealed: type[Top[MutableAny]] + reveal_type(type(top)()) # revealed: Top[MutableAny] + reveal_type(invoke(type(top))) # revealed: Top[MutableAny] + +def annotated_constructors(top: type[Top[MutableAny]], bottom: type[Bottom[MutableAny]]) -> None: + reveal_type(top) # revealed: type[Top[MutableAny]] + reveal_type(bottom) # revealed: type[Bottom[MutableAny]] + reveal_type(top()) # revealed: Top[MutableAny] + reveal_type(bottom()) # revealed: Bottom[MutableAny] + reveal_type(invoke(top)) # revealed: Top[MutableAny] + reveal_type(invoke(bottom)) # revealed: Bottom[MutableAny] +``` + +A protocol's constructor can explicitly return a value that is not an instance of the protocol. +Materializing the protocol must preserve that return type, including when its class object is +converted to a callable: + +```py +class IntConstructor(Protocol): + value: Any + + def __new__(cls) -> int: + return 1 + +def non_instance_constructors( + plain: type[IntConstructor], + top: type[Top[IntConstructor]], + bottom: type[Bottom[IntConstructor]], +) -> None: + reveal_type(invoke(plain)) # revealed: int + reveal_type(invoke(top)) # revealed: int + reveal_type(invoke(bottom)) # revealed: int +``` + +A custom protocol metaclass can likewise construct a value that is not a protocol instance. Its +`__call__` return type is preserved when the protocol is materialized. + +```py +class IntConstructorMetaclass(type(Protocol)): + def __call__(cls) -> int: + return 1 + +class MetaclassConstructor(Protocol, metaclass=IntConstructorMetaclass): + value: Any + +def metaclass_constructors( + plain: type[MetaclassConstructor], + top: type[Top[MetaclassConstructor]], + bottom: type[Bottom[MetaclassConstructor]], +) -> None: + reveal_type(invoke(plain)) # revealed: int + reveal_type(invoke(top)) # revealed: int + reveal_type(invoke(bottom)) # revealed: int +``` + +Overloaded constructors preserve each return type separately: an instance-returning overload uses +the materialized protocol, while an overload returning a different type retains that type. + +```py +from typing import Self, overload + +class MixedConstructor(Protocol): + value: Any + + @overload + def __new__(cls) -> Self: ... + @overload + def __new__(cls, value: int) -> int: ... + def __new__(cls, value: int | None = None) -> Self | int: + raise NotImplementedError + +def invoke_with_int[T](factory: Callable[[int], T]) -> T: + return factory(1) + +def mixed_constructors( + top: type[Top[MixedConstructor]], + bottom: type[Bottom[MixedConstructor]], +) -> None: + reveal_type(invoke(top)) # revealed: Top[MixedConstructor] + reveal_type(invoke(bottom)) # revealed: Bottom[MixedConstructor] + reveal_type(invoke_with_int(top)) # revealed: int + reveal_type(invoke_with_int(bottom)) # revealed: int +``` + +Materialization preserves sound class-member access: an ordinary instance attribute is not available +on `type[Top[MutableAny]]` or `type[Bottom[MutableAny]]`. + +```py +def class_instance_attributes(top: Top[MutableAny], bottom: Bottom[MutableAny]) -> None: + type(top).value # error: [unresolved-attribute] + type(bottom).value # error: [unresolved-attribute] + +def annotated_class_instance_attributes(top: type[Top[MutableAny]], bottom: type[Bottom[MutableAny]]) -> None: + top.value # error: [unresolved-attribute] + bottom.value # error: [unresolved-attribute] +``` + +### Writable properties + +A property setter is already a write, so its parameter is mapped only once: + +```py +from typing import Any, Protocol +from ty_extensions import Bottom, Top + +class WritableAny(Protocol): + @property + def value(self) -> Any: ... + @value.setter + def value(self, value: Any) -> None: ... + +def writable_top_property(top: Top[WritableAny]) -> None: + reveal_type(top.value) # revealed: object + top.value = 1 # error: [invalid-assignment] + +def writable_bottom_property(bottom: Bottom[WritableAny]) -> None: + bottom.value = object() + reveal_type(bottom.value) # revealed: Never +``` + +### Protocol relations + +`MutableAny` and `Top[MutableAny]` refer to the same protocol class, but they do not have the same +read and write requirements. Subtyping and union simplification must use those requirements: + +```py +from typing import Any, Protocol +from ty_extensions import Bottom, Top, static_assert +from ty_extensions._internal import is_subtype_of + +class MutableAny(Protocol): + value: Any + +static_assert(is_subtype_of(Bottom[MutableAny], MutableAny)) +static_assert(is_subtype_of(Bottom[MutableAny], Top[MutableAny])) +static_assert(is_subtype_of(MutableAny, Top[MutableAny])) +static_assert(not is_subtype_of(MutableAny, Bottom[MutableAny])) +static_assert(not is_subtype_of(Top[MutableAny], Bottom[MutableAny])) +static_assert(not is_subtype_of(Top[MutableAny], MutableAny)) + +def union_order( + plain_first: MutableAny | Top[MutableAny], + top_first: Top[MutableAny] | MutableAny, +) -> None: + reveal_type(plain_first) # revealed: Top[MutableAny] + reveal_type(top_first) # revealed: Top[MutableAny] + reveal_type(plain_first.value) # revealed: object + reveal_type(top_first.value) # revealed: object +``` + +Inheriting from a protocol must not bypass its materialized write requirement. A nominal subclass +and a structurally identical class therefore have the same result here: + +```py +class MutableAnySubclass(MutableAny): + value: int + +class StructuralMutableAny: + value: int + +static_assert(not is_subtype_of(MutableAnySubclass, Bottom[MutableAny])) +static_assert(not is_subtype_of(StructuralMutableAny, Bottom[MutableAny])) +``` + +An inherited `Any` member is materialized along with members declared directly on the protocol, so +it cannot satisfy a more specific inherited protocol: + +```py +class GenericBase[T](Protocol): + item: T + +class InheritedAny(GenericBase[Any], Protocol): + marker: Any + +def requires_int_base(value: GenericBase[int]) -> None: ... +def _(top: Top[InheritedAny]) -> None: + requires_int_base(top) # error: [invalid-argument-type] +``` + +Materializing an unrelated member does not erase explicit protocol inheritance, even when an +override is structurally incompatible with the base protocol. Materializing the fully static base +also preserves the nominal relationship: + +```py +class BaseProtocol(Protocol): + @property + def value(self) -> int: ... + +class ChildProtocol(BaseProtocol, Protocol): + marker: Any + + @property + def value(self) -> str: ... + +static_assert(is_subtype_of(Top[ChildProtocol], BaseProtocol)) +static_assert(is_subtype_of(ChildProtocol, Top[BaseProtocol])) +static_assert(is_subtype_of(ChildProtocol, Bottom[BaseProtocol])) +``` + +A covariant `Awaitable[int]` satisfies the top-materialized `Awaitable[object]` protocol. Narrowing +to that protocol must therefore preserve `Awaitable[int]` without retaining a redundant +intersection: + +```py +from typing import Awaitable +from typing_extensions import TypeIs + +static_assert(is_subtype_of(Awaitable[int], Top[Awaitable[object]])) + +def is_top_awaitable(value: object) -> TypeIs[Top[Awaitable[object]]]: + return True + +def narrow_awaitable(value: Awaitable[int]) -> None: + if is_top_awaitable(value): + reveal_type(value) # revealed: Awaitable[int] +``` + +### Class variables + +Class variables have separate read and write types. `Top` reads `object` and writes `Never`, while +`Bottom` reads `Never` and writes `object`. These requirements are preserved on both inferred and +explicitly annotated class objects: + +```py +from typing import Any, ClassVar, Protocol +from ty_extensions import Bottom, Top, static_assert +from ty_extensions._internal import is_subtype_of + +class ClassVarAny(Protocol): + value: ClassVar[Any] + +def class_writes(top: Top[ClassVarAny], bottom: Bottom[ClassVarAny]) -> None: + type(top).value = 1 # error: [invalid-assignment] + type(bottom).value = object() + +def class_reads(top: Top[ClassVarAny], bottom: Bottom[ClassVarAny]) -> None: + reveal_type(type(top).value) # revealed: object + reveal_type(type(bottom).value) # revealed: Never + +def annotated_class_writes(top: type[Top[ClassVarAny]], bottom: type[Bottom[ClassVarAny]]) -> None: + reveal_type(top) # revealed: type[Top[ClassVarAny]] + reveal_type(bottom) # revealed: type[Bottom[ClassVarAny]] + top.value = 1 # error: [invalid-assignment] + bottom.value = object() + +def annotated_class_reads(top: type[Top[ClassVarAny]], bottom: type[Bottom[ClassVarAny]]) -> None: + reveal_type(top.value) # revealed: object + reveal_type(bottom.value) # revealed: Never +``` + +Structural protocol checks use the mapped read and write types as well. `ClassVarInt` satisfies the +top-materialized protocol, but not the bottom-materialized one; a class missing the class variable +does not satisfy the top-materialized protocol: + +```py +class ClassVarInt: + value: ClassVar[int] = 1 + +class MissingClassVar: ... + +static_assert(is_subtype_of(ClassVarInt, Top[ClassVarAny])) +static_assert(not is_subtype_of(ClassVarInt, Bottom[ClassVarAny])) +top_class: type[Top[ClassVarAny]] = ClassVarInt +missing_top_class: type[Top[ClassVarAny]] = MissingClassVar # error: [invalid-assignment] +invalid_bottom_class: type[Bottom[ClassVarAny]] = ClassVarInt # error: [invalid-assignment] + +def materialized_bottom_class(bottom: Bottom[ClassVarAny]) -> None: + valid_bottom_class: type[Bottom[ClassVarAny]] = type(bottom) + reveal_type(valid_bottom_class) # revealed: type[Bottom[ClassVarAny]] +``` + +Union simplification preserves the materialized class variable regardless of operand order: + +```py +def class_union_order( + plain: ClassVarAny, + top: Top[ClassVarAny], + flag: bool, +) -> None: + plain_first = type(plain) if flag else type(top) + top_first = type(top) if flag else type(plain) + reveal_type(plain_first.value) # revealed: object + reveal_type(top_first.value) # revealed: object +``` + +### Methods through the class object + +Ordinary, static, and class methods use their materialized signatures when accessed through the +class object. Ordinary methods remain unbound: + +```py +from typing import Any, Protocol +from ty_extensions import Bottom, Top + +class DecoratedAny(Protocol): + def transform(self, value: Any) -> Any: ... + @staticmethod + def parse(value: Any) -> Any: ... + @classmethod + def create(cls, value: Any) -> Any: ... + +def decorated_class_access( + top: Top[DecoratedAny], + bottom: Bottom[DecoratedAny], +) -> None: + reveal_type(type(top).transform) # revealed: (self, /, value: Never) -> object + reveal_type(type(top).parse) # revealed: (value: Never) -> object + reveal_type(type(top).create) # revealed: (value: Never) -> object + reveal_type(type(bottom).transform) # revealed: (self, /, value: object) -> Never + reveal_type(type(bottom).parse) # revealed: (value: object) -> Never + reveal_type(type(bottom).create) # revealed: (value: object) -> Never +``` + +### Members outside the protocol interface + +`__init__` is not a protocol requirement, but accessing it on a materialized value still uses the +declaration on the protocol class: + +```py +from typing import Any, Protocol +from ty_extensions import Top + +class ProtocolWithInit(Protocol): + value: Any + + def __init__(self, value: int) -> None: ... + +def constructor(top: Top[ProtocolWithInit]) -> None: + reveal_type(top.__init__) # revealed: bound method Top[ProtocolWithInit].__init__(value: int) +``` + +### Read-only property deletion + +Materializing a read-only property must not make it deletable: + +```py +from typing import Any, Protocol +from typing_extensions import TypeIs +from ty_extensions import Top + +class ReadOnlyProperty(Protocol): + @property + def property(self) -> Any: ... + +def is_read_only_property(value: object) -> TypeIs[Top[ReadOnlyProperty]]: + return True + +def property_deletion( + top: Top[ReadOnlyProperty], + value: object, +) -> None: + del top.property # error: [invalid-assignment] + if is_read_only_property(value): + del value.property # error: [invalid-assignment] +``` + +### Descriptor-decorated properties + +A descriptor can expose separate read and write types. `Top` maps an `Any` read to `object` and an +`Any` write to `Never`; `Bottom` maps them in the opposite direction: + +```py +from typing import Any, Callable, Never, Protocol +from typing_extensions import TypeIs +from ty_extensions import Bottom, Top, static_assert +from ty_extensions._internal import is_subtype_of + +class Descriptor: + def __get__(self, instance: object, owner: type[object] | None = None) -> Any: ... + def __set__(self, instance: object, value: Any) -> None: ... + +def descriptor(function: Callable[..., Any]) -> Descriptor: + raise NotImplementedError + +class DescriptorProperty(Protocol): + @descriptor + def value(self) -> Any: ... + +class TopDescriptorProperty: + @property + def value(self) -> object: + return object() + + @value.setter + def value(self, value: Never) -> None: ... + +class NarrowBottomDescriptorProperty: + @property + def value(self) -> Never: + raise RuntimeError + + @value.setter + def value(self, value: int) -> None: ... + +static_assert(is_subtype_of(TopDescriptorProperty, Top[DescriptorProperty])) +static_assert(not is_subtype_of(NarrowBottomDescriptorProperty, Bottom[DescriptorProperty])) + +def top_descriptor_write(top: Top[DescriptorProperty]) -> None: + top.value = 1 # error: [invalid-assignment] + +def bottom_descriptor_write(bottom: Bottom[DescriptorProperty]) -> None: + bottom.value = object() + +def plain_descriptor_write(plain: DescriptorProperty) -> None: + plain.value = object() + +def is_descriptor_property(value: object) -> TypeIs[Top[DescriptorProperty]]: + return True + +def narrowed_descriptor_write(value: object) -> None: + if is_descriptor_property(value): + reveal_type(value) # revealed: Top[DescriptorProperty] + value.value = 1 # error: [invalid-assignment] +``` + +### Property accessor types + +Materializing a property with fully static exposed types is a no-op. The accessor's implicit +receiver and the setter's return type do not contribute to the property requirement: + +```py +from typing import Any, Protocol +from ty_extensions import Bottom, Top + +class FullyStaticProperty(Protocol): + @property + def value(self) -> int: ... + @value.setter + def value(self, value: int) -> Any: ... + +def fully_static_property( + top: Top[FullyStaticProperty], + bottom: Bottom[FullyStaticProperty], +) -> None: + reveal_type(top) # revealed: FullyStaticProperty + reveal_type(bottom) # revealed: FullyStaticProperty +``` + +### Assignment narrowing of materialized properties + +A materialized protocol exposes a property's return type, not the underlying descriptor, when +reading that property. Assignment narrowing must still recover the descriptor: its setter can +transform the assigned value, so the next read must not narrow to the assigned literal. + +```py +from typing import Any, Protocol +from ty_extensions import Bottom, Top + +class TransformingProperty(Protocol): + marker: Any + + @property + def value(self) -> int: ... + @value.setter + def value(self, value: int) -> None: ... + +def materialized_property_assignment_narrowing( + top: Top[TransformingProperty], + bottom: Bottom[TransformingProperty], +) -> None: + top.value = 1 + reveal_type(top.value) # revealed: int + bottom.value = 2 + reveal_type(bottom.value) # revealed: int +``` + +### Generic inference through inherited and structural protocols + +Generic inference uses a member's materialized type, not its original `Any`. This applies both to +inherited members and to the finite requirements of independently declared structural protocols. +Bounds, constraints, and invariant requirements must still reject an incompatible materialized +member instead of accepting an invalid call or selecting the wrong overload: + +```py +from typing import Any, Literal, Protocol, TypeVar, overload +from ty_extensions import Top + +class InferenceBase[T](Protocol): + @property + def item(self) -> T: ... + +class InheritedInferenceAny(InferenceBase[Any], Protocol): + marker: Any + +class StructuralInferenceAny(Protocol): + @property + def item(self) -> Any: ... + +def infer_item[T](value: InferenceBase[T]) -> T: + raise NotImplementedError + +def materialized_inference(inherited: Top[InheritedInferenceAny]) -> None: + reveal_type(infer_item(inherited)) # revealed: object + +def materialized_structural_inference(structural: Top[StructuralInferenceAny]) -> None: + reveal_type(infer_item(structural)) # revealed: object + +def bounded_item[T: str](value: InferenceBase[T]) -> T: + raise NotImplementedError + +def union_bounded_item[T: str | bytes](value: InferenceBase[T]) -> T: + raise NotImplementedError + +def constrained_item[T: (str, bytes)](value: InferenceBase[T]) -> T: + raise NotImplementedError + +LegacyConstrained = TypeVar("LegacyConstrained", str, bytes) + +def legacy_constrained_item(value: InferenceBase[LegacyConstrained]) -> LegacyConstrained: + raise NotImplementedError + +def invalid_materialized_bounds( + inherited: Top[InheritedInferenceAny], + structural: Top[StructuralInferenceAny], +) -> None: + bounded_item(inherited) # error: [invalid-argument-type] + bounded_item(structural) # error: [invalid-argument-type] + union_bounded_item(inherited) # error: [invalid-argument-type] + union_bounded_item(structural) # error: [invalid-argument-type] + constrained_item(inherited) # error: [invalid-argument-type] + constrained_item(structural) # error: [invalid-argument-type] + legacy_constrained_item(inherited) # error: [invalid-argument-type] + legacy_constrained_item(structural) # error: [invalid-argument-type] + +def consistent_item[T](value: InferenceBase[T], values: list[T]) -> T: + raise NotImplementedError + +def invalid_materialized_invariant_arguments( + inherited: Top[InheritedInferenceAny], + structural: Top[StructuralInferenceAny], + values: list[int], +) -> None: + consistent_item(inherited, values) # error: [invalid-argument-type] + consistent_item(structural, values) # error: [invalid-argument-type] + +class InvariantInferenceBase[T](Protocol): + item: T + +class InheritedInvariantAny(InvariantInferenceBase[Any], Protocol): + marker: Any + +class StructuralInvariantAny(Protocol): + item: Any + +def invariant_item[T](value: InvariantInferenceBase[T], required: T) -> T: + raise NotImplementedError + +def invalid_materialized_invariant_members( + inherited: Top[InheritedInvariantAny], + structural: Top[StructuralInvariantAny], +) -> None: + invariant_item(inherited, "required") # error: [invalid-argument-type] + invariant_item(structural, "required") # error: [invalid-argument-type] + +@overload +def select_item[T: str](value: InferenceBase[T]) -> Literal["bounded"]: ... +@overload +def select_item(value: object) -> Literal["fallback"]: ... +def select_item(value: object) -> Literal["bounded", "fallback"]: + return "fallback" + +@overload +def select_specific_item(value: InferenceBase[str]) -> Literal["str"]: ... +@overload +def select_specific_item(value: InferenceBase[bytes]) -> Literal["bytes"]: ... +def select_specific_item(value: object) -> str: + raise NotImplementedError + +def materialized_overload_resolution( + inherited: Top[InheritedInferenceAny], + structural: Top[StructuralInferenceAny], + valid: InferenceBase[str], +) -> None: + reveal_type(select_item(inherited)) # revealed: Literal["fallback"] + reveal_type(select_item(structural)) # revealed: Literal["fallback"] + reveal_type(select_item(valid)) # revealed: Literal["bounded"] + select_specific_item(inherited) # error: [no-matching-overload] + select_specific_item(structural) # error: [no-matching-overload] + +@overload +def select_consistent_item[T](value: InferenceBase[T], values: list[T]) -> T: ... +@overload +def select_consistent_item(value: object, values: list[int]) -> object: ... +def select_consistent_item(value: object, values: object) -> object: + raise NotImplementedError + +def materialized_invariant_overload_resolution( + inherited: Top[InheritedInferenceAny], + structural: Top[StructuralInferenceAny], + valid: InferenceBase[int], + values: list[int], +) -> None: + reveal_type(select_consistent_item(inherited, values)) # revealed: object + reveal_type(select_consistent_item(structural, values)) # revealed: object + reveal_type(select_consistent_item(valid, values)) # revealed: int +``` + +### Generic inference through recursive structural protocols + +A recursive protocol requirement must not cause inference to discard a structurally matching +protocol's materialization. The nonrecursive property establishes the correct specialization without +expanding the recursive property. + +```py +from __future__ import annotations + +from typing import Any, Literal, Protocol, overload +from ty_extensions import Bottom, Top, static_assert +from ty_extensions._internal import is_subtype_of + +class RecursiveValue[T](Protocol): + @property + def value(self) -> T: ... + @property + def child(self) -> RecursiveValue[T]: ... + +class RecursiveAny(Protocol): + @property + def value(self) -> Any: ... + @property + def child(self) -> RecursiveAny: ... + +static_assert(is_subtype_of(Top[RecursiveAny], RecursiveValue[object])) +static_assert(not is_subtype_of(Top[RecursiveAny], RecursiveValue[str])) +static_assert(is_subtype_of(Bottom[RecursiveAny], RecursiveValue[str])) +``` + +Inference preserves both materialization polarities: the top-materialized property infers `object`, +while the bottom-materialized property infers `Never`. + +```py +def infer_recursive_value[T](value: RecursiveValue[T]) -> T: + raise NotImplementedError + +def recursive_materialized_inference( + top: Top[RecursiveAny], + bottom: Bottom[RecursiveAny], + valid: RecursiveValue[str], +) -> None: + reveal_type(top.value) # revealed: object + reveal_type(infer_recursive_value(top)) # revealed: object + reveal_type(infer_recursive_value(valid)) # revealed: str + reveal_type(infer_recursive_value(bottom)) # revealed: Never +``` + +The nonrecursive property is used only to infer the specialization. The complete protocol must still +be checked, so a matching `value` cannot hide an incompatible `child`. + +```py +class WrongRecursiveAny(Protocol): + @property + def value(self) -> Any: ... + @property + def child(self) -> int: ... + +static_assert(not is_subtype_of(Top[WrongRecursiveAny], RecursiveValue[object])) + +def reject_incompatible_recursive_child(wrong: Top[WrongRecursiveAny]) -> None: + infer_recursive_value(wrong) # error: [invalid-argument-type] +``` + +A top-materialized `object` cannot satisfy a `str` bound or a `str`/`bytes` constraint. An ordinary +`RecursiveValue[str]` still satisfies both. + +```py +def bounded_recursive_value[T: str](value: RecursiveValue[T]) -> T: + raise NotImplementedError + +def constrained_recursive_value[T: (str, bytes)](value: RecursiveValue[T]) -> T: + raise NotImplementedError + +def recursive_materialized_bounds( + top: Top[RecursiveAny], + valid: RecursiveValue[str], +) -> None: + bounded_recursive_value(top) # error: [invalid-argument-type] + constrained_recursive_value(top) # error: [invalid-argument-type] + reveal_type(bounded_recursive_value(valid)) # revealed: str + reveal_type(constrained_recursive_value(valid)) # revealed: str +``` + +An invariant `list[T]` cannot narrow the materialized `object` property to `int`. + +```py +def infer_recursive_with_list[T](value: RecursiveValue[T], values: list[T]) -> T: + raise NotImplementedError + +def recursive_materialized_invariant_arguments( + top: Top[RecursiveAny], + valid: RecursiveValue[str], + ints: list[int], + strings: list[str], +) -> None: + infer_recursive_with_list(top, ints) # error: [invalid-argument-type] + reveal_type(infer_recursive_with_list(valid, strings)) # revealed: str +``` + +Overload resolution also respects the bound and the complete recursive requirement. Both an +incompatible materialized property and an incompatible child select the fallback or fail when no +fallback is available; the valid `str` specialization selects the bounded overload. + +```py +@overload +def select_recursive_value[T: str](value: RecursiveValue[T]) -> Literal["bounded"]: ... +@overload +def select_recursive_value(value: object) -> Literal["fallback"]: ... +def select_recursive_value(value: object) -> Literal["bounded", "fallback"]: + return "fallback" + +@overload +def select_specific_recursive_value(value: RecursiveValue[str]) -> Literal["str"]: ... +@overload +def select_specific_recursive_value(value: RecursiveValue[bytes]) -> Literal["bytes"]: ... +def select_specific_recursive_value(value: object) -> str: + raise NotImplementedError + +def recursive_materialized_overload_resolution( + top: Top[RecursiveAny], + wrong: Top[WrongRecursiveAny], + valid: RecursiveValue[str], +) -> None: + reveal_type(select_recursive_value(top)) # revealed: Literal["fallback"] + reveal_type(select_recursive_value(wrong)) # revealed: Literal["fallback"] + reveal_type(select_recursive_value(valid)) # revealed: Literal["bounded"] + select_specific_recursive_value(top) # error: [no-matching-overload] + select_specific_recursive_value(wrong) # error: [no-matching-overload] + reveal_type(select_specific_recursive_value(valid)) # revealed: Literal["str"] +``` + +### Generator delegation + +`yield from` uses the same materialized yield and return types as direct generator methods. Applying +another materialization must not change a result that no longer contains `Any`: + +```py +from collections.abc import Generator +from typing import Any, Protocol +from ty_extensions import Bottom, Top + +class MaterializedGenerator(Generator[Any, Any, Any], Protocol): + marker: Any + +def generator_delegation( + generator: Top[MaterializedGenerator], + nested: Bottom[Top[MaterializedGenerator]], +): + reveal_type(generator.__next__()) # revealed: object + result = yield from generator + reveal_type(result) # revealed: object + nested_result = yield from nested + reveal_type(nested_result) # revealed: object +``` + +The send type is contravariant. A top-materialized generator cannot accept values sent by a +`Generator[object, object, object]`, while a bottom-materialized generator can: + +```py +def top_generator_send( + generator: Top[MaterializedGenerator], +) -> Generator[object, object, object]: + result = yield from generator # error: [invalid-yield] + return result + +def bottom_generator_send( + generator: Bottom[MaterializedGenerator], +) -> Generator[object, object, object]: + result = yield from generator + return result +``` + +### `Self` binding + +`Self` may appear in `Top[GenericProtocol[Self]]` even when the protocol member itself is `Any`. It +must still bind to the class through which the attribute is accessed: + +```py +from typing import Any, Protocol, Self +from ty_extensions import Top + +class GenericProtocol[T](Protocol): + value: Any + +class SelfContainer: + member: Top[GenericProtocol[Self]] + +class SelfContainerChild(SelfContainer): + pass + +reveal_type(SelfContainerChild().member) # revealed: Top[GenericProtocol[SelfContainerChild]] +``` + +### Legacy type variables + +A legacy type variable in the protocol's type arguments still makes the enclosing function generic: + +```py +from typing import Any, Protocol, TypeVar +from ty_extensions import Top + +T = TypeVar("T") + +class LegacyProtocol(Protocol[T]): + value: Any + +def accepts_legacy(value: Top[LegacyProtocol[T]]) -> None: ... + +reveal_type(accepts_legacy) # revealed: def accepts_legacy[T](value: Top[LegacyProtocol[T]]) +``` + +### Generic aliases + +Expanding a generic alias preserves the materialized write type: + +```py +from typing import Any, Protocol +from ty_extensions import Bottom, Top + +class GenericMutable[T](Protocol): + value: T + +type MutableAlias[T] = GenericMutable[T] + +def alias_writes( + top: Top[MutableAlias[Any]], + bottom: Bottom[MutableAlias[Any]], +) -> None: + top.value = 1 # error: [invalid-assignment] + bottom.value = object() + +def annotated_generic_protocol_classes( + top: type[Top[GenericMutable[Any]]], + bottom: type[Bottom[GenericMutable[Any]]], + aliased_top: type[Top[MutableAlias[Any]]], + aliased_bottom: type[Bottom[MutableAlias[Any]]], +) -> None: + reveal_type(top) # revealed: type[Top[GenericMutable[Any]]] + reveal_type(bottom) # revealed: type[Bottom[GenericMutable[Any]]] + reveal_type(aliased_top) # revealed: type[Top[GenericMutable[Any]]] + reveal_type(aliased_bottom) # revealed: type[Bottom[GenericMutable[Any]]] +``` + +### Nested generic protocols + +A protocol nested inside another generic type preserves its separate read and write requirements +after materialization: + +```py +from typing import Any, Protocol +from ty_extensions import Bottom, Top + +class Leaf[T](Protocol): + value: T + +class Outer[T](Protocol): + leaf: Leaf[T] + +class ReadHolder[T]: + @property + def outer(self) -> Outer[T]: + raise NotImplementedError + +def nested_specialization( + holder: Top[ReadHolder[Any]], + top_leaf: Top[Leaf[Any]], + bottom_leaf: Bottom[Leaf[Any]], +) -> None: + reveal_type(holder.outer) # revealed: Top[Outer[Any]] + holder.outer.leaf = bottom_leaf + holder.outer.leaf = top_leaf # error: [invalid-assignment] +``` + +### Class-backed protocol specialization during interface construction + +An ordinary specialization of a class-backed protocol only maps its class specialization. It must +not inspect the protocol interface, because the specialization can occur while that same interface +is being constructed: + +```py +from __future__ import annotations + +from typing import Generic, Protocol, TypeVar, overload + +S = TypeVar("S") +T = TypeVar("T") + +class Unit(Protocol): + def __mul__(self, other: S | Quantity[S]): ... + +class Vector(Protocol): ... + +class Quantity(Generic[T], Protocol): + @overload + def __mul__(self, other: Unit | Quantity[S]): ... + @overload + def __mul__(self, other: Vector) -> Vector: ... +``` + +### Recursive protocols + +Materializing a recursive protocol preserves its wrapper without eagerly expanding its recursive +interface. Nonrecursive members are still materialized, and following the recursive child preserves +both the protocol and its materialization polarity. + +```py +from typing import Any, Protocol +from ty_extensions import Bottom, Top, static_assert +from ty_extensions._internal import is_equivalent_to + +type RecursiveAlias = RecursiveProtocol + +class RecursiveProtocol(Protocol): + marker: Any + + @property + def child(self) -> RecursiveAlias: ... + +static_assert(is_equivalent_to(Top[RecursiveProtocol], Top[Top[RecursiveProtocol]])) +static_assert(is_equivalent_to(Bottom[RecursiveProtocol], Bottom[Bottom[RecursiveProtocol]])) +static_assert(is_equivalent_to(Top[RecursiveProtocol], Bottom[Top[RecursiveProtocol]])) +static_assert(is_equivalent_to(Bottom[RecursiveProtocol], Top[Bottom[RecursiveProtocol]])) + +def recursive_top_materialization(top: Top[RecursiveProtocol]) -> None: + reveal_type(top) # revealed: Top[RecursiveProtocol] + reveal_type(top.marker) # revealed: object + top.marker = 1 # error: [invalid-assignment] + + reveal_type(top.child) # revealed: Top[RecursiveProtocol] + reveal_type(top.child.child) # revealed: Top[RecursiveProtocol] + reveal_type(top.child.marker) # revealed: object + top.child.marker = 1 # error: [invalid-assignment] + +def recursive_bottom_children(bottom: Bottom[RecursiveProtocol]) -> None: + reveal_type(bottom) # revealed: Bottom[RecursiveProtocol] + reveal_type(bottom.child) # revealed: Bottom[RecursiveProtocol] + reveal_type(bottom.child.child) # revealed: Bottom[RecursiveProtocol] + bottom.child.marker = object() + reveal_type(bottom.child.marker) # revealed: Never + +def recursive_bottom_marker(bottom: Bottom[RecursiveProtocol]) -> None: + bottom.marker = object() + reveal_type(bottom.marker) # revealed: Never + +def recursive_nested_materialization( + nested_top: Top[Top[RecursiveProtocol]], + nested_bottom: Bottom[Bottom[RecursiveProtocol]], +) -> None: + reveal_type(nested_top) # revealed: Top[RecursiveProtocol] + reveal_type(nested_top.marker) # revealed: object + reveal_type(nested_bottom) # revealed: Bottom[RecursiveProtocol] + reveal_type(nested_bottom.marker) # revealed: Never +``` + +### Display + +Materialized protocols display `Top` and `Bottom` around the protocol class: + +```py +from typing import Any, Protocol +from ty_extensions import Bottom, Top + +class ReadAny(Protocol): + @property + def value(self) -> Any: ... + +def _(top: Top[ReadAny], bottom: Bottom[ReadAny]) -> None: + reveal_type(top) # revealed: Top[ReadAny] + reveal_type(bottom) # revealed: Bottom[ReadAny] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/quantification.md b/crates/ty_python_semantic/resources/mdtest/type_properties/quantification.md new file mode 100644 index 0000000000..f8d1cd2434 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/quantification.md @@ -0,0 +1,313 @@ +# Quantification + +Quantification removes typevars from a constraint set, returning a new equivalent constraint set +that only references the remaining typevars. With existential quantification (`exists`), the result +holds when there is _at least one_ valid assignment of the removed variables that satisfies the +quantified expression. With universal quantification (`for_all`), the result holds when _every_ +valid assignment satisfies the quantified expression. + +This file contains several baseline test cases that validate our implementation of quantification. + +| Case | Formula | Expected result | +| ---- | -------------------------------- | ------------------------------------------- | +| C0 | `∃X. X = int ∧ A ≤ Invariant[X]` | Equivalent to `A ≤ Invariant[int]` | +| E1 | `∃X. U ≤ X ∧ X = V` | Equivalent to `U ≤ V` | +| E2 | `∃X. A ≤ Invariant[X] ∧ X ≤ B` | `A` and `B` must admit a common `X` | +| E3 | `∃X. A ≤ X ∧ Invariant[X] ≤ B` | `A` and `B` must admit a common `X` | +| E4 | `∃X. C₁(X, Y) ∧ C₂(X, Z)` | Solutions for `Y` and `Z` remain correlated | +| E5 | `∃X ∈ {int, str}. C(X, Y, Z)` | Solutions remain paired with each choice | +| E6 | `∀Y ∈ Dᵧ. ∃X ∈ Dₓ. R(X, Y)` | `X` may depend on the choice of `Y` | + +```toml +[environment] +python-version = "3.13" +``` + +## C0: grounded invariant + +In `∃X. X = int ∧ A ≤ Invariant[X]`, every assignment of `X` is _valid_ (i.e., satisfies the +implicit upper bound of `object`), but the only _satisfying_ assignment is `X = int`. That means the +result should be equivalent to `A ≤ Invariant[int]`. + +```py +from ty_extensions import static_assert +from ty_extensions._internal import ConstraintSet + +class Invariant[T]: + def get(self) -> T: + raise NotImplementedError + + def set(self, value: T) -> None: ... + +def grounded[X, A]() -> None: + # ∃X. X = int ∧ A ≤ Invariant[X] + body = ConstraintSet.equality(X, int) & ConstraintSet.upper_bound(A, Invariant[X]) + quantified = body.exists(tuple[X]) + + # TODO: revealed: tuple[Solution[X=int, A=list[int]]] + # revealed: tuple[Solution[X=int, A=Invariant[int] & Invariant[X@grounded]]] + reveal_type(body.solutions(inferable=tuple[X, A])) + # TODO: revealed: tuple[Solution[A=list[int]]] + # revealed: tuple[Solution[A=Invariant[int]]] + reveal_type(quantified.solutions(inferable=tuple[A])) + + # A ≤ Invariant[int] + expected = ConstraintSet.upper_bound(A, Invariant[int]) + static_assert(quantified == expected) + static_assert(~quantified == ~expected) +``` + +## E1: relational bridge + +There is an `X` satisfying `U ≤ X ∧ X = V` exactly when `U ≤ V`. + +```py +from ty_extensions import static_assert +from ty_extensions._internal import ConstraintSet + +def relational_bridge[X, U, V]() -> None: + # ∃X. U ≤ X ∧ X = V + body = ConstraintSet.upper_bound(U, X) & ConstraintSet.equality(X, V) + quantified = body.exists(tuple[X]) + + # TODO: revealed: tuple[Solution[V=object, U=object]] + # revealed: tuple[Solution[V=U@relational_bridge, U=V@relational_bridge]] + reveal_type(quantified.solutions(inferable=tuple[U, V])) + + # U ≤ V + expected = ConstraintSet.upper_bound(U, V) + static_assert(quantified == expected) + static_assert(~quantified == ~expected) +``` + +## E2: open invariant inverse image + +A specialization satisfies `∃X. A ≤ Invariant[X] ∧ X ≤ B` only if there is some `X` compatible with +both `A` and `B`. `A = Invariant[str]` and `B ≤ int` cannot satisfy the expression, so they must +satisfy its negation. + +```py +from ty_extensions import static_assert +from ty_extensions._internal import ConstraintSet + +class Invariant[T]: + def get(self) -> T: + raise NotImplementedError + + def set(self, value: T) -> None: ... + +def inverse_image[X, A, B]() -> None: + # ∃X. A ≤ Invariant[X] ∧ X ≤ B + body = ConstraintSet.upper_bound(A, Invariant[X]) & ConstraintSet.upper_bound(X, B) + quantified = body.exists(tuple[X]) + + # TODO: revealed: tuple[Solution[A=Invariant[object], B=object, X=object]] + # revealed: tuple[Solution[A=Invariant[X@inverse_image], B=X@inverse_image, X=B@inverse_image]] + reveal_type(body.solutions(inferable=tuple[X, A, B])) + # TODO: revealed: tuple[Solution[A=Invariant[object], B=object]] + # revealed: tuple[()] + reveal_type(quantified.solutions(inferable=tuple[A, B])) + + # Invariant[str] ≤ A ∧ B ≤ int + invalid = ConstraintSet.lower_bound(Invariant[str], A) & ConstraintSet.upper_bound(B, int) + # revealed: None + reveal_type((body & invalid).solutions(inferable=tuple[X, A, B])) + # TODO: revealed: None + # revealed: tuple[Solution[A=Invariant[str], B=int]] + reveal_type((quantified & invalid).solutions(inferable=tuple[A, B])) + + static_assert(not (quantified & invalid)) + # TODO: no error + # error: [static-assert-error] + static_assert((~quantified & invalid) == invalid) +``` + +## E3: witness-sensitive image + +For `∃X. A ≤ X ∧ Invariant[X] ≤ B`, each choice of `X` determines which values of `A` and `B` can +satisfy the expression. `A ≥ int` and `B ≤ Invariant[str]` cannot satisfy it, so they must satisfy +its negation. + +```py +from ty_extensions import static_assert +from ty_extensions._internal import ConstraintSet + +class Invariant[T]: + def get(self) -> T: + raise NotImplementedError + + def set(self, value: T) -> None: ... + +def witness_sensitive[X, A, B]() -> None: + # ∃X. A ≤ X ∧ Invariant[X] ≤ B + body = ConstraintSet.lower_bound(A, X) & ConstraintSet.lower_bound(Invariant[X], B) + quantified = body.exists(tuple[X]) + + # Each solution for A and B depends on the compatible choice of X. + # TODO: revealed: tuple[Solution[X=object, A=object, B=Invariant[object]]] + # revealed: tuple[Solution[A=X@witness_sensitive, X=A@witness_sensitive, B=Invariant[X@witness_sensitive]]] + reveal_type(body.solutions(inferable=tuple[X, A, B])) + # TODO: revealed: tuple[Solution[A=object, B=Invariant[object]]] + # revealed: tuple[()] + reveal_type(quantified.solutions(inferable=tuple[A, B])) + + # int ≤ A ∧ B ≤ Invariant[str] + invalid = ConstraintSet.lower_bound(int, A) & ConstraintSet.upper_bound(B, Invariant[str]) + # revealed: None + reveal_type((body & invalid).solutions(inferable=tuple[X, A, B])) + # TODO: revealed: None + # revealed: tuple[Solution[A=int, B=Invariant[str]]] + reveal_type((quantified & invalid).solutions(inferable=tuple[A, B])) + + static_assert(not (quantified & invalid)) + # TODO: no error + # error: [static-assert-error] + static_assert((~quantified & invalid) == invalid) +``` + +## E4: correlated visible outputs + +`C₁` relates `X` to `Y`, while `C₂` relates `X` to `Z`. Both constraints must hold for the same +choice of `X`. The two valid solution families are `(Y = int, Z = Invariant[int])` and +`(Y = str, Z = Invariant[str])`; the cross-pairing `(Y = int, Z = Invariant[str])` is invalid. + +```py +from ty_extensions import static_assert +from ty_extensions._internal import ConstraintSet + +class Invariant[T]: + def get(self) -> T: + raise NotImplementedError + + def set(self, value: T) -> None: ... + +def correlated_outputs[X, Y, Z]() -> None: + # C₁(X, Y) = (X = int ∧ Y = int) ∨ (X = str ∧ Y = str) + c1_int = ConstraintSet.equality(X, int) & ConstraintSet.equality(Y, int) + c1_str = ConstraintSet.equality(X, str) & ConstraintSet.equality(Y, str) + c1 = c1_int | c1_str + + # C₂(X, Z) = (Z = Invariant[X]) + c2 = ConstraintSet.equality(Z, Invariant[X]) + + # ∃X. C₁(X, Y) ∧ C₂(X, Z) + body = c1 & c2 + quantified = body.exists(tuple[X]) + + # TODO: revealed: tuple[Solution[X=int, Y=int, Z=Invariant[int]], Solution[X=str, Y=str, Z=Invariant[str]]] + # revealed: tuple[Solution[X=int | Y@correlated_outputs, Z=Invariant[X@correlated_outputs] | Invariant[int], Y=int], Solution[X=str | Y@correlated_outputs, Z=Invariant[X@correlated_outputs] | Invariant[str], Y=str]] + reveal_type(body.solutions(inferable=tuple[X, Y, Z])) + # revealed: tuple[Solution[Y=int, Z=Invariant[int]], Solution[Y=str, Z=Invariant[str]]] + reveal_type(quantified.solutions(inferable=tuple[Y, Z])) + + # (Y = int ∧ Z = Invariant[int]) ∨ (Y = str ∧ Z = Invariant[str]) + expected_int = ConstraintSet.equality(Y, int) & ConstraintSet.equality(Z, Invariant[int]) + expected_str = ConstraintSet.equality(Y, str) & ConstraintSet.equality(Z, Invariant[str]) + expected = expected_int | expected_str + static_assert(quantified == expected) + static_assert(~quantified == ~expected) + + # (Y = int ∧ Z = Invariant[str]) + invalid_cross = ConstraintSet.equality(Y, int) & ConstraintSet.equality(Z, Invariant[str]) + static_assert(not (quantified & invalid_cross)) + # revealed: None + reveal_type((quantified & invalid_cross).solutions(inferable=tuple[Y, Z])) +``` + +## E5: finite domain + +The declaration of `X` constrains it to be either `int` or `str`. Each valid choice gives a separate +solution family. After `X` is quantified, `Y` and `Z` must remain correlated in each solution, and +specializations outside the declared domain must be rejected. + +```py +from ty_extensions import static_assert +from ty_extensions._internal import ConstraintSet + +class Invariant[T]: + def get(self) -> T: + raise NotImplementedError + + def set(self, value: T) -> None: ... + +def finite_domain[X: (int, str), Y, Z]() -> None: + # ∃X ∈ {int, str}. C(X, Y, Z) + # C(X, Y, Z) = (Y = X) ∧ (Z = Invariant[X]) + body = ConstraintSet.equality(Y, X) & ConstraintSet.equality(Z, Invariant[X]) + quantified = body.exists(tuple[X]) + + # TODO: revealed: tuple[Solution[X=int, Y=int, Z=Invariant[int]], Solution[X=str, Y=str, Z=Invariant[str]]] + # revealed: tuple[Solution[X=Y@finite_domain, Y=X@finite_domain, Z=Invariant[X@finite_domain] | Invariant[Y@finite_domain]]] + reveal_type(body.solutions(inferable=tuple[X, Y, Z])) + # TODO: revealed: tuple[Solution[Y=int, Z=Invariant[int]], Solution[Y=str, Z=Invariant[str]]] + # revealed: tuple[Solution[Z=Invariant[Y@finite_domain]]] + reveal_type(quantified.solutions(inferable=tuple[Y, Z])) + + # (Y = int ∧ Z = Invariant[int]) ∨ (Y = str ∧ Z = Invariant[str]) + expected_int = ConstraintSet.equality(Y, int) & ConstraintSet.equality(Z, Invariant[int]) + expected_str = ConstraintSet.equality(Y, str) & ConstraintSet.equality(Z, Invariant[str]) + expected = expected_int | expected_str + # TODO: no error + # error: [static-assert-error] + static_assert(quantified == expected) + # TODO: no error + # error: [static-assert-error] + static_assert(~quantified == ~expected) + + # (Y = int ∧ Z = Invariant[str]) + invalid_cross = ConstraintSet.equality(Y, int) & ConstraintSet.equality(Z, Invariant[str]) + static_assert(not (quantified & invalid_cross)) + # revealed: None + reveal_type((quantified & invalid_cross).solutions(inferable=tuple[Y, Z])) + + # (Y = bytes ∧ Z = Invariant[bytes]) + invalid_domain = ConstraintSet.equality(Y, bytes) & ConstraintSet.equality(Z, Invariant[bytes]) + static_assert(not (quantified & invalid_domain)) + # TODO: revealed: None + # revealed: tuple[Solution[Z=Invariant[Y@finite_domain] | Invariant[bytes], Y=bytes]] + reveal_type((quantified & invalid_domain).solutions(inferable=tuple[Y, Z])) +``` + +## E6: alternation and negative polarity + +The declarations of `X` and `Y` constrain both variables to `int` or `str`. For every valid choice +of `Y`, there is a matching choice of `X`. Reversing the quantifiers would require one choice of `X` +to work for every `Y` and is therefore false. Negating the relation asks whether there is a `Y` with +no matching `X`; an `int`-only relation shows that a missing `str` case is rejected. + +```py +from ty_extensions import static_assert +from ty_extensions._internal import ConstraintSet + +def alternation[X: (int, str), Y: (int, str)]() -> None: + # R(X, Y) = (X = int ∧ Y = int) ∨ (X = str ∧ Y = str) + x_int = ConstraintSet.equality(X, int) + x_str = ConstraintSet.equality(X, str) + y_int = ConstraintSet.equality(Y, int) + y_str = ConstraintSet.equality(Y, str) + relation = (x_int & y_int) | (x_str & y_str) + + # ∀Y. ∃X. R(X, Y) + forall_y_exists_x = relation.exists(tuple[X]).for_all(tuple[Y]) + # TODO: no error + # error: [static-assert-error] + static_assert(forall_y_exists_x) + # TODO: no error + # error: [static-assert-error] + static_assert(not ~forall_y_exists_x) + + # ∃X. ∀Y. R(X, Y) + exists_x_forall_y = relation.for_all(tuple[Y]).exists(tuple[X]) + static_assert(not exists_x_forall_y) + + # ∃Y. ∀X. ¬R(X, Y) + counterexample = (~relation).for_all(tuple[X]).exists(tuple[Y]) + # TODO: no error + # error: [static-assert-error] + static_assert(not counterexample) + static_assert(counterexample == ~forall_y_exists_x) + + int_only = x_int & y_int + missing_str = int_only.exists(tuple[X]).for_all(tuple[Y]) + static_assert(not missing_str) +``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/satisfied_by_all_typevars.md b/crates/ty_python_semantic/resources/mdtest/type_properties/satisfied_by_all_typevars.md index c220b10116..1c32e94e33 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/satisfied_by_all_typevars.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/satisfied_by_all_typevars.md @@ -49,7 +49,7 @@ set. In a non-inferable position, that means the constraint set must be satisfie type. ```py -from typing import final, Never +from typing import final from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet @@ -68,24 +68,24 @@ def unbounded[T](): static_assert(not ConstraintSet.never().satisfied_by_all_typevars()) # (T = Never) is a valid specialization, which satisfies (T ≤ Unrelated). - static_assert(ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, Unrelated).satisfied_by_all_typevars(inferable=tuple[T])) # (T = Base) is a valid specialization, which does not satisfy (T ≤ Unrelated). - static_assert(not ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars()) + static_assert(not ConstraintSet.upper_bound(T, Unrelated).satisfied_by_all_typevars()) # (T = Base) is a valid specialization, which satisfies (T ≤ Super). - static_assert(ConstraintSet.range(Never, T, Super).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, Super).satisfied_by_all_typevars(inferable=tuple[T])) # (T = Unrelated) is a valid specialization, which does not satisfy (T ≤ Super). - static_assert(not ConstraintSet.range(Never, T, Super).satisfied_by_all_typevars()) + static_assert(not ConstraintSet.upper_bound(T, Super).satisfied_by_all_typevars()) # (T = Base) is a valid specialization, which satisfies (T ≤ Base). - static_assert(ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, Base).satisfied_by_all_typevars(inferable=tuple[T])) # (T = Unrelated) is a valid specialization, which does not satisfy (T ≤ Base). - static_assert(not ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars()) + static_assert(not ConstraintSet.upper_bound(T, Base).satisfied_by_all_typevars()) # (T = Sub) is a valid specialization, which satisfies (T ≤ Sub). - static_assert(ConstraintSet.range(Never, T, Sub).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, Sub).satisfied_by_all_typevars(inferable=tuple[T])) # (T = Unrelated) is a valid specialization, which does not satisfy (T ≤ Sub). - static_assert(not ConstraintSet.range(Never, T, Sub).satisfied_by_all_typevars()) + static_assert(not ConstraintSet.upper_bound(T, Sub).satisfied_by_all_typevars()) ``` ## Typevar with an upper bound @@ -115,30 +115,30 @@ def bounded[T: Base](): static_assert(not ConstraintSet.never().satisfied_by_all_typevars()) # (T = Base) is a valid specialization, which satisfies (T ≤ Super). - static_assert(ConstraintSet.range(Never, T, Super).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, Super).satisfied_by_all_typevars(inferable=tuple[T])) # Every valid specialization satisfies (T ≤ Base). Since (Base ≤ Super), every valid # specialization also satisfies (T ≤ Super). - static_assert(ConstraintSet.range(Never, T, Super).satisfied_by_all_typevars()) + static_assert(ConstraintSet.upper_bound(T, Super).satisfied_by_all_typevars()) # (T = Base) is a valid specialization, which satisfies (T ≤ Base). - static_assert(ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, Base).satisfied_by_all_typevars(inferable=tuple[T])) # Every valid specialization satisfies (T ≤ Base). - static_assert(ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars()) + static_assert(ConstraintSet.upper_bound(T, Base).satisfied_by_all_typevars()) # (T = Sub) is a valid specialization, which satisfies (T ≤ Sub). - static_assert(ConstraintSet.range(Never, T, Sub).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, Sub).satisfied_by_all_typevars(inferable=tuple[T])) # (T = Base) is a valid specialization, which does not satisfy (T ≤ Sub). - static_assert(not ConstraintSet.range(Never, T, Sub).satisfied_by_all_typevars()) + static_assert(not ConstraintSet.upper_bound(T, Sub).satisfied_by_all_typevars()) # (T = Never) is a valid specialization, which satisfies (T ≤ Unrelated). - constraints = ConstraintSet.range(Never, T, Unrelated) + constraints = ConstraintSet.upper_bound(T, Unrelated) static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) # (T = Base) is a valid specialization, which does not satisfy (T ≤ Unrelated). static_assert(not constraints.satisfied_by_all_typevars()) # Never is the only type that satisfies both (T ≤ Base) and (T ≤ Unrelated). So there is no # valid specialization that satisfies (T ≤ Unrelated ∧ T ≠ Never). - constraints = constraints & ~ConstraintSet.range(Never, T, Never) + constraints = constraints & ~ConstraintSet.equality(T, Never) static_assert(not constraints.satisfied_by_all_typevars(inferable=tuple[T])) static_assert(not constraints.satisfied_by_all_typevars()) ``` @@ -163,18 +163,18 @@ def bounded_by_gradual[T: Any](): # If we choose Base as the materialization for the upper bound, then (T = Base) is a valid # specialization, which satisfies (T ≤ Base). - static_assert(ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, Base).satisfied_by_all_typevars(inferable=tuple[T])) # We are free to choose any materialization of the upper bound, and only have to show that the # constraint set holds for that one materialization. Having chosen one materialization, we then # have to show that the constraint set holds for all valid specializations of that # materialization. If we choose Never as the materialization, then all valid specializations # must satisfy (T ≤ Never). That means there is only one valid specialization, (T = Never), # which satisfies (T ≤ Base). - static_assert(ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars()) + static_assert(ConstraintSet.upper_bound(T, Base).satisfied_by_all_typevars()) # If we choose Unrelated as the materialization, then (T = Unrelated) is a valid specialization, # which satisfies (T ≤ Unrelated). - constraints = ConstraintSet.range(Never, T, Unrelated) + constraints = ConstraintSet.upper_bound(T, Unrelated) static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) # If we choose Never as the materialization, then (T = Never) is the only valid specialization, # which satisfies (T ≤ Unrelated). @@ -182,7 +182,7 @@ def bounded_by_gradual[T: Any](): # If we choose Unrelated as the materialization, then (T = Unrelated) is a valid specialization, # which satisfies (T ≤ Unrelated ∧ T ≠ Never). - constraints = constraints & ~ConstraintSet.range(Never, T, Never) + constraints = constraints & ~ConstraintSet.equality(T, Never) static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) # There is no upper bound that we can choose to satisfy this constraint set in non-inferable # position. (T = Never) will be a valid assignment no matter what, and that does not satisfy @@ -206,7 +206,7 @@ def bounded_by_gradual[T: list[Any]](): # If we choose list[Base] as the materialization of the upper bound, then (T = list[Base]) is a # valid specialization, which satisfies (T ≤ list[Base]). - static_assert(ConstraintSet.range(Never, T, list[Base]).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, list[Base]).satisfied_by_all_typevars(inferable=tuple[T])) # If we choose Base as the materialization, then all valid specializations must satisfy # (T ≤ list[Base]). # We are free to choose any materialization of the upper bound, and only have to show that the @@ -214,11 +214,11 @@ def bounded_by_gradual[T: list[Any]](): # have to show that the constraint set holds for all valid specializations of that # materialization. If we choose list[Base] as the materialization, then all valid specializations # must satisfy (T ≤ list[Base]), which is exactly the constraint set that we need to satisfy. - static_assert(ConstraintSet.range(Never, T, list[Base]).satisfied_by_all_typevars()) + static_assert(ConstraintSet.upper_bound(T, list[Base]).satisfied_by_all_typevars()) # If we choose Unrelated as the materialization, then (T = list[Unrelated]) is a valid # specialization, which satisfies (T ≤ list[Unrelated]). - constraints = ConstraintSet.range(Never, T, list[Unrelated]) + constraints = ConstraintSet.upper_bound(T, list[Unrelated]) static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) # If we choose Unrelated as the materialization, then all valid specializations must satisfy # (T ≤ list[Unrelated]). @@ -226,7 +226,7 @@ def bounded_by_gradual[T: list[Any]](): # If we choose Unrelated as the materialization, then (T = list[Unrelated]) is a valid # specialization, which satisfies (T ≤ list[Unrelated] ∧ T ≠ Never). - constraints = constraints & ~ConstraintSet.range(Never, T, Never) + constraints = constraints & ~ConstraintSet.equality(T, Never) static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) # There is no upper bound that we can choose to satisfy this constraint set in non-inferable # position. (T = Never) will be a valid assignment no matter what, and that does not satisfy @@ -261,53 +261,53 @@ def constrained[T: (Base, Unrelated)](): static_assert(not ConstraintSet.never().satisfied_by_all_typevars()) # (T = Unrelated) is a valid specialization, which satisfies (T ≤ Unrelated). - static_assert(ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, Unrelated).satisfied_by_all_typevars(inferable=tuple[T])) # (T = Base) is a valid specialization, which does not satisfy (T ≤ Unrelated). - static_assert(not ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars()) + static_assert(not ConstraintSet.upper_bound(T, Unrelated).satisfied_by_all_typevars()) # (T = Base) is a valid specialization, which satisfies (T ≤ Super). - static_assert(ConstraintSet.range(Never, T, Super).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, Super).satisfied_by_all_typevars(inferable=tuple[T])) # (T = Unrelated) is a valid specialization, which does not satisfy (T ≤ Super). - static_assert(not ConstraintSet.range(Never, T, Super).satisfied_by_all_typevars()) + static_assert(not ConstraintSet.upper_bound(T, Super).satisfied_by_all_typevars()) # (T = Base) is a valid specialization, which satisfies (T ≤ Base). - static_assert(ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, Base).satisfied_by_all_typevars(inferable=tuple[T])) # (T = Unrelated) is a valid specialization, which does not satisfy (T ≤ Base). - static_assert(not ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars()) + static_assert(not ConstraintSet.upper_bound(T, Base).satisfied_by_all_typevars()) # Neither (T = Base) nor (T = Unrelated) satisfy (T ≤ Sub). - static_assert(not ConstraintSet.range(Never, T, Sub).satisfied_by_all_typevars(inferable=tuple[T])) - static_assert(not ConstraintSet.range(Never, T, Sub).satisfied_by_all_typevars()) + static_assert(not ConstraintSet.upper_bound(T, Sub).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(not ConstraintSet.upper_bound(T, Sub).satisfied_by_all_typevars()) # (T = Base) and (T = Unrelated) both satisfy (T ≤ Super ∨ T ≤ Unrelated). - constraints = ConstraintSet.range(Never, T, Super) | ConstraintSet.range(Never, T, Unrelated) + constraints = ConstraintSet.upper_bound(T, Super) | ConstraintSet.upper_bound(T, Unrelated) static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) static_assert(constraints.satisfied_by_all_typevars()) # (T = Base) and (T = Unrelated) both satisfy (T ≤ Base ∨ T ≤ Unrelated). - constraints = ConstraintSet.range(Never, T, Base) | ConstraintSet.range(Never, T, Unrelated) + constraints = ConstraintSet.upper_bound(T, Base) | ConstraintSet.upper_bound(T, Unrelated) static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) static_assert(constraints.satisfied_by_all_typevars()) # (T = Unrelated) is a valid specialization, which satisfies (T ≤ Sub ∨ T ≤ Unrelated). - constraints = ConstraintSet.range(Never, T, Sub) | ConstraintSet.range(Never, T, Unrelated) + constraints = ConstraintSet.upper_bound(T, Sub) | ConstraintSet.upper_bound(T, Unrelated) static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) # (T = Base) is a valid specialization, which does not satisfy (T ≤ Sub ∨ T ≤ Unrelated). static_assert(not constraints.satisfied_by_all_typevars()) # (T = Unrelated) is a valid specialization, which satisfies (T = Super ∨ T = Unrelated). - constraints = ConstraintSet.range(Super, T, Super) | ConstraintSet.range(Unrelated, T, Unrelated) + constraints = ConstraintSet.equality(T, Super) | ConstraintSet.equality(T, Unrelated) static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) # (T = Base) is a valid specialization, which does not satisfy (T = Super ∨ T = Unrelated). static_assert(not constraints.satisfied_by_all_typevars()) # (T = Base) and (T = Unrelated) both satisfy (T = Base ∨ T = Unrelated). - constraints = ConstraintSet.range(Base, T, Base) | ConstraintSet.range(Unrelated, T, Unrelated) + constraints = ConstraintSet.equality(T, Base) | ConstraintSet.equality(T, Unrelated) static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) static_assert(constraints.satisfied_by_all_typevars()) # (T = Unrelated) is a valid specialization, which satisfies (T = Sub ∨ T = Unrelated). - constraints = ConstraintSet.range(Sub, T, Sub) | ConstraintSet.range(Unrelated, T, Unrelated) + constraints = ConstraintSet.equality(T, Sub) | ConstraintSet.equality(T, Unrelated) static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) # (T = Base) is a valid specialization, which does not satisfy (T = Sub ∨ T = Unrelated). static_assert(not constraints.satisfied_by_all_typevars()) @@ -333,24 +333,24 @@ def constrained_by_gradual[T: (Base, Any)](): # If we choose Unrelated as the materialization of the gradual constraint, then (T = Unrelated) # is a valid specialization, which satisfies (T ≤ Unrelated). - static_assert(ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, Unrelated).satisfied_by_all_typevars(inferable=tuple[T])) # No matter which materialization we choose, (T = Base) is a valid specialization, which does # not satisfy (T ≤ Unrelated). - static_assert(not ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars()) + static_assert(not ConstraintSet.upper_bound(T, Unrelated).satisfied_by_all_typevars()) # If we choose Super as the materialization, then (T = Super) is a valid specialization, which # satisfies (T ≤ Super). - static_assert(ConstraintSet.range(Never, T, Super).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, Super).satisfied_by_all_typevars(inferable=tuple[T])) # If we choose Never as the materialization, then (T = Base) and (T = Never) are the only valid # specializations, both of which satisfy (T ≤ Super). - static_assert(ConstraintSet.range(Never, T, Super).satisfied_by_all_typevars()) + static_assert(ConstraintSet.upper_bound(T, Super).satisfied_by_all_typevars()) # If we choose Base as the materialization, then (T = Base) is a valid specialization, which # satisfies (T ≤ Base). - static_assert(ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, Base).satisfied_by_all_typevars(inferable=tuple[T])) # If we choose Never as the materialization, then (T = Base) and (T = Never) are the only valid # specializations, both of which satisfy (T ≤ Base). - static_assert(ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars()) + static_assert(ConstraintSet.upper_bound(T, Base).satisfied_by_all_typevars()) def constrained_by_two_gradual[T: (Any, Any)](): static_assert(ConstraintSet.always().satisfied_by_all_typevars(inferable=tuple[T])) @@ -361,17 +361,17 @@ def constrained_by_two_gradual[T: (Any, Any)](): # If we choose Unrelated as the materialization of either constraint, then (T = Unrelated) is a # valid specialization, which satisfies (T ≤ Unrelated). - static_assert(ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, Unrelated).satisfied_by_all_typevars(inferable=tuple[T])) # If we choose Unrelated as the materialization of both constraints, then (T = Unrelated) is the # only valid specialization, which satisfies (T ≤ Unrelated). - static_assert(ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars()) + static_assert(ConstraintSet.upper_bound(T, Unrelated).satisfied_by_all_typevars()) # If we choose Base as the materialization of either constraint, then (T = Base) is a valid # specialization, which satisfies (T ≤ Base). - static_assert(ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, Base).satisfied_by_all_typevars(inferable=tuple[T])) # If we choose Never as the materialization of both constraints, then (T = Never) is the only # valid specialization, which satisfies (T ≤ Base). - static_assert(ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars()) + static_assert(ConstraintSet.upper_bound(T, Base).satisfied_by_all_typevars()) ``` When a constraint is a more complex gradual type, we are still free to choose any materialization @@ -391,33 +391,33 @@ def constrained_by_gradual[T: (list[Base], list[Any])](): # No matter which materialization we choose, every valid specialization will be of the form # (T = list[X]). Because Unrelated is final, it is disjoint from all lists. There is therefore # no materialization or specialization that satisfies (T ≤ Unrelated). - static_assert(not ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars(inferable=tuple[T])) - static_assert(not ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars()) + static_assert(not ConstraintSet.upper_bound(T, Unrelated).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(not ConstraintSet.upper_bound(T, Unrelated).satisfied_by_all_typevars()) # If we choose list[Super] as the materialization, then (T = list[Super]) is a valid # specialization, which satisfies (T ≤ list[Super]). - static_assert(ConstraintSet.range(Never, T, list[Super]).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, list[Super]).satisfied_by_all_typevars(inferable=tuple[T])) # No matter which materialization we choose, (T = list[Base]) is a valid specialization, which # does not satisfy (T ≤ list[Super]). - static_assert(not ConstraintSet.range(Never, T, list[Super]).satisfied_by_all_typevars()) + static_assert(not ConstraintSet.upper_bound(T, list[Super]).satisfied_by_all_typevars()) # If we choose list[Base] as the materialization, then (T = list[Base]) is a valid # specialization, which satisfies (T ≤ list[Base]). - static_assert(ConstraintSet.range(Never, T, list[Base]).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, list[Base]).satisfied_by_all_typevars(inferable=tuple[T])) # If we choose list[Base] as the materialization, then all valid specializations must satisfy # (T ≤ list[Base]). - static_assert(ConstraintSet.range(Never, T, list[Base]).satisfied_by_all_typevars()) + static_assert(ConstraintSet.upper_bound(T, list[Base]).satisfied_by_all_typevars()) # If we choose list[Sub] as the materialization, then (T = list[Sub]) is a valid specialization, # which # satisfies (T ≤ list[Sub]). - static_assert(ConstraintSet.range(Never, T, list[Sub]).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, list[Sub]).satisfied_by_all_typevars(inferable=tuple[T])) # No matter which materialization we choose, (T = list[Base]) is a valid specialization, which # does not satisfy (T ≤ list[Sub]). - static_assert(not ConstraintSet.range(Never, T, list[Sub]).satisfied_by_all_typevars()) + static_assert(not ConstraintSet.upper_bound(T, list[Sub]).satisfied_by_all_typevars()) # If we choose list[Unrelated] as the materialization, then (T = list[Unrelated]) is a valid # specialization, which satisfies (T ≤ list[Unrelated]). - constraints = ConstraintSet.range(Never, T, list[Unrelated]) + constraints = ConstraintSet.upper_bound(T, list[Unrelated]) static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) # No matter which materialization we choose, (T = list[Base]) is a valid specialization, which # does not satisfy (T ≤ list[Unrelated]). @@ -425,7 +425,7 @@ def constrained_by_gradual[T: (list[Base], list[Any])](): # If we choose list[Unrelated] as the materialization, then (T = list[Unrelated]) is a valid # specialization, which satisfies (T ≤ list[Unrelated] ∧ T ≠ Never). - constraints = constraints & ~ConstraintSet.range(Never, T, Never) + constraints = constraints & ~ConstraintSet.equality(T, Never) static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) # There is no materialization that we can choose to satisfy this constraint set in non-inferable # position. (T = Never) will be a valid assignment no matter what, and that does not satisfy @@ -442,33 +442,33 @@ def constrained_by_two_gradual[T: (list[Any], list[Any])](): # No matter which materialization we choose, every valid specialization will be of the form # (T = list[X]). Because Unrelated is final, it is disjoint from all lists. There is therefore # no materialization or specialization that satisfies (T ≤ Unrelated). - static_assert(not ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars(inferable=tuple[T])) - static_assert(not ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars()) + static_assert(not ConstraintSet.upper_bound(T, Unrelated).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(not ConstraintSet.upper_bound(T, Unrelated).satisfied_by_all_typevars()) # If we choose list[Super] as the materialization, then (T = list[Super]) is a valid # specialization, which satisfies (T ≤ list[Super]). - static_assert(ConstraintSet.range(Never, T, list[Super]).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, list[Super]).satisfied_by_all_typevars(inferable=tuple[T])) # No matter which materialization we choose, (T = list[Base]) is a valid specialization, which # does not satisfy (T ≤ list[Super]). - static_assert(ConstraintSet.range(Never, T, list[Super]).satisfied_by_all_typevars()) + static_assert(ConstraintSet.upper_bound(T, list[Super]).satisfied_by_all_typevars()) # If we choose list[Base] as the materialization, then (T = list[Base]) is a valid # specialization, which satisfies (T ≤ list[Base]). - static_assert(ConstraintSet.range(Never, T, list[Base]).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, list[Base]).satisfied_by_all_typevars(inferable=tuple[T])) # If we choose Base as the materialization, then all valid specializations must satisfy # (T ≤ list[Base]). - static_assert(ConstraintSet.range(Never, T, list[Base]).satisfied_by_all_typevars()) + static_assert(ConstraintSet.upper_bound(T, list[Base]).satisfied_by_all_typevars()) # If we choose list[Sub] as the materialization, then (T = list[Sub]) is a valid specialization, # which satisfies (T ≤ list[Sub]). - static_assert(ConstraintSet.range(Never, T, list[Sub]).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, list[Sub]).satisfied_by_all_typevars(inferable=tuple[T])) # No matter which materialization we choose, (T = list[Base]) is a valid specialization, which # does not satisfy (T ≤ list[Sub]). - static_assert(ConstraintSet.range(Never, T, list[Sub]).satisfied_by_all_typevars()) + static_assert(ConstraintSet.upper_bound(T, list[Sub]).satisfied_by_all_typevars()) # If we choose list[Unrelated] as the materialization, then (T = list[Unrelated]) is a valid # specialization, which satisfies (T ≤ list[Unrelated]). - constraints = ConstraintSet.range(Never, T, list[Unrelated]) + constraints = ConstraintSet.upper_bound(T, list[Unrelated]) static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) # No matter which materialization we choose, (T = list[Base]) is a valid specialization, which # does not satisfy (T ≤ list[Unrelated]). @@ -476,7 +476,7 @@ def constrained_by_two_gradual[T: (list[Any], list[Any])](): # If we choose list[Unrelated] as the materialization, then (T = list[Unrelated]) is a valid # specialization, which satisfies (T ≤ list[Unrelated] ∧ T ≠ Never). - constraints = constraints & ~ConstraintSet.range(Never, T, Never) + constraints = constraints & ~ConstraintSet.equality(T, Never) static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) # There is no constraint that we can choose to satisfy this constraint set in non-inferable # position. (T = Never) will be a valid assignment no matter what, and that does not satisfy diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/truthiness.md b/crates/ty_python_semantic/resources/mdtest/type_properties/truthiness.md index e7157e9bd3..d2019720da 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/truthiness.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/truthiness.md @@ -122,6 +122,18 @@ static_assert(is_subtype_of(types.MethodWrapperType, AlwaysTruthy)) static_assert(is_subtype_of(types.WrapperDescriptorType, AlwaysTruthy)) ``` +### Subclassable special-cased classes + +`Path` and `super` cannot be inferred as always truthy because subclasses can override `__bool__`. + +```py +from pathlib import Path + +def _(path: Path, superclass: super): + reveal_type(bool(path)) # revealed: bool + reveal_type(bool(superclass)) # revealed: bool +``` + ### `Callable` types always have ambiguous truthiness ```py @@ -132,7 +144,7 @@ def f(x: Callable[..., Any], y: Callable[[int], str]): reveal_type(bool(y)) # revealed: bool ``` -But certain callable single-valued types are known to be always truthy: +But certain callable objects are known to be always truthy: ```py from types import FunctionType diff --git a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md index dc86e1d631..7a8087054c 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md +++ b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md @@ -627,13 +627,11 @@ error[override-of-final-variable]: Cannot override `module_a.Foo.X` | 5 | X = 2 | ^ Overrides a final variable from superclass `module_a.Foo` - | info: `module_a.Foo.X` is declared as `Final`, forbidding overrides --> src/module_a.py:4:5 | 4 | X: Final[int] = 1 | - `module_a.Foo.X` defined here - | ``` ### `Final` declaration without a value @@ -844,6 +842,34 @@ def bar(x: Foo, value: int): x.value = value ``` +### Protocol members initialized in `__init__` + +A protocol may initialize its own `Final` member in `__init__`, even if another method specializes +the protocol's `self` type. That specialization must not make the initializer appear to belong to a +different class. Assignments to another instance or outside the initializer remain invalid. + +```py +from __future__ import annotations + +from typing import Final, Protocol, TypeVar + +T = TypeVar("T", covariant=True) + +# `replace` takes a `T`, which a covariant type variable cannot appear in +# error: [invalid-generic-class] +class Owned(Protocol[T]): + owner: Final[T] + + def __init__(self, owner: T, other: Owned[T] | None = None) -> None: + self.owner = owner + if other is not None: + other.owner = owner # error: [invalid-assignment] + + def progress(self: Owned[int]) -> None: ... + def replace(self, owner: T) -> None: + self.owner = owner # error: [invalid-assignment] +``` + ### Explicit `Final` redeclaration Explicit `Final` redeclaration in the same scope is accepted (shadowing). @@ -1312,7 +1338,6 @@ error[invalid-assignment]: Reassignment of `Final` symbol `MY_CONSTANT` is not a | 3 | MY_CONSTANT: Final[int] = 1 | ---------- Symbol declared as `Final` here - | ``` Imported `Final` symbol: diff --git a/crates/ty_python_semantic/resources/mdtest/typed_dict.md b/crates/ty_python_semantic/resources/mdtest/typed_dict.md index 706dbdfb96..d97d37322a 100644 --- a/crates/ty_python_semantic/resources/mdtest/typed_dict.md +++ b/crates/ty_python_semantic/resources/mdtest/typed_dict.md @@ -2627,7 +2627,9 @@ python-version = "3.12" ``` ```py -from typing import Literal, TypedDict +from collections import ChainMap, OrderedDict, defaultdict +from collections.abc import Mapping, MutableMapping +from typing import Any, Literal, TypedDict A = TypedDict("A", {"type": Literal["a"]}) B = TypedDict("B", {"type": Literal["b"]}) @@ -2659,13 +2661,108 @@ Item = A | B | C | D | E | F | G | H | I | J | K | L | M | N | O | P | Q | R | S def _(item: Item) -> None: reveal_type(dict(item)) # revealed: dict[str, object] -# Runtime narrowing retains a `Top[dict[Unknown, Unknown]]` intersection around each `TypedDict`. -# Those intersections should still reuse the common protocol constraints of the union. +# Runtime narrowing preserves each `TypedDict` schema without exposing unrestricted dictionary +# operations. The union should still reuse its common protocol constraints. # Regression test for https://github.com/astral-sh/ty/issues/3974. def _(item: Item | str) -> None: if isinstance(item, dict): reveal_type(dict(item)) # revealed: dict[str, object] +``` + +A successful membership test for an undeclared key narrows each union member to an intersection with +a synthesized `TypedDict`. Its mapping methods should retain their precise types, and copying the +narrowed union should remain efficient even when each member has a distinct optional field: + +```py +from typing import NotRequired + +MembershipA = TypedDict("MembershipA", {"kind": Literal["a"], "field_a": NotRequired[int]}) +MembershipB = TypedDict("MembershipB", {"kind": Literal["b"], "field_b": NotRequired[int]}) +MembershipC = TypedDict("MembershipC", {"kind": Literal["c"], "field_c": NotRequired[int]}) +MembershipD = TypedDict("MembershipD", {"kind": Literal["d"], "field_d": NotRequired[int]}) +MembershipE = TypedDict("MembershipE", {"kind": Literal["e"], "field_e": NotRequired[int]}) +MembershipF = TypedDict("MembershipF", {"kind": Literal["f"], "field_f": NotRequired[int]}) +MembershipG = TypedDict("MembershipG", {"kind": Literal["g"], "field_g": NotRequired[int]}) +MembershipH = TypedDict("MembershipH", {"kind": Literal["h"], "field_h": NotRequired[int]}) +MembershipI = TypedDict("MembershipI", {"kind": Literal["i"], "field_i": NotRequired[int]}) +MembershipJ = TypedDict("MembershipJ", {"kind": Literal["j"], "field_j": NotRequired[int]}) + +type MembershipItem = ( + MembershipA + | MembershipB + | MembershipC + | MembershipD + | MembershipE + | MembershipF + | MembershipG + | MembershipH + | MembershipI + | MembershipJ +) + +def _(item: MembershipItem) -> None: + if "missing" in item: + reveal_type(item.keys()) # revealed: dict_keys[str, object] + reveal_type(item.items()) # revealed: dict_items[str, object] + reveal_type(item.values()) # revealed: dict_values[str, object] + reveal_type(item["missing"]) # revealed: object + reveal_type(dict(item)) # revealed: dict[str, object] + +def _(item: MembershipA) -> None: + if "missing" in item: + reveal_type(item.copy()) # revealed: MembershipA & +``` + +Adding a regular dictionary to the union should not make copying it slow: + +```py +def _(item: Item | dict[str, Any]) -> None: + reveal_type(dict(item)) # revealed: dict[str, object] + if isinstance(item, dict): + reveal_type(dict(item)) # revealed: dict[str, object] +``` + +An unrelated `Any` field on a `TypedDict` should not disable this optimization: + +```py +class ItemWithAny(TypedDict): + type: Literal["any"] + other: Any + +def _(item: Item | ItemWithAny | dict[str, Any]) -> None: + reveal_type(dict(item)) # revealed: dict[str, object] +``` + +`Mapping`, `MutableMapping`, and other standard-library mappings should also be copied efficiently: + +```py +def _(item: Item | Mapping[str, Any]) -> None: + reveal_type(dict(item)) # revealed: dict[str, object] + +def _(item: Item | MutableMapping[str, Any]) -> None: + reveal_type(dict(item)) # revealed: dict[str, object] + +def _(item: Item | OrderedDict[str, Any]) -> None: + reveal_type(dict(item)) # revealed: dict[str, object] +def _(item: Item | defaultdict[str, Any]) -> None: + reveal_type(dict(item)) # revealed: dict[str, object] + +def _(item: Item | ChainMap[str, Any]) -> None: + reveal_type(dict(item)) # revealed: dict[str, object] +``` + +A mapping should still be copied efficiently after `isinstance()` narrows it to a dictionary: + +```py +def _(item: Item | Mapping[str, Any]) -> None: + if isinstance(item, dict): + reveal_type(dict(item)) # revealed: dict[str, object] +``` + +The union can also be assembled from type aliases: + +```py type FirstGroup = A | B | C | D | E | F | G | H type SecondGroup = I | J | K | L | M | N | O | P type AliasedItem = FirstGroup | SecondGroup | Q | R | S | T | U | V | W | X @@ -2779,6 +2876,76 @@ def _(value: ClearA | ClearB) -> None: reveal_type(clear_result(value)) # revealed: None ``` +An `isinstance()` check against a protocol can establish that `__getitem__()` returns `Any`. That +return type must be preserved for unions containing a `TypedDict`: + +```py +from typing import Any, Literal, Protocol, TypeVar, TypedDict, runtime_checkable + +ValueT = TypeVar("ValueT", covariant=True) + +class GetValue(Protocol[ValueT]): + def __getitem__(self, key: Literal["value"], /) -> ValueT: ... + +class StringValue(TypedDict): + value: str + +@runtime_checkable +class GetAnyValue(Protocol): + def __getitem__(self, key: Literal["value"], /) -> Any: ... + +def get_value(value: GetValue[ValueT]) -> ValueT: + raise NotImplementedError + +def _(value: StringValue | dict[str, Any]) -> None: + if isinstance(value, GetAnyValue): + reveal_type(get_value(value)) # revealed: Any +``` + +The same `Any` result must remain valid when the mapping protocol uses a bounded type variable: + +```py +from _typeshed import SupportsKeysAndGetItem +from collections.abc import Iterable + +BoundedValueT = TypeVar("BoundedValueT", bound=str) + +@runtime_checkable +class AnyValueMapping(Protocol): + def keys(self) -> Iterable[str]: ... + def __getitem__(self, key: str, /) -> Any: ... + +def get_bounded_mapping(value: SupportsKeysAndGetItem[str, BoundedValueT]) -> BoundedValueT: + raise NotImplementedError + +def _(value: StringValue | dict[str, Any]) -> None: + if isinstance(value, AnyValueMapping): + reveal_type(get_bounded_mapping(value)) # revealed: Any +``` + +A `TypedDict` that permits extra items of type `Any` keeps that type when copied: + +```py +from typing_extensions import TypedDict as ExtensionsTypedDict + +class AnyExtraItems(ExtensionsTypedDict, extra_items=Any): ... + +def _(value: AnyExtraItems | dict[str, str]) -> None: + reveal_type(dict(value)) # revealed: dict[str, Any | str] +``` + +A union of two such `TypedDict`s must also preserve `Any` when copied or passed to a mapping +protocol with a bounded type variable: + +```py +class OtherAnyExtraItems(ExtensionsTypedDict, extra_items=Any): ... + +def _(value: AnyExtraItems | OtherAnyExtraItems) -> None: + reveal_type(dict(value)) # revealed: dict[str, Any] + dict(value)["x"].strip() + reveal_type(get_bounded_mapping(value)) # revealed: Any +``` + Rejected common-constraint probes must not affect fallback protocol inference: ```py @@ -3367,6 +3534,625 @@ static_assert(is_assignable_to(Items[Any], Items[int])) static_assert(not is_subtype_of(Items[Any], Items[int])) ``` +### Specialized constructor signatures + +An explicitly specialized constructor substitutes its type parameter in both the receiver and the +fields. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import TypedDict + +class Box[T](TypedDict): + value: T + +# revealed: Overload[(self: Box[int], map: Box[int], /, *, value: int = ...) -> None, (self: Box[int], /, *, value: int) -> None] +reveal_type(Box[int].__init__) +``` + +### Constructor inference from keyword arguments + +Both PEP 695 and legacy generic constructors infer their type arguments from keyword values. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Generic, TypeVar, TypedDict + +class Box[T](TypedDict): + value: T + +reveal_type(Box(value=1)) # revealed: Box[int] + +T = TypeVar("T") + +class LegacyBox(TypedDict, Generic[T]): + value: T + +reveal_type(LegacyBox(value=1)) # revealed: LegacyBox[int] +``` + +### Generic constructor diagnostics + +Generic constructors preserve the usual diagnostics for missing and unexpected fields. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import TypedDict + +class Box[T](TypedDict): + value: T + +Box() # error: [missing-typed-dict-key] +Box(value=1, extra=2) # error: [invalid-key] +``` + +An invalid field value points to the field declaration and retains the usual `TypedDict` +annotations. + +```py +class LabeledBox[T](TypedDict): + value: T + label: str + +# snapshot: invalid-argument-type +LabeledBox(value=1, label=2) +``` + +```snapshot +error[invalid-argument-type]: Invalid argument to key "label" with declared type `str` on TypedDict `LabeledBox` + --> src/mdtest_snippet.py:13:27 + | +13 | LabeledBox(value=1, label=2) + | ---------- ------^ + | | | | + | | | value of type `Literal[2]` + | | key has declared type `str` + | TypedDict `LabeledBox` +info: Item declaration + --> src/mdtest_snippet.py:10:5 + | +10 | label: str + | ---------- Item declared here +``` + +### Constructor inference from multiple fields + +Different fields can contribute different types to the same type parameter. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import TypedDict + +class Pair[T](TypedDict): + first: T + second: T + +reveal_type(Pair(first=1, second="x")) # revealed: Pair[int | str] +``` + +### Constructor inference from inherited fields + +An inherited field constrains the child class's type parameter. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import TypedDict + +class Base[T](TypedDict): + value: T + +class Child[T](Base[T]): + pass + +reveal_type(Child(value=1)) # revealed: Child[int] +``` + +### Constructor inference and mapping arguments + +A named keyword can infer the element type of a mutable container. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import TypedDict + +class ListBox[T](TypedDict): + value: list[T] + +reveal_type(ListBox(value=[1])) # revealed: ListBox[int] +``` + +Positional and unpacked dictionary literals are validated but do not yet infer type arguments. + +```py +# TODO: Infer `ListBox[int]`. +reveal_type(ListBox({"value": [1]})) # revealed: ListBox[Unknown] +# TODO: Infer `ListBox[int]`. +reveal_type(ListBox(**{"value": [1]})) # revealed: ListBox[Unknown] +``` + +A dictionary containing different field types, or multiple unpacked dictionaries, must not cause +spurious argument errors. + +```py +class Pair[T](TypedDict): + first: T + second: str + +reveal_type(Pair(**{"first": 1, "second": "x"})) # revealed: Pair[Unknown] +reveal_type(Pair(**{"first": 1}, **{"second": "x"})) # revealed: Pair[Unknown] +``` + +### Constructor inference from unpacked TypedDicts + +Unpacking a `TypedDict` with required keys contributes each field's type to constructor inference. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import NotRequired, TypedDict + +class Source(TypedDict): + value: int + +class Box[T](TypedDict): + value: T + +def unpack(source: Source): + reveal_type(Box(**source)) # revealed: Box[int] +``` + +A type alias to a single `TypedDict` contributes the same field information. + +```py +type SourceAlias = Source + +def unpack_alias(source: SourceAlias): + reveal_type(Box(**source)) # revealed: Box[int] +``` + +Different unpacked `TypedDict` arguments retain their separate field types. + +```py +class First(TypedDict): + first: int + +class Second(TypedDict): + second: str + +class Pair[T](TypedDict): + first: T + second: str + +def unpack_multiple(first: First, second: Second): + reveal_type(Pair(**first, **second)) # revealed: Pair[int] +``` + +An optional source key does not satisfy a required constructor field. + +```py +class MaybeSource(TypedDict): + value: NotRequired[int] + +def unpack_optional(source: MaybeSource): + Box(**source) # error: [missing-typed-dict-key] +``` + +### Constructor inference from unpacked TypedDict unions + +A union can associate different value types with different callbacks. Inference remains gradual +because combining those fields independently would reject a valid constructor call. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Callable, TypedDict + +class IntSource(TypedDict): + value: int + callback: Callable[[int], None] + +class StrSource(TypedDict): + value: str + callback: Callable[[str], None] + +class Box[T](TypedDict): + value: T + callback: Callable[[T], None] + +def unpack(source: IntSource | StrSource): + reveal_type(Box(**source)) # revealed: Box[Unknown] +``` + +### Constructor inference from recursive fields + +Recursive construction remains valid even though the outer constructor cannot yet infer its type +argument from the nested value. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import NotRequired, TypedDict + +class Node[T](TypedDict): + value: NotRequired[T] + child: NotRequired["Node[T]"] + +# TODO: Infer `Node[int]`. +reveal_type(Node(child=Node(value=1))) # revealed: Node[Unknown] +``` + +A recursive field wrapped in a union also remains diagnostic-free. + +```py +class UnionNode[T](TypedDict): + value: NotRequired[T] + child: NotRequired["UnionNode[T] | None"] + +# TODO: Infer `UnionNode[int]`. +reveal_type(UnionNode(child=UnionNode(value=1))) # revealed: UnionNode[Unknown] +``` + +The same applies when a type alias wraps the recursive union. + +```py +class AliasNode[T](TypedDict): + value: NotRequired[T] + child: NotRequired["AliasNodeChild[T]"] + +type AliasNodeChild[T] = AliasNode[T] | None + +# TODO: Infer `AliasNode[int]`. +reveal_type(AliasNode(child=AliasNode(value=1))) # revealed: AliasNode[Unknown] +``` + +### Constructor inference from nested values + +Nested `TypedDict` fields do not yet contribute constraints to the outer constructor. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import TypedDict + +class Inner[T](TypedDict): + value: T + +class Outer[T](TypedDict): + inner: Inner[T] + marker: T + +# TODO: Infer `Outer[int | str]`. +reveal_type(Outer(inner=Inner(value=1), marker="x")) # revealed: Outer[Unknown] +``` + +A nested dictionary literal also falls back without exposing an internal type parameter. + +```py +# TODO: Infer `Outer[int | str]`. +reveal_type(Outer(inner={"value": 1}, marker="x")) # revealed: Outer[Unknown] +``` + +A generic `TypedDict` nested in a container or type alias must not acquire an incompatible concrete +type from another field. + +```py +type MaybeInner[T] = Inner[T] | None + +class AliasOuter[T](TypedDict): + values: list[MaybeInner[T]] + marker: T + +# TODO: Infer `AliasOuter[int | str]`. +outer = AliasOuter(values=[Inner(value=1)], marker="x") +reveal_type(outer) # revealed: AliasOuter[Unknown] +item = outer["values"][0] +if item is not None: + reveal_type(item["value"]) # revealed: Unknown +``` + +A non-generic nested `TypedDict` does not prevent another field from inferring the type argument. + +```py +class FixedInner(TypedDict): + value: int + +class FixedOuter[T](TypedDict): + inner: FixedInner + marker: T + +reveal_type(FixedOuter(inner={"value": 1}, marker="x")) # revealed: FixedOuter[str] +``` + +A type alias without a nested `TypedDict` still contributes its ordinary field constraints. + +```py +type Values[T] = list[T] + +class AliasBox[T](TypedDict): + value: Values[T] + +reveal_type(AliasBox(value=[1])) # revealed: AliasBox[int] +``` + +### Constructor inference with upper bounds + +A literal upper bound preserves its literal, while an ordinary `int` upper bound permits the usual +promotion. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Literal, TypedDict + +class LiteralBound[T: Literal[1]](TypedDict): + value: T + +reveal_type(LiteralBound(value=1)) # revealed: LiteralBound[Literal[1]] + +class IntBound[T: int](TypedDict): + value: T + +reveal_type(IntBound(value=1)) # revealed: IntBound[int] +``` + +### Constructor inference with callable parameters + +Like other generic constructors, a callback must accept the promoted type inferred from another +field. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Callable, Literal, TypedDict + +class Box[T](TypedDict): + value: T + callback: Callable[[T], None] + +def accepts_one(value: Literal[1]) -> None: ... + +Box(value=1, callback=accepts_one) # error: [invalid-argument-type] +``` + +### Constructor inference with an expected type + +The expected type can preserve a literal that inference from the value alone would promote. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Literal, TypedDict + +class Box[T](TypedDict): + value: T + +literal: Box[Literal[1]] = Box(value=1) +``` + +A wider expected type is also respected because a mutable `TypedDict` is invariant. + +```py +class Animal: ... +class Dog(Animal): ... + +animal: Box[Animal] = Box(value=Dog()) +``` + +### Constructor inference with read-only fields + +A type parameter that appears only in a read-only field is covariant. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Generic, Literal, TypeVar, TypedDict +from typing_extensions import ReadOnly + +class Animal: ... +class Dog(Animal): ... + +class Box[T](TypedDict): + value: ReadOnly[T] + +dog = Box(value=Dog()) +animal: Box[Animal] = dog +``` + +A read-only field also preserves a literal when the inferred value is used with a narrower type. + +```py +literal_box = Box(value=1) +literal: Box[Literal[1]] = literal_box +``` + +A legacy type variable is invariant by default, so assigning `LegacyBox[Dog]` to `LegacyBox[Animal]` +should eventually produce an error. + +```py +T = TypeVar("T") + +class LegacyBox(TypedDict, Generic[T]): + value: ReadOnly[T] + +legacy_dog = LegacyBox(value=Dog()) +# TODO: Reject this assignment: https://github.com/astral-sh/ty/issues/1017 +legacy_animal: LegacyBox[Animal] = legacy_dog +``` + +### Constructor inference with contravariant fields + +A read-only field is covariant in its value, while a callable is contravariant in its parameter. +Combining them makes the `TypedDict`'s type parameter contravariant. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Callable, TypedDict +from typing_extensions import ReadOnly + +class Animal: ... +class Dog(Animal): ... + +class Consumer[T](TypedDict): + callback: ReadOnly[Callable[[T], None]] + +def accepts_animal(value: Animal) -> None: ... +def accepts_dog(value: Dog) -> None: ... + +dog_consumer: Consumer[Dog] = Consumer(callback=accepts_animal) +``` + +An incompatible callback reports its argument error without producing an additional assignment +error. + +```py +animal_consumer: Consumer[Animal] = Consumer( + callback=accepts_dog, # error: [invalid-argument-type] +) +``` + +### Constructor inference from extra items + +An extra keyword constrains the type parameter used by mutable extra items. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing_extensions import TypedDict + +class Box[T](TypedDict, extra_items=T): ... + +box = Box(value=1) +reveal_type(box) # revealed: Box[int] +box["value"] = "invalid" # error: [invalid-assignment] +``` + +A nested generic extra item should constrain its enclosing `TypedDict` without rejecting the inner +constructor. + +```py +class Inner[T](TypedDict): + value: T + +class NestedExtra[T](TypedDict, extra_items=Inner[T]): ... + +# TODO: Infer `NestedExtra[int]`. +reveal_type(NestedExtra(item=Inner(value=1))) # revealed: NestedExtra[Unknown] +``` + +### Constructor inference and context-sensitive arguments + +After inferring the type parameter, the constructor checks a lambda with its inferred parameter +type. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Callable, TypedDict + +class Box[T](TypedDict): + value: T + callback: Callable[[T], int] + +Box(value=1, callback=lambda x: x.upper()) # error: [unresolved-attribute] +``` + +### Constructor inference with a contextual callable + +An expected specialization supplies the parameter type of a lambda stored in a field. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Callable, TypedDict + +class Box[T](TypedDict): + value: T + +direct: Box[Callable[[int], int]] = Box(value=lambda x: x.upper()) # error: [unresolved-attribute] +optional: Box[Callable[[int], int]] | None = Box( + value=lambda x: x.upper(), # error: [unresolved-attribute] +) +``` + +### Constructor inference with a default type parameter + +A constructor argument takes precedence over the type parameter's default. + +```toml +[environment] +python-version = "3.13" +``` + +```py +from typing import TypedDict + +class Defaulted[T = str](TypedDict): + value: T + +reveal_type(Defaulted(value=1)) # revealed: Defaulted[int] +``` + ### Validation of generic `TypedDict`s ```toml @@ -4409,7 +5195,7 @@ def _(p: Person) -> None: reveal_type(p.setdefault("name", "Alice")) # revealed: str # __contains__ - reveal_type("name" in p) # revealed: bool + reveal_type("name" in p) # revealed: Literal[True] # __setitem__ p["name"] = "Alice" diff --git a/crates/ty_python_semantic/resources/mdtest/unary/custom.md b/crates/ty_python_semantic/resources/mdtest/unary/custom.md index 40fbcecc5c..ce8571c98f 100644 --- a/crates/ty_python_semantic/resources/mdtest/unary/custom.md +++ b/crates/ty_python_semantic/resources/mdtest/unary/custom.md @@ -167,7 +167,6 @@ error[unsupported-operator]: Unary operator `+` is not supported for object of t | 15 | reveal_type(+x) # revealed: bool | ^^ - | info: `No` does not implement `__pos__` @@ -176,7 +175,6 @@ error[unsupported-operator]: Unary operator `-` is not supported for object of t | 18 | reveal_type(-x) # revealed: str | ^^ - | info: `No` does not implement `__neg__` @@ -185,7 +183,6 @@ error[unsupported-operator]: Unary operator `~` is not supported for object of t | 21 | reveal_type(~x) # revealed: int | ^^ - | info: `No` does not implement `__invert__` ``` diff --git a/crates/ty_python_semantic/resources/mdtest/unary/not.md b/crates/ty_python_semantic/resources/mdtest/unary/not.md index 10ee1fccc8..98f8056931 100644 --- a/crates/ty_python_semantic/resources/mdtest/unary/not.md +++ b/crates/ty_python_semantic/resources/mdtest/unary/not.md @@ -231,6 +231,5 @@ error[unsupported-bool-conversion]: Boolean conversion is not supported for type | 5 | not NotBoolable() | ^^^^^^^^^^^^^^^^^ - | info: `__bool__` on `NotBoolable` must be callable ``` diff --git a/crates/ty_python_semantic/resources/mdtest/union_types.md b/crates/ty_python_semantic/resources/mdtest/union_types.md index 2825a7e032..ebf3edf134 100644 --- a/crates/ty_python_semantic/resources/mdtest/union_types.md +++ b/crates/ty_python_semantic/resources/mdtest/union_types.md @@ -153,7 +153,7 @@ def _( ## Do not erase `Unknown` ```py -from ty_extensions import Unknown +from ty_extensions._internal import Unknown def _(u1: Unknown | str, u2: str | Unknown) -> None: reveal_type(u1) # revealed: Unknown | str @@ -166,7 +166,7 @@ Since `Unknown` is a gradual type, it is not a subtype of anything, but multiple union are still redundant: ```py -from ty_extensions import Unknown +from ty_extensions._internal import Unknown def _(u1: Unknown | Unknown | str, u2: Unknown | str | Unknown, u3: str | Unknown | Unknown) -> None: reveal_type(u1) # revealed: Unknown | str @@ -179,7 +179,7 @@ def _(u1: Unknown | Unknown | str, u2: Unknown | str | Unknown, u3: str | Unknow Simplifications still apply when `Unknown` is present. ```py -from ty_extensions import Unknown +from ty_extensions._internal import Unknown def _(u1: int | Unknown | bool) -> None: reveal_type(u1) # revealed: int | Unknown @@ -348,8 +348,6 @@ python-version = "3.12" ```py from typing import Any -class Bivariant[T]: ... - class Covariant[T]: def get(self) -> T: raise NotImplementedError @@ -361,8 +359,6 @@ class Invariant[T]: mutable_attribute: T def _( - a: Bivariant[Any] | Bivariant[Any | str], - b: Bivariant[Any | str] | Bivariant[Any], c: Covariant[Any] | Covariant[Any | str], d: Covariant[Any | str] | Covariant[Any], e: Contravariant[Any | str] | Contravariant[Any], @@ -370,8 +366,6 @@ def _( g: Invariant[Any] | Invariant[Any | str], h: Invariant[Any | str] | Invariant[Any], ): - reveal_type(a) # revealed: Bivariant[Any] - reveal_type(b) # revealed: Bivariant[Any | str] reveal_type(c) # revealed: Covariant[Any | str] reveal_type(d) # revealed: Covariant[Any | str] reveal_type(e) # revealed: Contravariant[Any] @@ -379,3 +373,43 @@ def _( reveal_type(g) # revealed: Invariant[Any] | Invariant[Any | str] reveal_type(h) # revealed: Invariant[Any | str] | Invariant[Any] ``` + +A type alias does not make a gradual type argument static. Covariant unions simplify the same way +whether the gradual argument is written directly or hidden behind one or more aliases. + +```py +type GradualAlias = Any | str +type NestedGradualAlias = GradualAlias + +def gradual_aliases( + direct_first: Covariant[Any] | Covariant[GradualAlias], + direct_last: Covariant[GradualAlias] | Covariant[Any], + nested_first: Covariant[Any] | Covariant[NestedGradualAlias], + nested_last: Covariant[NestedGradualAlias] | Covariant[Any], +) -> None: + reveal_type(direct_first) # revealed: Covariant[GradualAlias] + reveal_type(direct_last) # revealed: Covariant[GradualAlias] + reveal_type(nested_first) # revealed: Covariant[NestedGradualAlias] + reveal_type(nested_last) # revealed: Covariant[NestedGradualAlias] +``` + +Matching materialization endpoints do not establish that gradual tuple arguments have the same +shape. A bounded generic must preserve which tuple position contains the gradual element. + +```py +from ty_extensions import Bottom, Top, static_assert +from ty_extensions._internal import is_equivalent_to + +type L = tuple[Any, int] +type R = tuple[int, Any] + +class C[T: tuple[int, int]]: + def get(self) -> T: + raise NotImplementedError + +static_assert(is_equivalent_to(Top[C[L]], Top[C[R]])) +static_assert(is_equivalent_to(Bottom[C[L]], Bottom[C[R]])) +static_assert(not is_equivalent_to(C[L], C[R])) +static_assert(not is_equivalent_to(C[L] | C[R], C[L])) +static_assert(not is_equivalent_to(C[R] | C[L], C[R])) +``` diff --git a/crates/ty_python_semantic/resources/mdtest/unreachable.md b/crates/ty_python_semantic/resources/mdtest/unreachable.md index ecf584447b..94b370ade1 100644 --- a/crates/ty_python_semantic/resources/mdtest/unreachable.md +++ b/crates/ty_python_semantic/resources/mdtest/unreachable.md @@ -644,7 +644,6 @@ error[invalid-type-form]: Variable of type `Never` is not allowed in a parameter | 4 | def f(x: module.AwesomeAPI): ... | ^^^^^^^^^^^^^^^^^ - | help: The variable may have been inferred as `Never` because its definition was inferred as being unreachable ``` diff --git a/crates/ty_python_semantic/resources/mdtest/with/async.md b/crates/ty_python_semantic/resources/mdtest/with/async.md index a99cdafecf..94e1317252 100644 --- a/crates/ty_python_semantic/resources/mdtest/with/async.md +++ b/crates/ty_python_semantic/resources/mdtest/with/async.md @@ -2,9 +2,7 @@ ## Basic `async with` statement -The type of the target variable in a `with` statement should be the return type from the context -manager's `__aenter__` method. However, `async with` statements aren't supported yet. This test -asserts that it doesn't emit any context manager-related errors. +An `async with` statement awaits the return value of `__aenter__` and binds the result. ```py class Target: ... @@ -104,12 +102,15 @@ async def main(): +A union can contain a valid context manager and an object with no context-manager methods. The valid +manager still determines the type of the value bound by `async with`. + ```py class Manager1: - def __aenter__(self) -> str: + async def __aenter__(self) -> str: return "foo" - def __aexit__(self, exc_type, exc_value, traceback): ... + async def __aexit__(self, exc_type, exc_value, traceback): ... class NotAContextManager: ... @@ -119,7 +120,43 @@ async def _(context_expr: Manager1 | NotAContextManager): reveal_type(f) # revealed: str ``` -## Context expression with "sometimes" callable `__aenter__` method +## Missing and non-awaitable methods in a union + +If one member of a union does not define the context-manager methods, still check the return values +of the methods defined on the other member. + +```py +class Manager: + def __aenter__(self) -> int: + return 0 + + def __aexit__(self, exc_type, exc, tb) -> bool: + return False + +class NotAManager: ... + +async def main(manager: Manager | NotAManager): + # snapshot: invalid-context-manager + async with manager as value: + reveal_type(value) # revealed: Unknown +``` + +```snapshot +error[invalid-context-manager]: Object of type `Manager | NotAManager` cannot be used with `async with` because `__aenter__` and `__aexit__` may be missing or return non-awaitables + --> src/mdtest_snippet.py:12:16 + | +12 | async with manager as value: + | ^^^^^^^ +info: `NotAManager` does not implement `__aenter__` or `__aexit__` +info: `__aenter__` returns `int`, which is not awaitable +info: `__aexit__` returns `bool`, which is not awaitable +info: Consider declaring the methods with `async def` +``` + +## Conditionally defined `__aenter__` method + +A conditionally defined `__aenter__` method may be missing. When it exists, its awaited return type +still determines the type of the bound value. ```py async def _(flag: bool): @@ -132,7 +169,7 @@ async def _(flag: bool): # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `async with` because the method `__aenter__` may be missing" async with Manager() as f: - reveal_type(f) # revealed: CoroutineType[Any, Any, str] + reveal_type(f) # revealed: str ``` ## Invalid `__aenter__` signature @@ -152,10 +189,10 @@ async def main(): reveal_type(f) # revealed: CoroutineType[Any, Any, str] ``` -## Accidental use of async `async with` +## Synchronous context manager in `async with` -If a asynchronous `async with` statement is used on a type with `__enter__` and `__exit__`, we show -a diagnostic hint that the user might have intended to use `with` instead. +An object that only defines `__enter__` and `__exit__` cannot be used with `async with`. Suggest +using `with` instead. ```py class Manager: @@ -174,7 +211,6 @@ error[invalid-context-manager]: Object of type `Manager` cannot be used with `as | 7 | async with Manager(): | ^^^^^^^^^ - | info: Objects of type `Manager` can be used as sync context managers info: Consider using `with` here ``` @@ -210,6 +246,236 @@ async def main(): pass ``` +## Non-awaitable `__aenter__` + +`async with` awaits the value returned by `__aenter__`. Returning an `int` therefore raises a +`TypeError`. + +```py +class Manager: + def __aenter__(self) -> int: + return 0 + + async def __aexit__(self, exc_type, exc, tb) -> None: ... + +async def main(): + # snapshot: invalid-context-manager + async with Manager(): + pass +``` + +```snapshot +error[invalid-context-manager]: Object of type `Manager` cannot be used with `async with` because `__aenter__` does not return an awaitable + --> src/mdtest_snippet.py:9:16 + | +9 | async with Manager(): + | ^^^^^^^^^ +info: `__aenter__` returns `int`, which is not awaitable +info: Consider declaring the method with `async def` +``` + +## Non-awaitable `__aexit__` + +`async with` also awaits the value returned by `__aexit__`. The value from `__aenter__` is still +bound before the invalid exit method runs. + +```py +class Manager: + async def __aenter__(self) -> int: + return 0 + + def __aexit__(self, exc_type, exc, tb) -> bool: + return False + +async def main(): + # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `async with` because `__aexit__` does not return an awaitable" + async with Manager() as value: + reveal_type(value) # revealed: int +``` + +## Non-awaitable `__aenter__` with missing `__aexit__` + +A missing exit method does not excuse an entry method that returns a non-awaitable: + +```py +class Manager: + def __aenter__(self) -> int: + return 0 + +async def main(): + # snapshot: invalid-context-manager + async with Manager(): + pass +``` + +```snapshot +error[invalid-context-manager]: Object of type `Manager` cannot be used with `async with` because it does not implement `__aexit__`, and `__aenter__` does not return an awaitable + --> src/mdtest_snippet.py:7:16 + | +7 | async with Manager(): + | ^^^^^^^^^ +info: `__aenter__` returns `int`, which is not awaitable +info: Consider declaring the method with `async def` +``` + +## Missing `__aenter__` with non-awaitable `__aexit__` + +An exit method must return an awaitable even when the entry method is missing: + +```py +class Manager: + def __aexit__(self, exc_type, exc, tb) -> bool: + return False + +async def main(): + # snapshot: invalid-context-manager + async with Manager(): + pass +``` + +```snapshot +error[invalid-context-manager]: Object of type `Manager` cannot be used with `async with` because it does not implement `__aenter__`, and `__aexit__` does not return an awaitable + --> src/mdtest_snippet.py:7:16 + | +7 | async with Manager(): + | ^^^^^^^^^ +info: `__aexit__` returns `bool`, which is not awaitable +info: Consider declaring the method with `async def` +``` + +## Non-awaitable `__aenter__` and `__aexit__` + +When neither method returns an awaitable, both are named in a single diagnostic: + +```py +class Manager: + def __aenter__(self) -> int: + return 0 + + def __aexit__(self, exc_type, exc, tb) -> bool: + return False + +async def main(): + # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `async with` because `__aenter__` and `__aexit__` do not return awaitables" + async with Manager(): + pass +``` + +## Awaitable return from a regular method + +A context-manager method does not need to be declared with `async def`. A regular method can return +an `Awaitable` instead. + +```py +from typing import Awaitable + +class Manager: + def __aenter__(self) -> Awaitable[int]: + raise NotImplementedError + + def __aexit__(self, exc_type, exc, tb) -> Awaitable[None]: + raise NotImplementedError + +async def main(): + async with Manager() as value: + reveal_type(value) # revealed: int +``` + +## Awaitable return from a custom `__await__` method + +An object is awaitable when its `__await__` method returns an iterator. + +```py +from typing import Generator + +class AwaitableValue: + def __await__(self) -> Generator[None, None, int]: + raise NotImplementedError + +class Manager: + def __aenter__(self) -> AwaitableValue: + raise NotImplementedError + + def __aexit__(self, exc_type, exc, tb) -> AwaitableValue: + raise NotImplementedError + +async def main(): + async with Manager() as value: + reveal_type(value) # revealed: int +``` + +## Union of awaitable return types + +When every possible return value is awaitable, the bound value includes the awaited result from each +union member. + +```py +from typing import Awaitable + +class Manager: + def __aenter__(self) -> Awaitable[int] | Awaitable[str]: + raise NotImplementedError + + def __aexit__(self, exc_type, exc, tb) -> Awaitable[None]: + raise NotImplementedError + +async def main(): + async with Manager() as value: + reveal_type(value) # revealed: int | str +``` + +## Union containing a non-awaitable return type + +Every possible return value must be awaitable. A union containing `int` does not satisfy that +requirement. + +```py +from typing import Awaitable + +class Manager: + def __aenter__(self) -> int | Awaitable[int]: + raise NotImplementedError + + async def __aexit__(self, exc_type, exc, tb) -> None: ... + +async def main(): + # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `async with` because `__aenter__` does not return an awaitable" + async with Manager(): + pass +``` + +## `Any` return type + +A return type of `Any` might be awaitable, so it must not produce an error. + +```py +from typing import Any + +class Manager: + def __aenter__(self) -> Any: ... + def __aexit__(self, exc_type, exc, tb) -> Any: ... + +async def main(): + async with Manager(): + pass +``` + +## Unannotated return type + +basedpython infers a return type for an unannotated method rather than leaving it `Unknown`, so a +context manager whose `__aenter__` cannot return an awaitable is rejected here: + +```py +class Manager: + def __aenter__(self): ... + def __aexit__(self, exc_type, exc, tb): ... + +async def main(): + # error: [invalid-context-manager] + async with Manager(): + pass +``` + ## `@asynccontextmanager` ```py diff --git a/crates/ty_python_semantic/resources/mdtest/with/sync.md b/crates/ty_python_semantic/resources/mdtest/with/sync.md index 733c7db405..0669d941dd 100644 --- a/crates/ty_python_semantic/resources/mdtest/with/sync.md +++ b/crates/ty_python_semantic/resources/mdtest/with/sync.md @@ -261,7 +261,6 @@ error[invalid-context-manager]: Object of type `Manager` cannot be used with `wi | 6 | with Manager(): | ^^^^^^^^^ - | info: Objects of type `Manager` can be used as async context managers info: Consider using `async with` here ``` diff --git a/crates/ty_python_semantic/resources/primer/flaky.txt b/crates/ty_python_semantic/resources/primer/flaky.txt index 87506ad1d4..14f3ceb0d1 100644 --- a/crates/ty_python_semantic/resources/primer/flaky.txt +++ b/crates/ty_python_semantic/resources/primer/flaky.txt @@ -1,4 +1,2 @@ -Expression -scikit-build-core -dd-trace-py +meson steam.py diff --git a/crates/ty_python_semantic/src/api_lockfile.rs b/crates/ty_python_semantic/src/api_lockfile.rs index 3e699573ab..5ce5b46c0f 100644 --- a/crates/ty_python_semantic/src/api_lockfile.rs +++ b/crates/ty_python_semantic/src/api_lockfile.rs @@ -46,6 +46,7 @@ use ty_python_core::{ use crate::Db; use crate::dunder_all::dunder_all_names; use crate::place::{place_from_bindings, place_from_declarations}; +use crate::types::ProgramEnvironment; use crate::types::enums::is_enum_class; use crate::types::function::{FunctionDecorators, FunctionType}; use crate::types::type_alias::TypeAliasType; @@ -69,7 +70,12 @@ const PUBLIC_MODULE_DUNDERS: &[&str] = &["__all__", "__author__", "__doc__", "__ /// see what target the lockfile was generated against (typing constructs /// like `Self`, `Required`, `NotRequired` resolve differently per /// version) -pub fn generate_api_lockfile<'db, I>(db: &'db dyn Db, files: I, python_version: &str) -> String +pub fn generate_api_lockfile<'db, I>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + files: I, + python_version: &str, +) -> String where I: IntoIterator, { @@ -80,13 +86,20 @@ where let mut module_count: usize = 0; for file in files { - let Some(module) = file_to_module(db, file) else { + let Some(module) = file_to_module(db, db.program_file(file).resolver_file(db)) else { continue; }; let module_name = module.name(db).as_str().to_string(); - let scope = global_scope(db, file); - emit_module_scope(db, scope, &module_name, &mut lines, &mut visited_classes); + let scope = global_scope(db, db.program_file(file)); + emit_module_scope( + db, + env, + scope, + &module_name, + &mut lines, + &mut visited_classes, + ); module_count += 1; } @@ -105,6 +118,7 @@ where fn emit_module_scope<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, scope: ScopeId<'db>, qualified_prefix: &str, lines: &mut Vec, @@ -113,7 +127,7 @@ fn emit_module_scope<'db>( let use_def_map = use_def_map(db, scope); let table = place_table(db, scope); let scope_file = scope.file(db); - let all_names = dunder_all_names(db, scope_file); + let all_names = dunder_all_names(db, db.program_file(scope_file)); for (symbol_id, declarations, bindings) in use_def_map.all_reachable_symbols() { let symbol = table.symbol(symbol_id); @@ -123,11 +137,11 @@ fn emit_module_scope<'db>( } let place_and_qualifiers = - place_from_declarations(db, declarations).ignore_conflicting_declarations(); + place_from_declarations(db, env, declarations).ignore_conflicting_declarations(); let declaration_ty = place_and_qualifiers.place.ignore_possibly_undefined(); let qualifiers = place_and_qualifiers.qualifiers; - let binding_ty = place_from_bindings(db, bindings) + let binding_ty = place_from_bindings(db, env, bindings) .place .ignore_possibly_undefined(); @@ -143,11 +157,20 @@ fn emit_module_scope<'db>( if let Some(owning) = owning_file && owning != scope_file { - lines.push(format!("{qualified}:r={}", render_reexport(db, ty))); + lines.push(format!("{qualified}:r={}", render_reexport(db, env, ty))); continue; } - emit_symbol(db, &qualified, name, ty, qualifiers, lines, visited_classes); + emit_symbol( + db, + env, + &qualified, + name, + ty, + qualifiers, + lines, + visited_classes, + ); } } @@ -166,8 +189,10 @@ fn type_definition_file<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option { } } +#[expect(clippy::too_many_arguments)] fn emit_symbol<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, qualified: &str, name: &Name, ty: Type<'db>, @@ -176,20 +201,23 @@ fn emit_symbol<'db>( visited_classes: &mut FxHashSet>, ) { match ty { - Type::ClassLiteral(class) => emit_class(db, qualified, name, class, lines, visited_classes), + Type::ClassLiteral(class) => { + emit_class(db, env, qualified, name, class, lines, visited_classes); + } Type::GenericAlias(alias) => emit_class( db, + env, qualified, name, ClassLiteral::Static(alias.origin(db)), lines, visited_classes, ), - Type::FunctionLiteral(function) => emit_function(db, qualified, function, lines), + Type::FunctionLiteral(function) => emit_function(db, env, qualified, function, lines), Type::KnownInstance(KnownInstanceType::TypeAliasType(alias)) | Type::TypeAlias(alias) => { - emit_type_alias(db, qualified, alias, lines); + emit_type_alias(db, env, qualified, alias, lines); } - Type::PropertyInstance(property) => emit_property(db, qualified, property, lines), + Type::PropertyInstance(property) => emit_property(db, env, qualified, property, lines), Type::Union(union) if union .elements(db) @@ -197,7 +225,7 @@ fn emit_symbol<'db>( .all(|t| matches!(t, Type::PropertyInstance(_))) => { let merged = merge_property_union(db, union); - emit_property(db, qualified, merged, lines); + emit_property(db, env, qualified, merged, lines); } Type::ModuleLiteral(module_lit) => { let target = module_lit.module(db).name(db).as_str(); @@ -207,7 +235,7 @@ fn emit_symbol<'db>( lines.push(format!( "{qualified}:v{}={}", render_qualifiers(qualifiers), - render_type(db, ty) + render_type(db, env, ty) )); } } @@ -215,6 +243,7 @@ fn emit_symbol<'db>( fn emit_class<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, qualified: &str, name: &Name, class: ClassLiteral<'db>, @@ -231,7 +260,7 @@ fn emit_class<'db>( ClassLiteral::Static(static_class) => static_class .explicit_bases(db) .iter() - .map(|base| render_class_base(db, *base)) + .map(|base| render_class_base(db, env, *base)) .collect::>() .join(","), _ => String::new(), @@ -247,18 +276,20 @@ fn emit_class<'db>( let class_self_name = name.clone(); emit_class_members( db, + env, body_scope, qualified, &class_self_name, lines, visited_classes, ); - emit_instance_attributes(db, body_scope, qualified, lines); + emit_instance_attributes(db, env, body_scope, qualified, lines); } } fn emit_class_members<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, scope: ScopeId<'db>, qualified_prefix: &str, class_self_name: &Name, @@ -279,11 +310,11 @@ fn emit_class_members<'db>( } let place_and_qualifiers = - place_from_declarations(db, declarations).ignore_conflicting_declarations(); + place_from_declarations(db, env, declarations).ignore_conflicting_declarations(); let declaration_ty = place_and_qualifiers.place.ignore_possibly_undefined(); let qualifiers = place_and_qualifiers.qualifiers; - let binding_ty = place_from_bindings(db, bindings) + let binding_ty = place_from_bindings(db, env, bindings) .place .ignore_possibly_undefined(); @@ -292,7 +323,16 @@ fn emit_class_members<'db>( }; let qualified = format!("{qualified_prefix}.{name}"); - emit_symbol(db, &qualified, name, ty, qualifiers, lines, visited_classes); + emit_symbol( + db, + env, + &qualified, + name, + ty, + qualifiers, + lines, + visited_classes, + ); } } @@ -301,12 +341,13 @@ fn emit_class_members<'db>( /// bindings as fallback fn emit_instance_attributes<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, body_scope: ScopeId<'db>, qualified_prefix: &str, lines: &mut Vec, ) { let file = body_scope.file(db); - let index = semantic_index(db, file); + let index = semantic_index(db, db.program_file(file)); let mut seen: FxHashSet = FxHashSet::default(); for function_scope_id in attribute_scopes(db, body_scope) { @@ -322,14 +363,15 @@ fn emit_instance_attributes<'db>( continue; } - let Some((ty, qualifiers)) = lookup_instance_attribute(db, body_scope, name) else { + let Some((ty, qualifiers)) = lookup_instance_attribute(db, env, body_scope, name) + else { continue; }; let qualified = format!("{qualified_prefix}.{name}"); lines.push(format!( "{qualified}:i{}={}", render_qualifiers(qualifiers), - render_type(db, ty) + render_type(db, env, ty) )); } } @@ -337,11 +379,12 @@ fn emit_instance_attributes<'db>( fn lookup_instance_attribute<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class_body_scope: ScopeId<'db>, name: &str, ) -> Option<(Type<'db>, TypeQualifiers)> { let file = class_body_scope.file(db); - let index = semantic_index(db, file); + let index = semantic_index(db, db.program_file(file)); // try declarations first across all attribute scopes for function_scope_id in attribute_scopes(db, class_body_scope) { @@ -351,7 +394,7 @@ fn lookup_instance_attribute<'db>( }; let use_def = index.use_def_map(function_scope_id); let place_and_qualifiers = - place_from_declarations(db, use_def.reachable_member_declarations(member)) + place_from_declarations(db, env, use_def.reachable_member_declarations(member)) .ignore_conflicting_declarations(); if let Some(ty) = place_and_qualifiers.place.ignore_possibly_undefined() { return Some((ty, place_and_qualifiers.qualifiers)); @@ -365,7 +408,7 @@ fn lookup_instance_attribute<'db>( continue; }; let use_def = index.use_def_map(function_scope_id); - let binding = place_from_bindings(db, use_def.reachable_member_bindings(member)); + let binding = place_from_bindings(db, env, use_def.reachable_member_bindings(member)); if let Some(ty) = binding.place.ignore_possibly_undefined() { return Some((ty, TypeQualifiers::empty())); } @@ -376,6 +419,7 @@ fn lookup_instance_attribute<'db>( fn emit_function<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, qualified: &str, function: FunctionType<'db>, lines: &mut Vec, @@ -383,13 +427,14 @@ fn emit_function<'db>( let decorators = render_function_decorators(db, function); for signature in function.signature(db) { let typevars = render_signature_typevars(db, signature); - let body = render_signature_body(db, signature); + let body = render_signature_body(db, env, signature); lines.push(format!("{qualified}:d{decorators}{typevars}{body}")); } } fn emit_type_alias<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, qualified: &str, alias: TypeAliasType<'db>, lines: &mut Vec, @@ -403,7 +448,7 @@ fn emit_type_alias<'db>( }; lines.push(format!( "{qualified}:t{typevars}={}", - render_type(db, alias.value_type(db)) + render_type(db, env, alias.value_type(db)) )); } @@ -432,6 +477,7 @@ fn merge_property_union<'db>(db: &'db dyn Db, union: UnionType<'db>) -> Property fn emit_property<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, qualified: &str, property: PropertyInstanceType<'db>, lines: &mut Vec, @@ -456,7 +502,7 @@ fn emit_property<'db>( } let accessors_str = accessors.join(","); let type_str = return_ty - .map(|ty| format!("={}", render_type(db, ty))) + .map(|ty| format!("={}", render_type(db, env, ty))) .unwrap_or_default(); lines.push(format!("{qualified}:p[{accessors_str}]{type_str}")); } @@ -566,7 +612,11 @@ fn render_generic_args<'db>(db: &'db dyn Db, context: crate::types::GenericConte } } -fn render_signature_body<'db>(db: &'db dyn Db, signature: &Signature<'db>) -> String { +fn render_signature_body<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + signature: &Signature<'db>, +) -> String { let parameters = signature.parameters(); // ignore `self`/`cls`, which ty synthesises as positional-only — leaking // the `/` marker into the lockfile for every method is noise without @@ -617,7 +667,7 @@ fn render_signature_body<'db>(db: &'db dyn Db, signature: &Signature<'db>) -> St emitted_kw_only_boundary = true; } - let annotation = render_type(db, parameter.annotated_type()); + let annotation = render_type(db, env, parameter.annotated_type()); let token = match parameter.kind() { ParameterKind::PositionalOnly { name, default_type } => { let label = match name.as_ref() { @@ -655,7 +705,7 @@ fn render_signature_body<'db>(db: &'db dyn Db, signature: &Signature<'db>) -> St out.push_str(&tokens.join(",")); out.push(')'); - write!(out, "->{}", render_type(db, signature.return_ty)).unwrap(); + write!(out, "->{}", render_type(db, env, signature.return_ty)).unwrap(); out } @@ -664,7 +714,7 @@ fn render_signature_body<'db>(db: &'db dyn Db, signature: &Signature<'db>) -> St /// disambiguated even if two modules export classes with the same short name. /// unions and intersections are recursively walked so each member keeps its /// module prefix -fn render_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> String { +fn render_type<'db>(db: &'db dyn Db, env: &ProgramEnvironment<'db>, ty: Type<'db>) -> String { if ty.is_none(db) { return "None".to_string(); } @@ -675,28 +725,28 @@ fn render_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> String { } match ty { Type::NominalInstance(instance) => { - let class_name = instance.class_name(db).as_str().to_owned(); + let class_name = instance.class_name(db, env).as_str().to_owned(); // anonymous named-tuple types: render structurally so two // identically-shaped anon NTs share a lockfile representation // and the synthesized hash-suffixed class name doesn't leak if class_name.starts_with("_AnonNamedTuple_") { - if let Some(structural) = render_anon_named_tuple(db, instance) { + if let Some(structural) = render_anon_named_tuple(db, env, instance) { return structural; } } let module = instance - .class_module_name(db) + .class_module_name(db, env) .map(|m| format!("{}.", m.as_str())) .unwrap_or_default(); let base = format!("{module}{class_name}"); // surface generic args so `list[int]` and `list[str]` don't both // collapse to `builtins.list` in the lockfile - if let Some(alias) = instance.class(db).into_generic_alias() { + if let Some(alias) = instance.class(db, env).into_generic_alias() { let args: Vec = alias .specialization(db) .types(db) .iter() - .map(|arg| render_type(db, *arg)) + .map(|arg| render_type(db, env, *arg)) .collect(); if !args.is_empty() { return format!("{base}[{}]", args.join(",")); @@ -711,7 +761,7 @@ fn render_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> String { .specialization(db) .types(db) .iter() - .map(|arg| render_type(db, *arg)) + .map(|arg| render_type(db, env, *arg)) .collect(); if args.is_empty() { origin @@ -730,7 +780,7 @@ fn render_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> String { has_none = true; } else if t.as_literal_value_kind().is_some() { // peel the `Literal[...]` wrapper if the display added it - let rendered = render_type(db, *t); + let rendered = render_type(db, env, *t); let inner = rendered .strip_prefix("Literal[") .and_then(|s| s.strip_suffix(']')) @@ -738,7 +788,7 @@ fn render_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> String { .unwrap_or(rendered); literal_parts.push(inner); } else { - other_parts.push(render_type(db, *t)); + other_parts.push(render_type(db, env, *t)); } } literal_parts.sort(); @@ -765,11 +815,11 @@ fn render_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> String { Type::Intersection(intersection) => { let mut positives: Vec = intersection .iter_positive(db) - .map(|t| render_type(db, t)) + .map(|t| render_type(db, env, t)) .collect(); let mut negatives: Vec = intersection .iter_negative(db) - .map(|t| render_type(db, t)) + .map(|t| render_type(db, env, t)) .collect(); positives.sort(); negatives.sort(); @@ -778,7 +828,7 @@ fn render_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> String { parts.extend(negatives.into_iter().map(|n| format!("~{n}"))); parts.join(" & ") } - _ => ty.display(db).to_string(), + _ => ty.display(db, env).to_string(), } } @@ -789,9 +839,10 @@ fn render_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> String { /// source may not expose body declarations to ty). fn render_anon_named_tuple<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, instance: NominalInstanceType<'db>, ) -> Option { - let class_literal = instance.class(db).class_literal(db); + let class_literal = instance.class(db, env).class_literal(db); if let ClassLiteral::Static(static_class) = class_literal { let body_scope = static_class.body_scope(db); let table = place_table(db, body_scope); @@ -804,11 +855,11 @@ fn render_anon_named_tuple<'db>( continue; } let place_and_qualifiers = - place_from_declarations(db, declarations).ignore_conflicting_declarations(); + place_from_declarations(db, env, declarations).ignore_conflicting_declarations(); let Some(decl_ty) = place_and_qualifiers.place.ignore_possibly_undefined() else { continue; }; - fields.push((name, render_type(db, decl_ty))); + fields.push((name, render_type(db, env, decl_ty))); } if !fields.is_empty() { let body = fields @@ -821,20 +872,20 @@ fn render_anon_named_tuple<'db>( } // fall back to tuple-spec elements when body declarations aren't // available - let spec = instance.tuple_spec(db)?; + let spec = instance.tuple_spec(db, env)?; let elements: Vec<&Type<'db>> = spec.fixed_elements().collect(); if elements.is_empty() { return None; } let body = elements .iter() - .map(|t| render_type(db, **t)) + .map(|t| render_type(db, env, **t)) .collect::>() .join(", "); Some(format!("({body})")) } -fn render_reexport<'db>(db: &'db dyn Db, ty: Type<'db>) -> String { +fn render_reexport<'db>(db: &'db dyn Db, env: &ProgramEnvironment<'db>, ty: Type<'db>) -> String { match ty { Type::ClassLiteral(class) => qualified_class_name(db, class), Type::GenericAlias(alias) => { @@ -843,7 +894,7 @@ fn render_reexport<'db>(db: &'db dyn Db, ty: Type<'db>) -> String { .specialization(db) .types(db) .iter() - .map(|arg| render_type(db, *arg)) + .map(|arg| render_type(db, env, *arg)) .collect(); if args.is_empty() { origin @@ -852,7 +903,7 @@ fn render_reexport<'db>(db: &'db dyn Db, ty: Type<'db>) -> String { } } Type::FunctionLiteral(function) => { - let module = file_to_module(db, function.file(db)) + let module = file_to_module(db, function.program_file(db).resolver_file(db)) .map(|m| format!("{}.", m.name(db).as_str())) .unwrap_or_default(); format!("{module}{}", function.name(db).as_str()) @@ -860,12 +911,12 @@ fn render_reexport<'db>(db: &'db dyn Db, ty: Type<'db>) -> String { Type::TypeAlias(TypeAliasType::PEP695(alias)) | Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(alias))) => { let file = alias.rhs_scope(db).file(db); - let module = file_to_module(db, file) + let module = file_to_module(db, db.program_file(file).resolver_file(db)) .map(|m| format!("{}.", m.name(db).as_str())) .unwrap_or_default(); format!("{module}{}", alias.name(db)) } - _ => render_type(db, ty), + _ => render_type(db, env, ty), } } @@ -927,7 +978,7 @@ fn is_named_tuple<'db>(db: &'db dyn Db, class: ClassLiteral<'db>) -> bool { } } -fn render_class_base<'db>(db: &'db dyn Db, ty: Type<'db>) -> String { +fn render_class_base<'db>(db: &'db dyn Db, env: &ProgramEnvironment<'db>, ty: Type<'db>) -> String { match ty { Type::ClassLiteral(class) => qualified_class_name(db, class), Type::GenericAlias(alias) => { @@ -936,7 +987,7 @@ fn render_class_base<'db>(db: &'db dyn Db, ty: Type<'db>) -> String { .specialization(db) .types(db) .iter() - .map(|arg| render_type(db, *arg)) + .map(|arg| render_type(db, env, *arg)) .collect(); if args.is_empty() { origin @@ -944,14 +995,14 @@ fn render_class_base<'db>(db: &'db dyn Db, ty: Type<'db>) -> String { format!("{origin}[{}]", args.join(",")) } } - _ => render_type(db, ty), + _ => render_type(db, env, ty), } } fn qualified_class_name<'db>(db: &'db dyn Db, class: ClassLiteral<'db>) -> String { let class_name = class.name(db).as_str(); let class_file = class.file(db); - if let Some(module) = file_to_module(db, class_file) { + if let Some(module) = file_to_module(db, db.program_file(class_file).resolver_file(db)) { format!("{}.{}", module.name(db).as_str(), class_name) } else { class_name.to_string() diff --git a/crates/ty_python_semantic/src/db.rs b/crates/ty_python_semantic/src/db.rs index f2c62e42c8..01f3ff5189 100644 --- a/crates/ty_python_semantic/src/db.rs +++ b/crates/ty_python_semantic/src/db.rs @@ -1,15 +1,21 @@ -use crate::AnalysisSettings; use crate::dependencies::DependencyManifest; use crate::lint::{LintRegistry, RuleSelection}; +use crate::{AnalysisSettings, PythonVersionWithSource}; use ruff_db::diagnostic::Diagnostic; use ruff_db::files::File; -use ty_python_core::Db as PythonCoreDb; +use ty_python_core::{Db as PythonCoreDb, ProgramFile}; /// Database giving access to semantic information about a Python program. #[salsa::db] pub trait Db: PythonCoreDb { fn check_file(&self, file: File) -> Vec; + /// Returns the program file for `file`. + fn program_file(&self, file: File) -> ProgramFile<'_>; + + /// Returns the Python version and its configuration source for `file`. + fn python_version_with_source(&self, file: File) -> &PythonVersionWithSource; + /// Resolves the rule selection for a given file. fn rule_selection(&self, file: File) -> &RuleSelection; @@ -88,7 +94,7 @@ pub(crate) mod tests { use anyhow::Context; use ty_python_core::platform::PythonPlatform; - use crate::{check_file_unwrap, default_lint_registry}; + use crate::{ProgramEnvironment, check_file_unwrap, default_lint_registry}; use ruff_db::Db as SourceDb; use ruff_db::files::Files; use ruff_db::system::{ @@ -96,8 +102,9 @@ pub(crate) mod tests { }; use ruff_db::vendored::VendoredFileSystem; use ruff_python_ast::PythonVersion; - use ty_module_resolver::{Db as ModuleResolverDb, SearchPathSettings, SearchPaths}; - use ty_python_core::program::{FallibleStrategy, Program, ProgramSettings}; + use ty_module_resolver::{Db as ModuleResolverDb, SearchPathSettings}; + use ty_python_core::TestProgramDb; + use ty_python_core::program::{FallibleStrategy, ProgramSettings}; use ty_site_packages::{PythonVersionSource, PythonVersionWithSource}; type Events = Arc>>; @@ -113,11 +120,14 @@ pub(crate) mod tests { rule_selection: Arc, analysis_settings: Arc, open_files: rustc_hash::FxHashSet, + program_settings: ProgramSettings, } impl TestDb { - pub(crate) fn new() -> Self { + fn new() -> Self { let events = Events::default(); + let vendored = ty_vendored::file_system().clone(); + let program_settings = ProgramSettings::empty(&vendored); Self { storage: salsa::Storage::new(Some(Box::new({ let events = events.clone(); @@ -128,15 +138,24 @@ pub(crate) mod tests { } }))), system: TestSystem::default(), - vendored: ty_vendored::file_system().clone(), + vendored, events, files: Files::default(), rule_selection: Arc::new(RuleSelection::from_registry(default_lint_registry())), analysis_settings: AnalysisSettings::default().into(), open_files: rustc_hash::FxHashSet::default(), + program_settings, } } + pub(crate) fn python_version(&self) -> PythonVersion { + self.program().python_version(self) + } + + pub(crate) fn program_environment(&self) -> ProgramEnvironment<'_> { + ProgramEnvironment::from_program(self.program()) + } + /// Marks `file` as open in the editor. /// /// This is untracked state: open a file before running any queries. @@ -196,10 +215,6 @@ pub(crate) mod tests { fn files(&self) -> &Files { &self.files } - - fn python_version(&self) -> PythonVersion { - Program::get(self).python_version(self) - } } #[salsa::db] @@ -209,6 +224,13 @@ pub(crate) mod tests { } } + #[salsa::db] + impl TestProgramDb for TestDb { + fn program_settings(&self) -> &ProgramSettings { + &self.program_settings + } + } + #[salsa::db] impl Db for TestDb { fn check_file(&self, file: File) -> Vec { @@ -216,7 +238,15 @@ pub(crate) mod tests { return Vec::new(); } - check_file_unwrap(self, file) + check_file_unwrap(self, self.program_file(file)) + } + + fn program_file(&self, file: File) -> ProgramFile<'_> { + self.program().program_file(self, file) + } + + fn python_version_with_source(&self, _file: File) -> &PythonVersionWithSource { + &self.program_settings.python_version } fn rule_selection(&self, _file: File) -> &RuleSelection { @@ -245,11 +275,7 @@ pub(crate) mod tests { } #[salsa::db] - impl ModuleResolverDb for TestDb { - fn search_paths(&self) -> &SearchPaths { - Program::get(self).search_paths(self) - } - } + impl ModuleResolverDb for TestDb {} #[salsa::db] impl salsa::Database for TestDb {} @@ -324,19 +350,18 @@ pub(crate) mod tests { ..SearchPathSettings::new(vec![src_root]) }; - Program::from_settings( - &db, - ProgramSettings { - python_version: PythonVersionWithSource { - version: self.python_version, - source: PythonVersionSource::default(), - }, - python_platform: self.python_platform, - search_paths: search_paths - .to_search_paths(db.system(), db.vendored(), &FallibleStrategy) - .context("Invalid search path settings")?, + let program_settings = ProgramSettings { + python_version: PythonVersionWithSource { + version: self.python_version, + source: PythonVersionSource::default(), }, - ); + python_platform: self.python_platform, + search_paths: search_paths + .to_search_paths(db.system(), db.vendored(), &FallibleStrategy) + .context("Invalid search path settings")?, + }; + program_settings.search_paths.try_register_static_roots(&db); + db.program_settings = program_settings; Ok(db) } diff --git a/crates/ty_python_semantic/src/dependencies.rs b/crates/ty_python_semantic/src/dependencies.rs index 648287762f..a9c8eeb928 100644 --- a/crates/ty_python_semantic/src/dependencies.rs +++ b/crates/ty_python_semantic/src/dependencies.rs @@ -250,7 +250,7 @@ pub fn import_standing<'db>( return ImportStanding::Unknown; }; - let index = distribution_index(db); + let index = distribution_index(db, db.program_file(file).resolver_environment(db)); if index.is_empty() { // nothing in the environment could be attributed to a distribution, so // an import being unattributable says nothing about the import @@ -291,7 +291,9 @@ fn is_shipped( manifest: &DependencyManifest, shipped_modules: Option<&[Box]>, ) -> bool { - let Some(module) = ty_module_resolver::file_to_module(db, file) else { + let Some(module) = + ty_module_resolver::file_to_module(db, db.program_file(file).resolver_file(db)) + else { return false; }; let Some(search_path) = module.search_path(db) else { diff --git a/crates/ty_python_semantic/src/diagnostic/mod.rs b/crates/ty_python_semantic/src/diagnostic/mod.rs index fcc86256fb..f8346f25a0 100644 --- a/crates/ty_python_semantic/src/diagnostic/mod.rs +++ b/crates/ty_python_semantic/src/diagnostic/mod.rs @@ -1,5 +1,5 @@ use crate::{ - Db, Program, PythonVersionSource, PythonVersionWithSource, lint::lint_documentation_url, + Db, PythonVersionSource, PythonVersionWithSource, lint::lint_documentation_url, types::TypeCheckDiagnostics, }; use levenshtein::{HideUnderscoredSuggestions, find_best_suggestion}; @@ -33,9 +33,10 @@ pub fn inferred_python_version_source_annotation( .as_ref() .and_then(|source| source.span(db)) .map(Annotation::primary), - PythonVersionSource::Cli | PythonVersionSource::Editor | PythonVersionSource::Default => { - None - } + PythonVersionSource::Cli + | PythonVersionSource::Editor + | PythonVersionSource::UvWorkspace + | PythonVersionSource::Default => None, } } @@ -43,13 +44,13 @@ pub fn inferred_python_version_source_annotation( /// /// 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( +pub(crate) fn add_inferred_python_version_hint_to_diagnostic( db: &dyn Db, + file: File, diagnostic: &mut Diagnostic, action: &str, ) { - let program = Program::get(db); - let PythonVersionWithSource { version, source } = program.python_version_with_source(db); + let PythonVersionWithSource { version, source } = db.python_version_with_source(file); match source { crate::PythonVersionSource::Cli => { @@ -100,6 +101,11 @@ pub fn add_inferred_python_version_hint_to_diagnostic( because it's the version of the selected Python interpreter in your editor", )); } + crate::PythonVersionSource::UvWorkspace => { + diagnostic.info(format_args!( + "Python {version} was assumed when {action} because it was provided by uv workspace metadata", + )); + } crate::PythonVersionSource::InstallationDirectoryLayout { site_packages_parent_dir, source: _, diff --git a/crates/ty_python_semantic/src/django_settings.rs b/crates/ty_python_semantic/src/django_settings.rs index f0852500f3..feadba0ec4 100644 --- a/crates/ty_python_semantic/src/django_settings.rs +++ b/crates/ty_python_semantic/src/django_settings.rs @@ -22,7 +22,9 @@ use ty_module_resolver::{ModuleName, file_to_module, resolve_module}; use crate::Db; use crate::place::imported_symbol; +use crate::types::ProgramEnvironment; use crate::types::{ClassType, Type}; +use ty_module_resolver::ImportingFile; /// the environment variable a project names its settings module with const SETTINGS_MODULE_VARIABLE: &str = "DJANGO_SETTINGS_MODULE"; @@ -81,7 +83,15 @@ pub fn settings_file(db: &dyn Db, namings: &[SettingsNaming]) -> Option { .find(|naming| is_entry_point(db, naming.file)) .or_else(|| namings.first())?; - resolve_module(db, naming.file, &ModuleName::new(&naming.module)?)?.file(db) + resolve_module( + db, + ImportingFile::File( + naming.file, + db.program_file(naming.file).resolver_environment(db), + ), + &ModuleName::new(&naming.module)?, + )? + .file(db) } /// the naming that is django's own entry point, the script `manage.py test`, @@ -112,7 +122,7 @@ pub fn settings_module_in_file(db: &dyn Db, file: File) -> Option return None; } - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, db.program_file(file).python_file(db)).load(db); let mut visitor = SettingsModuleVisitor { found: None }; visitor.visit_body(parsed.suite()); @@ -157,16 +167,20 @@ impl<'ast> Visitor<'ast> for SettingsModuleVisitor { /// whether `ty` is `django.conf.settings` — the one object whose attributes are /// the project's settings -pub(crate) fn is_settings_instance(db: &dyn Db, ty: Type<'_>) -> bool { +pub(crate) fn is_settings_instance( + db: &dyn Db, + env: &ProgramEnvironment<'_>, + ty: Type<'_>, +) -> bool { // a `LazySettings()` built by hand is inferred exactly, and the restriction // says nothing about which class it is let Type::NominalInstance(instance) = ty.erase_restriction(db) else { return false; }; - let class = instance.class(db).class_literal(db); + let class = instance.class(db, env).class_literal(db); class.name(db) == SETTINGS_CLASS - && file_to_module(db, class.file(db)) + && file_to_module(db, class.program_file(db).resolver_file(db)) .is_some_and(|module| module.name(db) == SETTINGS_MODULE) } @@ -177,7 +191,11 @@ pub(crate) fn is_settings_instance(db: &dyn Db, ty: Type<'_>) -> bool { /// today — whenever the module cannot be reached, does not bind the name, or /// binds it to something whose type says less than it appears to. see /// [`describes_the_setting`]. -pub(crate) fn settings_member<'db>(db: &'db dyn Db, name: &Name) -> Option> { +pub(crate) fn settings_member<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &Name, +) -> Option> { // django copies a name off the settings module only when `name.isupper()`, // so a module's `BASE_DIR` is a setting and its `os` is not if !is_setting_name(name) { @@ -185,14 +203,14 @@ pub(crate) fn settings_member<'db>(db: &'db dyn Db, name: &Name) -> Option bool { /// carries no arguments has no such gap: `ROOT_URLCONF = "project.urls"` is a /// `str` and there is nothing about the string's contents to be too narrow /// about. -fn describes_the_setting(db: &dyn Db, ty: Type<'_>) -> bool { +fn describes_the_setting(db: &dyn Db, env: &ProgramEnvironment<'_>, ty: Type<'_>) -> bool { match ty { Type::NominalInstance(instance) => { - matches!(instance.class(db), ClassType::NonGeneric(_)) + matches!(instance.class(db, env), ClassType::NonGeneric(_)) } Type::Union(union) => union .elements(db) .iter() - .all(|element| describes_the_setting(db, *element)), + .all(|element| describes_the_setting(db, env, *element)), _ => false, } } diff --git a/crates/ty_python_semantic/src/dunder_all.rs b/crates/ty_python_semantic/src/dunder_all.rs index c8fcf93be0..3a1dedc325 100644 --- a/crates/ty_python_semantic/src/dunder_all.rs +++ b/crates/ty_python_semantic/src/dunder_all.rs @@ -1,22 +1,22 @@ -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 rustc_hash::FxHashSet; -use ty_module_resolver::{ModuleName, resolve_module}; +use ty_module_resolver::{ImportingFile, ModuleName, resolve_module}; -use crate::Db; use crate::types::{Type, TypeContext, infer_expression_types}; -use ty_python_core::{SemanticIndex, Truthiness, semantic_index}; +use crate::{Db, ProgramEnvironment}; +use ty_python_core::{ProgramFile, SemanticIndex, Truthiness, semantic_index}; /// 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_initial=|_, _, _| None, heap_size=ruff_memory_usage::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(); +pub(crate) fn dunder_all_names(db: &dyn Db, file: ProgramFile<'_>) -> Option> { + let source_file = file.file(db); + let _span = tracing::trace_span!("dunder_all_names", file=?source_file.path(db)).entered(); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, file.python_file(db)).load(db); let index = semantic_index(db, file); let mut collector = DunderAllNamesCollector::new(db, file, index); collector.visit_body(module.suite()); @@ -26,7 +26,8 @@ pub(crate) fn dunder_all_names(db: &dyn Db, file: File) -> Option { db: &'db dyn Db, - file: File, + env: ProgramEnvironment<'db>, + file: ProgramFile<'db>, /// The semantic index for the module. index: &'db SemanticIndex<'db>, @@ -43,9 +44,10 @@ struct DunderAllNamesCollector<'db> { } impl<'db> DunderAllNamesCollector<'db> { - fn new(db: &'db dyn Db, file: File, index: &'db SemanticIndex<'db>) -> Self { + fn new(db: &'db dyn Db, file: ProgramFile<'db>, index: &'db SemanticIndex<'db>) -> Self { Self { db, + env: ProgramEnvironment::from_file(file), file, index, origin: None, @@ -70,6 +72,7 @@ impl<'db> DunderAllNamesCollector<'db> { /// /// Returns `true` if the expression is a valid list/tuple/set or module `__all__`, `false` otherwise. fn extend(&mut self, expr: &ast::Expr) -> bool { + let db = self.db; match expr { // `__all__ += [...]` // `__all__.extend([...])` @@ -83,14 +86,16 @@ impl<'db> DunderAllNamesCollector<'db> { 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(self.db) - .and_then(|file| dunder_all_names(self.db, file)) + .module(db) + .file(db) + .map(|file| ProgramFile::new(db, file, self.env.program(db))) + .and_then(|file| dunder_all_names(db, file)) else { // The module either does not have a `__all__` variable or it is invalid. return false; @@ -156,10 +161,17 @@ impl<'db> DunderAllNamesCollector<'db> { &self, import_from: &ast::StmtImportFrom, ) -> Option<&'db FxHashSet> { + let db = self.db; + + let importing_file = + ImportingFile::File(self.file.file(db), self.env.resolver_environment(db)); let module_name = - ModuleName::from_import_statement(self.db, self.file, import_from).ok()?; - let module = resolve_module(self.db, self.file, &module_name)?; - dunder_all_names(self.db, module.file(self.db)?) + ModuleName::from_import_statement(db, importing_file, import_from).ok()?; + let module = resolve_module(db, importing_file, &module_name)?; + dunder_all_names( + db, + ProgramFile::new(db, module.file(db)?, self.env.program(db)), + ) } /// Infer the type of a standalone expression. @@ -168,7 +180,8 @@ impl<'db> DunderAllNamesCollector<'db> { /// /// 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), TypeContext::default()) + let db = self.db; + infer_expression_types(db, self.index.expression(expr), TypeContext::default()) .expression_type(expr) } @@ -176,7 +189,10 @@ impl<'db> DunderAllNamesCollector<'db> { /// /// 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() + let db = self.db; + self.standalone_expression_type(expr) + .try_bool(db, &self.env) + .ok() } /// Add valid names to the set. @@ -197,10 +213,11 @@ impl<'db> DunderAllNamesCollector<'db> { /// Returns [`None`] if `__all__` is not defined in the current module or if it contains /// invalid elements. fn into_names(mut self) -> Option> { + let db = self.db; if self.origin.is_none() { None } else if self.invalid { - tracing::debug!("Invalid `__all__` in `{}`", self.file.path(self.db)); + tracing::debug!("Invalid `__all__` in `{}`", self.file.file(db).path(db)); None } else { self.names.shrink_to_fit(); diff --git a/crates/ty_python_semantic/src/fixes.rs b/crates/ty_python_semantic/src/fixes.rs index e8cef05179..a88617b453 100644 --- a/crates/ty_python_semantic/src/fixes.rs +++ b/crates/ty_python_semantic/src/fixes.rs @@ -1,4 +1,5 @@ use crate::{SuppressFix, is_unused_ignore_comment_lint, suppress_all}; +use ruff_db::PythonFile; use ruff_db::cancellation::{Canceled, CancellationToken}; use ruff_db::diagnostic::{DisplayDiagnosticConfig, DisplayDiagnostics}; use ruff_db::parsed::{ParsedModuleRef, parsed_module}; @@ -128,13 +129,14 @@ where continue; }; - let parsed = parsed_module(db, file); + let python_file = db.program_file(file).python_file(db); + let parsed = parsed_module(db, python_file); if parsed.load(db).has_syntax_errors() { tracing::warn!("Skipping file `{path}` with syntax errors"); continue; } - let fixes = fix_mode.fixes(db, file, diagnostics); + let fixes = fix_mode.fixes(db, python_file, diagnostics); if fixes.is_empty() { tracing::debug!("Skipping file `{path}` without applicable fixes."); @@ -379,7 +381,12 @@ impl FixMode { } } - fn fixes(self, db: &dyn Db, file: File, file_diagnostics: &[Diagnostic]) -> Vec { + fn fixes( + self, + db: &dyn Db, + file: PythonFile<'_>, + file_diagnostics: &[Diagnostic], + ) -> Vec { match self { FixMode::Suppress => { let suppressable_diagnostics: Vec<_> = file_diagnostics @@ -447,7 +454,7 @@ struct ApplicableFix { /// Gets fixed to: /// /// ```py - /// enumerate(0, "1") # ty:ignore[invalid-argument-type] + /// enumerate(0, "1") # ty: ignore[invalid-argument-type] /// ``` /// /// In which case `fixed_diagnostics` is 2. @@ -768,7 +775,8 @@ where let db = &*db; - let parsed = parsed_module(db, file.file); + let python_file = db.program_file(file.file).python_file(db); + let parsed = parsed_module(db, python_file); let parsed = parsed.load(db); let result = if parsed.has_syntax_errors() { @@ -778,7 +786,7 @@ where CheckResult::SyntaxError { diagnostic, file } } else { let diagnostics = check_file(db, file.file); - let fixes = fix_mode.fixes(db, file.file, &diagnostics); + let fixes = fix_mode.fixes(db, python_file, &diagnostics); file.applied_fixes += applied_fixes; file.diagnostics = Some(diagnostics); @@ -797,9 +805,6 @@ where #[cfg(test)] mod tests { - use std::collections::hash_map::Entry; - use std::hash::{DefaultHasher, Hash, Hasher}; - use insta::assert_snapshot; use ruff_db::cancellation::CancellationTokenSource; use ruff_db::diagnostic::{ @@ -813,6 +818,8 @@ mod tests { use ruff_diagnostics::{Applicability, Edit, Fix}; use ruff_text_size::{TextLen as _, TextRange, TextSize}; use rustc_hash::FxHashMap; + use std::collections::hash_map::Entry; + use std::hash::{DefaultHasher, Hash, Hasher}; use super::suppress_all_diagnostics; use crate::Db; @@ -832,7 +839,7 @@ mod tests { ## Fixed source ```py - a = b + 10 # ty:ignore[unresolved-reference] + a = b + 10 # ty: ignore[unresolved-reference] ``` "); } @@ -849,7 +856,7 @@ mod tests { ## Fixed source ```py - a = b + 10 + c # ty:ignore[unresolved-reference] + a = b + 10 + c # ty: ignore[unresolved-reference] ``` "); } @@ -868,7 +875,7 @@ mod tests { ```py import sys - a = b + 10 + sys.veeersion # ty:ignore[unresolved-attribute, unresolved-reference] + a = b + 10 + sys.veeersion # ty: ignore[unresolved-attribute, unresolved-reference] ``` "); } @@ -898,8 +905,12 @@ mod tests { 1 | import sys 2 | a = 5 + 10 # ty: ignore[unresolved-reference] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove the unused suppression comment + | + 1 | import sys + - a = 5 + 10 # ty: ignore[unresolved-reference] + 2 + a = 5 + 10 + | "); } @@ -929,7 +940,6 @@ mod tests { 1 | import sys 2 | a = x + | ^ - | error[invalid-syntax]: Expected an expression --> test.py:2:8 @@ -937,7 +947,6 @@ mod tests { 1 | import sys 2 | a = x + | ^ - | "); } @@ -967,8 +976,8 @@ mod tests { test( a = 10, - c = "unknown" # ty:ignore[unknown-argument] - ) # ty:ignore[missing-argument] + c = "unknown" # ty: ignore[unknown-argument] + ) # ty: ignore[missing-argument] ``` "#); } @@ -1013,8 +1022,8 @@ mod tests { def f(): diag = get_data() - diag["home_assistant"]["entities"] = sorted( # ty:ignore[invalid-assignment] - diag["home_assistant"]["entities"], key=lambda ent: ent["entity_id"] # ty:ignore[invalid-argument-type, not-subscriptable] + diag["home_assistant"]["entities"] = sorted( # ty: ignore[invalid-assignment] + diag["home_assistant"]["entities"], key=lambda ent: ent["entity_id"] # ty: ignore[invalid-argument-type, not-subscriptable] ) ``` "#); @@ -1056,8 +1065,8 @@ mod tests { def f(): diag = get_data() - diag["home_assistant"]["entities"] = sorted( # ty:ignore[invalid-assignment] - diag["home_assistant"]["entities"], key=lambda ent: ent["entity_id"] # ty:ignore[invalid-argument-type, not-subscriptable] + diag["home_assistant"]["entities"] = sorted( # ty: ignore[invalid-assignment] + diag["home_assistant"]["entities"], key=lambda ent: ent["entity_id"] # ty: ignore[invalid-argument-type, not-subscriptable] ); missing # ty: ignore[unresolved-reference] ``` "# @@ -1094,7 +1103,7 @@ class B(A): def test( self, b: str - ) -> A.b: # ty:ignore[invalid-method-override, unresolved-attribute] + ) -> A.b: # ty: ignore[invalid-method-override, unresolved-attribute] pass ``` "#); @@ -1130,7 +1139,7 @@ class B(A): def test( # ty:ignore[unresolved-reference, invalid-method-override] self, b: str - ) -> A.b: # ty:ignore[unresolved-attribute] + ) -> A.b: # ty: ignore[unresolved-attribute] pass ``` @@ -1146,6 +1155,12 @@ class B(A): 9 | b: str | help: Remove the unused suppression code + | + 6 | class B(A): + - def test( # ty:ignore[unresolved-reference, invalid-method-override] + 7 + def test( # ty:ignore[invalid-method-override] + 8 | self, + | "#); } @@ -1179,7 +1194,7 @@ class B(A): ## Fixed source ```py - value = missing # ty: ignore[] tracked by [123] # ty:ignore[unresolved-reference] + value = missing # ty: ignore[] tracked by [123] # ty: ignore[unresolved-reference] ``` ## Diagnostics after applying fixes @@ -1187,10 +1202,13 @@ class B(A): warning[unused-ignore-comment]: Unused `ty: ignore` without a code --> test.py:1:18 | - 1 | value = missing # ty: ignore[] tracked by [123] # ty:ignore[unresolved-reference] + 1 | value = missing # ty: ignore[] tracked by [123] # ty: ignore[unresolved-reference] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove the unused suppression comment + | + - value = missing # ty: ignore[] tracked by [123] # ty: ignore[unresolved-reference] + 1 + value = missing # ty: ignore[unresolved-reference] + | " ); } @@ -1233,6 +1251,11 @@ class B(A): 6 | ] | help: Remove the unused suppression comment + | + 3 | values = [ + - # ty: ignore[] tracked by [123] + 4 | missing, + | " ); } @@ -1269,6 +1292,12 @@ class B(A): 3 | value = 1 / 0 | help: Remove the unused suppression comment + | + 1 | seen_code = True + - # ty: ignore[ignore-comment-unknown-rule] # ty: ignore[not-a-rule] # ty: ignore[division-by-zero] + 2 + # ty: ignore[ignore-comment-unknown-rule] # ty: ignore[not-a-rule] + 3 | value = 1 / 0 + | " ); } @@ -1321,8 +1350,12 @@ class B(A): 2 | 3 | result: int = f(missing) # ty: ignore[division-by-zero, invalid-assignment, too-many-positional-arguments, unresolved-reference] | ^^^^^^^^^^^^^^^^ - | help: Remove the unused suppression code + | + 2 | + - result: int = f(missing) # ty: ignore[division-by-zero, invalid-assignment, too-many-positional-arguments, unresolved-reference] + 3 + result: int = f(missing) # ty: ignore[invalid-assignment, too-many-positional-arguments, unresolved-reference] + | "# ); } @@ -1548,6 +1581,12 @@ class B(A): 4 | # ty: ignore[invalid-argument-type, unresolved-reference] | help: Remove the unused suppression code + | + 1 | seen_code = True + - # ty: ignore[too-many-positional-arguments, unresolved-reference] + 2 + # ty: ignore[unresolved-reference] + 3 | values = [ + | warning[unused-ignore-comment]: Unused `ty: ignore` directive: 'invalid-argument-type' --> test.py:4:18 @@ -1560,6 +1599,12 @@ class B(A): 6 | absent, | help: Remove the unused suppression code + | + 3 | values = [ + - # ty: ignore[invalid-argument-type, unresolved-reference] + 4 + # ty: ignore[unresolved-reference] + 5 | missing, + | " ); } @@ -1693,12 +1738,12 @@ class B(A): assert_eq!(diagnostic.id(), LINT_ID); assert_eq!( - diagnostic.primary_message(), + diagnostic.headline_message(), "Variable `a` should be named `b`." ); assert_eq!(convergence_diagnostic.id(), DiagnosticId::InternalError); - assert_snapshot!(convergence_diagnostic.primary_message(), @"Fixes failed to converge after 10 iterations."); + assert_snapshot!(convergence_diagnostic.headline_message(), @"Fixes failed to converge after 10 iterations."); // It should keep the source text from the last allowed fix iteration. assert_eq!(&*source_text(&db, file), "a = 10"); @@ -1770,12 +1815,12 @@ class B(A): assert_eq!(diagnostic.id(), LINT_ID); assert_eq!( - diagnostic.primary_message(), + diagnostic.headline_message(), "Variable `b` should be named `c`." ); assert_eq!(syntax_error.id(), DiagnosticId::InternalError); - assert_snapshot!(syntax_error.primary_message(), @"Applying fixes introduced a syntax error. Reverting changes."); + assert_snapshot!(syntax_error.headline_message(), @"Applying fixes introduced a syntax error. Reverting changes."); // It should revert the source to the last known error free version. assert_eq!(&*source_text(&db, file), "b = 10"); @@ -1958,7 +2003,7 @@ class B(A): let file = system_path_to_file(&db, "test.py").unwrap(); - let parsed_before = parsed_module(&db, file); + let parsed_before = parsed_module(&db, db.program_file(file).python_file(&db)); let had_syntax_errors = parsed_before.load(&db).has_syntax_errors(); let diagnostics = db.check_file(file); @@ -2009,7 +2054,7 @@ class B(A): let fixed = source_text(&db, file); - let parsed = parsed_module(&db, file); + let parsed = parsed_module(&db, db.program_file(file).python_file(&db)); let parsed = parsed.load(&db); let diagnostics_after_applying_fixes = db.check_file(file); diff --git a/crates/ty_python_semantic/src/lib.rs b/crates/ty_python_semantic/src/lib.rs index e43b72684b..5108cd9799 100644 --- a/crates/ty_python_semantic/src/lib.rs +++ b/crates/ty_python_semantic/src/lib.rs @@ -9,11 +9,11 @@ use crate::suppression::{ }; use crate::types::check_types_with; pub use db::Db; -pub use diagnostic::{ - add_inferred_python_version_hint_to_diagnostic, inferred_python_version_source_annotation, -}; +pub(crate) use diagnostic::add_inferred_python_version_hint_to_diagnostic; +pub use diagnostic::inferred_python_version_source_annotation; pub use fixes::{fix_all_diagnostics, suppress_all_diagnostics}; pub use place::{basedpython_typing_added_in, basedpython_warnings_added_in}; +use ruff_db::PythonFile; use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticId, Severity, Span}; use ruff_db::files::File; use ruff_db::parsed::parsed_module; @@ -21,18 +21,18 @@ use ruff_db::source::{SourceTextError, source_text}; use rustc_hash::FxHasher; pub use semantic_model::{ Completion, DjangoLookupArgument, ExpectedStringLiteralCompletion, ExtensionOperatorRewrite, - HasDefinition, HasOptionalDefinition, HasType, ImplicitReceiverReference, MemberDefinition, - NameKind, PreludeDunderReceiver, SemanticModel, + HasDefinition, HasType, ImplicitReceiverReference, MemberDefinition, NameKind, + PreludeDunderReceiver, SemanticModel, }; use std::hash::BuildHasherDefault; -pub use suppression::{ - SuppressFix, UNUSED_IGNORE_COMMENT, is_unused_ignore_comment_lint, suppress_all, - suppress_single, -}; +pub use suppression::UNUSED_IGNORE_COMMENT; +pub use suppression::suppress_single; +pub(crate) use suppression::{SuppressFix, is_unused_ignore_comment_lint, suppress_all}; use ty_module_resolver::ModuleGlobSet; +pub use ty_python_core::Program; +use ty_python_core::ProgramFile; use ty_python_core::definition::docstring_from_body; use ty_python_core::platform::PythonPlatform; -use ty_python_core::program::Program; use ty_python_core::scope::ScopeId; use ty_python_core::{ BindingWithConstraintsIterator, DeclarationsIterator, FileScopeId, attribute_scopes, @@ -49,17 +49,17 @@ pub use types::conformance::{ pub use types::conversions::{ConversionImport, ConversionInfo}; pub use types::extensions::{ExtensionAttributeInfo, ExtensionMemberKind}; pub use types::ide_support::{ - ImportAliasResolution, OverridableMember, ResolvedDefinition, TypeHierarchyClass, - definitions_for_attribute, definitions_for_bin_op, definitions_for_django_lookup_root, - definitions_for_imported_symbol, definitions_for_name, definitions_for_unary_op, - map_stub_definition, type_hierarchy_prepare, type_hierarchy_subtypes, + ImplementationsFinder, ImportAliasResolution, OverridableMember, ResolvedDefinition, + TypeHierarchyClass, contains_identifier, definitions_for_attribute, definitions_for_bin_op, + definitions_for_django_lookup_root, definitions_for_imported_symbol, definitions_for_name, + definitions_for_unary_op, map_stub_definition, type_hierarchy_prepare, type_hierarchy_subtypes, type_hierarchy_supertypes, }; pub use types::reified_infer::{ ArgVariance, ErasedTargetReason, ErasedUnion, ParametricIsPlan, ProtocolMemberCheck, }; pub use types::visibility::private_symbols; -pub use types::{DisplaySettings, TypeQualifiers}; +pub use types::{DisplaySettings, ProgramEnvironment, TypeQualifiers}; pub mod api_lockfile; mod db; @@ -70,6 +70,7 @@ mod dunder_all; mod fixes; pub mod lint; pub(crate) mod place; +pub(crate) mod place_load; mod reachability; pub mod reified; mod semantic_model; @@ -98,7 +99,7 @@ pub fn default_lint_registry() -> &'static LintRegistry { } /// Register all known semantic lints. -pub fn register_lints(registry: &mut LintRegistryBuilder) { +fn register_lints(registry: &mut LintRegistryBuilder) { types::register_lints(registry); django_template::register_lints(registry); registry.register_lint(&UNUSED_IGNORE_COMMENT); @@ -114,6 +115,9 @@ pub fn register_lints(registry: &mut LintRegistryBuilder) { reason = "each flag is an independent analysis toggle; a state machine would not model them" )] pub struct AnalysisSettings { + /// Whether narrowing with generic classes uses the top materialization. + pub strict_generic_narrowing: bool, + /// Whether ty should use conservative equality and inequality semantics. pub strict_equality_semantics: bool, @@ -281,6 +285,7 @@ const OPAQUE_REPR_CLASSES: &[&str] = &[ impl Default for AnalysisSettings { fn default() -> Self { Self { + strict_generic_narrowing: false, strict_equality_semantics: false, respect_type_ignore_comments: true, allowed_unresolved_imports: ModuleGlobSet::empty(), @@ -314,8 +319,7 @@ pub(crate) fn attribute_assignments<'db, 's>( 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 index = semantic_index(db, class_body_scope.program_file(db)); attribute_scopes(db, class_body_scope).filter_map(|function_scope_id| { let place_table = index.place_table(function_scope_id); @@ -335,8 +339,7 @@ pub(crate) fn attribute_declarations<'db, 's>( 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 index = semantic_index(db, class_body_scope.program_file(db)); attribute_scopes(db, class_body_scope).filter_map(|function_scope_id| { let place_table = index.place_table(function_scope_id); @@ -350,19 +353,19 @@ pub(crate) fn attribute_declarations<'db, 's>( } /// Get the module-level docstring for the given file. -pub(crate) fn module_docstring(db: &dyn Db, file: File) -> Option { +pub(crate) fn module_docstring(db: &dyn Db, file: PythonFile<'_>) -> Option { let module = parsed_module(db, file).load(db); docstring_from_body(module.suite()) .map(|docstring_expr| docstring_expr.value.to_str().to_owned()) } -pub fn check_file_unwrap(db: &dyn Db, file: File) -> Vec { +pub fn check_file_unwrap(db: &dyn Db, file: ProgramFile<'_>) -> Vec { check_file(db, file) .map(<[ruff_db::diagnostic::Diagnostic]>::into_vec) .unwrap_or_else(|error| vec![error]) } -pub fn check_file(db: &dyn Db, file: File) -> Result, Diagnostic> { +pub fn check_file(db: &dyn Db, file: ProgramFile<'_>) -> Result, Diagnostic> { check_file_with(db, file, Vec::new()) } @@ -376,10 +379,10 @@ pub fn check_file(db: &dyn Db, file: File) -> Result, Diagnost /// used either way. pub fn check_file_with( db: &dyn Db, - file: File, + file: ProgramFile<'_>, external: Vec, ) -> Result, Diagnostic> { - with_display_for_file(db, file, || check_file_inner(db, file, external)) + with_display_for_file(db, file.file(db), || check_file_inner(db, file, external)) } /// Run `body` with the type display `file` is written in: basedpython surface @@ -399,35 +402,41 @@ pub fn with_display_for_file(db: &dyn Db, file: File, body: impl FnOnce() -> fn check_file_inner( db: &dyn Db, - file: File, + file: ProgramFile<'_>, external: Vec, ) -> Result, Diagnostic> { + let source_file = file.file(db); let mut diagnostics: Vec = Vec::new(); // Abort checking if there are IO errors. - let source = source_text(db, file); + let source = source_text(db, source_file); if let Some(read_error) = source.read_error() { return Err(IOErrorDiagnostic { - file, + file: source_file, error: read_error.clone(), } .to_diagnostic()); } - let parsed = parsed_module(db, file); + let parsed = parsed_module(db, file.python_file(db)); let parsed_ref = parsed.load(db); diagnostics.extend( parsed_ref .errors() .iter() - .map(|error| Diagnostic::invalid_syntax(file, &error.error, error)), + .map(|error| Diagnostic::invalid_syntax(source_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"); + let mut error = Diagnostic::invalid_syntax(source_file, error, error); + add_inferred_python_version_hint_to_diagnostic( + db, + source_file, + &mut error, + "parsing syntax", + ); error })); @@ -445,7 +454,7 @@ pub struct IOErrorDiagnostic { } impl IOErrorDiagnostic { - pub fn to_diagnostic(&self) -> Diagnostic { + fn to_diagnostic(&self) -> Diagnostic { let mut diag = Diagnostic::new(DiagnosticId::Io, Severity::Error, &self.error); diag.annotate(Annotation::primary(Span::from(self.file))); diag @@ -457,4 +466,4 @@ impl IOErrorDiagnostic { /// values that will soon converge, but where unioning in the early value causes an /// unrecoverable loss of precision. This constant controls how many iterations /// are considered likely to produce "tainted" results that should be discarded. -pub(crate) const TAINTED_CYCLES: u32 = 3; +const TAINTED_CYCLES: u32 = 3; diff --git a/crates/ty_python_semantic/src/lint.rs b/crates/ty_python_semantic/src/lint.rs index f182f422af..cfc55ac81a 100644 --- a/crates/ty_python_semantic/src/lint.rs +++ b/crates/ty_python_semantic/src/lint.rs @@ -123,7 +123,7 @@ impl LintMetadata { self.documentation_lines().join("\n") } - pub fn documentation_url(&self) -> String { + pub(crate) fn documentation_url(&self) -> String { lint_documentation_url(self.name()) } @@ -144,7 +144,7 @@ impl LintMetadata { } } -pub fn lint_documentation_url(lint_name: LintName) -> String { +pub(crate) fn lint_documentation_url(lint_name: LintName) -> String { format!("https://ty.dev/rules#{lint_name}") } @@ -205,11 +205,11 @@ impl LintStatus { LintStatus::Deprecated { since, reason } } - pub const fn removed(since: &'static str, reason: &'static str) -> Self { + pub(crate) const fn removed(since: &'static str, reason: &'static str) -> Self { LintStatus::Removed { since, reason } } - pub const fn is_removed(&self) -> bool { + const fn is_removed(&self) -> bool { matches!(self, LintStatus::Removed { .. }) } @@ -359,7 +359,7 @@ pub struct LintRegistryBuilder { impl LintRegistryBuilder { #[track_caller] - pub fn register_lint(&mut self, lint: &'static LintMetadata) { + pub(crate) fn register_lint(&mut self, lint: &'static LintMetadata) { assert_eq!( self.by_name.insert(&*lint.name, lint.into()), None, @@ -396,7 +396,7 @@ impl LintRegistryBuilder { ); } - pub fn build(self) -> LintRegistry { + pub(crate) fn build(self) -> LintRegistry { LintRegistry { lints: self.lints, by_name: self.by_name, @@ -597,12 +597,12 @@ impl RuleSelection { self.lints.get(&lint).map(|(severity, _)| *severity) } - pub fn get(&self, lint: LintId) -> Option<(Severity, LintSource)> { + pub(crate) 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 { + pub(crate) fn is_enabled(&self, lint: LintId) -> bool { self.severity(lint).is_some() } @@ -662,4 +662,7 @@ pub enum LintSource { /// The rule was enabled from the configuration in the editor. Editor, + + /// The rule was enabled by uv workspace metadata. + UvWorkspace, } diff --git a/crates/ty_python_semantic/src/place.rs b/crates/ty_python_semantic/src/place.rs index 53161789ff..507fb9181c 100644 --- a/crates/ty_python_semantic/src/place.rs +++ b/crates/ty_python_semantic/src/place.rs @@ -1,5 +1,5 @@ +use crate::ProgramEnvironment; use itertools::Either; -use ruff_db::files::File; use ruff_index::IndexSlice; use ruff_python_ast::PythonVersion; use ty_module_resolver::{ @@ -13,9 +13,10 @@ use crate::reachability::{ use crate::types::narrow::NarrowingEvaluatorExtension; use crate::types::{ DynamicType, KnownClass, MemberLookupPolicy, Type, TypeAndQualifiers, TypeQualifiers, - UnionBuilder, UnionType, binding_type, inferred_declaration, is_discarded_dict_key_assignment, + UnionBuilder, UnionType, binding_type, exists_at_runtime, inferred_declaration, + is_discarded_dict_key_assignment, }; -use crate::{Db, FxIndexSet, FxOrderSet, Program}; +use crate::{Db, FxIndexSet, FxOrderSet}; use ty_python_core::definition::{Definition, DefinitionKind, DefinitionState}; use ty_python_core::narrowing_constraints::ScopedNarrowingConstraint; use ty_python_core::place::ScopedPlaceId; @@ -26,8 +27,8 @@ use ty_python_core::reachability_constraints::{ use ty_python_core::scope::ScopeId; use ty_python_core::{ BindingWithConstraints, BindingWithConstraintsIterator, BoundnessAnalysis, - DeclarationWithConstraint, DeclarationsIterator, Truthiness, global_scope, place_table, - use_def_map, + DeclarationWithConstraint, DeclarationsIterator, ProgramFile, Truthiness, global_scope, + place_table, use_def_map, }; pub(crate) use implicit_globals::{ @@ -88,16 +89,23 @@ pub(crate) enum PublicTypePolicy { impl PublicTypePolicy { /// Apply the public-type policy to the raw type. - pub(crate) fn apply_if_needed<'db>(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { + fn apply_if_needed<'db>( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + ) -> Type<'db> { match self { Self::Raw => ty, - Self::Promote => ty.promote(db).promote_singletons(db), + Self::Promote => ty.promote(db, env).promote_singletons(db, env), } } } /// The source definition provenance for a place. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, get_size2::GetSize, salsa::SalsaValue)] +#[derive( + Debug, Clone, Copy, Default, Hash, PartialEq, Eq, get_size2::GetSize, salsa::SalsaValue, +)] pub(crate) enum Provenance<'db> { /// No source definition is known. #[default] @@ -136,7 +144,7 @@ impl<'db> Provenance<'db> { } /// A defined place with its raw type, origin, definedness, public-type policy, and provenance. -#[derive(Debug, Clone, Copy, PartialEq, Eq, get_size2::GetSize, salsa::SalsaValue)] +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, get_size2::GetSize, salsa::SalsaValue)] pub(crate) struct DefinedPlace<'db> { pub(crate) ty: Type<'db>, pub(crate) origin: TypeOrigin, @@ -146,7 +154,7 @@ pub(crate) struct DefinedPlace<'db> { } impl<'db> DefinedPlace<'db> { - pub(crate) fn new(ty: Type<'db>) -> Self { + fn new(ty: Type<'db>) -> Self { Self { ty, origin: TypeOrigin::Inferred, @@ -156,7 +164,7 @@ impl<'db> DefinedPlace<'db> { } } - pub(crate) fn with_origin(mut self, origin: TypeOrigin) -> Self { + fn with_origin(mut self, origin: TypeOrigin) -> Self { self.origin = origin; self } @@ -166,17 +174,17 @@ impl<'db> DefinedPlace<'db> { self } - pub(crate) fn with_public_type_policy(mut self, public_type_policy: PublicTypePolicy) -> Self { + fn with_public_type_policy(mut self, public_type_policy: PublicTypePolicy) -> Self { self.public_type_policy = public_type_policy; self } - pub(crate) fn with_definition(mut self, definition: Definition<'db>) -> Self { + fn with_definition(mut self, definition: Definition<'db>) -> Self { self.provenance = Provenance::SingleDefinition(definition); self } - pub(crate) fn with_provenance(mut self, provenance: Provenance<'db>) -> Self { + fn with_provenance(mut self, provenance: Provenance<'db>) -> Self { self.provenance = provenance; self } @@ -215,7 +223,9 @@ impl<'db> DefinedPlace<'db> { /// bound_or_declared: Place::Defined(DefinedPlace { ty: Literal[1], origin: TypeOrigin::Inferred, definedness: Definedness::PossiblyUndefined, .. }), /// non_existent: Place::Undefined, /// ``` -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, get_size2::GetSize, salsa::SalsaValue)] +#[derive( + Debug, Clone, Copy, Default, Hash, PartialEq, Eq, get_size2::GetSize, salsa::SalsaValue, +)] pub(crate) enum Place<'db> { Defined(DefinedPlace<'db>), #[default] @@ -309,10 +319,7 @@ impl<'db> Place<'db> { /// Set the public-type policy for this place. #[must_use] - pub(crate) fn with_public_type_policy( - self, - new_public_type_policy: PublicTypePolicy, - ) -> Place<'db> { + fn with_public_type_policy(self, new_public_type_policy: PublicTypePolicy) -> Place<'db> { match self { Place::Defined(defined) => { Place::Defined(defined.with_public_type_policy(new_public_type_policy)) @@ -332,15 +339,21 @@ impl<'db> Place<'db> { /// 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> { + pub(crate) fn try_call_dunder_get( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + owner: Type<'db>, + ) -> Place<'db> { match self { Place::Defined( place @ DefinedPlace { ty: Type::Union(union), .. }, - ) => union.map_with_boundness(db, |elem| { - Place::Defined(DefinedPlace { ty: *elem, ..place }).try_call_dunder_get(db, owner) + ) => union.map_with_boundness(db, env, |elem| { + Place::Defined(DefinedPlace { ty: *elem, ..place }) + .try_call_dunder_get(db, env, owner) }), Place::Defined( @@ -348,16 +361,19 @@ impl<'db> Place<'db> { ty: Type::Intersection(intersection), .. }, - ) => intersection.map_with_boundness(db, |elem| { - Place::Defined(DefinedPlace { ty: *elem, ..place }).try_call_dunder_get(db, owner) + ) => intersection.map_with_boundness(db, env, |elem| { + Place::Defined(DefinedPlace { ty: *elem, ..place }) + .try_call_dunder_get(db, env, owner) }), Place::Defined(defined) => { - if let Some((dunder_get_return_ty, _)) = - defined.ty.try_call_dunder_get(db, None, owner) - { + let result = defined + .ty + .try_call_dunder_get(db, env, None, owner) + .unwrap_or_else(|error| Some(error.fallback())); + if let Some(result) = result { Place::Defined(DefinedPlace { - ty: dunder_get_return_ty, + ty: result.return_type, provenance: Provenance::Unknown, ..defined }) @@ -414,14 +430,15 @@ impl<'db> LookupError<'db> { pub(crate) fn or_fall_back_to( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, fallback: PlaceAndQualifiers<'db>, ) -> LookupResult<'db> { - let fallback = fallback.into_lookup_result(db); + let fallback = fallback.into_lookup_result(db, env); match (&self, &fallback) { (LookupError::Undefined(_), _) => fallback, (LookupError::PossiblyUndefined { .. }, Err(LookupError::Undefined(_))) => Err(self), (LookupError::PossiblyUndefined(ty), Ok(ty2)) => Ok(TypeAndQualifiers::new( - UnionType::from_two_elements(db, ty.inner_type(), ty2.inner_type()), + UnionType::from_two_elements(db, env, ty.inner_type(), ty2.inner_type()), ty.origin().merge(ty2.origin()), ty.qualifiers().union(ty2.qualifiers()), ) @@ -429,7 +446,7 @@ impl<'db> LookupError<'db> { (LookupError::PossiblyUndefined(ty), Err(LookupError::PossiblyUndefined(ty2))) => { Err(LookupError::PossiblyUndefined( TypeAndQualifiers::new( - UnionType::from_two_elements(db, ty.inner_type(), ty2.inner_type()), + UnionType::from_two_elements(db, env, ty.inner_type(), ty2.inner_type()), ty.origin().merge(ty2.origin()), ty.qualifiers().union(ty2.qualifiers()), ) @@ -475,7 +492,7 @@ pub(crate) fn symbol<'db>( /// 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, + file: ProgramFile<'db>, name: &str, ) -> PlaceAndQualifiers<'db> { symbol_impl( @@ -497,11 +514,13 @@ pub(crate) fn explicit_global_symbol<'db>( #[allow(unused)] pub(crate) fn global_symbol<'db>( db: &'db dyn Db, - file: File, + file: ProgramFile<'db>, name: &str, ) -> PlaceAndQualifiers<'db> { - explicit_global_symbol(db, file, name) - .or_fall_back_to(db, || module_type_implicit_global_symbol(db, file, name)) + let env = ProgramEnvironment::from_file(file); + explicit_global_symbol(db, file, name).or_fall_back_to(db, &env, || { + module_type_implicit_global_symbol(db, file, name) + }) } /// Infers the public type of an imported symbol. @@ -512,10 +531,15 @@ pub(crate) fn global_symbol<'db>( /// `None` should be passed for the `file` parameter if looking up a symbol on a namespace package. pub(crate) fn imported_symbol<'db>( db: &'db dyn Db, - file: Option, + env: &ProgramEnvironment<'db>, + file: Option>, name: &str, requires_explicit_reexport: Option, ) -> PlaceAndQualifiers<'db> { + if let Some(file) = file { + debug_assert_eq!(file.program(db), env.program(db)); + } + // If it's not found in the global scope, check if it's present as an instance on // `types.ModuleType` or `builtins.object`. // @@ -533,7 +557,7 @@ pub(crate) fn imported_symbol<'db>( // module we're dealing with. file.map(|file| { let requires_explicit_reexport = requires_explicit_reexport.unwrap_or_else(|| { - if file.is_stub(db) { + if file.file(db).is_stub(db) { RequiresExplicitReExport::Yes } else { RequiresExplicitReExport::No @@ -549,7 +573,7 @@ pub(crate) fn imported_symbol<'db>( ) }) .unwrap_or_default() - .or_fall_back_to(db, || { + .or_fall_back_to(db, env, || { match name { "__file__" => { // We special-case `__file__` here because we know that for a successfully imported @@ -564,16 +588,16 @@ pub(crate) fn imported_symbol<'db>( // do not attempt to detect this; we just infer `str` still. This matches the // behaviour of other major type checkers. if file.is_some() { - Place::bound(KnownClass::Str.to_instance(db)).into() + Place::bound(KnownClass::Str.to_instance(db, env)).into() } else { - Place::bound(Type::none(db)).into() + Place::bound(Type::none(db, env)).into() } } "__getattr__" => Place::Undefined.into(), "__builtins__" => Place::bound(Type::any()).into(), _ => KnownClass::ModuleType - .to_instance(db) - .member_lookup_with_policy(db, name, MemberLookupPolicy::NO_GETATTR_LOOKUP), + .to_instance(db, env) + .member_lookup_with_policy(db, env, name, MemberLookupPolicy::NO_GETATTR_LOOKUP), } }) } @@ -585,31 +609,107 @@ pub(crate) fn imported_symbol<'db>( /// 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> { - let resolver = |module: Module<'_>| { - let file = module.file(db)?; +pub(crate) fn builtins_symbol<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + symbol: &str, +) -> PlaceAndQualifiers<'db> { + builtins_symbol_impl(db, env, symbol, BuiltinVisibility::All) + .map(|(_, symbol)| symbol) + .unwrap_or_default() +} + +/// Looks up `symbol` for implicit builtin fallback. +/// +/// Private type-checking-only definitions are implementation details, but private runtime +/// definitions from either the standard or project-level builtins remain available. +/// +/// ```python +/// # builtins.pyi +/// _T = TypeVar("_T") # Not available as an implicit builtin. +/// +/// # __builtins__.pyi +/// _custom: int # Available as an implicit builtin. +/// ``` +pub(crate) fn implicit_builtins_symbol<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + symbol: &str, +) -> PlaceAndQualifiers<'db> { + builtins_symbol_impl(db, env, symbol, BuiltinVisibility::RuntimeOnly) + .map(|(_, symbol)| symbol) + .unwrap_or_default() +} + +/// Returns the module scope that supplies `symbol` through implicit builtin fallback. +/// +/// Uses the same visibility rules as [`implicit_builtins_symbol`] so IDE definition lookup cannot +/// resolve a private typing-only helper that type inference considers undefined. +pub(crate) fn implicit_builtins_symbol_scope<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + symbol: &str, +) -> Option> { + builtins_symbol_impl(db, env, symbol, BuiltinVisibility::RuntimeOnly).map(|(scope, _)| scope) +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum BuiltinVisibility { + All, + RuntimeOnly, +} + +/// Resolves project-level builtins before standard builtins and optionally hides typing-only names. +/// +/// Returns the supplying module's scope together with the symbol so inference and IDE lookups can +/// share the same resolution and visibility policy. +fn builtins_symbol_impl<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + symbol: &str, + visibility: BuiltinVisibility, +) -> Option<(ScopeId<'db>, PlaceAndQualifiers<'db>)> { + let program = env.program(db); + let resolver_environment = program.resolver_environment(db); + let resolver = |module: Module<'db>| { + let file = ProgramFile::new(db, module.file(db)?, program); + let scope = global_scope(db, file); let found_symbol = symbol_impl( db, - global_scope(db, file), + scope, symbol, RequiresExplicitReExport::Yes, ConsideredDefinitions::EndOfScope, ) - .or_fall_back_to(db, || { + .or_fall_back_to(db, env, || { // 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, file, symbol) }); - // If this symbol is not present in project-level builtins, search in the default ones. - found_symbol - .ignore_possibly_undefined() - .map(|_| found_symbol) + found_symbol.ignore_possibly_undefined()?; + + if matches!(visibility, BuiltinVisibility::RuntimeOnly) + && let Place::Defined(defined) = found_symbol.place + && let Some(definition) = defined.provenance.definition() + && !exists_at_runtime(db, definition) + { + return None; + } + + Some((scope, found_symbol)) }; - resolve_module_confident(db, &ModuleName::new_static("__builtins__").unwrap()) - .and_then(&resolver) - .or_else(|| resolve_module_confident(db, &KnownModule::Builtins.name()).and_then(resolver)) - .unwrap_or_default() + // If this symbol is not present in project-level builtins, search in the default ones. + resolve_module_confident( + db, + resolver_environment, + &ModuleName::new_static("__builtins__").unwrap(), + ) + .and_then(&resolver) + .or_else(|| { + resolve_module_confident(db, resolver_environment, &KnownModule::Builtins.name()) + .and_then(resolver) + }) } /// Lookup the type of `symbol` in a given known module. @@ -617,13 +717,14 @@ pub(crate) fn builtins_symbol<'db>(db: &'db dyn Db, symbol: &str) -> PlaceAndQua /// Returns `Place::Undefined` if the given known module cannot be resolved for some reason. pub(crate) fn known_module_symbol<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, known_module: KnownModule, symbol: &str, ) -> PlaceAndQualifiers<'db> { - resolve_module_confident(db, &known_module.name()) + resolve_module_confident(db, env.resolver_environment(db), &known_module.name()) .and_then(|module| { - let file = module.file(db)?; - Some(imported_symbol(db, Some(file), symbol, None)) + let file = ProgramFile::new(db, module.file(db)?, env.program(db)); + Some(imported_symbol(db, env, Some(file), symbol, None)) }) .unwrap_or_default() } @@ -632,8 +733,12 @@ pub(crate) fn known_module_symbol<'db>( /// /// Returns `Place::Undefined` if the `typing` module isn't available for some reason. #[inline] -pub(crate) fn typing_symbol<'db>(db: &'db dyn Db, symbol: &str) -> PlaceAndQualifiers<'db> { - known_module_symbol(db, KnownModule::Typing, symbol) +pub(crate) fn typing_symbol<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + symbol: &str, +) -> PlaceAndQualifiers<'db> { + known_module_symbol(db, env, KnownModule::Typing, symbol) } pub(crate) fn is_basedpython_implicit_typing_name(name: &str) -> bool { @@ -690,24 +795,36 @@ pub fn basedpython_warnings_added_in(name: &str) -> Option { #[inline] pub(crate) fn typing_extensions_symbol<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, symbol: &str, ) -> PlaceAndQualifiers<'db> { - known_module_symbol(db, KnownModule::TypingExtensions, symbol) + known_module_symbol(db, env, 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) +pub(crate) fn builtins_module_scope<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, +) -> Option> { + core_module_scope(db, env, 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_confident(db, &core_module.name())?; - Some(global_scope(db, module.file(db)?)) +fn core_module_scope<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + core_module: KnownModule, +) -> Option> { + let program = env.program(db); + let module = resolve_module_confident(db, env.resolver_environment(db), &core_module.name())?; + Some(global_scope( + db, + ProgramFile::new(db, module.file(db)?, program), + )) } /// Infer the combined type from an iterator of bindings, and return it @@ -716,10 +833,12 @@ fn core_module_scope(db: &dyn Db, core_module: KnownModule) -> Option( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, bindings_with_constraints: BindingWithConstraintsIterator<'_, 'db>, ) -> PlaceWithDefinition<'db> { place_from_bindings_impl( db, + env, bindings_with_constraints, RequiresExplicitReExport::No, None, @@ -728,11 +847,13 @@ pub(super) fn place_from_bindings<'db>( pub(super) fn place_from_bindings_with_reachability_cache<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, bindings_with_constraints: BindingWithConstraintsIterator<'_, 'db>, reachability_cache: &ReachabilityEvaluationCache<'db>, ) -> PlaceWithDefinition<'db> { place_from_bindings_impl( db, + env, bindings_with_constraints, RequiresExplicitReExport::No, Some(reachability_cache), @@ -749,18 +870,21 @@ pub(super) fn place_from_bindings_with_reachability_cache<'db>( /// [`TypeQualifiers`] that have been specified on the declaration(s). pub(crate) fn place_from_declarations<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, declarations: DeclarationsIterator<'_, 'db>, ) -> PlaceFromDeclarationsResult<'db> { - place_from_declarations_impl(db, declarations, RequiresExplicitReExport::No, None) + place_from_declarations_impl(db, env, declarations, RequiresExplicitReExport::No, None) } pub(crate) fn place_from_declarations_with_reachability_cache<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, declarations: DeclarationsIterator<'_, 'db>, reachability_cache: &ReachabilityEvaluationCache<'db>, ) -> PlaceFromDeclarationsResult<'db> { place_from_declarations_impl( db, + env, declarations, RequiresExplicitReExport::No, Some(reachability_cache), @@ -823,7 +947,9 @@ impl<'db> PlaceFromDeclarationsResult<'db> { /// that this comes with a [`CLASS_VAR`] type qualifier. /// /// [`CLASS_VAR`]: crate::types::TypeQualifiers::CLASS_VAR -#[derive(Debug, Clone, Default, Copy, PartialEq, Eq, get_size2::GetSize, salsa::SalsaValue)] +#[derive( + Debug, Clone, Default, Copy, Hash, PartialEq, Eq, get_size2::GetSize, salsa::SalsaValue, +)] pub(crate) struct PlaceAndQualifiers<'db> { pub(crate) place: Place<'db>, pub(crate) qualifiers: TypeQualifiers, @@ -899,13 +1025,17 @@ impl<'db> PlaceAndQualifiers<'db> { /// /// For places whose public type differs from their raw stored type, this applies the /// public-type policy lazily during lookup. - pub(crate) fn into_lookup_result(self, db: &'db dyn Db) -> LookupResult<'db> { + pub(crate) fn into_lookup_result( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> LookupResult<'db> { match self { PlaceAndQualifiers { place: Place::Defined(place), qualifiers, } => { - let ty = place.public_type_policy.apply_if_needed(db, place.ty); + let ty = place.public_type_policy.apply_if_needed(db, env, place.ty); let type_and_qualifiers = TypeAndQualifiers::new(ty, place.origin, qualifiers) .with_provenance(place.provenance); match place.definedness { @@ -931,9 +1061,11 @@ impl<'db> PlaceAndQualifiers<'db> { pub(crate) fn unwrap_with_diagnostic( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, diagnostic_fn: impl FnOnce(LookupError<'db>) -> TypeAndQualifiers<'db>, ) -> TypeAndQualifiers<'db> { - self.into_lookup_result(db).unwrap_or_else(diagnostic_fn) + self.into_lookup_result(db, env) + .unwrap_or_else(diagnostic_fn) } /// Fallback (partially or fully) to another place if `self` is partially or fully unbound. @@ -950,16 +1082,18 @@ impl<'db> PlaceAndQualifiers<'db> { pub(crate) fn or_fall_back_to( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, fallback_fn: impl FnOnce() -> PlaceAndQualifiers<'db>, ) -> Self { - self.into_lookup_result(db) - .or_else(|lookup_error| lookup_error.or_fall_back_to(db, fallback_fn())) + self.into_lookup_result(db, env) + .or_else(|lookup_error| lookup_error.or_fall_back_to(db, env, fallback_fn())) .into() } pub(crate) fn cycle_normalized( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, previous_place: Self, cycle: &salsa::Cycle, ) -> Self { @@ -974,7 +1108,7 @@ impl<'db> PlaceAndQualifiers<'db> { // iteration into the current result; after the first couple iterations, the same // applies to boundness and qualifiers. (Place::Defined(prev), Place::Defined(current)) => Place::Defined(DefinedPlace { - ty: current.ty.cycle_normalized(db, prev.ty, cycle), + ty: current.ty.cycle_normalized(db, env, prev.ty, cycle), definedness: if cycle.iteration() <= 1 || matches!( (prev.definedness, current.definedness), @@ -995,7 +1129,7 @@ impl<'db> PlaceAndQualifiers<'db> { // However, the handling described above may reduce the exactness of reachability analysis, // so it may be better to remove it. In that case, this branch is necessary. (Place::Undefined, Place::Defined(current)) => Place::Defined(DefinedPlace { - ty: current.ty.recursive_type_normalized(db, cycle), + ty: current.ty.recursive_type_normalized(db, env, cycle), definedness: if cycle.iteration() <= 1 { current.definedness } else { @@ -1010,7 +1144,7 @@ impl<'db> PlaceAndQualifiers<'db> { Place::Undefined } else { Place::Defined(DefinedPlace { - ty: prev.ty.recursive_type_normalized(db, cycle), + ty: prev.ty.recursive_type_normalized(db, env, cycle), definedness: Definedness::PossiblyUndefined, ..prev }) @@ -1031,8 +1165,9 @@ impl<'db> From> for PlaceAndQualifiers<'db> { #[salsa::tracked( returns(copy), cycle_initial=|_, id, _, _, _, _| Place::bound(Type::divergent(id)).into(), - cycle_fn=|db, cycle, previous: &PlaceAndQualifiers<'db>, place: PlaceAndQualifiers<'db>, _, _, _, _| { - place.cycle_normalized(db, *previous, cycle) + cycle_fn=|db, cycle, previous: &PlaceAndQualifiers<'db>, place: PlaceAndQualifiers<'db>, scope: ScopeId<'db>, _, _, _| { + let env = ProgramEnvironment::from_scope(scope); + place.cycle_normalized(db, &env, *previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] @@ -1044,6 +1179,7 @@ pub(crate) fn place_by_id<'db>( considered_definitions: ConsideredDefinitions, ) -> PlaceAndQualifiers<'db> { let use_def = use_def_map(db, scope); + let env = ProgramEnvironment::from_scope(scope); // If the place is declared, the public type is based on declarations; otherwise, it's based // on inference from bindings. @@ -1053,8 +1189,9 @@ pub(crate) fn place_by_id<'db>( ConsideredDefinitions::AllReachable => use_def.reachable_declarations(place_id), }; - let declared = place_from_declarations_impl(db, declarations, requires_explicit_reexport, None) - .ignore_conflicting_declarations(); + let declared = + place_from_declarations_impl(db, &env, declarations, requires_explicit_reexport, None) + .ignore_conflicting_declarations(); let all_considered_bindings = || match considered_definitions { ConsideredDefinitions::EndOfScope => use_def.end_of_scope_bindings(place_id), @@ -1065,7 +1202,7 @@ pub(crate) fn place_by_id<'db>( // inferred type, without unioning with `Unknown`, because it cannot be modified. if let Some(qualifiers) = declared.is_bare_final() { let bindings = all_considered_bindings(); - return place_from_bindings_impl(db, bindings, requires_explicit_reexport, None) + return place_from_bindings_impl(db, &env, bindings, requires_explicit_reexport, None) .place .with_qualifiers(qualifiers); } @@ -1090,7 +1227,9 @@ pub(crate) fn place_by_id<'db>( qualifiers, } if qualifiers.contains(TypeQualifiers::CLASS_VAR) => { let bindings = all_considered_bindings(); - match place_from_bindings_impl(db, bindings, requires_explicit_reexport, None).place { + match place_from_bindings_impl(db, &env, bindings, requires_explicit_reexport, None) + .place + { Place::Defined(DefinedPlace { ty: inferred, origin, @@ -1101,7 +1240,7 @@ pub(crate) fn place_by_id<'db>( ty: if sound_types { inferred } else { - UnionType::from_two_elements(db, Type::unknown(), inferred) + UnionType::from_two_elements(db, &env, Type::unknown(), inferred) }, origin, definedness: boundness, @@ -1146,7 +1285,8 @@ pub(crate) fn place_by_id<'db>( } => { let bindings = all_considered_bindings(); let boundness_analysis = bindings.boundness_analysis(); - let inferred = place_from_bindings_impl(db, bindings, requires_explicit_reexport, None); + let inferred = + place_from_bindings_impl(db, &env, bindings, requires_explicit_reexport, None); let place = match inferred.place { // Place is possibly undeclared and definitely unbound @@ -1170,7 +1310,7 @@ pub(crate) fn place_by_id<'db>( provenance: inferred_provenance, .. }) => Place::Defined(DefinedPlace { - ty: UnionType::from_two_elements(db, inferred_ty, declared_ty), + ty: UnionType::from_two_elements(db, &env, inferred_ty, declared_ty), origin, definedness: if boundness_analysis == BoundnessAnalysis::AssumeBound { Definedness::AlwaysDefined @@ -1192,7 +1332,8 @@ pub(crate) fn place_by_id<'db>( let bindings = all_considered_bindings(); let boundness_analysis = bindings.boundness_analysis(); let mut inferred = - place_from_bindings_impl(db, bindings, requires_explicit_reexport, None).place; + place_from_bindings_impl(db, &env, bindings, requires_explicit_reexport, None) + .place; if boundness_analysis == BoundnessAnalysis::AssumeBound { if let Place::Defined(defined) = inferred { @@ -1343,7 +1484,8 @@ fn symbol_impl<'db>( let _span = tracing::trace_span!("symbol", ?name).entered(); let is_known_module = |known_module| { - file_to_module(db, scope.file(db)).is_some_and(|module| module.is_known(db, known_module)) + file_to_module(db, scope.program_file(db).resolver_file(db)) + .is_some_and(|module| module.is_known(db, known_module)) }; // Check the symbol name first to avoid a module-resolution query for every symbol lookup. @@ -1352,7 +1494,7 @@ fn symbol_impl<'db>( "version_info" => { return Place::bound(Type::sys_version_info()).into(); } - "platform" => match Program::get(db).python_platform(db) { + "platform" => match scope.program(db).python_platform(db) { crate::PythonPlatform::Identifier(platform) => { return Place::bound(Type::string_literal(db, platform.as_str())).into(); } @@ -1365,7 +1507,7 @@ fn symbol_impl<'db>( } if name == "name" && is_known_module(KnownModule::Os) { - match Program::get(db).python_platform(db) { + match scope.program(db).python_platform(db) { crate::PythonPlatform::Identifier(platform) => { // In CPython, `os.name` is `"nt"` on Windows and `"posix"` otherwise. let os_name = if platform == "win32" { "nt" } else { "posix" }; @@ -1394,7 +1536,9 @@ fn symbol_impl<'db>( /// Pre-computed reachability analysis for loop-back bindings in a loop header. #[salsa::tracked( returns(clone), - cycle_initial=|db, _, definition| loop_header_reachability_impl(db, definition, true), + cycle_initial=|db, _, definition: Definition<'db>| { + loop_header_reachability_impl(db, definition, true) + }, cycle_fn=loop_header_reachability_cycle_recover, heap_size = ruff_memory_usage::heap_size, )] @@ -1438,7 +1582,6 @@ fn loop_header_reachability_impl<'db>( let live_bindings: Vec<_> = loop_header.bindings_for_place(place).collect(); let use_exact_reachability = use_def.reachability_constraints().used_interiors().len() <= MAX_EXACT_LOOP_HEADER_REACHABILITY_NODES; - for live_binding in live_bindings { let reachability = if is_cycle_initial { Truthiness::Ambiguous @@ -1529,6 +1672,7 @@ pub(crate) struct ReachableLoopBinding<'db> { /// access any AST nodes from the file containing the declarations. fn place_from_bindings_impl<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, bindings_with_constraints: BindingWithConstraintsIterator<'_, 'db>, requires_explicit_reexport: RequiresExplicitReExport, reachability_cache: Option<&ReachabilityEvaluationCache<'db>>, @@ -1697,7 +1841,7 @@ fn place_from_bindings_impl<'db>( provenance = provenance.or(Provenance::SingleDefinition(binding)); let binding_ty = binding_type(db, binding); Some(( - narrowing_constraint.narrow(db, binding_ty, binding.place(db)), + narrowing_constraint.narrow(db, env, binding_ty, binding.place(db)), static_reachability, )) }, @@ -1705,7 +1849,7 @@ fn place_from_bindings_impl<'db>( let place = if let Some((first, first_reachability)) = types.next() { let ty = if let Some((second, second_reachability)) = types.next() { - let mut builder = PublicTypeBuilder::new(db); + let mut builder = PublicTypeBuilder::new(db, env); builder.add(first, first_reachability); builder.add(second, second_reachability); @@ -1778,11 +1922,11 @@ struct PublicTypeBuilder<'db> { } impl<'db> PublicTypeBuilder<'db> { - fn new(db: &'db dyn Db) -> Self { + fn new(db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Self { PublicTypeBuilder { db, queue: None, - builder: UnionBuilder::new(db), + builder: UnionBuilder::new(db, env), } } @@ -1797,10 +1941,11 @@ impl<'db> PublicTypeBuilder<'db> { } fn add(&mut self, element: Type<'db>, reachability: Truthiness) -> bool { + let db = self.db; match element { Type::FunctionLiteral(function) => { - let last_definition = function.literal(self.db).last_definition; - if last_definition.is_overload(self.db) { + let last_definition = function.literal(db).last_definition; + if last_definition.is_overload(db) { // Distinct overloaded function values can be assigned to the same public // symbol in separate branches. Preserve the queued value unless the next // overload belongs to the same place. @@ -1808,7 +1953,7 @@ impl<'db> PublicTypeBuilder<'db> { let Type::FunctionLiteral(queued_function) = queued else { return false; }; - function.has_same_place_as(self.db, queued_function) + function.has_same_place_as(db, queued_function) }) { self.drain_queue(); } @@ -1824,8 +1969,8 @@ impl<'db> PublicTypeBuilder<'db> { let Type::FunctionLiteral(queued_function) = queued else { return false; }; - let queued_definition = queued_function.last_definition(self.db); - function.contains_definition(self.db, queued_definition) + let queued_definition = queued_function.last_definition(db); + function.contains_definition(db, queued_definition) }) { self.queue = None; @@ -1860,21 +2005,29 @@ struct DeclaredTypeBuilder<'db> { } impl<'db> DeclaredTypeBuilder<'db> { - fn new(db: &'db dyn Db) -> Self { + fn new(db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Self { DeclaredTypeBuilder { - inner: PublicTypeBuilder::new(db), + inner: PublicTypeBuilder::new(db, env), qualifiers: TypeQualifiers::empty(), first_type: None, conflicting_types: FxOrderSet::default(), } } - fn add(&mut self, element: TypeAndQualifiers<'db>, reachability: Truthiness) { + fn add( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + element: TypeAndQualifiers<'db>, + reachability: Truthiness, + ) { + debug_assert!(std::ptr::eq(db, self.inner.db)); + let element_ty = element.inner_type(); if self.inner.add(element_ty, reachability) { if let Some(first_ty) = self.first_type { - if !first_ty.is_equivalent_to(self.inner.db, element_ty) { + if !first_ty.is_equivalent_to(db, env, element_ty) { self.conflicting_types.insert(element_ty); } } else { @@ -1911,6 +2064,7 @@ impl<'db> DeclaredTypeBuilder<'db> { /// access any AST nodes from the file containing the declarations. fn place_from_declarations_impl<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, declarations_iterator: DeclarationsIterator<'_, 'db>, requires_explicit_reexport: RequiresExplicitReExport, reachability_cache: Option<&ReachabilityEvaluationCache<'db>>, @@ -1982,11 +2136,11 @@ fn place_from_declarations_impl<'db>( if let Some((first, first_reachability)) = types.next() { let (declared, conflicting) = if let Some((second, second_reachability)) = types.next() { - let mut builder = DeclaredTypeBuilder::new(db); - builder.add(first, first_reachability); - builder.add(second, second_reachability); + let mut builder = DeclaredTypeBuilder::new(db, env); + builder.add(db, env, first, first_reachability); + builder.add(db, env, second, second_reachability); for (element, reachability) in types { - builder.add(element, reachability); + builder.add(db, env, element, reachability); } builder.build() } else { @@ -2038,7 +2192,7 @@ fn is_reexported(db: &dyn Db, definition: Definition<'_>) -> bool { // 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 { + let Some(all_names) = dunder_all_names(db, definition.program_file(db)) else { return false; }; let table = place_table(db, definition.scope(db)); @@ -2048,47 +2202,115 @@ fn is_reexported(db: &dyn Db, definition: Definition<'_>) -> bool { } pub(crate) mod implicit_globals { - use ruff_db::files::File; + use ruff_db::parsed::parsed_module; use ruff_python_ast as ast; use ruff_python_ast::name::Name; + use ty_module_resolver::KnownModule; - use crate::Program; use crate::db::Db; use crate::module_docstring; use crate::place::{Definedness, PlaceAndQualifiers}; - use crate::types::{ - ClassLiteral, KnownClass, MemberLookupPolicy, Parameter, Parameters, Signature, Type, - }; + use crate::reachability::evaluate_reachability; + use crate::types::{KnownClass, MemberLookupPolicy, Parameter, Parameters, Signature, Type}; + use crate::{Program, ProgramEnvironment}; use ruff_python_ast::PythonVersion; + use ty_python_core::definition::{DefinitionKind, DefinitionState}; + use ty_python_core::scope::{NodeWithScopeRef, ScopeId}; use ty_python_core::symbol::Symbol; - use ty_python_core::{place_table, use_def_map}; + use ty_python_core::{ProgramFile, place_table, semantic_index, use_def_map}; - use super::{DefinedPlace, Place, place_from_declarations}; + use super::{DefinedPlace, Place, core_module_scope, is_reexported, place_from_declarations}; + + /// Returns the body scope when all reachable, exported definitions of `name` + /// in a vendored module are the same direct class definition. + /// + /// This can be used as a fast-path to avoid query cycles. + fn try_vendored_class_scope<'db>( + db: &'db dyn Db, + module_scope: ScopeId<'db>, + name: &str, + ) -> Option> { + let program_file = module_scope.program_file(db); + let file = program_file.file(db); + if !file.path(db).is_vendored_path() { + return None; + } + let symbol_id = place_table(db, module_scope).symbol_id(name)?; + let use_def = use_def_map(db, module_scope); + let module = parsed_module(db, program_file.python_file(db)).load(db); + let index = semantic_index(db, program_file); + let mut body_scope = None; + + for binding in use_def.end_of_scope_symbol_bindings(symbol_id) { + let DefinitionState::Defined(definition) = binding.binding else { + continue; + }; + if file.is_stub(db) && !is_reexported(db, definition) { + continue; + } + if evaluate_reachability(db, use_def, binding.reachability_constraint).is_always_false() + { + continue; + } + + let DefinitionKind::Class(class) = definition.kind(db) else { + return None; + }; + let class_scope = index + .node_scope(NodeWithScopeRef::Class(class.node(&module))) + .to_scope_id(db, program_file); + if body_scope.is_some_and(|body_scope| body_scope != class_scope) { + return None; + } + body_scope = Some(class_scope); + } + + body_scope + } + + /// Return the body scope of the canonical `types.ModuleType` class. + fn module_type_body_scope<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { + module_type_body_scope_inner(db, env.program(db)) + } + + #[salsa::tracked(returns(copy), heap_size=ruff_memory_usage::heap_size)] + fn module_type_body_scope_inner<'db>( + db: &'db dyn Db, + program: Program<'db>, + ) -> Option> { + let env = ProgramEnvironment::from_program(program); + let module_scope = core_module_scope(db, &env, KnownModule::Types)?; + try_vendored_class_scope(db, module_scope, "ModuleType").or_else(|| { + KnownClass::ModuleType + .try_to_class_literal(db, &env) + .map(|class| class.body_scope(db)) + }) + } pub(crate) fn module_type_implicit_global_declaration<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, ) -> PlaceAndQualifiers<'db> { - if !module_type_symbols(db) + if !module_type_symbols(db, env) .iter() .any(|module_type_member| module_type_member == name) { return Place::Undefined.into(); } - let Type::ClassLiteral(module_type_class) = KnownClass::ModuleType.to_class_literal(db) - else { + let Some(module_type_scope) = module_type_body_scope(db, env) else { return Place::Undefined.into(); }; - let Some(class) = module_type_class.as_static() else { - return Place::Undefined.into(); - }; - let module_type_scope = class.body_scope(db); let place_table = place_table(db, module_type_scope); let Some(symbol_id) = place_table.symbol_id(name) else { return Place::Undefined.into(); }; place_from_declarations( db, + env, use_def_map(db, module_type_scope).end_of_scope_symbol_declarations(symbol_id), ) .ignore_conflicting_declarations() @@ -2110,53 +2332,54 @@ pub(crate) mod implicit_globals { /// global scope if they're being imported **from a different file**. pub(crate) fn module_type_implicit_global_symbol<'db>( db: &'db dyn Db, - file: File, + file: ProgramFile<'db>, name: &str, ) -> PlaceAndQualifiers<'db> { + let env = ProgramEnvironment::from_file(file); match name { // 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`. - "__file__" => Place::bound(KnownClass::Str.to_instance(db)).into(), + "__file__" => Place::bound(KnownClass::Str.to_instance(db, &env)).into(), // We special-case `__doc__` because a module with a literal docstring has `__doc__` // set to that string at runtime. We only narrow when a docstring is present: `__doc__` // may be set dynamically, so we fall back to the typeshed's `str | None`. - "__doc__" if module_docstring(db, file).is_some() => { + "__doc__" if module_docstring(db, file.python_file(db)).is_some() => { // Docstrings are stripped in `-OO` optimized mode, but here we assume that the // existence of an actual docstring AND the usage of `__doc__` is reason enough to // believe that it will exist at runtime. - Place::bound(KnownClass::Str.to_instance(db)).into() + Place::bound(KnownClass::Str.to_instance(db, &env)).into() } "__builtins__" => Place::bound(Type::any()).into(), - "__debug__" => Place::bound(KnownClass::Bool.to_instance(db)).into(), + "__debug__" => Place::bound(KnownClass::Bool.to_instance(db, &env)).into(), // Created lazily by the warnings machinery; may be absent. // Model as possibly-unbound to avoid false negatives. - "__warningregistry__" => { - Place::Defined( - DefinedPlace::new(KnownClass::Dict.to_specialized_instance( - db, - &[Type::any(), KnownClass::Int.to_instance(db)], - )) - .with_definedness(Definedness::PossiblyUndefined), - ) - .into() - } + "__warningregistry__" => Place::Defined( + DefinedPlace::new(KnownClass::Dict.to_specialized_instance( + db, + &env, + &[Type::any(), KnownClass::Int.to_instance(db, &env)], + )) + .with_definedness(Definedness::PossiblyUndefined), + ) + .into(), // Marked as possibly-unbound as it is only present in the module namespace // if at least one global symbol is annotated in the module. - "__annotate__" if Program::get(db).python_version(db) >= PythonVersion::PY314 => { + "__annotate__" if env.python_version(db) >= PythonVersion::PY314 => { let signature = Signature::new( Parameters::standard([Parameter::positional_only(Some(Name::new_static( "format", ))) - .with_annotated_type(KnownClass::Int.to_instance(db))]), + .with_annotated_type(KnownClass::Int.to_instance(db, &env))]), KnownClass::Dict.to_specialized_instance( db, - &[KnownClass::Str.to_instance(db), Type::any()], + &env, + &[KnownClass::Str.to_instance(db, &env), Type::any()], ), ); Place::Defined( @@ -2171,16 +2394,22 @@ pub(crate) mod implicit_globals { // 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) => - { + _ => { + if !module_type_symbols(db, &env) + .iter() + .any(|module_type_member| &**module_type_member == name) + { + return Place::Undefined.into(); + } KnownClass::ModuleType - .to_instance(db) - .member_lookup_with_policy(db, name, MemberLookupPolicy::NO_GETATTR_LOOKUP) + .to_instance(db, &env) + .member_lookup_with_policy( + db, + &env, + name, + MemberLookupPolicy::NO_GETATTR_LOOKUP, + ) } - - _ => Place::Undefined.into(), } } @@ -2201,25 +2430,11 @@ pub(crate) mod implicit_globals { /// 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), - cycle_initial=|_, _| smallvec::SmallVec::default(), - heap_size=ruff_memory_usage::heap_size - )] - fn module_type_symbols(db: &dyn Db) -> smallvec::SmallVec<[ast::name::Name; 8]> { - let Some(module_type) = KnownClass::ModuleType - .to_class_literal(db) - .as_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 ClassLiteral::Static(module_type) = module_type else { - return smallvec::SmallVec::default(); - }; - let module_type_symbol_table = place_table(db, module_type.body_scope(db)); + fn module_type_symbols_from_scope( + db: &dyn Db, + module_type_scope: ScopeId<'_>, + ) -> smallvec::SmallVec<[ast::name::Name; 8]> { + let module_type_symbol_table = place_table(db, module_type_scope); module_type_symbol_table .symbols() @@ -2235,22 +2450,48 @@ pub(crate) mod implicit_globals { .collect() } + fn module_type_symbols<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> &'db [ast::name::Name] { + module_type_symbols_inner(db, env.program(db)) + } + + #[salsa::tracked( + returns(deref), + cycle_initial=|_, _, _| smallvec::SmallVec::default(), + heap_size=ruff_memory_usage::heap_size + )] + fn module_type_symbols_inner<'db>( + db: &'db dyn Db, + program: Program<'db>, + ) -> smallvec::SmallVec<[ast::name::Name; 8]> { + let env = ProgramEnvironment::from_program(program); + let Some(module_type_scope) = module_type_body_scope(db, &env) else { + // The most likely way we get here is if a user specified a `--custom-typeshed-dir` + // without a resolvable `ModuleType` class in the `stdlib/types.pyi` stub. + return smallvec::SmallVec::default(); + }; + module_type_symbols_from_scope(db, module_type_scope) + } + /// Returns an iterator over all implicit module global symbols and their types. /// /// This is used for completions in the global scope of a module. It returns /// the correct types for special-cased symbols like `__file__` (which is `str` /// for the current module, not `str | None`). - pub(crate) fn all_implicit_module_globals( - db: &dyn Db, - file: File, - ) -> impl Iterator)> + '_ { + pub(crate) fn all_implicit_module_globals<'db>( + db: &'db dyn Db, + file: ProgramFile<'db>, + ) -> impl Iterator)> + 'db { // Special-cased implicit globals that are not in `module_type_symbols` let special_cased = ["__builtins__", "__debug__", "__warningregistry__"] .into_iter() .map(Name::new_static); // All symbols from ModuleType (already includes `__file__`, `__name__`, etc.) - let module_type_syms = module_type_symbols(db).iter().cloned(); + let env = ProgramEnvironment::from_file(file); + let module_type_syms = module_type_symbols(db, &env).iter().cloned(); // Combine and map to (name, type) pairs special_cased @@ -2270,7 +2511,9 @@ pub(crate) mod implicit_globals { #[test] fn module_type_symbols_includes_declared_types_but_not_referenced_types() { let db = setup_db(); - let symbol_names = module_type_symbols(&db); + let db = &db; + let env = db.program_environment(); + let symbol_names = module_type_symbols(db, &env); let dunder_name_symbol_name = ast::name::Name::new_static("__name__"); assert!(symbol_names.contains(&dunder_name_symbol_name)); @@ -2291,21 +2534,23 @@ pub(crate) mod implicit_globals { /// See pub(crate) fn class_body_implicit_symbol<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, ) -> PlaceAndQualifiers<'db> { match name { - "__qualname__" => Place::bound(KnownClass::Str.to_instance(db)).into(), - "__module__" => Place::bound(KnownClass::Str.to_instance(db)).into(), + "__qualname__" => Place::bound(KnownClass::Str.to_instance(db, env)).into(), + "__module__" => Place::bound(KnownClass::Str.to_instance(db, env)).into(), // __doc__ is `str` if there's a docstring, `None` if there isn't "__doc__" => Place::bound(UnionType::from_two_elements( db, - KnownClass::Str.to_instance(db), - Type::none(db), + env, + KnownClass::Str.to_instance(db, env), + Type::none(db, env), )) .into(), // __firstlineno__ was added in Python 3.13 - "__firstlineno__" if Program::get(db).python_version(db) >= PythonVersion::PY313 => { - Place::bound(KnownClass::Int.to_instance(db)).into() + "__firstlineno__" if env.python_version(db) >= PythonVersion::PY313 => { + Place::bound(KnownClass::Int.to_instance(db, env)).into() } _ => Place::Undefined.into(), } @@ -2348,7 +2593,7 @@ pub(crate) enum ConsideredDefinitions { #[cfg(test)] mod tests { use super::*; - use crate::db::tests::setup_db; + use crate::db::tests::{TestDb, setup_db}; #[test] fn test_symbol_or_fall_back_to() { @@ -2356,6 +2601,8 @@ mod tests { use TypeOrigin::Inferred; let db = setup_db(); + let db = &db; + let env = db.program_environment(); let ty1 = Type::int_literal(1); let ty2 = Type::int_literal(2); @@ -2404,22 +2651,22 @@ mod tests { }; // Start from an unbound symbol - assert_eq!(unbound().or_fall_back_to(&db, unbound), unbound()); + assert_eq!(unbound().or_fall_back_to(db, &env, unbound), unbound()); assert_eq!( - unbound().or_fall_back_to(&db, possibly_unbound_ty1), + unbound().or_fall_back_to(db, &env, possibly_unbound_ty1), possibly_unbound_ty1() ); - assert_eq!(unbound().or_fall_back_to(&db, bound_ty1), bound_ty1()); + assert_eq!(unbound().or_fall_back_to(db, &env, bound_ty1), bound_ty1()); // Start from a possibly unbound symbol assert_eq!( - possibly_unbound_ty1().or_fall_back_to(&db, unbound), + possibly_unbound_ty1().or_fall_back_to(db, &env, unbound), possibly_unbound_ty1() ); assert_eq!( - possibly_unbound_ty1().or_fall_back_to(&db, possibly_unbound_ty2), + possibly_unbound_ty1().or_fall_back_to(db, &env, possibly_unbound_ty2), Place::Defined(DefinedPlace { - ty: UnionType::from_elements(&db, [ty1, ty2]), + ty: UnionType::from_elements(db, &env, [ty1, ty2]), origin: Inferred, definedness: PossiblyUndefined, public_type_policy: PublicTypePolicy::Raw, @@ -2428,9 +2675,9 @@ mod tests { .into() ); assert_eq!( - possibly_unbound_ty1().or_fall_back_to(&db, bound_ty2), + possibly_unbound_ty1().or_fall_back_to(db, &env, bound_ty2), Place::Defined(DefinedPlace { - ty: UnionType::from_elements(&db, [ty1, ty2]), + ty: UnionType::from_elements(db, &env, [ty1, ty2]), origin: Inferred, definedness: AlwaysDefined, public_type_policy: PublicTypePolicy::Raw, @@ -2440,16 +2687,19 @@ mod tests { ); // 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, &env, unbound), bound_ty1()); assert_eq!( - bound_ty1().or_fall_back_to(&db, possibly_unbound_ty2), + bound_ty1().or_fall_back_to(db, &env, possibly_unbound_ty2), + bound_ty1() + ); + assert_eq!( + bound_ty1().or_fall_back_to(db, &env, bound_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>) { + fn assert_bound_string_symbol<'db>(db: &'db TestDb, symbol: Place<'db>) { assert!(matches!( symbol, Place::Defined(DefinedPlace { @@ -2458,25 +2708,37 @@ mod tests { .. }) )); - assert_eq!(symbol.expect_type(), KnownClass::Str.to_instance(db)); + assert_eq!( + symbol.expect_type(), + KnownClass::Str.to_instance(db, &db.program_environment()) + ); } #[test] fn implicit_builtin_globals() { let db = setup_db(); - assert_bound_string_symbol(&db, builtins_symbol(&db, "__name__").place); + assert_bound_string_symbol( + &db, + builtins_symbol(&db, &db.program_environment(), "__name__").place, + ); } #[test] fn implicit_typing_globals() { let db = setup_db(); - assert_bound_string_symbol(&db, typing_symbol(&db, "__name__").place); + assert_bound_string_symbol( + &db, + typing_symbol(&db, &db.program_environment(), "__name__").place, + ); } #[test] fn implicit_typing_extensions_globals() { let db = setup_db(); - assert_bound_string_symbol(&db, typing_extensions_symbol(&db, "__name__").place); + assert_bound_string_symbol( + &db, + typing_extensions_symbol(&db, &db.program_environment(), "__name__").place, + ); } #[test] @@ -2484,7 +2746,7 @@ mod tests { let db = setup_db(); assert_bound_string_symbol( &db, - known_module_symbol(&db, KnownModule::Sys, "__name__").place, + known_module_symbol(&db, &db.program_environment(), KnownModule::Sys, "__name__").place, ); } } diff --git a/crates/ty_python_semantic/src/place_load.rs b/crates/ty_python_semantic/src/place_load.rs new file mode 100644 index 0000000000..37c8d0591f --- /dev/null +++ b/crates/ty_python_semantic/src/place_load.rs @@ -0,0 +1,1118 @@ +//! This module combines the semantics of name resolution with the results of +//! reaching definition analysis to expose a [`PlaceLoadResolution`], which +//! provides a lazy iterator over the steps that resolve the value read from a +//! place. +//! +//! More specifically, a [`PlaceLoadResolution`] iterates over a series of +//! [`PlaceLoadResolutionStep`] values, each of which represents a phase of the +//! process that ultimately either supplies a definite value for a load or ends +//! in explicit failure: +//! +//! - A source ([`PlaceLoadResolutionStep::Source`]) (and its associated type- +//! narrowing constraints) which may supply the value for a load. +//! - A boolean condition ([`PlaceLoadResolutionStep::MemberResolutionCondition`]) +//! that determines whether resolution continues for member loads specifically +//! (i.e., `foo.bar.baz` as opposed to the plain symbol `foo`). This describes +//! the loads of member prefixes (e.g., `foo.bar` and `foo`) that must all be +//! unbound before resolution continues. +//! - A marker ([`PlaceLoadResolutionStep::Exhausted`]) which declares that the +//! resolution process ended in failure. +//! +//! We consume this model to different ends: +//! +//! - Type inference uses it to determine the type and definedness of a place +//! - The language server uses it to determine, e.g., what references to an +//! imported module should be rewritten in response to the module itself being +//! renamed +//! +//! ## Example +//! +//! ```py +//! from collections.abc import Callable +//! +//! def make_counter(start: int, enabled: bool) -> Callable[[], int | None]: +//! if enabled: +//! value = start +//! else: +//! value = None +//! +//! def next_value() -> int | None: +//! nonlocal value +//! if value is not None: +//! current = value # load U +//! value += 1 +//! return current +//! return None +//! +//! return next_value +//! ``` +//! +//! [`PlaceLoadResolution`] at `U` combines reaching definition analysis with a +//! lexical scope walk: +//! +//! 1. Reaching definition analysis from the use-def module supplies the binding +//! state for `value` at `U` in `next_value`. In this case, no value-binding +//! definition reaches U because the `value += 1` assignment occurs after `U`, +//! so the state records `value` as unbound. +//! 2. The use-def model also supplies the narrowing constraint `value is not None` +//! that is associated with the source. That constraint can affect an +//! inferred type but does not affect name resolution. +//! 3. Name resolution encounters the `nonlocal` declaration and continues +//! the lexical scope walk into `make_counter`, where `value` is owned. +//! 4. Once name resolution reaches the scope that owns value, it records +//! `make_counter.value` as an enclosing source of a potential value for `U`. +//! +//! Schematically, fully consuming the resulting [`PlaceLoadResolution`] yields: +//! +//! ```text +//! Source(Bindings(next_value.value at U)) // unbound +//! Source(DefinitionsFromOwningScope(make_counter.value)) +//! Exhausted(UnboundFree) +//! ``` +//! +//! While yielding those steps, the resolution accumulates `value is not None` +//! as a narrowing constraint and records that it crossed the `nonlocal` +//! declaration in `next_value`. +//! +//! The enclosing function's binding scope terminates name resolution, even if +//! none of its definitions supply a value at runtime. [`PlaceLoadResolution`] +//! therefore yields `Exhausted(UnboundFree)` if both sources are exhausted +//! instead of yielding module globals or builtins as later sources. In this +//! example, type inference establishes that the branches in `make_counter` +//! always define `value`, so the `Exhausted` step is unreachable. + +use ruff_python_ast::{self as ast, name::Name}; +use smallvec::SmallVec; +use ty_python_core::ast_ids::{HasScopedUseId, ScopedUseId}; +use ty_python_core::definition::Definition; +use ty_python_core::narrowing_constraints::ConstraintKey; +use ty_python_core::place::{PlaceExpr, PlaceExprRef, ScopedPlaceId}; +use ty_python_core::scope::{NodeWithScopeKind, ScopeId, ScopeKind}; +use ty_python_core::symbol::{ScopedSymbolId, Symbol}; +use ty_python_core::{ + AncestorsIter, BindingWithConstraintsIterator, EnclosingSnapshotResult, FileScopeId, + ProgramFile, SemanticIndex, +}; + +use crate::Db; + +/// Returns an iterator over the steps that resolve a value for a place load. +pub(crate) fn resolve_place_load<'db, 'ast>( + db: &'db dyn Db, + index: &'db SemanticIndex<'db>, + scope: ScopeId<'db>, + place_expr: PlaceExpr, + mode: PlaceLoadMode<'ast>, +) -> PlaceLoadResolution<'db, 'ast> { + PlaceLoadResolution::new( + PlaceLoadResolutionContext { + db, + index, + scope, + file: scope.program_file(db), + mode, + }, + place_expr, + ) +} + +/// Selects the binding state used for a place load's own scope. +#[derive(Clone, Copy)] +pub(crate) enum PlaceLoadMode<'ast> { + /// Resolve bindings live at an expression occurrence. + /// + /// For example, a caller resolving `value` in `print(value)` uses this mode so that only + /// bindings that reach that occurrence are considered. + AtExpression(ast::ExprRef<'ast>), + /// Resolve all bindings reachable in the scope. + /// + /// A caller uses this mode for an annotation in any of these contexts: + /// + /// - A stub file. + /// - A module containing `from __future__ import annotations`. + /// - Python 3.14 or later. + /// + /// Callers also use this mode for other deferred type expressions, including type-parameter + /// bounds and defaults and, in stub files, class bases and type alias values. + /// + /// For example, `Model` in `item: Model` can resolve to a class defined later in the scope. + Deferred, + /// Resolve reachable bindings in a parsed string annotation. + /// + /// A caller uses this mode for a name such as `Model` after parsing `item: "Model"`. The + /// parsed expression is not part of the original semantic index, so it may not have its own + /// place-table entry. + StringAnnotation, +} + +/// Exposes an iterator over the steps that resolve the value for a place load. +pub(crate) struct PlaceLoadResolution<'db, 'ast> { + /// The place expression whose loaded value is being resolved. + place_expr: PlaceExpr, + /// Read-only context shared by every source-selection phase. + context: PlaceLoadResolutionContext<'db, 'ast>, + /// The next node to visit in the resolution graph, or `None` after reaching a leaf. + next_node: Option>, + /// Narrowing constraints accumulated while resolution advances. + constraints: PlaceLoadConstraints, + /// Whether resolution has crossed a `global` or `nonlocal` declaration so far. + crosses_scope_declaration: bool, +} + +impl<'db> Iterator for PlaceLoadResolution<'db, '_> { + type Item = PlaceLoadResolutionStep<'db>; + + /// Lazily yields [`PlaceLoadResolutionStep`] values to describe the resolution process. + /// + /// Internally, this traverses a directed, acyclic graph that models the resolution process. + fn next(&mut self) -> Option { + while let Some(current_node) = self.next_node.take() { + match current_node { + PlaceLoadResolutionNode::LocalSource => { + self.next_node = + Some(PlaceLoadResolutionNode::AskConsumerWhetherToContinueForMember); + + if let Some((kind, exit_constraint)) = + self.context.local_source(self.place_expr()) + { + let source = self.constraints.source( + kind, + PlaceLoadSourceRole::Ordinary, + exit_constraint, + ); + return Some(PlaceLoadResolutionStep::Source(source)); + } + } + PlaceLoadResolutionNode::AskConsumerWhetherToContinueForMember => { + self.next_node = Some(PlaceLoadResolutionNode::DecideResolutionPath); + + if let Some(prefix_loads) = + self.context.place_expr_prefix_loads(self.place_expr()) + { + return Some(PlaceLoadResolutionStep::MemberResolutionCondition( + prefix_loads, + )); + } + } + PlaceLoadResolutionNode::DecideResolutionPath => { + self.next_node = Some(self.decide_resolution_path()); + } + PlaceLoadResolutionNode::DunderClassSource { + definition, + enclosing_scopes, + } => { + self.next_node = Some(PlaceLoadResolutionNode::EnclosingScopeSource( + enclosing_scopes, + )); + + return Some(PlaceLoadResolutionStep::Source( + PlaceLoadConstraints::unnarrowed_source( + PlaceLoadSourceKind::Implicit(ImplicitPlaceLoad::DunderClass( + definition, + )), + PlaceLoadSourceRole::Ordinary, + ), + )); + } + PlaceLoadResolutionNode::EnclosingScopeSource(mut scopes) => { + let (next_node, source) = self.resolve_enclosing_scopes(&mut scopes); + self.next_node = Some(next_node); + + if let Some(source) = source { + return Some(PlaceLoadResolutionStep::Source(source)); + } + } + PlaceLoadResolutionNode::ImplicitClassBodySource(forwarded_global_snapshot) => { + self.next_node = Some(forwarded_global_snapshot.map_or( + PlaceLoadResolutionNode::ExplicitGlobalSource( + PlaceLoadSourceRole::Ordinary, + ), + PlaceLoadResolutionNode::ForwardedGlobalSnapshotSource, + )); + + if self.context.is_class_body_scope() + && let Some(name) = self.loaded_symbol_name() + { + let source = self.constraints.source( + PlaceLoadSourceKind::Implicit(ImplicitPlaceLoad::ClassBodySymbol( + name.clone(), + )), + PlaceLoadSourceRole::Ordinary, + None, + ); + return Some(PlaceLoadResolutionStep::Source(source)); + } + } + PlaceLoadResolutionNode::ForwardedGlobalSnapshotSource(snapshot) => { + let ForwardedGlobalSnapshot { + bindings, + enclosing_scope, + } = snapshot; + self.next_node = Some(PlaceLoadResolutionNode::ImplicitGlobalSource); + + let source = self.constraints.source( + PlaceLoadSourceKind::Bindings(bindings), + PlaceLoadSourceRole::Ordinary, + Some(( + enclosing_scope, + ConstraintKey::NestedScope( + self.context.scope.file_scope_id(self.context.db), + ), + )), + ); + return Some(PlaceLoadResolutionStep::Source(source)); + } + PlaceLoadResolutionNode::ExplicitGlobalSource(role) => { + self.next_node = Some(PlaceLoadResolutionNode::ImplicitGlobalSource); + + if let Some(source) = self.resolve_global(role) { + return Some(PlaceLoadResolutionStep::Source(source)); + } + } + PlaceLoadResolutionNode::ImplicitGlobalSource => { + if let Some(name) = self.loaded_symbol_name().cloned() { + self.next_node = Some(PlaceLoadResolutionNode::BuiltinSource(name.clone())); + + let source = self.constraints.source( + PlaceLoadSourceKind::Implicit( + ImplicitPlaceLoad::ModuleImplicitGlobal { + file: self.context.file, + name, + }, + ), + PlaceLoadSourceRole::Ordinary, + None, + ); + return Some(PlaceLoadResolutionStep::Source(source)); + } + + self.next_node = + Some(PlaceLoadResolutionNode::Failure(PlaceLoadFailure::NotFound)); + } + PlaceLoadResolutionNode::BuiltinSource(name) => { + self.next_node = + Some(PlaceLoadResolutionNode::Failure(PlaceLoadFailure::NotFound)); + + return Some(PlaceLoadResolutionStep::Source( + PlaceLoadConstraints::unnarrowed_source( + PlaceLoadSourceKind::Implicit(ImplicitPlaceLoad::Builtin(name)), + PlaceLoadSourceRole::Ordinary, + ), + )); + } + PlaceLoadResolutionNode::Failure(failure) => { + return Some(PlaceLoadResolutionStep::Exhausted(failure)); + } + } + } + + None + } +} + +impl<'db, 'ast> PlaceLoadResolution<'db, 'ast> { + fn new(context: PlaceLoadResolutionContext<'db, 'ast>, place_expr: PlaceExpr) -> Self { + let crosses_scope_declaration = + context.symbol_has_scope_declaration(PlaceExprRef::from(&place_expr)); + Self { + context, + place_expr, + next_node: Some(PlaceLoadResolutionNode::LocalSource), + constraints: PlaceLoadConstraints::default(), + crosses_scope_declaration, + } + } + + fn decide_resolution_path(&mut self) -> PlaceLoadResolutionNode<'db> { + let db = self.context.db; + let scope = self.context.scope; + let file_scope = scope.file_scope_id(db); + let place_table = self.context.index.place_table(file_scope); + + let mut symbol_is_local = false; + let place_expr = PlaceExprRef::from(&self.place_expr); + if let Some(symbol) = place_expr.as_symbol() + && let Some(symbol_id) = place_table.symbol_id(symbol.name()) + { + let indexed_symbol = place_table.symbol(symbol_id); + symbol_is_local = indexed_symbol.is_local(); + + let class_body_global_fallback = self.context.is_class_body_scope() && symbol_is_local; + if self.context.skips_non_global_scopes(symbol_id) || class_body_global_fallback { + return PlaceLoadResolutionNode::ExplicitGlobalSource( + if class_body_global_fallback { + PlaceLoadSourceRole::ClassBodyGlobalFallback + } else { + PlaceLoadSourceRole::Ordinary + }, + ); + } + } + + if symbol_is_local { + return if scope.node(db).scope_kind().is_module() { + PlaceLoadResolutionNode::ImplicitGlobalSource + } else { + PlaceLoadResolutionNode::Failure(PlaceLoadFailure::UnboundLocal) + }; + } + + let mut scopes = self.context.index.ancestor_scopes(file_scope); + // The first scope is the input scope itself; skip it to arrive at the first true ancestor. + scopes.next(); + + if let PlaceExprRef::Symbol(symbol) = place_expr + && symbol.name() == "__class__" + && let Some(definition) = self.context.dunder_class_cell_definition() + { + PlaceLoadResolutionNode::DunderClassSource { + definition, + enclosing_scopes: scopes, + } + } else { + PlaceLoadResolutionNode::EnclosingScopeSource(scopes) + } + } + + fn resolve_enclosing_scopes( + &mut self, + scopes: &mut AncestorsIter<'db>, + ) -> (PlaceLoadResolutionNode<'db>, Option>) { + let db = self.context.db; + let scope = self.context.scope; + let file_scope = scope.file_scope_id(db); + + for (enclosing_file_scope, _) in scopes { + if enclosing_file_scope.is_global() { + break; + } + + let enclosing_scope = self.context.index.scope(enclosing_file_scope); + let is_lexical_enclosing_scope = self + .context + .is_lexical_enclosing_scope(enclosing_file_scope); + + let enclosing_place_table = self.context.index.place_table(enclosing_file_scope); + let place_expr = PlaceExprRef::from(&self.place_expr); + let enclosing_place_id = enclosing_place_table.place_id(place_expr); + let enclosing_place = enclosing_place_id.map(|id| enclosing_place_table.place(id)); + // A `global` declaration forwards the place to the module instead of making this + // enclosing scope its owner. A possibly-unbound snapshot must still fall through. + let forwards_to_global = is_lexical_enclosing_scope + && enclosing_place + .is_some_and(|place| place.as_symbol().is_some_and(Symbol::is_global)); + let root_place_was_reassigned = || { + enclosing_place_table + .parents(place_expr) + .any(|root| enclosing_place_table.place(root).is_bound()) + }; + + let mut eagerly_undefined = false; + if self.context.uses_enclosing_snapshots() { + match self.context.index.enclosing_snapshot( + enclosing_file_scope, + place_expr, + file_scope, + ) { + EnclosingSnapshotResult::FoundConstraint(constraint) => { + self.constraints.push( + enclosing_file_scope, + ConstraintKey::NarrowingConstraint(constraint), + ); + if scope.scope(db).is_eager() { + eagerly_undefined = true; + } + } + EnclosingSnapshotResult::FoundBindings(bindings) => { + if forwards_to_global { + self.crosses_scope_declaration = true; + return ( + PlaceLoadResolutionNode::ImplicitClassBodySource(Some( + ForwardedGlobalSnapshot { + bindings, + enclosing_scope: enclosing_file_scope, + }, + )), + None, + ); + } + + return ( + Self::node_after_enclosing_scope(enclosing_scope.kind()), + Some(self.constraints.source( + PlaceLoadSourceKind::Bindings(bindings), + PlaceLoadSourceRole::Ordinary, + Some(( + enclosing_file_scope, + ConstraintKey::NestedScope(file_scope), + )), + )), + ); + } + EnclosingSnapshotResult::NotFound => { + if root_place_was_reassigned() { + return ( + Self::node_after_enclosing_scope(enclosing_scope.kind()), + None, + ); + } + continue; + } + EnclosingSnapshotResult::NoLongerInEagerContext => { + if root_place_was_reassigned() { + return ( + Self::node_after_enclosing_scope(enclosing_scope.kind()), + None, + ); + } + } + } + } + + if !is_lexical_enclosing_scope { + continue; + } + + let (Some(enclosing_place_id), Some(enclosing_place)) = + (enclosing_place_id, enclosing_place) + else { + continue; + }; + + if forwards_to_global { + self.crosses_scope_declaration = true; + return (PlaceLoadResolutionNode::ImplicitClassBodySource(None), None); + } + // Keep walking across `nonlocal` declarations until reaching the owning scope. + if enclosing_place.as_symbol().is_some_and(Symbol::is_nonlocal) { + self.crosses_scope_declaration = true; + continue; + } + if !(enclosing_place.is_bound() || enclosing_place.is_declared()) { + continue; + } + + // The first bound or declared place owns the load. Its public value includes nested + // writes represented by synthetic definitions in this scope. + return ( + Self::node_after_enclosing_scope(enclosing_scope.kind()), + (!eagerly_undefined).then(|| { + self.constraints.source( + PlaceLoadSourceKind::DefinitionsFromOwningScope { + scope: enclosing_file_scope.to_scope_id(db, self.context.file), + id: enclosing_place_id, + }, + PlaceLoadSourceRole::Ordinary, + None, + ) + }), + ); + } + + (PlaceLoadResolutionNode::ImplicitClassBodySource(None), None) + } + + /// Resolves a load that has reached the module's explicit global scope. + /// + /// An eager nested scope uses the global snapshot captured when it began, so a class body + /// cannot see a module binding created only after that body finishes. + fn resolve_global(&mut self, role: PlaceLoadSourceRole) -> Option> { + let current_scope = self.context.scope.file_scope_id(self.context.db); + if current_scope.is_global() { + return None; + } + + if self.context.uses_enclosing_snapshots() { + match self.context.index.enclosing_snapshot( + FileScopeId::global(), + PlaceExprRef::from(&self.place_expr), + current_scope, + ) { + EnclosingSnapshotResult::FoundConstraint(constraint) => { + self.constraints.push( + FileScopeId::global(), + ConstraintKey::NarrowingConstraint(constraint), + ); + return None; + } + EnclosingSnapshotResult::FoundBindings(bindings) => { + return Some(self.constraints.source( + PlaceLoadSourceKind::Bindings(bindings), + role, + Some(( + FileScopeId::global(), + ConstraintKey::NestedScope(current_scope), + )), + )); + } + EnclosingSnapshotResult::NotFound => return None, + EnclosingSnapshotResult::NoLongerInEagerContext => {} + } + } + + let name = self.loaded_symbol_name()?.clone(); + Some(self.constraints.source( + PlaceLoadSourceKind::Implicit(ImplicitPlaceLoad::ExplicitGlobalSymbol { + file: self.context.file, + name, + }), + role, + None, + )) + } + + fn node_after_enclosing_scope(kind: ScopeKind) -> PlaceLoadResolutionNode<'db> { + if kind.is_class() { + PlaceLoadResolutionNode::ImplicitGlobalSource + } else { + PlaceLoadResolutionNode::Failure(PlaceLoadFailure::UnboundFree) + } + } + + pub(crate) fn narrowing_constraints_for( + &self, + source: &PlaceLoadSource<'_>, + ) -> &[(FileScopeId, ConstraintKey)] { + self.constraints.narrowing_constraints_for(source) + } + + pub(crate) fn into_constraints(self) -> Vec<(FileScopeId, ConstraintKey)> { + self.constraints.into_constraints() + } + + pub(crate) fn place_expr(&self) -> PlaceExprRef<'_> { + PlaceExprRef::from(&self.place_expr) + } + + /// Returns the loaded symbol's name, or `None` when the loaded place is a member. + /// + /// For example, this returns a name for `value`, but not for `value.attr` or `value[0]`. + fn loaded_symbol_name(&self) -> Option<&Name> { + self.place_expr().as_symbol().map(Symbol::name) + } +} + +pub(crate) enum PlaceLoadResolutionStep<'db> { + // A source that can supply the value for a load. + Source(PlaceLoadSource<'db>), + // A condition that the caller must evaluate to determine whether resolution should continue + // for a member load. + MemberResolutionCondition(PlaceExprPrefixLoads<'db>), + // A marker that declares that resolution ended in a explicit failure. + Exhausted(PlaceLoadFailure), +} + +/// One source that can supply the value of a place load, along with the +/// type narrowing constraints that apply to it. +/// +/// ## How constraint tracking is implemented +/// +/// [`PlaceLoadResolution`] stores one shared list of constraint keys. Each +/// source maintains an `entry_checkpoint` into that list, which identifies the +/// constraints used to narrow the source. +/// +/// When a key identifies the binding state used to construct a source, that key +/// becomes active after the source is requested, but applying it to the same +/// source again would duplicate work. +/// +/// ### Example +/// +/// ```py +/// from collections.abc import Callable +/// +/// def make_counter(start: int, enabled: bool) -> Callable[[], int | None]: +/// if enabled: +/// value = start +/// else: +/// value = None +/// +/// def next_value() -> int | None: +/// nonlocal value +/// if value is not None: +/// current = value # load U +/// value += 1 +/// return current +/// return None +/// +/// return next_value +/// ``` +/// +/// For `U` above, the constraint representation after both sources have been +/// requested is schematically: +/// +/// ```text +/// PlaceLoadResolution { +/// constraint_keys: [ +/// (next_value, UseId(U)), +/// ], +/// } +/// PlaceLoadSource { +/// kind: Bindings(next_value at U), +/// entry_checkpoint: 0, +/// } +/// PlaceLoadSource { +/// kind: DefinitionsFromOwningScope(make_counter.value), +/// entry_checkpoint: 1, +/// } +/// ``` +/// +/// The first source already comes from `bindings_at_use(U)`, so its `UseId` key +/// becomes active when the source is requested but is not applied on entry. If +/// that source is undefined and the consumer requests the next source, the +/// `UseId` key narrows the enclosing `int | None` place to `int`. If both +/// sources are exhausted, the key remains active for expression-level narrowing. +pub(crate) struct PlaceLoadSource<'db> { + /// How this source supplies the loaded value. + pub(crate) kind: PlaceLoadSourceKind<'db>, + /// Selects the constraints used to narrow this source. + entry_checkpoint: usize, + /// The role this source plays in the load. + role: PlaceLoadSourceRole, +} + +impl PlaceLoadSource<'_> { + /// Returns whether this source is the module fallback for a class-local name. + pub(crate) fn is_class_body_global_fallback(&self) -> bool { + self.role == PlaceLoadSourceRole::ClassBodyGlobalFallback + } + + /// Returns whether this source is considered after lexical name resolution. + pub(crate) fn is_post_lexical(&self) -> bool { + matches!( + self.kind, + PlaceLoadSourceKind::Implicit( + ImplicitPlaceLoad::ModuleImplicitGlobal { .. } | ImplicitPlaceLoad::Builtin(_) + ) + ) + } +} + +/// Describes how a source can supply a place's value. +pub(crate) enum PlaceLoadSourceKind<'db> { + /// Bindings already selected for this load state. + /// + /// For an ordinary expression, these are the bindings that reach that point: + /// + /// ```py + /// value = 1 + /// reveal_type(value) # Only the first binding reaches this load. + /// value = "later" + /// ``` + /// + /// An enclosing eager snapshot is likewise a point-in-time view. A deferred load instead + /// selects all bindings reachable in its scope. + Bindings(BindingWithConstraintsIterator<'db, 'db>), + /// The whole place in the scope that owns it. + /// + /// A free-variable load in a lazy nested scope can observe any definition reachable for the + /// owning place, rather than the state at a single point: + /// + /// ```py + /// def outer(): + /// value: int | str = 1 + /// + /// def inner(): + /// return value + /// + /// value = "later" + /// ``` + /// + /// Keeping the scope and place ID lets inference evaluate both the declaration and all + /// reachable bindings for `outer.value`; an already-selected binding iterator does not retain + /// that whole-place information. + DefinitionsFromOwningScope { + /// The scope containing the place. + scope: ScopeId<'db>, + /// The place within `scope`. + id: ScopedPlaceId, + }, + /// A source represented by a specialized query or rule. + Implicit(ImplicitPlaceLoad<'db>), +} + +/// A source that consumers evaluate using a specialized query or rule. +pub(crate) enum ImplicitPlaceLoad<'db> { + /// The implicit `__class__` cell for a method, lambda, or generator expression defined directly + /// in a class body, e.g.: + /// + /// ```py + /// class C: + /// def method(self): + /// return __class__ + /// ``` + DunderClass(Definition<'db>), + /// An implicit symbol supplied directly in a class body, e.g.: + /// + /// ```py + /// class C: + /// defining_module = __module__ + /// ``` + ClassBodySymbol(Name), + /// A symbol in the module's explicit global namespace, e.g.: + /// + /// ```py + /// answer = 42 + /// + /// def get_answer(): + /// return answer + /// ``` + ExplicitGlobalSymbol { file: ProgramFile<'db>, name: Name }, + /// An implicit attribute supplied by a module, such as `__name__`. + ModuleImplicitGlobal { file: ProgramFile<'db>, name: Name }, + /// A name supplied by the builtin namespace. + Builtin(Name), +} + +/// The role a source plays in a place load. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum PlaceLoadSourceRole { + /// The source follows ordinary Python name resolution rules. + Ordinary, + /// The source follows Python’s class-local-to-module fallback rules. + ClassBodyGlobalFallback, +} + +/// The reason resolution stops if the preceding sources do not supply a value. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum PlaceLoadFailure { + /// No additional place-load source applies. + /// + /// For a symbol load, this means runtime lookup raises `NameError`. + NotFound, + /// The current function-like binding scope owns the loaded symbol but + /// supplies no value. + /// + /// Loading the symbol at runtime raises `UnboundLocalError`. + UnboundLocal, + /// An enclosing function-like binding scope owns the place, so resolution + /// cannot continue to module globals or builtins. + /// + /// For a symbol load, an empty closure cell raises `NameError` at runtime. + UnboundFree, +} + +/// Compact descriptions of loads for the tracked prefixes of a place expression. +/// +/// Resolution continues past the local source only if every tracked prefix is locally undefined. +/// +/// For example, the enclosing binding of `obj.value` cannot supply the value read in `inner`: +/// +/// ```python +/// class Outer: +/// value: int +/// +/// class Inner: +/// value: str +/// +/// def outer(): +/// obj = Outer() +/// obj.value = 1 +/// +/// def inner(): +/// obj = Inner() +/// reveal_type(obj.value) # revealed: str +/// ``` +/// +/// The nested scope binds `obj` to a different object, so normal member lookup on the local +/// `obj` must handle the load instead. +pub(crate) struct PlaceExprPrefixLoads<'db> { + scope: ScopeId<'db>, + loads: SmallVec<[PlaceExprPrefixLoad; 2]>, +} + +impl<'db> PlaceExprPrefixLoads<'db> { + /// Creates prefix loads, returning `None` when the iterator is empty. + fn from_iter( + scope: ScopeId<'db>, + loads: impl IntoIterator, + ) -> Option { + let loads = loads.into_iter().collect::>(); + (!loads.is_empty()).then_some(Self { scope, loads }) + } + + /// Returns the scope containing the prefix loads. + pub(crate) fn scope(&self) -> ScopeId<'db> { + self.scope + } + + /// Iterates over the prefix loads. + pub(crate) fn iter(&self) -> impl Iterator + '_ { + self.loads.iter().copied() + } +} + +/// Describes how a consumer can evaluate one prefix of a place expression. +#[derive(Clone, Copy)] +pub(crate) enum PlaceExprPrefixLoad { + /// Use the bindings that reach this expression occurrence. + AtUse(ScopedUseId), + /// Use every binding reachable for this place in its scope. + AllReachable(ScopedPlaceId), + /// The syntax itself guarantees that the prefix is bound. + DefinitelyBound, +} + +/// Read-only context used to select sources for a place load. +#[derive(Clone, Copy)] +struct PlaceLoadResolutionContext<'db, 'ast> { + db: &'db dyn Db, + index: &'db SemanticIndex<'db>, + scope: ScopeId<'db>, + file: ProgramFile<'db>, + mode: PlaceLoadMode<'ast>, +} + +impl<'db> PlaceLoadResolutionContext<'db, '_> { + fn symbol_has_scope_declaration(self, place_expr: PlaceExprRef) -> bool { + let Some(symbol) = place_expr.as_symbol() else { + return false; + }; + let scope = self.scope.file_scope_id(self.db); + let table = self.index.place_table(scope); + let Some(symbol_id) = table.symbol_id(symbol.name()) else { + return false; + }; + let symbol = table.symbol(symbol_id); + symbol.is_global() || symbol.is_nonlocal() + } + + fn is_class_body_scope(self) -> bool { + self.scope.node(self.db).scope_kind().is_class() + } + + fn uses_enclosing_snapshots(self) -> bool { + matches!(self.mode, PlaceLoadMode::AtExpression(_)) + } + + fn is_lexical_enclosing_scope(self, enclosing_scope: FileScopeId) -> bool { + self.index.scope(enclosing_scope).kind().is_function_like() + || (self.scope.is_annotation(self.db) + && self.scope.scope(self.db).parent() == Some(enclosing_scope)) + } + + fn local_source( + self, + place_expr: PlaceExprRef, + ) -> Option<( + PlaceLoadSourceKind<'db>, + Option<(FileScopeId, ConstraintKey)>, + )> { + let scope = self.scope.file_scope_id(self.db); + let table = self.index.place_table(scope); + let use_def = self.index.use_def_map(scope); + + match self.mode { + PlaceLoadMode::AtExpression(expr_ref) => { + if expr_ref + .as_name_expr() + .is_some_and(|name| name.is_invalid()) + { + return None; + } + + let use_id = expr_ref.scoped_use_id(self.db, self.file); + Some(( + PlaceLoadSourceKind::Bindings(use_def.bindings_at_use(use_id)), + Some((scope, ConstraintKey::UseId(use_id))), + )) + } + PlaceLoadMode::Deferred | PlaceLoadMode::StringAnnotation => { + let source = table + .place_id(place_expr) + .map(|id| PlaceLoadSourceKind::Bindings(use_def.reachable_bindings(id))); + assert!( + source.is_some() || matches!(self.mode, PlaceLoadMode::StringAnnotation), + "Expected the place table to create a place for every valid PlaceExpr node" + ); + source.map(|source| (source, None)) + } + } + } + + /// Describes how to evaluate the tracked prefixes of `place_expr` in this scope. + fn place_expr_prefix_loads( + self, + place_expr: PlaceExprRef, + ) -> Option> { + let table = self.index.place_table(self.scope.file_scope_id(self.db)); + + PlaceExprPrefixLoads::from_iter( + self.scope, + table + .parents(place_expr) + .filter_map(|prefix_id| match self.mode { + PlaceLoadMode::Deferred | PlaceLoadMode::StringAnnotation => { + Some(PlaceExprPrefixLoad::AllReachable(prefix_id)) + } + PlaceLoadMode::AtExpression(mut prefix_expr_ref) => { + let prefix = table.place(prefix_id); + for _ in + 0..(place_expr.num_member_segments() - prefix.num_member_segments()) + { + prefix_expr_ref = match prefix_expr_ref { + ast::ExprRef::Attribute(attribute) => { + ast::ExprRef::from(&attribute.value) + } + ast::ExprRef::Subscript(subscript) => { + ast::ExprRef::from(&subscript.value) + } + _ => return None, + }; + } + + if prefix_expr_ref + .as_name_expr() + .is_some_and(|name| name.is_invalid()) + { + return None; + } + + if let ast::ExprRef::Named(named) = prefix_expr_ref { + return named + .target + .is_name_expr() + .then_some(PlaceExprPrefixLoad::DefinitelyBound); + } + + Some(PlaceExprPrefixLoad::AtUse( + prefix_expr_ref.scoped_use_id(self.db, self.file), + )) + } + }), + ) + } + + fn skips_non_global_scopes(self, symbol: ScopedSymbolId) -> bool { + let scope = self.scope.file_scope_id(self.db); + !scope.is_global() && self.index.symbol_is_global_in_scope(symbol, scope) + } + + fn dunder_class_cell_definition(self) -> Option> { + let current_scope = self.scope.file_scope_id(self.db); + if let Some(definition) = self.index.class_definition_of_method(current_scope) { + return Some(definition); + } + + let scope = self.index.scope(current_scope); + if !matches!( + scope.node(), + NodeWithScopeKind::Lambda(_) | NodeWithScopeKind::GeneratorExpression(_) + ) { + return None; + } + let class = self.index.parent_scope(current_scope)?.node().as_class()?; + Some(self.index.expect_single_definition(class)) + } +} + +/// A node in the acyclic graph traversed by a [`PlaceLoadResolution`]. +/// +/// Source-named nodes may yield a [`PlaceLoadResolutionStep::Source`]. The two verb-named nodes +/// either ask the consumer whether traversal should continue or decide which outgoing edge to +/// follow. Every transition advances toward a [`PlaceLoadResolutionNode::Failure`] leaf; no node +/// is revisited. +enum PlaceLoadResolutionNode<'db> { + /// The source selected from the load's own scope, if one exists. + LocalSource, + /// Ask the consumer whether resolution should continue for a member load. + AskConsumerWhetherToContinueForMember, + /// Decide whether resolution ends, continues through enclosing scopes, or moves to the module + /// scope. + DecideResolutionPath, + /// The implicit `__class__` source, followed by enclosing scopes. + DunderClassSource { + definition: Definition<'db>, + enclosing_scopes: AncestorsIter<'db>, + }, + /// A source from the remaining enclosing scopes, if one exists. + EnclosingScopeSource(AncestorsIter<'db>), + /// The implicit class-body source, followed by the applicable global source. + ImplicitClassBodySource(Option>), + /// Bindings from an enclosing `global` declaration that were visible when the nested eager + /// scope began. + ForwardedGlobalSnapshotSource(ForwardedGlobalSnapshot<'db>), + /// An explicit global source with the given role, if one exists. + ExplicitGlobalSource(PlaceLoadSourceRole), + /// An implicit global considered after explicit lookup, if one exists. + ImplicitGlobalSource, + /// The builtin with the given name. + BuiltinSource(Name), + /// The failure that ends resolution. + Failure(PlaceLoadFailure), +} + +struct ForwardedGlobalSnapshot<'db> { + bindings: BindingWithConstraintsIterator<'db, 'db>, + enclosing_scope: FileScopeId, +} + +/// Narrowing constraints accumulated while a consumer advances through a place load. +#[derive(Default)] +struct PlaceLoadConstraints { + constraint_keys: Vec<(FileScopeId, ConstraintKey)>, +} + +impl PlaceLoadConstraints { + /// Creates a source narrowed by the constraints accumulated before it. + /// + /// `exit_constraint`, when present, is activated only after the source is requested (that + /// source was already selected from the binding state identified by the constraint, so it is + /// deliberately not reapplied to the same source). + /// + /// For example, consider the load at `U`: + /// + /// ```py + /// def outer(value: int | None): + /// def inner(): + /// if value is not None: + /// return value # U + /// ``` + /// + /// The local source is selected by `bindings_at_use(U)`. Its `UseId(U)` is the exit constraint: + /// it is not applied again to that source, but becomes active if the source is unbound so that + /// the enclosing `outer.value` source is narrowed from `int | None` to `int`. + fn source<'db>( + &mut self, + kind: PlaceLoadSourceKind<'db>, + role: PlaceLoadSourceRole, + exit_constraint: Option<(FileScopeId, ConstraintKey)>, + ) -> PlaceLoadSource<'db> { + let entry_checkpoint = self.constraint_keys.len(); + self.constraint_keys.extend(exit_constraint); + PlaceLoadSource { + kind, + entry_checkpoint, + role, + } + } + + /// Creates a source without applying accumulated narrowing constraints to it. + fn unnarrowed_source( + kind: PlaceLoadSourceKind<'_>, + role: PlaceLoadSourceRole, + ) -> PlaceLoadSource<'_> { + PlaceLoadSource { + kind, + entry_checkpoint: 0, + role, + } + } + + /// Extends the list of constraints used by subsequent sources. + fn push(&mut self, scope: FileScopeId, key: ConstraintKey) { + self.constraint_keys.push((scope, key)); + } + + /// Returns the constraints used to narrow `source`. + fn narrowing_constraints_for( + &self, + source: &PlaceLoadSource<'_>, + ) -> &[(FileScopeId, ConstraintKey)] { + &self.constraint_keys[..source.entry_checkpoint] + } + + /// Returns the constraints activated by the sources that were requested. + fn into_constraints(self) -> Vec<(FileScopeId, ConstraintKey)> { + self.constraint_keys + } +} diff --git a/crates/ty_python_semantic/src/pull_types.rs b/crates/ty_python_semantic/src/pull_types.rs index f23f1e5b4d..36d3ab72a2 100644 --- a/crates/ty_python_semantic/src/pull_types.rs +++ b/crates/ty_python_semantic/src/pull_types.rs @@ -4,15 +4,16 @@ //! (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_db::parsed::parsed_module; use ruff_python_ast::{ self as ast, visitor::source_order, visitor::source_order::SourceOrderVisitor, }; +use ty_python_core::ProgramFile; -pub fn pull_types(db: &dyn Db, file: File) { +pub fn pull_types(db: &dyn Db, file: ProgramFile<'_>) { let mut visitor = PullTypesVisitor::new(db, file); - let ast = parsed_module(db, file).load(db); + let ast = parsed_module(db, file.python_file(db)).load(db); visitor.visit_body(ast.suite()); } @@ -22,7 +23,7 @@ struct PullTypesVisitor<'db> { } impl<'db> PullTypesVisitor<'db> { - fn new(db: &'db dyn Db, file: File) -> Self { + fn new(db: &'db dyn Db, file: ProgramFile<'db>) -> Self { Self { model: SemanticModel::new(db, file), } diff --git a/crates/ty_python_semantic/src/reachability.rs b/crates/ty_python_semantic/src/reachability.rs index 04b9dcb66b..1e0ba339f6 100644 --- a/crates/ty_python_semantic/src/reachability.rs +++ b/crates/ty_python_semantic/src/reachability.rs @@ -193,6 +193,7 @@ //! [Kleene]: //! [bdd]: https://en.wikipedia.org/wiki/Binary_decision_diagram +use crate::ProgramEnvironment; use std::cell::RefCell; use crate::types::function::KnownFunction; @@ -202,20 +203,18 @@ use crate::{ dunder_all::dunder_all_names, place::{DefinedPlace, Definedness, Place, RequiresExplicitReExport, imported_symbol}, types::{ - ActiveRecursionDetector, CallableTypes, ComparisonSoundnessPolicy, EnumClassLiteral, - KnownInstanceType, NarrowingConstraint, SpecialFormType, Type, TypeContext, UnionType, - callable_pattern_type, definite_match_pattern_type, - definite_match_pattern_type_for_subject, equality_truthiness, expand_type, - infer_expression_types, infer_narrowing_constraints, infer_same_file_expression_type, - mapping_pattern_type, pattern_binding_fallthrough_type, sequence_pattern_type_builder, - singleton_pattern_type, + CallableTypes, ComparisonSoundnessPolicy, EnumClassLiteral, KnownInstanceType, + NarrowingConstraint, SpecialFormType, Type, TypeContext, UnionType, callable_pattern_type, + definite_match_pattern_type, definite_match_pattern_type_for_subject, equality_truthiness, + expand_type, infer_expression_types, infer_narrowing_constraints, + infer_same_file_expression_type, mapping_pattern_type, pattern_binding_fallthrough_type, + sequence_pattern_type_builder, singleton_pattern_type, }, }; use ruff_index::{Idx, IndexSlice}; use ruff_python_ast::name::Name; use ruff_text_size::TextRange; use rustc_hash::{FxHashMap, FxHashSet}; -use salsa::plumbing::AsId; use smallvec::SmallVec; use ty_python_core::{ BindingWithConstraints, DeclarationWithConstraint, DeclarationsIterator, FileScopeId, @@ -242,8 +241,9 @@ use ty_python_core::{ #[salsa::tracked( returns(copy), cycle_initial = |_, id, _, _| Type::divergent(id), - cycle_fn = |db, cycle, previous: &Type<'db>, result: Type<'db>, _, _| { - result.cycle_normalized(db, *previous, cycle) + cycle_fn = |db: &'db dyn Db, cycle, previous: &Type<'db>, result: Type<'db>, predicate: PatternPredicate<'db>, _| { + let env = ProgramEnvironment::from_scope(predicate.scope(db)); + result.cycle_normalized(db, &env, *previous, cycle) }, heap_size = ruff_memory_usage::heap_size )] @@ -272,8 +272,9 @@ pub(crate) fn type_narrowed_by_previous_patterns<'db>( #[salsa::tracked( returns(copy), cycle_initial = |_, id, _, _| Type::divergent(id), - cycle_fn = |db, cycle, previous: &Type<'db>, result: Type<'db>, _, _| { - result.cycle_normalized(db, *previous, cycle) + cycle_fn = |db: &'db dyn Db, cycle, previous: &Type<'db>, result: Type<'db>, predicate: PatternPredicate<'db>, _| { + let env = ProgramEnvironment::from_scope(predicate.scope(db)); + result.cycle_normalized(db, &env, *previous, cycle) }, heap_size = ruff_memory_usage::heap_size )] @@ -282,7 +283,8 @@ fn type_narrowed_by_pattern<'db>( predicate: PatternPredicate<'db>, subject_ty: Type<'db>, ) -> Type<'db> { - pattern_binding_fallthrough_type(db, predicate.kind(db), subject_ty) + let env = ProgramEnvironment::from_file(predicate.program_file(db)); + pattern_binding_fallthrough_type(db, &env, predicate.kind(db), subject_ty) } /// Return the enum class and canonical member names represented by an enum-literal subject type. @@ -329,7 +331,9 @@ fn enum_literal_subject_names<'db>( add_enum_literal(db, &mut enum_class, &mut names, *element)?; } } - Type::TypeAlias(alias) => return enum_literal_subject_names(db, alias.value_type(db)), + Type::TypeAlias(alias) => { + return enum_literal_subject_names(db, alias.value_type(db)); + } _ => return None, } @@ -339,14 +343,15 @@ fn enum_literal_subject_names<'db>( /// Return the canonical enum-member name matched by a single value pattern. /// /// This recognizes patterns like `case Color.RED:` only when the pattern expression is -/// single-valued and belongs to the expected enum class. Enum aliases are resolved to their +/// an enum member belonging to the expected enum class. Enum aliases are resolved to their /// canonical member names before returning. fn enum_member_pattern_name<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, enum_class: EnumClassLiteral<'db>, kind: &PatternPredicateKind<'db>, ) -> Option { - let value_ty = definite_match_pattern_type(db, kind); + let value_ty = definite_match_pattern_type(db, env, kind); let enum_literal = value_ty.as_enum_literal()?; if enum_literal.enum_class_literal(db) != enum_class { return None; @@ -372,6 +377,7 @@ struct EnumMemberPatternCoverage { /// produces only a lower bound: it definitely matches `Color.GREEN`, but can match other members. fn enum_member_pattern_coverage<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, enum_class: EnumClassLiteral<'db>, kind: &PatternPredicateKind<'db>, ) -> EnumMemberPatternCoverage { @@ -382,7 +388,7 @@ fn enum_member_pattern_coverage<'db>( match kind { PatternPredicateKind::Or(alts) => { for alt in alts { - let alt_coverage = enum_member_pattern_coverage(db, enum_class, alt); + let alt_coverage = enum_member_pattern_coverage(db, env, enum_class, alt); coverage .definitely_matched .extend(alt_coverage.definitely_matched); @@ -390,10 +396,10 @@ fn enum_member_pattern_coverage<'db>( } } PatternPredicateKind::As(Some(inner), _) => { - return enum_member_pattern_coverage(db, enum_class, inner); + return enum_member_pattern_coverage(db, env, enum_class, inner); } _ => { - if let Some(name) = enum_member_pattern_name(db, enum_class, kind) { + if let Some(name) = enum_member_pattern_name(db, env, enum_class, kind) { coverage.definitely_matched.insert(name); } else { coverage.is_exact = false; @@ -410,11 +416,12 @@ fn enum_member_pattern_coverage<'db>( /// ambiguous because the guard can reject an otherwise matching enum member. fn analyze_enum_literal_union_pattern_predicate<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, predicate: PatternPredicate<'db>, subject_ty: Type<'db>, ) -> Option { let (enum_class, mut remaining_names) = enum_literal_subject_names(db, subject_ty)?; - let current_coverage = enum_member_pattern_coverage(db, enum_class, predicate.kind(db)); + let current_coverage = enum_member_pattern_coverage(db, env, enum_class, predicate.kind(db)); let current_names = ¤t_coverage.definitely_matched; if current_names.is_empty() { return None; @@ -429,7 +436,7 @@ fn analyze_enum_literal_union_pattern_predicate<'db>( } let previous_coverage = - enum_member_pattern_coverage(db, enum_class, previous_predicate.kind(db)); + enum_member_pattern_coverage(db, env, enum_class, previous_predicate.kind(db)); #[expect( clippy::iter_over_hash_type, reason = "set removal is independent of iteration order" @@ -475,16 +482,17 @@ pub(crate) fn analyze_pattern_predicate<'db>( db: &'db dyn Db, predicate: PatternPredicate<'db>, ) -> Truthiness { + let env = ProgramEnvironment::from_scope(predicate.scope(db)); let subject_ty = pattern_subject_type(db, predicate.subject(db)); if let Some(truthiness) = - analyze_enum_literal_union_pattern_predicate(db, predicate, subject_ty) + analyze_enum_literal_union_pattern_predicate(db, &env, predicate, subject_ty) { return truthiness; } - let coverage_subject_ty = expand_type(db, subject_ty) - .map(|types| UnionType::from_elements(db, types)) + let coverage_subject_ty = expand_type(db, &env, subject_ty) + .map(|types| UnionType::from_elements(db, &env, types)) .unwrap_or(subject_ty); let narrowed_subject_ty = type_narrowed_by_previous_patterns(db, predicate, coverage_subject_ty); @@ -503,8 +511,13 @@ pub(crate) fn analyze_pattern_predicate<'db>( return Truthiness::AlwaysTrue; } - let truthiness = - analyze_single_pattern_predicate_kind(db, predicate.kind(db), narrowed_subject_ty, None); + let truthiness = analyze_single_pattern_predicate_kind( + db, + &env, + predicate.kind(db), + narrowed_subject_ty, + None, + ); if truthiness == Truthiness::AlwaysTrue && predicate.guard(db).is_some() { // Fall back to ambiguous, the guard might change the result. @@ -528,10 +541,6 @@ fn accumulate_constraint<'db>( } } -std::thread_local! { - static ACTIVE_NON_TERMINAL_CALL_PREFIXES: ActiveRecursionDetector = ActiveRecursionDetector::default(); -} - const NON_TERMINAL_CALL_CHUNK_SIZE: usize = 16; const REACHABILITY_EVALUATION_CHUNK_SIZE: usize = 256; @@ -541,13 +550,14 @@ fn predicate_scope<'db>(db: &'db dyn Db, predicate: &Predicate<'db>) -> ScopeId< PredicateNode::IsNonTerminalCall(CallableAndCallExpr { callable, .. }) | PredicateNode::AssertsCall(CallableAndCallExpr { callable, .. }) => callable.scope(db), PredicateNode::Pattern(pattern) => pattern.scope(db), + PredicateNode::OrPatternAlternative(scope) => scope, PredicateNode::SubjectElementPattern(subject_element) => subject_element.pattern.scope(db), PredicateNode::IsNonEmptyIterable(expression) => expression.scope(db), PredicateNode::StarImportPlaceholder(star_import) => star_import.scope(db), } } -/// Infers preceding call predicates in source order. +/// Infers complete preceding blocks of call predicates in source order. /// /// Predicate IDs are assigned in source order, but the decision diagrams intentionally order /// predicates in reverse to reduce their size. Inferring a later call can depend on the @@ -561,10 +571,12 @@ fn predicate_scope<'db>(db: &'db dyn Db, predicate: &Predicate<'db>) -> ScopeId< /// accept the broader eager pass because it keeps the ordering simple, and checking a scope will /// typically exercise most of its predicates eventually. /// -/// Reentrant analysis of the same predicate graph skips the prefix pass: because the outer pass is -/// proceeding in source order, any preceding call needed by the current expression has already -/// been inferred. A different predicate graph performs its own pass, which is necessary when -/// inferring a call crosses into another large scope. +/// Reentrant analysis is handled by Salsa cycle recovery on the cached-range queries. The final +/// incomplete block is left for the reachability walk: it can add at most 15 nested call queries, +/// and analyzing it eagerly would bypass the range query's cycle recovery and could introduce a +/// divergent inference cycle. For large scopes, keeping the complete-block pass unconditional +/// ensures that tracked callers record the same dependencies on every thread. Small scopes do not +/// need prefix warming to bound the Salsa stack, so their calls are evaluated entirely on demand. fn analyze_non_terminal_call_prefix<'db>( db: &'db dyn Db, predicates: &IndexSlice>, @@ -577,51 +589,25 @@ fn analyze_non_terminal_call_prefix<'db>( .nth(NON_TERMINAL_CALL_CHUNK_SIZE) .is_some(); - ACTIVE_NON_TERMINAL_CALL_PREFIXES.with(|active| { - active.visit( - &scope.as_id(), - || {}, - || { - if !has_many_calls { - for predicate in &predicates.raw[..=root_predicate.index()] { - if matches!(predicate.node, PredicateNode::IsNonTerminalCall(_)) { - analyze_single(db, predicate); - } - } - return; - } - - let call_predicates = non_terminal_call_predicates(db, scope); - let call_count = - call_predicates.partition_point(|predicate| *predicate <= root_predicate); - if call_count <= NON_TERMINAL_CALL_CHUNK_SIZE { - analyze_non_terminal_calls(db, predicates, &call_predicates[..call_count]); - return; - } - - let mut start = 0; - let mut remaining = call_count / NON_TERMINAL_CALL_CHUNK_SIZE; - - while remaining > 0 { - let level = remaining.ilog2(); - let length = 1 << level; - analyze_non_terminal_call_range(db, scope, level, start >> level); - start += length; - remaining -= length; - } + if !has_many_calls { + return false; + } - let tail_start = - call_count / NON_TERMINAL_CALL_CHUNK_SIZE * NON_TERMINAL_CALL_CHUNK_SIZE; - analyze_non_terminal_calls( - db, - predicates, - &call_predicates[tail_start..call_count], - ); - }, - ); - }); + let call_predicates = non_terminal_call_predicates(db, scope); + let call_count = call_predicates.partition_point(|predicate| *predicate <= root_predicate); + let mut start = 0; + // Leave the incomplete final block demand-driven. Its reverse dependency chain is bounded by + // the block size, and every eagerly analyzed call remains behind a recoverable range query. + let mut remaining = call_count / NON_TERMINAL_CALL_CHUNK_SIZE; + while remaining > 0 { + let level = remaining.ilog2(); + let length = 1 << level; + analyze_non_terminal_call_range(db, scope, level, start >> level); + start += length; + remaining -= length; + } - has_many_calls + true } /// Returns the statement-call predicates for `scope` in source order. @@ -644,11 +630,12 @@ fn non_terminal_call_predicates<'db>( fn analyze_non_terminal_calls<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, predicates: &IndexSlice>, call_predicates: &[ScopedPredicateId], ) { for id in call_predicates { - analyze_single(db, &predicates[*id]); + analyze_single(db, env, &predicates[*id]); } } @@ -658,7 +645,15 @@ fn analyze_non_terminal_calls<'db>( /// queries. Splitting ranges in half keeps the Salsa query stack logarithmic even when the first /// requested prefix contains thousands of calls. Each leaf handles multiple calls iteratively to /// avoid retaining a Salsa argument and query result for every individual predicate. -#[salsa::tracked(returns(copy), heap_size = get_size2::GetSize::get_heap_size)] +/// +/// Analyzing a call can re-enter reachability through expression inference and request this same +/// range. Recovery is a no-op because the range only warms call queries; any call still needed for +/// reachability is evaluated directly by the decision-diagram walk. +#[salsa::tracked( + returns(copy), + cycle_initial = |_, _, _, _, _| (), + heap_size = get_size2::GetSize::get_heap_size +)] fn analyze_non_terminal_call_range<'db>( db: &'db dyn Db, scope: ScopeId<'db>, @@ -666,11 +661,12 @@ fn analyze_non_terminal_call_range<'db>( index: usize, ) { if level == 0 { + let env = ProgramEnvironment::from_scope(scope); let use_def = use_def_map(db, scope); let call_predicates = non_terminal_call_predicates(db, scope); let start = index * NON_TERMINAL_CALL_CHUNK_SIZE; let end = start + NON_TERMINAL_CALL_CHUNK_SIZE; - analyze_non_terminal_calls(db, use_def.predicates(), &call_predicates[start..end]); + analyze_non_terminal_calls(db, &env, use_def.predicates(), &call_predicates[start..end]); return; } @@ -741,6 +737,8 @@ fn evaluate_reachability_path<'db>( mut id: ScopedReachabilityConstraintId, mut use_checkpoint: bool, ) -> Truthiness { + let env = ProgramEnvironment::from_scope(scope); + loop { if let Some(reachability) = terminal_reachability(id) { return reachability; @@ -755,7 +753,7 @@ fn evaluate_reachability_path<'db>( return evaluate_reachability_checkpoint(db, scope, id); } - id = match analyze_single(db, &predicates[node.atom()]) { + id = match analyze_single(db, &env, &predicates[node.atom()]) { Truthiness::AlwaysTrue => node.if_true(), Truthiness::Ambiguous => node.if_ambiguous(), Truthiness::AlwaysFalse => node.if_false(), @@ -829,6 +827,7 @@ impl<'db> ReachabilityConstraintsExtension<'db> for ReachabilityConstraints { pub(crate) fn narrow_type_by_constraint<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, constraints: &NarrowingConstraints, predicates: &IndexSlice>, id: ScopedNarrowingConstraint, @@ -841,10 +840,11 @@ pub(crate) fn narrow_type_by_constraint<'db>( _ => {} } - let mut projector = NarrowingProjector::new(db, constraints, predicates, place); + let mut projector = NarrowingProjector::new(db, env, constraints, predicates, place); let projected_root = projector.project(id); let mut context = ProjectedNarrowingContext { db, + env, base_ty, graph: &projector.graph, joins: projector.graph.joins(projected_root), @@ -855,13 +855,14 @@ pub(crate) fn narrow_type_by_constraint<'db>( fn apply_accumulated_narrowing<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, base_ty: Type<'db>, accumulated: Option>, ) -> Type<'db> { match accumulated { Some(constraint) => NarrowingConstraint::intersection(base_ty) .merge_constraint_and(constraint) - .evaluate_constraint_type(db), + .evaluate_constraint_type(db, env), None => base_ty, } } @@ -1063,6 +1064,7 @@ impl ProjectedNarrowingGraph<'_> { /// Removes predicates that cannot narrow one place from a narrowing constraint. struct NarrowingProjector<'a, 'db> { db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, constraints: &'a NarrowingConstraints, predicates: &'a IndexSlice>, place: ScopedPlaceId, @@ -1074,12 +1076,14 @@ impl<'a, 'db> NarrowingProjector<'a, 'db> { /// Creates a projector for narrowing `place`. fn new( db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, constraints: &'a NarrowingConstraints, predicates: &'a IndexSlice>, place: ScopedPlaceId, ) -> Self { Self { db, + env, constraints, predicates, place, @@ -1096,12 +1100,14 @@ impl<'a, 'db> NarrowingProjector<'a, 'db> { Option>, Option>, ) { + let env = self.env; + let db = self.db; if let Some(cached) = self.graph.predicate_constraints_cache.get(&predicate_id) { return cached.clone(); } let constraints = - infer_narrowing_constraints(self.db, self.predicates[predicate_id], self.place); + infer_narrowing_constraints(db, env, self.predicates[predicate_id], self.place); self.graph .predicate_constraints_cache .insert(predicate_id, constraints.clone()); @@ -1117,6 +1123,7 @@ impl<'a, 'db> NarrowingProjector<'a, 'db> { FinishNonTerminal { id: Id, branch: Id }, FinishPredicate(Id), } + let db = self.db; let mut actions = SmallVec::<[Action; 8]>::new(); actions.push(Action::Visit(root)); @@ -1144,7 +1151,7 @@ impl<'a, 'db> NarrowingProjector<'a, 'db> { Action::AnalyzeNonTerminal(id) => { let node = self.constraints.get_interior_node(id); let predicate = self.predicates[node.atom]; - let branch = match analyze_single(self.db, &predicate) { + let branch = match analyze_single(db, self.env, &predicate) { Truthiness::AlwaysTrue => node.if_true, Truthiness::AlwaysFalse => node.if_false, Truthiness::Ambiguous => { @@ -1200,6 +1207,7 @@ impl<'a, 'db> NarrowingProjector<'a, 'db> { /// Evaluates narrowed types over a projected narrowing graph. struct ProjectedNarrowingContext<'a, 'db> { db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, base_ty: Type<'db>, graph: &'a ProjectedNarrowingGraph<'db>, /// Marks join boundaries in the projected DAG. @@ -1230,11 +1238,12 @@ impl<'db> ProjectedNarrowingContext<'_, 'db> { id: ProjectedNarrowingNodeId, accumulated: Option>, ) -> Type<'db> { + let db = self.db; if self.is_join(id) { // Preserve replacement narrowing order at a join: evaluate the shared suffix once, // then apply the incoming prefix constraint to its narrowed type. let suffix_ty = self.narrow_join(id); - return apply_accumulated_narrowing(self.db, suffix_ty, accumulated); + return apply_accumulated_narrowing(db, self.env, suffix_ty, accumulated); } self.narrow_uncached(id, accumulated) @@ -1246,12 +1255,13 @@ impl<'db> ProjectedNarrowingContext<'_, 'db> { id: ProjectedNarrowingNodeId, accumulated: Option>, ) -> Type<'db> { + let db = self.db; if id == ProjectedNarrowingNodeId::ALWAYS_FALSE { return Type::Never; } if id == ProjectedNarrowingNodeId::ALWAYS_TRUE { - apply_accumulated_narrowing(self.db, self.base_ty, accumulated) + apply_accumulated_narrowing(db, self.env, self.base_ty, accumulated) } else { let node = self.graph.node(id); let (pos_constraint, neg_constraint) = @@ -1277,8 +1287,8 @@ impl<'db> ProjectedNarrowingContext<'_, 'db> { let false_ty = self.narrow(node.if_false, false_accumulated); let true_or_uncertain = - UnionType::from_two_elements(self.db, true_ty, uncertain_ty); - UnionType::from_two_elements(self.db, true_or_uncertain, false_ty) + UnionType::from_two_elements(db, self.env, true_ty, uncertain_ty); + UnionType::from_two_elements(db, self.env, true_or_uncertain, false_ty) } } } @@ -1286,6 +1296,7 @@ impl<'db> ProjectedNarrowingContext<'_, 'db> { fn analyze_single_pattern_predicate_kind<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, predicate_kind: &PatternPredicateKind<'db>, subject_ty: Type<'db>, precomputed_definite_match_ty: Option>, @@ -1296,6 +1307,7 @@ fn analyze_single_pattern_predicate_kind<'db>( equality_truthiness( db, + env, subject_ty, value_ty, ComparisonSoundnessPolicy::from_analysis_settings( @@ -1304,11 +1316,11 @@ fn analyze_single_pattern_predicate_kind<'db>( ) } PatternPredicateKind::Singleton(singleton) => { - let singleton_ty = singleton_pattern_type(db, *singleton); + let singleton_ty = singleton_pattern_type(db, env, *singleton); - if subject_ty.is_equivalent_to(db, singleton_ty) { + if subject_ty.is_equivalent_to(db, env, singleton_ty) { Truthiness::AlwaysTrue - } else if subject_ty.is_disjoint_from(db, singleton_ty) { + } else if subject_ty.is_disjoint_from(db, env, singleton_ty) { Truthiness::AlwaysFalse } else { Truthiness::Ambiguous @@ -1324,21 +1336,23 @@ fn analyze_single_pattern_predicate_kind<'db>( let narrowed_subject_ty = remaining_subject_ty; let definitely_matched = - definite_match_pattern_type_for_subject(db, p, narrowed_subject_ty); + definite_match_pattern_type_for_subject(db, env, p, narrowed_subject_ty); - let truthiness = if narrowed_subject_ty.is_subtype_of(db, definitely_matched) { - Truthiness::AlwaysTrue - } else { - analyze_single_pattern_predicate_kind( - db, - p, - narrowed_subject_ty, - Some(definitely_matched), - ) - }; + let truthiness = + if narrowed_subject_ty.is_subtype_of(db, env, definitely_matched) { + Truthiness::AlwaysTrue + } else { + analyze_single_pattern_predicate_kind( + db, + env, + p, + narrowed_subject_ty, + Some(definitely_matched), + ) + }; remaining_subject_ty = - pattern_binding_fallthrough_type(db, p, narrowed_subject_ty); + pattern_binding_fallthrough_type(db, env, p, narrowed_subject_ty); truthiness }) // this is just a "max", but with a slight optimization: @@ -1362,7 +1376,9 @@ fn analyze_single_pattern_predicate_kind<'db>( // precision, never soundness PatternPredicateKind::And(predicates) => predicates .iter() - .map(|predicate| analyze_single_pattern_predicate_kind(db, predicate, subject_ty, None)) + .map(|predicate| { + analyze_single_pattern_predicate_kind(db, env, predicate, subject_ty, None) + }) .fold(Truthiness::AlwaysTrue, |acc, next| match (acc, next) { (Truthiness::AlwaysFalse, _) | (_, Truthiness::AlwaysFalse) => { Truthiness::AlwaysFalse @@ -1373,49 +1389,51 @@ fn analyze_single_pattern_predicate_kind<'db>( PatternPredicateKind::Class(kind) => { let class_ty = match infer_same_file_expression_type(db, kind.class, TypeContext::default()) { - Type::ClassLiteral(class) => Type::instance(db, class.top_materialization(db)), + Type::ClassLiteral(class) => { + Type::instance(db, env, class.top_materialization(db)) + } Type::SpecialForm(SpecialFormType::CollectionsAbcCallable) => { - callable_pattern_type(db) + callable_pattern_type(db, env) } _ => return Truthiness::Ambiguous, }; let definitely_matched = precomputed_definite_match_ty.unwrap_or_else(|| { - definite_match_pattern_type_for_subject(db, predicate_kind, subject_ty) + definite_match_pattern_type_for_subject(db, env, predicate_kind, subject_ty) }); - if subject_ty.is_equivalent_to(db, definitely_matched) - || subject_ty.is_subtype_of(db, definitely_matched) + if subject_ty.is_equivalent_to(db, env, definitely_matched) + || subject_ty.is_subtype_of(db, env, definitely_matched) { Truthiness::AlwaysTrue - } else if subject_ty.is_disjoint_from(db, class_ty) { + } else if subject_ty.is_disjoint_from(db, env, class_ty) { Truthiness::AlwaysFalse } else { Truthiness::Ambiguous } } PatternPredicateKind::Mapping(kind) => { - let mapping_ty = mapping_pattern_type(db); - if subject_ty.is_subtype_of(db, mapping_ty) { + let mapping_ty = mapping_pattern_type(db, env); + if subject_ty.is_subtype_of(db, env, mapping_ty) { if kind.is_irrefutable() { Truthiness::AlwaysTrue } else { Truthiness::Ambiguous } - } else if subject_ty.is_disjoint_from(db, mapping_ty) { + } else if subject_ty.is_disjoint_from(db, env, mapping_ty) { Truthiness::AlwaysFalse } else { Truthiness::Ambiguous } } PatternPredicateKind::Sequence(kind) => { - let sequence_ty = sequence_pattern_type_builder(db).build(); - if subject_ty.is_subtype_of(db, sequence_ty) { + let sequence_ty = sequence_pattern_type_builder(db, env).build(); + if subject_ty.is_subtype_of(db, env, sequence_ty) { if kind.is_irrefutable() { Truthiness::AlwaysTrue } else { Truthiness::Ambiguous } - } else if subject_ty.is_disjoint_from(db, sequence_ty) { + } else if subject_ty.is_disjoint_from(db, env, sequence_ty) { Truthiness::AlwaysFalse } else { Truthiness::Ambiguous @@ -1426,6 +1444,7 @@ fn analyze_single_pattern_predicate_kind<'db>( .map(|p| { analyze_single_pattern_predicate_kind( db, + env, p, subject_ty, precomputed_definite_match_ty, @@ -1454,6 +1473,7 @@ fn analyze_non_terminal_call<'db>( call_expr: Expression<'db>, is_await: bool, ) -> Truthiness { + let env = ProgramEnvironment::from_scope(callable.scope(db)); // 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 @@ -1487,7 +1507,7 @@ fn analyze_non_terminal_call<'db>( } let overloads_iterator = if let Some(callable) = ty - .try_upcast_to_callable(db) + .try_upcast_to_callable(db, &env) .and_then(CallableTypes::exactly_one) { callable.signatures(db).overloads.iter() @@ -1500,10 +1520,10 @@ fn analyze_non_terminal_call<'db>( let mut any_overload_is_generic = false; for overload in overloads_iterator { - let returns_never = overload.return_ty.is_equivalent_to(db, Type::Never); + let returns_never = overload.return_ty.is_equivalent_to(db, &env, Type::Never); no_overloads_return_never &= !returns_never; all_overloads_return_never &= returns_never; - any_overload_is_generic |= overload.return_ty.has_typevar(db); + any_overload_is_generic |= overload.return_ty.has_typevar(db, &env); } if no_overloads_return_never && !any_overload_is_generic && !is_await { @@ -1516,7 +1536,7 @@ fn analyze_non_terminal_call<'db>( // basedpython: a type variable the call left unsolved is solved to `Never`, which says // that the call's result cannot be described — not that the call does not return. - if call_expr_ty.is_equivalent_to(db, Type::Never) + if call_expr_ty.is_equivalent_to(db, &env, Type::Never) && !inference.is_unsolved_typevar_call(call_expr.node_ref(db)) { Truthiness::AlwaysFalse @@ -1535,13 +1555,13 @@ fn analyze_non_empty_iterable(db: &dyn Db, iterable: Expression) -> Truthiness { } } -fn analyze_single(db: &dyn Db, predicate: &Predicate) -> Truthiness { +fn analyze_single(db: &dyn Db, env: &ProgramEnvironment<'_>, predicate: &Predicate) -> Truthiness { let _span = tracing::trace_span!("analyze_single", ?predicate).entered(); match predicate.node { PredicateNode::Expression(test_expr) => { infer_same_file_expression_type(db, test_expr, TypeContext::default()) - .bool(db) + .bool(db, env) .negate_if(!predicate.is_positive) } PredicateNode::IsNonTerminalCall(CallableAndCallExpr { @@ -1554,6 +1574,7 @@ fn analyze_single(db: &dyn Db, predicate: &Predicate) -> Truthiness { // constraint, so its truth value is the fact that the call returned PredicateNode::AssertsCall(_) => Truthiness::AlwaysTrue.negate_if(!predicate.is_positive), PredicateNode::Pattern(inner) => analyze_pattern_predicate(db, inner), + PredicateNode::OrPatternAlternative(_) => Truthiness::Ambiguous, PredicateNode::SubjectElementPattern(subject_element) => { analyze_pattern_predicate(db, subject_element.pattern) } @@ -1563,9 +1584,8 @@ fn analyze_single(db: &dyn Db, predicate: &Predicate) -> Truthiness { PredicateNode::StarImportPlaceholder(star_import) => { let place_table = place_table(db, star_import.scope(db)); let symbol = place_table.symbol(star_import.symbol_id(db)); - let referenced_file = star_import.referenced_file(db); - - let requires_explicit_reexport = match dunder_all_names(db, referenced_file) { + let program_file = star_import.referenced_file(db); + let requires_explicit_reexport = match dunder_all_names(db, program_file) { Some(all_names) => { if all_names.contains(symbol.name()) { Some(RequiresExplicitReExport::No) @@ -1573,7 +1593,7 @@ fn analyze_single(db: &dyn Db, predicate: &Predicate) -> Truthiness { tracing::trace!( "Symbol `{}` (via star import) not found in `__all__` of `{}`", symbol.name(), - referenced_file.path(db) + program_file.file(db).path(db) ); return Truthiness::AlwaysFalse; } @@ -1583,7 +1603,8 @@ fn analyze_single(db: &dyn Db, predicate: &Predicate) -> Truthiness { match imported_symbol( db, - Some(referenced_file), + env, + Some(program_file), symbol.name(), requires_explicit_reexport, ) @@ -1606,7 +1627,7 @@ fn analyze_single(db: &dyn Db, predicate: &Predicate) -> Truthiness { /// Check whether a diagnostic emitted at `range` is in reachable code, considering both /// scope reachability and statement-level reachability within the scope. pub(crate) fn is_range_reachable<'db>( - db: &'db dyn crate::Db, + db: &'db dyn Db, index: &SemanticIndex<'db>, scope_id: FileScopeId, range: TextRange, @@ -1690,7 +1711,7 @@ impl<'db> ReachabilityEvaluationCache<'db> { /// predicate determines whether the constraint belongs to the primary scope. A primary-scope /// constraint from the primary graph is cached by dense index; all other constraints are cached /// by graph identity and id. - pub(crate) fn evaluate( + fn evaluate( &self, db: &'db dyn Db, constraints: &ReachabilityConstraints, @@ -1817,10 +1838,103 @@ mod tests { use crate::db::tests::setup_db; use ruff_db::files::system_path_to_file; use ruff_db::system::DbWithWritableSystem as _; + use ty_python_core::ProgramFile; use ty_python_core::narrowing_constraints::InteriorNode; use ty_python_core::predicate::Predicates; use ty_python_core::semantic_index; + #[test] + fn non_terminal_call_range_recovers_cross_file_cycle() -> anyhow::Result<()> { + let mut db = setup_db(); + let calls = " other.target.ping()\n".repeat(NON_TERMINAL_CALL_CHUNK_SIZE + 1); + let a = format!( + r#"from b import B + +class A: + def setup(self, other: B) -> None: +{calls} self.target = TargetA() + +class TargetA: + def ping(self) -> None: ... +"# + ); + let b = format!( + r#"from a import A + +class B: + def setup(self, other: A) -> None: +{calls} self.target = TargetB() + +class TargetB: + def ping(self) -> None: ... +"# + ); + db.write_files([("/src/a.py", a.as_str()), ("/src/b.py", b.as_str())])?; + + let file = system_path_to_file(&db, "/src/a.py").unwrap(); + let program_file = ProgramFile::new(&db, file, db.program_environment().program(&db)); + let index = semantic_index(&db, program_file); + let class_scope = index + .child_scopes(FileScopeId::global()) + .find(|(_, scope)| scope.node().as_class().is_some()) + .unwrap() + .0; + let setup_scope = index + .child_scopes(class_scope) + .find(|(_, scope)| scope.node().as_function().is_some()) + .unwrap() + .0 + .to_scope_id(&db, program_file); + + // Enter the range directly so it becomes the cycle head when inferring `other.target` + // reaches the other module and then re-enters this scope. + analyze_non_terminal_call_range(&db, setup_scope, 0, 0); + Ok(()) + } + + #[test] + fn non_terminal_call_range_invalidates_when_callable_changes() -> anyhow::Result<()> { + let mut db = setup_db(); + let source = format!( + "from dependency import callback\n\ndef f() -> None:\n{}", + " callback()\n".repeat(NON_TERMINAL_CALL_CHUNK_SIZE + 1) + ); + db.write_files([ + ("/src/dependency.py", "def callback() -> None: ..."), + ("/src/test.py", source.as_str()), + ])?; + + let file = system_path_to_file(&db, "/src/test.py").unwrap(); + let function_scope = { + let program_file = ProgramFile::new(&db, file, db.program_environment().program(&db)); + let index = semantic_index(&db, program_file); + index.child_scopes(FileScopeId::global()).next().unwrap().0 + }; + { + let program_file = ProgramFile::new(&db, file, db.program_environment().program(&db)); + let scope = function_scope.to_scope_id(&db, program_file); + let use_def = use_def_map(&db, scope); + assert!( + evaluate_reachability_constraint(&db, scope, use_def.end_of_scope_reachability(),) + .may_be_true() + ); + } + + db.write_file( + "/src/dependency.py", + "from typing import NoReturn\ndef callback() -> NoReturn: ...", + )?; + + let program_file = ProgramFile::new(&db, file, db.program_environment().program(&db)); + let scope = function_scope.to_scope_id(&db, program_file); + let use_def = use_def_map(&db, scope); + assert!( + evaluate_reachability_constraint(&db, scope, use_def.end_of_scope_reachability(),) + .is_always_false() + ); + Ok(()) + } + #[test] fn deep_constraint_projection_does_not_overflow() -> anyhow::Result<()> { const DEPTH: usize = 100_000; @@ -1840,7 +1954,9 @@ mod tests { )?; let file = system_path_to_file(&db, "/src/test.py").unwrap(); - let index = semantic_index(&db, file); + let program_file = + ProgramFile::new(&db, file, db.program_environment().program(&db)); + let index = semantic_index(&db, program_file); let function_scope = index.child_scopes(FileScopeId::global()).next().unwrap().0; let use_def = index.use_def_map(function_scope); let predicate = use_def @@ -1866,8 +1982,10 @@ mod tests { .collect(); let constraints = NarrowingConstraints::from_test_nodes(nodes); let x = index.place_table(function_scope).symbol_id("x").unwrap(); + let env = db.program_environment(); let mut projector = NarrowingProjector::new( &db, + &env, &constraints, &predicates, ScopedPlaceId::Symbol(x), diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index 87bcb9131b..a851d512ac 100644 --- a/crates/ty_python_semantic/src/semantic_model.rs +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -1,4 +1,5 @@ use compact_str::CompactString; +use ruff_db::PythonFile; use ruff_db::files::{File, FilePath}; use ruff_db::parsed::{parsed_module, parsed_string_annotation}; use ruff_db::source::{line_index, source_text}; @@ -10,22 +11,23 @@ use ruff_source_file::LineIndex; use ruff_text_size::{Ranged, TextRange}; use rustc_hash::{FxHashMap, FxHashSet}; use ty_module_resolver::{ - KnownModule, Module, ModuleName, list_modules, resolve_module, resolve_real_shadowable_module, + ImportingFile, KnownModule, Module, ModuleName, list_modules, resolve_module, }; use crate::Db; use crate::place::implicit_globals::all_implicit_module_globals; use crate::types::ide_support::{ImportAliasResolution, definition_for_name}; -use crate::types::list_members::{Member, all_members, all_reachable_members}; +use crate::types::list_members::{all_members, all_reachable_members}; use crate::types::{ - CycleDetector, SpecialFormType, Type, TypeQualifiers, binding_type, infer_complete_scope_types, - inferred_declaration, + CycleDetector, ProgramEnvironment, SpecialFormType, Type, TypeQualifiers, binding_type, + infer_complete_scope_types, inferred_declaration, }; use ty_python_core::definition::{Definition, DefinitionKind}; use ty_python_core::place_table; use ty_python_core::scope::{FileScopeId, Scope}; use ty_python_core::semantic_index; use ty_python_core::symbol::Symbol; +use ty_python_core::{Program, ProgramFile}; /// The primary interface the LSP should use for querying semantic information about a [`File`]. /// @@ -40,14 +42,14 @@ use ty_python_core::symbol::Symbol; /// methods will automatically handle using the string literal's AST node when necessary. pub struct SemanticModel<'db> { db: &'db dyn Db, - file: File, + file: ProgramFile<'db>, /// If `Some` then this `SemanticModel` is for analyzing the sub-AST of a string annotation. /// This expression will be used as a witness to the scope/location we're analyzing. in_string_annotation_expr: Option>, } impl<'db> SemanticModel<'db> { - pub fn new(db: &'db dyn Db, file: File) -> Self { + pub fn new(db: &'db dyn Db, file: ProgramFile<'db>) -> Self { Self { db, file, @@ -60,11 +62,27 @@ impl<'db> SemanticModel<'db> { } pub fn file(&self) -> File { + self.file.file(self.db) + } + + pub fn python_file(&self) -> PythonFile<'db> { + self.file.python_file(self.db) + } + + pub fn program_file(&self) -> ProgramFile<'db> { self.file } + pub fn program(&self) -> Program<'db> { + self.file.program(self.db) + } + + pub fn program_environment(&self) -> ProgramEnvironment<'db> { + ProgramEnvironment::from_file(self.program_file()) + } + pub fn file_path(&self) -> &FilePath { - self.file.path(self.db) + self.file().path(self.db) } /// basedpython: the source text of the specialization step the transpiler @@ -73,6 +91,7 @@ impl<'db> SemanticModel<'db> { /// no injectable spelling exists — the checker reports the latter as /// `unspecialized-reified-generic` pub fn reified_call_specialization(&self, call: &ast::ExprCall) -> Option { + let env = &self.program_environment(); let db = self.db; let callee_ty = call.func.inferred_type(self)?; let function = match callee_ty { @@ -96,7 +115,13 @@ impl<'db> SemanticModel<'db> { keywords.push((name.as_str(), keyword.value.inferred_type(self)?)); } crate::types::reified_infer::injectable_call_specialization( - db, self.file, callee_ty, function, positional, keywords, + db, + env, + self.file(), + callee_ty, + function, + positional, + keywords, ) } @@ -113,6 +138,7 @@ impl<'db> SemanticModel<'db> { class_def: &ast::StmtClassDef, ) -> Vec { use crate::types::conformance; + let env = &self.program_environment(); let db = self.db; let Some(crate::types::ClassLiteral::Static(extension)) = class_def @@ -132,12 +158,17 @@ impl<'db> SemanticModel<'db> { else { continue; }; - let Ok((spelling, import)) = - crate::types::conversions::class_reference(db, self.file, self, anchor, interface) - else { + let Ok((spelling, import)) = crate::types::conversions::class_reference( + db, + env, + self.file.file(db), + self, + anchor, + interface, + ) else { continue; }; - let table = conformance::witness_table(db, self.file, extension, interface); + let table = conformance::witness_table(db, self.file.file(db), extension, interface); let mut imports = Vec::new(); let mut entries = Vec::new(); for entry in table { @@ -163,7 +194,8 @@ impl<'db> SemanticModel<'db> { /// registration ran, so deferring that module's execution defers the /// conformance out of existence pub fn eagerly_imported_modules(&self) -> Vec { - crate::types::conformance::eagerly_imported_modules(self.db, self.file) + let db = self.db; + crate::types::conformance::eagerly_imported_modules(self.db, self.file.file(db)) } /// basedpython: when an attribute access reads a *requirement* off an @@ -178,9 +210,10 @@ impl<'db> SemanticModel<'db> { attribute: &ast::ExprAttribute, ) -> Option { use crate::types::conformance; + let env = self.program_environment(); let db = self.db; - if !self.file.source_type(db).is_basedpython() { + if !self.file.file(db).source_type(db).is_basedpython() { return None; } let receiver_ty = attribute.value.inferred_type(self)?; @@ -192,18 +225,19 @@ impl<'db> SemanticModel<'db> { let receiver_ty = if attribute.optional || crate::types::receivers::spine_has_optional(&attribute.value) { - crate::types::receivers::strip_none(db, receiver_ty) + crate::types::receivers::strip_none(db, &env, receiver_ty) } else { receiver_ty }; - let interface = receiver_ty.erase_restriction(db).nominal_class(db)?; + let interface = receiver_ty.erase_restriction(db).nominal_class(db, &env)?; let member = attribute.attr.as_str(); if !conformance::requires_witness_dispatch(db, interface, member) { return None; } let (spelling, import) = crate::types::conversions::class_reference( db, - self.file, + &env, + self.file.file(db), self, &attribute.value, interface, @@ -212,8 +246,8 @@ impl<'db> SemanticModel<'db> { // how the receiver reaches the witness: a `class def` takes the class, // a data member is read, anything else is fetched and called by the // parentheses that already follow the access - let member_ty = Type::instance(db, interface) - .member(db, member) + let member_ty = Type::instance(db, &env, interface) + .member(db, &env, member) .place .ignore_possibly_undefined(); let kind = match member_ty { @@ -246,11 +280,11 @@ impl<'db> SemanticModel<'db> { use crate::types::conformance; let db = self.db; - if !self.file.source_type(db).is_basedpython() { + if !self.file.file(db).source_type(db).is_basedpython() { return None; } let interface = target.inferred_type(self)?.to_class_type(db)?; - if !conformance::visible_conformances(db, self.file) + if !conformance::visible_conformances(db, self.file.file(db)) .iter() .any(|(_, declared)| declared.class_literal(db) == interface.class_literal(db)) { @@ -274,10 +308,11 @@ impl<'db> SemanticModel<'db> { &self, attribute: &ast::ExprAttribute, ) -> Option { + let env = self.program_environment(); let db = self.db; let receiver_ty = attribute.value.inferred_type(self)?; if !receiver_ty - .member(db, attribute.attr.as_str()) + .member(db, &env, attribute.attr.as_str()) .place .is_undefined() { @@ -285,7 +320,8 @@ impl<'db> SemanticModel<'db> { } let resolution = crate::types::extensions::resolve_extension_member( db, - self.file, + &env, + self.file(), receiver_ty, attribute.attr.as_str(), )?; @@ -293,7 +329,7 @@ impl<'db> SemanticModel<'db> { // `A()` is a `final A`, still a receiver a `class def` has to widen let receiver_is_class = receiver_ty .erase_restriction(db) - .nominal_class(db) + .nominal_class(db, &env) .is_none(); self.extension_rewrite(&resolution, attribute.attr.as_str(), receiver_is_class) } @@ -311,18 +347,28 @@ impl<'db> SemanticModel<'db> { &self, attribute: &ast::ExprAttribute, ) -> Option { + let env = self.program_environment(); let db = self.db; let name = attribute.attr.as_str(); if !crate::types::conversions::CONVERSION_DUNDERS.contains(&name) { return None; } let receiver_ty = attribute.value.inferred_type(self)?; - if !receiver_ty.member(db, name).place.is_undefined() { + if !receiver_ty.member(db, &env, name).place.is_undefined() { return None; } - let resolution = - crate::types::extensions::resolve_extension_member(db, self.file, receiver_ty, name)?; - if !crate::types::extensions::is_prelude_extension(db, self.file, resolution.extension) { + let resolution = crate::types::extensions::resolve_extension_member( + db, + &env, + self.file(), + receiver_ty, + name, + )?; + if !crate::types::extensions::is_prelude_extension( + db, + self.file.file(db), + resolution.extension, + ) { return None; } // a use-site modifier does not turn an instance into a class object: @@ -330,7 +376,7 @@ impl<'db> SemanticModel<'db> { Some( if receiver_ty .erase_restriction(db) - .nominal_class(db) + .nominal_class(db, &env) .is_none() { PreludeDunderReceiver::Class @@ -345,19 +391,29 @@ impl<'db> SemanticModel<'db> { /// for every operator the operand's own type supports, which is all of /// them outside a file that declares such an extension pub fn extension_operator_info(&self, expr: &Expr) -> Option { + let env = &self.program_environment(); let db = self.db; let operator = match expr { Expr::UnaryOp(unary) => { let operand = unary.operand.inferred_type(self)?; crate::types::extensions::unary_extension_operator( - db, self.file, unary.op, operand, + db, + env, + self.file(), + unary.op, + operand, )? } Expr::BinOp(binary) => { let left = binary.left.inferred_type(self)?; let right = binary.right.inferred_type(self)?; crate::types::extensions::binary_extension_operator( - db, self.file, left, binary.op, right, + db, + env, + self.file(), + left, + binary.op, + right, )? } // only a single comparison lowers: a chain (`a < b < c`) is two @@ -372,7 +428,12 @@ impl<'db> SemanticModel<'db> { let left = compare.left.inferred_type(self)?; let right = right_expr.inferred_type(self)?; crate::types::extensions::comparison_extension_operator( - db, self.file, left, *op, right, + db, + env, + self.file(), + left, + *op, + right, )? } _ => return None, @@ -394,15 +455,17 @@ impl<'db> SemanticModel<'db> { receiver_is_class: bool, ) -> Option { let db = self.db; - if crate::types::extensions::is_prelude_extension(db, self.file, resolution.extension) - || resolution.ambiguous_with.is_some_and(|other| { - crate::types::extensions::is_prelude_extension(db, self.file, other) - }) - { + if crate::types::extensions::is_prelude_extension( + db, + self.file.file(db), + resolution.extension, + ) || resolution.ambiguous_with.is_some_and(|other| { + crate::types::extensions::is_prelude_extension(db, self.file.file(db), other) + }) { return None; } let extension_file = resolution.extension.file(db); - let import_from = if extension_file == self.file { + let import_from = if extension_file == self.file.file(db) { None } else { // spelled the way this file already imports the module: ty's absolute @@ -411,7 +474,7 @@ impl<'db> SemanticModel<'db> { // no absolute spelling at all Some(crate::types::conversions::imported_module_spelling( db, - self.file, + self.file.file(db), extension_file, )?) }; @@ -452,8 +515,9 @@ impl<'db> SemanticModel<'db> { ruff_text_size::TextRange, crate::types::conversions::ConversionInfo, )> { + let env = &self.program_environment(); let db = self.db; - if !self.file.source_type(db).is_basedpython() { + if !self.file.file(db).source_type(db).is_basedpython() { return Vec::new(); } let Some(callable_ty) = call.func.inferred_type(self) else { @@ -479,7 +543,8 @@ impl<'db> SemanticModel<'db> { }; let Some(repair) = crate::types::conversions::repair_conversion( db, - self.file, + env, + self.file.file(db), argument_type, parameter_type, Some(value), @@ -488,7 +553,14 @@ impl<'db> SemanticModel<'db> { }; conversions.push(( value.range(), - crate::types::conversions::conversion_info(db, self.file, self, value, &repair), + crate::types::conversions::conversion_info( + db, + env, + self.file.file(db), + self, + value, + &repair, + ), )); } conversions @@ -504,13 +576,16 @@ impl<'db> SemanticModel<'db> { /// falls through to the full check, so the gate can only save work — it can /// never change an answer. fn call_may_convert(&self, call: &ast::ExprCall, callable_ty: Type<'db>) -> bool { + let env = &self.program_environment(); let db = self.db; - if !crate::types::conformance::visible_conformances(db, self.file).is_empty() { + if !crate::types::conformance::visible_conformances(db, self.file.file(db)).is_empty() { return true; } for argument in call.arguments.iter_source_order() { match argument.value().inferred_type(self) { - Some(ty) if crate::types::conversions::may_convert(db, self.file, ty) => { + Some(ty) + if crate::types::conversions::may_convert(db, env, self.file.file(db), ty) => + { return true; } // an argument whose type is unknown here could be anything @@ -527,7 +602,12 @@ impl<'db> SemanticModel<'db> { }; signature.iter().any(|overload| { overload.parameters().iter().any(|parameter| { - crate::types::conversions::may_convert(db, self.file, parameter.annotated_type()) + crate::types::conversions::may_convert( + db, + env, + self.file.file(db), + parameter.annotated_type(), + ) }) }) } @@ -547,27 +627,39 @@ impl<'db> SemanticModel<'db> { ruff_text_size::TextRange, crate::types::conversions::ConversionInfo, )> { + let env = &self.program_environment(); let db = self.db; - if !self.file.source_type(db).is_basedpython() { + if !self.file.file(db).source_type(db).is_basedpython() { return Vec::new(); } let Some((value, declared)) = self.conversion_site_of(stmt) else { return Vec::new(); }; - crate::types::conversions::value_conversions(db, self.file, self, value, declared) - .into_iter() - .map(|(range, repair)| { - // the anchor is the value being wrapped, which decides what the - // emitted names have to resolve to — for an element-wise - // conversion that is the element, not the whole literal - let anchor = - crate::types::conversions::expression_at(value, range).unwrap_or(value); - let info = crate::types::conversions::conversion_info( - db, self.file, self, anchor, &repair, - ); - (range, info) - }) - .collect() + crate::types::conversions::value_conversions( + db, + env, + self.file.file(db), + self, + value, + declared, + ) + .into_iter() + .map(|(range, repair)| { + // the anchor is the value being wrapped, which decides what the + // emitted names have to resolve to — for an element-wise + // conversion that is the element, not the whole literal + let anchor = crate::types::conversions::expression_at(value, range).unwrap_or(value); + let info = crate::types::conversions::conversion_info( + db, + env, + self.file.file(db), + self, + anchor, + &repair, + ); + (range, info) + }) + .collect() } /// the value expression and the type it is checked against, for a statement @@ -576,6 +668,7 @@ impl<'db> SemanticModel<'db> { &self, stmt: &'ast ast::Stmt, ) -> Option<(&'ast ast::Expr, Type<'db>)> { + let env = self.program_environment(); let db = self.db; match stmt { ast::Stmt::AnnAssign(assignment) => { @@ -594,7 +687,7 @@ impl<'db> SemanticModel<'db> { [ast::Expr::Attribute(attribute)] => { let object_ty = attribute.value.inferred_type(self)?; let declared = object_ty - .member(db, attribute.attr.as_str()) + .member(db, &env, attribute.attr.as_str()) .place .ignore_possibly_undefined()?; Some((&assignment.value, declared)) @@ -607,10 +700,11 @@ impl<'db> SemanticModel<'db> { let declarations = index .use_def_map(binding.file_scope(db)) .declarations_at_binding(binding); - let declared = crate::place::place_from_declarations(db, declarations) - .ignore_conflicting_declarations() - .place - .ignore_possibly_undefined()?; + let declared = + crate::place::place_from_declarations(db, &env, declarations) + .ignore_conflicting_declarations() + .place + .ignore_possibly_undefined()?; Some((&assignment.value, declared)) } _ => None, @@ -633,14 +727,14 @@ impl<'db> SemanticModel<'db> { let index = semantic_index(db, self.file); let file_scope = self.scope(node)?; let function_ref = index.scope(file_scope).node().as_function()?; - let module = parsed_module(db, self.file).load(db); + let module = parsed_module(db, self.file.python_file(db)).load(db); let function = function_ref.node(&module); // a generator's declared type describes the generator, not the returned // value; the checker checks those against the yield type instead if function.is_async || file_scope.is_generator_function(index) { return None; } - crate::types::conversions::function_declared_return_type(db, self.file, function) + crate::types::conversions::function_declared_return_type(db, self.file.file(db), function) } /// basedpython: whether subscripting `value` is a runtime `__getitem__` @@ -648,9 +742,10 @@ impl<'db> SemanticModel<'db> { /// subscript on it lowers to. `false` for a value the checker could not /// resolve — the specialization reading is the one it also checks pub fn subscript_is_getitem_call(&self, value: &Expr) -> bool { + let env = &self.program_environment(); value .inferred_type(self) - .is_some_and(|ty| crate::types::subscript::is_runtime_subscript(self.db, ty)) + .is_some_and(|ty| crate::types::subscript::is_runtime_subscript(self.db, env, ty)) } /// basedpython: whether an attribute access resolves through an *implicit @@ -659,6 +754,7 @@ impl<'db> SemanticModel<'db> { /// `fn(x)`. Like extensions, a receiver callable never shadows a declared /// member, and an extension member wins over it pub fn implicit_receiver_attribute(&self, attribute: &ast::ExprAttribute) -> bool { + let env = &self.program_environment(); let db = self.db; let Some(receiver_ty) = attribute.value.inferred_type(self) else { return false; @@ -668,7 +764,8 @@ impl<'db> SemanticModel<'db> { }; crate::types::receivers::is_implicit_receiver_attribute( db, - self.file, + env, + self.file(), scope.to_scope_id(db, self.file), attribute, receiver_ty, @@ -683,10 +780,12 @@ impl<'db> SemanticModel<'db> { &self, name: &ast::ExprName, ) -> Option { + let env = &self.program_environment(); let scope = self.scope(ast::AnyNodeRef::from(name))?; let resolved = crate::types::receivers::implicit_receiver_name( self.db, - self.file, + env, + self.file(), scope.to_scope_id(self.db, self.file), name.id.as_str(), )?; @@ -713,6 +812,7 @@ impl<'db> SemanticModel<'db> { /// every other call, and for any argument the checker did not read as a /// lookup, which the transpiler must then leave exactly as written pub fn django_lookup_arguments(&self, call: &ast::ExprCall) -> Vec { + let env = &self.program_environment(); let db = self.db; let Some(callee) = call.func.inferred_type(self) else { return Vec::new(); @@ -722,7 +822,8 @@ impl<'db> SemanticModel<'db> { }; crate::types::dedicated::django::lookup_call_lowering( db, - self.file, + env, + self.file(), scope.to_scope_id(db, self.file), callee, call, @@ -743,7 +844,8 @@ impl<'db> SemanticModel<'db> { let scope = self.scope(ast::AnyNodeRef::from(name))?; crate::types::context_sensitive::qualifier_for_unbound_name( self.db, - self.file, + &self.program_environment(), + self.file.file(self.db), scope.to_scope_id(self.db, self.file), name.id.as_str(), || name.inferred_type(self), @@ -758,12 +860,14 @@ impl<'db> SemanticModel<'db> { /// spelling exists — reification of constructors is best-effort, never /// an error pub fn reified_constructor_type_arguments(&self, call: &ast::ExprCall) -> Option { + let env = &self.program_environment(); let callee_ty = call.func.inferred_type(self)?; let class_literal = callee_ty.as_class_literal()?; let constructed = call.inferred_type(self)?; crate::types::reified_infer::constructor_specialization_display( self.db, - self.file, + env, + self.file(), class_literal, constructed, ) @@ -809,11 +913,20 @@ impl<'db> SemanticModel<'db> { lhs: &ast::Expr, rhs: &ast::Expr, ) -> Option { - let alias = - crate::types::reified_infer::parametric_is_target(self.db, rhs.inferred_type(self)?)?; + let env = &self.program_environment(); + let alias = crate::types::reified_infer::parametric_is_target( + self.db, + env, + rhs.inferred_type(self)?, + )?; let lhs_ty = lhs.inferred_type(self)?; Some(crate::types::reified_infer::classify_parametric_is( - self.db, self.file, lhs_ty, alias, rhs, + self.db, + env, + self.file(), + lhs_ty, + alias, + rhs, )) } @@ -826,13 +939,20 @@ impl<'db> SemanticModel<'db> { value: &ast::Expr, target: &ast::Expr, ) -> Option { + let env = &self.program_environment(); let alias = crate::types::reified_infer::parametric_cast_target( self.db, + env, target.inferred_type(self)?, )?; let value_ty = value.inferred_type(self)?; Some(crate::types::reified_infer::classify_parametric_is( - self.db, self.file, value_ty, alias, target, + self.db, + env, + self.file(), + value_ty, + alias, + target, )) } @@ -845,15 +965,17 @@ impl<'db> SemanticModel<'db> { &self, annotation: &ast::Expr, ) -> Option { + let env = &self.program_environment(); crate::types::reified_infer::erased_union( self.db, - self.file, + env, + self.file(), annotation.inferred_type(self)?, ) } pub fn line_index(&self) -> LineIndex { - line_index(self.db, self.file) + line_index(self.db, self.file()) } /// Returns a map from symbol name to that symbol's @@ -865,12 +987,13 @@ impl<'db> SemanticModel<'db> { &self, node: ast::AnyNodeRef<'_>, ) -> FxHashMap> { + let db = self.db; let mut members = FxHashMap::default(); - let index = semantic_index(self.db, self.file); + let program_file = self.program_file(); + let index = semantic_index(self.db, program_file); let Some(file_scope) = self.scope(node) else { return members; }; - for (file_scope, _) in index .visible_ancestor_scopes(file_scope) .collect::>() @@ -878,7 +1001,7 @@ impl<'db> SemanticModel<'db> { .rev() { for memberdef in - all_reachable_members(self.db, file_scope.to_scope_id(self.db, self.file)) + all_reachable_members(db, file_scope.to_scope_id(self.db, program_file)) { members.insert( memberdef.member.name, @@ -895,34 +1018,34 @@ impl<'db> SemanticModel<'db> { /// Resolve the given import made in this file to a Type pub fn resolve_module_type(&self, module: Option<&str>, level: u32) -> Option> { let module = self.resolve_module(module, level)?; - Some(Type::module_literal(self.db, self.file, module)) + Some(Type::module_literal(self.db, self.program_file(), module)) } /// Resolve the given import made in this file to a Module pub fn resolve_module(&self, module: Option<&str>, level: u32) -> Option> { + let importing_file = ImportingFile::File( + self.file(), + self.program_environment().resolver_environment(self.db), + ); let module_name = - ModuleName::from_identifier_parts(self.db, self.file, module, level).ok()?; - resolve_module(self.db, self.file, &module_name) + ModuleName::from_identifier_parts(self.db, importing_file, module, level).ok()?; + resolve_module(self.db, importing_file, &module_name) } /// Returns completions for symbols available in a `import ` context. pub fn import_completions(&self) -> Vec> { - let typing_extensions = ModuleName::new_static("typing_extensions").unwrap(); - let is_typing_extensions_available = self.file.is_stub(self.db) - || resolve_real_shadowable_module(self.db, self.file, &typing_extensions).is_some(); - list_modules(self.db) + let resolver_environment = self.program_environment().resolver_environment(self.db); + list_modules(self.db, resolver_environment) .iter() .copied() - .filter(|module| { - is_typing_extensions_available || module.name(self.db) != &typing_extensions - }) .map(|module| { let builtin = module.is_known(self.db, KnownModule::Builtins); - let ty = Type::module_literal(self.db, self.file, module); + let ty = Type::module_literal(self.db, self.program_file(), module); Completion { name: CompactString::new(module.name(self.db).as_str()), ty: Some(ty), builtin, + is_type_check_only: false, } }) .collect() @@ -930,7 +1053,14 @@ impl<'db> SemanticModel<'db> { /// Returns completions for symbols available in a `from module import ` context. pub fn from_import_completions(&self, import: &ast::StmtImportFrom) -> Vec> { - let module_name = match ModuleName::from_import_statement(self.db, self.file, import) { + let module_name = match ModuleName::from_import_statement( + self.db, + ImportingFile::File( + self.file(), + self.program_environment().resolver_environment(self.db), + ), + import, + ) { Ok(module_name) => module_name, Err(err) => { tracing::debug!( @@ -949,7 +1079,14 @@ impl<'db> SemanticModel<'db> { &self, module_name: &ModuleName, ) -> Vec> { - let Some(module) = resolve_module(self.db, self.file, module_name) else { + let Some(module) = resolve_module( + self.db, + ImportingFile::File( + self.file(), + self.program_environment().resolver_environment(self.db), + ), + module_name, + ) else { tracing::debug!("Could not resolve module from `{module_name:?}`"); return vec![]; }; @@ -959,11 +1096,19 @@ impl<'db> SemanticModel<'db> { /// 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, self.file, module_name) else { + let db = self.db; + let Some(module) = resolve_module( + self.db, + ImportingFile::File( + self.file(), + self.program_environment().resolver_environment(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 ty = Type::module_literal(self.db, self.program_file(), module); let builtin = module.is_known(self.db, KnownModule::Builtins); let private = self.foreign_private_symbols(ty); @@ -972,14 +1117,15 @@ impl<'db> SemanticModel<'db> { clippy::iter_over_hash_type, reason = "completion order is determined later by relevance ranking" )] - for Member { name, ty } in all_members(self.db, ty) { - if private.is_some_and(|names| names.contains(&name)) { + for member in all_members(db, &self.program_environment(), ty) { + if private.is_some_and(|names| names.contains(&member.name)) { continue; } completions.push(Completion { - name: CompactString::new(name), - ty: Some(ty), + name: CompactString::new(member.name), + ty: Some(member.ty), builtin, + is_type_check_only: member.is_type_check_only, }); } completions.extend(self.submodule_completions(&module)); @@ -992,12 +1138,13 @@ impl<'db> SemanticModel<'db> { let mut completions = vec![]; for submodule in module.all_submodules(self.db) { - let ty = Type::module_literal(self.db, self.file, *submodule); + let ty = Type::module_literal(self.db, self.program_file(), *submodule); let base = submodule.name(self.db).last_component(); completions.push(Completion { name: CompactString::new(base), ty: Some(ty), builtin, + is_type_check_only: false, }); } completions @@ -1007,11 +1154,12 @@ impl<'db> SemanticModel<'db> { /// module. they are the module's implementation, not its interface, so an /// IDE must not offer them here. fn foreign_private_symbols(&self, ty: Type<'db>) -> Option<&'db FxHashSet> { + let db = self.db; let Type::ModuleLiteral(module) = ty else { return None; }; let file = module.module(self.db).file(self.db)?; - if file == self.file { + if file == self.file.file(db) { return None; } Some(crate::types::visibility::private_symbols(self.db, file)) @@ -1019,18 +1167,20 @@ impl<'db> SemanticModel<'db> { /// Returns completions for symbols available in a `object.` context. pub fn attribute_completions(&self, node: &ast::ExprAttribute) -> Vec> { + let db = self.db; let Some(ty) = node.value.inferred_type(self) else { return Vec::new(); }; let private = self.foreign_private_symbols(ty); - all_members(self.db, ty) + all_members(db, &self.program_environment(), ty) .into_iter() .filter(|member| !private.is_some_and(|names| names.contains(&member.name))) .map(|member| Completion { name: CompactString::new(member.name), ty: Some(member.ty), builtin: false, + is_type_check_only: member.is_type_check_only, }) .collect() } @@ -1041,18 +1191,21 @@ impl<'db> SemanticModel<'db> { /// 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); + let db = self.db; + let program_file = self.program_file(); + let index = semantic_index(self.db, program_file); let Some(file_scope) = self.scope(node) else { return vec![]; }; let mut completions = vec![]; for (file_scope, _) in index.ancestor_scopes(file_scope) { completions.extend( - all_reachable_members(self.db, file_scope.to_scope_id(self.db, self.file)).map( + all_reachable_members(db, file_scope.to_scope_id(self.db, program_file)).map( |memberdef| Completion { name: CompactString::new(memberdef.member.name), ty: Some(memberdef.member.ty), builtin: false, + is_type_check_only: memberdef.member.is_type_check_only, }, ), ); @@ -1067,12 +1220,33 @@ impl<'db> SemanticModel<'db> { name: CompactString::new(name), ty: Some(ty), builtin: true, + is_type_check_only: false, }), ); + // Project-level builtins take precedence over the standard builtins. + let project_builtins = ModuleName::new_static("__builtins__").unwrap(); + let importing_file = + ImportingFile::File(self.file(), self.file.resolver_environment(self.db)); + if resolve_module(self.db, importing_file, &project_builtins).is_some() { + completions.extend( + self.module_completions(&project_builtins) + .into_iter() + .filter(|completion| !completion.is_type_check_only) + .map(|mut completion| { + completion.builtin = true; + completion + }), + ); + } + // Builtins are available in all scopes. - let builtins = ModuleName::new_static("builtins").expect("valid module name"); - completions.extend(self.module_completions(&builtins)); + let builtins = KnownModule::Builtins.name(); + completions.extend( + self.module_completions(&builtins) + .into_iter() + .filter(|completion| !completion.is_type_check_only), + ); // The above can sometimes result in duplicates. Get rid of them. completions.sort_by(|c1, c2| c1.name.cmp(&c2.name)); @@ -1084,7 +1258,7 @@ impl<'db> SemanticModel<'db> { /// Returns `true` if the given class definition's name was previously /// bound in the same scope (i.e., the class definition is a re-assignment). pub fn is_class_name_reassigned(&self, class_def: &ast::StmtClassDef) -> bool { - let index = semantic_index(self.db, self.file); + let index = semantic_index(self.db, self.program_file()); let definition = index.expect_single_definition(class_def); let scope = definition.scope(self.db); let table = place_table(self.db, scope); @@ -1094,7 +1268,7 @@ impl<'db> SemanticModel<'db> { /// Returns the scope in which `node` is defined (handles string annotations). pub fn scope(&self, node: ast::AnyNodeRef<'_>) -> Option { - let index = semantic_index(self.db, self.file); + let index = semantic_index(self.db, self.program_file()); match self.node_in_ast(node) { ast::AnyNodeRef::Identifier(identifier) => index.try_expression_scope_id(identifier), @@ -1148,7 +1322,7 @@ impl<'db> SemanticModel<'db> { &self, node: ast::AnyNodeRef<'_>, ) -> impl Iterator + '_ { - let index = semantic_index(self.db, self.file); + let index = semantic_index(self.db, self.program_file()); self.scope(node) .into_iter() .flat_map(move |scope| index.ancestor_scopes(scope)) @@ -1166,8 +1340,8 @@ impl<'db> SemanticModel<'db> { &self, covering_node: &CoveringNode<'_>, ) -> Option> { - let index = semantic_index(self.db, self.file); - let parsed = parsed_module(self.db, self.file).load(self.db); + let index = semantic_index(self.db, self.program_file()); + let parsed = parsed_module(self.db, self.python_file()).load(self.db); let target_range = covering_node.node().range(); for node in covering_node.ancestors() { @@ -1194,7 +1368,7 @@ impl<'db> SemanticModel<'db> { /// /// If we're analyzing a string annotation, it will return the string literal's node. /// Otherwise it will return the input. - pub fn node_in_ast<'a>(&'a self, node: ast::AnyNodeRef<'a>) -> ast::AnyNodeRef<'a> { + fn node_in_ast<'a>(&'a self, node: ast::AnyNodeRef<'a>) -> ast::AnyNodeRef<'a> { if let Some(string_annotation) = &self.in_string_annotation_expr { (&**string_annotation).into() } else { @@ -1206,7 +1380,7 @@ impl<'db> SemanticModel<'db> { /// /// If we're analyzing a string annotation, it will return the string literal's expression. /// Otherwise it will return the input. - pub fn expr_in_ast<'a>(&'a self, expr: &'a Expr) -> &'a Expr { + fn expr_in_ast<'a>(&'a self, expr: &'a Expr) -> &'a Expr { if let Some(string_annotation) = &self.in_string_annotation_expr { string_annotation } else { @@ -1218,7 +1392,7 @@ impl<'db> SemanticModel<'db> { /// /// If we're analyzing a string annotation, it will return the string literal's expression. /// Otherwise it will return the input. - pub fn expr_ref_in_ast<'a>(&'a self, expr: ExprRef<'a>) -> ExprRef<'a> { + fn expr_ref_in_ast<'a>(&'a self, expr: ExprRef<'a>) -> ExprRef<'a> { if let Some(string_annotation) = &self.in_string_annotation_expr { ExprRef::from(string_annotation) } else { @@ -1237,11 +1411,11 @@ impl<'db> SemanticModel<'db> { ) -> Option<(Parsed, Self)> { // Ask the inference engine whether this is actually a string annotation let expr = ExprRef::StringLiteral(string_expr); - let index = semantic_index(self.db, self.file); + let index = semantic_index(self.db, self.program_file()); // When looking up scopes, use the expr in the top-level AST // (we might be trying to enter a sub-sub-AST, so this isn't silly) let file_scope = index.expression_scope_id(&self.expr_ref_in_ast(expr)); - let scope = file_scope.to_scope_id(self.db, self.file); + let scope = file_scope.to_scope_id(self.db, self.program_file()); // When querying whether the expr is a string annotation, we do however use the actual expr // (the inference engine should record this information even for sub-nodes) if !infer_complete_scope_types(self.db, scope).is_string_annotation(expr) { @@ -1253,7 +1427,7 @@ impl<'db> SemanticModel<'db> { // The string_annotation will be used as the expr/node for any query that needs // to look up a node in the AST to prevent panics, because these sub-AST nodes // are not in the File's AST! - let source = source_text(self.db, self.file); + let source = source_text(self.db, self.file()); let string_literal = string_expr.as_single_part_string()?; let ast = parsed_string_annotation(source.as_str(), string_literal).ok()?; let model = Self { @@ -1281,8 +1455,8 @@ impl<'db> SemanticModel<'db> { match definition.kind(self.db) { DefinitionKind::TypeAlias(_) => true, DefinitionKind::AnnotatedAssignment(assignment) => { - let parsed = parsed_module(self.db, definition.file(self.db)); - let model = Self::new(self.db, definition.file(self.db)); + let parsed = parsed_module(self.db, definition.python_file(self.db)); + let model = Self::new(self.db, definition.program_file(self.db)); model.is_type_alias_annotation(assignment.annotation(&parsed.load(self.db))) } _ => false, @@ -1292,6 +1466,7 @@ impl<'db> SemanticModel<'db> { /// Returns the type qualifiers (e.g. `Final`, `ClassVar`) for a given expression, /// if the expression refers to a name or attribute with declared qualifiers. pub fn type_qualifiers(&self, expr: ExprRef<'_>) -> TypeQualifiers { + let db = self.db; match expr { ExprRef::Name(name) => { let Some(definition) = @@ -1300,7 +1475,7 @@ impl<'db> SemanticModel<'db> { return TypeQualifiers::empty(); }; let definition_file = definition.file(self.db); - let module = parsed_module(self.db, definition_file).load(self.db); + let module = parsed_module(self.db, definition.python_file(self.db)).load(self.db); if !definition .kind(self.db) .category(definition_file.is_stub(self.db), &module) @@ -1308,7 +1483,7 @@ impl<'db> SemanticModel<'db> { { return TypeQualifiers::empty(); } - let Some(declared) = inferred_declaration(self.db, definition).declared() else { + let Some(declared) = inferred_declaration(self.db(), definition).declared() else { return TypeQualifiers::empty(); }; declared.qualifiers() @@ -1319,7 +1494,8 @@ impl<'db> SemanticModel<'db> { }; value_ty .member_lookup_with_policy( - self.db, + db, + &self.program_environment(), &attr.attr.id, crate::types::MemberLookupPolicy::default(), ) @@ -1375,16 +1551,13 @@ impl<'db> SemanticModel<'db> { _ => Vec::new(), } } + let db = self.db; let Some(expected_ty) = self.string_literal_completion_expected_type(string_expr) else { return Vec::new(); }; - let mut candidates = collect( - self.db, - expected_ty, - &StringLiteralCandidatesVisitor::default(), - ); + let mut candidates = collect(db, expected_ty, &StringLiteralCandidatesVisitor::default()); candidates.sort_unstable_by(|left, right| left.value.cmp(&right.value)); candidates.dedup_by(|left, right| left.value == right.value); candidates @@ -1395,9 +1568,9 @@ impl<'db> SemanticModel<'db> { string_expr: &ast::ExprStringLiteral, ) -> Option> { let expr = ast::ExprRef::from(string_expr); - let index = semantic_index(self.db, self.file); + let index = semantic_index(self.db, self.program_file()); let file_scope = index.try_expression_scope_id(&self.expr_ref_in_ast(expr))?; - let scope = file_scope.to_scope_id(self.db, self.file); + let scope = file_scope.to_scope_id(self.db, self.program_file()); infer_complete_scope_types(self.db, scope).try_expected_type(expr) } @@ -1513,6 +1686,9 @@ pub struct Completion<'db> { /// use it mainly in tests so that we can write less /// noisy tests. pub builtin: bool, + /// Whether this symbol is known to exist only for type checking and should + /// be ranked below runtime values. + pub is_type_check_only: bool, } #[derive(Clone, Debug)] @@ -1537,7 +1713,7 @@ pub trait HasDefinition { fn definition<'db>(&self, model: &SemanticModel<'db>) -> Definition<'db>; } -pub trait HasOptionalDefinition { +pub(crate) trait HasOptionalDefinition { /// Returns the definition of `self`, if it has one. /// /// ## Panics @@ -1547,13 +1723,14 @@ pub trait HasOptionalDefinition { impl HasType for ast::ExprRef<'_> { fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Option> { - let index = semantic_index(model.db, model.file); + let file = model.program_file(); + let index = semantic_index(model.db, file); // TODO(#1637): semantic tokens is making this crash even with // `try_expr_ref_in_ast` guarding this, for now just use `try_expression_scope_id`. // The problematic input is `x: "float` (with a dangling quote). I imagine the issue // is we're too eagerly setting `is_string_annotation` in inference. let file_scope = index.try_expression_scope_id(&model.expr_ref_in_ast(*self))?; - let scope = file_scope.to_scope_id(model.db, model.file); + let scope = file_scope.to_scope_id(model.db, file); infer_complete_scope_types(model.db, scope).try_expression_type(*self) } @@ -1658,7 +1835,7 @@ macro_rules! impl_binding_has_ty_def { impl HasDefinition for $ty { #[inline] fn definition<'db>(&self, model: &SemanticModel<'db>) -> Definition<'db> { - let index = semantic_index(model.db, model.file); + let index = semantic_index(model.db, model.program_file()); index.expect_single_definition(self) } } @@ -1667,7 +1844,7 @@ macro_rules! impl_binding_has_ty_def { #[inline] fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Option> { let binding = HasDefinition::definition(self, model); - Some(binding_type(model.db, binding)) + Some(binding_type(model.db(), binding)) } } }; @@ -1707,8 +1884,11 @@ impl HasType for ast::Alias { if &self.name == "*" { return Some(Type::Never); } - let index = semantic_index(model.db, model.file); - Some(binding_type(model.db, index.expect_single_definition(self))) + let index = semantic_index(model.db, model.program_file()); + Some(binding_type( + model.db(), + index.expect_single_definition(self), + )) } } @@ -1716,7 +1896,7 @@ impl HasOptionalDefinition for ast::ExceptHandlerExceptHandler { fn optional_definition<'db>(&self, model: &SemanticModel<'db>) -> Option> { self.name.as_ref()?; - let index = semantic_index(model.db, model.file); + let index = semantic_index(model.db, model.program_file()); Some(index.expect_single_definition(self)) } } @@ -1724,7 +1904,7 @@ impl HasOptionalDefinition for ast::ExceptHandlerExceptHandler { impl HasType for ast::ExceptHandlerExceptHandler { fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Option> { let definition = self.optional_definition(model)?; - Some(binding_type(model.db, definition)) + Some(binding_type(model.db(), definition)) } } @@ -1734,6 +1914,7 @@ mod tests { use crate::{HasType, SemanticModel}; use ruff_db::files::system_path_to_file; use ruff_db::parsed::parsed_module; + use ty_python_core::ProgramFile; #[test] fn function_type() -> anyhow::Result<()> { @@ -1743,7 +1924,8 @@ mod tests { let foo = system_path_to_file(&db, "/src/foo.py").unwrap(); - let ast = parsed_module(&db, foo).load(&db); + let foo = ProgramFile::new(&db, foo, db.program_environment().program(&db)); + let ast = parsed_module(&db, foo.python_file(&db)).load(&db); let function = ast.suite()[0].as_function_def_stmt().unwrap(); let model = SemanticModel::new(&db, foo); @@ -1762,7 +1944,8 @@ mod tests { let foo = system_path_to_file(&db, "/src/foo.py").unwrap(); - let ast = parsed_module(&db, foo).load(&db); + let foo = ProgramFile::new(&db, foo, db.program_environment().program(&db)); + let ast = parsed_module(&db, foo.python_file(&db)).load(&db); let class = ast.suite()[0].as_class_def_stmt().unwrap(); let model = SemanticModel::new(&db, foo); @@ -1782,7 +1965,8 @@ mod tests { let bar = system_path_to_file(&db, "/src/bar.py").unwrap(); - let ast = parsed_module(&db, bar).load(&db); + let bar = ProgramFile::new(&db, bar, db.program_environment().program(&db)); + let ast = parsed_module(&db, bar.python_file(&db)).load(&db); let import = ast.suite()[0].as_import_from_stmt().unwrap(); let alias = &import.names[0]; diff --git a/crates/ty_python_semantic/src/subscript.rs b/crates/ty_python_semantic/src/subscript.rs index c03d11dd3e..af4438fa3d 100644 --- a/crates/ty_python_semantic/src/subscript.rs +++ b/crates/ty_python_semantic/src/subscript.rs @@ -4,17 +4,21 @@ use std::num::NonZeroI32; +use crate::{Db, ProgramEnvironment}; 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 py_index( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + index: i32, + ) -> Result; } fn from_nonnegative_i32(index: i32) -> usize { @@ -82,7 +86,12 @@ impl Nth { 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> { + fn py_index( + self, + _db: &'db dyn Db, + _ctx: &ProgramEnvironment<'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)) @@ -98,7 +107,12 @@ where { type Item = I; - fn py_index(self, _db: &'db dyn Db, index: i32) -> Result { + fn py_index( + self, + _db: &'db dyn Db, + _ctx: &ProgramEnvironment<'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), @@ -232,55 +246,72 @@ mod tests { #[test] fn py_index_empty() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); 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)); + assert_eq!(iter.clone().py_index(db, &env, 0), Err(OutOfBoundsError)); + assert_eq!(iter.clone().py_index(db, &env, 1), Err(OutOfBoundsError)); + assert_eq!(iter.clone().py_index(db, &env, -1), Err(OutOfBoundsError)); + assert_eq!( + iter.clone().py_index(db, &env, i32::MIN), + Err(OutOfBoundsError) + ); + assert_eq!( + iter.clone().py_index(db, &env, i32::MAX), + Err(OutOfBoundsError) + ); } #[test] fn py_index_single_element() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); 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)); + assert_eq!(iter.clone().py_index(db, &env, 0), Ok('a')); + assert_eq!(iter.clone().py_index(db, &env, 1), Err(OutOfBoundsError)); + assert_eq!(iter.clone().py_index(db, &env, -1), Ok('a')); + assert_eq!(iter.clone().py_index(db, &env, -2), Err(OutOfBoundsError)); } #[test] fn py_index_more_elements() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); 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, &env, 0), Ok('a')); + assert_eq!(iter.clone().py_index(db, &env, 1), Ok('b')); + assert_eq!(iter.clone().py_index(db, &env, 4), Ok('e')); + assert_eq!(iter.clone().py_index(db, &env, 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)); + assert_eq!(iter.clone().py_index(db, &env, -1), Ok('e')); + assert_eq!(iter.clone().py_index(db, &env, -2), Ok('d')); + assert_eq!(iter.clone().py_index(db, &env, -5), Ok('a')); + assert_eq!(iter.clone().py_index(db, &env, -6), Err(OutOfBoundsError)); } #[test] fn py_index_uses_full_index_range() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); 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)); + assert_eq!(iter.clone().py_index(db, &env, i32::MIN), Ok(2u32.pow(31))); + assert_eq!(iter.clone().py_index(db, &env, -2), Ok(u32::MAX - 2 + 1)); + assert_eq!(iter.clone().py_index(db, &env, -1), Ok(u32::MAX - 1 + 1)); + + assert_eq!(iter.clone().py_index(db, &env, 0), Ok(0)); + assert_eq!(iter.clone().py_index(db, &env, 1), Ok(1)); + assert_eq!( + iter.clone().py_index(db, &env, i32::MAX), + Ok(i32::MAX as u32) + ); } #[track_caller] diff --git a/crates/ty_python_semantic/src/suppression.rs b/crates/ty_python_semantic/src/suppression.rs index e9ebac6d57..f920971387 100644 --- a/crates/ty_python_semantic/src/suppression.rs +++ b/crates/ty_python_semantic/src/suppression.rs @@ -9,7 +9,7 @@ use std::hash::{Hash, Hasher}; use ruff_db::diagnostic::{ Annotation, Diagnostic, DiagnosticId, IntoDiagnosticMessage, LintName, Severity, Span, }; -use ruff_db::{files::File, parsed::parsed_module, source::source_text}; +use ruff_db::{PythonFile, files::File, parsed::parsed_module, source::source_text}; use ruff_python_ast::token::{TokenKind, Tokens}; use ruff_python_trivia::indentation_at_offset; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; @@ -17,7 +17,8 @@ use rustc_hash::FxHasher; use crate::diagnostic::DiagnosticGuard; use crate::lint::{GetLintError, Level, LintMetadata, LintRegistry, LintStatus}; -pub use crate::suppression::add_ignore::{SuppressFix, suppress_all, suppress_single}; +pub use crate::suppression::add_ignore::suppress_single; +pub(crate) use crate::suppression::add_ignore::{SuppressFix, suppress_all}; use crate::suppression::parser::{ ParseError, ParseErrorKind, SuppressionComment, SuppressionParser, }; @@ -70,16 +71,19 @@ declare_lint! { } } -pub fn is_unused_ignore_comment_lint(name: LintName) -> bool { +pub(crate) fn is_unused_ignore_comment_lint(name: LintName) -> bool { name == UNUSED_IGNORE_COMMENT.name() || name == UNUSED_TYPE_IGNORE_COMMENT.name() } #[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)] -pub(crate) fn suppressions(db: &dyn Db, file: File) -> Suppressions { +pub(crate) fn suppressions(db: &dyn Db, file: PythonFile<'_>) -> Suppressions { + let source_file = file.file(db); let parsed = parsed_module(db, file).load(db); - let source = source_text(db, file); + let source = source_text(db, source_file); - let respect_type_ignore = db.analysis_settings(file).respect_type_ignore_comments; + let respect_type_ignore = db + .analysis_settings(source_file) + .respect_type_ignore_comments; let mut builder = SuppressionsBuilder::new(&source, db.lint_registry()); let mut line_start = TextSize::default(); @@ -136,7 +140,7 @@ pub(crate) fn suppressions(db: &dyn Db, file: File) -> Suppressions { pub(crate) fn check_suppressions( db: &dyn Db, - file: File, + file: PythonFile<'_>, diagnostics: TypeCheckDiagnostics, ) -> Vec { let mut context = CheckSuppressionsContext::new(db, file, diagnostics); @@ -215,11 +219,11 @@ struct CheckSuppressionsContext<'a> { } impl<'a> CheckSuppressionsContext<'a> { - fn new(db: &'a dyn Db, file: File, diagnostics: TypeCheckDiagnostics) -> Self { + fn new(db: &'a dyn Db, file: PythonFile<'a>, diagnostics: TypeCheckDiagnostics) -> Self { let suppressions = suppressions(db, file); Self { db, - file, + file: file.file(db), suppressions, diagnostics: diagnostics.into(), } @@ -264,7 +268,7 @@ impl<'a> CheckSuppressionsContext<'a> { /// /// This type exists to separate the phases of "check if a diagnostic should /// be reported" and "build the actual diagnostic." -pub(crate) struct SuppressionDiagnosticGuardBuilder<'ctx, 'db> { +struct SuppressionDiagnosticGuardBuilder<'ctx, 'db> { ctx: &'ctx CheckSuppressionsContext<'db>, id: DiagnosticId, range: TextRange, @@ -294,10 +298,7 @@ impl<'ctx, 'db> SuppressionDiagnosticGuardBuilder<'ctx, 'db> { /// /// The diagnostic can be further mutated on the guard via its `DerefMut` /// impl to `Diagnostic`. - pub(crate) fn into_diagnostic( - self, - message: impl IntoDiagnosticMessage, - ) -> DiagnosticGuard<'ctx> { + fn into_diagnostic(self, message: impl IntoDiagnosticMessage) -> DiagnosticGuard<'ctx> { let mut diag = Diagnostic::new(self.id, self.severity, message); let primary_span = Span::from(self.ctx.file).with_range(self.range); @@ -914,7 +915,7 @@ impl IntervalIndex { #[cfg(test)] mod tests { - use ruff_db::files::system_path_to_file; + use ruff_db::{PythonFile, files::system_path_to_file}; use ruff_text_size::{TextLen as _, TextRange}; use super::suppressions; @@ -939,7 +940,7 @@ value = missing let missing_start = source.find("missing").unwrap().try_into().unwrap(); let missing_range = TextRange::at(missing_start, "missing".text_len()); - let suppressions = suppressions(&db, file); + let suppressions = suppressions(&db, PythonFile::new(&db, file, db.python_version())); assert_eq!(suppressions.inline.len(), 4); assert_eq!( suppressions @@ -966,7 +967,7 @@ value = missing let missing_start = source.find("missing").unwrap().try_into().unwrap(); let missing_range = TextRange::at(missing_start, "missing".text_len()); - let suppressions = suppressions(&db, file); + let suppressions = suppressions(&db, PythonFile::new(&db, file, db.python_version())); assert_eq!(suppressions.inline.len(), 4); assert_eq!( suppressions diff --git a/crates/ty_python_semantic/src/suppression/add_ignore.rs b/crates/ty_python_semantic/src/suppression/add_ignore.rs index f2b672e57c..2bd716e9a7 100644 --- a/crates/ty_python_semantic/src/suppression/add_ignore.rs +++ b/crates/ty_python_semantic/src/suppression/add_ignore.rs @@ -9,9 +9,9 @@ use std::collections::{BTreeMap, BTreeSet}; use std::fmt::Formatter; +use ruff_db::PythonFile; use ruff_db::diagnostic::LintName; use ruff_db::display::FormatterJoinExtension; -use ruff_db::files::File; use ruff_db::parsed::parsed_module; use ruff_db::source::source_text; use ruff_diagnostics::{Edit, Fix}; @@ -31,13 +31,13 @@ use crate::suppression::{ /// an edit. It appends codes once to each applicable existing suppression and otherwise inserts at /// most one end-of-line suppression at each destination. Every returned [`SuppressFix`] records /// how many diagnostics its edit accounts for. -pub fn suppress_all( +pub(crate) fn suppress_all( db: &dyn Db, - file: File, + file: PythonFile<'_>, ids_with_range: &[(LintName, TextRange)], ) -> Vec { let suppressions = suppressions(db, file); - let source = source_text(db, file); + let source = source_text(db, file.file(db)); let parsed = parsed_module(db, file).load(db); let tokens = parsed.tokens(); @@ -69,7 +69,7 @@ pub fn suppress_all( // // This is important because a suppression inserted at the end of a narrower range // can result in a start-line suppression for a wider range. In the example above, - // inserting a `ty:ignore` after `sorted(` suppresses the diagnostic with the narrower range + // inserting a `ty: ignore` after `sorted(` suppresses the diagnostic with the narrower range // but also the diagnostic with the wider range (because the suppression is on its start line). ids_with_suppression_range.sort_unstable_by_key(|(_, _, range)| (range.start(), range.end())); @@ -159,18 +159,18 @@ pub fn suppress_all( } /// Fix to suppress one or more diagnostics. -pub struct SuppressFix { - pub fix: Fix, +pub(crate) struct SuppressFix { + pub(crate) fix: Fix, /// The number of diagnostics that will be suppressed if this fix is applied. - pub suppressed_diagnostics: usize, + pub(crate) suppressed_diagnostics: usize, } /// Creates a fix to suppress a single lint. -pub fn suppress_single(db: &dyn Db, file: File, id: LintId, range: TextRange) -> Fix { +pub fn suppress_single(db: &dyn Db, file: PythonFile<'_>, id: LintId, range: TextRange) -> Fix { let suppression_range = suppression_range(db, file, range); let suppressions = suppressions(db, file); - let source = source_text(db, file); + let source = source_text(db, file.file(db)); let codes = &[id.name()]; if let Some(existing) = find_existing_suppression(suppressions, &source, range) { @@ -193,7 +193,7 @@ pub fn suppress_single(db: &dyn Db, file: File, id: LintId, range: TextRange) -> /// * If `range` is within a single-line interpolated expression, then the start and end are extended to the start and end of the enclosing interpolated string. /// * If there's a line continuation, then the suppression range is extended to include the following line too. /// * If there's a multiline string, then the suppression range is extended to cover the starting and ending line of the multiline string. -fn suppression_range(db: &dyn Db, file: File, range: TextRange) -> TextRange { +fn suppression_range(db: &dyn Db, file: PythonFile<'_>, range: TextRange) -> TextRange { // Always insert a new suppression at the end of the range to avoid having to deal with multiline strings // etc. Also make sure to not pass a sub-token range to `Tokens::after`. let parsed = parsed_module(db, file).load(db); @@ -242,7 +242,7 @@ fn add_end_of_line_suppression(source: &str, codes: &[LintName], line_end: TextS let trailing_whitespace_len = up_to_line_end.text_len() - up_to_first_content.text_len(); let insertion = format!( - " # ty:ignore[{codes}]", + " # ty: ignore[{codes}]", codes = Codes(SuppressionKind::Ty, codes) ); diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index c78d035334..62ed04c4b0 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -12,24 +12,26 @@ use std::time::Duration; use bitflags::bitflags; use call::{CallDunderError, CallError, CallErrorKind}; use context::InferContext; +pub use context::ProgramEnvironment; use ruff_db::Instant; use ruff_db::diagnostic::{Annotation, Diagnostic, Span}; -use ruff_db::files::File; use ruff_db::parsed::parsed_module; use ruff_python_ast as ast; use ruff_python_ast::helpers::TypeModifier; use ruff_python_ast::name::Name; use ruff_text_size::Ranged; use smallvec::smallvec_inline; -use ty_module_resolver::{KnownModule, Module, ModuleName, resolve_module}; +use ty_module_resolver::{ImportingFile, KnownModule, Module, ModuleName, resolve_module}; pub(crate) use self::callable::UpcastPolicy; +use self::class::ClassInstanceFlags; pub use self::cyclic::CycleDetector; -pub(crate) use self::cyclic::{ActiveRecursionDetector, TypeTransformer}; +pub(crate) use self::cyclic::TypeTransformer; +pub(crate) use self::diagnostic::TypeCheckDiagnostics; pub(crate) use self::diagnostic::register_lints; pub use self::diagnostic::{ - MISPLACED_DEPENDENCY, TypeCheckDiagnostics, UNDECLARED_DEPENDENCY, UNDEFINED_REVEAL, - UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, + MISPLACED_DEPENDENCY, UNDECLARED_DEPENDENCY, UNDEFINED_REVEAL, UNRESOLVED_IMPORT, + UNRESOLVED_REFERENCE, }; pub(crate) use self::infer::{ InferredDeclaration, TypeContext, infer_complete_scope_types, infer_deferred_types, @@ -49,17 +51,16 @@ pub(crate) use self::match_pattern::{ }; pub(crate) use self::relation_error::{ErrorContext, ErrorContextTree, ParameterDescription}; use self::set_theoretic::KnownUnion; +use self::set_theoretic::NegativeIntersectionElements; pub(crate) use self::set_theoretic::builder::{ IntersectionBuilder, UnionAccumulator, UnionBuilder, }; -pub use self::set_theoretic::{ - IntersectionType, NegativeIntersectionElements, NegativeIntersectionElementsIterator, UnionType, -}; +pub use self::set_theoretic::{IntersectionType, UnionType}; pub use self::signatures::ParameterKind; pub(crate) use self::signatures::Signature; pub(crate) use self::subclass_of::{SubclassOfInner, SubclassOfType}; pub(crate) use self::type_expansion::expand_type; -pub use crate::diagnostic::add_inferred_python_version_hint_to_diagnostic; +pub(crate) use crate::diagnostic::add_inferred_python_version_hint_to_diagnostic; use crate::django_settings; use crate::place::{ DefinedPlace, Definedness, Place, PlaceAndQualifiers, Provenance, TypeOrigin, @@ -79,7 +80,10 @@ pub use crate::types::dedicated::role::{ function_framework_role, }; pub use crate::types::deferred::{DeferredOperation, DeferredType}; -use crate::types::diagnostic::{INVALID_AWAIT, INVALID_TYPE_FORM}; +use crate::types::diagnostic::{ + AttributeAccessMethod, INVALID_AWAIT, INVALID_TYPE_FORM, report_bad_attribute_access_call, + report_bad_dunder_get_call, +}; pub use crate::types::display::{DisplaySettings, TypeDetail, TypeDisplayDetails}; pub use crate::types::enums::basedpython_is_keeps_identity; pub(crate) use crate::types::enums::{EnumClassLiteral, EnumComplementType, enum_metadata}; @@ -89,9 +93,7 @@ use crate::types::function::{ FunctionType, KnownFunction, }; pub(crate) use crate::types::generics::GenericContext; -use crate::types::generics::{ - ApplySpecialization, InferableTypeVars, Specialization, bind_typevar, -}; +use crate::types::generics::{ApplySpecialization, Specialization, bind_typevar}; use crate::types::infer::InferenceFlags; use crate::types::known_instance::{ InternedConstraintSet, InternedType, SentinelInstance, UnionTypeInstance, @@ -111,16 +113,17 @@ use crate::types::tuple::TupleSpec; pub use crate::types::type_alias::TypeAliasType; pub use crate::types::type_form::TypeFormType; pub(crate) use crate::types::typed_dict::TypedDictType; -use crate::types::typevar::TypeVarInstance; +pub(crate) use crate::types::typevar::TypeVarBoundOrConstraints; pub use crate::types::typevar::{ - BindingContext, BoundTypeVarIdentity, BoundTypeVarInstance, ParamSpecAttrKind, - TypeVarBoundOrConstraints, TypeVarKind, TypeVarNonce, + BindingContext, BoundTypeVarIdentity, BoundTypeVarInstance, ParamSpecAttrKind, TypeVarKind, + TypeVarNonce, }; +use crate::types::typevar::{TypeVarInstance, TypeVarSet}; pub use crate::types::unsafe_union::UnsafeUnionType; pub use crate::types::variance::TypeVarVariance; use crate::types::variance::VarianceInferable; -use crate::types::visitor::any_over_type; -use crate::{Db, FxOrderSet, Program}; +use crate::types::visitor::{any_over_type, dynamic_content}; +use crate::{Db, FxOrderSet, HasType, Program, SemanticModel}; pub(crate) use class::{ ClassLiteral, ClassLiteralFlags, ClassType, GenericAlias, StaticClassLiteral, }; @@ -145,12 +148,13 @@ pub enum UnpackedKwargs<'db> { pub(crate) use literal::{ BytesLiteralType, EnumLiteralType, LiteralValueType, LiteralValueTypeKind, StringLiteralType, }; +use ruff_db::files::File; pub use special_form::SpecialFormType; pub(crate) use special_form::TypedDictModule; -use ty_python_core::definition::Definition; +use ty_python_core::definition::{Definition, DefinitionKind}; use ty_python_core::place::ScopedPlaceId; use ty_python_core::scope::ScopeId; -use ty_python_core::{Truthiness, place_table, semantic_index, use_def_map}; +use ty_python_core::{ProgramFile, Truthiness, place_table, semantic_index, use_def_map}; mod attribute_write; mod bool; @@ -234,7 +238,7 @@ mod definition; mod property_tests; pub(crate) mod subscript; -pub fn check_types(db: &dyn Db, file: File) -> Vec { +pub fn check_types(db: &dyn Db, file: ProgramFile<'_>) -> Vec { check_types_with(db, file, Vec::new()) } @@ -244,11 +248,12 @@ pub fn check_types(db: &dyn Db, file: File) -> Vec { /// are applied here rather than where it was computed. pub(crate) fn check_types_with( db: &dyn Db, - file: File, + file: ProgramFile<'_>, external: Vec, ) -> Vec { - let _span = tracing::trace_span!("check_types", ?file).entered(); - tracing::debug!("Checking file '{path}'", path = file.path(db)); + let source_file = file.file(db); + let _span = tracing::trace_span!("check_types", ?source_file).entered(); + tracing::debug!("Checking file '{path}'", path = source_file.path(db)); let start = Instant::now(); @@ -273,18 +278,18 @@ pub(crate) fn check_types_with( index .semantic_syntax_errors() .iter() - .map(|error| Diagnostic::invalid_syntax(file, error, error)), + .map(|error| Diagnostic::invalid_syntax(source_file, error, error)), ); - report_external(db, file, external, &mut diagnostics); + report_external(db, source_file, external, &mut diagnostics); - let diagnostics = check_suppressions(db, file, diagnostics); + let diagnostics = check_suppressions(db, file.python_file(db), diagnostics); let elapsed = start.elapsed(); if elapsed >= Duration::from_millis(100) { tracing::info!( "Checking file `{path}` took more than 100ms ({elapsed:?})", - path = file.path(db) + path = source_file.path(db) ); } @@ -307,7 +312,7 @@ fn report_external( return; } - let suppressions = suppressions(db, file); + let suppressions = suppressions(db, db.program_file(file).python_file(db)); for diagnostic in external { let suppression = diagnostic @@ -330,6 +335,100 @@ pub(crate) fn binding_type<'db>(db: &'db dyn Db, definition: Definition<'db>) -> inference.binding_type(definition) } +/// Returns whether a definition represents a value that exists at runtime. +/// +/// Type-checking-only decorators and guards never represent runtime values. Private type-variable +/// declarations, explicit aliases, and unambiguous typing aliases in stub files are also +/// typing-only, while public aliases and genuine runtime values remain visible. +/// +/// ```python +/// _T = TypeVar("_T") # Typing-only helper. +/// _Alias: TypeAlias = list[int] # Typing-only alias. +/// _runtime_typevar = make_typevar() # Runtime value. +/// _runtime_callback = callbacks[0] # Runtime value. +/// ``` +#[salsa::tracked(returns(copy))] +pub(crate) fn exists_at_runtime<'db>(db: &'db dyn Db, definition: Definition<'db>) -> bool { + let file = definition.program_file(db); + let inference = infer_definition_types(db, definition); + let ty = inference.binding_type(definition); + + // A class or function decorated with `@type_check_only` never exists at runtime. + if ty.is_type_check_only(db) + || inference + .undecorated_type() + .is_some_and(|ty| ty.is_type_check_only(db)) + { + return false; + } + + let parsed = parsed_module(db, file.python_file(db)); + let module = parsed.load(db); + + // Definitions inside an `if TYPE_CHECKING` block are never available at runtime. + if semantic_index(db, file).is_in_type_checking_block( + definition.file_scope(db), + definition.full_range(db, &module).range(), + ) { + return false; + } + + // The remaining heuristics only apply to stub definitions. + if !file.file(db).is_stub(db) { + return true; + } + + let is_private = definition.place(db).as_symbol().is_some_and(|symbol| { + matches!( + NameKind::classify(place_table(db, definition.scope(db)).symbol(symbol).name()), + NameKind::Sunder + ) + }); + + if !is_private { + return true; + } + + // Private type variables, parameter specifications, and type-variable tuples in stubs are + // implementation details rather than runtime values. + if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = ty + && typevar.definition(db) == Some(definition) + { + return false; + } + + // Explicit PEP 613 and PEP 695 type aliases in stubs are also typing-only helpers. + let model = SemanticModel::new(db, file); + if model.is_type_alias_definition(definition) { + return false; + } + + let DefinitionKind::Assignment(assignment) = definition.kind(db) else { + return true; + }; + + // Treat only unambiguous union, `Literal`, and `Annotated` expressions as implicit aliases. + // Other expressions may also be aliases, but a false negative is preferable to incorrectly + // hiding a value that exists at runtime. + match (ty, assignment.value(&module)) { + ( + Type::KnownInstance(KnownInstanceType::UnionType(_)), + ast::Expr::BinOp(ast::ExprBinOp { + op: ast::Operator::BitOr, + .. + }), + ) => false, + ( + Type::KnownInstance(KnownInstanceType::Literal(_) | KnownInstanceType::Annotated(_)), + ast::Expr::Subscript(subscript), + ) => !matches!( + subscript.value.inferred_type(&model), + Some(Type::SpecialForm(_) | Type::ClassLiteral(_) | Type::GenericAlias(_)) + ), + _ => true, + } +} + /// Infer the type of a declaration, returning `Rejected` if it is not valid. pub(crate) fn inferred_declaration<'db>( db: &'db dyn Db, @@ -350,7 +449,7 @@ fn definition_expression_type<'db>( definition: Definition<'db>, expression: &ast::Expr, ) -> Type<'db> { - let file = definition.file(db); + let file = definition.program_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); @@ -377,7 +476,7 @@ fn definition_expression_annotation<'db>( definition: Definition<'db>, expression: &ast::Expr, ) -> TypeAndQualifiers<'db> { - let file = definition.file(db); + let file = definition.program_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); @@ -409,8 +508,8 @@ type MaterializationEquivalenceVisitor<'db> = /// Some recursive transformations visit the same type under more than one mapping mode within a /// single call chain. Keep separate cycle caches for those modes so one transformation cannot /// reuse the result of another. -#[derive(Default)] -pub(crate) struct ApplyTypeMappingVisitor<'db> { +pub(crate) struct ApplyTypeMappingVisitor<'env, 'db> { + env: &'env ProgramEnvironment<'db>, default: OnceCell>>, top_materialization: OnceCell>>, bottom_materialization: OnceCell>>, @@ -421,13 +520,27 @@ pub(crate) struct ApplyTypeMappingVisitor<'db> { materialization_equivalence: OnceCell>, } -impl<'db> ApplyTypeMappingVisitor<'db> { +impl<'env, 'db> ApplyTypeMappingVisitor<'env, 'db> { + fn new(env: &'env ProgramEnvironment<'db>) -> Self { + Self { + env, + default: OnceCell::default(), + top_materialization: OnceCell::default(), + bottom_materialization: OnceCell::default(), + top_specialization_materialization: OnceCell::default(), + bottom_specialization_materialization: OnceCell::default(), + promotion: OnceCell::default(), + skip_promotion: OnceCell::default(), + materialization_equivalence: OnceCell::default(), + } + } + fn materialization_equivalence(&self) -> &MaterializationEquivalenceVisitor<'db> { self.materialization_equivalence .get_or_init(|| Rc::new(CycleDetector::new(true))) } - pub(crate) fn visit( + fn visit( &self, db: &'db dyn Db, ty: Type<'db>, @@ -454,7 +567,7 @@ impl<'db> ApplyTypeMappingVisitor<'db> { .visit_type(db, ty, func) } - pub(crate) fn is_equivalent_to_materialization( + fn is_equivalent_to_materialization( &self, db: &'db dyn Db, left: Type<'db>, @@ -466,7 +579,7 @@ impl<'db> ApplyTypeMappingVisitor<'db> { }) } - pub(crate) fn for_new_materialization_root(&self) -> Self { + fn for_new_materialization_root(&self) -> Self { let materialization_equivalence = OnceCell::new(); let was_empty = materialization_equivalence.set(Rc::clone(self.materialization_equivalence())); @@ -474,7 +587,7 @@ impl<'db> ApplyTypeMappingVisitor<'db> { Self { materialization_equivalence, - ..Self::default() + ..Self::new(self.env) } } } @@ -487,14 +600,13 @@ pub(crate) type FindLegacyTypeVarsVisitor<'db> = pub(crate) struct FindLegacyTypeVars; /// A [`CycleDetector`] that is used in `visit_specialization` methods. -pub(crate) type SpecializationVisitor<'db> = - CycleDetector<'db, VisitSpecialization, Type<'db>, (), 3>; -pub(crate) struct VisitSpecialization; +type SpecializationVisitor<'db> = CycleDetector<'db, VisitSpecialization, Type<'db>, (), 3>; +struct VisitSpecialization; -/// How a generic type has been specialized. +/// Whether a type represents the upper or lower bound of a gradual type. /// -/// This matters only if there is at least one invariant type parameter. -/// For example, we represent `Top[list[Any]]` as a `GenericAlias` with +/// For generic specializations, this matters only if there is at least one invariant or constrained +/// type parameter. For example, we represent `Top[list[Any]]` as a `GenericAlias` with /// `MaterializationKind` set to Top, which we denote as `Top[list[Any]]`. /// A type `Top[list[T]]` includes all fully static list types `list[U]` where `U` is /// a supertype of `Bottom[T]` and a subtype of `Top[T]`. @@ -502,6 +614,9 @@ pub(crate) struct VisitSpecialization; /// Similarly, there is `Bottom[list[Any]]`. /// This type is harder to make sense of in a set-theoretic framework, but /// it is a subtype of all materializations of `list[Any]`. +/// +/// Recursive type aliases also retain their materialization kind so that materializing the alias +/// body preserves stable recursive references. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, get_size2::GetSize)] pub enum MaterializationKind { Top, @@ -511,7 +626,7 @@ pub enum MaterializationKind { impl MaterializationKind { /// Flip the materialization type: `Top` becomes `Bottom` and vice versa. #[must_use] - pub const fn flip(self) -> Self { + const fn flip(self) -> Self { match self { Self::Top => Self::Bottom, Self::Bottom => Self::Top, @@ -535,6 +650,265 @@ impl AttributeKind { } } +/// An interned description of an invalid implicit `__get__` call. +/// +/// Member lookup carries this compact context through unions and fallbacks. Expression inference +/// reconstructs the concrete [`CallError`] if the invalid access remains after applying lookup +/// fallbacks and local assignment information. +#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +struct DescriptorGetCallContext<'db> { + #[returns(copy)] + descriptor_type: Type<'db>, + #[returns(copy)] + callable_type: Type<'db>, + #[returns(copy)] + instance: Option>, + #[returns(copy)] + owner: Type<'db>, +} + +impl get_size2::GetSize for DescriptorGetCallContext<'_> {} + +impl<'db> DescriptorGetCallContext<'db> { + /// Reconstructs the implicit call and returns its error if the call is still invalid. + fn into_error(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Option> { + let descriptor_type = self.descriptor_type(db); + let instance = self.instance(db).unwrap_or_else(|| Type::none(db, env)); + let owner = self.owner(db); + self.callable_type(db) + .try_call( + db, + env, + &CallArguments::positional([descriptor_type, instance, owner]), + ) + .err() + } +} + +/// The type and descriptor kind produced by an implicit `__get__` call. +#[derive(Clone, Debug, Copy, PartialEq, Eq, get_size2::GetSize, salsa::SalsaValue)] +pub(crate) struct DescriptorGetResult<'db> { + pub(crate) return_type: Type<'db>, + kind: AttributeKind, +} + +/// A failed implicit descriptor call together with its recovery value. +#[derive(Clone, Debug, Copy, PartialEq, Eq, get_size2::GetSize, salsa::SalsaValue)] +pub(crate) struct DescriptorGetError<'db> { + fallback: DescriptorGetResult<'db>, + context: DescriptorGetCallContext<'db>, +} + +impl<'db> DescriptorGetError<'db> { + /// Returns the descriptor's declared return type and kind despite the invalid call. + pub(crate) const fn fallback(self) -> DescriptorGetResult<'db> { + self.fallback + } +} + +fn descriptor_get_result<'db>( + return_type: Type<'db>, + kind: AttributeKind, + error: Option>, +) -> Result>, DescriptorGetError<'db>> { + let result = DescriptorGetResult { return_type, kind }; + match error { + Some(context) => Err(DescriptorGetError { + fallback: result, + context, + }), + None => Ok(Some(result)), + } +} + +/// An operation that failed while resolving an attribute. +#[derive(Clone, Debug, Copy, Hash, PartialEq, Eq, get_size2::GetSize, salsa::SalsaValue)] +enum MemberLookupErrorKind<'db> { + DescriptorGet(DescriptorGetCallContext<'db>), + + /// An invalid fallback call, represented by its receiver and requested attribute name. + /// + /// Retaining only these arguments avoids storing call bindings in cached lookup results. + GetAttr { + receiver: Type<'db>, + name: Type<'db>, + }, + + /// An invalid attribute-interception call, represented by its receiver and attribute name. + GetAttribute { + receiver: Type<'db>, + name: Type<'db>, + }, +} + +/// A failed member lookup together with the member used to recover from the error. +#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +struct MemberLookupError<'db> { + #[returns(copy)] + fallback_member: PlaceAndQualifiers<'db>, + #[returns(copy)] + kind: MemberLookupErrorKind<'db>, +} + +impl get_size2::GetSize for MemberLookupError<'_> {} + +impl<'db> MemberLookupError<'db> { + /// Reports the failed implicit call unless the lookup is shadowed or used for deletion. + fn report_diagnostic( + self, + context: &InferContext<'db, '_>, + object_type: Type<'db>, + target: &ast::ExprAttribute, + assigned_type: Option>, + ) { + if matches!(target.ctx, ast::ExprContext::Del) { + return; + } + + let db = context.db(); + let env = context.program_environment(); + + match self.kind(db) { + MemberLookupErrorKind::DescriptorGet(call_context) + if (assigned_type.is_none() + || call_context.descriptor_type(db).is_data_descriptor(db, env)) + && let Some(failure) = call_context.into_error(db, env) => + { + report_bad_dunder_get_call( + context, + &failure, + object_type, + call_context.descriptor_type(db), + target, + ); + } + kind @ (MemberLookupErrorKind::GetAttr { receiver, name } + | MemberLookupErrorKind::GetAttribute { receiver, name }) => { + let method = if matches!(kind, MemberLookupErrorKind::GetAttr { .. }) { + AttributeAccessMethod::GetAttr + } else { + AttributeAccessMethod::GetAttribute + }; + + if method == AttributeAccessMethod::GetAttr && assigned_type.is_some() { + return; + } + + if let Err(CallDunderError::CallError(kind, bindings, _)) = receiver + .try_call_dunder( + db, + env, + method.as_str(), + CallArguments::positional([name]), + TypeContext::default(), + ) + { + let failure = CallError(kind, bindings); + report_bad_attribute_access_call( + context, + &failure, + object_type, + target, + method, + ); + } + } + MemberLookupErrorKind::DescriptorGet(_) => {} + } + } +} + +/// A resolved member or an implicit-call error that retains its recovery value. +/// +/// Unlike [`crate::place::LookupResult`], errors here describe failed attribute-access operations, +/// not undefined or possibly undefined places. +type MemberLookupResult<'db> = Result, MemberLookupError<'db>>; + +fn member_lookup_result<'db>( + db: &'db dyn Db, + member: PlaceAndQualifiers<'db>, + error: Option>, +) -> MemberLookupResult<'db> { + match error { + Some(kind) => Err(MemberLookupError::new(db, member, kind)), + None => Ok(member), + } +} + +fn map_member_lookup_type<'db>( + db: &'db dyn Db, + result: MemberLookupResult<'db>, + f: impl FnOnce(Type<'db>) -> Type<'db>, +) -> MemberLookupResult<'db> { + match result { + Ok(member) => Ok(member.map_type(f)), + Err(error) => Err(MemberLookupError::new( + db, + error.fallback_member(db).map_type(f), + error.kind(db), + )), + } +} + +fn member_lookup_or_fall_back_to<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + result: MemberLookupResult<'db>, + fallback_fn: impl FnOnce() -> MemberLookupResult<'db>, +) -> MemberLookupResult<'db> { + let member = result.unwrap_or_else(|error| error.fallback_member(db)); + match member.place { + Place::Undefined => fallback_fn(), + Place::Defined(DefinedPlace { + definedness: Definedness::AlwaysDefined, + .. + }) => result, + Place::Defined(DefinedPlace { + definedness: Definedness::PossiblyUndefined, + .. + }) => { + let fallback = fallback_fn(); + let fallback_member = fallback.unwrap_or_else(|error| error.fallback_member(db)); + member_lookup_result( + db, + member.or_fall_back_to(db, env, || fallback_member), + result + .err() + .map(|error| error.kind(db)) + .or_else(|| fallback.err().map(|error| error.kind(db))), + ) + } + } +} + +fn cycle_normalized_member_lookup<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + result: MemberLookupResult<'db>, + previous: MemberLookupResult<'db>, + cycle: &salsa::Cycle, +) -> MemberLookupResult<'db> { + let error = result + .err() + .map(|error| error.kind(db)) + .filter(|_| cycle.iteration() <= crate::TAINTED_CYCLES || previous.is_err()); + let member = result.unwrap_or_else(|error| error.fallback_member(db)); + let previous = previous.unwrap_or_else(|error| error.fallback_member(db)); + member_lookup_result(db, member.cycle_normalized(db, env, previous, cycle), error) +} + +impl<'db> From> for MemberLookupResult<'db> { + fn from(member: PlaceAndQualifiers<'db>) -> Self { + Ok(member) + } +} + +impl<'db> From> for MemberLookupResult<'db> { + fn from(place: Place<'db>) -> Self { + Ok(place.into()) + } +} + /// 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 @@ -598,32 +972,32 @@ impl MemberLookupPolicy { /// 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 { + 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 { + 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 { + const fn meta_class_no_type_fallback(self) -> bool { self.contains(Self::META_CLASS_NO_TYPE_FALLBACK) } /// Exclude attributes defined on `int` or `str` when looking up attributes. - pub(crate) const fn mro_no_int_or_str_fallback(self) -> bool { + const fn mro_no_int_or_str_fallback(self) -> bool { self.contains(Self::MRO_NO_INT_OR_STR_LOOKUP) } /// Do not call `__getattr__` during member lookup. - pub(crate) const fn no_getattr_lookup(self) -> bool { + const fn no_getattr_lookup(self) -> bool { self.contains(Self::NO_GETATTR_LOOKUP) } /// Ignore members that are only available through a dynamic type. - pub(crate) const fn require_concrete(self) -> bool { + const fn require_concrete(self) -> bool { self.contains(Self::REQUIRE_CONCRETE) } } @@ -637,6 +1011,8 @@ impl Default for MemberLookupPolicy { /// The common key for class-member and instance-member lookup. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] struct MemberLookupKey<'db> { + #[returns(copy)] + program: Program<'db>, #[returns(copy)] ty: Type<'db>, #[returns(ref)] @@ -648,7 +1024,7 @@ struct MemberLookupKey<'db> { /// 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); +pub struct TodoType(&'static str); #[cfg(debug_assertions)] impl std::fmt::Display for TodoType { @@ -765,7 +1141,7 @@ fn walk_property_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( impl get_size2::GetSize for PropertyInstanceType<'_> {} impl<'db> PropertyInstanceType<'db> { - pub fn new( + pub(crate) fn new( db: &'db dyn Db, getter: Option>, setter: Option>, @@ -793,8 +1169,8 @@ impl<'db> PropertyInstanceType<'db> { Self::new_internal(db, getter, setter, deleter, self.instance_class(db)) } - fn instance_fallback(self, db: &'db dyn Db) -> Type<'db> { - self.instance_class(db).to_instance(db) + fn instance_fallback(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + self.instance_class(db).to_instance(db, env) } /// Returns the [`PropertyAccessorRole`] that `def` plays in this property, or `None` when @@ -831,48 +1207,50 @@ impl<'db> PropertyInstanceType<'db> { fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { let getter = self .getter(db) - .map(|ty| ty.apply_type_mapping_impl(db, type_mapping, tcx, visitor)); + .map(|ty| ty.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor)); let setter = self .setter(db) - .map(|ty| ty.apply_type_mapping_impl(db, type_mapping, tcx, visitor)); + .map(|ty| ty.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor)); let deleter = self .deleter(db) - .map(|ty| ty.apply_type_mapping_impl(db, type_mapping, tcx, visitor)); + .map(|ty| ty.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor)); self.with_accessors(db, getter, setter, deleter) } fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { let getter = match self.getter(db) { - Some(ty) if nested => Some(ty.recursive_type_normalized_impl(db, div, true)?), + Some(ty) if nested => Some(ty.recursive_type_normalized_impl(db, env, div, true)?), Some(ty) => Some( - ty.recursive_type_normalized_impl(db, div, true) + ty.recursive_type_normalized_impl(db, env, div, true) .unwrap_or(div), ), None => None, }; let setter = match self.setter(db) { - Some(ty) if nested => Some(ty.recursive_type_normalized_impl(db, div, true)?), + Some(ty) if nested => Some(ty.recursive_type_normalized_impl(db, env, div, true)?), Some(ty) => Some( - ty.recursive_type_normalized_impl(db, div, true) + ty.recursive_type_normalized_impl(db, env, div, true) .unwrap_or(div), ), None => None, }; let deleter = match self.deleter(db) { - Some(ty) if nested => Some(ty.recursive_type_normalized_impl(db, div, true)?), + Some(ty) if nested => Some(ty.recursive_type_normalized_impl(db, env, div, true)?), Some(ty) => Some( - ty.recursive_type_normalized_impl(db, div, true) + ty.recursive_type_normalized_impl(db, env, div, true) .unwrap_or(div), ), None => None, @@ -883,18 +1261,19 @@ impl<'db> PropertyInstanceType<'db> { fn find_legacy_typevars_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, ) { if let Some(ty) = self.getter(db) { - ty.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + ty.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } if let Some(ty) = self.setter(db) { - ty.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + ty.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } if let Some(ty) = self.deleter(db) { - ty.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + ty.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } } } @@ -980,12 +1359,12 @@ pub struct DataclassParams<'db> { impl get_size2::GetSize for DataclassParams<'_> {} impl<'db> DataclassParams<'db> { - fn default_params(db: &'db dyn Db) -> Self { - Self::from_flags(db, DataclassFlags::default()) + fn default_params(db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Self { + Self::from_flags(db, env, DataclassFlags::default()) } - fn from_flags(db: &'db dyn Db, flags: DataclassFlags) -> Self { - let dataclasses_field = known_module_symbol(db, KnownModule::Dataclasses, "field") + fn from_flags(db: &'db dyn Db, env: &ProgramEnvironment<'db>, flags: DataclassFlags) -> Self { + let dataclasses_field = known_module_symbol(db, env, KnownModule::Dataclasses, "field") .place .ignore_possibly_undefined() .unwrap_or_else(Type::unknown); @@ -1001,9 +1380,10 @@ impl<'db> DataclassParams<'db> { ) } - pub(super) fn recursive_type_normalized_impl( + fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -1011,7 +1391,7 @@ impl<'db> DataclassParams<'db> { .field_specifiers(db) .iter() .map(|ty| { - let ty = ty.recursive_type_normalized_impl(db, div, true); + let ty = ty.recursive_type_normalized_impl(db, env, div, true); if nested { ty } else { Some(ty.unwrap_or(div)) } }) .collect::>>()?; @@ -1175,17 +1555,17 @@ pub(crate) enum InstanceProjection { } impl InstanceProjection { - pub(crate) const fn is_exact(&self) -> bool { + const fn is_exact(&self) -> bool { matches!(self, Self::Exact(_)) } - pub(crate) fn into_inner(self) -> T { + fn into_inner(self) -> T { match self { Self::Exact(value) | Self::OverApproximation(value) => value, } } - pub(crate) fn map(self, transform: impl FnOnce(T) -> U) -> InstanceProjection { + fn map(self, transform: impl FnOnce(T) -> U) -> InstanceProjection { match self { Self::Exact(value) => InstanceProjection::Exact(transform(value)), Self::OverApproximation(value) => { @@ -1194,7 +1574,7 @@ impl InstanceProjection { } } - pub(crate) const fn new(value: T, is_exact: bool) -> Self { + const fn new(value: T, is_exact: bool) -> Self { if is_exact { Self::Exact(value) } else { @@ -1203,9 +1583,12 @@ impl InstanceProjection { } } -/// An ordered pair of types shared by type-relation and set-theoretic queries. +/// An ordered pair of types and their Python version shared by type-relation and set-theoretic +/// queries. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] struct TypePair<'db> { + #[returns(copy)] + program: Program<'db>, #[returns(copy)] first: Type<'db>, #[returns(copy)] @@ -1218,6 +1601,7 @@ impl get_size2::GetSize for TypePair<'_> {} /// Helper for `recursive_type_normalized_impl` for `TypeGuardLike` types. fn recursive_type_normalize_type_guard_like<'db, T: TypeGuardLike<'db>>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, guard: T, div: Type<'db>, nested: bool, @@ -1225,11 +1609,11 @@ fn recursive_type_normalize_type_guard_like<'db, T: TypeGuardLike<'db>>( let ty = if nested { guard .type_argument(db) - .recursive_type_normalized_impl(db, div, true)? + .recursive_type_normalized_impl(db, env, div, true)? } else { guard .type_argument(db) - .recursive_type_normalized_impl(db, div, true) + .recursive_type_normalized_impl(db, env, div, true) .unwrap_or(div) }; Some(guard.with_type(db, ty)) @@ -1243,6 +1627,29 @@ struct GeneratorTypes<'db> { return_ty: Option>, } +impl<'db> GeneratorTypes<'db> { + /// Apply a generator's materialization with the variance of each operation. + fn materialize( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + kind: MaterializationKind, + ) -> Self { + let visitor = ApplyTypeMappingVisitor::new(env); + Self { + yield_ty: self + .yield_ty + .map(|ty| ty.materialize(db, env, kind, &visitor)), + send_ty: self + .send_ty + .map(|ty| ty.materialize(db, env, kind.flip(), &visitor)), + return_ty: self + .return_ty + .map(|ty| ty.materialize(db, env, kind, &visitor)), + } + } +} + fn object_type_form(db: &dyn Db) -> Type<'_> { TypeFormType::from_type_expression(db, Type::object()) } @@ -1250,7 +1657,12 @@ fn object_type_form(db: &dyn Db) -> Type<'_> { const NESTING_LIMIT: usize = 8; /// how deeply `ty` nests generic instances inside one another, saturating at `limit` -fn nesting_depth<'db>(db: &'db dyn Db, ty: Type<'db>, limit: usize) -> usize { +fn nesting_depth<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + limit: usize, +) -> usize { if limit == 0 { return 0; } @@ -1258,21 +1670,25 @@ fn nesting_depth<'db>(db: &'db dyn Db, ty: Type<'db>, limit: usize) -> usize { return union .elements(db) .iter() - .map(|element| nesting_depth(db, *element, limit)) + .map(|element| nesting_depth(db, env, *element, limit)) .max() .unwrap_or(0); } let Some(instance) = ty.as_nominal_instance() else { return 0; }; - let argument = match instance.tuple_spec(db) { - Some(spec) => spec.homogeneous_element_type(db), - None => match instance.class(db).class_literal_and_specialization(db).1 { - Some(specialization) => UnionType::from_elements(db, specialization.types(db)), + let argument = match instance.tuple_spec(db, env) { + Some(spec) => spec.homogeneous_element_type(db, env), + None => match instance + .class(db, env) + .class_literal_and_specialization(db) + .1 + { + Some(specialization) => UnionType::from_elements(db, env, specialization.types(db)), None => return 1, }, }; - 1 + nesting_depth(db, argument, limit - 1) + 1 + nesting_depth(db, env, argument, limit - 1) } #[salsa::tracked] @@ -1289,11 +1705,11 @@ impl<'db> Type<'db> { Self::Divergent(DivergentType::new(id)) } - pub(crate) const fn is_divergent(&self) -> bool { + const fn is_divergent(&self) -> bool { matches!(self, Type::Divergent(_)) } - pub(crate) const fn as_divergent(self) -> Option { + const fn as_divergent(self) -> Option { match self { Type::Divergent(divergent) => Some(divergent), _ => None, @@ -1338,6 +1754,17 @@ impl<'db> Type<'db> { }) } + pub(crate) fn is_fully_static(self, db: &'db dyn Db, env: &ProgramEnvironment) -> bool { + dynamic_content(db, env, self).is_absent() + } + + const fn as_intersection(self) -> Option> { + match self { + Type::Intersection(intersection) => Some(intersection), + _ => None, + } + } + pub const fn is_unknown(&self) -> bool { matches!( self, @@ -1361,14 +1788,14 @@ impl<'db> Type<'db> { } /// Returns `true` if this type contains a `Self` type variable. - pub(crate) fn contains_self(self, db: &'db dyn Db) -> bool { + fn contains_self(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool { if let Type::NominalInstance(instance) = self && !instance.is_definition_generic(db) { return false; } - any_over_type(db, self, false, |ty| { + any_over_type(db, env, self, false, |ty| { ty.as_typevar().is_some_and(|tv| tv.typevar(db).is_self(db)) }) } @@ -1377,11 +1804,11 @@ impl<'db> Type<'db> { /// /// `FunctionLiteral`, `BoundMethod`, and function-like `Callable` types return `false` /// because their `Self` binding is deferred to call time via the signature binding path. - fn supports_self_binding(&self, db: &'db dyn Db) -> bool { + fn supports_self_binding(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool { match self { Type::FunctionLiteral(_) | Type::BoundMethod(_) | Type::KnownBoundMethod(_) => false, Type::Callable(callable) if callable.is_function_like(db) => false, - _ => self.contains_self(db), + _ => self.contains_self(db, env), } } @@ -1392,26 +1819,43 @@ impl<'db> Type<'db> { /// /// Types that defer `Self` binding to call time (functions, bound methods, function-like /// callables) are skipped; see `supports_self_binding`. - pub(crate) fn bind_self_typevars(self, db: &'db dyn Db, self_type: Type<'db>) -> Self { - if !self.supports_self_binding(db) { + fn bind_self_typevars( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + self_type: Type<'db>, + ) -> Self { + if !self.supports_self_binding(db, env) { return self; } self.apply_type_mapping( db, - &TypeMapping::BindSelf(SelfBinding::new(db, self_type, None)), + env, + &TypeMapping::BindSelf(SelfBinding::new(db, env, self_type, None)), TypeContext::default(), ) } /// Returns `true` if `self` is [`Type::Callable`]. - pub(crate) const fn is_callable_type(&self) -> bool { + const fn is_callable_type(&self) -> bool { matches!(self, Type::Callable(..)) } pub(crate) fn cycle_normalized( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + previous: Self, + cycle: &salsa::Cycle, + ) -> Self { + self.cycle_normalized_impl(db, env, previous, cycle) + } + + pub(super) fn cycle_normalized_impl( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, previous: Self, cycle: &salsa::Cycle, ) -> Self { @@ -1430,19 +1874,19 @@ impl<'db> Type<'db> { // still ensures convergence in cases that are prone to oscillation. if cycle.iteration() <= crate::TAINTED_CYCLES { let self_degraded_by_overload = - any_over_type(db, self, false, |ty| { + any_over_type(db, env, self, false, |ty| { matches!( ty, Type::Dynamic(DynamicType::AmbiguousOverload) | Type::UnsafeUnion(_) ) - }) && !any_over_type(db, self, false, |ty| ty.is_divergent()) - && any_over_type(db, previous, false, |ty| ty.is_divergent()); + }) && !any_over_type(db, env, self, false, |ty| ty.is_divergent()) + && any_over_type(db, env, previous, false, |ty| ty.is_divergent()); // Generally, the precision of type inference improves with each iteration. // However, overload is an exception; as iterations progress, overload matching may become ambiguous, and a reversal of precision can occur. // This kind of precision degradation can be determined by whether the type contains // `DynamicType::AmbiguousOverload` or an unsafe union, the two results of an ambiguous overload match. if self_degraded_by_overload { - UnionType::from_elements_cycle_recovery(db, [previous, self]) + UnionType::from_elements_cycle_recovery(db, env, [previous, self]) } else { self } @@ -1456,11 +1900,11 @@ impl<'db> Type<'db> { // where the order of union types is different between the previous and current cycle. // We should use the previous union type as the base and only add new element types in // this cycle, if any. - let unioned = UnionType::from_elements_cycle_recovery(db, [previous, self]); - unioned.collapse_tuple_lengths(db) + let unioned = UnionType::from_elements_cycle_recovery(db, env, [previous, self]); + unioned.collapse_tuple_lengths(db, env) } - .recursive_type_normalized(db, cycle) - .without_growing_tuple_lengths(db, previous) + .recursive_type_normalized_impl_with_cycle(db, env, cycle) + .without_growing_tuple_lengths(db, env, previous) } /// The elements of `self` when it is a union, and `self` itself otherwise. @@ -1483,7 +1927,12 @@ impl<'db> Type<'db> { /// /// Only exact tuples are collapsed. A named tuple is a class whose length its author wrote /// down. - fn without_growing_tuple_lengths(self, db: &'db dyn Db, previous: Self) -> Self { + fn without_growing_tuple_lengths( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + previous: Self, + ) -> Self { let longest_tuple = |ty: Self| { ty.union_elements(db) .filter_map(|element| Some(element.exact_tuple_instance_spec(db)?.len().minimum())) @@ -1500,15 +1949,16 @@ impl<'db> Type<'db> { let mut elements = Vec::new(); for element in self.union_elements(db) { match element.exact_tuple_instance_spec(db) { - Some(spec) => elements.push(spec.homogeneous_element_type(db)), + Some(spec) => elements.push(spec.homogeneous_element_type(db, env)), None => rest.push(element), } } rest.push(Type::homogeneous_tuple( db, - UnionType::from_elements_cycle_recovery(db, elements), + env, + UnionType::from_elements_cycle_recovery(db, env, elements), )); - UnionType::from_elements_cycle_recovery(db, rest) + UnionType::from_elements_cycle_recovery(db, env, rest) } /// collapse a union that is accumulating one tuple per *length* into a single @@ -1523,7 +1973,7 @@ impl<'db> Type<'db> { /// only fixed-length members are folded, and only when there are several: two /// tuples of different length in a union is an ordinary type a program can mean, /// while a growing run of them is the shape that does not converge - fn collapse_tuple_lengths(self, db: &'db dyn Db) -> Self { + fn collapse_tuple_lengths(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Self { const GROWING: usize = 3; let Type::Union(union) = self else { return self; @@ -1534,7 +1984,7 @@ impl<'db> Type<'db> { .filter(|element| { element .as_nominal_instance() - .and_then(|instance| instance.tuple_spec(db)) + .and_then(|instance| instance.tuple_spec(db, env)) .is_some_and(|spec| spec.as_fixed_length().is_some()) }) .count(); @@ -1546,22 +1996,22 @@ impl<'db> Type<'db> { for element in elements { match element .as_nominal_instance() - .and_then(|instance| instance.tuple_spec(db)) + .and_then(|instance| instance.tuple_spec(db, env)) .filter(|spec| spec.as_fixed_length().is_some()) { - Some(spec) => element_types.push(spec.homogeneous_element_type(db)), + Some(spec) => element_types.push(spec.homogeneous_element_type(db, env)), None => kept.push(*element), } } - let element = UnionType::from_elements(db, element_types); + let element = UnionType::from_elements(db, env, element_types); kept.push(Type::tuple(Some( - crate::types::tuple::TupleType::homogeneous(db, element), + crate::types::tuple::TupleType::homogeneous(db, env, element), ))); - UnionType::from_elements(db, kept) + UnionType::from_elements(db, env, kept) } - pub(crate) fn is_deeply_nested(self, db: &'db dyn Db) -> bool { - nesting_depth(db, self, NESTING_LIMIT) == NESTING_LIMIT + pub(crate) fn is_deeply_nested(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool { + nesting_depth(db, env, self, NESTING_LIMIT) == NESTING_LIMIT } pub fn is_none(&self, db: &'db dyn Db) -> bool { @@ -1572,48 +2022,20 @@ impl<'db> Type<'db> { self.is_instance_of(db, KnownClass::Bool) } - fn is_enum(&self, db: &'db dyn Db) -> bool { + fn is_enum(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool { self.as_nominal_instance() - .is_some_and(|instance| enum_metadata(db, instance.class_literal(db)).is_some()) + .is_some_and(|instance| enum_metadata(db, instance.class_literal(db, env)).is_some()) } fn is_typealias_special_form(&self) -> bool { matches!(self, Type::SpecialForm(SpecialFormType::TypeAlias)) } - /// Return true if this type overrides __eq__ or __ne__ methods - fn overrides_equality(&self, db: &'db dyn Db) -> bool { - let check_dunder = |dunder_name, allowed_return_value| { - // Note that we do explicitly exclude dunder methods on `object`, `int` and `str` here. - // The reason for this is that we know that these dunder methods behave in a predictable way. - // Only custom dunder methods need to be examined here, as they might break single-valuedness - // by always returning `False`, for example. - let call_result = self.try_call_dunder_with_policy( - db, - dunder_name, - &mut CallArguments::positional([Type::unknown()]), - TypeContext::default(), - MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK - | MemberLookupPolicy::MRO_NO_INT_OR_STR_LOOKUP, - ); - let call_result = call_result.as_ref(); - call_result.is_ok_and(|bindings| { - bindings - .return_type(db) - .as_literal_value() - .and_then(literal::LiteralValueType::as_bool) - == Some(allowed_return_value) - }) || call_result.is_err_and(|err| matches!(err, CallDunderError::MethodNotAvailable)) - }; - - !(check_dunder("__eq__", true) && check_dunder("__ne__", false)) - } - pub fn is_notimplemented(&self, db: &'db dyn Db) -> bool { self.is_instance_of(db, KnownClass::NotImplementedType) } - pub(crate) fn is_todo(&self) -> bool { + fn is_todo(&self) -> bool { self.as_dynamic().is_some_and(|dynamic| match dynamic { DynamicType::Any | DynamicType::Unknown @@ -1633,7 +2055,7 @@ impl<'db> Type<'db> { /// /// For example, whereas `` is a generic type, `` /// is a specialization of that type. - pub(crate) fn is_specialized_generic(self, db: &'db dyn Db) -> bool { + fn is_specialized_generic(self, db: &'db dyn Db) -> bool { match self { Type::Union(union) => union .elements(db) @@ -1650,9 +2072,9 @@ impl<'db> Type<'db> { .any(|ty| ty.is_specialized_generic(db)) } Type::NominalInstance(instance_type) => instance_type.is_definition_generic(db), - Type::ProtocolInstance(protocol) => { - matches!(protocol.inner, Protocol::FromClass(class) if class.is_generic()) - } + Type::ProtocolInstance(protocol) => protocol + .class_origin(db) + .is_some_and(|class| class.is_generic()), Type::TypedDict(typed_dict) => typed_dict .defining_class() .is_some_and(ClassType::is_generic), @@ -1687,15 +2109,20 @@ impl<'db> Type<'db> { /// every assignability test about it consults. The common one is the hole /// `infer-unannotated-signatures` opens for an unannotated parameter: nothing in the /// body bounded it, so it is `Unknown` wearing a name and it proves exactly as little. - pub fn has_gradual_member(self, db: &'db dyn Db) -> bool { - self.has_gradual_member_impl(db, true) + pub fn has_gradual_member(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool { + self.has_gradual_member_impl(db, env, true) } /// `descend_typevars` is spent on the way into a bound, so a bound that mentions its own /// type variable — `T: T | int` — is walked once rather than for ever. - fn has_gradual_member_impl(self, db: &'db dyn Db, descend_typevars: bool) -> bool { + fn has_gradual_member_impl( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + descend_typevars: bool, + ) -> bool { let gradual = |element: Type<'db>| { - element.is_dynamic() || element.has_gradual_member_impl(db, descend_typevars) + element.is_dynamic() || element.has_gradual_member_impl(db, env, descend_typevars) }; match self { Type::Union(union) => union.elements(db).iter().copied().any(gradual), @@ -1704,9 +2131,9 @@ impl<'db> Type<'db> { } Type::TypeVar(bound_typevar) if descend_typevars => bound_typevar .typevar(db) - .upper_bound(db) + .upper_bound(db, env) .is_some_and(|bound| { - bound.is_dynamic() || bound.has_gradual_member_impl(db, false) + bound.is_dynamic() || bound.has_gradual_member_impl(db, env, false) }), _ => false, } @@ -1746,7 +2173,7 @@ impl<'db> Type<'db> { /// Currently checks for instances of `types.CoroutineType` (returned by `async def` calls). /// Unions are considered awaitable only if every element is awaitable. /// Intersections are considered awaitable if any positive element is awaitable. - pub(crate) fn is_awaitable(self, db: &'db dyn Db) -> bool { + fn is_awaitable(self, db: &'db dyn Db) -> bool { match self { Type::NominalInstance(instance) => { matches!(instance.known_class(db), Some(KnownClass::CoroutineType)) @@ -1786,54 +2213,69 @@ impl<'db> Type<'db> { } /// If the type is a specialized instance of the given `KnownClass`, returns the specialization. - pub(crate) fn known_specialization( + fn known_specialization( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, known_class: KnownClass, ) -> Option> { - let class_literal = known_class.try_to_class_literal(db)?; - self.specialization_of(db, class_literal) + let class_literal = known_class.try_to_class_literal(db, env)?; + self.specialization_of(db, env, class_literal) } /// If the type is a specialized instance of the given class, returns the specialization. - pub(crate) fn specialization_of( + fn specialization_of( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, expected_class: StaticClassLiteral<'_>, ) -> Option> { - self.nominal_class(db)? - .static_class_literal(db) + self.class_specialization(db, env) .filter(|(class_literal, _)| *class_literal == expected_class) - .and_then(|(_, specialization)| specialization) + .map(|(_, specialization)| specialization) } - /// If this type is a class instance, returns the class and its specialization. - pub(crate) fn class_specialization( + /// If this type is a class instance or class-backed `TypedDict`, returns its specialization. + fn class_specialization( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ) -> Option<(StaticClassLiteral<'db>, Specialization<'db>)> { - self.nominal_class(db)? + let class = match self { + Type::TypedDict(typed_dict) => typed_dict.defining_class()?, + _ => self.nominal_class(db, env)?, + }; + + class .static_class_literal(db) .and_then(|(class_literal, specialization)| Some((class_literal, specialization?))) } /// If this type is a class instance, returns its class. - pub(crate) fn nominal_class(self, db: &'db dyn Db) -> Option> { + pub(crate) fn nominal_class( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { match self { - Type::NominalInstance(instance) => Some(instance.class(db)), - Type::ProtocolInstance(instance) => instance.to_nominal_instance().map(|i| i.class(db)), - Type::TypeAlias(alias) => alias.value_type(db).nominal_class(db), - Type::NewTypeInstance(newtype) => newtype.concrete_base_type(db).nominal_class(db), + Type::NominalInstance(instance) => Some(instance.class(db, env)), + Type::ProtocolInstance(instance) => instance.class_origin(db).map(|class| *class), + Type::TypeAlias(alias) => alias.value_type(db).nominal_class(db, env), + Type::NewTypeInstance(newtype) => newtype.concrete_base_type(db).nominal_class(db, env), Type::TypeVar(typevar) => { let TypeVarBoundOrConstraints::UpperBound(bound) = - typevar.typevar(db).bound_or_constraints(db)? + typevar.typevar(db).bound_or_constraints(db, env)? else { return None; }; - bound.nominal_class(db) + bound.nominal_class(db, env) + } + Type::LiteralValue(literal) => { + literal.fallback_instance(db, env).nominal_class(db, env) + } + Type::PropertyInstance(property) => { + property.instance_fallback(db, env).nominal_class(db, env) } - Type::LiteralValue(literal) => literal.fallback_instance(db).nominal_class(db), - Type::PropertyInstance(property) => property.instance_fallback(db).nominal_class(db), _ => None, } } @@ -1843,43 +2285,47 @@ impl<'db> Type<'db> { /// /// This is the case for any type which may contain types in non-covariant position within it, /// e.g., nominal instances of a generic class, or callables. - pub(crate) fn may_prefer_declared_type(self, db: &'db dyn Db) -> bool { - self.class_specialization(db).is_some() || self.expand_eagerly(db).is_callable_type() + fn may_prefer_declared_type(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool { + self.class_specialization(db, env).is_some() + || self.expand_eagerly(db, env).is_callable_type() } /// 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).cached_materialization(db, MaterializationKind::Top) + fn top_materialization(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + (*self).cached_materialization(db, env.program(db), MaterializationKind::Top) } /// 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).cached_materialization(db, MaterializationKind::Bottom) + fn bottom_materialization(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + (*self).cached_materialization(db, env.program(db), MaterializationKind::Bottom) } #[salsa::tracked( returns(copy), - cycle_initial=|_, id, _, materialization_kind| { + cycle_initial=|_, id, _, _, materialization_kind| { Type::Divergent(DivergentType::new(id).materialized(materialization_kind)) }, - cycle_fn=|db, cycle, previous: &Type<'db>, value: Type<'db>, _, _| { - value.cycle_normalized(db, *previous, cycle) + cycle_fn=|db, cycle, previous: &Type<'db>, value: Type<'db>, _, program, _| { + value.cycle_normalized_impl(db, &ProgramEnvironment::from_program(program), *previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] fn cached_materialization( self, db: &'db dyn Db, + program: Program<'db>, materialization_kind: MaterializationKind, ) -> Type<'db> { + let env = &ProgramEnvironment::from_program(program); self.materialize( db, + env, materialization_kind, - &ApplyTypeMappingVisitor::default(), + &ApplyTypeMappingVisitor::new(env), ) } @@ -1887,9 +2333,13 @@ impl<'db> Type<'db> { /// /// I.e., for the type `tuple[int, str]`, this will return the tuple spec `[int, str]`. /// For a subclass of `tuple[int, str]`, it will return the same tuple spec. - fn tuple_instance_spec(&self, db: &'db dyn Db) -> Option>> { + fn tuple_instance_spec( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option>> { self.as_nominal_instance() - .and_then(|instance| instance.tuple_spec(db)) + .and_then(|instance| instance.tuple_spec(db, env)) } /// If this type is an *exact* tuple type (*not* a subclass of `tuple`), returns the @@ -1912,8 +2362,8 @@ impl<'db> Type<'db> { /// 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` (TODO: it should be the `TypeVar`'s upper - /// bound, if any) + /// - In covariant position, it's replaced with `object`, or the type variable's upper bound + /// when the dynamic type is a bounded generic argument /// - In contravariant position, it's replaced with `Never` /// - In invariant position, we replace the object with a special form recording that it's the top /// or bottom materialization. @@ -1924,25 +2374,27 @@ impl<'db> Type<'db> { /// - `materialize()` calls `apply_type_mapping()` (or `apply_type_mapping_impl()`) /// - `materialize_impl()` gets called from `apply_type_mapping()` or from another /// `materialize_impl()` - pub(crate) fn materialize( + fn materialize( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, materialization_kind: MaterializationKind, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Type<'db> { self.apply_type_mapping_impl( db, + env, &TypeMapping::Materialize(materialization_kind), TypeContext::default(), visitor, ) } - pub fn has_dynamic(self, db: &'db dyn Db) -> bool { - any_over_type(db, self, false, |ty| ty.is_dynamic()) + pub fn has_dynamic(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool { + any_over_type(db, env, self, false, |ty| ty.is_dynamic()) } - pub(crate) const fn as_special_form(self) -> Option { + const fn as_special_form(self) -> Option { match self { Type::SpecialForm(special_form) => Some(special_form), _ => None, @@ -1963,7 +2415,7 @@ impl<'db> Type<'db> { } } - pub(crate) const fn as_type_alias(self) -> Option> { + const fn as_type_alias(self) -> Option> { match self { Type::KnownInstance(KnownInstanceType::TypeAliasType(type_alias)) => Some(type_alias), _ => None, @@ -1972,7 +2424,7 @@ impl<'db> Type<'db> { /// If this type is a `Type::TypeAlias`, recursively resolves it to its /// underlying value type. Otherwise, returns `self` unchanged. - pub(crate) fn resolve_type_alias(self, db: &'db dyn Db) -> Type<'db> { + fn resolve_type_alias(self, db: &'db dyn Db) -> Type<'db> { let mut ty = self; while let Type::TypeAlias(alias) = ty { ty = alias.value_type(db); @@ -1980,9 +2432,26 @@ impl<'db> Type<'db> { ty } + /// Selects the constructor used for a type variable's upper bound. + /// + /// The meta-type of `object` simplifies to permissive bare `type`, so retain the exact class + /// object instead. Resolve aliases first so an alias of `object` cannot bypass that behavior. + fn constructor_for_typevar_bound( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { + let bound = self.resolve_type_alias(db); + if bound.is_object() { + KnownClass::Object.to_class_literal(db, env) + } else { + bound.to_meta_type(db, env) + } + } + /// Returns `Some(UnionType)` if this type behaves like a union. Apart from explicit unions, /// this returns `Some` for `TypeAlias`es of unions and `NewType`s of `float` and `complex`. - pub(crate) fn as_union_like(self, db: &'db dyn Db) -> Option> { + fn as_union_like(self, db: &'db dyn Db) -> Option> { match self.resolve_type_alias(db) { Type::Union(union) => Some(union), Type::NewTypeInstance(newtype) => newtype.concrete_base_type(db).as_union_like(db), @@ -1990,25 +2459,25 @@ impl<'db> Type<'db> { } } - pub(crate) const fn as_dynamic(self) -> Option> { + const fn as_dynamic(self) -> Option> { match self { Type::Dynamic(dynamic_type) => Some(dynamic_type), _ => None, } } - pub(crate) const fn as_callable(self) -> Option> { + const fn as_callable(self) -> Option> { match self { Type::Callable(callable_type) => Some(callable_type), _ => None, } } - pub(crate) const fn expect_dynamic(self) -> DynamicType<'db> { + const fn expect_dynamic(self) -> DynamicType<'db> { self.as_dynamic().expect("Expected a Type::Dynamic variant") } - pub(crate) const fn as_protocol_instance(self) -> Option> { + const fn as_protocol_instance(self) -> Option> { match self { Type::ProtocolInstance(instance) => Some(instance), _ => None, @@ -2018,14 +2487,19 @@ impl<'db> Type<'db> { /// basedpython: the protocol's data members, as `(name, instance-access type)` pairs — /// the keyword parameters `(**P) -> R` unpacks to. Methods are excluded: they describe /// how the value behaves, not a keyword a caller can pass. - pub(crate) fn protocol_data_members(self, db: &'db dyn Db) -> Option)>> { + pub(crate) fn protocol_data_members( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option)>> { let protocol = self.as_protocol_instance()?; Some( protocol .interface(db) + .base() .non_method_members(db) .into_iter() - .filter_map(|member| match member.reified_member_shape(db)? { + .filter_map(|member| match member.reified_member_shape(db, env)? { ReifiedMember::Attribute { ty, .. } => Some((Name::new(member.name()), ty)), ReifiedMember::Method { .. } => None, }) @@ -2038,7 +2512,11 @@ impl<'db> Type<'db> { /// The type checker and the transpiler must agree on this, or a `.by` file means one /// thing to `by check` and another to whatever reads its lowered `.py` — so both go /// through this one classifier. - pub fn unpacked_kwargs(self, db: &'db dyn Db) -> Option> { + pub fn unpacked_kwargs( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { let is_pack = match self { Type::TypeVar(typevar) => typevar.is_parameter_pack(db), Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => { @@ -2054,13 +2532,13 @@ impl<'db> Type<'db> { return Some(UnpackedKwargs::TypedDict); } resolved - .protocol_data_members(db) + .protocol_data_members(db, env) .map(UnpackedKwargs::Protocol) } #[cfg(test)] #[track_caller] - pub(crate) const fn expect_class_literal(self) -> ClassLiteral<'db> { + const fn expect_class_literal(self) -> ClassLiteral<'db> { self.as_class_literal() .expect("Expected a Type::ClassLiteral variant") } @@ -2115,11 +2593,11 @@ impl<'db> Type<'db> { } } - pub(crate) const fn is_typed_dict(&self) -> bool { + const fn is_typed_dict(&self) -> bool { matches!(self, Type::TypedDict(..)) } - pub(crate) const fn as_typed_dict(self) -> Option> { + const fn as_typed_dict(self) -> Option> { match self { Type::TypedDict(typed_dict) => Some(typed_dict), _ => None, @@ -2137,13 +2615,13 @@ impl<'db> Type<'db> { } } - pub const fn is_property_instance(&self) -> bool { + const fn is_property_instance(&self) -> bool { matches!(self, Type::PropertyInstance(..)) } pub(crate) fn module_literal( db: &'db dyn Db, - importing_file: File, + importing_file: ProgramFile<'db>, submodule: Module<'db>, ) -> Self { Self::ModuleLiteral(ModuleLiteralType::new( @@ -2153,7 +2631,7 @@ impl<'db> Type<'db> { )) } - pub(crate) const fn is_union(self) -> bool { + const fn is_union(self) -> bool { matches!(self, Type::Union(_)) } @@ -2172,18 +2650,18 @@ impl<'db> Type<'db> { #[cfg(test)] #[track_caller] - pub(crate) const fn expect_union(self) -> UnionType<'db> { + const fn expect_union(self) -> UnionType<'db> { self.as_union().expect("Expected a Type::Union variant") } - pub(crate) const fn is_intersection(self) -> bool { + const fn is_intersection(self) -> bool { matches!(self, Type::Intersection(_)) } /// Returns whether this is a "real" intersection type. (Negated types are represented by an /// intersection containing a single negative branch, which this method does _not_ consider a /// "real" intersection.) - pub(crate) fn is_nontrivial_intersection(self, db: &'db dyn Db) -> bool { + fn is_nontrivial_intersection(self, db: &'db dyn Db) -> bool { match self { Type::Intersection(intersection) => !intersection.is_simple_negation(db), _ => false, @@ -2199,7 +2677,7 @@ impl<'db> Type<'db> { #[cfg(test)] #[track_caller] - pub(crate) fn expect_function_literal(self) -> FunctionType<'db> { + fn expect_function_literal(self) -> FunctionType<'db> { self.as_function_literal() .expect("Expected a Type::FunctionLiteral variant") } @@ -2208,21 +2686,21 @@ impl<'db> Type<'db> { matches!(self, Type::FunctionLiteral(..)) } - pub(crate) fn as_string_literal(self) -> Option> { + fn as_string_literal(self) -> Option> { match self { Type::LiteralValue(literal) => literal.as_string(), _ => None, } } - pub(crate) fn as_int_literal(self) -> Option { + fn as_int_literal(self) -> Option { match self { Type::LiteralValue(literal) => literal.as_int(), _ => None, } } - pub(crate) fn as_int_like_literal(self) -> Option { + fn as_int_like_literal(self) -> Option { match self.as_literal_value_kind() { Some(LiteralValueTypeKind::Int(value)) => Some(value.as_i64()), Some(LiteralValueTypeKind::Bool(value)) => Some(i64::from(value)), @@ -2239,25 +2717,29 @@ impl<'db> Type<'db> { #[cfg(test)] #[track_caller] - pub(crate) fn expect_enum_literal(self) -> EnumLiteralType<'db> { + fn expect_enum_literal(self) -> EnumLiteralType<'db> { match self.as_literal_value_kind() { Some(LiteralValueTypeKind::Enum(e)) => e, _ => panic!("Expected a `LiteralValueTypeKind::Enum` variant"), } } - pub(crate) fn is_string_literal(&self) -> bool { + fn is_string_literal(&self) -> bool { self.as_literal_value() .is_some_and(literal::LiteralValueType::is_string) } /// Detects types which are valid to appear inside a `Literal[…]` type annotation. - pub(crate) fn is_literal_or_union_of_literals(&self, db: &'db dyn Db) -> bool { + fn is_literal_or_union_of_literals( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> bool { match self { Type::Union(union) => union .elements(db) .iter() - .all(|ty| ty.is_literal_or_union_of_literals(db)), + .all(|ty| ty.is_literal_or_union_of_literals(db, env)), Type::LiteralValue(literal) => match literal.kind() { LiteralValueTypeKind::String(_) | LiteralValueTypeKind::Bytes(_) @@ -2268,7 +2750,9 @@ impl<'db> Type<'db> { | LiteralValueTypeKind::Complex(_) => true, LiteralValueTypeKind::LiteralString => false, }, - Type::NominalInstance(_) => self.is_none(db) || self.is_bool(db) || self.is_enum(db), + Type::NominalInstance(_) => { + self.is_none(db) || self.is_bool(db) || self.is_enum(db, env) + } _ => false, } } @@ -2285,7 +2769,7 @@ impl<'db> Type<'db> { } /// Create a promotable enum literal. - pub(crate) fn enum_literal(value: EnumLiteralType<'db>) -> Self { + fn enum_literal(value: EnumLiteralType<'db>) -> Self { Self::LiteralValue(LiteralValueType::promotable(value)) } @@ -2317,7 +2801,7 @@ impl<'db> Type<'db> { } /// Create a promotable single-character string literal. - pub(crate) fn single_char_string_literal(db: &'db dyn Db, c: char) -> Self { + fn single_char_string_literal(db: &'db dyn Db, c: char) -> Self { Self::LiteralValue(LiteralValueType::promotable(StringLiteralType::new( db, c.to_compact_string(), @@ -2325,7 +2809,7 @@ impl<'db> Type<'db> { } /// Create a promotable bytes literal. - pub(crate) fn bytes_literal(db: &'db dyn Db, bytes: &[u8]) -> Self { + fn bytes_literal(db: &'db dyn Db, bytes: &[u8]) -> Self { Self::LiteralValue(LiteralValueType::promotable(BytesLiteralType::new( db, bytes, ))) @@ -2337,28 +2821,28 @@ impl<'db> Type<'db> { } /// Create a `LiteralString`. - pub(crate) fn literal_string() -> Self { + fn literal_string() -> Self { // Note that `LiteralString`s are never implicitly inferred, and so are always unpromotable. Self::LiteralValue(LiteralValueType::unpromotable( LiteralValueTypeKind::LiteralString, )) } - pub(crate) fn typed_dict(defining_class: impl Into>) -> Self { + fn typed_dict(defining_class: impl Into>) -> Self { Self::TypedDict(TypedDictType::new(defining_class.into())) } #[must_use] - pub(crate) fn negate(&self, db: &'db dyn Db) -> Type<'db> { + fn negate(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { // Avoid invoking the `IntersectionBuilder` for negations that are trivial. // // We verify that this always produces the same result as - // `IntersectionBuilder::new(db).add_negative(*self).build()` via the + // `IntersectionBuilder::new(db, env).add_negative(*self).build()` via the // property test `all_negated_types_identical_to_intersection_with_single_negated_element` match self { - Type::Overlapping(overlapping) => overlapping.value_type(db).negate(db), - Type::Restricted(restricted) => restricted.value_type(db).negate(db), - Type::Deferred(deferred) => deferred.reduced(db).negate(db), + Type::Overlapping(overlapping) => overlapping.value_type(db, env).negate(db, env), + Type::Restricted(restricted) => restricted.value_type(db).negate(db, env), + Type::Deferred(deferred) => deferred.reduced(db, env).negate(db, env), Type::Never => Type::object(), Type::Dynamic(_) => *self, @@ -2404,40 +2888,39 @@ impl<'db> Type<'db> { Type::Union(_) | Type::Intersection(_) | Type::EnumComplement(_) - | Type::UnsafeUnion(_) => IntersectionBuilder::new(db).add_negative(*self).build(), + | Type::UnsafeUnion(_) => IntersectionBuilder::new(db, env) + .add_negative(*self) + .build(), } } #[must_use] - pub(crate) fn negate_if(&self, db: &'db dyn Db, yes: bool) -> Type<'db> { - if yes { self.negate(db) } else { *self } + fn negate_if(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>, yes: bool) -> Type<'db> { + if yes { self.negate(db, env) } else { *self } } /// Return `true` if it is possible to spell an equivalent type to this one /// in user annotations without nonstandard extensions to the type system - pub(crate) fn is_spellable(&self, db: &'db dyn Db) -> bool { + fn is_spellable(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool { match self { - Type::Overlapping(overlapping) => overlapping.value_type(db).is_spellable(db), - Type::Restricted(restricted) => restricted.value_type(db).is_spellable(db), - Type::Deferred(deferred) => deferred.reduced(db).is_spellable(db), + Type::Overlapping(overlapping) => overlapping.value_type(db, env).is_spellable(db, env), + Type::Restricted(restricted) => restricted.value_type(db).is_spellable(db, env), + Type::Deferred(deferred) => deferred.reduced(db, env).is_spellable(db, env), Type::LiteralValue(_) | Type::Never | Type::NewTypeInstance(_) - | Type::NominalInstance(_) + | Type::NominalInstance(_) => true, // `TypedDict` and `Protocol` can be synthesized, // but it's always possible to create an equivalent type using a class definition. - | Type::TypedDict(_) - | Type::ProtocolInstance(_) + Type::TypedDict(_) | Type::ProtocolInstance(_) => true, // Not all `Callable` types are spellable using the `Callable` type form, // but they are all spellable using callback protocols. - | Type::Callable(_) + Type::Callable(_) => true, // `Unknown` and `@Todo` are nonstandard extensions, // but they are both exactly equivalent to `Any` - | Type::Dynamic(_) - | Type::TypeVar(_) - | Type::TypeAlias(_) - | Type::SubclassOf(_) => true, - Type::TypeForm(typeform) => typeform.type_argument(db).is_spellable(db), + Type::Dynamic(_) => true, + Type::TypeVar(_) | Type::TypeAlias(_) | Type::SubclassOf(_) => true, + Type::TypeForm(typeform) => typeform.type_argument(db).is_spellable(db, env), Type::Intersection(_) | Type::UnsafeUnion(_) => false, Type::EnumComplement(complement) => complement.is_spellable(db), Type::Divergent(_) @@ -2458,17 +2941,17 @@ impl<'db> Type<'db> { | Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::KnownInstance(_) => false, - Type::Union(union) => union.elements(db).iter().all(|ty| ty.is_spellable(db)), + Type::Union(union) => union.elements(db).iter().all(|ty| ty.is_spellable(db, env)), } } /// Return `true` if `self` is a type that is suitable for displaying /// in a "Did you mean...?" hint message in diagnostics - fn is_hintable(&self, db: &'db dyn Db) -> bool { + fn is_hintable(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool { match self { - Type::Overlapping(overlapping) => overlapping.value_type(db).is_hintable(db), - Type::Restricted(restricted) => restricted.value_type(db).is_hintable(db), - Type::Deferred(deferred) => deferred.reduced(db).is_hintable(db), + Type::Overlapping(overlapping) => overlapping.value_type(db, env).is_hintable(db, env), + Type::Restricted(restricted) => restricted.value_type(db).is_hintable(db, env), + Type::Deferred(deferred) => deferred.reduced(db, env).is_hintable(db, env), Type::NominalInstance(_) | Type::NewTypeInstance(_) | Type::LiteralValue(_) @@ -2510,17 +2993,17 @@ impl<'db> Type<'db> { Type::SubclassOf(subclass_of) => match subclass_of.subclass_of() { SubclassOfInner::Class(_) => true, SubclassOfInner::Protocol(_) => true, - SubclassOfInner::Dynamic(dynamic) => Type::Dynamic(dynamic).is_hintable(db), - SubclassOfInner::TypeVar(tvar) => Type::TypeVar(tvar).is_hintable(db), + SubclassOfInner::Dynamic(dynamic) => Type::Dynamic(dynamic).is_hintable(db, env), + SubclassOfInner::TypeVar(tvar) => Type::TypeVar(tvar).is_hintable(db, env), }, Type::TypeVar(tvar) => tvar.typevar(db).definition(db).is_some(), - Type::Union(union) => union.elements(db).iter().all(|ty| ty.is_hintable(db)), + Type::Union(union) => union.elements(db).iter().all(|ty| ty.is_hintable(db, env)), Type::TypedDict(td) => td.defining_class().is_some(), - Type::ProtocolInstance(ProtocolInstanceType { inner, .. }) => !inner.is_synthesized(), + Type::ProtocolInstance(protocol) => protocol.class_origin(db).is_some(), Type::Dynamic(dynamic) => match dynamic { DynamicType::Any => true, @@ -2538,11 +3021,7 @@ impl<'db> Type<'db> { /// based on the provided predicate. /// /// Otherwise, returns the type unchanged. - pub(crate) fn filter_union( - self, - db: &'db dyn Db, - f: impl FnMut(&Type<'db>) -> bool, - ) -> Type<'db> { + fn filter_union(self, db: &'db dyn Db, f: impl FnMut(&Type<'db>) -> bool) -> Type<'db> { if let Type::Union(union) = self.resolve_type_alias(db) { union.filter(db, f) } else { @@ -2553,22 +3032,21 @@ impl<'db> Type<'db> { /// If the type is a union, removes union elements that are disjoint from `target`. /// /// Otherwise, returns the type unchanged. - pub(crate) fn filter_disjoint_elements( + fn filter_disjoint_elements( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, target: Type<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, ) -> Type<'db> { let constraints = ConstraintSetBuilder::new(); self.filter_union(db, |elem| { !elem - .when_disjoint_from(db, target, &constraints, inferable) - .is_always_satisfied(db) + .when_disjoint_from(db, env, target, &constraints, inferable) + .is_always_satisfied(db, env) }) } - /// Returns the fallback instance type that a literal is an instance of, or `None` if the type - /// is not a literal. /// basedpython: whether this is a `type def` — a type function, applied with /// `[]` in a type expression and evaluated by executing its body pub fn is_type_fn(self, db: &'db dyn Db) -> bool { @@ -2591,15 +3069,21 @@ impl<'db> Type<'db> { self == Type::literal_string() } - pub(crate) fn literal_fallback_instance(self, db: &'db dyn Db) -> Option> { + /// Returns the fallback instance type that a literal is an instance of, or `None` if the type + /// is not a literal. + pub(crate) fn literal_fallback_instance( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'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::ModuleLiteral(_) => Some(KnownClass::ModuleType.to_instance(db)), - Type::FunctionLiteral(_) => Some(KnownClass::FunctionType.to_instance(db)), - Type::LiteralValue(literal) => Some(literal.fallback_instance(db)), + Type::ModuleLiteral(_) => Some(KnownClass::ModuleType.to_instance(db, env)), + Type::FunctionLiteral(_) => Some(KnownClass::FunctionType.to_instance(db, env)), + Type::LiteralValue(literal) => Some(literal.fallback_instance(db, env)), _ => None, } } @@ -2610,9 +3094,10 @@ impl<'db> Type<'db> { /// fallback instance type. For example, `def _() -> int` is promoted to `Callable[[], int]`, /// as opposed to `FunctionType`. #[must_use] - pub fn promote(self, db: &'db dyn Db) -> Type<'db> { + pub fn promote(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { self.apply_type_mapping( db, + env, &TypeMapping::Promote(PromotionMode::On, PromotionKind::Regular), TypeContext::default(), ) @@ -2628,7 +3113,12 @@ impl<'db> Type<'db> { /// A `.by` file has that model by definition rather than by configuration, so it takes /// the strict path whatever `strict-float` says. #[must_use] - pub(crate) fn promote_in(self, db: &'db dyn Db, file: ruff_db::files::File) -> Type<'db> { + pub(crate) fn promote_in( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + file: ruff_db::files::File, + ) -> Type<'db> { let kind = if file.source_type(db).is_basedpython() || db.analysis_settings(file).strict_float { PromotionKind::RegularStrictNumeric @@ -2637,14 +3127,19 @@ impl<'db> Type<'db> { }; self.apply_type_mapping( db, + env, &TypeMapping::Promote(PromotionMode::On, kind), TypeContext::default(), ) } /// Promote a top-level singleton type (like `None`, `EllipsisType`) to `T | Unknown`. - pub(crate) fn promote_singletons(self, db: &'db dyn Db) -> Type<'db> { - self.promote_singletons_impl(db) + pub(crate) fn promote_singletons( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { + self.promote_singletons_impl(db, env) } /// Promote class literals to the class objects represented by `type[...]`. @@ -2652,9 +3147,10 @@ impl<'db> Type<'db> { /// This is intentionally separate from regular promotion. Applying it during collection /// inference would lose useful precision for local and module-level collections of class /// objects. - pub(crate) fn promote_class_literals(self, db: &'db dyn Db) -> Type<'db> { + fn promote_class_literals(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { self.apply_type_mapping( db, + env, &TypeMapping::Promote(PromotionMode::On, PromotionKind::ClassLiteralsOnly), TypeContext::default(), ) @@ -2664,36 +3160,47 @@ impl<'db> Type<'db> { /// `T | Unknown` within nominal type parameters, without recursing into unions. /// Used for collection literal inference so that `[None]` is inferred as /// `list[None | Unknown]` rather than `list[None]`. - pub(crate) fn promote_singletons_recursively(self, db: &'db dyn Db) -> Type<'db> { + fn promote_singletons_recursively( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { self.apply_type_mapping( db, + env, &TypeMapping::Promote(PromotionMode::On, PromotionKind::SingletonsOnly), TypeContext::default(), ) } /// Like [`Type::promote`], but does not recurse into nested types. - fn promote_impl(self, db: &'db dyn Db) -> Type<'db> { + fn promote_impl(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match self { - Type::LiteralValue(literal) if literal.is_promotable() => literal.fallback_instance(db), + Type::LiteralValue(literal) if literal.is_promotable() => { + literal.fallback_instance(db, env) + } Type::FunctionLiteral(literal) => Type::Callable(literal.into_callable_type(db)), _ => self, } } /// Like [`Type::promote_impl`], but leaves a literal value as it is. - fn promote_impl_keeping_literals(self, db: &'db dyn Db) -> Type<'db> { + fn promote_impl_keeping_literals( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { match self { Type::LiteralValue(_) => self, - _ => self.promote_impl(db), + _ => self.promote_impl(db, env), } } /// Like [`Type::promote_singletons_recursively`], but does not recurse into nested types. - fn promote_singletons_impl(self, db: &'db dyn Db) -> Type<'db> { + fn promote_singletons_impl(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match self { Type::NominalInstance(instance) if instance.is_singleton(db) => { - UnionType::from_two_elements(db, self, Type::unknown()) + UnionType::from_two_elements(db, env, self, Type::unknown()) } _ => self, } @@ -2714,14 +3221,28 @@ impl<'db> Type<'db> { /// If this continues, the query will not converge, so this method is called in the cycle recovery function. /// Then `tuple[tuple[Divergent, Literal[1]], Literal[1]]` is replaced with `tuple[Divergent, Literal[1]]` and the query converges. #[must_use] - pub(crate) fn recursive_type_normalized(self, db: &'db dyn Db, cycle: &salsa::Cycle) -> Self { - cycle.head_ids().fold(self, |ty, id| { - ty.recursive_type_normalized_impl(db, Type::divergent(id), false) - .unwrap_or(Type::divergent(id)) - }) + pub(crate) fn recursive_type_normalized( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + cycle: &salsa::Cycle, + ) -> Self { + self.recursive_type_normalized_impl_with_cycle(db, env, cycle) } - /// Normalizes types including divergent types (recursive types), which is necessary for convergence of fixed-point iteration. + fn recursive_type_normalized_impl_with_cycle( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + cycle: &salsa::Cycle, + ) -> Self { + cycle.head_ids().fold(self, |ty, id| { + ty.recursive_type_normalized_impl(db, env, Type::divergent(id), false) + .unwrap_or(Type::divergent(id)) + }) + } + + /// Normalizes types including divergent types (recursive types), which is necessary for convergence of fixed-point iteration. /// When `nested` is true, propagate `None`. That is, if the type contains a `Divergent` type, the return value of this method is `None` (so we can use the `?` operator). /// When `nested` is false, create a type containing `Divergent` types instead of propagating `None` (we should use `unwrap_or(Divergent)`). /// This is to preserve the structure of the non-divergent parts of the type instead of completely collapsing the type containing a `Divergent` type into a `Divergent` type. @@ -2740,6 +3261,7 @@ impl<'db> Type<'db> { fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -2747,79 +3269,81 @@ impl<'db> Type<'db> { return None; } match self { - Type::Union(union) => union.recursive_type_normalized_impl(db, div, nested), + Type::Union(union) => union.recursive_type_normalized_impl(db, env, div, nested), Type::Intersection(intersection) => intersection - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Type::Intersection), // Like unions and intersections, an unsafe union is "flat" from the perspective // of recursive types, so `nested` is passed through unchanged. Type::UnsafeUnion(unsafe_union) => unsafe_union.try_map_elements(db, |element| { - element.recursive_type_normalized_impl(db, div, nested) + element.recursive_type_normalized_impl(db, env, div, nested) }), Type::EnumComplement(complement) => complement - .to_intersection(db) - .recursive_type_normalized_impl(db, div, nested), + .to_intersection(db, env) + .recursive_type_normalized_impl(db, env, div, nested), Type::Callable(callable) => callable - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Type::Callable), Type::ProtocolInstance(protocol) => protocol - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Type::ProtocolInstance), Type::NominalInstance(instance) => instance - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Type::NominalInstance), Type::FunctionLiteral(function) => function - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Type::FunctionLiteral), Type::PropertyInstance(property) => property - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Type::PropertyInstance), Type::KnownBoundMethod(method_kind) => method_kind - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Type::KnownBoundMethod), Type::BoundMethod(method) => method - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Type::BoundMethod), Type::BoundSuper(bound_super) => bound_super - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Type::BoundSuper), Type::GenericAlias(generic) => generic - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Type::GenericAlias), Type::ClassLiteral(class) => class - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Type::ClassLiteral), Type::SubclassOf(subclass_of) => subclass_of - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Type::SubclassOf), Type::TypeVar(_) => Some(self), Type::KnownInstance(known_instance) => known_instance - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Type::KnownInstance), Type::TypeIs(type_is) => { - recursive_type_normalize_type_guard_like(db, type_is, div, nested) + recursive_type_normalize_type_guard_like(db, env, type_is, div, nested) } Type::TypeGuard(type_guard) => { - recursive_type_normalize_type_guard_like(db, type_guard, div, nested) + recursive_type_normalize_type_guard_like(db, env, type_guard, div, nested) } Type::TypeForm(typeform) => typeform .type_argument(db) - .recursive_type_normalized_impl(db, div, true) + .recursive_type_normalized_impl(db, env, div, true) .map(|ty| TypeFormType::from_type_expression(db, ty)), Type::Overlapping(overlapping) => overlapping .type_argument(db) - .recursive_type_normalized_impl(db, div, true) + .recursive_type_normalized_impl(db, env, div, true) .map(|ty| OverlappingType::from_type_expression(db, ty)), Type::Restricted(restricted) => restricted .type_argument(db) - .recursive_type_normalized_impl(db, div, true) - .map(|ty| RestrictedType::from_type_expression(db, restricted.modifier(db), ty)), + .recursive_type_normalized_impl(db, env, div, true) + .map(|ty| { + RestrictedType::from_type_expression(db, env, restricted.modifier(db), ty) + }), Type::Deferred(deferred) => { let mut operands = Vec::with_capacity(deferred.operands(db).len()); for operand in deferred.operands(db) { - operands.push(operand.recursive_type_normalized_impl(db, div, true)?); + operands.push(operand.recursive_type_normalized_impl(db, env, div, true)?); } - Some(deferred.re_evaluate(db, operands.into_boxed_slice())) + Some(deferred.re_evaluate(db, env, operands.into_boxed_slice())) } Type::Divergent(_) => Some(self), Type::Dynamic(dynamic) => Some(Type::Dynamic(dynamic.recursive_type_normalized())), @@ -2829,7 +3353,7 @@ impl<'db> Type<'db> { } Type::TypeAlias(_) => Some(self), Type::NewTypeInstance(newtype) => newtype - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Type::NewTypeInstance), Type::AlwaysFalsy | Type::AlwaysTruthy @@ -2847,12 +3371,13 @@ impl<'db> Type<'db> { /// /// The provided closure will be called on any nested types, along with their variance with /// respect to the outermost type. - pub(crate) fn visit_specialization(self, db: &'db dyn Db, mut f: F) + fn visit_specialization(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>, mut f: F) where F: FnMut(Type<'db>, TypeVarVariance), { self.visit_specialization_impl( db, + env, TypeVarVariance::Covariant, &mut f, &SpecializationVisitor::default(), @@ -2862,26 +3387,27 @@ impl<'db> Type<'db> { fn visit_specialization_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, polarity: TypeVarVariance, f: &mut dyn FnMut(Type<'db>, TypeVarVariance), visitor: &SpecializationVisitor<'db>, ) { - let Some((_, specialization)) = self.class_specialization(db) else { + let Some((_, specialization)) = self.class_specialization(db, env) else { match self { Type::Union(union) => { for element in union.elements(db) { - element.visit_specialization_impl(db, polarity, f, visitor); + element.visit_specialization_impl(db, env, polarity, f, visitor); } } Type::Intersection(intersection) => { for element in intersection.positive(db) { - element.visit_specialization_impl(db, polarity, f, visitor); + element.visit_specialization_impl(db, env, polarity, f, visitor); } } Type::TypeAlias(alias) => visitor.visit(db, self, || { alias .value_type(db) - .visit_specialization_impl(db, polarity, f, visitor); + .visit_specialization_impl(db, env, polarity, f, visitor); }), Type::Callable(callable) => { for signature in callable.signatures(db) { @@ -2893,14 +3419,14 @@ impl<'db> Type<'db> { visitor.visit(db, parameter.annotated_type(), || { parameter .annotated_type() - .visit_specialization_impl(db, variance, f, visitor); + .visit_specialization_impl(db, env, variance, f, visitor); }); } visitor.visit(db, signature.return_ty, || { signature .return_ty - .visit_specialization_impl(db, polarity, f, visitor); + .visit_specialization_impl(db, env, polarity, f, visitor); }); } } @@ -2919,7 +3445,7 @@ impl<'db> Type<'db> { f(*ty, variance); visitor.visit(db, *ty, || { - ty.visit_specialization_impl(db, variance, f, visitor); + ty.visit_specialization_impl(db, env, variance, f, visitor); }); } } @@ -2928,11 +3454,11 @@ impl<'db> Type<'db> { /// /// 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 { + fn is_singleton(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool { match self { - Type::Overlapping(overlapping) => overlapping.value_type(db).is_singleton(db), - Type::Restricted(restricted) => restricted.value_type(db).is_singleton(db), - Type::Deferred(deferred) => deferred.reduced(db).is_singleton(db), + Type::Overlapping(overlapping) => overlapping.value_type(db, env).is_singleton(db, env), + Type::Restricted(restricted) => restricted.value_type(db).is_singleton(db, env), + Type::Deferred(deferred) => deferred.reduced(db, env).is_singleton(db, env), Type::Dynamic(_) | Type::Divergent(_) | Type::Never => false, Type::LiteralValue(literal) => match literal.kind() { @@ -2977,13 +3503,13 @@ impl<'db> Type<'db> { // 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(bound_typevar) => { - match bound_typevar.typevar(db).bound_or_constraints(db) { + match bound_typevar.typevar(db).bound_or_constraints(db, env) { None => false, Some(TypeVarBoundOrConstraints::UpperBound(_)) => false, Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints .elements(db) .iter() - .all(|constraint| constraint.is_singleton(db)), + .all(|constraint| constraint.is_singleton(db, env)), } } @@ -2995,15 +3521,7 @@ impl<'db> Type<'db> { | Type::WrapperDescriptor(..) | Type::ClassLiteral(..) | 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::SpecialForm(special_form) => special_form.is_guaranteed_singleton(), Type::KnownInstance(KnownInstanceType::Sentinel(_)) => true, Type::KnownInstance(_) => false, Type::Callable(_) => { @@ -3013,7 +3531,7 @@ impl<'db> Type<'db> { false } Type::BoundMethod(..) => { - // `BoundMethod` types are single-valued types, but not singleton types: + // `BoundMethod` types are not singleton types: // ```pycon // >>> class Foo: // ... def bar(self): pass @@ -3038,7 +3556,7 @@ impl<'db> Type<'db> { false } Type::Intersection(intersection) => intersection - .enum_complement(db) + .enum_complement(db, env) .is_some_and(|complement| complement.is_singleton(db)), Type::EnumComplement(complement) => complement.is_singleton(db), // Even if every element were a singleton, they are different singletons; which one @@ -3049,110 +3567,8 @@ impl<'db> Type<'db> { Type::TypeGuard(type_guard) => type_guard.is_bound(db), Type::TypeForm(_) => false, Type::TypedDict(_) => false, - Type::TypeAlias(alias) => alias.value_type(db).is_singleton(db), - Type::NewTypeInstance(newtype) => newtype.concrete_base_type(db).is_singleton(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::Overlapping(overlapping) => overlapping.value_type(db).is_single_valued(db), - Type::Restricted(restricted) => restricted.value_type(db).is_single_valued(db), - Type::Deferred(deferred) => deferred.reduced(db).is_single_valued(db), - // All empty ranges compare equal, but non-empty ranges can contain different values. - Type::KnownInstance(KnownInstanceType::Range { is_non_empty }) => !is_non_empty, - - // Each `partial()` call creates a distinct object at runtime. - Type::KnownInstance( - KnownInstanceType::FunctoolsPartial(_) | KnownInstanceType::FunctoolsPartialCall(_), - ) => false, - - Type::FunctionLiteral(..) - | Type::WrapperDescriptor(_) - | Type::KnownBoundMethod(_) - | Type::ModuleLiteral(..) - | Type::ClassLiteral(..) - | Type::GenericAlias(..) - | Type::SpecialForm(..) - | Type::KnownInstance(..) => true, - - Type::LiteralValue(literal) => match literal.kind() { - LiteralValueTypeKind::Enum(..) => !self.overrides_equality(db), - - LiteralValueTypeKind::Int(..) - | LiteralValueTypeKind::String(..) - | LiteralValueTypeKind::Bytes(..) - | LiteralValueTypeKind::Bool(_) - | LiteralValueTypeKind::Float(..) - | LiteralValueTypeKind::Complex(..) => true, - - LiteralValueTypeKind::LiteralString => false, - }, - - 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(bound_typevar) => { - match bound_typevar.typevar(db).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::NominalInstance(instance) => instance.is_single_valued(db), - Type::NewTypeInstance(newtype) => newtype.concrete_base_type(db).is_single_valued(db), - - Type::BoundSuper(_) => { - // At runtime two super instances never compare equal, even if their arguments are identical. - false - } - - Type::BoundMethod(_) => { - // Binding the same method to different instances yields different objects: `[].sort != [].sort` - false - } - - Type::TypeIs(type_is) => type_is.is_bound(db), - Type::TypeGuard(type_guard) => type_guard.is_bound(db), - Type::TypeForm(_) => false, - - Type::TypeAlias(alias) => alias.value_type(db).is_single_valued(db), - - Type::Dynamic(_) - | Type::Divergent(_) - | Type::Never - | Type::Union(..) - | Type::UnsafeUnion(_) - | Type::AlwaysTruthy - | Type::AlwaysFalsy - | Type::Callable(_) - | Type::PropertyInstance(_) - | Type::DataclassDecorator(_) - | Type::DataclassTransformer(_) - | Type::TypedDict(_) => false, - - Type::Intersection(intersection) => intersection - .enum_complement(db) - .is_some_and(|complement| complement.is_single_valued(db)), - Type::EnumComplement(complement) => complement.is_single_valued(db), + Type::TypeAlias(alias) => alias.value_type(db).is_singleton(db, env), + Type::NewTypeInstance(newtype) => newtype.concrete_base_type(db).is_singleton(db, env), } } @@ -3163,32 +3579,38 @@ impl<'db> Type<'db> { /// /// [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( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> Option> { + self.find_name_in_mro_with_policy(db, env, name, MemberLookupPolicy::default()) } fn find_name_in_mro_with_policy( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, policy: MemberLookupPolicy, ) -> Option> { if let Some(fallback) = (*self).materialized_divergent_fallback() { - return fallback.find_name_in_mro_with_policy(db, name, policy); + return fallback.find_name_in_mro_with_policy(db, env, name, policy); } match self { Type::Overlapping(overlapping) => overlapping - .value_type(db) - .find_name_in_mro_with_policy(db, name, policy), + .value_type(db, env) + .find_name_in_mro_with_policy(db, env, name, policy), Type::Restricted(restricted) => restricted .value_type(db) - .find_name_in_mro_with_policy(db, name, policy), + .find_name_in_mro_with_policy(db, env, name, policy), Type::Deferred(deferred) => deferred - .reduced(db) - .find_name_in_mro_with_policy(db, name, policy), - Type::Union(union) => Some(union.map_with_boundness_and_qualifiers(db, |elem| { - elem.find_name_in_mro_with_policy(db, name, policy) + .reduced(db, env) + .find_name_in_mro_with_policy(db, env, name, policy), + Type::Union(union) => Some(union.map_with_boundness_and_qualifiers(db, env, |elem| { + elem.find_name_in_mro_with_policy(db, env, 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 @@ -3196,8 +3618,8 @@ impl<'db> Type<'db> { .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) + Some(inter.map_with_boundness_and_qualifiers(db, env, |elem| { + elem.find_name_in_mro_with_policy(db, env, name, policy) // Fall back to Unbound, similar to the union case (see above). .unwrap_or_default() })) @@ -3205,7 +3627,7 @@ impl<'db> Type<'db> { Type::UnsafeUnion(unsafe_union) => { Some(unsafe_union.map_with_boundness_and_qualifiers(db, |elem| { - elem.find_name_in_mro_with_policy(db, name, policy) + elem.find_name_in_mro_with_policy(db, env, name, policy) // Fall back to Unbound, similar to the union case (see above). .unwrap_or_default() })) @@ -3216,7 +3638,7 @@ impl<'db> Type<'db> { Type::Dynamic(_) | Type::Divergent(_) | Type::Never => Some(Place::bound(self).into()), Type::ClassLiteral(class) if class.is_typed_dict(db) => { - Some(class.typed_dict_member(db, None, name, policy)) + Some(class.typed_dict_member(db, env, None, name, policy)) } Type::ClassLiteral(class) => { @@ -3250,27 +3672,33 @@ impl<'db> Type<'db> { .into(), ), - _ => Some(class.class_member(db, name, policy)), + _ => Some(class.class_member(db, env, name, policy)), } } Type::GenericAlias(alias) if alias.is_typed_dict(db) => { - Some(alias.origin(db).typed_dict_member(db, None, name, policy)) + Some(alias.origin(db).typed_dict_member( + db, + env, + (name == "__init__").then_some(alias.specialization(db)), + name, + policy, + )) } Type::GenericAlias(alias) => { - Some(ClassType::from(*alias).class_member(db, name, policy)) + Some(ClassType::from(*alias).class_member(db, env, name, policy)) } Type::SubclassOf(subclass_of_ty) => { - subclass_of_ty.find_name_in_mro_with_policy(db, name, policy) + subclass_of_ty.find_name_in_mro_with_policy(db, env, 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), + .to_class_literal(db, env) + .find_name_in_mro_with_policy(db, env, 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 @@ -3281,14 +3709,14 @@ impl<'db> Type<'db> { Some(Place::Undefined.into()) } else { KnownClass::Object - .to_class_literal(db) - .find_name_in_mro_with_policy(db, name, policy) + .to_class_literal(db, env) + .find_name_in_mro_with_policy(db, env, name, policy) } } Type::TypeAlias(alias) => alias .value_type(db) - .find_name_in_mro_with_policy(db, name, policy), + .find_name_in_mro_with_policy(db, env, name, policy), Type::FunctionLiteral(_) | Type::Callable(_) @@ -3316,21 +3744,26 @@ impl<'db> Type<'db> { } } - fn lookup_dunder_new(self, db: &'db dyn Db) -> Option> { - #[salsa::tracked(returns(copy), cycle_initial=|_, _, _, ()| None, heap_size=ruff_memory_usage::heap_size)] + fn lookup_dunder_new( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { + #[salsa::tracked(returns(copy), cycle_initial=|_, _, _, _| None, heap_size=ruff_memory_usage::heap_size)] fn lookup_dunder_new_inner<'db>( db: &'db dyn Db, + program: Program<'db>, ty: Type<'db>, - _: (), ) -> Option> { + let env = &ProgramEnvironment::from_program(program); let mut flags = MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK; - if !ty.is_subtype_of(db, KnownClass::Type.to_instance(db)) { + if !ty.is_subtype_of(db, env, KnownClass::Type.to_instance(db, env)) { flags |= MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK; } - ty.find_name_in_mro_with_policy(db, "__new__", flags) + ty.find_name_in_mro_with_policy(db, env, "__new__", flags) } - lookup_dunder_new_inner(db, self, ()) + lookup_dunder_new_inner(db, env.program(db), self) } /// Look up an attribute in the MRO of the meta-type of `self`. This returns class-level attributes @@ -3338,24 +3771,33 @@ impl<'db> Type<'db> { /// /// 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: &str) -> PlaceAndQualifiers<'db> { - self.class_member_with_policy(db, name, MemberLookupPolicy::default()) + fn class_member( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> PlaceAndQualifiers<'db> { + self.class_member_with_policy(db, env, name, MemberLookupPolicy::default()) } fn class_member_with_policy( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, policy: MemberLookupPolicy, ) -> PlaceAndQualifiers<'db> { - Self::class_member_with_policy_inner(db, MemberLookupKey::new(db, self, name, policy)) + Self::class_member_with_policy_inner( + db, + MemberLookupKey::new(db, env.program(db), self, name, policy), + ) } #[salsa::tracked( returns(copy), cycle_initial=|_, id, _| Place::bound(Type::divergent(id)).into(), - cycle_fn=|db, cycle, previous: &PlaceAndQualifiers<'db>, member: PlaceAndQualifiers<'db>, _| { - member.cycle_normalized(db, *previous, cycle) + cycle_fn=|db, cycle, previous: &PlaceAndQualifiers<'db>, member: PlaceAndQualifiers<'db>, key: MemberLookupKey<'db>| { + member.cycle_normalized(db, &ProgramEnvironment::from_program(key.program(db)), *previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] @@ -3366,24 +3808,38 @@ impl<'db> Type<'db> { let ty = key.ty(db); let name = key.name(db); let policy = key.policy(db); + let program = key.program(db); + let env = &ProgramEnvironment::from_program(program); - tracing::trace!("class_member: {}.{}", ty.display(db), name); + tracing::trace!("class_member: {}.{}", ty.display(db, env), name); if let Some(fallback) = ty.materialized_divergent_fallback() { - return fallback.class_member_with_policy(db, name, policy); + return fallback.class_member_with_policy(db, env, name, policy); + } + if let Type::ProtocolInstance(protocol) = ty + && let Some(origin) = protocol.materialized_origin(db) + { + let interface = protocol.interface(db); + return if interface.includes_member(db, name) { + interface.instance_member(db, env, name) + } else { + Type::instance(db, env, *origin).class_member_with_policy(db, env, name, policy) + }; } match ty { - Type::Union(union) => union.map_with_boundness_and_qualifiers(db, |elem| { - elem.class_member_with_policy(db, name, policy) + Type::Union(union) => union.map_with_boundness_and_qualifiers(db, env, |elem| { + elem.class_member_with_policy(db, env, name, policy) }), - Type::Intersection(inter) => inter.map_with_boundness_and_qualifiers(db, |elem| { - elem.class_member_with_policy(db, name, policy) + Type::Intersection(inter) => inter.map_with_boundness_and_qualifiers(db, env, |elem| { + elem.class_member_with_policy(db, env, name, policy) }), - // TODO: Once `to_meta_type` for the synthesized protocol is fully implemented, this handling should be removed. - Type::ProtocolInstance(ProtocolInstanceType { - inner: Protocol::Synthesized(_), - .. - }) => ty.instance_member(db, name), + Type::TypedDict(TypedDictType::Synthesized(synthesized)) => { + class::synthesized_typed_dict_class_member(db, env, synthesized, policy, name) + } + // TODO: Remove this once synthesized protocols have a precise meta-type. + Type::ProtocolInstance(protocol) if protocol.class_origin(db).is_none() => { + ty.instance_member(db, env, name) + } Type::LiteralValue(literal) if name == "__len__" @@ -3411,34 +3867,39 @@ impl<'db> Type<'db> { // their correct types instead of collapsing to `Any`/`Unknown`. Type::SubclassOf(subclass_of) if subclass_of.is_dynamic() => { let type_result = KnownClass::Type - .to_class_literal(db) - .find_name_in_mro_with_policy(db, name, policy) + .to_class_literal(db, env) + .find_name_in_mro_with_policy(db, env, name, policy) .expect("`find_name_in_mro` should return `Some` for a class literal"); if !type_result.place.is_undefined() { type_result } else { - ty.to_meta_type(db) - .find_name_in_mro_with_policy(db, name, policy) + ty.to_meta_type(db, env) + .find_name_in_mro_with_policy(db, env, name, policy) .expect( - "`Type::find_name_in_mro()` should return `Some()` when called on a meta-type", + "`Type::find_name_in_mro()` should return `Some()` \ + when called on a meta-type", ) } } - Type::NominalInstance(instance) => { - ty.to_meta_type(db) - .class_namespace_member(db, instance.class(db), name, policy) - } + Type::NominalInstance(instance) => ty.to_meta_type(db, env).class_namespace_member( + db, + env, + instance.class(db, env), + name, + policy, + ), - Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_) => { - ty.to_meta_type(db).class_object_member(db, name, policy) - } + Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_) => ty + .to_meta_type(db, env) + .class_object_member(db, env, name, policy), _ => ty - .to_meta_type(db) - .find_name_in_mro_with_policy(db, name, policy) + .to_meta_type(db, env) + .find_name_in_mro_with_policy(db, env, name, policy) .expect( - "`Type::find_name_in_mro()` should return `Some()` when called on a meta-type", + "`Type::find_name_in_mro()` should return `Some()` \ + when called on a meta-type", ), } } @@ -3450,21 +3911,23 @@ impl<'db> Type<'db> { /// Add those attributes using the same lookup as a concrete nominal instance. fn instance_lookup_class_member_with_policy( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, key: MemberLookupKey<'db>, ) -> PlaceAndQualifiers<'db> { let ty = key.ty(db); - if let Type::TypeVar(_) = ty - && let Some(class) = ty.nominal_class(db) - { - let name = key.name(db); - let policy = key.policy(db); + if let Type::TypeVar(_) = ty { + if let Some(class) = ty.nominal_class(db, env) { + let name = key.name(db); + let policy = key.policy(db); - ty.to_meta_type(db) - .class_namespace_member(db, class, name, policy) - } else { - Self::class_member_with_policy_inner(db, key) + return ty + .to_meta_type(db, env) + .class_namespace_member(db, env, class, name, policy); + } } + + Self::class_member_with_policy_inner(db, key) } /// Look up attributes stored in the namespace of a class object. @@ -3475,23 +3938,28 @@ impl<'db> Type<'db> { fn class_object_member( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, policy: MemberLookupPolicy, ) -> PlaceAndQualifiers<'db> { - let class_attr = self.find_name_in_mro_with_policy(db, name, policy).expect( - "Calling `class_object_member` on class literals and subclass-of types should always find an MRO", - ); + let class_attr = self + .find_name_in_mro_with_policy(db, env, name, policy) + .expect( + "Calling `class_object_member` on class literals and subclass-of types \ + should always find an MRO", + ); let own_class = match self { Type::SubclassOf(subclass_of) => match subclass_of.subclass_of() { SubclassOfInner::Protocol(protocol) => { - protocol.class_origin().map(|origin| *origin) + protocol.class_origin(db).map(|origin| *origin) } - subclass_of => subclass_of.into_class(db), + subclass_of => subclass_of.into_class(db, env), }, _ => self.to_class_type(db), }; - let own_class_attr = own_class.map(|class| class.own_class_member(db, None, name).inner); + let own_class_attr = + own_class.map(|class| class.own_class_member(db, env, None, name).inner); // A definitely-declared attribute in this class's own namespace is the contract for // values populated by metaclass initialization, analogous to a declared instance @@ -3513,17 +3981,20 @@ impl<'db> Type<'db> { return class_attr; } - let Some(metaclass_instance) = self.to_meta_type(db).to_instance_approximation(db) else { + let Some(metaclass_instance) = self + .to_meta_type(db, env) + .to_instance_approximation(db, env) + else { return class_attr; }; - let metaclass_attr = metaclass_instance.instance_member(db, name); + let metaclass_attr = metaclass_instance.instance_member(db, env, name); if own_declaration_definedness.is_some() { // A conditionally-declared attribute is a contract only on paths where that // declaration is present; the metaclass value is the fallback on other paths. - class_attr.or_fall_back_to(db, || metaclass_attr) + class_attr.or_fall_back_to(db, env, || metaclass_attr) } else { - metaclass_attr.or_fall_back_to(db, || class_attr) + metaclass_attr.or_fall_back_to(db, env, || class_attr) } } @@ -3564,21 +4035,22 @@ impl<'db> Type<'db> { fn class_namespace_member( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: ClassType<'db>, name: &str, policy: MemberLookupPolicy, ) -> PlaceAndQualifiers<'db> { let class_attr = self - .find_name_in_mro_with_policy(db, name, policy) + .find_name_in_mro_with_policy(db, env, name, policy) .expect("The meta-type of an instance-like type should always have an MRO"); let Some(metaclass) = class .metaclass(db) - .to_instance_approximation(db) - .and_then(|metaclass| metaclass.nominal_class(db)) + .to_instance_approximation(db, env) + .and_then(|metaclass| metaclass.nominal_class(db, env)) else { return class_attr; }; - let metaclass_member = metaclass.instance_member(db, name); + let metaclass_member = metaclass.instance_member(db, env, name); if metaclass_member.is_undefined() { return class_attr; } @@ -3588,6 +4060,7 @@ impl<'db> Type<'db> { let own_class_member = class.class_literal(db).class_member_from_mro( db, + env, name, policy, class.iter_mro(db).take(1), @@ -3602,6 +4075,7 @@ impl<'db> Type<'db> { .is_some_and(|symbol| { place_from_bindings( db, + env, use_def_map(db, scope).end_of_scope_symbol_bindings(symbol), ) .place @@ -3614,6 +4088,7 @@ impl<'db> Type<'db> { }; let inherited_class_member = class.class_literal(db).class_member_from_mro( db, + env, name, policy, class.iter_mro(db).skip(1), @@ -3625,8 +4100,8 @@ impl<'db> Type<'db> { metaclass_member }; let class_member = own_class_member - .or_fall_back_to(db, || metaclass_member) - .or_fall_back_to(db, || inherited_class_member); + .or_fall_back_to(db, env, || metaclass_member) + .or_fall_back_to(db, env, || inherited_class_member); let class_member = if metaclass_member_is_implicit { // Preserve the existing convention that an inferred instance member is assumed to be // available even when no lower-precedence fallback exists. @@ -3653,7 +4128,7 @@ impl<'db> Type<'db> { let Some(class_member_ty) = class_member.ignore_possibly_undefined() else { return dynamic_instance_fallback; }; - if !class_member_ty.may_be_data_descriptor(db) { + if !class_member_ty.may_be_data_descriptor(db, env) { return dynamic_instance_fallback; } let PlaceAndQualifiers { @@ -3671,12 +4146,12 @@ impl<'db> Type<'db> { union .elements(db) .iter() - .all(|ty| ty.may_be_data_descriptor(db)) + .all(|ty| ty.may_be_data_descriptor(db, env)) }); Place::Defined(DefinedPlace { ty: declaration .ty - .filter_union(db, |ty| ty.may_be_data_descriptor(db)), + .filter_union(db, |ty| ty.may_be_data_descriptor(db, env)), definedness: if all_arms_are_possible_data_descriptors { declaration.definedness } else { @@ -3685,7 +4160,7 @@ impl<'db> Type<'db> { ..declaration }) .with_qualifiers(qualifiers) - .or_fall_back_to(db, || dynamic_instance_fallback) + .or_fall_back_to(db, env, || dynamic_instance_fallback) } /// This function roughly corresponds to looking up an attribute in the `__dict__` of an object. @@ -3704,99 +4179,116 @@ impl<'db> Type<'db> { /// def __init__(self): /// self.b: str = "a" /// ``` - fn instance_member(&self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + fn instance_member( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> PlaceAndQualifiers<'db> { match self { - Type::Overlapping(overlapping) => overlapping.value_type(db).instance_member(db, name), - Type::Restricted(restricted) => restricted.value_type(db).instance_member(db, name), - Type::Deferred(deferred) => deferred.reduced(db).instance_member(db, name), - Type::Union(union) => { - union.map_with_boundness_and_qualifiers(db, |elem| elem.instance_member(db, name)) + Type::Overlapping(overlapping) => overlapping + .value_type(db, env) + .instance_member(db, env, name), + Type::Restricted(restricted) => { + restricted.value_type(db).instance_member(db, env, name) } + Type::Deferred(deferred) => deferred.reduced(db, env).instance_member(db, env, name), + Type::Union(union) => union.map_with_boundness_and_qualifiers(db, env, |elem| { + elem.instance_member(db, env, name) + }), Type::Intersection(intersection) => { - if let Some(complement) = intersection.enum_complement(db) { - enums::instance_member_for_enum_complement(db, complement, name) + if let Some(complement) = intersection.enum_complement(db, env) { + enums::instance_member_for_enum_complement(db, env, complement, name) } else { - intersection.map_with_boundness_and_qualifiers(db, |elem| { - elem.instance_member(db, name) + intersection.map_with_boundness_and_qualifiers(db, env, |elem| { + elem.instance_member(db, env, name) }) } } Type::EnumComplement(complement) => { - enums::instance_member_for_enum_complement(db, *complement, name) + enums::instance_member_for_enum_complement(db, env, *complement, name) } Type::UnsafeUnion(unsafe_union) => unsafe_union - .map_with_boundness_and_qualifiers(db, |elem| elem.instance_member(db, name)), + .map_with_boundness_and_qualifiers(db, |elem| elem.instance_member(db, env, name)), Type::Dynamic(_) | Type::Divergent(_) | Type::Never => Place::bound(self).into(), - Type::NominalInstance(instance) => instance.class(db).instance_member(db, name), - Type::NewTypeInstance(newtype) => { - newtype.concrete_base_type(db).instance_member(db, name) + Type::NominalInstance(instance) => { + instance.class(db, env).instance_member(db, env, name) } + Type::NewTypeInstance(newtype) => newtype + .concrete_base_type(db) + .instance_member(db, env, name), - Type::ProtocolInstance(protocol) => protocol.instance_member(db, name), + Type::ProtocolInstance(protocol) => protocol.instance_member(db, env, name), Type::FunctionLiteral(_) => KnownClass::FunctionType - .to_instance(db) - .instance_member(db, name), + .to_instance(db, env) + .instance_member(db, env, name), Type::BoundMethod(_) => KnownClass::MethodType - .to_instance(db) - .instance_member(db, name), - Type::KnownBoundMethod(method) => { - method.class().to_instance(db).instance_member(db, name) - } + .to_instance(db, env) + .instance_member(db, env, name), + Type::KnownBoundMethod(method) => method + .class() + .to_instance(db, env) + .instance_member(db, env, name), Type::WrapperDescriptor(_) => KnownClass::WrapperDescriptorType - .to_instance(db) - .instance_member(db, name), + .to_instance(db, env) + .instance_member(db, env, name), Type::DataclassDecorator(_) => KnownClass::FunctionType - .to_instance(db) - .instance_member(db, name), + .to_instance(db, env) + .instance_member(db, env, name), Type::Callable(_) | Type::DataclassTransformer(_) => { - Type::object().instance_member(db, name) + Type::object().instance_member(db, env, name) } Type::TypeVar(bound_typevar) => { - match bound_typevar.typevar(db).bound_or_constraints(db) { - None => Type::object().instance_member(db, name), + match bound_typevar.typevar(db).bound_or_constraints(db, env) { + None => Type::object().instance_member(db, env, name), Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - bound.instance_member(db, name) + bound.instance_member(db, env, name) } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints - .map_with_boundness_and_qualifiers(db, |constraint| { - constraint.instance_member(db, name) + .map_with_boundness_and_qualifiers(db, env, |constraint| { + constraint.instance_member(db, env, name) }), } } - Type::TypeIs(_) | Type::TypeGuard(_) => { - KnownClass::Bool.to_instance(db).instance_member(db, name) - } + Type::TypeIs(_) | Type::TypeGuard(_) => KnownClass::Bool + .to_instance(db, env) + .instance_member(db, env, name), - Type::LiteralValue(literal) => literal.fallback_instance(db).instance_member(db, name), + Type::LiteralValue(literal) => literal + .fallback_instance(db, env) + .instance_member(db, env, name), Type::AlwaysTruthy | Type::AlwaysFalsy | Type::TypeForm(_) => { - Type::object().instance_member(db, name) + Type::object().instance_member(db, env, name) } Type::ModuleLiteral(_) => KnownClass::ModuleType - .to_instance(db) - .instance_member(db, name), + .to_instance(db, env) + .instance_member(db, env, name), Type::SpecialForm(_) | Type::KnownInstance(_) => Place::Undefined.into(), - Type::PropertyInstance(property) => { - property.instance_fallback(db).instance_member(db, name) - } + Type::PropertyInstance(property) => property + .instance_class(db) + .to_instance(db, env) + .instance_member(db, env, 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), + Type::BoundSuper(_) => KnownClass::Super + .to_instance(db, env) + .instance_member(db, env, 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 @@ -3808,7 +4300,7 @@ impl<'db> Type<'db> { Type::TypedDict(_) => Place::Undefined.into(), - Type::TypeAlias(alias) => alias.value_type(db).instance_member(db, name), + Type::TypeAlias(alias) => alias.value_type(db).instance_member(db, env, name), } } @@ -3816,17 +4308,23 @@ impl<'db> Type<'db> { /// method corresponds to `inspect.getattr_static(, name)`. /// /// See also: [`Type::member`] - fn static_member(&self, db: &'db dyn Db, name: &str) -> Place<'db> { + fn static_member( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> Place<'db> { if let Type::ModuleLiteral(module) = self { - module.static_member(db, name).place - } else if let place @ Place::Defined(_) = self.class_member(db, name).place { + module.static_member(db, env, name).place + } else if let place @ Place::Defined(_) = self.class_member(db, env, name).place { place - } else if let Some(place @ Place::Defined(_)) = - self.find_name_in_mro(db, name).map(|inner| inner.place) + } else if let Some(place @ Place::Defined(_)) = self + .find_name_in_mro(db, env, name) + .map(|inner| inner.place) { place } else { - self.instance_member(db, name).place + self.instance_member(db, env, name).place } } @@ -3842,40 +4340,98 @@ impl<'db> Type<'db> { } } - /// 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. + /// Looks up `__get__` on the meta-type of `self` and calls it with `self`, `instance`, and + /// `owner`. Unlike other dunder methods, `__get__` is not itself looked up using the + /// descriptor protocol. + /// + /// Returns the resulting type and descriptor kind, or an error retaining the recovery value + /// when the implicit call is invalid. Returns `Ok(None)` when `__get__` is not defined. + /// + /// For example, accessing `C().value` below implicitly supplies the descriptor value, the + /// `C` instance, and `C`, so the declared method is missing two parameters: /// - /// 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. + /// ```python + /// class Descriptor: + /// def __get__(self): ... + /// + /// class C: + /// value = Descriptor() /// - /// If `__get__` is not defined on the meta-type, this method returns `None`. + /// C().value + /// ``` pub(crate) fn try_call_dunder_get( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, instance: Option>, owner: Type<'db>, - ) -> Option<(Type<'db>, AttributeKind)> { - #[salsa::tracked(returns(copy), cycle_initial=|_, _, _, _, _| None, heap_size=ruff_memory_usage::heap_size)] + ) -> Result>, DescriptorGetError<'db>> { + #[salsa::tracked(returns(copy), cycle_initial=|_, _, _, _, _, _| Ok(None), heap_size=ruff_memory_usage::heap_size)] fn try_call_dunder_get_inner<'db>( db: &'db dyn Db, + program: Program<'db>, ty: Type<'db>, instance: Option>, owner: Type<'db>, - ) -> Option<(Type<'db>, AttributeKind)> { + ) -> Result>, DescriptorGetError<'db>> { + let env = &ProgramEnvironment::from_program(program); if let Some(fallback) = ty.materialized_divergent_fallback() { - return fallback.try_call_dunder_get(db, instance, owner); + return fallback.try_call_dunder_get(db, env, instance, owner); } if let Some(dynamic) = ty.dynamic_descriptor_type() { - return Some((dynamic, AttributeKind::DataDescriptor)); + return Ok(Some(DescriptorGetResult { + return_type: dynamic, + kind: AttributeKind::DataDescriptor, + })); + } + + if let Some(union) = ty.as_union_like(db) { + let mut return_types = UnionBuilder::new(db, env); + let mut error = None; + let mut any_descriptor = false; + let mut all_data_descriptors = true; + + for alternative in union.elements(db) { + let result = alternative + .try_call_dunder_get(db, env, instance, owner) + .unwrap_or_else(|failure| { + error = error.or(Some(failure.context)); + Some(failure.fallback()) + }); + if let Some(DescriptorGetResult { return_type, kind }) = result { + any_descriptor = true; + all_data_descriptors &= kind.is_data(); + return_types = return_types.add(return_type); + } else { + all_data_descriptors = false; + return_types = return_types.add(*alternative); + } + } + + return if any_descriptor { + descriptor_get_result( + return_types.build(), + if all_data_descriptors { + AttributeKind::DataDescriptor + } else { + AttributeKind::NormalOrNonDataDescriptor + }, + error, + ) + } else { + Ok(None) + }; } match ty { Type::Callable(callable) if callable.is_staticmethod_like(db) => { // For "staticmethod-like" callables, model the behavior of `staticmethod.__get__`. // The underlying function is returned as-is, without binding self. - return Some((ty, AttributeKind::NormalOrNonDataDescriptor)); + return Ok(Some(DescriptorGetResult { + return_type: ty, + kind: AttributeKind::NormalOrNonDataDescriptor, + })); } Type::Callable(callable) if let is_function_like = callable.is_function_like(db) @@ -3884,24 +4440,28 @@ impl<'db> Type<'db> { // For "function-like" or "classmethod-like" callables, model the behavior of // `FunctionType.__get__` or `classmethod.__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() && is_function_like { - Some((ty, AttributeKind::NormalOrNonDataDescriptor)) + // 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. + let return_type = if instance.is_none() && is_function_like { + ty } else { let self_type = instance.unwrap_or_else(|| { // For classmethod-like callables, bind to the owner class. - owner.to_instance_approximation(db).unwrap_or(owner) + owner.to_instance_approximation(db, env).unwrap_or(owner) }); - Some(( - Type::Callable(callable.bind_self(db, Some(self_type))), - AttributeKind::NormalOrNonDataDescriptor, - )) + Type::Callable(callable.bind_self(db, env, Some(self_type))) }; + + return Ok(Some(DescriptorGetResult { + return_type, + kind: AttributeKind::NormalOrNonDataDescriptor, + })); } _ => {} } @@ -3910,16 +4470,16 @@ impl<'db> Type<'db> { ty: concrete_descr_get, .. }) = ty - .class_member_with_policy(db, "__get__", MemberLookupPolicy::REQUIRE_CONCRETE) + .class_member_with_policy(db, env, "__get__", MemberLookupPolicy::REQUIRE_CONCRETE) .place else { - return None; + return Ok(None); }; // A recursive member lookup can yield the internal cycle marker. It does not // represent a concrete descriptor method and must not escape through the access. if concrete_descr_get.is_divergent() { - return None; + return Ok(None); } // Descriptor special-method lookup checks the descriptor's type, so instance storage @@ -3929,46 +4489,58 @@ impl<'db> Type<'db> { definedness: descr_get_boundness, .. }) = ty - .class_member_with_policy(db, "__get__", MemberLookupPolicy::NO_INSTANCE_FALLBACK) + .class_member_with_policy( + db, + env, + "__get__", + MemberLookupPolicy::NO_INSTANCE_FALLBACK, + ) .place else { - return None; + return Ok(None); }; - let instance_ty = instance.unwrap_or_else(|| Type::none(db)); - let return_ty = descr_get - .try_call(db, &CallArguments::positional([ty, instance_ty, owner])) - .map(|bindings| { - if descr_get_boundness == Definedness::AlwaysDefined { - bindings.return_type(db) - } else { - UnionType::from_two_elements(db, bindings.return_type(db), ty) - } - }) - // TODO: an error when calling `__get__` will lead to a `TypeError` or similar at runtime; - // we should emit a diagnostic here instead of silently ignoring the error. - .unwrap_or_else(|CallError(_, bindings)| bindings.return_type(db)); - - let descriptor_kind = if ty.is_data_descriptor(db) { + let instance_ty = instance.unwrap_or_else(|| Type::none(db, env)); + let kind = if ty.is_data_descriptor(db, env) { AttributeKind::DataDescriptor } else { AttributeKind::NormalOrNonDataDescriptor }; + let (return_type, error) = match descr_get.try_call( + db, + env, + &CallArguments::positional([ty, instance_ty, owner]), + ) { + Ok(bindings) => (bindings.return_type(db, env), None), + Err(error) => ( + error.return_type(db, env), + Some(DescriptorGetCallContext::new( + db, ty, descr_get, instance, owner, + )), + ), + }; + let return_type = if descr_get_boundness == Definedness::AlwaysDefined { + return_type + } else { + UnionType::from_two_elements(db, env, return_type, ty) + }; - Some((return_ty, descriptor_kind)) + descriptor_get_result(return_type, kind, error) } tracing::trace!( "try_call_dunder_get: {}, {}, {}", - self.display(db), - instance.unwrap_or_else(|| Type::none(db)).display(db), - owner.display(db) + self.display(db, env), + instance + .unwrap_or_else(|| Type::none(db, env)) + .display(db, env), + owner.display(db, env) ); // Function descriptors have fixed binding behavior, so avoid retaining a tracked query // for every function and access context. if let Type::FunctionLiteral(function) = self { - let descriptor_result = if function.is_classmethod(db) { + let return_type = if function.is_classmethod(db) { Type::BoundMethod(BoundMethodType::new(db, function, owner)) } else if let Some(instance) = instance && !function.is_staticmethod(db) @@ -3978,10 +4550,13 @@ impl<'db> Type<'db> { self }; - return Some((descriptor_result, AttributeKind::NormalOrNonDataDescriptor)); + return Ok(Some(DescriptorGetResult { + return_type, + kind: AttributeKind::NormalOrNonDataDescriptor, + })); } - try_call_dunder_get_inner(db, self, instance, owner) + try_call_dunder_get_inner(db, env.program(db), self, instance, owner) } /// Look up `__get__` on the meta-type of `attribute`, and call it with `attribute`, `instance`, @@ -3989,10 +4564,15 @@ impl<'db> Type<'db> { /// and intersections explicitly. fn try_call_dunder_get_on_attribute( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, attribute: PlaceAndQualifiers<'db>, instance: Option>, owner: Type<'db>, - ) -> (PlaceAndQualifiers<'db>, AttributeKind) { + ) -> ( + PlaceAndQualifiers<'db>, + AttributeKind, + Option>, + ) { if let PlaceAndQualifiers { place: Place::Defined(DefinedPlace { @@ -4008,6 +4588,7 @@ impl<'db> Type<'db> { { return Self::try_call_dunder_get_on_attribute( db, + env, Place::Defined(DefinedPlace { ty: fallback, origin, @@ -4021,7 +4602,7 @@ impl<'db> Type<'db> { ); } - match attribute { + let (member, kind, error) = match attribute { // A directly dynamic attribute could be a data descriptor even though we cannot see // its methods. Preserve that uncertainty, along with the existing bottom and cycle // behavior, without performing member lookups that cannot add information. @@ -4032,7 +4613,7 @@ impl<'db> Type<'db> { .. }), qualifiers: _, - } => (attribute, AttributeKind::DataDescriptor), + } => (attribute, AttributeKind::DataDescriptor, None), PlaceAndQualifiers { place: @@ -4046,13 +4627,19 @@ impl<'db> Type<'db> { qualifiers, } => { let mut all_data_descriptors = true; - + let mut error = None; let place = union - .map_with_boundness(db, |elem| { - let ty = match elem.try_call_dunder_get(db, instance, owner) { - Some((ty, kind)) => { + .map_with_boundness(db, env, |elem| { + let result = elem + .try_call_dunder_get(db, env, instance, owner) + .unwrap_or_else(|failure| { + error = error.or(Some(failure.context)); + Some(failure.fallback()) + }); + let ty = match result { + Some(DescriptorGetResult { return_type, kind }) => { all_data_descriptors &= kind.is_data(); - ty + return_type } None => { all_data_descriptors = false; @@ -4076,7 +4663,7 @@ impl<'db> Type<'db> { AttributeKind::NormalOrNonDataDescriptor }; - (place, kind) + (place, kind, error) } attribute @ PlaceAndQualifiers { @@ -4089,16 +4676,22 @@ impl<'db> Type<'db> { provenance: attribute_provenance, }), qualifiers, - } => ( - if intersection.positive(db).is_empty() { + } => { + let mut error = None; + let place = if intersection.positive(db).is_empty() { attribute } else { intersection - .map_with_boundness(db, |elem| { + .map_with_boundness(db, env, |elem| { + let ty = elem + .try_call_dunder_get(db, env, instance, owner) + .unwrap_or_else(|failure| { + error = error.or(Some(failure.context)); + Some(failure.fallback()) + }) + .map_or(*elem, |result| result.return_type); Place::Defined(DefinedPlace { - ty: elem - .try_call_dunder_get(db, instance, owner) - .map_or(*elem, |(ty, _)| ty), + ty, origin, definedness, public_type_policy, @@ -4106,10 +4699,15 @@ impl<'db> Type<'db> { }) }) .with_qualifiers(qualifiers) - }, - // TODO: Discover data descriptors in intersections. - AttributeKind::NormalOrNonDataDescriptor, - ), + }; + ( + place, + // TODO: Discover data descriptors in intersections without decomposing the + // descriptor return type into an unsound intersection. + AttributeKind::NormalOrNonDataDescriptor, + error, + ) + } PlaceAndQualifiers { place: @@ -4122,34 +4720,42 @@ impl<'db> Type<'db> { }), qualifiers: _, } => { - if let Some((return_ty, attribute_kind)) = - attribute_ty.try_call_dunder_get(db, instance, owner) - { + let mut error = None; + let result = attribute_ty + .try_call_dunder_get(db, env, instance, owner) + .unwrap_or_else(|failure| { + error = Some(failure.context); + Some(failure.fallback()) + }); + if let Some(DescriptorGetResult { return_type, kind }) = result { ( Place::Defined(DefinedPlace { - ty: return_ty, + ty: return_type, origin, definedness: boundness, public_type_policy, provenance, }) .into(), - attribute_kind, + kind, + error, ) } else { - (attribute, AttributeKind::NormalOrNonDataDescriptor) + (attribute, AttributeKind::NormalOrNonDataDescriptor, None) } } - _ => (attribute, AttributeKind::NormalOrNonDataDescriptor), - } + _ => (attribute, AttributeKind::NormalOrNonDataDescriptor, None), + }; + + (member, kind, error) } /// 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. /// A directly dynamic type is treated as a data descriptor because it could inhabit one. - pub(crate) fn is_data_descriptor(self, d: &'db dyn Db) -> bool { - self.is_data_descriptor_impl(d, false) + fn is_data_descriptor(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool { + self.is_data_descriptor_impl(db, env.program(db), false) } /// Returns whether this type should be considered a possible data descriptor. @@ -4157,38 +4763,53 @@ impl<'db> Type<'db> { /// This is used to determine whether an attribute assignment is valid for narrowing. /// For practical convenience, dynamic union elements are not considered possible data /// descriptors here, because doing so would disable narrowing too frequently. - pub(crate) fn may_be_data_descriptor(self, d: &'db dyn Db) -> bool { - self.is_data_descriptor_impl(d, true) + fn may_be_data_descriptor(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool { + self.is_data_descriptor_impl(db, env.program(db), true) } /// Returns whether this type is known not to be a data descriptor. /// - /// Descriptor uncertainty only propagates through outer unions, intersections, and aliases; - /// type arguments do not affect the runtime descriptor class. - pub(crate) fn is_definitely_non_data_descriptor(self, db: &'db dyn Db) -> bool { - self.is_definitely_non_data_descriptor_impl(db, ()) + /// Descriptor uncertainty propagates through outer unions, intersections, and aliases. + /// `TypeForm` values and inexact `type[...]` values are also uncertain because their bounds + /// describe the represented instance types, not the runtime values whose metaclasses determine + /// descriptor behavior. + fn is_definitely_non_data_descriptor( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> bool { + self.is_definitely_non_data_descriptor_impl(db, env.program(db)) } // Recursive aliases use `true`, the identity for the all-of classifications above. #[salsa::tracked( returns(copy), - cycle_initial=|_, _, _, ()| true, + cycle_initial=|_, _, _, _| true, heap_size=ruff_memory_usage::heap_size )] - fn is_definitely_non_data_descriptor_impl(self, db: &'db dyn Db, (): ()) -> bool { + fn is_definitely_non_data_descriptor_impl( + self, + db: &'db dyn Db, + program: Program<'db>, + ) -> bool { + let env = &ProgramEnvironment::from_program(program); match self { Type::Dynamic(_) | Type::Divergent(_) | Type::TypeVar(_) => false, Type::Union(union) => union .elements(db) .iter() - .all(|ty| ty.is_definitely_non_data_descriptor_impl(db, ())), + .all(|ty| ty.is_definitely_non_data_descriptor_impl(db, program)), Type::Intersection(intersection) => intersection .iter_positive(db) - .all(|ty| ty.is_definitely_non_data_descriptor_impl(db, ())), + .all(|ty| ty.is_definitely_non_data_descriptor_impl(db, program)), Type::TypeAlias(alias) => alias .value_type(db) - .is_definitely_non_data_descriptor_impl(db, ()), - _ => !self.may_be_data_descriptor(db), + .is_definitely_non_data_descriptor_impl(db, program), + Type::NominalInstance(instance) if instance.has_known_class(db, KnownClass::Type) => { + false + } + Type::TypeForm(_) | Type::SubclassOf(_) => false, + _ => !self.may_be_data_descriptor(db, env), } } @@ -4196,10 +4817,16 @@ impl<'db> Type<'db> { // Seed recursive aliases with the corresponding identity value. #[salsa::tracked( returns(copy), - cycle_initial=|_, _, _, any_of_union: bool| !any_of_union, + cycle_initial=|_, _, _, _, any_of_union: bool| !any_of_union, heap_size=ruff_memory_usage::heap_size )] - fn is_data_descriptor_impl(self, db: &'db dyn Db, any_of_union: bool) -> bool { + fn is_data_descriptor_impl( + self, + db: &'db dyn Db, + program: Program<'db>, + any_of_union: bool, + ) -> bool { + let env = &ProgramEnvironment::from_program(program); match self { Type::Dynamic(_) => !any_of_union, Type::SubclassOf(_) if self.dynamic_descriptor_type().is_some() => true, @@ -4207,25 +4834,33 @@ impl<'db> Type<'db> { Type::Union(union) if any_of_union => union .elements(db) .iter() - .any(|ty| ty.is_data_descriptor_impl(db, any_of_union)), + .any(|ty| ty.is_data_descriptor_impl(db, program, any_of_union)), Type::Union(union) => union .elements(db) .iter() - .all(|ty| ty.is_data_descriptor_impl(db, any_of_union)), + .all(|ty| ty.is_data_descriptor_impl(db, program, any_of_union)), Type::Intersection(intersection) => intersection .iter_positive(db) - .any(|ty| ty.is_data_descriptor_impl(db, any_of_union)), - Type::TypeAlias(alias) => alias - .value_type(db) - .is_data_descriptor_impl(db, any_of_union), + .any(|ty| ty.is_data_descriptor_impl(db, program, any_of_union)), + Type::TypeAlias(alias) => { + alias + .value_type(db) + .is_data_descriptor_impl(db, program, any_of_union) + } _ => { !self - .class_member_with_policy(db, "__set__", MemberLookupPolicy::REQUIRE_CONCRETE) + .class_member_with_policy( + db, + env, + "__set__", + MemberLookupPolicy::REQUIRE_CONCRETE, + ) .place .is_undefined() || !self .class_member_with_policy( db, + env, "__delete__", MemberLookupPolicy::REQUIRE_CONCRETE, ) @@ -4251,36 +4886,51 @@ impl<'db> Type<'db> { /// back to lower-precedence stages of the descriptor protocol by building union types. fn invoke_descriptor_protocol( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, key: MemberLookupKey<'db>, receiver: Type<'db>, - fallback: PlaceAndQualifiers<'db>, + fallback: MemberLookupResult<'db>, policy: InstanceFallbackShadowsNonDataDescriptor, - ) -> PlaceAndQualifiers<'db> { - let ty = key.ty(db); + ) -> MemberLookupResult<'db> { + let meta_attr_plain = Self::instance_lookup_class_member_with_policy(db, env, key); + // A TypeVar retains its class identity when lookup is delegated to its bound, including + // after narrowing. Narrowing can also add an unrelated class to a mixin's `Self`, in which + // case the TypeVar alone is not a valid owner for descriptors from that class. + let owner = match receiver { + Type::TypeVar(_) => receiver, + Type::Intersection(intersection) => intersection + .positive(db) + .iter() + .copied() + .find(|element| element.is_type_var() && element.is_subtype_of(db, env, key.ty(db))) + .unwrap_or(key.ty(db)), + _ => key.ty(db), + } + .to_meta_type(db, env); let ( PlaceAndQualifiers { place: meta_attr, qualifiers: meta_attr_qualifiers, }, meta_attr_kind, - ) = Self::try_call_dunder_get_on_attribute( - db, - Self::instance_lookup_class_member_with_policy(db, key), - Some(receiver), - ty.to_meta_type(db), - ); + meta_attr_error, + ) = Self::try_call_dunder_get_on_attribute(db, env, meta_attr_plain, Some(receiver), owner); + let meta_attr_error = meta_attr_error.map(MemberLookupErrorKind::DescriptorGet); + let fallback_error = fallback.err().map(|error| error.kind(db)); let PlaceAndQualifiers { place: fallback, qualifiers: fallback_qualifiers, - } = fallback; + } = fallback.unwrap_or_else(|error| error.fallback_member(db)); 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::Defined(_), _, Place::Undefined) => { - meta_attr.with_qualifiers(meta_attr_qualifiers) - } + (meta_attr @ Place::Defined(_), _, Place::Undefined) => member_lookup_result( + db, + meta_attr.with_qualifiers(meta_attr_qualifiers), + meta_attr_error, + ), // `meta_attr` is the return type of a data descriptor and definitely bound, so we // return it. @@ -4291,7 +4941,11 @@ impl<'db> Type<'db> { }), AttributeKind::DataDescriptor, _, - ) => meta_attr.with_qualifiers(meta_attr_qualifiers), + ) => member_lookup_result( + db, + meta_attr.with_qualifiers(meta_attr_qualifiers), + meta_attr_error, + ), // `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 @@ -4312,14 +4966,18 @@ impl<'db> Type<'db> { public_type_policy: fallback_public_type_policy, provenance: fallback_provenance, }), - ) => Place::Defined(DefinedPlace { - ty: UnionType::from_two_elements(db, meta_attr_ty, fallback_ty), - origin: meta_origin.merge(fallback_origin), - definedness: fallback_boundness, - public_type_policy: fallback_public_type_policy, - provenance: fallback_provenance.or(meta_attr_provenance), - }) - .with_qualifiers(meta_attr_qualifiers.union(fallback_qualifiers)), + ) => member_lookup_result( + db, + Place::Defined(DefinedPlace { + ty: UnionType::from_two_elements(db, env, meta_attr_ty, fallback_ty), + origin: meta_origin.merge(fallback_origin), + definedness: fallback_boundness, + public_type_policy: fallback_public_type_policy, + provenance: fallback_provenance.or(meta_attr_provenance), + }) + .with_qualifiers(meta_attr_qualifiers.union(fallback_qualifiers)), + meta_attr_error.or(fallback_error), + ), // `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 @@ -4336,9 +4994,11 @@ impl<'db> Type<'db> { definedness: Definedness::AlwaysDefined, .. }), - ) if policy == InstanceFallbackShadowsNonDataDescriptor::Yes => { - fallback.with_qualifiers(fallback_qualifiers) - } + ) if policy == InstanceFallbackShadowsNonDataDescriptor::Yes => member_lookup_result( + db, + fallback.with_qualifiers(fallback_qualifiers), + fallback_error, + ), // `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 @@ -4359,17 +5019,25 @@ impl<'db> Type<'db> { public_type_policy: fallback_public_type_policy, provenance: fallback_provenance, }), - ) => Place::Defined(DefinedPlace { - ty: UnionType::from_two_elements(db, meta_attr_ty, fallback_ty), - origin: meta_origin.merge(fallback_origin), - definedness: meta_attr_boundness.max(fallback_boundness), - public_type_policy: fallback_public_type_policy, - provenance: fallback_provenance.or(meta_attr_provenance), - }) - .with_qualifiers(meta_attr_qualifiers.union(fallback_qualifiers)), + ) => member_lookup_result( + db, + Place::Defined(DefinedPlace { + ty: UnionType::from_two_elements(db, env, meta_attr_ty, fallback_ty), + origin: meta_origin.merge(fallback_origin), + definedness: meta_attr_boundness.max(fallback_boundness), + public_type_policy: fallback_public_type_policy, + provenance: fallback_provenance.or(meta_attr_provenance), + }) + .with_qualifiers(meta_attr_qualifiers.union(fallback_qualifiers)), + meta_attr_error.or(fallback_error), + ), // If the attribute is not found on the meta-type, we simply return the fallback. - (Place::Undefined, _, fallback) => fallback.with_qualifiers(fallback_qualifiers), + (Place::Undefined, _, fallback) => member_lookup_result( + db, + fallback.with_qualifiers(fallback_qualifiers), + fallback_error, + ), } } @@ -4378,11 +5046,31 @@ impl<'db> Type<'db> { /// /// 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, MemberLookupPolicy::default()) + pub(crate) fn member( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> PlaceAndQualifiers<'db> { + self.try_member_lookup(db, env, name) + .unwrap_or_else(|error| error.fallback_member(db)) + } + + /// Performs member lookup while retaining errors from implicit attribute-access methods. + fn try_member_lookup( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> MemberLookupResult<'db> { + self.member_lookup_with_policy_and_receiver( + db, + env, + name, + MemberLookupPolicy::default(), + None, + ) } /// Similar to [`Type::member`], but allows the caller to specify what policy should be used @@ -4390,10 +5078,12 @@ impl<'db> Type<'db> { pub(crate) fn member_lookup_with_policy( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, policy: MemberLookupPolicy, ) -> PlaceAndQualifiers<'db> { - self.member_lookup_with_policy_and_receiver(db, name, policy, None) + self.member_lookup_with_policy_and_receiver(db, env, name, policy, None) + .unwrap_or_else(|error| error.fallback_member(db)) } /// Perform member lookup while optionally binding descriptors and `Self` to a more precise @@ -4404,30 +5094,31 @@ impl<'db> Type<'db> { fn member_lookup_with_policy_and_receiver( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, policy: MemberLookupPolicy, receiver: Option>, - ) -> PlaceAndQualifiers<'db> { + ) -> MemberLookupResult<'db> { #[salsa::tracked( returns(copy), - cycle_initial=|_, id, _| Place::bound(Type::divergent(id)).into(), - cycle_fn=|db, cycle, previous: &PlaceAndQualifiers<'db>, member: PlaceAndQualifiers<'db>, _| { - member.cycle_normalized(db, *previous, cycle) + cycle_initial=|_, id, _| Ok(Place::bound(Type::divergent(id)).into()), + cycle_fn=|db, cycle, previous: &MemberLookupResult<'db>, member: MemberLookupResult<'db>, key: MemberLookupKey<'db>| { + cycle_normalized_member_lookup(db, &ProgramEnvironment::from_program(key.program(db)), member, *previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] fn member_lookup_with_policy_inner<'db>( db: &'db dyn Db, key: MemberLookupKey<'db>, - ) -> PlaceAndQualifiers<'db> { + ) -> MemberLookupResult<'db> { member_lookup_with_policy_impl(db, key, None) } #[salsa::tracked( returns(copy), - cycle_initial=|_, id, _, _| Place::bound(Type::divergent(id)).into(), - cycle_fn=|db, cycle, previous: &PlaceAndQualifiers<'db>, member: PlaceAndQualifiers<'db>, _, _| { - member.cycle_normalized(db, *previous, cycle) + cycle_initial=|_, id, _, _| Ok(Place::bound(Type::divergent(id)).into()), + cycle_fn=|db, cycle, previous: &MemberLookupResult<'db>, member: MemberLookupResult<'db>, key: MemberLookupKey<'db>, _| { + cycle_normalized_member_lookup(db, &ProgramEnvironment::from_program(key.program(db)), member, *previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] @@ -4435,7 +5126,7 @@ impl<'db> Type<'db> { db: &'db dyn Db, key: MemberLookupKey<'db>, receiver: Type<'db>, - ) -> PlaceAndQualifiers<'db> { + ) -> MemberLookupResult<'db> { member_lookup_with_policy_impl(db, key, Some(receiver)) } @@ -4443,21 +5134,23 @@ impl<'db> Type<'db> { db: &'db dyn Db, key: MemberLookupKey<'db>, receiver: Option>, - ) -> PlaceAndQualifiers<'db> { + ) -> MemberLookupResult<'db> { fn promote_inferred_attribute_class_literals<'db>( db: &'db dyn Db, - result: PlaceAndQualifiers<'db>, - ) -> PlaceAndQualifiers<'db> { + env: &ProgramEnvironment<'db>, + result: MemberLookupResult<'db>, + ) -> MemberLookupResult<'db> { + let member = result.unwrap_or_else(|error| error.fallback_member(db)); let should_promote = matches!( - result.place, + member.place, Place::Defined(DefinedPlace { origin: TypeOrigin::Inferred, .. }) - ) && !result.qualifiers.contains(TypeQualifiers::FINAL); + ) && !member.qualifiers.contains(TypeQualifiers::FINAL); if should_promote { - result.map_type(|ty| ty.promote_class_literals(db)) + map_member_lookup_type(db, result, |ty| ty.promote_class_literals(db, env)) } else { result } @@ -4465,9 +5158,10 @@ impl<'db> Type<'db> { fn instance_like_member_lookup<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, key: MemberLookupKey<'db>, receiver: Type<'db>, - ) -> PlaceAndQualifiers<'db> { + ) -> MemberLookupResult<'db> { let this = key.ty(db); let name = key.name(db); let name_str = name.as_str(); @@ -4479,7 +5173,7 @@ impl<'db> Type<'db> { .as_enum() .map(|enum_literal| enum_literal.enum_class_literal(db)), _ => this - .nominal_class(db) + .nominal_class(db, env) .map(|class| class.class_literal(db)) .and_then(|class| class.into_enum_class(db)), } && let Some(resolved_name) = enum_class.resolve_member(db, name) @@ -4492,77 +5186,108 @@ impl<'db> Type<'db> { .into(); } - let fallback = this.instance_member(db, name_str); + let fallback = this.instance_member(db, env, name_str); let result = Type::invoke_descriptor_protocol( db, + env, key, receiver, - fallback, + fallback.into(), InstanceFallbackShadowsNonDataDescriptor::No, ); - if result.is_class_var() && this.is_typed_dict() { + if result + .unwrap_or_else(|error| error.fallback_member(db)) + .is_class_var() + && this.is_typed_dict() + { // `ClassVar`s on `TypedDictFallback` cannot be accessed on inhabitants of `SomeTypedDict`. // They can only be accessed on `SomeTypedDict` directly. return Place::Undefined.into(); } - let result = this.fallback_to_getattr(db, name, result, key.policy(db)); + let result = this.fallback_to_getattr(db, env, name, result, key.policy(db)); // An inferred attribute accessed through an instance can resolve to an override // on a subclass, so an exact class object is not a safe public type here. - let result = result.map_type(|ty| ty.bind_self_typevars(db, receiver)); - promote_inferred_attribute_class_literals(db, result) + let result = map_member_lookup_type(db, result, |ty| { + ty.bind_self_typevars(db, env, receiver) + }); + promote_inferred_attribute_class_literals(db, env, result) } + let program = key.program(db); + let env = &ProgramEnvironment::from_program(program); let this = key.ty(db); let name = key.name(db); let name_str = name.as_str(); let policy = key.policy(db); - tracing::trace!("member_lookup_with_policy: {}.{}", this.display(db), name); + tracing::trace!( + "member_lookup_with_policy: {}.{}", + this.display(db, env), + name + ); if let Some(fallback) = this.materialized_divergent_fallback() { return fallback - .member_lookup_with_policy_and_receiver(db, name_str, policy, receiver); + .member_lookup_with_policy_and_receiver(db, env, name_str, policy, receiver); } match this { Type::Overlapping(overlapping) => overlapping - .value_type(db) - .member_lookup_with_policy_and_receiver(db, name_str, policy, receiver), + .value_type(db, env) + .member_lookup_with_policy_and_receiver(db, env, name_str, policy, receiver), Type::Restricted(restricted) => restricted .value_type(db) - .member_lookup_with_policy_and_receiver(db, name_str, policy, receiver), + .member_lookup_with_policy_and_receiver(db, env, name_str, policy, receiver), // basedpython: the deferral stands for the value, so it is the receiver the // lookup binds — the same way a type parameter is, rather than the bound it // resolves through. that is what lets `x.foo().foo()` stay one symbolic chain - Type::Deferred(deferred) => { - deferred.reduced(db).member_lookup_with_policy_and_receiver( + Type::Deferred(deferred) => deferred + .reduced(db, env) + .member_lookup_with_policy_and_receiver( db, + env, name_str, policy, Some(receiver.unwrap_or(this)), - ) + ), + Type::Union(union) => { + let mut error = None; + let member = union.map_with_boundness_and_qualifiers(db, env, |elem| { + let result = elem.member_lookup_with_policy_and_receiver( + db, env, name_str, policy, receiver, + ); + error = error.or_else(|| result.err().map(|error| error.kind(db))); + result.unwrap_or_else(|error| error.fallback_member(db)) + }); + member_lookup_result(db, member, error) } - Type::Union(union) => union.map_with_boundness_and_qualifiers(db, |elem| { - elem.member_lookup_with_policy_and_receiver(db, name_str, policy, receiver) - }), Type::Intersection(intersection) => { - if let Some(complement) = intersection.enum_complement(db) { - enums::member_lookup_for_enum_complement(db, complement, name_str, policy) + if let Some(complement) = intersection.enum_complement(db, env) { + enums::member_lookup_for_enum_complement( + db, env, complement, name_str, policy, + ) + .into() } else { let receiver = Some(receiver.unwrap_or(this)); - intersection.map_with_boundness_and_qualifiers(db, |elem| { - elem.member_lookup_with_policy_and_receiver( - db, name_str, policy, receiver, - ) - }) + let mut error = None; + let member = + intersection.map_with_boundness_and_qualifiers(db, env, |elem| { + let result = elem.member_lookup_with_policy_and_receiver( + db, env, name_str, policy, receiver, + ); + error = error.or_else(|| result.err().map(|error| error.kind(db))); + result.unwrap_or_else(|error| error.fallback_member(db)) + }); + member_lookup_result(db, member, error) } } Type::EnumComplement(complement) => { - enums::member_lookup_for_enum_complement(db, complement, name_str, policy) + enums::member_lookup_for_enum_complement(db, env, complement, name_str, policy) + .into() } // The member is available as long as *some* materialization has it. This is the @@ -4570,9 +5295,12 @@ impl<'db> Type<'db> { // answers both `.imag` and `.upper`. Type::UnsafeUnion(unsafe_union) => { let receiver = Some(receiver.unwrap_or(this)); - unsafe_union.map_with_boundness_and_qualifiers(db, |elem| { - elem.member_lookup_with_policy_and_receiver(db, name_str, policy, receiver) - }) + Ok(unsafe_union.map_with_boundness_and_qualifiers(db, |elem| { + elem.member_lookup_with_policy_and_receiver( + db, env, name_str, policy, receiver, + ) + .unwrap_or_else(|error| error.fallback_member(db)) + })) } Type::Dynamic(..) | Type::Divergent(_) | Type::Never => Place::bound(this).into(), @@ -4608,6 +5336,30 @@ impl<'db> Type<'db> { .into() } + Type::ClassLiteral(class) + if name == "lower_bound" && class.is_known(db, KnownClass::ConstraintSet) => + { + Place::bound(Type::KnownBoundMethod( + KnownBoundMethodType::ConstraintSetLowerBound, + )) + .into() + } + Type::ClassLiteral(class) + if name == "upper_bound" && class.is_known(db, KnownClass::ConstraintSet) => + { + Place::bound(Type::KnownBoundMethod( + KnownBoundMethodType::ConstraintSetUpperBound, + )) + .into() + } + Type::ClassLiteral(class) + if name == "equality" && class.is_known(db, KnownClass::ConstraintSet) => + { + Place::bound(Type::KnownBoundMethod( + KnownBoundMethodType::ConstraintSetEquality, + )) + .into() + } Type::ClassLiteral(class) if name == "range" && class.is_known(db, KnownClass::ConstraintSet) => { @@ -4648,6 +5400,14 @@ impl<'db> Type<'db> { )) .into() } + Type::KnownInstance(KnownInstanceType::ConstraintSet(tracked)) + if name == "exists" => + { + Place::bound(Type::KnownBoundMethod( + KnownBoundMethodType::ConstraintSetExists(tracked), + )) + .into() + } Type::KnownInstance(KnownInstanceType::ConstraintSet(tracked)) if name == "for_all" => { @@ -4699,14 +5459,20 @@ impl<'db> Type<'db> { Place::bound(Type::string_literal(db, typevar.name(db))).into() } Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) if name == "__bound__" => { - Place::bound(typevar.upper_bound(db).unwrap_or_else(|| Type::none(db))).into() + Place::bound( + typevar + .upper_bound(db, env) + .unwrap_or_else(|| Type::none(db, env)), + ) + .into() } Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) if name == "__constraints__" => { Place::bound(Type::heterogeneous_tuple( db, - typevar.constraints(db).into_iter().flatten(), + env, + typevar.constraints(db, env).into_iter().flatten(), )) .into() } @@ -4715,8 +5481,8 @@ impl<'db> Type<'db> { { Place::bound( typevar - .default_type(db) - .unwrap_or_else(|| KnownClass::NoDefaultType.to_instance(db)), + .default_type(db, env) + .unwrap_or_else(|| KnownClass::NoDefaultType.to_instance(db, env)), ) .into() } @@ -4764,29 +5530,33 @@ impl<'db> Type<'db> { Place::bound(Type::FunctionLiteral(bound_method.function(db))).into() } _ => { - KnownClass::MethodType - .to_instance(db) - .member_lookup_with_policy_and_receiver(db, name_str, policy, receiver) - .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. This - // changes the lookup object, so do not forward the bound-method - // receiver. - Type::FunctionLiteral(bound_method.function(db)) - .member_lookup_with_policy(db, name_str, policy) - }) + let result = KnownClass::MethodType + .to_instance(db, env) + .member_lookup_with_policy_and_receiver( + db, env, name_str, policy, receiver, + ); + member_lookup_or_fall_back_to(db, env, result, || { + // If an attribute is not available on the bound method object, + // it will be looked up on the underlying function object. This + // changes the lookup object, so do not forward the bound-method + // receiver. + Type::FunctionLiteral(bound_method.function(db)) + .member_lookup_with_policy_and_receiver( + db, env, name_str, policy, None, + ) + }) } }, Type::KnownBoundMethod(method) => method .class() - .to_instance(db) - .member_lookup_with_policy_and_receiver(db, name_str, policy, receiver), + .to_instance(db, env) + .member_lookup_with_policy_and_receiver(db, env, name_str, policy, receiver), Type::WrapperDescriptor(_) => KnownClass::WrapperDescriptorType - .to_instance(db) - .member_lookup_with_policy_and_receiver(db, name_str, policy, receiver), + .to_instance(db, env) + .member_lookup_with_policy_and_receiver(db, env, name_str, policy, receiver), Type::DataclassDecorator(_) => KnownClass::FunctionType - .to_instance(db) - .member_lookup_with_policy_and_receiver(db, name_str, policy, receiver), + .to_instance(db, env) + .member_lookup_with_policy_and_receiver(db, env, name_str, policy, receiver), Type::Callable(_) | Type::DataclassTransformer(_) if name_str == "__call__" => { Place::bound(this).into() @@ -4794,17 +5564,17 @@ impl<'db> Type<'db> { Type::Callable(callable) if callable.is_function_like(db) => { KnownClass::FunctionType - .to_instance(db) - .member_lookup_with_policy_and_receiver(db, name_str, policy, receiver) + .to_instance(db, env) + .member_lookup_with_policy_and_receiver(db, env, name_str, policy, receiver) } Type::Callable(_) | Type::DataclassTransformer(_) => Type::object() - .member_lookup_with_policy_and_receiver(db, name_str, policy, receiver), + .member_lookup_with_policy_and_receiver(db, env, name_str, policy, receiver), Type::NominalInstance(instance) if matches!(name_str, "major" | "minor") && instance.is_sys_version_info() => { - let python_version = Program::get(db).python_version(db); + let python_version = env.python_version(db); let segment = if name == "major" { python_version.major } else { @@ -4814,13 +5584,13 @@ impl<'db> Type<'db> { } Type::PropertyInstance(property) if name == "fget" => { - Place::bound(property.getter(db).unwrap_or(Type::none(db))).into() + Place::bound(property.getter(db).unwrap_or(Type::none(db, env))).into() } Type::PropertyInstance(property) if name == "fset" => { - Place::bound(property.setter(db).unwrap_or(Type::none(db))).into() + Place::bound(property.setter(db).unwrap_or(Type::none(db, env))).into() } Type::PropertyInstance(property) if name == "fdel" => { - Place::bound(property.deleter(db).unwrap_or(Type::none(db))).into() + Place::bound(property.deleter(db).unwrap_or(Type::none(db, env))).into() } Type::LiteralValue(literal) @@ -4836,7 +5606,7 @@ impl<'db> Type<'db> { Place::bound(Type::int_literal(i64::from(bool_value))).into() } - Type::ModuleLiteral(module) => module.static_member(db, name_str), + Type::ModuleLiteral(module) => module.static_member(db, env, name_str).into(), // If a protocol does not include a member and the policy disables falling back to // `object`, we return `Place::Undefined` here. This short-circuits attribute lookup @@ -4846,11 +5616,10 @@ impl<'db> Type<'db> { // // Note that we could do this for *all* protocols, but it's only *necessary* for synthesized // ones, and the standard logic is *probably* more performant for class-based protocols? - Type::ProtocolInstance(ProtocolInstanceType { - inner: Protocol::Synthesized(protocol), - .. - }) if policy.mro_no_object_fallback() - && !protocol.interface(db).includes_member(db, name_str) => + Type::ProtocolInstance(protocol) + if protocol.class_origin(db).is_none() + && policy.mro_no_object_fallback() + && !protocol.interface(db).includes_member(db, name_str) => { Place::Undefined.into() } @@ -4860,7 +5629,7 @@ impl<'db> Type<'db> { // bound method — see `protocol_class::symbolic_method_member` Type::ProtocolInstance(_) | Type::TypeVar(_) if let Some(member) = - protocol_class::symbolic_method_member(db, this, name, receiver) => + protocol_class::symbolic_method_member(db, env, this, name, receiver) => { Place::bound(member).into() } @@ -4875,23 +5644,26 @@ impl<'db> Type<'db> { Type::NewTypeInstance(new_type_instance) if this.as_union_like(db).is_some() => { new_type_instance .concrete_base_type(db) - .member_lookup_with_policy(db, name_str, policy) + .member_lookup_with_policy_and_receiver(db, env, name_str, policy, None) } Type::TypeAlias(alias) => alias .value_type(db) - .member_lookup_with_policy_and_receiver(db, name_str, policy, receiver), + .member_lookup_with_policy_and_receiver(db, env, name_str, policy, receiver), _ if policy.no_instance_fallback() => { let receiver = receiver.unwrap_or(this); - Type::invoke_descriptor_protocol( + let result = Type::invoke_descriptor_protocol( db, + env, key, receiver, Place::Undefined.into(), InstanceFallbackShadowsNonDataDescriptor::No, - ) - .map_type(|ty| ty.bind_self_typevars(db, receiver)) + ); + map_member_lookup_type(db, result, |ty| { + ty.bind_self_typevars(db, env, receiver) + }) } Type::LiteralValue(literal) @@ -4899,13 +5671,14 @@ impl<'db> Type<'db> { && let Some(enum_literal) = literal.as_enum() && !enums::class_defines_property( db, + env, enum_literal.enum_class(db), name_str, ) => { let enum_class = enum_literal.enum_class_literal(db); let is_enum_subclass = Type::ClassLiteral(enum_class.class_literal(db)) - .is_subtype_of(db, KnownClass::Enum.to_subclass_of(db)); + .is_subtype_of(db, env, KnownClass::Enum.to_subclass_of(db, env)); let ty = match name_str { "name" if is_enum_subclass => { @@ -4925,53 +5698,50 @@ impl<'db> Type<'db> { // a keyword-variadic pack shares the `ParamSpec` value representation, so it // resolves the same two components — basedpython has no source spelling for // them, and the type expression that names one is reported there - Type::TypeVar(typevar) if name_str == "args" && typevar.is_parameter_pack(db) => { - Place::declared(Type::TypeVar( - typevar.with_paramspec_attr(db, ParamSpecAttrKind::Args), - )) - .into() - } - Type::TypeVar(typevar) if name_str == "kwargs" && typevar.is_parameter_pack(db) => { - Place::declared(Type::TypeVar( - typevar.with_paramspec_attr(db, ParamSpecAttrKind::Kwargs), - )) - .into() + Type::TypeVar(typevar) + if typevar.is_parameter_pack(db) + && let Some(attr) = ParamSpecAttrKind::from_name(name_str) => + { + Place::declared(Type::TypeVar(typevar.with_paramspec_attr(db, attr))).into() } Type::TypeVar(typevar) => { let receiver = receiver.unwrap_or(this); - if let Some(bound) = typevar - .typevar(db) - .bound_or_constraints(db) - .map(|bound| bound.as_type(db)) - && bound.to_instance(db).is_some() + if let Some(bound_or_constraints) = + typevar.typevar(db).bound_or_constraints(db, env) { - // A TypeVar can be bounded by a class-object type such as `type[A]`, which - // requires the full lookup path rather than instance-member lookup. - return bound.member_lookup_with_policy_and_receiver( - db, - name_str, - policy, - Some(receiver), - ); + // Use the bound's complete lookup behavior, but retain the original + // receiver so descriptors and `Self` remain correctly specialized. + bound_or_constraints + .as_type(db, env) + .member_lookup_with_policy_and_receiver( + db, + env, + name_str, + policy, + Some(receiver), + ) + } else { + instance_like_member_lookup(db, env, key, receiver) } - - instance_like_member_lookup(db, key, receiver) } Type::NominalInstance(instance) if matches!(name_str, "name" | "_name_" | "value" | "_value_") - && let class_literal = instance.class_literal(db) + && let class_literal = instance.class_literal(db, env) && let Some(metadata) = enum_metadata(db, class_literal) - && !enums::class_defines_property(db, class_literal, name_str) => + && !enums::class_defines_property(db, env, class_literal, name_str) => { - let is_enum_subclass = Type::ClassLiteral(class_literal) - .is_subtype_of(db, KnownClass::Enum.to_subclass_of(db)); + let is_enum_subclass = Type::ClassLiteral(class_literal).is_subtype_of( + db, + env, + KnownClass::Enum.to_subclass_of(db, env), + ); let ty = match name_str { - "name" if is_enum_subclass => metadata.instance_name_type(db), - "_name_" => metadata.instance_name_type(db), - "value" if is_enum_subclass => metadata.instance_value_type(db), - "_value_" => metadata.instance_value_type(db), + "name" if is_enum_subclass => metadata.instance_name_type(db, env), + "_name_" => metadata.instance_name_type(db, env), + "value" if is_enum_subclass => metadata.instance_value_type(db, env), + "_value_" => metadata.instance_value_type(db, env), _ => None, }; @@ -4997,10 +5767,15 @@ impl<'db> Type<'db> { let wrapped = partial.wrapped(db).inner(db); let nominal_lookup = partial .partial(db) - .into_functools_partial_instance(db) - .member_lookup_with_policy_and_receiver(db, name_str, policy, receiver); + .into_functools_partial_instance(db, env) + .member_lookup_with_policy_and_receiver( + db, env, name_str, policy, receiver, + ); if name_str == "func" { - match nominal_lookup.place { + match nominal_lookup + .unwrap_or_else(|error| error.fallback_member(db)) + .place + { Place::Defined(DefinedPlace { origin, definedness, @@ -5037,13 +5812,16 @@ impl<'db> Type<'db> { | Type::TypeForm(..) | Type::TypedDict(_) => { let receiver = receiver.unwrap_or(this); - instance_like_member_lookup(db, key, receiver) + instance_like_member_lookup(db, env, key, receiver) } Type::ClassLiteral(..) | Type::GenericAlias(..) | Type::SubclassOf(..) => { // A class-object lookup can originate from a TypeVar bound such as `type[A]`. - // Retain that TypeVar as the receiver so `Self` binds to `T'instance`, not `A`. - let receiver = receiver.unwrap_or(this); + // Retain that TypeVar as the receiver so `Self` binds to `T'instance`, not `A`, + // unless its constraints also include non-class-object types. + let receiver = receiver + .filter(|receiver| receiver.to_instance_approximation(db, env).is_some()) + .unwrap_or(this); let enum_class = match this { Type::ClassLiteral(literal) => literal.into_enum_class(db), // a subscripted generic enum still reaches its own members: @@ -5054,7 +5832,7 @@ impl<'db> Type<'db> { } Type::SubclassOf(subclass_of) => subclass_of .subclass_of() - .into_class(db) + .into_class(db, env) .and_then(|class| class.class_literal(db).into_enum_class(db)), _ => None, }; @@ -5069,27 +5847,33 @@ impl<'db> Type<'db> { .into(); } - let class_attr_plain = this.class_object_member(db, name_str, policy); + let class_attr_plain = this.class_object_member(db, env, name_str, policy); - let self_instance = receiver.to_instance_approximation(db).expect( + let self_instance = receiver.to_instance_approximation(db, env).expect( "The receiver for a class-object lookup should always be instantiable", ); - let class_attr_plain = - class_attr_plain.map_type(|ty| ty.bind_self_typevars(db, self_instance)); + let class_attr_plain = class_attr_plain + .map_type(|ty| ty.bind_self_typevars(db, env, self_instance)); - let class_attr_fallback = Type::try_call_dunder_get_on_attribute( - db, - class_attr_plain, - None, - receiver, - ) - .0; + let (class_attr_fallback, _, class_attr_error) = + Type::try_call_dunder_get_on_attribute( + db, + env, + class_attr_plain, + None, + receiver, + ); let result = Type::invoke_descriptor_protocol( db, + env, key, receiver, - class_attr_fallback, + member_lookup_result( + db, + class_attr_fallback, + class_attr_error.map(MemberLookupErrorKind::DescriptorGet), + ), InstanceFallbackShadowsNonDataDescriptor::Yes, ); @@ -5099,13 +5883,13 @@ impl<'db> Type<'db> { // attribute access falls back to `__getattr__`/`__getattribute__` on the // class. `try_call_dunder` adds `NO_INSTANCE_FALLBACK`, which causes the // lookup to hit the catch-all that only checks the meta-type (the metaclass). - let result = this.fallback_to_getattr(db, name, result, policy); + let result = this.fallback_to_getattr(db, env, name, result, policy); // Unlike a specific class literal, `type[C]` can represent any subclass of // `C`, unless a `TypeVar` upper bound normalizes to a final class. let result = if let Type::SubclassOf(subclass_of) = this - && subclass_of.exact_typevar_upper_bound(db).is_none() + && subclass_of.exact_typevar_upper_bound(db, env).is_none() { - promote_inferred_attribute_class_literals(db, result) + promote_inferred_attribute_class_literals(db, env, result) } else { result }; @@ -5117,11 +5901,16 @@ impl<'db> Type<'db> { if let Type::SubclassOf(subclass_of) = this && let SubclassOfInner::Dynamic(dynamic) = subclass_of.subclass_of() { - result.map_type(|ty| { + map_member_lookup_type(db, result, |ty| { if ty.is_dynamic() { ty } else { - IntersectionType::from_two_elements(db, ty, Type::Dynamic(dynamic)) + IntersectionType::from_two_elements( + db, + env, + ty, + Type::Dynamic(dynamic), + ) } }) } else { @@ -5135,18 +5924,19 @@ impl<'db> Type<'db> { // 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); + let owner_attr = + bound_super.find_name_in_mro_after_pivot(db, env, name_str, policy); bound_super - .try_call_dunder_get_on_attribute(db, owner_attr) - .unwrap_or(owner_attr) + .try_call_dunder_get_on_attribute(db, env, owner_attr) + .unwrap_or_else(|| owner_attr.into()) } } } if self.materialized_divergent_fallback().is_none() { if name == "__class__" { - return Place::bound(self.dunder_class(db)).into(); + return Place::bound(self.dunder_class(db, env)).into(); } if matches!(self, Type::Dynamic(_) | Type::Divergent(_) | Type::Never) { @@ -5154,7 +5944,7 @@ impl<'db> Type<'db> { } } - let key = MemberLookupKey::new(db, self, name, policy); + let key = MemberLookupKey::new(db, env.program(db), self, name, policy); match receiver { Some(receiver) => member_lookup_with_policy_and_receiver_inner(db, key, receiver), None => member_lookup_with_policy_inner(db, key), @@ -5166,8 +5956,12 @@ impl<'db> Type<'db> { /// /// 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> { + fn len(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Option> { + fn non_negative_int_literal<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + ) -> Option> { match ty { // TODO: Emit diagnostic for non-integers and negative integers Type::LiteralValue(literal) => match literal.kind() { @@ -5175,28 +5969,29 @@ impl<'db> Type<'db> { LiteralValueTypeKind::Bool(value) => Some(Type::int_literal(i64::from(value))), _ => None, }, - Type::Union(union) => { - union.try_map(db, |element| non_negative_int_literal(db, *element)) - } + Type::Union(union) => union.try_map(db, env, |element| { + non_negative_int_literal(db, env, *element) + }), _ => None, } } let return_ty = match self.try_call_dunder( db, + env, "__len__", CallArguments::none(), TypeContext::default(), ) { - Ok(bindings) => bindings.return_type(db), - Err(CallDunderError::PossiblyUnbound { bindings, .. }) => bindings.return_type(db), + Ok(bindings) => bindings.return_type(db, env), + Err(CallDunderError::PossiblyUnbound { bindings, .. }) => bindings.return_type(db, env), // TODO: emit a diagnostic Err(CallDunderError::MethodNotAvailable) => return None, - Err(CallDunderError::CallError(_, bindings, _)) => bindings.return_type(db), + Err(CallDunderError::CallError(_, bindings, _)) => bindings.return_type(db, env), }; - non_negative_int_literal(db, return_ty) + non_negative_int_literal(db, env, return_ty) } /// If this type is a `ParamSpec` type variable, returns it. Otherwise, returns `None`. @@ -5210,13 +6005,23 @@ impl<'db> Type<'db> { // Returns the value type of a `__getitem__` dunder call on this object. // // Returns `None` if `__getitem__` is undefined or results in a call error. - fn getitem_dunder_call(self, db: &'db dyn Db, key: Option<&str>) -> Option> { + fn getitem_dunder_call( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + key: Option<&str>, + ) -> Option> { let key = key .map(|key| Type::string_literal(db, key)) .unwrap_or(Type::unknown()); match self - .member_lookup_with_policy(db, "__getitem__", MemberLookupPolicy::NO_INSTANCE_FALLBACK) + .member_lookup_with_policy( + db, + env, + "__getitem__", + MemberLookupPolicy::NO_INSTANCE_FALLBACK, + ) .place { Place::Defined(DefinedPlace { @@ -5224,9 +6029,9 @@ impl<'db> Type<'db> { definedness: Definedness::AlwaysDefined, .. }) => getitem_method - .try_call(db, &CallArguments::positional([key])) + .try_call(db, env, &CallArguments::positional([key])) .ok() - .map(|bindings| bindings.return_type(db)), + .map(|bindings| bindings.return_type(db, env)), _ => None, } @@ -5234,9 +6039,13 @@ impl<'db> Type<'db> { /// Returns the key and value types of this object if it was unpacked using `**`, /// or `None` if the object does not support unpacking. - fn unpack_keys_and_items(self, db: &'db dyn Db) -> Option<(Type<'db>, Type<'db>)> { + fn unpack_keys_and_items( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option<(Type<'db>, Type<'db>)> { let key_ty = match self - .member_lookup_with_policy(db, "keys", MemberLookupPolicy::NO_INSTANCE_FALLBACK) + .member_lookup_with_policy(db, env, "keys", MemberLookupPolicy::NO_INSTANCE_FALLBACK) .place { Place::Defined(DefinedPlace { @@ -5244,15 +6053,15 @@ impl<'db> Type<'db> { definedness: Definedness::AlwaysDefined, .. }) => keys_method - .try_call(db, &CallArguments::none()) + .try_call(db, env, &CallArguments::none()) .ok() .and_then(|bindings| { Some( bindings - .return_type(db) - .try_iterate(db) + .return_type(db, env) + .try_iterate(db, env) .ok()? - .homogeneous_element_type(db), + .homogeneous_element_type(db, env), ) })?, @@ -5260,7 +6069,7 @@ impl<'db> Type<'db> { }; let value_ty = self - .getitem_dunder_call(db, None) + .getitem_dunder_call(db, env, None) .unwrap_or(Type::unknown()); Some((key_ty, value_ty)) @@ -5276,28 +6085,31 @@ impl<'db> Type<'db> { /// 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> { + fn bindings(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Bindings<'db> { if let Some(fallback) = self.materialized_divergent_fallback() { - return fallback.bindings(db); + return fallback.bindings(db, env); } match self { - Type::Overlapping(overlapping) => overlapping.value_type(db).bindings(db), - Type::Restricted(restricted) => restricted.value_type(db).bindings(db), - Type::Deferred(deferred) => deferred.reduced(db).bindings(db), + Type::Overlapping(overlapping) => overlapping.value_type(db, env).bindings(db, env), + Type::Restricted(restricted) => restricted.value_type(db).bindings(db, env), + Type::Deferred(deferred) => deferred.reduced(db, env).bindings(db, env), Type::Callable(callable) => { CallableBinding::from_overloads(self, callable.signatures(db).iter().cloned()) .into() } Type::TypeVar(bound_typevar) => { - match bound_typevar.typevar(db).bound_or_constraints(db) { + match bound_typevar.typevar(db).bound_or_constraints(db, env) { None => CallableBinding::not_callable(self).into(), - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound.bindings(db), + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound.bindings(db, env), Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { Bindings::from_union( self, - constraints.elements(db).iter().map(|ty| ty.bindings(db)), + constraints + .elements(db) + .iter() + .map(|ty| ty.bindings(db, env)), ) } } @@ -5311,7 +6123,7 @@ impl<'db> Type<'db> { // checking it structurally again during call inference. if self_instance .as_protocol_instance() - .is_some_and(|protocol| protocol.to_nominal_instance().is_some()) + .is_some_and(|protocol| protocol.class_origin(db).is_some()) && signature .overloads .iter() @@ -5320,7 +6132,7 @@ impl<'db> Type<'db> { let mut binding = CallableBinding::from_overloads(self, signature.overloads.iter().cloned()) .with_bound_type(bound_method.typing_self_type(db)); - binding.bake_bound_type_into_overloads(db); + binding.bake_bound_type_into_overloads(db, env); binding.into() } else { CallableBinding::from_overloads(self, signature.overloads.iter().cloned()) @@ -5330,11 +6142,11 @@ impl<'db> Type<'db> { } Type::KnownBoundMethod(method) => { - CallableBinding::from_overloads(self, method.signatures(db)).into() + CallableBinding::from_overloads(self, method.signatures(db, env)).into() } Type::WrapperDescriptor(wrapper_descriptor) => { - CallableBinding::from_overloads(self, wrapper_descriptor.signatures(db)).into() + CallableBinding::from_overloads(self, wrapper_descriptor.signatures(db, env)).into() } // TODO: We should probably also check the original return type of the function @@ -5356,6 +6168,7 @@ impl<'db> Type<'db> { Some(KnownFunction::AssertType) => { let val_ty = BoundTypeVarInstance::synthetic( db, + env, Name::new_static("T"), TypeVarVariance::Invariant, ); @@ -5363,7 +6176,7 @@ impl<'db> Type<'db> { Binding::single( self, Signature::new_generic( - Some(GenericContext::from_typevar_instances(db, [val_ty])), + Some(GenericContext::from_typevar_instances(db, env, [val_ty])), Parameters::standard([ Parameter::positional_only(Some(Name::new_static("value"))) .with_annotated_type(Type::TypeVar(val_ty)), @@ -5409,9 +6222,10 @@ impl<'db> Type<'db> { .into(), Some(KnownFunction::Dataclass) => { + let python_version = env.python_version(db); let bool_parameter = |name: &'static str, default: bool| { Parameter::keyword_only(Name::new_static(name)) - .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_annotated_type(KnownClass::Bool.to_instance(db, env)) .with_default_type(Type::bool_literal(default)) }; @@ -5424,7 +6238,7 @@ impl<'db> Type<'db> { bool_parameter("frozen", false), ]; - if Program::get(db).python_version(db) >= ast::PythonVersion::PY310 { + if python_version >= ast::PythonVersion::PY310 { decorator_factory_parameters.extend([ bool_parameter("match_args", true), bool_parameter("kw_only", false), @@ -5432,7 +6246,7 @@ impl<'db> Type<'db> { ]); } - if Program::get(db).python_version(db) >= ast::PythonVersion::PY311 { + if python_version >= ast::PythonVersion::PY311 { decorator_factory_parameters.push(bool_parameter("weakref_slot", false)); } @@ -5452,13 +6266,13 @@ impl<'db> Type<'db> { [ // def dataclass(cls: None, /, *, ...) -> Callable[[type[_T]], type[_T]]: ... Signature::new( - Parameters::standard(parameters_with_cls(Type::none(db))), + Parameters::standard(parameters_with_cls(Type::none(db, env))), Type::unknown(), ), // def dataclass(cls: type[_T], /, *, ...) -> type[_T]: ... Signature::new( Parameters::standard(parameters_with_cls( - KnownClass::Type.to_instance(db), + KnownClass::Type.to_instance(db, env), )), Type::unknown(), ), @@ -5493,48 +6307,63 @@ impl<'db> Type<'db> { Type::ClassLiteral(class) => self // TODO this should be called from `constructor_bindings` for better consistency - .known_class_literal_bindings(db, class) - .unwrap_or_else(|| self.constructor_bindings(db, ClassType::NonGeneric(class))), + .known_class_literal_bindings(db, env, class) + .unwrap_or_else(|| { + self.constructor_bindings(db, env, ClassType::NonGeneric(class)) + }), - Type::GenericAlias(alias) => self.constructor_bindings(db, ClassType::Generic(alias)), + Type::GenericAlias(alias) => { + self.constructor_bindings(db, env, ClassType::Generic(alias)) + } Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() { SubclassOfInner::Dynamic(dynamic_type) => { Binding::single(self, Signature::dynamic(Type::Dynamic(dynamic_type))).into() } - SubclassOfInner::Class(class) => self.constructor_bindings(db, class), - SubclassOfInner::Protocol(protocol) => protocol.class_origin().map_or_else( + SubclassOfInner::Class(class) => self.constructor_bindings(db, env, class), + SubclassOfInner::Protocol(protocol) => protocol.class_origin(db).map_or_else( || Binding::single(self, Signature::dynamic(Type::unknown())).into(), - |origin| self.constructor_bindings(db, *origin), + |origin| { + let bindings = self.constructor_bindings(db, env, *origin); + if protocol.materialization_kind(db).is_some() { + bindings.with_constructed_instance_type( + db, + Type::ProtocolInstance(protocol), + ) + } else { + bindings + } + }, ), SubclassOfInner::TypeVar(tvar) => { let constructor_instance_type = Type::TypeVar(tvar); - let bindings = match tvar.typevar(db).bound_or_constraints(db) { - None => KnownClass::Type.to_instance(db).bindings(db), - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - bound.to_meta_type(db).bindings(db) + let bindings = match tvar.typevar(db).require_bound_or_constraints(db, env) { + TypeVarBoundOrConstraints::UpperBound(bound) => { + let constructor = bound.constructor_for_typevar_bound(db, env); + if let Type::ClassLiteral(class) = constructor + && let Some(bindings) = + self.known_class_literal_bindings(db, env, class) + { + bindings + } else { + constructor.bindings(db, env) + } } - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + TypeVarBoundOrConstraints::Constraints(constraints) => { Bindings::from_union( self, constraints .elements(db) .iter() - .map(|ty| ty.to_meta_type(db).bindings(db)), + .map(|ty| ty.to_meta_type(db, env).bindings(db, env)), ) } }; - // TODO We would ideally be able to just do `into_constructor_bindings` in the - // no-bounds/constraints case above (where we get back the bindings for - // `Type.__call__`), and just do `with_constructed_instance_type` in the - // bound/constrained cases, where we should get back constructor bindings (or - // if we don't, we probably shouldn't return `T` from the call?). But currently - // we can't because we special-case some built-in types to return regular - // (not constructor) bindings from `constructor_bindings()`. + // Some built-in constructors, including `object`, are special-cased as regular + // callable bindings. Wrap them so that every bound or constrained call has + // constructor context and constructs `T`; existing constructor bindings keep + // their original kind. bindings - // `into_constructor_bindings` is a no-op for already-constructor bindings, - // so we are just setting the `MetaclassCall` type for `Type.__call__`, or - // the special-cased builtin classes that return regular bindings. .into_constructor_bindings( constructor_instance_type, ConstructorCallableKind::MetaclassCall, @@ -5559,6 +6388,7 @@ impl<'db> Type<'db> { match self .member_lookup_with_policy( db, + env, "__call__", MemberLookupPolicy::NO_INSTANCE_FALLBACK, ) @@ -5569,7 +6399,7 @@ impl<'db> Type<'db> { definedness: boundness, .. }) => { - let mut bindings = dunder_callable.bindings(db); + let mut bindings = dunder_callable.bindings(db, env); bindings.replace_callable_type(dunder_callable, self); if boundness == Definedness::PossiblyUndefined { bindings.set_dunder_call_is_possibly_unbound(); @@ -5592,14 +6422,40 @@ impl<'db> Type<'db> { union .elements(db) .iter() - .map(|element| element.bindings(db)), + .map(|element| element.bindings(db, env)), ), + // A narrowed `type[T: Base] & type[Child]` still needs to construct `T & Child`, + // but its constructor must come from `Child`, not from `Base` as an independent, + // competing alternative. Flattening the projected instance lets intersection + // simplification select that constructor without discarding unrelated providers. + Type::Intersection(intersection) + if intersection.positive(db).iter().all(|element| { + // A metaclass instance also has an instance-space projection, but it can + // provide an independent `__call__`. Only simplify actual class-object + // variants so `type[Base] & Meta` retains both callable candidates. + matches!( + element.resolve_type_alias(db), + Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_) + ) + }) && let Some(instance_type) = self.to_instance_approximation(db, env) + && let Type::NominalInstance(lookup_instance) = + instance_type.flatten_typevars(db, env) + && let Some(bindings) = { + let bindings = lookup_instance.to_meta_type(db, env).bindings(db, env); + bindings.has_only_constructor_items().then_some(bindings) + } => + { + bindings + .with_constructed_instance_type(db, instance_type) + .with_callable_type(self) + } + Type::Intersection(intersection) => Bindings::from_intersection( self, intersection .positive_elements_or_object(db) - .map(|element| element.bindings(db)), + .map(|element| element.bindings(db, env)), ), // Callable as long as *some* materialization is, like an intersection; but the @@ -5610,24 +6466,28 @@ impl<'db> Type<'db> { unsafe_union .elements(db) .iter() - .map(|element| element.bindings(db)), + .map(|element| element.bindings(db, env)), ), - Type::EnumComplement(complement) => complement.to_intersection(db).bindings(db), + Type::EnumComplement(complement) => { + complement.to_intersection(db, env).bindings(db, env) + } Type::DataclassDecorator(_) => { let typevar = BoundTypeVarInstance::synthetic( db, + env, Name::new_static("T"), TypeVarVariance::Invariant, ); - let typevar_meta = SubclassOfType::from(db, typevar); - let context = GenericContext::from_typevar_instances(db, [typevar]); + let typevar_meta = SubclassOfType::from(db, env, typevar); + let context = GenericContext::from_typevar_instances(db, env, [typevar]); let parameters = [Parameter::positional_only(Some(Name::new_static("cls"))) .with_annotated_type(typevar_meta)]; // Intersect with `Any` for the return type to reflect the fact that the `dataclass()` // decorator adds methods to the class - let returns = IntersectionType::from_two_elements(db, typevar_meta, Type::any()); + let returns = + IntersectionType::from_two_elements(db, env, typevar_meta, Type::any()); let signature = Signature::new_generic( Some(context), Parameters::standard(parameters), @@ -5641,7 +6501,7 @@ impl<'db> Type<'db> { Type::LiteralValue(literal) => match literal.kind() { LiteralValueTypeKind::Enum(enum_literal) => { - enum_literal.enum_class_instance(db).bindings(db) + enum_literal.enum_class_instance(db, env).bindings(db, env) } _ => CallableBinding::not_callable(self).into(), }, @@ -5650,7 +6510,7 @@ impl<'db> Type<'db> { self, Signature::new( Parameters::standard([Parameter::positional_only(None) - .with_annotated_type(newtype.base(db).instance_type(db))]), + .with_annotated_type(newtype.base(db).instance_type(db, env))]), Type::NewTypeInstance(newtype), ), ) @@ -5659,13 +6519,13 @@ impl<'db> Type<'db> { Type::KnownInstance( KnownInstanceType::FunctoolsPartial(partial) | KnownInstanceType::FunctoolsPartialCall(partial), - ) => Type::Callable(partial.partial(db)).bindings(db), + ) => Type::Callable(partial.partial(db)).bindings(db, env), Type::KnownInstance(known_instance) => { - known_instance.instance_fallback(db).bindings(db) + known_instance.instance_fallback(db, env).bindings(db, env) } - Type::TypeAlias(alias) => alias.value_type(db).bindings(db), + Type::TypeAlias(alias) => alias.value_type(db).bindings(db, env), Type::PropertyInstance(_) | Type::AlwaysFalsy @@ -5682,6 +6542,7 @@ impl<'db> Type<'db> { fn known_class_literal_bindings( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: ClassLiteral<'db>, ) -> Option> { // TODO: Some of these cases date back to when we didn't even support overloads yet; see if @@ -5701,7 +6562,7 @@ impl<'db> Type<'db> { )) .with_annotated_type(Type::any()) .with_default_type(Type::bool_literal(false))]), - KnownClass::Bool.to_instance(db), + KnownClass::Bool.to_instance(db, env), ), ) .into(), @@ -5741,16 +6602,19 @@ impl<'db> Type<'db> { Parameter::positional_only(Some(Name::new_static("obj"))) .with_annotated_type(Type::any()), ]), - KnownClass::Super.to_instance(db), + KnownClass::Super.to_instance(db, env), ), Signature::new( Parameters::standard([Parameter::positional_only(Some( Name::new_static("t"), )) .with_annotated_type(Type::any())]), - KnownClass::Super.to_instance(db), + KnownClass::Super.to_instance(db, env), + ), + Signature::new( + Parameters::empty(), + KnownClass::Super.to_instance(db, env), ), - Signature::new(Parameters::empty(), KnownClass::Super.to_instance(db)), ], ) .into(), @@ -5769,7 +6633,7 @@ impl<'db> Type<'db> { // stacklevel: int = 1 // ) -> Self: ... // ``` - let warning_class_type = KnownClass::Warning.to_subclass_of(db); + let warning_class_type = KnownClass::Warning.to_subclass_of(db, env); Some( Binding::single( @@ -5781,15 +6645,16 @@ impl<'db> Type<'db> { Parameter::keyword_only(Name::new_static("category")) .with_annotated_type(UnionType::from_two_elements( db, + env, warning_class_type, - Type::none(db), + Type::none(db, env), )) .with_default_type(warning_class_type), Parameter::keyword_only(Name::new_static("stacklevel")) - .with_annotated_type(KnownClass::Int.to_instance(db)) + .with_annotated_type(KnownClass::Int.to_instance(db, env)) .with_default_type(Type::int_literal(1)), ]), - KnownClass::Deprecated.to_instance(db), + KnownClass::Deprecated.to_instance(db, env), ), ) .into(), @@ -5812,22 +6677,24 @@ impl<'db> Type<'db> { Signature::new( Parameters::standard([ Parameter::positional_or_keyword(Name::new_static("name")) - .with_annotated_type(KnownClass::Str.to_instance(db)), + .with_annotated_type(KnownClass::Str.to_instance(db, env)), Parameter::positional_or_keyword(Name::new_static("value")) .with_annotated_type(object_type_form(db)), Parameter::keyword_only(Name::new_static("type_params")) .with_annotated_type(Type::homogeneous_tuple( db, + env, UnionType::from_elements( db, + env, [ - KnownClass::TypeVar.to_instance(db), - KnownClass::ParamSpec.to_instance(db), - KnownClass::TypeVarTuple.to_instance(db), + KnownClass::TypeVar.to_instance(db, env), + KnownClass::ParamSpec.to_instance(db, env), + KnownClass::TypeVarTuple.to_instance(db, env), ], ), )) - .with_default_type(Type::empty_tuple(db)), + .with_default_type(Type::empty_tuple(db, env)), ]), Type::unknown(), ), @@ -5848,7 +6715,7 @@ impl<'db> Type<'db> { Parameter::positional_only(None).with_annotated_type(Type::any()), Parameter::positional_only(None).with_annotated_type(Type::any()), ]), - Type::none(db), + Type::none(db, env), ); let deleter_signature = Signature::new( Parameters::standard([ @@ -5865,31 +6732,35 @@ impl<'db> Type<'db> { Parameter::positional_or_keyword(Name::new_static("fget")) .with_annotated_type(UnionType::from_two_elements( db, + env, Type::single_callable(db, getter_signature), - Type::none(db), + Type::none(db, env), )) - .with_default_type(Type::none(db)), + .with_default_type(Type::none(db, env)), Parameter::positional_or_keyword(Name::new_static("fset")) .with_annotated_type(UnionType::from_two_elements( db, + env, Type::single_callable(db, setter_signature), - Type::none(db), + Type::none(db, env), )) - .with_default_type(Type::none(db)), + .with_default_type(Type::none(db, env)), Parameter::positional_or_keyword(Name::new_static("fdel")) .with_annotated_type(UnionType::from_two_elements( db, + env, Type::single_callable(db, deleter_signature), - Type::none(db), + Type::none(db, env), )) - .with_default_type(Type::none(db)), + .with_default_type(Type::none(db, env)), Parameter::positional_or_keyword(Name::new_static("doc")) .with_annotated_type(UnionType::from_two_elements( db, - KnownClass::Str.to_instance(db), - Type::none(db), + env, + KnownClass::Str.to_instance(db, env), + Type::none(db, env), )) - .with_default_type(Type::none(db)), + .with_default_type(Type::none(db, env)), ]), Type::unknown(), ), @@ -5905,6 +6776,7 @@ impl<'db> Type<'db> { // ``` let return_ty = BoundTypeVarInstance::synthetic( db, + env, Name::new_static("_T"), TypeVarVariance::Covariant, ); @@ -5913,7 +6785,7 @@ impl<'db> Type<'db> { Binding::single( self, Signature::new_generic( - Some(GenericContext::from_typevar_instances(db, [return_ty])), + Some(GenericContext::from_typevar_instances(db, env, [return_ty])), Parameters::concatenate( db, vec![ @@ -5928,8 +6800,11 @@ impl<'db> Type<'db> { ], ConcatenateTail::Gradual, ), - KnownClass::FunctoolsPartial - .to_specialized_instance(db, &[Type::TypeVar(return_ty)]), + KnownClass::FunctoolsPartial.to_specialized_instance( + db, + env, + &[Type::TypeVar(return_ty)], + ), ), ) .into(), @@ -5939,6 +6814,7 @@ impl<'db> Type<'db> { KnownClass::Tuple => { let element_ty = BoundTypeVarInstance::synthetic( db, + env, Name::new_static("T"), TypeVarVariance::Covariant, ); @@ -5954,17 +6830,24 @@ impl<'db> Type<'db> { CallableBinding::from_overloads( self, [ - Signature::new(Parameters::empty(), Type::empty_tuple(db)), + Signature::new(Parameters::empty(), Type::empty_tuple(db, env)), Signature::new_generic( - Some(GenericContext::from_typevar_instances(db, [element_ty])), + Some(GenericContext::from_typevar_instances( + db, + env, + [element_ty], + )), Parameters::standard([Parameter::positional_only(Some( Name::new_static("iterable"), )) .with_annotated_type( - KnownClass::Iterable - .to_specialized_instance(db, &[Type::TypeVar(element_ty)]), + KnownClass::Iterable.to_specialized_instance( + db, + env, + &[Type::TypeVar(element_ty)], + ), )]), - Type::homogeneous_tuple(db, Type::TypeVar(element_ty)), + Type::homogeneous_tuple(db, env, Type::TypeVar(element_ty)), ), ], ) @@ -5978,9 +6861,15 @@ impl<'db> Type<'db> { // Build bindings for constructor calls by combining `__new__`/`__init__` signatures. // Returns fallback bindings for cases that intentionally keep bespoke call behavior. - fn constructor_bindings(self, db: &'db dyn Db, class: ClassType<'db>) -> Bindings<'db> { + fn constructor_bindings( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + class: ClassType<'db>, + ) -> Bindings<'db> { fn resolve_dunder_new_callable<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, owner: Type<'db>, place: Place<'db>, ) -> Option<(Type<'db>, Definedness)> { @@ -5997,7 +6886,7 @@ impl<'db> Type<'db> { ) { return None; } - match place.try_call_dunder_get(db, owner) { + match place.try_call_dunder_get(db, env, owner) { Place::Defined(DefinedPlace { ty: callable, definedness, @@ -6008,6 +6897,7 @@ impl<'db> Type<'db> { } fn bind_constructor_new<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, bindings: Bindings<'db>, self_type: Type<'db>, ) -> Bindings<'db> { @@ -6017,7 +6907,7 @@ impl<'db> Type<'db> { // first, then bind `cls` for constructor-call semantics (the call site omits `cls`). // Note: This intentionally preserves `type.__call__` behavior for `@classmethod __new__`, // which receives an extra implicit `cls` and errors at call sites. - binding.bake_bound_type_into_overloads(db); + binding.bake_bound_type_into_overloads(db, env); binding.bound_type = Some(self_type); binding }) @@ -6029,7 +6919,7 @@ impl<'db> Type<'db> { // Keep bespoke constructor behavior for cases that don't map cleanly to `__new__`/`__init__`. let fallback_bindings = || { let return_type = self - .to_instance_approximation(db) + .to_instance_approximation(db, env) .unwrap_or(Type::unknown()); Binding::single( self, @@ -6042,11 +6932,12 @@ impl<'db> Type<'db> { .into() }; - // Checking TypedDict construction happens in `infer_call_expression_impl`. - // We don't want to use the synthesized binding for type inference, so here we just - // return a permissive fallback binding. - if class_literal.is_typed_dict(db) - || class::CodeGeneratorKind::TypedDict.matches(db, class_literal) + // Specialized and non-generic TypedDict constructors use their dedicated validation. + // An unspecialized generic constructor also needs its real `__init__` signature so + // ordinary call inference can solve the class type variables. + if (class_literal.is_typed_dict(db) + || class::CodeGeneratorKind::TypedDict.matches(db, class_literal)) + && (!matches!(self, Type::ClassLiteral(_)) || class_generic_context.is_none()) { return fallback_bindings(); } @@ -6076,9 +6967,9 @@ impl<'db> Type<'db> { // functional syntax for creating enum classes. TODO we should ideally check e.g. // `MyEnum(1)` to make sure `1` is a valid value for `MyEnum`. if KnownClass::Enum - .to_class_literal(db) + .to_class_literal(db, env) .to_class_type(db) - .is_some_and(|enum_class| class.is_subclass_of(db, enum_class)) + .is_some_and(|enum_class| class.is_subclass_of(db, env, enum_class)) { return fallback_bindings(); } @@ -6103,28 +6994,37 @@ impl<'db> Type<'db> { // until call-time overload resolution. let metaclass_dunder_call = self_type.member_lookup_with_policy( db, + env, "__call__", MemberLookupPolicy::NO_INSTANCE_FALLBACK | MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, ); - let Some(constructor_instance_ty) = self_type.to_instance_approximation(db) else { + let Some(constructor_instance_ty) = self_type.to_instance_approximation(db, env) else { return fallback_bindings(); }; - let new_method = self_type.lookup_dunder_new(db); + // TypedDict classes inherit `dict.__new__`, whose gradual `**kwargs` signature cannot + // constrain their type variables. Their synthesized `__init__` contains the actual field + // types, including generic extra items, so constructor inference should start there. + let new_method = if class_literal.is_typed_dict(db) { + None + } else { + self_type.lookup_dunder_new(db, env) + }; let init_method_no_object = constructor_instance_ty.member_lookup_with_policy( db, + env, "__init__", MemberLookupPolicy::NO_INSTANCE_FALLBACK | MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, ); let (new_bindings, has_any_new) = match new_method.as_ref().map(|method| method.place) { - Some(place) => match resolve_dunder_new_callable(db, self_type, place) { + Some(place) => match resolve_dunder_new_callable(db, env, self_type, place) { Some((new_callable, definedness)) => { let mut bindings = - bind_constructor_new(db, new_callable.bindings(db), self_type) + bind_constructor_new(db, env, new_callable.bindings(db, env), self_type) .into_constructor_bindings( constructor_instance_ty, ConstructorCallableKind::New, @@ -6151,7 +7051,7 @@ impl<'db> Type<'db> { _, ) => { let mut bindings = init_method - .bindings(db) + .bindings(db, env) .into_constructor_bindings( constructor_instance_ty, ConstructorCallableKind::Init, @@ -6165,6 +7065,7 @@ impl<'db> Type<'db> { (Place::Undefined, false) => { let init_method_with_object = constructor_instance_ty.member_lookup_with_policy( db, + env, "__init__", MemberLookupPolicy::NO_INSTANCE_FALLBACK, ); @@ -6175,7 +7076,7 @@ impl<'db> Type<'db> { .. }) => { let mut bindings = init_method - .bindings(db) + .bindings(db, env) .into_constructor_bindings( constructor_instance_ty, ConstructorCallableKind::Init, @@ -6229,7 +7130,7 @@ impl<'db> Type<'db> { }) = metaclass_dunder_call.place { let mut metaclass_bindings = metaclass_call_method - .bindings(db) + .bindings(db, env) .into_constructor_bindings( constructor_instance_ty, ConstructorCallableKind::MetaclassCall, @@ -6259,13 +7160,15 @@ impl<'db> Type<'db> { fn try_call( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, argument_types: &CallArguments<'_, 'db>, ) -> Result, CallError<'db>> { let constraints = ConstraintSetBuilder::new(); - self.bindings(db) - .match_parameters(db, argument_types) + self.bindings(db, env) + .match_parameters(db, env, argument_types) .check_types( db, + env, &constraints, argument_types, TypeContext::default(), @@ -6280,12 +7183,14 @@ impl<'db> Type<'db> { fn try_call_dunder( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, mut argument_types: CallArguments<'_, 'db>, tcx: TypeContext<'db>, ) -> Result, CallDunderError<'db>> { self.try_call_dunder_with_policy( db, + env, name, &mut argument_types, tcx, @@ -6303,23 +7208,31 @@ impl<'db> Type<'db> { fn try_call_dunder_with_policy( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, argument_types: &mut CallArguments<'_, 'db>, tcx: TypeContext<'db>, policy: MemberLookupPolicy, ) -> Result, CallDunderError<'db>> { if let Type::Intersection(intersection) = self { - return intersection.try_call_dunder_with_policy(db, name, argument_types, tcx, policy); + return intersection.try_call_dunder_with_policy( + db, + env, + name, + argument_types, + tcx, + policy, + ); } if let Type::Union(union) = self { - return union.try_call_dunder_with_policy(db, name, argument_types, tcx, policy); + return union.try_call_dunder_with_policy(db, env, name, argument_types, tcx, policy); } // Implicit calls to dunder methods never access instance members, so we pass // `NO_INSTANCE_FALLBACK` here in addition to other policies: let policy = policy | MemberLookupPolicy::NO_INSTANCE_FALLBACK; - match self.member_lookup_with_policy(db, name, policy).place { + match self.member_lookup_with_policy(db, env, name, policy).place { Place::Defined(DefinedPlace { ty: dunder_callable, definedness: boundness, @@ -6328,9 +7241,9 @@ impl<'db> Type<'db> { }) => { let constraints = ConstraintSetBuilder::new(); let bindings = dunder_callable - .bindings(db) - .match_parameters(db, argument_types) - .check_types(db, &constraints, argument_types, tcx, &[]); + .bindings(db, env) + .match_parameters(db, env, argument_types) + .check_types(db, env, &constraints, argument_types, tcx, &[]); let bindings = match bindings { Ok(bindings) => bindings, @@ -6361,11 +7274,12 @@ impl<'db> Type<'db> { fn try_call_dunder_on_class( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, argument_types: &CallArguments<'_, 'db>, tcx: TypeContext<'db>, ) -> Result, CallDunderError<'db>> { - match self.member(db, name).place { + match self.member(db, env, name).place { Place::Defined(DefinedPlace { ty: dunder_callable, definedness: boundness, @@ -6374,9 +7288,9 @@ impl<'db> Type<'db> { }) => { let constraints = ConstraintSetBuilder::new(); let bindings = dunder_callable - .bindings(db) - .match_parameters(db, argument_types) - .check_types(db, &constraints, argument_types, tcx, &[]); + .bindings(db, env) + .match_parameters(db, env, argument_types) + .check_types(db, env, &constraints, argument_types, tcx, &[]); let bindings = match bindings { Ok(bindings) => bindings, @@ -6397,102 +7311,170 @@ impl<'db> Type<'db> { } } + /// Return whether a custom `__getattribute__` could affect this lookup. + /// + /// Reusing the receiver class's existing MRO classification avoids interning a member-lookup + /// key just to determine whether an override exists. Class objects use their metaclass instead. + /// An unknown base can intercept a missing attribute or bypass a failing descriptor, but cannot + /// invalidate a definitely defined member. + fn custom_getattribute_may_affect_lookup( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + result: MemberLookupResult<'db>, + ) -> bool { + let Some(class) = self.nominal_class(db, env).or_else(|| { + self.to_meta_type(db, env) + .to_instance_approximation(db, env) + .and_then(|instance| instance.nominal_class(db, env)) + }) else { + return true; + }; + + let class = class.class_literal(db); + if class.as_static().is_none() { + return true; + } + + let flags = class.instance_flags(db); + if flags.contains(ClassInstanceFlags::HAS_CUSTOM_GETATTRIBUTE) { + return true; + } + + if !flags.contains(ClassInstanceFlags::HAS_DYNAMIC_GETATTRIBUTE) { + return false; + } + + !matches!( + result, + Ok(PlaceAndQualifiers { + place: Place::Defined(place), + .. + }) if place.is_definitely_defined() + ) + } + /// Apply `__getattr__` / `__getattribute__` fallback to an attribute-lookup result. /// - /// If `result` is already always-defined, return it unchanged. Otherwise, fall back to calling - /// `__getattribute__` (and then `__getattr__`) on the meta-type of `self`. + /// A custom `__getattribute__` can intercept even an always-defined normal lookup result. + /// Otherwise, an undefined or possibly-undefined result falls back to `__getattribute__` and + /// then `__getattr__` on the meta-type of `self`. fn fallback_to_getattr( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &Name, - result: PlaceAndQualifiers<'db>, + result: MemberLookupResult<'db>, policy: MemberLookupPolicy, - ) -> PlaceAndQualifiers<'db> { + ) -> MemberLookupResult<'db> { let custom_getattr_result = || { if policy.no_getattr_lookup() { - return Place::Undefined.into(); + return MemberLookupResult::from(Place::Undefined); } // `django.conf.settings` answers every name through `__getattr__`, but a // project that names its settings module says there what each of them // is. that reading comes first, and anything it cannot answer falls // through to the `Any` the stubs declare - if django_settings::is_settings_instance(db, self) - && let Some(setting) = django_settings::settings_member(db, name) + if django_settings::is_settings_instance(db, env, self) + && let Some(setting) = django_settings::settings_member(db, env, name) { return Place::bound(setting).into(); } - self.try_call_dunder( + let name_type = Type::string_literal(db, name); + match self.try_call_dunder( db, + env, "__getattr__", - CallArguments::positional([Type::string_literal(db, name)]), + CallArguments::positional([name_type]), TypeContext::default(), - ) - .map(|outcome| Place::bound(outcome.return_type(db))) - // TODO: Handle call errors here. - .unwrap_or_default() - .into() + ) { + Ok(outcome) => Place::bound(outcome.return_type(db, env)).into(), + Err(CallDunderError::CallError(_, bindings, _)) => member_lookup_result( + db, + Place::bound(bindings.return_type(db, env)).into(), + Some(MemberLookupErrorKind::GetAttr { + receiver: self, + name: name_type, + }), + ), + Err( + CallDunderError::PossiblyUnbound { .. } | CallDunderError::MethodNotAvailable, + ) => Place::Undefined.into(), + } }; - let custom_getattribute_result = || { - if "__getattribute__" == name.as_str() { - return Place::Undefined.into(); - } - - // A known instance's meta type is a class literal, so a receiver whose - // own class is `object` roots the lookup below at `object` itself — - // where `MRO_NO_OBJECT_FALLBACK` is deliberately ignored, so that - // `object()` still finds `object.__init__`. That would let - // `object.__getattribute__` answer every attribute with `Any`. Such a - // receiver has no custom `__getattribute__` to find in the first place. - // (Instances of `object` are unaffected: their meta type `type[object]` - // is normalized to `type`, which does honor the policy.) - if let Type::KnownInstance(known_instance) = self - && known_instance.class(db) == KnownClass::Object - { - return Place::Undefined.into(); - } + let getattribute_policy = MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK + | MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK; + if !self.custom_getattribute_may_affect_lookup(db, env, result) + || self + .class_member_with_policy(db, env, "__getattribute__", getattribute_policy) + .place + .is_undefined() + { + return member_lookup_or_fall_back_to(db, env, result, custom_getattr_result); + } + + // A known instance's meta type is a class literal, so a receiver whose own + // class is `object` roots the lookup below at `object` itself — where + // `MRO_NO_OBJECT_FALLBACK` is deliberately ignored, so that `object()` still + // finds `object.__init__`. That would let `object.__getattribute__` answer + // every attribute with `Any`. Such a receiver has no custom + // `__getattribute__` to find in the first place. (Instances of `object` are + // unaffected: their meta type `type[object]` is normalized to `type`, which + // does honor the policy.) + if let Type::KnownInstance(known_instance) = self + && known_instance.class(db) == KnownClass::Object + { + return member_lookup_or_fall_back_to(db, env, result, custom_getattr_result); + } - // Skip `object.__getattribute__`, which is the default mechanism we - // already model via the normal attribute-lookup path. - self.try_call_dunder_with_policy( + let name_type = Type::string_literal(db, name); + let custom_getattribute = match self.try_call_dunder_with_policy( + db, + env, + "__getattribute__", + &mut CallArguments::positional([name_type]), + TypeContext::default(), + getattribute_policy, + ) { + Ok(bindings) => Place::bound(bindings.return_type(db, env)).into(), + Err(CallDunderError::CallError(_, bindings, _)) => member_lookup_result( db, - "__getattribute__", - &mut CallArguments::positional([Type::string_literal(db, name)]), - TypeContext::default(), - MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, - ) - .map(|outcome| Place::bound(outcome.return_type(db))) - // TODO: Handle call errors here. - .unwrap_or_default() - .into() + Place::bound(bindings.return_type(db, env)).into(), + Some(MemberLookupErrorKind::GetAttribute { + receiver: self, + name: name_type, + }), + ), + Err(CallDunderError::PossiblyUnbound { .. }) => Place::Undefined.into(), + Err(CallDunderError::MethodNotAvailable) => { + return member_lookup_or_fall_back_to(db, env, result, custom_getattr_result); + } }; - match result { - member @ PlaceAndQualifiers { - place: - Place::Defined(DefinedPlace { - definedness: Definedness::AlwaysDefined, - .. - }), - qualifiers: _, - } => member, - member @ PlaceAndQualifiers { - place: - Place::Defined(DefinedPlace { - definedness: Definedness::PossiblyUndefined, - .. - }), - qualifiers: _, - } => member - .or_fall_back_to(db, custom_getattribute_result) - .or_fall_back_to(db, custom_getattr_result), - PlaceAndQualifiers { - place: Place::Undefined, - qualifiers: _, - } => custom_getattribute_result().or_fall_back_to(db, custom_getattr_result), + if let Err(error) = custom_getattribute { + let member = result.unwrap_or_else(|error| error.fallback_member(db)); + return Err(MemberLookupError::new( + db, + member.or_fall_back_to(db, env, || error.fallback_member(db)), + error.kind(db), + )); } + + // A custom override runs before the descriptor and might return without invoking it. + let result = if matches!( + result.err().map(|error| error.kind(db)), + Some(MemberLookupErrorKind::DescriptorGet(_)) + ) { + Ok(result.unwrap_or_else(|error| error.fallback_member(db))) + } else { + result + }; + + let result = member_lookup_or_fall_back_to(db, env, result, || custom_getattribute); + member_lookup_or_fall_back_to(db, env, result, custom_getattr_result) } /// Flatten typevars in a union or intersection by resolving them to their upper bounds @@ -6510,31 +7492,39 @@ impl<'db> Type<'db> { /// /// This only flattens typevars directly in unions and intersections; it does not descend /// into generic types or other nested structures. - fn flatten_typevars(self, db: &'db dyn Db) -> Type<'db> { + fn flatten_typevars(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match self { - Type::TypeVar(tvar) => match tvar.typevar(db).bound_or_constraints(db) { - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound.flatten_typevars(db), - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - constraints.as_type(db).flatten_typevars(db) + Type::TypeVar(tvar) => { + match tvar.typevar(db).bound_or_constraints(db, env) { + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + bound.flatten_typevars(db, env) + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + constraints.as_type(db, env).flatten_typevars(db, env) + } + // Unbounded typevar is effectively `object`. + None => Type::object(), } - // Unbounded typevar is effectively `object`. - None => Type::object(), - }, + } Type::Union(union) => { // Flatten each element and rebuild through the union builder. UnionType::from_elements( db, - union.elements(db).iter().map(|e| e.flatten_typevars(db)), + env, + union + .elements(db) + .iter() + .map(|e| e.flatten_typevars(db, env)), ) } Type::Intersection(intersection) => { // Flatten each positive element and rebuild through the intersection builder. - let mut builder = IntersectionBuilder::new(db); + let mut builder = IntersectionBuilder::new(db, env); for pos in intersection.positive(db) { - builder = builder.add_positive(pos.flatten_typevars(db)); + builder.add_positive_in_place(pos.flatten_typevars(db, env)); } for neg in intersection.negative(db) { - builder = builder.add_negative(neg.flatten_typevars(db)); + builder.add_negative_in_place(neg.flatten_typevars(db, env)); } builder.build() } @@ -6544,17 +7534,22 @@ impl<'db> Type<'db> { } /// Resolve the type of an `await …` expression where `self` is the type of the awaitable. - fn try_await(self, db: &'db dyn Db) -> Result, AwaitError<'db>> { + fn try_await( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Result, AwaitError<'db>> { let await_result = self.try_call_dunder( db, + env, "__await__", CallArguments::none(), TypeContext::default(), ); match await_result { Ok(bindings) => { - let return_type = bindings.return_type(db); - Ok(return_type.generator_return_type(db).ok_or_else(|| { + let return_type = bindings.return_type(db, env); + Ok(return_type.generator_return_type(db, env).ok_or_else(|| { AwaitError::InvalidReturnType(return_type, Box::new(bindings)) })?) } @@ -6566,7 +7561,11 @@ impl<'db> Type<'db> { /// /// This corresponds to the `ReturnT` parameter of the generic `typing.Generator[YieldT, SendT, ReturnT]` /// protocol. - fn generator_types(self, db: &'db dyn Db) -> Option> { + fn generator_types( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { // TODO: Ideally, we would first try to upcast `self` to an instance of `Generator` and *then* // match on the protocol instance to get the `ReturnType` type parameter. For now, implement // an ad-hoc solution that works for protocols and instances of classes that explicitly inherit @@ -6599,10 +7598,11 @@ impl<'db> Type<'db> { || class.is_known(db, KnownClass::AsyncIterator)) && let [yield_ty] = specialization.types(db) { + let none = Type::none(db, env); Some(GeneratorTypes { yield_ty: Some(*yield_ty), - send_ty: Some(Type::none(db)), - return_ty: Some(Type::none(db)), + send_ty: Some(none), + return_ty: Some(none), }) } else { None @@ -6610,20 +7610,26 @@ impl<'db> Type<'db> { }; match self { - Type::NominalInstance(instance) => { - instance.class(db).iter_mro(db).find_map(from_class_base) - } - Type::ProtocolInstance(ProtocolInstanceType { - inner: Protocol::FromClass(class), - .. - }) => class.iter_mro(db).find_map(from_class_base), + Type::NominalInstance(instance) => instance + .class(db, env) + .iter_mro(db) + .find_map(from_class_base), + Type::ProtocolInstance(protocol) => protocol + .class_origin(db) + .and_then(|class| class.iter_mro(db).find_map(from_class_base)) + .map(|types| { + protocol + .materialization_kind(db) + .map_or(types, |kind| types.materialize(db, env, kind)) + }), + Type::TypeAlias(alias) => alias.value_type(db).generator_types(db, env), Type::Union(union) => { - let mut yield_builder = Some(UnionBuilder::new(db)); - let mut send_builder = Some(UnionBuilder::new(db)); - let mut return_builder = Some(UnionBuilder::new(db)); + let mut yield_builder = Some(UnionBuilder::new(db, env)); + let mut send_builder = Some(UnionBuilder::new(db, env)); + let mut return_builder = Some(UnionBuilder::new(db, env)); for ty in union.elements(db) { - let gt = ty.generator_types(db)?; + let gt = ty.generator_types(db, env)?; match gt.yield_ty { Some(ty) => yield_builder = yield_builder.map(|b| b.add(ty)), None => yield_builder = None, @@ -6648,13 +7654,13 @@ impl<'db> Type<'db> { // Using `positive()` rather than `positive_elements_or_object()` is safe // here because `object` is not a generator, so falling back to it would // still return `None`. - let mut yield_builder = Some(IntersectionBuilder::new(db)); - let mut send_builder = Some(IntersectionBuilder::new(db)); - let mut return_builder = Some(IntersectionBuilder::new(db)); + let mut yield_builder = Some(IntersectionBuilder::new(db, env)); + let mut send_builder = Some(IntersectionBuilder::new(db, env)); + let mut return_builder = Some(IntersectionBuilder::new(db, env)); let mut any_success = false; for ty in intersection.positive(db) { - let Some(gt) = ty.generator_types(db) else { + let Some(gt) = ty.generator_types(db, env) else { continue; }; any_success = true; @@ -6697,13 +7703,21 @@ impl<'db> Type<'db> { } } - fn generator_return_type(self, db: &'db dyn Db) -> Option> { - self.generator_types(db) + fn generator_return_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { + self.generator_types(db, env) .and_then(|generator_types| generator_types.return_ty) } - fn generator_send_type(self, db: &'db dyn Db) -> Option> { - self.generator_types(db) + fn generator_send_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { + self.generator_types(db, env) .and_then(|generator_types| generator_types.send_ty) } @@ -6712,53 +7726,59 @@ impl<'db> Type<'db> { /// Use this only when an over-approximation is sound, such as constructor inference or a /// source-side relation. Target-side subtype checks must use [`Self::to_instance`]. #[must_use] - pub(crate) fn to_instance_approximation(self, db: &'db dyn Db) -> Option> { - self.to_instance(db).map(InstanceProjection::into_inner) + fn to_instance_approximation( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { + self.to_instance(db, env) + .map(InstanceProjection::into_inner) } /// Project this class-object type into its instance type while preserving projection quality. #[must_use] - pub(crate) fn to_instance(self, db: &'db dyn Db) -> Option>> { + fn to_instance( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option>> { match self { - Type::Overlapping(overlapping) => overlapping.value_type(db).to_instance(db), - Type::Restricted(restricted) => restricted.value_type(db).to_instance(db), - Type::Deferred(deferred) => deferred.reduced(db).to_instance(db), + Type::Overlapping(overlapping) => overlapping.value_type(db, env).to_instance(db, env), + Type::Restricted(restricted) => restricted.value_type(db).to_instance(db, env), + Type::Deferred(deferred) => deferred.reduced(db, env).to_instance(db, env), Type::Dynamic(_) | Type::Divergent(_) | Type::Never => { Some(InstanceProjection::Exact(self)) } Type::ClassLiteral(class) => Some(InstanceProjection::OverApproximation( - Type::instance(db, class.default_specialization(db)), + Type::instance(db, env, class.default_specialization(db)), )), Type::GenericAlias(alias) => Some(InstanceProjection::OverApproximation( - Type::instance(db, ClassType::from(alias)), + Type::instance(db, env, ClassType::from(alias)), + )), + Type::SubclassOf(subclass_of_ty) => Some(InstanceProjection::Exact( + subclass_of_ty.to_instance(db, env), )), - Type::SubclassOf(subclass_of_ty) => { - Some(InstanceProjection::Exact(subclass_of_ty.to_instance(db))) - } Type::KnownInstance(KnownInstanceType::NewType(newtype)) => Some( InstanceProjection::OverApproximation(Type::NewTypeInstance(newtype)), ), - Type::Union(union) => union.to_instance(db), - Type::UnsafeUnion(unsafe_union) => unsafe_union.to_instance(db), + Type::Union(union) => union.to_instance(db, env), + Type::UnsafeUnion(unsafe_union) => unsafe_union.to_instance(db, env), // 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(bound_typevar) => bound_typevar - .to_instance(db) + .to_instance(db, env) .map(|projection| projection.map(Type::TypeVar)), - Type::TypeAlias(alias) => alias.value_type(db).to_instance(db), - Type::Intersection(intersection) => intersection.to_instance(db), + Type::TypeAlias(alias) => alias.value_type(db).to_instance(db, env), + Type::Intersection(intersection) => intersection.to_instance(db, env), // An instance of class `C` may itself have instances if `C` is a subclass of `type`. - Type::NominalInstance(instance) - if KnownClass::Type - .to_class_literal(db) - .to_class_type(db) - .is_some_and(|type_class| { - instance.class(db).is_subclass_of(db, type_class) - }) => - { - Some(InstanceProjection::OverApproximation(Type::object())) - } + Type::NominalInstance(instance) => KnownClass::Type + .to_class_literal(db, env) + .to_class_type(db) + .is_some_and(|type_class| { + instance.class(db, env).is_subclass_of(db, env, type_class) + }) + .then_some(InstanceProjection::OverApproximation(Type::object())), Type::FunctionLiteral(_) | Type::Callable(..) | Type::KnownBoundMethod(_) @@ -6766,7 +7786,6 @@ impl<'db> Type<'db> { | Type::WrapperDescriptor(_) | Type::DataclassDecorator(_) | Type::DataclassTransformer(_) - | Type::NominalInstance(_) | Type::ProtocolInstance(_) | Type::SpecialForm(_) | Type::KnownInstance(_) @@ -6794,15 +7813,26 @@ impl<'db> Type<'db> { /// /// The `scope_id` and `typevar_binding_context` arguments must always come from the file we are currently inferring, so /// as to avoid cross-module AST dependency. - pub(crate) fn in_type_expression( + fn in_type_expression( + &self, + db: &'db dyn Db, + scope_id: ScopeId<'db>, + typevar_binding_context: Option>, + inference_flags: InferenceFlags, + ) -> Result, InvalidTypeExpressionError<'db>> { + self.in_type_expression_impl(db, scope_id, typevar_binding_context, inference_flags) + } + + fn in_type_expression_impl( &self, db: &'db dyn Db, scope_id: ScopeId<'db>, typevar_binding_context: Option>, inference_flags: InferenceFlags, ) -> Result, InvalidTypeExpressionError<'db>> { + let env = &ProgramEnvironment::from_scope(scope_id); match self { - Type::Overlapping(overlapping) => overlapping.value_type(db).in_type_expression( + Type::Overlapping(overlapping) => overlapping.value_type(db, env).in_type_expression( db, scope_id, typevar_binding_context, @@ -6814,7 +7844,7 @@ impl<'db> Type<'db> { typevar_binding_context, inference_flags, ), - Type::Deferred(deferred) => deferred.reduced(db).in_type_expression( + Type::Deferred(deferred) => deferred.reduced(db, env).in_type_expression( db, scope_id, typevar_binding_context, @@ -6867,7 +7897,7 @@ impl<'db> Type<'db> { is_vendored && inference_flags.contains(InferenceFlags::IN_RETURN_TYPE); let ty = match class.known(db) { Some(KnownClass::Complex) if !is_by && !strict_return => { - KnownUnion::Complex.to_type(db) + KnownUnion::Complex.to_type(db, env) } Some(KnownClass::Float) if !is_by @@ -6875,9 +7905,9 @@ impl<'db> Type<'db> { && !inference_flags .contains(InferenceFlags::DISABLE_INT_FLOAT_SPECIAL_CASE) => { - KnownUnion::Float.to_type(db) + KnownUnion::Float.to_type(db, env) } - _ => Type::instance(db, class.default_specialization(db)), + _ => Type::instance(db, env, class.default_specialization(db)), }; Ok(ty) } @@ -6891,7 +7921,7 @@ impl<'db> Type<'db> { { return Ok(union.apply_specialization(db, alias.specialization(db))); } - Ok(Type::instance(db, ClassType::from(*alias))) + Ok(Type::instance(db, env, ClassType::from(*alias))) } Type::SubclassOf(_) @@ -6928,7 +7958,7 @@ impl<'db> Type<'db> { // `Type::TypeAlias`. That is what lets it survive being resolved somewhere // its arguments are still type parameters, and reduce once they are known KnownInstanceType::TypeAliasType(alias) => { - Ok(match_type_application(db, *alias).unwrap_or(Type::TypeAlias(*alias))) + Ok(match_type_application(db, env, *alias).unwrap_or(Type::TypeAlias(*alias))) } KnownInstanceType::NewType(newtype) => Ok(Type::NewTypeInstance(*newtype)), KnownInstanceType::TypeVar(typevar) => { @@ -6955,7 +7985,7 @@ impl<'db> Type<'db> { fallback_type: Type::unknown(), }); } - let index = semantic_index(db, scope_id.file(db)); + let index = semantic_index(db, scope_id.program_file(db)); Ok(bind_typevar( db, index, @@ -7026,7 +8056,7 @@ impl<'db> Type<'db> { // (`int` -> instance of `int` -> subclass of `int`) can be lossy, but it is // okay for all valid arguments to `type[…]`. - Ok(instance.inner(db).to_meta_type(db)) + Ok(instance.inner(db).to_meta_type(db, env)) } KnownInstanceType::Callable(callable) => Ok(Type::Callable(*callable)), KnownInstanceType::LiteralStringAlias(ty) => Ok(ty.inner(db)), @@ -7046,16 +8076,15 @@ impl<'db> Type<'db> { Type::SpecialForm(special_form) => special_form .in_type_expression(db, scope_id, typevar_binding_context, inference_flags) .map_err(|err| { - let fallback_type = if matches!( - err, + let fallback_type = match err { InvalidTypeExpression::Concatenate - | InvalidTypeExpression::RequiresTwoArguments( - SpecialFormType::Concatenate - ) - ) { - Type::Dynamic(DynamicType::InvalidConcatenateUnknown) - } else { - Type::unknown() + | InvalidTypeExpression::RequiresTwoArguments( + SpecialFormType::Concatenate, + ) => Type::Dynamic(DynamicType::InvalidConcatenateUnknown), + InvalidTypeExpression::TypingSelfWithIncompatibleReceiver(typing_self) => { + Type::TypeVar(typing_self) + } + _ => Type::unknown(), }; InvalidTypeExpressionError { @@ -7065,10 +8094,10 @@ impl<'db> Type<'db> { }), Type::Union(union) => { - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); let mut invalid_expressions = smallvec::SmallVec::default(); for element in union.elements(db) { - match element.in_type_expression( + match element.in_type_expression_impl( db, scope_id, typevar_binding_context, @@ -7097,7 +8126,7 @@ impl<'db> Type<'db> { Type::Dynamic(_) | Type::Divergent(_) => Ok(*self), Type::NominalInstance(instance) => match instance.known_class(db) { - Some(KnownClass::NoneType) => Ok(Type::none(db)), + Some(KnownClass::NoneType) => Ok(Type::none(db, env)), // TODO: Emit an invalid-type-form diagnostic and recover to `Unknown` for // unrecognized `TypeVar` and `TypeVarTuple` instances. Some(KnownClass::TypeVar) => Ok(todo_type!( @@ -7105,7 +8134,8 @@ impl<'db> Type<'db> { )), Some(KnownClass::TypeVarTuple | KnownClass::ExtensionsTypeVarTuple) => { Ok(todo_type!( - "unrecognized `typing.TypeVarTuple` instances should be invalid type expressions" + "unrecognized `typing.TypeVarTuple` instances \ + should be invalid type expressions" )) } _ => Err(InvalidTypeExpressionError { @@ -7147,7 +8177,7 @@ impl<'db> Type<'db> { } } - Type::TypeAlias(alias) => alias.value_type(db).in_type_expression( + Type::TypeAlias(alias) => alias.value_type(db).in_type_expression_impl( db, scope_id, typevar_binding_context, @@ -7169,9 +8199,13 @@ impl<'db> Type<'db> { /// The native compiler needs it to decide whether a value has a class whose /// layout it emitted, and so whether an attribute read can be a field read at /// a compile-time offset rather than a `PyObject_GetAttr`. - pub fn nominal_class_name(self, db: &'db dyn Db) -> Option<&'db str> { + pub fn nominal_class_name( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option<&'db str> { match self { - Type::NominalInstance(instance) => Some(instance.class(db).name(db).as_str()), + Type::NominalInstance(instance) => Some(instance.class(db, env).name(db).as_str()), _ => None, } } @@ -7180,11 +8214,15 @@ impl<'db> Type<'db> { /// /// a consumer that compiles container operations needs the element to decide a /// representation for the buffer, which the class name alone does not give - pub fn list_element_type(self, db: &'db dyn Db) -> Option> { + pub fn list_element_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { let Type::NominalInstance(instance) = self else { return None; }; - let class = instance.class(db); + let class = instance.class(db, env); if class.name(db).as_str() != "list" { return None; } @@ -7201,16 +8239,16 @@ impl<'db> Type<'db> { /// declaring module and says nothing about a subclass inside it — a consumer /// that compiles a method call needs to know no override exists anywhere, and /// sealing licenses a switch over the known subclasses rather than a direct call - pub fn nominal_class_is_exact(self, db: &'db dyn Db) -> bool { + pub fn nominal_class_is_exact(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool { match self { - Type::NominalInstance(instance) => instance.class(db).is_final(db), + Type::NominalInstance(instance) => instance.class(db, env).is_final(db), _ => false, } } /// The type `NoneType` / `None` - pub fn none(db: &'db dyn Db) -> Type<'db> { - KnownClass::NoneType.to_instance(db) + pub fn none(db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + KnownClass::NoneType.to_instance(db, env) } /// Given a type that is assumed to represent an instance of a class, @@ -7219,26 +8257,28 @@ impl<'db> Type<'db> { /// Note: the return type of `type(obj)` is subtly different from this. /// See `Self::dunder_class` for more details. #[must_use] - pub(crate) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { + fn to_meta_type(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match self { - Type::Overlapping(overlapping) => overlapping.value_type(db).to_meta_type(db), - Type::Restricted(restricted) => restricted.value_type(db).to_meta_type(db), - Type::Deferred(deferred) => deferred.reduced(db).to_meta_type(db), + Type::Overlapping(overlapping) => overlapping.value_type(db, env).to_meta_type(db, env), + Type::Restricted(restricted) => restricted.value_type(db).to_meta_type(db, env), + Type::Deferred(deferred) => deferred.reduced(db, env).to_meta_type(db, env), 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(property) => property.instance_class(db).to_class_literal(db), - Type::Union(union) => union.map(db, |ty| ty.to_meta_type(db)), + Type::NominalInstance(instance) => instance.to_meta_type(db, env), + Type::KnownInstance(known_instance) => known_instance.to_meta_type(db, env), + Type::SpecialForm(special_form) => special_form.to_meta_type(db, env), + Type::PropertyInstance(property) => { + property.instance_class(db).to_class_literal(db, env) + } + Type::Union(union) => union.map(db, env, |ty| ty.to_meta_type(db, env)), Type::UnsafeUnion(unsafe_union) => { - unsafe_union.map_elements(db, |element| element.to_meta_type(db)) + unsafe_union.map_elements(db, |element| element.to_meta_type(db, env)) } - Type::TypeIs(_) | Type::TypeGuard(_) => KnownClass::Bool.to_class_literal(db), - Type::TypeForm(_) => Type::object().to_meta_type(db), + Type::TypeIs(_) | Type::TypeGuard(_) => KnownClass::Bool.to_class_literal(db, env), + Type::TypeForm(_) => Type::object().to_meta_type(db, env), Type::LiteralValue(literal) => match literal.kind() { - LiteralValueTypeKind::Bool(_) => KnownClass::Bool.to_class_literal(db), - LiteralValueTypeKind::Bytes(_) => KnownClass::Bytes.to_class_literal(db), - LiteralValueTypeKind::Int(_) => KnownClass::Int.to_class_literal(db), + LiteralValueTypeKind::Bool(_) => KnownClass::Bool.to_class_literal(db, env), + LiteralValueTypeKind::Bytes(_) => KnownClass::Bytes.to_class_literal(db, env), + LiteralValueTypeKind::Int(_) => KnownClass::Int.to_class_literal(db, env), LiteralValueTypeKind::Enum(enum_literal) => { // a based enum's unit variant is a singleton instance of its // own subclass, not of the enum — only the all-unit `Enum` @@ -7253,58 +8293,65 @@ impl<'db> Type<'db> { Type::ClassLiteral(variant_class.unwrap_or_else(|| enum_literal.enum_class(db))) } LiteralValueTypeKind::String(_) | LiteralValueTypeKind::LiteralString => { - KnownClass::Str.to_class_literal(db) + KnownClass::Str.to_class_literal(db, env) } - LiteralValueTypeKind::Float(_) => KnownClass::Float.to_class_literal(db), - LiteralValueTypeKind::Complex(_) => KnownClass::Complex.to_class_literal(db), + LiteralValueTypeKind::Float(_) => KnownClass::Float.to_class_literal(db, env), + LiteralValueTypeKind::Complex(_) => KnownClass::Complex.to_class_literal(db, env), }, - Type::FunctionLiteral(_) => KnownClass::FunctionType.to_class_literal(db), - Type::BoundMethod(_) => KnownClass::MethodType.to_class_literal(db), - Type::KnownBoundMethod(method) => method.class().to_class_literal(db), - Type::WrapperDescriptor(_) => KnownClass::WrapperDescriptorType.to_class_literal(db), - Type::DataclassDecorator(_) => KnownClass::FunctionType.to_class_literal(db), + Type::FunctionLiteral(_) => KnownClass::FunctionType.to_class_literal(db, env), + Type::BoundMethod(_) => KnownClass::MethodType.to_class_literal(db, env), + Type::KnownBoundMethod(method) => method.class().to_class_literal(db, env), + Type::WrapperDescriptor(_) => { + KnownClass::WrapperDescriptorType.to_class_literal(db, env) + } + Type::DataclassDecorator(_) => KnownClass::FunctionType.to_class_literal(db, env), Type::Callable(callable) if callable.is_function_like(db) => { - KnownClass::FunctionType.to_class_literal(db) + KnownClass::FunctionType.to_class_literal(db, env) + } + Type::Callable(_) | Type::DataclassTransformer(_) => { + KnownClass::Type.to_instance(db, env) } - Type::Callable(_) | Type::DataclassTransformer(_) => KnownClass::Type.to_instance(db), - Type::ModuleLiteral(_) => KnownClass::ModuleType.to_class_literal(db), + Type::ModuleLiteral(_) => KnownClass::ModuleType.to_class_literal(db, env), Type::TypeVar(bound_typevar) => { - SubclassOfType::from(db, SubclassOfInner::TypeVar(bound_typevar)) + SubclassOfType::from(db, env, SubclassOfInner::TypeVar(bound_typevar)) } Type::ClassLiteral(class) => class.metaclass(db), Type::GenericAlias(alias) => ClassType::from(alias).metaclass(db), - Type::SubclassOf(subclass_of_ty) => subclass_of_ty.to_meta_type(db), - Type::Dynamic(dynamic) => SubclassOfType::from(db, SubclassOfInner::Dynamic(dynamic)), + Type::SubclassOf(subclass_of_ty) => subclass_of_ty.to_meta_type(db, env), + Type::Dynamic(dynamic) => { + SubclassOfType::from(db, env, SubclassOfInner::Dynamic(dynamic)) + } Type::Divergent(_) => self, // TODO intersections Type::Intersection(intersection) => { - if let Some(alternatives) = intersection.finite_alternative_union(db) { - alternatives.to_meta_type(db) + if let Some(alternatives) = intersection.finite_alternative_union(db, env) { + alternatives.to_meta_type(db, env) } else { - SubclassOfType::try_from_type(db, todo_type!("Intersection meta-type")) + SubclassOfType::try_from_type(db, env, todo_type!("Intersection meta-type")) .expect("Type::Todo should be a valid `SubclassOfInner`") } } - Type::EnumComplement(complement) => { - complement.remaining_literal_union(db).to_meta_type(db) - } - Type::AlwaysTruthy | Type::AlwaysFalsy => KnownClass::Type.to_instance(db), - Type::BoundSuper(_) => KnownClass::Super.to_class_literal(db), + Type::EnumComplement(complement) => complement + .remaining_literal_union(db, env) + .to_meta_type(db, env), + Type::AlwaysTruthy | Type::AlwaysFalsy => KnownClass::Type.to_instance(db, env), + Type::BoundSuper(_) => KnownClass::Super.to_class_literal(db, env), // Class-member lookup on a protocol instance must use the protocol's nominal class. // The structural `type[Protocol]` view is exposed by `dunder_class` and explicit // `type[Protocol]` annotations instead. - Type::ProtocolInstance(protocol) => protocol.to_nominal_meta_type(db), + Type::ProtocolInstance(protocol) => protocol.to_nominal_meta_type(db, env), // `TypedDict` instances are instances of `dict` at runtime, but its important that we // understand a more specific meta type in order to correctly handle `__getitem__`. Type::TypedDict(typed_dict) => match typed_dict { - TypedDictType::Class(class) => SubclassOfType::from(db, class), + TypedDictType::Class(class) => SubclassOfType::from(db, env, class), TypedDictType::Synthesized(_) => SubclassOfType::from( db, + env, todo_type!("TypedDict synthesized meta-type").expect_dynamic(), ), }, - Type::TypeAlias(alias) => alias.value_type(db).to_meta_type(db), - Type::NewTypeInstance(newtype) => newtype.concrete_base_type(db).to_meta_type(db), + Type::TypeAlias(alias) => alias.value_type(db).to_meta_type(db, env), + Type::NewTypeInstance(newtype) => newtype.concrete_base_type(db).to_meta_type(db, env), } } @@ -7314,18 +8361,22 @@ impl<'db> Type<'db> { /// `type[dict[str, object]]`, because their inhabitants are instances of `dict` at runtime. /// Class-backed protocols return their structural `type[Protocol]` view. #[must_use] - pub(crate) fn dunder_class(self, db: &'db dyn Db) -> Type<'db> { + fn dunder_class(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match self { - Type::Union(union) => union.map(db, |element| element.dunder_class(db)), + Type::Union(union) => union.map(db, env, |element| element.dunder_class(db, env)), Type::UnsafeUnion(unsafe_union) => { - unsafe_union.map_elements(db, |element| element.dunder_class(db)) + unsafe_union.map_elements(db, |element| element.dunder_class(db, env)) } Type::Intersection(intersection) => intersection - .try_dunder_class(db) - .unwrap_or_else(|| self.to_meta_type(db)), - Type::ProtocolInstance(protocol) => protocol.to_meta_type(db), + .try_dunder_class(db, env) + .unwrap_or_else(|| self.to_meta_type(db, env)), + Type::ProtocolInstance(protocol) => protocol.to_meta_type(db, env), Type::TypedDict(_) => KnownClass::Dict - .to_specialized_class_type(db, &[KnownClass::Str.to_instance(db), Type::object()]) + .to_specialized_class_type( + db, + env, + &[KnownClass::Str.to_instance(db, env), Type::object()], + ) .map(Type::from) // Guard against user-customized typesheds with a broken `dict` class .unwrap_or_else(Type::unknown), @@ -7333,13 +8384,13 @@ impl<'db> Type<'db> { // answered by the value — otherwise the fallback below gives the // *meta* type, which for a `TypedDict` is the class it is modelled // by rather than the `dict` its inhabitants really are - Type::TypeAlias(alias) => alias.value_type(db).dunder_class(db), - _ => self.to_meta_type(db), + Type::TypeAlias(alias) => alias.value_type(db).dunder_class(db, env), + _ => self.to_meta_type(db, env), } } #[must_use] - pub(crate) fn apply_optional_specialization( + fn apply_optional_specialization( self, db: &'db dyn Db, specialization: Option>, @@ -7362,11 +8413,13 @@ impl<'db> Type<'db> { pub(crate) fn apply_projected_specialization( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, specialization: Specialization<'db>, ) -> Type<'db> { if specialization.projections(db).iter().any(Option::is_some) { self.apply_type_mapping( db, + env, &TypeMapping::ProjectUseSiteVariance { specialization: ApplySpecialization::Specialization(specialization), position: TypeVarVariance::Covariant, @@ -7383,10 +8436,11 @@ impl<'db> Type<'db> { pub(crate) fn apply_projected_optional_specialization( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, specialization: Option>, ) -> Type<'db> { if let Some(specialization) = specialization { - self.apply_projected_specialization(db, specialization) + self.apply_projected_specialization(db, env, specialization) } else { self } @@ -7398,7 +8452,7 @@ impl<'db> Type<'db> { /// 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. - pub(crate) fn apply_specialization( + fn apply_specialization( self, db: &'db dyn Db, specialization: Specialization<'db>, @@ -7435,11 +8489,15 @@ impl<'db> Type<'db> { ) | Type::KnownBoundMethod( KnownBoundMethodType::StrStartswith(_) + | KnownBoundMethodType::ConstraintSetLowerBound + | KnownBoundMethodType::ConstraintSetUpperBound + | KnownBoundMethodType::ConstraintSetEquality | KnownBoundMethodType::ConstraintSetRange | KnownBoundMethodType::ConstraintSetAlways | KnownBoundMethodType::ConstraintSetNever | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) | KnownBoundMethodType::ConstraintSetSatisfies(_) + | KnownBoundMethodType::ConstraintSetExists(_) | KnownBoundMethodType::ConstraintSetForAll(_) | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) | KnownBoundMethodType::ConstraintSetSolutionsFor(_) @@ -7456,8 +8514,11 @@ impl<'db> Type<'db> { #[salsa::tracked( returns(copy), cycle_initial=|_, id, _, _| Type::divergent(id), - cycle_fn=|db, cycle, previous: &Type<'db>, value: Type<'db>, _, _| { - value.cycle_normalized(db, *previous, cycle) + cycle_fn=|db, cycle, previous: &Type<'db>, value: Type<'db>, _, specialization: Specialization<'db>| { + let env = ProgramEnvironment::from_program( + specialization.generic_context(db).program(db), + ); + value.cycle_normalized_impl(db, &env, *previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] @@ -7466,6 +8527,7 @@ impl<'db> Type<'db> { db: &'db dyn Db, specialization: Specialization<'db>, ) -> Type<'db> { + let env = &ProgramEnvironment::from_program(specialization.generic_context(db).program(db)); let type_mapping = match specialization.materialization_kind(db) { None => TypeMapping::ApplySpecialization(ApplySpecialization::Specialization( specialization, @@ -7476,24 +8538,32 @@ impl<'db> Type<'db> { }, }; - self.apply_type_mapping(db, &type_mapping, TypeContext::default()) + self.apply_type_mapping(db, env, &type_mapping, TypeContext::default()) } fn apply_type_mapping<'a>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, ) -> Type<'db> { - self.apply_type_mapping_impl(db, type_mapping, tcx, &ApplyTypeMappingVisitor::default()) + self.apply_type_mapping_impl( + db, + env, + type_mapping, + tcx, + &ApplyTypeMappingVisitor::new(env), + ) } fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Type<'db> { // If we are binding `typing.Self`, and this type is what we are binding `Self` to, return // early. This is not just an optimization, it also prevents us from infinitely expanding @@ -7519,12 +8589,16 @@ impl<'db> Type<'db> { TypeMapping::Promote(PromotionMode::On, PromotionKind::ClassLiteralsOnly) ) { - return SubclassOfType::from(db, class.default_specialization(db)); + return SubclassOfType::from(db, visitor.env, class.default_specialization(db)); } match self { - Type::TypeVar(bound_typevar) => bound_typevar.apply_type_mapping_impl(db, type_mapping, visitor), - Type::KnownInstance(known_instance) => known_instance.apply_type_mapping_impl(db, type_mapping, tcx, visitor), + Type::TypeVar(bound_typevar) => { + bound_typevar.apply_type_mapping_impl(db, env, type_mapping, visitor) + } + Type::KnownInstance(known_instance) => { + known_instance.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor) + } // Re-evaluate the deferred operation against the mapped operands: once a // specialization substitutes the type parameter, `Dim + 1` with `Dim = 5` @@ -7533,9 +8607,11 @@ impl<'db> Type<'db> { let operands = deferred .operands(db) .iter() - .map(|operand| operand.apply_type_mapping_impl(db, type_mapping, tcx, visitor)) + .map(|operand| { + operand.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor) + }) .collect(); - deferred.re_evaluate(db, operands) + deferred.re_evaluate(db, env, operands) } Type::FunctionLiteral(function) => visitor.visit(db, self, type_mapping, || { @@ -7545,15 +8621,13 @@ impl<'db> Type<'db> { TypeMapping::Promote( PromotionMode::On, PromotionKind::Regular | PromotionKind::RegularKeepingLiterals, - ) => { - Type::FunctionLiteral(function.apply_type_mapping_impl( - db, - type_mapping, - tcx, - visitor, - )) - .promote_impl(db) - } + ) => Type::FunctionLiteral(function.apply_type_mapping_impl( + db, + type_mapping, + tcx, + visitor, + )) + .promote_impl(db, visitor.env), _ => Type::FunctionLiteral(function.apply_type_mapping_impl( db, type_mapping, @@ -7565,48 +8639,62 @@ impl<'db> Type<'db> { Type::BoundMethod(method) => Type::BoundMethod(BoundMethodType::new( db, - method.function(db).apply_type_mapping_impl(db, type_mapping, tcx, visitor), - method.self_instance(db).apply_type_mapping_impl(db, type_mapping, tcx, visitor), + method + .function(db) + .apply_type_mapping_impl(db, type_mapping, tcx, visitor), + method.self_instance(db).apply_type_mapping_impl( + db, + env, + type_mapping, + tcx, + visitor, + ), )), // `RegularStrictNumeric` is deliberately absent: it promotes everything // else and leaves `float` and `complex` exact - Type::NominalInstance(instance) if matches!(type_mapping, TypeMapping::Promote(PromotionMode::On, PromotionKind::Regular | PromotionKind::RegularKeepingLiterals)) => { + Type::NominalInstance(instance) + if matches!( + type_mapping, + TypeMapping::Promote( + PromotionMode::On, + PromotionKind::Regular | PromotionKind::RegularKeepingLiterals + ) + ) => + { match instance.known_class(db) { - Some(KnownClass::Complex) => KnownUnion::Complex.to_type(db), - Some(KnownClass::Float) => KnownUnion::Float.to_type(db), - _ => instance.apply_type_mapping_impl(db, type_mapping, tcx, visitor), + Some(KnownClass::Complex) => KnownUnion::Complex.to_type(db, visitor.env), + Some(KnownClass::Float) => KnownUnion::Float.to_type(db, visitor.env), + _ => instance.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor), } } - Type::NominalInstance(instance) if matches!(type_mapping, TypeMapping::Promote(PromotionMode::On, PromotionKind::SingletonsOnly)) => { + Type::NominalInstance(instance) + if matches!( + type_mapping, + TypeMapping::Promote(PromotionMode::On, PromotionKind::SingletonsOnly) + ) => + { if instance.is_singleton(db) { - self.promote_singletons_impl(db) + self.promote_singletons_impl(db, visitor.env) } else { - instance.apply_type_mapping_impl(db, type_mapping, tcx, visitor) + instance.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor) } } Type::NominalInstance(instance) => { - instance.apply_type_mapping_impl(db, type_mapping, tcx, visitor) - }, + instance.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor) + } Type::NewTypeInstance(newtype) => visitor.visit(db, self, type_mapping, || { Type::NewTypeInstance(newtype.map_base_class_type(db, |class_type| { - class_type.apply_type_mapping_impl(db, type_mapping, tcx, visitor) + class_type.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor) })) }), - Type::ProtocolInstance(instance) => { - // TODO: Add tests for materialization 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(instance.apply_type_mapping_impl(db, type_mapping, tcx, visitor)) - } + Type::ProtocolInstance(instance) => Type::ProtocolInstance( + instance.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor), + ), Type::KnownBoundMethod(KnownBoundMethodType::FunctionTypeDunderGet(function)) => { Type::KnownBoundMethod(KnownBoundMethodType::FunctionTypeDunderGet( @@ -7622,18 +8710,18 @@ impl<'db> Type<'db> { Type::KnownBoundMethod(KnownBoundMethodType::PropertyDunderGet(property)) => { Type::KnownBoundMethod(KnownBoundMethodType::PropertyDunderGet( - property.apply_type_mapping_impl(db, type_mapping, tcx, visitor), + property.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor), )) } Type::KnownBoundMethod(KnownBoundMethodType::PropertyDunderSet(property)) => { Type::KnownBoundMethod(KnownBoundMethodType::PropertyDunderSet( - property.apply_type_mapping_impl(db, type_mapping, tcx, visitor), + property.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor), )) } Type::KnownBoundMethod(KnownBoundMethodType::PropertyDunderDelete(property)) => { Type::KnownBoundMethod(KnownBoundMethodType::PropertyDunderDelete( - property.apply_type_mapping_impl(db, type_mapping, tcx, visitor), + property.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor), )) } @@ -7641,22 +8729,32 @@ impl<'db> Type<'db> { Type::Callable(callable.apply_type_mapping_impl(db, type_mapping, tcx, visitor)) }), - Type::GenericAlias(generic) => { - Type::GenericAlias(generic.apply_type_mapping_impl(db, type_mapping, tcx, visitor)) - } - - Type::TypedDict(typed_dict) => { - Type::TypedDict(typed_dict.apply_type_mapping_impl(db, type_mapping, tcx, visitor)) - } + Type::GenericAlias(generic) => Type::GenericAlias(generic.apply_type_mapping_impl( + db, + env, + type_mapping, + tcx, + visitor, + )), - Type::SubclassOf(subclass_of) => subclass_of.apply_type_mapping_impl(db, type_mapping, tcx, visitor), + Type::TypedDict(typed_dict) => Type::TypedDict(typed_dict.apply_type_mapping_impl( + db, + env, + type_mapping, + tcx, + visitor, + )), - Type::PropertyInstance(property) => { - Type::PropertyInstance(property.apply_type_mapping_impl(db, type_mapping, tcx, visitor)) + Type::SubclassOf(subclass_of) => { + subclass_of.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor) } - Type::Union(union) => union.map_leave_aliases(db, |element| { - element.apply_type_mapping_impl(db, type_mapping, tcx, visitor) + Type::PropertyInstance(property) => Type::PropertyInstance( + property.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor), + ), + + Type::Union(union) => union.map_leave_aliases(db, visitor.env, |element| { + element.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor) }), // Materializing an unsafe union picks *one* of its materializations, so the top @@ -7665,28 +8763,38 @@ impl<'db> Type<'db> { Type::UnsafeUnion(unsafe_union) => match type_mapping { TypeMapping::Materialize(MaterializationKind::Top) => UnionType::from_elements( db, + env, unsafe_union.elements(db).iter().map(|element| { - element.apply_type_mapping_impl(db, type_mapping, tcx, visitor) + element.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor) }), ), TypeMapping::Materialize(MaterializationKind::Bottom) => unsafe_union .elements(db) .iter() - .fold(IntersectionBuilder::new(db), |builder, element| { - builder.add_positive( - element.apply_type_mapping_impl(db, type_mapping, tcx, visitor), - ) + .fold(IntersectionBuilder::new(db, env), |builder, element| { + builder.add_positive(element.apply_type_mapping_impl( + db, + env, + type_mapping, + tcx, + visitor, + )) }) .build(), _ => unsafe_union.map_elements(db, |element| { - element.apply_type_mapping_impl(db, type_mapping, tcx, visitor) + element.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor) }), }, Type::Intersection(intersection) => { - let mut builder = IntersectionBuilder::new(db); + let mut builder = IntersectionBuilder::new(db, visitor.env); for positive in intersection.positive(db) { - builder = - builder.add_positive(positive.apply_type_mapping_impl(db, type_mapping, tcx, visitor)); + builder.add_positive_in_place(positive.apply_type_mapping_impl( + db, + env, + type_mapping, + tcx, + visitor, + )); } // Regular promotion should remove negative contributions from intersections, // so we don't preserve them here when regular promotion is enabled. @@ -7698,48 +8806,68 @@ impl<'db> Type<'db> { ) ) { for negative in intersection.negative(db) { - builder = builder.add_negative( - negative.apply_type_mapping_impl(db, &type_mapping.flip(), tcx, visitor), - ); + builder.add_negative_in_place(negative.apply_type_mapping_impl( + db, + env, + &type_mapping.flip(), + tcx, + visitor, + )); } } builder.build() } Type::EnumComplement(complement) => complement - .to_intersection(db) - .apply_type_mapping_impl(db, type_mapping, tcx, visitor), + .to_intersection(db, visitor.env) + .apply_type_mapping_impl(db, env, type_mapping, tcx, visitor), Type::TypeIs(type_is) => visitor.visit(db, self, type_mapping, || { type_is.with_type( db, - type_is - .type_argument(db) - .apply_type_mapping_impl(db, type_mapping, tcx, visitor), + type_is.type_argument(db).apply_type_mapping_impl( + db, + env, + type_mapping, + tcx, + visitor, + ), ) }), Type::TypeGuard(type_guard) => visitor.visit(db, self, type_mapping, || { type_guard.with_type( db, - type_guard - .return_type(db) - .apply_type_mapping_impl(db, type_mapping, tcx, visitor), + type_guard.return_type(db).apply_type_mapping_impl( + db, + env, + type_mapping, + tcx, + visitor, + ), ) }), Type::Overlapping(overlapping) => visitor.visit(db, self, type_mapping, || { OverlappingType::from_type_expression( db, - overlapping - .type_argument(db) - .apply_type_mapping_impl(db, type_mapping, tcx, visitor), + overlapping.type_argument(db).apply_type_mapping_impl( + db, + env, + type_mapping, + tcx, + visitor, + ), ) }), Type::Restricted(restricted) => visitor.visit(db, self, type_mapping, || { - let inner = restricted - .type_argument(db) - .apply_type_mapping_impl(db, type_mapping, tcx, visitor); + let inner = restricted.type_argument(db).apply_type_mapping_impl( + db, + env, + type_mapping, + tcx, + visitor, + ); // basedpython: `final A` inferred for a constructor call is extra // precision about one value, exactly like `Literal[1]` for `1`. // Promotion is where such precision is traded for a type a @@ -7752,38 +8880,48 @@ impl<'db> Type<'db> { { return inner; } - RestrictedType::from_type_expression(db, restricted.modifier(db), inner) + RestrictedType::from_type_expression(db, env, restricted.modifier(db), inner) }), Type::TypeForm(typeform) => visitor.visit(db, self, type_mapping, || { TypeFormType::from_type_expression( db, - typeform - .type_argument(db) - .apply_type_mapping_impl(db, type_mapping, tcx, visitor), + typeform.type_argument(db).apply_type_mapping_impl( + db, + env, + type_mapping, + tcx, + visitor, + ), ) }), Type::TypeAlias(alias) => { match type_mapping { + TypeMapping::Materialize(_) if alias.materialization_kind(db).is_some() => self, + TypeMapping::EagerExpansion if alias.materialization_kind(db).is_some() => { + alias.value_type(db).expand_eagerly(db, visitor.env) + } // For EagerExpansion, expand the raw value type. This path relies on Salsa's cycle // detection rather than the visitor's cycle detection, because the visitor tracks // Type values and `RecursiveList` is different from `RecursiveList[T]`. TypeMapping::EagerExpansion => { - alias.raw_value_type(db).expand_eagerly(db) - }, + alias.raw_value_type(db).expand_eagerly(db, visitor.env) + } // When specializing a generic type alias, instead of specializing the expanded type, the type alias itself is specialized. // Without this special handling, recursive type aliases would result in cycles, returning an unspecialized fallback type. TypeMapping::ApplySpecialization(specialization) - | TypeMapping::ApplySpecializationWithMaterialization { specialization, .. } - if matches!( + | TypeMapping::ApplySpecializationWithMaterialization { + specialization, .. + } if matches!( specialization, ApplySpecialization::Specialization(_) | ApplySpecialization::TypeAlias(_) | ApplySpecialization::Partial { .. } ) => { - let mut current_specialization = specialization.as_specialization(db).unwrap(); + let mut current_specialization = + specialization.as_specialization(db).unwrap(); if let TypeMapping::ApplySpecializationWithMaterialization { materialization_kind, .. @@ -7792,28 +8930,41 @@ impl<'db> Type<'db> { current_specialization = current_specialization .with_materialization_kind(db, Some(*materialization_kind)); } - Type::TypeAlias(alias.apply_specialization( - db, - |generic_context| { - alias - .specialization(db) - .unwrap_or_else(|| generic_context.default_specialization(db, None)) - .apply_specialization(db, current_specialization) - }, - )) + Type::TypeAlias(alias.apply_specialization(db, |generic_context| { + alias + .specialization(db) + .unwrap_or_else(|| generic_context.default_specialization(db, None)) + .apply_specialization(db, current_specialization) + })) } _ => { // IMPORTANT: All processing must happen inside a single visitor.visit() call so that if we encounter // this same TypeAlias again (e.g., in `type RecursiveT = int | tuple[RecursiveT, ...]`), the visitor // will detect the cycle and return the fallback value. let mapped = visitor.visit(db, self, type_mapping, || { - alias.value_type(db).apply_type_mapping_impl(db, type_mapping, tcx, visitor) + alias.value_type(db).apply_type_mapping_impl( + db, + env, + type_mapping, + tcx, + visitor, + ) }); // If the type mapping does not result in any change to this type alias, keep the - // alias node instead of eagerly expanding it. - if alias.value_type(db) == mapped { + // alias node instead of eagerly expanding it. A recursive backedge also returns + // the alias itself, and fully static aliases must retain their original identity. + if mapped == self || alias.value_type(db) == mapped { self + } else if let TypeMapping::Materialize(materialization_kind) = type_mapping + && matches!( + self.to_type_identity(db), + cyclic::TypeIdentity::RecursiveTypeAlias(_) + ) + { + Type::TypeAlias( + alias.with_materialization_kind(db, Some(*materialization_kind)), + ) } else { mapped } @@ -7822,50 +8973,50 @@ impl<'db> Type<'db> { } Type::LiteralValue(_) => match type_mapping { - TypeMapping::ApplySpecialization(_) | - TypeMapping::ApplySpecializationWithMaterialization { .. } | - TypeMapping::ProjectUseSiteVariance { .. } | - TypeMapping::BindLegacyTypevars(_) | - TypeMapping::FreshenBoundTypeVars { .. } | - TypeMapping::BindSelf { .. } | - TypeMapping::ReplaceSelf { .. } | - TypeMapping::Materialize(_) | - TypeMapping::ReplaceParameterDefaults | - TypeMapping::EagerExpansion | - TypeMapping::RescopeReturnCallables(_) | - TypeMapping::AttachRegexGroups(_) | - TypeMapping::Promote(PromotionMode::Off, _) | - TypeMapping::Promote( + TypeMapping::ApplySpecialization(_) + | TypeMapping::ApplySpecializationWithMaterialization { .. } + | TypeMapping::ProjectUseSiteVariance { .. } + | TypeMapping::BindLegacyTypevars(_) + | TypeMapping::FreshenBoundTypeVars { .. } + | TypeMapping::BindSelf { .. } + | TypeMapping::ReplaceSelf { .. } + | TypeMapping::Materialize(_) + | TypeMapping::ReplaceParameterDefaults + | TypeMapping::EagerExpansion + | TypeMapping::RescopeReturnCallables(_) + | TypeMapping::AttachRegexGroups(_) + | TypeMapping::Promote(PromotionMode::Off, _) + | TypeMapping::Promote( PromotionMode::On, PromotionKind::ClassLiteralsOnly | PromotionKind::SingletonsOnly, ) => self, TypeMapping::Promote( PromotionMode::On, PromotionKind::Regular | PromotionKind::RegularStrictNumeric, - ) => self.promote_impl(db), + ) => self.promote_impl(db, env), TypeMapping::Promote(PromotionMode::On, PromotionKind::RegularKeepingLiterals) => { - self.promote_impl_keeping_literals(db) + self.promote_impl_keeping_literals(db, env) } - } + }, Type::Dynamic(_) => match type_mapping { - TypeMapping::ApplySpecialization(_) | - TypeMapping::ApplySpecializationWithMaterialization { .. } | - TypeMapping::ProjectUseSiteVariance { .. } | - TypeMapping::BindLegacyTypevars(_) | - TypeMapping::FreshenBoundTypeVars { .. } | - TypeMapping::BindSelf(..) | - TypeMapping::ReplaceSelf { .. } | - TypeMapping::Promote(..) | - TypeMapping::ReplaceParameterDefaults | - TypeMapping::EagerExpansion | - TypeMapping::RescopeReturnCallables(_) | - TypeMapping::AttachRegexGroups(_) => self, + TypeMapping::ApplySpecialization(_) + | TypeMapping::ApplySpecializationWithMaterialization { .. } + | TypeMapping::ProjectUseSiteVariance { .. } + | TypeMapping::BindLegacyTypevars(_) + | TypeMapping::FreshenBoundTypeVars { .. } + | TypeMapping::BindSelf(..) + | TypeMapping::ReplaceSelf { .. } + | TypeMapping::Promote(..) + | TypeMapping::ReplaceParameterDefaults + | TypeMapping::EagerExpansion + | TypeMapping::RescopeReturnCallables(_) + | TypeMapping::AttachRegexGroups(_) => self, TypeMapping::Materialize(materialization_kind) => match materialization_kind { MaterializationKind::Top => Type::object(), MaterializationKind::Bottom => Type::Never, - } - } + }, + }, // `Divergent` is an internal cycle marker rather than a gradual type like `Any` or // `Unknown`. Preserve the marker across materialization, while recording whether this // occurrence should behave like the top (`object`) or bottom (`Never`) bound. @@ -7883,48 +9034,56 @@ impl<'db> Type<'db> { | Type::ModuleLiteral(_) | Type::KnownBoundMethod( KnownBoundMethodType::StrStartswith(_) + | KnownBoundMethodType::ConstraintSetLowerBound + | KnownBoundMethodType::ConstraintSetUpperBound + | KnownBoundMethodType::ConstraintSetEquality | KnownBoundMethodType::ConstraintSetRange | KnownBoundMethodType::ConstraintSetAlways | KnownBoundMethodType::ConstraintSetNever | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) | KnownBoundMethodType::ConstraintSetSatisfies(_) + | KnownBoundMethodType::ConstraintSetExists(_) | KnownBoundMethodType::ConstraintSetForAll(_) | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) | KnownBoundMethodType::ConstraintSetSolutionsFor(_) | KnownBoundMethodType::ConstraintSetSolutions(_) - | KnownBoundMethodType::ConstraintSetWithDetailedDisplay(_) + | KnownBoundMethodType::ConstraintSetWithDetailedDisplay(_), ) | Type::DataclassDecorator(_) | Type::DataclassTransformer(_) + | Type::BoundSuper(_) + | Type::SpecialForm(_) => self, + // 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(_) => self, + Type::ClassLiteral(_) => 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( + fn find_legacy_typevars( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, ) { self.find_legacy_typevars_impl( db, + env, binding_context, typevars, &FindLegacyTypeVarsVisitor::default(), ); } - pub(crate) fn find_legacy_typevars_impl( + fn find_legacy_typevars_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, @@ -7971,19 +9130,21 @@ impl<'db> Type<'db> { Type::FunctionLiteral(function) => { visitor.visit(db, self, || { - function.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + function.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); }); } Type::BoundMethod(method) => visitor.visit(db, self, || { method.self_instance(db).find_legacy_typevars_impl( db, + env, binding_context, typevars, visitor, ); method.function(db).find_legacy_typevars_impl( db, + env, binding_context, typevars, visitor, @@ -7994,7 +9155,7 @@ impl<'db> Type<'db> { KnownBoundMethodType::FunctionTypeDunderGet(function) | KnownBoundMethodType::FunctionTypeDunderCall(function), ) => visitor.visit(db, self, || { - function.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + function.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); }), Type::KnownBoundMethod( @@ -8002,57 +9163,57 @@ impl<'db> Type<'db> { | KnownBoundMethodType::PropertyDunderSet(property) | KnownBoundMethodType::PropertyDunderDelete(property), ) => visitor.visit(db, self, || { - property.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + property.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); }), Type::Callable(callable) => { - callable.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + callable.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } Type::PropertyInstance(property) => visitor.visit(db, self, || { - property.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + property.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); }), Type::Union(union) => { for element in union.elements(db) { - element.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + element.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } } Type::UnsafeUnion(unsafe_union) => { for element in unsafe_union.elements(db) { - element.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + element.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } } Type::Intersection(intersection) => { for positive in intersection.positive(db) { - positive.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + positive.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } for negative in intersection.negative(db) { - negative.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + negative.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } } Type::EnumComplement(complement) => { for rest in complement.rest(db) { - rest.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + rest.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } } Type::GenericAlias(alias) => { - alias.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + alias.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } Type::Deferred(deferred) => { for operand in deferred.operands(db) { - operand.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + operand.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } } Type::NominalInstance(instance) => { - instance.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + instance.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } Type::ProtocolInstance(instance) => { - instance.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + instance.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } Type::NewTypeInstance(_) => { @@ -8062,12 +9223,13 @@ impl<'db> Type<'db> { } Type::SubclassOf(subclass_of) => { - subclass_of.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + subclass_of.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } Type::TypeIs(type_is) => { type_is.type_argument(db).find_legacy_typevars_impl( db, + env, binding_context, typevars, visitor, @@ -8077,6 +9239,7 @@ impl<'db> Type<'db> { Type::TypeGuard(type_guard) => { type_guard.return_type(db).find_legacy_typevars_impl( db, + env, binding_context, typevars, visitor, @@ -8086,6 +9249,7 @@ impl<'db> Type<'db> { Type::TypeForm(typeform) => { typeform.type_argument(db).find_legacy_typevars_impl( db, + env, binding_context, typevars, visitor, @@ -8095,6 +9259,7 @@ impl<'db> Type<'db> { Type::Overlapping(overlapping) => { overlapping.type_argument(db).find_legacy_typevars_impl( db, + env, binding_context, typevars, visitor, @@ -8103,6 +9268,7 @@ impl<'db> Type<'db> { Type::Restricted(restricted) => { restricted.type_argument(db).find_legacy_typevars_impl( db, + env, binding_context, typevars, visitor, @@ -8113,6 +9279,7 @@ impl<'db> Type<'db> { visitor.visit(db, self, || { alias.value_type(db).find_legacy_typevars_impl( db, + env, binding_context, typevars, visitor, @@ -8125,6 +9292,7 @@ impl<'db> Type<'db> { if let Ok(union_type) = instance.union_type(db) { union_type.find_legacy_typevars_impl( db, + env, binding_context, typevars, visitor, @@ -8132,16 +9300,32 @@ impl<'db> Type<'db> { } } KnownInstanceType::Annotated(ty) | KnownInstanceType::WrappedOptional(ty) => { - ty.inner(db) - .find_legacy_typevars_impl(db, binding_context, typevars, visitor); + ty.inner(db).find_legacy_typevars_impl( + db, + env, + binding_context, + typevars, + visitor, + ); } KnownInstanceType::Callable(callable_type) => { - callable_type.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + callable_type.find_legacy_typevars_impl( + db, + env, + binding_context, + typevars, + visitor, + ); } KnownInstanceType::TypeGenericAlias(ty) | KnownInstanceType::LiteralStringAlias(ty) => { - ty.inner(db) - .find_legacy_typevars_impl(db, binding_context, typevars, visitor); + ty.inner(db).find_legacy_typevars_impl( + db, + env, + binding_context, + typevars, + visitor, + ); } KnownInstanceType::SubscriptedProtocol(_) | KnownInstanceType::SubscriptedGeneric(_) @@ -8179,11 +9363,15 @@ impl<'db> Type<'db> { | Type::WrapperDescriptor(_) | Type::KnownBoundMethod( KnownBoundMethodType::StrStartswith(_) + | KnownBoundMethodType::ConstraintSetLowerBound + | KnownBoundMethodType::ConstraintSetUpperBound + | KnownBoundMethodType::ConstraintSetEquality | KnownBoundMethodType::ConstraintSetRange | KnownBoundMethodType::ConstraintSetAlways | KnownBoundMethodType::ConstraintSetNever | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) | KnownBoundMethodType::ConstraintSetSatisfies(_) + | KnownBoundMethodType::ConstraintSetExists(_) | KnownBoundMethodType::ConstraintSetForAll(_) | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) | KnownBoundMethodType::ConstraintSetSolutionsFor(_) @@ -8203,28 +9391,35 @@ impl<'db> Type<'db> { /// Bind all unbound legacy type variables to the given context and then /// add all legacy typevars to the provided set. - pub(crate) fn bind_and_find_all_legacy_typevars( + fn bind_and_find_all_legacy_typevars( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, variables: &mut FxOrderSet>, ) { self.apply_type_mapping( db, + env, &TypeMapping::BindLegacyTypevars( binding_context .map(BindingContext::Definition) - .unwrap_or(BindingContext::Synthetic), + .unwrap_or(BindingContext::Synthetic(env.program(db))), ), TypeContext::default(), ) - .find_legacy_typevars(db, None, variables); + .find_legacy_typevars(db, env, None, variables); } /// Replace default types in parameters of callables with `Unknown`. - pub(crate) fn replace_parameter_defaults(self, db: &'db dyn Db) -> Type<'db> { + fn replace_parameter_defaults( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { self.apply_type_mapping( db, + env, &TypeMapping::ReplaceParameterDefaults, TypeContext::default(), ) @@ -8232,21 +9427,26 @@ impl<'db> Type<'db> { /// Returns the eagerly expanded type. /// In the case of recursive type aliases, this will diverge, so that part will be replaced with `Divergent`. - fn expand_eagerly(self, db: &'db dyn Db) -> Type<'db> { - self.expand_eagerly_(db, ()) + fn expand_eagerly(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + self.expand_eagerly_(db, env.program(db)) } - #[allow(clippy::used_underscore_binding)] #[salsa::tracked( returns(copy), - cycle_initial=|_, id, _, ()| Type::divergent(id), - cycle_fn=|db, cycle, previous: &Type<'db>, value: Type<'db>, _, ()| { - value.cycle_normalized(db, *previous, cycle) + cycle_initial=|_, id, _, _| Type::divergent(id), + cycle_fn=|db, cycle, previous: &Type<'db>, value: Type<'db>, _, program| { + value.cycle_normalized_impl(db, &ProgramEnvironment::from_program(program), *previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] - fn expand_eagerly_(self, db: &'db dyn Db, _unit: ()) -> Type<'db> { - self.apply_type_mapping(db, &TypeMapping::EagerExpansion, TypeContext::default()) + fn expand_eagerly_(self, db: &'db dyn Db, program: Program<'db>) -> Type<'db> { + let env = &ProgramEnvironment::from_program(program); + self.apply_type_mapping( + db, + env, + &TypeMapping::EagerExpansion, + TypeContext::default(), + ) } /// Return the string representation of this type when converted to string as it would be @@ -8255,10 +9455,10 @@ impl<'db> Type<'db> { /// 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(crate) fn str(&self, db: &'db dyn Db) -> Type<'db> { + fn str(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match self { Type::LiteralValue(literal) => match literal.kind() { - LiteralValueTypeKind::Int(_) | LiteralValueTypeKind::Bool(_) => self.repr(db), + LiteralValueTypeKind::Int(_) | LiteralValueTypeKind::Bool(_) => self.repr(db, env), LiteralValueTypeKind::String(_) | LiteralValueTypeKind::LiteralString => *self, LiteralValueTypeKind::Enum(enum_literal) => Type::string_literal( db, @@ -8268,33 +9468,37 @@ impl<'db> Type<'db> { name = enum_literal.name(db) ), ), - LiteralValueTypeKind::Bytes(_) => KnownClass::Str.to_instance(db), - LiteralValueTypeKind::Float(_) | LiteralValueTypeKind::Complex(_) => self.repr(db), + LiteralValueTypeKind::Bytes(_) => KnownClass::Str.to_instance(db, env), + LiteralValueTypeKind::Float(_) | LiteralValueTypeKind::Complex(_) => { + self.repr(db, env) + } }, Type::SpecialForm(special_form) => { Type::string_literal(db, special_form.to_compact_string()) } Type::KnownInstance(known_instance) => { - Type::string_literal(db, known_instance.repr(db).to_compact_string()) + Type::string_literal(db, known_instance.repr(db, env).to_compact_string()) } - ty if ty.is_subtype_of(db, Type::literal_string()) => Type::literal_string(), + ty if ty.is_subtype_of(db, env, Type::literal_string()) => Type::literal_string(), Type::Intersection(intersection) => { - if let Some(alternatives) = intersection.finite_alternative_union(db) { - alternatives.str(db) + if let Some(alternatives) = intersection.finite_alternative_union(db, env) { + alternatives.str(db, env) } else { - KnownClass::Str.to_instance(db) + KnownClass::Str.to_instance(db, env) } } - Type::EnumComplement(complement) => complement.remaining_literal_union(db).str(db), + Type::EnumComplement(complement) => { + complement.remaining_literal_union(db, env).str(db, env) + } // TODO: handle more complex types - _ => KnownClass::Str.to_instance(db), + _ => KnownClass::Str.to_instance(db, env), } } /// Return the string representation of this type as it would be provided by the `__repr__` /// method at runtime. #[must_use] - pub(crate) fn repr(&self, db: &'db dyn Db) -> Type<'db> { + fn repr(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match self { Type::LiteralValue(literal) => match literal.kind() { LiteralValueTypeKind::Int(number) => { @@ -8307,14 +9511,14 @@ impl<'db> Type<'db> { compact_str::format_compact!("'{}'", literal.value(db).escape_default()), ), LiteralValueTypeKind::LiteralString => Type::literal_string(), - _ => KnownClass::Str.to_instance(db), + _ => KnownClass::Str.to_instance(db, env), }, Type::SpecialForm(special_form) => Type::string_literal(db, &*special_form.to_string()), Type::KnownInstance(known_instance) => { - Type::string_literal(db, known_instance.repr(db).to_compact_string()) + Type::string_literal(db, known_instance.repr(db, env).to_compact_string()) } // TODO: handle more complex types - _ => KnownClass::Str.to_instance(db), + _ => KnownClass::Str.to_instance(db, env), } } @@ -8327,11 +9531,15 @@ impl<'db> Type<'db> { /// should be handled, especially when some variants don't have definitions, is /// specific to the call site. Exact singleton finite intersections delegate to /// their only alternative, since there is no ambiguity to preserve there. - pub fn definition(&self, db: &'db dyn Db) -> Option> { + pub fn definition( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { match self { - Self::Overlapping(overlapping) => overlapping.value_type(db).definition(db), - Self::Restricted(restricted) => restricted.value_type(db).definition(db), - Self::Deferred(deferred) => deferred.reduced(db).definition(db), + Self::Overlapping(overlapping) => overlapping.value_type(db, env).definition(db, env), + Self::Restricted(restricted) => restricted.value_type(db).definition(db, env), + Self::Deferred(deferred) => deferred.reduced(db, env).definition(db, env), Self::BoundMethod(method) => { Some(TypeDefinition::Function(method.function(db).definition(db))) } @@ -8341,7 +9549,7 @@ impl<'db> Type<'db> { Self::ModuleLiteral(module) => Some(TypeDefinition::Module(module.module(db))), Self::ClassLiteral(class_literal) => class_literal.type_definition(db), Self::GenericAlias(alias) => Some(TypeDefinition::StaticClass(alias.definition(db))), - Self::NominalInstance(instance) => instance.class(db).type_definition(db), + Self::NominalInstance(instance) => instance.class(db, env).type_definition(db), Self::KnownInstance(instance) => match instance { KnownInstanceType::TypeVar(var) => { Some(TypeDefinition::TypeVar(var.definition(db)?)) @@ -8358,80 +9566,89 @@ impl<'db> Type<'db> { Self::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() { SubclassOfInner::Dynamic(_) => None, SubclassOfInner::Class(class) => class.type_definition(db), - SubclassOfInner::Protocol(protocol) => protocol.class_origin()?.type_definition(db), + SubclassOfInner::Protocol(protocol) => { + protocol.class_origin(db)?.type_definition(db) + } SubclassOfInner::TypeVar(bound_typevar) => Some(TypeDefinition::TypeVar( bound_typevar.typevar(db).definition(db)?, )), }, - Self::TypeAlias(alias) => alias.value_type(db).definition(db), + Self::TypeAlias(alias) => alias.value_type(db).definition(db, env), Self::NewTypeInstance(newtype) => Some(TypeDefinition::NewType(newtype.definition(db))), Self::PropertyInstance(property) => property .getter(db) - .and_then(|getter| getter.definition(db)) - .or_else(|| property.setter(db).and_then(|setter| setter.definition(db))) + .and_then(|getter| getter.definition(db, env)) + .or_else(|| { + property + .setter(db) + .and_then(|setter| setter.definition(db, env)) + }) .or_else(|| { property .deleter(db) - .and_then(|deleter| deleter.definition(db)) + .and_then(|deleter| deleter.definition(db, env)) }), Self::LiteralValue(literal) => literal .as_enum() .and_then(|enum_lit| enum_lit.definition(db)) .map(TypeDefinition::EnumMember) - .or_else(|| self.to_meta_type(db).definition(db)), + .or_else(|| self.to_meta_type(db, env).definition(db, env)), Self::KnownBoundMethod(_) | Self::WrapperDescriptor(_) | Self::DataclassDecorator(_) | Self::DataclassTransformer(_) - | Self::BoundSuper(_) => self.to_meta_type(db).definition(db), + | Self::BoundSuper(_) => self.to_meta_type(db, env).definition(db, env), Self::TypeVar(bound_typevar) => Some(TypeDefinition::TypeVar( bound_typevar.typevar(db).definition(db)?, )), - Self::ProtocolInstance(protocol) => match protocol.inner { - Protocol::FromClass(class) => class.type_definition(db), - Protocol::Synthesized(_) => None, - }, + Self::ProtocolInstance(protocol) => protocol + .class_origin(db) + .and_then(|class| class.type_definition(db)), Self::TypedDict(typed_dict) => typed_dict.type_definition(db), Self::Union(_) | Self::UnsafeUnion(_) => None, Self::Intersection(intersection) => { - let alternatives = intersection.finite_alternatives(db)?; + let alternatives = intersection.finite_alternatives(db, env)?; let [alternative] = alternatives.as_slice() else { return None; }; - alternative.definition(db) + alternative.definition(db, env) } Self::EnumComplement(complement) => { - let alternatives = complement.remaining_literal_types(db); + let alternatives = complement.remaining_literal_types(db, env); let [alternative] = alternatives.as_slice() else { return None; }; - alternative.definition(db) + alternative.definition(db, env) } - Self::SpecialForm(special_form) => special_form.definition(db), - Self::Never => Type::SpecialForm(SpecialFormType::Never).definition(db), + Self::SpecialForm(special_form) => special_form.definition(db, env), + Self::Never => Type::SpecialForm(SpecialFormType::Never).definition(db, env), Self::Dynamic(DynamicType::Any) => { - Type::SpecialForm(SpecialFormType::Any).definition(db) + Type::SpecialForm(SpecialFormType::Any).definition(db, env) } Self::Dynamic( DynamicType::Unknown | DynamicType::UnknownGeneric(_) | DynamicType::AmbiguousOverload, - ) => Type::SpecialForm(SpecialFormType::Unknown).definition(db), - Self::Divergent(_) => Type::SpecialForm(SpecialFormType::Divergent).definition(db), + ) => Type::SpecialForm(SpecialFormType::Unknown).definition(db, env), + Self::Divergent(_) => Type::SpecialForm(SpecialFormType::Divergent).definition(db, env), Self::Dynamic(DynamicType::Todo(_)) => { - Type::SpecialForm(SpecialFormType::Todo).definition(db) + Type::SpecialForm(SpecialFormType::Todo).definition(db, env) + } + Self::AlwaysTruthy => { + Type::SpecialForm(SpecialFormType::AlwaysTruthy).definition(db, env) + } + Self::AlwaysFalsy => { + Type::SpecialForm(SpecialFormType::AlwaysFalsy).definition(db, env) } - Self::AlwaysTruthy => Type::SpecialForm(SpecialFormType::AlwaysTruthy).definition(db), - Self::AlwaysFalsy => Type::SpecialForm(SpecialFormType::AlwaysFalsy).definition(db), // These types have no definition Self::Dynamic( @@ -8508,11 +9725,15 @@ impl<'db> Type<'db> { } } - pub(crate) fn generic_origin(self, db: &'db dyn Db) -> Option> { + fn generic_origin( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { match self { Type::GenericAlias(generic) => Some(generic.origin(db)), Type::NominalInstance(instance) - if let ClassType::Generic(generic) = instance.class(db) => + if let ClassType::Generic(generic) = instance.class(db, env) => { Some(generic.origin(db)) } @@ -8523,34 +9744,41 @@ impl<'db> Type<'db> { /// Default-specialize all legacy typevars in this type. /// /// This is used when an implicit type alias is referenced without explicitly specializing it. - pub(crate) fn default_specialize(self, db: &'db dyn Db) -> Type<'db> { + fn default_specialize(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { let mut variables = FxOrderSet::default(); - self.find_legacy_typevars(db, None, &mut variables); - let generic_context = GenericContext::from_typevar_instances(db, variables); + self.find_legacy_typevars(db, env, None, &mut variables); + let generic_context = GenericContext::from_typevar_instances(db, env, variables); self.apply_specialization(db, generic_context.default_specialization(db, None)) } - pub(crate) fn from_truthiness(db: &'db dyn Db, truthiness: Truthiness) -> Self { + fn from_truthiness( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + truthiness: Truthiness, + ) -> Self { match truthiness { Truthiness::AlwaysTrue => Type::bool_literal(true), Truthiness::AlwaysFalse => Type::bool_literal(false), - Truthiness::Ambiguous => KnownClass::Bool.to_instance(db), + Truthiness::Ambiguous => KnownClass::Bool.to_instance(db, env), } } /// Return whether the negation of this type is a subtype of `target`, reusing `negated_cache` /// for type shapes whose negation must still be materialized. - pub(crate) fn negation_is_subtype_of_cached( + fn negation_is_subtype_of_cached( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, target: Type<'db>, negated_cache: &mut Option>, ) -> bool { match self { - Type::Intersection(intersection) => intersection.negation_is_subtype_of(db, target), + Type::Intersection(intersection) => { + intersection.negation_is_subtype_of(db, env, target) + } _ => { - let negated = negated_cache.get_or_insert_with(|| self.negate(db)); - negated.is_subtype_of(db, target) + let negated = negated_cache.get_or_insert_with(|| self.negate(db, env)); + negated.is_subtype_of(db, env, target) } } } @@ -8562,14 +9790,19 @@ impl<'db> IntersectionType<'db> { /// Applying De Morgan's law to an intersection produces a union. Checking each branch /// directly avoids constructing and simplifying that temporary union, which can be costly /// for the large intersections produced by repeated narrowing. - pub(crate) fn negation_is_subtype_of(self, db: &'db dyn Db, target: Type<'db>) -> bool { + fn negation_is_subtype_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + target: Type<'db>, + ) -> bool { self.positive(db) .iter() - .all(|positive| positive.negate(db).is_subtype_of(db, target)) + .all(|positive| positive.negate(db, env).is_subtype_of(db, env, target)) && self .negative(db) .iter() - .all(|negative| negative.is_subtype_of(db, target)) + .all(|negative| negative.is_subtype_of(db, env, target)) } // Calls the dunder on each element separately and combines the results. @@ -8581,13 +9814,21 @@ impl<'db> IntersectionType<'db> { fn try_call_dunder_with_policy( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, argument_types: &mut CallArguments<'_, 'db>, tcx: TypeContext<'db>, policy: MemberLookupPolicy, ) -> Result, CallDunderError<'db>> { - if let Some(alternatives) = self.finite_alternative_union(db) { - return alternatives.try_call_dunder_with_policy(db, name, argument_types, tcx, policy); + if let Some(alternatives) = self.finite_alternative_union(db, env) { + return alternatives.try_call_dunder_with_policy( + db, + env, + name, + argument_types, + tcx, + policy, + ); } // Using `positive()` rather than `positive_elements_or_object()` is safe @@ -8600,7 +9841,7 @@ impl<'db> IntersectionType<'db> { let mut error_provenance = Provenance::Unknown; for element in positive { - match element.try_call_dunder_with_policy(db, name, argument_types, tcx, policy) { + match element.try_call_dunder_with_policy(db, env, name, argument_types, tcx, policy) { Ok(bindings) => successful_bindings.push(bindings), Err(err) => { error_provenance = error_provenance.or(err.provenance()); @@ -8634,13 +9875,14 @@ impl<'db> UnionType<'db> { fn try_call_dunder_with_policy( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, argument_types: &mut CallArguments<'_, 'db>, tcx: TypeContext<'db>, policy: MemberLookupPolicy, ) -> Result, CallDunderError<'db>> { let elements = self.elements(db); - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); let mut unbound_on: Vec> = Vec::new(); let mut any_defined = false; let mut possibly_undefined = false; @@ -8650,6 +9892,7 @@ impl<'db> UnionType<'db> { match element .member_lookup_with_policy( db, + env, name, policy | MemberLookupPolicy::NO_INSTANCE_FALLBACK, ) @@ -8689,9 +9932,9 @@ impl<'db> UnionType<'db> { let dunder_callable = builder.build(); let constraints = ConstraintSetBuilder::new(); let bindings = match dunder_callable - .bindings(db) - .match_parameters(db, argument_types) - .check_types(db, &constraints, argument_types, tcx, &[]) + .bindings(db, env) + .match_parameters(db, env, argument_types) + .check_types(db, env, &constraints, argument_types, tcx, &[]) { Ok(bindings) => bindings, Err(CallError(kind, bindings)) => { @@ -8717,15 +9960,20 @@ impl<'db> From<&Type<'db>> for Type<'db> { } impl<'db> VarianceInferable<'db> for Type<'db> { - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { + fn variance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { tracing::trace!( "Checking variance of '{tvar}' in `{ty:?}`", tvar = typevar.identity.name(db), - ty = self.display(db), + ty = self.display(db, env), ); let v = match self { - Type::ClassLiteral(class_literal) => class_literal.variance_of(db, typevar), + Type::ClassLiteral(class_literal) => class_literal.variance_of(db, env, typevar), Type::FunctionLiteral(function_type) => { // TODO: do we need to replace self? @@ -8738,26 +9986,28 @@ impl<'db> VarianceInferable<'db> for Type<'db> { } Type::NominalInstance(nominal_instance_type) => { - nominal_instance_type.variance_of(db, typevar) + nominal_instance_type.variance_of(db, env, typevar) + } + Type::GenericAlias(generic_alias) => generic_alias.variance_of(db, env, typevar), + Type::Callable(callable_type) => { + callable_type.signatures(db).variance_of(db, env, typevar) } - Type::GenericAlias(generic_alias) => generic_alias.variance_of(db, typevar), - Type::Callable(callable_type) => callable_type.signatures(db).variance_of(db, typevar), // A type variable is always covariant in itself. Type::TypeVar(other_typevar) if other_typevar.identity(db) == typevar => { // type variables are covariant in themselves TypeVarVariance::Covariant } Type::ProtocolInstance(protocol_instance_type) => { - protocol_instance_type.variance_of(db, typevar) + protocol_instance_type.variance_of(db, env, typevar) } // unions are covariant in their disjuncts Type::Union(union_type) => union_type .elements(db) .iter() - .map(|ty| ty.variance_of(db, typevar)) + .map(|ty| ty.variance_of(db, env, typevar)) .collect(), - Type::UnsafeUnion(unsafe_union) => unsafe_union.variance_of(db, typevar), + Type::UnsafeUnion(unsafe_union) => unsafe_union.variance_of(db, env, typevar), // Products are covariant in their conjuncts. For negative // conjuncts, they're contravariant. To see this, suppose we have @@ -8768,32 +10018,32 @@ impl<'db> VarianceInferable<'db> for Type<'db> { Type::Intersection(intersection_type) => intersection_type .positive(db) .iter() - .map(|ty| ty.variance_of(db, typevar)) + .map(|ty| ty.variance_of(db, env, typevar)) .chain(intersection_type.negative(db).iter().map(|ty| { ty.with_polarity(TypeVarVariance::Contravariant) - .variance_of(db, typevar) + .variance_of(db, env, typevar) })) .collect(), - Type::EnumComplement(complement) => { - complement.to_intersection(db).variance_of(db, typevar) - } + Type::EnumComplement(complement) => complement + .to_intersection(db, env) + .variance_of(db, env, typevar), Type::PropertyInstance(property_instance_type) => property_instance_type .getter(db) .iter() .chain(&property_instance_type.setter(db)) .chain(&property_instance_type.deleter(db)) - .map(|ty| ty.variance_of(db, typevar)) + .map(|ty| ty.variance_of(db, env, typevar)) .collect(), - Type::SubclassOf(subclass_of_type) => subclass_of_type.variance_of(db, typevar), - Type::TypeIs(type_is_type) => type_is_type.variance_of(db, typevar), - Type::TypeGuard(type_guard_type) => type_guard_type.variance_of(db, typevar), - Type::TypeForm(typeform_type) => typeform_type.variance_of(db, typevar), - Type::Overlapping(overlapping_type) => overlapping_type.variance_of(db, typevar), - Type::Restricted(restricted_type) => restricted_type.variance_of(db, typevar), - Type::Deferred(deferred) => deferred.reduced(db).variance_of(db, typevar), - Type::KnownInstance(known_instance) => known_instance.variance_of(db, typevar), - Type::TypeAlias(alias) => alias.variance_of(db, typevar), - Type::TypedDict(typed_dict) => typed_dict.variance_of(db, typevar), + Type::SubclassOf(subclass_of_type) => subclass_of_type.variance_of(db, env, typevar), + Type::TypeIs(type_is_type) => type_is_type.variance_of(db, env, typevar), + Type::TypeGuard(type_guard_type) => type_guard_type.variance_of(db, env, typevar), + Type::TypeForm(typeform_type) => typeform_type.variance_of(db, env, typevar), + Type::Overlapping(overlapping_type) => overlapping_type.variance_of(db, env, typevar), + Type::Restricted(restricted_type) => restricted_type.variance_of(db, env, typevar), + Type::Deferred(deferred) => deferred.reduced(db, env).variance_of(db, env, typevar), + Type::KnownInstance(known_instance) => known_instance.variance_of(db, env, typevar), + Type::TypeAlias(alias) => alias.variance_of(db, env, typevar), + Type::TypedDict(typed_dict) => typed_dict.variance_of(db, env, typevar), Type::Dynamic(_) | Type::Divergent(_) | Type::Never @@ -8814,7 +10064,7 @@ impl<'db> VarianceInferable<'db> for Type<'db> { tracing::trace!( "Result of variance of '{tvar}' in `{ty:?}` is `{v:?}`", tvar = typevar.identity.name(db), - ty = self.display(db), + ty = self.display(db, env), ); v } @@ -8860,12 +10110,13 @@ pub enum PromotionKind { /// Returns the [`ClassLiteral`] that "owns" a `Self` typevar (i.e., the class from its upper bound). fn self_typevar_owner_class_literal<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, bound_typevar: BoundTypeVarInstance<'db>, ) -> Option> { bound_typevar .typevar(db) - .upper_bound(db) - .and_then(|ty| ty.nominal_class(db)) + .upper_bound(db, env) + .and_then(|ty| ty.nominal_class(db, env)) .map(|class| class.class_literal(db)) } @@ -8893,27 +10144,28 @@ pub struct SelfBinding<'db> { } impl<'db> SelfBinding<'db> { - pub(crate) fn self_type(&self) -> Type<'db> { + fn self_type(&self) -> Type<'db> { self.ty } - pub(crate) fn binding_context(&self) -> Option> { + fn binding_context(&self) -> Option> { self.binding_context } } impl<'db> SelfBinding<'db> { - pub(crate) fn new( + fn new( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, self_type: Type<'db>, binding_context: Option>, ) -> Self { let class_literal = match self_type { Type::TypeVar(typevar) if typevar.typevar(db).is_self(db) => { - self_typevar_owner_class_literal(db, typevar) + self_typevar_owner_class_literal(db, env, typevar) } _ => self_type - .nominal_class(db) + .nominal_class(db, env) .map(|class| class.class_literal(db)), }; @@ -8925,7 +10177,12 @@ impl<'db> SelfBinding<'db> { } /// Returns whether `bound_typevar` should be replaced by this binding's concrete self type. - fn should_bind(&self, db: &'db dyn Db, bound_typevar: BoundTypeVarInstance<'db>) -> bool { + fn should_bind( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + bound_typevar: BoundTypeVarInstance<'db>, + ) -> bool { if !bound_typevar.typevar(db).is_self(db) { return false; } @@ -8940,7 +10197,7 @@ impl<'db> SelfBinding<'db> { // If we can't determine either class, conservatively don't bind. self.class_literal.is_some_and(|class_literal| { let class_mro = class_mro_literals(db, class_literal); - self_typevar_owner_class_literal(db, bound_typevar) + self_typevar_owner_class_literal(db, env, bound_typevar) .is_none_or(|owner_class| class_mro.contains(&owner_class)) }) } @@ -9010,17 +10267,19 @@ pub enum TypeMapping<'a, 'db> { impl<'db> TypeMapping<'_, 'db> { /// Update the generic context of a [`Signature`] according to the current type mapping - pub(crate) fn update_signature_generic_context( + fn update_signature_generic_context( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, context: GenericContext<'db>, ) -> GenericContext<'db> { match self { TypeMapping::FreshenBoundTypeVars { .. } => GenericContext::from_typevar_instances( db, + env, context.variables(db).map(|bound_typevar| { Type::TypeVar(bound_typevar) - .apply_type_mapping(db, self, TypeContext::default()) + .apply_type_mapping(db, env, self, TypeContext::default()) .as_typevar() .unwrap_or(bound_typevar) }), @@ -9032,6 +10291,7 @@ impl<'db> TypeMapping<'_, 'db> { // (i.e., mapped to a non-TypeVar type) GenericContext::from_typevar_instances( db, + env, context.variables(db).filter(|bound_typevar| { // Keep the type variable if it's not in the specialization // or if it's mapped to itself (still a TypeVar) @@ -9062,6 +10322,7 @@ impl<'db> TypeMapping<'_, 'db> { } TypeMapping::ReplaceSelf { new_upper_bound } => GenericContext::from_typevar_instances( db, + env, context.variables(db).map(|typevar| { if typevar.typevar(db).is_self(db) { BoundTypeVarInstance::synthetic_self( @@ -9078,7 +10339,7 @@ impl<'db> TypeMapping<'_, 'db> { } /// Returns a new `TypeMapping` that should be applied in contravariant positions. - pub(crate) fn flip(&self) -> Self { + fn flip(&self) -> Self { match self { TypeMapping::Materialize(materialization_kind) => { TypeMapping::Materialize(materialization_kind.flip()) @@ -9197,7 +10458,7 @@ impl DynamicType<'_> { self } - pub(crate) fn is_todo(&self) -> bool { + fn is_todo(&self) -> bool { matches!(self, Self::Todo(_)) } } @@ -9266,7 +10527,8 @@ impl TypeQualifiers { Self::READ_ONLY => "ReadOnly", _ => { unreachable!( - "Only a single bit should be set when calling `TypeQualifiers::name` (got {self:?})" + "Only a single bit should be set \ + when calling `TypeQualifiers::name` (got {self:?})" ) } } @@ -9341,7 +10603,7 @@ impl<'db> TypeAndQualifiers<'db> { } } - pub(crate) fn declared(inner: Type<'db>) -> Self { + fn declared(inner: Type<'db>) -> Self { Self { inner, origin: TypeOrigin::Declared, @@ -9369,7 +10631,7 @@ impl<'db> TypeAndQualifiers<'db> { } /// Return `self` with an additional qualifier added to the set of qualifiers. - pub(crate) fn with_qualifier(mut self, qualifier: TypeQualifiers) -> Self { + fn with_qualifier(mut self, qualifier: TypeQualifiers) -> Self { self.qualifiers |= qualifier; self } @@ -9379,10 +10641,7 @@ impl<'db> TypeAndQualifiers<'db> { self.qualifiers } - pub(crate) fn map_type( - &self, - f: impl FnOnce(Type<'db>) -> Type<'db>, - ) -> TypeAndQualifiers<'db> { + fn map_type(&self, f: impl FnOnce(Type<'db>) -> Type<'db>) -> TypeAndQualifiers<'db> { TypeAndQualifiers { inner: f(self.inner), origin: self.origin, @@ -9408,16 +10667,18 @@ impl<'db> InvalidTypeExpressionError<'db> { node: &impl Ranged, flags: InferenceFlags, ) -> Type<'db> { + let db = context.db(); let InvalidTypeExpressionError { fallback_type, invalid_expressions, } = self; + let env = context.program_environment(); 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(), flags)); - error.add_subdiagnostics(context.db(), diagnostic, node); + let diagnostic = builder.into_diagnostic(error.reason(db, env, flags)); + error.add_subdiagnostics(db, env, diagnostic, node); } fallback_type } @@ -9468,6 +10729,8 @@ enum InvalidTypeExpression<'db> { TypingSelfInMetaclass, /// `typing.Self` cannot bound a type parameter of the class it belongs to. TypingSelfInClassTypeParameterBound, + /// `typing.Self` cannot be used with an incompatible explicit method receiver. + TypingSelfWithIncompatibleReceiver(BoundTypeVarInstance<'db>), /// Some types are always invalid in type expressions InvalidType(Type<'db>, ScopeId<'db>), InvalidBareParamSpec(TypeVarInstance<'db>), @@ -9477,29 +10740,39 @@ enum InvalidTypeExpression<'db> { } impl<'db> InvalidTypeExpression<'db> { - const fn reason(self, db: &'db dyn Db, flags: InferenceFlags) -> impl std::fmt::Display + 'db { + fn reason( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + flags: InferenceFlags, + ) -> impl std::fmt::Display + 'db { struct Display<'db> { error: InvalidTypeExpression<'db>, db: &'db dyn Db, + env: ProgramEnvironment<'db>, flags: InferenceFlags, } impl std::fmt::Display for Display<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let db = self.db; let location = self.flags.type_expression_context(); match self.error { InvalidTypeExpression::RequiresOneArgument(special_form) => write!( f, - "`{special_form}` requires exactly one argument when used in a {location}", + "`{special_form}` requires exactly one argument \ + when used in a {location}", ), InvalidTypeExpression::RequiresArguments(special_form) => write!( f, - "`{special_form}` requires at least one argument when used in a {location}", + "`{special_form}` requires at least one argument \ + when used in a {location}", ), InvalidTypeExpression::RequiresTwoArguments(special_form) => write!( f, - "`{special_form}` requires at least two arguments when used in a {location}", + "`{special_form}` requires at least two arguments \ + when used in a {location}", ), InvalidTypeExpression::Protocol => { write!(f, "`typing.Protocol` is not allowed in {location}s") @@ -9515,21 +10788,25 @@ impl<'db> InvalidTypeExpression<'db> { } InvalidTypeExpression::ConstraintSet => write!( f, - "`ty_extensions._internal.ConstraintSet` is not allowed in {location}s", + "`ty_extensions._internal.ConstraintSet` \ + is not allowed in {location}s", ), InvalidTypeExpression::ConstraintSetSolution => write!( f, - "`ty_extensions._internal.ConstraintSetSolution` is not allowed in {location}s", + "`ty_extensions._internal.ConstraintSetSolution` is not allowed \ + in {location}s", ), InvalidTypeExpression::GenericContext => { write!( f, - "`ty_extensions._internal.GenericContext` is not allowed in {location}s" + "`ty_extensions._internal.GenericContext` is not allowed \ + in {location}s" ) } InvalidTypeExpression::Specialization => write!( f, - "`ty_extensions._internal.Specialization` is not allowed in {location}s", + "`ty_extensions._internal.Specialization` \ + is not allowed in {location}s", ), InvalidTypeExpression::NamedTupleSpec => { write!(f, "`NamedTupleSpec` is not allowed in {location}s") @@ -9556,9 +10833,9 @@ impl<'db> InvalidTypeExpression<'db> { } else if qualifier.requires_one_argument() { write!( f, - "Type qualifier `{qualifier}` is not allowed in type expressions \ - (only in annotation expressions, and only with \ - exactly one argument)", + "Type qualifier `{qualifier}` is not allowed \ + in type expressions (only in annotation expressions, \ + and only with exactly one argument)", ) } else { write!( @@ -9580,27 +10857,32 @@ impl<'db> InvalidTypeExpression<'db> { InvalidTypeExpression::TypingSelfInClassTypeParameterBound => f.write_str( "`Self` cannot bound a type parameter of the class it belongs to", ), + InvalidTypeExpression::TypingSelfWithIncompatibleReceiver(_) => f.write_str( + "`Self` requires `self: Self` \ + or `cls: type[Self]` for annotated receivers", + ), InvalidTypeExpression::InvalidType(Type::FunctionLiteral(function), _) => { write!( f, "Function `{function}` is not valid in a {location}", - function = function.name(self.db) + function = function.name(db) ) } InvalidTypeExpression::InvalidType(Type::ModuleLiteral(module), _) => write!( f, "Module `{module}` is not valid in a {location}", - module = module.module(self.db).name(self.db) + module = module.module(db).name(db) ), InvalidTypeExpression::InvalidType(ty, _) => write!( f, "Variable of type `{ty}` is not allowed in a {location}", - ty = ty.display(self.db) + ty = ty.display(db, &self.env) ), InvalidTypeExpression::InvalidBareParamSpec(paramspec) => write!( f, - "Bare ParamSpec `{}` is not valid in this context in a {location}", - paramspec.name(self.db) + "Bare ParamSpec `{}` is not valid \ + in this context in a {location}", + paramspec.name(db) ), InvalidTypeExpression::InvalidBareKeywordVariadic(pack) => write!( f, @@ -9609,12 +10891,14 @@ impl<'db> InvalidTypeExpression<'db> { ), InvalidTypeExpression::InvalidBareTypeVarTuple(typevartuple) => write!( f, - "Bare TypeVarTuple `{}` is not valid in this context in a {location}", - typevartuple.name(self.db) + "Bare TypeVarTuple `{}` is not valid \ + in this context in a {location}", + typevartuple.name(db) ), InvalidTypeExpression::Concatenate => write!( f, - "`typing.Concatenate` is not allowed in this context in a {location}", + "`typing.Concatenate` is not allowed \ + in this context in a {location}", ), } } @@ -9623,6 +10907,7 @@ impl<'db> InvalidTypeExpression<'db> { Display { error: self, db, + env: env.clone(), flags, } } @@ -9630,6 +10915,7 @@ impl<'db> InvalidTypeExpression<'db> { fn add_subdiagnostics( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, mut diagnostic: LintDiagnosticGuard, node: &impl Ranged, ) { @@ -9644,7 +10930,7 @@ impl<'db> InvalidTypeExpression<'db> { let module = module.module(db); let module_name_final_part = module.name(db).last_component(); let Some(module_member_with_same_name) = ty - .member(db, module_name_final_part) + .member(db, env, module_name_final_part) .place .ignore_possibly_undefined() else { @@ -9656,7 +10942,7 @@ impl<'db> InvalidTypeExpression<'db> { { return; } - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Did you mean to use the module's member \ `{module_name_final_part}.{module_name_final_part}`?" )); @@ -9679,10 +10965,10 @@ impl<'db> InvalidTypeExpression<'db> { && function_body_scope .scope(db) .parent() - .map(|parent| parent.to_scope_id(db, function_body_scope.file(db))) - == builtins_module_scope(db) + .map(|parent| parent.to_scope_id(db, function_body_scope.program_file(db))) + == builtins_module_scope(db, env) { - diagnostic.set_primary_message("Did you mean `collections.abc.Callable`?"); + diagnostic.set_primary_annotation_message("Did you mean `collections.abc.Callable`?"); } else if matches!(self, InvalidTypeExpression::InvalidBareParamSpec(_)) { diagnostic.info("A bare ParamSpec is only valid:"); diagnostic.info(" - as the first argument to `Callable`"); @@ -9728,9 +11014,10 @@ impl<'db> AwaitError<'db> { }; let db = context.db(); + let env = context.program_environment(); let mut diag = builder.into_diagnostic( - format_args!("`{type}` is not awaitable", type = context_expression_type.display(db)), + format_args!("`{type}` is not awaitable", type = context_expression_type.display(db, env)), ); match self { Self::Call(CallDunderError::CallError(CallErrorKind::BindingError, bindings, _)) => { @@ -9754,7 +11041,7 @@ impl<'db> AwaitError<'db> { }; diag.info(format_args!("`__await__` is{possibly} not callable")); if let Some(definition) = attribute_provenance.definition() { - let module = parsed_module(db, definition.file(db)).load(db); + let module = parsed_module(db, definition.python_file(db)).load(db); diag.annotate( Annotation::secondary(definition.focus_range(db, &module).into()) .message("attribute defined here"), @@ -9770,7 +11057,7 @@ impl<'db> AwaitError<'db> { for ty in unbound_on { diag.info(format_args!( "`{}` does not implement `__await__`", - ty.display(db) + ty.display(db, env) )); } } @@ -9783,7 +11070,7 @@ impl<'db> AwaitError<'db> { } Self::Call(CallDunderError::MethodNotAvailable) => { diag.info("`__await__` is missing"); - if let Some(type_definition) = context_expression_type.definition(db) + if let Some(type_definition) = context_expression_type.definition(db, env) && let Some(definition_range) = type_definition.focus_range(db) { diag.annotate( @@ -9794,7 +11081,7 @@ impl<'db> AwaitError<'db> { Self::InvalidReturnType(return_type, bindings) => { diag.info(format_args!( "`__await__` returns `{return_type}`, which is not a valid iterator", - return_type = return_type.display(db) + return_type = return_type.display(db, env) )); if let Some(definition_spans) = bindings.callable_type().function_spans(db) { diag.annotate( @@ -9824,14 +11111,14 @@ pub struct ModuleLiteralType<'db> { /// the same underlying single-file module are understood by ty as being equivalent types /// in all situations. #[returns(copy)] - _importing_file: Option, + _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 { + fn importing_file(self, db: &'db dyn Db) -> Option> { debug_assert_eq!( self._importing_file(db).is_some(), self.module(db).kind(db).is_package() @@ -9903,25 +11190,42 @@ impl<'db> ModuleLiteralType<'db> { let relative_submodule_name = ModuleName::new(name)?; let mut absolute_submodule_name = self.module(db).name(db).clone(); absolute_submodule_name.extend(&relative_submodule_name); - let submodule = resolve_module(db, importing_file, &absolute_submodule_name)?; + let submodule = resolve_module( + db, + ImportingFile::File( + importing_file.file(db), + importing_file.resolver_environment(db), + ), + &absolute_submodule_name, + )?; Some(Type::module_literal(db, importing_file, submodule)) } - fn try_module_getattr(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + fn try_module_getattr( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> PlaceAndQualifiers<'db> { // For module literals, we want to try calling the module's own `__getattr__` function // if it exists. First, we need to look up the `__getattr__` function in the module's scope. - if let Some(file) = self.module(db).file(db) { - let getattr_symbol = imported_symbol(db, Some(file), "__getattr__", None); + let module = self.module(db); + if let Some(file) = module + .file(db) + .map(|file| ProgramFile::new(db, file, env.program(db))) + { + let getattr_symbol = imported_symbol(db, env, Some(file), "__getattr__", None); // If we found a __getattr__ function, try to call it with the name argument if let Place::Defined(place) = getattr_symbol.place && let Ok(outcome) = place.ty.try_call( db, + env, &CallArguments::positional([Type::string_literal(db, name)]), ) { return PlaceAndQualifiers { place: Place::Defined(DefinedPlace { - ty: outcome.return_type(db), + ty: outcome.return_type(db, env), provenance: Provenance::Unknown, ..place }), @@ -9933,14 +11237,20 @@ impl<'db> ModuleLiteralType<'db> { Place::Undefined.into() } - fn static_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + fn static_member( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> PlaceAndQualifiers<'db> { + let module = self.module(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__"); + .to_instance(db, env) + .member(db, env, "__dict__"); } // If the file that originally imported the module has also imported a submodule @@ -9958,11 +11268,14 @@ impl<'db> ModuleLiteralType<'db> { return Place::bound(submodule).into(); } - let place_and_qualifiers = imported_symbol(db, self.module(db).file(db), name, None); + let file = module + .file(db) + .map(|file| ProgramFile::new(db, file, env.program(db))); + let place_and_qualifiers = imported_symbol(db, env, file, name, None); // If the normal lookup failed, try to call the module's `__getattr__` function if place_and_qualifiers.place.is_undefined() { - return self.try_module_getattr(db, name); + return self.try_module_getattr(db, env, name); } // typeshed re-exports some special forms across modules (e.g. `collections.abc.Callable` @@ -9999,11 +11312,11 @@ pub(super) struct MetaclassCandidate<'db> { /// Information about a `@dataclass_transform`-decorated metaclass. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, get_size2::GetSize, salsa::SalsaValue)] pub(super) struct MetaclassTransformInfo<'db> { - pub(super) params: DataclassTransformerParams<'db>, + params: DataclassTransformerParams<'db>, /// Whether the metaclass providing these parameters was declared on the class itself /// (via an explicit `metaclass=` keyword) rather than inherited from a base class. - pub(super) from_explicit_metaclass: bool, + from_explicit_metaclass: bool, } #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] @@ -10028,7 +11341,7 @@ fn walk_typeis_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( impl get_size2::GetSize for TypeIsType<'_> {} impl<'db> TypeIsType<'db> { - pub(crate) fn place_name(self, db: &'db dyn Db) -> Option { + fn place_name(self, db: &'db dyn Db) -> Option { let (scope, place) = self.place_info(db)?; let table = place_table(db, scope); @@ -10037,44 +11350,31 @@ impl<'db> TypeIsType<'db> { /// Construct an unbound `TypeIs` return type from the user-written type expression. /// - /// The user-written type is preserved for `TypeIs` invariance checks, while the return type - /// used for narrowing applies the top materialization on demand. - /// /// ```python /// from typing import TypeIs /// /// def is_tuple(value: object) -> TypeIs[tuple[int, ...]]: /// return isinstance(value, tuple) /// ``` - pub(crate) fn from_type_expression(db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { + fn from_type_expression(db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { Type::TypeIs(Self::new(db, ty, None)) } - pub(crate) fn return_type(self, db: &'db dyn Db) -> Type<'db> { - // N.B. Using the top materialization here is a pragmatic decision that - // makes us produce more intuitive results given how `TypeIs` is used in - // the real world (in particular, in typeshed). However, there's some - // debate about whether this is really fully correct. See - // for more discussion. - self.type_argument(db).top_materialization(db) + fn return_type(self, db: &'db dyn Db) -> Type<'db> { + self.type_argument(db) } #[must_use] - pub(crate) fn bind( - self, - db: &'db dyn Db, - scope: ScopeId<'db>, - place: ScopedPlaceId, - ) -> Type<'db> { + fn bind(self, db: &'db dyn Db, scope: ScopeId<'db>, place: ScopedPlaceId) -> Type<'db> { Type::TypeIs(Self::new(db, self.type_argument(db), Some((scope, place)))) } #[must_use] - pub(crate) fn with_type(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { + fn with_type(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { Type::TypeIs(Self::new(db, ty, self.place_info(db))) } - pub(crate) fn is_bound(self, db: &'db dyn Db) -> bool { + fn is_bound(self, db: &'db dyn Db) -> bool { self.place_info(db).is_some() } } @@ -10082,10 +11382,15 @@ impl<'db> TypeIsType<'db> { impl<'db> VarianceInferable<'db> for TypeIsType<'db> { // See the [typing spec] on why `TypeIs` is invariant in its type. // [typing spec]: https://typing.python.org/en/latest/spec/narrowing.html#typeis - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { + fn variance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { self.type_argument(db) .with_polarity(TypeVarVariance::Invariant) - .variance_of(db, typevar) + .variance_of(db, env, typevar) } } @@ -10111,18 +11416,18 @@ fn walk_typeguard_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( impl get_size2::GetSize for TypeGuardType<'_> {} impl<'db> TypeGuardType<'db> { - pub(crate) fn place_name(self, db: &'db dyn Db) -> Option { + 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(place))) } - pub(crate) fn unbound(db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { + fn unbound(db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { Type::TypeGuard(Self::new(db, ty, None)) } - pub(crate) fn bound( + fn bound( db: &'db dyn Db, return_type: Type<'db>, scope: ScopeId<'db>, @@ -10132,21 +11437,16 @@ impl<'db> TypeGuardType<'db> { } #[must_use] - pub(crate) fn bind( - self, - db: &'db dyn Db, - scope: ScopeId<'db>, - place: ScopedPlaceId, - ) -> Type<'db> { + 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(crate) fn with_type(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { + fn with_type(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { Type::TypeGuard(Self::new(db, ty, self.place_info(db))) } - pub(crate) fn is_bound(self, db: &'db dyn Db) -> bool { + fn is_bound(self, db: &'db dyn Db) -> bool { self.place_info(db).is_some() } } @@ -10154,8 +11454,13 @@ impl<'db> TypeGuardType<'db> { impl<'db> VarianceInferable<'db> for TypeGuardType<'db> { // `TypeGuard` is covariant in its type parameter. See the `TypeGuard` // section of mdtest/generics/pep695/variance.md for details. - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { - self.return_type(db).variance_of(db, typevar) + fn variance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { + self.return_type(db).variance_of(db, env, typevar) } } @@ -10223,6 +11528,7 @@ impl<'db> TypeGuardLike<'db> for TypeGuardType<'db> { /// being added to the given class. pub(super) fn determine_upper_bound<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class_literal: ClassLiteral<'db>, is_known_base: impl Fn(ClassBase<'db>) -> bool, ) -> Type<'db> { @@ -10232,7 +11538,7 @@ pub(super) fn determine_upper_bound<'db>( .filter_map(ClassBase::into_class) .last() .unwrap_or_else(|| class_literal.unknown_specialization(db)); - Type::instance(db, upper_bound) + Type::instance(db, env, upper_bound) } // Make sure that the `Type` enum does not grow unexpectedly. diff --git a/crates/ty_python_semantic/src/types/attribute_write.rs b/crates/ty_python_semantic/src/types/attribute_write.rs index 6bc9e9fd5e..2df682715f 100644 --- a/crates/ty_python_semantic/src/types/attribute_write.rs +++ b/crates/ty_python_semantic/src/types/attribute_write.rs @@ -7,14 +7,18 @@ //! diagnostics, while protocol checking can evaluate the same lookup result using its active type //! relation and constraint set. +use crate::Db; use ty_module_resolver::KnownModule; +use ty_python_core::use_def_map; use super::call::CallArguments; use super::callable::CallableTypeKind; use super::safe_variance::private_member_write_type; use super::{KnownClass, KnownInstanceType, MemberLookupPolicy, Type, TypeQualifiers}; -use crate::Db; -use crate::place::{DefinedPlace, Definedness, Place, PlaceAndQualifiers, builtins_symbol}; +use crate::ProgramEnvironment; +use crate::place::{ + DefinedPlace, Definedness, Place, PlaceAndQualifiers, builtins_symbol, place_from_bindings, +}; /// The operation required to write an attribute. /// @@ -68,10 +72,11 @@ pub(super) enum ProtocolMemberWriteRequirement<'db> { AssignableTo(Type<'db>), /// Invoke every possible descriptor setter with the assigned value. /// - /// `domain` is the precisely derived write type when that domain fits in [`Type`]. It is used - /// for contextual inference and protocol compatibility, while descriptor calls remain the - /// authority for real assignments. `None` preserves a known write capability whose generic or - /// set-theoretic domain cannot be represented precisely. + /// `domain` is the precisely derived write type when that domain fits in [`Type`]. A + /// representable domain constrains contextual inference, assignment, and protocol + /// compatibility. Calling the original descriptor still validates the complete setter + /// contract. `None` preserves a known write capability whose generic or set-theoretic domain + /// cannot be represented precisely. Descriptor { descriptor_ty: Type<'db>, receiver_ty: Type<'db>, @@ -104,7 +109,8 @@ pub(super) enum InstanceAttributeWriteMember<'db> { /// /// A data descriptor on the metaclass takes precedence over the class object's own attributes, /// which in turn take precedence over definitely non-data metaclass members. If the metaclass -/// member is absent or possibly undefined, the class object's own attributes form the fallback. +/// member is absent, possibly undefined, or could be a non-data descriptor, the class object's own +/// attributes form the fallback. pub(super) enum ClassAttributeWriteMember<'db> { /// A metaclass member governs the write, optionally alongside a class-attribute fallback. Explicit { @@ -148,7 +154,7 @@ impl ExplicitAttributeWriteRequirement<'_> { } } -/// A write target found through a possibly absent fallback lookup. +/// A receiver-level write target that can govern the write instead of the type member. pub(super) enum FallbackAttributeWriteRequirement<'db> { /// Check the value against `ty`, retaining whether the declaration may be absent at runtime. AssignableTo { @@ -182,8 +188,8 @@ pub(super) enum FallbackAttributeWriteRequirement<'db> { /// ``` pub(super) enum AssignmentAttributeMembers<'db> { /// The type member governs the write, as `Meta.data` does above because it is a data descriptor. - /// If the type member may be missing, the corresponding receiver member (`C.data`) is retained - /// as `receiver_fallback`. + /// If the type member may be missing or may be a non-data descriptor, the corresponding + /// receiver member (`C.data`) is retained as `receiver_fallback`. TypeMember { member: PlaceAndQualifiers<'db>, receiver_fallback: Option>, @@ -222,19 +228,20 @@ impl<'db> AssignmentAttributeMembers<'db> { /// paths. It does not compare the assigned value with the resulting types. pub(super) fn attribute_write_requirement<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, object_ty: Type<'db>, attribute: &str, ) -> AttributeWriteRequirement<'db> { match object_ty { // parameter-only marker; behaves as the type a body sees (bound of `Key`) Type::Overlapping(overlapping) => { - attribute_write_requirement(db, overlapping.value_type(db), attribute) + attribute_write_requirement(db, env, overlapping.value_type(db, env), attribute) } Type::Restricted(restricted) => { - attribute_write_requirement(db, restricted.value_type(db), attribute) + attribute_write_requirement(db, env, restricted.value_type(db), attribute) } Type::Deferred(deferred) => { - attribute_write_requirement(db, deferred.reduced(db), attribute) + attribute_write_requirement(db, env, deferred.reduced(db, env), attribute) } Type::Union(union) => AttributeWriteRequirement::All { object_ty, @@ -256,11 +263,16 @@ pub(super) fn attribute_write_requirement<'db>( element_tys: unsafe_union.elements(db).to_vec(), }, - Type::EnumComplement(complement) => { - attribute_write_requirement(db, complement.remaining_literal_union(db), attribute) - } + Type::EnumComplement(complement) => attribute_write_requirement( + db, + env, + complement.remaining_literal_union(db, env), + attribute, + ), - Type::TypeAlias(alias) => attribute_write_requirement(db, alias.value_type(db), attribute), + Type::TypeAlias(alias) => { + attribute_write_requirement(db, env, alias.value_type(db), attribute) + } Type::NominalInstance(instance) if instance.has_known_class(db, KnownClass::Super) => { AttributeWriteRequirement::CannotAssign @@ -273,9 +285,9 @@ pub(super) fn attribute_write_requirement<'db>( Type::ProtocolInstance(protocol) => protocol .interface(db) - .instance_write_requirement(db, object_ty, attribute) + .instance_write_requirement(db, env, object_ty, attribute) .map_or_else( - || instance_attribute_write_requirement(db, object_ty, attribute), + || instance_attribute_write_requirement(db, env, object_ty, attribute), |(write, qualifiers)| AttributeWriteRequirement::ProtocolMember { write, qualifiers, @@ -302,13 +314,13 @@ pub(super) fn attribute_write_requirement<'db>( | Type::TypeForm(_) | Type::TypedDict(_) | Type::NewTypeInstance(_) => { - instance_attribute_write_requirement(db, object_ty, attribute) + instance_attribute_write_requirement(db, env, object_ty, attribute) } Type::SubclassOf(subclass_of) => subclass_of - .meta_write_requirement(db, attribute) + .meta_write_requirement(db, env, attribute) .map_or_else( - || class_attribute_write_requirement(db, object_ty, attribute), + || class_attribute_write_requirement(db, env, object_ty, attribute), |(write_ty, qualifiers)| AttributeWriteRequirement::ProtocolMember { write: write_ty.map(ProtocolMemberWriteRequirement::AssignableTo), qualifiers, @@ -316,18 +328,18 @@ pub(super) fn attribute_write_requirement<'db>( ), Type::ClassLiteral(..) | Type::GenericAlias(..) => { - class_attribute_write_requirement(db, object_ty, attribute) + class_attribute_write_requirement(db, env, object_ty, attribute) } Type::ModuleLiteral(module) => { - let symbol = if module - .module(db) + let resolved_module = module.module(db); + let symbol = if resolved_module .known(db) .is_some_and(KnownModule::is_builtins) { - builtins_symbol(db, attribute) + builtins_symbol(db, env, attribute) } else { - module.static_member(db, attribute) + module.static_member(db, env, attribute) }; AttributeWriteRequirement::Module(match symbol.place { Place::Defined(DefinedPlace { ty, .. }) => Some(ty), @@ -339,12 +351,13 @@ pub(super) fn attribute_write_requirement<'db>( fn instance_attribute_write_requirement<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, object_ty: Type<'db>, attribute: &str, ) -> AttributeWriteRequirement<'db> { AttributeWriteRequirement::Instance { object_ty, - member: instance_attribute_write_member_requirement(db, object_ty, attribute), + member: instance_attribute_write_member_requirement(db, env, object_ty, attribute), } } @@ -355,10 +368,11 @@ fn instance_attribute_write_requirement<'db>( /// `__setattr__`. fn instance_attribute_write_member_requirement<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, object_ty: Type<'db>, attribute: &str, ) -> InstanceAttributeWriteMember<'db> { - let Some(members) = assignment_attribute_members(db, object_ty, attribute) else { + let Some(members) = assignment_attribute_members(db, env, object_ty, attribute) else { return InstanceAttributeWriteMember::SetAttr; }; let (type_member, receiver_fallback) = match members { @@ -368,7 +382,7 @@ fn instance_attribute_write_member_requirement<'db>( } => (member, receiver_fallback), AssignmentAttributeMembers::ReceiverMember(member) => { return InstanceAttributeWriteMember::Instance(instance_fallback_write_requirement( - db, object_ty, attribute, member, + db, env, object_ty, attribute, member, )); } }; @@ -381,13 +395,14 @@ fn instance_attribute_write_member_requirement<'db>( } => InstanceAttributeWriteMember::Explicit { member: explicit_attribute_write_requirement( db, + env, object_ty, attribute, - ty.bind_self_typevars(db, object_ty), + ty.bind_self_typevars(db, env, object_ty), qualifiers, ), fallback: receiver_fallback.map(|fallback| { - instance_fallback_write_requirement(db, object_ty, attribute, fallback) + instance_fallback_write_requirement(db, env, object_ty, attribute, fallback) }), }, PlaceAndQualifiers { @@ -400,7 +415,7 @@ fn instance_attribute_write_member_requirement<'db>( .. }, ) => InstanceAttributeWriteMember::Instance(instance_fallback_write_requirement( - db, object_ty, attribute, fallback, + db, env, object_ty, attribute, fallback, )), _ => InstanceAttributeWriteMember::SetAttr, }, @@ -413,13 +428,14 @@ fn instance_attribute_write_member_requirement<'db>( /// declarations can be bound consistently with normal class-object member lookup. fn class_attribute_write_requirement<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, object_ty: Type<'db>, attribute: &str, ) -> AttributeWriteRequirement<'db> { - let Some(members) = assignment_attribute_members(db, object_ty, attribute) else { + let Some(members) = assignment_attribute_members(db, env, object_ty, attribute) else { return AttributeWriteRequirement::Unconstrained; }; - let Some(class_attr_self_ty) = object_ty.to_instance_approximation(db) else { + let Some(class_attr_self_ty) = object_ty.to_instance_approximation(db, env) else { return AttributeWriteRequirement::Unconstrained; }; let (type_member, receiver_fallback) = match members { @@ -431,7 +447,13 @@ fn class_attribute_write_requirement<'db>( return AttributeWriteRequirement::Class { object_ty, member: ClassAttributeWriteMember::ClassAttribute( - class_fallback_write_requirement(db, object_ty, class_attr_self_ty, member), + class_fallback_write_requirement( + db, + env, + object_ty, + class_attr_self_ty, + member, + ), ), }; } @@ -439,14 +461,32 @@ fn class_attribute_write_requirement<'db>( let member = match type_member { PlaceAndQualifiers { - place: Place::Defined(DefinedPlace { ty, .. }), + place: Place::Defined(place @ DefinedPlace { ty, .. }), qualifiers, - } => ClassAttributeWriteMember::Explicit { - member: explicit_attribute_write_requirement(db, object_ty, attribute, ty, qualifiers), - fallback: receiver_fallback.map(|fallback| { - class_fallback_write_requirement(db, object_ty, class_attr_self_ty, fallback) - }), - }, + } => { + let descriptor_ty = receiver_fallback + .and_then(|_| possible_class_attribute_descriptor(db, env, place)) + .unwrap_or(ty); + ClassAttributeWriteMember::Explicit { + member: explicit_attribute_write_requirement( + db, + env, + object_ty, + attribute, + descriptor_ty, + qualifiers, + ), + fallback: receiver_fallback.map(|fallback| { + class_fallback_write_requirement( + db, + env, + object_ty, + class_attr_self_ty, + fallback, + ) + }), + } + } PlaceAndQualifiers { place: Place::Undefined, .. @@ -458,13 +498,14 @@ fn class_attribute_write_requirement<'db>( }, ) => ClassAttributeWriteMember::ClassAttribute(class_fallback_write_requirement( db, + env, object_ty, class_attr_self_ty, fallback, )), _ => ClassAttributeWriteMember::Unresolved { has_instance_attribute: !class_attr_self_ty - .instance_member(db, attribute) + .instance_member(db, env, attribute) .place .is_undefined(), }, @@ -474,6 +515,46 @@ fn class_attribute_write_requirement<'db>( AttributeWriteRequirement::Class { object_ty, member } } +/// Recover the concrete descriptor hidden by an uncertain metaclass-member annotation. +/// +/// The declared type describes the descriptor object, not the values accepted by its setter. +/// Inspecting the binding preserves the setter's actual value contract: +/// +/// ```python +/// class DescriptorMeta(type): +/// def __set__(self, instance: object, value: str) -> None: ... +/// +/// class Descriptor(metaclass=DescriptorMeta): ... +/// +/// class Meta(type): +/// attribute: type[object] = Descriptor +/// ``` +fn possible_class_attribute_descriptor<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + member: DefinedPlace<'db>, +) -> Option> { + if member.ty.is_data_descriptor(db, env) || member.ty.is_definitely_non_data_descriptor(db, env) + { + return None; + } + + let definition = member.provenance.definition()?; + let use_def = use_def_map(db, definition.scope(db)); + let descriptor_ty = + place_from_bindings(db, env, use_def.end_of_scope_bindings(definition.place(db))) + .place + .ignore_possibly_undefined()?; + let descriptor_ty = match descriptor_ty.resolve_type_alias(db) { + Type::TypeForm(typeform) => typeform.type_argument(db).to_meta_type(db, env), + descriptor_ty => descriptor_ty, + }; + + descriptor_ty + .is_data_descriptor(db, env) + .then_some(descriptor_ty) +} + /// Convert an explicitly resolved member into either a descriptor call or a direct type check. /// /// Descriptor behavior is used only when `__set__` is found with @@ -481,6 +562,7 @@ fn class_attribute_write_requirement<'db>( /// ordinary attribute to be treated as a data descriptor. fn explicit_attribute_write_requirement<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, object_ty: Type<'db>, attribute: &str, attr_ty: Type<'db>, @@ -489,12 +571,12 @@ fn explicit_attribute_write_requirement<'db>( // basedpython safe variance: a private member does not specialize, so a widened view of the // class knows nothing about it — not even whether it is a descriptor whose `__set__` would // govern the write. There is nothing such a view can supply - if let Some(ty) = private_member_write_type(db, object_ty, attribute) { + if let Some(ty) = private_member_write_type(db, env, object_ty, attribute) { return ExplicitAttributeWriteRequirement::AssignableTo { ty, qualifiers }; } if let Place::Defined(DefinedPlace { ty: setter_ty, .. }) = attr_ty - .class_member_with_policy(db, "__set__", MemberLookupPolicy::REQUIRE_CONCRETE) + .class_member_with_policy(db, env, "__set__", MemberLookupPolicy::REQUIRE_CONCRETE) .place { ExplicitAttributeWriteRequirement::Descriptor { @@ -504,7 +586,7 @@ fn explicit_attribute_write_requirement<'db>( } } else { ExplicitAttributeWriteRequirement::AssignableTo { - ty: effective_write_type(db, object_ty, attribute, attr_ty), + ty: effective_write_type(db, env, object_ty, attribute, attr_ty), qualifiers, } } @@ -516,6 +598,7 @@ fn explicit_attribute_write_requirement<'db>( /// assignment diagnostic layer. fn instance_fallback_write_requirement<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, object_ty: Type<'db>, attribute: &str, fallback: PlaceAndQualifiers<'db>, @@ -531,9 +614,9 @@ fn instance_fallback_write_requirement<'db>( }; // basedpython safe variance: a private member does not specialize, and a widened view of the // class has nothing to write to it — see `explicit_attribute_write_requirement` - let ty = private_member_write_type(db, object_ty, attribute).unwrap_or_else(|| { - let ty = ty.bind_self_typevars(db, object_ty); - effective_write_type(db, object_ty, attribute, ty) + let ty = private_member_write_type(db, env, object_ty, attribute).unwrap_or_else(|| { + let ty = ty.bind_self_typevars(db, env, object_ty); + effective_write_type(db, env, object_ty, attribute, ty) }); FallbackAttributeWriteRequirement::AssignableTo { ty, @@ -545,6 +628,7 @@ fn instance_fallback_write_requirement<'db>( /// Convert a class-attribute fallback into a write type, binding `Self` to the class instance. fn class_fallback_write_requirement<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, object_ty: Type<'db>, class_attr_self_ty: Type<'db>, fallback: PlaceAndQualifiers<'db>, @@ -558,7 +642,7 @@ fn class_fallback_write_requirement<'db>( else { return FallbackAttributeWriteRequirement::PossiblyMissing; }; - let ty = ty.bind_self_typevars(db, class_attr_self_ty); + let ty = ty.bind_self_typevars(db, env, class_attr_self_ty); let ty = if matches!(object_ty, Type::ClassLiteral(_)) && let Type::FunctionLiteral(function) = ty && function.callable_type_kind(db) == CallableTypeKind::FunctionLike @@ -581,13 +665,14 @@ fn class_fallback_write_requirement<'db>( /// `(str) -> int` converter is read as `int` but accepts `str` assignments. fn effective_write_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, object_ty: Type<'db>, attribute: &str, attr_ty: Type<'db>, ) -> Type<'db> { if let Type::NominalInstance(instance) = object_ty && let Some(converter_ty) = instance - .class(db) + .class(db, env) .converter_input_type_for_field(db, attribute) { converter_ty @@ -614,55 +699,85 @@ fn effective_write_type<'db>( /// ``` pub(super) fn property_setter_returns_never<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, property_ty: Type<'db>, object_ty: Type<'db>, value_ty: Type<'db>, ) -> bool { property_ty.as_property_instance().is_some_and(|property| { property.setter(db).is_some_and(|setter| { - match setter.try_call(db, &CallArguments::positional([object_ty, value_ty])) { - Ok(result) => result.return_type(db).is_never(), - Err(error) => error.return_type(db).is_never(), + match setter.try_call(db, env, &CallArguments::positional([object_ty, value_ty])) { + Ok(result) => result.return_type(db, env).is_never(), + Err(error) => error.return_type(db, env).is_never(), } }) }) } -/// Return the class member that takes precedence over a definitely non-data metaclass member. -fn class_member_preceding_non_data_metaclass_member<'db>( +/// Resolve class-object members when a class attribute can shadow its metaclass member. +/// +/// A definitely non-data metaclass member is shadowed entirely. If the metaclass member's +/// descriptor status is uncertain, both members remain possible write targets. +/// +/// ```python +/// class Meta(type): +/// attribute = object() +/// +/// class C(metaclass=Meta): +/// attribute: int +/// +/// C.attribute = 1 +/// ``` +fn class_object_assignment_members<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, object_ty: Type<'db>, attribute: &str, type_member: PlaceAndQualifiers<'db>, -) -> Option> { +) -> Option> { if !matches!( object_ty, Type::ClassLiteral(..) | Type::GenericAlias(..) | Type::SubclassOf(..) - ) || !type_member - .place - .ignore_possibly_undefined()? - .is_definitely_non_data_descriptor(db) + ) { + return None; + } + + let type_member_ty = type_member.place.ignore_possibly_undefined()?; + let definitely_non_data_descriptor = type_member_ty.is_definitely_non_data_descriptor(db, env); + if !definitely_non_data_descriptor + && (type_member_ty.is_divergent() || type_member_ty.is_data_descriptor(db, env)) { return None; } - object_ty - .find_name_in_mro_with_policy(db, attribute, MemberLookupPolicy::default()) - .filter(|class_attr| !class_attr.place.is_undefined()) + let receiver_member = object_ty + .find_name_in_mro_with_policy(db, env, attribute, MemberLookupPolicy::default()) + .filter(|class_attr| !class_attr.place.is_undefined())?; + + Some(if definitely_non_data_descriptor { + AssignmentAttributeMembers::ReceiverMember(receiver_member) + } else { + AssignmentAttributeMembers::TypeMember { + member: type_member, + receiver_fallback: Some(receiver_member), + } + }) } /// Return the members considered by attribute assignment in lookup-precedence order. /// /// The type member comes from class-member lookup. A member found directly on the receiver is /// queried when the type member is absent or possibly undefined. For class objects, a class-MRO -/// member instead takes precedence over a definitely non-data metaclass member. Composite and -/// dynamic receiver types return `None`; their callers either decompose them before this point or -/// handle them without member lookup. +/// member instead takes precedence over a definitely non-data metaclass member and remains an +/// alternative when the metaclass member's descriptor status is uncertain. Composite and dynamic +/// receiver types return `None`; their callers either decompose them before this point or handle +/// them without member lookup. /// /// This helper deliberately does not bind `Self` or interpret descriptors so that assignment, /// protocol compatibility, and `Final` validation share exactly the same lookup precedence. pub(super) fn assignment_attribute_members<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, object_ty: Type<'db>, attribute: &str, ) -> Option> { @@ -673,14 +788,18 @@ pub(super) fn assignment_attribute_members<'db>( object_ty, Type::KnownInstance(KnownInstanceType::FunctoolsPartial(_)) ) { - object_ty.member(db, attribute) + object_ty.member(db, env, attribute) + } else if let Type::ProtocolInstance(protocol) = object_ty + && let Some(origin) = protocol.materialized_origin_property(db, attribute) + { + Type::instance(db, env, *origin).class_member(db, env, attribute) } else { - object_ty.class_member(db, attribute) + object_ty.class_member(db, env, attribute) }; - if let Some(receiver_member) = - class_member_preceding_non_data_metaclass_member(db, object_ty, attribute, type_member) + if let Some(members) = + class_object_assignment_members(db, env, object_ty, attribute, type_member) { - return Some(AssignmentAttributeMembers::ReceiverMember(receiver_member)); + return Some(members); } let needs_receiver_fallback = matches!( type_member.place, @@ -715,9 +834,9 @@ pub(super) fn assignment_attribute_members<'db>( | Type::Restricted(_) | Type::Deferred(_) | Type::TypedDict(_) - | Type::NewTypeInstance(_) => object_ty.instance_member(db, attribute), + | Type::NewTypeInstance(_) => object_ty.instance_member(db, env, attribute), Type::ClassLiteral(..) | Type::GenericAlias(..) | Type::SubclassOf(..) => { - object_ty.class_object_member(db, attribute, MemberLookupPolicy::default()) + object_ty.class_object_member(db, env, attribute, MemberLookupPolicy::default()) } Type::Union(..) | Type::Intersection(..) diff --git a/crates/ty_python_semantic/src/types/bool.rs b/crates/ty_python_semantic/src/types/bool.rs index 1201cbbc74..aa68539a59 100644 --- a/crates/ty_python_semantic/src/types/bool.rs +++ b/crates/ty_python_semantic/src/types/bool.rs @@ -1,14 +1,13 @@ +use crate::Db; +use crate::ProgramEnvironment; use ruff_db::diagnostic::{Annotation, SubDiagnostic, SubDiagnosticSeverity}; use ruff_text_size::{Ranged, TextRange}; -use crate::{ - Db, - types::{ - CallArguments, CallDunderError, ClassType, CycleDetector, KnownClass, KnownInstanceType, - LiteralValueTypeKind, SubclassOfInner, Type, TypeContext, TypeVarBoundOrConstraints, - UnionType, call::CallErrorKind, constraints::ConstraintSetBuilder, context::InferContext, - diagnostic::UNSUPPORTED_BOOL_CONVERSION, typed_dict::TypedDictField, - }, +use crate::types::{ + CallArguments, CallDunderError, ClassType, CycleDetector, KnownClass, KnownInstanceType, + LiteralValueTypeKind, SubclassOfInner, Type, TypeContext, TypeVarBoundOrConstraints, UnionType, + call::CallErrorKind, constraints::ConstraintSetBuilder, context::InferContext, + diagnostic::UNSUPPORTED_BOOL_CONVERSION, typed_dict::TypedDictField, }; use ty_python_core::Truthiness; @@ -18,9 +17,14 @@ impl<'db> Type<'db> { /// 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, &TryBoolVisitor::new(Ok(Truthiness::Ambiguous))) - .unwrap_or_else(|err| err.fallback_truthiness()) + pub(crate) fn bool(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Truthiness { + self.try_bool_impl( + db, + env, + true, + &TryBoolVisitor::new(Ok(Truthiness::Ambiguous)), + ) + .unwrap_or_else(|err| err.fallback_truthiness()) } /// Resolves the boolean value of a type. @@ -29,8 +33,17 @@ impl<'db> Type<'db> { /// 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, &TryBoolVisitor::new(Ok(Truthiness::Ambiguous))) + pub(crate) fn try_bool( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Result> { + self.try_bool_impl( + db, + env, + false, + &TryBoolVisitor::new(Ok(Truthiness::Ambiguous)), + ) } /// Resolves the boolean value of a type. @@ -48,6 +61,7 @@ impl<'db> Type<'db> { fn try_bool_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, allow_short_circuit: bool, visitor: &TryBoolVisitor<'db>, ) -> Result> { @@ -63,13 +77,15 @@ impl<'db> Type<'db> { let try_dunders = || { match self.try_call_dunder( db, + env, "__bool__", CallArguments::none(), TypeContext::default(), ) { Ok(outcome) => { - let return_type = outcome.return_type(db); - if !return_type.is_assignable_to(db, KnownClass::Bool.to_instance(db)) { + let return_type = outcome.return_type(db, env); + if !return_type.is_assignable_to(db, env, KnownClass::Bool.to_instance(db, env)) + { // The type has a `__bool__` method, but it doesn't return a // boolean. return Err(BoolError::IncorrectReturnType { @@ -83,12 +99,13 @@ impl<'db> Type<'db> { Err(CallDunderError::PossiblyUnbound { bindings: outcome, .. }) => { - let return_type = outcome.return_type(db); - if !return_type.is_assignable_to(db, KnownClass::Bool.to_instance(db)) { + let return_type = outcome.return_type(db, env); + if !return_type.is_assignable_to(db, env, KnownClass::Bool.to_instance(db, env)) + { // The type has a `__bool__` method, but it doesn't return a // boolean. return Err(BoolError::IncorrectReturnType { - return_type: outcome.return_type(db), + return_type: outcome.return_type(db, env), not_boolable_type: *self, }); } @@ -103,7 +120,7 @@ impl<'db> Type<'db> { // handling for tuples here isn't sound. Err(CallDunderError::MethodNotAvailable) if let Type::NominalInstance(instance) = self - && let Some(tuple_spec) = instance.tuple_spec(db) => + && let Some(tuple_spec) = instance.tuple_spec(db, env) => { Ok(tuple_spec.truthiness()) } @@ -113,19 +130,22 @@ impl<'db> Type<'db> { // and a subclass could add a `__bool__` method. Err(CallDunderError::MethodNotAvailable) if let Type::NominalInstance(instance) = self - && instance.class(db).is_final(db) => + && instance.class(db, env).is_final(db) => { match self.try_call_dunder( db, + env, "__len__", CallArguments::none(), TypeContext::default(), ) { Ok(outcome) => { - let return_type = outcome.return_type(db); - if return_type - .is_assignable_to(db, KnownClass::SupportsIndex.to_instance(db)) - { + let return_type = outcome.return_type(db, env); + if return_type.is_assignable_to( + db, + env, + KnownClass::SupportsIndex.to_instance(db, env), + ) { Ok(type_to_truthiness(return_type)) } else { // TODO: should report a diagnostic similar to if return type of `__bool__` @@ -145,7 +165,7 @@ impl<'db> Type<'db> { Err(CallDunderError::CallError(CallErrorKind::BindingError, bindings, _)) => { Err(BoolError::IncorrectArguments { - truthiness: type_to_truthiness(bindings.return_type(db)), + truthiness: type_to_truthiness(bindings.return_type(db, env)), not_boolable_type: *self, }) } @@ -171,7 +191,7 @@ impl<'db> Type<'db> { for element in union.elements(db) { let element_truthiness = - match element.try_bool_impl(db, allow_short_circuit, visitor) { + match element.try_bool_impl(db, env, allow_short_circuit, visitor) { Ok(truthiness) => truthiness, Err(err) => { has_errors = true; @@ -223,7 +243,7 @@ impl<'db> Type<'db> { Type::Restricted(restricted) => { restricted .value_type(db) - .try_bool_impl(db, allow_short_circuit, visitor)? + .try_bool_impl(db, env, allow_short_circuit, visitor)? } Type::TypedDict(td) => { @@ -240,8 +260,8 @@ impl<'db> Type<'db> { Type::KnownInstance(KnownInstanceType::ConstraintSet(tracked_set)) => { let constraints = ConstraintSetBuilder::new(); - let tracked_set = constraints.load(db, tracked_set.constraints(db)); - Truthiness::from(tracked_set.is_always_satisfied(db)) + let tracked_set = constraints.load(db, env, tracked_set.constraints(db)); + Truthiness::from(tracked_set.is_always_satisfied(db, env)) } Type::KnownInstance(KnownInstanceType::Range { is_non_empty }) => { @@ -263,36 +283,40 @@ impl<'db> Type<'db> { Type::AlwaysFalsy => Truthiness::AlwaysFalse, - Type::ClassLiteral(class) => { - class - .metaclass_instance_type(db) - .try_bool_impl(db, allow_short_circuit, visitor)? - } + Type::ClassLiteral(class) => class.metaclass_instance_type(db, env).try_bool_impl( + db, + env, + allow_short_circuit, + visitor, + )?, Type::GenericAlias(alias) => ClassType::from(*alias) - .metaclass_instance_type(db) - .try_bool_impl(db, allow_short_circuit, visitor)?, + .metaclass_instance_type(db, env) + .try_bool_impl(db, env, allow_short_circuit, visitor)?, Type::SubclassOf(subclass_of_ty) => { - match subclass_of_ty.subclass_of().with_transposed_type_var(db) { + match subclass_of_ty + .subclass_of() + .with_transposed_type_var(db, env) + { SubclassOfInner::Dynamic(_) => Truthiness::Ambiguous, SubclassOfInner::Class(class) => { - Type::from(class).try_bool_impl(db, allow_short_circuit, visitor)? + Type::from(class).try_bool_impl(db, env, allow_short_circuit, visitor)? } SubclassOfInner::Protocol(_) => Truthiness::Ambiguous, SubclassOfInner::TypeVar(bound_typevar) => Type::TypeVar(bound_typevar) - .try_bool_impl(db, allow_short_circuit, visitor)?, + .try_bool_impl(db, env, allow_short_circuit, visitor)?, } } Type::TypeVar(bound_typevar) => { - match bound_typevar.typevar(db).bound_or_constraints(db) { + match bound_typevar.typevar(db).bound_or_constraints(db, env) { None => Truthiness::Ambiguous, Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - bound.try_bool_impl(db, allow_short_circuit, visitor)? + bound.try_bool_impl(db, env, allow_short_circuit, visitor)? } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints - .as_type(db) - .try_bool_impl(db, allow_short_circuit, visitor)?, + .as_type(db, env) + .try_bool_impl(db, env, allow_short_circuit, visitor)?, } } @@ -309,12 +333,12 @@ impl<'db> Type<'db> { // Which materialization this is, is unknown, so the truthiness is only certain when // every materialization agrees: exactly the union face's answer. Type::UnsafeUnion(unsafe_union) => unsafe_union - .to_union(db) - .try_bool_impl(db, allow_short_circuit, visitor)?, + .to_union(db, env) + .try_bool_impl(db, env, allow_short_circuit, visitor)?, Type::Intersection(intersection) => { - if let Some(alternatives) = intersection.finite_alternative_union(db) { - alternatives.try_bool_impl(db, allow_short_circuit, visitor)? + if let Some(alternatives) = intersection.finite_alternative_union(db, env) { + alternatives.try_bool_impl(db, env, allow_short_circuit, visitor)? } else { // TODO Truthiness::Ambiguous @@ -322,14 +346,14 @@ impl<'db> Type<'db> { } Type::EnumComplement(complement) => complement - .remaining_literal_union(db) - .try_bool_impl(db, allow_short_circuit, visitor)?, + .remaining_literal_union(db, env) + .try_bool_impl(db, env, allow_short_circuit, visitor)?, Type::LiteralValue(literal) => match literal.kind() { LiteralValueTypeKind::LiteralString => Truthiness::Ambiguous, LiteralValueTypeKind::Enum(enum_type) => enum_type - .enum_class_instance(db) - .try_bool_impl(db, allow_short_circuit, visitor)?, + .enum_class_instance(db, env) + .try_bool_impl(db, env, allow_short_circuit, visitor)?, LiteralValueTypeKind::Int(num) => Truthiness::from(num.as_i64() != 0), LiteralValueTypeKind::Bool(bool) => Truthiness::from(bool), @@ -344,13 +368,14 @@ impl<'db> Type<'db> { Type::TypeAlias(alias) => visitor.visit(db, *self, || { alias .value_type(db) - .try_bool_impl(db, allow_short_circuit, visitor) + .try_bool_impl(db, env, allow_short_circuit, visitor) })?, - Type::NewTypeInstance(newtype) => { - newtype - .concrete_base_type(db) - .try_bool_impl(db, allow_short_circuit, visitor)? - } + Type::NewTypeInstance(newtype) => newtype.concrete_base_type(db).try_bool_impl( + db, + env, + allow_short_circuit, + visitor, + )?, }; Ok(truthiness) @@ -358,9 +383,9 @@ impl<'db> Type<'db> { } /// A [`CycleDetector`] that is used in `try_bool` methods. -pub(crate) type TryBoolVisitor<'db> = +type TryBoolVisitor<'db> = CycleDetector<'db, TryBool, Type<'db>, Result>, 3>; -pub(crate) struct TryBool; +struct TryBool; #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum BoolError<'db> { @@ -425,24 +450,26 @@ impl<'db> BoolError<'db> { } fn report_diagnostic_impl(&self, context: &InferContext, condition: TextRange) { + let db = context.db(); let Some(builder) = context.report_lint(&UNSUPPORTED_BOOL_CONVERSION, condition) else { return; }; + let env = context.program_environment(); match self { Self::IncorrectArguments { not_boolable_type, .. } => { let mut diag = builder.into_diagnostic(format_args!( "Boolean conversion is not supported for type `{}`", - not_boolable_type.display(context.db()) + not_boolable_type.display(db, env) )); let mut sub = SubDiagnostic::new( SubDiagnosticSeverity::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(context.db()) + .member(db, env, "__bool__") + .into_lookup_result(db, env) .ok() .and_then(|quals| quals.inner_type().parameter_span(context.db(), None)) { @@ -459,18 +486,18 @@ impl<'db> BoolError<'db> { } => { let mut diag = builder.into_diagnostic(format_args!( "Boolean conversion is not supported for type `{not_boolable}`", - not_boolable = not_boolable_type.display(context.db()), + not_boolable = not_boolable_type.display(db, env), )); let mut sub = SubDiagnostic::new( SubDiagnosticSeverity::Info, format_args!( "`{return_type}` is not assignable to `bool`", - return_type = return_type.display(context.db()), + return_type = return_type.display(db, env), ), ); if let Some((func_span, return_type_span)) = not_boolable_type - .member(context.db(), "__bool__") - .into_lookup_result(context.db()) + .member(db, env, "__bool__") + .into_lookup_result(db, env) .ok() .and_then(|quals| quals.inner_type().function_spans(context.db())) .and_then(|spans| Some((spans.name, spans.return_type?))) @@ -485,13 +512,13 @@ impl<'db> BoolError<'db> { Self::NotCallable { not_boolable_type } => { let mut diag = builder.into_diagnostic(format_args!( "Boolean conversion is not supported for type `{}`", - not_boolable_type.display(context.db()) + not_boolable_type.display(db, env) )); let sub = SubDiagnostic::new( SubDiagnosticSeverity::Info, format_args!( "`__bool__` on `{}` must be callable", - not_boolable_type.display(context.db()) + not_boolable_type.display(db, env) ), ); // TODO: It would be nice to create an annotation here for @@ -503,14 +530,14 @@ impl<'db> BoolError<'db> { let first_error = union .elements(context.db()) .iter() - .find_map(|element| element.try_bool(context.db()).err()) + .find_map(|element| element.try_bool(db, env).err()) .unwrap(); builder.into_diagnostic(format_args!( "Boolean conversion is not supported for union `{}` \ because `{}` doesn't implement `__bool__` correctly", - Type::Union(*union).display(context.db()), - first_error.not_boolable_type().display(context.db()), + Type::Union(*union).display(db, env), + first_error.not_boolable_type().display(db, env), )); } @@ -518,7 +545,7 @@ impl<'db> BoolError<'db> { builder.into_diagnostic(format_args!( "Boolean conversion is not supported for type `{}`; \ it incorrectly implements `__bool__`", - not_boolable_type.display(context.db()) + not_boolable_type.display(db, env) )); } } diff --git a/crates/ty_python_semantic/src/types/bound_super.rs b/crates/ty_python_semantic/src/types/bound_super.rs index 919fb42e69..b3bc1c5128 100644 --- a/crates/ty_python_semantic/src/types/bound_super.rs +++ b/crates/ty_python_semantic/src/types/bound_super.rs @@ -1,5 +1,6 @@ //! Logic for inferring `super()`, `super(x)` and `super(x, y)` calls. +use crate::ProgramEnvironment; use itertools::{Either, Itertools}; use ruff_db::diagnostic::Diagnostic; use ruff_python_ast::{AnyNodeRef, name::Name}; @@ -9,11 +10,13 @@ use crate::{ place::{Place, PlaceAndQualifiers}, types::{ BoundTypeVarInstance, ClassBase, ClassType, DivergentType, DynamicType, - IntersectionBuilder, KnownClass, MemberLookupPolicy, SpecialFormType, SubclassOfInner, - SubclassOfType, Type, TypeVarBoundOrConstraints, UnionBuilder, UnsafeUnionType, + IntersectionBuilder, KnownClass, MemberLookupErrorKind, MemberLookupPolicy, + MemberLookupResult, SpecialFormType, SubclassOfInner, SubclassOfType, Type, + TypeVarBoundOrConstraints, UnionBuilder, UnsafeUnionType, constraints::ConstraintSet, context::InferContext, diagnostic::{INVALID_SUPER_ARGUMENT, UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS}, + member_lookup_result, relation::EquivalenceChecker, signatures::{Parameter, Parameters, Signature}, typevar::{TypeVarConstraints, TypeVarInstance}, @@ -35,25 +38,30 @@ impl<'db> TypeVarOwnerContext<'db> { } } - fn has_implicit_upper_bound(self, db: &'db dyn Db) -> bool { - self.typevar(db).bound_or_constraints(db).is_none() + fn has_implicit_upper_bound(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool { + self.typevar(db).bound_or_constraints(db, env).is_none() } /// The bound or constraints of this typevar, as a type (i.e. constraints are unioned), wrapped /// in `SubclassOf` if this is a `SubclassOf` context. `object` if no bound/constraints. /// Used for error messages. - fn bound_or_constraints_type(self, db: &'db dyn Db) -> Type<'db> { + fn bound_or_constraints_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { match self { TypeVarOwnerContext::Bare(typevar) => typevar .typevar(db) - .require_bound_or_constraints(db) - .as_type(db), + .require_bound_or_constraints(db, env) + .as_type(db, env), TypeVarOwnerContext::SubclassOf(typevar) => SubclassOfType::try_from_instance( db, + env, typevar .typevar(db) - .require_bound_or_constraints(db) - .as_type(db), + .require_bound_or_constraints(db, env) + .as_type(db, env), ) .unwrap_or_else(SubclassOfType::subclass_of_unknown), } @@ -91,6 +99,7 @@ pub(crate) enum BoundSuperError<'db> { impl<'db> BoundSuperError<'db> { pub(super) fn report_diagnostic(&self, context: &'db InferContext<'db, '_>, node: AnyNodeRef) { + let db = context.db(); match self { BoundSuperError::AbstractOwnerType { owner_type, @@ -98,43 +107,46 @@ impl<'db> BoundSuperError<'db> { typevar_context, } => { if let Some(builder) = context.report_lint(&INVALID_SUPER_ARGUMENT, node) { + let env = context.program_environment(); if let Some(typevar_context) = typevar_context { let mut diagnostic = builder.into_diagnostic(format_args!( - "`{owner}` is a type variable with an abstract/structural type as \ - its bounds or constraints, in `super({pivot_class}, {owner})` call", - pivot_class = pivot_class.display(context.db()), - owner = owner_type.display(context.db()), + "`{owner}` is a type variable \ + with an abstract/structural type as its bounds or constraints, \ + in `super({pivot_class}, {owner})` call", + pivot_class = pivot_class.display(db, env), + owner = owner_type.display(db, env), )); - Self::describe_typevar(context.db(), &mut diagnostic, *typevar_context); + Self::describe_typevar(db, env, &mut diagnostic, *typevar_context); } else { builder.into_diagnostic(format_args!( "`{owner}` is an abstract/structural type in \ `super({pivot_class}, {owner})` call", - pivot_class = pivot_class.display(context.db()), - owner = owner_type.display(context.db()), + pivot_class = pivot_class.display(db, env), + owner = owner_type.display(db, env), )); } } } BoundSuperError::InvalidPivotClassType { pivot_class } => { if let Some(builder) = context.report_lint(&INVALID_SUPER_ARGUMENT, node) { + let env = context.program_environment(); match pivot_class { Type::GenericAlias(alias) => { builder.into_diagnostic(format_args!( "`types.GenericAlias` instance `{}` is not a valid class", - alias.display_with(context.db(), DisplaySettings::default()), + alias.display_with(db, env, DisplaySettings::default(),), )); } _ => { let mut diagnostic = builder.into_diagnostic("Argument is not a valid class"); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Argument has type `{}`", - pivot_class.display(context.db()) + pivot_class.display(db, env) )); diagnostic.set_concise_message(format_args!( "`{}` is not a valid class", - pivot_class.display(context.db()), + pivot_class.display(db, env), )); } } @@ -146,22 +158,25 @@ impl<'db> BoundSuperError<'db> { typevar_context, } => { if let Some(builder) = context.report_lint(&INVALID_SUPER_ARGUMENT, node) { + let env = context.program_environment(); let mut diagnostic = 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()), + pivot_class = pivot_class.display(db, env), + owner = owner.display(db, env), )); if let Some(typevar_context) = typevar_context { - Self::describe_typevar(context.db(), &mut diagnostic, *typevar_context); + Self::describe_typevar(db, env, &mut diagnostic, *typevar_context); diagnostic.info(format_args!( - "`{bounds_or_constraints}` is not an instance or subclass of `{pivot_class}`", - bounds_or_constraints = - typevar_context.bound_or_constraints_type(context.db()).display(context.db()), - pivot_class = pivot_class.display(context.db()), + "`{bounds_or_constraints}` is not an instance or subclass of \ + `{pivot_class}`", + bounds_or_constraints = typevar_context + .bound_or_constraints_type(db, env) + .display(db, env), + pivot_class = pivot_class.display(db, env), )); let typevar = typevar_context.typevar(context.db()); - if typevar_context.has_implicit_upper_bound(context.db()) { + if typevar_context.has_implicit_upper_bound(db, env) { diagnostic.help(format_args!( "Consider adding an upper bound to type variable `{}`", typevar.name(context.db()) @@ -186,11 +201,12 @@ impl<'db> BoundSuperError<'db> { /// and return the type variable's upper bound or the union of its constraints. fn describe_typevar( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, diagnostic: &mut Diagnostic, type_var_context: TypeVarOwnerContext<'db>, ) -> Type<'db> { let type_var = type_var_context.typevar(db); - match type_var_context.typevar(db).bound_or_constraints(db) { + match type_var_context.typevar(db).bound_or_constraints(db, env) { None => { diagnostic.info(format_args!( "Type variable `{}` has `object` as its implicit upper bound", @@ -202,7 +218,7 @@ impl<'db> BoundSuperError<'db> { diagnostic.info(format_args!( "Type variable `{}` has upper bound `{}`", type_var.name(db), - bound.display(db) + bound.display(db, env) )); bound } @@ -213,10 +229,10 @@ impl<'db> BoundSuperError<'db> { constraints .elements(db) .iter() - .map(|c| c.display(db)) + .map(|c| c.display(db, env)) .join(", ") )); - constraints.as_type(db) + constraints.as_type(db, env) } } } @@ -262,25 +278,30 @@ impl<'db> ResolvedSuperOwner<'db> { fn recursive_type_normalized_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { Some(Self { owner_type: self .owner_type - .recursive_type_normalized_impl(db, div, nested)?, + .recursive_type_normalized_impl(db, env, div, nested)?, lookup_anchor: self .lookup_anchor - .recursive_type_normalized_impl(db, div, nested)?, + .recursive_type_normalized_impl(db, env, div, nested)?, receiver: self.receiver, }) } - fn descriptor_binding(&self, db: &'db dyn Db) -> (Option>, Type<'db>) { + fn descriptor_binding( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> (Option>, Type<'db>) { match self.receiver { DescriptorReceiverKind::Class => (None, self.owner_type), DescriptorReceiverKind::Instance => { - (Some(self.owner_type), self.owner_type.to_meta_type(db)) + (Some(self.owner_type), self.owner_type.to_meta_type(db, env)) } } } @@ -297,6 +318,7 @@ impl<'db> SuperOwnerKind<'db> { fn recursive_type_normalized_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -306,18 +328,22 @@ impl<'db> SuperOwnerKind<'db> { } SuperOwnerKind::Divergent(_) => Some(*self), SuperOwnerKind::Resolved(resolved_owner) => Some(SuperOwnerKind::Resolved( - resolved_owner.recursive_type_normalized_impl(db, div, nested)?, + resolved_owner.recursive_type_normalized_impl(db, env, div, nested)?, )), } } - fn iter_mro(&self, db: &'db dyn Db) -> impl Iterator> + Clone { + fn iter_mro( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> impl Iterator> + Clone { match self { SuperOwnerKind::Dynamic(dynamic) => { - Either::Left(ClassBase::Dynamic(*dynamic).mro(db, None)) + Either::Left(ClassBase::Dynamic(*dynamic).mro(db, env, None)) } SuperOwnerKind::Divergent(divergent) => { - Either::Left(ClassBase::Divergent(*divergent).mro(db, None)) + Either::Left(ClassBase::Divergent(*divergent).mro(db, env, None)) } SuperOwnerKind::Resolved(resolved_owner) => { Either::Right(resolved_owner.lookup_anchor.iter_mro(db)) @@ -334,10 +360,16 @@ impl<'db> SuperOwnerKind<'db> { } } - fn descriptor_binding(self, db: &'db dyn Db) -> Option<(Option>, Type<'db>)> { + fn descriptor_binding( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option<(Option>, Type<'db>)> { match self { SuperOwnerKind::Dynamic(_) | SuperOwnerKind::Divergent(_) => None, - SuperOwnerKind::Resolved(resolved_owner) => Some(resolved_owner.descriptor_binding(db)), + SuperOwnerKind::Resolved(resolved_owner) => { + Some(resolved_owner.descriptor_binding(db, env)) + } } } } @@ -361,8 +393,12 @@ pub(super) fn walk_bound_super_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( ) { visitor.visit_type(db, Type::from(bound_super.pivot_class(db))); match bound_super.owner(db) { - SuperOwnerKind::Dynamic(dynamic) => visitor.visit_type(db, Type::Dynamic(dynamic)), - SuperOwnerKind::Divergent(divergent) => visitor.visit_type(db, Type::Divergent(divergent)), + SuperOwnerKind::Dynamic(dynamic) => { + visitor.visit_type(db, Type::Dynamic(dynamic)); + } + SuperOwnerKind::Divergent(divergent) => { + visitor.visit_type(db, Type::Divergent(divergent)); + } SuperOwnerKind::Resolved(resolved_owner) => { visitor.visit_type(db, resolved_owner.owner_type); visitor.visit_type(db, Type::from(resolved_owner.lookup_anchor)); @@ -486,21 +522,24 @@ impl<'db> BoundSuperType<'db> { /// However, the checking is skipped when any of the arguments is a dynamic type. pub(crate) fn build( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, pivot_class_type: Type<'db>, owner_type: Type<'db>, ) -> Result, BoundSuperError<'db>> { // basedpython: a hole nothing bounded is the gradual type it replaced, and `super()` // skips its checks entirely when an argument is gradual - if let Some(gradual) = crate::types::inferred_signature::gradual_hole(db, pivot_class_type) + if let Some(gradual) = + crate::types::inferred_signature::gradual_hole(db, env, pivot_class_type) { - return BoundSuperType::build(db, gradual, owner_type); + return BoundSuperType::build(db, env, gradual, owner_type); } - if let Some(gradual) = crate::types::inferred_signature::gradual_hole(db, owner_type) { - return BoundSuperType::build(db, pivot_class_type, gradual); + if let Some(gradual) = crate::types::inferred_signature::gradual_hole(db, env, owner_type) { + return BoundSuperType::build(db, env, pivot_class_type, gradual); } - let delegate_to = - |type_to_delegate_to| BoundSuperType::build(db, pivot_class_type, type_to_delegate_to); + let delegate_to = |type_to_delegate_to| { + BoundSuperType::build(db, env, pivot_class_type, type_to_delegate_to) + }; // Delegate but rewrite errors to preserve TypeVar context. let delegate_with_error_mapped = @@ -542,7 +581,7 @@ impl<'db> BoundSuperType<'db> { Type::ClassLiteral(class) => ClassBase::Class(ClassType::NonGeneric(class)), Type::SubclassOf(subclass_of) => match subclass_of.subclass_of() { SubclassOfInner::Dynamic(dynamic) => ClassBase::Dynamic(dynamic), - _ => match subclass_of.subclass_of().into_class(db) { + _ => match subclass_of.subclass_of().into_class(db, env) { Some(class) => ClassBase::Class(class), None => { return Err(BoundSuperError::InvalidPivotClassType { @@ -568,10 +607,10 @@ impl<'db> BoundSuperType<'db> { let build_constrained_union = |constraints: TypeVarConstraints<'db>, typevar: TypeVarOwnerContext<'db>| -> Result, BoundSuperError<'db>> { - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); for constraint in constraints.elements(db) { let class = match constraint { - Type::NominalInstance(instance) => Some(instance.class(db)), + Type::NominalInstance(instance) => Some(instance.class(db, env)), _ => constraint.to_class_type(db), }; match class { @@ -642,13 +681,13 @@ impl<'db> BoundSuperType<'db> { SubclassOfInner::Dynamic(dynamic) => SuperOwnerKind::Dynamic(dynamic), SubclassOfInner::TypeVar(bound_typevar) => { let typevar = bound_typevar.typevar(db); - match typevar.bound_or_constraints(db) { + match typevar.bound_or_constraints(db, env) { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { let class = match bound { - Type::NominalInstance(instance) => Some(instance.class(db)), - Type::ProtocolInstance(protocol) => protocol - .to_nominal_instance() - .map(|instance| instance.class(db)), + Type::NominalInstance(instance) => Some(instance.class(db, env)), + Type::ProtocolInstance(protocol) => { + protocol.class_origin(db).map(|class| *class) + } _ => None, }; if let Some(class) = class { @@ -662,7 +701,7 @@ impl<'db> BoundSuperType<'db> { Some(TypeVarOwnerContext::SubclassOf(bound_typevar)), )?) } else { - let subclass_of = SubclassOfType::try_from_instance(db, bound) + let subclass_of = SubclassOfType::try_from_instance(db, env, bound) .unwrap_or_else(SubclassOfType::subclass_of_unknown); return delegate_with_error_mapped( subclass_of, @@ -684,7 +723,7 @@ impl<'db> BoundSuperType<'db> { pivot_class_type, owner_type, owner_type, - ClassType::object(db), + ClassType::object(db, env), Some(TypeVarOwnerContext::SubclassOf(bound_typevar)), )?) } @@ -697,19 +736,19 @@ impl<'db> BoundSuperType<'db> { pivot_class, pivot_class_type, owner_type, - instance.class(db), + instance.class(db, env), None, )?) } Type::ProtocolInstance(protocol) => { - if let Some(nominal_instance) = protocol.to_nominal_instance() { + if let Some(class) = protocol.class_origin(db) { SuperOwnerKind::Resolved(Self::resolve_instance_super_owner( db, pivot_class, pivot_class_type, owner_type, - nominal_instance.class(db), + *class, None, )?) } else { @@ -725,18 +764,18 @@ impl<'db> BoundSuperType<'db> { return Ok(union .elements(db) .iter() - .try_fold(UnionBuilder::new(db), |builder, element| { + .try_fold(UnionBuilder::new(db, env), |builder, element| { delegate_to(*element).map(|ty| builder.add(ty)) })? .build()); } Type::Intersection(intersection) => { - let mut builder = IntersectionBuilder::new(db); + let mut builder = IntersectionBuilder::new(db, env); let mut one_good_element_found = false; for positive in intersection.positive(db) { if let Ok(good_element) = delegate_to(*positive) { one_good_element_found = true; - builder = builder.add_positive(good_element); + builder.add_positive_in_place(good_element); } } if !one_good_element_found { @@ -748,7 +787,7 @@ impl<'db> BoundSuperType<'db> { } for negative in intersection.negative(db) { if let Ok(good_element) = delegate_to(*negative) { - builder = builder.add_negative(good_element); + builder.add_negative_in_place(good_element); } } return Ok(builder.build()); @@ -772,29 +811,29 @@ impl<'db> BoundSuperType<'db> { return Ok(UnsafeUnionType::from_elements(db, elements)); } Type::EnumComplement(complement) => { - return delegate_to(complement.to_intersection(db)); + return delegate_to(complement.to_intersection(db, env)); } Type::TypeAlias(alias) => { return delegate_to(alias.value_type(db)); } Type::Overlapping(overlapping) => { - return delegate_to(overlapping.value_type(db)); + return delegate_to(overlapping.value_type(db, env)); } Type::Restricted(restricted) => { return delegate_to(restricted.value_type(db)); } Type::Deferred(deferred) => { - return delegate_to(deferred.reduced(db)); + return delegate_to(deferred.reduced(db, env)); } Type::TypeVar(bound_typevar) => { let typevar = bound_typevar.typevar(db); - match typevar.bound_or_constraints(db) { + match typevar.bound_or_constraints(db, env) { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { let class = match bound { - Type::NominalInstance(instance) => Some(instance.class(db)), - Type::ProtocolInstance(protocol) => protocol - .to_nominal_instance() - .map(|instance| instance.class(db)), + Type::NominalInstance(instance) => Some(instance.class(db, env)), + Type::ProtocolInstance(protocol) => { + protocol.class_origin(db).map(|class| *class) + } _ => None, }; if let Some(class) = class { @@ -826,59 +865,68 @@ impl<'db> BoundSuperType<'db> { pivot_class, pivot_class_type, owner_type, - ClassType::object(db), + ClassType::object(db, env), Some(TypeVarOwnerContext::Bare(bound_typevar)), )?) } } } Type::TypeIs(_) | Type::TypeGuard(_) => { - return delegate_to(KnownClass::Bool.to_instance(db)); + return delegate_to(KnownClass::Bool.to_instance(db, env)); + } + Type::LiteralValue(literal) => { + return delegate_to(literal.fallback_instance(db, env)); } - Type::LiteralValue(literal) => return delegate_to(literal.fallback_instance(db)), Type::SpecialForm(special_form) => { - return delegate_to(special_form.instance_fallback(db)); + return delegate_to(special_form.instance_fallback(db, env)); } Type::KnownInstance(instance) => { - return delegate_to(instance.instance_fallback(db)); + return delegate_to(instance.instance_fallback(db, env)); } Type::FunctionLiteral(_) | Type::DataclassDecorator(_) => { - return delegate_to(KnownClass::FunctionType.to_instance(db)); + return delegate_to(KnownClass::FunctionType.to_instance(db, env)); } Type::WrapperDescriptor(_) => { - return delegate_to(KnownClass::WrapperDescriptorType.to_instance(db)); + return delegate_to(KnownClass::WrapperDescriptorType.to_instance(db, env)); } Type::KnownBoundMethod(method) => { - return delegate_to(method.class().to_instance(db)); + return delegate_to(method.class().to_instance(db, env)); + } + Type::BoundMethod(_) => { + return delegate_to(KnownClass::MethodType.to_instance(db, env)); } - Type::BoundMethod(_) => return delegate_to(KnownClass::MethodType.to_instance(db)), Type::ModuleLiteral(_) => { - return delegate_to(KnownClass::ModuleType.to_instance(db)); + return delegate_to(KnownClass::ModuleType.to_instance(db, env)); + } + Type::GenericAlias(_) => { + return delegate_to(KnownClass::GenericAlias.to_instance(db, env)); } - Type::GenericAlias(_) => return delegate_to(KnownClass::GenericAlias.to_instance(db)), Type::PropertyInstance(property) => { - return delegate_to(property.instance_fallback(db)); + return delegate_to(property.instance_fallback(db, env)); + } + Type::BoundSuper(_) => { + return delegate_to(KnownClass::Super.to_instance(db, env)); } - Type::BoundSuper(_) => return delegate_to(KnownClass::Super.to_instance(db)), Type::TypedDict(td) => { // In general it isn't sound to upcast a `TypedDict` to a `dict`, // but here it seems like it's probably sound? - let mut key_builder = UnionBuilder::new(db); - let mut value_builder = UnionBuilder::new(db); + let mut key_builder = UnionBuilder::new(db, env); + let mut value_builder = UnionBuilder::new(db, env); for (name, field) in td.items(db) { key_builder = key_builder.add(Type::string_literal(db, name)); value_builder = value_builder.add(field.declared_ty); } - return delegate_to( - KnownClass::Dict - .to_specialized_instance(db, &[key_builder.build(), value_builder.build()]), - ); + return delegate_to(KnownClass::Dict.to_specialized_instance( + db, + env, + &[key_builder.build(), value_builder.build()], + )); } Type::NewTypeInstance(newtype) => { return delegate_to(newtype.concrete_base_type(db)); } Type::Callable(callable) if callable.is_function_like(db) => { - return delegate_to(KnownClass::FunctionType.to_instance(db)); + return delegate_to(KnownClass::FunctionType.to_instance(db, env)); } Type::AlwaysFalsy | Type::AlwaysTruthy @@ -903,10 +951,11 @@ impl<'db> BoundSuperType<'db> { fn skip_until_after_pivot( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, mro_iter: impl Iterator> + Clone, ) -> impl Iterator> + Clone { let Some(pivot_class) = self.pivot_class(db).into_class() else { - return Either::Left(ClassBase::Dynamic(DynamicType::Unknown).mro(db, None)); + return Either::Left(ClassBase::Dynamic(DynamicType::Unknown).mro(db, env, None)); }; let mut pivot_found = false; @@ -932,10 +981,17 @@ impl<'db> BoundSuperType<'db> { pub(super) fn try_call_dunder_get_on_attribute( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, attribute: PlaceAndQualifiers<'db>, - ) -> Option> { - let (instance, owner) = self.owner(db).descriptor_binding(db)?; - Some(Type::try_call_dunder_get_on_attribute(db, attribute, instance, owner).0) + ) -> Option> { + let (instance, owner) = self.owner(db).descriptor_binding(db, env)?; + let (member, _, descriptor_error) = + Type::try_call_dunder_get_on_attribute(db, env, attribute, instance, owner); + Some(member_lookup_result( + db, + member, + descriptor_error.map(MemberLookupErrorKind::DescriptorGet), + )) } /// Similar to `Type::find_name_in_mro_with_policy`, but performs lookup starting *after* the @@ -943,6 +999,7 @@ impl<'db> BoundSuperType<'db> { pub(super) fn find_name_in_mro_after_pivot( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, policy: MemberLookupPolicy, ) -> PlaceAndQualifiers<'db> { @@ -950,20 +1007,21 @@ impl<'db> BoundSuperType<'db> { let class = match &owner { SuperOwnerKind::Dynamic(dynamic) => { return Type::Dynamic(*dynamic) - .find_name_in_mro_with_policy(db, name, policy) + .find_name_in_mro_with_policy(db, env, name, policy) .expect("Calling `find_name_in_mro` on dynamic type should return `Some`"); } SuperOwnerKind::Divergent(_) => { return Type::unknown() - .find_name_in_mro_with_policy(db, name, policy) + .find_name_in_mro_with_policy(db, env, name, policy) .expect("Calling `find_name_in_mro` on Unknown should return `Some`"); } SuperOwnerKind::Resolved(resolved_owner) => resolved_owner.lookup_anchor, }; - let mut mro_after_pivot = self.skip_until_after_pivot(db, owner.iter_mro(db)); + let mut mro_after_pivot = self.skip_until_after_pivot(db, env, owner.iter_mro(db, env)); let class_literal = class.class_literal(db); - let result = class_literal.class_member_from_mro(db, name, policy, mro_after_pivot.clone()); + let result = + class_literal.class_member_from_mro(db, env, name, policy, mro_after_pivot.clone()); // TODO: Here we are hard-coding that __class_getitem__ is the only member defined in // typing._Generic in the typeshed, and we are hard-coding its signature. Ideally we would @@ -988,15 +1046,16 @@ impl<'db> BoundSuperType<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { Some(Self::new( db, self.pivot_class(db) - .recursive_type_normalized_impl(db, div, nested)?, + .recursive_type_normalized_impl(db, env, div, nested)?, self.owner(db) - .recursive_type_normalized_impl(db, div, nested)?, + .recursive_type_normalized_impl(db, env, div, nested)?, )) } } @@ -1046,7 +1105,7 @@ impl<'c, 'db> EquivalenceChecker<'_, 'c, 'db> { } (ClassBase::TypedDict(_), _) => self.never(), }; - if class_equivalence.is_never_satisfied(db) { + if class_equivalence.is_trivially_never_satisfied() { return self.never(); } let owner_equivalence = match (left.owner(db), right.owner(db)) { diff --git a/crates/ty_python_semantic/src/types/call.rs b/crates/ty_python_semantic/src/types/call.rs index e740bfb52d..ef954c6497 100644 --- a/crates/ty_python_semantic/src/types/call.rs +++ b/crates/ty_python_semantic/src/types/call.rs @@ -4,12 +4,15 @@ use crate::Db; use crate::place::Provenance; use crate::types::call::bind::BindingError; use crate::types::{MemberLookupPolicy, PropertyInstanceType}; +use crate::{Program, ProgramEnvironment}; use ruff_python_ast as ast; mod arguments; pub(crate) mod bind; pub(super) use arguments::{Argument, CallArguments}; -pub(super) use bind::{Binding, Bindings, CallableBinding, MatchedArgument}; +pub(super) use bind::{ + Binding, Bindings, CallDiagnosticOverride, CallableBinding, MatchedArgument, +}; /// Whether the right operand's reflected method has priority based on the possible runtime /// classes of both operands. @@ -27,11 +30,15 @@ enum ReflectedMethodPriority { /// /// This is intentionally conservative: a false negative only widens a binary operation's result, /// while a false positive could discard a valid normal-method result. -fn has_exact_runtime_class<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { +fn has_exact_runtime_class<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> bool { match ty { Type::ClassLiteral(_) | Type::LiteralValue(_) => true, - Type::NominalInstance(instance) => instance.class(db).is_final(db), - Type::TypeAlias(alias) => has_exact_runtime_class(db, alias.value_type(db)), + Type::NominalInstance(instance) => instance.class(db, env).is_final(db), + Type::TypeAlias(alias) => has_exact_runtime_class(db, env, alias.value_type(db)), _ => false, } } @@ -40,10 +47,14 @@ fn has_exact_runtime_class<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { /// /// Instances dispatch through their nominal class, while class objects dispatch through their /// metaclass. -fn operator_dispatch_class<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option> { +fn operator_dispatch_class<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Option> { match ty { Type::ClassLiteral(class) => class.metaclass(db).to_class_type(db), - _ => ty.nominal_class(db), + _ => ty.nominal_class(db, env), } } @@ -63,6 +74,7 @@ fn operator_dispatch_class<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, left_ty: Type<'db>, right_ty: Type<'db>, ) -> ReflectedMethodPriority { @@ -71,17 +83,17 @@ fn reflected_method_priority<'db>( } if let (Some(left_class), Some(right_class)) = ( - operator_dispatch_class(db, left_ty), - operator_dispatch_class(db, right_ty), + operator_dispatch_class(db, env, left_ty), + operator_dispatch_class(db, env, right_ty), ) && left_class.class_literal(db) != right_class.class_literal(db) && right_class.is_subtype_of_class_literal(db, left_class.class_literal(db)) { - if has_exact_runtime_class(db, left_ty) { + if has_exact_runtime_class(db, env, left_ty) { ReflectedMethodPriority::Definitely } else { ReflectedMethodPriority::Possibly } - } else if right_ty.is_subtype_of(db, left_ty) { + } else if right_ty.is_subtype_of(db, env, left_ty) { ReflectedMethodPriority::Possibly } else { ReflectedMethodPriority::Never @@ -89,16 +101,66 @@ fn reflected_method_priority<'db>( } impl<'db> Type<'db> { + /// Return the result of dispatching a rich comparison method between two operands. + /// + /// A strict subclass on the right takes precedence over the normal method on the left. + /// The caller remains responsible for operator-specific fallbacks such as identity-based + /// equality when neither comparison method is available. + pub(super) fn try_call_rich_comparison_dunder( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + left: Type<'db>, + right: Type<'db>, + dunder: &'static str, + reflected_dunder: &'static str, + policy: MemberLookupPolicy, + ) -> Option> { + let call_dunder = |name, receiver: Type<'db>, argument: Type<'db>| { + receiver + .try_call_dunder_with_policy( + db, + env, + name, + &mut CallArguments::positional([argument]), + TypeContext::default(), + policy, + ) + .map(|outcome| outcome.return_type(db, env)) + .ok() + }; + + match reflected_method_priority(db, env, left, right) { + ReflectedMethodPriority::Never => call_dunder(dunder, left, right) + .or_else(|| call_dunder(reflected_dunder, right, left)), + ReflectedMethodPriority::Possibly => { + match ( + call_dunder(dunder, left, right), + call_dunder(reflected_dunder, right, left), + ) { + (Some(normal), Some(reflected)) => { + Some(UnionType::from_two_elements(db, env, normal, reflected)) + } + (Some(result), None) | (None, Some(result)) => Some(result), + (None, None) => None, + } + } + ReflectedMethodPriority::Definitely => call_dunder(reflected_dunder, right, left) + .or_else(|| call_dunder(dunder, left, right)), + } + } + /// Memoize the pure return-type part of binary dunder resolution so repeated identical /// expressions don't re-run overload selection at every call site. pub(crate) fn try_call_bin_op_return_type( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, left_ty: Type<'db>, op: ast::Operator, right_ty: Type<'db>, ) -> Option> { Self::try_call_bin_op_return_type_with_tcx( db, + env, left_ty, op, right_ty, @@ -111,21 +173,25 @@ impl<'db> Type<'db> { /// output parameter can widen to the assignment target — e.g. `x: list[int | None] = a + a`. pub(crate) fn try_call_bin_op_return_type_with_tcx( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, left_ty: Type<'db>, op: ast::Operator, right_ty: Type<'db>, tcx: TypeContext<'db>, ) -> Option> { - #[salsa::tracked(returns(copy), cycle_initial=|_, _, _, _, _, _| None, heap_size=ruff_memory_usage::heap_size)] + #[salsa::tracked(returns(copy), cycle_initial=|_, _, _, _, _, _, _| None, heap_size=ruff_memory_usage::heap_size)] fn try_call_bin_op_return_type_impl<'db>( db: &'db dyn Db, + program: Program<'db>, left_ty: Type<'db>, op: ast::Operator, right_ty: Type<'db>, tcx: TypeContext<'db>, ) -> Option> { + let env = &ProgramEnvironment::from_program(program); Type::try_call_bin_op_with_policy( db, + env, left_ty, op, right_ty, @@ -133,20 +199,22 @@ impl<'db> Type<'db> { MemberLookupPolicy::default(), ) .ok() - .map(|bindings| bindings.return_type(db)) + .map(|bindings| bindings.return_type(db, env)) } - try_call_bin_op_return_type_impl(db, left_ty, op, right_ty, tcx) + try_call_bin_op_return_type_impl(db, env.program(db), left_ty, op, right_ty, tcx) } pub(crate) fn try_call_bin_op( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, left_ty: Type<'db>, op: ast::Operator, right_ty: Type<'db>, ) -> Result, CallBinOpError> { Self::try_call_bin_op_with_policy( db, + env, left_ty, op, right_ty, @@ -157,6 +225,7 @@ impl<'db> Type<'db> { pub(crate) fn try_call_bin_op_with_policy( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, left_ty: Type<'db>, op: ast::Operator, right_ty: Type<'db>, @@ -176,21 +245,23 @@ impl<'db> Type<'db> { // Runtime classes determine reflected priority, but static operand types may only // establish that priority conditionally. - let reflected_priority = reflected_method_priority(db, left_ty, right_ty); + let reflected_priority = reflected_method_priority(db, env, left_ty, right_ty); - let left_class = left_ty.to_meta_type(db); - let right_class = right_ty.to_meta_type(db); + let left_class = left_ty.to_meta_type(db, env); + let right_class = right_ty.to_meta_type(db, env); if reflected_priority != ReflectedMethodPriority::Never { let reflected_dunder = op.reflected_dunder(); - let rhs_reflected = right_class.member(db, reflected_dunder).place; + let rhs_reflected = right_class.member(db, env, reflected_dunder).place; // TODO: if `rhs_reflected` is possibly unbound, we should union the two possible // Bindings together if !rhs_reflected.is_undefined() - && !rhs_reflected - .is_equal_ignoring_provenance(left_class.member(db, reflected_dunder).place) + && !rhs_reflected.is_equal_ignoring_provenance( + left_class.member(db, env, reflected_dunder).place, + ) { let call_on_right_instance = right_ty.try_call_dunder_with_policy( db, + env, reflected_dunder, &mut CallArguments::positional([left_ty]), tcx, @@ -201,6 +272,7 @@ impl<'db> Type<'db> { return Ok(call_on_right_instance.or_else(|_| { left_ty.try_call_dunder_with_policy( db, + env, op.dunder(), &mut CallArguments::positional([right_ty]), tcx, @@ -211,6 +283,7 @@ impl<'db> Type<'db> { let call_on_left_instance = left_ty.try_call_dunder_with_policy( db, + env, op.dunder(), &mut CallArguments::positional([right_ty]), tcx, @@ -221,6 +294,7 @@ impl<'db> Type<'db> { (Ok(right_bindings), Ok(left_bindings)) => { let callable_type = UnionType::from_two_elements( db, + env, right_bindings.callable_type(), left_bindings.callable_type(), ); @@ -237,6 +311,7 @@ impl<'db> Type<'db> { let call_on_left_instance = left_ty.try_call_dunder_with_policy( db, + env, op.dunder(), &mut CallArguments::positional([right_ty]), tcx, @@ -249,6 +324,7 @@ impl<'db> Type<'db> { } else { Ok(right_ty.try_call_dunder_with_policy( db, + env, op.reflected_dunder(), &mut CallArguments::positional([left_ty]), tcx, @@ -275,8 +351,26 @@ impl<'db> CallError<'db> { self.1 } - pub(crate) fn return_type(&self, db: &'db dyn Db) -> Type<'db> { - self.1.return_type(db) + pub(crate) fn return_type(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + self.1.return_type(db, env) + } + + /// Returns `Some(property)` if the call error was caused by an attempt to read a property + /// that has no getter, and `None` otherwise. + pub(crate) fn as_attempt_to_get_property_with_no_getter( + &self, + ) -> Option> { + if self.0 != CallErrorKind::BindingError { + return None; + } + self.1 + .iter_flat() + .flatten() + .flat_map(bind::Binding::errors) + .find_map(|error| match error { + BindingError::PropertyHasNoGetter(property) => Some(*property), + _ => None, + }) } /// Returns `Some(property)` if the call error was caused by an attempt to set a property @@ -314,6 +408,16 @@ impl<'db> CallError<'db> { _ => None, }) } + + pub(crate) fn report_diagnostics_with_override( + &self, + context: &InferContext<'db, '_>, + node: ast::AnyNodeRef, + overrides: &CallDiagnosticOverride<'_>, + ) { + self.1 + .report_diagnostics_with_override(context, node, overrides); + } } /// The reason why calling a type failed. @@ -373,16 +477,24 @@ impl<'db> CallDunderError<'db> { } } - pub(super) fn return_type(&self, db: &'db dyn Db) -> Option> { + pub(super) fn return_type( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { match self { Self::MethodNotAvailable | Self::CallError(CallErrorKind::NotCallable, _, _) => None, - Self::CallError(_, bindings, _) => Some(bindings.return_type(db)), - Self::PossiblyUnbound { bindings, .. } => Some(bindings.return_type(db)), + Self::CallError(_, bindings, _) => Some(bindings.return_type(db, env)), + Self::PossiblyUnbound { bindings, .. } => Some(bindings.return_type(db, env)), } } - pub(super) fn fallback_return_type(&self, db: &'db dyn Db) -> Type<'db> { - self.return_type(db).unwrap_or(Type::unknown()) + pub(super) fn fallback_return_type( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { + self.return_type(db, env).unwrap_or(Type::unknown()) } } diff --git a/crates/ty_python_semantic/src/types/call/arguments.rs b/crates/ty_python_semantic/src/types/call/arguments.rs index 19b83ebe56..75548124f6 100644 --- a/crates/ty_python_semantic/src/types/call/arguments.rs +++ b/crates/ty_python_semantic/src/types/call/arguments.rs @@ -1,3 +1,4 @@ +use crate::Db; use std::borrow::Cow; use std::fmt::Display; @@ -5,7 +6,7 @@ use itertools::{Either, Itertools}; use ruff_python_ast as ast; use rustc_hash::FxHashMap; -use crate::Db; +use crate::ProgramEnvironment; use crate::types::enums::enum_metadata; use crate::types::tuple::Tuple; use crate::types::typed_dict::extract_unpacked_typed_dict_keys_from_value_type; @@ -56,7 +57,7 @@ pub(crate) struct CallArgumentTypes<'db> { } impl<'db> CallArgumentTypes<'db> { - pub(crate) fn new(fallback_ty: Option>) -> Self { + fn new(fallback_ty: Option>) -> Self { Self { fallback_type: fallback_ty, types: FxHashMap::default(), @@ -89,7 +90,7 @@ impl<'db> CallArgumentTypes<'db> { } /// Insert the type of this argument when inferred with the provided type context. - pub(crate) fn insert(&mut self, tcx: impl Into>, ty: Type<'db>) { + fn insert(&mut self, tcx: impl Into>, ty: Type<'db>) { match tcx.into().annotation() { None => self.fallback_type = Some(ty), Some(tcx) => { @@ -98,7 +99,7 @@ impl<'db> CallArgumentTypes<'db> { } } - pub(crate) fn iter(&self) -> impl Iterator, Type<'db>)> { + fn iter(&self) -> impl Iterator, Type<'db>)> { self.types .iter() .map(|(tcx, ty)| (TypeContext::new(Some(*tcx)), *ty)) @@ -261,7 +262,7 @@ impl<'a, 'db> CallArguments<'a, 'db> { } /// Create a new [`CallArguments`] starting from the specified index. - pub(crate) fn start_from(&self, index: usize) -> Self { + fn start_from(&self, index: usize) -> Self { Self { items: self.items[index..].to_vec(), } @@ -291,6 +292,7 @@ impl<'a, 'db> CallArguments<'a, 'db> { pub(crate) fn functools_partial_bound_arguments( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ) -> Option<(Self, bool)> { let bound_call_arguments = self.start_from(1); let mut can_synthesize_signature = true; @@ -300,7 +302,7 @@ impl<'a, 'db> CallArguments<'a, 'db> { match argument { Argument::Variadic => { if !matches!( - argument_ty.tuple_instance_spec(db), + argument_ty.tuple_instance_spec(db, env), Some(spec) if spec.as_fixed_length().is_some() ) { return None; @@ -310,7 +312,7 @@ impl<'a, 'db> CallArguments<'a, 'db> { // Known `TypedDict` items can still be checked against their target // parameters, even though possible hidden items prevent us from synthesizing // a precise partial signature. - extract_unpacked_typed_dict_keys_from_value_type(db, argument_ty)?; + extract_unpacked_typed_dict_keys_from_value_type(db, env, argument_ty)?; can_synthesize_signature = false; } Argument::Positional | Argument::Synthetic | Argument::Keyword(_) => {} @@ -326,7 +328,11 @@ impl<'a, 'db> CallArguments<'a, 'db> { /// 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(super) fn expand(&self, db: &'db dyn Db) -> impl Iterator> + '_ { + pub(super) fn expand( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> impl Iterator> + '_ { /// Represents the state of the expansion process. enum State<'a, 'db> { LimitReached(usize), @@ -361,6 +367,7 @@ impl<'a, 'db> CallArguments<'a, 'db> { } } + let env = env.clone(); let mut index = 0; std::iter::successors( @@ -382,7 +389,7 @@ impl<'a, 'db> CallArguments<'a, 'db> { // this only shows up in very convoluted instances of generic call inference across multiple // overloads, and is unlikely to happen in practice. if let Some(arg_type) = arg_type.get_default() - && let Some(expanded_types) = expand_type(db, arg_type) + && let Some(expanded_types) = expand_type(db, &env, arg_type) { break expanded_types; } @@ -427,31 +434,39 @@ impl<'a, 'db> CallArguments<'a, 'db> { }) } - pub(super) fn display(&self, db: &'db dyn Db) -> impl Display { - struct DisplayCallArgumentTypes<'a, 'db> { + pub(super) fn display<'env>( + &'env self, + db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, + ) -> impl Display + 'env { + struct DisplayCallArgumentTypes<'env, 'a, 'db> { types: &'a CallArgumentTypes<'db>, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, } - impl std::fmt::Display for DisplayCallArgumentTypes<'_, '_> { + impl std::fmt::Display for DisplayCallArgumentTypes<'_, '_, '_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let db = self.db; + let env = self.env; f.debug_map() .entries(self.types.iter().map(|(tcx, ty)| { ( - tcx.annotation().as_ref().map(|ty| ty.display(self.db)), - ty.display(self.db), + tcx.annotation().as_ref().map(|ty| ty.display(db, env)), + ty.display(db, env), ) })) .finish() } } - struct DisplayCallArguments<'a, 'db> { + struct DisplayCallArguments<'env, 'a, 'db> { call_arguments: &'a CallArguments<'a, 'db>, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, } - impl std::fmt::Display for DisplayCallArguments<'_, '_> { + impl std::fmt::Display for DisplayCallArguments<'_, '_, '_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("(")?; for (index, (argument, types)) in self.call_arguments.iter().enumerate() { @@ -463,23 +478,55 @@ impl<'a, 'db> CallArguments<'a, 'db> { write!( f, "self: {}", - DisplayCallArgumentTypes { types, db: self.db } + DisplayCallArgumentTypes { + types, + db: self.db, + env: self.env, + } )?; } Argument::Positional => { - write!(f, "{}", DisplayCallArgumentTypes { types, db: self.db })?; + write!( + f, + "{}", + DisplayCallArgumentTypes { + types, + db: self.db, + env: self.env, + } + )?; } Argument::Variadic => { - write!(f, "*{}", DisplayCallArgumentTypes { types, db: self.db })?; + write!( + f, + "*{}", + DisplayCallArgumentTypes { + types, + db: self.db, + env: self.env, + } + )?; } Argument::Keyword(name) => write!( f, "{}={}", name, - DisplayCallArgumentTypes { types, db: self.db } + DisplayCallArgumentTypes { + types, + db: self.db, + env: self.env, + } )?, Argument::Keywords => { - write!(f, "**{}", DisplayCallArgumentTypes { types, db: self.db })?; + write!( + f, + "**{}", + DisplayCallArgumentTypes { + types, + db: self.db, + env: self.env, + } + )?; } } } @@ -490,6 +537,7 @@ impl<'a, 'db> CallArguments<'a, 'db> { DisplayCallArguments { call_arguments: self, db, + env, } } } @@ -533,27 +581,31 @@ impl<'a, 'db> FromIterator<(Argument<'a>, Option>)> for CallArguments< /// Returns `true` if the type can be expanded into its subtypes. /// /// In other words, it returns `true` if [`expand_type`] returns [`Some`] for the given type. -pub(crate) fn is_expandable_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { +pub(crate) fn is_expandable_type<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> bool { match ty { Type::EnumComplement(_) => true, - Type::Intersection(intersection) => intersection.finite_alternatives(db).is_some(), + Type::Intersection(intersection) => intersection.finite_alternatives(db, env).is_some(), Type::NominalInstance(instance) => { - let class = instance.class(db); + let class = instance.class(db, env); if class.is_known(db, KnownClass::Bool) { return true; } - if let Some(tuple_spec) = instance.tuple_spec(db) + if let Some(tuple_spec) = instance.tuple_spec(db, env) && let Tuple::Fixed(fixed_length_tuple) = &*tuple_spec && fixed_length_tuple .iter_all_elements() - .any(|element| is_expandable_type(db, element)) + .any(|element| is_expandable_type(db, env, element)) { return true; } enum_metadata(db, class.class_literal(db)).is_some() } Type::Union(_) => true, - Type::TypeAlias(alias) => is_expandable_type(db, alias.value_type(db)), + Type::TypeAlias(alias) => is_expandable_type(db, env, alias.value_type(db)), _ => false, } } diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 1622665b99..3c342b62a7 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -27,6 +27,7 @@ use self::constructor::{ConstructorBinding, ConstructorContext}; use super::{Argument, CallArguments, CallError, CallErrorKind, InferContext, Signature, Type}; use crate::db::Db; use crate::dunder_all::dunder_all_names; +use crate::lint::LintMetadata; use crate::place::{DefinedPlace, Definedness, Place}; use crate::subscript::PyIndex; use crate::types::TypedDictType; @@ -35,6 +36,7 @@ use crate::types::callable::CallableTypeKind; use crate::types::constraints::{ ConstraintSet, ConstraintSetBuilder, PathBound, PathBounds, Solutions, }; +use crate::types::context::LintDiagnosticGuardBuilder; use crate::types::context_params::{ContextResolution, resolve_context_argument}; use crate::types::dedicated::django; use crate::types::dedicated::pydantic::{self, ConfigBoolean}; @@ -50,8 +52,7 @@ use crate::types::function::{ OverloadLiteral, }; use crate::types::generics::{ - GenericContext, InferableTypeVars, Specialization, SpecializationBuilder, SpecializationError, - TypeVarInference, + GenericContext, Specialization, SpecializationBuilder, SpecializationError, TypeVarInference, }; use crate::types::infer::original_class_type; use crate::types::known_instance::{FieldInstance, InternedConstraintSetSolution}; @@ -74,7 +75,8 @@ enum KeywordAggregateKind { /// `**kwargs: Unpack[T]` where `T` is bounded by `TypedDict`. TypedDict, } -use crate::types::typevar::{BoundTypeVarIdentity, TypeVarKind, TypeVarNonceGenerator}; +use crate::types::ProgramEnvironment; +use crate::types::typevar::{BoundTypeVarIdentity, TypeVarKind, TypeVarNonceGenerator, TypeVarSet}; use crate::types::visitor::{ TypeCollector, TypeKind, TypeVisitor, any_over_type, walk_non_atomic_type, walk_type_with_recursion_guard, @@ -84,28 +86,83 @@ use crate::types::{ ClassLiteral, DATACLASS_FLAGS, DataclassFlags, DataclassParams, DynamicType, GenericAlias, InternedConstraintSet, IntersectionType, KnownBoundMethodType, KnownClass, KnownInstanceType, LiteralValueTypeKind, NominalInstanceType, PropertyInstanceType, SelfBinding, SpecialFormType, - TypeAliasType, TypeContext, TypeMapping, TypeVarBoundOrConstraints, TypeVarVariance, - UnionAccumulator, UnionBuilder, UnionType, UnsafeUnionType, WrapperDescriptorKind, enums, - list_members, + TypeContext, TypeMapping, TypeVarBoundOrConstraints, TypeVarVariance, UnionAccumulator, + UnionBuilder, UnionType, UnsafeUnionType, WrapperDescriptorKind, enums, list_members, }; -use crate::{DisplaySettings, FxOrderSet, Program}; +use crate::{DisplaySettings, FxOrderSet}; use ruff_db::diagnostic::{Annotation, Diagnostic, Span, SubDiagnostic, SubDiagnosticSeverity}; use ruff_python_ast::{self as ast, AnyNodeRef, ArgOrKeyword, PythonVersion}; use ty_python_core::scope::ScopeId; -use ty_python_core::semantic_index; +use ty_python_core::{ProgramFile, semantic_index}; pub(crate) use self::constructor::ConstructorCallableKind; +/// Overrides the lint and headline message for a call diagnostic emitted from an implicit call. +/// +/// The original call-error message is retained on the primary annotation if the call reporter +/// does not supply its own annotation message, or as an info sub-diagnostic otherwise. `info` +/// explains why the call happened. `argument_ranges` maps synthetic call arguments back to source +/// ranges. +pub(crate) struct CallDiagnosticOverride<'a> { + pub(crate) lint: &'static LintMetadata, + pub(crate) message: String, + pub(crate) info: &'a str, + pub(crate) argument_ranges: &'a [TextRange], +} + +struct CallDiagnosticContext<'context, 'overrides, 'db, 'ast> { + context: &'context InferContext<'db, 'ast>, + overrides: Option<&'context CallDiagnosticOverride<'overrides>>, + argument_index_offset: usize, +} + +impl<'db> CallDiagnosticContext<'_, '_, 'db, '_> { + fn report_lint<'env, T: Ranged>( + &'env self, + lint: &'static LintMetadata, + ranged: T, + ) -> Option> { + let lint = self.overrides.map_or(lint, |overrides| overrides.lint); + self.context.report_lint(lint, ranged).map(|builder| { + if let Some(overrides) = self.overrides { + builder.with_message_override(overrides.message.clone(), overrides.info) + } else { + builder + } + }) + } + + fn get_range(&self, node: ast::AnyNodeRef<'_>, argument_index: Option) -> TextRange { + let argument_index = argument_index.map(|index| index + self.argument_index_offset); + self.overrides + .and_then(|overrides| { + argument_index.and_then(|index| overrides.argument_ranges.get(index)) + }) + .copied() + .unwrap_or_else(|| BindingError::get_node(node, argument_index).range()) + } +} + +impl<'db, 'ast> std::ops::Deref for CallDiagnosticContext<'_, '_, 'db, 'ast> { + type Target = InferContext<'db, 'ast>; + + fn deref(&self) -> &Self::Target { + self.context + } +} + fn generic_contexts_mentioned_in_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, ) -> FxOrderSet> { - struct GenericContextCollector<'db> { + struct GenericContextCollector<'a, 'db> { + env: &'a ProgramEnvironment<'db>, generic_contexts: RefCell>>, recursion_guard: TypeCollector<'db>, } - impl<'db> GenericContextCollector<'db> { + impl<'db> GenericContextCollector<'_, 'db> { fn visit_signature(&self, db: &'db dyn Db, signature: &Signature<'db>) { if let Some(generic_context) = signature.generic_context { self.generic_contexts.borrow_mut().insert(generic_context); @@ -120,7 +177,11 @@ fn generic_contexts_mentioned_in_type<'db>( } } - impl<'db> TypeVisitor<'db> for GenericContextCollector<'db> { + impl<'db> TypeVisitor<'db> for GenericContextCollector<'_, 'db> { + fn program_environment(&self) -> &ProgramEnvironment<'db> { + self.env + } + fn should_visit_lazy_type_attributes(&self) -> bool { false } @@ -144,6 +205,7 @@ fn generic_contexts_mentioned_in_type<'db>( } let collector = GenericContextCollector { + env, generic_contexts: RefCell::default(), recursion_guard: TypeCollector::default(), }; @@ -153,6 +215,7 @@ fn generic_contexts_mentioned_in_type<'db>( fn freshen_generic_contexts_in_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, generic_contexts: FxOrderSet>, nonce_generator: &TypeVarNonceGenerator<'db>, @@ -162,6 +225,7 @@ fn freshen_generic_contexts_in_type<'db>( .fold(ty, |ty, generic_context| { ty.apply_type_mapping( db, + env, &TypeMapping::FreshenBoundTypeVars { generic_context, delta: nonce_generator.next().value(), @@ -173,17 +237,15 @@ fn freshen_generic_contexts_in_type<'db>( fn inferable_typevars_from_tuple<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, instance: &NominalInstanceType<'db>, -) -> Option> { - let typevars: Option> = instance - .tuple_spec(db)? +) -> Option> { + let typevars: Option> = instance + .tuple_spec(db, env)? .fixed_elements() - .map(|ty| { - ty.as_typevar() - .map(|bound_typevar| bound_typevar.identity(db)) - }) + .map(|ty| ty.as_typevar()) .collect(); - typevars.map(|typevars| InferableTypeVars::from_typevars(db, typevars)) + typevars.map(|typevars| TypeVarSet::from_typevars(db, typevars)) } /// Priority levels for call errors in intersection types. @@ -230,16 +292,17 @@ impl<'db> CallableItem<'db> { } } - fn return_type(&self, db: &'db dyn Db) -> Type<'db> { + fn return_type(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match self { CallableItem::Regular(binding) => binding.return_type(), - CallableItem::Constructor(binding) => binding.return_type(db), + CallableItem::Constructor(binding) => binding.return_type(db, env), } } fn check_types( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, constraints: &ConstraintSetBuilder<'db>, argument_types: &CallArguments<'_, 'db>, call_expression_tcx: TypeContext<'db>, @@ -247,18 +310,34 @@ impl<'db> CallableItem<'db> { ) { match self { CallableItem::Regular(binding) => { - binding.check_types(db, constraints, argument_types, call_expression_tcx); + binding.check_types(db, env, constraints, argument_types, call_expression_tcx); } CallableItem::Constructor(binding) => { - binding.check_types(db, constraints, argument_types, call_expression_tcx, mode); + binding.check_types( + db, + env, + constraints, + argument_types, + call_expression_tcx, + mode, + ); } } } - fn match_parameters(&mut self, db: &'db dyn Db, arguments: &CallArguments<'_, 'db>) { + fn match_parameters( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + arguments: &CallArguments<'_, 'db>, + ) { match self { - CallableItem::Regular(binding) => binding.match_parameters(db, arguments), - CallableItem::Constructor(binding) => binding.match_parameters(db, arguments), + CallableItem::Regular(binding) => { + binding.match_parameters(db, env, arguments); + } + CallableItem::Constructor(binding) => { + binding.match_parameters(db, env, arguments); + } } } @@ -291,15 +370,16 @@ impl<'db> CallableItem<'db> { fn freshen_generic_contexts_in_place( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, nonce_generator: &TypeVarNonceGenerator<'db>, ) { match self { CallableItem::Regular(binding) => { - binding.freshen_generic_contexts_in_place(db, nonce_generator); + binding.freshen_generic_contexts_in_place(db, env, nonce_generator); + } + CallableItem::Constructor(binding) => { + binding.freshen_generic_contexts_in_place(db, env, nonce_generator); } - // TODO: Constructor freshening also has to keep constructor instance context in sync - // with `__new__`/`__init__` signatures. - CallableItem::Constructor(_) => {} } } @@ -328,20 +408,18 @@ impl<'db> CallableItem<'db> { self.callable().is_callable() } - fn callable_type(&self) -> Type<'db> { - self.callable().callable_type - } - /// Returns the reduced callable synthesized from this callable item. fn functools_partial_callable<'a>( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, partial_overload: &mut Binding<'db>, bound_call_arguments: &CallArguments<'a, 'db>, ) -> Option> { match self { CallableItem::Regular(binding) => CallableType::partially_apply( db, + env, binding.partial_signature_applications( db, partial_overload, @@ -381,6 +459,10 @@ impl<'db> CallableItem<'db> { /// This could be a single callable or several callables combined by [`ItemCombination`]. #[derive(Debug, Clone)] struct BindingsElement<'db> { + /// The callable type associated with this union element. For an intersection, retain the + /// complete source type because its bindings can omit negative contributions or represent + /// constructor methods instead of the called class objects. + pub(crate) callable_type: Type<'db>, items: SmallVec<[CallableItem<'db>; 1]>, combination: ItemCombination, } @@ -401,12 +483,12 @@ enum ItemCombination { impl ItemCombination { /// Recombine per-item types (return types, callable types) the way this combination does. - fn combine<'db, I>(self, db: &'db dyn Db, types: I) -> Type<'db> + fn combine<'db, I>(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>, types: I) -> Type<'db> where I: IntoIterator>, { match self { - ItemCombination::Intersection => IntersectionType::from_elements(db, types), + ItemCombination::Intersection => IntersectionType::from_elements(db, env, types), ItemCombination::UnsafeUnion => UnsafeUnionType::from_elements(db, types), } } @@ -434,14 +516,15 @@ impl<'db> BindingsElement<'db> { self.items.len() > 1 } - fn return_type(&self, db: &'db dyn Db) -> Type<'db> { + fn return_type(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { if self.is_callable() { self.combination.combine( db, + env, self.items .iter() .filter(|item| item.is_callable()) - .map(|item| item.return_type(db)), + .map(|item| item.return_type(db, env)), ) } else { Type::unknown() @@ -452,13 +535,21 @@ impl<'db> BindingsElement<'db> { fn check_types( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, constraints: &ConstraintSetBuilder<'db>, call_arguments: &CallArguments<'_, 'db>, call_expression_tcx: TypeContext<'db>, mode: CheckTypesMode, ) { for item in &mut self.items { - item.check_types(db, constraints, call_arguments, call_expression_tcx, mode); + item.check_types( + db, + env, + constraints, + call_arguments, + call_expression_tcx, + mode, + ); } } @@ -490,11 +581,18 @@ impl<'db> BindingsElement<'db> { /// `f: KnownCallable & Top[Callable[..., Awaitable[object]]]`, even though the top-callable /// call itself is unsafe. (We know that somewhere in the infinite-union of the top callable, /// there is a callable with the right parameters to match the call.) + /// + /// Likewise, a narrowed class can provide a more specific constructor signature than `type[T]`. + /// Even when the `type[T]` constructor rejects the arguments, its return type still constrains + /// the successful constructor call. fn retain_successful(&mut self, db: &'db dyn Db) { if self.is_intersection() && self.as_result(db).is_ok() { self.items.retain(|item| { item.as_result(db).is_ok() || item.error_priority(db) == CallErrorPriority::TopCallable + || item.as_constructor().is_some_and(|constructor| { + matches!(constructor.constructed_instance_type(), Type::TypeVar(_)) + }) }); } } @@ -578,7 +676,7 @@ pub(crate) enum CheckTypesMode { } impl CheckTypesMode { - pub(crate) fn is_provisional(self) -> bool { + fn is_provisional(self) -> bool { matches!(self, Self::Provisional) } } @@ -758,6 +856,7 @@ impl<'db> Bindings<'db> { } assert!(!inner_items_acc.is_empty()); let elements = smallvec![BindingsElement { + callable_type, items: inner_items_acc, combination, }]; @@ -774,11 +873,25 @@ impl<'db> Bindings<'db> { if self.callable_type == before { self.callable_type = after; } - for binding in self.iter_flat_mut() { - binding.replace_callable_type(before, after); + for element in &mut self.elements { + if element.callable_type == before { + element.callable_type = after; + } + for binding in element.callables_mut() { + binding.replace_callable_type(before, after); + } } } + /// Set the overall receiver without replacing individual constructor callables. + pub(crate) fn with_callable_type(mut self, callable_type: Type<'db>) -> Self { + self.callable_type = callable_type; + for element in &mut self.elements { + element.callable_type = callable_type; + } + self + } + pub(crate) fn with_constructed_instance_type( mut self, db: &'db dyn Db, @@ -879,6 +992,7 @@ impl<'db> Bindings<'db> { pub(crate) fn resolve_context_arguments( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, scope: ScopeId<'db>, call_offset: ruff_text_size::TextSize, ) { @@ -886,7 +1000,7 @@ impl<'db> Bindings<'db> { let [binding] = callable_binding.overloads.as_mut_slice() else { continue; }; - binding.resolve_context_arguments(db, scope, call_offset); + binding.resolve_context_arguments(db, env, scope, call_offset); } } @@ -905,6 +1019,12 @@ impl<'db> Bindings<'db> { .filter_map(CallableItem::as_constructor) } + /// Return whether every callable uses ordinary constructor binding semantics. + pub(crate) fn has_only_constructor_items(&self) -> bool { + self.iter_callable_items() + .all(|item| item.as_constructor().is_some()) + } + fn iter_constructor_items_mut(&mut self) -> impl Iterator> { self.iter_callable_items_mut() .filter_map(CallableItem::as_constructor_mut) @@ -995,6 +1115,7 @@ impl<'db> Bindings<'db> { pub(crate) fn map_types( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, mut map: impl FnMut(&CallableBinding<'db>) -> Option>, ) -> Type<'db> { let mut element_types = Vec::with_capacity(self.elements.len()); @@ -1007,11 +1128,11 @@ impl<'db> Bindings<'db> { } if !binding_types.is_empty() { - element_types.push(IntersectionType::from_elements(db, binding_types)); + element_types.push(IntersectionType::from_elements(db, env, binding_types)); } } - UnionType::from_elements(db, element_types) + UnionType::from_elements(db, env, element_types) } /// Maps each `CallableItem` to a type and combines results while preserving @@ -1022,6 +1143,7 @@ impl<'db> Bindings<'db> { fn map_item_types( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, mut map: impl FnMut(&CallableItem<'db>) -> Option>, ) -> Type<'db> { let mut element_types = Vec::with_capacity(self.elements.len()); @@ -1034,31 +1156,33 @@ impl<'db> Bindings<'db> { } if !item_types.is_empty() { - element_types.push(IntersectionType::from_elements(db, item_types)); + element_types.push(IntersectionType::from_elements(db, env, item_types)); } } - UnionType::from_elements(db, element_types) + UnionType::from_elements(db, env, element_types) } /// Builds matched bindings for the callable wrapped by `functools.partial(...)`. /// /// This handles the shared partial-specific preprocessing (callable validation and argument /// normalization) used by both inference and known-call evaluation. - pub(crate) fn functools_partial_matched_bindings<'a>( + fn functools_partial_matched_bindings<'a>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, wrapped_callable_ty: Type<'db>, call_arguments: &CallArguments<'a, 'db>, ) -> Option<(CallArguments<'a, 'db>, Bindings<'db>, bool)> { // We can only infer bound-argument context from an actual callable. - wrapped_callable_ty.try_upcast_to_callable(db)?; + wrapped_callable_ty.try_upcast_to_callable(db, env)?; let (bound_call_arguments, can_synthesize_signature) = - call_arguments.functools_partial_bound_arguments(db)?; + call_arguments.functools_partial_bound_arguments(db, env)?; - let mut partial_bindings = wrapped_callable_ty - .bindings(db) - .match_parameters(db, &bound_call_arguments); + let mut partial_bindings = + wrapped_callable_ty + .bindings(db, env) + .match_parameters(db, env, &bound_call_arguments); for binding in partial_bindings.iter_flat_mut() { binding.clear_missing_argument_errors_for_partial_application(); } @@ -1082,14 +1206,15 @@ impl<'db> Bindings<'db> { fn functools_partial_type<'a>( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, wrapped_callable_ty: Type<'db>, partial_overload: &mut Binding<'db>, bound_call_arguments: &CallArguments<'a, 'db>, ) -> Type<'db> { if wrapped_callable_ty.is_union() || wrapped_callable_ty.is_intersection() { - return self.map_item_types(db, |partial_item| { + return self.map_item_types(db, env, |partial_item| { partial_item - .functools_partial_callable(db, partial_overload, bound_call_arguments) + .functools_partial_callable(db, env, partial_overload, bound_call_arguments) .map(|callable| { callable.into_precise_functools_partial_instance(db, wrapped_callable_ty) }) @@ -1099,7 +1224,12 @@ impl<'db> Bindings<'db> { let partial_callables: SmallVec<[CallableType<'db>; 1]> = self .iter_callable_items() .filter_map(|partial_item| { - partial_item.functools_partial_callable(db, partial_overload, bound_call_arguments) + partial_item.functools_partial_callable( + db, + env, + partial_overload, + bound_call_arguments, + ) }) .collect(); @@ -1124,6 +1254,7 @@ impl<'db> Bindings<'db> { .elements .into_iter() .map(|elem| BindingsElement { + callable_type: elem.callable_type, items: elem.items.into_iter().map(|item| item.map(f)).collect(), combination: elem.combination, }) @@ -1138,6 +1269,7 @@ impl<'db> Bindings<'db> { fn freshen_generic_contexts_in_place( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, nonce_generator: &TypeVarNonceGenerator<'db>, ) { let enclosing_binding_contexts = self.enclosing_binding_contexts.take(); @@ -1146,7 +1278,7 @@ impl<'db> Bindings<'db> { .record_enclosing_binding_contexts(enclosing_binding_contexts.iter().copied()); } for item in self.iter_callable_items_mut() { - item.freshen_generic_contexts_in_place(db, nonce_generator); + item.freshen_generic_contexts_in_place(db, env, nonce_generator); } self.enclosing_binding_contexts = enclosing_binding_contexts; } @@ -1163,17 +1295,23 @@ impl<'db> Bindings<'db> { pub(crate) fn match_parameters( mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, arguments: &CallArguments<'_, 'db>, ) -> Self { let nonce_generator = TypeVarNonceGenerator::default(); - self.freshen_generic_contexts_in_place(db, &nonce_generator); - self.match_parameters_in_place(db, arguments); + self.freshen_generic_contexts_in_place(db, env, &nonce_generator); + self.match_parameters_in_place(db, env, arguments); self } - fn match_parameters_in_place(&mut self, db: &'db dyn Db, arguments: &CallArguments<'_, 'db>) { + fn match_parameters_in_place( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + arguments: &CallArguments<'_, 'db>, + ) { for item in self.iter_callable_items_mut() { - item.match_parameters(db, arguments); + item.match_parameters(db, env, arguments); } } @@ -1192,6 +1330,7 @@ impl<'db> Bindings<'db> { pub(crate) fn check_types( mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, constraints: &ConstraintSetBuilder<'db>, call_arguments: &CallArguments<'_, 'db>, call_expression_tcx: TypeContext<'db>, @@ -1199,6 +1338,7 @@ impl<'db> Bindings<'db> { ) -> Result> { match self.check_types_impl( db, + env, constraints, call_arguments, call_expression_tcx, @@ -1210,9 +1350,11 @@ impl<'db> Bindings<'db> { } } + #[expect(clippy::too_many_arguments)] pub(crate) fn check_types_impl( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, constraints: &ConstraintSetBuilder<'db>, call_arguments: &CallArguments<'_, 'db>, call_expression_tcx: TypeContext<'db>, @@ -1221,7 +1363,14 @@ impl<'db> Bindings<'db> { ) -> Result<(), CallErrorKind> { // Check types for each element (union variant) for element in &mut self.elements { - element.check_types(db, constraints, call_arguments, call_expression_tcx, mode); + element.check_types( + db, + env, + constraints, + call_arguments, + call_expression_tcx, + mode, + ); } // Generic call inference must maintain a stable set of overloads until the final round @@ -1230,13 +1379,14 @@ impl<'db> Bindings<'db> { return Ok(()); } - self.evaluate_known_cases(db, call_arguments, dataclass_field_specifiers); + self.evaluate_known_cases(db, env, call_arguments, dataclass_field_specifiers); // For constructor bindings with deferred downstream checks: validate downstream bindings // if the matched overload is instance-returning. for constructor in self.iter_constructor_items_mut() { constructor.check_downstream_constructor( db, + env, constraints, call_arguments, call_expression_tcx, @@ -1258,17 +1408,19 @@ impl<'db> Bindings<'db> { pub(crate) fn finalize_argument_inference( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, call_arguments: &CallArguments<'_, 'db>, dataclass_field_specifiers: &[Type<'db>], ) -> Result<(), CallErrorKind> { - self.evaluate_known_cases(db, call_arguments, dataclass_field_specifiers); + self.evaluate_known_cases(db, env, call_arguments, dataclass_field_specifiers); for constructor in self.iter_constructor_items_mut() { - if constructor.discard_downstream_constructor(db) + if constructor.discard_downstream_constructor(db, env) && let Some(downstream) = constructor.downstream_constructor_mut() { let _ = downstream.finalize_argument_inference( db, + env, call_arguments, dataclass_field_specifiers, ); @@ -1317,10 +1469,13 @@ impl<'db> Bindings<'db> { /// 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> { + pub(crate) fn return_type(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { UnionType::from_elements( db, - self.elements.iter().map(|element| element.return_type(db)), + env, + self.elements + .iter() + .map(|element| element.return_type(db, env)), ) } @@ -1400,13 +1555,46 @@ impl<'db> Bindings<'db> { context: &InferContext<'db, '_>, node: ast::AnyNodeRef, ) { + self.report_diagnostics_impl( + &CallDiagnosticContext { + context, + overrides: None, + argument_index_offset: 0, + }, + node, + ); + } + + pub(crate) fn report_diagnostics_with_override( + &self, + context: &InferContext<'db, '_>, + node: ast::AnyNodeRef, + overrides: &CallDiagnosticOverride<'_>, + ) { + self.report_diagnostics_impl( + &CallDiagnosticContext { + context, + overrides: Some(overrides), + argument_index_offset: 0, + }, + node, + ); + } + + fn report_diagnostics_impl( + &self, + context: &CallDiagnosticContext<'_, '_, 'db, '_>, + node: ast::AnyNodeRef, + ) { + let db = context.db(); + let env = context.program_environment(); // If all elements are not callable, report that the type as a whole is not callable. if self.elements.iter().all(|e| !e.is_callable()) { let range = all_arguments_range(node); if let Some(builder) = context.report_lint(&CALL_NON_CALLABLE, range) { builder.into_diagnostic(format_args!( "Object of type `{}` is not callable", - self.callable_type().display(context.db()) + self.callable_type().display(db, env) )); } return; @@ -1438,7 +1626,7 @@ impl<'db> Bindings<'db> { if !reported_ctor_init_callables.insert(downstream_bindings.callable_type()) { continue; } - downstream_bindings.report_diagnostics(context, node); + downstream_bindings.report_diagnostics_impl(context, node); } } @@ -1446,7 +1634,7 @@ impl<'db> Bindings<'db> { /// If the element is an intersection where all bindings failed, use priority hierarchy. fn report_element_diagnostics( &self, - context: &InferContext<'db, '_>, + context: &CallDiagnosticContext<'_, '_, 'db, '_>, node: ast::AnyNodeRef, element: &BindingsElement<'db>, ) { @@ -1462,12 +1650,6 @@ impl<'db> Bindings<'db> { // Find the highest priority error among bindings in this element let max_priority = element.error_priority(context.db()); - // Reconstruct the combined callable type from the bindings - let intersection_type = element.combination.combine( - context.db(), - element.items.iter().map(CallableItem::callable_type), - ); - // Only report errors from bindings with the highest priority for item in &element.items { let binding = item.callable(); @@ -1479,7 +1661,7 @@ impl<'db> Bindings<'db> { // Use layered diagnostic for intersection inside a union let layered_diag = LayeredDiagnostic { union_callable_type: self.callable_type(), - intersection_callable_type: intersection_type, + intersection_callable_type: element.callable_type, binding, }; binding.report_diagnostics( @@ -1491,7 +1673,7 @@ impl<'db> Bindings<'db> { } else { // Just intersection, no union context needed let intersection_diag = IntersectionDiagnostic { - callable_type: intersection_type, + callable_type: element.callable_type, binding, }; binding.report_diagnostics( @@ -1515,7 +1697,7 @@ impl<'db> Bindings<'db> { } let union_diag = UnionDiagnostic { callable_type: self.callable_type(), - binding, + variant_type: element.callable_type, }; binding.report_diagnostics( context, @@ -1532,6 +1714,7 @@ impl<'db> Bindings<'db> { fn evaluate_known_cases( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, call_arguments: &CallArguments<'_, 'db>, dataclass_field_specifiers: &[Type<'db>], ) { @@ -1562,7 +1745,7 @@ impl<'db> Bindings<'db> { BoundMethodType::new( db, function, - instance.to_meta_type(db), + instance.to_meta_type(db, env), ), )); } @@ -1598,7 +1781,7 @@ impl<'db> Bindings<'db> { BoundMethodType::new( db, *function, - instance.to_meta_type(db), + instance.to_meta_type(db, env), ), )); } @@ -1652,58 +1835,43 @@ impl<'db> Bindings<'db> { Some(Type::PropertyInstance(property)), Some(Type::KnownInstance(KnownInstanceType::TypeVar(typevar))), .., - ] => { - match property - .getter(db) - .and_then(Type::as_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(Type::heterogeneous_tuple( - db, - typevar.constraints(db).into_iter().flatten(), - )); - } - Some("__default__") => { - overload.set_return_type( - typevar.default_type(db).unwrap_or_else(|| { - KnownClass::NoDefaultType.to_instance(db) - }), - ); - } - _ => {} + ] => match property.getter(db).and_then(Type::as_function_literal) { + Some(getter) if getter.name(db) == "__name__" => { + overload.set_return_type(Type::string_literal( + db, + typevar.name(db), + )); } - } + Some(getter) if getter.name(db) == "__bound__" => { + overload.set_return_type( + typevar + .upper_bound(db, env) + .unwrap_or_else(|| Type::none(db, env)), + ); + } + Some(getter) if getter.name(db) == "__constraints__" => { + overload.set_return_type(Type::heterogeneous_tuple( + db, + env, + typevar.constraints(db, env).into_iter().flatten(), + )); + } + Some(getter) if getter.name(db) == "__default__" => { + overload.set_return_type( + typevar.default_type(db, env).unwrap_or_else(|| { + KnownClass::NoDefaultType.to_instance(db, env) + }), + ); + } + _ => {} + }, [Some(Type::PropertyInstance(property)), Some(instance), ..] => { if let Some(getter) = property.getter(db) { - if let Ok(return_ty) = getter - .try_call(db, &CallArguments::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()); - } + overload.check_property_getter(db, env, getter, *instance, 1); } else { overload .errors - .push(BindingError::PropertyHasNoSetter(*property)); + .push(BindingError::PropertyHasNoGetter(*property)); overload.set_return_type(Type::Never); } } @@ -1718,22 +1886,12 @@ impl<'db> Bindings<'db> { } [Some(instance), ..] => { if let Some(getter) = property.getter(db) { - if let Ok(return_ty) = getter - .try_call(db, &CallArguments::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()); - } + overload.check_property_getter(db, env, getter, *instance, 0); } else { overload.set_return_type(Type::Never); - overload.errors.push(BindingError::InternalCallError( - "property has no getter", - )); + overload + .errors + .push(BindingError::PropertyHasNoGetter(property)); } } _ => {} @@ -1749,23 +1907,8 @@ impl<'db> Bindings<'db> { ] = overload.parameter_types() { if let Some(setter) = property.setter(db) { - if let Ok(return_ty) = setter - .try_call(db, &CallArguments::positional([*instance, *value])) - .map(|binding| binding.return_type(db)) - { - // `property.__set__` returns `None` for ordinary setters, but - // preserving `Never` keeps non-returning setters divergent. - overload.set_return_type(if return_ty.is_never() { - return_ty - } else { - Type::none(db) - }); - } else { - overload.errors.push(BindingError::InternalCallError( - "calling the setter failed", - )); - overload.set_return_type(Type::unknown()); - } + overload + .check_property_setter(db, env, setter, *instance, *value, 1); } else { overload .errors @@ -1780,15 +1923,15 @@ impl<'db> Bindings<'db> { { if let Some(deleter) = property.deleter(db) { if let Ok(return_ty) = deleter - .try_call(db, &CallArguments::positional([*instance])) - .map(|binding| binding.return_type(db)) + .try_call(db, env, &CallArguments::positional([*instance])) + .map(|binding| binding.return_type(db, env)) { // `property.__delete__` returns `None` for ordinary deleters, // but preserving `Never` keeps non-returning deleters divergent. overload.set_return_type(if return_ty.is_never() { return_ty } else { - Type::none(db) + Type::none(db, env) }); } else { overload.errors.push(BindingError::InternalCallError( @@ -1807,23 +1950,8 @@ impl<'db> Bindings<'db> { Type::KnownBoundMethod(KnownBoundMethodType::PropertyDunderSet(property)) => { if let [Some(instance), Some(value), ..] = overload.parameter_types() { if let Some(setter) = property.setter(db) { - if let Ok(return_ty) = setter - .try_call(db, &CallArguments::positional([*instance, *value])) - .map(|binding| binding.return_type(db)) - { - // `property.__set__` returns `None` for ordinary setters, but - // preserving `Never` keeps non-returning setters divergent. - overload.set_return_type(if return_ty.is_never() { - return_ty - } else { - Type::none(db) - }); - } else { - overload.errors.push(BindingError::InternalCallError( - "calling the setter failed", - )); - overload.set_return_type(Type::unknown()); - } + overload + .check_property_setter(db, env, setter, *instance, *value, 0); } else { overload .errors @@ -1838,15 +1966,15 @@ impl<'db> Bindings<'db> { if let [Some(instance), ..] = overload.parameter_types() { if let Some(deleter) = property.deleter(db) { if let Ok(return_ty) = deleter - .try_call(db, &CallArguments::positional([*instance])) - .map(|binding| binding.return_type(db)) + .try_call(db, env, &CallArguments::positional([*instance])) + .map(|binding| binding.return_type(db, env)) { // `property.__delete__` returns `None` for ordinary deleters, // but preserving `Never` keeps non-returning deleters divergent. overload.set_return_type(if return_ty.is_never() { return_ty } else { - Type::none(db) + Type::none(db, env) }); } else { overload.errors.push(BindingError::InternalCallError( @@ -1915,9 +2043,11 @@ impl<'db> Bindings<'db> { "values" | "values_list" ) => { - if let Some(model) = - django::queryset_or_manager_model(db, bound_method.self_instance(db)) - { + if let Some(model) = django::queryset_or_manager_model( + db, + env, + bound_method.self_instance(db), + ) { let mut fields: Vec<&str> = Vec::new(); let mut all_literal = true; let mut flat = false; @@ -1945,16 +2075,18 @@ impl<'db> Bindings<'db> { } let row = if all_literal { if bound_method.function(db).name(db) == "values_list" { - django::values_list_row_type(db, model, &fields, flat, named) + django::values_list_row_type( + db, env, model, &fields, flat, named, + ) } else { - django::values_row_type(db, model, &fields) + django::values_row_type(db, env, model, &fields) } } else { None }; if let Some(row) = row && let Some(refined) = - django::with_queryset_row(db, overload.return_ty, row) + django::with_queryset_row(db, env, overload.return_ty, row) { overload.set_return_type(refined); } @@ -1974,6 +2106,7 @@ impl<'db> Bindings<'db> { }); if let Some(refined) = django::drf_method_return_type( db, + env, bound_method.function(db), bound_method.self_instance(db), overload.return_ty, @@ -2081,7 +2214,7 @@ impl<'db> Bindings<'db> { // otherwise mutable model. Only an explicit, literal `True` freezes the // field; a dynamic value degrades to not-frozen. let frozen = get_argument_type("frozen", false) - .is_some_and(|frozen| frozen.bool(db).is_always_true()); + .is_some_and(|frozen| frozen.bool(db, env).is_always_true()); // `dataclasses.field` and field-specifier functions of commonly used // libraries like `pydantic`, `attrs`, and `SQLAlchemy` all return @@ -2096,11 +2229,10 @@ impl<'db> Bindings<'db> { }; let init = init - .map(|init| !init.bool(db).is_always_false()) + .map(|init| !init.bool(db, env).is_always_false()) .unwrap_or(true); - let kw_only = if Program::get(db).python_version(db) >= PythonVersion::PY310 - { + let kw_only = if env.python_version(db) >= PythonVersion::PY310 { match kw_only.and_then(Type::as_literal_value_kind) { // We are more conservative here when turning the type for `kw_only` // into a bool, because a field specifier in a stub might use @@ -2128,10 +2260,10 @@ impl<'db> Bindings<'db> { // instances (`my_model.field = …`). The output type is used to validate // that the converter's return type is assignable to the field's declared type. let converter = converter.and_then(|converter_ty| { - let mut input_types = UnionBuilder::new(db); - let mut output_types = UnionBuilder::new(db); + let mut input_types = UnionBuilder::new(db, env); + let mut output_types = UnionBuilder::new(db, env); let mut found_any = false; - let bindings = converter_ty.bindings(db); + let bindings = converter_ty.bindings(db, env); // Note: `iter_callable_items` collapses the union/intersection // structure. In principle, if the converter is a union of callables, // we should only accept the intersection of all first parameter @@ -2152,7 +2284,7 @@ impl<'db> Bindings<'db> { let class_default_specialization = item .as_constructor() .map(ConstructorBinding::constructed_instance_type) - .and_then(|ty| ty.class_specialization(db)) + .and_then(|ty| ty.class_specialization(db, env)) .map(|(_, specialization)| { specialization .generic_context(db) @@ -2164,10 +2296,11 @@ impl<'db> Bindings<'db> { let default_specialization = class_default_specialization .or_else(|| { - overload - .signature - .generic_context - .map(|ctx| ctx.default_specialization(db, None)) + overload.signature.generic_context.map( + |generic_context| { + generic_context.default_specialization(db, None) + }, + ) }); if let Some(first_param) = params.get_positional(first_index) { @@ -2225,11 +2358,11 @@ impl<'db> Bindings<'db> { Type::FunctionLiteral(function_type) => match function_type.known(db) { Some(KnownFunction::IsEquivalentTo) => { if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() { - let ty_a = ty_a.project_type_form(db); - let ty_b = ty_b.project_type_form(db); + let ty_a = ty_a.project_type_form(db, env); + let ty_b = ty_b.project_type_form(db, env); let constraints = ConstraintSetBuilder::new(); let result = constraints.into_owned(|constraints| { - ty_a.when_equivalent_to(db, ty_b, constraints) + ty_a.when_equivalent_to(db, env, ty_b, constraints) }); let tracked = InternedConstraintSet::new(db, result); overload.set_return_type(Type::KnownInstance( @@ -2240,15 +2373,16 @@ impl<'db> Bindings<'db> { Some(KnownFunction::IsSubtypeOf) => { if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() { - let ty_a = ty_a.project_type_form(db); - let ty_b = ty_b.project_type_form(db); + let ty_a = ty_a.project_type_form(db, env); + let ty_b = ty_b.project_type_form(db, env); let constraints = ConstraintSetBuilder::new(); let result = constraints.into_owned(|constraints| { ty_a.when_subtype_of( db, + env, ty_b, constraints, - InferableTypeVars::None, + TypeVarSet::None, ) }); let tracked = InternedConstraintSet::new(db, result); @@ -2260,15 +2394,16 @@ impl<'db> Bindings<'db> { Some(KnownFunction::IsAssignableTo) => { if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() { - let ty_a = ty_a.project_type_form(db); - let ty_b = ty_b.project_type_form(db); + let ty_a = ty_a.project_type_form(db, env); + let ty_b = ty_b.project_type_form(db, env); let constraints = ConstraintSetBuilder::new(); let result = constraints.into_owned(|constraints| { ty_a.when_assignable_to( db, + env, ty_b, constraints, - InferableTypeVars::None, + TypeVarSet::None, ) }); let tracked = InternedConstraintSet::new(db, result); @@ -2280,11 +2415,16 @@ impl<'db> Bindings<'db> { Some(KnownFunction::IsConstraintSetAssignableTo) => { if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() { - let ty_a = ty_a.project_type_form(db); - let ty_b = ty_b.project_type_form(db); + let ty_a = ty_a.project_type_form(db, env); + let ty_b = ty_b.project_type_form(db, env); let constraints = ConstraintSetBuilder::new(); let result = constraints.into_owned(|constraints| { - ty_a.when_constraint_set_assignable_to(db, ty_b, constraints) + ty_a.when_constraint_set_assignable_to( + db, + env, + ty_b, + constraints, + ) }); let tracked = InternedConstraintSet::new(db, result); overload.set_return_type(Type::KnownInstance( @@ -2295,15 +2435,16 @@ impl<'db> Bindings<'db> { Some(KnownFunction::IsDisjointFrom) => { if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() { - let ty_a = ty_a.project_type_form(db); - let ty_b = ty_b.project_type_form(db); + let ty_a = ty_a.project_type_form(db, env); + let ty_b = ty_b.project_type_form(db, env); let constraints = ConstraintSetBuilder::new(); let result = constraints.into_owned(|constraints| { ty_a.when_disjoint_from( db, + env, ty_b, constraints, - InferableTypeVars::None, + TypeVarSet::None, ) }); let tracked = InternedConstraintSet::new(db, result); @@ -2316,15 +2457,7 @@ impl<'db> Bindings<'db> { Some(KnownFunction::IsSingleton) => { if let [Some(ty)] = overload.parameter_types() { overload.set_return_type(Type::bool_literal( - ty.project_type_form(db).is_singleton(db), - )); - } - } - - Some(KnownFunction::IsSingleValued) => { - if let [Some(ty)] = overload.parameter_types() { - overload.set_return_type(Type::bool_literal( - ty.project_type_form(db).is_single_valued(db), + ty.project_type_form(db, env).is_singleton(db, env), )); } } @@ -2341,6 +2474,7 @@ impl<'db> Bindings<'db> { |signature: &CallableSignature<'db>| { UnionType::try_from_elements( db, + env, signature.overloads.iter().map(|signature| { signature.generic_context.map(wrap_generic_context) }), @@ -2365,7 +2499,7 @@ impl<'db> Bindings<'db> { } Type::KnownInstance(KnownInstanceType::TypeAliasType( - TypeAliasType::PEP695(alias), + alias, )) => alias.generic_context(db).map(wrap_generic_context), _ => None, @@ -2374,6 +2508,7 @@ impl<'db> Bindings<'db> { let generic_context = match ty { Type::Union(union_type) => UnionType::try_from_elements( db, + env, union_type .elements(db) .iter() @@ -2383,7 +2518,7 @@ impl<'db> Bindings<'db> { }; overload.set_return_type( - generic_context.unwrap_or_else(|| Type::none(db)), + generic_context.unwrap_or_else(|| Type::none(db, env)), ); } } @@ -2395,16 +2530,18 @@ impl<'db> Bindings<'db> { let [Some(ty)] = overload.parameter_types() else { continue; }; - let Some(callables) = ty.try_upcast_to_callable(db).map(|callables| { - if into_callable == KnownFunction::IntoRegularCallable { - callables.map(|callable| callable.into_regular(db)) - } else { - callables - } - }) else { + let Some(callables) = + ty.try_upcast_to_callable(db, env).map(|callables| { + if into_callable == KnownFunction::IntoRegularCallable { + callables.map(|callable| callable.into_regular(db)) + } else { + callables + } + }) + else { continue; }; - overload.set_return_type(callables.into_type(db)); + overload.set_return_type(callables.into_type(db, env)); } Some(KnownFunction::DunderAllNames) => { @@ -2414,6 +2551,7 @@ impl<'db> Bindings<'db> { let all_names = module_literal .module(db) .file(db) + .map(|file| ProgramFile::new(db, file, env.program(db))) .map(|file| dunder_all_names(db, file)) .unwrap_or_default(); match all_names { @@ -2422,15 +2560,16 @@ impl<'db> Bindings<'db> { names.sort(); Type::heterogeneous_tuple( db, + env, names.iter().map(|name| { Type::string_literal(db, *name) }), ) } - None => Type::none(db), + None => Type::none(db, env), } } - _ => Type::none(db), + _ => Type::none(db, env), }); } } @@ -2444,6 +2583,7 @@ impl<'db> Bindings<'db> { { Type::heterogeneous_tuple( db, + env, metadata .members .keys() @@ -2464,7 +2604,8 @@ impl<'db> Bindings<'db> { if let [Some(ty)] = overload.parameter_types() { overload.set_return_type(Type::heterogeneous_tuple( db, - list_members::all_members(db, *ty) + env, + list_members::all_members(db, env, *ty) .into_iter() .sorted() .map(|member| Type::string_literal(db, &member.name)), @@ -2474,7 +2615,7 @@ impl<'db> Bindings<'db> { Some(KnownFunction::Len) => { if let [Some(first_arg)] = overload.parameter_types() - && let Some(len_ty) = first_arg.len(db) + && let Some(len_ty) = first_arg.len(db, env) { overload.set_return_type(len_ty); } @@ -2482,13 +2623,13 @@ impl<'db> Bindings<'db> { Some(KnownFunction::Repr) => { if let [Some(first_arg)] = overload.parameter_types() { - overload.set_return_type(first_arg.repr(db)); + overload.set_return_type(first_arg.repr(db, env)); } } Some(KnownFunction::Cast) => { if let [Some(casted_ty), Some(_)] = overload.parameter_types() { - overload.set_return_type(casted_ty.project_type_form(db)); + overload.set_return_type(casted_ty.project_type_form(db, env)); } } @@ -2516,10 +2657,14 @@ impl<'db> Bindings<'db> { .interface(db) .members(db) .map(|member| Type::string_literal(db, member.name())); - let specialization = UnionType::from_elements(db, member_names); + let specialization = + UnionType::from_elements(db, env, member_names); overload.set_return_type( - KnownClass::FrozenSet - .to_specialized_instance(db, &[specialization]), + KnownClass::FrozenSet.to_specialized_instance( + db, + env, + &[specialization], + ), ); } } @@ -2542,11 +2687,11 @@ impl<'db> Bindings<'db> { }; let union_with_default = - |ty| UnionType::from_two_elements(db, ty, default); + |ty| UnionType::from_two_elements(db, env, 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)) { + match instance_ty.static_member(db, env, attr_name.value(db)) { Place::Defined(DefinedPlace { ty, definedness: Definedness::AlwaysDefined, @@ -2655,7 +2800,7 @@ impl<'db> Bindings<'db> { _ => {} } - let params = DataclassParams::from_flags(db, flags); + let params = DataclassParams::from_flags(db, env, flags); if cls_argument.is_none_or(|cls_ty| cls_ty.is_none(db)) { overload.set_return_type(Type::DataclassDecorator(params)); @@ -2767,9 +2912,12 @@ impl<'db> Bindings<'db> { continue; }; - let return_type = parse_struct_format(db, format_literal.value(db)) - .map(|elements| Type::heterogeneous_tuple(db, elements)) - .unwrap_or_else(|| Type::homogeneous_tuple(db, Type::unknown())); + let return_type = + parse_struct_format(db, env, format_literal.value(db)) + .map(|elements| Type::heterogeneous_tuple(db, env, elements)) + .unwrap_or_else(|| { + Type::homogeneous_tuple(db, env, Type::unknown()) + }); overload.set_return_type(return_type); } @@ -2888,20 +3036,103 @@ impl<'db> Bindings<'db> { } }, + Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetLowerBound) => { + let [Some(lower), Some(typevar)] = overload.parameter_types() else { + return; + }; + let lower = lower.project_type_form(db, env); + let typevar = typevar.project_type_form(db, env); + let Type::TypeVar(typevar) = typevar else { + return; + }; + let constraints = ConstraintSetBuilder::new(); + let result = constraints.into_owned(|constraints| { + ConstraintSet::constrain_typevar_lower_bound( + db, + env, + constraints, + typevar, + lower, + ) + }); + let tracked = InternedConstraintSet::new(db, result); + overload.set_return_type(Type::KnownInstance( + KnownInstanceType::ConstraintSet(tracked), + )); + } + + Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetUpperBound) => { + let [Some(typevar), Some(upper)] = overload.parameter_types() else { + return; + }; + let typevar = typevar.project_type_form(db, env); + let upper = upper.project_type_form(db, env); + let Type::TypeVar(typevar) = typevar else { + return; + }; + let constraints = ConstraintSetBuilder::new(); + let result = constraints.into_owned(|constraints| { + ConstraintSet::constrain_typevar_upper_bound( + db, + env, + constraints, + typevar, + upper, + ) + }); + let tracked = InternedConstraintSet::new(db, result); + overload.set_return_type(Type::KnownInstance( + KnownInstanceType::ConstraintSet(tracked), + )); + } + + Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetEquality) => { + let [Some(typevar), Some(value)] = overload.parameter_types() else { + return; + }; + let typevar = typevar.project_type_form(db, env); + let value = value.project_type_form(db, env); + let Type::TypeVar(typevar) = typevar else { + return; + }; + let constraints = ConstraintSetBuilder::new(); + let result = constraints.into_owned(|constraints| { + ConstraintSet::constrain_typevar( + db, + env, + constraints, + typevar, + value, + value, + ) + }); + let tracked = InternedConstraintSet::new(db, result); + overload.set_return_type(Type::KnownInstance( + KnownInstanceType::ConstraintSet(tracked), + )); + } + Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetRange) => { let [Some(lower), Some(typevar), Some(upper)] = overload.parameter_types() else { return; }; - let lower = lower.project_type_form(db); - let typevar = typevar.project_type_form(db); - let upper = upper.project_type_form(db); + let lower = lower.project_type_form(db, env); + let typevar = typevar.project_type_form(db, env); + let upper = upper.project_type_form(db, env); let Type::TypeVar(typevar) = typevar else { return; }; let constraints = ConstraintSetBuilder::new(); let result = constraints.into_owned(|constraints| { - ConstraintSet::constrain_typevar(db, constraints, typevar, lower, upper) + ConstraintSet::constrain_typevar( + db, + env, + constraints, + typevar, + lower, + upper, + ) }); let tracked = InternedConstraintSet::new(db, result); overload.set_return_type(Type::KnownInstance( @@ -2941,20 +3172,22 @@ impl<'db> Bindings<'db> { let [Some(ty_a), Some(ty_b)] = overload.parameter_types() else { continue; }; - let ty_a = ty_a.project_type_form(db); - let ty_b = ty_b.project_type_form(db); + let ty_a = ty_a.project_type_form(db, env); + let ty_b = ty_b.project_type_form(db, env); let nonce_generator = TypeVarNonceGenerator::default(); let ty_a = freshen_generic_contexts_in_type( db, + env, ty_a, - generic_contexts_mentioned_in_type(db, ty_a), + generic_contexts_mentioned_in_type(db, env, ty_a), &nonce_generator, ); let ty_b = freshen_generic_contexts_in_type( db, + env, ty_b, - generic_contexts_mentioned_in_type(db, ty_b), + generic_contexts_mentioned_in_type(db, env, ty_b), &nonce_generator, ); @@ -2962,10 +3195,11 @@ impl<'db> Bindings<'db> { let result = constraints.into_owned(|constraints| { ty_a.when_subtype_of_assuming( db, + env, ty_b, - constraints.load(db, tracked.constraints(db)), + constraints.load(db, env, tracked.constraints(db)), constraints, - InferableTypeVars::None, + TypeVarSet::None, ) }); let tracked = InternedConstraintSet::new(db, result); @@ -2987,8 +3221,8 @@ impl<'db> Bindings<'db> { let constraints = ConstraintSetBuilder::new(); let result = constraints.into_owned(|constraints| { - let lhs = constraints.load(db, tracked.constraints(db)); - let rhs = constraints.load(db, other.constraints(db)); + let lhs = constraints.load(db, env, tracked.constraints(db)); + let rhs = constraints.load(db, env, other.constraints(db)); lhs.implies(db, constraints, || rhs) }); let tracked = InternedConstraintSet::new(db, result); @@ -2997,24 +3231,30 @@ impl<'db> Bindings<'db> { )); } - Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetForAll(tracked)) => { + Type::KnownBoundMethod( + method @ (KnownBoundMethodType::ConstraintSetExists(tracked) + | KnownBoundMethodType::ConstraintSetForAll(tracked)), + ) => { let [Some(typevars)] = overload.parameter_types() else { continue; }; - let Type::NominalInstance(instance) = typevars.project_type_form(db) else { + let Type::NominalInstance(instance) = typevars.project_type_form(db, env) + else { continue; }; - let Some(typevars) = inferable_typevars_from_tuple(db, &instance) else { + let Some(typevars) = inferable_typevars_from_tuple(db, env, &instance) + else { continue; }; let constraints = ConstraintSetBuilder::new(); let result = constraints.into_owned(|constraints| { - constraints.load(db, tracked.constraints(db)).for_all( - db, - constraints, - typevars, - ) + let set = constraints.load(db, env, tracked.constraints(db)); + if matches!(method, KnownBoundMethodType::ConstraintSetExists(_)) { + set.reduce_inferable(db, env, constraints, typevars) + } else { + set.for_all(db, env, constraints, typevars) + } }); let tracked = InternedConstraintSet::new(db, result); overload.set_return_type(Type::KnownInstance( @@ -3028,16 +3268,16 @@ impl<'db> Bindings<'db> { let extract_inferable = |instance: &NominalInstanceType<'db>| { if instance.has_known_class(db, KnownClass::NoneType) { // Caller explicitly passed None, so no typevars are inferable. - return Some(InferableTypeVars::None); + return Some(TypeVarSet::None); } - inferable_typevars_from_tuple(db, instance) + inferable_typevars_from_tuple(db, env, instance) }; let inferable = match overload.parameter_types() { // Caller did not provide argument, so no typevars are inferable. - [None] => InferableTypeVars::None, + [None] => TypeVarSet::None, [Some(ty)] => { - let Type::NominalInstance(instance) = ty.project_type_form(db) + let Type::NominalInstance(instance) = ty.project_type_form(db, env) else { continue; }; @@ -3050,8 +3290,9 @@ impl<'db> Bindings<'db> { }; let constraints = ConstraintSetBuilder::new(); - let set = constraints.load(db, tracked.constraints(db)); - let result = set.satisfied_by_all_typevars(db, &constraints, inferable); + let set = constraints.load(db, env, tracked.constraints(db)); + let result = + set.satisfied_by_all_typevars(db, env, &constraints, inferable); overload.set_return_type(Type::bool_literal(result)); } @@ -3061,22 +3302,24 @@ impl<'db> Bindings<'db> { let [Some(typevar), Some(inferable)] = overload.parameter_types() else { continue; }; - let Type::TypeVar(typevar) = typevar.project_type_form(db) else { + let Type::TypeVar(typevar) = typevar.project_type_form(db, env) else { continue; }; - let Type::NominalInstance(inferable) = inferable.project_type_form(db) + let Type::NominalInstance(inferable) = inferable.project_type_form(db, env) else { continue; }; - let Some(inferable) = inferable_typevars_from_tuple(db, &inferable) else { + let Some(inferable) = inferable_typevars_from_tuple(db, env, &inferable) + else { continue; }; let constraints = ConstraintSetBuilder::new(); - let set = constraints.load(db, tracked.constraints(db)); - let result = match set.solutions(db, &constraints, inferable) { + let set = constraints.load(db, env, tracked.constraints(db)); + let result = match set.solutions(db, env, &constraints, inferable) { Solutions::Constrained(paths) => Type::heterogeneous_tuple( db, + env, paths.into_iter().map(|path| { let path: Box<[_]> = path .into_iter() @@ -3087,8 +3330,8 @@ impl<'db> Bindings<'db> { )) }), ), - Solutions::Unsatisfiable => Type::none(db), - Solutions::Unconstrained => Type::empty_tuple(db), + Solutions::Unsatisfiable => Type::none(db, env), + Solutions::Unconstrained => Type::empty_tuple(db, env), }; overload.set_return_type(result); } @@ -3099,19 +3342,21 @@ impl<'db> Bindings<'db> { let [Some(inferable)] = overload.parameter_types() else { continue; }; - let Type::NominalInstance(inferable) = inferable.project_type_form(db) + let Type::NominalInstance(inferable) = inferable.project_type_form(db, env) else { continue; }; - let Some(inferable) = inferable_typevars_from_tuple(db, &inferable) else { + let Some(inferable) = inferable_typevars_from_tuple(db, env, &inferable) + else { continue; }; let constraints = ConstraintSetBuilder::new(); - let set = constraints.load(db, tracked.constraints(db)); - let result = match set.solutions(db, &constraints, inferable) { + let set = constraints.load(db, env, tracked.constraints(db)); + let result = match set.solutions(db, env, &constraints, inferable) { Solutions::Constrained(paths) => Type::heterogeneous_tuple( db, + env, paths.into_iter().map(|path| { Type::KnownInstance(KnownInstanceType::ConstraintSetSolution( InternedConstraintSetSolution::new( @@ -3121,8 +3366,8 @@ impl<'db> Bindings<'db> { )) }), ), - Solutions::Unsatisfiable => Type::none(db), - Solutions::Unconstrained => Type::empty_tuple(db), + Solutions::Unsatisfiable => Type::none(db, env), + Solutions::Unconstrained => Type::empty_tuple(db, env), }; overload.set_return_type(result); } @@ -3139,7 +3384,11 @@ impl<'db> Bindings<'db> { Type::ClassLiteral(class) => match class.known(db) { Some(KnownClass::Bool) => match overload.parameter_types() { [Some(arg)] => { - overload.set_return_type(Type::from_truthiness(db, arg.bool(db))); + overload.set_return_type(Type::from_truthiness( + db, + env, + arg.bool(db, env), + )); } [None] => overload.set_return_type(Type::bool_literal(false)), _ => {} @@ -3147,7 +3396,9 @@ impl<'db> Bindings<'db> { Some(KnownClass::Str) if overload_index == 0 => { match overload.parameter_types() { - [Some(arg)] => overload.set_return_type(arg.str(db)), + [Some(arg)] => { + overload.set_return_type(arg.str(db, env)); + } [None] => { overload.set_return_type(Type::string_literal(db, "")); } @@ -3157,7 +3408,7 @@ impl<'db> Bindings<'db> { Some(KnownClass::Type) if overload_index == 0 => { if let [Some(arg)] = overload.parameter_types() { - overload.set_return_type(arg.dunder_class(db)); + overload.set_return_type(arg.dunder_class(db, env)); } } @@ -3174,7 +3425,7 @@ impl<'db> Bindings<'db> { Some(KnownClass::FunctoolsPartial) => { if let Some(new_return_type) = - overload.functools_partial_return_type(db, call_arguments) + overload.functools_partial_return_type(db, env, call_arguments) { overload.set_return_type(new_return_type); } @@ -3191,11 +3442,15 @@ impl<'db> Bindings<'db> { // `__iter__ = None`, for example). That would be badly written Python code, but we still // need to be able to handle it without crashing. let return_type = if let Type::Union(union) = argument { - union.map(db, |element| { - Type::tuple(TupleType::new(db, &element.iterate(db))) + union.map(db, env, |element| { + Type::tuple(TupleType::new( + db, + env, + &element.iterate(db, env), + )) }) } else { - Type::tuple(TupleType::new(db, &argument.iterate(db))) + Type::tuple(TupleType::new(db, env, &argument.iterate(db, env))) }; overload.set_return_type(return_type); } @@ -3219,6 +3474,7 @@ impl<'db> From> for Bindings<'db> { Bindings { callable_type: from.callable_type, elements: smallvec_inline![BindingsElement { + callable_type: from.callable_type, items: smallvec_inline![CallableItem::Regular(from)], combination: ItemCombination::Intersection, }], @@ -3245,6 +3501,7 @@ impl<'db> From> for Bindings<'db> { Bindings { callable_type, elements: smallvec_inline![BindingsElement { + callable_type, items: smallvec_inline![CallableItem::Regular(callable_binding)], combination: ItemCombination::Intersection, }], @@ -3277,7 +3534,7 @@ pub(crate) struct CallableBinding<'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, + 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>, @@ -3339,7 +3596,15 @@ impl<'db> CallableBinding<'db> { signature_type: Type<'db>, overloads: impl IntoIterator>, ) -> Self { - Self::from_indexed_overloads(signature_type, overloads.into_iter().enumerate()) + Self::from_indexed_overloads( + signature_type, + overloads.into_iter().enumerate().map(|(index, signature)| { + ( + signature.source_overload_index().unwrap_or(index), + signature, + ) + }), + ) } /// Constructs a callable binding from overloads while preserving each overload's position in @@ -3347,7 +3612,7 @@ impl<'db> CallableBinding<'db> { /// /// The preserved indexes are used for diagnostics so filtered or reordered bindings can still /// point back to the correct overload declaration. - pub(crate) fn from_indexed_overloads( + fn from_indexed_overloads( signature_type: Type<'db>, overloads: impl IntoIterator)>, ) -> Self { @@ -3383,7 +3648,11 @@ impl<'db> CallableBinding<'db> { /// Rewrites overload signatures as if an implicit bound receiver argument had already been /// consumed, preserving the corresponding source-parameter offset for diagnostics. - pub(crate) fn bake_bound_type_into_overloads(&mut self, db: &'db dyn Db) { + pub(crate) fn bake_bound_type_into_overloads( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) { let Some(bound_self) = self.bound_type.take() else { return; }; @@ -3397,13 +3666,15 @@ impl<'db> CallableBinding<'db> { .parameters() .get(0) .is_some_and(Parameter::is_positional); - overload.signature = - overload - .signature - .bind_self_with_receiver(db, Some(bound_self), Some(typing_self)); + overload.signature = overload.signature.bind_self_with_receiver( + db, + env, + Some(bound_self), + Some(typing_self), + ); overload.return_ty = overload.initial_return_type(db); overload.receiver_self_type = Some(typing_self); - overload.rebind_self_in_return_type(db); + overload.rebind_self_in_return_type(db, env); overload.source_parameter_index_offset += usize::from(removed_receiver); } } @@ -3411,6 +3682,7 @@ impl<'db> CallableBinding<'db> { fn freshen_generic_contexts_in_place( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, nonce_generator: &TypeVarNonceGenerator<'db>, ) { if self @@ -3449,7 +3721,7 @@ impl<'db> CallableBinding<'db> { continue; }; if nonce_generator.should_freshen(db, generic_context) { - overload.freshen_bound_typevars(db, nonce.value()); + overload.freshen_bound_typevars(db, env, nonce.value()); } } } @@ -3584,7 +3856,7 @@ impl<'db> CallableBinding<'db> { .into_iter() .filter_map(|index| { self.overloads().get(index).map(|overload| { - overload.partial_signature_application(signature_arguments.as_ref(), db) + overload.partial_signature_application(db, signature_arguments.as_ref()) }) }) .collect(); @@ -3596,7 +3868,7 @@ impl<'db> CallableBinding<'db> { self } - pub(super) fn argument_matches_keyword_variadic(&self, argument_index: usize) -> bool { + fn argument_matches_keyword_variadic(&self, argument_index: usize) -> bool { let argument_index = argument_index + usize::from(self.bound_type.is_some()); self.matching_overloads().any(|(_, overload)| { overload @@ -3641,19 +3913,25 @@ impl<'db> CallableBinding<'db> { } } - fn match_parameters(&mut self, db: &'db dyn Db, arguments: &CallArguments<'_, 'db>) { + fn match_parameters( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + arguments: &CallArguments<'_, 'db>, + ) { // If this callable is a bound method, prepend the self instance onto the arguments list // before checking. let bound_arguments = arguments.with_self(self.bound_type); for overload in &mut self.overloads { - overload.match_parameters(db, bound_arguments.as_ref()); + overload.match_parameters(db, env, bound_arguments.as_ref()); } } fn check_types( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, constraints: &ConstraintSetBuilder<'db>, call_arguments: &CallArguments<'_, 'db>, call_expression_tcx: TypeContext<'db>, @@ -3664,8 +3942,8 @@ impl<'db> CallableBinding<'db> { let _span = tracing::trace_span!( "CallableBinding::check_types", - arguments = %call_arguments.display(db), - signature = %self.signature_type.display(db), + arguments = %call_arguments.display(db, env), + signature = %self.signature_type.display(db, env), ) .entered(); @@ -3682,7 +3960,7 @@ impl<'db> CallableBinding<'db> { // `*arg` where `arg` is a union of a 2-tuple and a 3-tuple, we shouldn't eliminate any // overload for arity reasons before trying argument expansion. let (should_retry_after_provisional_arity, overloads_for_expansion) = - if self.should_retry_after_provisional_arity(db, call_arguments.as_ref()) { + if self.should_retry_after_provisional_arity(db, env, call_arguments.as_ref()) { // We will retry all overloads after argument expansion. (true, (0..self.overloads.len()).collect()) } else { @@ -3694,6 +3972,7 @@ impl<'db> CallableBinding<'db> { if let [overload] = self.overloads.as_mut_slice() { overload.check_types( db, + env, constraints, call_arguments.as_ref(), call_expression_tcx, @@ -3707,6 +3986,7 @@ impl<'db> CallableBinding<'db> { self.matching_overload_before_type_checking = Some(index); self.overloads[index].check_types( db, + env, constraints, call_arguments.as_ref(), call_expression_tcx, @@ -3722,6 +4002,7 @@ impl<'db> CallableBinding<'db> { for (_, overload) in self.matching_overloads_mut() { overload.check_types( db, + env, constraints, call_arguments.as_ref(), call_expression_tcx, @@ -3770,6 +4051,7 @@ impl<'db> CallableBinding<'db> { // If two or more candidate overloads remain, proceed to step 5. self.filter_overloads_using_any_or_unknown( db, + env, constraints, call_arguments.as_ref(), &indexes, @@ -3791,7 +4073,7 @@ impl<'db> CallableBinding<'db> { // Step 3: Perform "argument type expansion". Reference: // https://typing.python.org/en/latest/spec/overload.html#argument-type-expansion - let mut expansions = call_arguments.expand(db).peekable(); + let mut expansions = call_arguments.expand(db, env).peekable(); // Return early if there are no argument types to expand. if expansions.peek().is_none() { @@ -3813,7 +4095,7 @@ impl<'db> CallableBinding<'db> { let Some(argument_type) = argument_types.get_default() else { continue; }; - if is_expandable_type(db, argument_type) { + if is_expandable_type(db, env, argument_type) { continue; } let mut is_argument_assignable_to_any_overload = false; @@ -3825,11 +4107,12 @@ impl<'db> CallableBinding<'db> { if argument_type .when_assignable_to( db, + env, parameter_type, constraints, overload.inferable_typevars, ) - .is_always_satisfied(db) + .is_always_satisfied(db, env) { is_argument_assignable_to_any_overload = true; break 'overload; @@ -3840,7 +4123,7 @@ impl<'db> CallableBinding<'db> { tracing::debug!( "Argument at {argument_index} (`{}`) is not assignable to any of the \ remaining overloads, skipping argument type expansion", - argument_type.display(db) + argument_type.display(db, env) ); return; } @@ -3884,7 +4167,7 @@ impl<'db> CallableBinding<'db> { for overload in &mut self.overloads { // Clear the state of all overloads before re-evaluating from step 1 overload.reset(db); - overload.match_parameters(db, expanded_arguments); + overload.match_parameters(db, env, expanded_arguments); } tracing::trace!( @@ -3894,7 +4177,13 @@ impl<'db> CallableBinding<'db> { ); for (_, overload) in self.matching_overloads_mut() { - overload.check_types(db, constraints, expanded_arguments, call_expression_tcx); + overload.check_types( + db, + env, + constraints, + expanded_arguments, + call_expression_tcx, + ); } tracing::trace!( @@ -3928,6 +4217,7 @@ impl<'db> CallableBinding<'db> { MatchingOverloadIndex::Multiple(indexes) => { self.filter_overloads_using_any_or_unknown( db, + env, constraints, expanded_arguments, &indexes, @@ -3982,7 +4272,7 @@ impl<'db> CallableBinding<'db> { // union to determine the final return type. self.overload_call_return_type = Some(OverloadCallReturnType::ArgumentTypeExpansion( - UnionType::from_elements(db, return_types), + UnionType::from_elements(db, env, return_types), )); return; @@ -4004,9 +4294,10 @@ impl<'db> CallableBinding<'db> { pub(crate) fn candidate_overload_indices( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, call_arguments: &CallArguments<'_, 'db>, ) -> SmallVec<[usize; 1]> { - if self.should_retry_after_provisional_arity(db, call_arguments) { + if self.should_retry_after_provisional_arity(db, env, call_arguments) { (0..self.overloads.len()).collect() } else { self.matching_overloads().map(|(index, _)| index).collect() @@ -4016,6 +4307,7 @@ impl<'db> CallableBinding<'db> { fn should_retry_after_provisional_arity( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, call_arguments: &CallArguments<'_, 'db>, ) -> bool { self.overloads.len() > 1 @@ -4024,7 +4316,7 @@ impl<'db> CallableBinding<'db> { matches!(argument, Argument::Variadic) && argument_types .get_default() - .is_some_and(|argument_type| is_expandable_type(db, argument_type)) + .is_some_and(|argument_type| is_expandable_type(db, env, argument_type)) }) } @@ -4069,6 +4361,7 @@ impl<'db> CallableBinding<'db> { fn filter_overloads_using_any_or_unknown( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, constraints: &ConstraintSetBuilder<'db>, arguments: &CallArguments<'_, 'db>, matching_overload_indexes: &[usize], @@ -4094,8 +4387,10 @@ impl<'db> CallableBinding<'db> { let raw_parameter_type = overload.signature.parameters() [matched_parameter.index] .annotated_type(); - let parameter_type = raw_parameter_type - .apply_optional_specialization(db, overload.specialization(db)); + let parameter_type = raw_parameter_type.apply_optional_specialization( + db, + overload.specialization(db, env), + ); OverloadFilterSlot { parameter: parameter_type, // Argument types are cached by the raw parameter type, even when @@ -4125,8 +4420,8 @@ impl<'db> CallableBinding<'db> { match (first_parameter_type, current_parameter_type) { (Some(first_parameter_type), Some(current_parameter_type)) => { if !first_parameter_type - .when_equivalent_to(db, current_parameter_type, constraints) - .is_always_satisfied(db) + .when_equivalent_to(db, env, current_parameter_type, constraints) + .is_always_satisfied(db, env) { participating_slot_indices.insert(slot_index); } @@ -4152,9 +4447,10 @@ impl<'db> CallableBinding<'db> { continue; } - let mut union_argument_type_builders = std::iter::repeat_with(|| UnionBuilder::new(db)) - .take(max_slot_count) - .collect::>(); + let mut union_argument_type_builders = + std::iter::repeat_with(|| UnionBuilder::new(db, env)) + .take(max_slot_count) + .collect::>(); let (_, current_slots) = &matching_overload_slots[upto]; @@ -4167,13 +4463,14 @@ impl<'db> CallableBinding<'db> { .map_or(Type::unknown(), |slot| slot.argument) }); union_argument_type_builders[slot_index] - .add_in_place(argument_type.top_materialization(db)); + .add_in_place(argument_type.top_materialization(db, env)); } } } let top_materialized_argument_type = Type::heterogeneous_tuple( db, + env, union_argument_type_builders .into_iter() .filter_map(|builder| { @@ -4185,7 +4482,7 @@ impl<'db> CallableBinding<'db> { }), ); - let mut union_parameter_types = std::iter::repeat_with(|| UnionBuilder::new(db)) + let mut union_parameter_types = std::iter::repeat_with(|| UnionBuilder::new(db, env)) .take(max_slot_count) .collect::>(); for (_, slots) in &matching_overload_slots[..=upto] { @@ -4198,6 +4495,7 @@ impl<'db> CallableBinding<'db> { let parameter_types = Type::heterogeneous_tuple( db, + env, union_parameter_types.into_iter().filter_map(|builder| { if builder.is_empty() { None @@ -4207,7 +4505,16 @@ impl<'db> CallableBinding<'db> { }), ); - if top_materialized_argument_type.is_assignable_to(db, parameter_types) { + if top_materialized_argument_type + .when_assignable_to( + db, + env, + parameter_types, + constraints, + self.overloads[*current_index].inferable_typevars, + ) + .is_always_satisfied(db, env) + { filter_remaining_overloads = true; } } @@ -4224,8 +4531,8 @@ impl<'db> CallableBinding<'db> { matching_overloads.all(|(_, overload)| { overload .return_type() - .when_equivalent_to(db, first_overload_return_type, constraints) - .is_always_satisfied(db) + .when_equivalent_to(db, env, first_overload_return_type, constraints) + .is_always_satisfied(db, env) }) } else { // No matching overload @@ -4269,7 +4576,7 @@ impl<'db> CallableBinding<'db> { Ok(()) } - pub(crate) fn is_callable(&self) -> bool { + fn is_callable(&self) -> bool { !self.overloads.is_empty() } @@ -4322,7 +4629,7 @@ impl<'db> CallableBinding<'db> { } /// Returns the index of the matching overload in the form of [`MatchingOverloadIndex`]. - pub(crate) fn matching_overload_index(&self) -> MatchingOverloadIndex { + fn matching_overload_index(&self) -> MatchingOverloadIndex { let mut matching_overloads = self.matching_overloads(); match matching_overloads.next() { None => MatchingOverloadIndex::None, @@ -4362,6 +4669,20 @@ impl<'db> CallableBinding<'db> { .and_then(|index| self.overloads.get(index)) } + /// Returns a failing overload only when the call's argument shape selected it uniquely. + /// + /// The overload chosen for diagnostics can be arbitrary when multiple signatures accept the + /// same argument shape. Its specialization must not determine a constructor's return type: + /// for example, `dict(value)` may match the shapes of both mapping and iterable overloads. + fn unambiguous_failing_overload(&self) -> Option<&Binding<'db>> { + match self.overloads.as_slice() { + [overload] => Some(overload), + _ => self + .matching_overload_before_type_checking + .and_then(|index| self.overloads.get(index)), + } + } + /// Returns an iterator over all the mutable overloads that matched for this call binding. pub(crate) fn matching_overloads_mut( &mut self, @@ -4382,7 +4703,7 @@ impl<'db> CallableBinding<'db> { /// /// 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> { + 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, @@ -4416,26 +4737,32 @@ impl<'db> CallableBinding<'db> { /// constructor call; diagnostics then name the class rather than the constructor method fn report_diagnostics( &self, - context: &InferContext<'db, '_>, + context: &CallDiagnosticContext<'_, '_, 'db, '_>, node: ast::AnyNodeRef, compound_diag: Option<&dyn CompoundDiagnostic>, constructed_instance_type: Option>, ) { + let env = context.program_environment(); let describe = |callable_type: Type<'db>| { constructed_instance_type - .and_then(|instance| CallableDescription::constructed_class(context.db(), instance)) + .and_then(|instance| { + CallableDescription::constructed_class(context.db(), env, instance) + }) .or_else(|| CallableDescription::new(context.db(), callable_type)) }; + let db = context.db(); + let env = context.program_environment(); + if !self.is_callable() { let range = all_arguments_range(node); if let Some(builder) = context.report_lint(&CALL_NON_CALLABLE, range) { let mut diag = builder.into_diagnostic(format_args!( "Object of type `{}` is not callable", - self.callable_type.display(context.db()), + self.callable_type.display(db, env), )); if let Some(compound_diag) = compound_diag { - compound_diag.add_context(context.db(), &mut diag); + compound_diag.add_context(db, env, &mut diag); } } return; @@ -4446,10 +4773,10 @@ impl<'db> CallableBinding<'db> { if let Some(builder) = context.report_lint(&CALL_NON_CALLABLE, range) { let mut diag = builder.into_diagnostic(format_args!( "Object of type `{}` is not callable (possibly missing `__call__` method)", - self.callable_type.display(context.db()), + self.callable_type.display(db, env), )); if let Some(compound_diag) = compound_diag { - compound_diag.add_context(context.db(), &mut diag); + compound_diag.add_context(db, env, &mut diag); } } return; @@ -4494,11 +4821,7 @@ impl<'db> CallableBinding<'db> { index: self.overloads[matching_overload_index].source_overload_index(), kind, function, - candidate_indexes: self.diagnostic_overload_indexes( - context.db(), - kind, - function, - ), + candidate_indexes: self.diagnostic_overload_indexes(db, kind, function), }); self.overloads[matching_overload_index].report_diagnostics( context, @@ -4521,11 +4844,7 @@ impl<'db> CallableBinding<'db> { index: self.overloads[matching_overload_index].source_overload_index(), kind, function, - candidate_indexes: self.diagnostic_overload_indexes( - context.db(), - kind, - function, - ), + candidate_indexes: self.diagnostic_overload_indexes(db, kind, function), }); self.overloads[matching_overload_index].report_diagnostics( context, @@ -4570,7 +4889,7 @@ impl<'db> CallableBinding<'db> { let (overloads, implementation) = function.overloads_and_implementation(context.db()); let diagnostic_overload_indexes = - self.diagnostic_overload_indexes(context.db(), kind, function); + self.diagnostic_overload_indexes(db, kind, function); let possible_overloads = diagnostic_overload_indexes .iter() .filter_map(|&index| overloads.get(index).copied()) @@ -4582,7 +4901,9 @@ impl<'db> CallableBinding<'db> { "First overload defined here", ); let file = function.file(context.db()); - let module = parsed_module(context.db(), file).load(context.db()); + let module = + parsed_module(context.db(), function.python_file(context.db())) + .load(context.db()); let node = overload.node(context.db(), function.file(context.db()), &module); let span = if node.body.len() == 1 { @@ -4604,7 +4925,7 @@ impl<'db> CallableBinding<'db> { for overload in possible_overloads.iter().take(MAXIMUM_OVERLOADS) { diag.info(format_args!( " {}", - overload.signature(context.db()).display(context.db()) + overload.signature(db).display(db, env) )); } if possible_overloads.len() > MAXIMUM_OVERLOADS { @@ -4627,7 +4948,7 @@ impl<'db> CallableBinding<'db> { } if let Some(compound_diag) = compound_diag { - compound_diag.add_context(context.db(), &mut diag); + compound_diag.add_context(db, env, &mut diag); } } } @@ -4691,6 +5012,8 @@ struct ArgumentMatcher<'a, 'db> { num_synthetic_args: usize, /// How many of `num_synthetic_args` consumed a positional parameter. num_synthetic_args_matched: usize, + /// Forwarded argument indices and the lengths of their fixed tuple prefixes and suffixes. + variable_length_positional_arguments: SmallVec<[(usize, usize, usize); 1]>, variadic_argument_matched_to_variadic_parameter: bool, /// Parameter indices that have explicit keyword arguments (e.g., `foo=value`). @@ -4727,6 +5050,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { first_excess_positional: None, num_synthetic_args: 0, num_synthetic_args_matched: 0, + variable_length_positional_arguments: SmallVec::new(), variadic_argument_matched_to_variadic_parameter: false, explicit_keyword_parameters, } @@ -4789,6 +5113,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { matched_argument.parameters.push(MatchedParameter { index: parameter_index, argument_type, + expected_type: None, provenance, }); matched_argument.matched = true; @@ -4879,6 +5204,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { fn match_variadic( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, argument_index: usize, argument: Argument<'a>, argument_type: Option>, @@ -4928,8 +5254,11 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { if self.parameters.variadic().is_none() && !self.has_later_positional_input(argument_index) => { - let tuple_specs: Vec<_> = - union.elements(db).iter().map(|ty| ty.iterate(db)).collect(); + let tuple_specs: Vec<_> = union + .elements(db) + .iter() + .map(|ty| ty.iterate(db, env)) + .collect(); let min_len = tuple_specs .iter() @@ -4952,7 +5281,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { if var_types.is_empty() { None } else { - Some(UnionType::from_elements_leave_aliases(db, var_types)) + Some(UnionType::from_elements_leave_aliases(db, env, var_types)) } }; @@ -4961,13 +5290,16 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { for index in 0..max_elements { let positional_types: Vec<_> = tuple_specs .iter() - .filter_map(|s| s.py_index(db, index).ok()) + .filter_map(|s| s.py_index(db, env, index).ok()) .collect(); if positional_types.is_empty() { break; } - argument_types_vec - .push(UnionType::from_elements_leave_aliases(db, positional_types)); + argument_types_vec.push(UnionType::from_elements_leave_aliases( + db, + env, + positional_types, + )); } let length = if any_variable || argument_types_vec.len() > min_len { @@ -4983,7 +5315,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { } } _ => { - let tuple = argument_type.iterate(db); + let tuple = argument_type.iterate(db, env); VariadicArgumentType::Other { argument_types: tuple.iter_element_types(db).collect(), length: tuple.len(), @@ -5017,6 +5349,10 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { // `variable_element.is_some()`) or if we have a union of different fixed-length tuples (in // which case `variable_element.is_none()`). let is_variable = length.is_variable(); + if let TupleLength::Variable(prefix, suffix) = length { + self.variable_length_positional_arguments + .push((argument_index, prefix, suffix)); + } let has_fixed_union_tail = is_variable && variable_element.is_none(); // We must be able to match up the fixed-length portion of the argument with positional @@ -5110,11 +5446,12 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { fn match_keyword_variadic( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, argument_index: usize, argument_type: Option>, ) { if let Some(unpacked) = - argument_type.and_then(|ty| extract_unpacked_typed_dict_from_value_type(db, ty)) + argument_type.and_then(|ty| extract_unpacked_typed_dict_from_value_type(db, env, ty)) { let openness = unpacked.openness; @@ -5149,7 +5486,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { let value_type = match argument_type { Some(argument_type) => argument_type .as_paramspec_typevar(db) - .or_else(|| argument_type.getitem_dunder_call(db, parameter_name)) + .or_else(|| argument_type.getitem_dunder_call(db, env, parameter_name)) .unwrap_or(Type::unknown()), None => Type::unknown(), @@ -5196,6 +5533,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { matched_argument.parameters.push(MatchedParameter { index: parameter_index, argument_type: Some(extra_items_ty), + expected_type: None, provenance: InvalidArgumentTypeProvenance::Argument, }); } @@ -5224,7 +5562,123 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { } } - fn finish(self) -> Box<[MatchedArgument<'db>]> { + /// Checks the positional requirements encoded inside an unpacked variadic annotation. + /// + /// Unlike ordinary `*args`, an unpacked tuple can require arguments and prescribe a different + /// type for each position: + /// + /// ```python + /// def callback(*args: *tuple[int, *tuple[str, ...], bytes]) -> None: ... + /// + /// callback(1, b"last") + /// callback(1, "middle", b"last") + /// ``` + /// + /// Store each matched tuple element separately so ordinary argument checking and inference can + /// use its type instead of the complete tuple annotation. + fn match_unpacked_variadic( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + missing: &mut Vec, + ) { + let Some((parameter_index, parameter)) = self.parameters.variadic() else { + return; + }; + if !parameter.has_starred_annotation() { + return; + } + let Some(tuple) = parameter.annotated_type().exact_tuple_instance_spec(db) else { + return; + }; + + let maximum = tuple.len().maximum(); + let mut argument_count = 0; + let mut first_variable = None; + let mut last_variable = None; + let mut first_excess_argument_index = None; + + for (argument_index, argument) in self.argument_matches.iter().enumerate() { + let match_count = argument.parameters.len(); + let variable_segment = self + .variable_length_positional_arguments + .iter() + .find(|(index, _, _)| *index == argument_index); + + for (position, matched) in argument.parameters.iter().enumerate() { + if matched.index != parameter_index { + continue; + } + + if maximum == Some(argument_count) { + first_excess_argument_index = self.get_argument_index(argument_index); + } + + if variable_segment.is_some_and(|(_, prefix, suffix)| { + position >= *prefix && position < match_count.saturating_sub(*suffix) + }) { + if first_variable.is_none() { + first_variable = Some(argument_count); + } + last_variable = Some(argument_count); + } + + argument_count += 1; + } + } + + let argument_length = first_variable + .zip(last_variable) + .map_or(TupleLength::Fixed(argument_count), |(first, last)| { + TupleLength::Variable(first, argument_count.saturating_sub(last + 1)) + }); + + if !argument_length.is_variable() && argument_count < tuple.len().minimum() { + missing.push(ParameterContext::new(parameter, parameter_index, false)); + // TODO: Check matched tuple elements even when required elements are missing. + return; + } + + if let Some(maximum) = maximum + && argument_length.minimum() > maximum + { + self.errors.push(BindingError::TooManyPositionalArguments { + first_excess_argument_index, + expected_positional_count: self.parameters.positional().count() + maximum, + provided_positional_count: self.next_positional, + }); + // TODO: Check matched tuple elements without inferring from excess arguments. + return; + } + + let Ok(expected) = tuple.resize(db, env, argument_length) else { + return; + }; + let variable_type = expected.variable_element_type(db); + let mut expected_types = expected.iter_element_types(db); + for (position, matched) in self + .argument_matches + .iter_mut() + .flat_map(|argument| argument.parameters.iter_mut()) + .filter(|matched| matched.index == parameter_index) + .enumerate() + { + matched.expected_type = if first_variable + .zip(last_variable) + .is_some_and(|(first, last)| position > first && position <= last) + { + variable_type + } else { + expected_types.next() + }; + } + } + + fn finish( + mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Box<[MatchedArgument<'db>]> { if let Some(first_excess_argument_index) = self.first_excess_positional { // synthetic arguments (a bound receiver, the instance a constructor is initialising) // are not written at the call site, so neither they nor the parameters they consumed @@ -5271,6 +5725,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { missing.push(ParameterContext::new(param, index, false)); } } + self.match_unpacked_variadic(db, env, &mut missing); if !missing.is_empty() { self.errors.push(BindingError::MissingArguments { parameters: ParameterContexts(missing), @@ -5284,7 +5739,9 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { struct ArgumentTypeChecker<'a, 'db> { db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, signature_type: Type<'db>, + constructor_kind: Option, signature: &'a Signature<'db>, arguments: &'a CallArguments<'a, 'db>, argument_matches: &'a [MatchedArgument<'db>], @@ -5294,7 +5751,7 @@ struct ArgumentTypeChecker<'a, 'db> { return_ty: Type<'db>, errors: &'a mut Vec>, - inferable_typevars: InferableTypeVars<'db>, + inferable_typevars: TypeVarSet<'db>, inference: Option>, /// Argument indices for which specialization inference has already produced a sufficiently @@ -5319,9 +5776,10 @@ enum KeywordUnpackKeyTypeCheck<'db> { /// Validate the key type of a keyword-unpack argument without checking its value type. fn validate_keyword_unpack_key_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, constraints: &ConstraintSetBuilder<'db>, argument_type: Type<'db>, - inferable_typevars: InferableTypeVars<'db>, + inferable_typevars: TypeVarSet<'db>, ) -> KeywordUnpackKeyTypeCheck<'db> { if matches!(argument_type, Type::TypedDict(_)) || argument_type.as_paramspec_typevar(db).is_some() @@ -5329,18 +5787,19 @@ fn validate_keyword_unpack_key_type<'db>( return KeywordUnpackKeyTypeCheck::NotApplicable; } - let Some((key_type, _)) = argument_type.unpack_keys_and_items(db) else { + let Some((key_type, _)) = argument_type.unpack_keys_and_items(db, env) else { return KeywordUnpackKeyTypeCheck::NotApplicable; }; if key_type .when_assignable_to( db, - KnownClass::Str.to_instance(db), + env, + KnownClass::Str.to_instance(db, env), constraints, inferable_typevars, ) - .is_always_satisfied(db) + .is_always_satisfied(db, env) { KeywordUnpackKeyTypeCheck::Valid } else { @@ -5352,7 +5811,9 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { #[expect(clippy::too_many_arguments)] fn new( db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, signature_type: Type<'db>, + constructor_kind: Option, signature: &'a Signature<'db>, arguments: &'a CallArguments<'a, 'db>, argument_matches: &'a [MatchedArgument<'db>], @@ -5363,7 +5824,9 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { ) -> Self { Self { db, + env, signature_type, + constructor_kind, signature, arguments, argument_matches, @@ -5372,7 +5835,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { call_expression_tcx, return_ty, errors, - inferable_typevars: InferableTypeVars::None, + inferable_typevars: TypeVarSet::None, inference: None, constraint_set_errors: vec![false; arguments.len()], } @@ -5429,9 +5892,138 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { .collect() } + /// Returns the source callable whose parameters supplied a `ParamSpec` specialization. + /// + /// Forwarded arguments are checked against the wrapped callable, not the wrapper's + /// `Callable[P, R]` parameter. Retaining that callable also lets diagnostics account for bound + /// receivers and leading parameters consumed by `Concatenate`. + /// + /// Return `None` when the callback has no source declaration, so diagnostics can fall back to + /// the forwarding function's actual variadic parameter instead of an unrelated parameter. + /// Callable objects and constructors use their existing bindings to preserve the active + /// constructor stage and any consumed receiver. + /// A `functools.partial` also requires accounting for arguments that were supplied before the + /// remaining signature was forwarded. + /// + /// ```python + /// from typing import Callable + /// + /// def wrapper[**P](fn: Callable[P, None], *args: P.args, **kwargs: P.kwargs): ... + /// def target(*, value: int) -> None: ... + /// wrapper(target, value="bad") # The parameter source is `target`. + /// ``` + fn paramspec_parameter_source( + &self, + paramspec: BoundTypeVarInstance<'db>, + overload_index: usize, + ) -> Option> { + let db = self.db; + let env = self.env; + + self.enumerate_argument_types() + .find_map(|(argument_index, _, argument, argument_types)| { + if matches!(argument, Argument::Synthetic) { + return None; + } + + self.argument_matches[argument_index] + .iter() + .find_map(|matched_parameter| { + let declared_type = + self.signature.parameters()[matched_parameter.index].annotated_type(); + let argument_type = argument_types.get_for_declared_type(declared_type); + let paramspec_prefix_len = |candidate: Type<'db>| { + candidate + .try_upcast_to_callable(db, env)? + .iter() + .find_map(|callable| { + callable.signatures(db).iter().find_map(|signature| { + let (prefix, declared_paramspec) = + signature.parameters().as_paramspec_with_prefix()?; + (declared_paramspec == paramspec).then_some(prefix.len()) + }) + }) + }; + let prefix_len = + if let Type::Union(union) = declared_type.resolve_type_alias(db) { + union.elements(db).iter().find_map(|candidate| { + let specialized_candidate = candidate + .apply_optional_specialization(db, self.specialization()); + argument_type + .is_assignable_to(db, env, specialized_candidate) + .then_some(*candidate) + .and_then(paramspec_prefix_len) + }) + } else { + paramspec_prefix_len(declared_type) + }?; + let (source_type, partial_signature) = match argument_type { + Type::KnownInstance( + KnownInstanceType::FunctoolsPartial(partial) + | KnownInstanceType::FunctoolsPartialCall(partial), + ) => { + let signatures = &partial.partial(db).signatures(db).overloads; + ( + partial.wrapped(db).inner(db), + signatures + .iter() + .find(|signature| { + signature.source_overload_index() + == Some(overload_index) + }) + .or_else(|| signatures.get(overload_index)), + ) + } + _ => (argument_type, None), + }; + let argument_bindings = source_type.bindings(db, env); + let callable = argument_bindings.single_item()?.callable(); + let (function, is_bound_method) = match callable.signature_type { + Type::FunctionLiteral(function) => (function, false), + Type::BoundMethod(method) => (method.function(db), true), + _ => return None, + }; + let source_binding = callable + .overloads() + .iter() + .find(|binding| binding.source_overload_index() == overload_index) + .or_else(|| callable.overloads().get(overload_index)) + .or_else(|| callable.overloads().first()); + let overload_index = + source_binding.map_or(overload_index, Binding::source_overload_index); + let source_parameter_index_offset = source_binding + .map_or(0, |binding| binding.source_parameter_index_offset) + + usize::from(callable.bound_type.is_some()); + let source_parameter_index_offset = + partial_signature + .zip(source_binding) + .and_then(|(partial_signature, source_binding)| { + let definition = partial_signature + .parameters() + .iter() + .find_map(Parameter::definition)?; + source_binding.signature.parameters().iter().position( + |parameter| parameter.definition() == Some(definition), + ) + }) + .map_or(source_parameter_index_offset, |index| { + index.max(source_parameter_index_offset) + }); + + Some(ForwardedParameterSource { + function, + is_bound_method, + parameter_index_offset: prefix_len + source_parameter_index_offset, + overload_index, + }) + }) + }) + } + fn specialization(&self) -> Option> { + let env = self.env; self.inference - .map(|inference| call_specialization(self.db, self.signature, inference)) + .map(|inference| call_specialization(self.db, env, self.signature, inference)) } /// The call's specialization with a type variable the call left unsolved kept gradual. @@ -5447,14 +6039,17 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { } fn infer_specialization(&mut self, constraints: &ConstraintSetBuilder<'db>) { + let env = self.env; + let db = self.db; let Some(generic_context) = self.signature.generic_context else { return; }; let return_with_tcx = Some(self.return_ty).zip(self.call_expression_tcx.annotation()); - self.inferable_typevars = generic_context.inferable_typevars(self.db); - let mut builder = SpecializationBuilder::new(self.db, constraints, self.inferable_typevars); + self.inferable_typevars = generic_context.inferable_typevars(db); + let mut builder = + SpecializationBuilder::new(db, self.env, constraints, self.inferable_typevars); // Type variables for which we inferred a declared type based on a partially specialized // type from an outer generic context. For these type variables, we may infer types that @@ -5484,17 +6079,19 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { let preferred_type_mappings = return_with_tcx .and_then(|(return_ty, tcx)| { if !tcx - .filter_union(self.db, |ty| ty.may_prefer_declared_type(self.db)) - .may_prefer_declared_type(self.db) + .filter_union(db, |ty| ty.may_prefer_declared_type(db, self.env)) + .may_prefer_declared_type(db, self.env) { return None; } let return_ty = - return_ty.filter_disjoint_elements(self.db, tcx, self.inferable_typevars); - let tcx = tcx.filter_disjoint_elements(self.db, return_ty, self.inferable_typevars); + return_ty.filter_disjoint_elements(db, self.env, tcx, self.inferable_typevars); + let tcx = + tcx.filter_disjoint_elements(db, self.env, return_ty, self.inferable_typevars); let path_bounds = return_ty.assignable_solutions_with_inferable( - self.db, + db, + self.env, tcx, self.inferable_typevars, ); @@ -5504,12 +6101,12 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { let mut variance_map: FxHashMap, TypeVarVariance> = FxHashMap::default(); let solutions = path_bounds.solve_with(|variance, path_bound| { - let identity = path_bound.bound_typevar.identity(self.db); + let identity = path_bound.bound_typevar.identity(db); variance_map .entry(identity) .and_modify(|current| *current = current.join(variance)) .or_insert(variance); - PathBounds::default_solve(self.db, constraints, path_bound) + PathBounds::default_solve(db, self.env, constraints, path_bound) }); let Solutions::Constrained(solutions) = solutions else { @@ -5521,7 +6118,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { for solution in &solutions { for binding in solution { - let identity = binding.bound_typevar.identity(self.db); + let identity = binding.bound_typevar.identity(db); // Avoid unnecessarily widening the return type based on a covariant // type parameter from the type context, as it can lead to argument @@ -5542,14 +6139,14 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { binding.bound_typevar, binding.solution, ) - .filter_union(self.db, |ty| { - if ty.has_unspecialized_type_var(self.db) { + .filter_union(db, |ty| { + if ty.has_unspecialized_type_var(db, self.env) { partially_specialized_declared_type.insert(identity); return false; } true }); - if inferred_ty.has_unspecialized_type_var(self.db) { + if inferred_ty.has_unspecialized_type_var(db, self.env) { continue; } @@ -5558,28 +6155,30 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { // `T@h | list[T@h]` from an outer generic scope) don't provide // useful concrete information and would cause over-expansion. let concrete_content = - inferred_ty.filter_union(self.db, |ty| !ty.has_typevar(self.db)); - if concrete_content.is_never() && inferred_ty.has_typevar(self.db) { + inferred_ty.filter_union(db, |ty| !ty.has_typevar(db, self.env)); + if concrete_content.is_never() && inferred_ty.has_typevar(db, self.env) { continue; } preferred .entry(identity) - .and_modify(|existing| existing.add(self.db, inferred_ty)) + .and_modify(|existing| { + existing.add(db, self.env, inferred_ty); + }) .or_insert_with(|| UnionAccumulator::new(inferred_ty)); } } let preferred: FxHashMap, Type<'db>> = preferred .into_iter() - .map(|(identity, accumulator)| (identity, accumulator.into_type(self.db))) + .map(|(identity, accumulator)| (identity, accumulator.into_type(db, self.env))) .collect(); // Add preferred types to the builder so they serve as the base mapping // when argument inference adds more types. for solution in &solutions { for binding in solution { - let identity = binding.bound_typevar.identity(self.db); + let identity = binding.bound_typevar.identity(db); if let Some(&ty) = preferred.get(&identity) { builder.add_type_mapping( binding.bound_typevar, @@ -5608,7 +6207,8 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { // Note that this will still lead to an invalid specialization, but may // produce more precise diagnostics. if !assignable_to_declared_type { - builder = SpecializationBuilder::new(self.db, constraints, self.inferable_typevars); + builder = + SpecializationBuilder::new(db, self.env, constraints, self.inferable_typevars); specialization_errors.clear(); self.constraint_set_errors.fill(false); @@ -5632,14 +6232,14 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { // literal values are kept unpromoted. if self.call_expression_tcx.preserve_literals && let Some(lower) = bounds.lower - && crate::types::visitor::any_over_type(self.db, lower, false, |ty| { + && crate::types::visitor::any_over_type(self.db, self.env, lower, false, |ty| { ty.as_literal_value().is_some() }) { return None; } - let bound_or_constraints = typevar.typevar(self.db).bound_or_constraints(self.db); + let bound_or_constraints = typevar.typevar(self.db).bound_or_constraints(self.db, env); // For constrained TypeVars, the inferred type is already one of the // constraints. Promoting literals would produce a type that doesn't @@ -5655,7 +6255,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { // Find all occurrences of the type variable in the return type. self.return_ty - .visit_specialization(self.db, |ty, variance| { + .visit_specialization(db, self.env, |ty, variance| { if ty != Type::TypeVar(typevar) { return; } @@ -5676,17 +6276,17 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { // asks a different question: whether a write can reach the // parameter. a class that only takes `T` in `__init__` has no such // write under either spelling, so the literal stands under both - if typevar.is_declared_invariant_but_never_written(self.db) { + if typevar.is_declared_invariant_but_never_written(self.db, env) { return None; } let lower = bounds.lower?; - let promoted = lower.promote(self.db); + let promoted = lower.promote(db, self.env); // If the TypeVar has an upper bound, only use the promoted type if it // still satisfies the bound. if let Some(TypeVarBoundOrConstraints::UpperBound(bound)) = bound_or_constraints { - if !promoted.is_assignable_to(self.db, bound) { + if !promoted.is_assignable_to(db, self.env, bound) { return None; } } @@ -5697,8 +6297,8 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { let mut choose = |typevar: BoundTypeVarInstance<'db>, bounds: Option<&PathBound<'db>>| { let bounds = bounds?; if let Some(lower) = bounds.lower - && let Some(&preferred_ty) = preferred_type_mappings.get(&typevar.identity(self.db)) - && lower.is_assignable_to(self.db, preferred_ty) + && let Some(&preferred_ty) = preferred_type_mappings.get(&typevar.identity(db)) + && lower.is_assignable_to(db, self.env, preferred_ty) { return Some(preferred_ty); } @@ -5717,7 +6317,9 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { continue; } - let formal = parameters[parameter_index].annotated_type(); + let formal = matched_parameter + .expected_type + .unwrap_or_else(|| parameters[parameter_index].annotated_type()); let actual = matched_parameter .argument_type .unwrap_or_else(|| argument_types.get_for_declared_type(formal)); @@ -5728,9 +6330,9 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { builder.build_diagnostic_inference_with(generic_context, argument_relations, choose) } }; - let specialization = call_specialization(self.db, self.signature, inference); + let specialization = call_specialization(self.db, env, self.signature, inference); - self.return_ty = self.return_ty.apply_specialization(self.db, specialization); + self.return_ty = self.return_ty.apply_specialization(db, specialization); self.inference = Some(inference); } @@ -5741,6 +6343,8 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { partially_specialized_declared_type: &FxHashSet>, specialization_errors: &mut Vec>, ) -> bool { + let env = self.env; + let db = self.db; let parameters = self.signature.parameters(); // A keyword-variadic parameter annotated with a type variable is solved from *all* the @@ -5762,7 +6366,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { for matched_parameter in self.argument_matches[argument_index].iter() { let parameter_index = matched_parameter.index; let parameter = ¶meters[parameter_index]; - let declared_type = parameter.annotated_type(); + let parameter_type = parameter.annotated_type(); if keyword_pack_parameter == Some(parameter_index) { // a splatted `**other` contributes no statically-known field names, so the // pack cannot be solved from it; leave it to the ordinary arity checks @@ -5773,19 +6377,20 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { .unwrap_or_else(Type::unknown); keyword_pack_fields.push( Parameter::keyword_only(Name::new(name)) - .with_annotated_type(field_type.promote(self.db)), + .with_annotated_type(field_type.promote(self.db, env)), ); } continue; } // TODO: Infer a `TypeVarTuple` from all matched positional arguments as a single - // tuple. Until then, skip per-argument inference. + // tuple. Fixed elements beside that pack can still infer ordinary type variables. if parameter.has_starred_annotation() + && matched_parameter.expected_type.is_none() && (matches!( - declared_type, - Type::TypeVar(typevar) if typevar.is_typevartuple(self.db) + parameter_type, + Type::TypeVar(typevar) if typevar.is_typevartuple(db) ) || matches!( - declared_type.exact_tuple_instance_spec(self.db).as_deref(), + parameter_type.exact_tuple_instance_spec(db).as_deref(), Some(TupleSpec::Variable(variable)) if matches!( variable.variable(), @@ -5799,6 +6404,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { continue; } + let declared_type = matched_parameter.expected_type.unwrap_or(parameter_type); let argument_type = argument_types.get_for_declared_type(declared_type); let specialization_result = builder.infer( declared_type, @@ -5864,6 +6470,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { mut argument_type: Type<'db>, matched_parameter: MatchedParameter<'db>, ) { + let db = self.db; let parameter_index = matched_parameter.index; let parameters = self.signature.parameters(); let parameter = ¶meters[parameter_index]; @@ -5880,10 +6487,57 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { return; } - let mut expected_ty = parameter.annotated_type(); + // Constraint inference has already checked the synthetic `cls`. For example: + // + // ```py + // from collections.abc import Callable + // from typing import Any, Self, overload + // + // @overload + // def callback[T](value: frozenset[T]) -> T: ... + // @overload + // def callback[T](value: T) -> T: ... + // + // class Mapper[R]: + // def __new__[T](cls, callback: Callable[[T], R], values: list[T]) -> Self: ... + // + // values: Any + // Mapper(callback, values) + // ``` + // + // The overloads provide alternative, correlated solutions for `T` and `R`. The current + // solver merges each TypeVar's solutions separately, losing that correlation. Applying + // the merged specialization to `cls` a second time then changes the receiver from + // `type[Mapper[frozenset[Never]]]` to + // `type[Mapper[frozenset[frozenset[Never]]]]`, incorrectly rejecting the valid call. + // + // A decorator using `Concatenate[type[U], P]` can also replace the inferred `type[Self]` + // receiver with its own `type[U]`, possibly through a type alias. Constraint inference has + // already checked the actual class against `type[U]`. If the other arguments cannot solve + // `U`, independently checking the class against `type[U]` again would reject the call + // solely because `U` remains unsolved. + // + // TODO: Remove this special case once solution extraction preserves correlations between + // TypeVars across alternative inference paths and constructor calls are solved in a single + // constraint set, so decorator-scoped receiver variables are not rechecked independently. + let constructor_receiver = matches!(argument, Argument::Synthetic) + && self.constructor_kind == Some(ConstructorCallableKind::New) + && matches!( + parameter.annotated_type().resolve_type_alias(db), + Type::SubclassOf(subclass_of) if subclass_of.into_type_var().is_some() + ); + + let mut expected_ty = matched_parameter + .expected_type + .unwrap_or_else(|| parameter.annotated_type()); + // basedpython: the *argument* reading of the specialization — a type variable the call + // left unsolved stays gradual here rather than becoming `Never`, which in a contravariant + // parameter position would say the parameter accepts nothing if let Some(specialization) = self.argument_specialization() { - argument_type = argument_type.apply_specialization(self.db, specialization); - expected_ty = expected_ty.apply_specialization(self.db, specialization); + if !constructor_receiver { + argument_type = argument_type.apply_specialization(db, specialization); + } + expected_ty = expected_ty.apply_specialization(db, specialization); } // Some typing special forms are valid class-info arguments at runtime but are not @@ -5894,7 +6548,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { && matches!( self.signature_type .as_function_literal() - .and_then(|function| function.known(self.db)), + .and_then(|function| function.known(db)), Some(KnownFunction::IsInstance | KnownFunction::IsSubclass) ) && argument_type @@ -5916,8 +6570,10 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { // building them in an earlier separate step. // // TODO: handle starred annotations, e.g. `*args: *Ts` or `*args: *tuple[int, *tuple[str, ...]]` + // An unresolved `*Ts` still has no per-element expected type. let type_error = if self.constraint_set_errors[argument_index] - || parameter.has_starred_annotation() + || constructor_receiver + || (parameter.has_starred_annotation() && matched_parameter.expected_type.is_none()) || is_valid_isinstance_target() { false @@ -5937,13 +6593,19 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { let key = overlapping.type_argument(self.db); !key.is_never() && argument_type - .when_assignable_to(self.db, key, constraints, self.inferable_typevars) - .is_never_satisfied(self.db) - && key.is_disjoint_from(self.db, argument_type) + .when_assignable_to(db, self.env, key, constraints, self.inferable_typevars) + .is_never_satisfied(db, self.env) + && key.is_disjoint_from(db, self.env, argument_type) } else { argument_type - .when_assignable_to(self.db, expected_ty, constraints, self.inferable_typevars) - .is_never_satisfied(self.db) + .when_assignable_to( + db, + self.env, + expected_ty, + constraints, + self.inferable_typevars, + ) + .is_never_satisfied(db, self.env) && !self.should_defer_typevartuple_callable_check( parameter.annotated_type(), expected_ty, @@ -5959,6 +6621,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { expected_ty, provided_ty: argument_type, provenance: matched_parameter.provenance, + parameter_source: None, }); } // We still update the actual type of the parameter in this binding to match the argument, @@ -5980,7 +6643,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { { builder.add_in_place(argument_type); } else if let Some(existing) = self.parameter_tys[parameter_index] { - let mut builder = UnionBuilder::new(self.db); + let mut builder = UnionBuilder::new(db, self.env); builder.add_in_place(existing); builder.add_in_place(argument_type); if self.parameter_ty_builders.is_empty() { @@ -6000,6 +6663,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { /// keyword arguments, so the type variable is solved from all of them at once rather than /// per-argument. fn keyword_aggregate_kind(&self, parameter: &Parameter<'db>) -> Option { + let env = self.env; if !parameter.is_keyword_variadic() { return None; } @@ -6009,7 +6673,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { { Some(KeywordAggregateKind::ParameterPack) } - annotated if annotated.is_typed_dict_bounded_typevar(self.db) => { + annotated if annotated.is_typed_dict_bounded_typevar(self.db, env) => { Some(KeywordAggregateKind::TypedDict) } _ => None, @@ -6033,16 +6697,17 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { expected_type: Type<'db>, argument_type: Type<'db>, ) -> bool { - let Some(declared_callables) = declared_type.try_upcast_to_callable(self.db) else { + let db = self.db; + let Some(declared_callables) = declared_type.try_upcast_to_callable(db, self.env) else { return false; }; let parameters_contain_typevartuple = declared_callables.iter().any(|callable| { - callable.signatures(self.db).iter().any(|signature| { + callable.signatures(db).iter().any(|signature| { signature.parameters().iter().any(|parameter| { - any_over_type(self.db, parameter.annotated_type(), false, |ty| { + any_over_type(db, self.env, parameter.annotated_type(), false, |ty| { matches!( ty, - Type::TypeVar(typevar) if typevar.is_typevartuple(self.db) + Type::TypeVar(typevar) if typevar.is_typevartuple(db) ) }) }) @@ -6052,28 +6717,28 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { return false; } - let Some(argument_callables) = argument_type.try_upcast_to_callable(self.db) else { + let Some(argument_callables) = argument_type.try_upcast_to_callable(db, self.env) else { return false; }; if argument_callables .iter() - .any(|callable| callable.signatures(self.db).overloads.len() > 1) + .any(|callable| callable.signatures(db).overloads.len() > 1) { return true; } let argument_is_generic = argument_callables.iter().any(|callable| { callable - .signatures(self.db) + .signatures(db) .iter() .any(|signature| signature.generic_context.is_some()) }); argument_is_generic && expected_type - .try_upcast_to_callable(self.db) + .try_upcast_to_callable(db, self.env) .is_some_and(|callables| { callables.iter().any(|callable| { - callable.signatures(self.db).iter().any(|signature| { + callable.signatures(db).iter().any(|signature| { signature .parameters() .variadic() @@ -6086,6 +6751,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { } fn check_argument_types(&mut self, constraints: &ConstraintSetBuilder<'db>) { + let db = self.db; let paramspec = self.signature.parameters().as_paramspec_with_prefix(); let paramspec_component_start = paramspec.and_then(|(prefix, paramspec)| { let prefix_len = prefix.len(); @@ -6111,7 +6777,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { return false; }; - typevar.is_paramspec(self.db) + typevar.is_paramspec(db) }); if has_paramspec_component_argument @@ -6208,18 +6874,19 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { paramspec_arguments: Option<&[(usize, Option)]>, paramspec: BoundTypeVarInstance<'db>, ) -> bool { + let db = self.db; let Some(Type::Callable(callable)) = self .specialization() - .and_then(|specialization| specialization.get(self.db, paramspec)) + .and_then(|specialization| specialization.get(db, paramspec)) else { return false; }; - if callable.kind(self.db) != CallableTypeKind::ParamSpecValue { + if callable.kind(db) != CallableTypeKind::ParamSpecValue { return false; } - let signatures = &callable.signatures(self.db).overloads; + let signatures = &callable.signatures(db).overloads; if signatures.is_empty() { return false; } @@ -6238,13 +6905,13 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { }; let error_argument_indices = error_argument_indices.as_deref(); - // Create Bindings with all overloads and perform full overload resolution let callable_binding = CallableBinding::from_overloads(self.signature_type, signatures.iter().cloned()); let bindings = match Bindings::from(callable_binding) - .match_parameters(self.db, &sub_arguments) + .match_parameters(db, self.env, &sub_arguments) .check_types( - self.db, + db, + self.env, constraints, &sub_arguments, self.call_expression_tcx, @@ -6259,13 +6926,43 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { .single_element() .expect("ParamSpec sub-call should only contain a single CallableBinding"); - let mut extend_errors = |errors: &[BindingError<'db>]| { - self.errors.extend( - errors - .iter() - .cloned() - .map(|err| err.maybe_remap_argument_indices(error_argument_indices)), - ); + let mut extend_errors = |binding: &Binding<'db>| { + let parameter_source = binding + .errors + .iter() + .find(|error| matches!(error, BindingError::InvalidArgumentType { .. })) + .and_then(|_| { + self.paramspec_parameter_source(paramspec, binding.source_overload_index()) + }); + let argument_matches = self.argument_matches; + + self.errors + .extend(binding.errors.iter().cloned().map(|mut error| { + if let BindingError::InvalidArgumentType { + parameter, + argument_index, + parameter_source: error_parameter_source, + .. + } = &mut error + && error_parameter_source.is_none() + { + if let Some(parameter_source) = parameter_source + && parameter_source + .source_parameter_index(db, parameter) + .is_some() + { + *error_parameter_source = Some(parameter_source); + } else if let Some(parameter_index) = argument_index + .and_then(|index| paramspec_arguments?.get(index)) + .and_then(|(index, _)| argument_matches[*index].parameters.first()) + .map(|parameter| parameter.index) + { + parameter.signature_parameter_index = parameter_index; + } + } + + error.maybe_remap_argument_indices(error_argument_indices) + })); }; let mut matching_overloads = callable_binding.matching_overloads(); @@ -6274,7 +6971,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { if let [binding] = callable_binding.overloads() { // This is not an overloaded function, so we can propagate its errors to the // outer bindings. - extend_errors(&binding.errors); + extend_errors(binding); } else { let index = callable_binding .best_failing_overload_index( @@ -6283,20 +6980,20 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { .unwrap_or(0); // TODO: We should also update the specialization for the `ParamSpec` to reflect // the matching overload here. - extend_errors(&callable_binding.overloads()[index].errors); + extend_errors(&callable_binding.overloads()[index]); } } (Some((_, binding)), None) => { // TODO: We should also update the specialization for the `ParamSpec` to reflect the // matching overload here. - extend_errors(&binding.errors); + extend_errors(binding); } (Some(_), Some(_)) => { if !matches!( callable_binding.overload_call_return_type, Some(OverloadCallReturnType::ArgumentTypeExpansion(_)) ) { - extend_errors(&callable_binding.overloads()[0].errors); + extend_errors(&callable_binding.overloads()[0]); } } } @@ -6339,7 +7036,8 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { argument_type: Type<'db>, paramspec_component_start: Option, ) { - if extract_unpacked_typed_dict_from_value_type(self.db, argument_type).is_some() { + let db = self.db; + if extract_unpacked_typed_dict_from_value_type(db, self.env, argument_type).is_some() { self.check_variadic_argument_type( constraints, argument_index, @@ -6350,28 +7048,28 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { return; } - let value_type_paramspec = - if let Some(paramspec) = argument_type.as_paramspec_typevar(self.db) { - Some(paramspec) - } else { - match validate_keyword_unpack_key_type( - self.db, - constraints, - argument_type, - self.inferable_typevars, - ) { - KeywordUnpackKeyTypeCheck::NotApplicable => return, - KeywordUnpackKeyTypeCheck::Valid => {} - KeywordUnpackKeyTypeCheck::Invalid(provided_ty) => { - self.errors.push(BindingError::InvalidKeyType { - argument_index: adjusted_argument_index, - provided_ty, - }); - } + let value_type_paramspec = if let Some(paramspec) = argument_type.as_paramspec_typevar(db) { + Some(paramspec) + } else { + match validate_keyword_unpack_key_type( + db, + self.env, + constraints, + argument_type, + self.inferable_typevars, + ) { + KeywordUnpackKeyTypeCheck::NotApplicable => return, + KeywordUnpackKeyTypeCheck::Valid => {} + KeywordUnpackKeyTypeCheck::Invalid(provided_ty) => { + self.errors.push(BindingError::InvalidKeyType { + argument_index: adjusted_argument_index, + provided_ty, + }); } + } - None - }; + None + }; for matched_parameter in self.argument_matches[argument_index].iter() { let parameter_index = matched_parameter.index; @@ -6387,7 +7085,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { .map(Name::as_str); argument_type - .getitem_dunder_call(self.db, parameter_name) + .getitem_dunder_call(db, self.env, parameter_name) .unwrap_or(Type::unknown()) }; @@ -6402,13 +7100,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { } } - fn finish( - self, - ) -> ( - InferableTypeVars<'db>, - Option>, - Type<'db>, - ) { + fn finish(self) -> (TypeVarSet<'db>, Option>, Type<'db>) { for (parameter_ty, builder) in self .parameter_tys .iter_mut() @@ -6449,6 +7141,9 @@ pub struct MatchedParameter<'db> { /// matching runs. argument_type: Option>, + /// The tuple element expected at this position in an unpacked variadic parameter. + expected_type: Option>, + /// Why this parameter match exists. provenance: InvalidArgumentTypeProvenance, } @@ -6573,9 +7268,10 @@ impl<'db> ArgumentTypeContext<'db> { #[derive(Debug, Clone, Copy)] pub(crate) struct UnknownParameterNameError; -#[derive(Clone, Copy)] +#[derive(Clone)] struct ParamSpecArgumentContext<'a, 'call, 'db> { db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, constraints: &'a ConstraintSetBuilder<'db>, binding: &'a CallableBinding<'db>, callable: CallableType<'db>, @@ -6587,16 +7283,22 @@ struct ParamSpecArgumentContext<'a, 'call, 'db> { /// Returns the number of occurrences of inferable type variables in the provided type. fn inferable_typevar_occurrences<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, ) -> usize { - struct InferableTypeVarVisitor<'db> { - inferable: InferableTypeVars<'db>, + struct InferableTypeVarVisitor<'a, 'db> { + env: &'a ProgramEnvironment<'db>, + inferable: TypeVarSet<'db>, count: Cell, stack: RefCell; 8]>>, } - impl<'db> TypeVisitor<'db> for InferableTypeVarVisitor<'db> { + impl<'db> TypeVisitor<'db> for InferableTypeVarVisitor<'_, 'db> { + fn program_environment(&self) -> &ProgramEnvironment<'db> { + self.env + } + fn should_visit_lazy_type_attributes(&self) -> bool { false } @@ -6628,6 +7330,7 @@ fn inferable_typevar_occurrences<'db>( } let visitor = InferableTypeVarVisitor { + env, inferable, count: Cell::new(0), stack: RefCell::default(), @@ -6654,6 +7357,7 @@ fn inferable_typevar_occurrences<'db>( /// argument checking, which needs the gradual reading — see `Binding::argument_specialization`. fn call_specialization<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, signature: &Signature<'db>, inference: TypeVarInference<'db>, ) -> Specialization<'db> { @@ -6676,7 +7380,7 @@ fn call_specialization<'db>( || typevar.is_parameter_pack(db) || typevar.is_typevartuple(db) || !matches!( - typevar.positional_variance(db), + typevar.positional_variance(db, env), TypeVarVariance::Covariant | TypeVarVariance::Bivariant ) { @@ -6711,7 +7415,7 @@ pub(crate) struct Binding<'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>, + signature_type: Type<'db>, /// Return type of the call. pub(crate) return_ty: Type<'db>, @@ -6720,7 +7424,7 @@ pub(crate) struct Binding<'db> { constructor_context: Option>, /// The inferable typevars in this signature. - inferable_typevars: InferableTypeVars<'db>, + inferable_typevars: TypeVarSet<'db>, /// The type-variable inference result for this binding, if the callable is generic. inference: Option>, @@ -6749,6 +7453,61 @@ pub(crate) struct Binding<'db> { } impl<'db> Binding<'db> { + /// Checks the getter invoked by `property.__get__`, retaining its error and recovery type. + fn check_property_getter( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + getter: Type<'db>, + instance: Type<'db>, + argument_index_offset: usize, + ) { + match getter.try_call(db, env, &CallArguments::positional([instance])) { + Ok(bindings) => self.set_return_type(bindings.return_type(db, env)), + Err(CallError(_, bindings)) => { + self.set_return_type(bindings.return_type(db, env)); + self.errors.push(BindingError::PropertyGetterCallError( + PropertyAccessorCallError { + bindings, + argument_index_offset, + }, + )); + } + } + } + + fn check_property_setter( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + setter: Type<'db>, + instance: Type<'db>, + value: Type<'db>, + argument_index_offset: usize, + ) { + match setter.try_call(db, env, &CallArguments::positional([instance, value])) { + Ok(bindings) => { + let return_ty = bindings.return_type(db, env); + // `property.__set__` returns `None` for ordinary setters, but preserving `Never` + // keeps non-returning setters divergent. + self.set_return_type(if return_ty.is_never() { + return_ty + } else { + Type::none(db, env) + }); + } + Err(CallError(_, bindings)) => { + self.errors.push(BindingError::PropertySetterCallError( + PropertyAccessorCallError { + bindings, + argument_index_offset, + }, + )); + self.set_return_type(Type::unknown()); + } + } + } + pub(crate) fn single(signature_type: Type<'db>, signature: Signature<'db>) -> Binding<'db> { let return_ty = signature.return_ty; Binding { @@ -6759,7 +7518,7 @@ impl<'db> Binding<'db> { signature_type, return_ty, constructor_context: None, - inferable_typevars: InferableTypeVars::None, + inferable_typevars: TypeVarSet::None, inference: None, argument_matches: Box::from([]), variadic_argument_matched_to_variadic_parameter: false, @@ -6773,8 +7532,8 @@ impl<'db> Binding<'db> { /// /// Specializing a call can introduce `Self` that was not in the signature when the receiver /// was bound, because a class type parameter's default can be `Self` (`class C[T = Self]`). - fn rebind_self_in_return_type(&mut self, db: &'db dyn Db) { - if !self.return_ty.contains_self(db) { + fn rebind_self_in_return_type(&mut self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) { + if !self.return_ty.contains_self(db, env) { return; } let receiver_self_type = match (self.receiver_self_type, self.callable_type) { @@ -6784,7 +7543,8 @@ impl<'db> Binding<'db> { }; self.return_ty = self.return_ty.apply_type_mapping( db, - &TypeMapping::BindSelf(SelfBinding::new(db, receiver_self_type, None)), + env, + &TypeMapping::BindSelf(SelfBinding::new(db, env, receiver_self_type, None)), TypeContext::default(), ); } @@ -6807,6 +7567,7 @@ impl<'db> Binding<'db> { pub(crate) fn typevar_occurrences_for_parameter( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding: &CallableBinding<'db>, argument_index: usize, ) -> usize { @@ -6825,6 +7586,7 @@ impl<'db> Binding<'db> { .map(|parameter| { inferable_typevar_occurrences( db, + env, self.signature.parameters()[parameter.index].annotated_type(), inferable_typevars, ) @@ -6879,17 +7641,19 @@ impl<'db> Binding<'db> { /// ``` fn paramspec_argument_context( &self, - context: ParamSpecArgumentContext<'_, '_, 'db>, + context: &ParamSpecArgumentContext<'_, '_, 'db>, ) -> Option> { let ParamSpecArgumentContext { db, + env, constraints, binding, callable, arguments_types, argument_index, call_expression_tcx, - } = context; + } = *context; + let (prefix, _) = self.signature.parameters().as_paramspec_with_prefix()?; let paramspec_argument_indices = self.paramspec_call_argument_indices(binding, prefix.len()); @@ -6908,9 +7672,10 @@ impl<'db> Binding<'db> { sub_arguments.clear_types(sub_argument_index); let mut specialized_bindings = - Bindings::from(specialized_binding).match_parameters(db, &sub_arguments); + Bindings::from(specialized_binding).match_parameters(db, env, &sub_arguments); let _ = specialized_bindings.check_types_impl( db, + env, constraints, &sub_arguments, call_expression_tcx, @@ -6936,13 +7701,14 @@ impl<'db> Binding<'db> { [specialized_parameter.index] .annotated_type(); let parameter_type = specialized_overload - .specialization(db) + .specialization(db, env) .map_or(parameter_type, |specialization| { parameter_type.apply_specialization(db, specialization) }); - (!parameter_type.has_dynamic(db) && !parameter_type.has_typevar_or_typevar_instance(db)) - .then_some(parameter_type) + (!parameter_type.has_dynamic(db, env) + && !parameter_type.has_typevar_or_typevar_instance(db, env)) + .then_some(parameter_type) } /// Returns the type context to use for bidirectional inference of a source call argument, @@ -6960,6 +7726,7 @@ impl<'db> Binding<'db> { pub(crate) fn argument_type_context( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, constraints: &ConstraintSetBuilder<'db>, binding: &CallableBinding<'db>, arguments_types: &CallArguments<'_, 'db>, @@ -6968,16 +7735,18 @@ impl<'db> Binding<'db> { specialization: Option>, ) -> Option> { let argument_matches = self.matched_argument_for_call_argument(binding, argument_index)?; - let [parameter] = argument_matches.parameters.as_slice() else { + let [matched_parameter] = argument_matches.parameters.as_slice() else { return None; }; - let parameter = &self.signature.parameters()[parameter.index]; - let mut parameter_type = parameter.annotated_type(); - let original_parameter_type = parameter_type; + let parameter = &self.signature.parameters()[matched_parameter.index]; + let original_parameter_type = parameter.annotated_type(); + let mut parameter_type = matched_parameter + .expected_type + .unwrap_or(original_parameter_type); let paramspec_callable = |paramspec| { let Type::Callable(callable) = self - .specialization(db) + .specialization(db, env) .and_then(|specialization| specialization.get(db, paramspec)) .or_else(|| { specialization.and_then(|specialization| specialization.get(db, paramspec)) @@ -6996,7 +7765,7 @@ impl<'db> Binding<'db> { if let Type::TypeVar(typevar) = parameter_type && !typevar.is_paramspec(db) && let Some(TypeVarBoundOrConstraints::UpperBound(bound)) = - typevar.typevar(db).bound_or_constraints(db) + typevar.typevar(db).bound_or_constraints(db, env) { return Some(ArgumentTypeContext::standard( original_parameter_type, @@ -7020,8 +7789,9 @@ impl<'db> Binding<'db> { if let Some(paramspec) = paramspec && let Some(callable) = paramspec_callable(paramspec) && let Some(specialized_parameter_type) = - self.paramspec_argument_context(ParamSpecArgumentContext { + self.paramspec_argument_context(&ParamSpecArgumentContext { db, + env, constraints, binding, callable, @@ -7055,6 +7825,7 @@ impl<'db> Binding<'db> { &self, db: &'db dyn Db, file: ruff_db::files::File, + env: &ProgramEnvironment<'db>, constraints: &ConstraintSetBuilder<'db>, call_expression_tcx: TypeContext<'db>, ) -> Option> { @@ -7068,19 +7839,24 @@ impl<'db> Binding<'db> { .unwrap_or(self.signature.return_ty); let path_bounds = normalized_return_ty.assignable_solutions_with_inferable( db, + env, declared_return_ty, generic_context.inferable_typevars(db), ); - if let Solutions::Constrained(solutions) = path_bounds.solve(db, constraints) { + if let Solutions::Constrained(solutions) = path_bounds.solve(db, env, constraints) { for solution in solutions { for binding in solution { let identity = binding.bound_typevar.identity(db); return_type_solutions .entry(identity) .and_modify(|existing| { - *existing = - UnionType::from_two_elements(db, *existing, binding.solution); + *existing = UnionType::from_two_elements( + db, + env, + *existing, + binding.solution, + ); }) .or_insert(binding.solution); } @@ -7102,13 +7878,13 @@ impl<'db> Binding<'db> { let call_expression_constraints = return_type_solutions.get(&identity).copied(); let argument_constraints = self - .specialization(db) + .specialization(db, env) .and_then(|specialization| specialization.get(db, typevar)) - .filter(|ty| !ty.has_dynamic(db)) + .filter(|ty| !ty.has_dynamic(db, env)) // the *argument's* file: this widens a solution inferred from // one, and a caller whose numeric model is strict must not get // `int | float` back as the context its argument is read against - .map(|ty| ty.promote_in(db, file)); + .map(|ty| ty.promote_in(db, env, file)); // TODO: We should similarly combine both the call expression and argument constraints // here. We currently only rely on argument constraints when there is no explicit declared @@ -7142,16 +7918,25 @@ impl<'db> Binding<'db> { } } - fn freshen_bound_typevars(&mut self, db: &'db dyn Db, delta: u32) { + fn freshen_bound_typevars( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + delta: u32, + ) { if self.signature.generic_context.is_none() { return; } - - self.signature = self.signature.freshen_bound_typevars(db, delta); + self.signature = self.signature.freshen_bound_typevars(db, env, delta); self.return_ty = self.initial_return_type(db); } - fn match_parameters(&mut self, db: &'db dyn Db, arguments: &CallArguments<'_, 'db>) { + fn match_parameters( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + arguments: &CallArguments<'_, 'db>, + ) { let parameters = self.signature.parameters(); let mut matcher = ArgumentMatcher::new(arguments, parameters, &mut self.errors); let mut keywords_arguments = vec![]; @@ -7166,6 +7951,7 @@ impl<'db> Binding<'db> { Argument::Variadic => { let _ = matcher.match_variadic( db, + env, argument_index, argument, // Splatted arguments are inferred without type context. @@ -7180,6 +7966,7 @@ impl<'db> Binding<'db> { for (keywords_index, keywords_type) in keywords_arguments { matcher.match_keyword_variadic( db, + env, keywords_index, // Splatted arguments are inferred without type context. keywords_type.get_default(), @@ -7188,12 +7975,13 @@ impl<'db> Binding<'db> { self.parameter_tys = vec![None; parameters.len()].into_boxed_slice(); self.variadic_argument_matched_to_variadic_parameter = matcher.variadic_argument_matched_to_variadic_parameter; - self.argument_matches = matcher.finish(); + self.argument_matches = matcher.finish(db, env); } fn check_types( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, constraints: &ConstraintSetBuilder<'db>, arguments: &CallArguments<'_, 'db>, call_expression_tcx: TypeContext<'db>, @@ -7212,13 +8000,15 @@ impl<'db> Binding<'db> { .iter() .all(|parameter| parameter.is_variadic() || parameter.is_keyword_variadic()) { - self.check_keyword_unpack_key_types(db, constraints, arguments); + self.check_keyword_unpack_key_types(db, env, constraints, arguments); return; } let mut checker = ArgumentTypeChecker::new( db, + env, self.signature_type, + self.constructor_context.map(ConstructorContext::kind), &self.signature, arguments, &self.argument_matches, @@ -7236,12 +8026,13 @@ impl<'db> Binding<'db> { (self.inferable_typevars, self.inference, self.return_ty) = checker.finish(); // Inference can substitute a class type parameter whose default is `Self`, so a `Self` // that was not there when the receiver was bound can appear here. - self.rebind_self_in_return_type(db); + self.rebind_self_in_return_type(db, env); } fn check_keyword_unpack_key_types( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, constraints: &ConstraintSetBuilder<'db>, arguments: &CallArguments<'_, 'db>, ) { @@ -7263,9 +8054,10 @@ impl<'db> Binding<'db> { if let KeywordUnpackKeyTypeCheck::Invalid(provided_ty) = validate_keyword_unpack_key_type( db, + env, constraints, argument_type, - InferableTypeVars::None, + TypeVarSet::None, ) { self.errors.push(BindingError::InvalidKeyType { @@ -7280,7 +8072,7 @@ impl<'db> Binding<'db> { self.return_ty = return_ty; } - pub(crate) fn return_type(&self) -> Type<'db> { + fn return_type(&self) -> Type<'db> { self.return_ty } @@ -7305,6 +8097,7 @@ impl<'db> Binding<'db> { fn functools_partial_return_type<'a>( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, call_arguments: &CallArguments<'a, 'db>, ) -> Option> { // `partial(...)` receives the wrapped callable as its first explicit argument (after @@ -7315,15 +8108,16 @@ impl<'db> Binding<'db> { }; let imprecise_return_type = self.return_ty; let failed_synthesis_return_type = - KnownClass::FunctoolsPartial.to_specialized_instance(db, &[Type::unknown()]); + KnownClass::FunctoolsPartial.to_specialized_instance(db, env, &[Type::unknown()]); let (bound_call_arguments, partial_bindings, can_synthesize_signature) = - Bindings::functools_partial_matched_bindings(db, func_ty, call_arguments)?; + Bindings::functools_partial_matched_bindings(db, env, func_ty, call_arguments)?; // Reuse call-binding machinery to resolve which wrapped overloads are compatible with // bound arguments and to surface binding diagnostics. let partial_bindings = match partial_bindings.check_types( db, + env, &ConstraintSetBuilder::new(), &bound_call_arguments, TypeContext::default(), @@ -7333,7 +8127,7 @@ impl<'db> Binding<'db> { Err(CallError(_, bindings)) => *bindings, }; let new_return_type = - partial_bindings.functools_partial_type(db, func_ty, self, &bound_call_arguments); + partial_bindings.functools_partial_type(db, env, func_ty, self, &bound_call_arguments); Some(if !can_synthesize_signature { imprecise_return_type @@ -7359,6 +8153,7 @@ impl<'db> Binding<'db> { fn resolve_context_arguments( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, scope: ScopeId<'db>, call_offset: ruff_text_size::TextSize, ) { @@ -7376,13 +8171,20 @@ impl<'db> Binding<'db> { continue; }; missing.0.retain(|parameter_context| { - let Some(parameter) = parameters.get(parameter_context.index) else { + let Some(parameter) = parameters.get(parameter_context.signature_parameter_index) + else { return true; }; if !parameter.is_context() { return true; } - match resolve_context_argument(db, scope, call_offset, parameter.annotated_type()) { + match resolve_context_argument( + db, + env, + scope, + call_offset, + parameter.annotated_type(), + ) { ContextResolution::Resolved { .. } => false, ContextResolution::NotFound => { context_errors.push(BindingError::NoContextArgument { @@ -7480,8 +8282,8 @@ impl<'db> Binding<'db> { /// Packages the information needed to synthesize this overload's reduced partial signature. fn partial_signature_application( &self, - arguments: &CallArguments<'_, 'db>, db: &'db dyn Db, + arguments: &CallArguments<'_, 'db>, ) -> PartialSignatureApplication<'db> { PartialSignatureApplication::new( self.signature.clone(), @@ -7495,7 +8297,7 @@ impl<'db> Binding<'db> { /// that parameter. /// /// Returns an error if the parameter name is not found. - pub(crate) fn parameter_type_by_name( + fn parameter_type_by_name( &self, parameter_name: &str, fallback_to_default: bool, @@ -7548,7 +8350,7 @@ impl<'db> Binding<'db> { fn report_diagnostics( &self, - context: &InferContext<'db, '_>, + context: &CallDiagnosticContext<'_, '_, 'db, '_>, node: ast::AnyNodeRef, callable_ty: Type<'db>, callable_description: Option<&CallableDescription>, @@ -7609,9 +8411,13 @@ impl<'db> Binding<'db> { &self.argument_matches } - pub(crate) fn specialization(&self, db: &'db dyn Db) -> Option> { + pub(crate) fn specialization( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { self.inference - .map(|inference| call_specialization(db, &self.signature, inference)) + .map(|inference| call_specialization(db, env, &self.signature, inference)) } pub(crate) fn errors(&self) -> &[BindingError<'db>] { @@ -7621,7 +8427,7 @@ impl<'db> Binding<'db> { /// Resets the state of this binding to its initial state. fn reset(&mut self, db: &'db dyn Db) { self.return_ty = self.initial_return_type(db); - self.inferable_typevars = InferableTypeVars::None; + self.inferable_typevars = TypeVarSet::None; self.inference = None; self.argument_matches = Box::from([]); self.parameter_tys = Box::from([]); @@ -7632,7 +8438,7 @@ impl<'db> Binding<'db> { #[derive(Clone, Debug)] struct BindingSnapshot<'db> { return_ty: Type<'db>, - inferable_typevars: InferableTypeVars<'db>, + inferable_typevars: TypeVarSet<'db>, inference: Option>, argument_matches: Box<[MatchedArgument<'db>]>, parameter_tys: Box<[Option>]>, @@ -7738,8 +8544,8 @@ impl CallableBindingSnapshotter { /// Describes a callable for the purposes of diagnostics. #[derive(Debug)] pub(crate) struct CallableDescription<'a> { - pub(crate) name: Cow<'a, str>, - pub(crate) kind: Option<&'static str>, + name: Cow<'a, str>, + kind: Option<&'static str>, } impl<'db> CallableDescription<'db> { @@ -7751,8 +8557,7 @@ impl<'db> CallableDescription<'db> { db: &'db dyn Db, function: FunctionType<'db>, ) -> Cow<'db, str> { - let file = function.file(db); - let semantic_index = semantic_index(db, file); + let semantic_index = semantic_index(db, function.program_file(db)); let enclosing_scope = semantic_index.scope(function.definition(db).file_scope(db)); if let Some(class_node) = enclosing_scope.node().as_class() && let Some(class) = @@ -7777,6 +8582,12 @@ impl<'db> CallableDescription<'db> { kind: Some("class"), name: Cow::Borrowed(class_type.name(db)), }), + Type::SubclassOf(subclass) if let Some(typevar) = subclass.into_type_var() => { + Some(CallableDescription { + kind: None, + name: Cow::Owned(format!("type[{}]", typevar.name(db))), + }) + } Type::BoundMethod(bound_method) => Some({ let function = bound_method.function(db); let kind = if function.name(db) == "__init__" { @@ -7828,9 +8639,10 @@ impl<'db> CallableDescription<'db> { /// the source at all fn constructed_class( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, instance_type: Type<'db>, ) -> Option> { - let class = instance_type.as_nominal_instance()?.class(db); + let class = instance_type.as_nominal_instance()?.class(db, env); Some(CallableDescription { kind: Some("class"), name: Cow::Borrowed(class.class_literal(db).name(db)), @@ -7852,7 +8664,12 @@ impl std::fmt::Display for CallableDescription<'_> { #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) struct ParameterContext { name: Option>, - index: usize, + + /// Position in the current, possibly specialized or expanded signature. + signature_parameter_index: usize, + + /// Position of the original source declaration, before any parameter expansion. + source_parameter_index: Option, /// basedpython: the parameter is the implicit receiver of a `T.() -> R` /// callable. It has no name to report, so say what it is instead @@ -7870,7 +8687,8 @@ impl ParameterContext { name: parameter .display_name() .map(ParameterDisplayName::into_owned), - index, + signature_parameter_index: index, + source_parameter_index: parameter.source_parameter_index(), is_receiver: parameter.is_receiver(), positional, } @@ -7881,14 +8699,14 @@ 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) + write!(f, "{} (`{name}`)", self.signature_parameter_index + 1) } else { write!(f, "`{name}`") } } else if self.is_receiver { - write!(f, "{} (the receiver)", self.index + 1) + write!(f, "{} (the receiver)", self.signature_parameter_index + 1) } else { - write!(f, "{}", self.index + 1) + write!(f, "{}", self.signature_parameter_index + 1) } } } @@ -7910,6 +8728,64 @@ impl std::fmt::Display for ParameterContexts { } } +/// The function and source offsets used to locate a forwarded `ParamSpec` parameter. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct ForwardedParameterSource<'db> { + function: FunctionType<'db>, + is_bound_method: bool, + parameter_index_offset: usize, + overload_index: usize, +} + +impl<'db> ForwardedParameterSource<'db> { + /// Locates the source parameter that accepted a forwarded argument. + /// + /// An unpacked variadic annotation can expand one source declaration into several callable + /// parameters. Map each expanded parameter back to its shared `*args` or `**kwargs` + /// declaration instead of interpreting its position as a source parameter index. Looking up + /// the original position through the cached signature also avoids making the caller depend on + /// the callback's entire AST. + /// + /// ```python + /// from typing import Unpack + /// + /// def callback(*args: Unpack[tuple[int, str]]) -> None: ... + /// ``` + fn source_parameter_index( + self, + db: &'db dyn Db, + parameter: &ParameterContext, + ) -> Option { + let parameter_index = parameter + .source_parameter_index + .unwrap_or(parameter.signature_parameter_index + self.parameter_index_offset); + self.function + .signature(db) + .overloads + .get(self.overload_index)? + .parameters() + .iter() + .any(|source_parameter| { + source_parameter.source_parameter_index() == Some(parameter_index) + }) + .then_some(parameter_index) + } + + /// Locates the matched source overload after restoring omitted receiver and prefix parameters. + fn parameter_span(self, db: &'db dyn Db, parameter: &ParameterContext) -> (Span, Span) { + let parameter_index = self + .source_parameter_index(db, parameter) + .unwrap_or(parameter.signature_parameter_index + self.parameter_index_offset); + let (overloads, implementation) = self.function.overloads_and_implementation(db); + overloads + .get(self.overload_index) + .copied() + .or(implementation) + .map(|overload| overload.parameter_span(db, Some(parameter_index))) + .unwrap_or_else(|| self.function.parameter_span(db, Some(parameter_index))) + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum InvalidArgumentTypeProvenance { Argument, @@ -7938,6 +8814,8 @@ pub(crate) enum BindingError<'db> { expected_ty: Type<'db>, provided_ty: Type<'db>, provenance: InvalidArgumentTypeProvenance, + /// The callable that actually declared the parameter, when reached through a `ParamSpec`. + parameter_source: Option>, }, /// The type of the keyword-variadic argument's key is not `str`. InvalidKeyType { @@ -7993,11 +8871,14 @@ pub(crate) enum BindingError<'db> { error: SpecializationError<'db>, argument_index: Option, }, + PropertyHasNoGetter(PropertyInstanceType<'db>), PropertyHasNoSetter(PropertyInstanceType<'db>), PropertyHasNoDeleter(PropertyInstanceType<'db>), + PropertyGetterCallError(PropertyAccessorCallError<'db>), + PropertySetterCallError(PropertyAccessorCallError<'db>), /// 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. + /// We use this variant to report errors in `property.__delete__`, which can occur when the + /// call to the underlying deleter 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. @@ -8012,6 +8893,31 @@ pub(crate) enum BindingError<'db> { InvalidDataclassArgument(InvalidDataclassArgument), } +#[derive(Clone, Debug)] +pub(crate) struct PropertyAccessorCallError<'db> { + bindings: Box>, + argument_index_offset: usize, +} + +impl PartialEq for PropertyAccessorCallError<'_> { + fn eq(&self, other: &Self) -> bool { + self.argument_index_offset == other.argument_index_offset + && self.bindings.callable_type() == other.bindings.callable_type() + && self + .bindings + .iter_flat() + .flatten() + .flat_map(Binding::errors) + .eq(other + .bindings + .iter_flat() + .flatten() + .flat_map(Binding::errors)) + } +} + +impl Eq for PropertyAccessorCallError<'_> {} + impl BindingError<'_> { /// Returns whether this error is relevant to `functools.partial(...)` construction. /// @@ -8035,7 +8941,7 @@ impl BindingError<'_> { ) } - pub(crate) fn maybe_apply_argument_index_offset(mut self, offset: Option) -> Self { + fn maybe_apply_argument_index_offset(mut self, offset: Option) -> Self { if let Some(offset) = offset { self.apply_argument_index_offset(offset); } @@ -8098,8 +9004,11 @@ impl BindingError<'_> { | BindingError::NoContextArgument { .. } | BindingError::AmbiguousContextArgument { .. } | BindingError::UnmatchedOverload + | BindingError::PropertyHasNoGetter(..) | BindingError::PropertyHasNoSetter(..) - | BindingError::PropertyHasNoDeleter(..) => {} + | BindingError::PropertyHasNoDeleter(..) + | BindingError::PropertyGetterCallError(..) + | BindingError::PropertySetterCallError(..) => {} } } @@ -8109,7 +9018,7 @@ impl BindingError<'_> { /// sub-call for a `ParamSpec`, where the argument indices are relative to the sub-call's /// argument list rather than the original call's argument list. The `offset` should be the /// number of arguments in the original call that were matched before the `ParamSpec` component. - pub(crate) fn apply_argument_index_offset(&mut self, offset: usize) { + fn apply_argument_index_offset(&mut self, offset: usize) { self.map_argument_indices(|argument_index| argument_index.map(|index| index + offset)); } } @@ -8157,8 +9066,11 @@ impl<'db> BindingError<'db> { // Semantic errors: the overload matched, but the usage is invalid Self::InvalidDataclassApplication(_) | Self::InvalidDataclassArgument(_) + | Self::PropertyHasNoGetter(_) | Self::PropertyHasNoSetter(_) | Self::PropertyHasNoDeleter(_) + | Self::PropertyGetterCallError(_) + | Self::PropertySetterCallError(_) | Self::CalledTopCallable(_) | Self::InternalCallError(_) => false, @@ -8181,7 +9093,7 @@ impl<'db> BindingError<'db> { #[expect(clippy::too_many_arguments)] fn report_diagnostic( &self, - context: &InferContext<'db, '_>, + context: &CallDiagnosticContext<'_, '_, 'db, '_>, node: ast::AnyNodeRef, callable_ty: Type<'db>, callable_description: Option<&CallableDescription>, @@ -8189,6 +9101,8 @@ impl<'db> BindingError<'db> { matching_overload: Option<&MatchingOverloadLiteral<'_>>, source_parameter_index_offset: usize, ) { + let db = context.db(); + let env = context.program_environment(); let callable_kind = match callable_ty { Type::FunctionLiteral(_) => "Function", Type::BoundMethod(_) => "Method", @@ -8202,13 +9116,14 @@ impl<'db> BindingError<'db> { expected_ty, provided_ty, provenance, + parameter_source, } => { // TODO: Ideally we would not emit diagnostics for `TypedDict` literal arguments // here (see `diagnostic::is_invalid_typed_dict_literal`). However, we may have // silenced diagnostics during overload evaluation, and rely on the assignability // diagnostic being emitted here. - let range = Self::get_node(node, *argument_index); + let range = context.get_range(node, *argument_index); // basedpython: a call argument is a conversion site — an in-scope // a conformance, a `__from__` / `__of__` on the parameter @@ -8226,6 +9141,7 @@ impl<'db> BindingError<'db> { && Self::argument_is_wrappable(node, *argument_index) && let Some(repair) = crate::types::conversions::repair_conversion( context.db(), + env, context.file(), *provided_ty, *expected_ty, @@ -8241,12 +9157,13 @@ impl<'db> BindingError<'db> { }; let display_settings = DisplaySettings::from_possibly_ambiguous_types( - context.db(), + db, + env, [provided_ty, expected_ty], ); let provided_ty_display = - provided_ty.display_with(context.db(), display_settings.clone()); - let expected_ty_display = expected_ty.display_with(context.db(), display_settings); + provided_ty.display_with(db, env, display_settings.clone()); + let expected_ty_display = expected_ty.display_with(db, env, display_settings); let mut diag = builder.into_diagnostic(format_args!( "Argument{} is incorrect", @@ -8258,29 +9175,70 @@ impl<'db> BindingError<'db> { provenance, InvalidArgumentTypeProvenance::OpenTypedDictExtraItems ) { - diag.set_primary_message(format_args!( + diag.set_primary_annotation_message(format_args!( "Possible extra items in unpacked open `TypedDict` have type \ `{provided_ty_display}`, expected `{expected_ty_display}`" )); } else { - diag.set_primary_message(format_args!( + diag.set_primary_annotation_message(format_args!( "Expected `{expected_ty_display}`, found `{provided_ty_display}`" )); } - let error_context = - provided_ty.assignability_error_context(context.db(), *expected_ty); - error_context.attach_to(context.db(), &mut diag); + let error_context = provided_ty.assignability_error_context(db, env, *expected_ty); + error_context.attach_to(db, env, &mut diag); + + if let Some(parameter_source) = parameter_source { + let (name_span, parameter_span) = + parameter_source.parameter_span(db, parameter); + let callable_kind = if parameter_source.is_bound_method { + "Method" + } else { + "Function" + }; + let mut sub = SubDiagnostic::new( + SubDiagnosticSeverity::Info, + format_args!("{callable_kind} defined here"), + ); + sub.annotate(Annotation::primary(name_span)); + sub.annotate( + Annotation::secondary(parameter_span).message("Parameter declared here"), + ); + diag.sub(sub); + } if let Some(matching_overload) = matching_overload { - if let Some(overload_literal) = matching_overload.get(context.db()) { + if let Some(overload_literal) = matching_overload.get(db) { let mut sub = SubDiagnostic::new( SubDiagnosticSeverity::Info, "Matching overload defined here", ); + let parameter_index = if parameter_source.is_some() { + let argument_is_positional = Self::get_argument_node( + node, + argument_index.map(|index| index + context.argument_index_offset), + ) + .map_or(parameter.positional, |argument| { + matches!(argument, ArgOrKeyword::Arg(_)) + }); + overload_literal + .signature(db) + .parameters() + .iter() + .position(|candidate| { + if argument_is_positional { + candidate.is_variadic() + } else { + candidate.is_keyword_variadic() + } + }) + .unwrap_or(parameter.signature_parameter_index) + } else { + parameter.signature_parameter_index + }; let (name_span, parameter_span) = overload_literal.parameter_span( context.db(), - Some(parameter.index + source_parameter_index_offset), + Some(parameter_index + source_parameter_index_offset), ); sub.annotate(Annotation::primary(name_span)); sub.annotate( @@ -8294,7 +9252,7 @@ impl<'db> BindingError<'db> { matching_overload.function.name(context.db()) )); for (overload_index, overload) in matching_overload - .candidate_overloads(context.db()) + .candidate_overloads(db) .take(MAXIMUM_OVERLOADS) { if overload_index == matching_overload.index { @@ -8302,7 +9260,7 @@ impl<'db> BindingError<'db> { } diag.info(format_args!( " {}", - overload.signature(context.db()).display(context.db()) + overload.signature(db).display(db, env) )); } if matching_overload.candidate_count() > MAXIMUM_OVERLOADS { @@ -8312,10 +9270,12 @@ impl<'db> BindingError<'db> { )); } } - } else if let Some((name_span, parameter_span)) = callable_ty.parameter_span( - context.db(), - Some(parameter.index + source_parameter_index_offset), - ) { + } else if parameter_source.is_none() + && let Some((name_span, parameter_span)) = callable_ty.parameter_span( + context.db(), + Some(parameter.signature_parameter_index + source_parameter_index_offset), + ) + { let mut sub = SubDiagnostic::new( SubDiagnosticSeverity::Info, format_args!("{callable_kind} defined here"), @@ -8328,44 +9288,44 @@ impl<'db> BindingError<'db> { } if let Some(compound_diag) = compound_diag { - compound_diag.add_context(context.db(), &mut diag); + compound_diag.add_context(db, env, &mut diag); } // If the type comes from first-party code, the user may have some control over // the parameter annotation; provide additional context to help them fix it. if callable_ty - .definition(context.db()) + .definition(db, env) .and_then(|definition| definition.file(context.db())) .is_some_and(|file| context.db().should_check_file(file)) { note_numbers_module_not_supported( - context.db(), + db, + env, &mut diag, *expected_ty, *provided_ty, ); } - add_invariant_generic_hints(context.db(), &mut diag, *expected_ty, *provided_ty); + add_invariant_generic_hints(db, env, &mut diag, *expected_ty, *provided_ty); } Self::InvalidKeyType { argument_index, provided_ty, } => { - let range = Self::get_node(node, *argument_index); + let range = context.get_range(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 provided_ty_display = provided_ty.display(db, env); let mut diag = builder.into_diagnostic( "Argument expression after ** must be a mapping with `str` key type", ); - diag.set_primary_message(format_args!("Found `{provided_ty_display}`")); + diag.set_primary_annotation_message(format_args!("Found `{provided_ty_display}`")); if let Some(compound_diag) = compound_diag { - compound_diag.add_context(context.db(), &mut diag); + compound_diag.add_context(db, env, &mut diag); } } @@ -8374,8 +9334,8 @@ impl<'db> BindingError<'db> { 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 range = context.get_range(node, *first_excess_argument_index); + if let Some(builder) = context.report_lint(&TOO_MANY_POSITIONAL_ARGUMENTS, range) { let mut diag = builder.into_diagnostic(format_args!( "Too many positional arguments{}: expected \ {expected_positional_count}, got {provided_positional_count}", @@ -8384,7 +9344,7 @@ impl<'db> BindingError<'db> { .unwrap_or_default() )); if let Some(compound_diag) = compound_diag { - compound_diag.add_context(context.db(), &mut diag); + compound_diag.add_context(db, env, &mut diag); } else if let Some(spans) = callable_ty.function_spans(context.db()) { let mut sub = SubDiagnostic::new( SubDiagnosticSeverity::Info, @@ -8410,12 +9370,14 @@ impl<'db> BindingError<'db> { .unwrap_or_default() )); if let Some(compound_diag) = compound_diag { - compound_diag.add_context(context.db(), &mut diag); + compound_diag.add_context(db, env, &mut diag); } else { let span = callable_ty.parameter_span( context.db(), - (parameters.0.len() == 1) - .then(|| parameters.0[0].index + source_parameter_index_offset), + (parameters.0.len() == 1).then(|| { + parameters.0[0].signature_parameter_index + + source_parameter_index_offset + }), ); if let Some((_, parameter_span)) = span { let mut sub = SubDiagnostic::new( @@ -8486,8 +9448,8 @@ impl<'db> BindingError<'db> { argument_name, argument_index, } => { - let node = Self::get_node(node, *argument_index); - if let Some(builder) = context.report_lint(&UNKNOWN_ARGUMENT, node) { + let range = context.get_range(node, *argument_index); + if let Some(builder) = context.report_lint(&UNKNOWN_ARGUMENT, range) { let mut diag = builder.into_diagnostic(format_args!( "Argument `{argument_name}` does not match any known parameter{}", callable_description @@ -8495,7 +9457,7 @@ impl<'db> BindingError<'db> { .unwrap_or_default() )); if let Some(compound_diag) = compound_diag { - compound_diag.add_context(context.db(), &mut diag); + compound_diag.add_context(db, env, &mut diag); } else if let Some(spans) = callable_ty.function_spans(context.db()) { let mut sub = SubDiagnostic::new( SubDiagnosticSeverity::Info, @@ -8508,8 +9470,8 @@ impl<'db> BindingError<'db> { } Self::UnknownKeywordVariadicArgument { argument_index } => { - let node = Self::get_node(node, *argument_index); - if let Some(builder) = context.report_lint(&UNKNOWN_ARGUMENT, node) { + let range = context.get_range(node, *argument_index); + if let Some(builder) = context.report_lint(&UNKNOWN_ARGUMENT, range) { let mut diag = builder.into_diagnostic(format_args!( "Unpacked argument may contain keyword arguments that do not match any known parameter{}", callable_description @@ -8517,7 +9479,7 @@ impl<'db> BindingError<'db> { .unwrap_or_default() )); if let Some(compound_diag) = compound_diag { - compound_diag.add_context(context.db(), &mut diag); + compound_diag.add_context(db, env, &mut diag); } else if let Some(spans) = callable_ty.function_spans(context.db()) { let mut sub = SubDiagnostic::new( SubDiagnosticSeverity::Info, @@ -8533,9 +9495,9 @@ impl<'db> BindingError<'db> { argument_index, parameter, } => { - let node = Self::get_node(node, *argument_index); + let range = context.get_range(node, *argument_index); if let Some(builder) = - context.report_lint(&POSITIONAL_ONLY_PARAMETER_AS_KWARG, node) + context.report_lint(&POSITIONAL_ONLY_PARAMETER_AS_KWARG, range) { let mut diag = builder.into_diagnostic(format_args!( "Positional-only parameter {parameter} passed as keyword argument{}", @@ -8544,7 +9506,7 @@ impl<'db> BindingError<'db> { .unwrap_or_default() )); if let Some(compound_diag) = compound_diag { - compound_diag.add_context(context.db(), &mut diag); + compound_diag.add_context(db, env, &mut diag); } else if let Some(spans) = callable_ty.function_spans(context.db()) { let mut sub = SubDiagnostic::new( SubDiagnosticSeverity::Info, @@ -8560,8 +9522,8 @@ impl<'db> BindingError<'db> { argument_index, parameter, } => { - let node = Self::get_node(node, *argument_index); - if let Some(builder) = context.report_lint(&PARAMETER_ALREADY_ASSIGNED, node) { + let range = context.get_range(node, *argument_index); + if let Some(builder) = context.report_lint(&PARAMETER_ALREADY_ASSIGNED, range) { let mut diag = builder.into_diagnostic(format_args!( "Multiple values provided for parameter {parameter}{}", callable_description @@ -8569,7 +9531,7 @@ impl<'db> BindingError<'db> { .unwrap_or_default() )); if let Some(compound_diag) = compound_diag { - compound_diag.add_context(context.db(), &mut diag); + compound_diag.add_context(db, env, &mut diag); } } } @@ -8578,13 +9540,12 @@ impl<'db> BindingError<'db> { error, argument_index, } => { - let range = Self::get_node(node, *argument_index); + let range = context.get_range(node, *argument_index); let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, range) else { return; }; - let argument_type = error.argument_type(); - let argument_ty_display = argument_type.display(context.db()); + let argument_ty_display = argument_type.display(db, env); let mut diag = builder.into_diagnostic(format_args!( "Argument{} is incorrect", @@ -8598,19 +9559,19 @@ impl<'db> BindingError<'db> { let typevar = bound_typevar.typevar(context.db()); let typevar_name = typevar.name(context.db()); let bound = typevar - .upper_bound(context.db()) + .upper_bound(context.db(), env) .expect("type variable should have an upper bound if this error occurs") - .display(context.db()); + .display(context.db(), env); // basedpython: the hole an unannotated parameter opens is a type variable // nobody wrote, so naming it as one would send the reader looking for a // declaration that does not exist if typevar.kind(context.db()) == TypeVarKind::InferredParameter { - diag.set_primary_message(format_args!( + diag.set_primary_annotation_message(format_args!( "Argument type `{argument_ty_display}` does not satisfy \ `{bound}`, inferred for parameter `{typevar_name}`" )); } else { - diag.set_primary_message(format_args!( + diag.set_primary_annotation_message(format_args!( "Argument type `{argument_ty_display}` does not \ satisfy upper bound `{bound}` of type variable `{typevar_name}`" )); @@ -8621,23 +9582,27 @@ impl<'db> BindingError<'db> { violation, .. } => { - diag.set_primary_message(violation.message(context.db(), *bound_typevar)); + diag.set_primary_annotation_message(violation.message( + context.db(), + env, + *bound_typevar, + )); } SpecializationError::MismatchedConstraint { bound_typevar, .. } => { let typevar = bound_typevar.typevar(context.db()); let typevar_name = typevar.name(context.db()); - diag.set_primary_message(format_args!( + diag.set_primary_annotation_message(format_args!( "Argument type `{argument_ty_display}` does not \ satisfy constraints ({}) of type variable `{typevar_name}`", typevar - .constraints(context.db()) + .constraints(db, env) .expect( "type variable should have constraints if this error occurs" ) .iter() .format_with(", ", |ty, f| f(&format_args!( "`{}`", - ty.display(context.db()) + ty.display(db, env) ))) )); } @@ -8645,8 +9610,9 @@ impl<'db> BindingError<'db> { let typevar = error.bound_typevar().typevar(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 module = + parsed_module(context.db(), typevar_definition.python_file(context.db())) + .load(context.db()); let typevar_range = typevar_definition.full_range(context.db(), &module); let mut sub = SubDiagnostic::new( SubDiagnosticSeverity::Info, @@ -8661,10 +9627,22 @@ impl<'db> BindingError<'db> { } if let Some(compound_diag) = compound_diag { - compound_diag.add_context(context.db(), &mut diag); + compound_diag.add_context(db, env, &mut diag); } } + Self::PropertyHasNoGetter(_) => { + BindingError::InternalCallError("property has no getter").report_diagnostic( + context, + node, + callable_ty, + callable_description, + compound_diag, + matching_overload, + source_parameter_index_offset, + ); + } + Self::PropertyHasNoSetter(_) => { BindingError::InternalCallError("property has no setter").report_diagnostic( context, @@ -8689,9 +9667,18 @@ impl<'db> BindingError<'db> { ); } + Self::PropertyGetterCallError(error) | Self::PropertySetterCallError(error) => { + let context = CallDiagnosticContext { + context: context.context, + overrides: context.overrides, + argument_index_offset: error.argument_index_offset, + }; + error.bindings.report_diagnostics_impl(&context, node); + } + Self::InternalCallError(reason) => { - let node = Self::get_node(node, None); - if let Some(builder) = context.report_lint(&CALL_NON_CALLABLE, node) { + let range = context.get_range(node, None); + if let Some(builder) = context.report_lint(&CALL_NON_CALLABLE, range) { let mut diag = builder.into_diagnostic(format_args!( "Call{} failed: {reason}", callable_description @@ -8699,7 +9686,7 @@ impl<'db> BindingError<'db> { .unwrap_or_default() )); if let Some(compound_diag) = compound_diag { - compound_diag.add_context(context.db(), &mut diag); + compound_diag.add_context(db, env, &mut diag); } } } @@ -8707,9 +9694,9 @@ impl<'db> BindingError<'db> { Self::UnmatchedOverload => {} Self::CalledTopCallable(callable_ty) => { - let node = Self::get_node(node, None); - if let Some(builder) = context.report_lint(&CALL_TOP_CALLABLE, node) { - let callable_ty_display = callable_ty.display(context.db()); + let range = context.get_range(node, None); + if let Some(builder) = context.report_lint(&CALL_TOP_CALLABLE, range) { + let callable_ty_display = callable_ty.display(db, env); let mut diag = builder.into_diagnostic(format_args!( "Object of type `{callable_ty_display}` is not safe to call; \ its signature is not known" @@ -8719,14 +9706,14 @@ impl<'db> BindingError<'db> { because there is no valid set of arguments for it", ); if let Some(compound_diag) = compound_diag { - compound_diag.add_context(context.db(), &mut diag); + compound_diag.add_context(db, env, &mut diag); } } } Self::InvalidDataclassApplication(target) => { - let node = Self::get_node(node, None); - if let Some(builder) = context.report_lint(&INVALID_DATACLASS, node) { + let range = context.get_range(node, None); + if let Some(builder) = context.report_lint(&INVALID_DATACLASS, range) { let (message, info) = match target { InvalidDataclassTarget::NamedTuple => ( "Cannot use `dataclass()` on a `NamedTuple` class", @@ -8751,8 +9738,8 @@ impl<'db> BindingError<'db> { } Self::InvalidDataclassArgument(argument) => { - let node = Self::get_node(node, None); - if let Some(builder) = context.report_lint(&INVALID_DATACLASS, node) { + let range = context.get_range(node, None); + if let Some(builder) = context.report_lint(&INVALID_DATACLASS, range) { builder.into_diagnostic(match argument { InvalidDataclassArgument::OrderRequiresEq => { "`order=True` requires `eq=True`" @@ -8835,7 +9822,7 @@ impl<'db> BindingError<'db> { /// Trait for adding context about compound types (unions/intersections) to diagnostics. trait CompoundDiagnostic { /// Adds context about any relevant compound type function types to the given diagnostic. - fn add_context(&self, db: &dyn Db, diag: &mut Diagnostic); + fn add_context(&self, db: &dyn Db, env: &ProgramEnvironment<'_>, diag: &mut Diagnostic); } /// Contains additional context for union specific diagnostics. @@ -8843,20 +9830,20 @@ trait CompoundDiagnostic { /// 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> { +struct UnionDiagnostic<'db> { /// The type of the union. callable_type: Type<'db>, - /// The specific binding that failed. - binding: &'b CallableBinding<'db>, + /// The type associated with the specific union variant that failed. + variant_type: Type<'db>, } -impl CompoundDiagnostic for UnionDiagnostic<'_, '_> { - fn add_context(&self, db: &dyn Db, diag: &mut Diagnostic) { +impl CompoundDiagnostic for UnionDiagnostic<'_> { + fn add_context(&self, db: &dyn Db, env: &ProgramEnvironment<'_>, diag: &mut Diagnostic) { let sub = SubDiagnostic::new( SubDiagnosticSeverity::Info, format_args!( "Union variant `{callable_ty}` is incompatible with this call site", - callable_ty = self.binding.callable_type.display(db), + callable_ty = self.variant_type.display(db, env), ), ); diag.sub(sub); @@ -8865,7 +9852,7 @@ impl CompoundDiagnostic for UnionDiagnostic<'_, '_> { SubDiagnosticSeverity::Info, format_args!( "Attempted to call union type `{}`", - self.callable_type.display(db) + self.callable_type.display(db, env) ), ); diag.sub(sub); @@ -8885,12 +9872,12 @@ struct IntersectionDiagnostic<'b, 'db> { } impl CompoundDiagnostic for IntersectionDiagnostic<'_, '_> { - fn add_context(&self, db: &dyn Db, diag: &mut Diagnostic) { + fn add_context(&self, db: &dyn Db, env: &ProgramEnvironment<'_>, diag: &mut Diagnostic) { let sub = SubDiagnostic::new( SubDiagnosticSeverity::Info, format_args!( "Intersection element `{callable_ty}` is incompatible with this call site", - callable_ty = self.binding.callable_type.display(db), + callable_ty = self.binding.callable_type.display(db, env), ), ); diag.sub(sub); @@ -8899,7 +9886,7 @@ impl CompoundDiagnostic for IntersectionDiagnostic<'_, '_> { SubDiagnosticSeverity::Info, format_args!( "Attempted to call intersection type `{}`", - self.callable_type.display(db) + self.callable_type.display(db, env) ), ); diag.sub(sub); @@ -8920,13 +9907,13 @@ struct LayeredDiagnostic<'b, 'db> { } impl CompoundDiagnostic for LayeredDiagnostic<'_, '_> { - fn add_context(&self, db: &dyn Db, diag: &mut Diagnostic) { + fn add_context(&self, db: &dyn Db, env: &ProgramEnvironment<'_>, diag: &mut Diagnostic) { // Add intersection context first (more specific) let sub = SubDiagnostic::new( SubDiagnosticSeverity::Info, format_args!( "Intersection element `{callable_ty}` is incompatible with this call site", - callable_ty = self.binding.callable_type.display(db), + callable_ty = self.binding.callable_type.display(db, env), ), ); diag.sub(sub); @@ -8935,7 +9922,7 @@ impl CompoundDiagnostic for LayeredDiagnostic<'_, '_> { SubDiagnosticSeverity::Info, format_args!( "Attempted to call intersection type `{}`", - self.intersection_callable_type.display(db) + self.intersection_callable_type.display(db, env) ), ); diag.sub(sub); @@ -8945,7 +9932,7 @@ impl CompoundDiagnostic for LayeredDiagnostic<'_, '_> { SubDiagnosticSeverity::Info, format_args!( "Attempted to call union type `{}`", - self.union_callable_type.display(db) + self.union_callable_type.display(db, env) ), ); diag.sub(sub); @@ -9034,7 +10021,11 @@ const STRUCT_FORMAT_MAX_REPETITION: usize = 32; /// /// Returns `None` if the format contains unsupported specifiers or /// repetition counts exceed the limit, indicating a fallback to `tuple[Unknown, ...]`. -fn parse_struct_format<'db>(db: &'db dyn Db, format_string: &str) -> Option>> { +fn parse_struct_format<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + format_string: &str, +) -> Option>> { // Strip the byte order/size/alignment prefix let format = format_string.trim_start_matches(['@', '=', '<', '>', '!']); let mut chars = format.chars().peekable(); @@ -9062,15 +10053,15 @@ fn parse_struct_format<'db>(db: &'db dyn Db, format_string: &str) -> Option continue, // Pad byte: no value produced - 's' | 'p' => (KnownClass::Bytes.to_instance(db), 1), - 'c' => (KnownClass::Bytes.to_instance(db), count), + 's' | 'p' => (KnownClass::Bytes.to_instance(db, env), 1), + 'c' => (KnownClass::Bytes.to_instance(db, env), count), 'b' | 'B' | 'h' | 'H' | 'i' | 'I' | 'l' | 'L' | 'q' | 'Q' | 'n' | 'N' | 'P' => { - (KnownClass::Int.to_instance(db), count) + (KnownClass::Int.to_instance(db, env), count) } - '?' => (KnownClass::Bool.to_instance(db), count), - 'e' | 'f' | 'd' => (KnownClass::Float.to_instance(db), count), - 'F' | 'D' if Program::get(db).python_version(db) >= PythonVersion::PY314 => { - (KnownClass::Complex.to_instance(db), count) + '?' => (KnownClass::Bool.to_instance(db, env), count), + 'e' | 'f' | 'd' => (KnownClass::Float.to_instance(db, env), count), + 'F' | 'D' if env.python_version(db) >= PythonVersion::PY314 => { + (KnownClass::Complex.to_instance(db, env), count) } _ => return None, }; diff --git a/crates/ty_python_semantic/src/types/call/bind/constructor.rs b/crates/ty_python_semantic/src/types/call/bind/constructor.rs index 74e3bb182b..1feee94d68 100644 --- a/crates/ty_python_semantic/src/types/call/bind/constructor.rs +++ b/crates/ty_python_semantic/src/types/call/bind/constructor.rs @@ -1,12 +1,18 @@ -use super::{Binding, Bindings, CallableBinding, CallableItem, CheckTypesMode}; -use crate::db::Db; +use super::{ + Binding, Bindings, CallableBinding, CallableItem, CheckTypesMode, + generic_context_has_parameter_pack, +}; +use crate::Db; +use crate::ProgramEnvironment; use crate::types::call::arguments::CallArguments; use crate::types::constraints::ConstraintSetBuilder; use crate::types::dedicated::django; -use crate::types::generics::Specialization; +use crate::types::generics::{GenericContext, Specialization}; use crate::types::signatures::Parameter; +use crate::types::typevar::TypeVarNonceGenerator; use crate::types::{ - BoundTypeVarInstance, ClassLiteral, DynamicType, Type, TypeContext, UnsafeUnionType, + ApplyTypeMappingVisitor, BoundTypeVarInstance, ClassLiteral, DynamicType, Type, TypeContext, + TypeMapping, UnsafeUnionType, }; /// Bindings for a constructor call. @@ -67,15 +73,107 @@ impl<'db> ConstructorBinding<'db> { self.downstream_constructor = Some(Box::new(bindings)); } + pub(super) fn freshen_generic_contexts_in_place( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + nonce_generator: &TypeVarNonceGenerator<'db>, + ) { + let instance_type = self.constructed_instance_type(); + let Some((_, specialization)) = instance_type.class_specialization(db, env) else { + return; + }; + let generic_context = specialization.generic_context(db); + if generic_context_has_parameter_pack(db, generic_context) + || !nonce_generator.should_freshen(db, generic_context) + { + return; + } + + let delta = nonce_generator.next().value(); + let type_mapping = TypeMapping::FreshenBoundTypeVars { + generic_context, + delta, + }; + let fresh_instance_type = + instance_type.apply_type_mapping(db, env, &type_mapping, TypeContext::default()); + // Only freshen a generic context that belongs to the constructed instance itself. + // `class_specialization` can also find a context through a class-object type variable's + // bound, but freshening that context would detach the constructor parameters from the + // receiver. + if fresh_instance_type == instance_type { + return; + } + self.freshen_class_typevars(db, env, generic_context, delta, fresh_instance_type); + } + + fn freshen_class_typevars( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + generic_context: GenericContext<'db>, + delta: u32, + fresh_instance_type: Type<'db>, + ) { + let type_mapping = TypeMapping::FreshenBoundTypeVars { + generic_context, + delta, + }; + + // Keep the source-level instance on `ConstructorBinding`; the final return type applies + // the inferred specialization to that instance. Only the per-overload context is + // call-local, so its instance must use the same fresh type variables as the signature. + let constructor_context = self.context().with_instance_type(fresh_instance_type); + let visitor = ApplyTypeMappingVisitor::new(env); + self.entry.bound_type = self.entry.bound_type.map(|bound_type| { + bound_type.apply_type_mapping_impl( + db, + env, + &type_mapping, + TypeContext::default(), + &visitor, + ) + }); + for overload in &mut self.entry.overloads { + overload.signature = overload.signature.apply_type_mapping_impl( + db, + &type_mapping, + TypeContext::default(), + &visitor, + ); + overload.set_constructor_context(db, constructor_context); + } + + if let Some(downstream) = self.downstream_constructor_mut() { + for downstream_binding in downstream + .iter_callable_items_mut() + .filter_map(CallableItem::as_constructor_mut) + { + downstream_binding.freshen_class_typevars( + db, + env, + generic_context, + delta, + fresh_instance_type, + ); + } + } + } + /// Match parameters for this constructor method and downstream constructors. - pub(super) fn match_parameters(&mut self, db: &'db dyn Db, arguments: &CallArguments<'_, 'db>) { - self.entry.match_parameters(db, arguments); + pub(super) fn match_parameters( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + arguments: &CallArguments<'_, 'db>, + ) { + self.entry.match_parameters(db, env, arguments); // We don't know at this point whether we'll need to check downstream constructors or not // (since we can't resolve return types yet), so we match parameters for all downstream // constructors; this may be needed for argument type contexts. if let Some(downstream) = self.downstream_constructor.as_mut() { - downstream.match_parameters_in_place(db, arguments); + downstream.match_parameters_in_place(db, env, arguments); } } @@ -86,13 +184,14 @@ impl<'db> ConstructorBinding<'db> { pub(super) fn check_types( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, constraints: &ConstraintSetBuilder<'db>, argument_types: &CallArguments<'_, 'db>, call_expression_tcx: TypeContext<'db>, mode: CheckTypesMode, ) { self.entry - .check_types(db, constraints, argument_types, call_expression_tcx); + .check_types(db, env, constraints, argument_types, call_expression_tcx); // Now that we've fully checked our own callable, we can determine whether downstream // constructors should be checked or not. @@ -100,6 +199,7 @@ impl<'db> ConstructorBinding<'db> { if let Some(downstream) = self.downstream_constructor_mut() { let _ = downstream.check_types_impl( db, + env, constraints, argument_types, call_expression_tcx, @@ -107,7 +207,7 @@ impl<'db> ConstructorBinding<'db> { mode, ); } - } else if !self.should_check_downstream(db) { + } else if !self.should_check_downstream(db, env) { // If not, we can discard the downstream constructor bindings entirely. self.downstream_constructor = None; } @@ -120,7 +220,7 @@ impl<'db> ConstructorBinding<'db> { /// the overall callable, because in multiple-matching-overload cases where the overload /// resolution algorithm might just collapse to `Unknown`, we want to make a more informed /// decision based on whether all overloads return instance types, or not. - fn should_check_downstream(&self, db: &'db dyn Db) -> bool { + fn should_check_downstream(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool { let constructor_kind = self.constructor_kind(); if constructor_kind.is_init() || self.downstream_constructor().is_none() { return false; @@ -132,21 +232,25 @@ impl<'db> ConstructorBinding<'db> { } let constructed_instance_type = self.constructed_instance_type(); - let constructor_class_literal = self.constructed_class_literal(db); + let constructor_class_literal = self.constructed_class_literal(db, env); // If any matching overload returns the constructed instance type itself, or an instance of // the constructed class, we need to check downstream constructors. callable.matching_overloads().any(|(_, overload)| { overload.return_ty == constructed_instance_type || constructor_class_literal.is_some_and(|class_literal| { - constructor_returns_instance(db, class_literal, overload.return_ty) + constructor_returns_instance(db, env, class_literal, overload.return_ty) }) }) } /// Discards an inactive downstream constructor. - pub(super) fn discard_downstream_constructor(&mut self, db: &'db dyn Db) -> bool { - if self.should_check_downstream(db) { + pub(super) fn discard_downstream_constructor( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> bool { + if self.should_check_downstream(db, env) { true } else { self.downstream_constructor = None; @@ -158,6 +262,7 @@ impl<'db> ConstructorBinding<'db> { pub(super) fn check_downstream_constructor( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, constraints: &ConstraintSetBuilder<'db>, argument_types: &CallArguments<'_, 'db>, call_expression_tcx: TypeContext<'db>, @@ -168,6 +273,7 @@ impl<'db> ConstructorBinding<'db> { // `as_result` that ultimately matter. let _ = downstream.check_types_impl( db, + env, constraints, argument_types, call_expression_tcx, @@ -210,7 +316,7 @@ impl<'db> ConstructorBinding<'db> { } /// Compute the overall effective return type of this `ConstructorBinding`. - pub(super) fn return_type(&self, db: &'db dyn Db) -> Type<'db> { + pub(super) fn return_type(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { // If we are checking downstream constructors, and the downstream constructor resolves to a // non-instance return, that becomes the effective constructor return. This can only happen // if we are a metaclass `__call__` returning an instance of the constructed class, but @@ -222,47 +328,66 @@ impl<'db> ConstructorBinding<'db> { // annotation. But no other type checker considers it an error, and it probably rarely if // ever comes up.) if let Some(downstream) = self.downstream_constructor() - && let Some(constructor_class_literal) = self.constructed_class_literal(db) + && let Some(constructor_class_literal) = self.constructed_class_literal(db, env) { - let downstream_return = downstream.return_type(db); - if !constructor_returns_instance(db, constructor_class_literal, downstream_return) { + let downstream_return = downstream.return_type(db, env); + if !constructor_returns_instance(db, env, constructor_class_literal, downstream_return) + { return downstream_return; } } // If `__new__` or metaclass `__call__` produced an explicit return type, use it // directly rather than building an instance of the constructed class. - if let Some(return_ty) = self.explicit_return_type(db) { + if let Some(return_ty) = self.explicit_return_type(db, env) { return return_ty; } - if let Some(pinned) = self.django_field_instance_type(db) { + if let Some(pinned) = self.django_field_instance_type(db, env) { return pinned; } - self.instance_return_type(db) + self.instance_return_type(db, env) } /// The type of the constructed instance itself, used when no overload dictates a more /// specific return type. - fn instance_return_type(&self, db: &'db dyn Db) -> Type<'db> { - self.constructed_instance_type() - .apply_optional_specialization(db, self.instance_return_specialization(db)) + fn instance_return_type(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + let instance_type = self.constructed_instance_type(); + if let Some(specialization) = self.instance_return_specialization(db, env) { + return instance_type.apply_optional_specialization(db, Some(specialization)); + } + // No overload accepted the arguments and nothing was inferred, so the instance is still + // carrying the class's own type parameters. They describe the class's declaration, not + // this call's result, so the reader gets `Unknown` rather than a type variable of a class + // they never named. + if self.first_matching_overload().is_none() + && let Some((_, class_specialization)) = instance_type.class_specialization(db, env) + { + return instance_type + .apply_optional_specialization(db, Some(erase_unsolved(db, class_specialization))); + } + instance_type } /// django field constructors: pin the `_ST`/`_GT` (or m2m `_To`/`_Through`) /// specialization that django-stubs leaves to its mypy plugin (see /// `dedicated/django.rs`). restricted to `__init__`-kind bindings because /// that is where the `to=`/`null=`/`through=` arguments bind - fn django_field_instance_type(&self, db: &'db dyn Db) -> Option> { + fn django_field_instance_type( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { if !self.constructor_kind().is_init() { return None; } - let class = self.constructed_class_literal(db)?.as_static()?; + let class = self.constructed_class_literal(db, env)?.as_static()?; let overload = self.first_matching_overload()?; let keyword = |name| overload.parameter_type_by_name(name, false).ok().flatten(); django::field_constructor_instance_type( db, + env, class, keyword("to"), keyword("null"), @@ -281,26 +406,36 @@ impl<'db> ConstructorBinding<'db> { /// resulting specialization can be applied either to the constructed instance type or to an /// explicit `__new__` / `__call__` return annotation that is an instance of the constructed /// type or a subclass. - fn instance_return_specialization(&self, db: &'db dyn Db) -> Option> { + fn instance_return_specialization( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { let constructed_instance_type = self.constructed_instance_type(); // This will be `None` if we're constructing a non-generic class. If we're constructing a // non-specialized generic class (`C(...)`), it'll be the identity specialization. If we're // constructing an already-specialized generic alias (`C[str](...)`), it'll be the // specialization of that alias. - let (_, class_specialization) = constructed_instance_type.class_specialization(db)?; + let (_, class_specialization) = constructed_instance_type.class_specialization(db, env)?; let static_class_literal = self - .constructed_class_literal(db) + .constructed_class_literal(db, env) .and_then(ClassLiteral::as_static); let class_context = class_specialization.generic_context(db); let mut combined: Option> = None; let mut combine_binding_specialization = |binding: &ConstructorBinding<'db>| { - let Some(overload) = binding.first_matching_overload() else { + let matching = binding.first_matching_overload(); + // no overload accepted the arguments, so nothing was inferred for the type + // parameters the accepted one would have filled in + let overload_failed = matching.is_none(); + let Some(overload) = + matching.or_else(|| binding.callable().unambiguous_failing_overload()) + else { return; }; let return_specialization = static_class_literal // Use the already-resolved overload return type when possible. - .and_then(|lit| overload.return_ty.specialization_of(db, lit)); + .and_then(|lit| overload.return_ty.specialization_of(db, env, lit)); // TODO All this handling of return-specialization vs self-specialization is a hacky // work-around to a situation that can occur with a case like `def __init__(self: @@ -320,11 +455,11 @@ impl<'db> ConstructorBinding<'db> { let self_parameter_specialization = static_class_literal.and_then(|lit| { let self_param_ty = overload.signature.parameters().get(0)?.annotated_type(); let resolved_self_param_ty = overload - .specialization(db) + .specialization(db, env) .map_or(self_param_ty, |specialization| { self_param_ty.apply_specialization(db, specialization) }); - resolved_self_param_ty.specialization_of(db, lit) + resolved_self_param_ty.specialization_of(db, env, lit) }); let refined_self_parameter_specialization = self_parameter_specialization.map(|specialization| { @@ -340,7 +475,7 @@ impl<'db> ConstructorBinding<'db> { } else { without_unknown }; - mapped_ty.promote(db) + mapped_ty.promote(db, env) }) .collect(); Specialization::new( @@ -357,13 +492,26 @@ impl<'db> ConstructorBinding<'db> { } else { refined_self_parameter_specialization .or(return_specialization) - .or_else(|| overload.specialization(db)?.restrict(db, class_context)) + .or_else(|| { + overload + .specialization(db, env)? + .restrict(db, class_context) + }) }; // end TODO let Some(specialization) = specialization else { return; }; + // A failing overload infers nothing, so any type parameter the specialization still + // maps to itself was never solved. Leaving it as the type variable would publish a + // type variable of the callee's own signature to everyone who reads the result, so + // say `Unknown` — the same answer a failed call to an overloaded function gives. + let specialization = if overload_failed { + erase_unsolved(db, specialization) + } else { + specialization + }; combined = Some(match combined { None => specialization, Some(previous) => previous.combine(db, specialization), @@ -396,8 +544,12 @@ impl<'db> ConstructorBinding<'db> { /// /// This must be called only after downstream constructor bindings have been type-checked, /// because instance-returning constructor paths may incorporate downstream specializations. - fn explicit_return_type(&self, db: &'db dyn Db) -> Option> { - if self.constructor_kind().is_init() || self.constructed_class_literal(db).is_none() { + fn explicit_return_type( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { + if self.constructor_kind().is_init() || self.constructed_class_literal(db, env).is_none() { return None; } @@ -410,9 +562,9 @@ impl<'db> ConstructorBinding<'db> { // consider all overloads' return types. (This increases the chances of an `Unknown` // return, but still preserves more precise returns in unambiguous cases.) if matching_overloads.clone().next().is_none() { - self.analyze_overload_returns(db, self.callable().overloads().iter(), false) + self.analyze_overload_returns(db, env, self.callable().overloads(), false) } else { - self.analyze_overload_returns(db, matching_overloads, true) + self.analyze_overload_returns(db, env, matching_overloads, true) } } @@ -426,6 +578,7 @@ impl<'db> ConstructorBinding<'db> { fn analyze_overload_returns<'a>( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, overloads: impl IntoIterator>, matched: bool, ) -> Option> @@ -441,7 +594,7 @@ impl<'db> ConstructorBinding<'db> { let mut saw_instance_return = false; let mut non_instance_returns: Vec> = Vec::new(); for overload in overloads { - let (return_ty, is_instance_return) = self.single_overload_return(db, overload); + let (return_ty, is_instance_return) = self.single_overload_return(db, env, overload); if is_instance_return { if saw_instance_return { sole_instance_return = None; @@ -463,7 +616,7 @@ impl<'db> ConstructorBinding<'db> { } if saw_instance_return { non_instance_returns.push( - sole_instance_return.unwrap_or_else(|| self.instance_return_type(db)), + sole_instance_return.unwrap_or_else(|| self.instance_return_type(db, env)), ); } Some(UnsafeUnionType::from_elements(db, non_instance_returns)) @@ -479,23 +632,28 @@ impl<'db> ConstructorBinding<'db> { fn single_overload_return( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, overload: &Binding<'db>, ) -> (Type<'db>, bool) { let return_ty = overload .unspecialized_return_type(db) .apply_optional_specialization( db, - overload.specialization(db).map(|specialization| { - self.unspecialize_class_type_variables(db, specialization) + overload.specialization(db, env).map(|specialization| { + self.unspecialize_class_type_variables(db, env, specialization) }), ); if self - .constructed_class_literal(db) - .is_some_and(|class_literal| constructor_returns_instance(db, class_literal, return_ty)) + .constructed_class_literal(db, env) + .is_some_and(|class_literal| { + constructor_returns_instance(db, env, class_literal, return_ty) + }) { return ( - return_ty - .apply_optional_specialization(db, self.instance_return_specialization(db)), + return_ty.apply_optional_specialization( + db, + self.instance_return_specialization(db, env), + ), true, ); } @@ -518,11 +676,12 @@ impl<'db> ConstructorBinding<'db> { fn unspecialize_class_type_variables( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, specialization: Specialization<'db>, ) -> Specialization<'db> { let Some(class_context) = self .constructed_instance_type() - .class_specialization(db) + .class_specialization(db, env) .map(|(_, specialization)| specialization.generic_context(db)) else { return specialization; @@ -556,11 +715,20 @@ impl<'db> ConstructorBinding<'db> { ) } - fn constructed_class_literal(&self, db: &'db dyn Db) -> Option> { - self.constructed_instance_type() + fn constructed_class_literal( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { + let instance_type = self.constructed_instance_type(); + let lookup_instance = match instance_type { + Type::Intersection(_) => instance_type.flatten_typevars(db, env), + _ => instance_type, + }; + lookup_instance .as_nominal_instance() // TODO may need to handle `Type::KnownInstance` here as well? - .map(|instance| instance.class(db).class_literal(db)) + .map(|instance| instance.class(db, env).class_literal(db)) } fn constructor_kind(&self) -> ConstructorCallableKind { @@ -593,7 +761,7 @@ impl<'db> ConstructorContext<'db> { self.instance_type } - fn kind(self) -> ConstructorCallableKind { + pub(super) fn kind(self) -> ConstructorCallableKind { self.kind } } @@ -620,6 +788,7 @@ impl ConstructorCallableKind { /// explicit `Any` is considered "not an instance", but an `Unknown` is considered "an instance". fn constructor_returns_instance<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class_literal: ClassLiteral<'db>, return_ty: Type<'db>, ) -> bool { @@ -627,10 +796,10 @@ fn constructor_returns_instance<'db>( Type::Union(union) => union .elements(db) .iter() - .all(|element| constructor_returns_instance(db, class_literal, *element)), + .all(|element| constructor_returns_instance(db, env, class_literal, *element)), Type::Intersection(intersection) => intersection .iter_positive(db) - .any(|element| constructor_returns_instance(db, class_literal, element)), + .any(|element| constructor_returns_instance(db, env, class_literal, element)), // Spec says an explicit `Any` return type should be considered non-instance. Type::Dynamic(DynamicType::Any) => false, // But a missing return annotation should be considered instance. @@ -640,7 +809,7 @@ fn constructor_returns_instance<'db>( // A `Never` constructor return is terminal and does not run downstream construction. Type::Never => false, Type::NominalInstance(instance) => instance - .class(db) + .class(db, env) .is_subtype_of_class_literal(db, class_literal), // We don't need to handle `ProtocolInstance` here, since the only way a protocol can be // instantiated is if a nominal class inherits it. If the nominal class inherits a @@ -675,7 +844,7 @@ impl<'db> Binding<'db> { return false; }; - let Type::SubclassOf(subclass_of) = cls_parameter_ty else { + let Type::SubclassOf(subclass_of) = cls_parameter_ty.resolve_type_alias(db) else { return false; }; let Some(cls_typevar) = subclass_of.into_type_var() else { @@ -743,3 +912,31 @@ impl<'db> Binding<'db> { } } } + +/// Replace every type parameter a specialization still maps to itself with `Unknown`. +/// +/// Such an entry means nothing was inferred for that parameter. That is fine while the +/// specialization is internal, but the constructed type is what everyone downstream reads, and a +/// type variable belonging to the callee's own signature means nothing to them. +fn erase_unsolved<'db>( + db: &'db dyn Db, + specialization: Specialization<'db>, +) -> Specialization<'db> { + let generic_context = specialization.generic_context(db); + let types: Box<[_]> = generic_context + .variables(db) + .zip(specialization.types(db)) + .map(|(variable, mapped_ty)| match mapped_ty { + Type::TypeVar(mapped) if *mapped == variable => Type::unknown(), + ty => *ty, + }) + .collect(); + Specialization::new( + db, + generic_context, + types, + specialization.materialization_kind(db), + None, + specialization.projections(db).to_vec().into_boxed_slice(), + ) +} diff --git a/crates/ty_python_semantic/src/types/call/bind/enum_property.rs b/crates/ty_python_semantic/src/types/call/bind/enum_property.rs index 1632373f06..b897cb7205 100644 --- a/crates/ty_python_semantic/src/types/call/bind/enum_property.rs +++ b/crates/ty_python_semantic/src/types/call/bind/enum_property.rs @@ -1,9 +1,8 @@ -use itertools::Itertools; - use super::Bindings; use crate::db::Db; use crate::types::call::CallArguments; use crate::types::{KnownClass, PropertyInstanceType, Type}; +use itertools::Itertools; impl<'db> Bindings<'db> { /// Replaces constructed `enum.property` instances with the property type derived from their diff --git a/crates/ty_python_semantic/src/types/callable.rs b/crates/ty_python_semantic/src/types/callable.rs index 457829ec51..95b380fe5c 100644 --- a/crates/ty_python_semantic/src/types/callable.rs +++ b/crates/ty_python_semantic/src/types/callable.rs @@ -1,3 +1,4 @@ +use crate::ProgramEnvironment; use ruff_python_ast::name::Name; use rustc_hash::FxHashSet; use smallvec::{SmallVec, smallvec_inline}; @@ -72,17 +73,23 @@ impl<'db> Type<'db> { .collect() } - pub(crate) fn try_upcast_to_callable(self, db: &'db dyn Db) -> Option> { - self.try_upcast_to_callable_with_policy(db, UpcastPolicy::default()) + pub(crate) fn try_upcast_to_callable( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { + self.try_upcast_to_callable_with_policy(db, env, UpcastPolicy::default()) } pub(crate) fn try_upcast_to_callable_with_recursive_fallback( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, recursive_definition: Option>, ) -> Option> { self.try_upcast_to_callable_with_policy_and_context( db, + env, UpcastPolicy::default(), CallableUpcastContext { recursive_definition, @@ -93,10 +100,12 @@ impl<'db> Type<'db> { pub(crate) fn try_upcast_to_callable_with_policy( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, policy: UpcastPolicy, ) -> Option> { self.try_upcast_to_callable_with_policy_and_context( db, + env, policy, CallableUpcastContext::default(), ) @@ -105,11 +114,13 @@ impl<'db> Type<'db> { fn try_upcast_to_callable_with_policy_and_context( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, policy: UpcastPolicy, context: CallableUpcastContext<'db>, ) -> Option> { if let Some(fallback) = self.materialized_divergent_fallback() { - return fallback.try_upcast_to_callable_with_policy_and_context(db, policy, context); + return fallback + .try_upcast_to_callable_with_policy_and_context(db, env, policy, context); } match self { @@ -117,15 +128,15 @@ impl<'db> Type<'db> { // parameter-only marker; behaves as the type a body sees (bound of `Key`) Type::Overlapping(overlapping) => overlapping - .value_type(db) - .try_upcast_to_callable_with_policy_and_context(db, policy, context), + .value_type(db, env) + .try_upcast_to_callable_with_policy_and_context(db, env, policy, context), Type::Restricted(restricted) => restricted .value_type(db) - .try_upcast_to_callable_with_policy_and_context(db, policy, context), + .try_upcast_to_callable_with_policy_and_context(db, env, policy, context), Type::Deferred(deferred) => deferred - .reduced(db) - .try_upcast_to_callable_with_policy_and_context(db, policy, context), + .reduced(db, env) + .try_upcast_to_callable_with_policy_and_context(db, env, policy, context), Type::Dynamic(_) => Some(CallableTypes::one(CallableType::function_like( db, @@ -157,6 +168,7 @@ impl<'db> Type<'db> { let call_symbol = self .member_lookup_with_policy( db, + env, "__call__", MemberLookupPolicy::NO_INSTANCE_FALLBACK, ) @@ -167,7 +179,7 @@ impl<'db> Type<'db> { { place .ty - .try_upcast_to_callable_with_policy_and_context(db, policy, context) + .try_upcast_to_callable_with_policy_and_context(db, env, policy, context) // The callable instance itself doesn't inherit the descriptor behavior of // its `__call__` method. .map(|callables| callables.map(|callable| callable.into_regular(db))) @@ -183,67 +195,74 @@ impl<'db> Type<'db> { Type::NewTypeInstance(newtype) => newtype .concrete_base_type(db) - .try_upcast_to_callable_with_policy_and_context(db, policy, context), + .try_upcast_to_callable_with_policy_and_context(db, env, policy, context), Type::SubclassOf(subclass_of_ty) if policy == UpcastPolicy::Sound => { Some(CallableTypes::one(CallableType::function_like( db, - Signature::new(Parameters::top(), subclass_of_ty.to_instance(db)), + Signature::new(Parameters::top(), subclass_of_ty.to_instance(db, env)), ))) } // 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::Protocol(protocol) => protocol - .class_origin() - .map(|origin| (*origin).into_callable(db)), - SubclassOfInner::TypeVar(tvar) => match tvar.typevar(db).bound_or_constraints(db) { - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - let upcast_callables = bound - .to_meta_type(db) - .try_upcast_to_callable_with_policy_and_context(db, policy, context)?; - Some(upcast_callables.map(|callable| { - let signatures = callable - .signatures(db) - .into_iter() - .map(|sig| sig.clone().with_return_type(Type::TypeVar(tvar))); - CallableType::new( - db, - CallableSignature::from_overloads(signatures), - callable.kind(db), - callable.provenance(db), - ) - })) + SubclassOfInner::Protocol(protocol) => protocol.class_origin(db).map(|origin| { + if protocol.materialization_kind(db).is_some() { + // The origin supplies the constructor, but the actual receiver retains + // `Top[P]` or `Bottom[P]`. Infer with both so instance-returning overloads + // are materialized without replacing explicit non-instance returns. + (*origin).into_callable_with_receiver(db, self) + } else { + (*origin).into_callable(db) } - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - let mut callables = SmallVec::new(); - for constraint in constraints.elements(db) { - let element_upcast = constraint - .to_meta_type(db) + }), + SubclassOfInner::TypeVar(tvar) => { + match tvar.typevar(db).require_bound_or_constraints(db, env) { + TypeVarBoundOrConstraints::UpperBound(bound) => { + let upcast_callables = bound + .constructor_for_typevar_bound(db, env) .try_upcast_to_callable_with_policy_and_context( - db, policy, context, + db, env, policy, context, )?; - for callable in element_upcast.into_inner() { + Some(upcast_callables.map(|callable| { let signatures = callable .signatures(db) .into_iter() .map(|sig| sig.clone().with_return_type(Type::TypeVar(tvar))); - callables.push(CallableType::new( + CallableType::new( db, CallableSignature::from_overloads(signatures), callable.kind(db), callable.provenance(db), - )); + ) + })) + } + TypeVarBoundOrConstraints::Constraints(constraints) => { + let mut callables = SmallVec::new(); + for constraint in constraints.elements(db) { + let element_upcast = constraint + .to_meta_type(db, env) + .try_upcast_to_callable_with_policy_and_context( + db, env, policy, context, + )?; + for callable in element_upcast.into_inner() { + let signatures = + callable.signatures(db).into_iter().map(|sig| { + sig.clone().with_return_type(Type::TypeVar(tvar)) + }); + callables.push(CallableType::new( + db, + CallableSignature::from_overloads(signatures), + callable.kind(db), + callable.provenance(db), + )); + } } + Some(CallableTypes::new(callables)) } - Some(CallableTypes::new(callables)) } - None => Some(CallableTypes::one(CallableType::single( - db, - Signature::new(Parameters::gradual_form(), Type::TypeVar(tvar)), - ))), - }, + } SubclassOfInner::Dynamic(_) => Some(CallableTypes::one(CallableType::single( db, Signature::new(Parameters::unknown(), Type::from(subclass_of_ty)), @@ -254,7 +273,7 @@ impl<'db> Type<'db> { let mut callables = SmallVec::new(); for element in union.elements(db) { let element_callable = element - .try_upcast_to_callable_with_policy_and_context(db, policy, context)?; + .try_upcast_to_callable_with_policy_and_context(db, env, policy, context)?; callables.extend(element_callable.into_inner()); } Some(CallableTypes::new(callables)) @@ -262,14 +281,14 @@ impl<'db> Type<'db> { Type::LiteralValue(literal) => match literal.kind() { LiteralValueTypeKind::Enum(enum_literal) => enum_literal - .enum_class_instance(db) - .try_upcast_to_callable_with_policy_and_context(db, policy, context), + .enum_class_instance(db, env) + .try_upcast_to_callable_with_policy_and_context(db, env, policy, context), _ => None, }, Type::TypeAlias(alias) => alias .value_type(db) - .try_upcast_to_callable_with_policy_and_context(db, policy, context), + .try_upcast_to_callable_with_policy_and_context(db, env, policy, context), Type::KnownBoundMethod(KnownBoundMethodType::FunctionTypeDunderCall(function)) if context.is_recursive_reference(db, function) => @@ -279,7 +298,7 @@ impl<'db> Type<'db> { Type::KnownBoundMethod(method) => Some(CallableTypes::one(CallableType::new( db, - CallableSignature::from_overloads(method.signatures(db)), + CallableSignature::from_overloads(method.signatures(db, env)), CallableTypeKind::Regular, CallableFunctionProvenance::None, ))), @@ -287,7 +306,7 @@ impl<'db> Type<'db> { Type::WrapperDescriptor(wrapper_descriptor) => { Some(CallableTypes::one(CallableType::new( db, - CallableSignature::from_overloads(wrapper_descriptor.signatures(db)), + CallableSignature::from_overloads(wrapper_descriptor.signatures(db, env)), CallableTypeKind::Regular, CallableFunctionProvenance::None, ))) @@ -298,7 +317,7 @@ impl<'db> Type<'db> { db, Signature::new( Parameters::standard([Parameter::positional_only(None) - .with_annotated_type(newtype.base(db).instance_type(db))]), + .with_annotated_type(newtype.base(db).instance_type(db, env))]), Type::NewTypeInstance(newtype), ), ))) @@ -318,21 +337,19 @@ impl<'db> Type<'db> { | KnownInstanceType::FunctoolsPartialCall(partial), ) => Some(CallableTypes::one(partial.partial(db))), - Type::Intersection(intersection) => { - intersection - .finite_alternative_union(db) - .and_then(|alternatives| { - alternatives.try_upcast_to_callable_with_policy(db, policy) - }) - } + Type::Intersection(intersection) => intersection + .finite_alternative_union(db, env) + .and_then(|alternatives| { + alternatives.try_upcast_to_callable_with_policy(db, env, policy) + }), // Only the materializations that are callable can be the one at hand; a // non-callable materialization does not disqualify the others. Type::UnsafeUnion(unsafe_union) => { let mut callables = SmallVec::new(); for element in unsafe_union.elements(db) { - if let Some(element_callable) = - element.try_upcast_to_callable_with_policy_and_context(db, policy, context) + if let Some(element_callable) = element + .try_upcast_to_callable_with_policy_and_context(db, env, policy, context) { callables.extend(element_callable.into_inner()); } @@ -341,8 +358,8 @@ impl<'db> Type<'db> { } Type::EnumComplement(complement) => complement - .remaining_literal_union(db) - .try_upcast_to_callable_with_policy_and_context(db, policy, context), + .remaining_literal_union(db, env) + .try_upcast_to_callable_with_policy_and_context(db, env, policy, context), // TODO Type::DataclassDecorator(_) @@ -527,10 +544,7 @@ impl<'db> CallableType<'db> { ) } - pub(crate) fn paramspec_value( - db: &'db dyn Db, - parameters: Parameters<'db>, - ) -> CallableType<'db> { + fn paramspec_value(db: &'db dyn Db, parameters: Parameters<'db>) -> CallableType<'db> { CallableType::new( db, CallableSignature::single(Signature::new(parameters, Type::unknown())), @@ -548,7 +562,7 @@ impl<'db> CallableType<'db> { matches!(self.kind(db), CallableTypeKind::FunctionLike) } - pub(crate) fn is_dunder_paramspec(self, db: &'db dyn Db) -> bool { + fn is_dunder_paramspec(self, db: &'db dyn Db) -> bool { matches!(self.kind(db), CallableTypeKind::DunderParamSpec) } @@ -586,20 +600,25 @@ impl<'db> CallableType<'db> { /// Returns the reduced callable produced by partially applying selected overloads. pub(crate) fn partially_apply( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, overloads: impl IntoIterator>, ) -> Option { Some(Self::new( db, - CallableSignature::partially_apply(db, overloads)?, + CallableSignature::partially_apply(db, env, overloads)?, CallableTypeKind::Regular, CallableFunctionProvenance::None, )) } /// Reifies this callable as the nominal `functools.partial[T]` instance for its return type. - pub(crate) fn into_functools_partial_instance(self, db: &'db dyn Db) -> Type<'db> { - let return_ty = self.signatures(db).overload_return_type_or_unknown(db); - KnownClass::FunctoolsPartial.to_specialized_instance(db, &[return_ty]) + pub(crate) fn into_functools_partial_instance( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { + let return_ty = self.signatures(db).overload_return_type_or_unknown(db, env); + KnownClass::FunctoolsPartial.to_specialized_instance(db, env, &[return_ty]) } /// Wraps this reduced callable as a synthetic `functools.partial(...)` instance type. @@ -616,6 +635,7 @@ impl<'db> CallableType<'db> { pub(crate) fn bind_self( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, self_type: Option>, ) -> CallableType<'db> { if self.is_dunder_paramspec(db) { @@ -624,7 +644,7 @@ impl<'db> CallableType<'db> { CallableType::new( db, - self.signatures(db).bind_self(db, self_type), + self.signatures(db).bind_self(db, env, self_type), self.kind(db), self.provenance(db), ) @@ -648,20 +668,26 @@ impl<'db> CallableType<'db> { ) } - pub(crate) fn apply_self(self, db: &'db dyn Db, self_type: Type<'db>) -> CallableType<'db> { - self.apply_self_with_receiver(db, self_type, self_type) + pub(crate) fn apply_self( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + self_type: Type<'db>, + ) -> CallableType<'db> { + self.apply_self_with_receiver(db, env, self_type, self_type) } pub(crate) fn apply_self_with_receiver( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, receiver_type: Type<'db>, self_type: Type<'db>, ) -> CallableType<'db> { CallableType::new( db, self.signatures(db) - .apply_self_with_receiver(db, receiver_type, self_type), + .apply_self_with_receiver(db, env, receiver_type, self_type), self.kind(db), self.provenance(db), ) @@ -683,13 +709,14 @@ impl<'db> CallableType<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { Some(CallableType::new( db, self.signatures(db) - .recursive_type_normalized_impl(db, div, nested)?, + .recursive_type_normalized_impl(db, env, div, nested)?, self.kind(db), self.provenance(db), )) @@ -700,7 +727,7 @@ impl<'db> CallableType<'db> { db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { if let TypeMapping::RescopeReturnCallables(replacements) = type_mapping { return replacements.get(&self).copied().unwrap_or(self); @@ -718,12 +745,13 @@ impl<'db> CallableType<'db> { pub(super) fn find_legacy_typevars_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, ) { self.signatures(db) - .find_legacy_typevars_impl(db, binding_context, typevars, visitor); + .find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } } @@ -738,7 +766,7 @@ impl<'db> CallableType<'db> { pub(crate) struct CallableTypes<'db>(SmallVec<[CallableType<'db>; 1]>); impl<'db> CallableTypes<'db> { - pub(super) fn new(callables: SmallVec<[CallableType<'db>; 1]>) -> Self { + fn new(callables: SmallVec<[CallableType<'db>; 1]>) -> Self { assert!(!callables.is_empty(), "CallableTypes should not be empty"); CallableTypes(callables) } @@ -764,7 +792,7 @@ impl<'db> CallableTypes<'db> { &self.0 } - pub(super) fn into_inner(self) -> SmallVec<[CallableType<'db>; 1]> { + fn into_inner(self) -> SmallVec<[CallableType<'db>; 1]> { self.0 } @@ -772,9 +800,9 @@ impl<'db> CallableTypes<'db> { self.0.iter() } - pub(crate) fn into_type(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn into_type(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { assert!(!self.0.is_empty(), "CallableTypes should not be empty"); - UnionType::from_elements(db, self.0.into_iter().map(Type::Callable)) + UnionType::from_elements(db, env, self.0.into_iter().map(Type::Callable)) } pub(crate) fn map(self, mut f: impl FnMut(CallableType<'db>) -> CallableType<'db>) -> Self { @@ -793,7 +821,10 @@ impl<'db> CallableTypes<'db> { for callable in self.0 { for signature in callable.signatures(db) { let signature = signature.clone(); - let dedup_key = signature.clone().with_definition(None); + let dedup_key = signature + .clone() + .with_definition(None) + .with_source_overload_index(None); if seen_overloads.insert(dedup_key) { overloads.push(signature); } diff --git a/crates/ty_python_semantic/src/types/character.rs b/crates/ty_python_semantic/src/types/character.rs index a5e5065766..59eca85e70 100644 --- a/crates/ty_python_semantic/src/types/character.rs +++ b/crates/ty_python_semantic/src/types/character.rs @@ -10,23 +10,32 @@ //! redundant)? ([`is_character_instance`]) use crate::Db; +use crate::types::ProgramEnvironment; use crate::types::{KnownClass, Type}; /// whether `ty` denotes the `Character` type — its instance type (the meaning /// of a bare `Character` in an annotation position) or the class literal /// `type[Character]` (its meaning in a value position). a union, optional, or /// `str` does not qualify -pub fn denotes_character<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { - is_character_instance(db, ty) +pub fn denotes_character<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> bool { + is_character_instance(db, env, ty) || ty .to_class_type(db) .is_some_and(|class| class.is_known(db, KnownClass::Character)) } /// whether `ty` is a `Character` instance — its class is exactly `Character` -pub fn is_character_instance<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { +pub fn is_character_instance<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> bool { matches!( ty, - Type::NominalInstance(instance) if instance.class(db).is_known(db, KnownClass::Character) + Type::NominalInstance(instance) if instance.class(db, env).is_known(db, KnownClass::Character) ) } diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 3b10c7ca3d..8a375d7e73 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -1,3 +1,4 @@ +use crate::ProgramEnvironment; use std::fmt::Write; pub(crate) use self::dynamic_literal::{ @@ -10,11 +11,13 @@ pub(super) use self::named_tuple::{ DynamicNamedTupleAnchor, DynamicNamedTupleLiteral, NamedTupleField, NamedTupleSpec, }; pub(crate) use self::static_literal::{ - ClassLiteralFlags, ExpandedClassBaseEntry, StaticClassLiteral, based_enum_has_payload_variants, - based_enum_of_variant, based_enum_unit_member_names, based_enum_unit_variant_class, - based_enum_variant_union, expanded_class_base_entries, + ClassLiteralFlags, ExpandedClassBaseEntry, FrozenDataclassDispatch, StaticClassLiteral, + based_enum_has_payload_variants, based_enum_of_variant, based_enum_unit_member_names, + based_enum_unit_variant_class, based_enum_variant_union, expanded_class_base_entries, +}; +pub(super) use self::typed_dict::{ + DynamicTypedDictAnchor, DynamicTypedDictLiteral, synthesized_typed_dict_class_member, }; -pub(super) use self::typed_dict::{DynamicTypedDictAnchor, DynamicTypedDictLiteral}; use super::dedicated::{django, pydantic, sqlalchemy}; use super::{ BoundTypeVarIdentity, BoundTypeVarInstance, MemberLookupPolicy, MroIterator, SpecialFormType, @@ -28,9 +31,7 @@ use crate::types::constraints::{ }; use crate::types::enums::enum_metadata; use crate::types::function::{AbstractMethodKind, DataclassTransformerParams}; -use crate::types::generics::{ - GenericContext, InferableTypeVars, Specialization, walk_specialization, -}; +use crate::types::generics::{GenericContext, Specialization, walk_specialization}; use crate::types::known_instance::DeprecatedInstance; use crate::types::member::Member; use crate::types::relation::{ @@ -40,6 +41,7 @@ use crate::types::signatures::{ CallableSignature, Parameter, Parameters, Signature, SignatureRelationVisitor, }; use crate::types::tuple::TupleSpec; +use crate::types::typevar::TypeVarSet; use crate::types::{ ApplyTypeMappingVisitor, CallableType, CallableTypes, DataclassParams, FindLegacyTypeVarsVisitor, IntersectionType, TypeContext, TypeMapping, TypedDictModule, @@ -55,11 +57,13 @@ use crate::{ }; use ruff_db::diagnostic::Span; use ruff_db::files::File; +use ruff_db::parsed::parsed_module; use ruff_python_ast::name::Name; -use ruff_python_ast::{self as ast}; -use ruff_text_size::TextRange; +use ruff_python_ast::{self as ast, NodeIndex}; +use ruff_text_size::{Ranged, TextRange}; use ty_python_core::definition::Definition; -use ty_python_core::{place_table, use_def_map}; +use ty_python_core::scope::ScopeId; +use ty_python_core::{ProgramFile, place_table, use_def_map}; mod dynamic_literal; mod enum_literal; @@ -68,8 +72,47 @@ mod named_tuple; mod static_literal; mod typed_dict; +#[derive(Clone, Copy)] +enum DynamicClassHeaderAnchor<'db> { + Definition(Definition<'db>), + ScopeOffset(u32), +} + +/// Returns the source range of a call that creates a dynamic class. +/// +/// ```python +/// Color = Enum("Color", "RED GREEN") +/// # ^^^^^^^^^^^^^^^^^^^^^^^^^^ +/// ``` +fn dynamic_class_header_range<'db>( + db: &'db dyn Db, + scope: ScopeId<'db>, + anchor: DynamicClassHeaderAnchor<'db>, +) -> TextRange { + let module = parsed_module(db, scope.python_file(db)).load(db); + match anchor { + DynamicClassHeaderAnchor::Definition(definition) => definition + .kind(db) + .value(&module) + .expect("dynamic class definitions should only be used for assignments") + .range(), + DynamicClassHeaderAnchor::ScopeOffset(offset) => { + let scope_anchor = scope.node(db).node_index().unwrap_or(NodeIndex::from(0)); + let anchor_u32 = scope_anchor + .as_u32() + .expect("anchor should not be NodeIndex::NONE"); + let absolute_index = NodeIndex::from(anchor_u32 + offset); + let node: &ast::ExprCall = module + .get_by_index(absolute_index) + .try_into() + .expect("scope offset should point to ExprCall"); + node.range() + } + } +} + bitflags::bitflags! { - /// Properties that affect the representation of instances of a class. + /// Properties shared by all instances of a class. /// /// This combines properties derived from the MRO into the existing class-classification /// query, avoiding a separate cached query for each property. @@ -79,6 +122,10 @@ bitflags::bitflags! { const TYPED_DICT = 1 << 0; /// The class directly or indirectly inherits from an explicit `Any` base. const INHERITS_FROM_EXPLICIT_ANY = 1 << 1; + /// The class may define or inherit a custom `__getattribute__` method. + const HAS_CUSTOM_GETATTRIBUTE = 1 << 2; + /// An unknown base may provide an attribute-interception method. + const HAS_DYNAMIC_GETATTRIBUTE = 1 << 3; } } @@ -136,6 +183,7 @@ impl<'db> CodeGeneratorKind<'db> { db: &'db dyn Db, class: StaticClassLiteral<'db>, ) -> Option> { + let env = ProgramEnvironment::from_scope(class.body_scope(db)); // If a class is directly decorated as a dataclass, it's a dataclass. // If a class' metaclass is a dataclass transformer, it's a dataclass. // If a class inherits from a base class that is a dataclass @@ -152,7 +200,7 @@ impl<'db> CodeGeneratorKind<'db> { info.params, )) } else if KnownClass::Type - .try_to_class_literal(db) + .try_to_class_literal(db, &env) .is_none_or(|type_class| { !class.is_subclass_of( db, @@ -253,7 +301,7 @@ impl<'db> CodeGeneratorKind<'db> { ) } - pub(super) fn dataclass_transformer_params(self) -> Option> { + fn dataclass_transformer_params(self) -> Option> { match self { Self::DataclassLike(params) => params, Self::Pydantic(_) @@ -309,7 +357,7 @@ impl<'db> CodeGeneratorKind<'db> { /// def f(c: C): /// c.value # okay, `value` will be set by `C`'s constructor /// ``` - pub(super) const fn treats_fields_as_instance_attributes(self) -> bool { + const fn treats_fields_as_instance_attributes(self) -> bool { matches!(self, Self::DataclassLike(_) | Self::Pydantic(_)) } @@ -324,7 +372,7 @@ impl<'db> CodeGeneratorKind<'db> { /// /// C(value=42) /// ``` - pub(super) fn synthesizes_constructor_signature_from_fields( + fn synthesizes_constructor_signature_from_fields( self, db: &'db dyn Db, class: StaticClassLiteral<'db>, @@ -390,6 +438,7 @@ impl<'db> GenericAlias<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -397,7 +446,7 @@ impl<'db> GenericAlias<'db> { db, self.origin(db), self.specialization(db) - .recursive_type_normalized_impl(db, div, nested)?, + .recursive_type_normalized_impl(db, env, div, nested)?, )) } @@ -408,19 +457,20 @@ impl<'db> GenericAlias<'db> { pub(super) fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { let tcx = tcx .annotation() - .and_then(|ty| ty.specialization_of(db, self.origin(db))) + .and_then(|ty| ty.specialization_of(db, env, self.origin(db))) .map(|specialization| specialization.types(db)) .unwrap_or(&[]); let original_specialization = self.specialization(db); let specialization = - original_specialization.apply_type_mapping_impl(db, type_mapping, tcx, visitor); + original_specialization.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor); if specialization == original_specialization { self } else { @@ -431,12 +481,18 @@ impl<'db> GenericAlias<'db> { pub(super) fn find_legacy_typevars_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, ) { - self.specialization(db) - .find_legacy_typevars_impl(db, binding_context, typevars, visitor); + self.specialization(db).find_legacy_typevars_impl( + db, + env, + binding_context, + typevars, + visitor, + ); } pub(crate) fn is_typed_dict(self, db: &'db dyn Db) -> bool { @@ -450,15 +506,31 @@ impl<'db> From> for Type<'db> { } } -#[salsa::tracked] impl<'db> VarianceInferable<'db> for GenericAlias<'db> { + fn variance_of( + self, + db: &'db dyn Db, + _: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { + self.variance_of_owner(db, typevar) + } +} + +#[salsa::tracked] +impl<'db> GenericAlias<'db> { #[salsa::tracked( returns(copy), cycle_initial=|_, _, _, _| TypeVarVariance::Bivariant, heap_size=ruff_memory_usage::heap_size )] - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { + fn variance_of_owner( + self, + db: &'db dyn Db, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { let origin = self.origin(db); + let env = ProgramEnvironment::from_file(origin.program_file(db)); let specialization = self.specialization(db); @@ -470,7 +542,8 @@ impl<'db> VarianceInferable<'db> for GenericAlias<'db> { .zip(specialization.types(db)) .map(|(generic_typevar, ty)| { if let Some(explicit_variance) = generic_typevar.typevar(db).explicit_variance(db) { - ty.with_polarity(explicit_variance).variance_of(db, typevar) + ty.with_polarity(explicit_variance) + .variance_of(db, &env, typevar) } else { // `with_polarity` composes the passed variance with the // inferred one. The inference is done lazily, as we can @@ -483,10 +556,10 @@ impl<'db> VarianceInferable<'db> for GenericAlias<'db> { // If salsa let us look at the cache, we could check first // to see if the class literal query was already run. - let typevar_variance_in_substituted_type = ty.variance_of(db, typevar); + let typevar_variance_in_substituted_type = ty.variance_of(db, &env, typevar); origin .with_polarity(typevar_variance_in_substituted_type) - .variance_of(db, generic_typevar.identity(db)) + .variance_of(db, &env, generic_typevar.identity(db)) } }) .collect() @@ -513,9 +586,9 @@ pub enum ClassLiteral<'db> { #[salsa::tracked] impl<'db> ClassLiteral<'db> { /// Return a `ClassLiteral` representing the class `builtins.object` - pub(super) fn object(db: &'db dyn Db) -> Self { + pub(super) fn object(db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Self { KnownClass::Object - .to_class_literal(db) + .to_class_literal(db, env) .as_class_literal() .expect("`object` should always be a non-generic class in typeshed") } @@ -523,21 +596,22 @@ impl<'db> ClassLiteral<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { match self { Self::Dynamic(dynamic) => Some(Self::Dynamic( - dynamic.recursive_type_normalized_impl(db, div, nested)?, + dynamic.recursive_type_normalized_impl(db, env, div, nested)?, )), Self::DynamicNamedTuple(named_tuple) => Some(Self::DynamicNamedTuple( - named_tuple.recursive_type_normalized_impl(db, div, nested)?, + named_tuple.recursive_type_normalized_impl(db, env, div, nested)?, )), Self::DynamicTypedDict(typed_dict) => Some(Self::DynamicTypedDict( - typed_dict.recursive_type_normalized_impl(db, div, nested)?, + typed_dict.recursive_type_normalized_impl(db, env, div, nested)?, )), Self::DynamicEnum(enum_literal) => Some(Self::DynamicEnum( - enum_literal.recursive_type_normalized_impl(db, div, nested)?, + enum_literal.recursive_type_normalized_impl(db, env, div, nested)?, )), Self::Static(_) => Some(self), } @@ -560,7 +634,7 @@ impl<'db> ClassLiteral<'db> { } /// Returns whether this class has PEP 695 type parameters. - pub(crate) fn has_pep_695_type_params(self, db: &'db dyn Db) -> bool { + fn has_pep_695_type_params(self, db: &'db dyn Db) -> bool { self.as_static() .is_some_and(|class| class.has_pep_695_type_params(db)) } @@ -570,7 +644,7 @@ impl<'db> ClassLiteral<'db> { MroIterator::new(db, self, None) } - /// Return the properties that affect how instances of this class are represented. + /// Return the properties shared by all instances of this class. pub(super) fn instance_flags(self, db: &'db dyn Db) -> ClassInstanceFlags { match self { Self::Static(literal) => literal.instance_flags(db), @@ -594,6 +668,12 @@ impl<'db> ClassLiteral<'db> { /// Return whether this class directly or indirectly inherits from an explicit `Any` base. pub(super) fn inherits_from_explicit_any(self, db: &'db dyn Db) -> bool { + if let Some(class) = self.as_static() + && (class.known(db).is_some() || !class.has_explicit_bases(db)) + { + return false; + } + self.instance_flags(db) .contains(ClassInstanceFlags::INHERITS_FROM_EXPLICIT_ANY) } @@ -613,15 +693,16 @@ impl<'db> ClassLiteral<'db> { pub(crate) fn class_member( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, policy: MemberLookupPolicy, ) -> PlaceAndQualifiers<'db> { match self { - Self::Static(class) => class.class_member(db, name, policy), - Self::Dynamic(class) => class.class_member(db, name, policy), - Self::DynamicNamedTuple(namedtuple) => namedtuple.class_member(db, name, policy), - Self::DynamicTypedDict(typeddict) => typeddict.class_member(db, name, policy), - Self::DynamicEnum(enum_lit) => enum_lit.class_member(db, name), + Self::Static(class) => class.class_member(db, env, name, policy), + Self::Dynamic(class) => class.class_member(db, env, name, policy), + Self::DynamicNamedTuple(namedtuple) => namedtuple.class_member(db, env, name, policy), + Self::DynamicTypedDict(typeddict) => typeddict.class_member(db, env, name, policy), + Self::DynamicEnum(enum_lit) => enum_lit.class_member(db, env, name), } } @@ -631,22 +712,24 @@ impl<'db> ClassLiteral<'db> { pub(super) fn class_member_from_mro( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, policy: MemberLookupPolicy, mro_iter: impl Iterator>, ) -> PlaceAndQualifiers<'db> { match self { - Self::Static(class) => class.class_member_from_mro(db, name, policy, mro_iter), + Self::Static(class) => class.class_member_from_mro(db, env, name, policy, mro_iter), Self::Dynamic(_) | Self::DynamicNamedTuple(_) | Self::DynamicTypedDict(_) | Self::DynamicEnum(_) => { // Dynamic classes don't have inherited generic context and are never `object`. - let result = MroLookup::new(db, mro_iter).class_member(name, policy, None, false); + let result = + MroLookup::new(db, env, mro_iter).class_member(name, policy, None, false); match result { - ClassMemberResult::Done(result) => result.finalize(db), + ClassMemberResult::Done(result) => result.finalize(db, env), ClassMemberResult::TypedDict(module) => { - typed_dict::typed_dict_fallback_class_member(db, module, policy, name) + typed_dict::typed_dict_fallback_class_member(db, env, module, policy, name) } } } @@ -663,13 +746,10 @@ impl<'db> ClassLiteral<'db> { /// For static classes, this applies default type arguments. /// For dynamic classes, this returns a non-generic class type. pub(crate) fn default_specialization(self, db: &'db dyn Db) -> ClassType<'db> { - match self { - Self::Static(class) => class.default_specialization(db), - Self::Dynamic(_) - | Self::DynamicNamedTuple(_) - | Self::DynamicTypedDict(_) - | Self::DynamicEnum(_) => ClassType::NonGeneric(self), - } + self.as_static().map_or_else( + || ClassType::NonGeneric(self), + |class| class.default_specialization(db), + ) } /// Returns the unknown specialization of this class. @@ -678,24 +758,18 @@ impl<'db> ClassLiteral<'db> { /// 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::Static(class) => class.unknown_specialization(db), - Self::Dynamic(_) - | Self::DynamicNamedTuple(_) - | Self::DynamicTypedDict(_) - | Self::DynamicEnum(_) => ClassType::NonGeneric(self), - } + self.as_static().map_or_else( + || ClassType::NonGeneric(self), + |class| class.unknown_specialization(db), + ) } /// Returns the identity specialization for this class (same as default for non-generic). pub(crate) fn identity_specialization(self, db: &'db dyn Db) -> ClassType<'db> { - match self { - Self::Static(class) => class.identity_specialization(db), - Self::Dynamic(_) - | Self::DynamicNamedTuple(_) - | Self::DynamicTypedDict(_) - | Self::DynamicEnum(_) => ClassType::NonGeneric(self), - } + self.as_static().map_or_else( + || ClassType::NonGeneric(self), + |class| class.identity_specialization(db), + ) } /// Returns the generic context if this is a generic class. @@ -719,20 +793,21 @@ impl<'db> ClassLiteral<'db> { /// Returns whether this class is `builtins.tuple` exactly pub(crate) fn is_tuple(self, db: &'db dyn Db) -> bool { - match self { - Self::Static(class) => class.is_tuple(db), - Self::Dynamic(_) - | Self::DynamicNamedTuple(_) - | Self::DynamicTypedDict(_) - | Self::DynamicEnum(_) => false, - } + self.as_static().is_some_and(|class| class.is_tuple(db)) } /// Return a type representing "the set of all instances of the metaclass of this class". - pub(crate) fn metaclass_instance_type(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn metaclass_instance_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { self.metaclass(db) - .to_instance_approximation(db) - .expect("`Type::to_instance()` should always return `Some()` when called on the type of a metaclass") + .to_instance_approximation(db, env) + .expect( + "`Type::to_instance()` should always return `Some()` \ + when called on the type of a metaclass", + ) } /// Returns whether this class is type-check only. @@ -752,6 +827,16 @@ impl<'db> ClassLiteral<'db> { } } + pub(crate) fn program_file(self, db: &'db dyn Db) -> ProgramFile<'db> { + match self { + Self::Static(class) => class.program_file(db), + Self::Dynamic(class) => class.scope(db).program_file(db), + Self::DynamicNamedTuple(class) => class.scope(db).program_file(db), + Self::DynamicTypedDict(class) => class.scope(db).program_file(db), + Self::DynamicEnum(enum_lit) => enum_lit.scope(db).program_file(db), + } + } + /// Returns the range of the class's "header". /// /// For static classes, this is the class name and any arguments passed to the `class` statement. @@ -805,7 +890,7 @@ impl<'db> ClassLiteral<'db> { /// ```python /// X = type("X", (), {"__lt__": lambda self, other: True}) /// ``` - pub(crate) fn has_own_ordering_method(self, db: &'db dyn Db) -> bool { + fn has_own_ordering_method(self, db: &'db dyn Db) -> bool { match self { Self::Static(class) => class.has_own_ordering_method(db), Self::Dynamic(class) => class.has_own_ordering_method(db), @@ -891,7 +976,7 @@ impl<'db> ClassLiteral<'db> { /// class Foo(int, X): ... /// TypeError: multiple bases have instance lay-out conflict /// ``` - pub(super) fn as_disjoint_base(self, db: &'db dyn Db) -> Option> { + fn as_disjoint_base(self, db: &'db dyn Db) -> Option> { match self { Self::Static(class) => class.as_disjoint_base(db), Self::Dynamic(class) => class.as_disjoint_base(db), @@ -903,13 +988,17 @@ impl<'db> ClassLiteral<'db> { } /// Returns a non-generic instance of this class. - pub(crate) fn to_non_generic_instance(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn to_non_generic_instance( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { match self { Self::Static(class) => class.to_non_generic_instance(db), Self::Dynamic(_) | Self::DynamicNamedTuple(_) | Self::DynamicTypedDict(_) - | Self::DynamicEnum(_) => Type::instance(db, ClassType::NonGeneric(self)), + | Self::DynamicEnum(_) => Type::instance(db, env, ClassType::NonGeneric(self)), } } @@ -941,15 +1030,16 @@ impl<'db> ClassLiteral<'db> { pub(crate) fn instance_member( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, specialization: Option>, name: &str, ) -> PlaceAndQualifiers<'db> { match self { - Self::Static(class) => class.instance_member(db, specialization, name), - Self::Dynamic(class) => class.instance_member(db, name), - Self::DynamicNamedTuple(namedtuple) => namedtuple.instance_member(db, name), + Self::Static(class) => class.instance_member(db, env, specialization, name), + Self::Dynamic(class) => class.instance_member(db, env, name), + Self::DynamicNamedTuple(namedtuple) => namedtuple.instance_member(db, env, name), Self::DynamicTypedDict(_) => PlaceAndQualifiers::default(), - Self::DynamicEnum(enum_lit) => enum_lit.instance_member(db, name), + Self::DynamicEnum(enum_lit) => enum_lit.instance_member(db, env, name), } } @@ -968,13 +1058,14 @@ impl<'db> ClassLiteral<'db> { pub(crate) fn typed_dict_member( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, specialization: Option>, name: &str, policy: MemberLookupPolicy, ) -> PlaceAndQualifiers<'db> { match self { - Self::Static(class) => class.typed_dict_member(db, specialization, name, policy), - Self::DynamicTypedDict(typeddict) => typeddict.class_member(db, name, policy), + Self::Static(class) => class.typed_dict_member(db, env, specialization, name, policy), + Self::DynamicTypedDict(typeddict) => typeddict.class_member(db, env, name, policy), Self::Dynamic(_) | Self::DynamicNamedTuple(_) | Self::DynamicEnum(_) => { Place::Undefined.into() } @@ -1005,7 +1096,8 @@ impl<'db> ClassLiteral<'db> { Self::Static(static_class) => static_class.explicit_bases(db).into(), Self::Dynamic(dynamic_class) => dynamic_class.explicit_bases(db).into(), Self::DynamicNamedTuple(namedtuple) => { - [Type::from(namedtuple.tuple_base_class(db))].into() + let env = ProgramEnvironment::from_scope(namedtuple.scope(db)); + [Type::from(namedtuple.tuple_base_class(db, &env))].into() } Self::DynamicTypedDict(_) => { // TypedDicts always inherit from `dict` @@ -1064,8 +1156,8 @@ pub enum ClassType<'db> { #[salsa::tracked] impl<'db> ClassType<'db> { /// Return a `ClassType` representing the class `builtins.object` - pub(super) fn object(db: &'db dyn Db) -> Self { - ClassType::NonGeneric(ClassLiteral::object(db)) + pub(super) fn object(db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Self { + ClassType::NonGeneric(ClassLiteral::object(db, env)) } pub(super) const fn is_generic(self) -> bool { @@ -1082,15 +1174,16 @@ impl<'db> ClassType<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { match self { Self::NonGeneric(class) => Some(Self::NonGeneric( - class.recursive_type_normalized_impl(db, div, nested)?, + class.recursive_type_normalized_impl(db, env, div, nested)?, )), Self::Generic(generic) => Some(Self::Generic( - generic.recursive_type_normalized_impl(db, div, nested)?, + generic.recursive_type_normalized_impl(db, env, div, nested)?, )), } } @@ -1160,14 +1253,17 @@ impl<'db> ClassType<'db> { | ClassLiteral::DynamicTypedDict(_) | ClassLiteral::DynamicEnum(_), ) => None, - Self::Generic(generic) => Some(( - generic.origin(db), - Some( - generic - .specialization(db) - .apply_optional_specialization(db, additional_specialization), - ), - )), + Self::Generic(generic) => { + let origin = generic.origin(db); + Some(( + origin, + Some( + generic + .specialization(db) + .apply_optional_specialization(db, additional_specialization), + ), + )) + } } } @@ -1194,7 +1290,7 @@ impl<'db> ClassType<'db> { } /// Return `Some` if this class is known to be a [`DisjointBase`], or `None` if it is not. - pub(super) fn as_disjoint_base(self, db: &'db dyn Db) -> Option> { + fn as_disjoint_base(self, db: &'db dyn Db) -> Option> { self.class_literal(db).as_disjoint_base(db) } @@ -1227,21 +1323,22 @@ impl<'db> ClassType<'db> { pub(super) fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { match self { // basedpython: a `{"key": T}` literal synthesizes a non-generic class whose schema // can still mention type variables, so the mapping has to reach inside it Self::NonGeneric(ClassLiteral::DynamicTypedDict(typeddict)) => { Self::NonGeneric(ClassLiteral::DynamicTypedDict( - typeddict.apply_type_mapping_impl(db, type_mapping, tcx, visitor), + typeddict.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor), )) } Self::NonGeneric(_) => self, Self::Generic(generic) => { - Self::Generic(generic.apply_type_mapping_impl(db, type_mapping, tcx, visitor)) + Self::Generic(generic.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor)) } } } @@ -1249,6 +1346,7 @@ impl<'db> ClassType<'db> { pub(super) fn find_legacy_typevars_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, @@ -1256,7 +1354,7 @@ impl<'db> ClassType<'db> { match self { Self::NonGeneric(_) => {} Self::Generic(generic) => { - generic.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + generic.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } } } @@ -1288,16 +1386,19 @@ impl<'db> ClassType<'db> { additional_specialization: Option>, ) -> MroIterator<'db> { match self { - Self::NonGeneric(class) => class.iter_mro(db), - Self::Generic(generic) => MroIterator::new( - db, - ClassLiteral::Static(generic.origin(db)), - Some( - generic - .specialization(db) - .apply_optional_specialization(db, additional_specialization), - ), - ), + Self::NonGeneric(class) => MroIterator::new(db, class, None), + Self::Generic(generic) => { + let origin = generic.origin(db); + MroIterator::new( + db, + ClassLiteral::Static(origin), + Some( + generic + .specialization(db) + .apply_optional_specialization(db, additional_specialization), + ), + ) + } } } @@ -1316,7 +1417,10 @@ impl<'db> ClassType<'db> { /// /// The value of the map is a struct containing information about the abstract method. #[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)] - pub(crate) fn abstract_methods(self, db: &'db dyn Db) -> FxIndexMap> { + pub(in crate::types) fn abstract_methods( + self, + db: &'db dyn Db, + ) -> FxIndexMap> { fn type_as_abstract_method<'db>( db: &'db dyn Db, ty: Type<'db>, @@ -1348,6 +1452,7 @@ impl<'db> ClassType<'db> { } let mut abstract_methods: FxIndexMap = FxIndexMap::default(); + let env = &ProgramEnvironment::from_file(self.class_literal(db).program_file(db)); // Iterate through the MRO in reverse order, // skipping `object` (we know it doesn't define any abstract methods) @@ -1360,7 +1465,7 @@ impl<'db> ClassType<'db> { // but we do recognise them as being able to override abstract methods defined in static classes. let ClassLiteral::Static(class_literal) = class.class_literal(db) else { abstract_methods - .retain(|name, _| class.own_class_member(db, None, name).is_undefined()); + .retain(|name, _| class.own_class_member(db, env, None, name).is_undefined()); continue; }; @@ -1373,7 +1478,7 @@ impl<'db> ClassType<'db> { // or this class has a `ClassVar` declaration by that name abstract_methods.retain(|name, _| { if class_literal - .own_synthesized_member(db, None, None, name) + .own_synthesized_member(db, env, None, None, name) .is_some() { return false; @@ -1381,7 +1486,7 @@ impl<'db> ClassType<'db> { place_table.symbol_id(name).is_none_or(|symbol_id| { let declarations = use_def_map.end_of_scope_symbol_declarations(symbol_id); - !place_from_declarations(db, declarations) + !place_from_declarations(db, env, declarations) .ignore_conflicting_declarations() .qualifiers .contains(TypeQualifiers::CLASS_VAR) @@ -1390,7 +1495,7 @@ impl<'db> ClassType<'db> { for (symbol_id, bindings_iterator) in use_def_map.all_end_of_scope_symbol_bindings() { let name = place_table.symbol(symbol_id).name(); - let place_and_definition = place_from_bindings(db, bindings_iterator); + let place_and_definition = place_from_bindings(db, env, bindings_iterator); let Place::Defined(DefinedPlace { ty, .. }) = place_and_definition.place else { continue; }; @@ -1426,15 +1531,21 @@ impl<'db> ClassType<'db> { } /// Return `true` if `other` is present in this class's MRO. - pub(super) fn is_subclass_of(self, db: &'db dyn Db, target: ClassType<'db>) -> bool { + pub(super) fn is_subclass_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + target: ClassType<'db>, + ) -> bool { let constraints = ConstraintSetBuilder::new(); let relation_visitor = HasRelationToVisitor::default(&constraints); let disjointness_visitor = IsDisjointVisitor::default(&constraints); let signature_relation_visitor = SignatureRelationVisitor::default(); - let materialization_visitor = ApplyTypeMappingVisitor::default(); + let materialization_visitor = ApplyTypeMappingVisitor::new(env); let checker = TypeRelationChecker::subtyping( + env, &constraints, - InferableTypeVars::None, + TypeVarSet::None, &relation_visitor, &disjointness_visitor, &signature_relation_visitor, @@ -1442,7 +1553,7 @@ impl<'db> ClassType<'db> { ); checker .check_class_pair(db, self, target) - .is_always_satisfied(db) + .is_always_satisfied(db, env) } /// Return the metaclass of this class, or `type[Unknown]` if the metaclass cannot be inferred. @@ -1471,15 +1582,16 @@ impl<'db> ClassType<'db> { } /// Return `true` if this class could exist in the MRO of `other`. - pub(super) fn could_exist_in_mro_of( + fn could_exist_in_mro_of( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, other: Self, constraints: &ConstraintSetBuilder<'db>, ) -> bool { self.could_exist_in_mro_of_impl(db, other, |this, other| { - this.is_disjoint_from(db, other, constraints, InferableTypeVars::None) - .is_always_satisfied(db) + this.is_disjoint_from(db, env, other, constraints, TypeVarSet::None) + .is_always_satisfied(db, env) }) } @@ -1488,13 +1600,14 @@ impl<'db> ClassType<'db> { pub(super) fn could_exist_in_mro_of_with_disjointness_checker<'c>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, other: Self, checker: &DisjointnessChecker<'_, 'c, 'db>, ) -> bool { self.could_exist_in_mro_of_impl(db, other, |this, other| { checker - .check_specialization_pair(db, this, other) - .is_always_satisfied(db) + .check_specialization_pair(db, env, this, other) + .is_always_satisfied(db, env) }) } @@ -1557,20 +1670,22 @@ impl<'db> ClassType<'db> { pub(super) fn could_coexist_in_mro_with( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, other: Self, constraints: &ConstraintSetBuilder<'db>, ) -> bool { self.could_coexist_in_mro_with_impl( db, + env, other, - |this, other| this.could_exist_in_mro_of(db, other, constraints), + |this, other| this.could_exist_in_mro_of(db, env, other, constraints), |this, other| { - this.is_disjoint_from(db, other, constraints, InferableTypeVars::None) - .is_always_satisfied(db) + this.is_disjoint_from(db, env, other, constraints, TypeVarSet::None) + .is_always_satisfied(db, env) }, |this, other| { - this.when_disjoint_from(db, other, constraints, InferableTypeVars::None) - .is_always_satisfied(db) + this.when_disjoint_from(db, env, other, constraints, TypeVarSet::None) + .is_always_satisfied(db, env) }, ) } @@ -1578,6 +1693,7 @@ impl<'db> ClassType<'db> { pub(super) fn could_coexist_in_mro_with_disjointness_checker<'c>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, other: Self, checker: &DisjointnessChecker<'_, 'c, 'db>, ) -> bool { @@ -1585,17 +1701,20 @@ impl<'db> ClassType<'db> { // metaclass checks so recursive class graphs keep the same cycle guard. self.could_coexist_in_mro_with_impl( db, + env, other, - |this, other| this.could_exist_in_mro_of_with_disjointness_checker(db, other, checker), + |this, other| { + this.could_exist_in_mro_of_with_disjointness_checker(db, env, other, checker) + }, |this, other| { checker - .check_specialization_pair(db, this, other) - .is_always_satisfied(db) + .check_specialization_pair(db, env, this, other) + .is_always_satisfied(db, env) }, |this, other| { checker .check_type_pair(db, this, other) - .is_always_satisfied(db) + .is_always_satisfied(db, env) }, ) } @@ -1603,6 +1722,7 @@ impl<'db> ClassType<'db> { fn could_coexist_in_mro_with_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, other: Self, could_exist_in_mro_of: impl Fn(Self, Self) -> bool, specializations_are_disjoint: impl Fn(Specialization<'db>, Specialization<'db>) -> bool, @@ -1644,7 +1764,7 @@ impl<'db> ClassType<'db> { // 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 type_class = KnownClass::Type.to_class_literal(db, env); let self_metaclass = self.metaclass(db); if self_metaclass == type_class { return true; @@ -1653,10 +1773,12 @@ impl<'db> ClassType<'db> { if other_metaclass == type_class { return true; } - let Some(self_metaclass_instance) = self_metaclass.to_instance_approximation(db) else { + let Some(self_metaclass_instance) = self_metaclass.to_instance_approximation(db, env) + else { return true; }; - let Some(other_metaclass_instance) = other_metaclass.to_instance_approximation(db) else { + let Some(other_metaclass_instance) = other_metaclass.to_instance_approximation(db, env) + else { return true; }; if types_are_disjoint(self_metaclass_instance, other_metaclass_instance) { @@ -1667,11 +1789,17 @@ impl<'db> ClassType<'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_approximation(db) - .expect("`Type::to_instance()` should always return `Some()` when called on the type of a metaclass") + pub(super) fn metaclass_instance_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { + self.metaclass(db) + .to_instance_approximation(db, env) + .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`. @@ -1682,13 +1810,15 @@ impl<'db> ClassType<'db> { pub(super) fn class_member( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, policy: MemberLookupPolicy, ) -> PlaceAndQualifiers<'db> { match self { - Self::NonGeneric(class) => class.class_member(db, name, policy), + Self::NonGeneric(class) => class.class_member(db, env, name, policy), Self::Generic(generic) => generic.origin(db).class_member_inner( db, + env, Some(generic.specialization(db)), name, policy, @@ -1710,6 +1840,7 @@ impl<'db> ClassType<'db> { pub(super) fn own_class_member( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, inherited_generic_context: Option>, name: &str, ) -> Member<'db> { @@ -1745,8 +1876,8 @@ impl<'db> ClassType<'db> { let specialization = specialization .map(|specialization| specialization.tuple_runtime_element_specialization(db)); class_literal - .own_class_member(db, inherited_generic_context, specialization, name) - .map_type(|ty| ty.apply_projected_optional_specialization(db, specialization)) + .own_class_member(db, env, inherited_generic_context, specialization, name) + .map_type(|ty| ty.apply_projected_optional_specialization(db, env, specialization)) }; match name { @@ -1756,12 +1887,12 @@ impl<'db> ClassType<'db> { .and_then(|tuple| tuple.len().into_fixed_length()) .and_then(|len| i64::try_from(len).ok()) .map(Type::int_literal) - .unwrap_or_else(|| KnownClass::Int.to_instance(db)); + .unwrap_or_else(|| KnownClass::Int.to_instance(db, env)); let parameters = Parameters::standard([Parameter::positional_only(Some( Name::new_static("self"), )) - .with_annotated_type(Type::instance(db, self))]); + .with_annotated_type(Type::instance(db, env, self))]); let synthesized_dunder_method = Type::function_like_callable(db, Signature::new(parameters, return_type)); @@ -1823,6 +1954,7 @@ impl<'db> ClassType<'db> { ) { let overload_return = UnionType::from_elements( db, + env, std::iter::once( variable_length_tuple.variable().element_type(db), ) @@ -1859,6 +1991,7 @@ impl<'db> ClassType<'db> { ) { let overload_return = UnionType::from_elements( db, + env, std::iter::once( variable_length_tuple.variable().element_type(db), ) @@ -1877,14 +2010,14 @@ impl<'db> ClassType<'db> { } } - let all_elements_unioned = tuple.homogeneous_element_type(db); + let all_elements_unioned = tuple.homogeneous_element_type(db, env); let mut overload_signatures = Vec::with_capacity(element_type_to_indices.len().saturating_add(2)); overload_signatures.extend(element_type_to_indices.into_iter().filter_map( |(return_type, mut indices)| { - if return_type.is_equivalent_to(db, all_elements_unioned) { + if return_type.is_equivalent_to(db, env, all_elements_unioned) { return None; } @@ -1893,6 +2026,7 @@ impl<'db> ClassType<'db> { let index_annotation = UnionType::from_elements( db, + env, indices.into_iter().map(Type::int_literal), ); @@ -1914,20 +2048,25 @@ impl<'db> ClassType<'db> { // __getitem__(self, index: slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> tuple[str | float | bytes, ...] // overload_signatures.push(synthesize_getitem_overload_signature( - KnownClass::SupportsIndex.to_instance(db), + KnownClass::SupportsIndex.to_instance(db, env), all_elements_unioned, )); let slice_bound = UnionType::from_elements( db, - [KnownClass::SupportsIndex.to_instance(db), Type::none(db)], + env, + [ + KnownClass::SupportsIndex.to_instance(db, env), + Type::none(db, env), + ], ); overload_signatures.push(synthesize_getitem_overload_signature( KnownClass::Slice.to_specialized_instance( db, + env, &[slice_bound, slice_bound, slice_bound], ), - Type::homogeneous_tuple(db, all_elements_unioned), + Type::homogeneous_tuple(db, env, all_elements_unioned), )); let getitem_signature = @@ -1968,26 +2107,28 @@ impl<'db> ClassType<'db> { assert_eq!( tuple.iter_element_types(db).count(), 1, - "Tuple specialization should have exactly one element when it has no length restriction" + "Tuple specialization should have exactly one element when it has \ + no length restriction" ); iterable_parameter = iterable_parameter.with_annotated_type( KnownClass::Iterable.to_specialized_instance( db, - &[tuple.homogeneous_element_type(db)], + env, + &[tuple.homogeneous_element_type(db, env)], ), ); } else { // But if the tuple is of a fixed length, or has a minimum length, we require a tuple rather // than an iterable, as a tuple is the only kind of iterable for which we can // specify a fixed length, or that the iterable must be at least a certain length. - iterable_parameter = - iterable_parameter.with_annotated_type(Type::instance(db, self)); + iterable_parameter = iterable_parameter + .with_annotated_type(Type::instance(db, env, self)); } } None => { // If the tuple isn't specialized at all, we allow any argument as long as it is iterable. iterable_parameter = iterable_parameter - .with_annotated_type(KnownClass::Iterable.to_instance(db)); + .with_annotated_type(KnownClass::Iterable.to_instance(db, env)); } } @@ -1997,12 +2138,12 @@ impl<'db> ClassType<'db> { // - a tuple with no minimum length if tuple.is_none_or(|tuple| tuple.len().minimum() == 0) { iterable_parameter = - iterable_parameter.with_default_type(Type::empty_tuple(db)); + iterable_parameter.with_default_type(Type::empty_tuple(db, env)); } let parameters = Parameters::standard([ Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(SubclassOfType::from(db, self)), + .with_annotated_type(SubclassOfType::from(db, env, self)), iterable_parameter, ]); @@ -2021,21 +2162,26 @@ impl<'db> ClassType<'db> { /// 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> { + pub(super) fn instance_member( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> PlaceAndQualifiers<'db> { match self { - Self::NonGeneric(ClassLiteral::Dynamic(class)) => class.instance_member(db, name), + Self::NonGeneric(ClassLiteral::Dynamic(class)) => class.instance_member(db, env, name), Self::NonGeneric(ClassLiteral::DynamicNamedTuple(namedtuple)) => { - namedtuple.instance_member(db, name) + namedtuple.instance_member(db, env, name) } Self::NonGeneric(ClassLiteral::DynamicTypedDict(_)) => PlaceAndQualifiers::default(), Self::NonGeneric(ClassLiteral::DynamicEnum(enum_lit)) => { - enum_lit.instance_member(db, name) + enum_lit.instance_member(db, env, name) } Self::NonGeneric(ClassLiteral::Static(class)) => { if class.is_typed_dict(db) { return Place::Undefined.into(); } - class.instance_member(db, None, name) + class.instance_member(db, env, None, name) } Self::Generic(generic) => { let class_literal = generic.origin(db); @@ -2046,8 +2192,8 @@ impl<'db> ClassType<'db> { } class_literal - .instance_member(db, Some(specialization), name) - .map_type(|ty| ty.apply_projected_specialization(db, specialization)) + .instance_member(db, env, Some(specialization), name) + .map_type(|ty| ty.apply_projected_specialization(db, env, specialization)) } } } @@ -2077,7 +2223,12 @@ impl<'db> ClassType<'db> { /// A helper function for `instance_member` that looks up the `name` attribute only on /// this class, not on its superclasses. - pub(super) fn own_instance_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { + pub(super) fn own_instance_member( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> Member<'db> { match self { Self::NonGeneric(ClassLiteral::Dynamic(dynamic)) => { dynamic.own_instance_member(db, name) @@ -2090,13 +2241,13 @@ impl<'db> ClassType<'db> { enum_lit.own_instance_member(db, name) } Self::NonGeneric(ClassLiteral::Static(class_literal)) => { - class_literal.own_instance_member(db, name) + class_literal.own_instance_member(db, env, name) } Self::Generic(generic) => { let specialization = generic.specialization(db); generic .origin(db) - .own_instance_member(db, name) + .own_instance_member(db, env, name) .map_type(|ty| ty.apply_optional_specialization(db, Some(specialization))) } } @@ -2104,12 +2255,26 @@ impl<'db> ClassType<'db> { /// 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) -> CallableTypes<'db> { + self.into_callable_with_receiver(db, Type::from(self)) + } + + /// Infer this class's constructor using the actual class-object receiver. + /// + /// A materialized protocol uses its class origin for constructor lookup, but `Self` must be + /// bound to the materialized receiver. Keeping lookup and receiver separate preserves both + /// instance-returning constructors and constructors that explicitly return another type. #[salsa::tracked( returns(clone), - cycle_initial=|db, _, _| CallableTypes::one(CallableType::bottom(db)), + cycle_initial=|db, _, _, _| CallableTypes::one(CallableType::bottom(db)), heap_size=ruff_memory_usage::heap_size )] - pub(super) fn into_callable(self, db: &'db dyn Db) -> CallableTypes<'db> { + pub(super) fn into_callable_with_receiver( + self, + db: &'db dyn Db, + receiver: Type<'db>, + ) -> CallableTypes<'db> { + let env = &ProgramEnvironment::from_file(self.class_literal(db).program_file(db)); // TODO: This mimics a lot of the logic in Type::try_call_from_constructor. Can we // consolidate the two? Can we invoke a class by upcasting the class into a Callable, and // then relying on the call binding machinery to Just Work™? @@ -2119,10 +2284,15 @@ impl<'db> ClassType<'db> { .static_class_literal(db) .and_then(|(class_literal, _)| class_literal.generic_context(db)); - let self_ty = Type::from(self); - let metaclass_dunder_call_function_symbol = self_ty + let lookup_type = Type::from(self); + let instance_type = receiver + .to_instance_approximation(db, env) + .unwrap_or_else(Type::unknown); + + let metaclass_dunder_call_function_symbol = lookup_type .member_lookup_with_policy( db, + env, "__call__", MemberLookupPolicy::NO_INSTANCE_FALLBACK | MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, @@ -2146,11 +2316,17 @@ impl<'db> ClassType<'db> { // for dynamic Enum creation. let is_actual_enum = enum_metadata(db, self.class_literal(db)).is_some(); if !is_actual_enum { - return CallableTypes::one(metaclass_dunder_call_function.into_callable_type(db)); + let callable = if receiver == lookup_type { + metaclass_dunder_call_function.into_callable_type(db) + } else { + metaclass_dunder_call_function + .into_callable_type_with_receiver(db, env, receiver, receiver) + }; + return CallableTypes::one(callable); } } - let dunder_new_function_symbol = self_ty.lookup_dunder_new(db); + let dunder_new_function_symbol = lookup_type.lookup_dunder_new(db, env); let dunder_new_signature = dunder_new_function_symbol .and_then(|place_and_quals| place_and_quals.ignore_possibly_undefined()) @@ -2161,21 +2337,23 @@ impl<'db> ClassType<'db> { }); let dunder_new_function = if let Some(dunder_new_signature) = dunder_new_signature { + let bound_signature = dunder_new_signature.bind_self_with_receiver( + db, + env, + Some(receiver), + Some(instance_type), + ); + // 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_signature.overloads.iter().any(|signature| { - !signature.return_ty.is_assignable_to( - db, - self_ty - .to_instance_approximation(db) - .expect("ClassType should be instantiable"), - ) - }); + let returns_non_subclass = bound_signature + .overloads + .iter() + .any(|signature| !signature.return_ty.is_assignable_to(db, env, instance_type)); - let instance_ty = Type::instance(db, self); let dunder_new_bound_method = CallableType::new( db, - dunder_new_signature.bind_self_with_receiver(db, Some(self_ty), Some(instance_ty)), + bound_signature, CallableTypeKind::Regular, CallableFunctionProvenance::None, ); @@ -2188,19 +2366,16 @@ impl<'db> ClassType<'db> { None }; - let dunder_init_function_symbol = self_ty + let dunder_init_function_symbol = lookup_type .member_lookup_with_policy( db, + env, "__init__", MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK | MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, ) .place; - let correct_return_type = self_ty - .to_instance_approximation(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`. @@ -2226,8 +2401,7 @@ impl<'db> ClassType<'db> { ty.as_typevar() .is_none_or(|bound_typevar| !bound_typevar.typevar(db).is_self(db)) }); - let return_type = self_annotation.unwrap_or(correct_return_type); - let instance_ty = Type::instance(db, self); + let return_type = self_annotation.unwrap_or(instance_type); let generic_context = GenericContext::merge_optional( db, class_generic_context, @@ -2239,10 +2413,12 @@ impl<'db> ClassType<'db> { return_type, ) .with_definition(signature.definition()) + .with_source_overload_index(signature.source_overload_index()) .bind_self_with_receiver( db, - Some(instance_ty), - Some(instance_ty), + env, + Some(instance_type), + Some(instance_type), ) }; @@ -2276,9 +2452,10 @@ impl<'db> ClassType<'db> { (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 + let new_function_symbol = lookup_type .member_lookup_with_policy( db, + env, "__new__", MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, ) @@ -2295,7 +2472,7 @@ impl<'db> ClassType<'db> { } CallableTypes::one( new_function - .into_bound_method_type(db, correct_return_type) + .into_bound_method_type(db, instance_type) .into_callable_type(db), ) } else { @@ -2305,7 +2482,7 @@ impl<'db> ClassType<'db> { Signature::new_generic( class_generic_context, Parameters::empty(), - correct_return_type, + instance_type, ), )) } @@ -2361,16 +2538,21 @@ impl<'db> From> for Type<'db> { } impl<'db> VarianceInferable<'db> for ClassType<'db> { - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { + fn variance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { match self { - Self::NonGeneric(ClassLiteral::Static(class)) => class.variance_of(db, typevar), + Self::NonGeneric(ClassLiteral::Static(class)) => class.variance_of(db, env, typevar), Self::NonGeneric( ClassLiteral::Dynamic(_) | ClassLiteral::DynamicNamedTuple(_) | ClassLiteral::DynamicTypedDict(_) | ClassLiteral::DynamicEnum(_), ) => TypeVarVariance::Bivariant, - Self::Generic(generic) => generic.variance_of(db, typevar), + Self::Generic(generic) => generic.variance_of(db, env, typevar), } } } @@ -2620,9 +2802,14 @@ impl<'db> Field<'db> { } impl<'db> VarianceInferable<'db> for ClassLiteral<'db> { - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { + fn variance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { match self { - Self::Static(class) => class.variance_of(db, typevar), + Self::Static(class) => class.variance_of(db, env, typevar), Self::Dynamic(_) | Self::DynamicNamedTuple(_) | Self::DynamicTypedDict(_) @@ -2638,13 +2825,18 @@ impl<'db> VarianceInferable<'db> for ClassLiteral<'db> { /// use this to avoid duplicating the MRO traversal logic. pub(super) struct MroLookup<'db, I> { db: &'db dyn Db, + env: ProgramEnvironment<'db>, mro_iter: I, } impl<'db, I: Iterator>> MroLookup<'db, I> { /// Create a new MRO lookup from a database and an MRO iterator. - pub(super) fn new(db: &'db dyn Db, mro_iter: I) -> Self { - Self { db, mro_iter } + fn new(db: &'db dyn Db, env: &ProgramEnvironment<'db>, mro_iter: I) -> Self { + Self { + db, + env: env.clone(), + mro_iter, + } } /// Look up a class member by iterating through the MRO. @@ -2661,7 +2853,7 @@ impl<'db, I: Iterator>> MroLookup<'db, I> { /// If we encounter a dynamic type in the MRO, we save it and after traversal: /// 1. Use it as the type if no other classes define the attribute, or /// 2. Intersect it with the type from non-dynamic MRO members. - pub(super) fn class_member( + fn class_member( self, name: &str, policy: MemberLookupPolicy, @@ -2669,6 +2861,7 @@ impl<'db, I: Iterator>> MroLookup<'db, I> { is_self_object: bool, ) -> ClassMemberResult<'db> { let db = self.db; + let mut dynamic_type: Option> = None; let mut lookup_result: LookupResult<'db> = Err(LookupError::Undefined(TypeQualifiers::empty())); @@ -2712,8 +2905,9 @@ impl<'db, I: Iterator>> MroLookup<'db, I> { lookup_result = lookup_result.or_else(|lookup_error| { lookup_error.or_fall_back_to( db, + &self.env, class - .own_class_member(db, inherited_generic_context, name) + .own_class_member(db, &self.env, inherited_generic_context, name) .inner, ) }); @@ -2742,9 +2936,9 @@ impl<'db, I: Iterator>> MroLookup<'db, I> { /// /// Returns `InstanceMemberResult::TypedDict` if a `TypedDict` base is encountered, /// allowing the caller to handle this case specially. - pub(super) fn instance_member(self, name: &str) -> InstanceMemberResult<'db> { + fn instance_member(self, name: &str) -> InstanceMemberResult<'db> { let db = self.db; - let mut union = UnionBuilder::new(db); + let mut union = UnionBuilder::new(db, &self.env); let mut union_qualifiers = TypeQualifiers::empty(); let mut is_definitely_bound = false; let mut provenance = Provenance::Unknown; @@ -2771,7 +2965,7 @@ impl<'db, I: Iterator>> MroLookup<'db, I> { .. }), qualifiers, - } = class.own_instance_member(db, name).inner + } = class.own_instance_member(db, &self.env, name).inner { if boundness == Definedness::AlwaysDefined { if origin.is_declared() { @@ -2838,7 +3032,7 @@ pub(super) struct CompletedMemberLookup<'db> { impl<'db> CompletedMemberLookup<'db> { /// Finalize the lookup result by handling dynamic type intersection. - pub(super) fn finalize(self, db: &'db dyn Db) -> PlaceAndQualifiers<'db> { + fn finalize(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> PlaceAndQualifiers<'db> { match ( PlaceAndQualifiers::from(self.lookup_result), self.dynamic_type, @@ -2851,7 +3045,7 @@ impl<'db> CompletedMemberLookup<'db> { qualifiers, }, Some(dynamic), - ) => Place::bound(IntersectionType::from_two_elements(db, ty, dynamic)) + ) => Place::bound(IntersectionType::from_two_elements(db, env, ty, dynamic)) .with_provenance(provenance) .with_qualifiers(qualifiers), @@ -2886,7 +3080,7 @@ pub(super) struct QualifiedClassName<'db> { } impl<'db> QualifiedClassName<'db> { - pub(super) fn from_class_literal(db: &'db dyn Db, class: ClassLiteral<'db>) -> Self { + fn from_class_literal(db: &'db dyn Db, class: ClassLiteral<'db>) -> Self { Self { db, class } } @@ -2902,7 +3096,7 @@ impl<'db> QualifiedClassName<'db> { let body_scope = class.body_scope(self.db); // Skip the class body scope itself. ( - body_scope.file(self.db), + body_scope.program_file(self.db), body_scope.file_scope_id(self.db), 1, ) @@ -2910,20 +3104,20 @@ impl<'db> QualifiedClassName<'db> { ClassLiteral::Dynamic(class) => { // Dynamic classes don't have a body scope; start from the enclosing scope. let scope = class.scope(self.db); - (scope.file(self.db), scope.file_scope_id(self.db), 0) + (scope.program_file(self.db), scope.file_scope_id(self.db), 0) } ClassLiteral::DynamicNamedTuple(namedtuple) => { // Dynamic namedtuples don't have a body scope; start from the enclosing scope. let scope = namedtuple.scope(self.db); - (scope.file(self.db), scope.file_scope_id(self.db), 0) + (scope.program_file(self.db), scope.file_scope_id(self.db), 0) } ClassLiteral::DynamicTypedDict(typeddict) => { let scope = typeddict.scope(self.db); - (scope.file(self.db), scope.file_scope_id(self.db), 0) + (scope.program_file(self.db), scope.file_scope_id(self.db), 0) } ClassLiteral::DynamicEnum(enum_lit) => { let scope = enum_lit.scope(self.db); - (scope.file(self.db), scope.file_scope_id(self.db), 0) + (scope.program_file(self.db), scope.file_scope_id(self.db), 0) } }; @@ -3064,12 +3258,19 @@ enum SlotsKind { impl SlotsKind { fn from(db: &dyn Db, base: StaticClassLiteral) -> Self { + let env = ProgramEnvironment::from_scope(base.body_scope(db)); let Place::Defined(DefinedPlace { ty: slots_ty, definedness: bound, .. }) = base - .own_class_member(db, base.inherited_generic_context(db), None, "__slots__") + .own_class_member( + db, + &env, + base.inherited_generic_context(db), + None, + "__slots__", + ) .inner .place else { @@ -3083,7 +3284,7 @@ impl SlotsKind { match slots_ty { // __slots__ = ("a", "b") Type::NominalInstance(nominal) => match nominal - .tuple_spec(db) + .tuple_spec(db, &env) .and_then(|spec| spec.len().into_fixed_length()) { Some(0) => Self::Empty, diff --git a/crates/ty_python_semantic/src/types/class/dynamic_literal.rs b/crates/ty_python_semantic/src/types/class/dynamic_literal.rs index 809ceec851..5757f67c3d 100644 --- a/crates/ty_python_semantic/src/types/class/dynamic_literal.rs +++ b/crates/ty_python_semantic/src/types/class/dynamic_literal.rs @@ -1,6 +1,7 @@ +use crate::ProgramEnvironment; use ruff_db::{diagnostic::Span, parsed::parsed_module}; -use ruff_python_ast::{self as ast, NodeIndex, name::Name}; -use ruff_text_size::{Ranged, TextRange}; +use ruff_python_ast::{self as ast, name::Name}; +use ruff_text_size::TextRange; use crate::{ Db, TypeQualifiers, @@ -9,7 +10,8 @@ use crate::{ ClassBase, ClassLiteral, ClassType, DataclassParams, KnownClass, MemberLookupPolicy, SubclassOfType, Type, class::{ - ClassMemberResult, CodeGeneratorKind, DisjointBase, InstanceMemberResult, MroLookup, + ClassMemberResult, CodeGeneratorKind, DisjointBase, DynamicClassHeaderAnchor, + InstanceMemberResult, MroLookup, dynamic_class_header_range, typed_dict::typed_dict_fallback_class_member, }, definition_expression_type, extract_fixed_length_iterable_element_types, @@ -112,6 +114,7 @@ impl<'db> DynamicClassAnchor<'db> { fn recursive_type_normalized_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -125,7 +128,7 @@ impl<'db> DynamicClassAnchor<'db> { let explicit_bases = explicit_bases .iter() .map(|base| { - let base = base.recursive_type_normalized_impl(db, div, true); + let base = base.recursive_type_normalized_impl(db, env, div, true); if nested { base } else { @@ -199,7 +202,10 @@ impl<'db> DynamicClassLiteral<'db> { db: &'db dyn Db, definition: Definition<'db>, ) -> Box<[Type<'db>]> { - let module = parsed_module(db, definition.file(db)).load(db); + let program_file = definition.program_file(db); + let python_file = program_file.python_file(db); + let env = ProgramEnvironment::from_file(program_file); + let module = parsed_module(db, python_file).load(db); let value = definition .kind(db) @@ -214,7 +220,7 @@ impl<'db> DynamicClassLiteral<'db> { }; // Use `definition_expression_type` for deferred inference support. - extract_fixed_length_iterable_element_types(db, bases_arg, |expr| { + extract_fixed_length_iterable_element_types(db, &env, bases_arg, |expr| { definition_expression_type(db, definition, expr) }) .unwrap_or_else(|| Box::from([Type::unknown()])) @@ -237,36 +243,15 @@ impl<'db> DynamicClassLiteral<'db> { /// Returns the range of the `type()` call expression that created this class. pub(crate) fn header_range(self, db: &'db dyn Db) -> TextRange { - let scope = self.scope(db); - let file = scope.file(db); - let module = parsed_module(db, file).load(db); - - match self.anchor(db) { + let anchor = match self.anchor(db) { DynamicClassAnchor::Definition(definition) => { - // For definitions, get the range from the definition's value. - // The `type()` call is the value of the assignment. - definition - .kind(db) - .value(&module) - .expect("DynamicClassAnchor::Definition should only be used for assignments") - .range() + DynamicClassHeaderAnchor::Definition(*definition) } DynamicClassAnchor::ScopeOffset { offset, .. } => { - // For dangling `type()` calls, compute the absolute index from the offset. - let scope_anchor = scope.node(db).node_index().unwrap_or(NodeIndex::from(0)); - let anchor_u32 = scope_anchor - .as_u32() - .expect("anchor should not be NodeIndex::NONE"); - let absolute_index = NodeIndex::from(anchor_u32 + *offset); - - // Get the node and return its range. - let node: &ast::ExprCall = module - .get_by_index(absolute_index) - .try_into() - .expect("scope offset should point to ExprCall"); - node.range() + DynamicClassHeaderAnchor::ScopeOffset(*offset) } - } + }; + dynamic_class_header_range(db, self.scope(db), anchor) } /// Get the metaclass of this dynamic class. @@ -291,12 +276,13 @@ impl<'db> DynamicClassLiteral<'db> { db: &'db dyn Db, ) -> Result, DynamicMetaclassConflict<'db>> { let original_bases = self.explicit_bases(db); + let env = ProgramEnvironment::from_scope(self.scope(db)); // If no bases, metaclass is `type`. // To dynamically create a class with no bases that has a custom metaclass, // you have to invoke that metaclass rather than `type()`. if original_bases.is_empty() { - return Ok(KnownClass::Type.to_class_literal(db)); + return Ok(KnownClass::Type.to_class_literal(db, &env)); } // If there's an MRO error, return unknown to avoid cascading errors. @@ -309,23 +295,23 @@ impl<'db> DynamicClassLiteral<'db> { // returned `Err(InvalidBases)` if any failed, causing us to return early. let bases: Vec> = original_bases .iter() - .filter_map(|base_type| ClassBase::try_from_type(db, *base_type, None)) + .filter_map(|base_type| ClassBase::try_from_type(db, &env, *base_type, None)) .collect(); // If all bases failed to convert, return type as the metaclass. if bases.is_empty() { - return Ok(KnownClass::Type.to_class_literal(db)); + return Ok(KnownClass::Type.to_class_literal(db, &env)); } // Start with the first base's metaclass as the candidate. - let mut candidate = bases[0].metaclass(db); + let mut candidate = bases[0].metaclass(db, &env); // Track which base the candidate metaclass came from. let (mut candidate_base, rest) = bases.split_first().unwrap(); // Reconcile with other bases' metaclasses. for base in rest { - let base_metaclass = base.metaclass(db); + let base_metaclass = base.metaclass(db, &env); // Get the ClassType for comparison. let Some(candidate_class) = candidate.to_class_type(db) else { @@ -337,14 +323,14 @@ impl<'db> DynamicClassLiteral<'db> { }; // If base's metaclass is more derived, use it. - if base_metaclass_class.is_subclass_of(db, candidate_class) { + if base_metaclass_class.is_subclass_of(db, &env, candidate_class) { candidate = base_metaclass; candidate_base = base; continue; } // If candidate is already more derived, keep it. - if candidate_class.is_subclass_of(db, base_metaclass_class) { + if candidate_class.is_subclass_of(db, &env, base_metaclass_class) { continue; } @@ -373,14 +359,19 @@ impl<'db> DynamicClassLiteral<'db> { } /// Look up an instance member by iterating through the MRO. - pub(crate) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { - match MroLookup::new(db, self.iter_mro(db)).instance_member(name) { + pub(crate) fn instance_member( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> PlaceAndQualifiers<'db> { + match MroLookup::new(db, env, self.iter_mro(db)).instance_member(name) { InstanceMemberResult::Done(result) => result, InstanceMemberResult::TypedDict => { // Simplified `TypedDict` handling without type mapping. KnownClass::TypedDictFallback - .to_instance(db) - .instance_member(db, name) + .to_instance(db, env) + .instance_member(db, env, name) } } } @@ -393,6 +384,7 @@ impl<'db> DynamicClassLiteral<'db> { pub(crate) fn class_member( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, policy: MemberLookupPolicy, ) -> PlaceAndQualifiers<'db> { @@ -404,9 +396,10 @@ impl<'db> DynamicClassLiteral<'db> { // Make this class look like a subclass of the `DataClassInstance` protocol. return Place::declared(KnownClass::Dict.to_specialized_instance( db, + env, &[ - KnownClass::Str.to_instance(db), - KnownClass::Field.to_specialized_instance(db, &[Type::any()]), + KnownClass::Str.to_instance(db, env), + KnownClass::Field.to_specialized_instance(db, env, &[Type::any()]), ], )) .with_qualifiers(TypeQualifiers::CLASS_VAR); @@ -416,15 +409,15 @@ impl<'db> DynamicClassLiteral<'db> { } } - let result = MroLookup::new(db, self.iter_mro(db)).class_member( + let result = MroLookup::new(db, env, self.iter_mro(db)).class_member( name, policy, None, // No inherited generic context. false, // Dynamic classes are never `object`. ); match result { - ClassMemberResult::Done(result) => result.finalize(db), + ClassMemberResult::Done(result) => result.finalize(db, env), ClassMemberResult::TypedDict(module) => { - typed_dict_fallback_class_member(db, module, policy, name) + typed_dict_fallback_class_member(db, env, module, policy, name) } } } @@ -461,7 +454,7 @@ impl<'db> DynamicClassLiteral<'db> { cycle_initial=|db, _, self_: DynamicClassLiteral<'db>| { Ok(Mro::from([ ClassBase::Class(ClassType::NonGeneric(ClassLiteral::Dynamic(self_))), - ClassBase::object(db), + ClassBase::object(db, &ProgramEnvironment::from_scope(self_.scope(db))), ])) }, heap_size=ruff_memory_usage::heap_size @@ -484,9 +477,12 @@ impl<'db> DynamicClassLiteral<'db> { // Check if the slots are non-empty let is_non_empty = match ty { // __slots__ = ("a", "b") - Type::NominalInstance(nominal) => nominal.tuple_spec(db).is_some_and(|spec| { - spec.len().into_fixed_length().is_some_and(|len| len > 0) - }), + Type::NominalInstance(nominal) => { + let env = ProgramEnvironment::from_scope(self.scope(db)); + nominal.tuple_spec(db, &env).is_some_and(|spec| { + spec.len().into_fixed_length().is_some_and(|len| len > 0) + }) + } // __slots__ = "abc" # Same as ("abc",) Type::LiteralValue(literal) if literal.is_string() => true, // Other types are considered dynamic/unknown @@ -536,23 +532,24 @@ impl<'db> DynamicClassLiteral<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { let anchor = self .anchor(db) - .recursive_type_normalized_impl(db, div, nested)?; + .recursive_type_normalized_impl(db, env, div, nested)?; let members = self .members(db) .iter() .map(|(name, ty)| { - let ty = ty.recursive_type_normalized_impl(db, div, true); + let ty = ty.recursive_type_normalized_impl(db, env, div, true); let ty = if nested { ty? } else { ty.unwrap_or(div) }; Some((name.clone(), ty)) }) .collect::>>()?; let dataclass_params = match self.dataclass_params(db) { - Some(params) => Some(params.recursive_type_normalized_impl(db, div, nested)?), + Some(params) => Some(params.recursive_type_normalized_impl(db, env, div, nested)?), None => None, }; diff --git a/crates/ty_python_semantic/src/types/class/enum_literal.rs b/crates/ty_python_semantic/src/types/class/enum_literal.rs index 69b01345ea..36535f410c 100644 --- a/crates/ty_python_semantic/src/types/class/enum_literal.rs +++ b/crates/ty_python_semantic/src/types/class/enum_literal.rs @@ -1,14 +1,16 @@ +use crate::ProgramEnvironment; use ruff_db::diagnostic::Span; -use ruff_db::parsed::parsed_module; use ruff_python_ast::name::Name; -use ruff_python_ast::{self as ast, NodeIndex}; -use ruff_text_size::{Ranged, TextRange}; +use ruff_text_size::TextRange; use crate::Db; use crate::place::{Place, PlaceAndQualifiers}; use crate::types::Type; use crate::types::class::known::KnownClass; -use crate::types::class::{ClassLiteral, ClassType, MemberLookupPolicy}; +use crate::types::class::{ + ClassLiteral, ClassType, DynamicClassHeaderAnchor, MemberLookupPolicy, + dynamic_class_header_range, +}; use crate::types::class_base::ClassBase; use crate::types::member::Member; use crate::types::mro::{DynamicMroError, Mro}; @@ -28,6 +30,7 @@ impl<'db> EnumSpec<'db> { fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -35,7 +38,7 @@ impl<'db> EnumSpec<'db> { .members(db) .iter() .map(|(name, ty)| { - let ty = ty.recursive_type_normalized_impl(db, div, true); + let ty = ty.recursive_type_normalized_impl(db, env, div, true); let ty = if nested { ty? } else { ty.unwrap_or(div) }; Some((name.clone(), ty)) }) @@ -69,13 +72,14 @@ impl<'db> DynamicEnumAnchor<'db> { fn recursive_type_normalized_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { match self { Self::Definition { definition, spec } => Some(Self::Definition { definition: *definition, - spec: spec.recursive_type_normalized_impl(db, div, nested)?, + spec: spec.recursive_type_normalized_impl(db, env, div, nested)?, }), Self::ScopeOffset { scope, @@ -84,7 +88,7 @@ impl<'db> DynamicEnumAnchor<'db> { } => Some(Self::ScopeOffset { scope: *scope, offset: *offset, - spec: spec.recursive_type_normalized_impl(db, div, nested)?, + spec: spec.recursive_type_normalized_impl(db, env, div, nested)?, }), } } @@ -109,12 +113,13 @@ impl<'db> DynamicEnumLiteral<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { let mixin_type = match self.mixin_type(db) { Some(mixin) => { - let mixin = mixin.recursive_type_normalized_impl(db, div, true); + let mixin = mixin.recursive_type_normalized_impl(db, env, div, true); Some(if nested { mixin? } else { mixin.unwrap_or(div) }) } None => None, @@ -124,7 +129,7 @@ impl<'db> DynamicEnumLiteral<'db> { db, self.name(db), self.anchor(db) - .recursive_type_normalized_impl(db, div, nested)?, + .recursive_type_normalized_impl(db, env, div, nested)?, self.base_class(db), mixin_type, )) @@ -159,42 +164,30 @@ impl<'db> DynamicEnumLiteral<'db> { if let Some(mixin) = self.mixin_type(db) { bases.push(mixin); } - bases.push(self.base_class(db).to_class_literal(db)); + let env = ProgramEnvironment::from_scope(self.scope(db)); + bases.push(self.base_class(db).to_class_literal(db, &env)); bases.into_boxed_slice() } pub(crate) fn header_range(self, db: &'db dyn Db) -> TextRange { - let scope = self.scope(db); - let file = scope.file(db); - let module = parsed_module(db, file).load(db); - match self.anchor(db) { - DynamicEnumAnchor::Definition { definition, .. } => definition - .kind(db) - .value(&module) - .expect("DynamicEnumAnchor::Definition should only be used for assignments") - .range(), + let anchor = match self.anchor(db) { + DynamicEnumAnchor::Definition { definition, .. } => { + DynamicClassHeaderAnchor::Definition(*definition) + } DynamicEnumAnchor::ScopeOffset { offset, .. } => { - let scope_anchor = scope.node(db).node_index().unwrap_or(NodeIndex::from(0)); - let anchor_u32 = scope_anchor - .as_u32() - .expect("anchor should not be NodeIndex::NONE"); - let absolute_index = NodeIndex::from(anchor_u32 + offset); - let node: &ast::ExprCall = module - .get_by_index(absolute_index) - .try_into() - .expect("scope offset should point to ExprCall"); - node.range() + DynamicClassHeaderAnchor::ScopeOffset(*offset) } - } + }; + dynamic_class_header_range(db, self.scope(db), anchor) } pub(super) fn header_span(self, db: &'db dyn Db) -> Span { Span::from(self.scope(db).file(db)).with_range(self.header_range(db)) } - #[expect(clippy::unused_self)] pub(crate) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { - KnownClass::EnumType.to_class_literal(db) + let env = ProgramEnvironment::from_scope(self.scope(db)); + KnownClass::EnumType.to_class_literal(db, &env) } #[salsa::tracked( @@ -203,7 +196,7 @@ impl<'db> DynamicEnumLiteral<'db> { cycle_initial=|db, _, self_: DynamicEnumLiteral<'db>| { Ok(Mro::from([ ClassBase::Class(ClassType::NonGeneric(ClassLiteral::DynamicEnum(self_))), - ClassBase::object(db), + ClassBase::object(db, &ProgramEnvironment::from_scope(self_.scope(db))), ])) } )] @@ -215,9 +208,9 @@ impl<'db> DynamicEnumLiteral<'db> { self.spec(db).has_known_members(db) } - fn mixin_class(self, db: &'db dyn Db) -> Option> { + fn mixin_class(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Option> { let mixin = self.mixin_type(db)?; - let ClassBase::Class(class) = ClassBase::try_from_type(db, mixin, None)? else { + let ClassBase::Class(class) = ClassBase::try_from_type(db, env, mixin, None)? else { return None; }; Some(class) @@ -258,22 +251,27 @@ impl<'db> DynamicEnumLiteral<'db> { /// /// If members are unknown and nothing was found in the MRO, returns `Unknown` /// as a last resort to avoid false `unresolved-attribute` errors. - pub(crate) fn class_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + pub(crate) fn class_member( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> PlaceAndQualifiers<'db> { let own = self.own_class_member(db, name); if !own.is_undefined() { return own.inner; } - if let Some(mixin_class) = self.mixin_class(db) { - let result = mixin_class.class_member(db, name, MemberLookupPolicy::default()); + if let Some(mixin_class) = self.mixin_class(db, env) { + let result = mixin_class.class_member(db, env, name, MemberLookupPolicy::default()); if !result.place.is_undefined() { return result; } } let result = self .base_class(db) - .to_class_literal(db) + .to_class_literal(db, env) .as_class_literal() - .map(|cls| cls.class_member(db, name, MemberLookupPolicy::default())) + .map(|cls| cls.class_member(db, env, name, MemberLookupPolicy::default())) .unwrap_or_else(|| Place::Undefined.into()); // When members are unknown (e.g. `Enum("E", some_var)`), any name could @@ -287,17 +285,22 @@ impl<'db> DynamicEnumLiteral<'db> { /// /// If members are unknown and nothing was found, returns `Unknown` /// as a last resort. - pub(crate) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { - if let Some(mixin_class) = self.mixin_class(db) { - let result = mixin_class.instance_member(db, name); + pub(crate) fn instance_member( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> PlaceAndQualifiers<'db> { + if let Some(mixin_class) = self.mixin_class(db, env) { + let result = mixin_class.instance_member(db, env, name); if !result.place.is_undefined() { return result; } } let result = self .base_class(db) - .to_instance(db) - .instance_member(db, name); + .to_instance(db, env) + .instance_member(db, env, name); self.with_unknown_member_fallback(db, result) } diff --git a/crates/ty_python_semantic/src/types/class/known.rs b/crates/ty_python_semantic/src/types/class/known.rs index c093fda291..a8167a52a7 100644 --- a/crates/ty_python_semantic/src/types/class/known.rs +++ b/crates/ty_python_semantic/src/types/class/known.rs @@ -1,5 +1,5 @@ use crate::{ - Db, Program, + Db, Program, ProgramEnvironment, place::{DefinedPlace, Definedness, Place, known_module_symbol}, types::{ Binding, ClassLiteral, ClassType, GenericContext, KnownInstanceType, StaticClassLiteral, @@ -13,7 +13,6 @@ use crate::{ known_instance::DeprecatedInstance, }, }; -use ruff_db::files::File; use ruff_python_ast as ast; use ruff_python_ast::PythonVersion; use rustc_hash::FxHashSet; @@ -21,7 +20,7 @@ use std::{ borrow::Cow, sync::{LazyLock, Mutex}, }; -use ty_module_resolver::{KnownModule, file_to_module}; +use ty_module_resolver::{ImportingFile, KnownModule, file_to_module}; use ty_python_core::{SemanticIndex, Truthiness, scope::NodeWithScopeKind}; /// Non-exhaustive enumeration of known classes (e.g. `builtins.int`, `typing.Any`, ...) to allow @@ -97,6 +96,7 @@ pub enum KnownClass { EllipsisType, // Typeshed NoneType, // Part of `types` for Python >= 3.10 + SupportsKeysAndGetItem, // Typing Awaitable, Generator, @@ -123,6 +123,7 @@ pub enum KnownClass { AsyncIterator, Sequence, Mapping, + MutableMapping, // typing_extensions ExtensionsTypeVar, // must be distinct from typing.TypeVar, backports new features ExtensionTypedDictFallback, @@ -221,7 +222,6 @@ impl KnownClass { | Self::TypeVarTuple | Self::ExtensionsTypeVarTuple | Self::Sentinel - | Self::Super | Self::WrapperDescriptorType | Self::UnionType | Self::GeneratorType @@ -229,8 +229,7 @@ impl KnownClass { | Self::MethodWrapperType | Self::CoroutineType | Self::BuiltinFunctionType - | Self::Template - | Self::Path => Some(Truthiness::AlwaysTrue), + | Self::Template => Some(Truthiness::AlwaysTrue), Self::NoneType => Some(Truthiness::AlwaysFalse), @@ -290,12 +289,11 @@ impl KnownClass { | Self::AsyncIterator | Self::Sequence | Self::Mapping - // 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::MutableMapping + | Self::SupportsKeysAndGetItem | Self::Staticmethod | Self::Classmethod + | Self::Super | Self::Awaitable | Self::Generator | Self::AsyncGenerator @@ -313,6 +311,7 @@ impl KnownClass { | Self::FunctoolsPartial | Self::ReMatch | Self::RePattern + | Self::Path | Self::ExtensionTypedDictFallback | Self::TypedDictFallback | Self::PydanticBaseModel @@ -332,6 +331,11 @@ impl KnownClass { | Self::SqlalchemyMapped | Self::ByStaticProperty => Some(Truthiness::Ambiguous), + // 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 => Some(Truthiness::Ambiguous), + Self::Tuple => None, } } @@ -420,6 +424,8 @@ impl KnownClass { | KnownClass::AsyncIterator | KnownClass::Sequence | KnownClass::Mapping + | KnownClass::MutableMapping + | KnownClass::SupportsKeysAndGetItem | KnownClass::ChainMap | KnownClass::Counter | KnownClass::DefaultDict @@ -548,6 +554,8 @@ impl KnownClass { | KnownClass::AsyncIterator | KnownClass::Sequence | KnownClass::Mapping + | KnownClass::MutableMapping + | KnownClass::SupportsKeysAndGetItem | KnownClass::ChainMap | KnownClass::Counter | KnownClass::DefaultDict @@ -677,6 +685,8 @@ impl KnownClass { | KnownClass::AsyncIterator | KnownClass::Sequence | KnownClass::Mapping + | KnownClass::MutableMapping + | KnownClass::SupportsKeysAndGetItem | KnownClass::ChainMap | KnownClass::Counter | KnownClass::DefaultDict @@ -737,6 +747,7 @@ impl KnownClass { match self { Self::Hashable | Self::SupportsIndex + | Self::SupportsKeysAndGetItem | Self::Iterable | Self::TyExtensionsAsyncIterable | Self::TyExtensionsAsyncIterator @@ -841,6 +852,7 @@ impl KnownClass { | Self::ReMatch | Self::RePattern | Self::Mapping + | Self::MutableMapping | Self::Sequence | Self::PydanticBaseModel | Self::PydanticBaseSettings @@ -952,6 +964,8 @@ impl KnownClass { | KnownClass::AsyncIterator | KnownClass::Sequence | KnownClass::Mapping + | KnownClass::MutableMapping + | KnownClass::SupportsKeysAndGetItem | KnownClass::ChainMap | KnownClass::Counter | KnownClass::DefaultDict @@ -993,7 +1007,7 @@ impl KnownClass { } } - pub(crate) fn name(self, db: &dyn Db) -> &'static str { + pub(crate) fn name(self, python_version: PythonVersion) -> &'static str { match self { Self::Bool => "bool", Self::Object => "object", @@ -1039,6 +1053,7 @@ impl KnownClass { Self::AsyncGeneratorType => "AsyncGeneratorType", Self::CoroutineType => "CoroutineType", Self::NoneType => "NoneType", + Self::SupportsKeysAndGetItem => "SupportsKeysAndGetItem", Self::SpecialForm => "_SpecialForm", Self::TypeVar => "TypeVar", Self::ExtensionsTypeVar => "TypeVar", @@ -1062,7 +1077,7 @@ impl KnownClass { Self::Enum => "Enum", Self::EnumProperty => "property", Self::EnumType => { - if Program::get(db).python_version(db) >= PythonVersion::PY311 { + if python_version >= PythonVersion::PY311 { "EnumType" } else { "EnumMeta" @@ -1087,6 +1102,7 @@ impl KnownClass { Self::AsyncIterator => "AsyncIterator", Self::Sequence => "Sequence", Self::Mapping => "Mapping", + Self::MutableMapping => "MutableMapping", // 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, @@ -1132,28 +1148,31 @@ impl KnownClass { } } - pub(crate) fn display(self, db: &dyn Db) -> impl std::fmt::Display + '_ { - struct KnownClassDisplay<'db> { - db: &'db dyn Db, + pub(crate) fn display(self, python_version: PythonVersion) -> impl std::fmt::Display { + struct KnownClassDisplay { class: KnownClass, + python_version: PythonVersion, } - impl std::fmt::Display for KnownClassDisplay<'_> { + impl std::fmt::Display for KnownClassDisplay { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let KnownClassDisplay { class: known_class, - db, + python_version, } = *self; write!( f, "{module}.{class}", - module = known_class.canonical_module(db), - class = known_class.name(db) + module = known_class.canonical_module(python_version), + class = known_class.name(python_version) ) } } - KnownClassDisplay { db, class: self } + KnownClassDisplay { + class: self, + python_version, + } } /// Look up a [`KnownClass`] in its canonical module and return a [`Type`] representing all @@ -1162,7 +1181,7 @@ impl KnownClass { /// /// If the class cannot be found, a debug-level log message will be emitted stating this. #[track_caller] - pub fn to_instance(self, db: &dyn Db) -> Type<'_> { + pub fn to_instance<'db>(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { debug_assert_ne!( self, KnownClass::Tuple, @@ -1172,30 +1191,35 @@ impl KnownClass { #[salsa::tracked(returns(copy), heap_size=ruff_memory_usage::heap_size)] fn known_class_to_instance<'db>( db: &'db dyn Db, - class: KnownClassArgument<'db>, + argument: KnownClassArgument<'db>, ) -> Type<'db> { - class + let env = &ProgramEnvironment::from_program(argument.program(db)); + argument .class(db) - .to_class_literal(db) + .to_class_literal(db, env) .to_class_type(db) - .map(|class| Type::instance(db, class)) + .map(|class| Type::instance(db, env, class)) .unwrap_or_else(Type::unknown) } - known_class_to_instance(db, KnownClassArgument::new(db, self)) + known_class_to_instance(db, KnownClassArgument::new(db, self, env.program(db))) } /// Similar to [`KnownClass::to_instance`], but returns the Unknown-specialization where each type /// parameter is specialized to `Unknown`. #[track_caller] - pub(crate) fn to_instance_unknown(self, db: &dyn Db) -> Type<'_> { + pub(crate) fn to_instance_unknown<'db>( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { debug_assert_ne!( self, KnownClass::Tuple, "Use `Type::heterogeneous_tuple` or `Type::homogeneous_tuple` to create `tuple` instances" ); - self.try_to_class_literal(db) - .map(|literal| Type::instance(db, literal.unknown_specialization(db))) + self.try_to_class_literal(db, env) + .map(|literal| Type::instance(db, env, literal.unknown_specialization(db))) .unwrap_or_else(Type::unknown) } @@ -1207,6 +1231,7 @@ impl KnownClass { pub(crate) fn to_specialized_class_type<'t, 'db, T>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, specialization: T, ) -> Option> where @@ -1215,6 +1240,7 @@ impl KnownClass { { fn to_specialized_class_type_impl<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: KnownClass, class_literal: StaticClassLiteral<'db>, specialization: Cow<[Type<'db>]>, @@ -1229,7 +1255,7 @@ impl KnownClass { tracing::info!( "Wrong number of types when specializing {}. \ Falling back to default specialization for the symbol instead.", - class.display(db) + class.display(env.python_version(db)) ); } return class_literal.default_specialization(db); @@ -1239,12 +1265,16 @@ impl KnownClass { .apply_specialization(db, |_| generic_context.specialize(db, specialization)) } - let class_literal = self.to_class_literal(db).as_class_literal()?.as_static()?; + let class_literal = self + .to_class_literal(db, env) + .as_class_literal()? + .as_static()?; let generic_context = class_literal.generic_context(db)?; let specialization = specialization.into(); Some(to_specialized_class_type_impl( db, + env, self, class_literal, specialization, @@ -1261,6 +1291,7 @@ impl KnownClass { pub(crate) fn to_specialized_instance<'t, 'db, T>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, specialization: T, ) -> Type<'db> where @@ -1272,27 +1303,31 @@ impl KnownClass { KnownClass::Tuple, "Use `Type::heterogeneous_tuple` or `Type::homogeneous_tuple` to create `tuple` instances" ); - self.to_specialized_class_type(db, specialization) - .and_then(|class_type| Type::from(class_type).to_instance_approximation(db)) + self.to_specialized_class_type(db, env, specialization) + .and_then(|class_type| Type::from(class_type).to_instance_approximation(db, env)) .unwrap_or_else(Type::unknown) } /// Look up a [`KnownClass`] in its canonical module. /// /// Lookup errors are logged when the cached query executes. - fn lookup_class_literal( + fn lookup_class_literal<'db>( self, - db: &dyn Db, - ) -> Result>, KnownClassLookupError<'_>> { + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Result>, KnownClassLookupError<'db>> { #[salsa::tracked(returns(copy), cycle_initial=|_, _, _| Ok(None), heap_size=ruff_memory_usage::heap_size)] fn known_class_to_class_literal<'db>( db: &'db dyn Db, - class: KnownClassArgument<'db>, + argument: KnownClassArgument<'db>, ) -> Result>, KnownClassLookupError<'db>> { - let class = class.class(db); - let module = class.canonical_module(db); + let program = argument.program(db); + let env = &ProgramEnvironment::from_program(program); + let python_version = env.python_version(db); + let class = argument.class(db); + let module = class.canonical_module(python_version); let third_party = module.is_third_party(); - let symbol = known_module_symbol(db, module, class.name(db)).place; + let symbol = known_module_symbol(db, env, module, class.name(python_version)).place; let result = match symbol { Place::Defined(DefinedPlace { ty: Type::ClassLiteral(ClassLiteral::Static(class_literal)), @@ -1321,11 +1356,11 @@ impl KnownClass { lookup_error, KnownClassLookupError::ClassPossiblyUnbound { .. } ) { - tracing::info!("{}", lookup_error.display(db, class)); + tracing::info!("{}", lookup_error.display(db, env, class)); } else { tracing::info!( "{}. Falling back to `Unknown` for the symbol instead.", - lookup_error.display(db, class) + lookup_error.display(db, env, class) ); } } @@ -1333,15 +1368,19 @@ impl KnownClass { result } - known_class_to_class_literal(db, KnownClassArgument::new(db, self)) + known_class_to_class_literal(db, KnownClassArgument::new(db, self, env.program(db))) } /// Look up a [`KnownClass`] in its canonical module and return a [`Type`] representing that /// class literal. /// /// If the class cannot be found, a debug-level log message will be emitted stating this. - pub(crate) fn try_to_class_literal(self, db: &dyn Db) -> Option> { - match self.lookup_class_literal(db) { + pub(crate) fn try_to_class_literal<'db>( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { + match self.lookup_class_literal(db, env) { Ok(class_literal) => class_literal, Err(KnownClassLookupError::ClassPossiblyUnbound { class_literal, .. }) => { Some(class_literal) @@ -1357,8 +1396,12 @@ impl KnownClass { /// class literal. /// /// If the class cannot be found, 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) + pub(crate) fn to_class_literal<'db>( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { + self.try_to_class_literal(db, env) .map(|class| Type::ClassLiteral(ClassLiteral::Static(class))) .unwrap_or_else(Type::unknown) } @@ -1367,41 +1410,48 @@ impl KnownClass { /// and all possible subclasses of the class. /// /// If the class cannot be found, a debug-level log message will be emitted stating this. - pub fn to_subclass_of(self, db: &dyn Db) -> Type<'_> { - self.to_class_literal(db) + pub fn to_subclass_of<'db>(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + self.to_class_literal(db, env) .to_class_type(db) - .map(|class| SubclassOfType::from(db, class)) + .map(|class| SubclassOfType::from(db, env, class)) .unwrap_or_else(SubclassOfType::subclass_of_unknown) } pub(crate) fn to_specialized_subclass_of<'db>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, specialization: &[Type<'db>], ) -> Type<'db> { - self.to_specialized_class_type(db, specialization) - .map(|class_type| SubclassOfType::from(db, class_type)) + self.to_specialized_class_type(db, env, specialization) + .map(|class_type| SubclassOfType::from(db, env, class_type)) .unwrap_or_else(SubclassOfType::subclass_of_unknown) } /// Return `true` if this symbol can be resolved to a class definition `class` in its canonical /// module, *and* `class` is a subclass of `other`. - pub(crate) fn is_subclass_of<'db>(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { - self.lookup_class_literal(db) + pub(crate) fn is_subclass_of<'db>( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + other: ClassType<'db>, + ) -> bool { + self.lookup_class_literal(db, env) .is_ok_and(|class| class.is_some_and(|class| class.is_subclass_of(db, None, other))) } pub(crate) fn when_subclass_of<'db, 'c>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, other: ClassType<'db>, constraints: &'c ConstraintSetBuilder<'db>, ) -> ConstraintSet<'db, 'c> { - ConstraintSet::from_bool(constraints, self.is_subclass_of(db, other)) + ConstraintSet::from_bool(constraints, self.is_subclass_of(db, env, other)) } /// Return the module in which we should look up the definition for this class - pub(super) fn canonical_module(self, db: &dyn Db) -> KnownModule { + fn canonical_module(self, python_version: PythonVersion) -> KnownModule { match self { Self::Bool | Self::Object @@ -1457,7 +1507,7 @@ impl KnownClass { | Self::EllipsisType | Self::NotImplementedType | Self::WrapperDescriptorType => KnownModule::Types, - Self::NoneType => KnownModule::Typeshed, + Self::NoneType | Self::SupportsKeysAndGetItem => KnownModule::Typeshed, Self::Awaitable | Self::Generator | Self::AsyncGenerator @@ -1469,6 +1519,7 @@ impl KnownClass { | Self::AsyncIterator | Self::Sequence | Self::Mapping + | Self::MutableMapping | Self::ProtocolMeta | Self::ParamSpec | Self::Hashable @@ -1483,22 +1534,20 @@ impl KnownClass { | Self::ExtensionTypedDictFallback | Self::NewType => KnownModule::TypingExtensions, Self::TypeVarTuple => { - if Program::get(db).python_version(db) >= PythonVersion::PY311 { + if python_version >= PythonVersion::PY311 { KnownModule::Typing } else { KnownModule::TypingExtensions } } Self::Sentinel => { - if Program::get(db).python_version(db) >= PythonVersion::PY315 { + if python_version >= PythonVersion::PY315 { KnownModule::Builtins } else { 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: @@ -1548,141 +1597,6 @@ impl KnownClass { } } - /// Returns `Some(true)` if all instances of this `KnownClass` compare equal. - /// Returns `None` for `KnownClass::Tuple`, since whether or not a tuple type - /// is single-valued depends on the tuple spec. - pub(crate) const fn is_single_valued(self) -> Option { - match self { - Self::NoneType - | Self::NoDefaultType - | Self::EllipsisType - | Self::NotImplementedType => Some(true), - - Self::Bool - | Self::Object - | Self::Bytes - | Self::Bytearray - | Self::Memoryview - | Self::Range - | Self::Type - | Self::Int - | Self::Float - | Self::Complex - | Self::Str - | Self::List - | Self::Set - | Self::FrozenSet - | Self::Dict - | Self::Slice - | Self::Property - | Self::BaseException - | Self::BaseExceptionGroup - | Self::Exception - | Self::Warning - | Self::NotImplementedError - | Self::AssertionError - | Self::RuntimeError - | Self::ExceptionGroup - | Self::Staticmethod - | Self::Classmethod - | Self::Awaitable - | Self::Generator - | Self::AsyncGenerator - | Self::Deprecated - | Self::GenericAlias - | Self::ModuleType - | Self::FunctionType - | Self::GeneratorType - | Self::AsyncGeneratorType - | Self::CoroutineType - | Self::MethodType - | Self::MethodWrapperType - | Self::WrapperDescriptorType - | Self::SpecialForm - | Self::ChainMap - | Self::Counter - | Self::DefaultDict - | Self::Deque - | Self::OrderedDict - | Self::VersionInfo - | Self::Hashable - | Self::SupportsIndex - | Self::StdlibAlias - | Self::TypeAliasType - | Self::TypeVar - | Self::ExtensionsTypeVar - | Self::ParamSpec - | Self::ExtensionsParamSpec - | Self::ParamSpecArgs - | Self::ParamSpecKwargs - | Self::TypeVarTuple - | Self::ExtensionsTypeVarTuple - | Self::Sentinel - | Self::Enum - | Self::EnumProperty - | Self::EnumType - | Self::Auto - | Self::Member - | Self::Nonmember - | Self::StrEnum - | Self::IntEnum - | Self::Flag - | Self::IntFlag - | Self::ABCMeta - | Self::Super - | Self::NewType - | Self::Field - | Self::KwOnly - | Self::Iterable - | Self::TyExtensionsAsyncIterable - | Self::TyExtensionsAsyncIterator - | Self::TyExtensionsIterable - | Self::Iterator - | Self::TyExtensionsIterator - | Self::AsyncIterator - | Self::Sequence - | Self::Mapping - | Self::NamedTupleFallback - | Self::NamedTupleLike - | Self::Character - | Self::ConstraintSet - | Self::ConstraintSetSolution - | Self::GenericContext - | Self::Specialization - | Self::TypedDictFallback - | Self::ExtensionTypedDictFallback - | Self::BuiltinFunctionType - | Self::ProtocolMeta - | Self::Template - | Self::Path - | Self::UnionType - | Self::FunctoolsPartial - | Self::ReMatch - | Self::RePattern - | Self::PydanticBaseModel - | Self::PydanticBaseSettings - | Self::PydanticConfigDict - | Self::PydanticRootModel - | Self::PydanticStrict - | Self::DjangoModel - | Self::DjangoField - | Self::DjangoForeignKey - | Self::DjangoOneToOneField - | Self::DjangoManyToManyField - | Self::DjangoManager - | Self::DjangoQuerySet - | Self::SqlalchemyDeclarativeBase - | Self::SqlalchemyMappedAsDataclass - | Self::SqlalchemyMapped - | Self::ByStaticProperty => Some(false), - - Self::Tuple => None, - } - } - - /// 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(crate) const fn is_singleton(self) -> bool { match self { Self::NoneType @@ -1776,6 +1690,8 @@ impl KnownClass { | Self::AsyncIterator | Self::Sequence | Self::Mapping + | Self::MutableMapping + | Self::SupportsKeysAndGetItem | Self::NamedTupleFallback | Self::NamedTupleLike | Self::Character @@ -1813,7 +1729,7 @@ impl KnownClass { pub(crate) fn try_from_file_and_name( db: &dyn Db, - file: File, + file: ImportingFile<'_>, class_name: &str, ) -> Option { // We assert that this match is exhaustive over the right-hand side in the unit test @@ -1853,6 +1769,7 @@ impl KnownClass { "deprecated" => &[Self::Deprecated], "GenericAlias" => &[Self::GenericAlias], "NoneType" => &[Self::NoneType], + "SupportsKeysAndGetItem" => &[Self::SupportsKeysAndGetItem], "ModuleType" => &[Self::ModuleType], "GeneratorType" => &[Self::GeneratorType], "AsyncGeneratorType" => &[Self::AsyncGeneratorType], @@ -1872,6 +1789,7 @@ impl KnownClass { "AsyncIterator" => &[Self::AsyncIterator, Self::TyExtensionsAsyncIterator], "Sequence" => &[Self::Sequence], "Mapping" => &[Self::Mapping], + "MutableMapping" => &[Self::MutableMapping], "ParamSpec" => &[Self::ParamSpec, Self::ExtensionsParamSpec], "ParamSpecArgs" => &[Self::ParamSpecArgs], "ParamSpecKwargs" => &[Self::ParamSpecKwargs], @@ -1891,12 +1809,8 @@ impl KnownClass { "SupportsIndex" => &[Self::SupportsIndex], "Enum" => &[Self::Enum], "EnumMeta" => &[Self::EnumType], - "EnumType" if Program::get(db).python_version(db) >= PythonVersion::PY311 => { - &[Self::EnumType] - } - "StrEnum" if Program::get(db).python_version(db) >= PythonVersion::PY311 => { - &[Self::StrEnum] - } + "EnumType" if file.python_version(db) >= PythonVersion::PY311 => &[Self::EnumType], + "StrEnum" if file.python_version(db) >= PythonVersion::PY311 => &[Self::StrEnum], "IntEnum" => &[Self::IntEnum], "Flag" => &[Self::Flag], "IntFlag" => &[Self::IntFlag], @@ -1943,16 +1857,16 @@ impl KnownClass { _ => return None, }; - let module = file_to_module(db, file)?.known(db)?; - + let module = file_to_module(db, file.resolver_file(db))?.known(db)?; + let python_version = file.python_version(db); candidates .iter() .copied() - .find(|&candidate| candidate.check_module(db, module)) + .find(|&candidate| candidate.check_module(python_version, module)) } /// Return `true` if the module of `self` matches `module` - fn check_module(self, db: &dyn Db, module: KnownModule) -> bool { + fn check_module(self, python_version: PythonVersion, module: KnownModule) -> bool { match self { Self::Bool | Self::Object @@ -1978,7 +1892,6 @@ impl KnownClass { | Self::DefaultDict | Self::Deque | Self::OrderedDict - | Self::StdlibAlias // no equivalent class exists in typing_extensions, nor ever will | Self::ModuleType | Self::VersionInfo | Self::BaseException @@ -2017,6 +1930,7 @@ impl KnownClass { | Self::Field | Self::KwOnly | Self::NamedTupleFallback + | Self::SupportsKeysAndGetItem | Self::TypedDictFallback | Self::ExtensionTypedDictFallback | Self::TypeVar @@ -2059,8 +1973,12 @@ impl KnownClass { | Self::SqlalchemyDeclarativeBase | Self::SqlalchemyMappedAsDataclass | Self::SqlalchemyMapped - | Self::ByStaticProperty => module == self.canonical_module(db), + | Self::ByStaticProperty => module == self.canonical_module(python_version), + + // no equivalent class exists in typing_extensions, nor ever will + Self::StdlibAlias => module == self.canonical_module(python_version), Self::NoneType => matches!(module, KnownModule::Typeshed | KnownModule::Types), + Self::SpecialForm | Self::TypeAliasType | Self::NoDefaultType @@ -2073,9 +1991,16 @@ impl KnownClass { | Self::AsyncIterator | Self::Sequence | Self::Mapping + | Self::MutableMapping | Self::ProtocolMeta - | Self::NewType => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions), - Self::Deprecated => matches!(module, KnownModule::Warnings | KnownModule::TypingExtensions), + | Self::NewType => { + matches!(module, KnownModule::Typing | KnownModule::TypingExtensions) + } + + Self::Deprecated => matches!( + module, + KnownModule::Warnings | KnownModule::TypingExtensions + ), } } @@ -2100,7 +2025,8 @@ impl KnownClass { // 2. The first parameter of the current function (typically `self` or `cls`) match overload.parameter_types() { [] => { - let Some(enclosing_class) = nearest_enclosing_class(db, index, scope) + let Some(enclosing_class) = + nearest_enclosing_class(context.db(), index, scope) else { BoundSuperError::UnavailableImplicitArguments .report_diagnostic(context, call_expression.into()); @@ -2109,7 +2035,9 @@ impl KnownClass { }; // Check if the enclosing class is a `NamedTuple`, which forbids the use of `super()`. - if CodeGeneratorKind::NamedTuple.matches(db, enclosing_class.into()) { + if CodeGeneratorKind::NamedTuple + .matches(context.db(), enclosing_class.into()) + { if let Some(builder) = context .report_lint(&SUPER_CALL_IN_NAMED_TUPLE_METHOD, call_expression) { @@ -2146,10 +2074,11 @@ impl KnownClass { }; let definition = index.expect_single_definition(first_param); - let first_param = binding_type(db, definition); + let first_param = binding_type(context.db(), definition); let bound_super = BoundSuperType::build( db, + context.program_environment(), Type::ClassLiteral(ClassLiteral::Static(enclosing_class)), first_param, ) @@ -2162,8 +2091,12 @@ impl KnownClass { } [Some(pivot_class_type), Some(owner_type)] => { // Check if the enclosing class is a `NamedTuple`, which forbids the use of `super()`. - if let Some(enclosing_class) = nearest_enclosing_class(db, index, scope) { - if CodeGeneratorKind::NamedTuple.matches(db, enclosing_class.into()) { + if let Some(enclosing_class) = + nearest_enclosing_class(context.db(), index, scope) + { + if CodeGeneratorKind::NamedTuple + .matches(context.db(), enclosing_class.into()) + { if let Some(builder) = context .report_lint(&SUPER_CALL_IN_NAMED_TUPLE_METHOD, call_expression) { @@ -2177,11 +2110,16 @@ impl KnownClass { } } - let bound_super = BoundSuperType::build(db, *pivot_class_type, *owner_type) - .unwrap_or_else(|err| { - err.report_diagnostic(context, call_expression.into()); - Type::unknown() - }); + let bound_super = BoundSuperType::build( + db, + context.program_environment(), + *pivot_class_type, + *owner_type, + ) + .unwrap_or_else(|err| { + err.report_diagnostic(context, call_expression.into()); + Type::unknown() + }); overload.set_return_type(bound_super); } _ => {} @@ -2226,6 +2164,9 @@ impl KnownClass { struct KnownClassArgument { #[returns(copy)] class: KnownClass, + + #[returns(copy)] + program: Program<'db>, } /// Enumeration of ways in which looking up a [`KnownClass`] in its canonical module could fail. @@ -2255,19 +2196,31 @@ impl<'db> KnownClassLookupError<'db> { } } - fn display(&self, db: &'db dyn Db, class: KnownClass) -> impl std::fmt::Display + 'db { - struct ErrorDisplay<'db> { + fn display<'env>( + &self, + db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, + class: KnownClass, + ) -> impl std::fmt::Display + 'env { + struct ErrorDisplay<'env, 'db> { db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, class: KnownClass, error: KnownClassLookupError<'db>, } - impl std::fmt::Display for ErrorDisplay<'_> { + 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); + let db = self.db; + let ErrorDisplay { + db: _, + env, + class, + error, + } = self; + + let python_version = env.python_version(db); + let class = class.display(python_version); let location = if error.is_third_party() { "" } else { @@ -2283,7 +2236,7 @@ impl<'db> KnownClassLookupError<'db> { f, "Error looking up `{class}`{location}: 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), + found_type = found_type.display(db, env), ), KnownClassLookupError::ClassPossiblyUnbound { .. } => write!( f, @@ -2296,6 +2249,7 @@ impl<'db> KnownClassLookupError<'db> { ErrorDisplay { db, + env, class, error: *self, } @@ -2305,33 +2259,37 @@ impl<'db> KnownClassLookupError<'db> { #[cfg(test)] mod tests { use super::*; - use crate::db::tests::setup_db; + use crate::db::tests::{TestDbBuilder, setup_db}; use crate::{PythonVersionSource, PythonVersionWithSource}; - use salsa::Setter; use strum::IntoEnumIterator; use ty_module_resolver::resolve_module_confident; + use ty_python_core::TestProgramDb as _; + use ty_python_core::program::{Program, ProgramSettings}; #[test] fn known_class_roundtrip_from_str() { - let mut db = setup_db(); - Program::get(&db) - .set_python_version_with_source(&mut db) - .to(PythonVersionWithSource { - version: PythonVersion::latest_preview(), - source: PythonVersionSource::default(), - }); + let db = TestDbBuilder::new() + .with_python_version(PythonVersion::latest_preview()) + .build() + .expect("valid TestDb setup"); + let python_version = db.python_version(); + let resolver_environment = db.program_environment().resolver_environment(&db); for class in KnownClass::iter() { - if class.canonical_module(&db).is_third_party() { + if class.canonical_module(python_version).is_third_party() { continue; } - let class_name = class.name(&db); - let class_module = - resolve_module_confident(&db, &class.canonical_module(&db).name()).unwrap(); + let class_name = class.name(python_version); + let class_module = resolve_module_confident( + &db, + resolver_environment, + &class.canonical_module(python_version).name(), + ) + .unwrap(); assert_eq!( KnownClass::try_from_file_and_name( &db, - class_module.file(&db).unwrap(), + ImportingFile::File(class_module.file(&db).unwrap(), resolver_environment), class_name ), Some(class), @@ -2342,28 +2300,26 @@ mod tests { #[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(), - }); + let db = TestDbBuilder::new() + .with_python_version(PythonVersion::latest_ty()) + .build() + .expect("valid TestDb setup"); + let python_version = db.python_version(); + let env = db.program_environment(); for class in KnownClass::iter() { - if class.canonical_module(&db).is_third_party() { + if class.canonical_module(python_version).is_third_party() { continue; } // Check the class can be looked up successfully - class.try_to_class_literal(&db).unwrap(); + class.try_to_class_literal(&db, &env).unwrap(); // We can't call `KnownClass::Tuple.to_instance()`; // there are assertions to ensure that we always call `Type::homogeneous_tuple()` // or `Type::heterogeneous_tuple()` instead.` if class != KnownClass::Tuple { assert_ne!( - class.to_instance(&db), + class.to_instance(&db, &env), Type::unknown(), "Unexpectedly fell back to `Unknown` for `{class:?}`" ); @@ -2373,14 +2329,15 @@ mod tests { #[test] fn known_class_doesnt_fallback_to_unknown_unexpectedly_on_low_python_version() { - let mut db = setup_db(); + let db = setup_db(); // First, collect the `KnownClass` variants // and sort them according to the version they were added in. // This makes the test far faster as it minimizes the number of times // we need to change the Python version in the loop. + let python_version = db.python_version(); let mut classes: Vec<(KnownClass, PythonVersion)> = KnownClass::iter() - .filter(|class| !class.canonical_module(&db).is_third_party()) + .filter(|class| !class.canonical_module(python_version).is_third_party()) .map(|class| { let version_added = match class { KnownClass::Template => PythonVersion::PY314, @@ -2402,29 +2359,35 @@ mod tests { classes.sort_unstable_by_key(|(_, version)| *version); - let program = Program::get(&db); + let mut program = db.program(); let mut current_version = program.python_version(&db); + let python_platform = program.python_platform(&db).clone(); + let search_paths = program.search_paths(&db).clone(); for (class, version_added) in classes { if version_added != current_version { - program - .set_python_version_with_source(&mut db) - .to(PythonVersionWithSource { + let settings = ProgramSettings { + python_version: PythonVersionWithSource { version: version_added, source: PythonVersionSource::default(), - }); + }, + python_platform: python_platform.clone(), + search_paths: search_paths.clone(), + }; + program = Program::from_settings(&db, settings); current_version = version_added; } // Check the class can be looked up successfully - class.try_to_class_literal(&db).unwrap(); + let env = ProgramEnvironment::from_program(program); + class.try_to_class_literal(&db, &env).unwrap(); // We can't call `KnownClass::Tuple.to_instance()`; // there are assertions to ensure that we always call `Type::homogeneous_tuple()` // or `Type::heterogeneous_tuple()` instead.` if class != KnownClass::Tuple { assert_ne!( - class.to_instance(&db), + class.to_instance(&db, &env), Type::unknown(), "Unexpectedly fell back to `Unknown` for `{class:?}` on Python {version_added}" ); diff --git a/crates/ty_python_semantic/src/types/class/named_tuple.rs b/crates/ty_python_semantic/src/types/class/named_tuple.rs index fbb3b24608..199470acf6 100644 --- a/crates/ty_python_semantic/src/types/class/named_tuple.rs +++ b/crates/ty_python_semantic/src/types/class/named_tuple.rs @@ -1,16 +1,20 @@ +use crate::ProgramEnvironment; use ruff_db::{diagnostic::Span, parsed::parsed_module}; -use ruff_python_ast as ast; -use ruff_python_ast::{NodeIndex, PythonVersion, name::Name}; -use ruff_text_size::{Ranged, TextRange}; +use ruff_python_ast::{PythonVersion, name::Name}; +use ruff_text_size::TextRange; use crate::{ - Db, Program, + Db, place::{Place, PlaceAndQualifiers}, types::{ BindingContext, BoundTypeVarInstance, ClassBase, ClassLiteral, ClassType, GenericContext, KnownClass, KnownInstanceType, MemberLookupPolicy, Parameter, Parameters, PropertyInstanceType, Signature, SubclassOfType, Type, TypeContext, TypeMapping, - definition_expression_type, member::Member, mro::Mro, tuple::TupleType, + class::{DynamicClassHeaderAnchor, dynamic_class_header_range}, + definition_expression_type, + member::Member, + mro::Mro, + tuple::TupleType, }, }; use ty_python_core::{definition::Definition, scope::ScopeId}; @@ -24,6 +28,7 @@ use ty_python_core::{definition::Definition, scope::ScopeId}; /// generic context in the synthesized `__new__` signature. pub(super) fn synthesize_namedtuple_class_member<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, instance_ty: Type<'db>, fields: impl Iterator>, @@ -32,8 +37,11 @@ pub(super) fn synthesize_namedtuple_class_member<'db>( match name { "__new__" => { // __new__(cls, field1, field2, ...) -> Self - let self_typevar = - BoundTypeVarInstance::synthetic_self(db, instance_ty, BindingContext::Synthetic); + let self_typevar = BoundTypeVarInstance::synthetic_self( + db, + instance_ty, + BindingContext::Synthetic(env.program(db)), + ); let self_ty = Type::TypeVar(self_typevar); let variables = inherited_generic_context @@ -41,12 +49,12 @@ pub(super) fn synthesize_namedtuple_class_member<'db>( .flat_map(|ctx| ctx.variables(db)) .chain(std::iter::once(self_typevar)); - let generic_context = GenericContext::from_typevar_instances(db, variables); + let generic_context = GenericContext::from_typevar_instances(db, env, variables); // CPython generates namedtuple `__new__` as `(_cls, field1, ...)` so field names like // `cls` remain usable as keyword arguments at call sites. let first_parameter = Parameter::positional_or_keyword(Name::new_static("_cls")) - .with_annotated_type(SubclassOfType::from(db, self_typevar)); + .with_annotated_type(SubclassOfType::from(db, env, self_typevar)); let parameters = std::iter::once(first_parameter).chain(fields.map(|field| { Parameter::positional_or_keyword(field.name) @@ -63,25 +71,25 @@ pub(super) fn synthesize_namedtuple_class_member<'db>( Some(Type::function_like_callable(db, signature)) } "__match_args__" => { - if Program::get(db).python_version(db) < PythonVersion::PY310 { + if env.python_version(db) < PythonVersion::PY310 { return None; } // __match_args__: tuple[Literal["field1"], Literal["field2"], ...] let field_types = fields.map(|field| Type::string_literal(db, &field.name)); - Some(Type::heterogeneous_tuple(db, field_types)) + Some(Type::heterogeneous_tuple(db, env, field_types)) } "_fields" => { // _fields: tuple[Literal["field1"], Literal["field2"], ...] let field_types = fields.map(|field| Type::string_literal(db, &field.name)); - Some(Type::heterogeneous_tuple(db, field_types)) + Some(Type::heterogeneous_tuple(db, env, field_types)) } "__slots__" => { // __slots__: tuple[()] - always empty for namedtuples - Some(Type::empty_tuple(db)) + Some(Type::empty_tuple(db, env)) } "_replace" | "__replace__" => { - if name == "__replace__" && Program::get(db).python_version(db) < PythonVersion::PY313 { + if name == "__replace__" && env.python_version(db) < PythonVersion::PY313 { return None; } @@ -89,7 +97,7 @@ pub(super) fn synthesize_namedtuple_class_member<'db>( let self_ty = Type::TypeVar(BoundTypeVarInstance::synthetic_self( db, instance_ty, - BindingContext::Synthetic, + BindingContext::Synthetic(env.program(db)), )); let first_parameter = Parameter::positional_or_keyword(Name::new_static("self")) @@ -112,10 +120,10 @@ pub(super) fn synthesize_namedtuple_class_member<'db>( _ => { // Fall back to NamedTupleFallback for other synthesized methods. KnownClass::NamedTupleFallback - .to_class_literal(db) + .to_class_literal(db, env) .as_class_literal()? .as_static()? - .own_class_member(db, inherited_generic_context, None, name) + .own_class_member(db, env, inherited_generic_context, None, name) .ignore_possibly_undefined() } } @@ -165,6 +173,7 @@ impl<'db> DynamicNamedTupleLiteral<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -172,7 +181,7 @@ impl<'db> DynamicNamedTupleLiteral<'db> { db, self.name(db), self.anchor(db) - .recursive_type_normalized_impl(db, div, nested)?, + .recursive_type_normalized_impl(db, env, div, nested)?, )) } } @@ -198,43 +207,22 @@ impl<'db> DynamicNamedTupleLiteral<'db> { } /// Returns an instance type for this dynamic namedtuple. - pub(crate) fn to_instance(self, db: &'db dyn Db) -> Type<'db> { - Type::instance(db, ClassType::NonGeneric(self.into())) + fn to_instance(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + Type::instance(db, env, ClassType::NonGeneric(self.into())) } /// Returns the range of the namedtuple call expression. pub(crate) fn header_range(self, db: &'db dyn Db) -> TextRange { - let scope = self.scope(db); - let file = scope.file(db); - let module = parsed_module(db, file).load(db); - - match self.anchor(db) { + let anchor = match self.anchor(db) { DynamicNamedTupleAnchor::CollectionsDefinition { definition, .. } | DynamicNamedTupleAnchor::TypingDefinition(definition) => { - // For definitions, get the range from the definition's value. - // The namedtuple call is the value of the assignment. - definition - .kind(db) - .value(&module) - .expect("DynamicClassAnchor::Definition should only be used for assignments") - .range() + DynamicClassHeaderAnchor::Definition(*definition) } DynamicNamedTupleAnchor::ScopeOffset { offset, .. } => { - // For dangling calls, compute the absolute index from the offset. - let scope_anchor = scope.node(db).node_index().unwrap_or(NodeIndex::from(0)); - let anchor_u32 = scope_anchor - .as_u32() - .expect("anchor should not be NodeIndex::NONE"); - let absolute_index = NodeIndex::from(anchor_u32 + offset); - - // Get the node and return its range. - let node: &ast::ExprCall = module - .get_by_index(absolute_index) - .try_into() - .expect("scope offset should point to ExprCall"); - node.range() + DynamicClassHeaderAnchor::ScopeOffset(*offset) } - } + }; + dynamic_class_header_range(db, self.scope(db), anchor) } /// Returns a [`Span`] pointing to the namedtuple call expression. @@ -260,42 +248,49 @@ impl<'db> DynamicNamedTupleLiteral<'db> { #[salsa::tracked( returns(ref), heap_size=ruff_memory_usage::heap_size, - cycle_initial=|db, _, self_| Mro::from_error( - db, ClassType::NonGeneric(ClassLiteral::DynamicNamedTuple(self_)), + cycle_initial=|db, _, self_: DynamicNamedTupleLiteral<'db>| Mro::from_error( + db, + &ProgramEnvironment::from_scope(self_.scope(db)), + ClassType::NonGeneric(ClassLiteral::DynamicNamedTuple(self_)), ), )] pub(crate) fn mro(self, db: &'db dyn Db) -> Mro<'db> { + let env = ProgramEnvironment::from_scope(self.scope(db)); let self_base = ClassBase::Class(ClassType::NonGeneric(self.into())); - let tuple_class = self.tuple_base_class(db); + let tuple_class = self.tuple_base_class(db, &env); std::iter::once(self_base) .chain(tuple_class.iter_mro(db)) .collect() } - /// Get the metaclass of this dynamic namedtuple. + /// Returns the metaclass of this namedtuple. /// /// Namedtuples always have `type` as their metaclass. pub(crate) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { - let _ = self; - KnownClass::Type.to_class_literal(db) + let env = ProgramEnvironment::from_scope(self.scope(db)); + KnownClass::Type.to_class_literal(db, &env) } /// Compute the specialized tuple class that this namedtuple inherits from. /// /// For example, `namedtuple("Point", [("x", int), ("y", int)])` inherits from `tuple[int, int]`. - pub(crate) fn tuple_base_class(self, db: &'db dyn Db) -> ClassType<'db> { + pub(crate) fn tuple_base_class( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> ClassType<'db> { // If fields are unknown, return `tuple[Unknown, ...]` to avoid false positives // like index-out-of-bounds errors. if !self.has_known_fields(db) { - return TupleType::homogeneous(db, Type::unknown()).to_class_type(db); + return TupleType::homogeneous(db, env, Type::unknown()).to_class_type(db); } let field_types = self.fields(db).iter().map(|field| field.ty); - TupleType::heterogeneous(db, field_types) - .map(|t| t.to_class_type(db)) + TupleType::heterogeneous(db, env, field_types) + .map(|tuple| tuple.to_class_type(db)) .unwrap_or_else(|| { KnownClass::Tuple - .to_class_literal(db) + .to_class_literal(db, env) .as_class_literal() .expect("tuple should be a class literal") .default_specialization(db) @@ -315,7 +310,12 @@ impl<'db> DynamicNamedTupleLiteral<'db> { } /// Look up an instance member by name (including superclasses). - pub(crate) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + pub(crate) fn instance_member( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> PlaceAndQualifiers<'db> { // First check own instance members. let result = self.own_instance_member(db, name); if !result.is_undefined() { @@ -323,13 +323,14 @@ impl<'db> DynamicNamedTupleLiteral<'db> { } // Fall back to the tuple base type for other attributes. - Type::instance(db, self.tuple_base_class(db)).instance_member(db, name) + Type::instance(db, env, self.tuple_base_class(db, env)).instance_member(db, env, name) } /// Look up a class-level member by name. pub(crate) fn class_member( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, policy: MemberLookupPolicy, ) -> PlaceAndQualifiers<'db> { @@ -343,7 +344,9 @@ impl<'db> DynamicNamedTupleLiteral<'db> { // *specialized* `tuple[int, str]`, not the bare `tuple` literal, so // inherited members keep the element types — `__iter__` answers // `Iterator[int | str]` rather than `Iterator[Unknown]` - let result = self.tuple_base_class(db).class_member(db, name, policy); + let result = self + .tuple_base_class(db, env) + .class_member(db, env, name, policy); // If fields are unknown (dynamic) and the attribute wasn't found, // return `Any` instead of failing. @@ -359,8 +362,9 @@ impl<'db> DynamicNamedTupleLiteral<'db> { /// This only checks synthesized members and field properties, without falling /// back to tuple or other base classes. pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { + let env = ProgramEnvironment::from_scope(self.scope(db)); // Handle synthesized namedtuple attributes. - if let Some(ty) = self.synthesized_class_member(db, name) { + if let Some(ty) = self.synthesized_class_member(db, &env, name) { return Member::definitely_declared(ty); } @@ -375,8 +379,13 @@ impl<'db> DynamicNamedTupleLiteral<'db> { } /// Generate synthesized class members for namedtuples. - fn synthesized_class_member(self, db: &'db dyn Db, name: &str) -> Option> { - let instance_ty = self.to_instance(db); + fn synthesized_class_member( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> Option> { + let instance_ty = self.to_instance(db, env); // When fields are unknown, handle constructor and field-specific methods specially. if !self.has_known_fields(db) { @@ -389,14 +398,15 @@ impl<'db> DynamicNamedTupleLiteral<'db> { // For other field-specific methods, fall through to NamedTupleFallback. "__match_args__" | "_fields" | "_replace" | "__replace__" => { return KnownClass::NamedTupleFallback - .to_class_literal(db) + .to_class_literal(db, env) .as_class_literal()? .as_static()? - .own_class_member(db, None, None, name) + .own_class_member(db, env, None, None, name) .ignore_possibly_undefined() .map(|ty| { ty.apply_type_mapping( db, + env, &TypeMapping::ReplaceSelf { new_upper_bound: instance_ty, }, @@ -410,6 +420,7 @@ impl<'db> DynamicNamedTupleLiteral<'db> { let result = synthesize_namedtuple_class_member( db, + env, name, instance_ty, self.fields(db).iter().cloned(), @@ -427,6 +438,7 @@ impl<'db> DynamicNamedTupleLiteral<'db> { result.map(|ty| { ty.apply_type_mapping( db, + env, &TypeMapping::ReplaceSelf { new_upper_bound: instance_ty, }, @@ -443,7 +455,8 @@ impl<'db> DynamicNamedTupleLiteral<'db> { heap_size=ruff_memory_usage::heap_size )] fn deferred_spec<'db>(db: &'db dyn Db, definition: Definition<'db>) -> NamedTupleSpec<'db> { - let module = parsed_module(db, definition.file(db)).load(db); + let python_file = definition.python_file(db); + let module = parsed_module(db, python_file).load(db); let node = definition .kind(db) .value(&module) @@ -538,13 +551,14 @@ impl<'db> DynamicNamedTupleAnchor<'db> { fn recursive_type_normalized_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { match self { Self::CollectionsDefinition { definition, spec } => Some(Self::CollectionsDefinition { definition: *definition, - spec: spec.recursive_type_normalized_impl(db, div, nested)?, + spec: spec.recursive_type_normalized_impl(db, env, div, nested)?, }), Self::TypingDefinition(definition) => Some(Self::TypingDefinition(*definition)), Self::ScopeOffset { @@ -554,7 +568,7 @@ impl<'db> DynamicNamedTupleAnchor<'db> { } => Some(Self::ScopeOffset { scope: *scope, offset: *offset, - spec: spec.recursive_type_normalized_impl(db, div, nested)?, + spec: spec.recursive_type_normalized_impl(db, env, div, nested)?, }), } } @@ -585,6 +599,7 @@ impl<'db> NamedTupleSpec<'db> { pub(crate) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -592,11 +607,11 @@ impl<'db> NamedTupleSpec<'db> { .fields(db) .iter() .map(|f| { - let ty = f.ty.recursive_type_normalized_impl(db, div, true); + let ty = f.ty.recursive_type_normalized_impl(db, env, div, true); let ty = if nested { ty? } else { ty.unwrap_or(div) }; let default = match f.default { Some(default) => { - let default = default.recursive_type_normalized_impl(db, div, true); + let default = default.recursive_type_normalized_impl(db, env, div, true); Some(if nested { default? } else { diff --git a/crates/ty_python_semantic/src/types/class/static_literal.rs b/crates/ty_python_semantic/src/types/class/static_literal.rs index e75229a436..88404dd403 100644 --- a/crates/ty_python_semantic/src/types/class/static_literal.rs +++ b/crates/ty_python_semantic/src/types/class/static_literal.rs @@ -1,5 +1,7 @@ +use crate::ProgramEnvironment; use itertools::{Either, Itertools}; use ruff_db::{ + PythonFile, diagnostic::Span, files::File, parsed::{ParsedModuleRef, parsed_module}, @@ -11,12 +13,14 @@ use rustc_hash::FxHashSet; use std::cell::RefCell; use crate::{ - Db, FxIndexMap, FxIndexSet, Program, TypeQualifiers, + Db, FxIndexMap, FxIndexSet, TypeQualifiers, place::{ DefinedPlace, Definedness, Place, PlaceAndQualifiers, Provenance, PublicTypePolicy, TypeOrigin, place_from_bindings, place_from_declarations, }, - reachability::{DeclarationsIteratorExtension, binding_reachability}, + reachability::{ + DeclarationsIteratorExtension, ReachabilityConstraintsExtension, binding_reachability, + }, types::{ ApplyTypeMappingVisitor, BoundTypeVarIdentity, BoundTypeVarInstance, CallArguments, CallableType, ClassBase, ClassLiteral, ClassType, DATACLASS_FLAGS, DataclassFlags, @@ -25,6 +29,7 @@ use crate::{ MetaclassTransformInfo, Parameter, Parameters, PropertyInstanceType, Signature, SpecialFormType, StaticMroError, SubclassOfType, Truthiness, Type, TypeContext, TypeMapping, TypeVarVariance, TypedDictModule, UnionBuilder, UnionType, binding_type, + bound_super::BoundSuperType, call::{CallError, CallErrorKind}, callable::{CallableFunctionProvenance, CallableTypeKind}, class::{ @@ -44,7 +49,7 @@ use crate::{ is_implicit_staticmethod, }, generics::Specialization, - infer::{infer_unpack_types, original_class_type}, + infer::{infer_definition_types, infer_unpack_types, original_class_type}, infer_expression_type, inferred_declaration, known_instance::{DeprecatedInstance, FieldInstance}, member::{Member, class_member}, @@ -58,7 +63,7 @@ use crate::{ }; use crate::{attribute_assignments, attribute_declarations}; use ty_python_core::{ - attribute_scopes, + ProgramFile, attribute_scopes, definition::{Definition, DefinitionKind, DefinitionState, TargetKind}, place_table, scope::{Scope, ScopeId}, @@ -173,6 +178,98 @@ impl<'db> StaticClassLiteral<'db> { } } +/// The result of [`StaticClassLiteral::inherited_frozen_dataclass_dispatch`]. +/// +/// See that method for details on how generated frozen-dataclass methods handle fields and +/// non-fields on subclass instances. +#[derive(Clone, Copy)] +pub(crate) enum FrozenDataclassDispatch<'db> { + /// A reachable frozen dataclass rejects assignment to or deletion of one of its fields. + FrozenField, + /// Every reachable frozen method delegates, with lookup resuming after this base. + Delegate(StaticClassLiteral<'db>), +} + +impl<'db> FrozenDataclassDispatch<'db> { + /// Returns the receiver for the next step of assignment or deletion validation. + /// + /// Validation stays on `object_ty` for a frozen field because the generated method rejects the + /// mutation. For a non-field, the generated method calls `super(frozen_base, object_ty)`, so + /// lookup must resume after the last frozen base. For example, assigning `Child().y` for + /// `class Child(Frozen, Later)` uses `super(Frozen, child)` when `y` is not a field of `Frozen`; + /// this preserves a later `__setattr__` or a descriptor for `y`. + pub(crate) fn receiver( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + object_ty: Type<'db>, + ) -> Type<'db> { + match self { + Self::FrozenField => object_ty, + Self::Delegate(frozen_base) => BoundSuperType::build( + db, + env, + Type::ClassLiteral(ClassLiteral::Static(frozen_base)), + object_ty, + ) + .unwrap_or(object_ty), + } + } +} + +/// A method synthesized for a frozen dataclass. +#[derive(Clone, Copy)] +enum FrozenDataclassMethod { + SetAttr, + DelAttr, +} + +impl FrozenDataclassMethod { + /// Returns the frozen-dataclass method for `name`, if it is `__setattr__` or `__delattr__`. + fn from_name(name: &str) -> Option { + match name { + "__setattr__" => Some(Self::SetAttr), + "__delattr__" => Some(Self::DelAttr), + _ => None, + } + } + + /// Returns the corresponding Python special-method name. + const fn name(self) -> &'static str { + match self { + Self::SetAttr => "__setattr__", + Self::DelAttr => "__delattr__", + } + } +} + +/// Fields protected by reachable frozen-dataclass methods. +struct InheritedFrozenDataclassFields<'db> { + names: Box<[Name]>, + /// The final frozen dataclass whose generated method participates in dispatch. + /// + /// For a non-field, mutation validation resumes after this class in the MRO. + last_frozen_base: StaticClassLiteral<'db>, +} + +/// Annotated fields and class-variable declarations collected from one class body. +/// +/// Class variables are not constructor parameters, but they can mask inherited dataclass fields: +/// +/// ```python +/// @dataclass +/// class Child(Base): +/// value: ClassVar[int] +/// required: int +/// ``` +/// +/// Here, `required` is a constructor field and `value` masks an inherited `Base.value` field. +#[derive(Debug, Default, PartialEq, Eq, get_size2::GetSize, salsa::SalsaValue)] +struct OwnClassFields<'db> { + fields: FxIndexMap>, + class_variables: Box<[Name]>, +} + #[salsa::tracked] impl<'db> StaticClassLiteral<'db> { /// Return `true` if this class represents `known_class` @@ -189,7 +286,7 @@ impl<'db> StaticClassLiteral<'db> { /// /// When the base namedtuple's fields were determined dynamically (e.g., from a variable), /// we can't synthesize precise method signatures and should fall back to `NamedTupleFallback`. - pub(crate) fn namedtuple_base_has_unknown_fields(self, db: &'db dyn Db) -> bool { + fn namedtuple_base_has_unknown_fields(self, db: &'db dyn Db) -> bool { self.explicit_bases(db).iter().any(|base| match base { Type::ClassLiteral(ClassLiteral::DynamicNamedTuple(namedtuple)) => { !namedtuple.has_known_fields(db) @@ -283,7 +380,7 @@ impl<'db> StaticClassLiteral<'db> { /// /// Note: We use direct scope lookups here to avoid infinite recursion /// through `own_class_member` -> `own_synthesized_member`. - pub(super) fn total_ordering_root_method( + fn total_ordering_root_method( self, db: &'db dyn Db, specialization: Option>, @@ -369,11 +466,12 @@ impl<'db> StaticClassLiteral<'db> { )] fn pep695_generic_context_inner(self, db: &'db dyn Db) -> Option> { let scope = self.body_scope(db); - let file = scope.file(db); - let parsed = parsed_module(db, file).load(db); + let program_file = scope.program_file(db); + let python_file = program_file.python_file(db); + let parsed = parsed_module(db, python_file).load(db); let class_def_node = scope.node(db).expect_class().node(&parsed); if let Some(type_params) = class_def_node.type_params.as_ref() { - let index = semantic_index(db, file); + let index = semantic_index(db, program_file); let definition = index.expect_single_definition(class_def_node); return Some(GenericContext::from_type_params( db, @@ -389,7 +487,7 @@ impl<'db> StaticClassLiteral<'db> { // is also injected as a base in `explicit_bases`, giving the variant the // matching subtype relationship) if class_def_node.is_enum_variant() { - let index = semantic_index(db, file); + let index = semantic_index(db, scope.program_file(db)); return index .ancestor_scopes(scope.file_scope_id(db)) .skip(1) @@ -450,13 +548,17 @@ impl<'db> StaticClassLiteral<'db> { self, db: &'db dyn Db, ) -> FxIndexSet> { - #[derive(Default)] - struct CollectTypeVars<'db> { + struct CollectTypeVars<'a, 'db> { + env: &'a ProgramEnvironment<'db>, typevars: RefCell>>, recursion_guard: TypeCollector<'db>, } - impl<'db> TypeVisitor<'db> for CollectTypeVars<'db> { + impl<'db> TypeVisitor<'db> for CollectTypeVars<'_, 'db> { + fn program_environment(&self) -> &ProgramEnvironment<'db> { + self.env + } + fn should_visit_lazy_type_attributes(&self) -> bool { false } @@ -482,7 +584,12 @@ impl<'db> StaticClassLiteral<'db> { } } - let visitor = CollectTypeVars::default(); + let env = ProgramEnvironment::from_scope(self.body_scope(db)); + let visitor = CollectTypeVars { + env: &env, + typevars: RefCell::default(), + recursion_guard: TypeCollector::default(), + }; for base in self.explicit_bases(db) { visitor.visit_type(db, *base); } @@ -498,6 +605,14 @@ impl<'db> StaticClassLiteral<'db> { self.body_scope(db).file(db) } + pub(crate) fn python_file(self, db: &'db dyn Db) -> PythonFile<'db> { + self.body_scope(db).python_file(db) + } + + pub(crate) fn program_file(self, db: &'db dyn Db) -> ProgramFile<'db> { + self.body_scope(db).program_file(db) + } + /// Return the original [`ast::StmtClassDef`] node associated with this class /// /// ## Note @@ -509,7 +624,7 @@ impl<'db> StaticClassLiteral<'db> { 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)); + let index = semantic_index(db, body_scope.program_file(db)); index.expect_single_definition(body_scope.node(db).expect_class()) } @@ -523,7 +638,7 @@ impl<'db> StaticClassLiteral<'db> { /// Tracked because it reads the class's AST node. #[salsa::tracked(returns(deref), heap_size = ruff_memory_usage::heap_size)] pub(crate) fn valueless_declarations(self, db: &'db dyn Db) -> Box<[Name]> { - let module = parsed_module(db, self.file(db)).load(db); + let module = parsed_module(db, self.program_file(db).python_file(db)).load(db); self.node(db, &module) .body .iter() @@ -564,12 +679,14 @@ impl<'db> StaticClassLiteral<'db> { pub(crate) fn top_materialization(self, db: &'db dyn Db) -> ClassType<'db> { self.apply_specialization(db, |generic_context| { + let env = ProgramEnvironment::from_program(generic_context.program(db)); generic_context - .default_specialization(db, self.known(db)) + .unknown_specialization(db, self.known(db)) .materialize_impl( db, + &env, MaterializationKind::Top, - &ApplyTypeMappingVisitor::default(), + &ApplyTypeMappingVisitor::new(&env), ) }) } @@ -588,7 +705,7 @@ impl<'db> StaticClassLiteral<'db> { /// 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) + generic_context.unknown_specialization(db, self.known(db)) }) } @@ -618,17 +735,19 @@ impl<'db> StaticClassLiteral<'db> { db: &'db dyn Db, class: StaticClassLiteral<'db>, ) -> Box<[Type<'db>]> { + let env = &ProgramEnvironment::from_file(class.program_file(db)); tracing::trace!( "StaticClassLiteral::explicit_bases_query: {}", class.name(db) ); - let module = parsed_module(db, class.file(db)).load(db); + let program_file = class.program_file(db); + let python_file = program_file.python_file(db); + let module = parsed_module(db, python_file).load(db); let class_stmt = class.node(db, &module); let class_definition = - semantic_index(db, class.file(db)).expect_single_definition(class_stmt); - + semantic_index(db, program_file).expect_single_definition(class_stmt); let mut bases: Vec> = expanded_class_base_entries(db, class.known(db), class_stmt, class_definition) .into_iter() @@ -641,7 +760,7 @@ impl<'db> StaticClassLiteral<'db> { // transpile step. `has_injected_base` lists every form this applies to and is // what keeps `HAS_EXPLICIT_BASES` in step with it if class_stmt.has_synthetic_marker("enum_class") { - bases.push(KnownClass::Enum.to_class_literal(db)); + bases.push(KnownClass::Enum.to_class_literal(db, env)); } if class_stmt.has_synthetic_marker("protocol_class") { bases.push(Type::SpecialForm(crate::types::SpecialFormType::Protocol)); @@ -653,7 +772,7 @@ impl<'db> StaticClassLiteral<'db> { // enum. payload-bearing enums lower to a sealed class hierarchy // instead and must not gain the base if class_stmt.is_based_enum() && class_stmt.is_all_unit_enum() { - bases.push(KnownClass::Enum.to_class_literal(db)); + bases.push(KnownClass::Enum.to_class_literal(db, env)); } // a based-enum variant subclasses its enum: so methods/classmethods/ @@ -662,7 +781,7 @@ impl<'db> StaticClassLiteral<'db> { // specialized by its own typevars (`Tree[T]`) so a generic variant stays // generic in the enum's type parameters if class_stmt.is_enum_variant() { - let index = semantic_index(db, class.file(db)); + let index = semantic_index(db, class.program_file(db)); if let Some(enum_literal) = index .ancestor_scopes(class.body_scope(db).file_scope_id(db)) .skip(1) @@ -710,11 +829,12 @@ impl<'db> StaticClassLiteral<'db> { /// Iterate over this class's explicit bases, resolving them in the same way as MRO /// construction, filtering out any bases that are not fully static class objects. fn fully_static_explicit_bases(self, db: &'db dyn Db) -> impl Iterator> { + let env = ProgramEnvironment::from_scope(self.body_scope(db)); self.explicit_bases(db) .iter() .copied() .filter_map(move |ty| { - ClassBase::try_from_type(db, ty, Some(ClassLiteral::Static(self))) + ClassBase::try_from_type(db, &env, ty, Some(ClassLiteral::Static(self))) .and_then(ClassBase::into_class) }) } @@ -759,9 +879,12 @@ impl<'db> StaticClassLiteral<'db> { #[salsa::tracked(returns(deref), cycle_initial=|_, _, _| Box::default(), heap_size=ruff_memory_usage::heap_size)] fn decorators_inner(self, db: &'db dyn Db) -> Box<[Type<'db>]> { + let env = &ProgramEnvironment::from_file(self.program_file(db)); tracing::trace!("StaticClassLiteral::decorators: {}", self.name(db)); - let module = parsed_module(db, self.file(db)).load(db); + let program_file = self.program_file(db); + let python_file = program_file.python_file(db); + let module = parsed_module(db, python_file).load(db); let class_stmt = self.node(db, &module); if class_stmt.decorator_list.is_empty() { @@ -769,7 +892,7 @@ impl<'db> StaticClassLiteral<'db> { } let class_definition = - semantic_index(db, self.file(db)).expect_single_definition(class_stmt); + semantic_index(db, self.program_file(db)).expect_single_definition(class_stmt); class_stmt .decorator_list @@ -777,6 +900,7 @@ impl<'db> StaticClassLiteral<'db> { .map(|decorator_node| { if let Some(target) = crate::types::function::synthetic_decorator_target_type( db, + env, self.file(db), decorator_node, ) { @@ -800,10 +924,12 @@ impl<'db> StaticClassLiteral<'db> { /// Iterate through the decorators on this class, returning the index of the first one /// that is either `@dataclass` or `@dataclass(...)`. pub(crate) fn find_dataclass_decorator_position(self, db: &'db dyn Db) -> Option { - let module = parsed_module(db, self.file(db)).load(db); + let program_file = self.program_file(db); + let python_file = program_file.python_file(db); + let module = parsed_module(db, python_file).load(db); let class_stmt = self.node(db, &module); let class_definition = - semantic_index(db, self.file(db)).expect_single_definition(class_stmt); + semantic_index(db, program_file).expect_single_definition(class_stmt); class_stmt.decorator_list.iter().position(|decorator| { let decorator_callable = decorator @@ -822,7 +948,7 @@ impl<'db> StaticClassLiteral<'db> { /// what the runtime transform emits (it only sees same-file subclasses). #[salsa::tracked(returns(deref), heap_size=ruff_memory_usage::heap_size)] pub(crate) fn sealed_members(self, db: &'db dyn Db) -> Box<[StaticClassLiteral<'db>]> { - let global = ty_python_core::global_scope(db, self.file(db)); + let global = ty_python_core::global_scope(db, self.program_file(db)); let mut members = Vec::new(); for symbol in place_table(db, global).symbols() { let Some(Type::ClassLiteral(ClassLiteral::Static(candidate))) = @@ -866,23 +992,51 @@ impl<'db> StaticClassLiteral<'db> { /// attribute on a class at runtime. /// /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order + pub(in crate::types) fn try_mro( + self, + db: &'db dyn Db, + specialization: Option>, + ) -> Result<&'db Mro<'db>, &'db StaticMroError<'db>> { + match specialization { + None => self.try_mro_unspecialized(db), + Some(specialization) => self.try_mro_specialized(db, specialization), + } + } + + #[salsa::tracked( + returns(as_ref), + cycle_initial=|db, _, self_: StaticClassLiteral<'db>| { + let env = ProgramEnvironment::from_scope(self_.body_scope(db)); + Err(StaticMroError::cycle( + db, &env, + self_.apply_optional_specialization(db, None), + )) + }, + heap_size=ruff_memory_usage::heap_size + )] + fn try_mro_unspecialized(self, db: &'db dyn Db) -> Result, StaticMroError<'db>> { + tracing::trace!("StaticClassLiteral::try_mro: {}", self.name(db)); + Mro::of_static_class(db, self, None) + } + #[salsa::tracked( returns(as_ref), cycle_initial=|db, _, self_: StaticClassLiteral<'db>, specialization| { + let env = ProgramEnvironment::from_scope(self_.body_scope(db)); Err(StaticMroError::cycle( - db, - self_.apply_optional_specialization(db, specialization), + db, &env, + self_.apply_optional_specialization(db, Some(specialization)), )) }, heap_size=ruff_memory_usage::heap_size )] - pub(crate) fn try_mro( + fn try_mro_specialized( self, db: &'db dyn Db, - specialization: Option>, + specialization: Specialization<'db>, ) -> Result, StaticMroError<'db>> { tracing::trace!("StaticClassLiteral::try_mro: {}", self.name(db)); - Mro::of_static_class(db, self, specialization) + Mro::of_static_class(db, self, Some(specialization)) } /// Iterate over the [method resolution order] ("MRO") of the class. @@ -914,7 +1068,51 @@ impl<'db> StaticClassLiteral<'db> { .contains(&ClassBase::Class(other)) } - /// Return the properties that affect how instances of this class are represented. + /// Return whether this class defines its own non-default `__getattribute__`. + /// + /// An explicit metaclass can install the method even when the class body does not define it: + /// + /// ```python + /// def interceptor(self, name): ... + /// + /// class Meta(type): + /// def __init__(cls, *args): + /// cls.__getattribute__ = interceptor + /// + /// class Example(metaclass=Meta): ... + /// ``` + fn has_own_custom_getattribute(self, db: &'db dyn Db) -> bool { + if matches!(self.known(db), Some(KnownClass::Object | KnownClass::Type)) { + return false; + } + + if place_table(db, self.body_scope(db)) + .symbol_id("__getattribute__") + .is_some() + { + return true; + } + + if !self.has_explicit_metaclass(db) { + return false; + } + + let Some(metaclass) = self.metaclass(db).to_class_type(db) else { + return true; + }; + + metaclass.iter_mro(db).any(|base| match base { + ClassBase::Any | ClassBase::Dynamic(_) | ClassBase::Divergent(_) => true, + ClassBase::Class(base) => base.static_class_literal(db).is_none_or(|(base, _)| { + implicit_attribute_names(db, base.body_scope(db)) + .binary_search(&Name::new_static("__getattribute__")) + .is_ok() + }), + ClassBase::Generic | ClassBase::Protocol | ClassBase::TypedDict(_) => false, + }) + } + + /// Return the properties shared by all instances of this class. pub(super) fn instance_flags(self, db: &'db dyn Db) -> ClassInstanceFlags { #[salsa::tracked( returns(copy), @@ -927,28 +1125,45 @@ impl<'db> StaticClassLiteral<'db> { ) -> ClassInstanceFlags { let mut flags = ClassInstanceFlags::empty(); for base in class.iter_mro(db, None) { - if base.is_typed_dict() { - flags.insert(ClassInstanceFlags::TYPED_DICT); - } - if base.is_explicit_any_base() { - flags.insert(ClassInstanceFlags::INHERITS_FROM_EXPLICIT_ANY); + match base { + ClassBase::Any => flags.insert( + ClassInstanceFlags::INHERITS_FROM_EXPLICIT_ANY + | ClassInstanceFlags::HAS_DYNAMIC_GETATTRIBUTE, + ), + ClassBase::Dynamic(_) | ClassBase::Divergent(_) => { + flags.insert(ClassInstanceFlags::HAS_DYNAMIC_GETATTRIBUTE); + } + ClassBase::TypedDict(_) => flags.insert(ClassInstanceFlags::TYPED_DICT), + ClassBase::Class(class) + if class + .static_class_literal(db) + .is_none_or(|(class, _)| class.has_own_custom_getattribute(db)) => + { + flags.insert(ClassInstanceFlags::HAS_CUSTOM_GETATTRIBUTE); + } + ClassBase::Class(_) | ClassBase::Generic | ClassBase::Protocol => {} } } flags } - if let Some(known) = self.known(db) { - return if known.is_typed_dict_subclass() { + let mut flags = if let Some(known) = self.known(db) { + if known.is_typed_dict_subclass() { ClassInstanceFlags::TYPED_DICT } else { ClassInstanceFlags::empty() - }; - } + } + } else if self.has_explicit_bases(db) { + return instance_flags_inner(db, self); + } else { + ClassInstanceFlags::empty() + }; - if !self.has_explicit_bases(db) { - return ClassInstanceFlags::empty(); - } - instance_flags_inner(db, self) + flags.set( + ClassInstanceFlags::HAS_CUSTOM_GETATTRIBUTE, + self.has_own_custom_getattribute(db), + ); + flags } /// Return the module defining the `TypedDict` base of this class. @@ -961,8 +1176,14 @@ impl<'db> StaticClassLiteral<'db> { /// Return `true` if this class constitutes a typed dict specification (inherits from /// `typing.TypedDict` or `typing_extensions.TypedDict`, either directly or indirectly). pub fn is_typed_dict(self, db: &'db dyn Db) -> bool { - self.instance_flags(db) - .contains(ClassInstanceFlags::TYPED_DICT) + if let Some(known) = self.known(db) { + return known.is_typed_dict_subclass(); + } + + self.has_explicit_bases(db) + && self + .instance_flags(db) + .contains(ClassInstanceFlags::TYPED_DICT) } /// Return `true` if this class is, or inherits from, a `NamedTuple` (inherits from @@ -988,7 +1209,7 @@ impl<'db> StaticClassLiteral<'db> { return None; } - let module = parsed_module(db, self.file(db)).load(db); + let module = parsed_module(db, self.python_file(db)).load(db); let class_stmt = self.node(db, &module); Some(typed_dict_params_from_class_def(class_stmt)) } @@ -1013,7 +1234,7 @@ impl<'db> StaticClassLiteral<'db> { if let Some(transformer_params) = transformer_params.as_mut() && let Some(class_def) = self.definition(db).kind(db).as_class() { - let module = parsed_module(db, self.file(db)).load(db); + let module = parsed_module(db, self.python_file(db)).load(db); if let Some(arguments) = &class_def.node(&module).arguments { let mut flags = transformer_params.flags(db); @@ -1156,6 +1377,9 @@ impl<'db> StaticClassLiteral<'db> { db: &'db dyn Db, class: StaticClassLiteral<'db>, ) -> Result<(Type<'db>, Option>), MetaclassError<'db>> { + let program_file = class.program_file(db); + let python_file = program_file.python_file(db); + let env = ProgramEnvironment::from_file(program_file); tracing::trace!("StaticClassLiteral::try_metaclass: {}", class.name(db)); // Identify the class's own metaclass (or take the first base class's metaclass). @@ -1171,7 +1395,7 @@ impl<'db> StaticClassLiteral<'db> { return Ok((SubclassOfType::subclass_of_unknown(), None)); } - let module = parsed_module(db, class.file(db)).load(db); + let module = parsed_module(db, python_file).load(db); let explicit_metaclass = class.explicit_metaclass(db, &module); @@ -1183,7 +1407,7 @@ impl<'db> StaticClassLiteral<'db> { .specialization(db) .types(db) .iter() - .any(|ty| ty.has_typevar_or_typevar_instance(db)); + .any(|ty| ty.has_typevar_or_typevar_instance(db, &env)); if specialization_has_typevars { return Err(MetaclassError { kind: MetaclassErrorKind::GenericMetaclass, @@ -1203,7 +1427,7 @@ impl<'db> StaticClassLiteral<'db> { .unwrap_or(class); (base_class.metaclass(db), base_class_literal) } else { - (KnownClass::Type.to_class_literal(db), class) + (KnownClass::Type.to_class_literal(db, &env), class) }; let mut candidate = if let Some(metaclass_ty) = metaclass.to_class_type(db) { @@ -1213,15 +1437,18 @@ impl<'db> StaticClassLiteral<'db> { } } else { let name = Type::string_literal(db, class.name(db)); - let bases = Type::heterogeneous_tuple(db, class.explicit_bases(db)); - let namespace = KnownClass::Dict - .to_specialized_instance(db, &[KnownClass::Str.to_instance(db), Type::any()]); + let bases = Type::heterogeneous_tuple(db, &env, class.explicit_bases(db)); + let namespace = KnownClass::Dict.to_specialized_instance( + db, + &env, + &[KnownClass::Str.to_instance(db, &env), Type::any()], + ); // TODO: Other keyword arguments? let arguments = CallArguments::positional([name, bases, namespace]); - let return_ty_result = match metaclass.try_call(db, &arguments) { - Ok(bindings) => Ok(bindings.return_type(db)), + let return_ty_result = match metaclass.try_call(db, &env, &arguments) { + Ok(bindings) => Ok(bindings.return_type(db, &env)), Err(CallError(CallErrorKind::NotCallable, bindings)) => Err(MetaclassError { kind: MetaclassErrorKind::NotCallable(bindings.callable_type()), @@ -1230,7 +1457,7 @@ impl<'db> StaticClassLiteral<'db> { // 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)) + Ok(bindings.return_type(db, &env)) } Err(CallError(CallErrorKind::PossiblyNotCallable, _)) => Err(MetaclassError { @@ -1238,7 +1465,7 @@ impl<'db> StaticClassLiteral<'db> { }), }; - return return_ty_result.map(|ty| (ty.to_meta_type(db), None)); + return return_ty_result.map(|ty| (ty.to_meta_type(db, &env), None)); }; // Reconcile all base classes' metaclasses with the candidate metaclass. @@ -1257,14 +1484,14 @@ impl<'db> StaticClassLiteral<'db> { .static_class_literal(db) .map(|(lit, _)| lit) .unwrap_or(class); - if metaclass.is_subclass_of(db, candidate.metaclass) { + if metaclass.is_subclass_of(db, &env, candidate.metaclass) { candidate = MetaclassCandidate { metaclass, explicit_metaclass_of: base_class_literal, }; continue; } - if candidate.metaclass.is_subclass_of(db, metaclass) { + if candidate.metaclass.is_subclass_of(db, &env, metaclass) { continue; } return Err(MetaclassError { @@ -1293,7 +1520,8 @@ impl<'db> StaticClassLiteral<'db> { } if !self.has_explicit_bases(db) && !self.has_explicit_metaclass(db) { - return Ok((KnownClass::Type.to_class_literal(db), None)); + let env = ProgramEnvironment::from_scope(self.body_scope(db)); + return Ok((KnownClass::Type.to_class_literal(db, &env), None)); } try_metaclass_inner(db, self) } @@ -1306,30 +1534,37 @@ impl<'db> StaticClassLiteral<'db> { pub(super) fn class_member( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, policy: MemberLookupPolicy, ) -> PlaceAndQualifiers<'db> { - self.class_member_inner(db, None, name, policy) + self.class_member_inner(db, env, None, name, policy) } pub(super) fn class_member_inner( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, specialization: Option>, name: &str, policy: MemberLookupPolicy, ) -> PlaceAndQualifiers<'db> { - self.class_member_from_mro(db, name, policy, self.iter_mro(db, specialization)) + self.class_member_from_mro(db, env, name, policy, self.iter_mro(db, specialization)) } pub(crate) fn class_member_from_mro( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, policy: MemberLookupPolicy, mro_iter: impl Iterator>, ) -> PlaceAndQualifiers<'db> { - fn into_function_like_callable<'d>(db: &'d dyn Db, ty: Type<'d>) -> Type<'d> { + fn into_function_like_callable<'d>( + db: &'d dyn Db, + env: &ProgramEnvironment<'d>, + ty: Type<'d>, + ) -> Type<'d> { match ty { Type::Callable(callable_ty) if callable_ty.is_regular(db) @@ -1337,16 +1572,17 @@ impl<'db> StaticClassLiteral<'db> { { Type::Callable(callable_ty.into_function_like(db)) } - Type::Union(union) => { - union.map(db, |element| into_function_like_callable(db, *element)) - } - Type::Intersection(intersection) => intersection - .map_positive(db, |element| into_function_like_callable(db, *element)), + Type::Union(union) => union.map(db, env, |element| { + into_function_like_callable(db, env, *element) + }), + Type::Intersection(intersection) => intersection.map_positive(db, env, |element| { + into_function_like_callable(db, env, *element) + }), _ => ty, } } - let result = MroLookup::new(db, mro_iter).class_member( + let result = MroLookup::new(db, env, mro_iter).class_member( name, policy, self.inherited_generic_context(db), @@ -1354,16 +1590,21 @@ impl<'db> StaticClassLiteral<'db> { ); let mut member = match result { - ClassMemberResult::Done(result) => result.finalize(db), - ClassMemberResult::TypedDict(module) => { - typed_dict_class_member(db, self.identity_specialization(db), module, policy, name) - } + ClassMemberResult::Done(result) => result.finalize(db, env), + ClassMemberResult::TypedDict(module) => typed_dict_class_member( + db, + env, + self.identity_specialization(db), + module, + policy, + name, + ), }; // We generally treat dunder attributes with `Callable` types as function-like callables. // See `callables_as_descriptors.md` for more details. if name.starts_with("__") && name.ends_with("__") { - member = member.map_type(|ty| into_function_like_callable(db, ty)); + member = member.map_type(|ty| into_function_like_callable(db, env, ty)); } member @@ -1378,11 +1619,16 @@ impl<'db> StaticClassLiteral<'db> { pub(super) fn own_class_member( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, inherited_generic_context: Option>, specialization: Option>, name: &str, ) -> Member<'db> { - fn into_dunder_paramspec_callable<'d>(db: &'d dyn Db, ty: Type<'d>) -> Type<'d> { + fn into_dunder_paramspec_callable<'d>( + db: &'d dyn Db, + env: &ProgramEnvironment<'d>, + ty: Type<'d>, + ) -> Type<'d> { match ty { Type::Callable(callable_ty) if callable_ty.is_regular(db) @@ -1390,11 +1636,12 @@ impl<'db> StaticClassLiteral<'db> { { Type::Callable(callable_ty.into_dunder_paramspec(db)) } - Type::Union(union) => { - union.map(db, |element| into_dunder_paramspec_callable(db, *element)) - } - Type::Intersection(intersection) => intersection - .map_positive(db, |element| into_dunder_paramspec_callable(db, *element)), + Type::Union(union) => union.map(db, env, |element| { + into_dunder_paramspec_callable(db, env, *element) + }), + Type::Intersection(intersection) => intersection.map_positive(db, env, |element| { + into_dunder_paramspec_callable(db, env, *element) + }), _ => ty, } } @@ -1408,9 +1655,10 @@ impl<'db> StaticClassLiteral<'db> { return Member { inner: Place::declared(KnownClass::Dict.to_specialized_instance( db, + env, &[ - KnownClass::Str.to_instance(db), - KnownClass::Field.to_specialized_instance(db, &[Type::any()]), + KnownClass::Str.to_instance(db, env), + KnownClass::Field.to_specialized_instance(db, env, &[Type::any()]), ], )) .with_qualifiers(TypeQualifiers::CLASS_VAR), @@ -1443,7 +1691,7 @@ impl<'db> StaticClassLiteral<'db> { let body_scope = self.body_scope(db); let member = class_member(db, body_scope, name).map_type(|ty| { let ty = if name.starts_with("__") && name.ends_with("__") { - into_dunder_paramspec_callable(db, ty) + into_dunder_paramspec_callable(db, env, ty) } else { ty }; @@ -1473,9 +1721,13 @@ impl<'db> StaticClassLiteral<'db> { }); if member.is_undefined() { - if let Some(synthesized_member) = - self.own_synthesized_member(db, specialization, inherited_generic_context, name) - { + if let Some(synthesized_member) = self.own_synthesized_member( + db, + env, + specialization, + inherited_generic_context, + name, + ) { return Member::definitely_declared(synthesized_member); } // The symbol was not found in the class scope. It might still be implicitly defined in `@classmethod`s. @@ -1501,8 +1753,8 @@ impl<'db> StaticClassLiteral<'db> { // At runtime, the enum metaclass unwraps the value, so accessing the attribute // returns the inner value, not the `nonmember` wrapper. if let Some(ty) = member.inner.place.raw_type() - && let Some(value_ty) = try_unwrap_nonmember_value(db, ty) - && is_enum_class_by_inheritance(db, self) + && let Some(value_ty) = try_unwrap_nonmember_value(db, env, ty) + && is_enum_class_by_inheritance(db, env, self) { return Member::definitely_declared(value_ty); } @@ -1515,6 +1767,7 @@ impl<'db> StaticClassLiteral<'db> { pub(crate) fn own_synthesized_member( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, specialization: Option>, inherited_generic_context: Option>, name: &str, @@ -1523,9 +1776,13 @@ impl<'db> StaticClassLiteral<'db> { // its same-module direct subclasses (matching the runtime transform). if name == "__sealed_members__" && self.is_sealed(db) { let elements = self.sealed_members(db).iter().map(|member| { - SubclassOfType::from(db, ClassLiteral::Static(*member).default_specialization(db)) + SubclassOfType::from( + db, + env, + ClassLiteral::Static(*member).default_specialization(db), + ) }); - return Some(Type::heterogeneous_tuple(db, elements)); + return Some(Type::heterogeneous_tuple(db, env, elements)); } // Handle `@functools.total_ordering`: synthesize comparison methods @@ -1551,9 +1808,9 @@ impl<'db> StaticClassLiteral<'db> { }) && self.has_ordering_method_in_mro(db, specialization) && let Some(root_method_ty) = self.total_ordering_root_method(db, specialization) - && let Some(callables) = root_method_ty.try_upcast_to_callable(db) + && let Some(callables) = root_method_ty.try_upcast_to_callable(db, env) { - let bool_ty = KnownClass::Bool.to_instance(db); + let bool_ty = KnownClass::Bool.to_instance(db, env); let synthesized_callables = callables.map(|callable| { let signatures = CallableSignature::from_overloads( callable.signatures(db).iter().map(|signature| { @@ -1562,7 +1819,7 @@ impl<'db> StaticClassLiteral<'db> { // def __gt__(self, other): return not (self == other or self < other) // If `__lt__` returns `int`, then `__gt__` could return `int | bool`. let return_ty = - UnionType::from_two_elements(db, signature.return_ty, bool_ty); + UnionType::from_two_elements(db, env, signature.return_ty, bool_ty); Signature::new_generic( signature.generic_context, signature.parameters().clone(), @@ -1578,18 +1835,19 @@ impl<'db> StaticClassLiteral<'db> { ) }); - return Some(synthesized_callables.into_type(db)); + return Some(synthesized_callables.into_type(db, env)); } // An ordinary subclass of a frozen dataclass is not itself dataclass-like, so the // `CodeGeneratorKind::from_class` check below would return `None` before dataclass-like // synthesis runs. Still, an instance of such a subclass inherits the frozen dataclass's - // generated `__setattr__`, which rejects writes to frozen base fields. - if name == "__setattr__" - && let Some(synthesized_setattr) = - self.own_frozen_dataclass_subclass_setattr(db, specialization) + // generated `__setattr__` and `__delattr__`, which reject assignments and deletions of + // frozen base fields. + if let Some(method) = FrozenDataclassMethod::from_name(name) + && let Some(synthesized_method) = + self.own_frozen_dataclass_subclass_method(db, env, specialization, method) { - return Some(synthesized_setattr); + return Some(synthesized_method); } let field_policy = CodeGeneratorKind::from_class(db, self.into())?; @@ -1603,8 +1861,11 @@ impl<'db> StaticClassLiteral<'db> { || (field_policy.is_pydantic() && pydantic::constructor_fields_are_optional(db, self))); - let instance_ty = - Type::instance(db, self.apply_optional_specialization(db, specialization)); + let instance_ty = Type::instance( + db, + env, + self.apply_optional_specialization(db, specialization), + ); let signature_from_fields = |mut parameters: Vec<_>, return_ty: Type<'db>| { if name == "__init__" && field_policy.is_pydantic() { @@ -1666,8 +1927,9 @@ impl<'db> StaticClassLiteral<'db> { }; let mut field_ty = field.declared_ty; - if name == "__init__" && !init { - // Skip fields with `init=False` + if !init && (name == "__init__" || field_policy.is_pydantic()) { + // Fields with `init=False` are excluded from constructors. Pydantic's private + // and internal fields are also excluded from replacement. continue; } @@ -1678,7 +1940,7 @@ impl<'db> StaticClassLiteral<'db> { continue; } - let dunder_set = field_ty.class_member(db, "__set__"); + let dunder_set = field_ty.class_member(db, env, "__set__"); if let Place::Defined(DefinedPlace { ty: dunder_set, definedness: Definedness::AlwaysDefined, @@ -1700,8 +1962,8 @@ impl<'db> StaticClassLiteral<'db> { // // We union parameter types across overloads of a single callable, intersect // callable bindings inside an intersection element, and union outer elements. - field_ty = dunder_set.bindings(db).map_types(db, |binding| { - let mut value_types = UnionBuilder::new(db); + field_ty = dunder_set.bindings(db, env).map_types(db, env, |binding| { + let mut value_types = UnionBuilder::new(db, env); let mut has_value_type = false; for overload in binding { if let Some(value_param) = @@ -1724,8 +1986,9 @@ impl<'db> StaticClassLiteral<'db> { if let Some(ref mut default_ty) = default_ty { *default_ty = default_ty - .try_call_dunder_get(db, None, Type::from(self)) - .map(|(return_ty, _)| return_ty) + .try_call_dunder_get(db, env, None, Type::from(self)) + .unwrap_or_else(|error| Some(error.fallback())) + .map(|result| result.return_type) .unwrap_or_else(Type::unknown); } } @@ -1805,6 +2068,9 @@ impl<'db> StaticClassLiteral<'db> { } (false, false) => {} } + } else if name == "__replace__" && field_policy.is_pydantic() { + // Pydantic updates model fields by name rather than by initialization alias. + add_parameter_with_name(field_name.clone(), default_ty); } else { // Use the alias name if provided, otherwise use the field name. let parameter_name = @@ -1818,6 +2084,7 @@ impl<'db> StaticClassLiteral<'db> { if name == "__init__" && field_policy == CodeGeneratorKind::Django { for (parameter_name, parameter_ty) in django::extra_constructor_parameters( db, + env, self.fields(db, specialization, field_policy), ) { if parameters @@ -1891,7 +2158,7 @@ impl<'db> StaticClassLiteral<'db> { let self_parameter = Parameter::positional_or_keyword(Name::new_static("self")) // TODO: could be `Self`. .with_annotated_type(instance_ty); - signature_from_fields(vec![self_parameter], Type::none(db)) + signature_from_fields(vec![self_parameter], Type::none(db, env)) } (CodeGeneratorKind::Django, name) => { // dunder lookups happen while inferring the very definitions the @@ -1904,6 +2171,7 @@ impl<'db> StaticClassLiteral<'db> { } django::synthesized_model_attribute( db, + env, self, self.fields(db, specialization, field_policy), name, @@ -1917,14 +2185,15 @@ impl<'db> StaticClassLiteral<'db> { // When the namedtuple base has unknown fields, fall back to NamedTupleFallback // which has generic signatures that accept any arguments. KnownClass::NamedTupleFallback - .to_class_literal(db) + .to_class_literal(db, env) .as_class_literal()? .as_static()? - .own_class_member(db, inherited_generic_context, None, name) + .own_class_member(db, env, inherited_generic_context, None, name) .ignore_possibly_undefined() .map(|ty| { ty.apply_type_mapping( db, + env, &TypeMapping::ReplaceSelf { new_upper_bound: instance_ty, }, @@ -1951,6 +2220,7 @@ impl<'db> StaticClassLiteral<'db> { }); synthesize_namedtuple_class_member( db, + env, name, instance_ty, fields_iter, @@ -1974,7 +2244,7 @@ impl<'db> StaticClassLiteral<'db> { // TODO: could be `Self`. .with_annotated_type(instance_ty), ]), - KnownClass::Bool.to_instance(db), + KnownClass::Bool.to_instance(db, env), ); Some(Type::function_like_callable(db, signature)) @@ -1991,12 +2261,12 @@ impl<'db> StaticClassLiteral<'db> { "self", )) .with_annotated_type(instance_ty)]), - KnownClass::Int.to_instance(db), + KnownClass::Int.to_instance(db, env), ); Some(Type::function_like_callable(db, signature)) } else if eq && !frozen { - Some(Type::none(db)) + Some(Type::none(db, env)) } else { // No `__hash__` is generated, fall back to `object.__hash__` None @@ -2008,8 +2278,7 @@ impl<'db> StaticClassLiteral<'db> { // the lowering emits a dataclass at basedpython's 3.10 floor whatever // python version the project happens to advertise (field_policy @ CodeGeneratorKind::DataclassLike(_), "__match_args__") - if Program::get(db).python_version(db) >= PythonVersion::PY310 - || self.is_enum_variant(db) => + if env.python_version(db) >= PythonVersion::PY310 || self.is_enum_variant(db) => { if !self.has_dataclass_param(db, field_policy, DataclassFlags::MATCH_ARGS) { return None; @@ -2029,10 +2298,10 @@ impl<'db> StaticClassLiteral<'db> { } }) .map(|(name, _)| Type::string_literal(db, name)); - Some(Type::heterogeneous_tuple(db, match_args)) + Some(Type::heterogeneous_tuple(db, env, match_args)) } (field_policy @ CodeGeneratorKind::DataclassLike(_), "__weakref__") - if Program::get(db).python_version(db) >= PythonVersion::PY311 => + if env.python_version(db) >= PythonVersion::PY311 => { if !self.has_dataclass_param(db, field_policy, DataclassFlags::WEAKREF_SLOT) || !self.has_dataclass_param(db, field_policy, DataclassFlags::SLOTS) @@ -2044,23 +2313,26 @@ impl<'db> StaticClassLiteral<'db> { // model it precisely. Some(UnionType::from_two_elements( db, + env, Type::any(), - Type::none(db), + Type::none(db, env), )) } (CodeGeneratorKind::NamedTuple, name) if name != "__init__" => { KnownClass::NamedTupleFallback - .to_class_literal(db) + .to_class_literal(db, env) .as_class_literal()? .as_static()? - .own_class_member(db, self.inherited_generic_context(db), None, name) + .own_class_member(db, env, self.inherited_generic_context(db), None, name) .ignore_possibly_undefined() .map(|ty| { ty.apply_type_mapping( db, + env, &TypeMapping::ReplaceSelf { new_upper_bound: determine_upper_bound( db, + env, ClassLiteral::Static(self), |base| { base.into_class() @@ -2072,9 +2344,10 @@ impl<'db> StaticClassLiteral<'db> { ) }) } - (CodeGeneratorKind::DataclassLike(_), "__replace__") - if Program::get(db).python_version(db) >= PythonVersion::PY313 => - { + ( + CodeGeneratorKind::DataclassLike(_) | CodeGeneratorKind::Pydantic(_), + "__replace__", + ) if env.python_version(db) >= PythonVersion::PY313 => { let self_parameter = Parameter::positional_or_keyword(Name::new_static("self")) .with_annotated_type(instance_ty); @@ -2134,8 +2407,8 @@ impl<'db> StaticClassLiteral<'db> { || self.inherits_frozen_model_setattr(db, specialization) { let overloads = frozen_overloads.into_iter().chain([setattr_signature( - KnownClass::Str.to_instance(db), - Type::none(db), + KnownClass::Str.to_instance(db, env), + Type::none(db, env), )]); return Some(Type::Callable(CallableType::new( db, @@ -2147,18 +2420,33 @@ impl<'db> StaticClassLiteral<'db> { } None } + (CodeGeneratorKind::DataclassLike(_), "__delattr__") + if self.is_frozen_dataclass(db) == Some(true) => + { + let signature = Signature::new( + Parameters::standard([ + Parameter::positional_or_keyword(Name::new_static("self")) + .with_annotated_type(instance_ty), + Parameter::positional_or_keyword(Name::new_static("name")), + ]), + Type::Never, + ); + + Some(Type::function_like_callable(db, signature)) + } (field_policy @ CodeGeneratorKind::DataclassLike(_), "__slots__") - if Program::get(db).python_version(db) >= PythonVersion::PY310 => + if env.python_version(db) >= PythonVersion::PY310 => { self.has_dataclass_param(db, field_policy, DataclassFlags::SLOTS) .then(|| { let fields = self.fields(db, specialization, field_policy); let slots = fields.keys().map(|name| Type::string_literal(db, name)); - Type::heterogeneous_tuple(db, slots) + Type::heterogeneous_tuple(db, env, slots) }) } (CodeGeneratorKind::TypedDict, name) => synthesize_typed_dict_method( db, + env, instance_ty .as_typed_dict() .expect("TypedDict code generation should use a TypedDict instance"), @@ -2196,44 +2484,57 @@ impl<'db> StaticClassLiteral<'db> { .unwrap_or(false) } - /// Synthesize a `__setattr__` view for an ordinary subclass of a frozen dataclass. + /// Synthesize a `__setattr__` or `__delattr__` view for an ordinary subclass of a frozen + /// dataclass. /// - /// CPython's generated frozen-dataclass `__setattr__` rejects all writes on exact instances of - /// the frozen dataclass, but on subclass instances it only rejects writes to that dataclass's - /// fields before delegating to the next `__setattr__` in the MRO. - fn own_frozen_dataclass_subclass_setattr( + /// CPython's generated frozen-dataclass `__setattr__` and `__delattr__` reject all assignments + /// and deletions on exact instances of the frozen dataclass, but on subclass instances they + /// only reject assignments and deletions of that dataclass's fields before delegating to the + /// next method in the MRO. + fn own_frozen_dataclass_subclass_method( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, specialization: Option>, + method: FrozenDataclassMethod, ) -> Option> { if CodeGeneratorKind::from_static_class(db, self).is_some() { return None; } let frozen_base_fields = - self.inherited_non_slotted_frozen_dataclass_fields(db, specialization)?; - - let instance_ty = - Type::instance(db, self.apply_optional_specialization(db, specialization)); - let setattr_signature = |name_ty, return_ty| { - Signature::new( - Parameters::standard([ - Parameter::positional_or_keyword(Name::new_static("self")) - .with_annotated_type(instance_ty), - Parameter::positional_or_keyword(Name::new_static("name")) - .with_annotated_type(name_ty), + self.inherited_non_slotted_frozen_dataclass_fields(db, specialization, method.name())?; + + let instance_ty = Type::instance( + db, + env, + self.apply_optional_specialization(db, specialization), + ); + let method_signature = |name_ty, return_ty| { + let self_parameter = Parameter::positional_or_keyword(Name::new_static("self")) + .with_annotated_type(instance_ty); + let name_parameter = Parameter::positional_or_keyword(Name::new_static("name")) + .with_annotated_type(name_ty); + let parameters = match method { + FrozenDataclassMethod::SetAttr => Parameters::standard([ + self_parameter, + name_parameter, Parameter::positional_or_keyword(Name::new_static("value")), ]), - return_ty, - ) + FrozenDataclassMethod::DelAttr => { + Parameters::standard([self_parameter, name_parameter]) + } + }; + Signature::new(parameters, return_ty) }; let overloads = frozen_base_fields - .keys() - .map(|field| setattr_signature(Type::string_literal(db, field), Type::Never)) - .chain([setattr_signature( - KnownClass::Str.to_instance(db), - Type::none(db), + .names + .iter() + .map(|field| method_signature(Type::string_literal(db, field), Type::Never)) + .chain([method_signature( + KnownClass::Str.to_instance(db, env), + Type::none(db, env), )]); Some(Type::Callable(CallableType::new( @@ -2244,51 +2545,120 @@ impl<'db> StaticClassLiteral<'db> { ))) } - /// Return the inherited frozen dataclass fields whose generated `__setattr__` still controls - /// assignments on this class. + /// Determines how an inherited generated frozen-dataclass `method` handles `name`. + /// + /// CPython's generated `__setattr__` and `__delattr__` reject every mutation when called on an + /// instance of the exact frozen class. On an ordinary subclass instance, they reject only + /// dataclass fields and delegate other names with `super(frozen_class, instance)`. + /// + /// If multiple frozen dataclasses are reachable before an explicit implementation of + /// `method`, a non-field delegates past each generated method. + /// [`FrozenDataclassDispatch::Delegate`] stores the last frozen base so the caller can perform + /// the equivalent lookup once, after all of them. + pub(crate) fn inherited_frozen_dataclass_dispatch( + self, + db: &'db dyn Db, + specialization: Option>, + method: &str, + name: &str, + ) -> Option> { + if CodeGeneratorKind::from_static_class(db, self).is_some() + || class_member(db, self.body_scope(db), method) + .ignore_possibly_undefined() + .is_some() + { + return None; + } + + let frozen_base_fields = + self.inherited_non_slotted_frozen_dataclass_fields(db, specialization, method)?; + + if frozen_base_fields + .names + .iter() + .any(|field| field.as_str() == name) + { + Some(FrozenDataclassDispatch::FrozenField) + } else { + Some(FrozenDataclassDispatch::Delegate( + frozen_base_fields.last_frozen_base, + )) + } + } + + /// Returns the inherited fields whose generated `__setattr__` or `__delattr__` still applies. fn inherited_non_slotted_frozen_dataclass_fields( self, db: &'db dyn Db, specialization: Option>, - ) -> Option<&'db FxIndexMap>> { + method: &str, + ) -> Option> { + let mut names = FxIndexSet::default(); + let mut last_frozen_base = None; + for base in self.iter_mro(db, specialization).skip(1) { - let (base_class, base_specialization) = base.into_class()?.static_class_literal(db)?; + let Some(base_class_type) = base.into_class() else { + break; + }; + let Some((base_class, base_specialization)) = base_class_type.static_class_literal(db) + else { + break; + }; - // Stop if another class in the MRO replaces the generated frozen setter: + // Stop if another class in the MRO replaces the relevant generated frozen method: // // @dataclass(frozen=True) // class Frozen: x: int // // class Mutable(Frozen): // def __setattr__(self, name: str, value: object) -> None: ... + // def __delattr__(self, name: str) -> None: ... // // class Child(Mutable): ... // - // Writes to `Child().x` dispatch to `Mutable.__setattr__`, not to the synthesized - // `Frozen.__setattr__`. - if class_member(db, base_class.body_scope(db), "__setattr__") + // Writes and deletions of `Child().x` dispatch to the corresponding `Mutable` method, + // not to the synthesized `Frozen` method. + if class_member(db, base_class.body_scope(db), method) .ignore_possibly_undefined() .is_some() { - return None; + break; } if base_class.is_frozen_dataclass(db) == Some(true) { let field_policy @ CodeGeneratorKind::DataclassLike(_) = CodeGeneratorKind::from_static_class(db, base_class)? else { - return None; + break; }; if base_class.has_dataclass_param(db, field_policy, DataclassFlags::SLOTS) { - return None; + break; } - return Some(base_class.fields(db, base_specialization, field_policy)); + names.extend( + base_class + .fields(db, base_specialization, field_policy) + .iter() + .filter(|(_, field)| { + !matches!( + field.kind, + FieldKind::Dataclass { + init_only: true, + .. + } + ) + }) + .map(|(name, _)| name.clone()), + ); + last_frozen_base = Some(base_class); } } - None + Some(InheritedFrozenDataclassFields { + names: names.into_iter().collect(), + last_frozen_base: last_frozen_base?, + }) } /// Member lookup for classes that inherit from `typing.TypedDict`. @@ -2299,11 +2669,12 @@ impl<'db> StaticClassLiteral<'db> { pub(crate) fn typed_dict_member( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, specialization: Option>, name: &str, policy: MemberLookupPolicy, ) -> PlaceAndQualifiers<'db> { - if let Some(member) = self.own_synthesized_member(db, specialization, None, name) { + if let Some(member) = self.own_synthesized_member(db, env, specialization, None, name) { Place::bound(member).into() } else { let class = match specialization { @@ -2315,7 +2686,7 @@ impl<'db> StaticClassLiteral<'db> { let Some(module) = self.typed_dict_module(db) else { return Place::Undefined.into(); }; - typed_dict_class_member(db, class, module, policy, name) + typed_dict_class_member(db, env, class, module, policy, name) } } @@ -2359,6 +2730,7 @@ impl<'db> StaticClassLiteral<'db> { "Collecting `fields` for NamedTuples should short-circuit in `fields()`" ); + let mut class_variables = FxIndexSet::default(); let mut map: FxIndexMap<_, _> = self .iter_mro(db, specialization) .rev() @@ -2387,12 +2759,24 @@ impl<'db> StaticClassLiteral<'db> { None }) .flat_map(|source| match source { - FieldSource::Static(class, specialization) => Either::Left( - class - .own_fields(db, specialization, field_policy) - .iter() - .map(|(name, field)| (name.clone(), field.clone())), - ), + FieldSource::Static(class, specialization) => { + let own_fields = + class.own_fields_with_class_variables(db, specialization, field_policy); + + if field_policy.is_dataclass_like() { + class_variables.extend(own_fields.class_variables.iter().cloned()); + for name in own_fields.fields.keys() { + class_variables.swap_remove(name); + } + } + + Either::Left( + own_fields + .fields + .iter() + .map(|(name, field)| (name.clone(), field.clone())), + ) + } FieldSource::DynamicTypedDict(typeddict) => { Either::Right(typeddict.items(db).iter().map(|(name, td_field)| { ( @@ -2415,12 +2799,19 @@ impl<'db> StaticClassLiteral<'db> { // We collect into a FxOrderMap here to deduplicate attributes .collect(); + if field_policy.is_dataclass_like() { + // `own_fields` excludes class variables, but their declarations can still mask + // inherited fields. Delay removal so restoring a field preserves its original slot. + map.retain(|name, _| !class_variables.contains(name)); + } + map.shrink_to_fit(); map } pub(crate) fn validate_members(self, context: &InferContext<'db, '_>) { let db = context.db(); + let env = context.program_environment(); let Some(field_policy) = CodeGeneratorKind::from_static_class(db, self) else { return; }; @@ -2428,7 +2819,7 @@ impl<'db> StaticClassLiteral<'db> { let table = place_table(db, class_body_scope); let use_def = use_def_map(db, class_body_scope); for (symbol_id, declarations) in use_def.all_end_of_scope_symbol_declarations() { - let result = place_from_declarations(db, declarations.clone()); + let result = place_from_declarations(db, env, declarations.clone()); let attr = result.ignore_conflicting_declarations(); let symbol = table.symbol(symbol_id); let name = symbol.name(); @@ -2498,26 +2889,57 @@ impl<'db> StaticClassLiteral<'db> { /// including properties inherited from class-level dataclass parameters (like `kw_only=True`) /// and dataclass-transform parameters (like `kw_only_default=True`). They do not represent /// only what is explicitly specified in each field definition. + pub(crate) fn own_fields( + self, + db: &'db dyn Db, + specialization: Option>, + field_policy: CodeGeneratorKind<'db>, + ) -> &'db FxIndexMap> { + &self + .own_fields_with_class_variables(db, specialization, field_policy) + .fields + } + + fn own_fields_with_class_variables( + self, + db: &'db dyn Db, + specialization: Option>, + field_policy: CodeGeneratorKind<'db>, + ) -> &'db OwnClassFields<'db> { + self.own_fields_inner(db, specialization, field_policy) + } + + /// Collects ordered constructor fields and `ClassVar` masks in one pass over a class body. + /// + /// Keeping both together avoids reinterpreting declarations while merging inherited fields. #[salsa::tracked( returns(ref), - cycle_initial=|_, _, _, _, _| FxIndexMap::default(), + cycle_initial=|_, _, _, _, _| OwnClassFields::default(), heap_size=get_size2::GetSize::get_heap_size )] - pub(crate) fn own_fields( + fn own_fields_inner( self, db: &'db dyn Db, specialization: Option>, field_policy: CodeGeneratorKind<'db>, - ) -> FxIndexMap> { + ) -> OwnClassFields<'db> { + let env = &ProgramEnvironment::from_file(self.program_file(db)); if field_policy == CodeGeneratorKind::Django { - return self.django_own_fields(db, specialization); + return OwnClassFields { + fields: self.django_own_fields(db, env, specialization), + class_variables: Box::default(), + }; } if field_policy == CodeGeneratorKind::SqlalchemyDeclarative { - return self.sqlalchemy_own_fields(db, specialization); + return OwnClassFields { + fields: self.sqlalchemy_own_fields(db, env, specialization), + class_variables: Box::default(), + }; } let class_body_scope = self.body_scope(db); + let env = ProgramEnvironment::from_scope(class_body_scope); let table = place_table(db, class_body_scope); let use_def = use_def_map(db, class_body_scope); @@ -2533,9 +2955,11 @@ impl<'db> StaticClassLiteral<'db> { } else { false }; - let dataclass_kw_only_default = field_policy - .is_dataclass_like() - .then(|| self.has_dataclass_param(db, field_policy, DataclassFlags::KW_ONLY)); + let dataclass_kw_only_default = field_policy.is_dataclass_like().then(|| { + let own_field_policy = + CodeGeneratorKind::from_class(db, self.into()).unwrap_or(field_policy); + self.has_dataclass_param(db, own_field_policy, DataclassFlags::KW_ONLY) + }); let mut kw_only_sentinel_field_seen = false; let mut field_declarations = Vec::new(); @@ -2574,7 +2998,7 @@ impl<'db> StaticClassLiteral<'db> { continue; }; - let result = place_from_declarations(db, declarations.clone()); + let result = place_from_declarations(db, &env, declarations.clone()); field_declarations.push((first_declaration_order, symbol_id, result)); } @@ -2582,11 +3006,15 @@ impl<'db> StaticClassLiteral<'db> { .sort_unstable_by_key(|(first_declaration_order, _, _)| *first_declaration_order); let mut attributes = FxIndexMap::default(); + let mut class_variables = Vec::new(); for (_, symbol_id, result) in field_declarations { let symbol = table.symbol(symbol_id); let first_declaration = result.first_declaration; let attr = result.ignore_conflicting_declarations(); if attr.is_class_var() { + if field_policy.is_dataclass_like() { + class_variables.push(symbol.name().clone()); + } continue; } @@ -2595,7 +3023,7 @@ impl<'db> StaticClassLiteral<'db> { None } else { let bindings = use_def.end_of_scope_symbol_bindings(symbol_id); - place_from_bindings(db, bindings) + place_from_bindings(db, &env, bindings) .place .ignore_possibly_undefined() }; @@ -2610,8 +3038,13 @@ impl<'db> StaticClassLiteral<'db> { let mut strict = pydantic::ConfigBoolean::Unspecified; let mut frozen = false; if field_policy.is_pydantic() { - let metadata = - pydantic::field_metadata(db, first_declaration, default_ty, specialization); + let metadata = pydantic::field_metadata( + db, + &env, + first_declaration, + default_ty, + specialization, + ); default_ty = metadata.default_ty; init = metadata.init; alias = metadata.alias; @@ -2663,9 +3096,8 @@ impl<'db> StaticClassLiteral<'db> { }, CodeGeneratorKind::Pydantic(_) => FieldKind::Pydantic { default_ty, - // Pydantic treats underscore-prefixed annotations as private attributes, - // which are instance attributes but never constructor parameters. - init: init && !symbol.name().starts_with('_'), + // Private attributes are instance attributes but never constructor parameters. + init: init && !pydantic::is_private_attribute(symbol.name()), alias, strict, frozen, @@ -2727,7 +3159,73 @@ impl<'db> StaticClassLiteral<'db> { attributes.shrink_to_fit(); - attributes + OwnClassFields { + fields: attributes, + class_variables: class_variables.into_boxed_slice(), + } + } + + /// Return the type qualifiers attached to each reachable annotated assignment in source order. + /// + /// This uses the declaration history rather than [`StaticClassLiteral::own_fields`], because a + /// later method or nested class can replace the symbol's binding while leaving its entry in + /// `__annotations__`: + /// + /// ```python + /// class Example(NamedTuple): + /// value: Final[int] + /// def value(self) -> int: ... + /// ``` + /// + /// Each qualifier remains paired with its own definition so diagnostics can point to the + /// annotation that introduced it, including when declarations occur in different branches. + pub(crate) fn own_annotated_qualifiers( + self, + db: &'db dyn Db, + ) -> Vec<(Name, TypeQualifiers, Definition<'db>)> { + let body_scope = self.body_scope(db); + let table = place_table(db, body_scope); + let use_def = use_def_map(db, body_scope); + let mut annotated_qualifiers = Vec::new(); + + for (symbol_id, _) in use_def.all_end_of_scope_symbol_declarations() { + let declarations = use_def.reachable_symbol_declarations(symbol_id); + let predicates = declarations.predicates(); + let reachability_constraints = declarations.reachability_constraints(); + + for declaration in declarations { + if reachability_constraints + .evaluate(db, predicates, declaration.reachability_constraint) + .is_always_false() + { + continue; + } + + let DefinitionState::Defined(definition) = declaration.declaration else { + continue; + }; + if !matches!(definition.kind(db), DefinitionKind::AnnotatedAssignment(..)) { + continue; + } + + let Some(declared) = inferred_declaration(db, definition).declared() else { + continue; + }; + annotated_qualifiers.push(( + declaration.declaration_order, + table.symbol(symbol_id).name().clone(), + declared.qualifiers(), + definition, + )); + } + } + + annotated_qualifiers + .sort_unstable_by_key(|(declaration_order, _, _, _)| *declaration_order); + annotated_qualifiers + .into_iter() + .map(|(_, name, qualifiers, definition)| (name, qualifiers, definition)) + .collect() } /// Django's value-inferred fields: *unannotated* class-body assignments @@ -2738,12 +3236,13 @@ impl<'db> StaticClassLiteral<'db> { fn django_own_fields( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, specialization: Option>, ) -> FxIndexMap> { 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); - let module = parsed_module(db, self.file(db)).load(db); + let module = parsed_module(db, self.program_file(db).python_file(db)).load(db); let mut field_bindings = Vec::new(); for (symbol_id, bindings) in use_def.all_end_of_scope_symbol_bindings() { @@ -2760,13 +3259,13 @@ impl<'db> StaticClassLiteral<'db> { let (Some(order), Some(definition)) = (first_order, last_definition) else { continue; }; - let Some(ty) = place_from_bindings(db, bindings) + let Some(ty) = place_from_bindings(db, env, bindings) .place .ignore_possibly_undefined() else { continue; }; - if !django::is_field_instance(db, ty) { + if !django::is_field_instance(db, env, ty) { continue; } field_bindings.push((order, symbol_id, ty, definition)); @@ -2781,7 +3280,7 @@ impl<'db> StaticClassLiteral<'db> { kind: FieldKind::Django { primary_key: facts.primary_key, null: facts.null, - many_to_many: django::is_many_to_many_instance(db, ty), + many_to_many: django::is_many_to_many_instance(db, env, ty), related_name: facts.related_name, has_choices: facts.has_choices, }, @@ -2801,6 +3300,7 @@ impl<'db> StaticClassLiteral<'db> { fn sqlalchemy_own_fields( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, specialization: Option>, ) -> FxIndexMap> { let class_body_scope = self.body_scope(db); @@ -2837,7 +3337,7 @@ impl<'db> StaticClassLiteral<'db> { continue; }; - let result = place_from_declarations(db, declarations.clone()); + let result = place_from_declarations(db, env, declarations.clone()); field_declarations.push((first_declaration_order, symbol_id, result)); } field_declarations @@ -2855,7 +3355,7 @@ impl<'db> StaticClassLiteral<'db> { continue; }; // only `Mapped[T]` annotations are fields; unwrap to `T` - let Some(field_ty) = sqlalchemy::mapped_field_type(db, attr_ty) else { + let Some(field_ty) = sqlalchemy::mapped_field_type(db, env, attr_ty) else { continue; }; fields.insert( @@ -2877,6 +3377,7 @@ impl<'db> StaticClassLiteral<'db> { pub(super) fn instance_member( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, specialization: Option>, name: &str, ) -> PlaceAndQualifiers<'db> { @@ -2884,16 +3385,21 @@ impl<'db> StaticClassLiteral<'db> { return Place::Undefined.into(); } - match MroLookup::new(db, self.iter_mro(db, specialization)).instance_member(name) { + match MroLookup::new(db, env, self.iter_mro(db, specialization)).instance_member(name) { InstanceMemberResult::Done(result) => result, InstanceMemberResult::TypedDict => KnownClass::TypedDictFallback - .to_instance(db) - .instance_member(db, name) + .to_instance(db, env) + .instance_member(db, env, name) .map_type(|ty| { ty.apply_type_mapping( db, + env, &TypeMapping::ReplaceSelf { - new_upper_bound: Type::instance(db, self.unknown_specialization(db)), + new_upper_bound: Type::instance( + db, + env, + self.unknown_specialization(db), + ), }, TypeContext::default(), ) @@ -2945,19 +3451,21 @@ impl<'db> StaticClassLiteral<'db> { let class_body_scope = attribute.class_body_scope(db); let name = attribute.name(db).as_str(); let target_method_decorator = attribute.target_method_decorator(db); + let program_file = class_body_scope.program_file(db); + let python_file = program_file.python_file(db); + let env = &ProgramEnvironment::from_file(program_file); // If we do not see any declarations of an attribute, neither in the class body nor in // any method, we build a union of the raw types inferred from all bindings of that // attribute, then apply public-type promotion to the final union. - let mut union_of_inferred_types = UnionBuilder::new(db); + let mut union_of_inferred_types = UnionBuilder::new(db, env); let mut qualifiers = TypeQualifiers::IMPLICIT_INSTANCE_ATTRIBUTE; let mut is_attribute_bound = false; let mut provenance = Provenance::Unknown; - let file = class_body_scope.file(db); - let module = parsed_module(db, file).load(db); - let index = semantic_index(db, file); + let module = parsed_module(db, python_file).load(db); + let index = semantic_index(db, program_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: &Scope| { @@ -3171,7 +3679,11 @@ impl<'db> StaticClassLiteral<'db> { TypeContext::default(), ); // TODO: Potential diagnostics resulting from the iterable are currently not reported. - Some(iterable_ty.iterate(db).homogeneous_element_type(db)) + Some( + iterable_ty + .iterate(db, env) + .homogeneous_element_type(db, env), + ) } }, DefinitionKind::WithItem(with_item) => match with_item.target_kind() { @@ -3194,9 +3706,9 @@ impl<'db> StaticClassLiteral<'db> { TypeContext::default(), ); Some(if with_item.is_async() { - context_ty.aenter(db) + context_ty.aenter(db, env) } else { - context_ty.enter(db) + context_ty.enter(db, env) }) } }, @@ -3221,13 +3733,16 @@ impl<'db> StaticClassLiteral<'db> { TypeContext::default(), ); // TODO: Potential diagnostics resulting from the iterable are currently not reported. - Some(iterable_ty.iterate(db).homogeneous_element_type(db)) + Some( + iterable_ty + .iterate(db, env) + .homogeneous_element_type(db, env), + ) } } } DefinitionKind::AugmentedAssignment(_) => { - // TODO: - None + Some(infer_definition_types(db, binding).binding_type(binding)) } DefinitionKind::NamedExpression(_) => { // A named expression whose target is an attribute is syntactically prohibited @@ -3248,8 +3763,8 @@ impl<'db> StaticClassLiteral<'db> { Place::bound( union_of_inferred_types .build() - .promote_in(db, class_body_scope.file(db)) - .promote_singletons(db), + .promote_in(db, env, class_body_scope.file(db)) + .promote_singletons(db, env), ) .with_provenance(provenance) .with_qualifiers(qualifiers) @@ -3261,7 +3776,12 @@ impl<'db> StaticClassLiteral<'db> { /// A helper function for `instance_member` that looks up the `name` attribute only on /// this class, not on its superclasses. - pub(super) fn own_instance_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { + pub(super) fn own_instance_member( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> Member<'db> { // TODO: There are many things that are not yet implemented here: // - `typing.Final` // - Proper diagnostics @@ -3285,7 +3805,7 @@ impl<'db> StaticClassLiteral<'db> { let declarations = use_def.end_of_scope_symbol_declarations(symbol_id); let declared_and_qualifiers = - place_from_declarations(db, declarations).ignore_conflicting_declarations(); + place_from_declarations(db, env, declarations).ignore_conflicting_declarations(); match declared_and_qualifiers { PlaceAndQualifiers { @@ -3325,7 +3845,7 @@ impl<'db> StaticClassLiteral<'db> { // The attribute is declared in the class body. let bindings = use_def.end_of_scope_symbol_bindings(symbol_id); - let inferred = place_from_bindings(db, bindings).place; + let inferred = place_from_bindings(db, env, bindings).place; let has_binding = !inferred.is_undefined(); if has_binding { @@ -3351,6 +3871,7 @@ impl<'db> StaticClassLiteral<'db> { inner: Place::Defined(DefinedPlace { ty: UnionType::from_two_elements( db, + env, declared_ty, implicit_ty, ), @@ -3363,7 +3884,10 @@ impl<'db> StaticClassLiteral<'db> { } } } else if self.is_own_dataclass_instance_field(db, name) - && declared_ty.class_member(db, "__get__").place.is_undefined() + && declared_ty + .class_member(db, env, "__get__") + .place + .is_undefined() { // For dataclass-like classes, declared fields are assigned // by the synthesized `__init__`, so they are instance @@ -3415,6 +3939,7 @@ impl<'db> StaticClassLiteral<'db> { inner: Place::Defined(DefinedPlace { ty: UnionType::from_two_elements( db, + env, declared_ty, implicit_ty, ), @@ -3502,7 +4027,8 @@ impl<'db> StaticClassLiteral<'db> { } pub(super) fn to_non_generic_instance(self, db: &'db dyn Db) -> Type<'db> { - Type::instance(db, ClassType::NonGeneric(self.into())) + let env = ProgramEnvironment::from_scope(self.body_scope(db)); + Type::instance(db, &env, ClassType::NonGeneric(self.into())) } /// Return this class' involvement in an inheritance cycle, if any. @@ -3510,6 +4036,10 @@ impl<'db> StaticClassLiteral<'db> { /// A class definition like this will fail at runtime, /// but we must be resilient to it or we could panic. pub(crate) fn inheritance_cycle(self, db: &'db dyn Db) -> Option { + if !self.has_explicit_bases(db) { + return None; + } + #[salsa::tracked(returns(copy), cycle_initial=|_, _, _| None, heap_size=ruff_memory_usage::heap_size)] fn inheritance_cycle_inner<'db>( db: &'db dyn Db, @@ -3554,7 +4084,6 @@ impl<'db> StaticClassLiteral<'db> { } tracing::trace!("Class::inheritance_cycle: {}", class.name(db)); - let visited_classes = &mut FxIndexSet::default(); if !is_cyclically_defined_recursive( db, @@ -3570,9 +4099,6 @@ impl<'db> StaticClassLiteral<'db> { } } - if !self.has_explicit_bases(db) { - return None; - } inheritance_cycle_inner(db, self) } @@ -3592,7 +4118,7 @@ impl<'db> StaticClassLiteral<'db> { /// ``` pub(crate) 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 module = parsed_module(db, class_scope.python_file(db)).load(db); let class_node = self.node(db, &module); let class_name = &class_node.name; TextRange::new( @@ -3608,7 +4134,7 @@ impl<'db> StaticClassLiteral<'db> { /// Returns the range of the class's name pub(crate) fn focus_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 module = parsed_module(db, class_scope.python_file(db)).load(db); let class_node = self.node(db, &module); class_node.name.range() } @@ -3707,16 +4233,69 @@ fn expanded_fixed_length_starred_class_base_tuple<'db>( }; let starred_ty = definition_expression_type(db, class_definition, &starred.value); - let Tuple::Fixed(tuple) = starred_ty.tuple_instance_spec(db)?.into_owned() else { + let env = ProgramEnvironment::from_definition(class_definition); + let Tuple::Fixed(tuple) = starred_ty.tuple_instance_spec(db, &env)?.into_owned() else { return None; }; Some(tuple) } -#[salsa::tracked] impl<'db> VarianceInferable<'db> for StaticClassLiteral<'db> { - #[salsa::tracked(returns(copy), cycle_initial=|_, _, _, _| TypeVarVariance::Bivariant, heap_size=ruff_memory_usage::heap_size)] - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { + fn variance_of( + self, + db: &'db dyn Db, + _: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { + let bivariant_private_attributes = db + .analysis_settings(self.body_scope(db).file(db)) + .bivariant_private_attributes; + self.variance_of_owner(db, typevar, bivariant_private_attributes) + } +} + +impl<'db> StaticClassLiteral<'db> { + /// basedpython: whether no member of this class mentions `typevar`. + /// + /// The typing spec reports an inferred-bivariant class parameter as covariant, and does so + /// because the parameter is *unused* — nothing can tell two specializations apart. This fork + /// also concludes bivariance for a parameter that only a private member mentions, which is a + /// parameter the class genuinely uses, so that conclusion has to survive the spec's fallback. + /// + /// Re-reading the class with `bivariant-private-attributes` switched off separates the two: + /// with the fork's rule disabled a private member is merely immutable, and so reports + /// covariance, leaving bivariance to mean what the spec means by it. + pub(crate) fn typevar_is_unused( + self, + db: &'db dyn Db, + typevar: BoundTypeVarIdentity<'db>, + ) -> bool { + self.variance_of_owner(db, typevar, false) == TypeVarVariance::Bivariant + } + + /// The variance this class's own members require of `typevar`, ignoring its bases. + pub(crate) fn own_variance_of( + self, + db: &'db dyn Db, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { + let bivariant_private_attributes = db + .analysis_settings(self.body_scope(db).file(db)) + .bivariant_private_attributes; + self.own_variance_of_with(db, typevar, bivariant_private_attributes) + } +} + +#[salsa::tracked] +impl<'db> StaticClassLiteral<'db> { + #[salsa::tracked(returns(copy), cycle_initial=|_, _, _, _, _| TypeVarVariance::Bivariant, heap_size=ruff_memory_usage::heap_size)] + fn variance_of_owner( + self, + db: &'db dyn Db, + typevar: BoundTypeVarIdentity<'db>, + bivariant_private_attributes: bool, + ) -> TypeVarVariance { + let env = ProgramEnvironment::from_scope(self.body_scope(db)); let typevar_in_generic_context = self .generic_context(db) .is_some_and(|generic_context| generic_context.contains(db, typevar)); @@ -3724,13 +4303,12 @@ impl<'db> VarianceInferable<'db> for StaticClassLiteral<'db> { if !typevar_in_generic_context { return TypeVarVariance::Bivariant; } - let explicit_bases_variances = self .explicit_bases(db) .iter() - .map(|class| class.variance_of(db, typevar)); + .map(|class| class.variance_of(db, &env, typevar)); - std::iter::once(self.own_variance_of(db, typevar)) + std::iter::once(self.own_variance_of_with(db, typevar, bivariant_private_attributes)) .chain(explicit_bases_variances) .collect() } @@ -3744,12 +4322,14 @@ impl<'db> StaticClassLiteral<'db> { /// This is what a declared variance has to agree with: an incompatible base is reported /// against that base instead, so folding the bases in here would report the same problem /// twice. - #[salsa::tracked(returns(copy), cycle_initial=|_, _, _, _| TypeVarVariance::Bivariant, heap_size=ruff_memory_usage::heap_size)] - pub(crate) fn own_variance_of( + #[salsa::tracked(returns(copy), cycle_initial=|_, _, _, _, _| TypeVarVariance::Bivariant, heap_size=ruff_memory_usage::heap_size)] + pub(crate) fn own_variance_of_with( self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>, + bivariant_private_attributes: bool, ) -> TypeVarVariance { + let env = &ProgramEnvironment::from_file(self.program_file(db)); let typevar_in_generic_context = self .generic_context(db) .is_some_and(|generic_context| generic_context.contains(db, typevar)); @@ -3760,7 +4340,7 @@ impl<'db> StaticClassLiteral<'db> { let class_body_scope = self.body_scope(db); let file = class_body_scope.file(db); - let index = semantic_index(db, file); + let index = semantic_index(db, db.program_file(file)); let field_policy = CodeGeneratorKind::from_static_class(db, self); @@ -3810,7 +4390,7 @@ impl<'db> StaticClassLiteral<'db> { field .declared_ty .with_polarity(default_attribute_variance) - .variance_of(db, typevar) + .variance_of(db, env, typevar) }); let init_name: &Name = &"__init__".into(); @@ -3822,13 +4402,16 @@ impl<'db> StaticClassLiteral<'db> { use_def_map .all_end_of_scope_symbol_declarations() .map(|(symbol_id, declarations)| { - let place_and_qual = - place_from_declarations(db, declarations).ignore_conflicting_declarations(); + let place_and_qual = place_from_declarations(db, env, declarations) + .ignore_conflicting_declarations(); (symbol_id, place_and_qual) }) .chain(use_def_map.all_end_of_scope_symbol_bindings().map( |(symbol_id, bindings)| { - (symbol_id, place_from_bindings(db, bindings).place.into()) + ( + symbol_id, + place_from_bindings(db, env, bindings).place.into(), + ) }, )) .filter_map(|(symbol_id, place_and_qual)| { @@ -3856,11 +4439,9 @@ impl<'db> StaticClassLiteral<'db> { }) .dedup(); - let bivariant_private_attributes = db.analysis_settings(file).bivariant_private_attributes; - let attribute_variances = attribute_names .map(|name| { - let place_and_quals = self.own_instance_member(db, &name).inner; + let place_and_quals = self.own_instance_member(db, env, &name).inner; (name, place_and_quals) }) .chain(attribute_places_and_qualifiers) @@ -3877,9 +4458,12 @@ impl<'db> StaticClassLiteral<'db> { TypeVarVariance::Bivariant } else if place_and_qual .qualifiers - // `CLASS_VAR || FINAL` is really `all()`, but - // we want to be robust against new qualifiers - .intersects(TypeQualifiers::CLASS_VAR | TypeQualifiers::FINAL) + // None of these fields can be mutated through an instance. + .intersects( + TypeQualifiers::CLASS_VAR + | TypeQualifiers::FINAL + | TypeQualifiers::READ_ONLY, + ) // We don't allow mutation of methods or properties || ty.is_function_literal() || ty.is_property_instance() @@ -3897,12 +4481,12 @@ impl<'db> StaticClassLiteral<'db> { // type variable, but they could if it's a // callable type. They can't be mutated on instances. // - // FINAL: final attributes are immutable, and thus covariant + // FINAL and READ_ONLY: immutable fields are covariant. TypeVarVariance::Covariant } else { default_attribute_variance }; - ty.with_polarity(variance).variance_of(db, typevar) + ty.with_polarity(variance).variance_of(db, env, typevar) }) }); @@ -3917,7 +4501,7 @@ impl<'db> StaticClassLiteral<'db> { extra_items .declared_ty .with_polarity(polarity) - .variance_of(db, typevar) + .variance_of(db, env, typevar) }); attribute_variances @@ -3956,14 +4540,15 @@ pub(crate) fn based_enum_variant_union<'db>( db: &'db dyn Db, class: StaticClassLiteral<'db>, ) -> Option> { - let module = parsed_module(db, class.file(db)).load(db); + let env = &ProgramEnvironment::from_file(class.program_file(db)); + let module = parsed_module(db, class.program_file(db).python_file(db)).load(db); let class_stmt = class.node(db, &module); // only payload-bearing based enums denote a union of variant classes; an // all-unit enum is an idiomatic `Enum` (its name is the enum type itself) if !class_stmt.is_based_enum() || class_stmt.is_all_unit_enum() { return None; } - let index = semantic_index(db, class.file(db)); + let index = semantic_index(db, class.program_file(db)); let mut elements: Vec> = Vec::new(); for stmt in &class_stmt.body { if let ast::Stmt::ClassDef(variant) = stmt @@ -3992,17 +4577,18 @@ pub(crate) fn based_enum_variant_union<'db>( { elements.push(Type::instance( db, + env, variant_literal.identity_specialization(db), )); } else if let Some(instance) = - binding_type(db, definition).to_instance_approximation(db) + binding_type(db, definition).to_instance_approximation(db, env) { elements.push(instance); } } } } - (!elements.is_empty()).then(|| UnionType::from_elements(db, elements)) + (!elements.is_empty()).then(|| UnionType::from_elements(db, env, elements)) } /// The based enum a variant class belongs to — the class whose body declares it. @@ -4015,7 +4601,7 @@ pub(crate) fn based_enum_of_variant<'db>( return None; } let file = variant.file(db); - let index = semantic_index(db, file); + let index = semantic_index(db, db.program_file(file)); index .ancestor_scopes(variant.body_scope(db).file_scope_id(db)) .skip(1) @@ -4033,7 +4619,7 @@ pub(crate) fn based_enum_unit_member_names<'db>( db: &'db dyn Db, class: StaticClassLiteral<'db>, ) -> Option> { - let module = parsed_module(db, class.file(db)).load(db); + let module = parsed_module(db, class.program_file(db).python_file(db)).load(db); let class_stmt = class.node(db, &module); if !class_stmt.is_based_enum() { return None; @@ -4064,12 +4650,12 @@ pub(crate) fn based_enum_unit_variant_class<'db>( class: StaticClassLiteral<'db>, name: &str, ) -> Option> { - let module = parsed_module(db, class.file(db)).load(db); + let module = parsed_module(db, class.program_file(db).python_file(db)).load(db); let class_stmt = class.node(db, &module); if !class_stmt.is_based_enum() || class_stmt.is_all_unit_enum() { return None; } - let index = semantic_index(db, class.file(db)); + let index = semantic_index(db, class.program_file(db)); class_stmt.body.iter().find_map(|stmt| { let ast::Stmt::ClassDef(variant) = stmt else { return None; @@ -4095,7 +4681,7 @@ pub(crate) fn based_enum_has_payload_variants<'db>( db: &'db dyn Db, class: StaticClassLiteral<'db>, ) -> bool { - let module = parsed_module(db, class.file(db)).load(db); + let module = parsed_module(db, class.program_file(db).python_file(db)).load(db); let class_stmt = class.node(db, &module); class_stmt.is_based_enum() && class_stmt.body.iter().any(|stmt| { @@ -4108,7 +4694,7 @@ fn explicit_bases_cycle_initial<'db>( id: salsa::Id, literal: StaticClassLiteral<'db>, ) -> Box<[Type<'db>]> { - let module = parsed_module(db, literal.file(db)).load(db); + let module = parsed_module(db, literal.python_file(db)).load(db); let class_stmt = literal.node(db, &module); // Try to produce a list of `Divergent` types of the right length. However, if one or more of // the bases is a starred expression, we don't know how many entries that will eventually @@ -4121,15 +4707,16 @@ fn explicit_bases_cycle_fn<'db>( cycle: &salsa::Cycle, previous: &[Type<'db>], current: Box<[Type<'db>]>, - _literal: StaticClassLiteral<'db>, + literal: StaticClassLiteral<'db>, ) -> Box<[Type<'db>]> { if previous.len() == current.len() { + let env = ProgramEnvironment::from_scope(literal.body_scope(db)); // As long as the length of bases hasn't changed, use the same "monotonic widening" // strategy that we use with most types, to avoid oscillations. current .iter() .zip(previous.iter()) - .map(|(curr, prev)| curr.cycle_normalized(db, *prev, cycle)) + .map(|(curr, prev)| curr.cycle_normalized(db, &env, *prev, cycle)) .collect() } else { // The length of bases has changed, presumably because we expanded a starred expression. We @@ -4155,7 +4742,7 @@ impl get_size2::GetSize for ImplicitAttributeName<'_> {} #[salsa::tracked(returns(deref), heap_size=ruff_memory_usage::heap_size)] fn implicit_attribute_names<'db>(db: &'db dyn Db, class_body_scope: ScopeId<'db>) -> Box<[Name]> { - let index = semantic_index(db, class_body_scope.file(db)); + let index = semantic_index(db, class_body_scope.program_file(db)); let mut names = Vec::new(); for function_scope_id in attribute_scopes(db, class_body_scope) { @@ -4177,11 +4764,12 @@ fn implicit_attribute_cycle_recover<'db>( cycle: &salsa::Cycle, previous_member: &Member<'db>, member: Member<'db>, - _attribute: ImplicitAttributeName<'db>, + attribute: ImplicitAttributeName<'db>, ) -> Member<'db> { + let env = ProgramEnvironment::from_scope(attribute.class_body_scope(db)); let inner = member .inner - .cycle_normalized(db, previous_member.inner, cycle); + .cycle_normalized(db, &env, previous_member.inner, cycle); Member { inner } } @@ -4197,7 +4785,7 @@ fn annotated_field_specifier<'db>( let DefinitionKind::AnnotatedAssignment(assignment) = definition.kind(db) else { return None; }; - let module = parsed_module(db, definition.file(db)).load(db); + let module = parsed_module(db, definition.program_file(db).python_file(db)).load(db); let ast::Expr::Subscript(subscript) = assignment.annotation(&module) else { return None; }; diff --git a/crates/ty_python_semantic/src/types/class/typed_dict.rs b/crates/ty_python_semantic/src/types/class/typed_dict.rs index 03662b29a9..3752342cf7 100644 --- a/crates/ty_python_semantic/src/types/class/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/class/typed_dict.rs @@ -1,25 +1,25 @@ +use crate::ProgramEnvironment; use std::borrow::Cow; use itertools::Either; use ruff_db::diagnostic::Span; -use ruff_db::parsed::parsed_module; -use ruff_python_ast as ast; -use ruff_python_ast::NodeIndex; use ruff_python_ast::name::Name; use ruff_python_stdlib::identifiers::is_identifier; -use ruff_text_size::{Ranged, TextRange}; +use ruff_text_size::TextRange; use ty_module_resolver::KnownModule; use crate::place::PlaceAndQualifiers; use crate::place::known_module_symbol; use crate::types::callable::{CallableFunctionProvenance, CallableTypeKind}; +use crate::types::class::{DynamicClassHeaderAnchor, dynamic_class_header_range}; use crate::types::generics::GenericContext; use crate::types::member::Member; use crate::types::mro::Mro; use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature}; use crate::types::typed_dict::{ - TypedDictField, TypedDictFieldBuilder, TypedDictOpenness, TypedDictSchema, - deferred_functional_typed_dict_openness, deferred_functional_typed_dict_schema, + SynthesizedTypedDictType, TypedDictField, TypedDictFieldBuilder, TypedDictOpenness, + TypedDictSchema, deferred_functional_typed_dict_openness, + deferred_functional_typed_dict_schema, }; use crate::types::{ ApplyTypeMappingVisitor, BoundTypeVarInstance, CallableType, ClassBase, ClassLiteral, @@ -32,51 +32,67 @@ use ty_python_core::scope::ScopeId; pub(super) fn synthesize_typed_dict_method<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typed_dict: TypedDictType<'db>, method_name: &str, fields: impl Fn() -> TypedDictFields<'db>, ) -> Option> { let instance_ty = Type::TypedDict(typed_dict); match method_name { - "__init__" => Some(synthesize_typed_dict_init(db, typed_dict, fields())), - "__getitem__" => Some(synthesize_typed_dict_getitem(db, typed_dict, fields())), - "__setitem__" => Some(synthesize_typed_dict_setitem(db, typed_dict, fields())), - "__delitem__" => Some(synthesize_typed_dict_delitem(db, typed_dict, fields())), - "get" => Some(synthesize_typed_dict_get(db, typed_dict, fields())), - "update" => Some(synthesize_typed_dict_update(db, typed_dict, fields())), - "pop" => Some(synthesize_typed_dict_pop(db, typed_dict, fields())), - "setdefault" => Some(synthesize_typed_dict_setdefault(db, typed_dict, fields())), + "__init__" => Some(synthesize_typed_dict_init(db, env, typed_dict, fields())), + "__getitem__" => Some(synthesize_typed_dict_getitem(db, env, typed_dict, fields())), + "__setitem__" => Some(synthesize_typed_dict_setitem(db, env, typed_dict, fields())), + "__delitem__" => Some(synthesize_typed_dict_delitem(db, env, typed_dict, fields())), + "get" => Some(synthesize_typed_dict_get(db, env, typed_dict, fields())), + "update" => Some(synthesize_typed_dict_update(db, env, typed_dict, fields())), + "pop" => Some(synthesize_typed_dict_pop(db, env, typed_dict, fields())), + "setdefault" => Some(synthesize_typed_dict_setdefault( + db, + env, + typed_dict, + fields(), + )), "clear" if typed_dict.supports_arbitrary_key_deletion(db) => Some( - synthesize_typed_dict_no_argument_method(db, typed_dict, Type::none(db)), + synthesize_typed_dict_no_argument_method(db, typed_dict, Type::none(db, env)), ), "popitem" if typed_dict.supports_arbitrary_key_deletion(db) => { let return_ty = Type::heterogeneous_tuple( db, - [KnownClass::Str.to_instance(db), typed_dict.value_type(db)], + env, + [ + KnownClass::Str.to_instance(db, env), + typed_dict.value_type(db, env), + ], ); Some(synthesize_typed_dict_no_argument_method( db, typed_dict, return_ty, )) } "__iter__" if typed_dict.openness(db).is_closed() => { - let return_ty = - KnownClass::Iterator.to_specialized_instance(db, &[typed_dict.key_type(db)]); + let return_ty = KnownClass::Iterator.to_specialized_instance( + db, + env, + &[typed_dict.key_type(db, env)], + ); Some(synthesize_typed_dict_no_argument_method( db, typed_dict, return_ty, )) } "items" if !typed_dict.openness(db).is_implicitly_open() => Some( - synthesize_typed_dict_view_method(db, typed_dict, "dict_items"), + synthesize_typed_dict_view_method(db, env, typed_dict, "dict_items"), ), "keys" if !typed_dict.openness(db).is_implicitly_open() => Some( - synthesize_typed_dict_view_method(db, typed_dict, "dict_keys"), + synthesize_typed_dict_view_method(db, env, typed_dict, "dict_keys"), ), "values" if !typed_dict.openness(db).is_implicitly_open() => Some( - synthesize_typed_dict_view_method(db, typed_dict, "dict_values"), + synthesize_typed_dict_view_method(db, env, typed_dict, "dict_values"), ), - "__or__" | "__ror__" | "__ior__" => { - Some(synthesize_typed_dict_merge(db, instance_ty, method_name)) - } + "__or__" | "__ror__" | "__ior__" => Some(synthesize_typed_dict_merge( + db, + env, + instance_ty, + method_name, + )), _ => None, } } @@ -118,17 +134,26 @@ impl<'db> TypedDictFields<'db> { /// 1. `__init__(self, __map: TD, /, *, field1: T1 = ..., field2: T2 = ...) -> None` /// Allows passing another instance of the `TypedDict` when creating a new instance. /// Technically, `__map` could accept a subset of the `TypedDict` if the remaining -/// fields are provided as keyword arguments, but we don't model that in the -/// synthesized `__init__`, since this signature is primarily used for IDE support. +/// fields are provided as keyword arguments. Such mixed calls use dedicated constructor +/// validation instead because this overload cannot describe the overwritten mapping entries. /// Fields that are not valid Python identifiers are collapsed into `**kwargs`. /// 2. `__init__(self, *, field1: T1, field2: T2 = ...) -> None` /// Keyword-only. Fields that are not valid Python identifiers are collapsed into `**kwargs`. fn synthesize_typed_dict_init<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typed_dict: TypedDictType<'db>, fields: TypedDictFields<'db>, ) -> Type<'db> { let instance_ty = Type::TypedDict(typed_dict); + // Only a bare generic class exposes a generic method. Explicit aliases already substitute + // their arguments into both the receiver and the fields. + let generic_context = typed_dict.defining_class().and_then(|class| { + let alias = class.into_generic_alias()?; + let specialization = alias.specialization(db); + let generic_context = specialization.generic_context(db); + (specialization == generic_context.identity_specialization(db)).then_some(generic_context) + }); let keyword_fields: Vec<_> = fields .iter() .filter(|(name, _)| is_identifier(name)) @@ -158,14 +183,15 @@ fn synthesize_typed_dict_init<'db>( .with_definition(field.first_declaration()) }); - let map_overload = Signature::new( + let map_overload = Signature::new_generic( + generic_context, Parameters::standard( [self_param.clone(), map_param] .into_iter() .chain(params_with_default) .chain(keyword_rest_param.clone()), ), - Type::none(db), + Type::none(db, env), ); let keyword_field_params = keyword_fields.iter().map(|(name, field)| { @@ -179,13 +205,14 @@ fn synthesize_typed_dict_init<'db>( } }); - let keyword_overload = Signature::new( + let keyword_overload = Signature::new_generic( + generic_context, Parameters::standard( std::iter::once(self_param) .chain(keyword_field_params) .chain(keyword_rest_param), ), - Type::none(db), + Type::none(db, env), ); Type::Callable(CallableType::new( @@ -199,6 +226,7 @@ fn synthesize_typed_dict_init<'db>( /// Synthesize the `__getitem__` method for a `TypedDict`. fn synthesize_typed_dict_getitem<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typed_dict: TypedDictType<'db>, fields: TypedDictFields<'db>, ) -> Type<'db> { @@ -220,10 +248,10 @@ fn synthesize_typed_dict_getitem<'db>( Parameter::positional_only(Some(Name::new_static("self"))) .with_annotated_type(instance_ty), Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(KnownClass::Str.to_instance(db)), + .with_annotated_type(KnownClass::Str.to_instance(db, env)), ]), if typed_dict.explicit_extra_items(db).is_some() { - typed_dict.value_type(db) + typed_dict.value_type(db, env) } else { Type::object() }, @@ -240,6 +268,7 @@ fn synthesize_typed_dict_getitem<'db>( /// Synthesize the `__setitem__` method for a `TypedDict`. fn synthesize_typed_dict_setitem<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typed_dict: TypedDictType<'db>, fields: TypedDictFields<'db>, ) -> Type<'db> { @@ -248,7 +277,7 @@ fn synthesize_typed_dict_setitem<'db>( .iter() .filter(|(_, field)| !field.is_read_only()) .peekable(); - let arbitrary_key_mutation_type = typed_dict.arbitrary_key_mutation_type(db); + let arbitrary_key_mutation_type = typed_dict.arbitrary_key_mutation_type(db, env); if writable_fields.peek().is_none() && arbitrary_key_mutation_type.is_none() { let parameters = [ @@ -259,7 +288,7 @@ fn synthesize_typed_dict_setitem<'db>( Parameter::positional_only(Some(Name::new_static("value"))) .with_annotated_type(Type::any()), ]; - let signature = Signature::new(Parameters::standard(parameters), Type::none(db)); + let signature = Signature::new(Parameters::standard(parameters), Type::none(db, env)); return Type::function_like_callable(db, signature); } @@ -274,18 +303,18 @@ fn synthesize_typed_dict_setitem<'db>( Parameter::positional_only(Some(Name::new_static("value"))) .with_annotated_type(field.declared_ty), ]; - Signature::new(Parameters::standard(parameters), Type::none(db)) + Signature::new(Parameters::standard(parameters), Type::none(db, env)) }) .chain(arbitrary_key_mutation_type.map(|value_ty| { let parameters = [ Parameter::positional_only(Some(Name::new_static("self"))) .with_annotated_type(instance_ty), Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(KnownClass::Str.to_instance(db)), + .with_annotated_type(KnownClass::Str.to_instance(db, env)), Parameter::positional_only(Some(Name::new_static("value"))) .with_annotated_type(value_ty), ]; - Signature::new(Parameters::standard(parameters), Type::none(db)) + Signature::new(Parameters::standard(parameters), Type::none(db, env)) })); Type::Callable(CallableType::new( @@ -299,6 +328,7 @@ fn synthesize_typed_dict_setitem<'db>( /// Synthesize the `__delitem__` method for a `TypedDict`. fn synthesize_typed_dict_delitem<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typed_dict: TypedDictType<'db>, fields: TypedDictFields<'db>, ) -> Type<'db> { @@ -316,7 +346,7 @@ fn synthesize_typed_dict_delitem<'db>( Parameter::positional_only(Some(Name::new_static("key"))) .with_annotated_type(Type::Never), ]; - let signature = Signature::new(Parameters::standard(parameters), Type::none(db)); + let signature = Signature::new(Parameters::standard(parameters), Type::none(db, env)); return Type::function_like_callable(db, signature); } @@ -329,16 +359,16 @@ fn synthesize_typed_dict_delitem<'db>( Parameter::positional_only(Some(Name::new_static("key"))) .with_annotated_type(key_type), ]; - Signature::new(Parameters::standard(parameters), Type::none(db)) + Signature::new(Parameters::standard(parameters), Type::none(db, env)) }) .chain(supports_arbitrary_key_deletion.then(|| { let parameters = [ Parameter::positional_only(Some(Name::new_static("self"))) .with_annotated_type(instance_ty), Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(KnownClass::Str.to_instance(db)), + .with_annotated_type(KnownClass::Str.to_instance(db, env)), ]; - Signature::new(Parameters::standard(parameters), Type::none(db)) + Signature::new(Parameters::standard(parameters), Type::none(db, env)) })); Type::Callable(CallableType::new( @@ -352,6 +382,7 @@ fn synthesize_typed_dict_delitem<'db>( /// Synthesize the `get` method for a `TypedDict`. fn synthesize_typed_dict_get<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typed_dict: TypedDictType<'db>, fields: TypedDictFields<'db>, ) -> Type<'db> { @@ -359,7 +390,7 @@ fn synthesize_typed_dict_get<'db>( let fallback_value_ty = if typed_dict.openness(db).is_implicitly_open() { Type::unknown() } else { - typed_dict.value_type(db) + typed_dict.value_type(db, env) }; let overloads = fields .iter() @@ -377,12 +408,13 @@ fn synthesize_typed_dict_get<'db>( if field.is_required() { field.declared_ty } else { - UnionType::from_two_elements(db, field.declared_ty, Type::none(db)) + UnionType::from_two_elements(db, env, field.declared_ty, Type::none(db, env)) }, ); let t_default = BoundTypeVarInstance::synthetic( db, + env, Name::new_static("T"), TypeVarVariance::Covariant, ); @@ -396,12 +428,17 @@ fn synthesize_typed_dict_get<'db>( .with_annotated_type(Type::TypeVar(t_default)), ]; let get_with_default_sig = Signature::new_generic( - Some(GenericContext::from_typevar_instances(db, [t_default])), + Some(GenericContext::from_typevar_instances(db, env, [t_default])), Parameters::standard(get_with_default_sig_params), if field.is_required() { field.declared_ty } else { - UnionType::from_two_elements(db, field.declared_ty, Type::TypeVar(t_default)) + UnionType::from_two_elements( + db, + env, + field.declared_ty, + Type::TypeVar(t_default), + ) }, ); @@ -434,13 +471,14 @@ fn synthesize_typed_dict_get<'db>( Parameter::positional_only(Some(Name::new_static("self"))) .with_annotated_type(instance_ty), Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(KnownClass::Str.to_instance(db)), + .with_annotated_type(KnownClass::Str.to_instance(db, env)), ]), - UnionType::from_two_elements(db, fallback_value_ty, Type::none(db)), + UnionType::from_two_elements(db, env, fallback_value_ty, Type::none(db, env)), ))) .chain(std::iter::once({ let t_default = BoundTypeVarInstance::synthetic( db, + env, Name::new_static("T"), TypeVarVariance::Covariant, ); @@ -449,15 +487,15 @@ fn synthesize_typed_dict_get<'db>( Parameter::positional_only(Some(Name::new_static("self"))) .with_annotated_type(instance_ty), Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(KnownClass::Str.to_instance(db)), + .with_annotated_type(KnownClass::Str.to_instance(db, env)), Parameter::positional_only(Some(Name::new_static("default"))) .with_annotated_type(Type::TypeVar(t_default)), ]; Signature::new_generic( - Some(GenericContext::from_typevar_instances(db, [t_default])), + Some(GenericContext::from_typevar_instances(db, env, [t_default])), Parameters::standard(parameters), - UnionType::from_two_elements(db, fallback_value_ty, Type::TypeVar(t_default)), + UnionType::from_two_elements(db, env, fallback_value_ty, Type::TypeVar(t_default)), ) })); @@ -472,6 +510,7 @@ fn synthesize_typed_dict_get<'db>( /// Synthesize the `update` method for a `TypedDict`. fn synthesize_typed_dict_update<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typed_dict: TypedDictType<'db>, fields: TypedDictFields<'db>, ) -> Type<'db> { @@ -501,16 +540,26 @@ fn synthesize_typed_dict_update<'db>( let update_patch_ty = Type::TypedDict(typed_dict.to_update_patch(db)); - let mapping_ty = typed_dict.dict_value_type(db).map(|value_ty| { - KnownClass::Mapping - .to_specialized_instance(db, &[KnownClass::Str.to_instance(db), value_ty]) - }); - let iterable_ty = typed_dict.arbitrary_key_mutation_type(db).map(|value_ty| { - let item_ty = Type::heterogeneous_tuple(db, [KnownClass::Str.to_instance(db), value_ty]); - KnownClass::Iterable.to_specialized_instance(db, &[item_ty]) + let mapping_ty = typed_dict.dict_value_type(db, env).map(|value_ty| { + KnownClass::Mapping.to_specialized_instance( + db, + env, + &[KnownClass::Str.to_instance(db, env), value_ty], + ) }); + let iterable_ty = typed_dict + .arbitrary_key_mutation_type(db, env) + .map(|value_ty| { + let item_ty = Type::heterogeneous_tuple( + db, + env, + [KnownClass::Str.to_instance(db, env), value_ty], + ); + KnownClass::Iterable.to_specialized_instance(db, env, &[item_ty]) + }); let value_ty = UnionType::from_elements( db, + env, std::iter::once(update_patch_ty) .chain(mapping_ty) .chain(iterable_ty), @@ -520,18 +569,19 @@ fn synthesize_typed_dict_update<'db>( Parameter::positional_only(Some(Name::new_static("self"))).with_annotated_type(instance_ty), Parameter::positional_only(Some(Name::new_static("value"))) .with_annotated_type(value_ty) - .with_default_type(Type::none(db)), + .with_default_type(Type::none(db, env)), ] .into_iter() .chain(keyword_parameters); - let update_signature = Signature::new(Parameters::standard(parameters), Type::none(db)); + let update_signature = Signature::new(Parameters::standard(parameters), Type::none(db, env)); Type::function_like_callable(db, update_signature) } /// Synthesize the `pop` method for a `TypedDict`. fn synthesize_typed_dict_pop<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typed_dict: TypedDictType<'db>, fields: TypedDictFields<'db>, ) -> Type<'db> { @@ -558,8 +608,12 @@ fn synthesize_typed_dict_pop<'db>( value_ty, ); - let t_default = - BoundTypeVarInstance::synthetic(db, Name::new_static("T"), TypeVarVariance::Covariant); + let t_default = BoundTypeVarInstance::synthetic( + db, + env, + Name::new_static("T"), + TypeVarVariance::Covariant, + ); let pop_with_default_parameters = [ Parameter::positional_only(Some(Name::new_static("self"))) .with_annotated_type(instance_ty), @@ -568,9 +622,9 @@ fn synthesize_typed_dict_pop<'db>( .with_annotated_type(Type::TypeVar(t_default)), ]; let pop_with_default_sig = Signature::new_generic( - Some(GenericContext::from_typevar_instances(db, [t_default])), + Some(GenericContext::from_typevar_instances(db, env, [t_default])), Parameters::standard(pop_with_default_parameters), - UnionType::from_two_elements(db, value_ty, Type::TypeVar(t_default)), + UnionType::from_two_elements(db, env, value_ty, Type::TypeVar(t_default)), ); [pop_sig, pop_with_typed_default_sig, pop_with_default_sig] @@ -585,7 +639,12 @@ fn synthesize_typed_dict_pop<'db>( .chain( typed_dict .supports_arbitrary_key_deletion(db) - .then(|| pop_overloads(KnownClass::Str.to_instance(db), typed_dict.value_type(db))) + .then(|| { + pop_overloads( + KnownClass::Str.to_instance(db, env), + typed_dict.value_type(db, env), + ) + }) .into_iter() .flatten(), ); @@ -601,6 +660,7 @@ fn synthesize_typed_dict_pop<'db>( /// Synthesize the `setdefault` method for a `TypedDict`. fn synthesize_typed_dict_setdefault<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typed_dict: TypedDictType<'db>, fields: TypedDictFields<'db>, ) -> Type<'db> { @@ -623,17 +683,20 @@ fn synthesize_typed_dict_setdefault<'db>( }) .chain( typed_dict - .arbitrary_key_mutation_type(db) + .arbitrary_key_mutation_type(db, env) .map(|default_ty| { let parameters = [ Parameter::positional_only(Some(Name::new_static("self"))) .with_annotated_type(instance_ty), Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(KnownClass::Str.to_instance(db)), + .with_annotated_type(KnownClass::Str.to_instance(db, env)), Parameter::positional_only(Some(Name::new_static("default"))) .with_annotated_type(default_ty), ]; - Signature::new(Parameters::standard(parameters), typed_dict.value_type(db)) + Signature::new( + Parameters::standard(parameters), + typed_dict.value_type(db, env), + ) }), ); @@ -663,20 +726,23 @@ fn synthesize_typed_dict_no_argument_method<'db>( /// Synthesize `items`, `keys`, or `values` for a closed or extra-items `TypedDict`. fn synthesize_typed_dict_view_method<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typed_dict: TypedDictType<'db>, view_name: &str, ) -> Type<'db> { - let return_ty = known_module_symbol(db, KnownModule::CollectionsAbcInternal, view_name) + let return_ty = known_module_symbol(db, env, KnownModule::CollectionsAbcInternal, view_name) .place .ignore_possibly_undefined() .and_then(Type::as_class_literal) .map(|class| { class.apply_specialization(db, |generic_context| { - generic_context - .specialize(db, &[typed_dict.key_type(db), typed_dict.value_type(db)]) + generic_context.specialize( + db, + &[typed_dict.key_type(db, env), typed_dict.value_type(db, env)], + ) }) }) - .and_then(|class| Type::from(class).to_instance_approximation(db)) + .and_then(|class| Type::from(class).to_instance_approximation(db, env)) .unwrap_or_else(Type::unknown); synthesize_typed_dict_no_argument_method(db, typed_dict, return_ty) @@ -685,6 +751,7 @@ fn synthesize_typed_dict_view_method<'db>( /// Synthesize a merge operator (`__or__`, `__ror__`, or `__ior__`) for a `TypedDict`. fn synthesize_typed_dict_merge<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, instance_ty: Type<'db>, name: &str, ) -> Type<'db> { @@ -716,14 +783,18 @@ fn synthesize_typed_dict_merge<'db>( instance_ty }; - let dict_param_ty = KnownClass::Dict - .to_specialized_instance(db, &[KnownClass::Str.to_instance(db), Type::any()]); + let dict_param_ty = KnownClass::Dict.to_specialized_instance( + db, + env, + &[KnownClass::Str.to_instance(db, env), Type::any()], + ); let dict_return_ty = KnownClass::Dict.to_specialized_instance( db, + env, &[ - KnownClass::Str.to_instance(db), - KnownClass::Object.to_instance(db), + KnownClass::Str.to_instance(db, env), + KnownClass::Object.to_instance(db, env), ], ); @@ -807,6 +878,7 @@ impl<'db> DynamicTypedDictAnchor<'db> { fn recursive_type_normalized_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -820,8 +892,8 @@ impl<'db> DynamicTypedDictAnchor<'db> { } => Some(Self::ScopeOffset { scope: *scope, offset: *offset, - schema: schema.recursive_type_normalized_impl(db, div, nested)?, - openness: openness.recursive_type_normalized_impl(db, div, nested)?, + schema: schema.recursive_type_normalized_impl(db, env, div, nested)?, + openness: openness.recursive_type_normalized_impl(db, env, div, nested)?, }), Self::Synthesized { scope, @@ -831,10 +903,10 @@ impl<'db> DynamicTypedDictAnchor<'db> { } => Some(Self::Synthesized { scope: *scope, range: *range, - schema: schema.recursive_type_normalized_impl(db, div, nested)?, + schema: schema.recursive_type_normalized_impl(db, env, div, nested)?, packs: packs .iter() - .map(|pack| pack.recursive_type_normalized_impl(db, div, true)) + .map(|pack| pack.recursive_type_normalized_impl(db, env, div, true)) .collect::>>()?, }), } @@ -867,6 +939,7 @@ impl<'db> DynamicTypedDictLiteral<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -874,7 +947,7 @@ impl<'db> DynamicTypedDictLiteral<'db> { db, self.name(db), self.anchor(db) - .recursive_type_normalized_impl(db, div, nested)?, + .recursive_type_normalized_impl(db, env, div, nested)?, self.typed_dict_module(db), )) } @@ -889,9 +962,10 @@ impl<'db> DynamicTypedDictLiteral<'db> { pub(super) fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { let DynamicTypedDictAnchor::Synthesized { scope, @@ -910,14 +984,14 @@ impl<'db> DynamicTypedDictLiteral<'db> { name.clone(), field .clone() - .apply_type_mapping_impl(db, type_mapping, tcx, visitor), + .apply_type_mapping_impl(db, env, type_mapping, tcx, visitor), ) }) .collect(); let mut pending = Vec::with_capacity(packs.len()); for pack in packs { - let pack = pack.apply_type_mapping_impl(db, type_mapping, tcx, visitor); + let pack = pack.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor); match pack.keyword_pack_fields(db) { Some(fields) => { for (name, field_ty) in fields { @@ -972,42 +1046,18 @@ impl<'db> DynamicTypedDictLiteral<'db> { /// Returns the range of the `TypedDict` call expression. pub(crate) fn header_range(self, db: &'db dyn Db) -> TextRange { - let scope = self.scope(db); - let file = scope.file(db); - let module = parsed_module(db, file).load(db); - - match self.anchor(db) { + let anchor = match self.anchor(db) { DynamicTypedDictAnchor::Definition(definition) => { - // For definitions, get the range from the definition's value. - // The TypedDict call is the value of the assignment. - definition - .kind(db) - .value(&module) - .expect( - "DynamicTypedDictAnchor::Definition should only be used for assignments", - ) - .range() + DynamicClassHeaderAnchor::Definition(*definition) } DynamicTypedDictAnchor::ScopeOffset { offset, .. } => { - // For dangling calls, compute the absolute index from the offset. - let scope_anchor = scope.node(db).node_index().unwrap_or(NodeIndex::from(0)); - let anchor_u32 = scope_anchor - .as_u32() - .expect("anchor should not be NodeIndex::NONE"); - let absolute_index = NodeIndex::from(anchor_u32 + offset); - - // Get the node and return its range. - let node: &ast::ExprCall = module - .get_by_index(absolute_index) - .try_into() - .expect("scope offset should point to ExprCall"); - node.range() + DynamicClassHeaderAnchor::ScopeOffset(*offset) } DynamicTypedDictAnchor::Synthesized { range, .. } => { - let _ = module; - *range + return *range; } - } + }; + dynamic_class_header_range(db, self.scope(db), anchor) } /// Returns a [`Span`] pointing to the `TypedDict` call expression. @@ -1060,7 +1110,8 @@ impl<'db> DynamicTypedDictLiteral<'db> { #[salsa::tracked(returns(ref), heap_size = ruff_memory_usage::heap_size)] pub(crate) fn mro(self, db: &'db dyn Db) -> Mro<'db> { let self_base = ClassBase::Class(ClassType::NonGeneric(self.into())); - let object_class = ClassType::object(db); + let env = ProgramEnvironment::from_scope(self.scope(db)); + let object_class = ClassType::object(db, &env); Mro::from([ self_base, ClassBase::TypedDict(self.typed_dict_module(db)), @@ -1068,19 +1119,20 @@ impl<'db> DynamicTypedDictLiteral<'db> { ]) } - /// Get the metaclass of this `TypedDict`. + /// Returns the metaclass of this `TypedDict`. /// /// `TypedDict`s use `type` as their metaclass. - #[expect(clippy::unused_self)] pub(crate) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { - KnownClass::Type.to_class_literal(db) + let env = ProgramEnvironment::from_scope(self.scope(db)); + KnownClass::Type.to_class_literal(db, &env) } /// Look up a class-level member defined directly on this `TypedDict` (not inherited). pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { + let env = ProgramEnvironment::from_scope(self.scope(db)); let typed_dict = TypedDictType::new(ClassType::NonGeneric(ClassLiteral::DynamicTypedDict(self))); - synthesize_typed_dict_method(db, typed_dict, name, || { + synthesize_typed_dict_method(db, &env, typed_dict, name, || { TypedDictFields::Dynamic(self.items(db)) }) .map(Member::definitely_declared) @@ -1091,6 +1143,7 @@ impl<'db> DynamicTypedDictLiteral<'db> { pub(crate) fn class_member( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, policy: MemberLookupPolicy, ) -> PlaceAndQualifiers<'db> { @@ -1104,6 +1157,7 @@ impl<'db> DynamicTypedDictLiteral<'db> { // This mirrors the behavior of StaticClassLiteral::typed_dict_member. typed_dict_class_member( db, + env, ClassType::NonGeneric(ClassLiteral::DynamicTypedDict(self)), self.typed_dict_module(db), policy, @@ -1112,8 +1166,36 @@ impl<'db> DynamicTypedDictLiteral<'db> { } } +/// Resolves members of a schema that has no defining `TypedDict` class. +pub(in crate::types) fn synthesized_typed_dict_class_member<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + synthesized: SynthesizedTypedDictType<'db>, + lookup_policy: MemberLookupPolicy, + name: &str, +) -> PlaceAndQualifiers<'db> { + let typed_dict = TypedDictType::Synthesized(synthesized); + + if let Some(member) = synthesize_typed_dict_method(db, env, typed_dict, name, || { + TypedDictFields::Dynamic(synthesized.items(db)) + }) { + return Member::definitely_declared(member).inner; + } + + typed_dict_inherited_class_member( + db, + env, + typed_dict, + TypedDictModule::Typing, + lookup_policy, + name, + || Type::TypedDict(typed_dict), + ) +} + pub(super) fn typed_dict_fallback_class_member<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, module: TypedDictModule, lookup_policy: MemberLookupPolicy, name: &str, @@ -1124,34 +1206,60 @@ pub(super) fn typed_dict_fallback_class_member<'db>( }; fallback - .to_class_literal(db) - .find_name_in_mro_with_policy(db, name, lookup_policy) + .to_class_literal(db, env) + .find_name_in_mro_with_policy(db, env, name, lookup_policy) .expect("Will return Some() when called on class literal") } pub(super) fn typed_dict_class_member<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: ClassType<'db>, module: TypedDictModule, lookup_policy: MemberLookupPolicy, name: &str, ) -> PlaceAndQualifiers<'db> { let self_class = class.class_literal(db); - let fallback_member = typed_dict_fallback_class_member(db, module, lookup_policy, name) + + typed_dict_inherited_class_member( + db, + env, + TypedDictType::new(class), + module, + lookup_policy, + name, + || determine_upper_bound(db, env, self_class, ClassBase::is_typed_dict), + ) +} + +fn typed_dict_inherited_class_member<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typed_dict: TypedDictType<'db>, + module: TypedDictModule, + lookup_policy: MemberLookupPolicy, + name: &str, + new_upper_bound: impl FnOnce() -> Type<'db>, +) -> PlaceAndQualifiers<'db> { + let fallback_member = typed_dict_fallback_class_member(db, env, module, lookup_policy, name) .map_type(|ty| { - let new_upper_bound = determine_upper_bound(db, self_class, ClassBase::is_typed_dict); - let mapping = TypeMapping::ReplaceSelf { new_upper_bound }; - ty.apply_type_mapping(db, &mapping, TypeContext::default()) + let mapping = TypeMapping::ReplaceSelf { + new_upper_bound: new_upper_bound(), + }; + ty.apply_type_mapping(db, env, &mapping, TypeContext::default()) }); if !fallback_member.is_undefined() { return fallback_member; } - if let Some(value_ty) = TypedDictType::new(class).dict_value_type(db) - && let Some(dict_class) = KnownClass::Dict - .to_specialized_class_type(db, &[KnownClass::Str.to_instance(db), value_ty]) + if let Some(value_ty) = typed_dict.dict_value_type(db, env) + && let Some(dict_class) = KnownClass::Dict.to_specialized_class_type( + db, + env, + &[KnownClass::Str.to_instance(db, env), value_ty], + ) { - let member = dict_class.class_member(db, name, lookup_policy); + let member = dict_class.class_member(db, env, name, lookup_policy); if !member.is_undefined() { return member; } diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index dc068d40e1..1058afa7f5 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -1,3 +1,4 @@ +use crate::ProgramEnvironment; use crate::types::class::CodeGeneratorKind; use crate::types::generics::{ApplySpecialization, Specialization}; use crate::types::mro::MroIterator; @@ -45,6 +46,7 @@ impl<'db> ClassBase<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -52,7 +54,7 @@ impl<'db> ClassBase<'db> { Self::Dynamic(dynamic) => Some(Self::Dynamic(dynamic.recursive_type_normalized())), Self::Divergent(_) => Some(self), Self::Class(class) => Some(Self::Class( - class.recursive_type_normalized_impl(db, div, nested)?, + class.recursive_type_normalized_impl(db, env, div, nested)?, )), Self::Any | Self::Protocol | Self::Generic | Self::TypedDict(_) => Some(self), } @@ -79,8 +81,8 @@ impl<'db> ClassBase<'db> { } /// Return a `ClassBase` representing the class `builtins.object` - pub(super) fn object(db: &'db dyn Db) -> Self { - Self::Class(ClassType::object(db)) + pub(super) fn object(db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Self { + Self::Class(ClassType::object(db, env)) } pub(super) const fn is_typed_dict(self) -> bool { @@ -113,13 +115,14 @@ impl<'db> ClassBase<'db> { /// Convert an explicit base while preserving a direct use of the `Any` special form. pub(super) fn try_from_explicit_base( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, subclass: Option>, ) -> Option { if matches!(ty, Type::SpecialForm(SpecialFormType::Any)) { Some(Self::Any) } else { - Self::try_from_type(db, ty, subclass) + Self::try_from_type(db, env, ty, subclass) } } @@ -128,23 +131,26 @@ impl<'db> ClassBase<'db> { /// Return `None` if `ty` is not an acceptable type for a class base. pub(super) fn try_from_type( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, subclass: Option>, ) -> Option { // basedpython: a hole nothing bounded is the gradual type it replaced, and a gradual base // is a base - if let Some(gradual) = crate::types::inferred_signature::gradual_hole(db, ty) { - return Self::try_from_type(db, gradual, subclass); + if let Some(gradual) = crate::types::inferred_signature::gradual_hole(db, env, ty) { + return Self::try_from_type(db, env, gradual, subclass); } match ty { // parameter-only marker; behaves as the type a body sees (bound of `Key`) Type::Overlapping(overlapping) => { - Self::try_from_type(db, overlapping.value_type(db), subclass) + Self::try_from_type(db, env, overlapping.value_type(db, env), subclass) } Type::Restricted(restricted) => { - Self::try_from_type(db, restricted.value_type(db), subclass) + Self::try_from_type(db, env, restricted.value_type(db), subclass) + } + Type::Deferred(deferred) => { + Self::try_from_type(db, env, deferred.reduced(db, env), subclass) } - Type::Deferred(deferred) => Self::try_from_type(db, deferred.reduced(db), subclass), Type::Dynamic(dynamic) => Some(Self::Dynamic(dynamic)), Type::Divergent(divergent) => Some(Self::Divergent(divergent)), Type::ClassLiteral(literal) => Some(Self::Class(literal.default_specialization(db))), @@ -152,7 +158,7 @@ impl<'db> ClassBase<'db> { Type::NominalInstance(instance) if instance.has_known_class(db, KnownClass::GenericAlias) => { - Self::try_from_type(db, todo_type!("GenericAlias instance"), subclass) + Self::try_from_type(db, env, todo_type!("GenericAlias instance"), subclass) } Type::SubclassOf(subclass_of) => subclass_of .subclass_of() @@ -162,9 +168,9 @@ impl<'db> ClassBase<'db> { let valid_element = inter .positive(db) .iter() - .find_map(|elem| ClassBase::try_from_type(db, *elem, subclass))?; + .find_map(|elem| ClassBase::try_from_type(db, env, *elem, subclass))?; - if ty.is_disjoint_from(db, KnownClass::Type.to_instance(db)) { + if ty.is_disjoint_from(db, env, KnownClass::Type.to_instance(db, env)) { None } else { Some(valid_element) @@ -175,7 +181,7 @@ impl<'db> ClassBase<'db> { Type::UnsafeUnion(unsafe_union) => unsafe_union .elements(db) .iter() - .find_map(|element| ClassBase::try_from_type(db, *element, subclass)), + .find_map(|element| ClassBase::try_from_type(db, env, *element, subclass)), Type::Union(union) => { if let Some(module) = TypedDictModule::from_type(db, ty) { return Some(ClassBase::TypedDict(module)); @@ -199,7 +205,7 @@ impl<'db> ClassBase<'db> { if union .elements(db) .iter() - .all(|elem| ClassBase::try_from_type(db, *elem, subclass).is_some()) + .all(|elem| ClassBase::try_from_type(db, env, *elem, subclass).is_some()) { Some(ClassBase::Dynamic(*dynamic)) } else { @@ -212,10 +218,10 @@ impl<'db> ClassBase<'db> { // in which case we want to treat `Never` in a forgiving way and silence diagnostics Type::Never => Some(ClassBase::unknown()), - Type::TypeAlias(alias) => Self::try_from_type(db, alias.value_type(db), subclass), + Type::TypeAlias(alias) => Self::try_from_type(db, env, alias.value_type(db), subclass), Type::NewTypeInstance(newtype) => { - ClassBase::try_from_type(db, newtype.concrete_base_type(db), subclass) + ClassBase::try_from_type(db, env, newtype.concrete_base_type(db), subclass) } Type::PropertyInstance(_) @@ -242,6 +248,10 @@ impl<'db> ClassBase<'db> { Type::KnownInstance(known_instance) => match known_instance { KnownInstanceType::SubscriptedGeneric(_) => Some(Self::Generic), KnownInstanceType::SubscriptedProtocol(_) => Some(Self::Protocol), + // A class inheriting from a newtype would make intuitive sense, but newtype + // wrappers are just identity callables at runtime, so this sort of inheritance + // doesn't work and isn't allowed. + KnownInstanceType::NewType(_) => None, KnownInstanceType::TypeAliasType(_) | KnownInstanceType::TypeVar(_) | KnownInstanceType::Deprecated(_) @@ -258,24 +268,19 @@ impl<'db> ClassBase<'db> { | KnownInstanceType::NamedTupleSpec(_) | KnownInstanceType::Sentinel(_) | KnownInstanceType::Range { .. } - // A class inheriting from a newtype would make intuitive sense, but newtype - // wrappers are just identity callables at runtime, so this sort of inheritance - // doesn't work and isn't allowed. - | KnownInstanceType::NewType(_) | KnownInstanceType::FunctoolsPartial(_) | KnownInstanceType::FunctoolsPartialCall(_) => None, - KnownInstanceType::TypeGenericAlias(_) => { - Self::try_from_type(db, KnownClass::Type.to_class_literal(db), subclass) - } - KnownInstanceType::Annotated(ty) => { - match ty.inner(db) { - Type::Dynamic(dynamic) => Some(Self::Dynamic(dynamic)), - Type::NominalInstance(instance) => { - Some(Self::Class(instance.class(db))) - } - _ => None, - } - } + KnownInstanceType::TypeGenericAlias(_) => Self::try_from_type( + db, + env, + KnownClass::Type.to_class_literal(db, env), + subclass, + ), + KnownInstanceType::Annotated(ty) => match ty.inner(db) { + Type::Dynamic(dynamic) => Some(Self::Dynamic(dynamic)), + Type::NominalInstance(instance) => Some(Self::Class(instance.class(db, env))), + _ => None, + }, }, Type::SpecialForm(special_form) => match special_form { @@ -320,8 +325,10 @@ impl<'db> ClassBase<'db> { let fields = class.own_fields(db, None, CodeGeneratorKind::NamedTuple); Self::try_from_type( db, + env, TupleType::heterogeneous( db, + env, fields.values().map(|field| field.declared_ty), )? .to_class_type(db) @@ -331,21 +338,31 @@ impl<'db> ClassBase<'db> { } // TODO: Classes inheriting from `typing.Type` also have `Generic` in their MRO - SpecialFormType::Type => { - Self::try_from_type(db, KnownClass::Type.to_class_literal(db), subclass) - } - - SpecialFormType::Tuple => { - Self::try_from_type(db, KnownClass::Tuple.to_class_literal(db), subclass) - } - - SpecialFormType::LegacyStdlibAlias(alias) => { - Self::try_from_type(db, alias.aliased_class().to_class_literal(db), subclass) - } + SpecialFormType::Type => Self::try_from_type( + db, + env, + KnownClass::Type.to_class_literal(db, env), + subclass, + ), + + SpecialFormType::Tuple => Self::try_from_type( + db, + env, + KnownClass::Tuple.to_class_literal(db, env), + subclass, + ), + + SpecialFormType::LegacyStdlibAlias(alias) => Self::try_from_type( + db, + env, + alias.aliased_class().to_class_literal(db, env), + subclass, + ), SpecialFormType::TypingCallable | SpecialFormType::CollectionsAbcCallable => { Self::try_from_type( db, + env, todo_type!("Support for Callable as a base class"), subclass, ) @@ -367,27 +384,30 @@ impl<'db> ClassBase<'db> { } /// Return the metaclass of this class base. - pub(crate) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn metaclass(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match self { Self::Class(class) => class.metaclass(db), Self::Any => Type::Dynamic(DynamicType::Any), Self::Dynamic(dynamic) => Type::Dynamic(dynamic), Self::Divergent(divergent) => Type::Divergent(divergent), // TODO: all `Protocol` classes actually have `_ProtocolMeta` as their metaclass. - Self::Protocol | Self::Generic | Self::TypedDict(_) => KnownClass::Type.to_instance(db), + Self::Protocol | Self::Generic | Self::TypedDict(_) => { + KnownClass::Type.to_instance(db, env) + } } } fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { match self { Self::Class(class) => { - Self::Class(class.apply_type_mapping_impl(db, type_mapping, tcx, visitor)) + Self::Class(class.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor)) } Self::Any | Self::Dynamic(_) @@ -404,29 +424,38 @@ impl<'db> ClassBase<'db> { specialization: Option>, ) -> Self { if let Some(specialization) = specialization { + let env = + &ProgramEnvironment::from_program(specialization.generic_context(db).program(db)); let new_self = self.apply_type_mapping_impl( db, + env, &TypeMapping::ApplySpecialization(ApplySpecialization::Specialization( specialization, )), TypeContext::default(), - &ApplyTypeMappingVisitor::default(), + &ApplyTypeMappingVisitor::new(env), ); match specialization.materialization_kind(db) { None => new_self, - Some(materialization_kind) => new_self.materialize(db, materialization_kind), + Some(materialization_kind) => new_self.materialize(db, env, materialization_kind), } } else { self } } - fn materialize(self, db: &'db dyn Db, kind: MaterializationKind) -> Self { + fn materialize( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + kind: MaterializationKind, + ) -> Self { self.apply_type_mapping_impl( db, + env, &TypeMapping::Materialize(kind), TypeContext::default(), - &ApplyTypeMappingVisitor::default(), + &ApplyTypeMappingVisitor::new(env), ) } @@ -457,44 +486,54 @@ impl<'db> ClassBase<'db> { pub(super) fn mro( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, additional_specialization: Option>, ) -> impl Iterator> + Clone { match self { - ClassBase::Protocol => ClassBaseMroIterator::length_3(db, self, ClassBase::Generic), + ClassBase::Protocol => { + ClassBaseMroIterator::length_3(db, env, self, ClassBase::Generic) + } ClassBase::Any | ClassBase::Dynamic(_) | ClassBase::Divergent(_) | ClassBase::Generic - | ClassBase::TypedDict(_) => ClassBaseMroIterator::length_2(db, self), + | ClassBase::TypedDict(_) => ClassBaseMroIterator::length_2(db, env, self), ClassBase::Class(class) => { ClassBaseMroIterator::from_class(db, class, additional_specialization) } } } - pub(super) fn display(self, db: &'db dyn Db) -> impl std::fmt::Display { - self.display_with(db, DisplaySettings::default()) + pub(super) fn display( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> impl std::fmt::Display { + self.display_with(db, env, DisplaySettings::default()) } - pub(super) fn display_with( + pub(super) fn display_with<'env>( self, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, display_settings: DisplaySettings<'db>, - ) -> impl std::fmt::Display { - struct ClassBaseDisplay<'db> { + ) -> impl std::fmt::Display + 'env { + struct ClassBaseDisplay<'env, 'db> { db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, base: ClassBase<'db>, settings: DisplaySettings<'db>, } - impl std::fmt::Display for ClassBaseDisplay<'_> { + impl std::fmt::Display for ClassBaseDisplay<'_, '_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let db = self.db; match self.base { ClassBase::Any => f.write_str("Any"), ClassBase::Dynamic(dynamic) => dynamic.fmt(f), ClassBase::Divergent(_) => f.write_str("Divergent"), ClassBase::Class(class) => Type::from(class) - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt(f), ClassBase::Protocol => f.write_str("typing.Protocol"), ClassBase::Generic => f.write_str("typing.Generic"), @@ -505,6 +544,7 @@ impl<'db> ClassBase<'db> { ClassBaseDisplay { db, + env, base: self, settings: display_settings, } @@ -547,13 +587,24 @@ enum ClassBaseMroIterator<'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()) + fn length_2( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + first_element: ClassBase<'db>, + ) -> Self { + ClassBaseMroIterator::Length2([first_element, ClassBase::object(db, env)].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()) + fn length_3( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + element_1: ClassBase<'db>, + element_2: ClassBase<'db>, + ) -> Self { + ClassBaseMroIterator::Length3( + [element_1, element_2, ClassBase::object(db, env)].into_iter(), + ) } /// Iterate over the MRO of an arbitrary class. The MRO may be of any length. diff --git a/crates/ty_python_semantic/src/types/conformance.rs b/crates/ty_python_semantic/src/types/conformance.rs index 561b9ca5d9..9799d1e6ac 100644 --- a/crates/ty_python_semantic/src/types/conformance.rs +++ b/crates/ty_python_semantic/src/types/conformance.rs @@ -39,6 +39,7 @@ use ruff_db::files::File; use ty_module_resolver::{ModuleName, resolve_module}; use crate::Db; +use crate::types::ProgramEnvironment; use crate::types::Type; use crate::types::class::{ClassLiteral, ClassType, StaticClassLiteral}; use crate::types::context::InferContext; @@ -47,6 +48,7 @@ use crate::types::extensions::{ applicable_extensions, backing_function_name, extended_class, extension_applies, extensions_in_module, own_member, }; +use ty_module_resolver::ImportingFile; /// the protocols (or abstract classes) a conformance extension declares its /// target conforms to — the extension's explicit bases, which is exactly where @@ -125,7 +127,12 @@ pub fn declares_conformances(db: &dyn Db, file: File) -> bool { pub(crate) fn eagerly_imported_modules(db: &dyn Db, file: File) -> Vec { let mut eager = Vec::new(); for module_name in imported_module_names(db, file) { - let Some(target) = resolve_module(db, file, &module_name).and_then(|m| m.file(db)) else { + let Some(target) = resolve_module( + db, + ImportingFile::File(file, db.program_file(file).resolver_environment(db)), + &module_name, + ) + .and_then(|m| m.file(db)) else { continue; }; if target != file && *declares_conformances(db, target) { @@ -137,7 +144,7 @@ pub(crate) fn eagerly_imported_modules(db: &dyn Db, file: File) -> Vec { /// every module named by an `import` or a `from ... import` in `file` fn imported_module_names(db: &dyn Db, file: File) -> Vec { - ty_python_core::semantic_index(db, file) + ty_python_core::semantic_index(db, db.program_file(file)) .imported_modules() .cloned() .chain( @@ -157,6 +164,7 @@ fn imported_module_names(db: &dyn Db, file: File) -> Vec { /// they are reached through the conforming type pub(crate) fn conformance_for<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, receiver_class: ClassType<'db>, protocol: ClassLiteral<'db>, @@ -165,7 +173,7 @@ pub(crate) fn conformance_for<'db>( if declared.class_literal(db) != protocol { continue; } - if extension_applies(db, extension, receiver_class).is_some() { + if extension_applies(db, env, extension, receiver_class).is_some() { return Some(declared); } } @@ -182,6 +190,7 @@ pub(crate) fn conformance_for<'db>( /// no file to ask pub(crate) fn repair_with_conformance<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, source: Type<'db>, target: Type<'db>, @@ -194,18 +203,18 @@ pub(crate) fn repair_with_conformance<'db>( return None; } // a repair only ever *adds* an assignment that fails without it - if source.is_assignable_to(db, target) { + if source.is_assignable_to(db, env, target) { return None; } // a use-site modifier says nothing about which class the value is an // instance of, so `final Widget` finds `Widget`'s conformances - let source_class = source.erase_restriction(db).nominal_class(db)?; + let source_class = source.erase_restriction(db).nominal_class(db, env)?; for &(extension, protocol) in conformances { - if extension_applies(db, extension, source_class).is_none() { + if extension_applies(db, env, extension, source_class).is_none() { continue; } - if Type::instance(db, protocol).is_assignable_to(db, target) { + if Type::instance(db, env, protocol).is_assignable_to(db, env, target) { return Some(protocol); } } @@ -406,6 +415,7 @@ pub(crate) fn validate_conformance_declaration<'db>( extension: StaticClassLiteral<'db>, class_node: &ast::StmtClassDef, ) { + let env = context.program_environment(); let db = context.db(); let declared = declared_conformances(db, extension); if declared.is_empty() { @@ -431,17 +441,20 @@ pub(crate) fn validate_conformance_declaration<'db>( if let Some(builder) = context.report_lint(&INVALID_CONFORMANCE, node.range()) { let mut diagnostic = builder.into_diagnostic("a conformance list names interfaces".to_string()); - diagnostic.info(format_args!("`{}` is not a class", base_ty.display(db))); + diagnostic.info(format_args!( + "`{}` is not a class", + base_ty.display(db, env) + )); } continue; }; let interface = &interface; - let interface_instance = Type::instance(db, *interface); + let interface_instance = Type::instance(db, env, *interface); if !is_conformable(db, *interface) { if let Some(builder) = context.report_lint(&INVALID_CONFORMANCE, node.range()) { let mut diagnostic = builder.into_diagnostic(format_args!( "`{}` is not a protocol", - interface_instance.display(db), + interface_instance.display(db, env), )); diagnostic.info( "a conformance names a protocol; an abstract class carries concrete members a \ @@ -459,7 +472,7 @@ pub(crate) fn validate_conformance_declaration<'db>( let mut diagnostic = builder.into_diagnostic(format_args!( "`{}` is already conformed to `{}` here", target.name(db), - interface_instance.display(db), + interface_instance.display(db, env), )); diagnostic.info(format_args!( "the other conformance is declared in `{}`", @@ -560,29 +573,29 @@ pub(crate) fn validate_conformance_declaration<'db>( // `def show(self) -> int` silently answered a `-> str` requirement for requirement in interface_requirements(db, *interface) { let name = requirement.as_str(); - let supplied = bound_own_member(db, extension, name) - .or_else(|| bound_target_member(db, target, name)); + let supplied = bound_own_member(db, env, extension, name) + .or_else(|| bound_target_member(db, env, target, name)); let (Some(supplied), Some(expected)) = ( supplied, interface_instance - .member(db, name) + .member(db, env, name) .place .ignore_possibly_undefined() .map(|expected| shed_receiver(db, expected)), ) else { continue; }; - if !supplied.is_assignable_to(db, expected) + if !supplied.is_assignable_to(db, env, expected) && let Some(builder) = context.report_lint(&INVALID_CONFORMANCE, node.range()) { let mut diagnostic = builder.into_diagnostic(format_args!( "`{name}` does not match the member `{}` declares", - interface_instance.display(db), + interface_instance.display(db, env), )); diagnostic.info(format_args!( "expected `{}`, found `{}`", - expected.display(db), - supplied.display(db), + expected.display(db, env), + supplied.display(db, env), )); } } @@ -603,7 +616,7 @@ pub(crate) fn validate_conformance_declaration<'db>( name, ) .is_none() - && !target_declares(db, target, name) + && !target_declares(db, env, target, name) }) .map(|requirement| format!("`{requirement}`")) .collect(); @@ -613,7 +626,7 @@ pub(crate) fn validate_conformance_declaration<'db>( let mut diagnostic = builder.into_diagnostic(format_args!( "`{}` does not answer every member of `{}`", target.name(db), - interface_instance.display(db), + interface_instance.display(db, env), )); diagnostic.info(format_args!("missing: {}", missing.join(", "))); diagnostic.help( @@ -627,14 +640,17 @@ pub(crate) fn validate_conformance_declaration<'db>( /// extended type — the shape a caller reaching it through the interface gets fn bound_own_member<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, extension: StaticClassLiteral<'db>, name: &str, ) -> Option> { let member = own_member(db, extension, name)?; - let receiver = Type::instance(db, super::extensions::body_view_class(db, extension)?); + let receiver = Type::instance(db, env, super::extensions::body_view_class(db, extension)?); let bound = member - .try_call_dunder_get(db, Some(receiver), receiver.to_meta_type(db)) - .map_or(member, |(bound, _)| bound); + .try_call_dunder_get(db, env, Some(receiver), receiver.to_meta_type(db, env)) + .ok() + .flatten() + .map_or(member, |result| result.return_type); Some(shed_receiver(db, bound)) } @@ -653,12 +669,13 @@ fn shed_receiver<'db>(db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { /// the member `name` the conforming type answers itself, bound against it fn bound_target_member<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, target: ClassLiteral<'db>, name: &str, ) -> Option> { - let receiver = Type::instance(db, target.unknown_specialization(db)); + let receiver = Type::instance(db, env, target.unknown_specialization(db)); let member = receiver - .member(db, name) + .member(db, env, name) .place .ignore_possibly_undefined()?; Some(shed_receiver(db, member)) @@ -696,12 +713,17 @@ fn conflicting_conformance<'db>( /// does the conforming type already answer `name` itself, without the /// conformance having to supply it? -fn target_declares<'db>(db: &'db dyn Db, target: ClassLiteral<'db>, name: &str) -> bool { +fn target_declares<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + target: ClassLiteral<'db>, + name: &str, +) -> bool { // `object`'s members count: every value really does answer `__str__` and // friends at runtime, and excluding them reported a `Stringy` protocol as // unanswered by a class that satisfies it perfectly well - !Type::instance(db, target.unknown_specialization(db)) - .member(db, name) + !Type::instance(db, env, target.unknown_specialization(db)) + .member(db, env, name) .place .is_undefined() } diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs index 783d2f6f31..50f7c6fc8a 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -86,7 +86,7 @@ //! //! [duboc]: https://gldubc.github.io/#thesis -use std::cell::{Cell, Ref, RefCell}; +use std::cell::{Cell, RefCell}; use std::cmp::Ordering; use std::collections::VecDeque; use std::convert::Infallible; @@ -101,12 +101,13 @@ use itertools::Itertools; use ruff_index::{Idx, IndexVec, newtype_index}; use rustc_hash::{FxHashMap, FxHashSet}; use smallvec::SmallVec; +use ty_python_core::Program; use ty_python_core::rank::RankBitBox; use ty_static::EnvVars; use crate::types::class::GenericAlias; -use crate::types::generics::InferableTypeVars; -use crate::types::typevar::{BoundTypeVarIdentity, walk_bound_type_var_type}; +use crate::types::constraints::support::{Support, SupportId}; +use crate::types::typevar::{BoundTypeVarIdentity, TypeVarSet}; use crate::types::variance::VarianceInferable; use crate::types::visitor::{ TypeCollector, TypeKind, TypeVisitor, any_over_type, walk_non_atomic_type, @@ -116,7 +117,9 @@ use crate::types::{ ApplyTypeMappingVisitor, BoundTypeVarInstance, IntersectionType, Type, TypeContext, TypeMapping, TypePair, TypeVarBoundOrConstraints, TypeVarVariance, UnionType, }; -use crate::{Db, FxIndexMap, FxIndexSet, FxOrderSet}; +use crate::{Db, FxIndexMap, FxIndexSet, FxOrderSet, ProgramEnvironment}; + +mod support; /// An extension trait for building constraint sets from [`Option`] values. pub(crate) trait OptionConstraintsExtension { @@ -170,8 +173,8 @@ pub(crate) trait IteratorConstraintsExtension { /// Returns the constraints under which any element of the iterator holds. /// /// This method short-circuits; if we encounter any element that - /// [`is_always_satisfied`][ConstraintSet::is_always_satisfied], then the overall result - /// must be as well, and we stop consuming elements from the iterator. + /// [`is_trivially_always_satisfied`][ConstraintSet::is_trivially_always_satisfied], then the + /// overall result must be as well, and we stop consuming elements from the iterator. fn when_any<'db, 'c>( self, db: &'db dyn Db, @@ -182,8 +185,8 @@ pub(crate) trait IteratorConstraintsExtension { /// Returns the constraints under which every element of the iterator holds. /// /// This method short-circuits; if we encounter any element that - /// [`is_never_satisfied`][ConstraintSet::is_never_satisfied], then the overall result - /// must be as well, and we stop consuming elements from the iterator. + /// [`is_trivially_never_satisfied`][ConstraintSet::is_trivially_never_satisfied], then the + /// overall result must be as well, and we stop consuming elements from the iterator. fn when_all<'db, 'c>( self, db: &'db dyn Db, @@ -198,38 +201,36 @@ where { fn when_any<'db, 'c>( self, - db: &'db dyn Db, + _db: &'db dyn Db, builder: &'c ConstraintSetBuilder<'db>, mut f: impl FnMut(T) -> ConstraintSet<'db, 'c>, ) -> ConstraintSet<'db, 'c> { - let node = NodeId::distributed_or( - db, + let (node, source_order) = NodeId::distributed_or( builder, self.map(|element| { let constraint = f(element); constraint.verify_builder(builder); - constraint.node + (constraint.node, constraint.source_order) }), ); - ConstraintSet::from_node(builder, node) + ConstraintSet::from_node(builder, node, source_order) } fn when_all<'db, 'c>( self, - db: &'db dyn Db, + _db: &'db dyn Db, builder: &'c ConstraintSetBuilder<'db>, mut f: impl FnMut(T) -> ConstraintSet<'db, 'c>, ) -> ConstraintSet<'db, 'c> { - let node = NodeId::distributed_and( - db, + let (node, source_order) = NodeId::distributed_and( builder, self.map(|element| { let constraint = f(element); constraint.verify_builder(builder); - constraint.node + (constraint.node, constraint.source_order) }), ); - ConstraintSet::from_node(builder, node) + ConstraintSet::from_node(builder, node, source_order) } } @@ -247,22 +248,31 @@ where #[derive(Clone, Debug, Eq, Hash, PartialEq, get_size2::GetSize, salsa::SalsaValue)] pub struct OwnedConstraintSet<'db> { node: NodeId, + source_order: Option, inner: Option>>, } #[derive(Clone, Debug, Eq, Hash, PartialEq, get_size2::GetSize, salsa::SalsaValue)] struct OwnedConstraintSetInner<'db> { constraints: Box<[Constraint<'db>]>, + constraint_supports: Box<[SupportId]>, constraint_indices: RankBitBox, typevars: IndexVec>, nodes: Box<[InteriorNodeData]>, + node_supports: Box<[SupportId]>, node_indices: RankBitBox, + supports: Box<[Support]>, + support_indices: RankBitBox, + /// A dense, canonical source-order tree whose IDs are independent of sidecar construction + /// history. + source_orders: Box<[SourceOrder]>, } impl Default for OwnedConstraintSet<'_> { fn default() -> Self { Self { node: ALWAYS_FALSE, + source_order: None, inner: None, } } @@ -272,6 +282,7 @@ impl<'db> OwnedConstraintSet<'db> { pub(crate) fn always() -> Self { Self { node: ALWAYS_TRUE, + source_order: None, inner: None, } } @@ -302,17 +313,27 @@ impl<'db> OwnedConstraintSet<'db> { let builder = ConstraintSetBuilder { storage: RefCell::new(storage), }; - let set = ConstraintSet::from_node(&builder, self.node); + let set = ConstraintSet::from_node(&builder, self.node, self.source_order); f(&builder, set) } + /// Returns the types in constraints that are still reachable from the decision diagram. + /// + /// Source ordering also retains quantified-away constraints to preserve binding order, but + /// their type variables must not participate in semantic walks or callable freshening. pub(crate) fn types(&self) -> impl Iterator> + '_ { self.inner.iter().flat_map(|inner| { - inner.constraints.iter().flat_map(|constraint| { - std::iter::once(Type::TypeVar(constraint.typevar)) - .chain(constraint.bounds.lower) - .chain(constraint.bounds.upper) - }) + inner + .nodes + .iter() + .map(|node| node.constraint) + .unique() + .map(|constraint| inner.constraints[inner.retained_constraint_index(constraint)]) + .flat_map(|constraint| { + std::iter::once(Type::TypeVar(constraint.typevar)) + .chain(constraint.bounds.lower) + .chain(constraint.bounds.upper) + }) }) } } @@ -337,6 +358,16 @@ impl OwnedConstraintSetInner<'_> { ); self.constraint_indices.rank(index) as usize } + + fn retained_support_index(&self, id: SupportId) -> usize { + let index = id.index(); + debug_assert_eq!( + self.support_indices.get_bit(index), + Some(true), + "should not access constraint set support that was marked unused", + ); + self.support_indices.rank(index) as usize + } } /// A set of constraints under which a type property holds. @@ -354,6 +385,10 @@ pub struct ConstraintSet<'db, 'c> { /// The BDD representing this constraint set node: NodeId, + /// The source ordering of the constraints in this constraint set. Will be `None` for terminal + /// nodes. + source_order: Option, + /// A reference to the builder that holds the storage for this constraint set's BDD builder: &'c ConstraintSetBuilder<'db>, @@ -362,20 +397,25 @@ pub struct ConstraintSet<'db, 'c> { } impl<'db, 'c> ConstraintSet<'db, 'c> { - fn from_node(builder: &'c ConstraintSetBuilder<'db>, node: NodeId) -> Self { + fn from_node( + builder: &'c ConstraintSetBuilder<'db>, + node: NodeId, + source_order: Option, + ) -> Self { Self { node, + source_order, builder, _invariant: PhantomData, } } fn never(builder: &'c ConstraintSetBuilder<'db>) -> Self { - Self::from_node(builder, ALWAYS_FALSE) + Self::from_node(builder, ALWAYS_FALSE, None) } fn always(builder: &'c ConstraintSetBuilder<'db>) -> Self { - Self::from_node(builder, ALWAYS_TRUE) + Self::from_node(builder, ALWAYS_TRUE, None) } pub(crate) fn from_bool(builder: &'c ConstraintSetBuilder<'db>, b: bool) -> Self { @@ -389,46 +429,50 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { /// Returns a constraint set that constrains a typevar to an explicit range of types. pub(crate) fn constrain_typevar( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, builder: &'c ConstraintSetBuilder<'db>, typevar: BoundTypeVarInstance<'db>, lower: Type<'db>, upper: Type<'db>, ) -> Self { - Self::constrain_typevar_with_bounds(db, builder, typevar, Some(lower), Some(upper)) + Self::constrain_typevar_with_bounds(db, env, builder, typevar, Some(lower), Some(upper)) } /// Returns a constraint set that constrains a typevar with explicit lower and/or upper bounds. pub(crate) fn constrain_typevar_with_bounds( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, builder: &'c ConstraintSetBuilder<'db>, typevar: BoundTypeVarInstance<'db>, lower: Option>, upper: Option>, ) -> Self { - Self::from_node( - builder, - Constraint::new_node_with_bounds(db, builder, typevar, lower, upper), - ) + let mut storage = builder.storage.borrow_mut(); + let (node, source_order) = + Constraint::new_node_with_bounds(db, env, &mut storage, typevar, lower, upper); + Self::from_node(builder, node, source_order) } /// Returns a constraint set that constrains a typevar to be a supertype of `lower`. pub(crate) fn constrain_typevar_lower_bound( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, builder: &'c ConstraintSetBuilder<'db>, typevar: BoundTypeVarInstance<'db>, lower: Type<'db>, ) -> Self { - Self::constrain_typevar_with_bounds(db, builder, typevar, Some(lower), None) + Self::constrain_typevar_with_bounds(db, env, builder, typevar, Some(lower), None) } /// Returns a constraint set that constrains a typevar to be a subtype of `upper`. pub(crate) fn constrain_typevar_upper_bound( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, builder: &'c ConstraintSetBuilder<'db>, typevar: BoundTypeVarInstance<'db>, upper: Type<'db>, ) -> Self { - Self::constrain_typevar_with_bounds(db, builder, typevar, None, Some(upper)) + Self::constrain_typevar_with_bounds(db, env, builder, typevar, None, Some(upper)) } /// Verifies that this constraint set was created by `builder` @@ -437,14 +481,41 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { debug_assert!(std::ptr::eq(self.builder, builder)); } - /// Returns whether this constraint set never holds - pub(crate) fn is_never_satisfied(self, db: &'db dyn Db) -> bool { - self.node.is_never_satisfied(db, self.builder) + /// Returns whether this constraint set never holds. + pub(crate) fn is_never_satisfied(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool { + let mut storage = self.builder.storage.borrow_mut(); + self.node + .is_never_satisfied(db, env, &mut storage, self.source_order) + } + + /// Returns whether this constraint set is the `never` terminal. + /// + /// A nonterminal constraint set can also never be satisfied, so `false` does not prove that + /// the set is satisfiable. Use [`Self::is_never_satisfied`] when false negatives are not + /// acceptable. + pub(crate) fn is_trivially_never_satisfied(self) -> bool { + self.node == ALWAYS_FALSE + } + + /// Returns whether this constraint set always holds. + #[inline] + pub(crate) fn is_always_satisfied( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> bool { + let mut storage = self.builder.storage.borrow_mut(); + self.node + .is_always_satisfied(db, env, &mut storage, self.source_order) } - /// Returns whether this constraint set always holds - pub(crate) fn is_always_satisfied(self, db: &'db dyn Db) -> bool { - self.node.is_always_satisfied(db, self.builder) + /// Returns whether this constraint set is the `always` terminal. + /// + /// A nonterminal constraint set can also always be satisfied, so `false` does not prove that + /// the set is not always satisfied. Use [`Self::is_always_satisfied`] when false negatives are + /// not acceptable. + pub(crate) fn is_trivially_always_satisfied(self) -> bool { + self.node == ALWAYS_TRUE } /// Returns the constraints under which `lhs` is a subtype of `rhs`, assuming that the @@ -453,12 +524,18 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { pub(crate) fn implies_subtype_of( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, builder: &'c ConstraintSetBuilder<'db>, lhs: Type<'db>, rhs: Type<'db>, ) -> Self { self.verify_builder(builder); - Self::from_node(builder, self.node.implies_subtype_of(db, builder, lhs, rhs)) + let mut storage = builder.storage.borrow_mut(); + let (node, extra_source_order) = + self.node + .implies_subtype_of(db, env, &mut storage, lhs, rhs); + let source_order = storage.ordered_source_order(self.source_order, extra_source_order); + Self::from_node(builder, node, source_order) } /// Returns whether this constraint set is satisfied by all of the typevars that it mentions. @@ -478,17 +555,19 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { pub(crate) fn satisfied_by_all_typevars( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, builder: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, ) -> bool { self.verify_builder(builder); - self.node.satisfied_by_all_typevars(db, builder, inferable) + let mut storage = builder.storage.borrow_mut(); + self.node + .satisfied_by_all_typevars(db, env, &mut storage, inferable, self.source_order) } /// Updates this constraint set to hold the union of itself and another constraint set. /// - /// In the result, `self` will appear before `other` according to the `source_order` of the BDD - /// nodes. + /// In the result's source order, `self` will appear before `other`. pub(crate) fn union( &mut self, _db: &'db dyn Db, @@ -496,14 +575,15 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { other: Self, ) -> Self { self.verify_builder(builder); - self.node = self.node.or_with_offset(builder, other.node); + let mut storage = builder.storage.borrow_mut(); + self.node = self.node.or(&mut storage, other.node); + self.source_order = storage.ordered_source_order(self.source_order, other.source_order); *self } /// Updates this constraint set to hold the intersection of itself and another constraint set. /// - /// In the result, `self` will appear before `other` according to the `source_order` of the BDD - /// nodes. + /// In the result's source order, `self` will appear before `other`. pub(crate) fn intersect( &mut self, _db: &'db dyn Db, @@ -511,22 +591,24 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { other: Self, ) -> Self { self.verify_builder(builder); - self.node = self.node.and_with_offset(builder, other.node); + let mut storage = builder.storage.borrow_mut(); + self.node = self.node.and(&mut storage, other.node); + self.source_order = storage.ordered_source_order(self.source_order, other.source_order); *self } /// Returns the negation of this constraint set. pub(crate) fn negate(self, _db: &'db dyn Db, builder: &'c ConstraintSetBuilder<'db>) -> Self { self.verify_builder(builder); - Self::from_node(builder, self.node.negate(builder)) + let mut storage = builder.storage.borrow_mut(); + Self::from_node(builder, self.node.negate(&mut storage), self.source_order) } /// Returns the intersection of this constraint set and another. The other constraint set is /// provided as a thunk, to implement short-circuiting: the thunk is not forced if the /// constraint set is already saturated. /// - /// In the result, `self` will appear before `other` according to the `source_order` of the BDD - /// nodes. + /// In the result's source order, `self` will appear before `other`. #[inline] pub(crate) fn and( mut self, @@ -535,7 +617,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { other: impl FnOnce() -> Self, ) -> Self { self.verify_builder(builder); - if !self.is_never_satisfied(db) { + if !self.is_trivially_never_satisfied() { let other = other(); other.verify_builder(builder); self.intersect(db, builder, other); @@ -547,8 +629,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { /// as a thunk, to implement short-circuiting: the thunk is not forced if the constraint set is /// already saturated. /// - /// In the result, `self` will appear before `other` according to the `source_order` of the BDD - /// nodes. + /// In the result's source order, `self` will appear before `other`. pub(crate) fn or( mut self, db: &'db dyn Db, @@ -556,7 +637,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { other: impl FnOnce() -> Self, ) -> Self { self.verify_builder(builder); - if !self.is_always_satisfied(db) { + if !self.is_trivially_always_satisfied() { let other = other(); other.verify_builder(builder); self.union(db, builder, other); @@ -566,8 +647,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { /// Returns a constraint set encoding that this constraint set implies another. /// - /// In the result, `self` will appear before `other` according to the `source_order` of the BDD - /// nodes. + /// In the result's source order, `self` will appear before `other`. pub(crate) fn implies( self, db: &'db dyn Db, @@ -579,8 +659,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { /// Returns a constraint set encoding that this constraint set is equivalent to another. /// - /// In the result, `self` will appear before `other` according to the `source_order` of the BDD - /// nodes. + /// In the result's source order, `self` will appear before `other`. pub(crate) fn iff( self, _db: &'db dyn Db, @@ -588,7 +667,10 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { other: Self, ) -> Self { self.verify_builder(builder); - Self::from_node(builder, self.node.iff_with_offset(builder, other.node)) + let mut storage = builder.storage.borrow_mut(); + let node = self.node.iff(&mut storage, other.node); + let source_order = storage.ordered_source_order(self.source_order, other.source_order); + Self::from_node(builder, node, source_order) } /// Reduces the set of inferable typevars for this constraint set. You provide the typevars that @@ -598,11 +680,17 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { pub(crate) fn reduce_inferable( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, builder: &'c ConstraintSetBuilder<'db>, - to_remove: InferableTypeVars<'db>, + to_remove: TypeVarSet<'db>, ) -> Self { self.verify_builder(builder); - Self::from_node(builder, self.node.exists(db, builder, to_remove)) + let mut storage = builder.storage.borrow_mut(); + let (node, derived_source_order) = + self.node + .exists(db, env, &mut storage, to_remove, self.source_order); + let source_order = storage.ordered_source_order(self.source_order, derived_source_order); + Self::from_node(builder, node, source_order) } /// Applies a type mapping to every constraint in this constraint set. @@ -611,12 +699,12 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { db: &'db dyn Db, type_mapping: &TypeMapping<'_, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { fn rebuild_node( - builder: &ConstraintSetBuilder<'_>, + storage: &mut ConstraintSetStorage<'_>, old_node: NodeId, - mapped_constraints: &FxHashMap, + mapped_constraints: &FxHashMap)>, mapped_nodes: &mut FxHashMap, ) -> NodeId { if old_node.is_terminal() { @@ -626,88 +714,117 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { return *mapped; } - let old_interior = builder.interior_node_data(old_node); - let condition = mapped_constraints[&old_interior.constraint] - .with_adjusted_source_order(builder, old_interior.source_order.saturating_sub(1)); + let old_interior = storage.interior_node_data(old_node); + let (condition, _) = mapped_constraints[&old_interior.constraint]; let if_true = rebuild_node( - builder, + storage, old_interior.if_true, mapped_constraints, mapped_nodes, ); let if_uncertain = rebuild_node( - builder, + storage, old_interior.if_uncertain, mapped_constraints, mapped_nodes, ); let if_false = rebuild_node( - builder, + storage, old_interior.if_false, mapped_constraints, mapped_nodes, ); - let mapped = condition.ite_uncertain(builder, if_true, if_uncertain, if_false); + let mapped = condition.ite_uncertain(storage, if_true, if_uncertain, if_false); mapped_nodes.insert(old_node, mapped); mapped } + let env = visitor.env; - let builder = self.builder; - let mut mapped_constraints = FxHashMap::default(); + // We have to collect this into a temporary vec since we can't hold an open borrow on the + // storage during the apply_type_mapping calls below, since they also need to borrow the + // storage. + let storage = self.builder.storage.borrow(); + let mut constraints = SmallVec::<[_; 8]>::new(); self.node - .for_each_unique_constraint(builder, &mut |constraint_id, _| { - if mapped_constraints.contains_key(&constraint_id) { - return; - } + .for_each_unique_constraint(&storage, &mut |constraint_id| { + let constraint = storage.constraint_data(constraint_id); + constraints.push((constraint_id, constraint)); + }); + drop(storage); - let constraint = builder.constraint_data(constraint_id); - let subject = Type::TypeVar(constraint.typevar).apply_type_mapping_impl( - db, - type_mapping, - tcx, - visitor, - ); - let lower = constraint - .bounds - .lower - .map(|lower| lower.apply_type_mapping_impl(db, type_mapping, tcx, visitor)); - let upper = constraint - .bounds - .upper - .map(|upper| upper.apply_type_mapping_impl(db, type_mapping, tcx, visitor)); + let mut mapped_constraints = FxHashMap::default(); + for (constraint_id, constraint) in constraints { + if mapped_constraints.contains_key(&constraint_id) { + continue; + } - let mapped = if let Type::TypeVar(typevar) = subject { - Constraint::new_node_with_bounds(db, builder, typevar, lower, upper) - } else { - let lower_holds = lower.map_or(ALWAYS_TRUE, |lower| { - builder - .load( - db, - &lower.when_constraint_set_assignable_to_owned(db, subject), - ) - .node - }); - let upper_holds = upper.map_or(ALWAYS_TRUE, |upper| { - builder - .load( - db, - &subject.when_constraint_set_assignable_to_owned(db, upper), - ) - .node - }); - lower_holds.and_with_offset(builder, upper_holds) + let subject = Type::TypeVar(constraint.typevar).apply_type_mapping_impl( + db, + env, + type_mapping, + tcx, + visitor, + ); + let lower = constraint + .bounds + .lower + .map(|lower| lower.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor)); + let upper = constraint + .bounds + .upper + .map(|upper| upper.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor)); + + let env = visitor.env; + let mut storage = self.builder.storage.borrow_mut(); + let mapped = if let Type::TypeVar(typevar) = subject { + Constraint::new_node_with_bounds(db, env, &mut storage, typevar, lower, upper) + } else { + let (lower_holds, lower_holds_source_order) = match lower { + Some(lower) => storage.load( + db, + env, + &lower.when_constraint_set_assignable_to_owned(db, env, subject), + ), + None => (ALWAYS_TRUE, None), }; - mapped_constraints.insert(constraint_id, mapped); - }); + let (upper_holds, upper_holds_source_order) = match upper { + Some(upper) => storage.load( + db, + env, + &subject.when_constraint_set_assignable_to_owned(db, env, upper), + ), + None => (ALWAYS_TRUE, None), + }; + ( + lower_holds.and(&mut storage, upper_holds), + storage + .ordered_source_order(lower_holds_source_order, upper_holds_source_order), + ) + }; + mapped_constraints.insert(constraint_id, mapped); + } + let mut storage = self.builder.storage.borrow_mut(); + let source_order = storage + .calculate_source_orders(self.source_order) + .into_iter() + .fold(None, |source_order, constraint| { + mapped_constraints.get(&constraint).map_or( + source_order, + |(_, mapped_source_order)| { + storage.ordered_source_order(source_order, *mapped_source_order) + }, + ) + }); Self::from_node( - builder, + self.builder, rebuild_node( - builder, + &mut storage, self.node, &mapped_constraints, &mut FxHashMap::default(), ), + source_order, ) } @@ -725,23 +842,20 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { pub(crate) fn for_all( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, builder: &'c ConstraintSetBuilder<'db>, - to_remove: InferableTypeVars<'db>, + to_remove: TypeVarSet<'db>, ) -> Self { self.verify_builder(builder); - if to_remove == InferableTypeVars::None { + if to_remove == TypeVarSet::None { return self; } // Universal and existential quantification are duals. Reusing existential abstraction // also keeps this operation on its cached, single-pass implementation. - Self::from_node( - builder, - self.node - .negate(builder) - .exists(db, builder, to_remove) - .negate(builder), - ) + self.negate(db, builder) + .reduce_inferable(db, env, builder, to_remove) + .negate(db, builder) } /// Computes solutions for each BDD path, using a caller-provided hook to select solutions. @@ -756,42 +870,102 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { pub(crate) fn solutions( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, builder: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, ) -> Solutions<'db> { - self.solutions_with(db, builder, inferable, |_variance, path_bound| { - PathBounds::default_solve(db, builder, path_bound) + self.solutions_with(db, env, builder, inferable, |_variance, path_bound| { + PathBounds::default_solve(db, env, builder, path_bound) }) } pub(crate) fn solutions_with( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, builder: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, choose: impl FnMut(TypeVarVariance, &PathBound<'db>) -> Result>, ()>, ) -> Solutions<'db> { self.verify_builder(builder); - self.node.solutions_with(db, builder, inferable, choose) + let mut storage = builder.storage.borrow_mut(); + let path_bounds = PathBounds::compute( + db, + env, + &mut storage, + self.node, + inferable, + self.source_order, + ); + drop(storage); + path_bounds.solve_with(choose) } - pub(crate) fn display(self, db: &'db dyn Db) -> impl Display { - self.node - .simplify_for_display(db, self.builder) - .display(db, self.builder) + pub(crate) fn display( + self, + db: &'db dyn Db, + env: &'c ProgramEnvironment<'db>, + ) -> impl Display + 'c { + struct DisplayConstraintSet<'c, 'db> { + node: NodeId, + db: &'db dyn Db, + env: &'c ProgramEnvironment<'db>, + builder: &'c ConstraintSetBuilder<'db>, + } + + impl Display for DisplayConstraintSet<'_, '_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let storage = self.builder.storage.borrow(); + Display::fmt(&self.node.display(self.db, self.env, &storage), f) + } + } + + DisplayConstraintSet { + node: self.node, + db, + env, + builder: self.builder, + } } #[expect(dead_code)] // Keep this around for debugging purposes - pub(crate) fn display_graph<'a>( + fn display_graph<'a>( self, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, prefix: &'a dyn Display, ) -> impl Display + 'a where 'db: 'a, 'c: 'a, { - self.node.display_graph(db, self.builder, prefix) + struct DisplayConstraintSet<'a, 'c, 'db> { + node: NodeId, + prefix: &'a dyn Display, + db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, + builder: &'c ConstraintSetBuilder<'db>, + } + + impl Display for DisplayConstraintSet<'_, '_, '_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let storage = self.builder.storage.borrow(); + Display::fmt( + &self + .node + .display_graph(self.db, self.env, &storage, self.prefix), + f, + ) + } + } + + DisplayConstraintSet { + node: self.node, + prefix, + db, + env, + builder: self.builder, + } } } @@ -828,6 +1002,8 @@ pub(crate) struct ConstraintSetBuilder<'db> { storage: RefCell>, } +type ExistsCacheKey<'db> = (NodeId, TypeVarSet<'db>, Option); + #[derive(Debug, Default)] struct ConstraintSetStorage<'db> { /// Compacted owned storage overlaid onto this builder. This is used by @@ -856,6 +1032,19 @@ struct ConstraintSetStorage<'db> { /// The BDD nodes that appear in any of the constraint sets constructed in this builder. nodes: IndexVec, + supports: IndexVec, + constraint_supports: IndexVec, + node_supports: IndexVec, + + /// Encodes an ordering on the constraints in a constraint set, which is based on the order + /// that the constraints (or more accurately, the Python expressions they're derived from) + /// appear in the source code. This ensures that any union and intersections types that appear + /// in solutions are constructed in a stable (and source-consistent) order. + /// + /// This is encoded as a binary tree over [`ConstraintId`]s. A preorder traversal of that tree + /// defines the ordering. + source_orders: IndexVec, + // Everything below are the memoization tables for the arenas and for our BDD operations. constraint_cache: FxHashMap, ConstraintId>, typevar_cache: FxHashMap, TypeVarId>, @@ -863,17 +1052,19 @@ struct ConstraintSetStorage<'db> { /// Avoid repeatedly walking deep constraint bounds without imposing Salsa-query overhead on /// the many shallow bounds that are cheap to walk once. constraint_bound_depth_cache: FxHashMap, + source_order_cache: FxHashMap, constraint_implication_cache: FxHashMap<(ConstraintId, ConstraintId), bool>, /// Only caches completed top-level results. Recursive results depend on active path - /// assignments and must not use this cache. + /// assignments and must not use this cache. A BDD's satisfiability does not depend on the + /// source order used to traverse it. never_satisfied_cache: FxHashMap, negate_cache: FxHashMap, - or_cache: FxHashMap<(NodeId, NodeId, usize), NodeId>, - and_cache: FxHashMap<(NodeId, NodeId, usize), NodeId>, - exists_cache: FxHashMap<(NodeId, InferableTypeVars<'db>), NodeId>, - restrict_one_cache: FxHashMap<(NodeId, ConstraintAssignment), (NodeId, bool)>, - simplify_cache: FxHashMap, + or_cache: FxHashMap<(NodeId, NodeId), NodeId>, + and_cache: FxHashMap<(NodeId, NodeId), NodeId>, + /// Existential abstraction derives new constraints in source order and returns their + /// source-order sidecar, so distinct orderings of the same BDD must not share a cache entry. + exists_cache: FxHashMap, (NodeId, Option)>, single_sequent_cache: FxHashMap, pair_sequent_cache: FxHashMap<(ConstraintId, ConstraintId), SequentMap>, @@ -909,6 +1100,14 @@ impl ConstraintSetStorage<'_> { .zip(compacted.nodes.iter().copied()) .map(|(old_index, node)| (node, NodeId::from_usize(old_index))), ); + self.source_order_cache.extend( + compacted + .source_orders + .iter() + .copied() + .enumerate() + .map(|(index, source_order)| (source_order, SourceOrderId::from_usize(index))), + ); } fn adjusted_node_id(&self, id: NodeId) -> NodeId { @@ -925,6 +1124,20 @@ impl ConstraintSetStorage<'_> { id } + fn adjusted_support_id(&self, id: SupportId) -> SupportId { + if let Some(compacted) = &self.compacted { + return id + compacted.support_indices.len(); + } + id + } + + fn adjusted_source_order_id(&self, id: SourceOrderId) -> SourceOrderId { + if let Some(compacted) = &self.compacted { + return id + compacted.source_orders.len(); + } + id + } + fn adjusted_typevar_id(&self, id: TypeVarId) -> TypeVarId { if let Some(compacted) = &self.compacted { return id + compacted.typevars.len(); @@ -951,26 +1164,65 @@ impl<'db> ConstraintSetBuilder<'db> { let constraint = f(&self); let node = constraint.node; if node.is_terminal() { - return OwnedConstraintSet { node, inner: None }; + return OwnedConstraintSet { + node, + source_order: None, + inner: None, + }; } + let source_order = constraint + .source_order + .expect("non-terminal BDD should have source_order"); + // Combining constraint sets can allocate a new source-order tree even when the BDD is + // unchanged. Preserve each constraint's first source position, but rebuild the persisted + // sidecar densely so redundant combinations cannot affect its IDs or owned-set equality. + // Unlike node and constraint IDs, source-order IDs are not embedded in the BDD, so the + // sidecar can be rebuilt without remapping the BDD. let mut storage = self.storage.into_inner(); + let source_constraints = storage.calculate_source_orders(Some(source_order)); + let mut used_nodes = RankBitBox::bits_with_capacity(storage.nodes.len()); let mut used_constraints = RankBitBox::bits_with_capacity(storage.constraints.len()); + let mut used_supports = RankBitBox::bits_with_capacity(storage.supports.len()); + let mut stack = vec![node]; while let Some(node) = stack.pop() { if node.is_terminal() || used_nodes[node.index()] { continue; } - let interior = storage.nodes[node]; + let interior = storage.interior_node_data(node); + let node_support = storage + .node_support_id(node) + .expect("node should be non-terminal"); + let constraint_support = storage.constraint_support_id(interior.constraint); used_nodes.set(node.index(), true); used_constraints.set(interior.constraint.index(), true); + used_supports.set(node_support.index(), true); + used_supports.set(constraint_support.index(), true); stack.push(interior.if_true); stack.push(interior.if_uncertain); stack.push(interior.if_false); } + + let mut source_orders: IndexVec = + IndexVec::with_capacity(source_constraints.len().saturating_mul(2).saturating_sub(1)); + let source_order = source_constraints + .into_iter() + .fold(None, |left, source_constraint| { + used_constraints.set(source_constraint.index(), true); + let right = source_orders.push(SourceOrder::Constraint(source_constraint)); + + Some(match left { + Some(left) => source_orders.push(SourceOrder::Ordered(left, right)), + None => right, + }) + }) + .expect("non-terminal BDD should have source_order"); + used_nodes.truncate(used_nodes.last_one().map_or(0, |last| last + 1)); used_constraints.truncate(used_constraints.last_one().map_or(0, |last| last + 1)); + used_supports.truncate(used_supports.last_one().map_or(0, |last| last + 1)); let nodes = storage .nodes @@ -978,24 +1230,52 @@ impl<'db> ConstraintSetBuilder<'db> { .zip(&used_nodes) .filter_map(|(node, used)| used.then_some(node)) .collect(); + let node_supports = storage + .node_supports + .into_iter() + .zip(&used_nodes) + .filter_map(|(support, used)| used.then_some(support)) + .collect(); let node_indices = RankBitBox::from_bits(used_nodes); + let constraints = storage .constraints .into_iter() .zip(&used_constraints) .filter_map(|(constraint, used)| used.then_some(constraint)) .collect(); + let constraint_supports = storage + .constraint_supports + .into_iter() + .zip(&used_constraints) + .filter_map(|(support, used)| used.then_some(support)) + .collect(); let constraint_indices = RankBitBox::from_bits(used_constraints); + + let supports = storage + .supports + .into_iter() + .zip(&used_supports) + .filter_map(|(support, used)| used.then_some(support)) + .collect(); + let support_indices = RankBitBox::from_bits(used_supports); + storage.typevars.shrink_to_fit(); OwnedConstraintSet { node, + source_order: Some(source_order), inner: Some(Arc::new(OwnedConstraintSetInner { constraints, + constraint_supports, constraint_indices, typevars: storage.typevars, nodes, + node_supports, node_indices, + supports, + support_indices, + source_orders: source_orders.raw.into_boxed_slice(), })), } } @@ -1013,131 +1293,51 @@ impl<'db> ConstraintSetBuilder<'db> { pub(crate) fn load<'c>( &'c self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, other: &OwnedConstraintSet<'db>, ) -> ConstraintSet<'db, 'c> { - fn rebuild_node<'db>( - builder: &ConstraintSetBuilder<'db>, - inner: &OwnedConstraintSetInner<'db>, - constraints: &[NodeId], - cache: &mut FxHashMap, - old_node: NodeId, - ) -> NodeId { - if old_node.is_terminal() { - return old_node; - } - if let Some(remapped) = cache.get(&old_node) { - return *remapped; - } - - let old_node_index = inner.retained_node_index(old_node); - let old_interior = inner.nodes[old_node_index]; - let if_true = rebuild_node(builder, inner, constraints, cache, old_interior.if_true); - let if_uncertain = rebuild_node( - builder, - inner, - constraints, - cache, - old_interior.if_uncertain, - ); - let if_false = rebuild_node(builder, inner, constraints, cache, old_interior.if_false); - // `Constraint::new_node` creates standalone nodes whose source order starts at 1. - // Shift the reloaded condition back to the source order recorded in the owned set; - // solution extraction uses this order for deterministic unions and intersections. - let old_constraint_index = inner.retained_constraint_index(old_interior.constraint); - let condition = constraints[old_constraint_index] - .with_adjusted_source_order(builder, old_interior.source_order.saturating_sub(1)); - let remapped = condition.ite_uncertain(builder, if_true, if_uncertain, if_false); - - cache.insert(old_node, remapped); - remapped - } - - if other.node.is_terminal() { - return ConstraintSet::from_node(self, other.node); - } - let inner = other - .inner - .as_ref() - .expect("storage-free owned constraint sets must have terminal roots"); - - if inner.nodes.len() == 1 { - let old_interior = inner.nodes[inner.retained_node_index(other.node)]; - let old_constraint = - inner.constraints[inner.retained_constraint_index(old_interior.constraint)]; - let condition = Constraint::new_node_with_bounds( - db, - self, - old_constraint.typevar, - old_constraint.bounds.lower, - old_constraint.bounds.upper, - ) - .with_adjusted_source_order(self, old_interior.source_order.saturating_sub(1)); - let node = condition.ite_uncertain( - self, - old_interior.if_true, - old_interior.if_uncertain, - old_interior.if_false, - ); - return ConstraintSet::from_node(self, node); - } - - // Load all of the constraints into the this builder first, to maximize the chance that the - // constraints and typevars will appear in the same order. (This is important because many - // of our mdtests try to force a particular ordering, to test that our algorithms are all - // order-independent.) - let constraints: Box<[_]> = inner - .constraints - .iter() - .map(|old_constraint| { - Constraint::new_node_with_bounds( - db, - self, - old_constraint.typevar, - old_constraint.bounds.lower, - old_constraint.bounds.upper, - ) - }) - .collect(); - - // Maps NodeIds in the OwnedConstraintSet to the corresponding NodeIds in this builder. - let mut cache = FxHashMap::default(); - let node = rebuild_node(self, inner, &constraints, &mut cache, other.node); - ConstraintSet::from_node(self, node) + let mut storage = self.storage.borrow_mut(); + let (node, source_order) = storage.load(db, env, other); + ConstraintSet::from_node(self, node, source_order) } +} +impl<'db> ConstraintSetStorage<'db> { /// Interns a single typevar, giving it a stable order in this builder - fn intern_typevar(&self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarId { + fn intern_typevar(&mut self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarId { let identity = typevar.identity(db); - let mut storage = self.storage.borrow_mut(); - storage.ensure_overlay_identity_caches(); - if let Some(id) = storage.typevar_cache.get(&identity) { + self.ensure_overlay_identity_caches(); + if let Some(id) = self.typevar_cache.get(&identity) { return *id; } - let id = storage.typevars.push(identity); - let id = storage.adjusted_typevar_id(id); - storage.typevar_cache.insert(identity, id); + let id = self.typevars.push(identity); + let id = self.adjusted_typevar_id(id); + self.typevar_cache.insert(identity, id); id } /// Interns all of the typevars mentioned in a type in a stable order. - fn intern_mentioned_typevars_in_type(&self, db: &'db dyn Db, ty: Type<'db>) { + fn intern_mentioned_typevars_in_type( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + support: &mut Support, + ) { struct InternMentionedTypevars<'a, 'db> { - builder: &'a ConstraintSetBuilder<'db>, + env: &'a ProgramEnvironment<'db>, + storage: RefCell<&'a mut ConstraintSetStorage<'db>>, + support: RefCell<&'a mut Support>, recursion_guard: TypeCollector<'db>, } impl<'db> TypeVisitor<'db> for InternMentionedTypevars<'_, 'db> { - fn should_visit_lazy_type_attributes(&self) -> bool { - false + fn program_environment(&self) -> &ProgramEnvironment<'db> { + self.env } - fn visit_bound_type_var_type( - &self, - db: &'db dyn Db, - bound_typevar: BoundTypeVarInstance<'db>, - ) { - self.builder.intern_typevar(db, bound_typevar); - walk_bound_type_var_type(db, bound_typevar, self); + fn should_visit_lazy_type_attributes(&self) -> bool { + false } fn visit_generic_alias_type(&self, db: &'db dyn Db, alias: GenericAlias<'db>) { @@ -1147,12 +1347,20 @@ impl<'db> ConstraintSetBuilder<'db> { } fn visit_type(&self, db: &'db dyn Db, ty: Type<'db>) { + if let Type::TypeVar(bound_typevar) = ty { + let mut storage = self.storage.borrow_mut(); + let typevar = storage.intern_typevar(db, bound_typevar); + let mut support = self.support.borrow_mut(); + support.insert(typevar); + } walk_type_with_recursion_guard(db, ty, self, &self.recursion_guard); } } InternMentionedTypevars { - builder: self, + env, + storage: RefCell::new(self), + support: RefCell::new(support), recursion_guard: TypeCollector::default(), } .visit_type(db, ty); @@ -1160,90 +1368,97 @@ impl<'db> ConstraintSetBuilder<'db> { /// Interns all of the typevars mentioned in a constraint in a stable order. fn intern_constraint_typevars( - &self, + &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typevar: BoundTypeVarInstance<'db>, bounds: ConstraintBounds<'db>, - ) { - self.intern_typevar(db, typevar); + ) -> Support { + let mut support = Support::default(); + support.insert(self.intern_typevar(db, typevar)); if let Some(lower) = bounds.lower { - self.intern_mentioned_typevars_in_type(db, lower); + self.intern_mentioned_typevars_in_type(db, env, lower, &mut support); } if let Some(upper) = bounds.upper { - self.intern_mentioned_typevars_in_type(db, upper); + self.intern_mentioned_typevars_in_type(db, env, upper, &mut support); } + support } - fn intern_constraint(&self, db: &'db dyn Db, data: Constraint<'db>) -> ConstraintId { - self.intern_constraint_typevars(db, data.typevar, data.bounds); + fn intern_constraint( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + data: Constraint<'db>, + ) -> ConstraintId { + let support = self.intern_constraint_typevars(db, env, data.typevar, data.bounds); - let mut storage = self.storage.borrow_mut(); - storage.ensure_overlay_identity_caches(); - if let Some(id) = storage.constraint_cache.get(&data) { + self.ensure_overlay_identity_caches(); + if let Some(id) = self.constraint_cache.get(&data) { return *id; } - let id = storage.constraints.push(data); - let id = storage.adjusted_constraint_id(id); - storage.constraint_cache.insert(data, id); + let support_id = self.intern_support(support); + let id = self.constraints.push(data); + self.constraint_supports.push(support_id); + let id = self.adjusted_constraint_id(id); + self.constraint_cache.insert(data, id); id } - fn intern_interior_node(&self, data: InteriorNodeData) -> NodeId { - let mut storage = self.storage.borrow_mut(); - storage.ensure_overlay_identity_caches(); - if let Some(id) = storage.node_cache.get(&data) { + fn intern_interior_node(&mut self, data: InteriorNodeData) -> NodeId { + self.ensure_overlay_identity_caches(); + if let Some(id) = self.node_cache.get(&data) { return *id; } - let id = storage.nodes.push(data); - let id = storage.adjusted_node_id(id); - storage.node_cache.insert(data, id); + + let mut support = Support::default(); + support |= self.constraint_support(data.constraint); + support |= self.node_support(data.if_true); + support |= self.node_support(data.if_uncertain); + support |= self.node_support(data.if_false); + let support = self.intern_support(support); + + let id = self.nodes.push(data); + self.node_supports.push(support); + let id = self.adjusted_node_id(id); + self.node_cache.insert(data, id); id } - fn typevar_id(&self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarId { + fn typevar_id(&mut self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarId { let identity = typevar.identity(db); - let mut storage = self.storage.borrow_mut(); - storage.ensure_overlay_identity_caches(); - storage - .typevar_cache + self.ensure_overlay_identity_caches(); + self.typevar_cache .get(&identity) .copied() .expect("typevar should be interned before ordering") } fn constraint_data(&self, constraint: ConstraintId) -> Constraint<'db> { - let storage = self.storage.borrow(); - if let Some(compacted) = &storage.compacted { + if let Some(compacted) = &self.compacted { let index = constraint.index(); let split = compacted.constraint_indices.len(); if index < split { let compacted_index = compacted.retained_constraint_index(constraint); return compacted.constraints[compacted_index]; } - return storage.constraints[ConstraintId::from_usize(index - split)]; + return self.constraints[ConstraintId::from_usize(index - split)]; } - storage.constraints[constraint] + self.constraints[constraint] } fn cached_constraint_bound_depth( - &self, + &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, constraint: ConstraintId, ) -> (u16, u16) { - if let Some(depth) = self - .storage - .borrow() - .constraint_bound_depth_cache - .get(&constraint) - { + if let Some(depth) = self.constraint_bound_depth_cache.get(&constraint) { return *depth; } - let depth = self.constraint_data(constraint).bound_depth(db); - self.storage - .borrow_mut() - .constraint_bound_depth_cache - .insert(constraint, depth); + let depth = self.constraint_data(constraint).bound_depth(db, env); + self.constraint_bound_depth_cache.insert(constraint, depth); depth } @@ -1261,66 +1476,295 @@ impl<'db> ConstraintSetBuilder<'db> { /// antecedents and its consequent. (Measuring growth rather than absolute depth avoids /// penalizing a complex concrete bound that is merely propagated unchanged.) fn sequent_fuel_cost( - &self, + &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, constraint: ConstraintId, antecedent_constructor_depth: u16, ) -> u16 { - let (constructor_depth, typevar_depth) = self.cached_constraint_bound_depth(db, constraint); + let (constructor_depth, typevar_depth) = + self.cached_constraint_bound_depth(db, env, constraint); let constructor_growth = constructor_depth.saturating_sub(antecedent_constructor_depth); typevar_depth.max(constructor_growth).saturating_add(1) } fn cached_constraint_implies( - &self, + &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ante: ConstraintId, post: ConstraintId, ) -> bool { let key = (ante, post); - if let Some(result) = self.storage.borrow().constraint_implication_cache.get(&key) { + if let Some(result) = self.constraint_implication_cache.get(&key) { return *result; } - let result = ante.implies(db, self, post); - self.storage - .borrow_mut() - .constraint_implication_cache - .insert(key, result); + let result = ante.implies(db, env, self, post); + self.constraint_implication_cache.insert(key, result); result } fn cached_is_constraint_set_subtype_of( - &self, + &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, source: Type<'db>, target: Type<'db>, ) -> bool { let key = (source, target); - if let Some(result) = self.storage.borrow().constraint_set_subtype_cache.get(&key) { + if let Some(result) = self.constraint_set_subtype_cache.get(&key) { return *result; } - let result = source.is_constraint_set_subtype_of(db, target); - self.storage - .borrow_mut() - .constraint_set_subtype_cache - .insert(key, result); + let result = source.is_constraint_set_subtype_of(db, env, target); + self.constraint_set_subtype_cache.insert(key, result); result } fn interior_node_data(&self, node: NodeId) -> InteriorNodeData { - let storage = self.storage.borrow(); - if let Some(compacted) = &storage.compacted { + if let Some(compacted) = &self.compacted { let index = node.index(); let split = compacted.node_indices.len(); if index < split { let compacted_index = compacted.retained_node_index(node); return compacted.nodes[compacted_index]; } - return storage.nodes[NodeId::from_usize(index - split)]; + return self.nodes[NodeId::from_usize(index - split)]; + } + self.nodes[node] + } + + fn intern_source_order(&mut self, data: SourceOrder) -> SourceOrderId { + self.ensure_overlay_identity_caches(); + if let Some(id) = self.source_order_cache.get(&data) { + return *id; + } + let id = self.source_orders.push(data); + let id = self.adjusted_source_order_id(id); + self.source_order_cache.insert(data, id); + id + } + + /// Repeating a source-order tree cannot change the first occurrence of any constraint, so + /// combining identical trees must reuse their existing sidecar. + fn ordered_source_order( + &mut self, + left: Option, + right: Option, + ) -> Option { + match (left, right) { + (None, None) => None, + (None, other) | (other, None) => other, + (Some(left), Some(right)) if left == right => Some(left), + (Some(left), Some(right)) => { + Some(self.intern_source_order(SourceOrder::Ordered(left, right))) + } + } + } + + fn constraint_source_order(&mut self, constraint: ConstraintId) -> SourceOrderId { + self.intern_source_order(SourceOrder::Constraint(constraint)) + } + + fn source_order_data(&self, source_order: SourceOrderId) -> SourceOrder { + if let Some(compacted) = &self.compacted { + let index = source_order.index(); + let split = compacted.source_orders.len(); + if index < split { + return compacted.source_orders[index]; + } + return self.source_orders[SourceOrderId::from_usize(index - split)]; + } + self.source_orders[source_order] + } + + fn calculate_source_orders( + &self, + source_order: Option, + ) -> FxIndexSet { + fn walk( + storage: &ConstraintSetStorage, + current: SourceOrderId, + result: &mut FxIndexSet, + ) { + match storage.source_order_data(current) { + SourceOrder::Ordered(left, right) => { + walk(storage, left, result); + walk(storage, right, result); + } + SourceOrder::Constraint(constraint) => { + result.insert(constraint); + } + } + } + + let mut result = FxIndexSet::default(); + if let Some(source_order) = source_order { + walk(self, source_order, &mut result); + } + result + } + + fn intern_support(&mut self, data: Support) -> SupportId { + let id = self.supports.push(data); + self.adjusted_support_id(id) + } + + fn typevar_data(&self, typevar: TypeVarId) -> BoundTypeVarIdentity<'db> { + if let Some(compacted) = &self.compacted { + let index = typevar.index(); + let split = compacted.typevars.len(); + if index < split { + return compacted.typevars[typevar]; + } + return self.typevars[TypeVarId::from_usize(index - split)]; + } + self.typevars[typevar] + } + + fn support_data(&self, support: SupportId) -> &Support { + if let Some(compacted) = &self.compacted { + let index = support.index(); + let split = compacted.support_indices.len(); + if index < split { + let compacted_index = compacted.retained_support_index(support); + return &compacted.supports[compacted_index]; + } + return &self.supports[SupportId::from_usize(index - split)]; + } + &self.supports[support] + } + + fn constraint_support_id(&self, constraint: ConstraintId) -> SupportId { + if let Some(compacted) = &self.compacted { + let index = constraint.index(); + let split = compacted.constraint_indices.len(); + if index < split { + let compacted_index = compacted.retained_constraint_index(constraint); + return compacted.constraint_supports[compacted_index]; + } + return self.constraint_supports[ConstraintId::from_usize(index - split)]; + } + self.constraint_supports[constraint] + } + + fn constraint_support(&self, constraint: ConstraintId) -> &Support { + self.support_data(self.constraint_support_id(constraint)) + } + + fn node_support_id(&self, node: NodeId) -> Option { + if node.is_terminal() { + return None; + } + if let Some(compacted) = &self.compacted { + let index = node.index(); + let split = compacted.node_indices.len(); + if index < split { + let compacted_index = compacted.retained_node_index(node); + return Some(compacted.node_supports[compacted_index]); + } + return Some(self.node_supports[NodeId::from_usize(index - split)]); + } + Some(self.node_supports[node]) + } + + fn node_support(&self, node: NodeId) -> Option<&Support> { + self.node_support_id(node) + .map(|support| self.support_data(support)) + } + + /// Loads an [`OwnedConstraintSet`] into this storage. + fn load( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + other: &OwnedConstraintSet<'db>, + ) -> (NodeId, Option) { + fn rebuild_node<'db>( + storage: &mut ConstraintSetStorage<'db>, + inner: &OwnedConstraintSetInner<'db>, + constraints: &[(NodeId, Option)], + cache: &mut FxHashMap, + old_node: NodeId, + ) -> NodeId { + if old_node.is_terminal() { + return old_node; + } + if let Some(remapped) = cache.get(&old_node) { + return *remapped; + } + + let old_node_index = inner.retained_node_index(old_node); + let old_interior = inner.nodes[old_node_index]; + let if_true = rebuild_node(storage, inner, constraints, cache, old_interior.if_true); + let if_uncertain = rebuild_node( + storage, + inner, + constraints, + cache, + old_interior.if_uncertain, + ); + let if_false = rebuild_node(storage, inner, constraints, cache, old_interior.if_false); + let old_constraint_index = inner.retained_constraint_index(old_interior.constraint); + let (condition, _) = constraints[old_constraint_index]; + let remapped = condition.ite_uncertain(storage, if_true, if_uncertain, if_false); + + cache.insert(old_node, remapped); + remapped + } + + if other.node.is_terminal() { + return (other.node, None); + } + let inner = other + .inner + .as_ref() + .expect("storage-free owned constraint sets must have terminal roots"); + + // Load all of the constraints into the this storage first, to maximize the chance that the + // constraints and typevars will appear in the same order. (This is important because many + // of our mdtests try to force a particular ordering, to test that our algorithms are all + // order-independent.) + let constraints: Box<[_]> = inner + .constraints + .iter() + .map(|old_constraint| { + Constraint::new_node_with_bounds( + db, + env, + self, + old_constraint.typevar, + old_constraint.bounds.lower, + old_constraint.bounds.upper, + ) + }) + .collect(); + + let mut source_orders = vec![None; inner.source_orders.len()]; + for (i, old_source_order) in inner.source_orders.iter().copied().enumerate() { + match old_source_order { + SourceOrder::Ordered(old_left, old_right) => { + let new_left = source_orders[old_left.index()]; + let new_right = source_orders[old_right.index()]; + source_orders[i] = self.ordered_source_order(new_left, new_right); + } + SourceOrder::Constraint(old_constraint) => { + let old_constraint_index = inner.retained_constraint_index(old_constraint); + let (_, constraint_source_order) = constraints[old_constraint_index]; + source_orders[i] = constraint_source_order; + } + } } - storage.nodes[node] + + // Maps NodeIds in the OwnedConstraintSet to the corresponding NodeIds in this builder. + let mut cache = FxHashMap::default(); + let node = rebuild_node(self, inner, &constraints, &mut cache, other.node); + let old_source_order = other + .source_order + .expect("non-terminal constraint set should have a source_order"); + let source_order = source_orders[old_source_order.index()]; + (node, source_order) } } @@ -1337,11 +1781,11 @@ impl<'db> BoundTypeVarInstance<'db> { fn can_be_bound_for( self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, typevar: Self, ) -> bool { - wobble_index(builder.typevar_id(db, self).index()) - < wobble_index(builder.typevar_id(db, typevar).index()) + wobble_index(storage.typevar_id(db, self).index()) + < wobble_index(storage.typevar_id(db, typevar).index()) } } @@ -1389,12 +1833,6 @@ enum IntersectionResult<'db> { Disjoint, } -impl IntersectionResult<'_> { - fn is_disjoint(self) -> bool { - matches!(self, IntersectionResult::Disjoint) - } -} - /// The index of a bound typevar within a [`ConstraintSetStorage`]. #[newtype_index] #[derive(Ord, PartialOrd, get_size2::GetSize)] @@ -1405,12 +1843,23 @@ pub struct TypeVarId; #[derive(get_size2::GetSize)] pub struct ConstraintId; +#[newtype_index] +#[derive(get_size2::GetSize)] +struct SourceOrderId; + +/// The nodes of the tree that defines source ordering for a constraint set. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize, salsa::SalsaValue)] +enum SourceOrder { + Ordered(SourceOrderId, SourceOrderId), + Constraint(ConstraintId), +} + /// An individual constraint in a constraint set. This restricts a single typevar to be within a /// lower and upper bound. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize, salsa::SalsaValue)] pub(crate) struct Constraint<'db> { - pub(crate) typevar: BoundTypeVarInstance<'db>, - pub(crate) bounds: ConstraintBounds<'db>, + typevar: BoundTypeVarInstance<'db>, + bounds: ConstraintBounds<'db>, } /// The explicit lower and upper bounds inferred for a typevar on one constraint path. @@ -1441,11 +1890,17 @@ impl<'db> ConstraintBounds<'db> { self.upper.is_some() } - pub(crate) fn materialized_lower(self) -> Type<'db> { + fn as_equality(self) -> Option> { + let lower = self.lower?; + let upper = self.upper?; + (lower == upper).then_some(lower) + } + + fn materialized_lower(self) -> Type<'db> { self.lower.unwrap_or(Type::Never) } - pub(crate) fn materialized_upper(self) -> Type<'db> { + fn materialized_upper(self) -> Type<'db> { self.upper.unwrap_or(Type::object()) } } @@ -1460,64 +1915,68 @@ impl<'db> ConstraintBounds<'db> { /// constraints) we solve to `Unknown`. An upper bound of `object` is treated as an explicit /// request for "any type" as a solution, so we solve it to `object`. /// -/// As an optimization, we will remove redundant clauses as we build up an `UpperBound`. This -/// reduces the amount of work `IntersectionBuilder` needs to do when producing the solution for -/// this upper bound. +/// Redundant clauses are retained while accumulating the bound, avoiding repeated relation checks +/// for every newly discovered clause. Consumers that require one effective bound can recover it +/// with [`UpperBound::as_single_bound`] without eagerly expanding large intersections of unions. #[derive(Clone, Debug, Default, Eq, Hash, PartialEq, get_size2::GetSize, salsa::SalsaValue)] pub(crate) struct UpperBound<'db> { clauses: FxOrderSet>, } impl<'db> UpperBound<'db> { - pub(crate) fn none() -> Self { + fn none() -> Self { Self::default() } /// Creates an upper bound from one explicit clause. /// /// This preserves an explicit `object` clause so callers can distinguish `T <= object` from a - /// missing upper bound. Use [`UpperBound::add_clause`] when accumulating clauses that should - /// be canonicalized by redundancy pruning. - pub(crate) fn from_clause(clause: Type<'db>) -> Self { + /// missing upper bound. Use [`UpperBound::add_clause`] when accumulating multiple clauses. + fn from_clause(clause: Type<'db>) -> Self { let clauses = FxOrderSet::from_iter([clause]); Self { clauses } } - #[cfg(test)] - pub(crate) fn from_clauses( - db: &'db dyn Db, - clauses: impl IntoIterator>, - ) -> Self { - let mut upper = Self::none(); - for clause in clauses { - upper.add_clause(db, clause); - } - upper - } - - pub(crate) fn is_empty(&self) -> bool { + fn is_empty(&self) -> bool { self.clauses.is_empty() } - pub(crate) fn has_explicit_bound(&self) -> bool { + fn has_explicit_bound(&self) -> bool { !self.is_empty() } - fn as_single_bound(&self) -> Option> { - if self.clauses.len() != 1 { - return None; - } - self.clauses.first().copied() + /// Returns an existing upper-bound clause if every other clause is redundant with it. + /// + /// This preserves constrained type variables without distributing unions: expanding + /// `S & (int | str)` into `(S & int) | (S & str)` would otherwise lose `S` as the single + /// effective bound. Returns `None` instead of materializing intersections when no existing + /// clause dominates the others. A missing bound remains distinct from an explicit `object`. + pub(crate) fn as_single_bound( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { + let mut clauses = self.clauses.iter().copied(); + let first = clauses.next()?; + let candidate = clauses.fold(first, |candidate, clause| { + if candidate.is_redundant_with(db, env, clause) { + candidate + } else { + clause + } + }); + + self.clauses + .iter() + .all(|clause| candidate.is_redundant_with(db, env, *clause)) + .then_some(candidate) } fn is_never(&self) -> bool { self.clauses.len() == 1 && self.clauses.contains(&Type::Never) } - pub(crate) fn add_clause(&mut self, db: &'db dyn Db, clause: Type<'db>) { - // This `Never` fast path is an optimization. The general redundancy-pruning loop below - // should also handle it correctly, but spelling it out avoids unnecessary relation checks - // and keeps the stored representation canonical. + fn add_clause(&mut self, clause: Type<'db>) { if self.is_never() { return; } @@ -1528,84 +1987,85 @@ impl<'db> UpperBound<'db> { return; } - // Do not special-case `object` here. An explicit `object` clause should be preserved when - // it is the only clause, so `T <= object` remains distinguishable from a missing upper - // bound. If another clause already exists, the general redundancy check below treats - // `object` as redundant; if a narrower clause is added later, the retain step removes the - // existing `object` clause. - // - // First check if there's an existing upper bound clause that is a subtype of the new type. - // If so, adding the new type does nothing to the intersection. - if self - .clauses - .iter() - .any(|existing| existing.is_redundant_with(db, clause)) - { - return; - } - - // Otherwise remove any existing clauses that are a supertype of the new type, since the - // intersection will clip them to the new type. - self.clauses - .retain(|existing| !clause.is_redundant_with(db, *existing)); self.clauses.insert(clause); } - pub(crate) fn shrink_to_fit(&mut self) { + fn shrink_to_fit(&mut self) { self.clauses.shrink_to_fit(); } /// Exact conversion to an ordinary [`Type`]. This may be expensive: if any stored clause is a /// union, [`IntersectionType::from_elements`] converts this factored CNF representation into /// ty's ordinary DNF representation by distributing intersections over unions. - pub(crate) fn materialize_exact(&self, db: &'db dyn Db) -> Type<'db> { - IntersectionType::from_elements(db, self.clauses.iter().copied()) + pub(crate) fn materialize_exact( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { + IntersectionType::from_elements(db, env, self.clauses.iter().copied()) } fn has_visible_union_clause(&self) -> bool { self.clauses.iter().copied().any(Type::is_union) } - pub(crate) fn is_satisfied_by(&self, db: &'db dyn Db, ty: Type<'db>) -> bool { + fn is_satisfied_by( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + ) -> bool { self.clauses .iter() - .all(|clause| ty.is_constraint_set_assignable_to(db, *clause)) + .all(|clause| ty.is_constraint_set_assignable_to(db, env, *clause)) } /// Returns the constraints under which `lower` is assignable to every stored upper clause. - fn when_satisfied_by<'c>( + fn when_satisfied_by( &self, db: &'db dyn Db, - builder: &'c ConstraintSetBuilder<'db>, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, lower: Type<'db>, - ) -> ConstraintSet<'db, 'c> { - self.clauses.iter().when_all(db, builder, |clause| { - let when_clause = lower.when_constraint_set_assignable_to_owned(db, *clause); - builder.load(db, &when_clause) - }) + ) -> (NodeId, Option) { + let mut node = ALWAYS_TRUE; + let mut source_order = None; + for clause in &self.clauses { + let when_clause = lower.when_constraint_set_assignable_to_owned(db, env, *clause); + let (clause_node, clause_source_order) = storage.load(db, env, &when_clause); + node = node.and(storage, clause_node); + source_order = storage.ordered_source_order(source_order, clause_source_order); + if node == ALWAYS_FALSE { + break; + } + } + (node, source_order) } } impl ConstraintId { fn new<'db>( db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, typevar: BoundTypeVarInstance<'db>, lower: Type<'db>, upper: Type<'db>, ) -> ConstraintId { - Self::new_with_bounds(db, builder, typevar, Some(lower), Some(upper)) + Self::new_with_bounds(db, env, storage, typevar, Some(lower), Some(upper)) } fn new_with_bounds<'db>( db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, typevar: BoundTypeVarInstance<'db>, lower: Option>, upper: Option>, ) -> ConstraintId { - builder.intern_constraint( + storage.intern_constraint( db, + env, Constraint { typevar, bounds: ConstraintBounds::new(lower, upper), @@ -1619,20 +2079,30 @@ impl ConstraintId { /// /// Atomic types and bare typevars have constructor depth zero. The typevar depth is `0` if `ty` /// does not contain any typevars. -fn max_constructor_and_typevar_depth<'db>(db: &'db dyn Db, ty: Type<'db>) -> (u16, u16) { +fn max_constructor_and_typevar_depth<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> (u16, u16) { fn max_constructor_and_typevar_depth_impl<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, _dummy: (), ) -> (u16, u16) { - struct TypeDepthVisitor<'db> { + struct TypeDepthVisitor<'a, 'db> { + env: &'a ProgramEnvironment<'db>, active: RefCell>>, current_depth: Cell, max_constructor_depth: Cell, max_typevar_depth: Cell, } - impl<'db> TypeVisitor<'db> for TypeDepthVisitor<'db> { + impl<'db> TypeVisitor<'db> for TypeDepthVisitor<'_, 'db> { + fn program_environment(&self) -> &ProgramEnvironment<'db> { + self.env + } + fn should_visit_lazy_type_attributes(&self) -> bool { false } @@ -1663,6 +2133,7 @@ fn max_constructor_and_typevar_depth<'db>(db: &'db dyn Db, ty: Type<'db>) -> (u1 } let visitor = TypeDepthVisitor { + env, active: RefCell::default(), current_depth: Cell::default(), max_constructor_depth: Cell::default(), @@ -1675,15 +2146,15 @@ fn max_constructor_and_typevar_depth<'db>(db: &'db dyn Db, ty: Type<'db>) -> (u1 ) } - max_constructor_and_typevar_depth_impl(db, ty, ()) + max_constructor_and_typevar_depth_impl(db, env, ty, ()) } impl<'db> Constraint<'db> { - fn bound_depth(self, db: &'db dyn Db) -> (u16, u16) { + fn bound_depth(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> (u16, u16) { let both_bounds = iter::chain(self.bounds.lower, self.bounds.upper); both_bounds.fold((0, 0), |(constructor_depth, typevar_depth), bound| { let (bound_constructor_depth, bound_typevar_depth) = - max_constructor_and_typevar_depth(db, bound); + max_constructor_and_typevar_depth(db, env, bound); ( constructor_depth.max(bound_constructor_depth), typevar_depth.max(bound_typevar_depth), @@ -1709,31 +2180,19 @@ impl<'db> Constraint<'db> { keeps_lower || keeps_upper } - /// Returns a new range constraint. - /// - /// Panics if `lower` and `upper` are not both fully static. - fn new_node( - db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, - typevar: BoundTypeVarInstance<'db>, - lower: Type<'db>, - upper: Type<'db>, - ) -> NodeId { - Self::new_node_with_bounds(db, builder, typevar, Some(lower), Some(upper)) - } - /// Returns a new range constraint, preserving whether each bound was present explicitly. /// /// Panics if present `lower` and `upper` bounds are not fully static. fn new_node_with_bounds( db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, typevar: BoundTypeVarInstance<'db>, mut lower: Option>, mut upper: Option>, - ) -> NodeId { + ) -> (NodeId, Option) { if lower.is_none() && upper.is_none() { - return ALWAYS_TRUE; + return (ALWAYS_TRUE, None); } // It's not useful for an upper bound to be an intersection type, or for a lower bound to @@ -1746,19 +2205,20 @@ impl<'db> Constraint<'db> { // (α | β) ≤ T ⇔ (α ≤ T) ∧ (β ≤ T) if let Some(Type::Union(lower_union)) = lower { let mut result = ALWAYS_TRUE; + let mut source_order = None; for lower_element in lower_union.elements(db) { - result = result.and_with_offset( - builder, - Constraint::new_node_with_bounds( - db, - builder, - typevar, - Some(*lower_element), - upper, - ), + let (element_node, element_source_order) = Constraint::new_node_with_bounds( + db, + env, + storage, + typevar, + Some(*lower_element), + upper, ); + result = result.and(storage, element_node); + source_order = storage.ordered_source_order(source_order, element_source_order); } - return result; + return (result, source_order); } // A negated type ¬α is represented as an intersection with no positive elements, and a // single negative element. We _don't_ want to treat that an "intersection" for the @@ -1767,31 +2227,32 @@ impl<'db> Constraint<'db> { && !upper_intersection.is_simple_negation(db) { let mut result = ALWAYS_TRUE; + let mut source_order = None; for upper_element in upper_intersection.iter_positive(db) { - result = result.and_with_offset( - builder, - Constraint::new_node_with_bounds( - db, - builder, - typevar, - lower, - Some(upper_element), - ), + let (element_node, element_source_order) = Constraint::new_node_with_bounds( + db, + env, + storage, + typevar, + lower, + Some(upper_element), ); + result = result.and(storage, element_node); + source_order = storage.ordered_source_order(source_order, element_source_order); } for upper_element in upper_intersection.iter_negative(db) { - result = result.and_with_offset( - builder, - Constraint::new_node_with_bounds( - db, - builder, - typevar, - lower, - Some(upper_element.negate(db)), - ), + let (element_node, element_source_order) = Constraint::new_node_with_bounds( + db, + env, + storage, + typevar, + lower, + Some(upper_element.negate(db, env)), ); + result = result.and(storage, element_node); + source_order = storage.ordered_source_order(source_order, element_source_order); } - return result; + return (result, source_order); } // Two identical typevars must always solve to the same type, so it is not useful to have @@ -1818,12 +2279,11 @@ impl<'db> Constraint<'db> { }) }) => { - return Node::new_constraint( - builder, - ConstraintId::new(db, builder, typevar, Type::Never, Type::object()), - 1, - ) - .negate(builder); + let constraint = + ConstraintId::new(db, env, storage, typevar, Type::Never, Type::object()); + let (node, source_order) = Node::new_constraint(storage, constraint); + let node = node.negate(storage); + return (node, source_order); } _ => {} } @@ -1845,7 +2305,7 @@ impl<'db> Constraint<'db> { _ => {} } - builder.intern_constraint_typevars(db, typevar, ConstraintBounds::new(lower, upper)); + storage.intern_constraint_typevars(db, env, typevar, ConstraintBounds::new(lower, upper)); // If `lower ≰ upper` for every possible assignment of typevars, then the constraint cannot // be satisfied, since there is no type that is both greater than `lower`, and less than @@ -1854,10 +2314,11 @@ impl<'db> Constraint<'db> { // typevars — e.g., `Sequence[int] ≤ A ≤ Sequence[T]` is satisfiable when `int ≤ T`. let effective_lower = lower.unwrap_or(Type::Never); let effective_upper = upper.unwrap_or(Type::object()); - let when = effective_lower.when_constraint_set_assignable_to_owned(db, effective_upper); - let is_never_satisfied = when.query(|_builder, when| when.is_never_satisfied(db)); + let when = + effective_lower.when_constraint_set_assignable_to_owned(db, env, effective_upper); + let is_never_satisfied = when.query(|_storage, when| when.is_never_satisfied(db, env)); if is_never_satisfied { - return ALWAYS_FALSE; + return (ALWAYS_FALSE, None); } // We have an (arbitrary) ordering for typevars. If the upper and/or lower bounds are @@ -1869,101 +2330,104 @@ impl<'db> Constraint<'db> { match (effective_lower, effective_upper) { // L ≤ T ≤ L == (T ≤ [L] ≤ T) (Type::TypeVar(lower), Type::TypeVar(upper)) if lower.is_same_typevar_as(db, upper) => { - let (bound, typevar) = if lower.can_be_bound_for(db, builder, typevar) { + let (bound, typevar) = if lower.can_be_bound_for(db, storage, typevar) { (lower, typevar) } else { (typevar, lower) }; - Node::new_constraint( - builder, - ConstraintId::new( - db, - builder, - typevar, - Type::TypeVar(bound), - Type::TypeVar(bound), - ), - 1, - ) + let constraint = ConstraintId::new( + db, + env, + storage, + typevar, + Type::TypeVar(bound), + Type::TypeVar(bound), + ); + Node::new_constraint(storage, constraint) } // L ≤ T ≤ U == ([L] ≤ T) && (T ≤ [U]) (Type::TypeVar(lower), Type::TypeVar(upper)) - if typevar.can_be_bound_for(db, builder, lower) - && typevar.can_be_bound_for(db, builder, upper) => + if typevar.can_be_bound_for(db, storage, lower) + && typevar.can_be_bound_for(db, storage, upper) => { - let lower = Node::new_constraint( - builder, - ConstraintId::new_with_bounds( - db, - builder, - lower, - None, - Some(Type::TypeVar(typevar)), - ), - 1, + let lower_constraint = ConstraintId::new_with_bounds( + db, + env, + storage, + lower, + None, + Some(Type::TypeVar(typevar)), ); - let upper = Node::new_constraint( - builder, - ConstraintId::new_with_bounds( - db, - builder, - upper, - Some(Type::TypeVar(typevar)), - None, - ), - 1, + let (lower_node, lower_source_order) = + Node::new_constraint(storage, lower_constraint); + let upper_constraint = ConstraintId::new_with_bounds( + db, + env, + storage, + upper, + Some(Type::TypeVar(typevar)), + None, ); - lower.and(builder, upper) + let (upper_node, upper_source_order) = + Node::new_constraint(storage, upper_constraint); + let node = lower_node.and(storage, upper_node); + let source_order = + storage.ordered_source_order(lower_source_order, upper_source_order); + (node, source_order) } // L ≤ T ≤ U == ([L] ≤ T) && ([T] ≤ U) - (Type::TypeVar(lower), _) if typevar.can_be_bound_for(db, builder, lower) => { - let lower = Node::new_constraint( - builder, - ConstraintId::new_with_bounds( - db, - builder, - lower, - None, - Some(Type::TypeVar(typevar)), - ), - 1, + (Type::TypeVar(lower), _) if typevar.can_be_bound_for(db, storage, lower) => { + let lower_constraint = ConstraintId::new_with_bounds( + db, + env, + storage, + lower, + None, + Some(Type::TypeVar(typevar)), ); - let upper = if upper.is_none() { - ALWAYS_TRUE + let (lower_node, lower_source_order) = + Node::new_constraint(storage, lower_constraint); + let (upper_node, upper_source_order) = if upper.is_none() { + (ALWAYS_TRUE, None) } else { - Constraint::new_node_with_bounds(db, builder, typevar, None, upper) + Constraint::new_node_with_bounds(db, env, storage, typevar, None, upper) }; - lower.and(builder, upper) + let node = lower_node.and(storage, upper_node); + let source_order = + storage.ordered_source_order(lower_source_order, upper_source_order); + (node, source_order) } // L ≤ T ≤ U == (L ≤ [T]) && (T ≤ [U]) - (_, Type::TypeVar(upper)) if typevar.can_be_bound_for(db, builder, upper) => { - let lower = if lower.is_none() { - ALWAYS_TRUE + (_, Type::TypeVar(upper)) if typevar.can_be_bound_for(db, storage, upper) => { + let (lower_node, lower_source_order) = if lower.is_none() { + (ALWAYS_TRUE, None) } else { - Constraint::new_node_with_bounds(db, builder, typevar, lower, None) + Constraint::new_node_with_bounds(db, env, storage, typevar, lower, None) }; - let upper = Node::new_constraint( - builder, - ConstraintId::new_with_bounds( - db, - builder, - upper, - Some(Type::TypeVar(typevar)), - None, - ), - 1, + let upper_constraint = ConstraintId::new_with_bounds( + db, + env, + storage, + upper, + Some(Type::TypeVar(typevar)), + None, ); - lower.and(builder, upper) + let (upper_node, upper_source_order) = + Node::new_constraint(storage, upper_constraint); + let node = lower_node.and(storage, upper_node); + let source_order = + storage.ordered_source_order(lower_source_order, upper_source_order); + (node, source_order) } - _ => Node::new_constraint( - builder, - ConstraintId::new_with_bounds(db, builder, typevar, lower, upper), - 1, - ), + _ => { + let constraint = + ConstraintId::new_with_bounds(db, env, storage, typevar, lower, upper); + Node::new_constraint(storage, constraint) + } } } } @@ -2011,16 +2475,16 @@ impl ConstraintId { /// Returns whether this constraint implies another — i.e., whether every type that /// satisfies this constraint also satisfies `other`. /// - /// This is used to simplify how we display constraint sets, by removing redundant constraints - /// from a clause. + /// This is used to avoid adding redundant implications to a sequent map. fn implies<'db>( self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, other: Self, ) -> bool { - let self_constraint = builder.constraint_data(self); - let other_constraint = builder.constraint_data(other); + let self_constraint = storage.constraint_data(self); + let other_constraint = storage.constraint_data(other); if !self_constraint .typevar .is_same_typevar_as(db, other_constraint.typevar) @@ -2030,35 +2494,51 @@ impl ConstraintId { other_constraint .bounds .materialized_lower() - .is_constraint_set_assignable_to(db, self_constraint.bounds.materialized_lower()) + .is_constraint_set_assignable_to(db, env, self_constraint.bounds.materialized_lower()) && self_constraint .bounds .materialized_upper() - .is_constraint_set_assignable_to(db, other_constraint.bounds.materialized_upper()) + .is_constraint_set_assignable_to( + db, + env, + other_constraint.bounds.materialized_upper(), + ) } /// Returns the intersection of two range constraints, or `None` if the intersection is empty. fn intersect<'db>( self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, other: Self, ) -> IntersectionResult<'db> { - let self_constraint = builder.constraint_data(self); - let other_constraint = builder.constraint_data(other); + let self_constraint = storage.constraint_data(self); + let other_constraint = storage.constraint_data(other); + + // A typevar cannot be exactly equal to two different types under any specialization. This + // is stronger than checking whether the types are disjoint: two classes can have a common + // subclass, which makes their upper-bound constraints compatible, but that subclass is not + // exactly equal to either class. + if let Some(left) = self_constraint.bounds.as_equality() + && let Some(right) = other_constraint.bounds.as_equality() + && !left.can_be_constraint_set_equivalent_to(db, env, right) + { + return IntersectionResult::Disjoint; + } // (s₁ ≤ α ≤ t₁) ∧ (s₂ ≤ α ≤ t₂) = (s₁ ∪ s₂) ≤ α ≤ (t₁ ∩ t₂)) let lower = match (self_constraint.bounds.lower, other_constraint.bounds.lower) { - (Some(left), Some(right)) => Some(UnionType::from_two_elements(db, left, right)), + (Some(left), Some(right)) => Some(UnionType::from_two_elements(db, env, left, right)), (Some(lower), None) | (None, Some(lower)) => Some(lower), (None, None) => None, }; let mut merged_upper = UpperBound::none(); if let Some(upper) = self_constraint.bounds.upper { - merged_upper.add_clause(db, upper); + merged_upper.add_clause(upper); } if let Some(upper) = other_constraint.bounds.upper { - merged_upper.add_clause(db, upper); + merged_upper.add_clause(upper); } let effective_lower = lower.unwrap_or(Type::Never); @@ -2068,8 +2548,9 @@ impl ConstraintId { // rather than a universal check ("is `lower ≤ upper` for *all* assignments?"), because the // bounds may mention typevars — e.g., `Sequence[int] ≤ A ≤ Sequence[T]` is satisfiable // when `int ≤ T`, even though it's not universally true for all `T`. - let when = merged_upper.when_satisfied_by(db, builder, effective_lower); - if when.is_never_satisfied(db) { + let (when, source_order) = + merged_upper.when_satisfied_by(db, env, storage, effective_lower); + if when.is_never_satisfied(db, env, storage, source_order) { return IntersectionResult::Disjoint; } @@ -2081,7 +2562,7 @@ impl ConstraintId { return IntersectionResult::CannotSimplify; } - let upper = (!merged_upper.is_empty()).then(|| merged_upper.materialize_exact(db)); + let upper = (!merged_upper.is_empty()).then(|| merged_upper.materialize_exact(db, env)); if upper.is_some_and(|upper| upper.is_nontrivial_intersection(db)) { return IntersectionResult::CannotSimplify; @@ -2093,12 +2574,13 @@ impl ConstraintId { }) } - pub(crate) fn display<'db>( + fn display<'db, 'a>( self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, - ) -> impl Display { - self.when_true().display(db, builder) + env: &'a ProgramEnvironment<'db>, + storage: &'a ConstraintSetStorage<'db>, + ) -> impl Display + 'a { + self.when_true().display(db, env, storage) } } @@ -2148,48 +2630,39 @@ enum Node { impl NodeId { /// Creates a new BDD node, applying local TDD reductions. fn new( - builder: &ConstraintSetBuilder<'_>, + storage: &mut ConstraintSetStorage<'_>, constraint: ConstraintId, if_true: NodeId, if_false: NodeId, - source_order: usize, ) -> NodeId { - Self::with_uncertain( - builder, - constraint, - if_true, - ALWAYS_FALSE, - if_false, - source_order, - ) + Self::with_uncertain(storage, constraint, if_true, ALWAYS_FALSE, if_false) } /// Creates a new TDD node with an explicit `if_uncertain` branch, applying local reductions. fn with_uncertain( - builder: &ConstraintSetBuilder<'_>, + storage: &mut ConstraintSetStorage<'_>, constraint: ConstraintId, if_true: NodeId, if_uncertain: NodeId, if_false: NodeId, - source_order: usize, ) -> NodeId { debug_assert!( if_true - .root_constraint(builder) + .root_constraint(storage) .is_none_or(|root_constraint| { root_constraint.ordering() > constraint.ordering() }) ); debug_assert!( if_uncertain - .root_constraint(builder) + .root_constraint(storage) .is_none_or(|root_constraint| { root_constraint.ordering() > constraint.ordering() }) ); debug_assert!( if_false - .root_constraint(builder) + .root_constraint(storage) .is_none_or(|root_constraint| { root_constraint.ordering() > constraint.ordering() }) @@ -2223,17 +2696,11 @@ impl NodeId { return if_uncertain; } - let max_source_order = source_order - .max(if_true.max_source_order(builder)) - .max(if_uncertain.max_source_order(builder)) - .max(if_false.max_source_order(builder)); - builder.intern_interior_node(InteriorNodeData { + storage.intern_interior_node(InteriorNodeData { constraint, if_true, if_uncertain, if_false, - source_order, - max_source_order, }) } } @@ -2242,17 +2709,12 @@ impl Node { /// Creates a new BDD node for an individual constraint. (The BDD will evaluate to `true` when /// the constraint holds, and to `false` when it does not.) fn new_constraint( - builder: &ConstraintSetBuilder<'_>, + storage: &mut ConstraintSetStorage<'_>, constraint: ConstraintId, - source_order: usize, - ) -> NodeId { - NodeId::with_uncertain( - builder, - constraint, - ALWAYS_TRUE, - ALWAYS_FALSE, - ALWAYS_FALSE, - source_order, + ) -> (NodeId, Option) { + ( + NodeId::with_uncertain(storage, constraint, ALWAYS_TRUE, ALWAYS_FALSE, ALWAYS_FALSE), + Some(storage.constraint_source_order(constraint)), ) } @@ -2262,42 +2724,26 @@ impl Node { /// negation of that BDD node. For an unconstrained constraint, the result holds regardless /// of the constraint's truth value.) fn new_satisfied_constraint( - builder: &ConstraintSetBuilder<'_>, + storage: &mut ConstraintSetStorage<'_>, constraint: ConstraintAssignment, - source_order: usize, - ) -> NodeId { - match constraint { - ConstraintAssignment::Positive(constraint) => NodeId::with_uncertain( - builder, - constraint, - ALWAYS_TRUE, - ALWAYS_FALSE, - ALWAYS_FALSE, - source_order, - ), - ConstraintAssignment::Negative(constraint) => NodeId::with_uncertain( - builder, - constraint, - ALWAYS_FALSE, - ALWAYS_FALSE, - ALWAYS_TRUE, - source_order, - ), + ) -> (NodeId, Option) { + let constraint_id = constraint.constraint(); + let node = match constraint { + ConstraintAssignment::Positive(constraint) => { + NodeId::with_uncertain(storage, constraint, ALWAYS_TRUE, ALWAYS_FALSE, ALWAYS_FALSE) + } + ConstraintAssignment::Negative(constraint) => { + NodeId::with_uncertain(storage, constraint, ALWAYS_FALSE, ALWAYS_FALSE, ALWAYS_TRUE) + } + // The result holds regardless of the constraint's truth value, so only + // `if_uncertain` needs to be `ALWAYS_TRUE` — `n? 0: 1: 0`. It would also be + // correct to use `n? 1: 1: 1` (i.e., `ALWAYS_TRUE` for all outgoing edges), but + // that would throw away some of the efficiency gains this representation gives us. ConstraintAssignment::Unconstrained(constraint) => { - // The result holds regardless of the constraint's truth value, so only - // `if_uncertain` needs to be `ALWAYS_TRUE` — `n? 0: 1: 0`. It would also be - // correct to use `n? 1: 1: 1` (i.e., `ALWAYS_TRUE` for all outgoing edges), but - // that would throw away some of the efficiency gains this representation gives us. - NodeId::with_uncertain( - builder, - constraint, - ALWAYS_FALSE, - ALWAYS_TRUE, - ALWAYS_FALSE, - source_order, - ) + NodeId::with_uncertain(storage, constraint, ALWAYS_FALSE, ALWAYS_TRUE, ALWAYS_FALSE) } - } + }; + (node, Some(storage.constraint_source_order(constraint_id))) } } @@ -2324,48 +2770,17 @@ impl NodeId { /// Returns the BDD variable of the root node of this BDD, or `None` if this BDD is a terminal /// node. - fn root_constraint(self, builder: &ConstraintSetBuilder<'_>) -> Option { + fn root_constraint(self, storage: &ConstraintSetStorage<'_>) -> Option { if self.is_terminal() { return None; } - let interior = builder.interior_node_data(self); + let interior = storage.interior_node_data(self); Some(interior.constraint) } - fn max_source_order(self, builder: &ConstraintSetBuilder<'_>) -> usize { - if self.is_terminal() { - return 0; - } - let interior = builder.interior_node_data(self); - interior.max_source_order - } - - /// Returns a copy of this BDD node with all `source_order`s adjusted by the given amount. - fn with_adjusted_source_order(self, builder: &ConstraintSetBuilder<'_>, delta: usize) -> Self { - if delta == 0 { - return self; - } - match self.node() { - Node::AlwaysTrue | Node::AlwaysFalse => self, - Node::Interior(_) => { - let interior = builder.interior_node_data(self); - NodeId::with_uncertain( - builder, - interior.constraint, - interior.if_true.with_adjusted_source_order(builder, delta), - interior - .if_uncertain - .with_adjusted_source_order(builder, delta), - interior.if_false.with_adjusted_source_order(builder, delta), - interior.source_order + delta, - ) - } - } - } - /// Checks whether this BDD represents a single conjunction (of an arbitrary number of /// positive or negative constraints). - fn is_single_conjunction(self, builder: &ConstraintSetBuilder<'_>) -> bool { + fn is_single_conjunction(self, storage: &mut ConstraintSetStorage<'_>) -> bool { // A BDD can be viewed as an encoding of the formula's DNF representation (OR of ANDs). // Each path from the root node to the `always` terminals represents one of the disjoints. // The constraints that we encounter on the path represent the conjoints. That means that a @@ -2383,7 +2798,7 @@ impl NodeId { Node::AlwaysTrue => return true, Node::AlwaysFalse => return false, Node::Interior(interior) => { - let data = builder.interior_node_data(interior.node()); + let data = storage.interior_node_data(interior.node()); // If both if_true and if_false point to non-never, there are multiple paths to // `always`, so this cannot be a simple conjunction. @@ -2412,121 +2827,107 @@ impl NodeId { fn is_always_satisfied<'db>( self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, + source_order: Option, ) -> bool { match self.node() { Node::AlwaysTrue => true, Node::AlwaysFalse => false, Node::Interior(interior) => { - let mut path = interior.path_assignments(builder); - path.visit_negated(db, builder, self, &mut IsNeverSatisfiedVisitor) + let mut path = interior.path_assignments(storage, source_order); + path.visit_negated(db, env, storage, self, &mut IsNeverSatisfiedVisitor) .is_continue() } } } /// Returns whether this BDD represent the constant function `false`. - fn is_never_satisfied<'db>(self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>) -> bool { + fn is_never_satisfied<'db>( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, + source_order: Option, + ) -> bool { + /// Checks whether this BDD is a single conjunction, where either (a) every constraint is + /// positive lower-bound-only, or (b) every constraint is a positive upper-bound-only. If + /// so, `object` or `Never` respectively is a valid solution regardless of the contents of + /// the constraints. + fn simple_conjunction_is_satisfiable( + storage: &mut ConstraintSetStorage<'_>, + mut node: NodeId, + ) -> bool { + let mut found_lower = false; + let mut found_upper = false; + loop { + match node.node() { + Node::AlwaysTrue => return true, + Node::AlwaysFalse => return false, + + Node::Interior(_) => { + let interior = storage.interior_node_data(node); + + if interior.if_false != ALWAYS_FALSE + || interior.if_uncertain != ALWAYS_FALSE + { + // Not a single conjunction + return false; + } + + let constraint = storage.constraint_data(interior.constraint); + found_lower |= constraint.bounds.lower.is_some(); + found_upper |= constraint.bounds.upper.is_some(); + if found_lower && found_upper { + // Might be a single conjunction, but doesn't contain _only_ + // lower-bound-only or upper-bound-only constraints + return false; + } + + node = interior.if_true; + } + } + } + } + match self.node() { Node::AlwaysTrue => false, Node::AlwaysFalse => true, Node::Interior(interior) => { - if let Some(result) = builder.storage.borrow().never_satisfied_cache.get(&self) { + if let Some(result) = storage.never_satisfied_cache.get(&self) { return *result; } - let mut path = interior.path_assignments(builder); - let result = path - .visit(db, builder, self, &mut IsNeverSatisfiedVisitor) - .is_continue(); - builder - .storage - .borrow_mut() - .never_satisfied_cache - .insert(self, result); + let result = if simple_conjunction_is_satisfiable(storage, self) { + false + } else { + let mut path = interior.path_assignments(storage, source_order); + path.visit(db, env, storage, self, &mut IsNeverSatisfiedVisitor) + .is_continue() + }; + storage.never_satisfied_cache.insert(self, result); result } } } - fn solutions_with<'db>( - self, - db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, - choose: impl FnMut(TypeVarVariance, &PathBound<'db>) -> Result>, ()>, - ) -> Solutions<'db> { - let path_bounds = PathBounds::compute(db, builder, self, inferable); - path_bounds.solve_with(choose) - } - /// Returns the negation of this BDD. - fn negate(self, builder: &ConstraintSetBuilder<'_>) -> Self { + fn negate(self, storage: &mut ConstraintSetStorage<'_>) -> Self { match self.node() { Node::AlwaysTrue => ALWAYS_FALSE, Node::AlwaysFalse => ALWAYS_TRUE, - Node::Interior(interior) => interior.negate(builder), + Node::Interior(interior) => interior.negate(storage), } } /// Returns the `or` or union of two BDDs. - /// - /// In the result, `self` will appear before `other` according to the `source_order` of the BDD - /// nodes. - fn or_with_offset(self, builder: &ConstraintSetBuilder<'_>, other: Self) -> Self { - // To ensure that `self` appears before `other` in `source_order`, we add the maximum - // `source_order` of the lhs to all of the `source_order`s in the rhs. - // - // TODO: If we store `other_offset` as a new field on InteriorNode, we might be able to - // avoid all of the extra work in the calls to with_adjusted_source_order, and apply the - // adjustment lazily when walking a BDD tree. (ditto below in the other _with_offset - // methods) - let other_offset = self.max_source_order(builder); - self.or_inner(builder, other, other_offset) - } - - fn or(self, builder: &ConstraintSetBuilder<'_>, other: Self) -> Self { - self.or_inner(builder, other, 0) - } - - fn or_inner( - self, - builder: &ConstraintSetBuilder<'_>, - other: Self, - other_offset: usize, - ) -> Self { + fn or(self, storage: &mut ConstraintSetStorage<'_>, other: Self) -> Self { match (self.node(), other.node()) { - (Node::AlwaysTrue, Node::AlwaysTrue) => ALWAYS_TRUE, - (Node::AlwaysTrue, Node::Interior(_)) => { - let other_interior = builder.interior_node_data(other); - // If lhs is always true, then the overall result is true for any assignment of - // rhs. - NodeId::with_uncertain( - builder, - other_interior.constraint, - ALWAYS_FALSE, - ALWAYS_TRUE, - ALWAYS_FALSE, - other_interior.source_order + other_offset, - ) - } - (Node::Interior(_), Node::AlwaysTrue) => { - let self_interior = builder.interior_node_data(self); - // If rhs is always true, then the overall result is true for any assignment of - // lhs. - NodeId::with_uncertain( - builder, - self_interior.constraint, - ALWAYS_FALSE, - ALWAYS_TRUE, - ALWAYS_FALSE, - self_interior.source_order, - ) - } - (Node::AlwaysFalse, _) => other.with_adjusted_source_order(builder, other_offset), + (Node::AlwaysTrue, _) | (_, Node::AlwaysTrue) => ALWAYS_TRUE, + (Node::AlwaysFalse, _) => other, (_, Node::AlwaysFalse) => self, (Node::Interior(self_interior), Node::Interior(other_interior)) => { - self_interior.or(builder, other_interior, other_offset) + self_interior.or(storage, other_interior) } } } @@ -2548,15 +2949,14 @@ impl NodeId { /// You must also provide the "zero" and "one" units of the operator. The "zero" is the value /// that has no effect (`0 ∨ a = a`). It is returned if the iterator is empty. The "one" is the /// value that saturates (`1 ∨ a = 1`). We use this to short-circuit; if any element BDD or any - /// intermediate result evaluates to "one", we can return early. - fn tree_fold<'db>( - db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, - nodes: impl Iterator, + /// intermediate result is the "one" terminal, we can return early. + fn tree_fold( + builder: &ConstraintSetBuilder<'_>, + nodes: impl Iterator)>, zero: Self, - is_one: impl Fn(Self, &'db dyn Db, &ConstraintSetBuilder<'db>) -> bool, - mut combine: impl FnMut(Self, &ConstraintSetBuilder<'db>, Self) -> Self, - ) -> Self { + one: Self, + mut combine: impl FnMut(Self, &mut ConstraintSetStorage<'_>, Self) -> Self, + ) -> (Self, Option) { // To implement the "linear" shape described above, we could collect the iterator elements // into a vector, and then use the fold at the bottom of this method to combine the // elements using the operator. @@ -2581,156 +2981,95 @@ impl NodeId { // // We use a SmallVec for the accumulator so that we don't have to spill over to the heap // until the iterator passes 256 elements. - let mut accumulator: SmallVec<[(NodeId, u8); 8]> = SmallVec::default(); - for node in nodes { - if is_one(node, db, builder) { - return node; + let mut accumulator: SmallVec<[(NodeId, Option, u8); 8]> = + SmallVec::default(); + for (node, source_order) in nodes { + if node == one { + return (node, source_order); } - let (mut node, mut depth) = (node, 0); + let (mut node, mut source_order, mut depth) = (node, source_order, 0); while accumulator .last() - .is_some_and(|(_, existing)| *existing == depth) + .is_some_and(|(_, _, existing)| *existing == depth) { - let (existing, _) = accumulator.pop().expect("accumulator should not be empty"); - node = combine(existing, builder, node); - if is_one(node, db, builder) { - return node; + let (existing_node, existing_source_order, _) = + accumulator.pop().expect("accumulator should not be empty"); + let mut storage = builder.storage.borrow_mut(); + node = combine(existing_node, &mut storage, node); + source_order = storage.ordered_source_order(existing_source_order, source_order); + if node == one { + return (node, source_order); } depth += 1; } - accumulator.push((node, depth)); + accumulator.push((node, source_order, depth)); } // At this point, we've consumed all of the iterator. The length of the accumulator will be // the same as the number of 1 bits in the length of the iterator. We do a final fold to // produce the overall result. - accumulator - .into_iter() - .fold(zero, |result, (node, _)| combine(result, builder, node)) - } - - fn distributed_or<'db>( - db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, - nodes: impl Iterator, - ) -> Self { - Self::tree_fold( - db, - builder, - nodes, - ALWAYS_FALSE, - Self::is_always_satisfied, - Self::or_with_offset, - ) - } - - fn distributed_and<'db>( - db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, - nodes: impl Iterator, - ) -> Self { - Self::tree_fold( - db, - builder, - nodes, - ALWAYS_TRUE, - Self::is_never_satisfied, - Self::and_with_offset, + let mut storage = builder.storage.borrow_mut(); + accumulator.into_iter().fold( + (zero, None), + |(result_node, result_source_order), (node, source_order, _)| { + ( + combine(result_node, &mut storage, node), + storage.ordered_source_order(result_source_order, source_order), + ) + }, ) } - /// Returns the `and` or intersection of two BDDs. - /// - /// In the result, `self` will appear before `other` according to the `source_order` of the BDD - /// nodes. - fn and_with_offset(self, builder: &ConstraintSetBuilder<'_>, other: Self) -> Self { - // To ensure that `self` appears before `other` in `source_order`, we add the maximum - // `source_order` of the lhs to all of the `source_order`s in the rhs. - let other_offset = self.max_source_order(builder); - self.and_inner(builder, other, other_offset) + fn distributed_or( + builder: &ConstraintSetBuilder<'_>, + nodes: impl Iterator)>, + ) -> (Self, Option) { + Self::tree_fold(builder, nodes, ALWAYS_FALSE, ALWAYS_TRUE, Self::or) } - fn and(self, builder: &ConstraintSetBuilder<'_>, other: Self) -> Self { - self.and_inner(builder, other, 0) + fn distributed_and( + builder: &ConstraintSetBuilder<'_>, + nodes: impl Iterator)>, + ) -> (Self, Option) { + Self::tree_fold(builder, nodes, ALWAYS_TRUE, ALWAYS_FALSE, Self::and) } - fn and_inner( - self, - builder: &ConstraintSetBuilder<'_>, - other: Self, - other_offset: usize, - ) -> Self { + /// Returns the `and` or intersection of two BDDs. + fn and(self, storage: &mut ConstraintSetStorage<'_>, other: Self) -> Self { match (self.node(), other.node()) { - (Node::AlwaysFalse, Node::AlwaysFalse) => ALWAYS_FALSE, - (Node::AlwaysFalse, Node::Interior(_)) => { - let other_interior = builder.interior_node_data(other); - NodeId::new( - builder, - other_interior.constraint, - ALWAYS_FALSE, - ALWAYS_FALSE, - other_interior.source_order + other_offset, - ) - } - (Node::Interior(_), Node::AlwaysFalse) => { - let self_interior = builder.interior_node_data(self); - NodeId::new( - builder, - self_interior.constraint, - ALWAYS_FALSE, - ALWAYS_FALSE, - self_interior.source_order, - ) - } - (Node::AlwaysTrue, _) => other.with_adjusted_source_order(builder, other_offset), + (Node::AlwaysFalse, _) | (_, Node::AlwaysFalse) => ALWAYS_FALSE, + (Node::AlwaysTrue, _) => other, (_, Node::AlwaysTrue) => self, (Node::Interior(self_interior), Node::Interior(other_interior)) => { - self_interior.and(builder, other_interior, other_offset) + self_interior.and(storage, other_interior) } } } - fn implies(self, builder: &ConstraintSetBuilder<'_>, other: Self) -> Self { + fn implies(self, storage: &mut ConstraintSetStorage<'_>, other: Self) -> Self { // p → q == ¬p ∨ q - self.negate(builder).or(builder, other) + self.negate(storage).or(storage, other) } /// Returns a new BDD that evaluates to `true` when both input BDDs evaluate to the same /// result. - /// - /// In the result, `self` will appear before `other` according to the `source_order` of the BDD - /// nodes. - fn iff_with_offset(self, builder: &ConstraintSetBuilder<'_>, other: Self) -> Self { - // To ensure that `self` appears before `other` in `source_order`, we add the maximum - // `source_order` of the lhs to all of the `source_order`s in the rhs. - let other_offset = self.max_source_order(builder); - self.iff_inner(builder, other, other_offset) - } - - fn iff(self, builder: &ConstraintSetBuilder<'_>, other: Self) -> Self { - self.iff_inner(builder, other, 0) - } - - fn iff_inner( - self, - builder: &ConstraintSetBuilder<'_>, - other: Self, - other_offset: usize, - ) -> Self { + fn iff(self, storage: &mut ConstraintSetStorage<'_>, other: Self) -> Self { // iff(a, b) = (a ∧ b) ∨ (¬a ∧ ¬b) - let a_and_b = self.and_inner(builder, other, other_offset); - let not_a_and_not_b = - self.negate(builder) - .and_inner(builder, other.negate(builder), other_offset); - a_and_b.or(builder, not_a_and_not_b) + let a_and_b = self.and(storage, other); + let not_a = self.negate(storage); + let not_b = other.negate(storage); + let not_a_and_not_b = not_a.and(storage, not_b); + a_and_b.or(storage, not_a_and_not_b) } /// Returns the `if-then-else` of three BDDs: when `self` evaluates to `true`, it returns what /// `then_node` evaluates to; otherwise it returns what `else_node` evaluates to. - fn ite(self, builder: &ConstraintSetBuilder<'_>, then_node: Self, else_node: Self) -> Self { - self.and(builder, then_node) - .or(builder, self.negate(builder).and(builder, else_node)) + fn ite(self, storage: &mut ConstraintSetStorage<'_>, then_node: Self, else_node: Self) -> Self { + let if_true = self.and(storage, then_node); + let negated = self.negate(storage); + let if_false = negated.and(storage, else_node); + if_true.or(storage, if_false) } /// Returns the TDD `if-then-else` of four BDDs: when `self` evaluates to `true`, it returns @@ -2738,7 +3077,7 @@ impl NodeId { /// `else_node` evaluates to; and `uncertain_node` is included regardless of `self`'s value. fn ite_uncertain( self, - builder: &ConstraintSetBuilder<'_>, + storage: &mut ConstraintSetStorage<'_>, then_node: Self, uncertain_node: Self, else_node: Self, @@ -2748,10 +3087,10 @@ impl NodeId { } match self.node() { - Node::AlwaysTrue => then_node.or(builder, uncertain_node), - Node::AlwaysFalse => else_node.or(builder, uncertain_node), + Node::AlwaysTrue => then_node.or(storage, uncertain_node), + Node::AlwaysFalse => else_node.or(storage, uncertain_node), Node::Interior(_) => { - let interior = builder.interior_node_data(self); + let interior = storage.interior_node_data(self); // Fast path for a bare positive constraint whose branches are still later in the // BDD variable ordering. This is the common case when loading an owned TDD into a // fresh builder, and lets us preserve an existing uncertain branch directly. @@ -2759,31 +3098,32 @@ impl NodeId { && interior.if_uncertain == ALWAYS_FALSE && interior.if_false == ALWAYS_FALSE && then_node - .root_constraint(builder) + .root_constraint(storage) .is_none_or(|root| root.ordering() > interior.constraint.ordering()) && uncertain_node - .root_constraint(builder) + .root_constraint(storage) .is_none_or(|root| root.ordering() > interior.constraint.ordering()) && else_node - .root_constraint(builder) + .root_constraint(storage) .is_none_or(|root| root.ordering() > interior.constraint.ordering()) { return NodeId::with_uncertain( - builder, + storage, interior.constraint, then_node, uncertain_node, else_node, - interior.source_order, ); } // For compound conditions, or when the new builder's variable ordering requires // one of the branches to move above `self`, fall back to the semantic expansion: // `(self ∧ then_node) ∨ uncertain_node ∨ (¬self ∧ else_node)`. - self.and(builder, then_node) - .or(builder, uncertain_node) - .or(builder, self.negate(builder).and(builder, else_node)) + let if_true = self.and(storage, then_node); + let if_true_or_uncertain = if_true.or(storage, uncertain_node); + let negated = self.negate(storage); + let if_false = negated.and(storage, else_node); + if_true_or_uncertain.or(storage, if_false) } } } @@ -2791,10 +3131,11 @@ impl NodeId { fn implies_subtype_of<'db>( self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, lhs: Type<'db>, rhs: Type<'db>, - ) -> Self { + ) -> (Self, Option) { // When checking subtyping involving a typevar, we can turn the subtyping check into a // constraint (i.e, "is `T` a subtype of `int` becomes the constraint `T ≤ int`), and then // check when the BDD implies that constraint. @@ -2803,32 +3144,37 @@ impl NodeId { // these types are coming in from arbitrary subtyping checks that the caller might want to // perform. So we have to take the appropriate materialization when translating the check // into a constraint. - let constraint = match (lhs, rhs) { + let (constraint, constraint_source_order) = match (lhs, rhs) { (Type::TypeVar(bound_typevar), _) => Constraint::new_node_with_bounds( db, - builder, + env, + storage, bound_typevar, None, - Some(rhs.bottom_materialization(db)), + Some(rhs.bottom_materialization(db, env)), ), (_, Type::TypeVar(bound_typevar)) => Constraint::new_node_with_bounds( db, - builder, + env, + storage, bound_typevar, - Some(lhs.top_materialization(db)), + Some(lhs.top_materialization(db, env)), None, ), _ => panic!("at least one type should be a typevar"), }; - self.implies(builder, constraint) + let node = self.implies(storage, constraint); + (node, constraint_source_order) } fn satisfied_by_all_typevars<'db>( self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, + inferable: TypeVarSet<'db>, + source_order: Option, ) -> bool { match self.node() { Node::AlwaysTrue => return true, @@ -2837,28 +3183,41 @@ impl NodeId { } let mut typevars = FxHashSet::default(); - self.for_each_unique_constraint(builder, &mut |constraint, _| { - let constraint = builder.constraint_data(constraint); + self.for_each_unique_constraint_mut(storage, &mut |storage, constraint| { + let constraint = storage.constraint_data(constraint); typevars.insert(constraint.typevar); }); + // Specializations can introduce constraints that do not appear in the original BDD. + // Compose full constraint sets so those constraints retain their source orders when the + // resulting BDD is traversed. + // Returns if some specialization satisfies this constraint set. - let some_specialization_satisfies = move |specializations: NodeId| { - let when_satisfied = specializations - .implies(builder, self) - .and(builder, specializations); - !when_satisfied.is_never_satisfied(db, builder) - }; + let some_specialization_satisfies = + |storage: &mut ConstraintSetStorage<'db>, + specializations: (NodeId, Option)| { + let (specializations, specializations_source_order) = specializations; + let when_satisfied = specializations + .implies(storage, self) + .and(storage, specializations); + let source_order = + storage.ordered_source_order(source_order, specializations_source_order); + !when_satisfied.is_never_satisfied(db, env, storage, source_order) + }; // Returns if all specializations satisfy this constraint set. - let all_specializations_satisfy = move |specializations: NodeId| { - let when_satisfied = specializations - .implies(builder, self) - .and(builder, specializations); - when_satisfied - .iff(builder, specializations) - .is_always_satisfied(db, builder) - }; + let all_specializations_satisfy = + |storage: &mut ConstraintSetStorage<'db>, + specializations: (NodeId, Option)| { + let (specializations, specializations_source_order) = specializations; + let when_satisfied = specializations + .implies(storage, self) + .and(storage, specializations) + .iff(storage, specializations); + let source_order = + storage.ordered_source_order(source_order, specializations_source_order); + when_satisfied.is_always_satisfied(db, env, storage, source_order) + }; #[expect( clippy::iter_over_hash_type, @@ -2868,8 +3227,8 @@ impl NodeId { if typevar.is_inferable(db, inferable) { // If the typevar is in inferable position, we need to verify that some valid // specialization satisfies the constraint set. - let valid_specializations = typevar.valid_specializations(db, builder); - if !some_specialization_satisfies(valid_specializations) { + let valid_specializations = typevar.valid_specializations(db, env, storage); + if !some_specialization_satisfies(storage, valid_specializations) { return false; } } else { @@ -2885,12 +3244,12 @@ impl NodeId { // constraint to refer to the synthetic typevar instead of the original gradual // constraint. let (static_specializations, gradual_constraints) = - typevar.required_specializations(db, builder); - if !all_specializations_satisfy(static_specializations) { + typevar.required_specializations(db, env, storage); + if !all_specializations_satisfy(storage, static_specializations) { return false; } for gradual_constraint in gradual_constraints { - if !some_specialization_satisfies(gradual_constraint) { + if !some_specialization_satisfies(storage, gradual_constraint) { return false; } } @@ -2906,27 +3265,26 @@ impl NodeId { fn exists<'db>( self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, - bound_typevars: InferableTypeVars<'db>, - ) -> Self { - if bound_typevars == InferableTypeVars::None { - return self; + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, + bound_typevars: TypeVarSet<'db>, + source_order: Option, + ) -> (Self, Option) { + if bound_typevars == TypeVarSet::None { + return (self, None); } let Node::Interior(interior) = self.node() else { - return self; + return (self, None); }; - let key = (self, bound_typevars); - let storage = builder.storage.borrow(); + let key = (self, bound_typevars, source_order); if let Some(result) = storage.exists_cache.get(&key) { return *result; } - drop(storage); - let result = interior.exists_inner(db, builder, bound_typevars); + let result = interior.exists_inner(db, env, storage, bound_typevars, source_order); - let mut storage = builder.storage.borrow_mut(); storage.exists_cache.insert(key, result); result } @@ -2934,172 +3292,17 @@ impl NodeId { fn remove_noninferable<'db>( self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, - ) -> Self { - match self.node() { - Node::AlwaysTrue => ALWAYS_TRUE, - Node::AlwaysFalse => ALWAYS_FALSE, - Node::Interior(interior) => interior.remove_noninferable(db, builder, inferable), - } - } - - /// Returns a new BDD that returns the same results as `self`, but with some inputs fixed to - /// particular values. (Those variables will not be checked when evaluating the result, and - /// will not be present in the result.) - /// - /// Also returns whether _all_ of the restricted variables appeared in the BDD. - fn restrict<'db>( - self, - db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, - assignment: impl IntoIterator, - ) -> (Self, bool) { - assignment - .into_iter() - .fold((self, true), |(restricted, found), assignment| { - let (restricted, found_this) = restricted.restrict_one(db, builder, assignment); - (restricted, found && found_this) - }) - } - - /// Returns a new BDD that returns the same results as `self`, but with one input fixed to a - /// particular value. (That variable will be not be checked when evaluating the result, and - /// will not be present in the result.) - /// - /// Also returns whether the restricted variable appeared in the BDD. - fn restrict_one<'db>( - self, - db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, - assignment: ConstraintAssignment, - ) -> (Self, bool) { + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, + inferable: TypeVarSet<'db>, + source_order: Option, + ) -> (Self, Option) { match self.node() { - Node::AlwaysTrue | Node::AlwaysFalse => (self, false), - Node::Interior(interior) => interior.restrict_one(db, builder, assignment), - } - } - - /// Returns a new BDD with any occurrence of `left ∧ right` replaced with `replacement`. - #[expect(clippy::too_many_arguments)] - fn substitute_intersection<'db>( - self, - db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, - left: ConstraintAssignment, - left_source_order: usize, - right: ConstraintAssignment, - right_source_order: usize, - replacement: NodeId, - ) -> Self { - // We perform a Shannon expansion to find out what the input BDD evaluates to when: - // - left and right are both true - // - left is false - // - left is true and right is false - // This covers the entire truth table of `left ∧ right`. - let (when_left_and_right, both_found) = self.restrict(db, builder, [left, right]); - if !both_found { - // If left and right are not both present in the input BDD, we should not even attempt - // the substitution, since the Shannon expansion might introduce the missing variables! - // That confuses us below when we try to detect whether the substitution is consistent - // with the input. - return self; - } - let (when_not_left, _) = self.restrict(db, builder, [left.negated()]); - let (when_left_but_not_right, _) = self.restrict(db, builder, [left, right.negated()]); - - // The result should test `replacement`, and when it's true, it should produce the same - // output that input would when `left ∧ right` is true. When replacement is false, it - // should fall back on testing left and right individually to make sure we produce the - // correct outputs in the `¬(left ∧ right)` case. So the result is - // - // if replacement - // when_left_and_right - // else if not left - // when_not_left - // else if not right - // when_left_but_not_right - // else - // false - // - // (Note that the `else` branch shouldn't be reachable, but we have to provide something!) - let left_node = Node::new_satisfied_constraint(builder, left, left_source_order); - let right_node = Node::new_satisfied_constraint(builder, right, right_source_order); - let right_result = right_node.ite(builder, ALWAYS_FALSE, when_left_but_not_right); - let left_result = left_node.ite(builder, right_result, when_not_left); - let result = replacement.ite(builder, when_left_and_right, left_result); - - // Lastly, verify that the result is consistent with the input. (It must produce the same - // results when `left ∧ right`.) If it doesn't, the substitution isn't valid, and we should - // return the original BDD unmodified. - let validity = replacement.iff(builder, left_node.and(builder, right_node)); - let constrained_original = self.and(builder, validity); - let constrained_replacement = result.and(builder, validity); - if constrained_original == constrained_replacement { - result - } else { - self - } - } - - /// Returns a new BDD with any occurrence of `left ∨ right` replaced with `replacement`. - #[expect(clippy::too_many_arguments)] - fn substitute_union<'db>( - self, - db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, - left: ConstraintAssignment, - left_source_order: usize, - right: ConstraintAssignment, - right_source_order: usize, - replacement: NodeId, - ) -> Self { - // We perform a Shannon expansion to find out what the input BDD evaluates to when: - // - left and right are both true - // - left is true and right is false - // - left is false and right is true - // - left and right are both false - // This covers the entire truth table of `left ∨ right`. - let (when_l1_r1, both_found) = self.restrict(db, builder, [left, right]); - if !both_found { - // If left and right are not both present in the input BDD, we should not even attempt - // the substitution, since the Shannon expansion might introduce the missing variables! - // That confuses us below when we try to detect whether the substitution is consistent - // with the input. - return self; - } - let (when_l0_r0, _) = self.restrict(db, builder, [left.negated(), right.negated()]); - let (when_l1_r0, _) = self.restrict(db, builder, [left, right.negated()]); - let (when_l0_r1, _) = self.restrict(db, builder, [left.negated(), right]); - - // The result should test `replacement`, and when it's true, it should produce the same - // output that input would when `left ∨ right` is true. For OR, this is the union of what - // the input produces for the three cases that comprise `left ∨ right`. When `replacement` - // is false, the result should produce the same output that input would when - // `¬(left ∨ right)`, i.e. when `left ∧ right`. So the result is - // - // if replacement - // or(when_l1_r1, when_l1_r0, when_r0_l1) - // else - // when_l0_r0 - let result = replacement.ite( - builder, - when_l1_r0.or(builder, when_l0_r1.or(builder, when_l1_r1)), - when_l0_r0, - ); - - // Lastly, verify that the result is consistent with the input. (It must produce the same - // results when `left ∨ right`.) If it doesn't, the substitution isn't valid, and we should - // return the original BDD unmodified. - let left_node = Node::new_satisfied_constraint(builder, left, left_source_order); - let right_node = Node::new_satisfied_constraint(builder, right, right_source_order); - let validity = replacement.iff(builder, left_node.or(builder, right_node)); - let constrained_original = self.and(builder, validity); - let constrained_replacement = result.and(builder, validity); - if constrained_original == constrained_replacement { - result - } else { - self + Node::AlwaysTrue => (ALWAYS_TRUE, None), + Node::AlwaysFalse => (ALWAYS_FALSE, None), + Node::Interior(interior) => { + interior.remove_noninferable(db, env, storage, inferable, source_order) + } } } @@ -3110,84 +3313,76 @@ impl NodeId { /// root-to-leaf occurrence can be exponential in the presence of shared subgraphs. fn for_each_unique_constraint( self, - builder: &ConstraintSetBuilder<'_>, - f: &mut dyn FnMut(ConstraintId, usize), + storage: &ConstraintSetStorage<'_>, + f: &mut dyn FnMut(ConstraintId), ) { fn walk( node: NodeId, - builder: &ConstraintSetBuilder<'_>, + storage: &ConstraintSetStorage<'_>, seen: &mut FxHashSet, - f: &mut dyn FnMut(ConstraintId, usize), + f: &mut dyn FnMut(ConstraintId), ) { if node.is_terminal() || !seen.insert(node) { return; } - let interior = builder.interior_node_data(node); - f(interior.constraint, interior.source_order); - walk(interior.if_true, builder, seen, f); - walk(interior.if_uncertain, builder, seen, f); - walk(interior.if_false, builder, seen, f); + let interior = storage.interior_node_data(node); + f(interior.constraint); + walk(interior.if_true, storage, seen, f); + walk(interior.if_uncertain, storage, seen, f); + walk(interior.if_false, storage, seen, f); } - walk(self, builder, &mut FxHashSet::default(), f); + walk(self, storage, &mut FxHashSet::default(), f); } - /// Simplifies a BDD, replacing constraints with simpler or smaller constraints where possible. - /// - /// TODO: [Historical note] This is now used only for display purposes, but previously was also - /// used to ensure that we added the "transitive closure" to each BDD. The constraints in a BDD - /// are not independent; some combinations of constraints can imply other constraints. This - /// affects us in two ways: First, it means that certain combinations are impossible. (If - /// `a → b` then `a ∧ ¬b` can never happen.) Second, it means that certain constraints can be - /// inferred even if they do not explicitly appear in the BDD. It is important to take this - /// into account in several BDD operations (satisfiability, existential quantification, etc). - /// Before, we used this method to _add_ the transitive closure to a BDD, in an attempt to make - /// sure that it holds "all the facts" that would be needed to satisfy any query we might make. - /// We also used this method to calculate the "domain" of the BDD to help rule out invalid - /// inputs. However, this was at odds with using this method for display purposes, where our - /// goal is to _remove_ redundant information, so as to not clutter up the display. To resolve - /// this dilemma, all of the correctness uses have been refactored to use [`SequentMap`] - /// instead. It tracks the same information in a more efficient and lazy way, and never tries - /// to remove redundant information. For expediency, however, we did not make any changes to - /// this method, other than to stop tracking the domain (which was never used for display - /// purposes). That means we have some tech debt here, since there is a lot of duplicate logic - /// between `simplify_for_display` and `SequentMap`. It would be nice to update our display - /// logic to use the sequent map as much as possible. But that can happen later. - fn simplify_for_display<'db>( + fn for_each_unique_constraint_mut<'db>( self, - db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, - ) -> Self { - match self.node() { - Node::AlwaysTrue | Node::AlwaysFalse => self, - Node::Interior(interior) => interior.simplify(db, builder), + storage: &mut ConstraintSetStorage<'db>, + f: &mut dyn FnMut(&mut ConstraintSetStorage<'db>, ConstraintId), + ) { + fn walk<'db>( + node: NodeId, + storage: &mut ConstraintSetStorage<'db>, + seen: &mut FxHashSet, + f: &mut dyn FnMut(&mut ConstraintSetStorage<'db>, ConstraintId), + ) { + if node.is_terminal() || !seen.insert(node) { + return; + } + let interior = storage.interior_node_data(node); + f(storage, interior.constraint); + walk(interior.if_true, storage, seen, f); + walk(interior.if_uncertain, storage, seen, f); + walk(interior.if_false, storage, seen, f); } + + walk(self, storage, &mut FxHashSet::default(), f); } /// Returns clauses describing all of the variable assignments that cause this BDD to evaluate /// to `true`. (This translates the boolean function that this BDD represents into DNF form.) - fn satisfied_clauses(self, builder: &ConstraintSetBuilder<'_>) -> SatisfiedClauses { + fn satisfied_clauses(self, storage: &ConstraintSetStorage<'_>) -> SatisfiedClauses { struct Searcher { clauses: SatisfiedClauses, current_clause: SatisfiedClause, } impl Searcher { - fn visit_node(&mut self, builder: &ConstraintSetBuilder<'_>, node: NodeId) { + fn visit_node(&mut self, storage: &ConstraintSetStorage<'_>, node: NodeId) { match node.node() { Node::AlwaysFalse => {} Node::AlwaysTrue => self.clauses.push(self.current_clause.clone()), Node::Interior(_) => { - let interior = builder.interior_node_data(node); + let interior = storage.interior_node_data(node); self.current_clause.push(interior.constraint.when_true()); - self.visit_node(builder, interior.if_true); + self.visit_node(storage, interior.if_true); self.current_clause.pop(); self.current_clause .push(interior.constraint.when_unconstrained()); - self.visit_node(builder, interior.if_uncertain); + self.visit_node(storage, interior.if_uncertain); self.current_clause.pop(); self.current_clause.push(interior.constraint.when_false()); - self.visit_node(builder, interior.if_false); + self.visit_node(storage, interior.if_false); self.current_clause.pop(); } } @@ -3198,20 +3393,24 @@ impl NodeId { clauses: SatisfiedClauses::default(), current_clause: SatisfiedClause::default(), }; - searcher.visit_node(builder, self); + searcher.visit_node(storage, self); searcher.clauses } - fn display<'db>(self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>) -> impl Display { - // To render a BDD in DNF form, you perform a depth-first search of the BDD tree, looking - // for any path that leads to the AlwaysTrue terminal. Each such path represents one of the - // intersection clauses in the DNF form. The path traverses zero or more interior nodes, - // and takes either the true or false edge from each one. That gives you the positive or - // negative individual constraints in the path's clause. + fn display<'db, 'a>( + self, + db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, + storage: &'a ConstraintSetStorage<'db>, + ) -> impl Display + 'a { + // Render the BDD directly as an unsimplified DNF formula. Each root-to-true path becomes + // one clause, with true, uncertain, and false edges contributing positive, unconstrained, + // and negative assignments respectively. struct DisplayNode<'db, 'c> { node: NodeId, db: &'db dyn Db, - builder: &'c ConstraintSetBuilder<'db>, + env: &'c ProgramEnvironment<'db>, + storage: &'c ConstraintSetStorage<'db>, } impl Display for DisplayNode<'_, '_> { @@ -3219,11 +3418,14 @@ impl NodeId { match self.node.node() { Node::AlwaysTrue => f.write_str("always"), Node::AlwaysFalse => f.write_str("never"), - Node::Interior(_) => { - let mut clauses = self.node.satisfied_clauses(self.builder); - clauses.simplify(self.db, self.builder); - Display::fmt(&clauses.display(self.db, self.builder), f) - } + Node::Interior(_) => Display::fmt( + &self.node.satisfied_clauses(self.storage).display( + self.db, + self.env, + self.storage, + ), + f, + ), } } } @@ -3231,7 +3433,8 @@ impl NodeId { DisplayNode { node: self, db, - builder, + env, + storage, } } @@ -3256,12 +3459,14 @@ impl NodeId { fn display_graph<'db, 'a>( self, db: &'db dyn Db, - builder: &'a ConstraintSetBuilder<'db>, + env: &'a ProgramEnvironment<'db>, + storage: &'a ConstraintSetStorage<'db>, prefix: &'a dyn Display, ) -> impl Display + 'a { struct DisplayNode<'a, 'db> { db: &'db dyn Db, - builder: &'a ConstraintSetBuilder<'db>, + env: &'a ProgramEnvironment<'db>, + storage: &'a ConstraintSetStorage<'db>, node: NodeId, prefix: &'a dyn Display, seen: RefCell>, @@ -3269,7 +3474,8 @@ impl NodeId { fn format_node<'db>( db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + env: &ProgramEnvironment<'db>, + storage: &ConstraintSetStorage<'db>, node: NodeId, prefix: &dyn Display, seen: &RefCell>, @@ -3283,20 +3489,19 @@ impl NodeId { if !is_new { return write!(f, "<{index}> SHARED"); } - let interior = builder.interior_node_data(node); + let interior = storage.interior_node_data(node); write!( f, - "<{index}> {} {}/{}", - interior.constraint.display(db, builder), - interior.source_order, - interior.max_source_order, + "<{index}> {}", + interior.constraint.display(db, env, storage) )?; // Calling display_graph recursively here causes rustc to claim that the // expect(unused) up above is unfulfilled! write!(f, "\n{prefix}┡━₁ ")?; format_node( db, - builder, + env, + storage, interior.if_true, &format_args!("{prefix}│ "), seen, @@ -3305,7 +3510,8 @@ impl NodeId { write!(f, "\n{prefix}├─? ")?; format_node( db, - builder, + env, + storage, interior.if_uncertain, &format_args!("{prefix}│ "), seen, @@ -3314,7 +3520,8 @@ impl NodeId { write!(f, "\n{prefix}└─₀ ")?; format_node( db, - builder, + env, + storage, interior.if_false, &format_args!("{prefix} "), seen, @@ -3327,13 +3534,23 @@ impl NodeId { impl Display for DisplayNode<'_, '_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - format_node(self.db, self.builder, self.node, self.prefix, &self.seen, f) + let db = self.db; + format_node( + db, + self.env, + self.storage, + self.node, + self.prefix, + &self.seen, + f, + ) } } DisplayNode { db, - builder, + env, + storage, node: self, prefix, seen: RefCell::default(), @@ -3388,16 +3605,6 @@ struct InteriorNodeData { if_true: NodeId, if_uncertain: NodeId, if_false: NodeId, - - /// Represents the order in which this node's constraint was added to the containing constraint - /// set, relative to all of the other constraints in the set. This starts off at 1 for a simple - /// single-constraint set (e.g. created with [`Node::new_constraint`] or - /// [`Node::new_satisfied_constraint`]). It will get incremented, if needed, as that simple BDD - /// is combined into larger BDDs. - source_order: usize, - - /// The maximum `source_order` across this node and all of its descendants. - max_source_order: usize, } /// Accumulates lower and upper bounds for a single typevar on a single BDD path. @@ -3417,39 +3624,44 @@ struct ConstraintBoundsBuilder<'db> { } impl<'db> ConstraintBoundsBuilder<'db> { - fn classify_evidence(&mut self, db: &'db dyn Db, ty: Type<'db>) { - if ty.has_unspecialized_type_var(db) { + fn classify_evidence(&mut self, db: &'db dyn Db, env: &ProgramEnvironment<'db>, ty: Type<'db>) { + if ty.has_unspecialized_type_var(db, env) { return; } - if ty.bottom_materialization(db) == ty.top_materialization(db) { + if ty.bottom_materialization(db, env) == ty.top_materialization(db, env) { self.has_static_evidence = true; } else { self.has_gradual_evidence = true; } } - fn add_lower(&mut self, db: &'db dyn Db, ty: Type<'db>) { + fn add_lower(&mut self, db: &'db dyn Db, env: &ProgramEnvironment<'db>, ty: Type<'db>) { // Lower bounds are unioned. Our type representation is in DNF, so unioning a new // element is typically cheap (in that it does not involve a combinatorial // explosion from distributing the clause through an existing disjunction). So we // don't need to be as clever here as in `add_upper`. - self.classify_evidence(db, ty); + self.classify_evidence(db, env, ty); self.lower.insert(ty); } - fn add_upper(&mut self, db: &'db dyn Db, ty: Type<'db>) { - self.classify_evidence(db, ty); - self.upper.add_clause(db, ty); + fn add_upper(&mut self, db: &'db dyn Db, env: &ProgramEnvironment<'db>, ty: Type<'db>) { + self.classify_evidence(db, env, ty); + self.upper.add_clause(ty); } - fn finish(self, db: &'db dyn Db, bound_typevar: BoundTypeVarInstance<'db>) -> PathBound<'db> { + fn finish( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + bound_typevar: BoundTypeVarInstance<'db>, + ) -> PathBound<'db> { let Self { lower, mut upper, has_gradual_evidence, has_static_evidence, } = self; - let lower = (!lower.is_empty()).then(|| UnionType::from_elements(db, lower)); + let lower = (!lower.is_empty()).then(|| UnionType::from_elements(db, env, lower)); upper.shrink_to_fit(); PathBound { bound_typevar, @@ -3480,7 +3692,7 @@ impl<'db> PathBound<'db> { } } - pub(crate) fn variance(&self) -> TypeVarVariance { + fn variance(&self) -> TypeVarVariance { match (self.lower, self.has_upper()) { (None, true) => TypeVarVariance::Covariant, (Some(_), false) => TypeVarVariance::Contravariant, @@ -3489,7 +3701,7 @@ impl<'db> PathBound<'db> { } } - pub(crate) fn lower_or_never(&self) -> Type<'db> { + fn lower_or_never(&self) -> Type<'db> { self.lower.unwrap_or(Type::Never) } @@ -3508,25 +3720,39 @@ impl<'db> Type<'db> { pub(crate) fn assignable_solutions_with_inferable( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, target: Type<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, ) -> &'db PathBounds<'db> { #[salsa::tracked( returns(ref), - cycle_initial=|_, _, _, _, _| PathBounds::Unsatisfiable, + cycle_initial=|_, _, _, _, _, _| PathBounds::Unsatisfiable, heap_size=ruff_memory_usage::heap_size, )] fn assignable_solutions_impl<'db>( db: &'db dyn Db, + program: Program<'db>, source: Type<'db>, target: Type<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, ) -> PathBounds<'db> { - let when = source.when_constraint_set_assignable_to_owned(db, target); - when.query(|builder, when| PathBounds::compute(db, builder, when.node, inferable)) + let env = &ProgramEnvironment::from_program(program); + let when = source.when_constraint_set_assignable_to_owned(db, env, target); + when.query(|builder, when| { + let mut storage = builder.storage.borrow_mut(); + PathBounds::compute( + db, + env, + &mut storage, + when.node, + inferable, + when.source_order, + ) + }) } - assignable_solutions_impl(db, self, target, inferable) + let program = env.program(db); + assignable_solutions_impl(db, program, self, target, inferable) } } @@ -3536,10 +3762,12 @@ impl<'db> Type<'db> { heap_size = get_size2::GetSize::get_heap_size )] fn is_possibly_constraint_set_assignable<'db>(db: &'db dyn Db, types: TypePair<'db>) -> bool { + let program = types.program(db); + let env = &ProgramEnvironment::from_program(program); types .first(db) - .when_constraint_set_assignable_to_owned(db, types.second(db)) - .query(|_builder, when| !when.is_never_satisfied(db)) + .when_constraint_set_assignable_to_owned(db, env, types.second(db)) + .query(|_storage, when| !when.is_never_satisfied(db, env)) } /// Per-path bounds for all typevars. Each element is the set of typevar bounds for one BDD path. @@ -3557,26 +3785,37 @@ impl<'db> PathBounds<'db> { /// typevar that appears in the path's constraints. fn compute( db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, node: NodeId, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, + source_order: Option, ) -> Self { - #[derive(Default)] - struct CollectVisitor { + struct CollectVisitor<'a> { + source_orders: &'a FxIndexSet, sorted_paths: Vec>, } - impl PathFold for CollectVisitor { + impl PathFold for CollectVisitor<'_> { type Result = (); type Break = Infallible; fn satisfied<'db>( &mut self, _db: &'db dyn Db, - _builder: &ConstraintSetBuilder<'db>, + _storage: &mut ConstraintSetStorage<'db>, path: &PathAssignments, ) -> ControlFlow { - let mut path: Vec<_> = path.positive_constraints().collect(); + let mut path: Vec<_> = path + .positive_constraints() + .map(|(constraint, source_constraint)| { + let source_order = self + .source_orders + .get_index_of(&source_constraint) + .expect("every TDD constraint should have a source order"); + (constraint, source_order) + }) + .collect(); path.sort_by_key(|(_, source_order)| *source_order); self.sorted_paths.push(path); ControlFlow::Continue(()) @@ -3585,7 +3824,7 @@ impl<'db> PathBounds<'db> { fn unsatisfied<'db>( &mut self, _db: &'db dyn Db, - _builder: &ConstraintSetBuilder<'db>, + _storage: &mut ConstraintSetStorage<'db>, _path: &PathAssignments, ) -> ControlFlow { ControlFlow::Continue(()) @@ -3594,7 +3833,7 @@ impl<'db> PathBounds<'db> { fn impossible<'db>( &mut self, _db: &'db dyn Db, - _builder: &ConstraintSetBuilder<'db>, + _storage: &mut ConstraintSetStorage<'db>, _path: &PathAssignments, ) -> ControlFlow { ControlFlow::Continue(()) @@ -3603,7 +3842,7 @@ impl<'db> PathBounds<'db> { fn combine<'db>( &mut self, _db: &'db dyn Db, - _builder: &ConstraintSetBuilder<'db>, + _storage: &mut ConstraintSetStorage<'db>, _if_true: Self::Result, _if_uncertain: Self::Result, _if_false: Self::Result, @@ -3612,13 +3851,21 @@ impl<'db> PathBounds<'db> { } } - if let Some(path_bounds) = - Self::compute_simple_bound_conjunction(db, builder, node, inferable) - { + let mut source_orders = storage.calculate_source_orders(source_order); + if let Some(path_bounds) = Self::compute_simple_bound_conjunction( + db, + env, + storage, + &source_orders, + node, + inferable, + ) { return path_bounds; } - let node = node.remove_noninferable(db, builder, inferable); + let (node, derived_source_order) = + node.remove_noninferable(db, env, storage, inferable, source_order); + source_orders.extend(storage.calculate_source_orders(derived_source_order)); let interior = match node.node() { Node::AlwaysTrue => return PathBounds::Unconstrained, Node::AlwaysFalse => return PathBounds::Unsatisfiable, @@ -3630,9 +3877,16 @@ impl<'db> PathBounds<'db> { // come out of `PathAssignment`s with identical `source_order`s, but if they do, those // "tied" constraints will still be ordered in a stable way. So we need a stable sort to // retain that stable per-tie ordering. - let mut collect_visitor = CollectVisitor::default(); - let mut path = interior.path_assignments(builder); - let _ = path.visit(db, builder, node, &mut collect_visitor); + let mut collect_visitor = CollectVisitor { + source_orders: &source_orders, + sorted_paths: Vec::new(), + }; + // Sequent discovery must also happen in source order. Sorting the collected paths below + // is too late: sequent pairs are not commutative, and TDD traversal order can otherwise + // discard gradual evidence before solution extraction. + let path_source_order = storage.ordered_source_order(source_order, derived_source_order); + let mut path = interior.path_assignments(storage, path_source_order); + let _ = path.visit(db, env, storage, node, &mut collect_visitor); collect_visitor.sorted_paths.sort_by(|path1, path2| { let source_orders1 = path1.iter().map(|(_, source_order)| *source_order); let source_orders2 = path2.iter().map(|(_, source_order)| *source_order); @@ -3646,32 +3900,32 @@ impl<'db> PathBounds<'db> { for path in collect_visitor.sorted_paths { mappings.clear(); for (constraint, _) in path { - let constraint = builder.constraint_data(constraint); + let constraint = storage.constraint_data(constraint); let typevar = constraint.typevar; if let Some(lower) = constraint.bounds.lower { let bounds = mappings.entry(typevar).or_default(); - bounds.add_lower(db, lower); + bounds.add_lower(db, env, lower); if let Type::TypeVar(lower_bound_typevar) = lower { let bounds = mappings.entry(lower_bound_typevar).or_default(); - bounds.add_upper(db, Type::TypeVar(typevar)); + bounds.add_upper(db, env, Type::TypeVar(typevar)); } } if let Some(upper) = constraint.bounds.upper { let bounds = mappings.entry(typevar).or_default(); - bounds.add_upper(db, upper); + bounds.add_upper(db, env, upper); if let Type::TypeVar(upper_bound_typevar) = upper { let bounds = mappings.entry(upper_bound_typevar).or_default(); - bounds.add_lower(db, Type::TypeVar(typevar)); + bounds.add_lower(db, env, Type::TypeVar(typevar)); } } } let path_bounds = mappings .drain(..) - .map(|(bound_typevar, bounds)| bounds.finish(db, bound_typevar)) + .map(|(bound_typevar, bounds)| bounds.finish(db, env, bound_typevar)) .collect(); result.push(path_bounds); } @@ -3687,9 +3941,11 @@ impl<'db> PathBounds<'db> { /// accumulated bound against the typevar's declared bound or constraints. fn compute_simple_bound_conjunction( db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, + source_orders: &FxIndexSet, node: NodeId, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, ) -> Option { match node.node() { Node::AlwaysTrue => return Some(PathBounds::Unconstrained), @@ -3704,19 +3960,19 @@ impl<'db> PathBounds<'db> { Node::AlwaysTrue => break, Node::AlwaysFalse => return None, Node::Interior(_) => { - let interior = builder.interior_node_data(current); + let interior = storage.interior_node_data(current); if interior.if_uncertain != ALWAYS_FALSE || interior.if_false != ALWAYS_FALSE { return None; } - let constraint = builder.constraint_data(interior.constraint); + let constraint = storage.constraint_data(interior.constraint); if !constraint.typevar.is_inferable(db, inferable) { return None; } - if iter::chain(constraint.bounds.lower, constraint.bounds.upper) - .any(|bound| bound.has_typevar(db) || bound.has_unspecialized_type_var(db)) - { + if iter::chain(constraint.bounds.lower, constraint.bounds.upper).any(|bound| { + bound.has_typevar(db, env) || bound.has_unspecialized_type_var(db, env) + }) { return None; } @@ -3724,7 +3980,9 @@ impl<'db> PathBounds<'db> { constraints.push(( constraint.typevar, constraint.bounds, - interior.source_order, + source_orders + .get_index_of(&interior.constraint) + .expect("every TDD constraint should have a source order"), )); } } @@ -3736,16 +3994,16 @@ impl<'db> PathBounds<'db> { for (typevar, constraint, _) in constraints { let bounds = mappings.entry(typevar).or_default(); if let Some(lower) = constraint.lower { - bounds.add_lower(db, lower); + bounds.add_lower(db, env, lower); } if let Some(upper) = constraint.upper { - bounds.add_upper(db, upper); + bounds.add_upper(db, env, upper); } } let path = mappings .drain(..) - .map(|(bound_typevar, bounds)| bounds.finish(db, bound_typevar)) + .map(|(bound_typevar, bounds)| bounds.finish(db, env, bound_typevar)) .collect(); Some(PathBounds::Constrained(Box::new([path]))) } @@ -3753,9 +4011,12 @@ impl<'db> PathBounds<'db> { pub(crate) fn solve( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, builder: &ConstraintSetBuilder<'db>, ) -> Solutions<'db> { - self.solve_with(|_variance, path_bound| PathBounds::default_solve(db, builder, path_bound)) + self.solve_with(|_variance, path_bound| { + PathBounds::default_solve(db, env, builder, path_bound) + }) } /// Solves each path by applying a per-typevar solver function, collecting valid solutions. @@ -3809,6 +4070,7 @@ impl<'db> PathBounds<'db> { /// - `Err(())` if the path is invalid (bounds violate the typevar's declared constraints) pub(crate) fn default_solve( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, builder: &ConstraintSetBuilder<'db>, path_bound: &PathBound<'db>, ) -> Result>, ()> { @@ -3820,16 +4082,19 @@ impl<'db> PathBounds<'db> { let bound_typevar = path_bound.bound_typevar; let lower = path_bound.lower_or_never(); - match bound_typevar.typevar(db).require_bound_or_constraints(db) { + match bound_typevar + .typevar(db) + .require_bound_or_constraints(db, env) + { TypeVarBoundOrConstraints::UpperBound(bound) => { - let declared_upper = bound.top_materialization(db); + let declared_upper = bound.top_materialization(db, env); // basedpython: a declared lower bound raises the floor of every solution. The // narrowest type above both it and any inferred lower bound is their union let path_lower = match (path_bound.lower, bound_typevar.typevar(db).lower_bound(db)) { (Some(inferred), Some(declared)) => { - Some(UnionType::from_two_elements(db, inferred, declared)) + Some(UnionType::from_two_elements(db, env, inferred, declared)) } (Some(inferred), None) => Some(inferred), (None, declared) => declared, @@ -3839,9 +4104,13 @@ impl<'db> PathBounds<'db> { // upper bound (which may include TypeVar bounds/constraints). The upper bound // should only be used as a fallback when no concrete type was inferred. if let Some(lower) = path_lower { - if !path_bound.upper.is_satisfied_by(db, lower) { - let when_upper = path_bound.upper.when_satisfied_by(db, builder, lower); - if when_upper.is_never_satisfied(db) { + if !path_bound.upper.is_satisfied_by(db, env, lower) { + let mut storage = builder.storage.borrow_mut(); + let (when_upper, source_order) = + path_bound + .upper + .when_satisfied_by(db, env, &mut storage, lower); + if when_upper.is_never_satisfied(db, env, &mut storage, source_order) { // This path does not satisfy the accumulated upper bound, and is // therefore not a valid specialization. return Err(()); @@ -3850,7 +4119,7 @@ impl<'db> PathBounds<'db> { if !is_possibly_constraint_set_assignable( db, - TypePair::new(db, lower, declared_upper), + TypePair::new(db, env.program(db), lower, declared_upper), ) { // This path does not satisfy the typevar's declared upper bound, and is // therefore not a valid specialization. @@ -3863,6 +4132,7 @@ impl<'db> PathBounds<'db> { if path_bound.has_upper() { return Ok(IntersectionType::bounded_from_elements( db, + env, path_bound .upper .clauses @@ -3910,34 +4180,50 @@ impl<'db> PathBounds<'db> { // Lower-bound evidence asks for the narrowest compatible declared constraint // above the lower bound. With only upper-bound evidence, ask for the widest // compatible declared constraint below the upper bound. If the candidates are - // equivalent or incomparable, keep the current best to preserve the TypeVar's + // assignable in both directions, prefer a fully static constraint over a + // gradual one. Otherwise, keep the current best to preserve the TypeVar's // declared constraint order. - if path_bound.lower.is_some() { - candidate.is_subtype_of(db, current_best) - && !current_best.is_subtype_of(db, candidate) + let candidate_assignable_to_best = + candidate.is_assignable_to(db, env, current_best); + let best_assignable_to_candidate = + current_best.is_assignable_to(db, env, candidate); + + if candidate_assignable_to_best != best_assignable_to_candidate { + if path_bound.lower.is_some() { + candidate_assignable_to_best + } else { + best_assignable_to_candidate + } + } else if candidate_assignable_to_best { + let candidate_is_static = candidate.bottom_materialization(db, env) + == candidate.top_materialization(db, env); + let best_is_static = current_best.bottom_materialization(db, env) + == current_best.top_materialization(db, env); + candidate_is_static && !best_is_static } else { - current_best.is_subtype_of(db, candidate) - && !candidate.is_subtype_of(db, current_best) + false } }; for constraint in constraints.elements(db).iter().copied() { - let constraint_lower = constraint.bottom_materialization(db); - let constraint_upper = constraint.top_materialization(db); + let constraint_lower = constraint.bottom_materialization(db, env); + let constraint_upper = constraint.top_materialization(db, env); // A gradual constraint can choose any materialization that satisfies this // path. Its top materialization is the most permissive target for lower-bound // evidence, while its bottom materialization is the most permissive source // for upper-bound evidence. let when_lower = - lower.when_constraint_set_assignable_to_owned(db, constraint_upper); - let when_upper = + lower.when_constraint_set_assignable_to_owned(db, env, constraint_upper); + let mut storage = builder.storage.borrow_mut(); + let (when_upper, upper_source_order) = path_bound .upper - .when_satisfied_by(db, builder, constraint_lower); - let when = builder - .load(db, &when_lower) - .and(db, builder, || when_upper); - if when.is_never_satisfied(db) { + .when_satisfied_by(db, env, &mut storage, constraint_lower); + let (when_lower, lower_source_order) = storage.load(db, env, &when_lower); + let when = when_lower.and(&mut storage, when_upper); + let source_order = + storage.ordered_source_order(lower_source_order, upper_source_order); + if when.is_never_satisfied(db, env, &mut storage, source_order) { continue; } @@ -3958,7 +4244,7 @@ impl<'db> PathBounds<'db> { }; if let (Some(ty @ Type::TypeVar(_)), _) | (_, Some(ty @ Type::TypeVar(_))) = - (path_bound.lower, path_bound.upper.as_single_bound()) + (path_bound.lower, path_bound.upper.as_single_bound(db, env)) { // This path relates two TypeVars, such as passing `S` to a parameter typed as // `T: (int, str)`. The compatibility check above has verified that at least @@ -3982,6 +4268,7 @@ impl<'db> PathBounds<'db> { } else if path_bound.has_upper() { Ok(IntersectionType::bounded_from_elements( db, + env, path_bound.upper.clauses.iter().copied(), )) } else { @@ -4000,107 +4287,94 @@ impl InteriorNode { self.0 } - fn negate(self, builder: &ConstraintSetBuilder<'_>) -> NodeId { + fn negate(self, storage: &mut ConstraintSetStorage<'_>) -> NodeId { let key = self.node(); - let storage = builder.storage.borrow(); if let Some(result) = storage.negate_cache.get(&key) { return *result; } - drop(storage); // negate(n ? C : U : D) = n ? negate(or(C, U)) : 0 : negate(or(D, U)) // // The uncertain branch U is absorbed into C and D via union before negation. The result's // uncertain branch is always zero. When U = 0 (the common case), this degenerates to the // standard binary BDD leaf-swap: n ? negate(C) : 0 : negate(D). - let interior = builder.interior_node_data(self.node()); - let not_true = interior.if_true.negate(builder); - let not_uncertain = interior.if_uncertain.negate(builder); - let not_false = interior.if_false.negate(builder); - let result = NodeId::new( - builder, - interior.constraint, - not_true.and(builder, not_uncertain), - not_false.and(builder, not_uncertain), - interior.source_order, - ); + let interior = storage.interior_node_data(self.node()); + let not_true = interior.if_true.negate(storage); + let not_uncertain = interior.if_uncertain.negate(storage); + let not_false = interior.if_false.negate(storage); + let if_true = not_true.and(storage, not_uncertain); + let if_false = not_false.and(storage, not_uncertain); + let result = NodeId::new(storage, interior.constraint, if_true, if_false); - let mut storage = builder.storage.borrow_mut(); storage.negate_cache.insert(key, result); result } - fn or(self, builder: &ConstraintSetBuilder<'_>, other: Self, other_offset: usize) -> NodeId { - let key = (self.node(), other.node(), other_offset); - let storage = builder.storage.borrow(); + fn or(self, storage: &mut ConstraintSetStorage<'_>, other: Self) -> NodeId { + let key = (self.node(), other.node()); if let Some(result) = storage.or_cache.get(&key) { return *result; } - drop(storage); - let self_interior = builder.interior_node_data(self.node()); + let self_interior = storage.interior_node_data(self.node()); let self_ordering = self_interior.constraint.ordering(); - let other_interior = builder.interior_node_data(other.node()); + let other_interior = storage.interior_node_data(other.node()); let other_ordering = other_interior.constraint.ordering(); let result = match self_ordering.cmp(&other_ordering) { - Ordering::Equal => NodeId::with_uncertain( - builder, - self_interior.constraint, - self_interior - .if_true - .or_inner(builder, other_interior.if_true, other_offset), - self_interior.if_uncertain.or_inner( - builder, - other_interior.if_uncertain, - other_offset, - ), - self_interior - .if_false - .or_inner(builder, other_interior.if_false, other_offset), - self_interior.source_order, - ), + Ordering::Equal => { + let if_true = self_interior.if_true.or(storage, other_interior.if_true); + let if_uncertain = self_interior + .if_uncertain + .or(storage, other_interior.if_uncertain); + let if_false = self_interior.if_false.or(storage, other_interior.if_false); + NodeId::with_uncertain( + storage, + self_interior.constraint, + if_true, + if_uncertain, + if_false, + ) + } // This is from Frisch's original description of TDDs. If self < other, we check self // first. Instead of distributing other into the if_true and if_false branches, we // "park" it in the if_uncertain branch. That causes us to only evaluate other "lazily" // when needed. - Ordering::Less => NodeId::with_uncertain( - builder, - self_interior.constraint, - self_interior.if_true, - self_interior - .if_uncertain - .or_inner(builder, other.node(), other_offset), - self_interior.if_false, - self_interior.source_order, - ), + Ordering::Less => { + let if_uncertain = self_interior.if_uncertain.or(storage, other.node()); + NodeId::with_uncertain( + storage, + self_interior.constraint, + self_interior.if_true, + if_uncertain, + self_interior.if_false, + ) + } // Ditto above but for the other variable ordering - Ordering::Greater => NodeId::with_uncertain( - builder, - other_interior.constraint, - other_interior.if_true, - self.node() - .or_inner(builder, other_interior.if_uncertain, other_offset), - other_interior.if_false, - other_interior.source_order + other_offset, - ), + Ordering::Greater => { + let if_uncertain = self.node().or(storage, other_interior.if_uncertain); + NodeId::with_uncertain( + storage, + other_interior.constraint, + other_interior.if_true, + if_uncertain, + other_interior.if_false, + ) + } }; - let mut storage = builder.storage.borrow_mut(); storage.or_cache.insert(key, result); result } - fn and(self, builder: &ConstraintSetBuilder<'_>, other: Self, other_offset: usize) -> NodeId { - let key = (self.node(), other.node(), other_offset); - let storage = builder.storage.borrow(); + fn and(self, storage: &mut ConstraintSetStorage<'_>, other: Self) -> NodeId { + let key = (self.node(), other.node()); if let Some(result) = storage.and_cache.get(&key) { return *result; } - drop(storage); - let self_interior = builder.interior_node_data(self.node()); + let self_interior = storage.interior_node_data(self.node()); let self_ordering = self_interior.constraint.ordering(); - let other_interior = builder.interior_node_data(other.node()); + let other_interior = storage.interior_node_data(other.node()); let other_ordering = other_interior.constraint.ordering(); let result = match self_ordering.cmp(&other_ordering) { // This is one of Duboc's optimizations over Frisch's original TDD operators. Frisch @@ -4113,88 +4387,59 @@ impl InteriorNode { // // See [Duboc2026], §11.2 for more details. Ordering::Equal => { - let if_true = self_interior + let other_if_true = other_interior .if_true - .and_inner( - builder, - other_interior.if_true.or_inner( - builder, - other_interior.if_uncertain, - other_offset, - ), - other_offset, - ) - .or_inner( - builder, - self_interior.if_uncertain.and_inner( - builder, - other_interior.if_true, - other_offset, - ), - 0, - ); - let if_uncertain = self_interior.if_uncertain.and_inner( - builder, - other_interior.if_uncertain, - other_offset, - ); - let if_false = self_interior - .if_false - .and_inner( - builder, - other_interior.if_uncertain.or_inner( - builder, - other_interior.if_false, - other_offset, - ), - other_offset, - ) - .or_inner( - builder, - self_interior.if_uncertain.and_inner( - builder, - other_interior.if_false, - other_offset, - ), - 0, - ); + .or(storage, other_interior.if_uncertain); + let true_from_true = self_interior.if_true.and(storage, other_if_true); + let true_from_uncertain = self_interior + .if_uncertain + .and(storage, other_interior.if_true); + let if_true = true_from_true.or(storage, true_from_uncertain); + let if_uncertain = self_interior + .if_uncertain + .and(storage, other_interior.if_uncertain); + let other_if_false = other_interior + .if_uncertain + .or(storage, other_interior.if_false); + let false_from_false = self_interior.if_false.and(storage, other_if_false); + let false_from_uncertain = self_interior + .if_uncertain + .and(storage, other_interior.if_false); + let if_false = false_from_false.or(storage, false_from_uncertain); NodeId::with_uncertain( - builder, + storage, self_interior.constraint, if_true, if_uncertain, if_false, - self_interior.source_order, ) } - Ordering::Less => NodeId::with_uncertain( - builder, - self_interior.constraint, - self_interior - .if_true - .and_inner(builder, other.node(), other_offset), - self_interior - .if_uncertain - .and_inner(builder, other.node(), other_offset), - self_interior - .if_false - .and_inner(builder, other.node(), other_offset), - self_interior.source_order, - ), - Ordering::Greater => NodeId::with_uncertain( - builder, - other_interior.constraint, - self.node() - .and_inner(builder, other_interior.if_true, other_offset), - self.node() - .and_inner(builder, other_interior.if_uncertain, other_offset), - self.node() - .and_inner(builder, other_interior.if_false, other_offset), - other_interior.source_order + other_offset, - ), + Ordering::Less => { + let if_true = self_interior.if_true.and(storage, other.node()); + let if_uncertain = self_interior.if_uncertain.and(storage, other.node()); + let if_false = self_interior.if_false.and(storage, other.node()); + NodeId::with_uncertain( + storage, + self_interior.constraint, + if_true, + if_uncertain, + if_false, + ) + } + Ordering::Greater => { + let if_true = self.node().and(storage, other_interior.if_true); + let if_uncertain = self.node().and(storage, other_interior.if_uncertain); + let if_false = self.node().and(storage, other_interior.if_false); + NodeId::with_uncertain( + storage, + other_interior.constraint, + if_true, + if_uncertain, + if_false, + ) + } }; - let mut storage = builder.storage.borrow_mut(); storage.and_cache.insert(key, result); result } @@ -4202,31 +4447,26 @@ impl InteriorNode { fn exists_inner<'db>( self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, - bound_typevars: InferableTypeVars<'db>, - ) -> NodeId { - let mentions_typevar = |ty: Type<'db>| match ty { - Type::TypeVar(typevar) => typevar.is_inferable(db, bound_typevars), - _ => false, - }; + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, + bound_typevars: TypeVarSet<'db>, + source_order: Option, + ) -> (NodeId, Option) { self.abstract_inner( db, - builder, + env, + storage, + source_order, // Remove any node that constrains one of `bound_typevars`, or that has a lower/upper // bound that mentions one of them. Removed constraints are still added to `path`, so // the sequent map can propagate any derived constraints that do not mention the // quantified typevars. - &mut |constraint| { - let constraint = builder.constraint_data(constraint); - constraint.typevar.is_inferable(db, bound_typevars) - || constraint - .bounds - .lower - .is_some_and(|lower| any_over_type(db, lower, false, mentions_typevar)) - || constraint - .bounds - .upper - .is_some_and(|upper| any_over_type(db, upper, false, mentions_typevar)) + &mut |storage: &ConstraintSetStorage<'_>, constraint| { + let support = storage.constraint_support(constraint); + support.iter().any(|typevar| { + let typevar = storage.typevar_data(typevar); + typevar.is_inferable(db, bound_typevars) + }) }, ) } @@ -4234,16 +4474,20 @@ impl InteriorNode { fn remove_noninferable<'db>( self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, - ) -> NodeId { - let is_bare_inferable_typevar = |ty: Type<'db>| { + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, + inferable: TypeVarSet<'db>, + source_order: Option, + ) -> (NodeId, Option) { + let is_bare_inferable_typevar = |ty: Type<'_>| { ty.as_typevar() .is_some_and(|bound_typevar| bound_typevar.is_inferable(db, inferable)) }; self.abstract_inner( db, - builder, + env, + storage, + source_order, // We only want to keep constraints on inferable typevars. If the constraint's typevar // is itself inferable, we keep it. We also need to keep some constraints in // non-inferable typevars, if their lower or upper bound is a bare inferable typevar. @@ -4253,8 +4497,8 @@ impl InteriorNode { // either as `Never ≤ I ≤ N` or `I ≤ N ≤ object`, depending on typevar ordering. If we // only checked the inferability of the constrained typevar, we would keep the first // encoding but remove the second. - &mut |constraint| { - let constraint = builder.constraint_data(constraint); + &mut |storage: &ConstraintSetStorage<'_>, constraint| { + let constraint = storage.constraint_data(constraint); !constraint.typevar.is_inferable(db, inferable) && !constraint .bounds @@ -4271,11 +4515,13 @@ impl InteriorNode { fn abstract_inner<'db, F>( self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, + source_order: Option, should_remove: F, - ) -> NodeId + ) -> (NodeId, Option) where - F: FnMut(ConstraintId) -> bool, + F: FnMut(&ConstraintSetStorage<'_>, ConstraintId) -> bool, { #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum Disposition { @@ -4289,64 +4535,64 @@ impl InteriorNode { impl PathVisitor for AbstractVisitor where - F: FnMut(ConstraintId) -> bool, + F: FnMut(&ConstraintSetStorage<'_>, ConstraintId) -> bool, { - type Result = NodeId; - type Interior = (Disposition, ConstraintId, usize); + type Result = (NodeId, Option); + type Interior = (Disposition, ConstraintId); type Break = Infallible; fn visit_satisfied<'db>( &mut self, _db: &'db dyn Db, - _builder: &ConstraintSetBuilder<'db>, + _storage: &mut ConstraintSetStorage<'db>, _path: &PathAssignments, ) -> ControlFlow { - ControlFlow::Continue(ALWAYS_TRUE) + ControlFlow::Continue((ALWAYS_TRUE, None)) } fn visit_unsatisfied<'db>( &mut self, _db: &'db dyn Db, - _builder: &ConstraintSetBuilder<'db>, + _storage: &mut ConstraintSetStorage<'db>, _path: &PathAssignments, ) -> ControlFlow { - ControlFlow::Continue(ALWAYS_FALSE) + ControlFlow::Continue((ALWAYS_FALSE, None)) } fn visit_impossible<'db>( &mut self, _db: &'db dyn Db, - _builder: &ConstraintSetBuilder<'db>, + _storage: &mut ConstraintSetStorage<'db>, _path: &PathAssignments, ) -> ControlFlow { - ControlFlow::Continue(ALWAYS_FALSE) + ControlFlow::Continue((ALWAYS_FALSE, None)) } fn enter_interior<'db>( &mut self, _db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, interior: InteriorNode, ) -> ControlFlow { - let interior = builder.interior_node_data(interior.node()); - let disposition = if (self.should_remove)(interior.constraint) { + let interior = storage.interior_node_data(interior.node()); + let disposition = if (self.should_remove)(storage, interior.constraint) { Disposition::Remove } else { Disposition::Keep }; - ControlFlow::Continue((disposition, interior.constraint, interior.source_order)) + ControlFlow::Continue((disposition, interior.constraint)) } fn visit_edge<'db>( &mut self, _db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, interior: &Self::Interior, subtree: Self::Result, path: &PathAssignments, new_range: Range, ) -> ControlFlow { - let (disposition, _, _) = interior; + let (disposition, _) = interior; match disposition { // If we are keeping this node, we don't need to add any derived facts to the // result; we can always re-derive them later. @@ -4354,29 +4600,22 @@ impl InteriorNode { // If we are removing this node, we have to check if there are any derived facts // that depend on the constraint we're about to remove. If so, we need to - // "remember" them by AND-ing them in with the corresponding branch. We currently - // reuse the `source_order` of the constraint being removed when we add these - // derived facts. + // "remember" them by AND-ing them in with the corresponding branch. Disposition::Remove => { - ControlFlow::Continue( - path.assignments[new_range] - .iter() - .filter(|(assignment, _)| { - // Don't add back any derived facts if they are ones that we would have - // removed! - !(self.should_remove)(assignment.constraint()) - }) - .fold(subtree, |subtree, (assignment, (source_order, _))| { - subtree.and( - builder, - Node::new_satisfied_constraint( - builder, - *assignment, - *source_order, - ), - ) - }), - ) + let (mut result, mut result_source_order) = subtree; + for (assignment, _) in &path.assignments[new_range] { + // Don't add back any derived facts if they are ones that we would have + // removed! + if (self.should_remove)(storage, assignment.constraint()) { + continue; + } + let (assignment, assignment_source_order) = + Node::new_satisfied_constraint(storage, *assignment); + result = result.and(storage, assignment); + result_source_order = storage + .ordered_source_order(result_source_order, assignment_source_order); + } + ControlFlow::Continue((result, result_source_order)) } } } @@ -4384,13 +4623,13 @@ impl InteriorNode { fn leave_interior<'db>( &mut self, _db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, interior: &Self::Interior, if_true: Self::Result, if_uncertain: Self::Result, if_false: Self::Result, ) -> ControlFlow { - let (disposition, constraint, source_order) = interior; + let (disposition, constraint) = interior; match disposition { // If we are keeping this node, absorb the uncertain branch into both the true // and false branches before constructing the ITE, matching TDD semantics: when @@ -4401,11 +4640,21 @@ impl InteriorNode { // derived constraints into the result, and those constraints might appear before this // one in the BDD ordering. Disposition::Keep => { - let guard = Node::new_constraint(builder, *constraint, *source_order); - ControlFlow::Continue(guard.ite( - builder, - if_true.or(builder, if_uncertain), - if_false.or(builder, if_uncertain), + let (guard, guard_source_order) = + Node::new_constraint(storage, *constraint); + let (if_true, if_true_source_order) = if_true; + let (if_uncertain, if_uncertain_source_order) = if_uncertain; + let (if_false, if_false_source_order) = if_false; + let if_true = if_true.or(storage, if_uncertain); + let if_false = if_false.or(storage, if_uncertain); + let node = guard.ite(storage, if_true, if_false); + let left_source_order = + storage.ordered_source_order(guard_source_order, if_true_source_order); + let right_source_order = storage + .ordered_source_order(if_uncertain_source_order, if_false_source_order); + ControlFlow::Continue(( + node, + storage.ordered_source_order(left_source_order, right_source_order), )) } @@ -4413,560 +4662,50 @@ impl InteriorNode { // outgoing edges. That is, the result is true if there's any assignment of // this node's constraint that is true. (We will have already added any // necessary derived facts in the `visit_edge` method.) - Disposition::Remove => ControlFlow::Continue( - if_true.or(builder, if_uncertain).or(builder, if_false), - ), + Disposition::Remove => { + let (if_true, if_true_source_order) = if_true; + let (if_uncertain, if_uncertain_source_order) = if_uncertain; + let (if_false, if_false_source_order) = if_false; + let node = if_true.or(storage, if_uncertain).or(storage, if_false); + let source_order = storage + .ordered_source_order(if_true_source_order, if_uncertain_source_order); + ControlFlow::Continue(( + node, + storage.ordered_source_order(source_order, if_false_source_order), + )) + } } } } - let mut path = self.path_assignments(builder); + let mut path = self.path_assignments(storage, source_order); let mut visitor = AbstractVisitor { should_remove }; - let ControlFlow::Continue(result) = path.visit(db, builder, self.node(), &mut visitor); + let ControlFlow::Continue(result) = path.visit(db, env, storage, self.node(), &mut visitor); result } - fn restrict_one<'db>( + fn path_assignments( self, - db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, - assignment: ConstraintAssignment, - ) -> (NodeId, bool) { - let key = (self.node(), assignment); - let storage = builder.storage.borrow(); - if let Some(result) = storage.restrict_one_cache.get(&key) { - return *result; - } - drop(storage); - - let self_interior = builder.interior_node_data(self.node()); - let self_ordering = self_interior.constraint.ordering(); - let result = if assignment.constraint().ordering() < self_ordering { - // If this node's variable is larger than the assignment's variable, then we have reached a - // point in the BDD where the assignment can no longer affect the result, - // and we can return early. - (self.node(), false) - } else { - // Otherwise, check if this node's variable is in the assignment. If so, substitute the - // variable by replacing this node with the appropriate edge(s). When restricting a - // TDD, the uncertain branch is folded in. - if assignment == self_interior.constraint.when_true() { - // restrict(n? C: U: D, n == true) = C ∨ U - ( - self_interior - .if_true - .or(builder, self_interior.if_uncertain), - true, - ) - } else if assignment == self_interior.constraint.when_false() { - // restrict(n? C: U: D, n == false) = D ∨ U - ( - self_interior - .if_false - .or(builder, self_interior.if_uncertain), - true, - ) - } else if assignment == self_interior.constraint.when_unconstrained() { - // restrict(n? C: U: D, n is unconstrained) = C ∨ U ∨ D - ( - self_interior - .if_true - .or(builder, self_interior.if_uncertain) - .or(builder, self_interior.if_false), - true, - ) - } else { - let (if_true, found_in_true) = - self_interior.if_true.restrict_one(db, builder, assignment); - let (if_uncertain, found_in_uncertain) = self_interior - .if_uncertain - .restrict_one(db, builder, assignment); - let (if_false, found_in_false) = - self_interior.if_false.restrict_one(db, builder, assignment); - ( - NodeId::with_uncertain( - builder, - self_interior.constraint, - if_true, - if_uncertain, - if_false, - self_interior.source_order, - ), - found_in_true || found_in_uncertain || found_in_false, - ) - } - }; - - let mut storage = builder.storage.borrow_mut(); - storage.restrict_one_cache.insert(key, result); - result - } - - fn path_assignments(self, builder: &ConstraintSetBuilder<'_>) -> PathAssignments { - // Sort the constraints in this BDD by their `source_order`s before adding them to the - // sequent map. This ensures that constraints appear in the sequent map in a stable order. - // The constraints mentioned in a BDD should all have distinct `source_order`s, so an - // unstable sort is fine. + storage: &mut ConstraintSetStorage<'_>, + source_order: Option, + ) -> PathAssignments { let mut constraints: SmallVec<[_; 8]> = SmallVec::new(); self.node() - .for_each_unique_constraint(builder, &mut |constraint, source_order| { - constraints.push((constraint, source_order)); - }); - constraints.sort_unstable_by_key(|(_, source_order)| *source_order); - - PathAssignments::new(constraints.into_iter().map(|(constraint, _)| constraint)) - } - - /// Returns a simplified version of a BDD. - /// - /// This is calculated by looking at the relationships that exist between the constraints that - /// are mentioned in the BDD. For instance, if one constraint implies another (`x → y`), then - /// `x ∧ ¬y` is not a valid input, and we can rewrite any occurrences of `x ∨ y` into `y`. - fn simplify<'db>(self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>) -> NodeId { - let key = self.node(); - let storage = builder.storage.borrow(); - if let Some(result) = storage.simplify_cache.get(&key) { - return *result; - } - drop(storage); - - // To simplify a non-terminal BDD, we find all pairs of constraints that are mentioned in - // the BDD. If any of those pairs can be simplified to some other BDD, we perform a - // substitution to replace the pair with the simplification. - // - // Some of the simplifications create _new_ constraints that weren't originally present in - // the BDD. If we encounter one of those cases, we need to check if we can simplify things - // further relative to that new constraint. - // - // To handle this, we keep track of the individual constraints that we have already - // discovered (`seen_constraints`), and a queue of constraint pairs that we still need to - // check (`to_visit`). - - // Seed the seen set with all of the constraints that are present in the input BDD, and the - // visit queue with all pairs of those constraints. (We use "combinations" because we don't - // need to compare a constraint against itself, and because ordering doesn't matter.) - let mut seen_constraints = FxHashSet::default(); - let mut source_orders = FxHashMap::default(); - self.node() - .for_each_unique_constraint(builder, &mut |constraint, source_order| { - seen_constraints.insert(constraint); - source_orders - .entry(constraint) - .and_modify(|existing: &mut usize| *existing = (*existing).min(source_order)) - .or_insert(source_order); + .for_each_unique_constraint(storage, &mut |constraint| { + constraints.push(constraint); }); - let mut to_visit: Vec<(_, _)> = (seen_constraints.iter().copied()) - .array_combinations() - .map(|[left, right]| (left, right)) - .collect(); - - // Repeatedly pop constraint pairs off of the visit queue, checking whether each pair can - // be simplified. If we add any derived constraints, we will place them at the end in - // source order. (We do not have any test cases that depend on constraint sets being - // displayed in a consistent ordering, so we don't need to be clever in assigning these - // `source_order`s.) - let mut simplified = self.node(); - let self_interior = builder.interior_node_data(self.node()); - let mut next_source_order = self_interior.max_source_order + 1; - while let Some((left_constraint, right_constraint)) = to_visit.pop() { - let left_source_order = source_orders[&left_constraint]; - let right_source_order = source_orders[&right_constraint]; - - // If the constraints refer to different typevars, the only simplifications we can make - // are of the form `S ≤ T ∧ T ≤ int → S ≤ int`. - let left_constraint_data = builder.constraint_data(left_constraint); - let left_typevar = left_constraint_data.typevar; - let right_constraint_data = builder.constraint_data(right_constraint); - let right_typevar = right_constraint_data.typevar; - if !left_typevar.is_same_typevar_as(db, right_typevar) { - // We've structured our constraints so that a typevar's upper/lower bound can only - // be another typevar if the bound is "later" in our arbitrary ordering. That means - // we only have to check this pair of constraints in one direction — though we do - // have to figure out which of the two typevars is constrained, and which one is - // the upper/lower bound. - let (bound_constraint, constrained_constraint) = - if left_typevar.can_be_bound_for(db, builder, right_typevar) { - (left_constraint, right_constraint) - } else { - (right_constraint, left_constraint) - }; - let bound_constraint_data = builder.constraint_data(bound_constraint); - let bound_typevar = bound_constraint_data.typevar; - let constrained_constraint_data = builder.constraint_data(constrained_constraint); - let constrained_typevar = constrained_constraint_data.typevar; - - // We then look for cases where the "constrained" typevar's upper and/or lower - // bound matches the "bound" typevar. If so, we're going to add an implication to - // the constraint set that replaces the upper/lower bound that matched with the - // bound constraint's corresponding bound. - let (new_lower, new_upper) = match ( - constrained_constraint_data.bounds.lower, - constrained_constraint_data.bounds.upper, - ) { - // (B ≤ C ≤ B) ∧ (BL ≤ B ≤ BU) → (BL ≤ C ≤ BU) - ( - Some(Type::TypeVar(constrained_lower)), - Some(Type::TypeVar(constrained_upper)), - ) if constrained_lower.is_same_typevar_as(db, bound_typevar) - && constrained_upper.is_same_typevar_as(db, bound_typevar) => - { - ( - bound_constraint_data.bounds.lower, - bound_constraint_data.bounds.upper, - ) - } - - // (CL ≤ C ≤ B) ∧ (BL ≤ B ≤ BU) → (CL ≤ C ≤ BU) - (constrained_lower, Some(Type::TypeVar(constrained_upper))) - if constrained_upper.is_same_typevar_as(db, bound_typevar) => - { - (constrained_lower, bound_constraint_data.bounds.upper) - } - - // (B ≤ C ≤ CU) ∧ (BL ≤ B ≤ BU) → (BL ≤ C ≤ CU) - (Some(Type::TypeVar(constrained_lower)), constrained_upper) - if constrained_lower.is_same_typevar_as(db, bound_typevar) => - { - (bound_constraint_data.bounds.lower, constrained_upper) - } - - _ => continue, - }; - - let new_constraint = ConstraintId::new_with_bounds( - db, - builder, - constrained_typevar, - new_lower, - new_upper, - ); - if seen_constraints.contains(&new_constraint) { - continue; - } - let new_node = Node::new_constraint(builder, new_constraint, next_source_order); - next_source_order += 1; - let positive_left_node = Node::new_satisfied_constraint( - builder, - left_constraint.when_true(), - left_source_order, - ); - let positive_right_node = Node::new_satisfied_constraint( - builder, - right_constraint.when_true(), - right_source_order, - ); - let lhs = positive_left_node.and(builder, positive_right_node); - let intersection = new_node.ite(builder, lhs, ALWAYS_FALSE); - simplified = simplified.and(builder, intersection); - continue; - } - - // From here on out we know that both constraints constrain the same typevar. The - // clause above will propagate all that we know about the current typevar relative to - // other typevars, producing constraints on this typevar that have concrete lower/upper - // bounds. That means we can skip the simplifications below if any bound is another - // typevar. - if left_constraint_data - .bounds - .lower - .is_some_and(Type::is_type_var) - || left_constraint_data - .bounds - .upper - .is_some_and(Type::is_type_var) - || right_constraint_data - .bounds - .lower - .is_some_and(Type::is_type_var) - || right_constraint_data - .bounds - .upper - .is_some_and(Type::is_type_var) - { - continue; - } - - // Containment: The range of one constraint might completely contain the range of the - // other. If so, there are several potential simplifications. - let larger_smaller = if left_constraint.implies(db, builder, right_constraint) { - Some(( - right_constraint, - right_source_order, - left_constraint, - left_source_order, - )) - } else if right_constraint.implies(db, builder, left_constraint) { - Some(( - left_constraint, - left_source_order, - right_constraint, - right_source_order, - )) - } else { - None - }; - if let Some(( - larger_constraint, - larger_source_order, - smaller_constraint, - smaller_source_order, - )) = larger_smaller - { - let positive_larger_node = Node::new_satisfied_constraint( - builder, - larger_constraint.when_true(), - larger_source_order, - ); - let negative_larger_node = Node::new_satisfied_constraint( - builder, - larger_constraint.when_false(), - larger_source_order, - ); - - // larger ∨ smaller = larger - simplified = simplified.substitute_union( - db, - builder, - larger_constraint.when_true(), - larger_source_order, - smaller_constraint.when_true(), - smaller_source_order, - positive_larger_node, - ); - - // ¬larger ∧ ¬smaller = ¬larger - simplified = simplified.substitute_intersection( - db, - builder, - larger_constraint.when_false(), - larger_source_order, - smaller_constraint.when_false(), - smaller_source_order, - negative_larger_node, - ); - - // smaller ∧ ¬larger = false - // (¬larger removes everything that's present in smaller) - simplified = simplified.substitute_intersection( - db, - builder, - larger_constraint.when_false(), - larger_source_order, - smaller_constraint.when_true(), - smaller_source_order, - ALWAYS_FALSE, - ); - - // larger ∨ ¬smaller = true - // (larger fills in everything that's missing in ¬smaller) - simplified = simplified.substitute_union( - db, - builder, - larger_constraint.when_true(), - larger_source_order, - smaller_constraint.when_false(), - smaller_source_order, - ALWAYS_TRUE, - ); - } - - // There are some simplifications we can make when the intersection of the two - // constraints is empty, and others that we can make when the intersection is - // non-empty. - match left_constraint.intersect(db, builder, right_constraint) { - IntersectionResult::Simplified(intersection_constraint_data) => { - let intersection_constraint = - builder.intern_constraint(db, intersection_constraint_data); - - // If the intersection is non-empty, we need to create a new constraint to - // represent that intersection. We also need to add the new constraint to our - // seen set and (if we haven't already seen it) to the to-visit queue. - if seen_constraints.insert(intersection_constraint) { - source_orders.insert(intersection_constraint, next_source_order); - to_visit.extend( - (seen_constraints.iter().copied()) - .filter(|seen| *seen != intersection_constraint) - .map(|seen| (seen, intersection_constraint)), - ); - } - let positive_intersection_node = Node::new_satisfied_constraint( - builder, - intersection_constraint.when_true(), - next_source_order, - ); - let negative_intersection_node = Node::new_satisfied_constraint( - builder, - intersection_constraint.when_false(), - next_source_order, - ); - next_source_order += 1; - - let positive_left_node = Node::new_satisfied_constraint( - builder, - left_constraint.when_true(), - left_source_order, - ); - let negative_left_node = Node::new_satisfied_constraint( - builder, - left_constraint.when_false(), - left_source_order, - ); - - let positive_right_node = Node::new_satisfied_constraint( - builder, - right_constraint.when_true(), - right_source_order, - ); - let negative_right_node = Node::new_satisfied_constraint( - builder, - right_constraint.when_false(), - right_source_order, - ); - - // left ∧ right = intersection - simplified = simplified.substitute_intersection( - db, - builder, - left_constraint.when_true(), - left_source_order, - right_constraint.when_true(), - right_source_order, - positive_intersection_node, - ); - - // ¬left ∨ ¬right = ¬intersection - simplified = simplified.substitute_union( - db, - builder, - left_constraint.when_false(), - left_source_order, - right_constraint.when_false(), - right_source_order, - negative_intersection_node, - ); - - // left ∧ ¬right = left ∧ ¬intersection - // (clip the negative constraint to the smallest range that actually removes - // something from positive constraint) - simplified = simplified.substitute_intersection( - db, - builder, - left_constraint.when_true(), - left_source_order, - right_constraint.when_false(), - right_source_order, - positive_left_node.and(builder, negative_intersection_node), - ); - - // ¬left ∧ right = ¬intersection ∧ right - // (save as above but reversed) - simplified = simplified.substitute_intersection( - db, - builder, - left_constraint.when_false(), - left_source_order, - right_constraint.when_true(), - right_source_order, - positive_right_node.and(builder, negative_intersection_node), - ); - - // left ∨ ¬right = intersection ∨ ¬right - // (clip the positive constraint to the smallest range that actually adds - // something to the negative constraint) - simplified = simplified.substitute_union( - db, - builder, - left_constraint.when_true(), - left_source_order, - right_constraint.when_false(), - right_source_order, - negative_right_node.or(builder, positive_intersection_node), - ); - - // ¬left ∨ right = ¬left ∨ intersection - // (save as above but reversed) - simplified = simplified.substitute_union( - db, - builder, - left_constraint.when_false(), - left_source_order, - right_constraint.when_true(), - right_source_order, - negative_left_node.or(builder, positive_intersection_node), - ); - } - - // If the intersection doesn't simplify to a single clause, we shouldn't update the - // BDD. - IntersectionResult::CannotSimplify => {} - - IntersectionResult::Disjoint => { - // All of the below hold because we just proved that the intersection of left - // and right is empty. - - let positive_left_node = Node::new_satisfied_constraint( - builder, - left_constraint.when_true(), - left_source_order, - ); - let positive_right_node = Node::new_satisfied_constraint( - builder, - right_constraint.when_true(), - right_source_order, - ); - - // left ∧ right = false - simplified = simplified.substitute_intersection( - db, - builder, - left_constraint.when_true(), - left_source_order, - right_constraint.when_true(), - right_source_order, - ALWAYS_FALSE, - ); - - // ¬left ∨ ¬right = true - simplified = simplified.substitute_union( - db, - builder, - left_constraint.when_false(), - left_source_order, - right_constraint.when_false(), - right_source_order, - ALWAYS_TRUE, - ); - - // left ∧ ¬right = left - // (there is nothing in the hole of ¬right that overlaps with left) - simplified = simplified.substitute_intersection( - db, - builder, - left_constraint.when_true(), - left_source_order, - right_constraint.when_false(), - right_source_order, - positive_left_node, - ); - - // ¬left ∧ right = right - // (save as above but reversed) - simplified = simplified.substitute_intersection( - db, - builder, - left_constraint.when_false(), - left_source_order, - right_constraint.when_true(), - right_source_order, - positive_right_node, - ); - } - } - } - - let mut storage = builder.storage.borrow_mut(); - storage.simplify_cache.insert(key, simplified); - simplified + let source_orders = storage.calculate_source_orders(source_order); + // `PathAssignments` seeds its insertion-ordered discovered-constraint map from this list, + // and uses that order when constructing non-commutative sequent pairs. Do not replace this + // with TDD traversal order: doing so can change inference and lose gradual constraints. + // Every constraint in the TDD must appear in the sidecar. If an operation introduces new + // constraints, it must preserve their source orders rather than invent an order here. + constraints.sort_by_key(|constraint| { + source_orders + .get_index_of(constraint) + .expect("every BDD constraint should have a source-order entry") + }); + PathAssignments::new(constraints) } } @@ -5019,82 +4758,17 @@ impl ConstraintAssignment { } } - fn negate(&mut self) { - *self = self.negated(); - } - - /// Returns whether this constraint implies another — i.e., whether every type that - /// satisfies this constraint also satisfies `other`. - /// - /// This is used to simplify how we display constraint sets, by removing redundant constraints - /// from a clause. - fn implies<'db>( + fn display<'db, 'a>( self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, - other: Self, - ) -> bool { - match (self, other) { - // For two positive constraints, one range has to fully contain the other; the smaller - // constraint implies the larger. - // - // ....|----other-----|.... - // ......|---self---|...... - ( - ConstraintAssignment::Positive(self_constraint), - ConstraintAssignment::Positive(other_constraint), - ) => self_constraint.implies(db, builder, other_constraint), - - // For two negative constraints, one range has to fully contain the other; the ranges - // represent "holes", though, so the constraint with the larger range implies the one - // with the smaller. - // - // |-----|...other...|-----| - // |---|.....self......|---| - ( - ConstraintAssignment::Negative(self_constraint), - ConstraintAssignment::Negative(other_constraint), - ) => other_constraint.implies(db, builder, self_constraint), - - // For a positive and negative constraint, the ranges have to be disjoint, and the - // positive range implies the negative range. - // - // |---------------|...self...|---| - // ..|---other---|................| - ( - ConstraintAssignment::Positive(self_constraint), - ConstraintAssignment::Negative(other_constraint), - ) => self_constraint - .intersect(db, builder, other_constraint) - .is_disjoint(), - - // It's theoretically possible for a negative constraint to imply a positive constraint - // if the positive constraint is always satisfied (`Never ≤ T ≤ object`). But we never - // create constraints of that form, so with our representation, a negative constraint - // can never imply a positive constraint. - // - // |------other-------| - // |---|...self...|---| - (ConstraintAssignment::Negative(_), ConstraintAssignment::Positive(_)) => false, - - // An `Unconstrained` assignment means "this constraint can go either way." It does - // not imply any positive or negative assignment, and no positive or negative - // assignment implies it. The only trivially true case is Unconstrained => Unconstrained - // for the same constraint. - ( - ConstraintAssignment::Unconstrained(self_constraint), - ConstraintAssignment::Unconstrained(other_constraint), - ) => self_constraint == other_constraint, - (ConstraintAssignment::Unconstrained(_), _) - | (_, ConstraintAssignment::Unconstrained(_)) => false, - } - } - - fn display<'db>(self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>) -> impl Display { + env: &'a ProgramEnvironment<'db>, + storage: &'a ConstraintSetStorage<'db>, + ) -> impl Display + 'a { struct DisplayConstraintAssignment<'db, 'c> { assignment: ConstraintAssignment, db: &'db dyn Db, - builder: &'c ConstraintSetBuilder<'db>, + env: &'c ProgramEnvironment<'db>, + storage: &'c ConstraintSetStorage<'db>, } impl DisplayConstraintAssignment<'_, '_> { @@ -5117,17 +4791,19 @@ impl ConstraintAssignment { impl Display for DisplayConstraintAssignment<'_, '_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let constraint_data = self.builder.constraint_data(self.assignment.constraint()); + let db = self.db; + + let constraint_data = self.storage.constraint_data(self.assignment.constraint()); let lower = constraint_data.bounds.materialized_lower(); let upper = constraint_data.bounds.materialized_upper(); let typevar = constraint_data.typevar; - if lower.is_equivalent_to(self.db, upper) { + if lower.is_equivalent_to(db, self.env, upper) { // If this typevar is equivalent to another, output the constraint in a // consistent alphabetical order, regardless of the salsa ordering that we are // using the in BDD. if let Type::TypeVar(bound) = lower { - let bound = bound.identity(self.db).display(self.db).to_string(); - let typevar = typevar.identity(self.db).display(self.db).to_string(); + let bound = bound.identity(db).display(db).to_string(); + let typevar = typevar.identity(db).display(db).to_string(); let (smaller, larger) = if bound < typevar { (bound, typevar) } else { @@ -5139,9 +4815,9 @@ impl ConstraintAssignment { return write!( f, "({} {} {})", - typevar.identity(self.db).display(self.db), + typevar.identity(db).display(db), self.equality_sign(), - lower.display(self.db) + lower.display(db, self.env) ); } @@ -5149,7 +4825,7 @@ impl ConstraintAssignment { return write!( f, "({} {} *)", - typevar.identity(self.db).display(self.db), + typevar.identity(db).display(db), self.equality_sign() ); } @@ -5157,11 +4833,11 @@ impl ConstraintAssignment { f.write_str(self.range_prefix())?; f.write_str("(")?; if !lower.is_never() { - write!(f, "{} ≤ ", lower.display(self.db))?; + write!(f, "{} ≤ ", lower.display(db, self.env))?; } - typevar.identity(self.db).display(self.db).fmt(f)?; + typevar.identity(db).display(db).fmt(f)?; if !upper.is_object() { - write!(f, " ≤ {}", upper.display(self.db))?; + write!(f, " ≤ {}", upper.display(db, self.env))?; } f.write_str(")") } @@ -5170,7 +4846,8 @@ impl ConstraintAssignment { DisplayConstraintAssignment { assignment: self, db, - builder, + env, + storage, } } } @@ -5244,30 +4921,22 @@ impl SequentMap { /// constraint. fn for_constraint<'db, 'c>( db: &'db dyn Db, - builder: &'c ConstraintSetBuilder<'db>, + env: &ProgramEnvironment<'db>, + storage: &'c mut ConstraintSetStorage<'db>, constraint: ConstraintId, - ) -> Ref<'c, Self> { + ) -> &'c Self { let key = constraint; - let storage = builder.storage.borrow(); - if let Ok(map) = Ref::filter_map(storage, |storage| storage.single_sequent_cache.get(&key)) - { - return map; + if !storage.single_sequent_cache.contains_key(&key) { + tracing::trace!( + target: "ty_python_semantic::types::constraints::SequentMap", + constraint = %constraint.display(db, env, storage), + "add sequents for constraint", + ); + let mut map = SequentMap::default(); + map.add_sequents_for_single(db, env, storage, constraint); + storage.single_sequent_cache.insert(key, map); } - - tracing::trace!( - target: "ty_python_semantic::types::constraints::SequentMap", - constraint = %constraint.display(db, builder), - "add sequents for constraint", - ); - let mut map = SequentMap::default(); - map.add_sequents_for_single(db, builder, constraint); - - let mut storage = builder.storage.borrow_mut(); - storage.single_sequent_cache.insert(key, map); - drop(storage); - - let storage = builder.storage.borrow(); - Ref::map(storage, |storage| &storage.single_sequent_cache[&key]) + &storage.single_sequent_cache[&key] } /// Returns a sequent map containing the sequents that we can infer from a pair of constraints. @@ -5278,31 +4947,68 @@ impl SequentMap { /// that retain that ordering.) fn for_constraint_pair<'db, 'c>( db: &'db dyn Db, - builder: &'c ConstraintSetBuilder<'db>, + env: &ProgramEnvironment<'db>, + storage: &'c mut ConstraintSetStorage<'db>, left: ConstraintId, right: ConstraintId, - ) -> Ref<'c, Self> { + ) -> &'c Self { let key = (left, right); - let storage = builder.storage.borrow(); - if let Ok(map) = Ref::filter_map(storage, |storage| storage.pair_sequent_cache.get(&key)) { - return map; + if !storage.pair_sequent_cache.contains_key(&key) { + tracing::trace!( + target: "ty_python_semantic::types::constraints::SequentMap", + left = %left.display(db, env, storage), + right = %right.display(db, env, storage), + "add sequents for constraint pair", + ); + let mut map = SequentMap::default(); + map.add_sequents_for_pair(db, env, storage, left, right); + storage.pair_sequent_cache.insert(key, map); } + &storage.pair_sequent_cache[&key] + } - tracing::trace!( - target: "ty_python_semantic::types::constraints::SequentMap", - left = %left.display(db, builder), - right = %right.display(db, builder), - "add sequents for constraint pair", - ); - let mut map = SequentMap::default(); - map.add_sequents_for_pair(db, builder, left, right); + /// Quickly determines whether two constraints cannot possibly produce any sequents when passed + /// to [`for_constraint_pair`][Self::for_constraint_pair]. If this returns `true`, it is safe + /// to skip calling `for_constraint_pair` for this pair of constraints. + fn pair_cannot_produce_sequents<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, + left: ConstraintId, + right: ConstraintId, + ) -> bool { + // Currently, the only pattern we look for is when two constraints that have _only_ lower + // bounds, where those lower bounds are disjoint. Given `l₁ ≤ T ∧ l₂ ≤ T`, the only + // sequent we could theoretically produce is `(l₁ | l₂) ≤ T`. But we don't store that as a + // single constraint; we always break that apart into the two smaller constraints that we + // started with. + + let left = storage.constraint_data(left); + let right = storage.constraint_data(right); + if !left.typevar.is_same_typevar_as(db, right.typevar) { + return false; + } - let mut storage = builder.storage.borrow_mut(); - storage.pair_sequent_cache.insert(key, map); - drop(storage); + let ( + ConstraintBounds { + lower: Some(left_lower), + upper: None, + }, + ConstraintBounds { + lower: Some(right_lower), + upper: None, + }, + ) = (left.bounds, right.bounds) + else { + return false; + }; - let storage = builder.storage.borrow(); - Ref::map(storage, |storage| &storage.pair_sequent_cache[&key]) + // This call might need its own borrow of the builder's storage, so create a new builder + // that it can use. + let builder = ConstraintSetBuilder::new(); + left_lower + .when_trivially_disjoint_from(db, env, right_lower, &builder, TypeVarSet::None) + .is_trivially_always_satisfied() } fn add_single_tautology(&mut self, ante: ConstraintId) { @@ -5317,27 +5023,33 @@ impl SequentMap { fn add_pair_implication<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, ante1: ConstraintId, ante2: ConstraintId, post: ConstraintId, ) { // If the post constraint is unsatisfiable, then the antecedents contradict each other. - let post_data = builder.constraint_data(post); - let when = builder.load( + let post_data = storage.constraint_data(post); + let (when, source_order) = storage.load( db, + env, &post_data .bounds .materialized_lower() - .when_constraint_set_assignable_to_owned(db, post_data.bounds.materialized_upper()), + .when_constraint_set_assignable_to_owned( + db, + env, + post_data.bounds.materialized_upper(), + ), ); - if when.is_never_satisfied(db) { + if when.is_never_satisfied(db, env, storage, source_order) { self.add_pair_impossibility(ante1, ante2); return; } // If either antecedent implies the consequent on its own, this new sequent is redundant. - if ante1.implies(db, builder, post) || ante2.implies(db, builder, post) { + if ante1.implies(db, env, storage, post) || ante2.implies(db, env, storage, post) { return; } @@ -5357,12 +5069,13 @@ impl SequentMap { fn add_sequents_for_single<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, constraint: ConstraintId, ) { // If this constraint binds its typevar to `Never ≤ T ≤ object`, then the typevar can take // on any type, and the constraint is always satisfied. - let constraint_data = builder.constraint_data(constraint); + let constraint_data = storage.constraint_data(constraint); let lower = constraint_data.bounds.materialized_lower(); let upper = constraint_data.bounds.materialized_upper(); if lower.is_never() && upper.is_object() { @@ -5414,21 +5127,25 @@ impl SequentMap { return; } - let when = builder.load( + let (when, source_order) = storage.load( db, - &lower.when_constraint_set_assignable_to_owned(db, upper), + env, + &lower.when_constraint_set_assignable_to_owned(db, env, upper), ); // If L is _never_ assignable to U, this constraint would violate transitivity, and should // never have been added. - debug_assert!(!when.is_never_satisfied(db)); + #[expect(clippy::debug_assert_with_mut_call)] + { + debug_assert!(!when.is_never_satisfied(db, env, storage, source_order)); + } // Fast path: If L is trivially always assignable to U, there are no derived constraints // that we can infer. This would be handled correctly by the logic below, but this is a // useful early return. Since we only use this check as an early return happy path, we can // accept false negatives. That lets us use the simpler and cheaper check against // ALWAYS_TRUE, rather than a more expensive is_always_satisfiable call. - if when.node == ALWAYS_TRUE { + if when == ALWAYS_TRUE { return; } @@ -5464,8 +5181,8 @@ impl SequentMap { // it once for _every_ root→always path in the BDD. (That would require resetting the // PathAssignments state for each of those paths, which is why the logic would have to // move.) - let mut node = when.node; - if !node.is_single_conjunction(builder) { + let mut node = when; + if !node.is_single_conjunction(storage) { return; } @@ -5473,7 +5190,7 @@ impl SequentMap { match node.node() { Node::AlwaysTrue | Node::AlwaysFalse => break, Node::Interior(interior) => { - let interior = builder.interior_node_data(interior.node()); + let interior = storage.interior_node_data(interior.node()); if interior.if_true != ALWAYS_FALSE { self.add_single_implication(constraint, interior.constraint); node = interior.if_true; @@ -5489,7 +5206,8 @@ impl SequentMap { fn add_sequents_for_pair<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, left_constraint: ConstraintId, right_constraint: ConstraintId, ) { @@ -5514,19 +5232,20 @@ impl SequentMap { // // If all of the lower and upper bounds are concrete (i.e., not typevars), then there // several _other_ sequents that we can add, as handled by `add_concrete_sequents`. - let left_constraint_data = builder.constraint_data(left_constraint); + let left_constraint_data = storage.constraint_data(left_constraint); let left_typevar = left_constraint_data.typevar; - let right_constraint_data = builder.constraint_data(right_constraint); + let right_constraint_data = storage.constraint_data(right_constraint); let right_typevar = right_constraint_data.typevar; if !left_typevar.is_same_typevar_as(db, right_typevar) { self.add_mutual_sequents_for_different_typevars( db, - builder, + env, + storage, left_constraint, right_constraint, ); - self.add_nested_typevar_sequents(db, builder, left_constraint, right_constraint); + self.add_nested_typevar_sequents(db, env, storage, left_constraint, right_constraint); } else if left_constraint_data .bounds .lower @@ -5546,19 +5265,21 @@ impl SequentMap { { self.add_mutual_sequents_for_same_typevars( db, - builder, + env, + storage, left_constraint, right_constraint, ); } else { - self.add_concrete_sequents(db, builder, left_constraint, right_constraint); + self.add_concrete_sequents(db, env, storage, left_constraint, right_constraint); } } fn add_mutual_sequents_for_different_typevars<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, left_constraint: ConstraintId, right_constraint: ConstraintId, ) { @@ -5567,12 +5288,12 @@ impl SequentMap { // we only have to check this pair of constraints in one direction — though we do // have to figure out which of the two typevars is constrained, and which one is // the upper/lower bound. - let left_constraint_data = builder.constraint_data(left_constraint); + let left_constraint_data = storage.constraint_data(left_constraint); let left_typevar = left_constraint_data.typevar; - let right_constraint_data = builder.constraint_data(right_constraint); + let right_constraint_data = storage.constraint_data(right_constraint); let right_typevar = right_constraint_data.typevar; let (bound_constraint, constrained_constraint) = - if left_typevar.can_be_bound_for(db, builder, right_typevar) { + if left_typevar.can_be_bound_for(db, storage, right_typevar) { (left_constraint, right_constraint) } else { (right_constraint, left_constraint) @@ -5582,9 +5303,9 @@ impl SequentMap { // matches the "bound" typevar. If so, we're going to add an implication sequent that // replaces the upper/lower bound that matched with the bound constraint's corresponding // bound. - let bound_constraint_data = builder.constraint_data(bound_constraint); + let bound_constraint_data = storage.constraint_data(bound_constraint); let bound_typevar = bound_constraint_data.typevar; - let constrained_constraint_data = builder.constraint_data(constrained_constraint); + let constrained_constraint_data = storage.constraint_data(constrained_constraint); let constrained_typevar = constrained_constraint_data.typevar; // Transitive pivots require subtyping; classes with dynamic bases can be assignable to @@ -5628,10 +5349,11 @@ impl SequentMap { (constrained_lower, Some(constrained_upper), Some(bound_lower), _) if !constrained_upper.is_never() && !constrained_upper.is_object() - && builder.cached_is_constraint_set_subtype_of( + && storage.cached_is_constraint_set_subtype_of( db, - constrained_upper.top_materialization(db), - bound_lower.bottom_materialization(db), + env, + constrained_upper.top_materialization(db, env), + bound_lower.bottom_materialization(db, env), ) => { (constrained_lower, Some(Type::TypeVar(bound_typevar))) @@ -5641,10 +5363,11 @@ impl SequentMap { (Some(constrained_lower), constrained_upper, _, Some(bound_upper)) if !constrained_lower.is_never() && !constrained_lower.is_object() - && builder.cached_is_constraint_set_subtype_of( + && storage.cached_is_constraint_set_subtype_of( db, - bound_upper.top_materialization(db), - constrained_lower.bottom_materialization(db), + env, + bound_upper.top_materialization(db, env), + constrained_lower.bottom_materialization(db, env), ) => { (Some(Type::TypeVar(bound_typevar)), constrained_upper) @@ -5677,11 +5400,12 @@ impl SequentMap { // `(Never ≤ [A] ≤ T)` and `(T ≤ [B] ≤ object)`. // This preserves the relationship while keeping all derived constraints canonical. if let Some(Type::TypeVar(lower_bound_typevar)) = new_lower - && !lower_bound_typevar.can_be_bound_for(db, builder, constrained_typevar) + && !lower_bound_typevar.can_be_bound_for(db, storage, constrained_typevar) { post_constraints.push(ConstraintId::new_with_bounds( db, - builder, + env, + storage, lower_bound_typevar, None, Some(Type::TypeVar(constrained_typevar)), @@ -5690,11 +5414,12 @@ impl SequentMap { } if let Some(Type::TypeVar(upper_bound_typevar)) = new_upper - && !upper_bound_typevar.can_be_bound_for(db, builder, constrained_typevar) + && !upper_bound_typevar.can_be_bound_for(db, storage, constrained_typevar) { post_constraints.push(ConstraintId::new_with_bounds( db, - builder, + env, + storage, upper_bound_typevar, Some(Type::TypeVar(constrained_typevar)), None, @@ -5707,7 +5432,8 @@ impl SequentMap { { post_constraints.push(ConstraintId::new_with_bounds( db, - builder, + env, + storage, constrained_typevar, constrained_lower, constrained_upper, @@ -5717,7 +5443,8 @@ impl SequentMap { for post_constraint in post_constraints { self.add_pair_implication( db, - builder, + env, + storage, left_constraint, right_constraint, post_constraint, @@ -5737,7 +5464,8 @@ impl SequentMap { fn add_nested_typevar_sequents<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, left_constraint: ConstraintId, right_constraint: ConstraintId, ) { @@ -5745,23 +5473,23 @@ impl SequentMap { let has_typevar_bound = |bounds: ConstraintBounds<'db>| { bounds .lower - .is_some_and(|bound| any_over_type(db, bound, true, Type::is_type_var)) + .is_some_and(|bound| any_over_type(db, env, bound, true, Type::is_type_var)) || bounds .upper - .is_some_and(|bound| any_over_type(db, bound, true, Type::is_type_var)) + .is_some_and(|bound| any_over_type(db, env, bound, true, Type::is_type_var)) }; - if !has_typevar_bound(builder.constraint_data(left_constraint).bounds) - && !has_typevar_bound(builder.constraint_data(right_constraint).bounds) + if !has_typevar_bound(storage.constraint_data(left_constraint).bounds) + && !has_typevar_bound(storage.constraint_data(right_constraint).bounds) { return; } let mut try_tightening = |bound_constraint: ConstraintId, constrained_constraint: ConstraintId| { - let bound_data = builder.constraint_data(bound_constraint); + let bound_data = storage.constraint_data(bound_constraint); let bound_typevar = bound_data.typevar; let bound_identity = bound_typevar.identity(db); - let constrained_data = builder.constraint_data(constrained_constraint); + let constrained_data = storage.constraint_data(constrained_constraint); let constrained_typevar = constrained_data.typevar; let constrained_identity = constrained_typevar.identity(db); let constrained_lower = constrained_data.bounds.materialized_lower(); @@ -5777,8 +5505,8 @@ impl SequentMap { // instead of calling `variance_of` on them. This avoids a large number of tiny // tracked `variance_of` queries in hot paths. let replacement_mentions_bound_or_constrained = |replacement: Type<'db>| { - replacement.variance_of(db, bound_identity) != TypeVarVariance::Bivariant - || replacement.variance_of(db, constrained_identity) + replacement.variance_of(db, env, bound_identity) != TypeVarVariance::Bivariant + || replacement.variance_of(db, env, constrained_identity) != TypeVarVariance::Bivariant }; @@ -5792,7 +5520,7 @@ impl SequentMap { // need an alternative representation for "typevar not present" // (e.g., `Option`). let upper_replacement = match ( - constrained_upper.variance_of(db, bound_identity), + constrained_upper.variance_of(db, env, bound_identity), bound_data.bounds.lower, bound_data.bounds.upper, ) { @@ -5833,19 +5561,25 @@ impl SequentMap { !replacement_mentions_bound_or_constrained(*replacement) }); if let Some(replacement) = upper_replacement { - let new_upper = - constrained_upper.substitute_one_typevar(db, bound_typevar, replacement); + let new_upper = constrained_upper.substitute_one_typevar( + db, + env, + bound_typevar, + replacement, + ); if new_upper != constrained_upper { let post = ConstraintId::new_with_bounds( db, - builder, + env, + storage, constrained_typevar, constrained_data.bounds.lower, Some(new_upper), ); self.add_pair_implication( db, - builder, + env, + storage, bound_constraint, constrained_constraint, post, @@ -5855,7 +5589,7 @@ impl SequentMap { // Check the lower bound of the constrained constraint for nested occurrences. let lower_replacement = match ( - constrained_lower.variance_of(db, bound_identity), + constrained_lower.variance_of(db, env, bound_identity), bound_data.bounds.lower, bound_data.bounds.upper, ) { @@ -5894,19 +5628,25 @@ impl SequentMap { !replacement_mentions_bound_or_constrained(*replacement) }); if let Some(replacement) = lower_replacement { - let new_lower = - constrained_lower.substitute_one_typevar(db, bound_typevar, replacement); + let new_lower = constrained_lower.substitute_one_typevar( + db, + env, + bound_typevar, + replacement, + ); if new_lower != constrained_lower { let post = ConstraintId::new_with_bounds( db, - builder, + env, + storage, constrained_typevar, Some(new_lower), constrained_data.bounds.upper, ); self.add_pair_implication( db, - builder, + env, + storage, bound_constraint, constrained_constraint, post, @@ -5941,10 +5681,10 @@ impl SequentMap { // bound constraint's typevar. let mut try_weakening = |bound_constraint: ConstraintId, constrained_constraint: ConstraintId| { - let bound_data = builder.constraint_data(bound_constraint); + let bound_data = storage.constraint_data(bound_constraint); let bound_typevar = bound_data.typevar; let bound_lower = bound_data.bounds.materialized_lower(); - let constrained_data = builder.constraint_data(constrained_constraint); + let constrained_data = storage.constraint_data(constrained_constraint); let constrained_typevar = constrained_data.typevar; let constrained_lower = constrained_data.bounds.materialized_lower(); let constrained_upper = constrained_data.bounds.materialized_upper(); @@ -5974,7 +5714,8 @@ impl SequentMap { && !constrained_upper.is_never() && !constrained_upper.is_object() && !constrained_upper.is_dynamic() - && match constrained_upper.variance_of(db, nested_typevar.identity(db)) { + && match constrained_upper.variance_of(db, env, nested_typevar.identity(db)) + { TypeVarVariance::Bivariant => false, TypeVarVariance::Covariant => !is_upper_bound, TypeVarVariance::Contravariant => is_upper_bound, @@ -5986,20 +5727,23 @@ impl SequentMap { if should_weaken_upper { let new_upper = constrained_upper.substitute_one_typevar( db, + env, nested_typevar, replacement, ); if new_upper != constrained_upper { let post = ConstraintId::new_with_bounds( db, - builder, + env, + storage, constrained_typevar, constrained_data.bounds.lower, Some(new_upper), ); self.add_pair_implication( db, - builder, + env, + storage, bound_constraint, constrained_constraint, post, @@ -6012,7 +5756,8 @@ impl SequentMap { && !constrained_lower.is_never() && !constrained_lower.is_object() && !constrained_lower.is_dynamic() - && match constrained_lower.variance_of(db, nested_typevar.identity(db)) { + && match constrained_lower.variance_of(db, env, nested_typevar.identity(db)) + { TypeVarVariance::Bivariant => false, TypeVarVariance::Covariant => is_upper_bound, TypeVarVariance::Contravariant => !is_upper_bound, @@ -6024,20 +5769,23 @@ impl SequentMap { if should_weaken_lower { let new_lower = constrained_lower.substitute_one_typevar( db, + env, nested_typevar, replacement, ); if new_lower != constrained_lower { let post = ConstraintId::new_with_bounds( db, - builder, + env, + storage, constrained_typevar, Some(new_lower), constrained_data.bounds.upper, ); self.add_pair_implication( db, - builder, + env, + storage, bound_constraint, constrained_constraint, post, @@ -6065,19 +5813,20 @@ impl SequentMap { fn add_mutual_sequents_for_same_typevars<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, left_constraint: ConstraintId, right_constraint: ConstraintId, ) { let mut try_one_direction = |left_constraint: ConstraintId, right_constraint: ConstraintId| { - let left_constraint_data = builder.constraint_data(left_constraint); + let left_constraint_data = storage.constraint_data(left_constraint); let left_lower = left_constraint_data.bounds.lower; let left_upper = left_constraint_data.bounds.upper; - let right_constraint_data = builder.constraint_data(right_constraint); + let right_constraint_data = storage.constraint_data(right_constraint); let right_lower = right_constraint_data.bounds.lower; let right_upper = right_constraint_data.bounds.upper; - let new_constraints = + let mut new_constraints = |bound_typevar: BoundTypeVarInstance<'db>, mut right_lower: Option>, mut right_upper: Option>| { @@ -6105,11 +5854,12 @@ impl SequentMap { let mut constrained_upper = right_upper.filter(|upper| !upper.is_object()); if let Some(Type::TypeVar(lower_bound_typevar)) = right_lower - && !lower_bound_typevar.can_be_bound_for(db, builder, bound_typevar) + && !lower_bound_typevar.can_be_bound_for(db, storage, bound_typevar) { post_constraints.push(ConstraintId::new_with_bounds( db, - builder, + env, + storage, lower_bound_typevar, None, Some(Type::TypeVar(bound_typevar)), @@ -6118,11 +5868,12 @@ impl SequentMap { } if let Some(Type::TypeVar(upper_bound_typevar)) = right_upper - && !upper_bound_typevar.can_be_bound_for(db, builder, bound_typevar) + && !upper_bound_typevar.can_be_bound_for(db, storage, bound_typevar) { post_constraints.push(ConstraintId::new_with_bounds( db, - builder, + env, + storage, upper_bound_typevar, Some(Type::TypeVar(bound_typevar)), None, @@ -6135,7 +5886,8 @@ impl SequentMap { { post_constraints.push(ConstraintId::new_with_bounds( db, - builder, + env, + storage, bound_typevar, constrained_lower, constrained_upper, @@ -6162,7 +5914,8 @@ impl SequentMap { for post_constraint in post_constraints { self.add_pair_implication( db, - builder, + env, + storage, left_constraint, right_constraint, post_constraint, @@ -6177,7 +5930,8 @@ impl SequentMap { fn add_concrete_sequents<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, left_constraint: ConstraintId, right_constraint: ConstraintId, ) { @@ -6186,39 +5940,40 @@ impl SequentMap { // identify constraints that are identical besides e.g. ordering of union/intersection // elements. (For instance, when processing `T ≤ τ₁ & τ₂` and `T ≤ τ₂ & τ₁`, these clauses // would add sequents for `(T ≤ τ₁ & τ₂) → (T ≤ τ₂ & τ₁)` and vice versa.) - if builder.cached_constraint_implies(db, left_constraint, right_constraint) { + if storage.cached_constraint_implies(db, env, left_constraint, right_constraint) { tracing::trace!( target: "ty_python_semantic::types::constraints::SequentMap", - left = %left_constraint.display(db, builder), - right = %right_constraint.display(db, builder), + left = %left_constraint.display(db, env, storage), + right = %right_constraint.display(db, env, storage), "left implies right", ); self.add_single_implication(left_constraint, right_constraint); } - if builder.cached_constraint_implies(db, right_constraint, left_constraint) { + if storage.cached_constraint_implies(db, env, right_constraint, left_constraint) { tracing::trace!( target: "ty_python_semantic::types::constraints::SequentMap", - left = %left_constraint.display(db, builder), - right = %right_constraint.display(db, builder), + left = %left_constraint.display(db, env, storage), + right = %right_constraint.display(db, env, storage), "right implies left", ); self.add_single_implication(right_constraint, left_constraint); } - match left_constraint.intersect(db, builder, right_constraint) { + match left_constraint.intersect(db, env, storage, right_constraint) { IntersectionResult::Simplified(intersection_constraint_data) => { let intersection_constraint = - builder.intern_constraint(db, intersection_constraint_data); + storage.intern_constraint(db, env, intersection_constraint_data); tracing::trace!( target: "ty_python_semantic::types::constraints::SequentMap", - left = %left_constraint.display(db, builder), - right = %right_constraint.display(db, builder), - intersection = %intersection_constraint.display(db, builder), + left = %left_constraint.display(db, env, storage), + right = %right_constraint.display(db, env, storage), + intersection = %intersection_constraint.display(db, env, storage), "left and right overlap", ); self.add_pair_implication( db, - builder, + env, + storage, left_constraint, right_constraint, intersection_constraint, @@ -6235,8 +5990,8 @@ impl SequentMap { IntersectionResult::Disjoint => { tracing::trace!( target: "ty_python_semantic::types::constraints::SequentMap", - left = %left_constraint.display(db, builder), - right = %right_constraint.display(db, builder), + left = %left_constraint.display(db, env, storage), + right = %right_constraint.display(db, env, storage), "left and right are disjoint", ); self.add_pair_impossibility(left_constraint, right_constraint); @@ -6248,18 +6003,21 @@ impl SequentMap { fn display<'db, 'a>( &'a self, db: &'db dyn Db, - builder: &'a ConstraintSetBuilder<'db>, + env: &'a ProgramEnvironment<'db>, + storage: &'a ConstraintSetStorage<'db>, prefix: &'a dyn Display, ) -> impl Display + 'a { struct DisplaySequentMap<'a, 'db> { map: &'a SequentMap, prefix: &'a dyn Display, db: &'db dyn Db, - builder: &'a ConstraintSetBuilder<'db>, + env: &'a ProgramEnvironment<'db>, + storage: &'a ConstraintSetStorage<'db>, } impl Display for DisplaySequentMap<'_, '_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let db = self.db; let mut first = true; let mut maybe_write_prefix = |f: &mut std::fmt::Formatter<'_>| { if first { @@ -6279,8 +6037,8 @@ impl SequentMap { write!( f, "{} ∧ {} → false", - ante1.display(self.db, self.builder), - ante2.display(self.db, self.builder), + ante1.display(db, self.env, self.storage), + ante2.display(db, self.env, self.storage), )?; } @@ -6289,9 +6047,9 @@ impl SequentMap { write!( f, "{} ∧ {} → {}", - ante1.display(self.db, self.builder), - ante2.display(self.db, self.builder), - post.display(self.db, self.builder), + ante1.display(db, self.env, self.storage), + ante2.display(db, self.env, self.storage), + post.display(db, self.env, self.storage), )?; } @@ -6300,8 +6058,8 @@ impl SequentMap { write!( f, "{} → {}", - ante.display(self.db, self.builder), - post.display(self.db, self.builder) + ante.display(db, self.env, self.storage), + post.display(db, self.env, self.storage) )?; } } @@ -6318,7 +6076,8 @@ impl SequentMap { map: self, prefix, db, - builder, + env, + storage, } } } @@ -6367,7 +6126,7 @@ trait PathVisitor { fn visit_satisfied<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, path: &PathAssignments, ) -> ControlFlow; @@ -6377,7 +6136,7 @@ trait PathVisitor { fn visit_unsatisfied<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, path: &PathAssignments, ) -> ControlFlow; @@ -6388,7 +6147,7 @@ trait PathVisitor { fn visit_impossible<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, path: &PathAssignments, ) -> ControlFlow; @@ -6399,7 +6158,7 @@ trait PathVisitor { fn enter_interior<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, interior_node: InteriorNode, ) -> ControlFlow; @@ -6409,7 +6168,7 @@ trait PathVisitor { fn visit_edge<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, interior_value: &Self::Interior, subtree: Self::Result, path: &PathAssignments, @@ -6421,7 +6180,7 @@ trait PathVisitor { fn leave_interior<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, interior_value: &Self::Interior, if_true: Self::Result, if_uncertain: Self::Result, @@ -6442,7 +6201,7 @@ trait PathFold { fn satisfied<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, path: &PathAssignments, ) -> ControlFlow; @@ -6450,7 +6209,7 @@ trait PathFold { fn unsatisfied<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, path: &PathAssignments, ) -> ControlFlow; @@ -6458,7 +6217,7 @@ trait PathFold { fn impossible<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, path: &PathAssignments, ) -> ControlFlow; @@ -6467,7 +6226,7 @@ trait PathFold { fn combine<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, if_true: Self::Result, if_uncertain: Self::Result, if_false: Self::Result, @@ -6485,34 +6244,34 @@ where fn visit_satisfied<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, path: &PathAssignments, ) -> ControlFlow { - PathFold::satisfied(self, db, builder, path) + PathFold::satisfied(self, db, storage, path) } fn visit_unsatisfied<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, path: &PathAssignments, ) -> ControlFlow { - PathFold::unsatisfied(self, db, builder, path) + PathFold::unsatisfied(self, db, storage, path) } fn visit_impossible<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, path: &PathAssignments, ) -> ControlFlow { - PathFold::impossible(self, db, builder, path) + PathFold::impossible(self, db, storage, path) } fn enter_interior<'db>( &mut self, _db: &'db dyn Db, - _builder: &ConstraintSetBuilder<'db>, + _storage: &mut ConstraintSetStorage<'db>, _interior_node: InteriorNode, ) -> ControlFlow { ControlFlow::Continue(()) @@ -6521,7 +6280,7 @@ where fn visit_edge<'db>( &mut self, _db: &'db dyn Db, - _builder: &ConstraintSetBuilder<'db>, + _storage: &mut ConstraintSetStorage<'db>, _interior_value: &Self::Interior, subtree: Self::Result, _path: &PathAssignments, @@ -6533,13 +6292,13 @@ where fn leave_interior<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, _interior_value: &Self::Interior, if_true: Self::Result, if_uncertain: Self::Result, if_false: Self::Result, ) -> ControlFlow { - PathFold::combine(self, db, builder, if_true, if_uncertain, if_false) + PathFold::combine(self, db, storage, if_true, if_uncertain, if_false) } } @@ -6555,7 +6314,7 @@ impl PathFold for IsNeverSatisfiedVisitor { fn satisfied<'db>( &mut self, _db: &'db dyn Db, - _builder: &ConstraintSetBuilder<'db>, + _storage: &mut ConstraintSetStorage<'db>, _path: &PathAssignments, ) -> ControlFlow { ControlFlow::Break(()) @@ -6564,7 +6323,7 @@ impl PathFold for IsNeverSatisfiedVisitor { fn unsatisfied<'db>( &mut self, _db: &'db dyn Db, - _builder: &ConstraintSetBuilder<'db>, + _storage: &mut ConstraintSetStorage<'db>, _path: &PathAssignments, ) -> ControlFlow { ControlFlow::Continue(()) @@ -6573,7 +6332,7 @@ impl PathFold for IsNeverSatisfiedVisitor { fn impossible<'db>( &mut self, _db: &'db dyn Db, - _builder: &ConstraintSetBuilder<'db>, + _storage: &mut ConstraintSetStorage<'db>, _path: &PathAssignments, ) -> ControlFlow { ControlFlow::Continue(()) @@ -6582,7 +6341,7 @@ impl PathFold for IsNeverSatisfiedVisitor { fn combine<'db>( &mut self, _db: &'db dyn Db, - _builder: &ConstraintSetBuilder<'db>, + _storage: &mut ConstraintSetStorage<'db>, _if_true: Self::Result, _if_uncertain: Self::Result, _if_false: Self::Result, @@ -6621,8 +6380,9 @@ impl PathFold for IsNeverSatisfiedVisitor { pub(crate) struct PathAssignments { /// All of the rules that we know for inferring derived constraints on the current path. sequents: Vec, - /// Each assignment's source order and the first per-path fuel value with which it was derived. - assignments: FxIndexMap, + /// Each assignment's source constraint and the first per-path fuel value with which it was + /// derived. + assignments: FxIndexMap, /// Additional per-path fuel values that can derive an assignment, keyed by its index in /// `assignments`. These are stored separately so that branch-local additions can be rolled /// back by truncating the set. Only the greatest fuel value participates in further @@ -6634,6 +6394,8 @@ pub(crate) struct PathAssignments { /// ensures a stable order for all of the derived constraints that we create, while still /// letting us create them lazily.) discovered: FxIndexMap, + /// Constraint pairs that we have already checked and added to `sequents`. + elaborated_pairs: FxHashSet<(ConstraintId, ConstraintId)>, /// Derived assignments that have been queued up to be added to the current path. assignment_queue: VecDeque<(ConstraintAssignment, AssignmentFuel)>, @@ -6709,6 +6471,7 @@ impl PathAssignments { assignments: FxIndexMap::default(), additional_fuels: Vec::default(), discovered, + elaborated_pairs: FxHashSet::default(), remaining_overall_fuel: OVERALL_FUEL_BUDGET, assignment_queue: VecDeque::default(), new_assignments: FxIndexMap::default(), @@ -6718,34 +6481,37 @@ impl PathAssignments { fn visit<'db, V>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, node: NodeId, visitor: &mut V, ) -> ControlFlow where V: PathVisitor, { - self.visit_inner(db, builder, node, visitor, false) + self.visit_inner(db, env, storage, node, visitor, false) } /// Visits the paths of the negation of `node`, without constructing that negation eagerly. fn visit_negated<'db, V>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, node: NodeId, visitor: &mut V, ) -> ControlFlow where V: PathVisitor, { - self.visit_inner(db, builder, node, visitor, true) + self.visit_inner(db, env, storage, node, visitor, true) } fn visit_inner<'db, V>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, node: NodeId, visitor: &mut V, negated: bool, @@ -6754,36 +6520,36 @@ impl PathAssignments { V: PathVisitor, { match node.node() { - Node::AlwaysTrue if negated => visitor.visit_unsatisfied(db, builder, self), - Node::AlwaysTrue => visitor.visit_satisfied(db, builder, self), + Node::AlwaysTrue if negated => visitor.visit_unsatisfied(db, storage, self), + Node::AlwaysTrue => visitor.visit_satisfied(db, storage, self), - Node::AlwaysFalse if negated => visitor.visit_satisfied(db, builder, self), - Node::AlwaysFalse => visitor.visit_unsatisfied(db, builder, self), + Node::AlwaysFalse if negated => visitor.visit_satisfied(db, storage, self), + Node::AlwaysFalse => visitor.visit_unsatisfied(db, storage, self), Node::Interior(interior) => { - let interior_value = visitor.enter_interior(db, builder, interior)?; - let interior = builder.interior_node_data(node); + let interior_value = visitor.enter_interior(db, storage, interior)?; + let interior = storage.interior_node_data(node); let true_subtree = if negated { - interior.if_true.or(builder, interior.if_uncertain) + interior.if_true.or(storage, interior.if_uncertain) } else { interior.if_true }; let if_true = self.walk_edge( db, - builder, + env, + storage, interior.constraint.when_true(), - interior.source_order, - |path, new_range, found_conflict| { + |storage, path, new_range, found_conflict| { let subtree = if found_conflict { - visitor.visit_impossible(db, builder, path) + visitor.visit_impossible(db, storage, path) } else { - path.visit_inner(db, builder, true_subtree, visitor, negated) + path.visit_inner(db, env, storage, true_subtree, visitor, negated) }; match subtree { ControlFlow::Continue(subtree) => visitor.visit_edge( db, - builder, + storage, &interior_value, subtree, path, @@ -6795,24 +6561,31 @@ impl PathAssignments { )?; let if_uncertain = if negated { - let subtree = visitor.visit_impossible(db, builder, self)?; - visitor.visit_edge(db, builder, &interior_value, subtree, self, 0..0)? + let subtree = visitor.visit_impossible(db, storage, self)?; + visitor.visit_edge(db, storage, &interior_value, subtree, self, 0..0)? } else { self.walk_edge( db, - builder, + env, + storage, interior.constraint.when_unconstrained(), - interior.source_order, - |path, new_range, found_conflict| { + |storage, path, new_range, found_conflict| { let subtree = if found_conflict { - visitor.visit_impossible(db, builder, path) + visitor.visit_impossible(db, storage, path) } else { - path.visit_inner(db, builder, interior.if_uncertain, visitor, false) + path.visit_inner( + db, + env, + storage, + interior.if_uncertain, + visitor, + false, + ) }; match subtree { ControlFlow::Continue(subtree) => visitor.visit_edge( db, - builder, + storage, &interior_value, subtree, path, @@ -6825,25 +6598,25 @@ impl PathAssignments { }; let false_subtree = if negated { - interior.if_false.or(builder, interior.if_uncertain) + interior.if_false.or(storage, interior.if_uncertain) } else { interior.if_false }; let if_false = self.walk_edge( db, - builder, + env, + storage, interior.constraint.when_false(), - interior.source_order, - |path, new_range, found_conflict| { + |storage, path, new_range, found_conflict| { let subtree = if found_conflict { - visitor.visit_impossible(db, builder, path) + visitor.visit_impossible(db, storage, path) } else { - path.visit_inner(db, builder, false_subtree, visitor, negated) + path.visit_inner(db, env, storage, false_subtree, visitor, negated) }; match subtree { ControlFlow::Continue(subtree) => visitor.visit_edge( db, - builder, + storage, &interior_value, subtree, path, @@ -6856,7 +6629,7 @@ impl PathAssignments { visitor.leave_interior( db, - builder, + storage, &interior_value, if_true, if_uncertain, @@ -6891,10 +6664,10 @@ impl PathAssignments { fn walk_edge<'db, R>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, assignment: ConstraintAssignment, - source_order: usize, - f: impl FnOnce(&mut Self, Range, bool) -> R, + f: impl FnOnce(&mut ConstraintSetStorage<'db>, &mut Self, Range, bool) -> R, ) -> R { // Record a snapshot of the assignments that we already knew held — both so that we can // pass along the range of which assignments are new, and so that we can reset back to this @@ -6909,17 +6682,18 @@ impl PathAssignments { before = %format_args!( "[{}]", self.assignments[..start].iter().map(|(assignment, _)| { - assignment.display(db, builder) + assignment.display(db, env, storage) }).format(", "), ), - edge = %assignment.display(db, builder), + edge = %assignment.display(db, env, storage), "walk edge", ); debug_assert!(self.assignment_queue.is_empty()); self.assignment_queue .push_back((assignment, AssignmentFuel::origin())); + let source_constraint = assignment.constraint(); let found_conflict = self - .drain_assignment_queue(db, builder, source_order) + .drain_assignment_queue(db, env, storage, source_constraint) .is_err(); if !found_conflict { tracing::trace!( @@ -6927,7 +6701,7 @@ impl PathAssignments { new = %format_args!( "[{}]", self.assignments[start..].iter().map(|(assignment, _)| { - assignment.display(db, builder) + assignment.display(db, env, storage) }).format(", "), ), "new assignments", @@ -6939,7 +6713,7 @@ impl PathAssignments { // `add_assignment` call above — that is, the new assignment for this edge along with // the derived information we inferred from it. let end = self.assignments.len(); - let result = f(self, start..end, found_conflict); + let result = f(storage, self, start..end, found_conflict); // Reset back to where we were before following this edge, so that the caller can reuse a // single instance for the entire BDD traversal. @@ -6950,13 +6724,15 @@ impl PathAssignments { result } - pub(crate) fn positive_constraints(&self) -> impl Iterator + '_ { - self.assignments - .iter() - .filter_map(|(assignment, (source_order, _))| match assignment { - ConstraintAssignment::Positive(constraint) => Some((*constraint, *source_order)), + fn positive_constraints(&self) -> impl Iterator + '_ { + self.assignments.iter().filter_map( + |(assignment, (source_constraint, _))| match assignment { + ConstraintAssignment::Positive(constraint) => { + Some((*constraint, *source_constraint)) + } ConstraintAssignment::Negative(_) | ConstraintAssignment::Unconstrained(_) => None, - }) + }, + ) } fn assignment_holds(&self, assignment: ConstraintAssignment) -> bool { @@ -6989,22 +6765,40 @@ impl PathAssignments { fn discover_constraint<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, constraint: ConstraintId, ) { // If we've already processed this constraint, we can skip it. - let existing = self.discovered.insert(constraint, true); + let (constraint_index, existing) = self.discovered.insert_full(constraint, true); let already_processed = existing.is_some_and(|existing| existing); if already_processed { return; } - let single_map = SequentMap::for_constraint(db, builder, constraint); + let single_map = SequentMap::for_constraint(db, env, storage, constraint); self.sequents.extend_from_slice(&single_map.sequents); - drop(single_map); - for existing in self.discovered.keys().dropping_back(1) { - let pair_map = SequentMap::for_constraint_pair(db, builder, *existing, constraint); + for (existing_index, (existing, _)) in self.discovered.iter().enumerate() { + if *existing == constraint { + continue; + } + + if SequentMap::pair_cannot_produce_sequents(db, env, storage, *existing, constraint) { + continue; + } + + let (a, b) = if existing_index < constraint_index { + (*existing, constraint) + } else { + (constraint, *existing) + }; + if !self.elaborated_pairs.insert((a, b)) { + // We've already elaborated this pair of constraints. + continue; + } + + let pair_map = SequentMap::for_constraint_pair(db, env, storage, a, b); self.sequents.extend_from_slice(&pair_map.sequents); } } @@ -7012,11 +6806,12 @@ impl PathAssignments { fn drain_assignment_queue<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, - source_order: usize, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, + source_constraint: ConstraintId, ) -> Result<(), PathAssignmentConflict> { while let Some((assignment, fuel)) = self.assignment_queue.pop_front() { - self.add_assignment(db, builder, assignment, source_order, fuel)?; + self.add_assignment(db, env, storage, assignment, source_constraint, fuel)?; } Ok(()) } @@ -7027,9 +6822,10 @@ impl PathAssignments { fn add_assignment<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, assignment: ConstraintAssignment, - source_order: usize, + source_constraint: ConstraintId, fuel: AssignmentFuel, ) -> Result<(), PathAssignmentConflict> { if matches!(assignment, ConstraintAssignment::Unconstrained(_)) { @@ -7045,7 +6841,7 @@ impl PathAssignments { // assignment, but as an optimization we can return early without actually querying the // sequent map. self.assignments - .insert(assignment, (source_order, fuel.remaining)); + .insert(assignment, (source_constraint, fuel.remaining)); return Ok(()); } @@ -7053,11 +6849,11 @@ impl PathAssignments { if self.assignments.contains_key(&assignment.negated()) { tracing::trace!( target: "ty_python_semantic::types::constraints::PathAssignment", - assignment = %assignment.display(db, builder), + assignment = %assignment.display(db, env, storage), facts = %format_args!( "[{}]", self.assignments.iter().map(|(assignment, _)| { - assignment.display(db, builder) + assignment.display(db, env, storage) }).format(", "), ), "found contradiction", @@ -7074,19 +6870,19 @@ impl PathAssignments { None => return Ok(()), }; } - entry.insert((source_order, fuel.remaining)); + entry.insert((source_constraint, fuel.remaining)); } Entry::Occupied(mut entry) => { let index = entry.index(); - let (existing_source_order, existing_fuel) = entry.get_mut(); + let (existing_source_constraint, existing_fuel) = entry.get_mut(); // If a constraint appears both as an "origin" constraint (it actually appears in // the BDD structure) and as a "derived" constraint (we infer it from other - // constraints), we should prefer the origin source_order, regardless of which + // constraints), we should prefer the origin source constraint, regardless of which // order we encounter the various constraints in the BDD. if !fuel.is_derived() { - *existing_source_order = source_order; + *existing_source_constraint = source_constraint; } // We've already seen this assignment, and in theory have already queried the @@ -7117,24 +6913,18 @@ impl PathAssignments { } } - // Then use our sequents to add additional facts that we know to be true. We currently - // reuse the `source_order` of the "real" constraint passed into `walk_edge` when we add - // these derived facts. - // - // TODO: This might not be stable enough, if we add more than one derived fact for this - // constraint. If we still see inconsistent test output, we might need a more complex - // way of tracking source order for derived facts. + // Then use our sequents to add additional facts that we know to be true. // // TODO: This is very naive at the moment, partly for expediency, and partly because we // don't anticipate the sequent maps to be very large. We might consider avoiding the // brute-force search. self.new_assignments.clear(); - self.discover_constraint(db, builder, assignment.constraint()); + self.discover_constraint(db, env, storage, assignment.constraint()); for i in 0..self.sequents.len() { let sequent = self.sequents[i]; - self.check_sequent(db, builder, sequent)?; + self.check_sequent(db, env, storage, sequent)?; } // If we were able to derive any new assignments from this one, add them to the processing @@ -7156,20 +6946,23 @@ impl PathAssignments { fn check_sequent<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, sequent: Sequent, ) -> Result<(), PathAssignmentConflict> { match sequent { - Sequent::SingleTautology { ante } => self.check_single_tautology(db, builder, ante), + Sequent::SingleTautology { ante } => { + self.check_single_tautology(db, env, storage, ante) + } Sequent::PairImpossibility { ante1, ante2 } => { - self.check_pair_impossibility(db, builder, ante1, ante2) + self.check_pair_impossibility(db, env, storage, ante1, ante2) } Sequent::PairImplication { ante1, ante2, post } => { - self.check_pair_implication(db, builder, ante1, ante2, post); + self.check_pair_implication(db, env, storage, ante1, ante2, post); Ok(()) } Sequent::SingleImplication { ante, post } => { - self.check_single_implication(db, builder, ante, post); + self.check_single_implication(db, env, storage, ante, post); Ok(()) } } @@ -7178,7 +6971,8 @@ impl PathAssignments { fn check_single_tautology<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, ante: ConstraintId, ) -> Result<(), PathAssignmentConflict> { if self.assignment_holds(ante.when_false()) { @@ -7186,11 +6980,11 @@ impl PathAssignments { // it's false. tracing::trace!( target: "ty_python_semantic::types::constraints::PathAssignment", - ante = %ante.display(db, builder), + ante = %ante.display(db, env, storage), facts = %format_args!( "[{}]", self.assignments.iter().map(|(assignment, _)| { - assignment.display(db, builder) + assignment.display(db, env, storage) }).format(", "), ), "found contradiction", @@ -7204,7 +6998,8 @@ impl PathAssignments { fn check_pair_impossibility<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, ante1: ConstraintId, ante2: ConstraintId, ) -> Result<(), PathAssignmentConflict> { @@ -7213,12 +7008,12 @@ impl PathAssignments { // current path asserts that both are true. tracing::trace!( target: "ty_python_semantic::types::constraints::PathAssignment", - ante1 = %ante1.display(db, builder), - ante2 = %ante2.display(db, builder), + ante1 = %ante1.display(db, env, storage), + ante2 = %ante2.display(db, env, storage), facts = %format_args!( "[{}]", self.assignments.iter().map(|(assignment, _)| { - assignment.display(db, builder) + assignment.display(db, env, storage) }).format(", "), ), "found contradiction", @@ -7232,7 +7027,8 @@ impl PathAssignments { fn check_pair_implication<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, ante1: ConstraintId, ante2: ConstraintId, post: ConstraintId, @@ -7244,10 +7040,10 @@ impl PathAssignments { return; }; let available_fuel = ante1_fuel.min(ante2_fuel); - let (ante1_constructor_depth, _) = builder.cached_constraint_bound_depth(db, ante1); - let (ante2_constructor_depth, _) = builder.cached_constraint_bound_depth(db, ante2); + let (ante1_constructor_depth, _) = storage.cached_constraint_bound_depth(db, env, ante1); + let (ante2_constructor_depth, _) = storage.cached_constraint_bound_depth(db, env, ante2); let antecedent_constructor_depth = ante1_constructor_depth.max(ante2_constructor_depth); - let fuel_cost = builder.sequent_fuel_cost(db, post, antecedent_constructor_depth); + let fuel_cost = storage.sequent_fuel_cost(db, env, post, antecedent_constructor_depth); if let Some(post_fuel) = available_fuel.checked_sub(fuel_cost) { self.enqueue_assignment( post.when_true(), @@ -7259,20 +7055,22 @@ impl PathAssignments { fn check_single_implication<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, ante: ConstraintId, post: ConstraintId, ) { let Some(available_fuel) = self.max_remaining_fuel_for(ante.when_true()) else { return; }; - let ante_data = builder.constraint_data(ante); - let (antecedent_constructor_depth, _) = builder.cached_constraint_bound_depth(db, ante); - let post_data = builder.constraint_data(post); + let ante_data = storage.constraint_data(ante); + let (antecedent_constructor_depth, _) = + storage.cached_constraint_bound_depth(db, env, ante); + let post_data = storage.constraint_data(post); let fuel_cost = if post_data.is_bound_projection_of(db, ante_data) { 1 } else { - builder.sequent_fuel_cost(db, post, antecedent_constructor_depth) + storage.sequent_fuel_cost(db, env, post, antecedent_constructor_depth) }; if let Some(post_fuel) = available_fuel.checked_sub(fuel_cost) { self.enqueue_assignment( @@ -7303,66 +7101,12 @@ impl SatisfiedClause { .expect("clause vector should not be empty"); } - /// Invokes a closure with the last constraint in this clause negated. Returns the clause back - /// to its original state after invoking the closure. - fn with_negated_last_constraint(&mut self, f: impl for<'a> FnOnce(&'a Self)) { - if self.constraints.is_empty() { - return; - } - let last_index = self.constraints.len() - 1; - self.constraints[last_index].negate(); - f(self); - self.constraints[last_index].negate(); - } - - /// Removes another clause from this clause, if it appears as a prefix of this clause. Returns - /// whether the prefix was removed. - fn remove_prefix(&mut self, prefix: &SatisfiedClause) -> bool { - if self.constraints.starts_with(&prefix.constraints) { - self.constraints.drain(0..prefix.constraints.len()); - return true; - } - false - } - - /// Simplifies this clause by removing constraints that are implied by other constraints in the - /// clause. (Clauses are the intersection of constraints, so if two clauses are redundant, we - /// want to remove the larger one and keep the smaller one.) - /// - /// Returns a boolean that indicates whether any simplifications were made. - fn simplify<'db>(&mut self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>) -> bool { - let mut changes_made = false; - let mut i = 0; - // Loop through each constraint, comparing it with any constraints that appear later in the - // list. - 'outer: while i < self.constraints.len() { - let mut j = i + 1; - while j < self.constraints.len() { - if self.constraints[j].implies(db, builder, self.constraints[i]) { - // If constraint `i` is removed, then we don't need to compare it with any - // later constraints in the list. Note that we continue the outer loop, instead - // of breaking from the inner loop, so that we don't bump index `i` below. - // (We'll have swapped another element into place at that index, and want to - // make sure that we process it.) - self.constraints.swap_remove(i); - changes_made = true; - continue 'outer; - } else if self.constraints[i].implies(db, builder, self.constraints[j]) { - // If constraint `j` is removed, then we can continue the inner loop. We will - // swap a new element into place at index `j`, and will continue comparing the - // constraint at index `i` with later constraints. - self.constraints.swap_remove(j); - changes_made = true; - } else { - j += 1; - } - } - i += 1; - } - changes_made - } - - fn display<'db>(&self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>) -> String { + fn display<'db>( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + storage: &ConstraintSetStorage<'db>, + ) -> String { if self.constraints.is_empty() { return String::from("always"); } @@ -7373,7 +7117,7 @@ impl SatisfiedClause { let mut constraints: Vec<_> = self .constraints .iter() - .map(|constraint| constraint.display(db, builder).to_string()) + .map(|constraint| constraint.display(db, env, storage).to_string()) .collect(); constraints.sort(); @@ -7406,86 +7150,12 @@ impl SatisfiedClauses { self.clauses.push(clause); } - /// Simplifies the DNF representation, removing redundancies that do not change the underlying - /// function. (This is used when displaying a BDD, to make sure that the representation that we - /// show is as simple as possible while still producing the same results.) - fn simplify<'db>(&mut self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>) { - // First simplify each clause individually, by removing constraints that are implied by - // other constraints in the clause. - for clause in &mut self.clauses { - clause.simplify(db, builder); - } - - while self.simplify_one_round() { - // Keep going - } - - // We can remove any clauses that have been simplified to the point where they are empty. - // (Clauses are intersections, so an empty clause is `false`, which does not contribute - // anything to the outer union.) - self.clauses.retain(|clause| !clause.constraints.is_empty()); - } - - fn simplify_one_round(&mut self) -> bool { - let mut changes_made = false; - - // First remove any duplicate clauses. (The clause list will start out with no duplicates - // in the first round of simplification, because of the guarantees provided by the BDD - // structure. But earlier rounds of simplification might have made some clauses redundant.) - // Note that we have to loop through the vector element indexes manually, since we might - // remove elements in each iteration. - let mut i = 0; - while i < self.clauses.len() { - let mut j = i + 1; - while j < self.clauses.len() { - if self.clauses[i] == self.clauses[j] { - self.clauses.swap_remove(j); - changes_made = true; - } else { - j += 1; - } - } - i += 1; - } - if changes_made { - return true; - } - - // Then look for "prefix simplifications". That is, looks for patterns - // - // (A ∧ B) ∨ (A ∧ ¬B ∧ ...) - // - // and replaces them with - // - // (A ∧ B) ∨ (...) - for i in 0..self.clauses.len() { - let (clause, rest) = self.clauses[..=i] - .split_last_mut() - .expect("index should be in range"); - clause.with_negated_last_constraint(|clause| { - for existing in rest { - changes_made |= existing.remove_prefix(clause); - } - }); - - let (clause, rest) = self.clauses[i..] - .split_first_mut() - .expect("index should be in range"); - clause.with_negated_last_constraint(|clause| { - for existing in rest { - changes_made |= existing.remove_prefix(clause); - } - }); - - if changes_made { - return true; - } - } - - false - } - - fn display<'db>(&self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>) -> String { + fn display<'db>( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + storage: &ConstraintSetStorage<'db>, + ) -> String { // This is a bit heavy-handed, but we need to output the clauses in a consistent order // even though Salsa IDs are assigned non-deterministically. This Display output is only // used in test cases, so we don't need to over-optimize it. @@ -7496,7 +7166,7 @@ impl SatisfiedClauses { let mut clauses: Vec<_> = self .clauses .iter() - .map(|clause| clause.display(db, builder)) + .map(|clause| clause.display(db, env, storage)) .collect(); clauses.sort(); clauses.join(" ∨ ") @@ -7507,10 +7177,15 @@ impl<'db> BoundTypeVarInstance<'db> { /// Returns the valid specializations of a typevar. This is used when checking a constraint set /// when this typevar is in inferable position, where we only need _some_ specialization to /// satisfy the constraint set. - fn valid_specializations(self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>) -> NodeId { + fn valid_specializations( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, + ) -> (NodeId, Option) { if self.paramspec_attr(db).is_some() { // P.args and P.kwargs are variadic, and do not have an upper bound or constraints. - return ALWAYS_TRUE; + return (ALWAYS_TRUE, None); } // For gradual upper bounds and constraints, we are free to choose any materialization that @@ -7523,29 +7198,37 @@ impl<'db> BoundTypeVarInstance<'db> { // _equality_ comparisons, not _subtyping_ comparisons — since we are only going to check // that _some_ valid specialization satisfies the constraint set, it's correct for us to // return the range of valid materializations that we can choose from. - match self.typevar(db).bound_or_constraints(db) { - None => ALWAYS_TRUE, + match self.typevar(db).bound_or_constraints(db, env) { + None => (ALWAYS_TRUE, None), Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - let bound = bound.top_materialization(db); + let bound = bound.top_materialization(db, env); // basedpython: a bound range `T: Lower..Upper` also pins the bottom of the // interval. take its bottom materialization, which is the most permissive choice let lower = self .typevar(db) .lower_bound(db) - .map(|lower| lower.bottom_materialization(db)); - Constraint::new_node_with_bounds(db, builder, self, lower, Some(bound)) + .map(|lower| lower.bottom_materialization(db, env)); + Constraint::new_node_with_bounds(db, env, storage, self, lower, Some(bound)) } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { let mut specializations = ALWAYS_FALSE; + let mut source_order = None; for constraint in constraints.elements(db) { - let constraint_lower = constraint.bottom_materialization(db); - let constraint_upper = constraint.top_materialization(db); - specializations = specializations.or_with_offset( - builder, - Constraint::new_node(db, builder, self, constraint_lower, constraint_upper), + let constraint_lower = constraint.bottom_materialization(db, env); + let constraint_upper = constraint.top_materialization(db, env); + let (constraint, constraint_source_order) = Constraint::new_node_with_bounds( + db, + env, + storage, + self, + Some(constraint_lower), + Some(constraint_upper), ); + specializations = specializations.or(storage, constraint); + source_order = + storage.ordered_source_order(source_order, constraint_source_order); } - specializations + (specializations, source_order) } } } @@ -7563,47 +7246,63 @@ impl<'db> BoundTypeVarInstance<'db> { /// specifies the required specializations, and the iterator will be empty. For a constrained /// typevar, the primary result will include the fully static constraints, and the iterator /// will include an entry for each non-fully-static constraint. + #[expect(clippy::type_complexity)] fn required_specializations( self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, - ) -> (NodeId, Vec) { + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, + ) -> ( + (NodeId, Option), + Vec<(NodeId, Option)>, + ) { // For upper bounds and constraints, we are free to choose any materialization that makes // the check succeed. In non-inferable positions, it is most helpful to choose a // materialization that is as restrictive as possible, since that minimizes the number of // valid specializations that must satisfy the check. We therefore take the bottom // materialization of the bound or constraints. - match self.typevar(db).bound_or_constraints(db) { - None => (ALWAYS_TRUE, Vec::new()), + match self.typevar(db).bound_or_constraints(db, env) { + None => ((ALWAYS_TRUE, None), Vec::new()), Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - let bound = bound.bottom_materialization(db); + let bound = bound.bottom_materialization(db, env); // basedpython: mirror `possible_specializations`, but take the most restrictive // choice for the bottom of the interval let lower = self .typevar(db) .lower_bound(db) - .map(|lower| lower.top_materialization(db)); + .map(|lower| lower.top_materialization(db, env)); ( - Constraint::new_node_with_bounds(db, builder, self, lower, Some(bound)), + Constraint::new_node_with_bounds(db, env, storage, self, lower, Some(bound)), Vec::new(), ) } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { let mut non_gradual_constraints = ALWAYS_FALSE; + let mut non_gradual_source_order = None; let mut gradual_constraints = Vec::new(); for constraint in constraints.elements(db) { - let constraint_lower = constraint.bottom_materialization(db); - let constraint_upper = constraint.top_materialization(db); - let constraint = - Constraint::new_node(db, builder, self, constraint_lower, constraint_upper); + let constraint_lower = constraint.bottom_materialization(db, env); + let constraint_upper = constraint.top_materialization(db, env); + let constraint = Constraint::new_node_with_bounds( + db, + env, + storage, + self, + Some(constraint_lower), + Some(constraint_upper), + ); if constraint_lower == constraint_upper { - non_gradual_constraints = - non_gradual_constraints.or_with_offset(builder, constraint); + non_gradual_constraints = non_gradual_constraints.or(storage, constraint.0); + non_gradual_source_order = + storage.ordered_source_order(non_gradual_source_order, constraint.1); } else { gradual_constraints.push(constraint); } } - (non_gradual_constraints, gradual_constraints) + ( + (non_gradual_constraints, non_gradual_source_order), + gradual_constraints, + ) } } } @@ -7616,122 +7315,334 @@ mod tests { use indoc::indoc; use pretty_assertions::assert_eq; - use crate::db::tests::setup_db; + use crate::db::tests::{TestDb, setup_db}; use crate::types::generics::ApplySpecialization; - use crate::types::{BoundTypeVarInstance, KnownClass, TypeVarVariance}; + use crate::types::{BoundTypeVarInstance, KnownClass, SubclassOfType, TypeVarVariance}; use ruff_python_ast::name::Name; - fn create_typevar<'db>(db: &'db dyn Db, name: &'static str) -> BoundTypeVarInstance<'db> { - BoundTypeVarInstance::synthetic(db, Name::new_static(name), TypeVarVariance::Invariant) + fn create_typevar<'db>(db: &'db TestDb, name: &'static str) -> BoundTypeVarInstance<'db> { + BoundTypeVarInstance::synthetic( + db, + &db.program_environment(), + Name::new_static(name), + TypeVarVariance::Invariant, + ) } fn create_constraint<'db, 'c>( - db: &'db dyn Db, + db: &'db TestDb, builder: &'c ConstraintSetBuilder<'db>, bound_typevar: BoundTypeVarInstance<'db>, bound: KnownClass, ) -> ConstraintSet<'db, 'c> { - let ty = bound.to_instance(db); - ConstraintSet::constrain_typevar(db, builder, bound_typevar, ty, ty) + let env = db.program_environment(); + let ty = bound.to_instance(db, &env); + ConstraintSet::constrain_typevar(db, &env, builder, bound_typevar, ty, ty) } - fn known_instance(db: &dyn Db, class: KnownClass) -> Type<'_> { - class.to_instance(db) + fn known_instance(db: &TestDb, class: KnownClass) -> Type<'_> { + class.to_instance(db, &db.program_environment()) } #[test] fn type_mapping_updates_constraint_bounds() { // (list[U] ≤ T ≤ list[U])[U ↦ int] = (list[int] ≤ T ≤ list[int]) let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); let builder = ConstraintSetBuilder::new(); - let list_of_u = KnownClass::List.to_specialized_instance(&db, &[Type::TypeVar(u)]); - let set = ConstraintSet::constrain_typevar(&db, &builder, t, list_of_u, list_of_u); + let list_of_u = KnownClass::List.to_specialized_instance(db, &env, &[Type::TypeVar(u)]); + let set = ConstraintSet::constrain_typevar(db, &env, &builder, t, list_of_u, list_of_u); - let int = KnownClass::Int.to_instance(&db); + let int = KnownClass::Int.to_instance(db, &env); let mapped = set.apply_type_mapping_impl( - &db, + db, &TypeMapping::ApplySpecialization(ApplySpecialization::Single(u, int)), TypeContext::default(), - &ApplyTypeMappingVisitor::default(), + &ApplyTypeMappingVisitor::new(&env), ); - let list_of_int = KnownClass::List.to_specialized_instance(&db, &[int]); - let expected = ConstraintSet::constrain_typevar(&db, &builder, t, list_of_int, list_of_int); + let list_of_int = KnownClass::List.to_specialized_instance(db, &env, &[int]); + let expected = + ConstraintSet::constrain_typevar(db, &env, &builder, t, list_of_int, list_of_int); - assert!(mapped.iff(&db, &builder, expected).is_always_satisfied(&db)); + assert!( + mapped + .iff(db, &builder, expected) + .is_always_satisfied(db, &env) + ); } #[test] fn type_mapping_evaluates_mapped_subjects() { // ((T = int) ∧ ¬(T = str))[T ↦ int] = true let db = setup_db(); - let t = create_typevar(&db, "T"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); let builder = ConstraintSetBuilder::new(); - let set = create_constraint(&db, &builder, t, KnownClass::Int).and(&db, &builder, || { - create_constraint(&db, &builder, t, KnownClass::Str).negate(&db, &builder) + let set = create_constraint(db, &builder, t, KnownClass::Int).and(db, &builder, || { + create_constraint(db, &builder, t, KnownClass::Str).negate(db, &builder) }); let mapped = set.apply_type_mapping_impl( - &db, + db, &TypeMapping::ApplySpecialization(ApplySpecialization::Single( t, - KnownClass::Int.to_instance(&db), + KnownClass::Int.to_instance(db, &env), )), TypeContext::default(), - &ApplyTypeMappingVisitor::default(), + &ApplyTypeMappingVisitor::new(&env), ); - assert!(mapped.is_always_satisfied(&db)); + assert!(mapped.is_always_satisfied(db, &env)); } #[test] - fn upper_bound_prunes_duplicates_and_redundant_supertypes() { + fn type_mapping_handles_absorbed_constraints_in_source_order() { let db = setup_db(); - let int = known_instance(&db, KnownClass::Int); - let bool = known_instance(&db, KnownClass::Bool); - let str = known_instance(&db, KnownClass::Str); - - let mut upper = UpperBound::from_clauses(&db, [int, str, int]); - assert_eq!(upper.clauses, FxOrderSet::from_iter([int, str])); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let builder = ConstraintSetBuilder::new(); + let str = create_constraint(db, &builder, t, KnownClass::Str); + let int = create_constraint(db, &builder, t, KnownClass::Int); + let set = str.or(db, &builder, || int).and(db, &builder, || str); - // `bool` is narrower than `int`, so it replaces the redundant `int` clause while - // preserving the relative order of the remaining clauses. - upper.add_clause(&db, bool); - assert_eq!(upper.clauses, FxOrderSet::from_iter([str, bool])); + let mapped = set.apply_type_mapping_impl( + db, + &TypeMapping::ApplySpecialization(ApplySpecialization::Single( + t, + KnownClass::Str.to_instance(db, &env), + )), + TypeContext::default(), + &ApplyTypeMappingVisitor::new(&env), + ); - upper.add_clause(&db, int); - assert_eq!(upper.clauses, FxOrderSet::from_iter([str, bool])); + assert!(mapped.is_always_satisfied(db, &env)); } #[test] fn upper_bound_collapses_never() { let db = setup_db(); - let int = known_instance(&db, KnownClass::Int); + let db = &db; + let env = db.program_environment(); + let int = known_instance(db, KnownClass::Int); let mut upper = UpperBound::from_clause(int); - upper.add_clause(&db, Type::Never); + upper.add_clause(Type::Never); assert_eq!(upper.clauses, FxOrderSet::from_iter([Type::Never])); - assert_eq!(upper.materialize_exact(&db), Type::Never); + assert_eq!(upper.materialize_exact(db, &env), Type::Never); - upper.add_clause(&db, int); + upper.add_clause(int); assert_eq!(upper.clauses, FxOrderSet::from_iter([Type::Never])); } + #[test] + fn upper_bound_recovers_redundant_single_bounds() { + let db = setup_db(); + let db = &db; + let env = db.program_environment(); + let int = known_instance(db, KnownClass::Int); + let bool = known_instance(db, KnownClass::Bool); + let str = known_instance(db, KnownClass::Str); + let int_or_str = UnionType::from_two_elements(db, &env, int, str); + let u = create_typevar(db, "U").map_bound_or_constraints(db, |_| { + Some(TypeVarBoundOrConstraints::UpperBound(int_or_str)) + }); + let u = Type::TypeVar(u); + + for (clauses, expected) in [ + ([Type::object(), int], int), + ([int, Type::object()], int), + ([int, bool], bool), + ([bool, int], bool), + ([int_or_str, u], u), + ([u, int_or_str], u), + ] { + let mut upper = UpperBound::none(); + for clause in clauses { + upper.add_clause(clause); + } + + assert_eq!(upper.clauses.len(), 2); + assert_eq!(upper.as_single_bound(db, &env), Some(expected)); + } + } + + #[test] + fn upper_bound_distinguishes_missing_bound_from_explicit_object() { + let db = setup_db(); + let db = &db; + let env = db.program_environment(); + + assert_eq!(UpperBound::none().as_single_bound(db, &env), None); + assert_eq!( + UpperBound::from_clause(Type::object()).as_single_bound(db, &env), + Some(Type::object()) + ); + } + + #[test] + fn upper_bound_does_not_materialize_overlapping_union_clauses() { + let db = setup_db(); + let db = &db; + let env = db.program_environment(); + let int = known_instance(db, KnownClass::Int); + let str = known_instance(db, KnownClass::Str); + let bytes = known_instance(db, KnownClass::Bytes); + let int_or_str = UnionType::from_two_elements(db, &env, int, str); + let int_or_bytes = UnionType::from_two_elements(db, &env, int, bytes); + + for clauses in [[int_or_str, int_or_bytes], [int_or_bytes, int_or_str]] { + let mut upper = UpperBound::none(); + for clause in clauses { + upper.add_clause(clause); + } + + assert_eq!(upper.materialize_exact(db, &env), int); + assert_eq!(upper.as_single_bound(db, &env), None); + } + } + + #[test] + fn upper_bound_does_not_treat_nontrivial_intersection_as_single_bound() { + let db = setup_db(); + let db = &db; + let env = db.program_environment(); + let int = known_instance(db, KnownClass::Int); + let u = Type::TypeVar(create_typevar(db, "U")); + let mut upper = UpperBound::from_clause(u); + upper.add_clause(int); + + assert!( + upper + .materialize_exact(db, &env) + .is_nontrivial_intersection(db) + ); + assert_eq!(upper.as_single_bound(db, &env), None); + } + + #[test] + fn trivial_disjointness_does_not_claim_bounded_typevar_class_is_disjoint() { + let db = setup_db(); + let db = &db; + let env = db.program_environment(); + let builder = ConstraintSetBuilder::new(); + let bool = known_instance(db, KnownClass::Bool); + let u = create_typevar(db, "U") + .map_bound_or_constraints(db, |_| Some(TypeVarBoundOrConstraints::UpperBound(bool))); + let type_of_u = SubclassOfType::from(db, &env, u); + let bool_class = KnownClass::Bool.to_class_literal(db, &env); + + for (left, right) in [(type_of_u, bool_class), (bool_class, type_of_u)] { + let trivial = + left.when_trivially_disjoint_from(db, &env, right, &builder, TypeVarSet::None); + let full = left.when_disjoint_from(db, &env, right, &builder, TypeVarSet::None); + + assert!(trivial.is_trivially_never_satisfied()); + assert!(!full.is_always_satisfied(db, &env)); + } + } + + #[test] + fn trivial_disjointness_implies_full_disjointness() { + let db = setup_db(); + let db = &db; + let env = db.program_environment(); + let builder = ConstraintSetBuilder::new(); + let bool = known_instance(db, KnownClass::Bool); + let u = create_typevar(db, "U") + .map_bound_or_constraints(db, |_| Some(TypeVarBoundOrConstraints::UpperBound(bool))); + let types = [ + Type::Never, + Type::object(), + bool, + known_instance(db, KnownClass::Int), + known_instance(db, KnownClass::Str), + Type::int_literal(0), + Type::int_literal(1), + Type::bool_literal(true), + Type::bool_literal(false), + Type::string_literal(db, "value"), + KnownClass::Bool.to_class_literal(db, &env), + KnownClass::Int.to_class_literal(db, &env), + SubclassOfType::from(db, &env, u), + ]; + let mut positive_results = 0; + + for left in types { + for right in types { + let trivial = + left.when_trivially_disjoint_from(db, &env, right, &builder, TypeVarSet::None); + if trivial.is_trivially_always_satisfied() { + positive_results += 1; + assert!( + left.when_disjoint_from(db, &env, right, &builder, TypeVarSet::None) + .is_always_satisfied(db, &env), + "cheap disjointness incorrectly accepts `{}` and `{}`", + left.display(db, &env), + right.display(db, &env) + ); + } + } + } + + assert!(positive_results > 0); + } + + #[test] + fn overlapping_lower_bounds_do_not_skip_nonempty_sequent_map() { + let db = setup_db(); + let db = &db; + let env = db.program_environment(); + let builder = ConstraintSetBuilder::new(); + let t = create_typevar(db, "T"); + let bool = known_instance(db, KnownClass::Bool); + let u = create_typevar(db, "U") + .map_bound_or_constraints(db, |_| Some(TypeVarBoundOrConstraints::UpperBound(bool))); + let type_of_u = SubclassOfType::from(db, &env, u); + let bool_class = KnownClass::Bool.to_class_literal(db, &env); + let mut storage = builder.storage.borrow_mut(); + let left = ConstraintId::new_with_bounds(db, &env, &mut storage, t, Some(type_of_u), None); + let right = + ConstraintId::new_with_bounds(db, &env, &mut storage, t, Some(bool_class), None); + + for (left, right) in [(left, right), (right, left)] { + let sequents = SequentMap::for_constraint_pair(db, &env, &mut storage, left, right); + + assert!( + sequents + .sequents + .iter() + .any(|sequent| matches!(sequent, Sequent::SingleImplication { .. })) + ); + assert!(!SequentMap::pair_cannot_produce_sequents( + db, + &env, + &mut storage, + left, + right + )); + } + } + #[test] fn simple_lower_bound_conjunction_skips_sequent_analysis() { let db = setup_db(); - let t = create_typevar(&db, "T"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); let builder = ConstraintSetBuilder::new(); - let int = KnownClass::Int.to_instance(&db); - let str = KnownClass::Str.to_instance(&db); - let set = ConstraintSet::constrain_typevar_lower_bound(&db, &builder, t, int).and( - &db, + let int = KnownClass::Int.to_instance(db, &env); + let str = KnownClass::Str.to_instance(db, &env); + let set = ConstraintSet::constrain_typevar_lower_bound(db, &env, &builder, t, int).and( + db, &builder, - || ConstraintSet::constrain_typevar_lower_bound(&db, &builder, t, str), + || ConstraintSet::constrain_typevar_lower_bound(db, &env, &builder, t, str), ); - let inferable = - InferableTypeVars::from_typevars(&db, std::iter::once(t.identity(&db)).collect()); + let inferable = TypeVarSet::from_typevars(db, [t]); let (single_sequents, pair_sequents) = { let storage = builder.storage.borrow(); ( @@ -7740,12 +7651,12 @@ mod tests { ) }; - let solutions = set.solutions(&db, &builder, inferable); + let solutions = set.solutions(db, &env, &builder, inferable); assert_eq!( solutions, Solutions::Constrained(vec![vec![TypeVarSolution { bound_typevar: t, - solution: UnionType::from_elements(&db, [int, str]), + solution: UnionType::from_elements(db, &env, [int, str]), }]]) ); @@ -7757,18 +7668,18 @@ mod tests { #[test] fn simple_exact_bound_conjunction_skips_sequent_analysis() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); let builder = ConstraintSetBuilder::new(); - let int = KnownClass::Int.to_instance(&db); - let set = - ConstraintSet::constrain_typevar(&db, &builder, t, int, int).and(&db, &builder, || { - ConstraintSet::constrain_typevar(&db, &builder, u, int, int) - }); - let inferable = InferableTypeVars::from_typevars( - &db, - [t.identity(&db), u.identity(&db)].into_iter().collect(), + let int = KnownClass::Int.to_instance(db, &env); + let set = ConstraintSet::constrain_typevar(db, &env, &builder, t, int, int).and( + db, + &builder, + || ConstraintSet::constrain_typevar(db, &env, &builder, u, int, int), ); + let inferable = TypeVarSet::from_typevars(db, [t, u]); let (single_sequents, pair_sequents) = { let storage = builder.storage.borrow(); ( @@ -7777,7 +7688,7 @@ mod tests { ) }; - let Solutions::Constrained(solutions) = set.solutions(&db, &builder, inferable) else { + let Solutions::Constrained(solutions) = set.solutions(db, &env, &builder, inferable) else { panic!("expected constrained solutions"); }; assert_eq!(solutions.len(), 1); @@ -7799,16 +7710,18 @@ mod tests { #[test] fn simple_unsatisfiable_exact_bound_conjunction_skips_sequent_analysis() { let db = setup_db(); - let t = create_typevar(&db, "T"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); let builder = ConstraintSetBuilder::new(); - let int = KnownClass::Int.to_instance(&db); - let str = KnownClass::Str.to_instance(&db); - let set = - ConstraintSet::constrain_typevar(&db, &builder, t, int, int).and(&db, &builder, || { - ConstraintSet::constrain_typevar(&db, &builder, t, str, str) - }); - let inferable = - InferableTypeVars::from_typevars(&db, std::iter::once(t.identity(&db)).collect()); + let int = KnownClass::Int.to_instance(db, &env); + let str = KnownClass::Str.to_instance(db, &env); + let set = ConstraintSet::constrain_typevar(db, &env, &builder, t, int, int).and( + db, + &builder, + || ConstraintSet::constrain_typevar(db, &env, &builder, t, str, str), + ); + let inferable = TypeVarSet::from_typevars(db, [t]); let (single_sequents, pair_sequents) = { let storage = builder.storage.borrow(); ( @@ -7818,7 +7731,7 @@ mod tests { }; assert_eq!( - set.solutions(&db, &builder, inferable), + set.solutions(db, &env, &builder, inferable), Solutions::Unsatisfiable ); @@ -7830,7 +7743,9 @@ mod tests { #[test] fn default_solve_leaves_unbounded_typevar_unsolved_without_bounds() { let db = setup_db(); - let t = create_typevar(&db, "T"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); let builder = ConstraintSetBuilder::new(); let path_bound = PathBound { bound_typevar: t, @@ -7840,7 +7755,7 @@ mod tests { }; assert_eq!( - PathBounds::default_solve(&db, &builder, &path_bound), + PathBounds::default_solve(db, &env, &builder, &path_bound), Ok(None) ); } @@ -7848,22 +7763,33 @@ mod tests { #[test] fn constraint_intersection_detects_disjoint_union_upper_bounds() { let db = setup_db(); - let t = create_typevar(&db, "T"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); let builder = ConstraintSetBuilder::new(); - let int = known_instance(&db, KnownClass::Int); - let str = known_instance(&db, KnownClass::Str); - let bytes = known_instance(&db, KnownClass::Bytes); - let bytearray = known_instance(&db, KnownClass::Bytearray); - let int_or_str = UnionType::from_two_elements(&db, int, str); - let bytes_or_bytearray = UnionType::from_two_elements(&db, bytes, bytearray); - let left = ConstraintId::new_with_bounds(&db, &builder, t, Some(int), Some(int_or_str)); - let right = ConstraintId::new_with_bounds(&db, &builder, t, None, Some(bytes_or_bytearray)); + let int = known_instance(db, KnownClass::Int); + let str = known_instance(db, KnownClass::Str); + let bytes = known_instance(db, KnownClass::Bytes); + let bytearray = known_instance(db, KnownClass::Bytearray); + let int_or_str = UnionType::from_two_elements(db, &env, int, str); + let bytes_or_bytearray = UnionType::from_two_elements(db, &env, bytes, bytearray); + let mut storage = builder.storage.borrow_mut(); + let left = + ConstraintId::new_with_bounds(db, &env, &mut storage, t, Some(int), Some(int_or_str)); + let right = ConstraintId::new_with_bounds( + db, + &env, + &mut storage, + t, + None, + Some(bytes_or_bytearray), + ); // Check satisfiability against each upper clause before punting on the union-bearing // merged upper bound. The old size heuristic returned `CannotSimplify` here before // discovering that `int` cannot satisfy the second upper clause. assert!(matches!( - left.intersect(&db, &builder, right), + left.intersect(db, &env, &mut storage, right), IntersectionResult::Disjoint )); } @@ -7871,25 +7797,31 @@ mod tests { #[test] fn constraint_implications_are_cached() { let db = setup_db(); - let t = create_typevar(&db, "T"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); let builder = ConstraintSetBuilder::new(); + let mut storage = builder.storage.borrow_mut(); let t_int = ConstraintId::new( - &db, - &builder, + db, + &env, + &mut storage, t, Type::Never, - KnownClass::Int.to_instance(&db), + KnownClass::Int.to_instance(db, &env), ); let t_bool = ConstraintId::new( - &db, - &builder, + db, + &env, + &mut storage, t, Type::Never, - KnownClass::Bool.to_instance(&db), + KnownClass::Bool.to_instance(db, &env), ); - assert!(builder.cached_constraint_implies(&db, t_bool, t_int)); - assert!(builder.cached_constraint_implies(&db, t_bool, t_int)); + assert!(storage.cached_constraint_implies(db, &env, t_bool, t_int)); + assert!(storage.cached_constraint_implies(db, &env, t_bool, t_int)); + drop(storage); { let storage = builder.storage.borrow(); @@ -7900,8 +7832,10 @@ mod tests { assert_eq!(storage.constraint_implication_cache.len(), 1); } - assert!(!builder.cached_constraint_implies(&db, t_int, t_bool)); - assert!(!builder.cached_constraint_implies(&db, t_int, t_bool)); + let mut storage = builder.storage.borrow_mut(); + assert!(!storage.cached_constraint_implies(db, &env, t_int, t_bool)); + assert!(!storage.cached_constraint_implies(db, &env, t_int, t_bool)); + drop(storage); let storage = builder.storage.borrow(); assert_eq!( @@ -7911,21 +7845,151 @@ mod tests { assert_eq!(storage.constraint_implication_cache.len(), 2); } + #[test] + fn trivial_satisfaction_only_recognizes_terminals() { + let db = setup_db(); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let builder = ConstraintSetBuilder::new(); + let t_int = create_constraint(db, &builder, t, KnownClass::Int); + let t_str = create_constraint(db, &builder, t, KnownClass::Str); + let impossible = t_int.and(db, &builder, || t_str); + + assert!(ConstraintSet::always(&builder).is_trivially_always_satisfied()); + assert!(!ConstraintSet::always(&builder).is_trivially_never_satisfied()); + assert!(ConstraintSet::never(&builder).is_trivially_never_satisfied()); + assert!(!ConstraintSet::never(&builder).is_trivially_always_satisfied()); + assert!(!t_int.is_trivially_always_satisfied()); + assert!(!t_int.is_trivially_never_satisfied()); + assert!(impossible.is_never_satisfied(db, &env)); + assert!(!impossible.is_trivially_never_satisfied()); + + let t_bool_upper = ConstraintSet::constrain_typevar_upper_bound( + db, + &env, + &builder, + t, + KnownClass::Bool.to_instance(db, &env), + ); + let t_int_upper = ConstraintSet::constrain_typevar_upper_bound( + db, + &env, + &builder, + t, + KnownClass::Int.to_instance(db, &env), + ); + let tautology = t_bool_upper + .negate(db, &builder) + .or(db, &builder, || t_int_upper); + + assert!(tautology.is_always_satisfied(db, &env)); + assert!(!tautology.is_trivially_always_satisfied()); + } + + #[test] + fn combinators_only_short_circuit_on_terminal_saturation() { + let db = setup_db(); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let builder = ConstraintSetBuilder::new(); + let t_int = create_constraint(db, &builder, t, KnownClass::Int); + let t_str = create_constraint(db, &builder, t, KnownClass::Str); + let impossible = t_int.and(db, &builder, || t_str); + let t_bool_upper = ConstraintSet::constrain_typevar_upper_bound( + db, + &env, + &builder, + t, + KnownClass::Bool.to_instance(db, &env), + ); + let t_int_upper = ConstraintSet::constrain_typevar_upper_bound( + db, + &env, + &builder, + t, + KnownClass::Int.to_instance(db, &env), + ); + let tautology = t_bool_upper + .negate(db, &builder) + .or(db, &builder, || t_int_upper); + + let forced = Cell::new(0); + ConstraintSet::never(&builder).and(db, &builder, || { + forced.set(forced.get() + 1); + t_int + }); + ConstraintSet::always(&builder).or(db, &builder, || { + forced.set(forced.get() + 1); + t_int + }); + assert_eq!(forced.get(), 0); + + impossible.and(db, &builder, || { + forced.set(forced.get() + 1); + t_int + }); + tautology.or(db, &builder, || { + forced.set(forced.get() + 1); + t_int + }); + assert_eq!(forced.get(), 2); + + let visited = Cell::new(0); + [impossible, t_int] + .into_iter() + .when_all(db, &builder, |set| { + visited.set(visited.get() + 1); + set + }); + assert_eq!(visited.get(), 2); + + visited.set(0); + [tautology, t_int] + .into_iter() + .when_any(db, &builder, |set| { + visited.set(visited.get() + 1); + set + }); + assert_eq!(visited.get(), 2); + + visited.set(0); + [ConstraintSet::never(&builder), t_int] + .into_iter() + .when_all(db, &builder, |set| { + visited.set(visited.get() + 1); + set + }); + assert_eq!(visited.get(), 1); + + visited.set(0); + [ConstraintSet::always(&builder), t_int] + .into_iter() + .when_any(db, &builder, |set| { + visited.set(visited.get() + 1); + set + }); + assert_eq!(visited.get(), 1); + } + #[test] fn never_satisfied_results_are_cached() { let db = setup_db(); - let t = create_typevar(&db, "T"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); let builder = ConstraintSetBuilder::new(); - let t_int = create_constraint(&db, &builder, t, KnownClass::Int); - let t_str = create_constraint(&db, &builder, t, KnownClass::Str); - let impossible = t_int.and(&db, &builder, || t_str); + let t_int = create_constraint(db, &builder, t, KnownClass::Int); + let t_str = create_constraint(db, &builder, t, KnownClass::Str); + let impossible = t_int.and(db, &builder, || t_str); - assert!(!t_int.is_never_satisfied(&db)); - assert!(!t_int.is_never_satisfied(&db)); - assert!(impossible.is_never_satisfied(&db)); - assert!(impossible.is_never_satisfied(&db)); - assert!(ConstraintSet::never(&builder).is_never_satisfied(&db)); - assert!(!ConstraintSet::always(&builder).is_never_satisfied(&db)); + assert!(!t_int.is_never_satisfied(db, &env)); + assert!(!t_int.is_never_satisfied(db, &env)); + assert!(impossible.is_never_satisfied(db, &env)); + assert!(impossible.is_never_satisfied(db, &env)); + assert!(ConstraintSet::never(&builder).is_never_satisfied(db, &env)); + assert!(!ConstraintSet::always(&builder).is_never_satisfied(db, &env)); { let storage = builder.storage.borrow(); @@ -7937,21 +8001,37 @@ mod tests { assert_eq!(storage.never_satisfied_cache.len(), 2); } - let owned = create_compacted_owned_set(&db); + let owned = create_compacted_owned_set(db); owned.query(|builder, set| { - assert!(!set.is_never_satisfied(&db)); - assert!(!set.is_never_satisfied(&db)); - assert_eq!( - builder - .storage - .borrow() - .never_satisfied_cache - .get(&set.node), - Some(&false) - ); + assert!(!set.is_never_satisfied(db, &env)); + assert!(!set.is_never_satisfied(db, &env)); + let storage = builder.storage.borrow(); + assert_eq!(storage.never_satisfied_cache.get(&set.node), Some(&false)); }); } + #[test] + fn never_satisfied_cache_is_shared_across_source_orders() { + let db = setup_db(); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); + let builder = ConstraintSetBuilder::new(); + let t_int = create_constraint(db, &builder, t, KnownClass::Int); + let u_str = create_constraint(db, &builder, u, KnownClass::Str); + + let first = t_int.and(db, &builder, || u_str); + let second = u_str.and(db, &builder, || t_int); + + assert_eq!(first.node, second.node); + assert_ne!(first.source_order, second.source_order); + assert!(!first.is_never_satisfied(db, &env)); + assert!(!second.is_never_satisfied(db, &env)); + let storage = builder.storage.borrow(); + assert_eq!(storage.never_satisfied_cache.len(), 1); + } + #[derive(Clone, Copy)] struct PermutedConstraint<'db>( BoundTypeVarInstance<'db>, @@ -7960,9 +8040,14 @@ mod tests { ); impl<'db> PermutedConstraint<'db> { - fn node(self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>) -> NodeId { + fn node( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, + ) -> NodeId { let PermutedConstraint(typevar, lower, upper) = self; - Constraint::new_node_with_bounds(db, builder, typevar, lower, upper) + Constraint::new_node_with_bounds(db, env, storage, typevar, lower, upper).0 } } @@ -7976,30 +8061,27 @@ mod tests { /// that we get that specific result for each permutation. #[track_caller] fn check_solutions_for_constraint_orderings<'db>( - db: &'db dyn Db, + db: &'db TestDb, typevars: &[BoundTypeVarInstance<'db>], atoms: &[PermutedConstraint<'db>], - build_bdd: impl Fn(&ConstraintSetBuilder<'db>) -> NodeId, + build_bdd: impl Fn(&mut ConstraintSetStorage<'db>) -> NodeId, expected: impl IntoIterator, ) { - let inferable = InferableTypeVars::from_typevars( - db, - typevars - .iter() - .map(|typevar| typevar.identity(db)) - .collect(), - ); + let env = db.program_environment(); + let inferable = TypeVarSet::from_typevars(db, typevars.iter().copied()); let mut signatures = FxIndexSet::default(); for constraint_order in (0..atoms.len()).permutations(atoms.len()) { let builder = ConstraintSetBuilder::new(); + let mut storage = builder.storage.borrow_mut(); for typevar in typevars { - builder.intern_typevar(db, *typevar); + storage.intern_typevar(db, *typevar); } for index in constraint_order { let PermutedConstraint(typevar, lower, upper) = atoms[index]; - builder.intern_constraint( + storage.intern_constraint( db, + &env, Constraint { typevar, bounds: ConstraintBounds::new(lower, upper), @@ -8007,8 +8089,24 @@ mod tests { ); } - let set = ConstraintSet::from_node(&builder, build_bdd(&builder)); - let solutions = set.solutions(db, &builder, inferable); + let node = build_bdd(&mut storage); + let source_order = atoms.iter().fold(None, |source_order, atom| { + let PermutedConstraint(typevar, lower, upper) = *atom; + let constraint = storage.intern_constraint( + db, + &env, + Constraint { + typevar, + bounds: ConstraintBounds::new(lower, upper), + }, + ); + let constraint_source_order = storage.constraint_source_order(constraint); + storage.ordered_source_order(source_order, Some(constraint_source_order)) + }); + drop(storage); + + let set = ConstraintSet::from_node(&builder, node, source_order); + let solutions = set.solutions(db, &env, &builder, inferable); let mut merged = FxHashMap::default(); if let Solutions::Constrained(paths) = &solutions { for path in paths { @@ -8016,8 +8114,12 @@ mod tests { merged .entry(binding.bound_typevar) .and_modify(|existing| { - *existing = - UnionType::from_two_elements(db, *existing, binding.solution); + *existing = UnionType::from_two_elements( + db, + &env, + *existing, + binding.solution, + ); }) .or_insert(binding.solution); } @@ -8027,7 +8129,11 @@ mod tests { .iter() .filter_map(|typevar| { merged.get(typevar).map(|ty| { - format!("{}={}", typevar.identity(db).display(db), ty.display(db)) + format!( + "{}={}", + typevar.identity(db).display(db), + ty.display(db, &env) + ) }) }) .join(", "); @@ -8042,7 +8148,7 @@ mod tests { format!( "{}={}", binding.bound_typevar.identity(db).display(db), - binding.solution.display(db) + binding.solution.display(db, &env) ) }) .join(", ") @@ -8051,8 +8157,8 @@ mod tests { }; signatures.insert(format!( "never={} always={} merged=[{merged}] paths=[{paths}]", - set.is_never_satisfied(db), - set.is_always_satisfied(db), + set.is_never_satisfied(db, &env), + set.is_always_satisfied(db, &env), )); } @@ -8060,16 +8166,141 @@ mod tests { assert_eq!(signatures, expected); } + #[test] + fn constraint_absorption_is_independent_of_constraint_order() { + let db = setup_db(); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let str = KnownClass::Str.to_instance(db, &env); + let int = KnownClass::Int.to_instance(db, &env); + let atoms = [ + PermutedConstraint(t, Some(str), None), + PermutedConstraint(t, Some(int), None), + ]; + + check_solutions_for_constraint_orderings( + db, + &[t], + &atoms, + |storage| { + let [str_t, int_t] = atoms.map(|atom| atom.node(db, &env, storage)); + str_t.or(storage, int_t).and(storage, str_t) + }, + ["never=false always=false merged=[T=str] paths=[T=str]"], + ); + + check_solutions_for_constraint_orderings( + db, + &[t], + &atoms, + |storage| { + let [str_t, int_t] = atoms.map(|atom| atom.node(db, &env, storage)); + str_t.or(storage, int_t) + }, + ["never=false always=false merged=[T=str | int] paths=[T=str; T=int]"], + ); + } + + #[test] + fn compound_constraint_absorption_is_independent_of_constraint_order() { + let db = setup_db(); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); + let str = KnownClass::Str.to_instance(db, &env); + let bytes = KnownClass::Bytes.to_instance(db, &env); + let int = KnownClass::Int.to_instance(db, &env); + let atoms = [ + PermutedConstraint(t, Some(str), None), + PermutedConstraint(u, Some(bytes), None), + PermutedConstraint(t, Some(int), None), + ]; + + check_solutions_for_constraint_orderings( + db, + &[t, u], + &atoms, + |storage| { + let [str_t, bytes_u, int_t] = atoms.map(|atom| atom.node(db, &env, storage)); + let compound = str_t.and(storage, bytes_u); + compound.or(storage, int_t).and(storage, compound) + }, + ["never=false always=false merged=[T=str, U=bytes] paths=[T=str, U=bytes]"], + ); + } + + #[test] + fn compound_constraint_absorption_preserves_binding_source_order() { + let db = setup_db(); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); + let x = create_typevar(db, "X"); + let str = KnownClass::Str.to_instance(db, &env); + let bytes = KnownClass::Bytes.to_instance(db, &env); + let int = KnownClass::Int.to_instance(db, &env); + let atoms = [ + PermutedConstraint(t, Some(str), None), + PermutedConstraint(u, Some(bytes), None), + PermutedConstraint(x, Some(int), None), + ]; + + check_solutions_for_constraint_orderings( + db, + &[t, u, x], + &atoms, + |storage| { + let [str_t, bytes_u, int_x] = atoms.map(|atom| atom.node(db, &env, storage)); + let early = int_x.and(storage, str_t).and(storage, bytes_u); + let late = bytes_u.and(storage, str_t); + early.or(storage, late) + }, + ["never=false always=false merged=[T=str, U=bytes] paths=[T=str, U=bytes]"], + ); + } + + #[test] + fn constraint_partition_is_independent_of_constraint_order() { + let db = setup_db(); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let str = KnownClass::Str.to_instance(db, &env); + let int = KnownClass::Int.to_instance(db, &env); + let atoms = [ + PermutedConstraint(t, Some(str), None), + PermutedConstraint(t, Some(int), None), + ]; + + check_solutions_for_constraint_orderings( + db, + &[t], + &atoms, + |storage| { + let [str_t, int_t] = atoms.map(|atom| atom.node(db, &env, storage)); + let true_path = int_t.and(storage, str_t); + let false_path = int_t.negate(storage).and(storage, str_t); + true_path.or(storage, false_path) + }, + ["never=false always=false merged=[T=str] paths=[T=str]"], + ); + } + #[test] fn constraint_ordering_changes_nested_transitive_solutions() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); - let v = create_typevar(&db, "V"); - let int = KnownClass::Int.to_instance(&db); - let bytes = KnownClass::Bytes.to_instance(&db); - let list_u = KnownClass::List.to_specialized_instance(&db, &[Type::TypeVar(u)]); - let list_int = KnownClass::List.to_specialized_instance(&db, &[int]); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); + let v = create_typevar(db, "V"); + let int = KnownClass::Int.to_instance(db, &env); + let bytes = KnownClass::Bytes.to_instance(db, &env); + let list_u = KnownClass::List.to_specialized_instance(db, &env, &[Type::TypeVar(u)]); + let list_int = KnownClass::List.to_specialized_instance(db, &env, &[int]); let atoms = [ PermutedConstraint(t, None, Some(list_u)), PermutedConstraint(u, None, Some(int)), @@ -8078,16 +8309,16 @@ mod tests { ]; check_solutions_for_constraint_orderings( - &db, + db, &[t, u, v], &atoms, - |builder| { + |storage| { let [t_list_u, u_int, list_int_t, bytes_v] = - atoms.map(|atom| atom.node(&db, builder)); + atoms.map(|atom| atom.node(db, &env, storage)); t_list_u - .and_with_offset(builder, u_int) - .and_with_offset(builder, list_int_t) - .or_with_offset(builder, bytes_v) + .and(storage, u_int) + .and(storage, list_int_t) + .or(storage, bytes_v) }, // TODO: All permutations should produce the first result. TDD traversal currently // leaks irrelevant positive constraints onto the `V = bytes` alternative. @@ -8103,11 +8334,13 @@ mod tests { #[test] fn constraint_ordering_changes_negated_alternative_solutions() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); - let int = KnownClass::Int.to_instance(&db); - let str = KnownClass::Str.to_instance(&db); - let bytes = KnownClass::Bytes.to_instance(&db); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); + let int = KnownClass::Int.to_instance(db, &env); + let str = KnownClass::Str.to_instance(db, &env); + let bytes = KnownClass::Bytes.to_instance(db, &env); let atoms = [ PermutedConstraint(t, None, Some(int)), PermutedConstraint(t, None, Some(str)), @@ -8115,15 +8348,15 @@ mod tests { ]; check_solutions_for_constraint_orderings( - &db, + db, &[t, u], &atoms, - |builder| { - let [t_int, t_str, bytes_u] = atoms.map(|atom| atom.node(&db, builder)); + |storage| { + let [t_int, t_str, bytes_u] = atoms.map(|atom| atom.node(db, &env, storage)); t_int - .or_with_offset(builder, t_str) - .negate(builder) - .or_with_offset(builder, bytes_u) + .or(storage, t_str) + .negate(storage) + .or(storage, bytes_u) }, // TODO: All permutations should produce the first result. A satisfied alternative // should not infer `T` from unrelated positive decisions made earlier in a BDD path. @@ -8138,10 +8371,12 @@ mod tests { #[test] fn constraint_ordering_changes_derived_upper_bound_display() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); - let int = KnownClass::Int.to_instance(&db); - let str = KnownClass::Str.to_instance(&db); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); + let int = KnownClass::Int.to_instance(db, &env); + let str = KnownClass::Str.to_instance(db, &env); let atoms = [ PermutedConstraint(t, None, Some(int)), PermutedConstraint(t, None, Some(str)), @@ -8150,19 +8385,18 @@ mod tests { ]; check_solutions_for_constraint_orderings( - &db, + db, &[t, u], &atoms, - |builder| { - let [t_int, t_str, int_t, u_int] = atoms.map(|atom| atom.node(&db, builder)); + |storage| { + let [t_int, t_str, int_t, u_int] = atoms.map(|atom| atom.node(db, &env, storage)); t_int - .or_with_offset(builder, t_str) - .and_with_offset(builder, int_t) - .and_with_offset(builder, u_int) + .or(storage, t_str) + .and(storage, int_t) + .and(storage, u_int) }, - // TODO: `SequentMap::for_constraint_pair` can receive its inputs in BDD order, not - // source order. That changes which equivalent upper-bound intersection is constructed - // first. + // TODO: Constraint-ID permutations can still change which equivalent upper-bound + // intersection is constructed first. [ "never=false always=false merged=[T=int | U, U=T & int] paths=[T=int | U, U=T & int]", "never=false always=false merged=[T=int | U, U=int & T] paths=[T=int | U, U=int & T]", @@ -8172,44 +8406,47 @@ mod tests { #[track_caller] fn check_display_graph<'db, 'c>( - db: &'db dyn Db, + db: &'db TestDb, builder: &'c ConstraintSetBuilder<'db>, set: ConstraintSet<'db, 'c>, expected: &str, ) { + let env = db.program_environment(); + let storage = builder.storage.borrow(); let expected = expected.trim_end(); - let actual = set.node.display_graph(db, builder, &"").to_string(); + let actual = set.node.display_graph(db, &env, &storage, &"").to_string(); assert_eq!(expected, actual); } #[test] fn test_display_graph_output() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); + let db = &db; + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); let constraints = ConstraintSetBuilder::new(); - let t_str = create_constraint(&db, &constraints, t, KnownClass::Str); - let t_bool = create_constraint(&db, &constraints, t, KnownClass::Bool); - let u_str = create_constraint(&db, &constraints, u, KnownClass::Str); - let u_bool = create_constraint(&db, &constraints, u, KnownClass::Bool); + let t_str = create_constraint(db, &constraints, t, KnownClass::Str); + let t_bool = create_constraint(db, &constraints, t, KnownClass::Bool); + let u_str = create_constraint(db, &constraints, u, KnownClass::Str); + let u_bool = create_constraint(db, &constraints, u, KnownClass::Bool); // Construct this in a different order than above to make the source_orders more // interesting. - let set = (u_str.or(&db, &constraints, || u_bool)) - .and(&db, &constraints, || t_str.or(&db, &constraints, || t_bool)); + let set = (u_str.or(db, &constraints, || u_bool)) + .and(db, &constraints, || t_str.or(db, &constraints, || t_bool)); check_display_graph( - &db, + db, &constraints, set, indoc! {r#" - <0> (U = bool) 2/4 - ┡━₁ <1> (T = bool) 4/4 + <0> (U = bool) + ┡━₁ <1> (T = bool) │ ┡━₁ always - │ ├─? <2> (T = str) 3/3 + │ ├─? <2> (T = str) │ │ ┡━₁ always │ │ ├─? never │ │ └─₀ never │ └─₀ never - ├─? <3> (U = str) 1/4 + ├─? <3> (U = str) │ ┡━₁ <1> SHARED │ ├─? never │ └─₀ never @@ -8232,7 +8469,7 @@ mod tests { &builder, t_int, indoc! {r#" - <0> (T = int) 1/1 + <0> (T = int) ┡━₁ always ├─? never └─₀ never @@ -8245,24 +8482,25 @@ mod tests { #[test] fn tdd_union_creates_uncertain_branches() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); + let db = &db; + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); let builder = ConstraintSetBuilder::new(); // Neither lhs nor rhs have uncertain branches (checked above). The operand with the // "lower" BDD variable (in this case, the lhs) is parked into a new uncertain branch in // the union result. - let t_int = create_constraint(&db, &builder, t, KnownClass::Int); - let u_str = create_constraint(&db, &builder, u, KnownClass::Str); - let union = t_int.or(&db, &builder, || u_str); + let t_int = create_constraint(db, &builder, t, KnownClass::Int); + let u_str = create_constraint(db, &builder, u, KnownClass::Str); + let union = t_int.or(db, &builder, || u_str); check_display_graph( - &db, + db, &builder, union, indoc! {r#" - <0> (U = str) 2/2 + <0> (U = str) ┡━₁ always - ├─? <1> (T = int) 1/1 + ├─? <1> (T = int) │ ┡━₁ always │ ├─? never │ └─₀ never @@ -8276,33 +8514,34 @@ mod tests { #[test] fn tdd_intersection_preserves_uncertain() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); + let db = &db; + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); let builder = ConstraintSetBuilder::new(); - let t_int = create_constraint(&db, &builder, t, KnownClass::Int); - let u_str = create_constraint(&db, &builder, u, KnownClass::Str); - let t_bool = create_constraint(&db, &builder, t, KnownClass::Bool); - let u_int = create_constraint(&db, &builder, u, KnownClass::Int); + let t_int = create_constraint(db, &builder, t, KnownClass::Int); + let u_str = create_constraint(db, &builder, u, KnownClass::Str); + let t_bool = create_constraint(db, &builder, t, KnownClass::Bool); + let u_int = create_constraint(db, &builder, u, KnownClass::Int); // lhs and rhs both have uncertain branches (checked above). These uncertain branches are // carried through to the intersection result. - let lhs = t_int.or(&db, &builder, || u_str); - let rhs = t_bool.or(&db, &builder, || u_int); - let intersection = lhs.and(&db, &builder, || rhs); + let lhs = t_int.or(db, &builder, || u_str); + let rhs = t_bool.or(db, &builder, || u_int); + let intersection = lhs.and(db, &builder, || rhs); check_display_graph( - &db, + db, &builder, intersection, indoc! {r#" - <0> (U = int) 4/4 - ┡━₁ <1> (U = str) 2/2 + <0> (U = int) + ┡━₁ <1> (U = str) │ ┡━₁ always - │ ├─? <2> (T = int) 1/1 + │ ├─? <2> (T = int) │ │ ┡━₁ always │ │ ├─? never │ │ └─₀ never │ └─₀ never - ├─? <3> (T = bool) 3/3 + ├─? <3> (T = bool) │ ┡━₁ <1> SHARED │ ├─? never │ └─₀ never @@ -8315,22 +8554,23 @@ mod tests { #[test] fn tdd_negation_produces_flat_tdd() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); + let db = &db; + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); let builder = ConstraintSetBuilder::new(); - let t_int = create_constraint(&db, &builder, t, KnownClass::Int); - let u_str = create_constraint(&db, &builder, u, KnownClass::Str); - let union = t_int.or(&db, &builder, || u_str); - let negated = union.negate(&db, &builder); + let t_int = create_constraint(db, &builder, t, KnownClass::Int); + let u_str = create_constraint(db, &builder, u, KnownClass::Str); + let union = t_int.or(db, &builder, || u_str); + let negated = union.negate(db, &builder); check_display_graph( - &db, + db, &builder, negated, indoc! {r#" - <0> (U = str) 2/2 + <0> (U = str) ┡━₁ never ├─? never - └─₀ <1> (T = int) 1/1 + └─₀ <1> (T = int) ┡━₁ never ├─? never └─₀ always @@ -8341,59 +8581,71 @@ mod tests { #[test] fn tdd_negation_correctness() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); let builder = ConstraintSetBuilder::new(); - let t_int = create_constraint(&db, &builder, t, KnownClass::Int); - let u_str = create_constraint(&db, &builder, u, KnownClass::Str); - let tdd = t_int.or(&db, &builder, || u_str); - let negated = tdd.negate(&db, &builder); + let t_int = create_constraint(db, &builder, t, KnownClass::Int); + let u_str = create_constraint(db, &builder, u, KnownClass::Str); + let tdd = t_int.or(db, &builder, || u_str); + let negated = tdd.negate(db, &builder); // T ∧ ¬T == false - assert!(tdd.and(&db, &builder, || negated).is_never_satisfied(&db)); + assert!( + tdd.and(db, &builder, || negated) + .is_never_satisfied(db, &env) + ); // T ∨ ¬T == true - assert!(tdd.or(&db, &builder, || negated).is_always_satisfied(&db)); + assert!( + tdd.or(db, &builder, || negated) + .is_always_satisfied(db, &env) + ); } #[test] fn eager_and_lazy_negation_are_equivalent() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); let builder = ConstraintSetBuilder::new(); - let t_int = create_constraint(&db, &builder, t, KnownClass::Int); - let t_bool = create_constraint(&db, &builder, t, KnownClass::Bool); - let u_str = create_constraint(&db, &builder, u, KnownClass::Str); - let u_int = create_constraint(&db, &builder, u, KnownClass::Int); + let t_int = create_constraint(db, &builder, t, KnownClass::Int); + let t_bool = create_constraint(db, &builder, t, KnownClass::Bool); + let u_str = create_constraint(db, &builder, u, KnownClass::Str); + let u_int = create_constraint(db, &builder, u, KnownClass::Int); - let lhs = t_int.or(&db, &builder, || u_str); - let rhs = t_bool.or(&db, &builder, || u_int); - let intersection = lhs.and(&db, &builder, || rhs); - let tautology = lhs.or(&db, &builder, || lhs.negate(&db, &builder)); + let lhs = t_int.or(db, &builder, || u_str); + let rhs = t_bool.or(db, &builder, || u_int); + let intersection = lhs.and(db, &builder, || rhs); + let tautology = lhs.or(db, &builder, || lhs.negate(db, &builder)); let t_bool_upper = ConstraintSet::constrain_typevar_upper_bound( - &db, + db, + &env, &builder, t, - KnownClass::Bool.to_instance(&db), + KnownClass::Bool.to_instance(db, &env), ); let t_int_upper = ConstraintSet::constrain_typevar_upper_bound( - &db, + db, + &env, &builder, t, - KnownClass::Int.to_instance(&db), + KnownClass::Int.to_instance(db, &env), ); let implication = t_bool_upper - .negate(&db, &builder) - .or(&db, &builder, || t_int_upper); + .negate(db, &builder) + .or(db, &builder, || t_int_upper); for set in [lhs, rhs, intersection, tautology, implication] { assert_eq!( - set.is_always_satisfied(&db), - set.negate(&db, &builder).is_never_satisfied(&db) + set.is_always_satisfied(db, &env), + set.negate(db, &builder).is_never_satisfied(db, &env) ); } } @@ -8413,7 +8665,11 @@ mod tests { } impl ReconstructPathFold { - fn result(&self, at: PathFoldBreak, result: NodeId) -> ControlFlow { + fn result( + &self, + at: PathFoldBreak, + result: (NodeId, Option), + ) -> ControlFlow)> { if self.break_at == Some(at) { ControlFlow::Break(at) } else { @@ -8423,99 +8679,142 @@ mod tests { } impl PathFold for ReconstructPathFold { - type Result = NodeId; + type Result = (NodeId, Option); type Break = PathFoldBreak; fn satisfied<'db>( &mut self, _db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, path: &PathAssignments, ) -> ControlFlow { - let result = path.assignments.iter().fold( - ALWAYS_TRUE, - |result, (assignment, (source_order, _))| { - result.and( - builder, - Node::new_satisfied_constraint(builder, *assignment, *source_order), - ) - }, - ); + let result = + path.assignments + .iter() + .fold((ALWAYS_TRUE, None), |result, (assignment, _)| { + let (node, source_order) = result; + let (assignment, assignment_source_order) = + Node::new_satisfied_constraint(storage, *assignment); + ( + node.and(storage, assignment), + storage.ordered_source_order(source_order, assignment_source_order), + ) + }); self.result(PathFoldBreak::Satisfied, result) } fn unsatisfied<'db>( &mut self, _db: &'db dyn Db, - _builder: &ConstraintSetBuilder<'db>, + _storage: &mut ConstraintSetStorage<'db>, _path: &PathAssignments, ) -> ControlFlow { - self.result(PathFoldBreak::Unsatisfied, ALWAYS_FALSE) + self.result(PathFoldBreak::Unsatisfied, (ALWAYS_FALSE, None)) } fn impossible<'db>( &mut self, _db: &'db dyn Db, - _builder: &ConstraintSetBuilder<'db>, + _storage: &mut ConstraintSetStorage<'db>, _path: &PathAssignments, ) -> ControlFlow { - self.result(PathFoldBreak::Impossible, ALWAYS_FALSE) + self.result(PathFoldBreak::Impossible, (ALWAYS_FALSE, None)) } fn combine<'db>( &mut self, _db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, if_true: Self::Result, if_uncertain: Self::Result, if_false: Self::Result, ) -> ControlFlow { - let result = if_true.or(builder, if_uncertain).or(builder, if_false); - self.result(PathFoldBreak::Combine, result) + let (if_true, if_true_source_order) = if_true; + let (if_uncertain, if_uncertain_source_order) = if_uncertain; + let (if_false, if_false_source_order) = if_false; + let node = if_true.or(storage, if_uncertain).or(storage, if_false); + let source_order = + storage.ordered_source_order(if_true_source_order, if_uncertain_source_order); + let source_order = storage.ordered_source_order(source_order, if_false_source_order); + self.result(PathFoldBreak::Combine, (node, source_order)) } } - fn path_assignments_for(builder: &ConstraintSetBuilder<'_>, node: NodeId) -> PathAssignments { + fn path_assignments_for( + builder: &ConstraintSetBuilder<'_>, + node: NodeId, + source_order: Option, + ) -> PathAssignments { match node.node() { Node::AlwaysTrue | Node::AlwaysFalse => PathAssignments::new([]), - Node::Interior(interior) => interior.path_assignments(builder), + Node::Interior(interior) => { + let mut storage = builder.storage.borrow_mut(); + interior.path_assignments(&mut storage, source_order) + } } } + #[test] + fn path_assignments_follow_constraint_source_order() { + let db = setup_db(); + let db = &db; + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); + let builder = ConstraintSetBuilder::new(); + let t_int = create_constraint(db, &builder, t, KnownClass::Int); + let u_str = create_constraint(db, &builder, u, KnownClass::Str); + + // Construct the set in the opposite order from constraint creation. This ensures the + // initializer follows the sidecar rather than either TDD traversal or constraint IDs. + let set = u_str.and(db, &builder, || t_int); + let path = path_assignments_for(&builder, set.node, set.source_order); + let storage = builder.storage.borrow(); + let expected = + [u_str.node, t_int.node].map(|node| storage.interior_node_data(node).constraint); + let actual: Vec<_> = path.discovered.keys().copied().collect(); + + assert_eq!(actual, expected); + } + #[test] fn path_fold_reconstructs_constraint_sets() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); - let v = create_typevar(&db, "V"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); + let v = create_typevar(db, "V"); let builder = ConstraintSetBuilder::new(); - let t_int = create_constraint(&db, &builder, t, KnownClass::Int); - let t_str = create_constraint(&db, &builder, t, KnownClass::Str); - let u_int = create_constraint(&db, &builder, u, KnownClass::Int); - let v_bytes = create_constraint(&db, &builder, v, KnownClass::Bytes); - let union = t_int.or(&db, &builder, || u_int); - let intersection = union.and(&db, &builder, || t_str.or(&db, &builder, || v_bytes)); - let contradiction = t_int.and(&db, &builder, || t_str); - let tautology = union.or(&db, &builder, || union.negate(&db, &builder)); - - let t_u = ConstraintSet::constrain_typevar_upper_bound(&db, &builder, t, Type::TypeVar(u)); + let t_int = create_constraint(db, &builder, t, KnownClass::Int); + let t_str = create_constraint(db, &builder, t, KnownClass::Str); + let u_int = create_constraint(db, &builder, u, KnownClass::Int); + let v_bytes = create_constraint(db, &builder, v, KnownClass::Bytes); + let union = t_int.or(db, &builder, || u_int); + let intersection = union.and(db, &builder, || t_str.or(db, &builder, || v_bytes)); + let contradiction = t_int.and(db, &builder, || t_str); + let tautology = union.or(db, &builder, || union.negate(db, &builder)); + + let t_u = + ConstraintSet::constrain_typevar_upper_bound(db, &env, &builder, t, Type::TypeVar(u)); let u_int_upper = ConstraintSet::constrain_typevar_upper_bound( - &db, + db, + &env, &builder, u, - KnownClass::Int.to_instance(&db), + KnownClass::Int.to_instance(db, &env), ); let int_t = ConstraintSet::constrain_typevar_lower_bound( - &db, + db, + &env, &builder, t, - KnownClass::Int.to_instance(&db), + KnownClass::Int.to_instance(db, &env), ); let transitive = t_u - .and(&db, &builder, || u_int_upper) - .and(&db, &builder, || int_t) - .or(&db, &builder, || v_bytes); + .and(db, &builder, || u_int_upper) + .and(db, &builder, || int_t) + .or(db, &builder, || v_bytes); for set in [ ConstraintSet::always(&builder), @@ -8526,17 +8825,20 @@ mod tests { tautology, transitive, ] { - let mut path = path_assignments_for(&builder, set.node); + let mut path = path_assignments_for(&builder, set.node, set.source_order); let mut fold = ReconstructPathFold { break_at: None }; - let ControlFlow::Continue(reconstructed) = - path.visit(&db, &builder, set.node, &mut fold) + let mut storage = builder.storage.borrow_mut(); + let ControlFlow::Continue((reconstructed, reconstructed_source_order)) = + path.visit(db, &env, &mut storage, set.node, &mut fold) else { panic!("reconstruction unexpectedly aborted"); }; - let reconstructed = ConstraintSet::from_node(&builder, reconstructed); + drop(storage); + let reconstructed = + ConstraintSet::from_node(&builder, reconstructed, reconstructed_source_order); assert!( - set.iff(&db, &builder, reconstructed) - .is_always_satisfied(&db) + set.iff(db, &builder, reconstructed) + .is_always_satisfied(db, &env) ); } } @@ -8544,15 +8846,15 @@ mod tests { #[test] fn path_fold_break_restores_path_assignments() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); let builder = ConstraintSetBuilder::new(); - let t_int = create_constraint(&db, &builder, t, KnownClass::Int); - let t_str = create_constraint(&db, &builder, t, KnownClass::Str); - let u_int = create_constraint(&db, &builder, u, KnownClass::Int); - let set = t_int - .and(&db, &builder, || t_str) - .or(&db, &builder, || u_int); + let t_int = create_constraint(db, &builder, t, KnownClass::Int); + let t_str = create_constraint(db, &builder, t, KnownClass::Str); + let u_int = create_constraint(db, &builder, u, KnownClass::Int); + let set = t_int.and(db, &builder, || t_str).or(db, &builder, || u_int); for break_at in [ PathFoldBreak::Satisfied, @@ -8560,25 +8862,28 @@ mod tests { PathFoldBreak::Impossible, PathFoldBreak::Combine, ] { - let mut path = path_assignments_for(&builder, set.node); + let mut path = path_assignments_for(&builder, set.node, set.source_order); let mut aborting_fold = ReconstructPathFold { break_at: Some(break_at), }; + let mut storage = builder.storage.borrow_mut(); assert_eq!( - path.visit(&db, &builder, set.node, &mut aborting_fold), + path.visit(db, &env, &mut storage, set.node, &mut aborting_fold), ControlFlow::Break(break_at) ); let mut completing_fold = ReconstructPathFold { break_at: None }; - let ControlFlow::Continue(reconstructed) = - path.visit(&db, &builder, set.node, &mut completing_fold) + let ControlFlow::Continue((reconstructed, reconstructed_source_order)) = + path.visit(db, &env, &mut storage, set.node, &mut completing_fold) else { panic!("reconstruction unexpectedly aborted after {break_at:?}"); }; - let reconstructed = ConstraintSet::from_node(&builder, reconstructed); + drop(storage); + let reconstructed = + ConstraintSet::from_node(&builder, reconstructed, reconstructed_source_order); assert!( - set.iff(&db, &builder, reconstructed) - .is_always_satisfied(&db) + set.iff(db, &builder, reconstructed) + .is_always_satisfied(db, &env) ); } } @@ -8588,38 +8893,69 @@ mod tests { #[test] fn tdd_double_negation() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); let builder = ConstraintSetBuilder::new(); - let t_int = create_constraint(&db, &builder, t, KnownClass::Int); - let u_str = create_constraint(&db, &builder, u, KnownClass::Str); - let tdd = t_int.or(&db, &builder, || u_str); - let negated = tdd.negate(&db, &builder); - let double_negated = negated.negate(&db, &builder); - let equivalent = tdd.iff(&db, &builder, double_negated); - assert!(equivalent.is_always_satisfied(&db)); + let t_int = create_constraint(db, &builder, t, KnownClass::Int); + let u_str = create_constraint(db, &builder, u, KnownClass::Str); + let tdd = t_int.or(db, &builder, || u_str); + let negated = tdd.negate(db, &builder); + let double_negated = negated.negate(db, &builder); + let equivalent = tdd.iff(db, &builder, double_negated); + assert!(equivalent.is_always_satisfied(db, &env)); } /// `iff(T, T)` is always satisfied for TDDs with uncertain branches. #[test] fn tdd_iff_self() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); let builder = ConstraintSetBuilder::new(); - let t_int = create_constraint(&db, &builder, t, KnownClass::Int); - let u_str = create_constraint(&db, &builder, u, KnownClass::Str); - let tdd = t_int.or(&db, &builder, || u_str); + let t_int = create_constraint(db, &builder, t, KnownClass::Int); + let u_str = create_constraint(db, &builder, u, KnownClass::Str); + let tdd = t_int.or(db, &builder, || u_str); // iff(T, T) == true - assert!(tdd.iff(&db, &builder, tdd).is_always_satisfied(&db)); + assert!(tdd.iff(db, &builder, tdd).is_always_satisfied(db, &env)); // iff(T, ¬T) == false - let negated = tdd.negate(&db, &builder); - assert!(tdd.iff(&db, &builder, negated).is_never_satisfied(&db)); + let negated = tdd.negate(db, &builder); + assert!(tdd.iff(db, &builder, negated).is_never_satisfied(db, &env)); + } + + #[test] + fn constraint_set_source_order_combination_is_idempotent() { + let db = setup_db(); + let db = &db; + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); + let builder = ConstraintSetBuilder::new(); + let t_int = create_constraint(db, &builder, t, KnownClass::Int); + let u_str = create_constraint(db, &builder, u, KnownClass::Str); + let combined = t_int.and(db, &builder, || u_str); + + for original in [t_int, combined] { + let storage = builder.storage.borrow(); + let original_source_order_count = storage.source_orders.len(); + drop(storage); + let intersection = original.and(db, &builder, || original); + let union = original.or(db, &builder, || original); + + assert_eq!(intersection.node, original.node); + assert_eq!(intersection.source_order, original.source_order); + assert_eq!(union.node, original.node); + assert_eq!(union.source_order, original.source_order); + let storage = builder.storage.borrow(); + assert_eq!(storage.source_orders.len(), original_source_order_count); + } } - fn create_compacted_owned_set(db: &dyn Db) -> OwnedConstraintSet<'_> { + fn create_compacted_owned_set(db: &TestDb) -> OwnedConstraintSet<'_> { let t = create_typevar(db, "T"); let u = create_typevar(db, "U"); let v = create_typevar(db, "V"); @@ -8641,8 +8977,10 @@ mod tests { .expect("nonterminal root should retain storage"); assert_eq!(owned.node.index(), 2); + assert_eq!(owned.source_order.map(SourceOrderId::index), Some(0)); assert_eq!(inner.nodes.len(), 1); assert_eq!(inner.constraints.len(), 1); + assert_eq!(inner.source_orders.len(), 1); assert_eq!(inner.node_indices.len(), 3); assert_eq!(inner.constraint_indices.len(), 3); assert_eq!(inner.node_indices.iter_ones().collect::>(), vec![2]); @@ -8654,6 +8992,68 @@ mod tests { assert!(owned.node.index() >= inner.nodes.len()); } + #[test] + fn owned_constraint_set_type_walk_excludes_quantified_constraints() { + let db = setup_db(); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); + + let owned = ConstraintSetBuilder::new().into_owned(|builder| { + let t_int = create_constraint(db, builder, t, KnownClass::Int); + let u_str = create_constraint(db, builder, u, KnownClass::Str); + t_int.and(db, builder, || u_str).reduce_inferable( + db, + &env, + builder, + TypeVarSet::from_typevars(db, [t]), + ) + }); + + assert_eq!( + owned + .types() + .filter_map(Type::as_typevar) + .collect::>(), + vec![u], + ); + assert_eq!( + owned.inner.as_ref().map(|inner| inner.source_orders.len()), + Some(3), + ); + } + + #[test] + fn owned_constraint_set_source_order_ignores_construction_history() { + let db = setup_db(); + let db = &db; + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); + + let build = |include_redundant_combination| { + ConstraintSetBuilder::new().into_owned(|builder| { + let t_int = create_constraint(db, builder, t, KnownClass::Int); + let u_str = create_constraint(db, builder, u, KnownClass::Str); + let combined = t_int.and(db, builder, || u_str); + + if include_redundant_combination { + // Repeating one constraint leaves the BDD and first-occurrence source order + // unchanged, but creates a distinct, reachable source-order tree. Both trees + // must compact to the same owned set. + let redundant = combined.and(db, builder, || t_int); + assert_eq!(redundant.node, combined.node); + assert_ne!(redundant.source_order, combined.source_order); + redundant + } else { + combined + } + }) + }; + + assert_eq!(build(false), build(true)); + } + #[test] fn owned_constraint_set_query_reads_compacted_overlay() { let db = setup_db(); @@ -8665,7 +9065,7 @@ mod tests { builder, set, indoc! {r#" - <0> (V = bool) 1/1 + <0> (V = bool) ┡━₁ always ├─? never └─₀ never @@ -8683,10 +9083,12 @@ mod tests { #[test] fn owned_constraint_set_mutating_query_allocates_after_overlay() { let db = setup_db(); - let owned = create_compacted_owned_set(&db); + let db = &db; + let env = db.program_environment(); + let owned = create_compacted_owned_set(db); owned.query(|builder, set| { - let (node_split, constraint_split, typevar_split) = { + let (node_split, constraint_split, typevar_split, source_order_split) = { let storage = builder.storage.borrow(); let compacted = storage .compacted @@ -8696,22 +9098,38 @@ mod tests { compacted.node_indices.len(), compacted.constraint_indices.len(), compacted.typevars.len(), + compacted.source_orders.len(), ) }; - let w = create_typevar(&db, "W"); - let w_str = create_constraint(&db, builder, w, KnownClass::Str); + let mut storage = builder.storage.borrow_mut(); + let existing_constraint = storage.interior_node_data(set.node).constraint; + assert_eq!( + Some(storage.constraint_source_order(existing_constraint)), + set.source_order + ); + drop(storage); + + let w = create_typevar(db, "W"); + let w_str = create_constraint(db, builder, w, KnownClass::Str); + let mut storage = builder.storage.borrow_mut(); let new_constraint = w_str .node - .root_constraint(builder) + .root_constraint(&storage) .expect("new constraint should be nonterminal"); assert!(w_str.node.index() >= node_split); assert!(new_constraint.index() >= constraint_split); - assert!(builder.typevar_id(&db, w).index() >= typevar_split); + assert!(storage.typevar_id(db, w).index() >= typevar_split); + drop(storage); + assert!( + w_str + .source_order + .is_some_and(|source_order| source_order.index() >= source_order_split) + ); - let combined = set.and(&db, builder, || w_str); - assert!(!combined.is_never_satisfied(&db)); + let combined = set.and(db, builder, || w_str); + assert!(!combined.is_never_satisfied(db, &env)); let storage = builder.storage.borrow(); assert!(!storage.nodes.is_empty()); @@ -8723,16 +9141,18 @@ mod tests { #[test] fn owned_constraint_set_load_reads_compacted_storage() { let db = setup_db(); - let owned = create_compacted_owned_set(&db); + let db = &db; + let env = db.program_environment(); + let owned = create_compacted_owned_set(db); let builder = ConstraintSetBuilder::new(); - let loaded = builder.load(&db, &owned); + let loaded = builder.load(db, &env, &owned); check_display_graph( - &db, + db, &builder, loaded, indoc! {r#" - <0> (V = bool) 1/1 + <0> (V = bool) ┡━₁ always ├─? never └─₀ never @@ -8743,16 +9163,18 @@ mod tests { #[test] fn terminal_owned_constraint_set_discards_storage() { let db = setup_db(); - let t = create_typevar(&db, "T"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); let owned = ConstraintSetBuilder::new().into_owned(|builder| { - let _unused = create_constraint(&db, builder, t, KnownClass::Int); + let _unused = create_constraint(db, builder, t, KnownClass::Int); ConstraintSet::always(builder) }); assert!(owned.inner.is_none()); owned.query(|builder, set| { - assert!(set.is_always_satisfied(&db)); + assert!(set.is_always_satisfied(db, &env)); let storage = builder.storage.borrow(); assert!(storage.compacted.is_none()); assert!(storage.nodes.is_empty()); @@ -8761,8 +9183,8 @@ mod tests { }); let builder = ConstraintSetBuilder::new(); - let loaded = builder.load(&db, &owned); - assert!(loaded.is_always_satisfied(&db)); + let loaded = builder.load(db, &env, &owned); + assert!(loaded.is_always_satisfied(db, &env)); } /// Round-trip through `OwnedConstraintSet`: build a TDD with uncertain branches, convert to @@ -8770,23 +9192,25 @@ mod tests { #[test] fn tdd_owned_round_trip() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); // Build a TDD with uncertain branches and convert to owned let builder = ConstraintSetBuilder::new(); let owned = builder.into_owned(|builder| { - let t_int = create_constraint(&db, builder, t, KnownClass::Int); - let u_str = create_constraint(&db, builder, u, KnownClass::Str); - let result = t_int.or(&db, builder, || u_str); + let t_int = create_constraint(db, builder, t, KnownClass::Int); + let u_str = create_constraint(db, builder, u, KnownClass::Str); + let result = t_int.or(db, builder, || u_str); check_display_graph( - &db, + db, builder, result, indoc! {r#" - <0> (U = str) 2/2 + <0> (U = str) ┡━₁ always - ├─? <1> (T = int) 1/1 + ├─? <1> (T = int) │ ┡━₁ always │ ├─? never │ └─₀ never @@ -8798,15 +9222,15 @@ mod tests { // Load into a new builder let builder = ConstraintSetBuilder::new(); - let loaded = builder.load(&db, &owned); + let loaded = builder.load(db, &env, &owned); check_display_graph( - &db, + db, &builder, loaded, indoc! {r#" - <0> (U = str) 2/2 + <0> (U = str) ┡━₁ always - ├─? <1> (T = int) 1/1 + ├─? <1> (T = int) │ ┡━₁ always │ ├─? never │ └─₀ never diff --git a/crates/ty_python_semantic/src/types/constraints/support.rs b/crates/ty_python_semantic/src/types/constraints/support.rs new file mode 100644 index 0000000000..5b9a09ce67 --- /dev/null +++ b/crates/ty_python_semantic/src/types/constraints/support.rs @@ -0,0 +1,86 @@ +//! Tracks the support of each constraint and interior node in a BDD. +//! +//! The support of a constraint is the set of typevars mentioned anywhere in the constraint +//! (either the subject, or anywhere in the lower or upper bound). +//! +//! The support of a node is the union of the supports of every constraint reachable from that +//! node. + +use std::ops::BitOrAssign; + +use crate::types::constraints::TypeVarId; + +use ruff_index::newtype_index; +use smallvec::SmallVec; + +#[newtype_index] +#[derive(get_size2::GetSize)] +pub(super) struct SupportId; + +#[derive(Clone, Debug, Default, Eq, Hash, PartialEq, get_size2::GetSize, salsa::SalsaValue)] +pub(super) struct Support { + chunks: SmallVec<[usize; 2]>, +} + +const CHUNK_SIZE: usize = usize::BITS as usize; + +impl Support { + /// Adds a typevar to this support. + pub(super) fn insert(&mut self, typevar: TypeVarId) { + let index = typevar.index(); + let chunks_needed = (index + 1).div_ceil(CHUNK_SIZE); + if self.chunks.len() < chunks_needed { + self.chunks.resize(chunks_needed, 0); + } + + let chunk_index = index / CHUNK_SIZE; + let bit_index_within_chunk = index % CHUNK_SIZE; + let bit_mask_within_chunk = 1 << bit_index_within_chunk; + self.chunks[chunk_index] |= bit_mask_within_chunk; + } + + /// Returns an iterator of all of the typevars in this support. + pub(super) fn iter(&self) -> impl Iterator + '_ { + // Iterate through all of the chunks + let mut next_chunk_start = 0; + self.chunks.iter().copied().flat_map(move |mut chunk| { + // Figure out the starting index of this chunk + let chunk_start = next_chunk_start; + next_chunk_start += CHUNK_SIZE; + + // Iterate through the set bits in this chunk + std::iter::from_fn(move || { + // Find the lowest set bit, if there is one + let index = chunk.trailing_zeros() as usize; + if index == CHUNK_SIZE { + return None; + } + + // Clear out the bit we just found. + chunk ^= 1 << index; + + // And then return it, converted into a TypeVarId + Some(TypeVarId::from_usize(chunk_start + index)) + }) + }) + } +} + +impl BitOrAssign<&Self> for Support { + fn bitor_assign(&mut self, rhs: &Self) { + if self.chunks.len() < rhs.chunks.len() { + self.chunks.resize(rhs.chunks.len(), 0); + } + for (lhs, rhs) in std::iter::zip(&mut self.chunks, &rhs.chunks) { + *lhs |= *rhs; + } + } +} + +impl BitOrAssign> for Support { + fn bitor_assign(&mut self, rhs: Option<&Self>) { + if let Some(rhs) = rhs { + *self |= rhs; + } + } +} diff --git a/crates/ty_python_semantic/src/types/context.rs b/crates/ty_python_semantic/src/types/context.rs index 0eaa06d914..c47d4297f6 100644 --- a/crates/ty_python_semantic/src/types/context.rs +++ b/crates/ty_python_semantic/src/types/context.rs @@ -1,13 +1,16 @@ -use std::fmt; +use std::{cell::Cell, fmt, hint::cold_path, marker::PhantomData}; use drop_bomb::DebugDropBomb; +use ruff_db::PythonFile; use ruff_db::diagnostic::DiagnosticTag; use ruff_db::parsed::ParsedModuleRef; use ruff_db::{ diagnostic::{Annotation, Diagnostic, DiagnosticId, IntoDiagnosticMessage, Severity, Span}, files::File, }; +use ruff_python_ast::PythonVersion; use ruff_text_size::{Ranged, TextRange}; +use salsa::plumbing::{AsId, FromId, Id}; use super::{Type, TypeCheckDiagnostics, infer_definition_types}; @@ -18,12 +21,109 @@ use crate::types::diagnostic::{INVALID_TYPE_FORM, UNBOUND_TYPE_VARIABLE}; use crate::types::function::FunctionDecorators; use crate::types::infer::InferenceFlags; use crate::{ - Db, + Db, Program, lint::{LintId, LintMetadata}, suppression::suppressions, }; +use ty_module_resolver::ResolverEnvironment; +use ty_python_core::definition::Definition; use ty_python_core::scope::ScopeId; -use ty_python_core::semantic_index; +use ty_python_core::{ProgramFile, semantic_index}; + +/// The lazily resolved program used by a semantic operation. +#[derive(Clone)] +pub struct ProgramEnvironment<'db> { + environment: Cell, + lifetime: PhantomData<&'db ()>, +} + +impl<'db> ProgramEnvironment<'db> { + /// Creates an environment that lazily obtains its program from `file`. + pub fn from_file(file: ProgramFile<'db>) -> Self { + Self { + environment: Cell::new(ProgramSource::File(file.as_id())), + lifetime: PhantomData, + } + } + + /// Creates an environment that lazily obtains its program from `definition`. + pub fn from_definition(definition: Definition<'db>) -> Self { + Self { + environment: Cell::new(ProgramSource::Definition(definition.as_id())), + lifetime: PhantomData, + } + } + + /// Creates an environment that lazily obtains its program from `scope`. + pub fn from_scope(scope: ScopeId<'db>) -> Self { + Self { + environment: Cell::new(ProgramSource::Scope(scope.as_id())), + lifetime: PhantomData, + } + } + + /// Creates an environment with an already-established program. + pub fn from_program(program: Program<'db>) -> Self { + Self { + environment: Cell::new(ProgramSource::Program(program.as_id())), + lifetime: PhantomData, + } + } + + /// Returns the program used by this operation. + #[inline] + pub fn program(&self, db: &'db dyn Db) -> Program<'db> { + let program = match self.environment.get() { + ProgramSource::Program(id) => return Program::from_id(id), + ProgramSource::File(file) => { + cold_path(); + // The source handle and database share `'db`; re-wrapping the stored ingredient + // ID immediately before the read restores the original database lifetime. + ProgramFile::from_id(file).program(db) + } + ProgramSource::Definition(definition) => { + cold_path(); + // The source handle and database share `'db`; re-wrapping the stored ingredient + // ID immediately before the read restores the original database lifetime. + Definition::from_id(definition).program(db) + } + ProgramSource::Scope(scope) => { + cold_path(); + // The source handle and database share `'db`; re-wrapping the stored ingredient + // ID immediately before the read restores the original database lifetime. + ScopeId::from_id(scope).program(db) + } + }; + + self.environment + .set(ProgramSource::Program(program.as_id())); + program + } + + /// Returns the Python version used by this operation. + #[inline] + pub fn python_version(&self, db: &'db dyn Db) -> PythonVersion { + self.program(db).python_version(db) + } + + /// Returns the resolver environment used by this operation. + #[inline] + pub fn resolver_environment(&self, db: &'db dyn Db) -> ResolverEnvironment<'db> { + self.program(db).resolver_environment(db) + } +} + +#[derive(Clone, Copy)] +enum ProgramSource { + Program(Id), + // Salsa interned handles are thin `Id` wrappers, so converting between `ProgramFile` and `Id` + // is an inlined representation change with no database lookup. Keeping the lifetime-bearing + // `ProgramFile` out of the `Cell` preserves covariance in `'db`; replacing this variant after + // the first read avoids repeated Salsa ingredient reads in hot, recursive type operations. + File(Id), + Definition(Id), + Scope(Id), +} /// Context for inferring the types of a single file. /// @@ -39,8 +139,10 @@ use ty_python_core::semantic_index; /// on the current inference result. pub(crate) struct InferContext<'db, 'ast> { db: &'db dyn Db, + program_environment: &'ast ProgramEnvironment<'db>, scope: ScopeId<'db>, file: File, + program_file: ProgramFile<'db>, module: &'ast ParsedModuleRef, diagnostics: std::cell::RefCell, diagnostics_suppressed: bool, @@ -50,12 +152,25 @@ pub(crate) struct InferContext<'db, 'ast> { } impl<'db, 'ast> InferContext<'db, 'ast> { - pub(crate) fn new(db: &'db dyn Db, scope: ScopeId<'db>, module: &'ast ParsedModuleRef) -> Self { + pub(crate) fn new( + db: &'db dyn Db, + program_environment: &'ast ProgramEnvironment<'db>, + scope: ScopeId<'db>, + file: File, + program_file: ProgramFile<'db>, + module: &'ast ParsedModuleRef, + ) -> Self { + debug_assert_eq!(scope.program_file(db), program_file); + debug_assert_eq!(program_file.file(db), file); + debug_assert_eq!(program_environment.program(db), scope.program(db)); + Self { db, + program_environment, scope, module, - file: scope.file(db), + file, + program_file, diagnostics: std::cell::RefCell::new(TypeCheckDiagnostics::default()), diagnostics_suppressed: false, inference_flags: InferenceFlags::empty(), @@ -70,6 +185,19 @@ impl<'db, 'ast> InferContext<'db, 'ast> { self.file } + pub(crate) fn python_file(&self) -> PythonFile<'db> { + self.program_file.python_file(self.db()) + } + + pub(crate) fn program_file(&self) -> ProgramFile<'db> { + self.program_file + } + + #[inline] + pub(crate) fn program_environment(&self) -> &'ast ProgramEnvironment<'db> { + self.program_environment + } + /// The module for which the types are inferred. pub(crate) fn module(&self) -> &'ast ParsedModuleRef { self.module @@ -97,6 +225,7 @@ impl<'db, 'ast> InferContext<'db, 'ast> { Annotation::secondary(self.span(ranged)) } + #[inline] pub(crate) fn db(&self) -> &'db dyn Db { self.db } @@ -129,7 +258,7 @@ impl<'db, 'ast> InferContext<'db, 'ast> { /// The severity of the diagnostic returned is automatically determined /// by the given lint and configuration. The message given to /// `LintDiagnosticGuardBuilder::to_diagnostic` is used to construct the - /// initial diagnostic and should be considered the "top-level message" of + /// initial diagnostic and should be considered the "headline message" of /// the diagnostic. (i.e., If nothing else about the diagnostic is seen, /// aside from its identifier, the message is probably the thing you'd pick /// to show.) @@ -139,7 +268,7 @@ impl<'db, 'ast> InferContext<'db, 'ast> { /// typing context. (That means the range given _must_ be valid for the /// `File` currently being type checked.) This primary annotation does /// not have a message attached to it, but callers can attach one via - /// `LintDiagnosticGuard::set_primary_message`. + /// `LintDiagnosticGuard::set_primary_annotation_message`. /// /// After using the builder to make a guard, once the guard is dropped, the /// diagnostic is added to the context, unless there is something in the @@ -187,9 +316,9 @@ impl<'db, 'ast> InferContext<'db, 'ast> { // Accessing the semantic index here is fine because // the index belongs to the same file as for which we emit the diagnostic. - let index = semantic_index(self.db, self.file); + let index = semantic_index(self.db(), self.program_file); - let scope_id = self.scope.file_scope_id(self.db); + let scope_id = self.scope.file_scope_id(self.db()); // Inspect all ancestor function scopes by walking bottom up and check // if any is decorated with `@no_type_check`. We use the undecorated type @@ -200,12 +329,12 @@ impl<'db, 'ast> InferContext<'db, 'ast> { .ancestor_scopes(scope_id) .filter_map(|(_, scope)| scope.node().as_function()) .filter_map(|node| { - infer_definition_types(self.db, index.expect_single_definition(node)) + infer_definition_types(self.db(), index.expect_single_definition(node)) .undecorated_type() .and_then(Type::as_function_literal) }) .any(|function_ty| { - function_ty.has_known_decorator(self.db, FunctionDecorators::NO_TYPE_CHECK) + function_ty.has_known_decorator(self.db(), FunctionDecorators::NO_TYPE_CHECK) }) } @@ -214,9 +343,10 @@ impl<'db, 'ast> InferContext<'db, 'ast> { /// This checks both whether the scope itself is reachable and whether the /// specific statement or expression containing this range is reachable. fn is_range_reachable(&self, range: TextRange) -> bool { - let index = semantic_index(self.db, self.file); - let scope_id = self.scope.file_scope_id(self.db); - is_range_reachable(self.db, index, scope_id, range) + let db = self.db; + let index = semantic_index(self.db(), self.program_file); + let scope_id = self.scope.file_scope_id(self.db()); + is_range_reachable(db, index, scope_id, range) } /// Are we currently inferring types in a stub file? @@ -246,10 +376,14 @@ impl<'db, 'ast> InferContext<'db, 'ast> { impl fmt::Debug for InferContext<'_, '_> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("TyContext") + f.debug_struct("InferContext") + .field("db", &"") + .field("scope", &self.scope) .field("file", &self.file) + .field("program_file", &self.program_file) .field("diagnostics", &self.diagnostics) - .field("defused", &self.bomb) + .field("diagnostics_suppressed", &self.diagnostics_suppressed) + .field("inference_flags", &self.inference_flags) .finish() } } @@ -263,7 +397,7 @@ impl fmt::Debug for InferContext<'_, '_> { /// /// * On `Drop`, the underlying diagnostic is added to the typing context. /// * Some convenience methods for mutating the underlying `Diagnostic` -/// in lint context. For example, `LintDiagnosticGuard::set_primary_message` +/// in lint context. For example, `LintDiagnosticGuard::set_primary_annotation_message` /// will attach a message to the primary span on the diagnostic. pub(super) struct LintDiagnosticGuard<'db, 'ctx> { /// The typing context. @@ -274,6 +408,7 @@ pub(super) struct LintDiagnosticGuard<'db, 'ctx> { diag: Option, source: LintSource, + message_override: Option, } impl LintDiagnosticGuard<'_, '_> { @@ -289,7 +424,7 @@ impl LintDiagnosticGuard<'_, '_> { /// /// Callers can add additional primary or secondary annotations via the /// `DerefMut` trait implementation to a `Diagnostic`. - pub(super) fn set_primary_message(&mut self, message: impl IntoDiagnosticMessage) { + pub(super) fn set_primary_annotation_message(&mut self, message: impl IntoDiagnosticMessage) { // N.B. It is normally bad juju to define `self` methods // on types that implement `Deref`. Instead, it's idiomatic // to do `fn foo(this: &mut LintDiagnosticGuard)`, which in @@ -363,6 +498,22 @@ impl Drop for LintDiagnosticGuard<'_, '_> { // once. let mut diag = self.diag.take().unwrap(); + if let Some(message_override) = self.message_override.take() { + let primary_annotation_has_message = diag + .primary_annotation() + .and_then(Annotation::get_message) + .is_some_and(|message| !message.is_empty()); + let original_message = diag.headline_message().to_string(); + if primary_annotation_has_message { + diag.prepend_info(original_message); + } else if let Some(annotation) = diag.primary_annotation_mut() { + annotation.set_message(original_message); + } + + diag.set_headline_message(message_override); + diag.clear_concise_message(); + } + if self.ctx.db().verbose() { let rule = diag.id(); @@ -379,6 +530,9 @@ impl Drop for LintDiagnosticGuard<'_, '_> { LintSource::Editor => { format!("rule `{rule}` was selected in the editor settings") } + LintSource::UvWorkspace => { + format!("rule `{rule}` was selected by uv workspace metadata") + } }); } @@ -418,6 +572,7 @@ pub(super) struct LintDiagnosticGuardBuilder<'db, 'ctx> { severity: Severity, source: LintSource, primary_range: TextRange, + message_override: Option<(String, String)>, } impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> { @@ -440,12 +595,12 @@ impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> { // returns a rule selector for a given file that respects the package's settings, // any global pragma comments in the file, and any per-file-ignores. - if !ctx.db.should_check_file(ctx.file) { + if !ctx.db().should_check_file(ctx.file) { return None; } // Skip over diagnostics if the rule // is disabled. - let (severity, source) = ctx.db.rule_selection(ctx.file).get(lint)?; + let (severity, source) = ctx.db().rule_selection(ctx.file).get(lint)?; // If we're not in type checking mode, // we can bail now. if ctx.is_in_no_type_check() { @@ -476,7 +631,7 @@ impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> { let (severity, source) = Self::severity_and_source(ctx, lint_id)?; - let suppressions = suppressions(ctx.db(), ctx.file()); + let suppressions = suppressions(ctx.db(), ctx.python_file()); if let Some(suppression) = suppressions.find_suppression(range, lint_id) { ctx.diagnostics.borrow_mut().mark_used(suppression.id()); return None; @@ -495,6 +650,7 @@ impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> { severity, source, primary_range: range, + message_override: None, }) } @@ -504,29 +660,44 @@ impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> { /// the ID and severity derived from the `LintMetadata` used to create /// this builder. The diagnostic also includes a primary annotation /// without a message. To add a message to this primary annotation, use - /// `LintDiagnosticGuard::set_primary_message`. + /// `LintDiagnosticGuard::set_primary_annotation_message`. /// /// The diagnostic can be further mutated on the guard via its `DerefMut` /// impl to `Diagnostic`. + /// + /// If a message override is present, it is applied when the diagnostic is finalized. `message` + /// is retained on the primary annotation if the annotation has no message, or as an info + /// sub-diagnostic otherwise. Any custom concise message is discarded. pub(super) fn into_diagnostic( self, message: impl std::fmt::Display, ) -> LintDiagnosticGuard<'db, 'ctx> { - let mut diag = Diagnostic::new(DiagnosticId::Lint(self.id.name()), self.severity, message); - diag.set_documentation_url(Some(self.id.documentation_url())); - // This is why `LintDiagnosticGuard::set_primary_message` exists. - // We add the primary annotation here (because it's required), but - // the optional message can be added later. We could accept it here - // in this `build` method, but we already accept the main diagnostic - // message. So the messages are likely to be quite confusable. + // This is why `LintDiagnosticGuard::set_primary_annotation_message` exists. + // We add the primary annotation here (because it's required). Without a message + // override, its optional message can be added later via `set_primary_annotation_message`. let primary_span = Span::from(self.ctx.file()).with_range(self.primary_range); + let mut diag = Diagnostic::new(DiagnosticId::Lint(self.id.name()), self.severity, message); diag.annotate(Annotation::primary(primary_span)); + let message_override = self.message_override.map(|(message, info)| { + diag.info(info); + message + }); + diag.set_documentation_url(Some(self.id.documentation_url())); LintDiagnosticGuard { ctx: self.ctx, source: self.source, diag: Some(diag), + message_override, } } + + /// Replace the headline message when the diagnostic is finalized and add an info + /// sub-diagnostic. The original message is retained on the primary annotation if it has no + /// message, or as an info sub-diagnostic otherwise. + pub(super) fn with_message_override(mut self, message: String, info: &str) -> Self { + self.message_override = Some((message, info.to_string())); + self + } } /// A builder for constructing a diagnostic guard. @@ -553,7 +724,7 @@ impl<'db, 'ctx> DiagnosticGuardBuilder<'db, 'ctx> { return None; } - if !ctx.db.should_check_file(ctx.file) { + if !ctx.db().should_check_file(ctx.file) { return None; } Some(DiagnosticGuardBuilder { ctx, id, severity }) diff --git a/crates/ty_python_semantic/src/types/context_manager.rs b/crates/ty_python_semantic/src/types/context_manager.rs index cc4f07181a..c4b9071837 100644 --- a/crates/ty_python_semantic/src/types/context_manager.rs +++ b/crates/ty_python_semantic/src/types/context_manager.rs @@ -1,7 +1,9 @@ +use crate::Db; +use crate::ProgramEnvironment; use crate::{ - Db, FxOrderSet, + FxOrderSet, types::{ - CallArguments, CallDunderError, Type, TypeContext, call::CallErrorKind, + Bindings, CallArguments, CallDunderError, Type, TypeContext, call::CallErrorKind, context::InferContext, diagnostic::INVALID_CONTEXT_MANAGER, }, }; @@ -13,18 +15,18 @@ impl<'db> Type<'db> { /// /// This method should only be used outside of type checking because it omits any errors. /// For type checking, use [`try_enter_with_mode`](Self::try_enter_with_mode) instead. - pub(super) fn enter(self, db: &'db dyn Db) -> Type<'db> { - self.try_enter_with_mode(db, EvaluationMode::Sync) - .unwrap_or_else(|err| err.fallback_enter_type(db)) + pub(super) fn enter(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + self.try_enter_with_mode(db, env, EvaluationMode::Sync) + .unwrap_or_else(|err| err.fallback_enter_type(db, env)) } /// 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_with_mode`](Self::try_enter_with_mode) instead. - pub(super) fn aenter(self, db: &'db dyn Db) -> Type<'db> { - self.try_enter_with_mode(db, EvaluationMode::Async) - .unwrap_or_else(|err| err.fallback_enter_type(db)) + pub(super) fn aenter(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + self.try_enter_with_mode(db, env, EvaluationMode::Async) + .unwrap_or_else(|err| err.fallback_enter_type(db, env)) } /// Given the type of an object that is used as a context manager (i.e. in a `with` statement), @@ -38,6 +40,7 @@ impl<'db> Type<'db> { pub(super) fn try_enter_with_mode( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, mode: EvaluationMode, ) -> Result, ContextManagerError<'db>> { let (enter_method, exit_method) = match mode { @@ -47,34 +50,74 @@ impl<'db> Type<'db> { let enter = self.try_call_dunder( db, + env, enter_method, CallArguments::none(), TypeContext::default(), ); let exit = self.try_call_dunder( db, + env, exit_method, - CallArguments::positional([Type::none(db), Type::none(db), Type::none(db)]), + CallArguments::positional([ + Type::none(db, env), + Type::none(db, env), + Type::none(db, env), + ]), TypeContext::default(), ); + let awaited_enter_type = if mode.is_async() { + let return_type = |call: &Result, CallDunderError<'db>>| match call { + Ok(bindings) => Some(bindings.return_type(db, env)), + Err(CallDunderError::PossiblyUnbound { bindings, .. }) => { + Some(bindings.return_type(db, env)) + } + Err(CallDunderError::MethodNotAvailable | CallDunderError::CallError(..)) => None, + }; + + let enter_return_type = return_type(&enter); + let exit_return_type = return_type(&exit); + let awaited_enter_type = + enter_return_type.and_then(|return_type| return_type.try_await(db, env).ok()); + let awaited_exit_type = + exit_return_type.and_then(|return_type| return_type.try_await(db, env).ok()); + let non_awaitable_enter = enter_return_type.filter(|_| awaited_enter_type.is_none()); + let non_awaitable_exit = exit_return_type.filter(|_| awaited_exit_type.is_none()); + + if let Some(non_awaitable) = + NonAwaitableMethods::from_parts(non_awaitable_enter, non_awaitable_exit) + { + return Err(ContextManagerError::NotAwaitable { + enter_return_type: awaited_enter_type.unwrap_or(Type::unknown()), + non_awaitable, + enter_error: enter.err().map(Box::new), + exit_error: exit.err().map(Box::new), + }); + } + + awaited_enter_type + } else { + None + }; + // TODO: Make use of Protocols when we support it (the manager be assignable to `contextlib.AbstractContextManager`). match (enter, exit) { (Ok(enter), Ok(_)) => { - let ty = enter.return_type(db); + let return_type = enter.return_type(db, env); Ok(if mode.is_async() { - ty.try_await(db).unwrap_or(Type::unknown()) + awaited_enter_type.unwrap_or(Type::unknown()) } else { - ty + return_type }) } (Ok(enter), Err(exit_error)) => { - let ty = enter.return_type(db); + let return_type = enter.return_type(db, env); Err(ContextManagerError::Exit { enter_return_type: if mode.is_async() { - ty.try_await(db).unwrap_or(Type::unknown()) + awaited_enter_type.unwrap_or(Type::unknown()) } else { - ty + return_type }, exit_error, mode, @@ -105,31 +148,96 @@ pub(super) enum ContextManagerError<'db> { exit_error: CallDunderError<'db>, mode: EvaluationMode, }, + /// At least one async context-manager method returns a non-awaitable, possibly in addition to + /// a missing or invalid method. + NotAwaitable { + /// The type bound to the `as` target, already awaited when `__aenter__` allowed it. + enter_return_type: Type<'db>, + non_awaitable: NonAwaitableMethods<'db>, + enter_error: Option>>, + exit_error: Option>>, + }, +} + +/// Which of `__aenter__` and `__aexit__` returned a value that cannot be awaited, and what each +/// of them returned. +/// +/// At least one method must be at fault for the enclosing error to exist, which is why this is an +/// enum rather than a pair of `Option`s or a collection that could be empty. +#[derive(Debug)] +pub(super) enum NonAwaitableMethods<'db> { + Enter(Type<'db>), + Exit(Type<'db>), + Both { enter: Type<'db>, exit: Type<'db> }, +} + +impl<'db> NonAwaitableMethods<'db> { + /// Builds the error description from whichever methods are at fault, or `None` if both + /// returned awaitables and there is nothing to report. + fn from_parts(enter: Option>, exit: Option>) -> Option { + match (enter, exit) { + (Some(enter), Some(exit)) => Some(Self::Both { enter, exit }), + (Some(enter), None) => Some(Self::Enter(enter)), + (None, Some(exit)) => Some(Self::Exit(exit)), + (None, None) => None, + } + } + + /// The offending return types, paired with the name of the method that returned each one. + fn named_return_types( + &self, + enter_method: &'static str, + exit_method: &'static str, + ) -> Vec<(&'static str, Type<'db>)> { + match self { + Self::Enter(enter) => vec![(enter_method, *enter)], + Self::Exit(exit) => vec![(exit_method, *exit)], + Self::Both { enter, exit } => vec![(enter_method, *enter), (exit_method, *exit)], + } + } + + const fn is_both(&self) -> bool { + matches!(self, Self::Both { .. }) + } } impl<'db> ContextManagerError<'db> { - pub(super) fn fallback_enter_type(&self, db: &'db dyn Db) -> Type<'db> { - self.enter_type(db).unwrap_or(Type::unknown()) + pub(super) fn fallback_enter_type( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { + self.enter_type(db, env).unwrap_or(Type::unknown()) } /// Returns the `__enter__` or `__aenter__` return type if it is known, /// or `None` if the type never has a callable `__enter__` or `__aenter__` attribute - fn enter_type(&self, db: &'db dyn Db) -> Option> { + fn enter_type(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Option> { match self { Self::Exit { enter_return_type, exit_error: _, mode: _, + } + | Self::NotAwaitable { + enter_return_type, .. } => Some(*enter_return_type), - Self::Enter(enter_error, _) + Self::Enter(enter_error, mode) | Self::EnterAndExit { enter_error, exit_error: _, - mode: _, + mode, } => match enter_error { - CallDunderError::PossiblyUnbound { bindings, .. } => Some(bindings.return_type(db)), + CallDunderError::PossiblyUnbound { bindings, .. } => { + let return_type = bindings.return_type(db, env); + Some(if mode.is_async() { + return_type.try_await(db, env).unwrap_or(Type::unknown()) + } else { + return_type + }) + } CallDunderError::CallError(CallErrorKind::NotCallable, _, _) => None, - CallDunderError::CallError(_, bindings, _) => Some(bindings.return_type(db)), + CallDunderError::CallError(_, bindings, _) => Some(bindings.return_type(db, env)), CallDunderError::MethodNotAvailable => None, }, } @@ -150,6 +258,7 @@ impl<'db> ContextManagerError<'db> { _ => FxOrderSet::default(), } } + let db = context.db(); let Some(builder) = context.report_lint(&INVALID_CONTEXT_MANAGER, context_expression_node) else { @@ -160,6 +269,8 @@ impl<'db> ContextManagerError<'db> { Self::Exit { mode, .. } | Self::Enter(_, mode) | Self::EnterAndExit { mode, .. } => { *mode } + // `NotAwaitable` is only ever constructed for `async with`. + Self::NotAwaitable { .. } => EvaluationMode::Async, }; let (enter_method, exit_method) = match mode { @@ -204,7 +315,7 @@ impl<'db> ContextManagerError<'db> { } }; - let db = context.db(); + let env = context.program_environment(); let formatted_errors = match self { Self::Exit { @@ -218,6 +329,53 @@ impl<'db> ContextManagerError<'db> { exit_error, mode: _, } => format_call_dunder_errors(enter_error, enter_method, exit_error, exit_method), + Self::NotAwaitable { + non_awaitable, + enter_error, + exit_error, + .. + } => { + let methods = non_awaitable + .named_return_types(enter_method, exit_method) + .iter() + .map(|(name, _)| format!("`{name}`")) + .collect::>() + .join(" and "); + let await_error = if non_awaitable.is_both() { + format!("{methods} do not return awaitables") + } else { + format!("{methods} does not return an awaitable") + }; + + match (enter_error.as_deref(), exit_error.as_deref()) { + ( + Some(CallDunderError::PossiblyUnbound { .. }), + Some(CallDunderError::PossiblyUnbound { .. }), + ) if non_awaitable.is_both() => { + format!( + "`{enter_method}` and `{exit_method}` may be missing or return non-awaitables" + ) + } + (Some(enter_error), Some(exit_error)) => format!( + "{}, and {await_error}", + format_call_dunder_errors( + enter_error, + enter_method, + exit_error, + exit_method + ) + ), + (Some(enter_error), None) => format!( + "{}, and {await_error}", + format_call_dunder_error(enter_error, enter_method) + ), + (None, Some(exit_error)) => format!( + "{}, and {await_error}", + format_call_dunder_error(exit_error, exit_method) + ), + (None, None) => await_error, + } + } }; // Suggest using `async with` if only async methods are available in a sync context, @@ -229,7 +387,7 @@ impl<'db> ContextManagerError<'db> { let mut diag = builder.into_diagnostic(format_args!( "Object of type `{}` cannot be used with `{}` because {}", - context_expression_type.display(db), + context_expression_type.display(db, env), with_kw, formatted_errors, )); @@ -240,7 +398,7 @@ impl<'db> ContextManagerError<'db> { for ty in &exit_unbound_on { diag.info(format_args!( "`{}` does not implement `{exit_method}`", - ty.display(db) + ty.display(db, env) )); } } @@ -249,7 +407,7 @@ impl<'db> ContextManagerError<'db> { for ty in &enter_unbound_on { diag.info(format_args!( "`{}` does not implement `{enter_method}`", - ty.display(db) + ty.display(db, env) )); } } @@ -265,12 +423,48 @@ impl<'db> ContextManagerError<'db> { if exit_unbound_on.contains(ty) { diag.info(format_args!( "`{}` does not implement `{enter_method}` or `{exit_method}`", - ty.display(db) + ty.display(db, env) + )); + } else { + diag.info(format_args!( + "`{}` does not implement `{enter_method}`", + ty.display(db, env) + )); + } + } + + for ty in &exit_unbound_on { + if !enter_unbound_on.contains(ty) { + diag.info(format_args!( + "`{}` does not implement `{exit_method}`", + ty.display(db, env) + )); + } + } + } + Self::NotAwaitable { + non_awaitable, + enter_error, + exit_error, + .. + } => { + let enter_unbound_on = enter_error + .as_deref() + .map_or_else(FxOrderSet::default, unbound_on); + let exit_unbound_on = exit_error + .as_deref() + .map_or_else(FxOrderSet::default, unbound_on); + + for ty in &enter_unbound_on { + if exit_unbound_on.contains(ty) { + diag.info(format_args!( + "`{}` does not implement `{enter_method}` or `{exit_method}`", + ty.display(db, env) )); } else { diag.info(format_args!( "`{}` does not implement `{enter_method}`", - ty.display(db) + ty.display(db, env) )); } } @@ -279,13 +473,32 @@ impl<'db> ContextManagerError<'db> { if !enter_unbound_on.contains(ty) { diag.info(format_args!( "`{}` does not implement `{exit_method}`", - ty.display(db) + ty.display(db, env) )); } } + + for (method, return_type) in + non_awaitable.named_return_types(enter_method, exit_method) + { + diag.info(format_args!( + "`{method}` returns `{}`, which is not awaitable", + return_type.display(db, env) + )); + } + if non_awaitable.is_both() { + diag.info("Consider declaring the methods with `async def`"); + } else { + diag.info("Consider declaring the method with `async def`"); + } } } + // Do not suggest switching between `with` and `async with` for a non-awaitable return. + if matches!(self, Self::NotAwaitable { .. }) { + return; + } + let (alt_mode, alt_enter_method, alt_exit_method, alt_with_kw) = match mode { EvaluationMode::Sync => ("async", "__aenter__", "__aexit__", "async with"), EvaluationMode::Async => ("sync", "__enter__", "__exit__", "with"), @@ -293,12 +506,14 @@ impl<'db> ContextManagerError<'db> { let alt_enter = context_expression_type.try_call_dunder( db, + env, alt_enter_method, CallArguments::none(), TypeContext::default(), ); let alt_exit = context_expression_type.try_call_dunder( db, + env, alt_exit_method, CallArguments::positional([Type::unknown(), Type::unknown(), Type::unknown()]), TypeContext::default(), @@ -309,7 +524,7 @@ impl<'db> ContextManagerError<'db> { { diag.info(format_args!( "Objects of type `{}` can be used as {} context managers", - context_expression_type.display(db), + context_expression_type.display(db, env), alt_mode )); diag.info(format!("Consider using `{alt_with_kw}` here")); diff --git a/crates/ty_python_semantic/src/types/context_params.rs b/crates/ty_python_semantic/src/types/context_params.rs index 4c3b533efd..ce8feb1717 100644 --- a/crates/ty_python_semantic/src/types/context_params.rs +++ b/crates/ty_python_semantic/src/types/context_params.rs @@ -25,6 +25,7 @@ use ty_python_core::scope::{NodeWithScopeKind, ScopeId}; use ty_python_core::semantic_index; use crate::Db; +use crate::types::ProgramEnvironment; use crate::types::soundness::single_signature; use crate::types::{Type, binding_type}; @@ -56,13 +57,14 @@ struct Candidate<'db> { /// call at `call_offset` inside `scope` pub(crate) fn resolve_context_argument<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, scope: ScopeId<'db>, call_offset: TextSize, parameter_ty: Type<'db>, ) -> ContextResolution<'db> { let file = scope.file(db); - let index = semantic_index(db, file); - let module = parsed_module(db, file).load(db); + let index = semantic_index(db, db.program_file(file)); + let module = parsed_module(db, db.program_file(file).python_file(db)).load(db); for (file_scope_id, ancestor) in index.visible_ancestor_scopes(scope.file_scope_id(db)) { let is_call_scope = file_scope_id == scope.file_scope_id(db); @@ -93,7 +95,7 @@ pub(crate) fn resolve_context_argument<'db>( .into_iter() .filter_map(|candidate| { let ty = binding_type(db, candidate.definition); - ty.is_assignable_to(db, parameter_ty).then_some(( + ty.is_assignable_to(db, env, parameter_ty).then_some(( candidate.name, ty, candidate.definition, @@ -141,6 +143,7 @@ pub struct ImplicitContextArgument { /// a parameter is not knowable statically pub fn implicit_context_arguments<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, callee: Type<'db>, call: &ast::ExprCall, @@ -156,11 +159,11 @@ pub fn implicit_context_arguments<'db>( }; let parameters = signature.parameters(); - let index = semantic_index(db, file); + let index = semantic_index(db, db.program_file(file)); let Some(file_scope_id) = index.try_expression_scope_id(&ast::ExprRef::from(call)) else { return Vec::new(); }; - let scope = file_scope_id.to_scope_id(db, file); + let scope = file_scope_id.to_scope_id(db, db.program_file(file)); let positional_count = call.arguments.args.len(); let mut positional_index = 0; @@ -189,12 +192,20 @@ pub fn implicit_context_arguments<'db>( name: variable, definition, .. - } = resolve_context_argument(db, scope, call.range().start(), parameter.annotated_type()) - { + } = resolve_context_argument( + db, + env, + scope, + call.range().start(), + parameter.annotated_type(), + ) { implicit.push(ImplicitContextArgument { parameter: name.clone(), variable, - declaration: definition.focus_range(db, &parsed_module(db, file).load(db)), + declaration: definition.focus_range( + db, + &parsed_module(db, db.program_file(file).python_file(db)).load(db), + ), }); } } diff --git a/crates/ty_python_semantic/src/types/context_sensitive.rs b/crates/ty_python_semantic/src/types/context_sensitive.rs index a3184192b7..ddf2bf5579 100644 --- a/crates/ty_python_semantic/src/types/context_sensitive.rs +++ b/crates/ty_python_semantic/src/types/context_sensitive.rs @@ -32,6 +32,7 @@ use ty_python_core::{place_table, semantic_index}; use crate::Db; use crate::place::{ConsideredDefinitions, symbol}; +use crate::types::ProgramEnvironment; use crate::types::class::{ClassLiteral, ClassType, based_enum_of_variant}; use crate::types::class_base::ClassBase; use crate::types::literal::EnumLiteralType; @@ -59,11 +60,16 @@ enum Search<'db> { } /// the enum member `name` names anywhere in the expected type -fn search<'db>(db: &'db dyn Db, target: Type<'db>, name: &str) -> Search<'db> { +fn search<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + target: Type<'db>, + name: &str, +) -> Search<'db> { let mut found: Option> = None; let mut ambiguous = None; - let _ = for_each_candidate(db, target, &mut |candidate| { - let Some(member) = member_of(db, candidate, name) else { + let _ = for_each_candidate(db, env, target, &mut |candidate| { + let Some(member) = member_of(db, env, candidate, name) else { return ControlFlow::Continue(()); }; match found { @@ -91,6 +97,7 @@ fn search<'db>(db: &'db dyn Db, target: Type<'db>, name: &str) -> Search<'db> { /// context offers no unambiguous enum member of that name pub(crate) fn resolve_in_context<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, scope: ScopeId<'db>, tcx: TypeContext<'db>, @@ -102,10 +109,10 @@ pub(crate) fn resolve_in_context<'db>( // scope binds at all — so a name the scope binds anywhere keeps its ordinary // meaning here too, or `a: Color = Red` followed by `Red = 1` would check // clean and `NameError` at runtime - if claimed_by_lexical_scope(db, file, scope, name) { + if claimed_by_lexical_scope(db, env, file, scope, name) { return None; } - let Search::Found(member) = search(db, target, name) else { + let Search::Found(member) = search(db, env, target, name) else { return None; }; is_nameable(db, file, scope, member.enum_class).then_some(member) @@ -127,15 +134,16 @@ pub(crate) enum Miss<'db> { pub(crate) fn explain_miss<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, scope: ScopeId<'db>, tcx: TypeContext<'db>, name: &str, ) -> Option> { let target = tcx.context_sensitive_target()?; - match search(db, target, name) { + match search(db, env, target, name) { Search::Ambiguous(first, second) => Some(Miss::Ambiguous(first, second)), - Search::Found(member) if claimed_by_lexical_scope(db, file, scope, name) => { + Search::Found(member) if claimed_by_lexical_scope(db, env, file, scope, name) => { Some(Miss::Shadowed(member.enum_class)) } Search::Found(member) if !is_nameable(db, file, scope, member.enum_class) => { @@ -150,20 +158,23 @@ pub(crate) fn explain_miss<'db>( /// its variants, and an optional enum is a union with `None`) pub(crate) fn for_each_candidate<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, target: Type<'db>, visit: &mut impl FnMut(ClassType<'db>) -> ControlFlow<()>, ) -> ControlFlow<()> { match target { Type::Union(union) => { for element in union.elements(db) { - for_each_candidate(db, *element, visit)?; + for_each_candidate(db, env, *element, visit)?; } ControlFlow::Continue(()) } - Type::NominalInstance(instance) => visit(instance.class(db)), - Type::TypeAlias(alias) => for_each_candidate(db, alias.value_type(db), visit), + Type::NominalInstance(instance) => visit(instance.class(db, env)), + Type::TypeAlias(alias) => for_each_candidate(db, env, alias.value_type(db), visit), _ => match target.as_enum_literal() { - Some(literal) => for_each_candidate(db, literal.enum_class_instance(db), visit), + Some(literal) => { + for_each_candidate(db, env, literal.enum_class_instance(db, env), visit) + } None => ControlFlow::Continue(()), }, } @@ -173,6 +184,7 @@ pub(crate) fn for_each_candidate<'db>( /// variant reaches its enum's other variants through the enum base it subclasses fn member_of<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: ClassType<'db>, name: &str, ) -> Option> { @@ -189,7 +201,7 @@ fn member_of<'db>( } // a based enum's payload variant, whose class is declared in the enum body if let Some(ty) = base - .own_class_member(db, None, name) + .own_class_member(db, env, None, name) .ignore_possibly_undefined() && is_variant_class(db, ty) { @@ -219,9 +231,9 @@ fn is_nameable<'db>( enum_class: ClassLiteral<'db>, ) -> bool { let name = enum_class.name(db); - let index = semantic_index(db, file); + let index = semantic_index(db, db.program_file(file)); for (ancestor_id, _) in index.visible_ancestor_scopes(scope.file_scope_id(db)) { - let ancestor_scope = ancestor_id.to_scope_id(db, file); + let ancestor_scope = ancestor_id.to_scope_id(db, db.program_file(file)); let Some(place) = place_table(db, ancestor_scope).symbol_by_name(name) else { continue; }; @@ -253,6 +265,7 @@ fn is_nameable<'db>( /// its type belongs to, and can name that enum here, lowers to `.` pub(crate) fn qualifier_for_unbound_name<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, scope: ScopeId<'db>, name: &str, @@ -262,19 +275,19 @@ pub(crate) fn qualifier_for_unbound_name<'db>( // a builtin, keeps its ordinary spelling. checked before the name's type is // asked for, so a file that uses no context-sensitive name is never inferred // on its account - if claimed_by_lexical_scope(db, file, scope, name) { + if claimed_by_lexical_scope(db, env, file, scope, name) { return None; } // a trailing lambda block's receiver member answers *before* this fallback // does, so a receiver whose member happens to be an enum value keeps the // receiver-parameter lowering the checker resolved it to - if receivers::implicit_receiver_name(db, file, scope, name).is_some() { + if receivers::implicit_receiver_name(db, env, file, scope, name).is_some() { return None; } let enum_class = enum_class_of(db, resolved_type()?)?; // the spelling must be the enum's own member, not merely a value of that // enum's type reached under some other name - if !declares_member(db, enum_class, name) { + if !declares_member(db, env, enum_class, name) { return None; } is_nameable(db, file, scope, enum_class).then(|| enum_class.name(db)) @@ -291,7 +304,12 @@ fn enum_class_of<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option(db: &'db dyn Db, enum_class: ClassLiteral<'db>, name: &str) -> bool { +fn declares_member<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + enum_class: ClassLiteral<'db>, + name: &str, +) -> bool { if enum_class .into_enum_class(db) .is_some_and(|enum_class| enum_class.resolve_member(db, &Name::new(name)).is_some()) @@ -300,7 +318,7 @@ fn declares_member<'db>(db: &'db dyn Db, enum_class: ClassLiteral<'db>, name: &s } enum_class .default_specialization(db) - .own_class_member(db, None, name) + .own_class_member(db, env, None, name) .ignore_possibly_undefined() .is_some_and(|ty| is_variant_class(db, ty)) } diff --git a/crates/ty_python_semantic/src/types/conversions.rs b/crates/ty_python_semantic/src/types/conversions.rs index d14818deeb..b163f6a258 100644 --- a/crates/ty_python_semantic/src/types/conversions.rs +++ b/crates/ty_python_semantic/src/types/conversions.rs @@ -31,6 +31,7 @@ use ty_python_core::semantic_index; use crate::Db; use crate::place::builtins_symbol; +use crate::types::ProgramEnvironment; use crate::types::call::CallArguments; use crate::types::class::{ClassLiteral, ClassType, KnownClass, StaticClassLiteral}; use crate::types::context::InferContext; @@ -39,6 +40,7 @@ use crate::types::extensions::{self, ExtensionMemberKind, ExtensionMemberResolut use crate::types::function::FunctionType; use crate::types::signatures::Parameters; use crate::types::{MemberLookupPolicy, Type, TypeContext}; +use ty_module_resolver::ImportingFile; /// the classmethod on a target that converts a value of some other type pub(crate) const FROM: &str = "__from__"; @@ -83,7 +85,7 @@ pub(crate) enum Route<'db> { impl<'db> Route<'db> { /// how the route reads in a diagnostic - fn describe(self, db: &'db dyn Db) -> String { + fn describe(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> String { let dunder = |class: ClassType<'db>, source: DunderSource<'db>, dunder: &str| match source { DunderSource::Declared => format!("{}.{dunder}", class.name(db)), // two extensions supplying the same dunder read alike without this, @@ -98,7 +100,7 @@ impl<'db> Route<'db> { Route::Conformance(protocol) => format!("conformance to `{}`", protocol.name(db)), Route::From(class, source) => dunder(class, source, FROM), Route::Of(class, source) => dunder(class, source, OF), - Route::Into(source) => format!("{}.{INTO}", source.display(db)), + Route::Into(source) => format!("{}.{INTO}", source.display(db, env)), } } } @@ -124,6 +126,7 @@ pub(crate) struct ConversionRepair<'db> { /// generic can ask, which is why `list[Celsius]` is not a `list[Fahrenheit]`. pub(crate) fn repair_conversion<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, source: Type<'db>, target: Type<'db>, @@ -134,7 +137,7 @@ pub(crate) fn repair_conversion<'db>( } // a conversion only ever *adds* an assignment that fails without it, so no // code that checks today changes meaning - if source.is_assignable_to(db, target) { + if source.is_assignable_to(db, env, target) { return None; } // the value being converted is an ordinary value of the type it was @@ -142,10 +145,12 @@ pub(crate) fn repair_conversion<'db>( let source = source.erase_restriction(db); let mut routes: Vec> = Vec::new(); - if let Some(protocol) = super::conformance::repair_with_conformance(db, file, source, target) { + if let Some(protocol) = + super::conformance::repair_with_conformance(db, env, file, source, target) + { routes.push(Route::Conformance(protocol)); } - dunder_routes(db, file, source, target, value, &mut routes); + dunder_routes(db, env, file, source, target, value, &mut routes); let mut routes = routes.into_iter(); let route = routes.next()?; @@ -159,6 +164,7 @@ pub(crate) fn repair_conversion<'db>( /// order so that an ambiguity reads the same way whichever site asks fn dunder_routes<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, source: Type<'db>, target: Type<'db>, @@ -168,7 +174,7 @@ fn dunder_routes<'db>( let literal = value.is_some_and(is_literal_expression); for arm in union_arms(db, target) { - let Some(class) = arm.nominal_class(db) else { + let Some(class) = arm.nominal_class(db, env) else { continue; }; for dunder in [FROM, OF] { @@ -180,7 +186,7 @@ fn dunder_routes<'db>( let sources = std::iter::once(source).chain( value .filter(|_| dunder == OF) - .and_then(|value| empty_display_type(db, value)), + .and_then(|value| empty_display_type(db, env, value)), ); let route = |dunder_source| { if dunder == FROM { @@ -194,9 +200,16 @@ fn dunder_routes<'db>( // unless the member really is a classmethod. resolving the route the // same way the declaration is validated keeps a malformed dunder // from converting anything - if conversion_classmethod(db, class, dunder).is_some() { + if conversion_classmethod(db, env, class, dunder).is_some() { if sources.clone().any(|source| { - converts(db, arm, dunder, CallArguments::positional([source]), target) + converts( + db, + env, + arm, + dunder, + CallArguments::positional([source]), + target, + ) }) { routes.push(route(DunderSource::Declared)); } @@ -205,10 +218,10 @@ fn dunder_routes<'db>( // a type that declares no conversion of its own may still be given // one from outside. `try_call_dunder` cannot see an extension // member, so it is resolved and called directly - for member in extension_classmethods(db, file, class, dunder) { + for member in extension_classmethods(db, env, file, class, dunder) { if sources .clone() - .any(|source| calls_to(db, member.ty, source, target)) + .any(|source| calls_to(db, env, member.ty, source, target)) { routes.push(route(DunderSource::Extension(member.extension))); } @@ -216,7 +229,8 @@ fn dunder_routes<'db>( } } - if source_declares_into(db, source) && converts(db, source, INTO, CallArguments::none(), target) + if source_declares_into(db, env, source) + && converts(db, env, source, INTO, CallArguments::none(), target) { routes.push(Route::Into(source)); } @@ -227,23 +241,38 @@ fn dunder_routes<'db>( /// and descriptor binding all come from it rather than being re-derived here fn converts<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, receiver: Type<'db>, dunder: &str, arguments: CallArguments<'_, 'db>, target: Type<'db>, ) -> bool { receiver - .try_call_dunder(db, dunder, arguments, TypeContext::default()) - .is_ok_and(|bindings| bindings.return_type(db).is_assignable_to(db, target)) + .try_call_dunder(db, env, dunder, arguments, TypeContext::default()) + .is_ok_and(|bindings| { + bindings + .return_type(db, env) + .is_assignable_to(db, env, target) + }) } /// the same question for an already-bound member: does calling it with `source` /// produce something the target accepts? An extension member does not live on /// the receiver's meta-type, so it is resolved first and called here -fn calls_to<'db>(db: &'db dyn Db, member: Type<'db>, source: Type<'db>, target: Type<'db>) -> bool { +fn calls_to<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + member: Type<'db>, + source: Type<'db>, + target: Type<'db>, +) -> bool { member - .try_call(db, &CallArguments::positional([source])) - .is_ok_and(|bindings| bindings.return_type(db).is_assignable_to(db, target)) + .try_call(db, env, &CallArguments::positional([source])) + .is_ok_and(|bindings| { + bindings + .return_type(db, env) + .is_assignable_to(db, env, target) + }) } /// the `__from__` / `__of__` an `extension` supplies for `class`, bound to the @@ -253,11 +282,12 @@ fn calls_to<'db>(db: &'db dyn Db, member: Type<'db>, source: Type<'db>, target: /// the site reports the ambiguity rather than silently picking the first fn extension_classmethods<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, class: ClassType<'db>, dunder: &str, ) -> Vec> { - extensions::resolve_extension_members(db, file, Type::from(class), dunder) + extensions::resolve_extension_members(db, env, file, Type::from(class), dunder) .into_iter() .filter(|resolution| resolution.kind == ExtensionMemberKind::ClassMethod) .collect() @@ -272,13 +302,17 @@ fn extension_classmethods<'db>( /// display and nothing else — and unlike ordinary inference it has the syntax in /// hand. Offered *beside* the widened type rather than replacing it, so a dunder /// that accepts `dict[str, int]` still takes `{}` the way it always has -fn empty_display_type<'db>(db: &'db dyn Db, value: &ast::Expr) -> Option> { +fn empty_display_type<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + value: &ast::Expr, +) -> Option> { match value { ast::Expr::Dict(_) if is_empty_display(value) => { - Some(KnownClass::Dict.to_specialized_instance(db, &[Type::Never, Type::Never])) + Some(KnownClass::Dict.to_specialized_instance(db, env, &[Type::Never, Type::Never])) } ast::Expr::List(_) if is_empty_display(value) => { - Some(KnownClass::List.to_specialized_instance(db, &[Type::Never])) + Some(KnownClass::List.to_specialized_instance(db, env, &[Type::Never])) } _ => None, } @@ -312,12 +346,16 @@ fn union_arms<'db>(db: &'db dyn Db, ty: Type<'db>) -> Vec> { /// The lowered `x.__into__()` runs against whichever arm the value actually is, /// so one arm without it would be an `AttributeError` at runtime. Requiring all /// of them is what lets a union source convert at all -fn source_declares_into<'db>(db: &'db dyn Db, source: Type<'db>) -> bool { +fn source_declares_into<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + source: Type<'db>, +) -> bool { let arms = union_arms(db, source); !arms.is_empty() && arms.iter().all(|arm| { - arm.nominal_class(db) - .is_some_and(|class| conversion_method(db, class).is_some()) + arm.nominal_class(db, env) + .is_some_and(|class| conversion_method(db, env, class).is_some()) }) } @@ -325,11 +363,12 @@ fn source_declares_into<'db>(db: &'db dyn Db, source: Type<'db>) -> bool { /// lowered call needs pub(crate) fn conversion_classmethod<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: ClassType<'db>, dunder: &str, ) -> Option> { match class - .class_member(db, dunder, MemberLookupPolicy::default()) + .class_member(db, env, dunder, MemberLookupPolicy::default()) .place .ignore_possibly_undefined()? { @@ -343,10 +382,11 @@ pub(crate) fn conversion_classmethod<'db>( /// no target, so there would be nothing to dispatch on at runtime pub(crate) fn conversion_method<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: ClassType<'db>, ) -> Option> { match class - .class_member(db, INTO, MemberLookupPolicy::default()) + .class_member(db, env, INTO, MemberLookupPolicy::default()) .place .ignore_possibly_undefined()? { @@ -369,10 +409,11 @@ pub(crate) fn conversion_method<'db>( /// binding the gate exists to avoid #[salsa::tracked(heap_size = ruff_memory_usage::heap_size)] fn class_declares_conversion<'db>(db: &'db dyn Db, class: StaticClassLiteral<'db>) -> bool { + let env = &ProgramEnvironment::from_file(class.program_file(db)); let class = class.identity_specialization(db); CONVERSION_DUNDERS.iter().any(|dunder| { !class - .class_member(db, dunder, MemberLookupPolicy::default()) + .class_member(db, env, dunder, MemberLookupPolicy::default()) .place .is_undefined() }) @@ -381,9 +422,17 @@ fn class_declares_conversion<'db>(db: &'db dyn Db, class: StaticClassLiteral<'db /// might `ty` be one end of a conversion? the call gate's question, deliberately /// over-approximate in both directions: a `true` only costs the full check that /// would have run anyway, and anything this cannot classify answers `true` -pub(crate) fn may_convert<'db>(db: &'db dyn Db, file: File, ty: Type<'db>) -> bool { +pub(crate) fn may_convert<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + file: File, + ty: Type<'db>, +) -> bool { union_arms(db, ty).iter().any(|arm| { - match arm.nominal_class(db).map(|class| class.class_literal(db)) { + match arm + .nominal_class(db, env) + .map(|class| class.class_literal(db)) + { Some(ClassLiteral::Static(literal)) => { *class_declares_conversion(db, literal) || extensions::extension_converts_class(db, file, literal) @@ -469,13 +518,23 @@ struct ImportSpelling<'a> { impl ImportSpelling<'_> { fn resolves(&self, name: &ModuleName) -> bool { - resolve_module(self.db, self.from_file, name).and_then(|module| module.file(self.db)) + let db = self.db; + resolve_module( + self.db, + ImportingFile::File( + self.from_file, + db.program_file(self.from_file).resolver_environment(db), + ), + name, + ) + .and_then(|module| module.file(self.db)) == Some(self.target) } } impl<'ast> ast::visitor::Visitor<'ast> for ImportSpelling<'_> { fn visit_stmt(&mut self, stmt: &'ast ast::Stmt) { + let db = self.db; if self.found.is_some() { return; } @@ -491,8 +550,14 @@ impl<'ast> ast::visitor::Visitor<'ast> for ImportSpelling<'_> { } } ast::Stmt::ImportFrom(import) => { - if let Ok(name) = ModuleName::from_import_statement(self.db, self.from_file, import) - && self.resolves(&name) + if let Ok(name) = ModuleName::from_import_statement( + self.db, + ImportingFile::File( + self.from_file, + db.program_file(self.from_file).resolver_environment(db), + ), + import, + ) && self.resolves(&name) { // keep the leading dots: a relative import is how this file // addresses the module, and the absolute name may not resolve @@ -522,7 +587,8 @@ pub(crate) fn imported_module_spelling( from_file: File, target: File, ) -> Option { - let module = ruff_db::parsed::parsed_module(db, from_file).load(db); + let module = + ruff_db::parsed::parsed_module(db, db.program_file(from_file).python_file(db)).load(db); let mut spelling = ImportSpelling { db, from_file, @@ -555,7 +621,16 @@ pub(crate) fn from_imported_modules(db: &dyn Db, file: File) -> Box<[ModuleName] impl<'ast> ast::visitor::Visitor<'ast> for Collector<'_> { fn visit_stmt(&mut self, stmt: &'ast ast::Stmt) { if let ast::Stmt::ImportFrom(import) = stmt - && let Ok(name) = ModuleName::from_import_statement(self.db, self.file, import) + && let Ok(name) = ModuleName::from_import_statement( + self.db, + ImportingFile::File( + self.file, + self.db + .program_file(self.file) + .resolver_environment(self.db), + ), + import, + ) && !self.modules.contains(&name) { self.modules.push(name); @@ -564,7 +639,7 @@ pub(crate) fn from_imported_modules(db: &dyn Db, file: File) -> Box<[ModuleName] } } - let module = ruff_db::parsed::parsed_module(db, file).load(db); + let module = ruff_db::parsed::parsed_module(db, db.program_file(file).python_file(db)).load(db); let mut collector = Collector { db, file, @@ -589,7 +664,7 @@ pub(crate) fn function_declared_return_type<'db>( file: File, function: &ast::StmtFunctionDef, ) -> Option> { - let index = semantic_index(db, file); + let index = semantic_index(db, db.program_file(file)); let definition = index.expect_single_definition(function); let Type::FunctionLiteral(literal) = crate::types::binding_type(db, definition) else { return None; @@ -636,14 +711,15 @@ pub(crate) fn addressable_elements(value: &ast::Expr) -> Option> /// type, else what iterating the declared type yields pub(crate) fn declared_element_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, declared: Type<'db>, ) -> Option> { // a mapping is keyed, and only its values sit at the literal's element // positions (`{"k": b}`); iterating one would give the *key* type - if let Some((_, value)) = declared.unpack_keys_and_items(db) { + if let Some((_, value)) = declared.unpack_keys_and_items(db, env) { return Some(value); } - let element = declared.iterate(db).homogeneous_element_type(db); + let element = declared.iterate(db, env).homogeneous_element_type(db, env); (!element.is_unknown() && !element.is_never()).then_some(element) } @@ -659,6 +735,7 @@ pub(crate) fn call_parameter_types<'db>( call: &ast::ExprCall, ) -> Option>>> { use crate::types::constraints::ConstraintSetBuilder; + let env = &model.program_environment(); let db = model.db(); let arguments = CallArguments::from_arguments_typed(&call.arguments, |splatted_value| { @@ -669,10 +746,16 @@ pub(crate) fn call_parameter_types<'db>( // suppresses its diagnostic rather than making the argument assignable — so // the parameter types have to be read out of either outcome let bindings = match callable_ty - .bindings(db) - .match_parameters(db, &arguments) - .check_types(db, &constraints, &arguments, TypeContext::default(), &[]) - { + .bindings(db, env) + .match_parameters(db, env, &arguments) + .check_types( + db, + env, + &constraints, + &arguments, + TypeContext::default(), + &[], + ) { Ok(bindings) => bindings, Err(error) => *error.into_bindings(), }; @@ -687,6 +770,7 @@ pub(crate) fn report_ambiguous_conversion<'db>( node: impl Ranged, repair: &ConversionRepair<'db>, ) { + let env = context.program_environment(); let db = context.db(); if repair.ambiguous_with.is_empty() { return; @@ -697,7 +781,7 @@ pub(crate) fn report_ambiguous_conversion<'db>( let mut diagnostic = builder.into_diagnostic("More than one conversion applies here"); let names: Vec = std::iter::once(repair.route) .chain(repair.ambiguous_with.iter().copied()) - .map(|route| format!("`{}`", route.describe(db))) + .map(|route| format!("`{}`", route.describe(db, env))) .collect(); diagnostic.info(format_args!("{} all convert this value", names.join(", "))); diagnostic.help("Remove all but one of them, or write the conversion you want explicitly"); @@ -718,6 +802,7 @@ pub(crate) fn report_ambiguous_conversion<'db>( /// depends on ordering pub(crate) fn value_conversions<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, model: &crate::SemanticModel<'db>, value: &ast::Expr, @@ -726,10 +811,10 @@ pub(crate) fn value_conversions<'db>( let Some(value_ty) = crate::HasType::inferred_type(value, model) else { return Vec::new(); }; - if let Some(repair) = repair_conversion(db, file, value_ty, declared, Some(value)) { + if let Some(repair) = repair_conversion(db, env, file, value_ty, declared, Some(value)) { return vec![(value.range(), repair)]; } - element_conversions(db, file, model, value, declared) + element_conversions(db, env, file, model, value, declared) } /// the per-element conversions a collection literal needs to satisfy `declared`. @@ -738,6 +823,7 @@ pub(crate) fn value_conversions<'db>( /// would leave the value unassignable, and the ordinary error is the right report fn element_conversions<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, model: &crate::SemanticModel<'db>, value: &ast::Expr, @@ -746,10 +832,10 @@ fn element_conversions<'db>( let Some(elements) = addressable_elements(value) else { return Vec::new(); }; - let Some(element_target) = declared_element_type(db, declared) else { + let Some(element_target) = declared_element_type(db, env, declared) else { return Vec::new(); }; - if !display_kind_fits(db, model, value, declared) { + if !display_kind_fits(db, env, model, value, declared) { return Vec::new(); } let mut conversions = Vec::new(); @@ -757,10 +843,10 @@ fn element_conversions<'db>( let Some(element_ty) = crate::HasType::inferred_type(element, model) else { return Vec::new(); }; - if element_ty.is_assignable_to(db, element_target) { + if element_ty.is_assignable_to(db, env, element_target) { continue; } - match repair_conversion(db, file, element_ty, element_target, Some(element)) { + match repair_conversion(db, env, file, element_ty, element_target, Some(element)) { Some(repair) => conversions.push((element.range(), repair)), // one element that neither fits nor converts sinks the whole value None => return Vec::new(), @@ -780,13 +866,15 @@ fn element_conversions<'db>( /// `Unknown` because only the *kind* is in question here fn display_kind_fits<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, model: &crate::SemanticModel<'db>, value: &ast::Expr, declared: Type<'db>, ) -> bool { let erased = |ty: Type<'db>| { - ty.nominal_class(db) - .map(|class| Type::instance(db, class.class_literal(db).unknown_specialization(db))) + ty.nominal_class(db, env).map(|class| { + Type::instance(db, env, class.class_literal(db).unknown_specialization(db)) + }) }; let Some(value_ty) = crate::HasType::inferred_type(value, model).and_then(erased) else { return true; @@ -796,7 +884,7 @@ fn display_kind_fits<'db>( let Some(declared) = erased(declared) else { return true; }; - value_ty.is_assignable_to(db, declared) + value_ty.is_assignable_to(db, env, declared) } /// the sub-expression of `value` covering exactly `range`. @@ -856,6 +944,7 @@ pub enum ConversionInfo { /// wrapped, which decides what the emitted names have to resolve to pub(crate) fn conversion_info<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, from_file: File, model: &crate::SemanticModel<'db>, anchor: &ast::Expr, @@ -880,10 +969,10 @@ pub(crate) fn conversion_info<'db>( imports: Vec::new(), }, Route::From(class, source) => { - dunder_call_info(db, from_file, model, anchor, class, FROM, source) + dunder_call_info(db, env, from_file, model, anchor, class, FROM, source) } Route::Of(class, source) => { - dunder_call_info(db, from_file, model, anchor, class, OF, source) + dunder_call_info(db, env, from_file, model, anchor, class, OF, source) } // the receiver is the value itself, so nothing has to be named or // imported. the parentheses are what make it safe to wrap an operand of @@ -905,8 +994,10 @@ pub(crate) fn conversion_info<'db>( /// whatever that extension lowers to: the target's own constructor for a prelude /// declaration (`{1}` in a `frozenset[int]` context is `frozenset({1})`), and /// the backing function for one a module declares +#[expect(clippy::too_many_arguments)] fn dunder_call_info<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, from_file: File, model: &crate::SemanticModel<'db>, anchor: &ast::Expr, @@ -917,7 +1008,7 @@ fn dunder_call_info<'db>( if let DunderSource::Extension(extension) = source && !extensions::is_prelude_extension(db, from_file, extension) { - return backing_call_info(db, from_file, model, anchor, class, extension, dunder); + return backing_call_info(db, env, from_file, model, anchor, class, extension, dunder); } // a prelude conversion means construction, so the emitted call is the class // itself — spelled, and shadow-checked, exactly as the dunder call would be @@ -934,7 +1025,7 @@ fn dunder_call_info<'db>( // because construction *is* the conversion here — a real `T.__of__(x)` needs // its argument, and an empty display holds nothing another pass could edit let replaces_value = constructs && is_empty_display(anchor); - match class_reference(db, from_file, model, anchor, class) { + match class_reference(db, env, from_file, model, anchor, class) { Ok((name, import)) => ConversionInfo::Call { prefix: spelling(&name), suffix: ")".to_owned(), @@ -954,8 +1045,10 @@ fn dunder_call_info<'db>( /// read off the class. The class is what the ordering check watches, because a /// `class` statement binds its name late while the backing function is hoisted /// above the module +#[expect(clippy::too_many_arguments)] fn backing_call_info<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, from_file: File, model: &crate::SemanticModel<'db>, anchor: &ast::Expr, @@ -980,7 +1073,7 @@ fn backing_call_info<'db>( alias: function.clone(), }); } - let (receiver, class_import) = match class_reference(db, from_file, model, anchor, class) { + let (receiver, class_import) = match class_reference(db, env, from_file, model, anchor, class) { Ok(spelling) => spelling, Err(reason) => return ConversionInfo::Rejected(reason), }; @@ -1016,6 +1109,7 @@ fn conversion_alias(name: &str) -> String { /// an aliased import. `Err` when neither is possible pub(crate) fn class_reference<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, from_file: File, model: &crate::SemanticModel<'db>, anchor: &ast::Expr, @@ -1027,7 +1121,7 @@ pub(crate) fn class_reference<'db>( let name = literal.name(db).to_string(); // a builtin needs no import — the name is already there — but it can still // be shadowed by a local, which would send the emitted call elsewhere - if literal.file(db) != from_file && is_builtin_class(db, literal) { + if literal.file(db) != from_file && is_builtin_class(db, env, literal) { return if name_is_shadowed_at(db, from_file, model, anchor, &name) { Err(format!( "the conversion this value needs goes through the builtin `{name}`, which is \ @@ -1073,8 +1167,12 @@ pub(crate) fn class_reference<'db>( /// rather than importing it under an alias. Asked by identity, not by module /// path: a class that merely *lives* in `builtins` but is shadowed there by /// something else is not what the bare name would reach -fn is_builtin_class<'db>(db: &'db dyn Db, class: StaticClassLiteral<'db>) -> bool { - builtins_symbol(db, class.name(db)) +fn is_builtin_class<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + class: StaticClassLiteral<'db>, +) -> bool { + builtins_symbol(db, env, class.name(db)) .place .ignore_possibly_undefined() .and_then(Type::as_class_literal) @@ -1097,7 +1195,7 @@ fn name_is_shadowed_at<'db>( let Some(scope) = model.scope(ast::AnyNodeRef::from(anchor)) else { return false; }; - let index = semantic_index(db, file); + let index = semantic_index(db, db.program_file(file)); index.ancestor_scopes(scope).any(|(id, _)| { // a scope's place table holds every name the scope *mentions*, so // merely naming the class — which the annotation of the very assignment @@ -1120,6 +1218,7 @@ pub(crate) fn validate_conversion_dunders<'db>( class: StaticClassLiteral<'db>, class_node: &ast::StmtClassDef, ) { + let env = context.program_environment(); let db = context.db(); // only basedpython has conversions. in a `.py` file `__from__` and friends // are ordinary method names that mean nothing to anyone, and inventing an @@ -1144,7 +1243,7 @@ pub(crate) fn validate_conversion_dunders<'db>( } reported.push(name); let member = class_type - .class_member(db, name, MemberLookupPolicy::default()) + .class_member(db, env, name, MemberLookupPolicy::default()) .place .ignore_possibly_undefined(); let Some(Type::FunctionLiteral(function)) = member else { @@ -1183,6 +1282,7 @@ fn validate_from_or_of<'db>( function: FunctionType<'db>, function_node: &ast::StmtFunctionDef, ) { + let env = context.program_environment(); let db = context.db(); let name = function_node.name.as_str(); if !function.is_classmethod(db) { @@ -1197,7 +1297,7 @@ fn validate_from_or_of<'db>( } return; } - let instance = Type::instance(db, class); + let instance = Type::instance(db, env, class); for signature in function.signature(db) { let (required, takes_positional) = arity_after_receiver(signature.parameters()); if required > 1 || !takes_positional { @@ -1213,13 +1313,13 @@ fn validate_from_or_of<'db>( } return; } - if !signature.return_ty.is_assignable_to(db, instance) { + if !signature.return_ty.is_assignable_to(db, env, instance) { if let Some(builder) = context.report_lint(&INVALID_CONVERSION, &function_node.name) { let mut diagnostic = builder .into_diagnostic(format_args!("`{name}` must return `{}`", class.name(db))); diagnostic.info(format_args!( "it returns `{}`, so no conversion site would ever accept it", - signature.return_ty.display(db), + signature.return_ty.display(db, env), )); } return; diff --git a/crates/ty_python_semantic/src/types/cyclic.rs b/crates/ty_python_semantic/src/types/cyclic.rs index f4dc8343d0..38a8a066d9 100644 --- a/crates/ty_python_semantic/src/types/cyclic.rs +++ b/crates/ty_python_semantic/src/types/cyclic.rs @@ -31,11 +31,11 @@ use rustc_hash::{FxHashMap, FxHashSet}; use smallvec::SmallVec; use ty_python_core::definition::Definition; -use crate::Db; use crate::types::function::FunctionLiteral; use crate::types::generics::Specialization; use crate::types::visitor::{TypeCollector, TypeVisitor, walk_type_with_recursion_guard}; use crate::types::{ClassType, ProtocolInstanceType, Type, TypeAliasType, TypedDictType}; +use crate::{Db, ProgramEnvironment}; /// The type identity used for recursive checks/transformations. #[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)] @@ -78,7 +78,7 @@ impl<'db> Type<'db> { #[allow(clippy::inline_always)] #[inline(always)] - pub(crate) fn recursive_identity(self, db: &'db dyn Db) -> Option> { + fn recursive_identity(self, db: &'db dyn Db) -> Option> { match self { // We can create a self-referential function type: e.g. `def f(x: "TypeOf[f]"): reveal_type(x)` // To avoid the difficulty of equality checking for function types containing this, we simply use `literal` for equality checking. @@ -106,6 +106,7 @@ impl<'db> Type<'db> { } struct DefinitionReferenceVisitor<'db> { + env: ProgramEnvironment<'db>, target: Definition<'db>, active_definitions: ActiveRecursionDetector>, visited_types: TypeCollector<'db>, @@ -122,6 +123,7 @@ impl<'db> DefinitionReferenceVisitor<'db> { fn new(target: Definition<'db>) -> Self { Self { + env: ProgramEnvironment::from_definition(target), target, active_definitions: ActiveRecursionDetector::default(), visited_types: TypeCollector::default(), @@ -138,7 +140,7 @@ impl<'db> DefinitionReferenceVisitor<'db> { } let class = match ty { - Type::ProtocolInstance(protocol) => *protocol.class_origin()?, + Type::ProtocolInstance(protocol) => *protocol.class_origin(db)?, Type::TypedDict(typed_dict) => typed_dict.defining_class()?, _ => return None, }; @@ -158,7 +160,9 @@ impl<'db> DefinitionReferenceVisitor<'db> { fn visit_definition_body(&self, db: &'db dyn Db, ty: Type<'db>) { match ty { Type::TypeAlias(alias) => self.visit_type_alias_type(db, alias), - Type::ProtocolInstance(protocol) => self.visit_protocol_instance_type(db, protocol), + Type::ProtocolInstance(protocol) => { + self.visit_protocol_instance_type(db, protocol); + } Type::TypedDict(typed_dict) => self.visit_typed_dict_type(db, typed_dict), _ => {} } @@ -166,6 +170,10 @@ impl<'db> DefinitionReferenceVisitor<'db> { } impl<'db> TypeVisitor<'db> for DefinitionReferenceVisitor<'db> { + fn program_environment(&self) -> &ProgramEnvironment<'db> { + &self.env + } + fn should_visit_lazy_type_attributes(&self) -> bool { false } @@ -198,7 +206,7 @@ impl<'db> TypeVisitor<'db> for DefinitionReferenceVisitor<'db> { } fn visit_protocol_instance_type(&self, db: &'db dyn Db, protocol: ProtocolInstanceType<'db>) { - if let Some(class) = protocol.class_origin() { + if let Some(class) = protocol.class_origin(db) { class.walk_recursive_member_types(db, self); } } @@ -229,21 +237,22 @@ impl<'db> TypeAliasType<'db> { impl<'db> ProtocolInstanceType<'db> { fn definition(self, db: &'db dyn Db) -> Option> { - let (origin, _) = self.class_origin()?.static_class_literal(db)?; + let (origin, _) = self.class_origin(db)?.static_class_literal(db)?; Some(origin.definition(db)) } fn is_recursive(self, db: &'db dyn Db) -> bool { - let Some(class) = self.class_origin() else { + let Some(class) = self.class_origin(db) else { return false; }; let Some((origin, _)) = class.static_class_literal(db) else { return false; }; let definition = origin.definition(db); + let env = ProgramEnvironment::from_definition(definition); // Inspect the definition without its current specialization. Otherwise, a finite // type such as `Protocol[Protocol[int]]` would appear recursive. - let unspecialized = Type::instance(db, ClassType::NonGeneric(origin.into())); + let unspecialized = Type::instance(db, &env, ClassType::NonGeneric(origin.into())); DefinitionReferenceVisitor::references(db, unspecialized, definition) } } @@ -347,7 +356,7 @@ impl<'db, Tag, T, R, const INLINE_CAPACITY: usize> CycleDetector<'db, Tag, T, R, where T: HasIdentity<'db>, { - pub fn new(fallback: R) -> Self { + pub(crate) fn new(fallback: R) -> Self { CycleDetector { seen: RefCell::new(SmallVec::new()), cache: RefCell::new(CycleDetectorCache::new()), @@ -677,13 +686,15 @@ impl Drop for ActiveRecursionGuard<'_, T> { #[cfg(test)] mod tests { use super::{CycleDetector, CycleDetectorVisit, Db, HasIdentity, TypeIdentity}; - use crate::db::tests::{TestDb, setup_db}; + use crate::ProgramEnvironment; + use crate::db::tests::setup_db; use crate::place::global_symbol; use crate::types::Type; use ruff_db::files::system_path_to_file; use ruff_db::system::DbWithWritableSystem; use std::cell::Cell; use std::hash::{Hash, Hasher}; + use ty_python_core::ProgramFile; struct TestVisit; @@ -748,12 +759,17 @@ mod tests { fn to_identity(&self, _db: &'db dyn Db) -> Self::Id {} } - fn global_instance_type<'db>(db: &'db TestDb, name: &str) -> Type<'db> { + fn global_instance_type<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> Type<'db> { let file = system_path_to_file(db, "/src/a.py").unwrap(); + let file = ProgramFile::new(db, file, env.program(db)); global_symbol(db, file, name) .place .expect_type() - .to_instance_approximation(db) + .to_instance_approximation(db, env) .unwrap() } @@ -785,16 +801,17 @@ class RecursivePropertySetter[T](Protocol): ) .unwrap(); + let env = db.program_environment(); assert_eq!( - global_instance_type(&db, "GenericProperty").recursive_identity(&db), + global_instance_type(&db, &env, "GenericProperty").recursive_identity(&db), None ); assert!(matches!( - global_instance_type(&db, "RecursiveProperty").recursive_identity(&db), + global_instance_type(&db, &env, "RecursiveProperty").recursive_identity(&db), Some(TypeIdentity::RecursiveProtocol(_)) )); assert!(matches!( - global_instance_type(&db, "RecursivePropertySetter").recursive_identity(&db), + global_instance_type(&db, &env, "RecursivePropertySetter").recursive_identity(&db), Some(TypeIdentity::RecursiveProtocol(_)) )); } @@ -802,26 +819,28 @@ class RecursivePropertySetter[T](Protocol): #[test] fn caches_results_and_spills_after_two_entries() { let db = setup_db(); + let db = &db; let detector = Detector::new(0); - assert_eq!(detector.visit(&db, 1, || 10), 10); - assert_eq!(detector.visit(&db, 1, || 40), 10); - assert_eq!(detector.visit(&db, 2, || 20), 20); + assert_eq!(detector.visit(db, 1, || 10), 10); + assert_eq!(detector.visit(db, 1, || 40), 10); + assert_eq!(detector.visit(db, 2, || 20), 20); assert!(!detector.cache.borrow().is_spilled()); - assert_eq!(detector.visit(&db, 3, || 30), 30); + assert_eq!(detector.visit(db, 3, || 30), 30); assert!(detector.cache.borrow().is_spilled()); - assert_eq!(detector.visit(&db, 2, || 40), 20); - assert_eq!(detector.visit(&db, 3, || 40), 30); + assert_eq!(detector.visit(db, 2, || 40), 20); + assert_eq!(detector.visit(db, 3, || 40), 30); } #[test] fn nested_visit_short_circuits_on_cycle() { let db = setup_db(); + let db = &db; let detector = Detector::new(0); assert_eq!( - detector.visit(&db, 1, || detector.visit(&db, 1, || 20) + 10), + detector.visit(db, 1, || detector.visit(db, 1, || 20) + 10), 10 ); } @@ -829,12 +848,13 @@ class RecursivePropertySetter[T](Protocol): #[test] fn computes_each_active_identity_once() { let db = setup_db(); + let db = &db; let identity_calls = Cell::new(0); let detector = CycleDetector::, u8, 1>::new(0); assert_eq!( - detector.visit(&db, CountingIdentityItem::new(1, &identity_calls), || { - detector.visit(&db, CountingIdentityItem::new(3, &identity_calls), || 1) + detector.visit(db, CountingIdentityItem::new(1, &identity_calls), || { + detector.visit(db, CountingIdentityItem::new(3, &identity_calls), || 1) }), 1 ); @@ -844,12 +864,13 @@ class RecursivePropertySetter[T](Protocol): #[test] fn skips_identity_for_distinct_candidates() { let db = setup_db(); + let db = &db; let identity_calls = Cell::new(0); let detector = CycleDetector::, u8, 1>::new(0); assert_eq!( - detector.visit(&db, CountingIdentityItem::new(1, &identity_calls), || { - detector.visit(&db, CountingIdentityItem::new(2, &identity_calls), || 1) + detector.visit(db, CountingIdentityItem::new(1, &identity_calls), || { + detector.visit(db, CountingIdentityItem::new(2, &identity_calls), || 1) }), 1 ); @@ -859,15 +880,16 @@ class RecursivePropertySetter[T](Protocol): #[test] fn skips_identity_without_a_distinct_active_item() { let db = setup_db(); + let db = &db; let identity_calls = Cell::new(0); let detector = CycleDetector::, u8, 1>::new(0); assert_eq!( - detector.visit(&db, CountingIdentityItem::new(1, &identity_calls), || 1), + detector.visit(db, CountingIdentityItem::new(1, &identity_calls), || 1), 1 ); assert_eq!( - detector.visit(&db, CountingIdentityItem::new(1, &identity_calls), || 2), + detector.visit(db, CountingIdentityItem::new(1, &identity_calls), || 2), 1 ); assert_eq!(identity_calls.get(), 0); @@ -876,32 +898,33 @@ class RecursivePropertySetter[T](Protocol): #[test] fn different_items_with_same_identity_form_cycle() { let db = setup_db(); + let db = &db; let detector = CycleDetector::::new(0); let CycleDetectorVisit::Pending(pending) = - detector.begin_visit(&db, ConstantIdentityItem(1)) + detector.begin_visit(db, ConstantIdentityItem(1)) else { panic!("the first identity should be pending"); }; - let CycleDetectorVisit::Cycle(item) = detector.begin_visit(&db, ConstantIdentityItem(2)) + let CycleDetectorVisit::Cycle(item) = detector.begin_visit(db, ConstantIdentityItem(2)) else { panic!("a different item with the same identity should form a cycle"); }; assert_eq!(item.0, 2); detector.finish_visit(pending, 1); - let CycleDetectorVisit::Ready(seen) = detector.begin_visit(&db, ConstantIdentityItem(1)) + let CycleDetectorVisit::Ready(seen) = detector.begin_visit(db, ConstantIdentityItem(1)) else { panic!("the first identity should be ready after the pending visit is finished"); }; assert_eq!(seen, 1); let CycleDetectorVisit::Pending(pending) = - detector.begin_visit(&db, ConstantIdentityItem(2)) + detector.begin_visit(db, ConstantIdentityItem(2)) else { panic!("the second identity should be pending after the first is finished"); }; detector.finish_visit(pending, 2); - let CycleDetectorVisit::Ready(seen) = detector.begin_visit(&db, ConstantIdentityItem(2)) + let CycleDetectorVisit::Ready(seen) = detector.begin_visit(db, ConstantIdentityItem(2)) else { panic!("the second identity should be ready after the pending visit is finished"); }; diff --git a/crates/ty_python_semantic/src/types/dedicated/django.rs b/crates/ty_python_semantic/src/types/dedicated/django.rs index 09a9a46e03..e8236bcecf 100644 --- a/crates/ty_python_semantic/src/types/dedicated/django.rs +++ b/crates/ty_python_semantic/src/types/dedicated/django.rs @@ -22,6 +22,7 @@ use ty_python_core::scope::ScopeId; use ty_python_core::{global_scope, place_table, use_def_map}; use crate::place::{Place, known_module_symbol}; +use crate::types::ProgramEnvironment; use crate::types::class::{CodeGeneratorKind, Field, FieldKind}; use crate::types::function::FunctionType; use crate::types::list_members::all_end_of_scope_members; @@ -76,7 +77,7 @@ fn has_base_named(db: &dyn Db, class: StaticClassLiteral<'_>, module: &str, name .filter_map(|candidate| candidate.class_literal(db).as_static()) .any(|candidate| { candidate.name(db) == name - && file_to_module(db, candidate.file(db)) + && file_to_module(db, candidate.program_file(db).resolver_file(db)) .is_some_and(|candidate_module| candidate_module.name(db) == module) }) } @@ -126,10 +127,11 @@ pub(in crate::types) fn drf_view_model<'db>( db: &'db dyn Db, class: StaticClassLiteral<'db>, ) -> Option> { + let env = &ProgramEnvironment::from_file(class.program_file(db)); if !is_drf_generic_view(db, class) { return None; } - queryset_or_manager_model(db, own_body_binding(db, class, "queryset")?) + queryset_or_manager_model(db, env, own_body_binding(db, class, "queryset")?) } /// the specialized instance type constructed by a django field constructor @@ -146,6 +148,7 @@ pub(in crate::types) fn drf_view_model<'db>( /// custom field without markers) degrades to no pinning pub(in crate::types) fn field_constructor_instance_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: StaticClassLiteral<'db>, to_arg: Option>, null_arg: Option>, @@ -163,13 +166,15 @@ pub(in crate::types) fn field_constructor_instance_type<'db>( // `Unknown` explicitly: `_ST`/`_GT` appear in no constructor parameter, so leaving them to // the call's own inference would solve them to `Never` rather than leave them gradual Some( - pinned_field_instance_type(db, class, to_arg, null_arg, through_arg) - .unwrap_or_else(|| specialized_instance(db, class, [Type::unknown(), Type::unknown()])), + pinned_field_instance_type(db, env, class, to_arg, null_arg, through_arg).unwrap_or_else( + || specialized_instance(db, env, class, [Type::unknown(), Type::unknown()]), + ), ) } fn pinned_field_instance_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: StaticClassLiteral<'db>, to_arg: Option>, null_arg: Option>, @@ -177,21 +182,21 @@ fn pinned_field_instance_type<'db>( ) -> Option> { // `ManyToManyField` is generic over `(_To, _Through)`, not `(_ST, _GT)` if has_base(db, class, KnownClass::DjangoManyToManyField) { - let target = model_target_instance(db, to_arg?)?; + let target = model_target_instance(db, env, to_arg?)?; let through = through_arg - .and_then(|through| model_target_instance(db, through)) + .and_then(|through| model_target_instance(db, env, through)) .unwrap_or_else(Type::unknown); - return Some(specialized_instance(db, class, [target, through])); + return Some(specialized_instance(db, env, class, [target, through])); } let set_marker = marker_type(db, class, "_pyi_private_set_type")?; let get_marker = marker_type(db, class, "_pyi_private_get_type")?; let (mut set_ty, mut get_ty) = if is_relation_field_class(db, class) { - let target = model_target_instance(db, to_arg?)?; + let target = model_target_instance(db, env, to_arg?)?; ( - replace_dynamic(db, set_marker, target), - replace_dynamic(db, get_marker, target), + replace_dynamic(db, env, set_marker, target), + replace_dynamic(db, env, get_marker, target), ) } else { if contains_dynamic(db, set_marker) || contains_dynamic(db, get_marker) { @@ -201,11 +206,11 @@ fn pinned_field_instance_type<'db>( }; if is_null(null_arg)? { - set_ty = UnionType::from_two_elements(db, set_ty, Type::none(db)); - get_ty = UnionType::from_two_elements(db, get_ty, Type::none(db)); + set_ty = UnionType::from_two_elements(db, env, set_ty, Type::none(db, env)); + get_ty = UnionType::from_two_elements(db, env, get_ty, Type::none(db, env)); } - Some(specialized_instance(db, class, [set_ty, get_ty])) + Some(specialized_instance(db, env, class, [set_ty, get_ty])) } /// resolve a literal `null=` argument: absent or `False` → `Some(false)`, @@ -221,12 +226,16 @@ fn is_null(null_arg: Option>) -> Option { /// the instance type of a `to=`/`through=` argument, when it statically /// resolves to a django model class -fn model_target_instance<'db>(db: &'db dyn Db, to_arg: Type<'db>) -> Option> { +fn model_target_instance<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + to_arg: Type<'db>, +) -> Option> { let class = to_arg.as_class_literal()?; if !class.as_static().is_some_and(|class| is_model(db, class)) { return None; } - to_arg.to_instance_approximation(db) + to_arg.to_instance_approximation(db, env) } /// the first `name` declaration found on the mro, in mro order @@ -246,9 +255,14 @@ fn marker_type<'db>( /// substitute the dynamic parts of a relation-field marker (`Any` in /// `Any | Combinable`) with the resolved `to=` model instance type -fn replace_dynamic<'db>(db: &'db dyn Db, marker: Type<'db>, replacement: Type<'db>) -> Type<'db> { +fn replace_dynamic<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + marker: Type<'db>, + replacement: Type<'db>, +) -> Type<'db> { match marker { - Type::Union(union) => union.map(db, |element| { + Type::Union(union) => union.map(db, env, |element| { if element.is_dynamic() { replacement } else { @@ -269,34 +283,43 @@ fn contains_dynamic(db: &dyn Db, ty: Type<'_>) -> bool { fn specialized_instance<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: StaticClassLiteral<'db>, types: [Type<'db>; 2], ) -> Type<'db> { let class_type = class.apply_specialization(db, |generic_context| { generic_context.specialize(db, Cow::Owned(types.to_vec())) }); - Type::instance(db, class_type) + Type::instance(db, env, class_type) } /// `ty` is an instance of a `django.db.models.Field` subclass -pub(in crate::types) fn is_field_instance(db: &dyn Db, ty: Type<'_>) -> bool { - instance_static_class(db, ty).is_some_and(|class| is_field_class(db, class)) +pub(in crate::types) fn is_field_instance( + db: &dyn Db, + env: &ProgramEnvironment<'_>, + ty: Type<'_>, +) -> bool { + instance_static_class(db, env, ty).is_some_and(|class| is_field_class(db, class)) } -fn is_relation_field_instance(db: &dyn Db, ty: Type<'_>) -> bool { - instance_static_class(db, ty).is_some_and(|class| is_relation_field_class(db, class)) +fn is_relation_field_instance(db: &dyn Db, env: &ProgramEnvironment<'_>, ty: Type<'_>) -> bool { + instance_static_class(db, env, ty).is_some_and(|class| is_relation_field_class(db, class)) } -pub(in crate::types) fn is_many_to_many_instance(db: &dyn Db, ty: Type<'_>) -> bool { - instance_static_class(db, ty) +pub(in crate::types) fn is_many_to_many_instance( + db: &dyn Db, + env: &ProgramEnvironment<'_>, + ty: Type<'_>, +) -> bool { + instance_static_class(db, env, ty) .is_some_and(|class| has_base(db, class, KnownClass::DjangoManyToManyField)) } /// `ty` is an instance of `JSONField` — the one built-in field whose `__` /// segments are arbitrary object keys and array indices rather than a closed set /// of lookups -fn is_json_field_instance(db: &dyn Db, ty: Type<'_>) -> bool { - instance_static_class(db, ty) +fn is_json_field_instance(db: &dyn Db, env: &ProgramEnvironment<'_>, ty: Type<'_>) -> bool { + instance_static_class(db, env, ty) .is_some_and(|class| has_base_named(db, class, "django.db.models.fields.json", "JSONField")) } @@ -307,12 +330,20 @@ fn is_json_field_instance(db: &dyn Db, ty: Type<'_>) -> bool { /// and a constructor call in a `.by` file infers as `final CharField[…]` — a /// restricted type, whose nominal class is nothing at all. reading through the /// modifier is what keeps a model's fields visible there -fn nominal_class<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option> { - ty.erase_restriction(db).nominal_class(db) +fn nominal_class<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Option> { + ty.erase_restriction(db).nominal_class(db, env) } -fn instance_static_class<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option> { - nominal_class(db, ty)?.class_literal(db).as_static() +fn instance_static_class<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Option> { + nominal_class(db, env, ty)?.class_literal(db).as_static() } /// the per-field facts read from a field constructor call in a class-body @@ -380,8 +411,12 @@ pub(in crate::types) fn is_abstract_model<'db>( } /// the `_GT` (instance read) side of a pinned field instance type -fn field_get_type<'db>(db: &'db dyn Db, field_ty: Type<'db>) -> Option> { - let ClassType::Generic(alias) = nominal_class(db, field_ty)? else { +fn field_get_type<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + field_ty: Type<'db>, +) -> Option> { + let ClassType::Generic(alias) = nominal_class(db, env, field_ty)? else { return None; }; let [_, get_ty] = alias.specialization(db).types(db) else { @@ -392,8 +427,12 @@ fn field_get_type<'db>(db: &'db dyn Db, field_ty: Type<'db>) -> Option /// the `_ST` (assignment/lookup) side of a pinned field instance type — the /// type django accepts when writing the field or filtering on it exactly -fn field_set_type<'db>(db: &'db dyn Db, field_ty: Type<'db>) -> Option> { - let ClassType::Generic(alias) = nominal_class(db, field_ty)? else { +fn field_set_type<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + field_ty: Type<'db>, +) -> Option> { + let ClassType::Generic(alias) = nominal_class(db, env, field_ty)? else { return None; }; let [set_ty, _] = alias.specialization(db).types(db) else { @@ -405,7 +444,11 @@ fn field_set_type<'db>(db: &'db dyn Db, field_ty: Type<'db>) -> Option /// the runtime read type of a model's primary key: the explicit /// `primary_key=True` field's read type, or `int` for the auto `id` /// (`BigAutoField` per modern defaults) -fn model_pk_type<'db>(db: &'db dyn Db, fields: &FxIndexMap>) -> Type<'db> { +fn model_pk_type<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + fields: &FxIndexMap>, +) -> Type<'db> { for field in fields.values() { if matches!( &field.kind, @@ -414,30 +457,38 @@ fn model_pk_type<'db>(db: &'db dyn Db, fields: &FxIndexMap>) -> .. } ) { - return field_get_type(db, field.declared_ty).unwrap_or_else(Type::unknown); + return field_get_type(db, env, field.declared_ty).unwrap_or_else(Type::unknown); } } - KnownClass::Int.to_instance(db) + KnownClass::Int.to_instance(db, env) } /// the type of a to-one relation field's `_id` attname: the target /// model's primary-key type, `| None` when the field is nullable -fn attname_type<'db>(db: &'db dyn Db, field: &Field<'db>) -> Option> { +fn attname_type<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + field: &Field<'db>, +) -> Option> { let FieldKind::Django { null, .. } = &field.kind else { return None; }; - if !is_relation_field_instance(db, field.declared_ty) { + if !is_relation_field_instance(db, env, field.declared_ty) { return None; } - let target = - field_get_type(db, field.declared_ty)?.filter_union(db, |element| !element.is_none(db)); - let target_class = instance_static_class(db, target)?; + let target = field_get_type(db, env, field.declared_ty)? + .filter_union(db, |element| !element.is_none(db)); + let target_class = instance_static_class(db, env, target)?; if !is_model(db, target_class) { return None; } - let target_pk = model_pk_type(db, target_class.fields(db, None, CodeGeneratorKind::Django)); + let target_pk = model_pk_type( + db, + env, + target_class.fields(db, None, CodeGeneratorKind::Django), + ); Some(if *null { - UnionType::from_two_elements(db, target_pk, Type::none(db)) + UnionType::from_two_elements(db, env, target_pk, Type::none(db, env)) } else { target_pk }) @@ -449,6 +500,7 @@ fn attname_type<'db>(db: &'db dyn Db, field: &Field<'db>) -> Option> { /// get nothing — their concrete subclasses do pub(in crate::types) fn synthesized_model_attribute<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: StaticClassLiteral<'db>, fields: &FxIndexMap>, name: &str, @@ -457,7 +509,7 @@ pub(in crate::types) fn synthesized_model_attribute<'db>( return None; } match name { - "pk" => Some(model_pk_type(db, fields)), + "pk" => Some(model_pk_type(db, env, fields)), "id" => { let has_explicit_pk = fields.values().any(|field| { matches!( @@ -468,7 +520,7 @@ pub(in crate::types) fn synthesized_model_attribute<'db>( } ) }); - (!has_explicit_pk).then(|| KnownClass::Int.to_instance(db)) + (!has_explicit_pk).then(|| KnownClass::Int.to_instance(db, env)) } _ => { // `get__display()` for a field declared with `choices=` @@ -483,23 +535,27 @@ pub(in crate::types) fn synthesized_model_attribute<'db>( }) ) { - return Some(display_method(db, class)); + return Some(display_method(db, env, class)); } let field_name = name.strip_suffix("_id")?; - attname_type(db, fields.get(field_name)?) + attname_type(db, env, fields.get(field_name)?) } } } /// the `() -> str` bound method django synthesizes for a choices field's /// `get__display` -fn display_method<'db>(db: &'db dyn Db, class: StaticClassLiteral<'db>) -> Type<'db> { - let self_ty = Type::instance(db, class.default_specialization(db)); +fn display_method<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + class: StaticClassLiteral<'db>, +) -> Type<'db> { + let self_ty = Type::instance(db, env, class.default_specialization(db)); let signature = Signature::new( Parameters::standard([ Parameter::positional_or_keyword(Name::new_static("self")).with_annotated_type(self_ty) ]), - KnownClass::Str.to_instance(db), + KnownClass::Str.to_instance(db, env), ); Type::function_like_callable(db, signature) } @@ -520,6 +576,7 @@ pub(in crate::types) fn reverse_accessors<'db>( db: &'db dyn Db, class: StaticClassLiteral<'db>, ) -> FxIndexMap> { + let env = &ProgramEnvironment::from_file(class.program_file(db)); let mut accessors = FxIndexMap::default(); if class.is_known(db, KnownClass::DjangoModel) || is_abstract_model(db, class) { return accessors; @@ -528,7 +585,7 @@ pub(in crate::types) fn reverse_accessors<'db>( // enumerate the module's class *definitions* structurally rather than // resolving every global symbol: inferring arbitrary bindings from here // can cycle back into member lookups that consult this query - let global = global_scope(db, class.file(db)); + let global = global_scope(db, class.program_file(db)); let use_def = use_def_map(db, global); let mut sources = Vec::new(); for (_, bindings) in use_def.all_end_of_scope_symbol_bindings() { @@ -548,8 +605,8 @@ pub(in crate::types) fn reverse_accessors<'db>( continue; } for field in source.fields(db, None, CodeGeneratorKind::Django).values() { - let is_m2m = is_many_to_many_instance(db, field.declared_ty); - if !is_relation_field_instance(db, field.declared_ty) && !is_m2m { + let is_m2m = is_many_to_many_instance(db, env, field.declared_ty); + if !is_relation_field_instance(db, env, field.declared_ty) && !is_m2m { continue; } @@ -557,12 +614,12 @@ pub(in crate::types) fn reverse_accessors<'db>( // field it is the first (`_To`) specialization argument, and the // second (`_Through`) carries the through model let (target, through) = if is_m2m { - match m2m_target_and_through(db, field.declared_ty) { + match m2m_target_and_through(db, env, field.declared_ty) { Some((target, through)) => (target, Some(through)), None => continue, } } else { - match field_get_type(db, field.declared_ty) { + match field_get_type(db, env, field.declared_ty) { Some(target) => ( target.filter_union(db, |element| !element.is_none(db)), None, @@ -570,7 +627,7 @@ pub(in crate::types) fn reverse_accessors<'db>( None => continue, } }; - if instance_static_class(db, target) != Some(class) { + if instance_static_class(db, env, target) != Some(class) { continue; } let FieldKind::Django { related_name, .. } = &field.kind else { @@ -581,7 +638,7 @@ pub(in crate::types) fn reverse_accessors<'db>( continue; } let one_to_one = - instance_static_class(db, field.declared_ty).is_some_and(|field_class| { + instance_static_class(db, env, field.declared_ty).is_some_and(|field_class| { has_base(db, field_class, KnownClass::DjangoOneToOneField) }); let accessor = match related_name { @@ -589,14 +646,14 @@ pub(in crate::types) fn reverse_accessors<'db>( None if one_to_one => Name::new(source.name(db).to_lowercase()), None => Name::new(format!("{}_set", source.name(db).to_lowercase())), }; - let source_instance = Type::instance(db, source.default_specialization(db)); + let source_instance = Type::instance(db, env, source.default_specialization(db)); let accessor_ty = if let Some(through) = through { // the reverse of a many-to-many is itself a many-to-many manager - many_related_manager_instance(db, source_instance, through) + many_related_manager_instance(db, env, source_instance, through) } else if one_to_one { Some(source_instance) } else { - related_manager_instance(db, source_instance) + related_manager_instance(db, env, source_instance) }; if let Some(accessor_ty) = accessor_ty { accessors.insert(accessor, accessor_ty); @@ -611,9 +668,10 @@ pub(in crate::types) fn reverse_accessors<'db>( /// target model and through model (the latter `Unknown` for an implicit table) fn m2m_target_and_through<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, field_ty: Type<'db>, ) -> Option<(Type<'db>, Type<'db>)> { - let ClassType::Generic(alias) = nominal_class(db, field_ty)? else { + let ClassType::Generic(alias) = nominal_class(db, env, field_ty)? else { return None; }; let [target, through] = alias.specialization(db).types(db) else { @@ -626,11 +684,13 @@ fn m2m_target_and_through<'db>( /// module, or `None` when it doesn't resolve (degrade to no accessor) fn many_related_manager_instance<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, source: Type<'db>, through: Type<'db>, ) -> Option> { let manager = known_module_symbol( db, + env, KnownModule::DjangoDbModelsFieldsRelatedDescriptors, "ManyRelatedManager", ) @@ -645,14 +705,19 @@ fn many_related_manager_instance<'db>( }; let class_type = class.apply_specialization(db, |generic_context| generic_context.specialize(db, args)); - Some(Type::instance(db, class_type)) + Some(Type::instance(db, env, class_type)) } /// `RelatedManager[source]` from the stubs' `related_descriptors` module, /// or `None` when it doesn't resolve (degrade to no accessor) -fn related_manager_instance<'db>(db: &'db dyn Db, source: Type<'db>) -> Option> { +fn related_manager_instance<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + source: Type<'db>, +) -> Option> { let manager = known_module_symbol( db, + env, KnownModule::DjangoDbModelsFieldsRelatedDescriptors, "RelatedManager", ) @@ -666,13 +731,14 @@ fn related_manager_instance<'db>(db: &'db dyn Db, source: Type<'db>) -> Option( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: StaticClassLiteral<'db>, fields: &FxIndexMap>, ) -> Vec { @@ -693,7 +759,7 @@ pub(in crate::types) fn synthesized_member_names<'db>( names.push(Name::new_static("id")); } for (name, field) in fields { - if is_relation_field_instance(db, field.declared_ty) { + if is_relation_field_instance(db, env, field.declared_ty) { names.push(Name::new(format!("{name}_id"))); } } @@ -706,17 +772,23 @@ pub(in crate::types) fn synthesized_member_names<'db>( /// all optional and none-able — requiredness is a `save` concern pub(in crate::types) fn extra_constructor_parameters<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, fields: &FxIndexMap>, ) -> Vec<(Name, Type<'db>)> { let mut extras = Vec::new(); if !fields.contains_key("pk") { extras.push(( Name::new_static("pk"), - UnionType::from_two_elements(db, model_pk_type(db, fields), Type::none(db)), + UnionType::from_two_elements( + db, + env, + model_pk_type(db, env, fields), + Type::none(db, env), + ), )); } for (name, field) in fields { - let Some(attname_ty) = attname_type(db, field) else { + let Some(attname_ty) = attname_type(db, env, field) else { continue; }; let attname = Name::new(format!("{name}_id")); @@ -725,7 +797,7 @@ pub(in crate::types) fn extra_constructor_parameters<'db>( } extras.push(( attname, - UnionType::from_two_elements(db, attname_ty, Type::none(db)), + UnionType::from_two_elements(db, env, attname_ty, Type::none(db, env)), )); } extras @@ -745,9 +817,10 @@ pub(in crate::types) fn extra_constructor_parameters<'db>( /// the model a `Manager[M]` / `QuerySet[M, _]` instance is parameterized by pub(in crate::types) fn queryset_or_manager_model<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, self_instance: Type<'db>, ) -> Option> { - let class = nominal_class(db, self_instance)?; + let class = nominal_class(db, env, self_instance)?; let is_qs_or_manager = class.class_literal(db).as_static().is_some_and(|literal| { has_base(db, literal, KnownClass::DjangoManager) || has_base(db, literal, KnownClass::DjangoQuerySet) @@ -759,7 +832,7 @@ pub(in crate::types) fn queryset_or_manager_model<'db>( return None; }; let model_instance = alias.specialization(db).types(db).first()?; - let model = instance_static_class(db, *model_instance)?; + let model = instance_static_class(db, env, *model_instance)?; is_model(db, model).then_some(model) } @@ -826,13 +899,14 @@ struct FieldRef<'db> { /// `model` to a reference for path walking fn field_ref<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, model: StaticClassLiteral<'db>, name: &str, ) -> Option> { let fields = model.fields(db, None, CodeGeneratorKind::Django); if name == "pk" { - let pk = model_pk_type(db, fields); + let pk = model_pk_type(db, env, fields); return Some(FieldRef { relation_model: None, is_relation: false, @@ -843,15 +917,15 @@ fn field_ref<'db>( } if let Some(field) = fields.get(name) { - if is_relation_field_instance(db, field.declared_ty) { - let target = field_get_type(db, field.declared_ty) + if is_relation_field_instance(db, env, field.declared_ty) { + let target = field_get_type(db, env, field.declared_ty) .map(|ty| ty.filter_union(db, |element| !element.is_none(db))); let relation_model = target - .and_then(|target| instance_static_class(db, target)) + .and_then(|target| instance_static_class(db, env, target)) .filter(|target| is_model(db, *target)); // a bare relation in a `values()` row reads as the target's pk let value_type = relation_model.map_or_else(Type::unknown, |target| { - model_pk_type(db, target.fields(db, None, CodeGeneratorKind::Django)) + model_pk_type(db, env, target.fields(db, None, CodeGeneratorKind::Django)) }); return Some(FieldRef { relation_model, @@ -864,14 +938,14 @@ fn field_ref<'db>( return Some(FieldRef { relation_model: None, is_relation: false, - set_type: field_set_type(db, field.declared_ty).unwrap_or_else(Type::unknown), - value_type: field_get_type(db, field.declared_ty).unwrap_or_else(Type::unknown), + set_type: field_set_type(db, env, field.declared_ty).unwrap_or_else(Type::unknown), + value_type: field_get_type(db, env, field.declared_ty).unwrap_or_else(Type::unknown), declared_type: Some(field.declared_ty), }); } // synthesized names: the auto `id` and `_id` attnames - if let Some(synthesized) = synthesized_model_attribute(db, model, fields, name) { + if let Some(synthesized) = synthesized_model_attribute(db, env, model, fields, name) { return Some(FieldRef { relation_model: None, is_relation: false, @@ -888,6 +962,7 @@ fn field_ref<'db>( /// `set_type`; `None` for lookups without a checkable operand fn concrete_lookup_operand<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, set_type: Type<'db>, lookups: &[&str], ) -> Option> { @@ -899,9 +974,9 @@ fn concrete_lookup_operand<'db>( "exact" | "iexact" | "gt" | "gte" | "lt" | "lte" => Some(set_type), "contains" | "icontains" | "startswith" | "istartswith" | "endswith" | "iendswith" | "regex" | "iregex" | "search" | "trigram_similar" => { - Some(KnownClass::Str.to_instance(db)) + Some(KnownClass::Str.to_instance(db, env)) } - "isnull" => Some(KnownClass::Bool.to_instance(db)), + "isnull" => Some(KnownClass::Bool.to_instance(db, env)), // `in`/`range` take iterables, date/time transforms chain further — // skip rather than risk a false positive _ => None, @@ -919,6 +994,7 @@ fn is_relation_lookup(name: &str) -> bool { /// resolve a lookup key (`author__name__startswith`) against `model` pub(in crate::types) fn resolve_lookup<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, model: StaticClassLiteral<'db>, key: &str, ) -> FieldResolution<'db> { @@ -930,7 +1006,7 @@ pub(in crate::types) fn resolve_lookup<'db>( let segment = segments[index]; let is_last = index + 1 == segments.len(); - let Some(field) = field_ref(db, model, segment) else { + let Some(field) = field_ref(db, env, model, segment) else { // only the leading segment is unambiguously a field position; a // later unknown segment was already classified as a lookup below return FieldResolution::Unknown { @@ -951,7 +1027,7 @@ pub(in crate::types) fn resolve_lookup<'db>( let next = segments[index + 1]; // after a relation hop the next segment is expected to be a field // on the target model — traverse into it - if field_ref(db, target, next).is_some() { + if field_ref(db, env, target, next).is_some() { model = target; index += 1; continue; @@ -961,7 +1037,7 @@ pub(in crate::types) fn resolve_lookup<'db>( // is almost certainly a typo if index + 2 == segments.len() && is_relation_lookup(next) { let operand = match next { - "isnull" => Some(KnownClass::Bool.to_instance(db)), + "isnull" => Some(KnownClass::Bool.to_instance(db, env)), _ => None, }; return FieldResolution::Resolved { operand }; @@ -977,7 +1053,7 @@ pub(in crate::types) fn resolve_lookup<'db>( operand: Some(field.set_type), }; } - let operand = concrete_lookup_operand(db, field.set_type, &segments[index + 1..]); + let operand = concrete_lookup_operand(db, env, field.set_type, &segments[index + 1..]); return FieldResolution::Resolved { operand }; } } @@ -1152,19 +1228,20 @@ fn assemble_key(segments: &[Cow<'_, str>]) -> Option { /// field the model declares fn path_field<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, model: StaticClassLiteral<'db>, names: &[&str], ) -> Option> { let (&last, leading) = names.split_last()?; let mut model = model; for name in leading { - let field = field_ref(db, model, name)?; + let field = field_ref(db, env, model, name)?; if !field.is_relation { return None; } model = field.relation_model?; } - field_ref(db, model, last) + field_ref(db, env, model, last) } /// the type the leading name of a lookup path takes: the target model's instance @@ -1172,20 +1249,21 @@ fn path_field<'db>( /// model they traverse into; the field's own read type otherwise fn lookup_root_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, model: StaticClassLiteral<'db>, name: &str, ) -> Option> { // a many-to-many field's `_GT` is its *through* model, not its target if let Some(field) = model.fields(db, None, CodeGeneratorKind::Django).get(name) - && is_many_to_many_instance(db, field.declared_ty) + && is_many_to_many_instance(db, env, field.declared_ty) { - return m2m_target_and_through(db, field.declared_ty).map(|(target, _)| target); + return m2m_target_and_through(db, env, field.declared_ty).map(|(target, _)| target); } - let field = field_ref(db, model, name)?; + let field = field_ref(db, env, model, name)?; if field.is_relation { // an unresolved relation target leaves nothing to traverse into let target = field.relation_model?; - return Some(Type::instance(db, target.default_specialization(db))); + return Some(Type::instance(db, env, target.default_specialization(db))); } Some(field.value_type) } @@ -1197,6 +1275,7 @@ fn lookup_root_type<'db>( /// meaning it has today, and the transpiler leaves it exactly as written pub(crate) fn lookup_expressions<'a, 'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, scope: ScopeId<'db>, model: StaticClassLiteral<'db>, @@ -1205,7 +1284,7 @@ pub(crate) fn lookup_expressions<'a, 'db>( let mut classified: Vec>> = arguments .args .iter() - .map(|argument| lookup_expression(db, file, scope, model, argument)) + .map(|argument| lookup_expression(db, env, file, scope, model, argument)) .collect(); // a lookup lowers to a keyword argument, which python requires after every @@ -1246,6 +1325,7 @@ pub(crate) fn lookup_expressions<'a, 'db>( /// manager method that takes them as positional expressions pub(crate) fn lookup_call_model<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, callee: Type<'db>, ) -> Option> { let Type::BoundMethod(bound_method) = callee else { @@ -1257,7 +1337,7 @@ pub(crate) fn lookup_call_model<'db>( { return None; } - queryset_or_manager_model(db, bound_method.self_instance(db)) + queryset_or_manager_model(db, env, bound_method.self_instance(db)) } /// the keyword a lookup expression lowers to, as source ranges — the whole @@ -1278,19 +1358,20 @@ pub(crate) struct LookupLowering { /// reports it, and lowering it would emit a keyword django itself rejects pub(crate) fn lookup_call_lowering<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, scope: ScopeId<'db>, callee: Type<'db>, call: &ast::ExprCall, ) -> Vec { - let Some(model) = lookup_call_model(db, callee) else { + let Some(model) = lookup_call_model(db, env, callee) else { return Vec::new(); }; - lookup_expressions(db, file, scope, model, &call.arguments) + lookup_expressions(db, env, file, scope, model, &call.arguments) .into_iter() .filter(|lookup| { matches!( - resolve_lookup(db, model, &lookup.key), + resolve_lookup(db, env, model, &lookup.key), FieldResolution::Resolved { .. } ) }) @@ -1304,6 +1385,7 @@ pub(crate) fn lookup_call_lowering<'db>( fn lookup_expression<'a, 'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, scope: ScopeId<'db>, model: StaticClassLiteral<'db>, @@ -1323,10 +1405,10 @@ fn lookup_expression<'a, 'db>( // anything that already claims the name wins, exactly as it does for an // implicit receiver: this is asked of a raw name by the transpiler too, so it // takes the wider of the two name-fallback gates - if claimed_by_name_resolution(db, file, scope, root.id.as_str()) { + if claimed_by_name_resolution(db, env, file, scope, root.id.as_str()) { return None; } - let root_type = lookup_root_type(db, model, root.id.as_str())?; + let root_type = lookup_root_type(db, env, model, root.id.as_str())?; // the dotted part of the path names the field; the subscripts after it index // into it, so a dot *after* a subscript names nothing django can spell @@ -1346,8 +1428,8 @@ fn lookup_expression<'a, 'db>( // only a json field carries the key and index transforms a subscript // spells. on anything else django rejects the keyword at runtime, so a // subscript there is left as written - let field = path_field(db, model, &names)?; - if !is_json_field_instance(db, field.declared_type?) { + let field = path_field(db, env, model, &names)?; + if !is_json_field_instance(db, env, field.declared_type?) { return None; } } @@ -1372,11 +1454,12 @@ fn lookup_expression<'a, 'db>( /// when the key names no field / attname of the model pub(in crate::types) fn resolve_create_kwarg<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, model: StaticClassLiteral<'db>, key: &str, ) -> FieldResolution<'db> { // create() keys are plain field names or attnames — no `__` traversal - match field_ref(db, model, key) { + match field_ref(db, env, model, key) { Some(field) if field.is_relation => FieldResolution::Resolved { operand: None }, Some(field) => FieldResolution::Resolved { operand: Some(field.set_type), @@ -1392,6 +1475,7 @@ pub(in crate::types) fn resolve_create_kwarg<'db>( /// `-` (descending) is stripped. returns the offending segment when unknown pub(in crate::types) fn resolve_field_name<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, model: StaticClassLiteral<'db>, name: &str, ) -> FieldResolution<'db> { @@ -1400,7 +1484,7 @@ pub(in crate::types) fn resolve_field_name<'db>( if name == "?" || name.is_empty() { return FieldResolution::Resolved { operand: None }; } - match resolve_lookup(db, model, name) { + match resolve_lookup(db, env, model, name) { FieldResolution::Unknown { model, segment } => FieldResolution::Unknown { model, segment }, FieldResolution::Resolved { .. } => FieldResolution::Resolved { operand: None }, } @@ -1464,12 +1548,13 @@ impl MetaFieldsDeclarer { /// member lookup: the declaring class's body is the scope this runs from pub(in crate::types) fn is_meta_fields_entry_valid<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, model: StaticClassLiteral<'db>, declarer: StaticClassLiteral<'db>, name: &str, ) -> bool { - let model_instance = Type::instance(db, model.default_specialization(db)); - if !matches!(model_instance.member(db, name).place, Place::Undefined) { + let model_instance = Type::instance(db, env, model.default_specialization(db)); + if !matches!(model_instance.member(db, env, name).place, Place::Undefined) { return true; } declarer @@ -1565,15 +1650,16 @@ impl<'db> FieldListKind<'db> { pub(in crate::types) fn resolve( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, model: StaticClassLiteral<'db>, entry: &str, ) -> FieldResolution<'db> { match self { // `order_by` syntax — `-` and `?` included - Self::Ordering => resolve_field_name(db, model, entry), - Self::ViewFieldNames => resolve_lookup(db, model, entry), + Self::Ordering => resolve_field_name(db, env, model, entry), + Self::ViewFieldNames => resolve_lookup(db, env, model, entry), Self::MetaFields { declaring } => { - if is_meta_fields_entry_valid(db, model, declaring, entry) { + if is_meta_fields_entry_valid(db, env, model, declaring, entry) { FieldResolution::Resolved { operand: None } } else { FieldResolution::Unknown { @@ -1590,7 +1676,7 @@ impl<'db> FieldListKind<'db> { if path.is_empty() { return FieldResolution::Resolved { operand: None }; } - resolve_lookup(db, model, path) + resolve_lookup(db, env, model, path) } } } @@ -1601,13 +1687,14 @@ impl<'db> FieldListKind<'db> { /// when the path can't be statically resolved (so refinement is skipped) fn field_value_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, model: StaticClassLiteral<'db>, key: &str, ) -> Option> { let segments: Vec<&str> = key.split("__").collect(); let mut model = model; for (index, segment) in segments.iter().enumerate() { - let field = field_ref(db, model, segment)?; + let field = field_ref(db, env, model, segment)?; let is_last = index + 1 == segments.len(); if field.is_relation && !is_last { // traverse into the related model for the next segment @@ -1625,6 +1712,7 @@ fn field_value_type<'db>( /// keep the stub type (`named=True`, no fields, or any unresolved field) pub(in crate::types) fn values_list_row_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, model: StaticClassLiteral<'db>, fields: &[&str], flat: bool, @@ -1637,14 +1725,14 @@ pub(in crate::types) fn values_list_row_type<'db>( } let values: Option>> = fields .iter() - .map(|field| field_value_type(db, model, field)) + .map(|field| field_value_type(db, env, model, field)) .collect(); let values = values?; if flat { // `flat=True` is only valid with a single field return (values.len() == 1).then(|| values[0]); } - Some(Type::heterogeneous_tuple(db, values)) + Some(Type::heterogeneous_tuple(db, env, values)) } /// refine a `values(*fields)` call's row type to `dict[str, ( /// `dict[str, Any]`). `None` to keep the stub type pub(in crate::types) fn values_row_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, model: StaticClassLiteral<'db>, fields: &[&str], ) -> Option> { @@ -1660,19 +1749,24 @@ pub(in crate::types) fn values_row_type<'db>( } let values: Option>> = fields .iter() - .map(|field| field_value_type(db, model, field)) + .map(|field| field_value_type(db, env, model, field)) .collect(); - let value = UnionType::from_elements(db, values?); - Some(KnownClass::Dict.to_specialized_instance(db, &[KnownClass::Str.to_instance(db), value])) + let value = UnionType::from_elements(db, env, values?); + Some(KnownClass::Dict.to_specialized_instance( + db, + env, + &[KnownClass::Str.to_instance(db, env), value], + )) } /// rebuild a `QuerySet[Model, _Row]` return type with a refined `_Row` pub(in crate::types) fn with_queryset_row<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, queryset_ty: Type<'db>, row: Type<'db>, ) -> Option> { - let ClassType::Generic(alias) = nominal_class(db, queryset_ty)? else { + let ClassType::Generic(alias) = nominal_class(db, env, queryset_ty)? else { return None; }; let [model_arg, _] = alias.specialization(db).types(db) else { @@ -1684,7 +1778,7 @@ pub(in crate::types) fn with_queryset_row<'db>( .apply_specialization(db, |generic_context| { generic_context.specialize(db, Cow::Owned(vec![model_arg, row])) }); - Some(Type::instance(db, class_type)) + Some(Type::instance(db, env, class_type)) } // --------------------------------------------------------------------------- @@ -1703,7 +1797,7 @@ pub(in crate::types) fn with_queryset_row<'db>( /// whether `file` belongs to the `rest_framework` package fn is_drf_module(db: &dyn Db, file: File) -> bool { - file_to_module(db, file).is_some_and(|module| { + file_to_module(db, db.program_file(file).resolver_file(db)).is_some_and(|module| { let name = module.name(db).as_str(); name == "rest_framework" || name.starts_with("rest_framework.") }) @@ -1779,14 +1873,15 @@ fn declares_outside_drf<'db>(db: &'db dyn Db, class: StaticClassLiteral<'db>, na /// fires only where today's answer carries no information at all fn substitute_model<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, declared_return: Type<'db>, model: StaticClassLiteral<'db>, ) -> Option> { - let model_instance = Type::instance(db, model.default_specialization(db)); + let model_instance = Type::instance(db, env, model.default_specialization(db)); if declared_return.is_dynamic() { return Some(model_instance); } - let ClassType::Generic(alias) = nominal_class(db, declared_return)? else { + let ClassType::Generic(alias) = nominal_class(db, env, declared_return)? else { return None; }; let arity = alias.specialization(db).types(db).len(); @@ -1803,7 +1898,7 @@ fn substitute_model<'db>( .apply_specialization(db, |generic_context| { generic_context.specialize(db, Cow::Owned(vec![model_instance; arity])) }); - Some(Type::instance(db, class_type)) + Some(Type::instance(db, env, class_type)) } /// the more precise return type a drf method has once the receiver's class @@ -1814,6 +1909,7 @@ fn substitute_model<'db>( /// the result is then a list serializer rather than the serializer class pub(in crate::types) fn drf_method_return_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, method: FunctionType<'db>, self_instance: Type<'db>, declared_return: Type<'db>, @@ -1825,11 +1921,11 @@ pub(in crate::types) fn drf_method_return_type<'db>( if !is_drf_module(db, method.file(db)) { return None; } - let class = instance_static_class(db, self_instance)?; + let class = instance_static_class(db, env, self_instance)?; let name = method.name(db).as_str(); let refined = match name { "get_queryset" | "get_object" => { - substitute_model(db, declared_return, drf_view_model(db, class)?)? + substitute_model(db, env, declared_return, drf_view_model(db, class)?)? } "get_serializer" | "get_serializer_class" => { // `get_serializer` builds its result by calling @@ -1840,22 +1936,23 @@ pub(in crate::types) fn drf_method_return_type<'db>( } let serializer = Type::instance( db, + env, drf_view_serializer(db, class)?.default_specialization(db), ); if name == "get_serializer" { serializer } else { - serializer.to_meta_type(db) + serializer.to_meta_type(db, env) } } "save" | "create" | "update" => { - substitute_model(db, declared_return, drf_serializer_model(db, class)?)? + substitute_model(db, env, declared_return, drf_serializer_model(db, class)?)? } _ => return None, }; // never contradict a view or serializer that *did* write its type argument refined - .is_assignable_to(db, declared_return) + .is_assignable_to(db, env, declared_return) .then_some(refined) } @@ -1945,6 +2042,7 @@ const ALTERS_DATA_METHODS: &[&str] = &[ /// `update`, and django renders a field pub(in crate::types) fn refuses_template_call<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, receiver: Type<'db>, name: &str, member: Type<'db>, @@ -1955,7 +2053,7 @@ pub(in crate::types) fn refuses_template_call<'db>( if !matches!(member, Type::FunctionLiteral(_) | Type::BoundMethod(_)) { return false; } - let Some(class) = instance_static_class(db, receiver) else { + let Some(class) = instance_static_class(db, env, receiver) else { return false; }; @@ -1973,7 +2071,7 @@ fn declared_by_django<'db>(db: &'db dyn Db, class: StaticClassLiteral<'db>, name .filter_map(ClassBase::into_class) .filter_map(|candidate| candidate.class_literal(db).as_static()) .filter(|candidate| { - file_to_module(db, candidate.file(db)) + file_to_module(db, candidate.program_file(db).resolver_file(db)) .is_some_and(|module| module.name(db).as_str().starts_with("django.")) }) .any(|candidate| { diff --git a/crates/ty_python_semantic/src/types/dedicated/pydantic.rs b/crates/ty_python_semantic/src/types/dedicated/pydantic.rs index b1a9f0d3d9..4832fa6950 100644 --- a/crates/ty_python_semantic/src/types/dedicated/pydantic.rs +++ b/crates/ty_python_semantic/src/types/dedicated/pydantic.rs @@ -1,3 +1,4 @@ +use crate::ProgramEnvironment; use char_str::CharStr; use ruff_db::parsed::parsed_module; use ruff_python_ast::{ArgOrKeyword, Arguments, Expr, ExprCall, ExprDict, Keyword, name::Name}; @@ -27,6 +28,11 @@ use crate::types::{ }; use crate::{Db, SemanticModel}; +/// Pydantic treats underscore-prefixed annotations as private instance attributes. +pub(in crate::types) fn is_private_attribute(name: &str) -> bool { + name.starts_with('_') +} + /// Metadata that controls Pydantic-specific model synthesis. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub(crate) struct ModelMetadata<'db> { @@ -140,16 +146,17 @@ impl<'db> FieldMetadata<'db> { fn collect_from_annotation( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, definition: Definition<'db>, specialization: Option>, ) { - let module = parsed_module(db, definition.file(db)).load(db); + let module = parsed_module(db, definition.python_file(db)).load(db); let DefinitionKind::AnnotatedAssignment(assignment) = definition.kind(db) else { return; }; let annotation = assignment.annotation(&module); - if self.collect_from_annotated(db, definition, annotation, specialization) { + if self.collect_from_annotated(db, env, definition, annotation, specialization) { return; } @@ -168,7 +175,7 @@ impl<'db> FieldMetadata<'db> { // using `StrictInt = Annotated[int, Strict()]`. Since we don't retain the `Annotated` // metadata, we need to follow the alias back to its definition and parse the metadata // from there. - let model = SemanticModel::new(db, definition.file(db)); + let model = SemanticModel::new(db, definition.program_file(db)); let Some(alias_definition) = definitions_for_name( &model, name.id.as_str(), @@ -180,7 +187,7 @@ impl<'db> FieldMetadata<'db> { return; }; - let module = parsed_module(db, alias_definition.file(db)).load(db); + let module = parsed_module(db, alias_definition.python_file(db)).load(db); let kind = alias_definition.kind(db); let value = match &kind { DefinitionKind::Assignment(assignment) => assignment.value(&module), @@ -193,13 +200,14 @@ impl<'db> FieldMetadata<'db> { _ => return, }; - self.collect_from_annotated(db, alias_definition, value, specialization); + self.collect_from_annotated(db, env, alias_definition, value, specialization); } /// Collect Pydantic field metadata from the `Annotated` part of a field's annotation. fn collect_from_annotated( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, definition: Definition<'db>, annotation: &Expr, specialization: Option>, @@ -246,7 +254,7 @@ impl<'db> FieldMetadata<'db> { if let Type::KnownInstance(KnownInstanceType::Field(field)) = field_type { self.merge_field(db, field, specialization); } else { - self.merge_field_call(db, definition, call, field_type, specialization); + self.merge_field_call(db, env, definition, call, field_type, specialization); } } } @@ -280,6 +288,7 @@ impl<'db> FieldMetadata<'db> { fn merge_field_call( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, definition: Definition<'db>, call: &ExprCall, call_type: Type<'db>, @@ -297,7 +306,8 @@ impl<'db> FieldMetadata<'db> { if let Some(init) = call.arguments.find_keyword("init") { let init = definition_expression_type(db, definition, &init.value); - self.init &= !init.bool(db).is_always_false(); + let env = ProgramEnvironment::from_definition(definition); + self.init &= !init.bool(db, &env).is_always_false(); } if let Some(alias) = call @@ -319,7 +329,7 @@ impl<'db> FieldMetadata<'db> { if let Some(frozen) = call.arguments.find_keyword("frozen") { let frozen = definition_expression_type(db, definition, &frozen.value); - self.frozen |= frozen.bool(db).is_always_true(); + self.frozen |= frozen.bool(db, env).is_always_true(); } } } @@ -327,13 +337,14 @@ impl<'db> FieldMetadata<'db> { /// Resolve a Pydantic field's metadata from its annotation and right-hand side. pub(in crate::types) fn field_metadata<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, definition: Option>, rhs_type: Option>, specialization: Option>, ) -> FieldMetadata<'db> { let mut metadata = FieldMetadata::default(); if let Some(definition) = definition { - metadata.collect_from_annotation(db, definition, specialization); + metadata.collect_from_annotation(db, env, definition, specialization); } metadata.collect_from_rhs_type(db, rhs_type, specialization); metadata @@ -491,13 +502,24 @@ fn config_boolean( }) } -pub(in crate::types) fn is_model(db: &dyn Db, class: StaticClassLiteral<'_>) -> bool { +pub(in crate::types) fn is_model<'db>(db: &'db dyn Db, class: StaticClassLiteral<'db>) -> bool { class .iter_mro(db, None) .filter_map(ClassBase::into_class) .any(|base| base.is_known(db, KnownClass::PydanticBaseModel)) } +/// Return whether `ty` is an instance of a Pydantic model. +pub(in crate::types) fn is_model_instance( + db: &dyn Db, + env: &ProgramEnvironment<'_>, + ty: Type<'_>, +) -> bool { + ty.nominal_class(db, env) + .and_then(|class| class.static_class_literal(db)) + .is_some_and(|(class, _)| is_model(db, class)) +} + /// Return whether a field specifier's `default` argument provides a default value. /// /// Pydantic's `Field(...)` uses the ellipsis as a required-field sentinel, so it does not provide @@ -651,7 +673,7 @@ fn own_model_config(db: &dyn Db, class: StaticClassLiteral<'_>) -> Option assignment.value(&module), @@ -769,7 +791,7 @@ fn model_config_from_dict(db: &dyn Db, definition: Definition<'_>, dict: &ExprDi fn class_keyword_config(db: &dyn Db, class: StaticClassLiteral<'_>) -> ModelConfig { let definition = class.definition(db); - let module = parsed_module(db, class.file(db)).load(db); + let module = parsed_module(db, class.python_file(db)).load(db); let kind = definition.kind(db); let Some(class) = kind.as_class() else { return ModelConfig::default(); @@ -831,7 +853,8 @@ pub(in crate::types) fn constructor_parameter_type<'db>( return field_type; } - lax_input_type(db, field_type) + let env = ProgramEnvironment::from_scope(class.body_scope(db)); + lax_input_type(db, &env, field_type) } /// Return whether `field_name` has a Pydantic field validator that receives the raw input. @@ -891,7 +914,7 @@ fn function_has_before_or_plain_field_validator<'db>( let DefinitionKind::Function(function) = definition.kind(db) else { return false; }; - let module = parsed_module(db, definition.file(db)).load(db); + let module = parsed_module(db, definition.python_file(db)).load(db); let function_node = function.node(&module); if function_node.decorator_list.is_empty() { return false; @@ -934,12 +957,17 @@ fn function_has_before_or_plain_field_validator<'db>( } /// Return the documented Python input type accepted by Pydantic for `field_type` in lax mode. -fn lax_input_type<'db>(db: &'db dyn Db, field_type: Type<'db>) -> Type<'db> { - lax_input_type_impl(db, field_type, &mut FxHashSet::default()) +fn lax_input_type<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + field_type: Type<'db>, +) -> Type<'db> { + lax_input_type_impl(db, env, field_type, &mut FxHashSet::default()) } fn lax_input_type_impl<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, field_type: Type<'db>, expanding_types: &mut FxHashSet>, ) -> Type<'db> { @@ -951,35 +979,36 @@ fn lax_input_type_impl<'db>( if !expanding_types.insert(field_type) { return Type::any(); } - let result = lax_input_type_impl(db, alias.value_type(db), expanding_types); + let result = lax_input_type_impl(db, env, alias.value_type(db), expanding_types); expanding_types.remove(&field_type); return result; } if field_type.as_union().and_then(|union| union.known(db)) == Some(KnownUnion::Float) { - return lax_alias(db, "LaxFloat"); + return lax_alias(db, env, "LaxFloat"); } if let Type::Union(union) = field_type { return UnionType::from_elements_leave_aliases( db, + env, union .elements(db) .iter() - .map(|element| lax_input_type_impl(db, *element, expanding_types)), + .map(|element| lax_input_type_impl(db, env, *element, expanding_types)), ); } - if let Some(input_type) = root_model_input_type(db, field_type, expanding_types) { + if let Some(input_type) = root_model_input_type(db, env, field_type, expanding_types) { return input_type; } - if let Some(input_type) = model_input_type(db, field_type) { + if let Some(input_type) = model_input_type(db, env, field_type) { return input_type; } let known_class = field_type - .nominal_class(db) + .nominal_class(db, env) .and_then(|class| class.known(db)); if matches!( @@ -994,25 +1023,29 @@ fn lax_input_type_impl<'db>( | KnownClass::Tuple ) ) { - let Ok(elements) = field_type.try_iterate(db) else { + let Ok(elements) = field_type.try_iterate(db, env) else { return Type::any(); }; - let element_type = - lax_input_type_impl(db, elements.homogeneous_element_type(db), expanding_types); - return KnownClass::Iterable.to_specialized_instance(db, &[element_type]); + let element_type = lax_input_type_impl( + db, + env, + elements.homogeneous_element_type(db, env), + expanding_types, + ); + return KnownClass::Iterable.to_specialized_instance(db, env, &[element_type]); } if matches!(known_class, Some(KnownClass::Dict | KnownClass::Mapping)) { - let Some(specialization) = - known_class.and_then(|known_class| field_type.known_specialization(db, known_class)) + let Some(specialization) = known_class + .and_then(|known_class| field_type.known_specialization(db, env, known_class)) else { return Type::any(); }; let [key_type, value_type] = specialization.types(db) else { return Type::any(); }; - let value_type = lax_input_type_impl(db, *value_type, expanding_types); - return KnownClass::Mapping.to_specialized_instance(db, &[*key_type, value_type]); + let value_type = lax_input_type_impl(db, env, *value_type, expanding_types); + return KnownClass::Mapping.to_specialized_instance(db, env, &[*key_type, value_type]); } let builtin_alias = match known_class { @@ -1025,10 +1058,10 @@ fn lax_input_type_impl<'db>( _ => None, }; if let Some(alias) = builtin_alias { - return lax_alias(db, alias); + return lax_alias(db, env, alias); } - let Some((module, symbol, class)) = instance_symbol(db, field_type) else { + let Some((module, symbol, class)) = instance_symbol(db, env, field_type) else { return Type::any(); }; let symbol_alias = match (module, symbol) { @@ -1048,23 +1081,23 @@ fn lax_input_type_impl<'db>( _ => None, }; if let Some(alias) = symbol_alias { - return lax_alias(db, alias); + return lax_alias(db, env, alias); } let alias = if (module, symbol) == (KnownModule::Re, "Pattern") { - let Some(specialization) = field_type.specialization_of(db, class) else { + let Some(specialization) = field_type.specialization_of(db, env, class) else { return Type::any(); }; let [pattern_type] = specialization.types(db) else { return Type::any(); }; if pattern_type - .nominal_class(db) + .nominal_class(db, env) .is_some_and(|class| class.is_known(db, KnownClass::Str)) { "LaxStrPattern" } else if pattern_type - .nominal_class(db) + .nominal_class(db, env) .is_some_and(|class| class.is_known(db, KnownClass::Bytes)) { "LaxBytesPattern" @@ -1075,7 +1108,7 @@ fn lax_input_type_impl<'db>( return Type::any(); }; - lax_alias(db, alias) + lax_alias(db, env, alias) } /// Return the input type accepted for a Pydantic root model field. @@ -1085,10 +1118,13 @@ fn lax_input_type_impl<'db>( /// `IntList` instance and an `Iterable[LaxInt]`. fn root_model_input_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, field_type: Type<'db>, expanding_types: &mut FxHashSet>, ) -> Option> { - let (class, specialization) = field_type.nominal_class(db)?.static_class_literal(db)?; + let (class, specialization) = field_type + .nominal_class(db, env)? + .static_class_literal(db)?; if !is_root_model(db, class) { return None; } @@ -1105,15 +1141,16 @@ fn root_model_input_type<'db>( if !expanding_types.insert(field_type) { return Some(Type::any()); } - let root_input_type = lax_input_type_impl(db, root_field.declared_ty, expanding_types); + let root_input_type = lax_input_type_impl(db, env, root_field.declared_ty, expanding_types); expanding_types.remove(&field_type); // In lax mode, Pydantic accepts a Box[str] when a Box[int] is expected, so we widen // to a gradual specialization here. Widening to `Box[LaxStr]` would only work for // covariant generics. - let model_instance = Type::instance(db, class.unknown_specialization(db)); + let model_instance = Type::instance(db, env, class.unknown_specialization(db)); Some(UnionType::from_two_elements( db, + env, model_instance, root_input_type, )) @@ -1124,8 +1161,14 @@ fn root_model_input_type<'db>( /// By default, Pydantic accepts either an instance of the model or a mapping of string keys to /// input values. Other custom validators can accept additional input types, which are not modeled /// here. -fn model_input_type<'db>(db: &'db dyn Db, field_type: Type<'db>) -> Option> { - let (class, _) = field_type.nominal_class(db)?.static_class_literal(db)?; +fn model_input_type<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + field_type: Type<'db>, +) -> Option> { + let (class, _) = field_type + .nominal_class(db, env)? + .static_class_literal(db)?; if !is_model(db, class) || is_root_model(db, class) { return None; } @@ -1138,25 +1181,34 @@ fn model_input_type<'db>(db: &'db dyn Db, field_type: Type<'db>) -> Option( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, ) -> Option<(KnownModule, &'db str, StaticClassLiteral<'db>)> { - let class = ty.nominal_class(db)?.class_literal(db).as_static()?; - let module = file_to_module(db, class.file(db))?.known(db)?; + let class = ty.nominal_class(db, env)?.class_literal(db).as_static()?; + let module = file_to_module(db, class.program_file(db).resolver_file(db))?.known(db)?; Some((module, class.name(db).as_str(), class)) } /// Return a lax-input alias like `LaxInt` from `ty_extensions.pydantic`. -fn lax_alias<'db>(db: &'db dyn Db, name: &str) -> Type<'db> { - match known_module_symbol(db, KnownModule::TyExtensionsPydantic, name) +fn lax_alias<'db>(db: &'db dyn Db, env: &ProgramEnvironment<'db>, name: &str) -> Type<'db> { + match known_module_symbol(db, env, KnownModule::TyExtensionsPydantic, name) .place .ignore_possibly_undefined() { @@ -1243,7 +1295,7 @@ pub(in crate::types) fn model_init_accepts_extra( } /// Return `true` if extra keywords passed to `class` are silently discarded by Pydantic. -pub(in crate::types) fn model_init_discards_extra( +fn model_init_discards_extra( db: &dyn Db, class: StaticClassLiteral<'_>, metadata: ModelMetadata<'_>, diff --git a/crates/ty_python_semantic/src/types/dedicated/pytest.rs b/crates/ty_python_semantic/src/types/dedicated/pytest.rs index 81a8ac2f23..ba9f3bb684 100644 --- a/crates/ty_python_semantic/src/types/dedicated/pytest.rs +++ b/crates/ty_python_semantic/src/types/dedicated/pytest.rs @@ -30,6 +30,7 @@ use ty_python_core::semantic_index; use crate::Db; use crate::place::known_module_symbol; +use crate::types::ProgramEnvironment; use crate::types::dedicated::role::function_framework_role; use crate::types::{ FunctionType, KnownClass, KnownFunction, Type, definition_expression_type, @@ -67,7 +68,7 @@ pub(in crate::types) fn is_fixture_function<'db>( fn fixture_marker<'db>(db: &'db dyn Db, function: FunctionType<'db>) -> Option { let file = function.file(db); let definition = function.definition(db); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, db.program_file(file).python_file(db)).load(db); let node = function.node(db, file, &module); let types = infer_definition_types(db, definition); @@ -122,8 +123,8 @@ pub(in crate::types) fn module_fixtures( db: &dyn Db, file: File, ) -> FxHashMap> { - let parsed = parsed_module(db, file).load(db); - let index = semantic_index(db, file); + let parsed = parsed_module(db, db.program_file(file).python_file(db)).load(db); + let index = semantic_index(db, db.program_file(file)); let mut fixtures = FxHashMap::default(); for statement in parsed.suite() { @@ -182,26 +183,28 @@ pub(in crate::types) fn conftest_chain(db: &dyn Db, file: File) -> Vec { /// then the builtin fixtures. the first hit wins. pub(in crate::types) fn resolve_fixture<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, name: &str, ) -> Option> { if let Some(function) = module_fixtures(db, file).get(name) { - return Some(resolved_from_function(db, *function)); + return Some(resolved_from_function(db, env, *function)); } for conftest in conftest_chain(db, file) { if let Some(function) = module_fixtures(db, *conftest).get(name) { - return Some(resolved_from_function(db, *function)); + return Some(resolved_from_function(db, env, *function)); } } - resolve_builtin_fixture(db, name) + resolve_builtin_fixture(db, env, name) } fn resolved_from_function<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, function: FunctionType<'db>, ) -> ResolvedFixture<'db> { ResolvedFixture { - provided_type: fixture_provided_type(db, function), + provided_type: fixture_provided_type(db, env, function), definition: Some(function.definition(db)), } } @@ -223,14 +226,18 @@ const BUILTIN_FIXTURE_MODULES: &[(&str, &str)] = &[ ("pytestconfig", "_pytest.fixtures"), ]; -fn resolve_builtin_fixture<'db>(db: &'db dyn Db, name: &str) -> Option> { +fn resolve_builtin_fixture<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, +) -> Option> { // `request` is injected by pytest itself rather than defined as a // fixture function; its type is `FixtureRequest` if name == "request" { - let request = known_module_symbol(db, KnownModule::PytestFixtures, "FixtureRequest") + let request = known_module_symbol(db, env, KnownModule::PytestFixtures, "FixtureRequest") .place .ignore_possibly_undefined()? - .to_instance_approximation(db)?; + .to_instance_approximation(db, env)?; return Some(ResolvedFixture { provided_type: Some(request), definition: None, @@ -240,9 +247,13 @@ fn resolve_builtin_fixture<'db>(db: &'db dyn Db, name: &str) -> Option(db: &'db dyn Db, name: &str) -> Option T` fixture provides `T`. pub(in crate::types) fn fixture_provided_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, function: FunctionType<'db>, ) -> Option> { let signature = function.signature(db); @@ -260,11 +272,15 @@ pub(in crate::types) fn fixture_provided_type<'db>( if return_type.is_dynamic() { return None; } - Some(unwrap_generator(db, return_type)) + Some(unwrap_generator(db, env, return_type)) } /// the yielded element of a generator/iterator type, or `ty` unchanged. -fn unwrap_generator<'db>(db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { +fn unwrap_generator<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Type<'db> { for known in [ KnownClass::Generator, KnownClass::AsyncGenerator, @@ -272,7 +288,7 @@ fn unwrap_generator<'db>(db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { KnownClass::AsyncIterator, KnownClass::Iterable, ] { - if let Some(specialization) = ty.known_specialization(db, known) { + if let Some(specialization) = ty.known_specialization(db, env, known) { if let Some(element) = specialization.types(db).first() { return *element; } @@ -283,15 +299,20 @@ fn unwrap_generator<'db>(db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { /// `true` if `ty` is an instance of pytest's `MarkGenerator` — the type of /// `pytest.mark`, whose `.parametrize` attribute builds the decorator. -pub(in crate::types) fn is_mark_generator<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { +pub(in crate::types) fn is_mark_generator<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> bool { let Some(class) = ty - .nominal_class(db) + .nominal_class(db, env) .and_then(|class| class.class_literal(db).as_static()) else { return false; }; class.name(db).as_str() == "MarkGenerator" - && file_to_module(db, class.file(db)).and_then(|module| module.known(db)) + && file_to_module(db, class.program_file(db).resolver_file(db)) + .and_then(|module| module.known(db)) == Some(KnownModule::PytestMarkStructures) } @@ -311,6 +332,7 @@ pub(in crate::types) struct ParametrizeMarker<'ast> { /// not static literals (dynamic → not checkable). pub(in crate::types) fn parametrize_marker<'ast>( db: &dyn Db, + env: &ProgramEnvironment<'_>, function: FunctionType<'_>, decorator: &'ast ast::Decorator, ) -> Option> { @@ -324,7 +346,7 @@ pub(in crate::types) fn parametrize_marker<'ast>( return None; } let types = infer_definition_types(db, function.definition(db)); - if !is_mark_generator(db, types.expression_type(attribute.value.as_ref())) { + if !is_mark_generator(db, env, types.expression_type(attribute.value.as_ref())) { return None; } let argnames = call.arguments.find_argument_value("argnames", 0)?; @@ -345,13 +367,14 @@ pub(in crate::types) fn parametrized_names( db: &dyn Db, function: FunctionType<'_>, ) -> FxHashSet { + let env = &ProgramEnvironment::from_file(function.program_file(db)); let file = function.file(db); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, db.program_file(file).python_file(db)).load(db); let mut names: FxHashSet = function .node(db, file, &module) .decorator_list .iter() - .filter_map(|decorator| parametrize_marker(db, function, decorator)) + .filter_map(|decorator| parametrize_marker(db, env, function, decorator)) .flat_map(|marker| marker.names) .collect(); names.shrink_to_fit(); @@ -367,6 +390,7 @@ pub(in crate::types) fn parametrized_names( /// fixture resolves or the resolved one's type cannot be derived. pub(in crate::types) fn injected_parameter_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, function: FunctionType<'db>, name: &str, ) -> Option> { @@ -374,7 +398,7 @@ pub(in crate::types) fn injected_parameter_type<'db>( if parametrized_names(db, function).contains(name) { return None; } - resolve_fixture(db, function.file(db), name)?.provided_type + resolve_fixture(db, env, function.file(db), name)?.provided_type } /// parse `@pytest.mark.parametrize` argnames — a comma-separated string or a diff --git a/crates/ty_python_semantic/src/types/dedicated/role.rs b/crates/ty_python_semantic/src/types/dedicated/role.rs index 0ffbf95235..ab663cb8ba 100644 --- a/crates/ty_python_semantic/src/types/dedicated/role.rs +++ b/crates/ty_python_semantic/src/types/dedicated/role.rs @@ -119,8 +119,8 @@ mod tests { .build()?; let file = system_path_to_file(&db, "/src/main.py")?; - let module = parsed_module(&db, file).load(&db); - let model = SemanticModel::new(&db, file); + let module = parsed_module(&db, db.program_file(file).python_file(&db)).load(&db); + let model = SemanticModel::new(&db, crate::Db::program_file(&db, file)); let class_def = module .suite() .iter() @@ -153,8 +153,8 @@ mod tests { .build()?; let file = system_path_to_file(&db, "/src/main.py")?; - let module = parsed_module(&db, file).load(&db); - let model = SemanticModel::new(&db, file); + let module = parsed_module(&db, db.program_file(file).python_file(&db)).load(&db); + let model = SemanticModel::new(&db, crate::Db::program_file(&db, file)); let class_def = module .suite() .iter() @@ -219,8 +219,8 @@ mod tests { .build()?; let file = system_path_to_file(&db, "/src/main.py")?; - let module = parsed_module(&db, file).load(&db); - let model = SemanticModel::new(&db, file); + let module = parsed_module(&db, db.program_file(file).python_file(&db)).load(&db); + let model = SemanticModel::new(&db, crate::Db::program_file(&db, file)); let class_def = module .suite() .iter() @@ -314,8 +314,8 @@ mod tests { ) .build()?; let file = system_path_to_file(&db, "/src/main.py")?; - let module = parsed_module(&db, file).load(&db); - let model = SemanticModel::new(&db, file); + let module = parsed_module(&db, db.program_file(file).python_file(&db)).load(&db); + let model = SemanticModel::new(&db, crate::Db::program_file(&db, file)); let class_def = module .suite() .iter() @@ -355,8 +355,8 @@ mod tests { let db = builder.build()?; let file = system_path_to_file(&db, target)?; - let module = parsed_module(&db, file).load(&db); - let index = semantic_index(&db, file); + let module = parsed_module(&db, db.program_file(file).python_file(&db)).load(&db); + let index = semantic_index(&db, db.program_file(file)); let function_node = module .suite() .iter() diff --git a/crates/ty_python_semantic/src/types/dedicated/sqlalchemy.rs b/crates/ty_python_semantic/src/types/dedicated/sqlalchemy.rs index 464b72728d..05f13485ba 100644 --- a/crates/ty_python_semantic/src/types/dedicated/sqlalchemy.rs +++ b/crates/ty_python_semantic/src/types/dedicated/sqlalchemy.rs @@ -10,6 +10,7 @@ //! `docs/basedpython/frameworks/sqlalchemy.md` use crate::Db; +use crate::types::ProgramEnvironment; use crate::types::{ClassBase, KnownClass, StaticClassLiteral, Type}; /// `class` is a sqlalchemy 2.0 declarative model: `DeclarativeBase` in its @@ -38,10 +39,11 @@ pub(in crate::types) fn is_declarative(db: &dyn Db, class: StaticClassLiteral<'_ /// and are therefore not fields pub(in crate::types) fn mapped_field_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, declared_ty: Type<'db>, ) -> Option> { - let mapped = KnownClass::SqlalchemyMapped.try_to_class_literal(db)?; - let specialization = declared_ty.specialization_of(db, mapped)?; + let mapped = KnownClass::SqlalchemyMapped.try_to_class_literal(db, env)?; + let specialization = declared_ty.specialization_of(db, env, mapped)?; let [element] = specialization.types(db) else { return None; }; diff --git a/crates/ty_python_semantic/src/types/deferred.rs b/crates/ty_python_semantic/src/types/deferred.rs index 465dc1e85e..595d86bf40 100644 --- a/crates/ty_python_semantic/src/types/deferred.rs +++ b/crates/ty_python_semantic/src/types/deferred.rs @@ -43,6 +43,7 @@ use super::infer::{ deferred_comparison, fold_tuple_concat, fold_tuple_repeat, literal_binary_op, literal_unary_op, }; use super::visitor::{self, any_over_type}; +use crate::types::ProgramEnvironment; use crate::types::call::CallArguments; use crate::types::match_type::{MatchTypeOutcome, evaluate_match_type}; use crate::types::type_fn::{ @@ -160,13 +161,14 @@ impl<'db> DeferredType<'db> { /// (non-literal) result (`int + Literal[1]` → `int`). pub(crate) fn build( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, operation: &DeferredOperation, operands: Box<[Type<'db>]>, ) -> Type<'db> { if operation .deferring_operands(&operands) .iter() - .any(|operand| operand_is_symbolic(db, *operand)) + .any(|operand| operand_is_symbolic(db, env, *operand)) { // an operation nested deeper than anything a type expression could spell is not a // relationship anybody wrote down: it is a value a loop is accumulating, one layer @@ -176,19 +178,23 @@ impl<'db> DeferredType<'db> { if operands.iter().any(|operand| { deferral_depth(db, *operand, DEFERRAL_DEPTH_LIMIT) >= DEFERRAL_DEPTH_LIMIT }) { - return reduce(db, operation, &operands); + return reduce(db, env, operation, &operands); } return Type::Deferred(Self::new(db, operation.clone(), operands)); } - evaluate(db, operation, &operands).unwrap_or_else(Type::unknown) + evaluate(db, env, operation, &operands).unwrap_or_else(Type::unknown) } /// Whether an operation over these operands must be deferred, i.e. some operand /// still mentions a type parameter. - pub(crate) fn is_deferred(db: &'db dyn Db, operands: &[Type<'db>]) -> bool { + pub(crate) fn is_deferred( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + operands: &[Type<'db>], + ) -> bool { operands .iter() - .any(|operand| operand_is_symbolic(db, *operand)) + .any(|operand| operand_is_symbolic(db, env, *operand)) } /// The non-symbolic meaning of the operation: what the result type would be if @@ -196,15 +202,20 @@ impl<'db> DeferredType<'db> { /// reduces to `int`, a comparison reduces to `bool`. Every non-mapping operation /// delegates here, so a deferred operation is indistinguishable from its reduced /// form everywhere except under type-mapping. - pub(crate) fn reduced(self, db: &'db dyn Db) -> Type<'db> { - reduce(db, self.operation(db), self.operands(db)) + pub(crate) fn reduced(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + reduce(db, env, self.operation(db), self.operands(db)) } /// Re-evaluate the operation against operands to which a type-mapping has already /// been applied. Folds when the operands became concrete, stays symbolic while /// still unresolved. - pub(crate) fn re_evaluate(self, db: &'db dyn Db, operands: Box<[Type<'db>]>) -> Type<'db> { - Self::build(db, self.operation(db), operands) + pub(crate) fn re_evaluate( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + operands: Box<[Type<'db>]>, + ) -> Type<'db> { + Self::build(db, env, self.operation(db), operands) } /// basedpython: whether this deferral is an attribute type (`T.a`). @@ -237,9 +248,13 @@ pub(crate) const fn is_symbolic_operand(ty: Type<'_>) -> bool { /// can be compared against a declared return type. A non-integer operand — `str` /// concatenation, say — has no such form, so keeping its operation symbolic would buy a /// weaker relation and nothing else. -pub(crate) fn is_integer_operand<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { - ty.reduce_deferred(db) - .is_subtype_of(db, KnownClass::Int.to_instance(db)) +pub(crate) fn is_integer_operand<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> bool { + ty.reduce_deferred(db, env) + .is_subtype_of(db, env, KnownClass::Int.to_instance(db, env)) } /// basedpython: an integer expression over type parameters flattened to @@ -391,9 +406,13 @@ impl<'db> LinearForm<'db> { impl<'db> Type<'db> { /// Collapse a top-level [`DeferredType`] to its reduced form; any other type is /// returned unchanged. - pub(crate) fn reduce_deferred(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn reduce_deferred( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { match self { - Type::Deferred(deferred) => deferred.reduced(db), + Type::Deferred(deferred) => deferred.reduced(db, env), _ => self, } } @@ -426,6 +445,7 @@ const DEFERRAL_DEPTH_LIMIT: usize = 8; /// each operand were replaced by its upper bound. fn reduce<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, operation: &DeferredOperation, operands: &[Type<'db>], ) -> Type<'db> { @@ -449,7 +469,7 @@ fn reduce<'db>( let operands: Box<[Type<'db>]> = operands .iter() - .map(|operand| operand.reduce_deferred(db)) + .map(|operand| operand.reduce_deferred(db, env)) .collect(); // an attribute type is the one operation whose receiver has to be substituted rather @@ -463,17 +483,17 @@ fn reduce<'db>( let receiver = match receiver.as_typevar() { Some(bound_typevar) => bound_typevar .typevar(db) - .require_bound_or_constraints(db) - .as_type(db), + .require_bound_or_constraints(db, env) + .as_type(db, env), None => *receiver, }; return receiver - .member(db, name) + .member(db, env, name) .ignore_possibly_undefined() .unwrap_or_else(Type::unknown); } - evaluate(db, operation, &operands).unwrap_or_else(Type::unknown) + evaluate(db, env, operation, &operands).unwrap_or_else(Type::unknown) } /// Evaluate a deferred operation against operands that no longer mention a type @@ -481,32 +501,33 @@ fn reduce<'db>( /// is genuinely unsupported between the operands. fn evaluate<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, operation: &DeferredOperation, operands: &[Type<'db>], ) -> Option> { match *operation { DeferredOperation::Binary(op) => { let [left, right] = operands else { return None }; - literal_binary_op(db, *left, *right, op, true) + literal_binary_op(db, env, *left, *right, op, true) // the same tuple folds the value inferrer applies: without them // `(X,) * Dim` would re-evaluate through typeshed's `tuple.__mul__` and // widen to `tuple[X, ...]`, throwing away the length the fold just learned .or_else(|| match op { - ast::Operator::Mult => fold_tuple_repeat(db, *left, *right) - .or_else(|| fold_tuple_repeat(db, *right, *left)), - ast::Operator::Add => fold_tuple_concat(db, *left, *right), + ast::Operator::Mult => fold_tuple_repeat(db, env, *left, *right) + .or_else(|| fold_tuple_repeat(db, env, *right, *left)), + ast::Operator::Add => fold_tuple_concat(db, env, *left, *right), _ => None, }) - .or_else(|| Type::try_call_bin_op_return_type(db, *left, op, *right)) + .or_else(|| Type::try_call_bin_op_return_type(db, env, *left, op, *right)) } DeferredOperation::Attribute(ref name) => { let [receiver] = operands else { return None }; - receiver.member(db, name).ignore_possibly_undefined() + receiver.member(db, env, name).ignore_possibly_undefined() } DeferredOperation::Unary(op) => { let [operand] = operands else { return None }; if let Type::LiteralValue(literal) = operand - && let Some(folded) = literal_unary_op(db, op, *literal) + && let Some(folded) = literal_unary_op(db, env, op, *literal) { return Some(folded); } @@ -517,9 +538,15 @@ fn evaluate<'db>( _ => return None, }; operand - .try_call_dunder(db, dunder, CallArguments::none(), TypeContext::default()) + .try_call_dunder( + db, + env, + dunder, + CallArguments::none(), + TypeContext::default(), + ) .ok() - .map(|bindings| bindings.return_type(db)) + .map(|bindings| bindings.return_type(db, env)) } DeferredOperation::Call => { let [callee, args @ ..] = operands else { @@ -533,15 +560,15 @@ fn evaluate<'db>( let callee = match callee { Type::BoundMethod(method) => method .self_instance(db) - .member(db, method.function(db).name(db)) + .member(db, env, method.function(db).name(db)) .ignore_possibly_undefined() .unwrap_or(*callee), _ => *callee, }; callee - .try_call(db, &CallArguments::positional(args.iter().copied())) + .try_call(db, env, &CallArguments::positional(args.iter().copied())) .ok() - .map(|bindings| bindings.return_type(db)) + .map(|bindings| bindings.return_type(db, env)) } DeferredOperation::TypeFn => { let [Type::FunctionLiteral(function), arguments @ ..] = operands else { @@ -584,8 +611,8 @@ fn evaluate<'db>( // rich comparisons fold to a `Literal[bool]` for literal operands and to // `bool` otherwise; identity/membership operators fall back to `bool` Some( - deferred_comparison(db, *left, op, *right) - .unwrap_or_else(|| KnownClass::Bool.to_instance(db)), + deferred_comparison(db, env, *left, op, *right) + .unwrap_or_else(|| KnownClass::Bool.to_instance(db, env)), ) } } @@ -593,8 +620,8 @@ fn evaluate<'db>( /// Whether `ty` still mentions a type parameter (or a nested deferred operation), /// meaning an operation over it cannot be evaluated yet. -fn operand_is_symbolic<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { - any_over_type(db, ty, false, |t| { +fn operand_is_symbolic<'db>(db: &'db dyn Db, env: &ProgramEnvironment<'db>, ty: Type<'db>) -> bool { + any_over_type(db, env, ty, false, |t| { t.as_typevar().is_some() || matches!(t, Type::Deferred(_)) }) } diff --git a/crates/ty_python_semantic/src/types/definition.rs b/crates/ty_python_semantic/src/types/definition.rs index 1720dad7df..000eb3d00a 100644 --- a/crates/ty_python_semantic/src/types/definition.rs +++ b/crates/ty_python_semantic/src/types/definition.rs @@ -33,7 +33,7 @@ impl TypeDefinition<'_> { | Self::SpecialForm(definition) | Self::NewType(definition) | Self::EnumMember(definition) => { - let module = parsed_module(db, definition.file(db)).load(db); + let module = parsed_module(db, definition.python_file(db)).load(db); Some(definition.focus_range(db, &module)) } } @@ -54,7 +54,7 @@ impl TypeDefinition<'_> { | Self::SpecialForm(definition) | Self::NewType(definition) | Self::EnumMember(definition) => { - let module = parsed_module(db, definition.file(db)).load(db); + let module = parsed_module(db, definition.python_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 index 9b565dffc4..ed01f24f26 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -5,12 +5,11 @@ use super::{ CallArguments, CallDunderError, ClassBase, ClassLiteral, GenericAlias, KnownClass, StaticClassLiteral, add_inferred_python_version_hint_to_diagnostic, }; -use crate::diagnostic::did_you_mean; -use crate::diagnostic::format_enumeration; +use crate::diagnostic::{did_you_mean, format_enumeration}; use crate::lint::{Level, LintRegistryBuilder, LintStatus}; use crate::place::{DefinedPlace, Place, place_from_bindings}; use crate::suppression::FileSuppressionId; -use crate::types::call::CallError; +use crate::types::call::{CallDiagnosticOverride, CallError}; use crate::types::class::{ CodeGeneratorKind, DisjointBase, DisjointBaseKind, ExpandedClassBaseEntry, MethodDecorator, }; @@ -18,6 +17,7 @@ use crate::types::function::{FunctionDecorators, FunctionType, KnownFunction, Ov use crate::types::infer::UnsupportedComparisonError; use crate::types::overrides::MethodKind; use crate::types::protocol_class::ProtocolMember; +use crate::types::special_form::TypeQualifier; use crate::types::string_annotation::{ ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION, IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, INVALID_SYNTAX_IN_FORWARD_ANNOTATION, RAW_STRING_TYPE_ANNOTATION, @@ -26,19 +26,20 @@ use crate::types::tuple::TupleSpec; use crate::types::typed_dict::TypedDictSchema; use crate::types::typevar::TypeVarInstance; use crate::types::{ - BoundTypeVarInstance, ClassType, DynamicType, ErrorContextTree, LintDiagnosticGuard, Protocol, - ProtocolInstanceType, SpecialFormType, SubclassOfInner, Type, TypeContext, TypeVarVariance, - binding_type, protocol_class::ProtocolClass, + BoundTypeVarInstance, ClassType, DynamicType, ErrorContextTree, LintDiagnosticGuard, + SpecialFormType, SubclassOfInner, Type, TypeContext, TypeVarVariance, binding_type, + protocol_class::ProtocolClass, }; use crate::types::{ KnownInstanceType, LiteralValueTypeKind, MemberLookupPolicy, TypeVarKind, TypedDictType, UnionType, }; -use crate::{Db, DisplaySettings, FxIndexMap, Program, declare_lint}; +use crate::{Db, DisplaySettings, FxIndexMap, ProgramEnvironment, declare_lint}; use itertools::Itertools; use ruff_db::source::source_text; use ruff_db::{ diagnostic::{Annotation, Diagnostic, Span, SubDiagnostic, SubDiagnosticSeverity}, + files::File, parsed::parsed_module, }; use ruff_diagnostics::{Edit, Fix, IsolationLevel}; @@ -52,7 +53,7 @@ use std::fmt::{self, Formatter}; use ty_module_resolver::{KnownModule, Module, ModuleName, file_to_module}; use ty_python_core::definition::{Definition, DefinitionKind}; use ty_python_core::place::{PlaceTable, ScopedPlaceId}; -use ty_python_core::{global_scope, place_table, use_def_map}; +use ty_python_core::{ProgramFile, global_scope, place_table, use_def_map}; const RUNTIME_CHECKABLE_DOCS_URL: &str = "https://docs.python.org/3/library/typing.html#typing.runtime_checkable"; @@ -84,7 +85,9 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { registry.register_lint(&ISINSTANCE_AGAINST_TYPED_DICT); registry.register_lint(&INVALID_ARGUMENT_TYPE); registry.register_lint(&INVALID_RETURN_TYPE); + registry.register_lint(&UNSOUND_RETURN_STATEMENT); registry.register_lint(&INVALID_YIELD); + registry.register_lint(&UNSOUND_YIELD); registry.register_lint(&INVALID_ASSIGNMENT); registry.register_lint(&REFUTABLE_DESTRUCTURING); registry.register_lint(&ITERATION_OVER_CHARACTER); @@ -180,6 +183,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { registry.register_lint(&FINAL_ON_NON_METHOD); registry.register_lint(&FINAL_ON_VARIABLE); registry.register_lint(&FINAL_WITHOUT_VALUE); + registry.register_lint(&ABSTRACT_AND_FINAL_METHOD); registry.register_lint(&ABSTRACT_METHOD_IN_FINAL_CLASS); registry.register_lint(&CALL_ABSTRACT_METHOD); registry.register_lint(&TYPE_ASSERTION_FAILURE); @@ -478,6 +482,16 @@ declare_lint! { } } +declare_lint! { + #[expect(clippy::doc_link_with_quotes)] + #[doc = include_str!("../../resources/lint_docs/unsound-return-statement.md")] + pub(crate) static UNSOUND_RETURN_STATEMENT = { + summary: "detects return statements that unsoundly return a type that is not a subtype of the function's annotated return type", + status: LintStatus::stable("0.0.70"), + default_level: Level::Ignore, + } +} + declare_lint! { #[doc = include_str!("../../resources/lint_docs/invalid-yield.md")] pub(crate) static INVALID_YIELD = { @@ -487,6 +501,15 @@ declare_lint! { } } +declare_lint! { + #[doc = include_str!("../../resources/lint_docs/unsound-yield.md")] + pub(crate) static UNSOUND_YIELD = { + summary: "detects yield expressions that unsoundly yield a type that is not a subtype of the generator's annotated yield type", + status: LintStatus::stable("0.0.70"), + default_level: Level::Ignore, + } +} + declare_lint! { #[doc = include_str!("../../resources/lint_docs/empty-body.md")] pub(crate) static EMPTY_BODY = { @@ -2246,6 +2269,15 @@ declare_lint! { } } +declare_lint! { + #[doc = include_str!("../../resources/lint_docs/abstract-and-final-method.md")] + pub(crate) static ABSTRACT_AND_FINAL_METHOD = { + summary: "detects methods that are both abstract and final", + status: LintStatus::stable("0.0.64"), + default_level: Level::Error, + } +} + declare_lint! { #[doc = include_str!("../../resources/lint_docs/abstract-method-in-final-class.md")] pub(crate) static ABSTRACT_METHOD_IN_FINAL_CLASS = { @@ -2970,18 +3002,20 @@ pub(crate) fn report_mismatched_type_name<'db>( actual_name: Option<&str>, actual_name_ty: Type<'db>, ) { + let db = context.db(); if let Some(builder) = context.report_lint(&MISMATCHED_TYPE_NAME, node) { let mut diagnostic = builder.into_diagnostic(format_args!( "The name passed to `{constructor}` must match the variable it is assigned to" )); if let Some(actual_name) = actual_name { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Expected \"{expected_name}\", got \"{actual_name}\"" )); } else { - diagnostic.set_primary_message(format_args!( + let env = context.program_environment(); + diagnostic.set_primary_annotation_message(format_args!( "Expected \"{expected_name}\", got variable of type `{}`", - actual_name_ty.display(context.db()) + actual_name_ty.display(db, env) )); } } @@ -3026,7 +3060,7 @@ impl TypeCheckDiagnostics { self.diagnostics.is_empty() && self.used_suppressions.is_empty() } - pub fn iter(&self) -> std::slice::Iter<'_, Diagnostic> { + fn iter(&self) -> std::slice::Iter<'_, Diagnostic> { self.diagnostics().iter() } @@ -3069,12 +3103,14 @@ pub(super) fn report_index_out_of_bounds( length: impl std::fmt::Display, index: i64, ) { + let db = context.db(); let Some(builder) = context.report_lint(&INDEX_OUT_OF_BOUNDS, node) else { return; }; + let env = &context.program_environment(); builder.into_diagnostic(format_args!( "Index {index} is out of bounds for {kind} `{}` with length {length}", - tuple_ty.display(context.db()) + tuple_ty.display(db, env) )); } @@ -3085,18 +3121,20 @@ pub(super) fn report_not_subscriptable( not_subscriptable_ty: Type, method: &str, ) { + let db = context.db(); let Some(builder) = context.report_lint(&NOT_SUBSCRIPTABLE, node) else { return; }; + let env = &context.program_environment(); if method == "__delitem__" { builder.into_diagnostic(format_args!( "Cannot delete subscript on object of type `{}` with no `{method}` method", - not_subscriptable_ty.display(context.db()) + not_subscriptable_ty.display(db, env) )); } else { builder.into_diagnostic(format_args!( "Cannot subscript object of type `{}` with no `{method}` method", - not_subscriptable_ty.display(context.db()) + not_subscriptable_ty.display(db, env) )); } } @@ -3122,17 +3160,18 @@ pub(crate) fn is_invalid_typed_dict_literal( && matches!(source, AnyNodeRef::ExprDict(_)) } -fn report_invalid_assignment_with_message<'db, 'ctx: 'db, T: Ranged>( - context: &'ctx InferContext, +fn report_invalid_assignment_with_message<'db, 'env: 'db, T: Ranged>( + context: &'env InferContext, node: T, message: std::fmt::Arguments, -) -> Option> { +) -> Option> { let builder = context.report_lint(&INVALID_ASSIGNMENT, node)?; Some(builder.into_diagnostic(message)) } pub(super) fn note_numbers_module_not_supported<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, diag: &mut Diagnostic, target_ty: Type<'db>, value_ty: Type<'db>, @@ -3141,13 +3180,21 @@ pub(super) fn note_numbers_module_not_supported<'db>( [KnownClass::Int, KnownClass::Float, KnownClass::Complex]; if let Type::NominalInstance(target_instance) = target_ty { - let file = target_instance.class(db).class_literal(db).file(db); - if let Some(module) = file_to_module(db, file) + let file = target_instance + .class(db, env) + .class_literal(db) + .program_file(db); + if let Some(module) = file_to_module(db, file.resolver_file(db)) && module.is_known(db, KnownModule::Numbers) { let is_numeric = value_ty.is_subtype_of( db, - UnionType::from_elements(db, BUILTIN_NUMBERS.iter().map(|cls| cls.to_instance(db))), + env, + UnionType::from_elements( + db, + env, + BUILTIN_NUMBERS.iter().map(|cls| cls.to_instance(db, env)), + ), ); if is_numeric { @@ -3181,7 +3228,8 @@ fn covariant_supertype_hint<'db>( ), [1], ) => Some( - "Consider using the supertype `collections.abc.Mapping`, which is covariant in its value type", + "Consider using the supertype `collections.abc.Mapping`, \ + which is covariant in its value type", ), _ => None, } @@ -3191,15 +3239,16 @@ fn covariant_supertype_hint<'db>( /// that fails due to invariance. pub(super) fn add_invariant_generic_hints<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, diag: &mut Diagnostic, expected_ty: Type<'db>, provided_ty: Type<'db>, ) { - let Some((expected_class, expected_specialization)) = expected_ty.class_specialization(db) + let Some((expected_class, expected_specialization)) = expected_ty.class_specialization(db, env) else { return; }; - let Some((provided_class, provided_specialization)) = provided_ty.class_specialization(db) + let Some((provided_class, provided_specialization)) = provided_ty.class_specialization(db, env) else { return; }; @@ -3220,13 +3269,13 @@ pub(super) fn add_invariant_generic_hints<'db>( .enumerate() .filter_map(|(index, ((bound_typevar, expected_arg), provided_arg))| { (bound_typevar.variance(db) == TypeVarVariance::Invariant - && !expected_arg.is_equivalent_to(db, *provided_arg)) + && !expected_arg.is_equivalent_to(db, env, *provided_arg)) .then_some((index, expected_arg, provided_arg)) }); let mut mismatch_indices = Vec::new(); for (index, expected_arg, provided_arg) in mismatched_invariant_arguments { - if !provided_arg.is_assignable_to(db, *expected_arg) { + if !provided_arg.is_assignable_to(db, env, *expected_arg) { return; } mismatch_indices.push(index); @@ -3320,6 +3369,7 @@ pub(super) fn report_bool_as_int<'db>( value_ty: Type<'db>, target_ty: Type<'db>, ) { + let env = context.program_environment(); let db = context.db(); if !is_boolean_type(db, value_ty) || !target_admits_bool_via_int(db, target_ty) { return; @@ -3329,8 +3379,8 @@ pub(super) fn report_bool_as_int<'db>( }; let mut diagnostic = builder.into_diagnostic(format_args!( "`{}` is implicitly used as `{}`", - value_ty.display(db), - target_ty.display(db) + value_ty.display(db, env), + target_ty.display(db, env) )); diagnostic.help("Write `int(...)` if the number is meant, or annotate `bool` if the flag is"); } @@ -3391,11 +3441,13 @@ pub(super) fn report_invalid_assignment<'db>( target_ty: Type, value_ty: Type<'db>, ) { + let env = context.program_environment(); + let db = context.db(); let definition_kind = definition.kind(context.db()); let value_node = assigned_value_node(context, definition); if let Some(value_node) = value_node - && is_invalid_typed_dict_literal(context.db(), target_ty, value_node.into()) + && is_invalid_typed_dict_literal(db, target_ty, value_node.into()) { return; } @@ -3409,9 +3461,10 @@ pub(super) fn report_invalid_assignment<'db>( if let Some(value_node) = value_node && is_conversion_site(definition_kind, target_node) { - let model = crate::SemanticModel::new(context.db(), context.file()); + let model = crate::SemanticModel::new(context.db(), db.program_file(context.file())); let conversions = crate::types::conversions::value_conversions( context.db(), + env, context.file(), &model, value_node, @@ -3423,8 +3476,8 @@ pub(super) fn report_invalid_assignment<'db>( } } - let settings = - DisplaySettings::from_possibly_ambiguous_types(context.db(), [target_ty, value_ty]); + let env = &context.program_environment(); + let settings = DisplaySettings::from_possibly_ambiguous_types(db, env, [target_ty, value_ty]); let diagnostic_range = if let Some(value_node) = value_node { // Expand the range to include parentheses around the value, if any. This allows @@ -3447,8 +3500,8 @@ pub(super) fn report_invalid_assignment<'db>( diagnostic_range, format_args!( "Object of type `{}` is not assignable to `{}`", - value_ty.display_with(context.db(), settings.clone()), - target_ty.display_with(context.db(), settings) + value_ty.display_with(db, env, settings.clone()), + target_ty.display_with(db, env, settings) ), ) else { return; @@ -3458,13 +3511,15 @@ pub(super) fn report_invalid_assignment<'db>( match target_ty { Type::ClassLiteral(class) => { diag.info(format_args!( - "Implicit shadowing of class `{}`. Add an annotation to make it explicit if this is intentional", + "Implicit shadowing of class `{}`. \ + Add an annotation to make it explicit if this is intentional", class.name(context.db()), )); } Type::FunctionLiteral(function) => { diag.info(format_args!( - "Implicit shadowing of function `{}`. Add an annotation to make it explicit if this is intentional", + "Implicit shadowing of function `{}`. \ + Add an annotation to make it explicit if this is intentional", function.name(context.db()), )); } @@ -3486,27 +3541,27 @@ pub(super) fn report_invalid_assignment<'db>( // Otherwise, annotate the target with its declared type. diag.annotate(context.secondary(target_node).message(format_args!( "Declared type `{}`", - target_ty.display(context.db()), + target_ty.display(db, env) ))); } } - diag.set_primary_message(format_args!( + diag.set_primary_annotation_message(format_args!( "Incompatible value of type `{}`", - value_ty.display(context.db()), + value_ty.display(db, env), )); - let error_context = value_ty.assignability_error_context(context.db(), target_ty); - error_context.attach_to(context.db(), &mut diag); + let error_context = value_ty.assignability_error_context(db, env, target_ty); + error_context.attach_to(db, env, &mut diag); // Overwrite the concise message to avoid showing the value type twice - let message = diag.primary_message().to_string(); + let message = diag.headline_message().to_string(); diag.set_concise_message(message); } // special case message - note_numbers_module_not_supported(context.db(), &mut diag, target_ty, value_ty); - add_invariant_generic_hints(context.db(), &mut diag, target_ty, value_ty); + note_numbers_module_not_supported(db, env, &mut diag, target_ty, value_ty); + add_invariant_generic_hints(db, env, &mut diag, target_ty, value_ty); } pub(super) fn report_invalid_attribute_assignment( @@ -3516,63 +3571,193 @@ pub(super) fn report_invalid_attribute_assignment( source_ty: Type, attribute_name: &'_ str, ) { + let db = context.db(); // TODO: Ideally we would not emit diagnostics for `TypedDict` literal arguments // here (see `diagnostic::is_invalid_typed_dict_literal`). However, we may have // silenced diagnostics during attribute resolution, and rely on the assignability // diagnostic being emitted here. + let env = &context.program_environment(); let Some(mut diag) = report_invalid_assignment_with_message( context, range, format_args!( "Object of type `{}` is not assignable to attribute `{attribute_name}` of type `{}`", - source_ty.display(context.db()), - target_ty.display(context.db()), + source_ty.display(db, env), + target_ty.display(db, env), ), ) else { return; }; - let error_context = source_ty.assignability_error_context(context.db(), target_ty); - error_context.attach_to(context.db(), &mut diag); + let error_context = source_ty.assignability_error_context(db, env, target_ty); + error_context.attach_to(db, env, &mut diag); +} + +/// Reports an invalid implicit call to a descriptor's `__get__` method. +pub(super) fn report_bad_dunder_get_call<'db>( + context: &InferContext<'db, '_>, + failure: &CallError<'db>, + object_type: Type<'db>, + descriptor_type: Type<'db>, + target: &ast::ExprAttribute, +) { + let db = context.db(); + let env = &context.program_environment(); + let attribute = target.attr.as_str(); + if let Some(property) = failure.as_attempt_to_get_property_with_no_getter() { + let Some(builder) = context.report_lint(&INVALID_ATTRIBUTE_ACCESS, target) else { + return; + }; + let object_type = object_type.display(db, env); + let mut diagnostic = builder.into_diagnostic(format_args!( + "Cannot read property `{attribute}` \ + on object of type `{object_type}` \ + because it has no getter", + )); + if let Some(file_range) = property + .setter(db) + .and_then(|setter| setter.definition(db, env)) + .or_else(|| { + property + .deleter(db) + .and_then(|deleter| deleter.definition(db, env)) + }) + .and_then(|definition| definition.focus_range(db)) + { + diagnostic.annotate(Annotation::secondary(Span::from(file_range)).message( + format_args!("Property `{object_type}.{attribute}` defined here with no getter"), + )); + diagnostic.set_primary_annotation_message(format_args!( + "Attempted access to `{object_type}.{attribute}` here" + )); + } + } else { + failure.report_diagnostics_with_override( + context, + target.into(), + &CallDiagnosticOverride { + lint: &INVALID_ATTRIBUTE_ACCESS, + message: format!( + "Invalid access to descriptor attribute `{attribute}` on type `{}`", + object_type.display(db, env), + ), + info: &format!( + "This access implicitly calls `__get__` on a descriptor of type `{}`", + descriptor_type.display(db, env), + ), + argument_ranges: &[target.range(), target.value.range(), target.value.range()], + }, + ); + } +} + +/// A special method invoked implicitly while accessing an attribute. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum AttributeAccessMethod { + GetAttr, + GetAttribute, +} + +impl AttributeAccessMethod { + pub(super) const fn as_str(self) -> &'static str { + match self { + Self::GetAttr => "__getattr__", + Self::GetAttribute => "__getattribute__", + } + } +} + +/// Reports an invalid implicit `__getattr__` or `__getattribute__` call. +/// +/// ```python +/// class C: +/// def __getattr__(self) -> int: ... +/// +/// C().missing # Invalid: Python passes the attribute name to __getattr__. +/// ``` +/// +/// Preserves the underlying call diagnostic and explains why attribute access invoked the method. +pub(super) fn report_bad_attribute_access_call<'db>( + context: &InferContext<'db, '_>, + failure: &CallError<'db>, + object_type: Type<'db>, + target: &ast::ExprAttribute, + method: AttributeAccessMethod, +) { + let db = context.db(); + let env = &context.program_environment(); + let attribute = target.attr.as_str(); + + failure.report_diagnostics_with_override( + context, + target.into(), + &CallDiagnosticOverride { + lint: &INVALID_ATTRIBUTE_ACCESS, + message: format!( + "Invalid access to attribute `{attribute}` on type `{}`", + object_type.display(db, env), + ), + info: &format!("This access implicitly calls `{}`", method.as_str()), + argument_ranges: &[target.range()], + }, + ); } pub(super) fn report_bad_dunder_set_call<'db>( context: &InferContext<'db, '_>, dunder_set_failure: &CallError<'db>, - attribute: &str, object_type: Type<'db>, + descriptor_type: Type<'db>, + includes_descriptor_argument: bool, target: &ast::ExprAttribute, + value: &ast::Expr, ) { - let Some(builder) = context.report_lint(&INVALID_ASSIGNMENT, target) else { - return; - }; let db = context.db(); + let env = &context.program_environment(); + let attribute = target.attr.as_str(); if let Some(property) = dunder_set_failure.as_attempt_to_set_property_with_no_setter() { - let object_type = object_type.display(db); + let Some(builder) = context.report_lint(&INVALID_ASSIGNMENT, target) else { + return; + }; + let object_type = object_type.display(db, env); let mut diagnostic = builder.into_diagnostic(format_args!( "Cannot assign to read-only property `{attribute}` on object of type `{object_type}`", )); if let Some(file_range) = property .getter(db) - .and_then(|getter| getter.definition(db)) + .and_then(|getter| getter.definition(db, env)) .and_then(|definition| definition.focus_range(db)) { diagnostic.annotate(Annotation::secondary(Span::from(file_range)).message( format_args!("Property `{object_type}.{attribute}` defined here with no setter"), )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Attempted assignment to `{object_type}.{attribute}` here" )); } } else { - // 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_type.display(db) - )); + let argument_ranges = if includes_descriptor_argument { + &[target.range(), target.value.range(), value.range()][..] + } else { + &[target.value.range(), value.range()][..] + }; + dunder_set_failure.report_diagnostics_with_override( + context, + target.into(), + &CallDiagnosticOverride { + lint: &INVALID_ASSIGNMENT, + message: format!( + "Invalid assignment to data descriptor attribute `{attribute}` on type `{}`", + object_type.display(db, env) + ), + info: &format!( + "This assignment implicitly calls `__set__` on a descriptor of type `{}`", + descriptor_type.display(db, env) + ), + argument_ranges, + }, + ); } } @@ -3587,21 +3772,26 @@ pub(super) fn report_bad_dunder_delete_call<'db>( return; }; let db = context.db(); + let env = &context.program_environment(); if let Some(property) = dunder_delete_failure.as_attempt_to_delete_property_with_no_deleter() { - let object_type = object_type.display(db); + let object_type = object_type.display(db, env); let mut diagnostic = builder.into_diagnostic(format_args!( "Cannot delete read-only property `{attribute}` on object of type `{object_type}`", )); if let Some(file_range) = property .getter(db) - .and_then(|getter| getter.definition(db)) - .or_else(|| property.setter(db).and_then(|setter| setter.definition(db))) + .and_then(|getter| getter.definition(db, env)) + .or_else(|| { + property + .setter(db) + .and_then(|setter| setter.definition(db, env)) + }) .and_then(|definition| definition.focus_range(db)) { diagnostic.annotate(Annotation::secondary(Span::from(file_range)).message( format_args!("Property `{object_type}.{attribute}` defined here with no deleter"), )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Attempted deletion of `{object_type}.{attribute}` here" )); } @@ -3609,7 +3799,7 @@ pub(super) fn report_bad_dunder_delete_call<'db>( builder.into_diagnostic(format_args!( "Invalid deletion of data descriptor attribute \ `{attribute}` on type `{}` with custom `__delete__` method", - object_type.display(db) + object_type.display(db, env) )); } } @@ -3621,18 +3811,20 @@ pub(super) fn report_bad_dunder_delattr_call( target: &ast::ExprAttribute, binding_error: bool, ) { + let db = context.db(); let Some(builder) = context.report_lint(&INVALID_ASSIGNMENT, target) else { return; }; - let db = context.db(); + let env = &context.program_environment(); let mut diagnostic = builder.into_diagnostic(format_args!( "Cannot delete attribute `{attribute}` on type `{}` with custom `__delattr__` method", - object_type.display(db), + object_type.display(db, env), )); if binding_error { diagnostic.info(format_args!( - "Type `{}` has a `__delattr__` method, but it cannot be called with the expected arguments", - object_type.display(db) + "Type `{}` has a `__delattr__` method, \ + but it cannot be called with the expected arguments", + object_type.display(db, env) )); diagnostic.info( "Expected a signature at least as permissive as \ @@ -3648,29 +3840,78 @@ pub(super) fn report_invalid_return_type( expected_ty: Type, actual_ty: Type, ) { + let db = context.db(); let Some(builder) = context.report_lint(&INVALID_RETURN_TYPE, object_range) else { return; }; + let env = &context.program_environment(); let settings = - DisplaySettings::from_possibly_ambiguous_types(context.db(), [expected_ty, actual_ty]); + DisplaySettings::from_possibly_ambiguous_types(db, env, [expected_ty, actual_ty]); 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!( + diag.set_primary_annotation_message(format_args!( "expected `{expected_ty}`, found `{actual_ty}`", - expected_ty = expected_ty.display_with(context.db(), settings.clone()), - actual_ty = actual_ty.display_with(context.db(), settings.clone()), + expected_ty = expected_ty.display_with(db, env, settings.clone()), + actual_ty = actual_ty.display_with(db, env, settings.clone()), )); diag.annotate( Annotation::secondary(return_type_span).message(format_args!( "Expected `{expected_ty}` because of return type", - expected_ty = expected_ty.display_with(context.db(), settings), + expected_ty = expected_ty.display_with(db, env, settings), )), ); - let error_context = actual_ty.assignability_error_context(context.db(), expected_ty); - error_context.attach_to(context.db(), &mut diag); + let error_context = actual_ty.assignability_error_context(db, env, expected_ty); + error_context.attach_to(db, env, &mut diag); +} + +pub(super) fn report_unsound_return_statement( + context: &InferContext, + object_range: impl Ranged, + return_type_range: impl Ranged, + expected_ty: Type, + actual_ty: Type, +) { + let db = context.db(); + let Some(builder) = context.report_lint(&UNSOUND_RETURN_STATEMENT, object_range) else { + return; + }; + + let env = &context.program_environment(); + + // `TypeIs`-annotated functions are expected to return `bool`; + // this needs to be normalized before we figure out the error context + // and before we display the types. + let expected_ty = match expected_ty.resolve_type_alias(db) { + Type::TypeIs(_) | Type::TypeGuard(_) => KnownClass::Bool.to_instance(db, env), + _ => expected_ty, + }; + + let settings = + DisplaySettings::from_possibly_ambiguous_types(db, env, [expected_ty, actual_ty]); + + let mut diag = builder.into_diagnostic("Unsound return statement"); + let actual_ty_display = actual_ty.display_with(db, env, settings.clone()); + let expected_ty_display = expected_ty.display_with(db, env, settings); + + diag.set_concise_message(format_args!( + "Unsound return statement: `{actual_ty_display}` is not a subtype \ + of `{expected_ty_display}`" + )); + diag.set_primary_annotation_message(format_args!("Inferred as `{actual_ty_display}`")); + diag.annotate(context.secondary(return_type_range).message(format_args!( + "Expected a subtype of `{expected_ty_display}` because of the return type", + ))); + + diag.info(format_args!( + "`{actual_ty_display}` is assignable to `{expected_ty_display}`, \ + but not a subtype of `{expected_ty_display}`", + )); + let error_context = actual_ty.pure_redundancy_error_context(db, env, expected_ty); + error_context.attach_to(db, env, &mut diag); + diag.help("Consider using an `assert` to narrow the type prior to the `return` statement"); } pub(super) fn report_invalid_generator_function_return_type( @@ -3679,15 +3920,17 @@ pub(super) fn report_invalid_generator_function_return_type( inferred_return: KnownClass, expected_ty: Type, ) { + let db = context.db(); let Some(builder) = context.report_lint(&INVALID_RETURN_TYPE, return_type_range) else { return; }; + let env = &context.program_environment(); 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!( + let inferred_ty = inferred_return.display(env.python_version(db)); + diag.set_primary_annotation_message(format_args!( "expected `{expected_ty}`, found `{inferred_ty}`", - expected_ty = expected_ty.display(context.db()), + expected_ty = expected_ty.display(db, env), )); let (description, link) = if inferred_return == KnownClass::AsyncGeneratorType { @@ -3722,28 +3965,32 @@ pub(super) fn report_invalid_generator_yield_type( actual_ty: Type, kind: GeneratorMismatchKind, ) { + let db = context.db(); let Some(builder) = context.report_lint(&INVALID_YIELD, object_range) else { return; }; + let env = &context.program_environment(); let settings = - DisplaySettings::from_possibly_ambiguous_types(context.db(), [expected_ty, actual_ty]); - let expected_display = expected_ty.display_with(context.db(), settings.clone()); - let actual_display = actual_ty.display_with(context.db(), settings); + DisplaySettings::from_possibly_ambiguous_types(db, env, [expected_ty, actual_ty]); + let expected_display = expected_ty.display_with(db, env, settings.clone()); + let actual_display = actual_ty.display_with(db, env, settings); let (kind_name, title, concise) = match kind { GeneratorMismatchKind::YieldType => ( "yield", "Yield expression type does not match annotation", format!( - "Yield type `{actual_display}` does not match annotated yield type `{expected_display}`" + "Yield type `{actual_display}` does not match annotated yield type \ + `{expected_display}`" ), ), GeneratorMismatchKind::SendType => ( "send", "Send type does not match annotation", format!( - "Send type `{actual_display}` does not match annotated send type `{expected_display}`" + "Send type `{actual_display}` does not match annotated send type \ + `{expected_display}`" ), ), }; @@ -3758,7 +4005,7 @@ pub(super) fn report_invalid_generator_yield_type( format!("generator with send type `{actual_display}`, expected `{expected_display}`") } }; - diag.set_primary_message(primary); + diag.set_primary_annotation_message(primary); if let Some(return_type_span) = return_type_span { diag.annotate(Annotation::secondary(return_type_span).message(format!( @@ -3766,8 +4013,80 @@ pub(super) fn report_invalid_generator_yield_type( ))); } - let error_context = actual_ty.assignability_error_context(context.db(), expected_ty); - error_context.attach_to(context.db(), &mut diag); + let error_context = actual_ty.assignability_error_context(db, env, expected_ty); + error_context.attach_to(db, env, &mut diag); +} + +pub(super) fn report_unsound_yield( + context: &InferContext, + yield_value: impl Ranged, + kind: YieldKind, + return_type_span: Option, + expected_ty: Type, + actual_ty: Type, +) { + let db = context.db(); + let Some(builder) = context.report_lint(&UNSOUND_YIELD, yield_value) else { + return; + }; + + let env = context.program_environment(); + let settings = + DisplaySettings::from_possibly_ambiguous_types(db, env, [expected_ty, actual_ty]); + let actual_display = actual_ty.display_with(db, env, settings.clone()); + let expected_display = expected_ty.display_with(db, env, settings); + + let mut diagnostic = builder.into_diagnostic(format_args!("Unsound `{kind}`")); + diagnostic.set_concise_message(format_args!( + "Unsound `{kind}`: `{actual_display}` is not a subtype of `{expected_display}`" + )); + + match kind { + YieldKind::Yield => diagnostic + .set_primary_annotation_message(format_args!("Inferred as `{actual_display}`")), + YieldKind::YieldFrom => diagnostic.set_primary_annotation_message(format_args!( + "Yielded elements inferred as `{actual_display}`" + )), + } + + if let Some(return_type_span) = return_type_span { + diagnostic.annotate( + Annotation::secondary(return_type_span).message(format_args!( + "Expected a subtype of `{expected_display}` because of the yield type" + )), + ); + } + + diagnostic.info(format_args!( + "`{actual_display}` is assignable to `{expected_display}`, \ + but not a subtype of `{expected_display}`" + )); + let error_context = actual_ty.pure_redundancy_error_context(db, env, expected_ty); + error_context.attach_to(db, env, &mut diagnostic); + + match kind { + YieldKind::Yield => { + diagnostic.help("Consider using an `assert` to narrow the type before yielding it"); + } + YieldKind::YieldFrom => diagnostic.help( + "Consider using `assert`s to narrow the types of the elements before yielding them", + ), + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub(super) enum YieldKind { + Yield, + YieldFrom, +} + +impl std::fmt::Display for YieldKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + YieldKind::Yield => f.write_str("yield"), + YieldKind::YieldFrom => f.write_str("yield from"), + } + } } pub(super) fn report_implicit_return_type( @@ -3790,12 +4109,14 @@ pub(super) fn report_implicit_return_type( let Some(builder) = context.report_lint(lint_to_use, range) else { return; }; + let env = &context.program_environment(); // 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), + "Function always implicitly returns `None`, \ + which is not assignable to return type `{}`", + expected_ty.display(db, env), )); diag.info( "Consider changing the return annotation to `-> None` or adding a `return` statement", @@ -3804,7 +4125,7 @@ pub(super) fn report_implicit_return_type( } else { builder.into_diagnostic(format_args!( "Function can implicitly return `None`, which is not assignable to return type `{}`", - expected_ty.display(db), + expected_ty.display(db, env), )) }; if !has_empty_body { @@ -3872,6 +4193,7 @@ pub(super) fn report_possibly_missing_attribute( return; }; let db = context.db(); + let env = &context.program_environment(); match object_ty { Type::ModuleLiteral(module) => builder.into_diagnostic(format_args!( "Member `{attribute}` may be missing on module `{}`", @@ -3883,11 +4205,11 @@ pub(super) fn report_possibly_missing_attribute( )), Type::GenericAlias(alias) => builder.into_diagnostic(format_args!( "Attribute `{attribute}` may be missing on class `{}`", - alias.display(db), + alias.display(db, env), )), _ => builder.into_diagnostic(format_args!( "Attribute `{attribute}` may be missing on object of type `{}`", - object_ty.display(db), + object_ty.display(db, env), )), }; } @@ -3898,21 +4220,23 @@ pub(super) fn report_invalid_exception_tuple_caught<'db, 'ast>( node_type: Type<'db>, invalid_tuple_nodes: impl IntoIterator)>, ) { + let db = context.db(); let Some(builder) = context.report_lint(&INVALID_EXCEPTION_CAUGHT, node) else { return; }; + let env = &context.program_environment(); let mut diagnostic = builder.into_diagnostic("Invalid tuple caught in an exception handler"); diagnostic.set_concise_message(format_args!( "Cannot catch object of type `{}` in an exception handler", - node_type.display(context.db()) + node_type.display(db, env) )); for (sub_node, ty) in invalid_tuple_nodes { let span = context.span(sub_node); diagnostic.annotate(Annotation::secondary(span.clone()).message(format_args!( "Invalid element of type `{}`", - ty.display(context.db()) + ty.display(db, env) ))); if ty.is_notimplemented(context.db()) { diagnostic.annotate( @@ -3927,27 +4251,29 @@ pub(super) fn report_invalid_exception_tuple_caught<'db, 'ast>( } pub(super) fn report_invalid_exception_caught(context: &InferContext, node: &ast::Expr, ty: Type) { + let db = context.db(); let Some(builder) = context.report_lint(&INVALID_EXCEPTION_CAUGHT, node) else { return; }; + let env = &context.program_environment(); let mut diagnostic = if ty.is_notimplemented(context.db()) { let mut diag = builder.into_diagnostic("Cannot catch `NotImplemented` in an exception handler"); - diag.set_primary_message("Did you mean `NotImplementedError`?"); + diag.set_primary_annotation_message("Did you mean `NotImplementedError`?"); diag } else { let mut diag = builder.into_diagnostic(format_args!( "Invalid {thing} caught in an exception handler", - thing = if ty.tuple_instance_spec(context.db()).is_some() { + thing = if ty.tuple_instance_spec(db, env).is_some() { "tuple" } else { "object" }, )); - diag.set_primary_message(format_args!( + diag.set_primary_annotation_message(format_args!( "Object has type `{}`", - ty.display(context.db()) + ty.display(db, env) )); diag }; @@ -3962,36 +4288,40 @@ pub(crate) fn report_invalid_exception_raised( raised_node: &ast::Expr, raise_type: Type, ) { + let db = context.db(); let Some(builder) = context.report_lint(&INVALID_RAISE, raised_node) else { return; }; + let env = &context.program_environment(); if raise_type.is_notimplemented(context.db()) { let mut diagnostic = builder.into_diagnostic(format_args!("Cannot raise `NotImplemented`")); - diagnostic.set_primary_message("Did you mean `NotImplementedError`?"); + diagnostic.set_primary_annotation_message("Did you mean `NotImplementedError`?"); diagnostic.info("Can only raise an instance or subclass of `BaseException`"); } else { let mut diagnostic = builder.into_diagnostic(format_args!( "Cannot raise object of type `{}`", - raise_type.display(context.db()) + raise_type.display(db, env) )); - diagnostic.set_primary_message("Not an instance or subclass of `BaseException`"); + diagnostic.set_primary_annotation_message("Not an instance or subclass of `BaseException`"); } } pub(crate) fn report_invalid_exception_cause(context: &InferContext, node: &ast::Expr, ty: Type) { + let db = context.db(); let Some(builder) = context.report_lint(&INVALID_RAISE, node) else { return; }; + let env = &context.program_environment(); let mut diagnostic = if ty.is_notimplemented(context.db()) { let mut diag = builder.into_diagnostic(format_args!( "Cannot use `NotImplemented` as an exception cause", )); - diag.set_primary_message("Did you mean `NotImplementedError`?"); + diag.set_primary_annotation_message("Did you mean `NotImplementedError`?"); diag } else { builder.into_diagnostic(format_args!( "Cannot use object of type `{}` as an exception cause", - ty.display(context.db()) + ty.display(db, env) )) }; diagnostic.info( @@ -4017,7 +4347,7 @@ pub(crate) fn report_instance_layout_conflict( let mut diagnostic = builder .into_diagnostic("Class will raise `TypeError` at runtime due to incompatible bases"); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Bases {} cannot be combined in multiple inheritance", disjoint_bases.describe_problematic_class_bases(db) )); @@ -4045,14 +4375,15 @@ pub(crate) fn report_instance_layout_conflict( match disjoint_base.kind { DisjointBaseKind::DefinesSlots => { annotation = annotation.message(format_args!( - "`{base}` instances have a distinct memory layout because `{base}` defines non-empty `__slots__`", + "`{base}` instances have a distinct memory layout \ + because `{base}` defines non-empty `__slots__`", base = originating_base.name(db) )); } DisjointBaseKind::DisjointBaseDecorator => { annotation = annotation.message(format_args!( - "`{base}` instances have a distinct memory layout because of the way `{base}` \ - is implemented in a C extension", + "`{base}` instances have a distinct memory layout \ + because of the way `{base}` is implemented in a C extension", base = originating_base.name(db) )); } @@ -4071,8 +4402,8 @@ pub(crate) fn report_instance_layout_conflict( additional_annotation = match disjoint_base.kind { DisjointBaseKind::DefinesSlots => additional_annotation.message(format_args!( - "`{disjoint_base}` instances have a distinct memory layout because `{disjoint_base}` \ - defines non-empty `__slots__`", + "`{disjoint_base}` instances have a distinct memory layout \ + because `{disjoint_base}` defines non-empty `__slots__`", disjoint_base = disjoint_base.class.name(db), )), @@ -4247,7 +4578,7 @@ pub(crate) fn report_bad_argument_to_get_protocol_members( }; 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.set_primary_annotation_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( @@ -4282,10 +4613,11 @@ pub(crate) fn report_bad_argument_to_protocol_interface( }; let db = context.db(); let mut diagnostic = builder.into_diagnostic("Invalid argument to `reveal_protocol_interface`"); - diagnostic - .set_primary_message("Only protocol classes can be passed to `reveal_protocol_interface`"); + diagnostic.set_primary_annotation_message( + "Only protocol classes can be passed to `reveal_protocol_interface`", + ); - if let Some(class) = param_type.to_class_type(context.db()) { + if let Some(class) = param_type.to_class_type(db) { let mut class_def_diagnostic = SubDiagnostic::new( SubDiagnosticSeverity::Info, format_args!( @@ -4324,15 +4656,16 @@ pub(crate) fn report_invalid_class_match_pattern( pattern_cls: T, cls_ty: Type, ) { + let db = context.db(); let Some(builder) = context.report_lint(&INVALID_MATCH_PATTERN, pattern_cls) else { return; }; - let db = context.db(); - let class_display = cls_ty.display(db); + let env = &context.program_environment(); + let class_display = cls_ty.display(db, env); let mut diagnostic = builder.into_diagnostic(format_args!( "`{class_display}` cannot be used in a class pattern because it is not a type" )); - diagnostic.set_primary_message("This will raise `TypeError` at runtime"); + diagnostic.set_primary_annotation_message("This will raise `TypeError` at runtime"); } pub(crate) fn report_too_many_positional_patterns_for_class_pattern( @@ -4346,7 +4679,8 @@ pub(crate) fn report_too_many_positional_patterns_for_class_pattern( return; }; builder.into_diagnostic(format_args!( - "Too many positional subpatterns for `{class_display}`: expected {positional_limit}, got {positional_count}" + "Too many positional subpatterns for `{class_display}`: \ + expected {positional_limit}, got {positional_count}" )); } @@ -4356,20 +4690,21 @@ pub(crate) fn report_invalid_match_args_type( match_args_ty: Type, cls_ty: Type, ) { + let db = context.db(); let Some(builder) = context.report_lint(&INVALID_MATCH_PATTERN, pattern) else { return; }; - let db = context.db(); - let class_display = cls_ty.display(db); - let match_args_display = match_args_ty.display(db); + let env = &context.program_environment(); + let class_display = cls_ty.display(db, env); + let match_args_display = match_args_ty.display(db, env); builder.into_diagnostic(format_args!( "`__match_args__` for `{class_display}` must be an exact tuple, not `{match_args_display}`" )); } -pub(crate) fn add_type_expression_reference_link<'db, 'ctx>( - mut diag: LintDiagnosticGuard<'db, 'ctx>, -) -> LintDiagnosticGuard<'db, 'ctx> { +pub(crate) fn add_type_expression_reference_link<'db, 'env>( + mut diag: LintDiagnosticGuard<'db, 'env>, +) -> LintDiagnosticGuard<'db, 'env> { 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", @@ -4392,11 +4727,12 @@ pub(crate) fn report_runtime_check_against_non_runtime_checkable_protocol( 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"); + diagnostic.set_primary_annotation_message("This call will raise `TypeError` at runtime"); add_non_runtime_checkable_protocol_context(db, &mut diagnostic, protocol); 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`" + "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(format_args!("See {RUNTIME_CHECKABLE_DOCS_URL}")); } @@ -4419,7 +4755,7 @@ pub(crate) fn report_issubclass_check_against_protocol_with_non_method_members<' "`{class_name}` cannot be used as the second argument to `issubclass` \ as it is a protocol with non-method members" )); - diagnostic.set_primary_message("This call will raise `TypeError` at runtime"); + diagnostic.set_primary_annotation_message("This call will raise `TypeError` at runtime"); if let [single_member] = non_method_members { let mut sub = SubDiagnostic::new( SubDiagnosticSeverity::Info, @@ -4427,8 +4763,7 @@ pub(crate) fn report_issubclass_check_against_protocol_with_non_method_members<' if it has non-method members", ); if let Some(definition) = single_member.definition() { - let file = definition.file(db); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, definition.python_file(db)).load(db); let span = Span::from(definition.focus_range(db, &module)); sub.annotate(Annotation::primary(span).message(format_args!( "Non-method member `{}` declared here", @@ -4452,8 +4787,7 @@ pub(crate) fn report_issubclass_check_against_protocol_with_non_method_members<' .iter() .find_map(|member| Some((member.name(), member.definition()?))) { - let file = definition.file(db); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, definition.python_file(db)).load(db); let span = Span::from(definition.focus_range(db, &module)); sub.annotate( Annotation::primary(span) @@ -4475,10 +4809,11 @@ pub(crate) fn report_runtime_check_against_typed_dict( }; let class_name = class.name(context.db()); let mut diagnostic = builder.into_diagnostic(format_args!( - "`TypedDict` class `{class_name}` cannot be used as the second argument to `{function_name}`", + "`TypedDict` class `{class_name}` cannot be used as the second argument \ + to `{function_name}`", function_name = function.name() )); - diagnostic.set_primary_message("This call will raise `TypeError` at runtime"); + diagnostic.set_primary_annotation_message("This call will raise `TypeError` at runtime"); } pub(crate) fn report_match_pattern_against_non_runtime_checkable_protocol( @@ -4494,11 +4829,12 @@ pub(crate) fn report_match_pattern_against_non_runtime_checkable_protocol( let mut diagnostic = builder.into_diagnostic(format_args!( "`TypedDict` class `{class_name}` cannot be used in a class pattern", )); - diagnostic.set_primary_message("This will raise `TypeError` at runtime"); + diagnostic.set_primary_annotation_message("This will raise `TypeError` at runtime"); } fn add_non_runtime_checkable_protocol_context<'db>( @@ -4551,7 +4887,7 @@ pub(crate) fn report_attempted_protocol_instantiation( 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"); + diagnostic.set_primary_annotation_message("This call will raise `TypeError` at runtime"); let mut class_def_diagnostic = SubDiagnostic::new( SubDiagnosticSeverity::Info, @@ -4576,7 +4912,7 @@ pub(crate) fn report_call_to_abstract_method( let db = context.db(); let name = function.name(db); let mut diag = builder.into_diagnostic(format_args!("Cannot call `{name}` on class object")); - diag.set_primary_message(format_args!( + diag.set_primary_annotation_message(format_args!( "`{name}` is an abstract {method_kind} with a trivial body" )); let span = abstract_method_span( @@ -4601,7 +4937,7 @@ pub(super) fn abstract_method_span<'db>( }; let file = function.file(db); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, function.python_file(db)).load(db); let node = implementation.node(db, file, &module); let source_text = source_text(db, file); @@ -4635,24 +4971,25 @@ pub(crate) fn report_undeclared_protocol_member( /// We want to avoid suggesting an annotation for e.g. `x = None`, /// because the user almost certainly doesn't want to write `x: None = None`. /// We also want to avoid suggesting invalid syntax such as `x: = int`. - fn should_give_hint<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { + fn should_give_hint<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + ) -> bool { let class = match ty { - Type::ProtocolInstance(ProtocolInstanceType { - inner: Protocol::FromClass(_), - .. - }) => return true, + Type::ProtocolInstance(protocol) if protocol.class_origin(db).is_some() => return true, Type::SubclassOf(subclass_of) => match subclass_of.subclass_of() { SubclassOfInner::Class(class) => class, SubclassOfInner::Protocol(_) => return true, SubclassOfInner::Dynamic(DynamicType::Any) => return true, SubclassOfInner::Dynamic(_) | SubclassOfInner::TypeVar(_) => return false, }, - Type::NominalInstance(instance) => instance.class(db), + Type::NominalInstance(instance) => instance.class(db, env), Type::Union(union) => { return union .elements(db) .iter() - .all(|elem| should_give_hint(db, *elem)); + .all(|elem| should_give_hint(db, env, *elem)); } _ => return false, }; @@ -4682,22 +5019,22 @@ pub(crate) fn report_undeclared_protocol_member( .into_diagnostic("Cannot assign to undeclared variable in the body of a protocol class"); if definition.kind(db).is_unannotated_assignment() { + let env = &context.program_environment(); let binding_type = binding_type(db, definition); + let suggestion = binding_type.promote(db, env); - let suggestion = binding_type.promote(db); - - if should_give_hint(db, suggestion) { - diagnostic.set_primary_message(format_args!( + if should_give_hint(db, env, suggestion) { + diagnostic.set_primary_annotation_message(format_args!( "Consider adding an annotation, e.g. `{symbol_name}: {} = ...`", - suggestion.display(db) + suggestion.display(db, env) )); } else { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Consider adding an annotation for `{symbol_name}`" )); } } else { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "`{symbol_name}` is not declared as a protocol member" )); } @@ -4724,7 +5061,7 @@ pub(crate) fn report_undeclared_protocol_attribute( let symbol_name = target.attr.as_str(); let mut diagnostic = builder.into_diagnostic("Cannot assign to an undeclared attribute in a protocol method"); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "`{symbol_name}` is not declared as a protocol member" )); @@ -4831,9 +5168,10 @@ pub(crate) fn report_invalid_or_unsupported_base( class: StaticClassLiteral, ) { let db = context.db(); - let instance_of_type = KnownClass::Type.to_instance(db); + let env = &context.program_environment(); + let instance_of_type = KnownClass::Type.to_instance(db, env); - if base_type.is_assignable_to(db, instance_of_type) { + if base_type.is_assignable_to(db, env, instance_of_type) { report_unsupported_base(context, base_node, base_type, class); return; } @@ -4856,7 +5194,7 @@ pub(crate) fn report_invalid_or_unsupported_base( return; } - let tuple_of_types = Type::homogeneous_tuple(db, instance_of_type); + let tuple_of_types = Type::homogeneous_tuple(db, env, instance_of_type); let explain_mro_entries = |diagnostic: &mut LintDiagnosticGuard| { diagnostic.info( @@ -4865,14 +5203,19 @@ pub(crate) fn report_invalid_or_unsupported_base( ); }; + let env = &context.program_environment(); match base_type.try_call_dunder( db, + env, "__mro_entries__", CallArguments::positional([tuple_of_types]), TypeContext::default(), ) { Ok(ret) => { - if ret.return_type(db).is_assignable_to(db, tuple_of_types) { + if ret + .return_type(db, env) + .is_assignable_to(db, env, tuple_of_types) + { report_unsupported_base(context, base_node, base_type, class); } else { let Some(mut diagnostic) = @@ -4882,8 +5225,9 @@ pub(crate) fn report_invalid_or_unsupported_base( }; 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) + "Type `{}` has an `__mro_entries__` method, \ + but it does not return a tuple of types", + base_type.display(db, env) )); } } @@ -4899,13 +5243,13 @@ pub(crate) fn report_invalid_or_unsupported_base( explain_mro_entries(&mut diagnostic); diagnostic.info(format_args!( "Type `{}` may have an `__mro_entries__` attribute, but it may be missing", - base_type.display(db) + base_type.display(db, env) )); if let Some(unbound_on) = unbound_on { for ty in unbound_on { diagnostic.info(format_args!( "`{}` does not implement `__mro_entries__`", - ty.display(db) + ty.display(db, env) )); } } @@ -4914,7 +5258,7 @@ pub(crate) fn report_invalid_or_unsupported_base( explain_mro_entries(&mut diagnostic); diagnostic.info(format_args!( "Type `{}` has an `__mro_entries__` attribute, but it is not callable", - base_type.display(db) + base_type.display(db, env) )); } CallDunderError::CallError(CallErrorKind::BindingError, _, _) => { @@ -4922,7 +5266,7 @@ pub(crate) fn report_invalid_or_unsupported_base( diagnostic.info(format_args!( "Type `{}` has an `__mro_entries__` method, \ but it cannot be called with the expected arguments", - base_type.display(db) + base_type.display(db, env) )); diagnostic.info( "Expected a signature at least as permissive as \ @@ -4934,7 +5278,7 @@ pub(crate) fn report_invalid_or_unsupported_base( diagnostic.info(format_args!( "Type `{}` has an `__mro_entries__` method, \ but it may not be callable", - base_type.display(db) + base_type.display(db, env) )); } } @@ -4952,29 +5296,34 @@ pub(crate) fn report_unsupported_base( return; }; let db = context.db(); + let env = &context.program_environment(); let mut diagnostic = builder.into_diagnostic("Unsupported class base"); - diagnostic.set_primary_message(format_args!("Has type `{}`", base_type.display(db))); + diagnostic + .set_primary_annotation_message(format_args!("Has type `{}`", base_type.display(db, env))); diagnostic.set_concise_message(format_args!( "Unsupported class base with type `{}`", - base_type.display(db) + base_type.display(db, env) )); diagnostic.info(format_args!( - "ty cannot resolve a consistent method resolution order (MRO) for class `{}` due to this base", + "ty cannot resolve a consistent method resolution order (MRO) for class `{}` \ + due to this base", class.name(db) )); diagnostic.info("Only class objects or `Any` are supported as class bases"); } -fn report_invalid_base<'ctx, 'db>( - context: &'ctx InferContext<'db, '_>, +fn report_invalid_base<'env, 'db>( + context: &'env InferContext<'db, '_>, base_node: &ast::Expr, base_type: Type<'db>, class: StaticClassLiteral<'db>, -) -> Option> { +) -> Option> { + let db = context.db(); let builder = context.report_lint(&INVALID_BASE, base_node)?; + let env = &context.program_environment(); let mut diagnostic = builder.into_diagnostic(format_args!( "Invalid class base with type `{}`", - base_type.display(context.db()) + base_type.display(db, env) )); diagnostic.info(format_args!( "Definition of class `{}` will raise `TypeError` at runtime", @@ -4994,10 +5343,11 @@ pub(crate) fn report_invalid_key_on_typed_dict<'db>( ) { let db = context.db(); if let Some(builder) = context.report_lint(&INVALID_KEY, key_node) { + let env = &context.program_environment(); match key_ty.as_string_literal() { Some(key) => { let key = key.value(db); - let typed_dict_name = typed_dict_ty.display(db); + let typed_dict_name = typed_dict_ty.display(db, env); let mut diagnostic = builder.into_diagnostic(format_args!( "Unknown key \"{key}\" for TypedDict `{typed_dict_name}`", @@ -5011,7 +5361,7 @@ pub(crate) fn report_invalid_key_on_typed_dict<'db>( } else { "intersection" }, - full_object_ty = full_object_ty.display(db) + full_object_ty = full_object_ty.display(db, env) )) } else { context @@ -5029,26 +5379,30 @@ pub(crate) fn report_invalid_key_on_typed_dict<'db>( "{quote}{suggestion}{quote}", quote = literal.value.first_literal_flags().quote_str() ); - diagnostic - .set_primary_message(format_args!("Did you mean {quoted_suggestion}?")); + diagnostic.set_primary_annotation_message(format_args!( + "Did you mean {quoted_suggestion}?" + )); diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( quoted_suggestion, key_node.range(), ))); } else { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Unknown key \"{key}\" - did you mean \"{suggestion}\"?", )); } diagnostic.set_concise_message(format_args!( - "Unknown key \"{key}\" for TypedDict `{typed_dict_name}` - did you mean \"{suggestion}\"?", + "Unknown key \"{key}\" for TypedDict `{typed_dict_name}` - \ + did you mean \"{suggestion}\"?", )); } else { - diagnostic.set_primary_message(format_args!("Unknown key \"{key}\"")); + diagnostic + .set_primary_annotation_message(format_args!("Unknown key \"{key}\"")); if let Some(full_ty) = full_object_ty { diagnostic.set_concise_message(format_args!( - "Unknown key \"{key}\" for TypedDict `{typed_dict_name}` (subscripted object has type `{full_ty}`)", - full_ty = full_ty.display(db), + "Unknown key \"{key}\" for TypedDict `{typed_dict_name}` \ + (subscripted object has type `{full_ty}`)", + full_ty = full_ty.display(db, env), )); } else { diagnostic.set_concise_message(format_args!( @@ -5061,14 +5415,14 @@ pub(crate) fn report_invalid_key_on_typed_dict<'db>( let mut diagnostic = builder.into_diagnostic(format_args!( "TypedDict `{}` can only be subscripted with a string literal key, \ got key of type `{}`", - typed_dict_ty.display(db), - key_ty.display(db), + typed_dict_ty.display(db, env), + key_ty.display(db, env), )); if let Some(full_object_ty) = full_object_ty { diagnostic.info(format_args!( "The full type of the subscripted object is `{}`", - full_object_ty.display(db) + full_object_ty.display(db, env) )); } } @@ -5096,7 +5450,7 @@ pub(super) fn report_namedtuple_field_without_default_after_field_with_default<' "NamedTuple field without default value cannot follow field(s) with default value(s)", ); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Field `{field}` defined here without a default value", )); @@ -5145,11 +5499,11 @@ pub(super) fn report_named_tuple_field_with_leading_underscore<'db>( builder.into_diagnostic("NamedTuple field name cannot start with an underscore"); if field_definition.is_some() { - diagnostic.set_primary_message( + diagnostic.set_primary_annotation_message( "Class definition will raise `TypeError` at runtime due to this field", ); } else { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Class definition will raise `TypeError` at runtime due to field `{field_name}`", )); } @@ -5159,6 +5513,32 @@ pub(super) fn report_named_tuple_field_with_leading_underscore<'db>( )); } +/// Report a `NamedTuple` field annotated with a type qualifier that `NamedTuple` does not accept. +/// +/// The diagnostic is anchored to the annotated assignment that introduced the qualifier. It does +/// not claim that class creation fails at runtime because deferred and wrapped annotations can +/// preserve the qualifier without passing it directly to `typing._type_check`. +pub(super) fn report_invalid_named_tuple_field_qualifier<'db>( + context: &InferContext<'db, '_>, + field_name: &str, + qualifier: TypeQualifier, + field_definition: Definition<'db>, +) { + let db = context.db(); + let module = context.module(); + let qualifier = qualifier.name(); + let diagnostic_range = field_definition.kind(db).full_range(module); + let Some(builder) = context.report_lint(&INVALID_NAMED_TUPLE, diagnostic_range) else { + return; + }; + let mut diagnostic = builder.into_diagnostic(format_args!( + "Type qualifier `{qualifier}` is not allowed in a NamedTuple field" + )); + diagnostic.set_concise_message(format_args!( + "Type qualifier `{qualifier}` is not allowed on NamedTuple field `{field_name}`" + )); +} + pub(crate) fn report_missing_typed_dict_key<'db>( context: &InferContext<'db, '_>, constructor_node: AnyNodeRef, @@ -5167,7 +5547,8 @@ pub(crate) fn report_missing_typed_dict_key<'db>( ) { let db = context.db(); if let Some(builder) = context.report_lint(&MISSING_TYPED_DICT_KEY, constructor_node) { - let typed_dict_name = typed_dict_ty.display(db); + let env = &context.program_environment(); + let typed_dict_name = typed_dict_ty.display(db, env); builder.into_diagnostic(format_args!( "Missing required key '{missing_field}' in TypedDict `{typed_dict_name}` constructor", )); @@ -5182,7 +5563,8 @@ pub(crate) fn report_cannot_pop_required_field_on_typed_dict<'db>( ) { let db = context.db(); if let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, key_node) { - let typed_dict_name = typed_dict_ty.display(db); + let env = &context.program_environment(); + let typed_dict_name = typed_dict_ty.display(db, env); builder.into_diagnostic(format_args!( "Cannot pop required field '{field_name}' from TypedDict `{typed_dict_name}`", )); @@ -5212,15 +5594,17 @@ pub(crate) fn report_cannot_delete_typed_dict_key<'db>( let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, key_node) else { return; }; + let env = &context.program_environment(); - let typed_dict_name = Type::TypedDict(typed_dict_ty).display(db); + let typed_dict_name = Type::TypedDict(typed_dict_ty).display(db, env); let mut diagnostic = match error_kind { TypedDictDeleteErrorKind::RequiredKey => builder.into_diagnostic(format_args!( "Cannot delete required key \"{field_name}\" from TypedDict `{typed_dict_name}`" )), TypedDictDeleteErrorKind::ReadOnlyExtraItem => builder.into_diagnostic(format_args!( - "Cannot delete read-only extra item \"{field_name}\" from TypedDict `{typed_dict_name}`" + "Cannot delete read-only extra item \"{field_name}\" \ + from TypedDict `{typed_dict_name}`" )), TypedDictDeleteErrorKind::UnknownKey => builder.into_diagnostic(format_args!( "Cannot delete unknown key \"{field_name}\" from TypedDict `{typed_dict_name}`" @@ -5232,7 +5616,7 @@ pub(crate) fn report_cannot_delete_typed_dict_key<'db>( && let Some(declaration) = field.first_declaration() { let file = declaration.file(db); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, declaration.python_file(db)).load(db); let mut sub = SubDiagnostic::new(SubDiagnosticSeverity::Info, "Field defined here"); for message in [ @@ -5262,7 +5646,8 @@ pub(crate) fn report_cannot_delete_typed_dict_key<'db>( // Add hint about how to allow deletion if matches!(error_kind, TypedDictDeleteErrorKind::RequiredKey) { diagnostic.info( - "Only keys marked as `NotRequired` (or in a TypedDict with `total=False`) can be deleted", + "Only keys marked as `NotRequired` \ + (or in a TypedDict with `total=False`) can be deleted", ); } } @@ -5277,7 +5662,7 @@ pub(crate) fn report_invalid_type_param_order<'db>( let db = context.db(); let base_index = class - .explicit_bases(db) + .explicit_bases(context.db()) .iter() .position(|base| { matches!( @@ -5289,8 +5674,9 @@ pub(crate) fn report_invalid_type_param_order<'db>( ) }) .expect( - "It should not be possible for a class to have a legacy generic context \ - if it does not inherit from `Protocol[]` or `Generic[]`", + "It should not be possible for a class to have \ + a legacy generic context if it does \ + not inherit from `Protocol[]` or `Generic[]`", ); let base_node = &node.bases()[base_index]; @@ -5317,14 +5703,14 @@ pub(crate) fn report_invalid_type_param_order<'db>( )); if let [single_typevar] = invalid_later_typevars { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Type variable `{}` does not have a default", single_typevar.name(db), )); } else { let later_typevars = format_enumeration(invalid_later_typevars.iter().map(|tv| tv.name(db))); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Type variables {later_typevars} do not have defaults", )); } @@ -5341,10 +5727,9 @@ pub(crate) fn report_invalid_type_param_order<'db>( let Some(definition) = tvar.definition(db) else { continue; }; - let file = definition.file(db); diagnostic.annotate( Annotation::secondary(Span::from( - definition.full_range(db, &parsed_module(db, file).load(db)), + definition.full_range(db, &parsed_module(db, definition.python_file(db)).load(db)), )) .message(format_args!("`{}` defined here", tvar.name(db))), ); @@ -5388,10 +5773,9 @@ pub(crate) fn report_invalid_typevar_default_reference<'db>( let Some(definition) = tvar.definition(db) else { continue; }; - let file = definition.file(db); diagnostic.annotate( Annotation::secondary(Span::from( - definition.full_range(db, &parsed_module(db, file).load(db)), + definition.full_range(db, &parsed_module(db, definition.python_file(db)).load(db)), )) .message(format_args!("`{}` defined here", tvar.name(db))), ); @@ -5419,6 +5803,7 @@ pub(crate) fn report_inconsistent_generic_bases<'db>( base_nodes: Option<&[ast::Expr]>, ) -> bool { let db = context.db(); + let env = &context.program_environment(); // Maps each generic ancestor's class literal to the first // specialization seen and the index of the explicit base it // came from. @@ -5470,41 +5855,41 @@ pub(crate) fn report_inconsistent_generic_bases<'db>( ) { diagnostic.annotate(context.secondary(earlier_base).message(format_args!( "Earlier class base inherits from `{}`", - earlier_alias.display(db) + earlier_alias.display(db, env) ))); let later_annotation = context.secondary(later_base); diagnostic.annotate(if later_is_direct { later_annotation.message(format_args!( "Later class base is `{}`", - supercls_alias.display(db) + supercls_alias.display(db, env) )) } else { later_annotation.message(format_args!( "Later class base inherits from `{}`", - supercls_alias.display(db) + supercls_alias.display(db, env) )) }); } else { diagnostic.info(format_args!( "Earlier class base inherits from `{}`", - earlier_alias.display(db) + earlier_alias.display(db, env) )); if later_is_direct { diagnostic.info(format_args!( "Later class base is `{}`", - supercls_alias.display(db) + supercls_alias.display(db, env) )); } else { diagnostic.info(format_args!( "Later class base inherits from `{}`", - supercls_alias.display(db) + supercls_alias.display(db, env) )); } } diagnostic.set_concise_message(format_args!( "Inconsistent type arguments: class cannot inherit from both `{}` and `{}`", - supercls_alias.display(db), - earlier_alias.display(db) + supercls_alias.display(db, env), + earlier_alias.display(db, env) )); return true; } @@ -5546,18 +5931,20 @@ pub(crate) fn report_shadowed_type_variable<'db>( TypeVarKind::LegacyTypeVarTuple | TypeVarKind::Pep695TypeVarTuple => "TypeVarTuple", }; let mut diagnostic = builder.into_diagnostic(format_args!( - "Generic {kind} `{name}` uses {typevar_kind} `{typevar_name}` already bound by an enclosing scope", + "Generic {kind} `{name}` uses {typevar_kind} `{typevar_name}` \ + already bound by an enclosing scope", )); diagnostic.set_concise_message(format_args!( - "Generic {kind} `{name}` uses {typevar_kind} `{typevar_name}` already bound by an enclosing scope", + "Generic {kind} `{name}` uses {typevar_kind} `{typevar_name}` \ + already bound by an enclosing scope", )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "`{typevar_name}` used in {kind} definition here" )); let Some(other_definition) = other_typevar.binding_context(db).definition() else { return; }; - let span = match binding_type(db, other_definition) { + let span = match binding_type(context.db(), other_definition) { Type::ClassLiteral(class) => class.header_span(db), Type::FunctionLiteral(function) => function.spans(db).signature, _ => return, @@ -5586,6 +5973,7 @@ pub(super) fn report_invalid_reified_override<'db>( error: crate::types::reified_infer::ReifiedOverrideError<'db>, ) { use crate::types::reified_infer::ReifiedOverrideError; + let env = context.program_environment(); let db = context.db(); @@ -5612,7 +6000,7 @@ pub(super) fn report_invalid_reified_override<'db>( let mut diagnostic = builder.into_diagnostic(format_args!("Invalid override of method `{member}`")); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "reified type parameters are incompatible with `{overridden_method}`" )); @@ -5659,8 +6047,8 @@ pub(super) fn report_invalid_reified_override<'db>( "the bound of type parameter `{sub_name}` (`{}`) rejects specializations \ `{overridden_method}` permits for `{base_name}` (`{}`) — bounds are \ contravariant", - sub_admissible.display(db), - base_admissible.display(db), + sub_admissible.display(db, env), + base_admissible.display(db, env), )); } } @@ -5671,7 +6059,7 @@ pub(super) fn report_invalid_reified_override<'db>( ty: Type::FunctionLiteral(superclass_function), .. }) = superclass - .class_member(db, member, MemberLookupPolicy::default()) + .class_member(db, env, member, MemberLookupPolicy::default()) .place { diagnostic.annotate( @@ -5734,12 +6122,13 @@ pub(super) fn report_invalid_method_override<'db>( let mut diagnostic = builder.into_diagnostic(format_args!("Invalid override of method `{member}`")); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Definition is incompatible with `{overridden_method}`" )); + let env = &context.program_environment(); let class_member = |cls: ClassType<'db>| { - cls.class_member(db, member, MemberLookupPolicy::default()) + cls.class_member(db, env, member, MemberLookupPolicy::default()) .place }; @@ -5765,7 +6154,7 @@ pub(super) fn report_invalid_method_override<'db>( )); } - error_context().attach_to(context.db(), &mut diagnostic); + error_context().attach_to(db, env, &mut diagnostic); diagnostic.info("This violates the Liskov Substitution Principle"); @@ -5789,10 +6178,10 @@ pub(super) fn report_invalid_method_override<'db>( .next() && let Some(definition) = binding.binding.definition() { - let definition_span = Span::from( - definition - .full_range(db, &parsed_module(db, superclass_scope.file(db)).load(db)), - ); + let definition_span = Span::from(definition.full_range( + db, + &parsed_module(db, superclass_scope.python_file(db)).load(db), + )); let superclass_function_span = match superclass_type { Type::FunctionLiteral(function) => Some(signature_span(function)), @@ -5917,7 +6306,7 @@ pub(super) fn report_incompatible_base_method<'db>( "Base classes for class `{}` define method `{member}` incompatibly", class.name(db) )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "`{selected_name}.{member}` is incompatible with `{contract_name}.{member}`" )); if selected_decorator != contract_decorator { @@ -5927,14 +6316,14 @@ pub(super) fn report_incompatible_base_method<'db>( contract_decorator.description(), )); } - error_context().attach_to(db, &mut diagnostic); + error_context().attach_to(db, context.program_environment(), &mut diagnostic); diagnostic.info("This violates the Liskov Substitution Principle"); for (definition, owner_name) in [ (selected_definition, selected_name), (contract_definition, contract_name), ] { - let module = parsed_module(db, definition.file(db)).load(db); + let module = parsed_module(db, definition.python_file(db)).load(db); diagnostic.annotate( Annotation::secondary(Span::from(definition.focus_range(db, &module))) .message(format_args!("`{owner_name}.{member}` defined here")), @@ -5953,7 +6342,6 @@ pub(super) fn report_overridden_final_method<'db>( superclass_method_defs: &[FunctionType<'db>], ) { let db = context.db(); - // Some hijinks so that we emit a diagnostic on the property getter rather than the property setter let property_getter_definition = if subclass_definition.kind(db).is_function_def() && let Type::PropertyInstance(property) = subclass_type @@ -5986,7 +6374,7 @@ pub(super) fn report_overridden_final_method<'db>( let mut diagnostic = builder.into_diagnostic(format_args!("Cannot override `{superclass_name}.{member}`")); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Overrides a definition from superclass `{superclass_name}`" )); diagnostic.set_concise_message(format_args!( @@ -6018,13 +6406,13 @@ pub(super) fn report_overridden_final_method<'db>( sub.annotate( Annotation::secondary(Span::from(superclass_function_literal.focus_range( db, - &parsed_module(db, first_final_superclass_definition.file(db)).load(db), + &parsed_module(db, first_final_superclass_definition.python_file(db)).load(db), ))) .message(format_args!("`{superclass_name}.{member}` defined here")), ); if let Some(decorator_span) = - superclass_function_literal.find_known_decorator_span(db, KnownFunction::Final) + superclass_function_literal.find_known_decorator_span(context.db(), KnownFunction::Final) { sub.annotate(Annotation::secondary(decorator_span)); } @@ -6154,7 +6542,7 @@ pub(super) fn report_overridden_final_variable<'db>( let mut diagnostic = builder.into_diagnostic(format_args!("Cannot override `{superclass_name}.{member}`")); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Overrides a final variable from superclass `{superclass_name}`" )); diagnostic.set_concise_message(format_args!( @@ -6169,10 +6557,10 @@ pub(super) fn report_overridden_final_variable<'db>( ), ); sub.annotate( - Annotation::secondary(Span::from( - superclass_def - .focus_range(db, &parsed_module(db, superclass_def.file(db)).load(db)), - )) + Annotation::secondary(Span::from(superclass_def.focus_range( + db, + &parsed_module(db, superclass_def.python_file(db)).load(db), + ))) .message(format_args!("`{superclass_name}.{member}` defined here")), ); diagnostic.sub(sub); @@ -6193,43 +6581,44 @@ pub(super) fn report_unsupported_comparison<'db>( right_ty: Type<'db>, ) { let db = context.db(); - let Some(diagnostic_builder) = context.report_lint(&UNSUPPORTED_OPERATOR, range) else { return; }; + let env = &context.program_environment(); let display_settings = DisplaySettings::from_possibly_ambiguous_types( db, + env, [error.left_ty, error.right_ty, left_ty, right_ty], ); let mut diagnostic = diagnostic_builder.into_diagnostic(format_args!("Unsupported `{}` operation", error.op)); - if left_ty.is_equivalent_to(db, right_ty) { - diagnostic.set_primary_message(format_args!( + if left_ty.is_equivalent_to(db, env, right_ty) { + diagnostic.set_primary_annotation_message(format_args!( "Both operands have type `{}`", - left_ty.display_with(db, display_settings.clone()) + left_ty.display_with(db, env, display_settings.clone()) )); diagnostic.annotate(context.secondary(left)); diagnostic.annotate(context.secondary(right)); diagnostic.set_concise_message(format_args!( "Operator `{}` is not supported between two objects of type `{}`", error.op, - left_ty.display_with(db, display_settings.clone()) + left_ty.display_with(db, env, display_settings.clone()) )); } else { for (ty, expr) in [(left_ty, left), (right_ty, right)] { diagnostic.annotate(context.secondary(expr).message(format_args!( "Has type `{}`", - ty.display_with(db, display_settings.clone()) + ty.display_with(db, env, display_settings.clone()) ))); } diagnostic.set_concise_message(format_args!( "Operator `{}` is not supported between objects of type `{}` and `{}`", error.op, - left_ty.display_with(db, display_settings.clone()), - right_ty.display_with(db, display_settings.clone()) + left_ty.display_with(db, env, display_settings.clone()), + right_ty.display_with(db, env, display_settings.clone()) )); } @@ -6242,8 +6631,9 @@ pub(super) fn report_unsupported_comparison<'db>( // - `error.left_ty` is `Literal["foo"]` // - `error.right_ty` is `Literal[3]` if (error.left_ty, error.right_ty) != (left_ty, right_ty) { - if let Some(TupleSpec::Fixed(lhs_spec)) = left_ty.tuple_instance_spec(db).as_deref() - && let Some(TupleSpec::Fixed(rhs_spec)) = right_ty.tuple_instance_spec(db).as_deref() + if let Some(TupleSpec::Fixed(lhs_spec)) = left_ty.tuple_instance_spec(db, env).as_deref() + && let Some(TupleSpec::Fixed(rhs_spec)) = + right_ty.tuple_instance_spec(db, env).as_deref() && lhs_spec.len() == rhs_spec.len() && let Some(position) = lhs_spec .all_elements() @@ -6251,13 +6641,13 @@ pub(super) fn report_unsupported_comparison<'db>( .zip(rhs_spec.all_elements()) .position(|tup| tup == (&error.left_ty, &error.right_ty)) { - if error.left_ty.is_equivalent_to(db, error.right_ty) { + if error.left_ty.is_equivalent_to(db, env, error.right_ty) { diagnostic.info(format_args!( "Operation fails because operator `{}` is not supported between \ the tuple elements at index {} (both of type `{}`)", error.op, position + 1, - error.left_ty.display_with(db, display_settings), + error.left_ty.display_with(db, env, display_settings), )); } else { diagnostic.info(format_args!( @@ -6265,25 +6655,29 @@ pub(super) fn report_unsupported_comparison<'db>( the tuple elements at index {} (of type `{}` and `{}`)", error.op, position + 1, - error.left_ty.display_with(db, display_settings.clone()), - error.right_ty.display_with(db, display_settings), + error + .left_ty + .display_with(db, env, display_settings.clone()), + error.right_ty.display_with(db, env, display_settings), )); } } else { - if error.left_ty.is_equivalent_to(db, error.right_ty) { + if error.left_ty.is_equivalent_to(db, env, error.right_ty) { diagnostic.info(format_args!( "Operation fails because operator `{}` is not supported \ between two objects of type `{}`", error.op, - error.left_ty.display_with(db, display_settings), + error.left_ty.display_with(db, env, display_settings), )); } else { diagnostic.info(format_args!( "Operation fails because operator `{}` is not supported \ between objects of type `{}` and `{}`", error.op, - error.left_ty.display_with(db, display_settings.clone()), - error.right_ty.display_with(db, display_settings) + error + .left_ty + .display_with(db, env, display_settings.clone()), + error.right_ty.display_with(db, env, display_settings) )); } } @@ -6358,33 +6752,35 @@ fn report_unsupported_binary_operation_impl<'a>( ) -> Option> { let db = context.db(); let diagnostic_builder = context.report_lint(&UNSUPPORTED_OPERATOR, range)?; - let display_settings = DisplaySettings::from_possibly_ambiguous_types(db, [left_ty, right_ty]); + let env = &context.program_environment(); + let display_settings = + DisplaySettings::from_possibly_ambiguous_types(db, env, [left_ty, right_ty]); let mut diagnostic = diagnostic_builder.into_diagnostic(format_args!("Unsupported `{operator}` operation")); - if left_ty.is_equivalent_to(db, right_ty) { - diagnostic.set_primary_message(format_args!( + if left_ty.is_equivalent_to(db, env, right_ty) { + diagnostic.set_primary_annotation_message(format_args!( "Both operands have type `{}`", - left_ty.display_with(db, display_settings.clone()) + left_ty.display_with(db, env, display_settings.clone()) )); diagnostic.annotate(context.secondary(left)); diagnostic.annotate(context.secondary(right)); diagnostic.set_concise_message(format_args!( "Operator `{operator}` is not supported between two objects of type `{}`", - left_ty.display_with(db, display_settings.clone()) + left_ty.display_with(db, env, display_settings.clone()) )); } else { for (ty, expr) in [(left_ty, left), (right_ty, right)] { diagnostic.annotate(context.secondary(expr).message(format_args!( "Has type `{}`", - ty.display_with(db, display_settings.clone()) + ty.display_with(db, env, display_settings.clone()) ))); } diagnostic.set_concise_message(format_args!( "Operator `{operator}` is not supported between objects of type `{}` and `{}`", - left_ty.display_with(db, display_settings.clone()), - right_ty.display_with(db, display_settings.clone()) + left_ty.display_with(db, env, display_settings.clone()), + right_ty.display_with(db, env, display_settings.clone()) )); } @@ -6400,7 +6796,6 @@ pub(super) fn report_bad_frozen_dataclass_inheritance<'db>( base_is_frozen: bool, ) { let db = context.db(); - let Some(builder) = context.report_lint(&INVALID_FROZEN_DATACLASS_SUBCLASS, class.header_range(db)) else { @@ -6415,7 +6810,7 @@ pub(super) fn report_bad_frozen_dataclass_inheritance<'db>( class.name(db), base_class.name(db) )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Subclass `{}` is not frozen but base class `{}` is", class.name(db), base_class.name(db) @@ -6429,7 +6824,7 @@ pub(super) fn report_bad_frozen_dataclass_inheritance<'db>( class.name(db), base_class.name(db) )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Subclass `{}` is frozen but base class `{}` is not", class.name(db), base_class.name(db) @@ -6459,7 +6854,7 @@ pub(super) fn report_bad_frozen_dataclass_inheritance<'db>( ); let base_class_file = base_class.file(db); - let module = parsed_module(db, base_class_file).load(db); + let module = parsed_module(db, base_class.python_file(db)).load(db); let decorator_range = base_class .body_scope(db) @@ -6493,7 +6888,7 @@ pub(super) fn report_invalid_total_ordering( let mut diagnostic = builder.into_diagnostic( "Class decorated with `@total_ordering` must define at least one ordering method", ); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "`{}` does not define `__lt__`, `__le__`, `__gt__`, or `__ge__`", class.name(db) )); @@ -6515,9 +6910,11 @@ pub(super) fn report_invalid_total_ordering_call( }; let mut diagnostic = builder.into_diagnostic( - "`@functools.total_ordering` requires at least one ordering method (`__lt__`, `__le__`, `__gt__`, or `__ge__`) to be defined", + "`@functools.total_ordering` requires at least one ordering method \ + (`__lt__`, `__le__`, `__gt__`, or `__ge__`) \ + to be defined", ); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "`{}` does not define `__lt__`, `__le__`, `__gt__`, or `__ge__`", class.name(db) )); @@ -6536,6 +6933,8 @@ pub(super) fn report_invalid_total_ordering_call( /// The function returns `true` if a hint was added, `false` otherwise. pub(super) fn hint_if_stdlib_submodule_exists_on_other_versions( db: &dyn Db, + file: File, + env: &ProgramEnvironment<'_>, diagnostic: &mut Diagnostic, full_submodule_name: &ModuleName, parent_module: Module, @@ -6548,14 +6947,14 @@ pub(super) fn hint_if_stdlib_submodule_exists_on_other_versions( return false; } - let program = Program::get(db); + let program = env.program(db); let typeshed_versions = program.search_paths(db).typeshed_versions(); let Some(version_range) = typeshed_versions.exact(full_submodule_name) else { return false; }; - let python_version = program.python_version(db); + let python_version = parent_module.python_version(db); if version_range.contains(python_version) { return false; } @@ -6568,7 +6967,7 @@ pub(super) fn hint_if_stdlib_submodule_exists_on_other_versions( version_range = version_range.diagnostic_display(), )); - add_inferred_python_version_hint_to_diagnostic(db, diagnostic, "resolving modules"); + add_inferred_python_version_hint_to_diagnostic(db, file, diagnostic, "resolving modules"); true } @@ -6583,6 +6982,7 @@ pub(super) fn hint_if_stdlib_submodule_exists_on_other_versions( /// misconfigured their Python version. pub(super) fn hint_if_stdlib_attribute_exists_on_other_versions( db: &dyn Db, + source_file: ProgramFile<'_>, mut diagnostic: LintDiagnosticGuard, value_type: Type, attr: &str, @@ -6595,7 +6995,7 @@ pub(super) fn hint_if_stdlib_attribute_exists_on_other_versions( return; }; let module = module_ty.module(db); - let Some(file) = module.file(db) else { + let Some(module_file) = module.file(db) else { return; }; let Some(search_path) = module.search_path(db) else { @@ -6608,7 +7008,8 @@ pub(super) fn hint_if_stdlib_attribute_exists_on_other_versions( // We populate place_table entries for stdlib items across all known versions and platforms, // so if this lookup succeeds then we know that this lookup *could* succeed with possible // configuration changes. - let symbol_table = place_table(db, global_scope(db, file)); + let program_file = ProgramFile::new(db, module_file, source_file.program(db)); + let symbol_table = place_table(db, global_scope(db, program_file)); let Some(symbol) = symbol_table.symbol_by_name(attr) else { return; }; @@ -6623,7 +7024,12 @@ pub(super) fn hint_if_stdlib_attribute_exists_on_other_versions( // TODO: determine what version they need to be on // TODO: also mention the platform we're assuming // TODO: determine what platform they need to be on - add_inferred_python_version_hint_to_diagnostic(db, &mut diagnostic, action); + add_inferred_python_version_hint_to_diagnostic( + db, + source_file.file(db), + &mut diagnostic, + action, + ); } pub(super) fn report_invalid_concatenate_last_arg<'db>( @@ -6631,14 +7037,16 @@ pub(super) fn report_invalid_concatenate_last_arg<'db>( last_arg: &ast::Expr, last_arg_type: Type<'db>, ) { + let db = context.db(); if let Some(builder) = context.report_lint(&INVALID_TYPE_ARGUMENTS, last_arg) { + let env = &context.program_environment(); let mut diag = builder.into_diagnostic( "The last argument to `typing.Concatenate` must be either `...` or a `ParamSpec` \ type variable", ); - diag.set_primary_message(format_args!( + diag.set_primary_annotation_message(format_args!( "Got `{}`", - last_arg_type.display(context.db()) + last_arg_type.display(db, env) )); } } @@ -6662,7 +7070,7 @@ pub(super) fn report_subclass_of_class_with_non_callable_init_subclass<'db>( let class_name = class.name(db); let mut diagnostic = builder.into_diagnostic(format_args!("Invalid definition of class `{class_name}`")); - + let env = &context.program_environment(); let class_and_def = class .iter_mro(db, None) .filter_map(|base| base.into_class()?.class_literal(db).as_static()) @@ -6672,7 +7080,7 @@ pub(super) fn report_subclass_of_class_with_non_callable_init_subclass<'db>( let symbol = place_table.symbol_id("__init_subclass__")?; let use_def = use_def_map(db, scope); let bindings = use_def.end_of_scope_bindings(ScopedPlaceId::Symbol(symbol)); - let place_with_def = place_from_bindings(db, bindings); + let place_with_def = place_from_bindings(db, env, bindings); if place_with_def.place.is_undefined() { return None; } @@ -6681,10 +7089,10 @@ pub(super) fn report_subclass_of_class_with_non_callable_init_subclass<'db>( if let Some((superclass, definition)) = class_and_def { let superclass_name = superclass.name(db); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Superclass `{superclass_name}` cannot be subclassed", )); - let definition_module = parsed_module(db, definition.file(db)); + let definition_module = parsed_module(db, definition.python_file(db)); let mut annotation = Annotation::secondary(Span::from( definition.focus_range(db, &definition_module.load(db)), )); @@ -6696,7 +7104,7 @@ pub(super) fn report_subclass_of_class_with_non_callable_init_subclass<'db>( annotation = annotation.message(format_args!( "`{superclass_name}.__init_subclass__` has type `{}`, \ which is not callable", - bindings.callable_type().display(db) + bindings.callable_type().display(db, env) )); } else { diagnostic.set_concise_message(format_args!( @@ -6706,12 +7114,12 @@ pub(super) fn report_subclass_of_class_with_non_callable_init_subclass<'db>( annotation = annotation.message(format_args!( "`{superclass_name}.__init_subclass__` has type `{}`, \ which may not be callable", - bindings.callable_type().display(db) + bindings.callable_type().display(db, env) )); } diagnostic.annotate(annotation); } else if err_kind == CallErrorKind::NotCallable { - diagnostic.set_primary_message( + diagnostic.set_primary_annotation_message( "`class` statement will fail because `__init_subclass__` \ on a superclass is not callable", ); @@ -6720,7 +7128,7 @@ pub(super) fn report_subclass_of_class_with_non_callable_init_subclass<'db>( `__init_subclass__` definition on a superclass", )); } else { - diagnostic.set_primary_message( + diagnostic.set_primary_annotation_message( "`class` statement may fail because `__init_subclass__` \ on a superclass may not be callable", ); diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 12535c54f4..2e5eafb4b5 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -1,5 +1,6 @@ //! Display implementations for types. +use crate::ProgramEnvironment; use std::borrow::Cow; use std::cell::RefCell; use std::collections::hash_map::Entry; @@ -7,6 +8,7 @@ use std::fmt::{self, Display, Formatter, Write}; use std::rc::Rc; use ruff_db::files::FilePath; +use ruff_db::parsed::parsed_module; use ruff_db::source::{line_index, source_text}; use ruff_python_ast as ast; use ruff_python_ast::str::{Quote, TripleQuotes}; @@ -15,7 +17,6 @@ use ruff_source_file::LineColumn; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use rustc_hash::{FxHashMap, FxHashSet}; -use ruff_db::parsed::parsed_module; use ty_module_resolver::file_to_module; use crate::Db; @@ -34,11 +35,12 @@ use crate::types::typevar::BoundTypeVarIdentity; use crate::types::visitor::TypeVisitor; use crate::types::{ CallableType, DeferredOperation, DeferredType, DynamicType, IntersectionType, - KnownBoundMethodType, KnownClass, KnownInstanceType, LiteralValueType, LiteralValueTypeKind, - MaterializationKind, ParamSpecAttrKind, PropertyInstanceType, Protocol, ProtocolInstanceType, + KnownBoundMethodType, KnownClass, KnownInstanceType, KnownUnion, LiteralValueType, + LiteralValueTypeKind, MaterializationKind, ParamSpecAttrKind, PropertyInstanceType, Protocol, SpecialFormType, StringLiteralType, SubclassOfInner, SubclassOfType, Type, TypeAliasType, TypeGuardLike, TypedDictModule, TypedDictType, UnionType, WrapperDescriptorKind, visitor, }; +use ty_python_core::ProgramFile; use ty_python_core::definition::Definition; use ty_python_core::scope::{FileScopeId, ScopeKind}; use ty_python_core::semantic_index; @@ -105,6 +107,18 @@ impl SignatureNameDisplay { } } +/// Controls whether numeric-tower unions use annotation spelling or expose their exact members. +/// +/// basedpython only ever expands them. A reader is told what a type *is*, and the +/// promotion is surfaced as an inlay hint on the modules that enable python's float +/// semantics rather than hidden inside a type that reads as something narrower. +#[derive(Debug, Clone, Copy, Default)] +enum NumericTowerDisplay { + /// Display every exact member, such as `int | float`. + #[default] + Expanded, +} + /// Settings for displaying types and signatures #[derive(Debug, Clone, Default)] #[expect( @@ -113,7 +127,7 @@ impl SignatureNameDisplay { )] pub struct DisplaySettings<'db> { /// Whether rendering can be multiline - pub multiline: bool, + multiline: bool, /// Whether callable signatures should include their definition name. signature_name_display: SignatureNameDisplay, /// Class names that should be displayed fully qualified @@ -123,17 +137,19 @@ pub struct DisplaySettings<'db> { /// (e.g., `A.Alias` instead of just `Alias`) qualified_type_aliases: Rc>, /// Whether long unions and literals are displayed in full - pub preserve_full_unions: bool, + preserve_full_unions: bool, + /// How numeric-tower unions should be displayed. + numeric_tower_display: NumericTowerDisplay, /// Scopes that are currently active in the display context (e.g. function scopes /// whose type parameters are currently being displayed). /// Used to suppress redundant `@{scope}` suffixes for type variables. - pub active_scopes: Rc>>, + active_scopes: Rc>>, /// Function types that are currently being displayed. /// Used to prevent infinite recursion when displaying self-referential function types. - pub visited_function_types: Rc>>, + visited_function_types: Rc>>, /// Whether to hide the return type of the outermost signature. /// Return types of nested callable types inside parameters are still shown. - pub hide_return_type: bool, + hide_return_type: bool, /// basedpython: whether the caller has already written the `def ` this signature /// belongs to, as the bound-method display does. Such a signature is a *declaration*, so it /// leaves out a `None` return the way the source may. @@ -188,15 +204,27 @@ impl<'db> DisplaySettings<'db> { } #[must_use] - pub fn preserve_long_unions(self) -> Self { + pub(crate) fn preserve_long_unions(self) -> Self { Self { preserve_full_unions: true, ..self } } + /// Expands numeric-tower unions so explanations can refer to their individual members. + /// + /// For example, a relation error that discusses the `int` member of a `float` annotation + /// displays the union as `int | float*` instead of hiding that member behind `float`. + #[must_use] + pub(crate) fn expand_numeric_tower_unions(&self) -> Self { + Self { + numeric_tower_display: NumericTowerDisplay::Expanded, + ..self.clone() + } + } + #[must_use] - pub fn disallow_signature_name(&self) -> Self { + pub(crate) fn disallow_signature_name(&self) -> Self { Self { signature_name_display: SignatureNameDisplay::Disallow, ..self.clone() @@ -212,7 +240,7 @@ impl<'db> DisplaySettings<'db> { } #[must_use] - pub fn hide_return_type(&self) -> Self { + pub(crate) fn hide_return_type(&self) -> Self { Self { hide_return_type: true, ..self.clone() @@ -257,13 +285,17 @@ impl<'db> DisplaySettings<'db> { } #[must_use] - pub fn from_possibly_ambiguous_types(db: &'db dyn Db, types: I) -> Self + pub fn from_possibly_ambiguous_types( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + types: I, + ) -> Self where I: IntoIterator, T: Into>, { fn build_display_settings<'db>( - collector: &AmbiguousNameCollector<'db>, + collector: &AmbiguousNameCollector<'_, 'db>, ) -> DisplaySettings<'db> { // Both classes and type aliases use the same qualification map since // a class and type alias with the same name need to be disambiguated. @@ -275,7 +307,11 @@ impl<'db> DisplaySettings<'db> { } } - let collector = AmbiguousNameCollector::default(); + let collector = AmbiguousNameCollector { + env, + visited_types: RefCell::default(), + names: RefCell::default(), + }; for ty in types { collector.visit_type(db, ty.into()); @@ -522,13 +558,13 @@ impl QualificationLevel { } } -#[derive(Debug, Default)] -struct AmbiguousNameCollector<'db> { +struct AmbiguousNameCollector<'a, 'db> { + env: &'a ProgramEnvironment<'db>, visited_types: RefCell>>, names: RefCell>>, } -impl<'db> AmbiguousNameCollector<'db> { +impl<'db> AmbiguousNameCollector<'_, 'db> { /// Records an item for ambiguity tracking. /// /// This updates the ambiguity state for items with the same name: @@ -612,7 +648,11 @@ enum AmbiguityState<'db> { RequiresFileAndLineNumber, } -impl<'db> TypeVisitor<'db> for AmbiguousNameCollector<'db> { +impl<'db> TypeVisitor<'db> for AmbiguousNameCollector<'_, 'db> { + fn program_environment(&self) -> &ProgramEnvironment<'db> { + self.env + } + fn should_visit_lazy_type_attributes(&self) -> bool { false } @@ -632,10 +672,9 @@ impl<'db> TypeVisitor<'db> for AmbiguousNameCollector<'db> { // Visit the class (as if it were a nominal-instance type) // rather than the protocol members, if it is a class-based protocol. // (For the purposes of displaying the type, we'll use the class name.) - Type::ProtocolInstance(ProtocolInstanceType { - inner: Protocol::FromClass(class), - .. - }) => return self.visit_type(db, Type::from(class)), + Type::ProtocolInstance(protocol) if let Some(class) = protocol.class_origin(db) => { + return self.visit_type(db, Type::from(class)); + } // no need to recurse into TypeVar bounds/constraints Type::TypeVar(_) => return, _ => {} @@ -652,42 +691,73 @@ impl<'db> TypeVisitor<'db> for AmbiguousNameCollector<'db> { } impl<'db> Type<'db> { - pub fn display(self, db: &'db dyn Db) -> DisplayType<'db> { + pub fn display<'env>( + self, + db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, + ) -> DisplayType<'env, 'db> { DisplayType { ty: self, - settings: DisplaySettings::from_possibly_ambiguous_types(db, [self]), + settings: DisplaySettings::from_possibly_ambiguous_types(db, env, [self]), db, + env, } } - pub fn display_with(self, db: &'db dyn Db, settings: DisplaySettings<'db>) -> DisplayType<'db> { + pub fn display_with<'env>( + self, + db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, + settings: DisplaySettings<'db>, + ) -> DisplayType<'env, 'db> { DisplayType { ty: self, db, + env, settings, } } - fn representation( + fn representation<'env>( self, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, settings: DisplaySettings<'db>, - ) -> DisplayRepresentation<'db> { + ) -> DisplayRepresentation<'env, 'db> { DisplayRepresentation { db, + env, ty: self, settings, } } } -pub struct DisplayType<'db> { +pub struct DisplayType<'env, 'db> { ty: Type<'db>, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, settings: DisplaySettings<'db>, } -impl<'db> DisplayType<'db> { +impl<'db> DisplayType<'_, 'db> { + /// Allows this type display to span multiple lines while preserving inferred qualification. + #[must_use] + pub fn multiline(self) -> Self { + Self { + settings: self.settings.multiline(), + ..self + } + } + + #[must_use] + pub(crate) fn preserve_long_unions(self) -> Self { + Self { + settings: self.settings.preserve_long_unions(), + ..self + } + } + pub fn to_string_parts(&self) -> TypeDisplayDetails<'db> { let mut f = TypeWriter::Details(TypeDetailsWriter::new()); self.fmt_detailed(&mut f).unwrap(); @@ -699,9 +769,10 @@ impl<'db> DisplayType<'db> { } } -impl<'db> FmtDetailed<'db> for DisplayType<'db> { +impl<'db> FmtDetailed<'db> for DisplayType<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { - let representation = self.ty.representation(self.db, self.settings.clone()); + let db = self.db; + let representation = self.ty.representation(db, self.env, self.settings.clone()); match self.ty.as_literal_value_kind() { Some( LiteralValueTypeKind::Int(_) @@ -733,13 +804,13 @@ impl<'db> FmtDetailed<'db> for DisplayType<'db> { } } -impl Display for DisplayType<'_> { +impl Display for DisplayType<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_detailed(&mut TypeWriter::Formatter(f)) } } -impl fmt::Debug for DisplayType<'_> { +impl fmt::Debug for DisplayType<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { Display::fmt(self, f) } @@ -799,11 +870,11 @@ fn fmt_file_location<'db>( /// A vector of path components in order (e.g., `["module", "OuterClass", "InnerClass"]`) pub(super) fn qualified_name_components_from_scope( db: &dyn Db, - file: ruff_db::files::File, + file: ProgramFile<'_>, file_scope_id: FileScopeId, skip_count: usize, ) -> Vec { - let module_ast = parsed_module(db, file).load(db); + let module_ast = parsed_module(db, file.python_file(db)).load(db); let index = semantic_index(db, file); let mut name_parts = vec![]; @@ -829,7 +900,7 @@ pub(super) fn qualified_name_components_from_scope( } } - if let Some(module) = file_to_module(db, file) { + if let Some(module) = file_to_module(db, file.resolver_file(db)) { let module_name = module.name(db); name_parts.push(module_name.as_str().to_string()); } @@ -839,23 +910,31 @@ pub(super) fn qualified_name_components_from_scope( } impl<'db> ClassLiteral<'db> { - fn display_with(self, db: &'db dyn Db, settings: DisplaySettings<'db>) -> ClassDisplay<'db> { + fn display_with<'env>( + self, + db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, + settings: DisplaySettings<'db>, + ) -> ClassDisplay<'env, 'db> { ClassDisplay { db, + env, class: self, settings, } } } -struct ClassDisplay<'db> { +struct ClassDisplay<'env, 'db> { db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, class: ClassLiteral<'db>, settings: DisplaySettings<'db>, } -impl<'db> FmtDetailed<'db> for ClassDisplay<'db> { +impl<'db> FmtDetailed<'db> for ClassDisplay<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { + let env = self.env; // basedpython anonymous named tuples render as their surface syntax // `(name: T, ...)` rather than the synthesized `_AnonNamedTuple_` // class name. Positional fields use the synthetic `arg` name and @@ -883,7 +962,7 @@ impl<'db> FmtDetailed<'db> for ClassDisplay<'db> { } field .ty - .display_with(self.db, self.settings.clone()) + .display_with(self.db, env, self.settings.clone()) .fmt_detailed(&mut f)?; } f.write_char(')')?; @@ -910,7 +989,7 @@ impl<'db> FmtDetailed<'db> for ClassDisplay<'db> { } } -impl Display for ClassDisplay<'_> { +impl Display for ClassDisplay<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_detailed(&mut TypeWriter::Formatter(f)) } @@ -930,14 +1009,20 @@ impl<'db> TypeAliasType<'db> { } /// Returns a source-style display of this type alias's declaration. - pub fn display_declaration(self, db: &'db dyn Db) -> impl Display + 'db { + pub fn display_declaration<'env>( + self, + db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, + ) -> impl Display + 'env { let value_ty = self.raw_value_type(db); DisplayTypeAliasDeclaration { db, + env, type_alias: self, value_ty, settings: DisplaySettings::from_possibly_ambiguous_types( db, + env, [Type::TypeAlias(self), value_ty], ), } @@ -969,7 +1054,10 @@ impl<'db> FmtDetailed<'db> for TypeAliasDisplay<'db> { let definition = self.type_alias.definition(self.db); let file = definition.file(self.db); let offset = definition - .focus_range(self.db, &parsed_module(self.db, file).load(self.db)) + .focus_range( + self.db, + &parsed_module(self.db, definition.python_file(self.db)).load(self.db), + ) .range() .start(); fmt_file_location(self.db, file, offset, f)?; @@ -985,37 +1073,37 @@ impl Display for TypeAliasDisplay<'_> { } /// A source-style display of a type alias declaration. -struct DisplayTypeAliasDeclaration<'db> { +struct DisplayTypeAliasDeclaration<'env, 'db> { db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, type_alias: TypeAliasType<'db>, value_ty: Type<'db>, settings: DisplaySettings<'db>, } -impl<'db> FmtDetailed<'db> for DisplayTypeAliasDeclaration<'db> { +impl<'db> FmtDetailed<'db> for DisplayTypeAliasDeclaration<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { - let generic_context = self.type_alias.generic_context(self.db); + let db = self.db; + let generic_context = self.type_alias.generic_context(db); let settings = self .settings - .with_generic_context(self.db, generic_context.as_ref()); + .with_generic_context(db, generic_context.as_ref()); f.write_str("type ")?; self.type_alias - .display_with(self.db, settings.clone()) + .display_with(db, settings.clone()) .fmt_detailed(f)?; if let Some(generic_context) = generic_context { - generic_context - .display_with(self.db, settings.clone()) - .fmt_detailed(f)?; + generic_context.display(db).fmt_detailed(f)?; } f.write_str(" = ")?; self.value_ty - .display_with(self.db, settings) + .display_with(db, self.env, settings) .fmt_detailed(f) } } -impl Display for DisplayTypeAliasDeclaration<'_> { +impl Display for DisplayTypeAliasDeclaration<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_detailed(&mut TypeWriter::Formatter(f)) } @@ -1024,6 +1112,7 @@ impl Display for DisplayTypeAliasDeclaration<'_> { /// Helper for displaying `TypeGuardLike` types `TypeIs` and `TypeGuard`. fn fmt_type_guard_like<'db, T: TypeGuardLike<'db>>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, guard: T, settings: &DisplaySettings<'db>, f: &mut TypeWriter<'_, '_, 'db>, @@ -1033,7 +1122,7 @@ fn fmt_type_guard_like<'db, T: TypeGuardLike<'db>>( f.write_char('[')?; guard .type_argument(db) - .display_with(db, settings.singleline()) + .display_with(db, env, settings.singleline()) .fmt_detailed(f)?; if let Some(name) = guard.place_name(db) { f.set_invalid_type_annotation(); @@ -1082,6 +1171,7 @@ fn deferred_call_receiver<'db>( /// parenthesised. fn fmt_deferred_operation<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, deferred: DeferredType<'db>, settings: &DisplaySettings<'db>, minimum_binding_power: u8, @@ -1095,9 +1185,9 @@ fn fmt_deferred_operation<'db>( let operand = |f: &mut TypeWriter<'_, '_, 'db>, ty: Type<'db>, minimum: u8| match ty { Type::Deferred(nested) if nested.is_checked(db) => { - fmt_deferred_operation(db, nested, settings, minimum, f) + fmt_deferred_operation(db, env, nested, settings, minimum, f) } - _ => ty.display_with(db, settings.clone()).fmt_detailed(f), + _ => ty.display_with(db, env, settings.clone()).fmt_detailed(f), }; // a receiver has to bind tighter than every operator, so an arithmetic one is @@ -1141,8 +1231,8 @@ fn fmt_deferred_operation<'db>( // count, or a call through a callee that names no receiver, is still better // shown reduced than not at all _ => Type::Deferred(deferred) - .reduce_deferred(db) - .display_with(db, settings.clone()) + .reduce_deferred(db, env) + .display_with(db, env, settings.clone()) .fmt_detailed(f)?, } @@ -1155,9 +1245,10 @@ fn fmt_deferred_operation<'db>( /// 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> { +struct DisplayRepresentation<'env, 'db> { ty: Type<'db>, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, settings: DisplaySettings<'db>, } @@ -1169,14 +1260,16 @@ fn property_display_name(db: &dyn Db, property: PropertyInstanceType<'_>) -> &'s } } -impl Display for DisplayRepresentation<'_> { +impl Display for DisplayRepresentation<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_detailed(&mut TypeWriter::Formatter(f)) } } -impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { +impl<'db> FmtDetailed<'db> for DisplayRepresentation<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { + let env = self.env; + let db = self.db; match self.ty { Type::Dynamic(dynamic) => { if dynamic.is_todo() { @@ -1187,38 +1280,71 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { Type::Divergent(_) => f.with_type(self.ty).write_str("Divergent"), Type::Never => f.with_type(self.ty).write_str("Never"), Type::NominalInstance(instance) => { - let class = instance.class(self.db); + let class = instance.class(db, self.env); - match (class, class.known(self.db)) { + match (class, class.known(db)) { (_, Some(KnownClass::NoneType)) => f.with_type(self.ty).write_str("None"), - (_, Some(KnownClass::NoDefaultType)) => f.with_type(self.ty).write_str("NoDefault"), + (_, Some(KnownClass::NoDefaultType)) => { + f.with_type(self.ty).write_str("NoDefault") + } (ClassType::Generic(alias), Some(KnownClass::Tuple)) => alias - .specialization(self.db) - .tuple(self.db) - .expect("Specialization::tuple() should always return `Some()` for `KnownClass::Tuple`") - .display_with(self.db, self.settings.clone()) + .specialization(db) + .tuple(db) + .expect( + "Specialization::tuple() should always return `Some()` for \ + `KnownClass::Tuple`", + ) + .display_with(db, self.env, self.settings.clone()) + .fmt_detailed(f), + (ClassType::NonGeneric(class), _) => class + .display_with(db, env, self.settings.clone()) + .fmt_detailed(f), + (ClassType::Generic(alias), _) => alias + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f), - (ClassType::NonGeneric(class), _) => { - class.display_with(self.db, self.settings.clone()).fmt_detailed(f) - }, - (ClassType::Generic(alias), _) => alias.display_with(self.db, self.settings.clone()).fmt_detailed(f), } } Type::ProtocolInstance(protocol) => match protocol.inner { Protocol::FromClass(class) => match *class { ClassType::NonGeneric(class) => class - .display_with(self.db, self.settings.clone()) + .display_with(db, env, self.settings.clone()) .fmt_detailed(f), ClassType::Generic(alias) => alias - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f), }, + Protocol::Materialized(materialized) => { + let materialization_kind = protocol.display_materialization_kind(db, self.env); + if let Some(kind) = materialization_kind { + let (name, form) = match kind { + MaterializationKind::Top => ("Top", SpecialFormType::Top), + MaterializationKind::Bottom => ("Bottom", SpecialFormType::Bottom), + }; + f.with_type(Type::SpecialForm(form)).write_str(name)?; + f.write_char('[')?; + } + + match *materialized.origin(db) { + ClassType::NonGeneric(class) => class + .display_with(db, env, self.settings.clone()) + .fmt_detailed(f), + ClassType::Generic(alias) => alias + .display_with(db, self.env, self.settings.clone()) + .fmt_detailed(f), + }?; + + if materialization_kind.is_some() { + f.write_char(']')?; + } + Ok(()) + } Protocol::Synthesized(synthetic) => { // basedpython: a structural type *is* writable here — `protocol(...)` is // the syntax that declares one — so it is spelled rather than described if basedpython_display_enabled() && let Some(inline) = DisplayInlineProtocol::new( self.db, + env, synthetic.interface(self.db), self.settings.clone(), ) @@ -1247,7 +1373,7 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { } } for (i, pack) in packs.iter().enumerate() { - write!(f, "**{}", pack.display(self.db))?; + write!(f, "**{}", pack.display(self.db, env))?; if i + 1 != packs.len() { f.write_str(", ")?; } @@ -1257,15 +1383,14 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { }, Type::PropertyInstance(property) => f .with_type(self.ty) - .write_str(property_display_name(self.db, property)), + .write_str(property_display_name(db, property)), Type::ModuleLiteral(module) => { f.set_invalid_type_annotation(); f.write_char('<')?; - f.with_type(KnownClass::ModuleType.to_class_literal(self.db)) + f.with_type(KnownClass::ModuleType.to_class_literal(db, self.env)) .write_str("module")?; f.write_str(" '")?; - f.with_type(self.ty) - .write_str(module.module(self.db).name(self.db))?; + f.with_type(self.ty).write_str(module.module(db).name(db))?; f.write_str("'>") } Type::ClassLiteral(class) => { @@ -1273,7 +1398,7 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { let mut f = f.with_type(self.ty); f.write_str("") } @@ -1282,56 +1407,56 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { let mut f = f.with_type(self.ty); f.write_str("") } Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { SubclassOfInner::Class(ClassType::NonGeneric(class)) => { - f.with_type(KnownClass::Type.to_class_literal(self.db)) + f.with_type(KnownClass::Type.to_class_literal(db, self.env)) .write_str("type")?; f.write_char('[')?; class - .display_with(self.db, self.settings.clone()) + .display_with(db, env, self.settings.clone()) .fmt_detailed(f)?; f.write_char(']') } SubclassOfInner::Class(ClassType::Generic(alias)) => { - f.with_type(KnownClass::Type.to_class_literal(self.db)) + f.with_type(KnownClass::Type.to_class_literal(db, self.env)) .write_str("type")?; f.write_char('[')?; alias - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f)?; f.write_char(']') } SubclassOfInner::Dynamic(dynamic) => { - f.with_type(KnownClass::Type.to_class_literal(self.db)) + f.with_type(KnownClass::Type.to_class_literal(db, self.env)) .write_str("type")?; f.write_char('[')?; write!(f.with_type(Type::Dynamic(dynamic)), "{dynamic}")?; f.write_char(']') } SubclassOfInner::Protocol(protocol) => { - f.with_type(KnownClass::Type.to_class_literal(self.db)) + f.with_type(KnownClass::Type.to_class_literal(db, self.env)) .write_str("type")?; f.write_char('[')?; Type::ProtocolInstance(protocol) - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f)?; f.write_char(']') } SubclassOfInner::TypeVar(bound_typevar) => { f.set_invalid_type_annotation(); - f.with_type(KnownClass::Type.to_class_literal(self.db)) + f.with_type(KnownClass::Type.to_class_literal(db, self.env)) .write_str("type")?; f.write_char('[')?; write!( f.with_type(Type::TypeVar(bound_typevar)), "{}", bound_typevar - .identity(self.db) - .display_with(self.db, self.settings.clone()) + .identity(db) + .display_with(db, self.settings.clone()) )?; f.write_char(']') } @@ -1341,42 +1466,44 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { write!(f.with_type(self.ty), "") } Type::KnownInstance(known_instance) => known_instance - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f), Type::FunctionLiteral(function) => function - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f), Type::Callable(callable) => callable - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f), Type::BoundMethod(bound_method) => { - let function = bound_method.function(self.db); - let self_ty = bound_method.self_instance(self.db); - let bound_signatures = bound_method.bound_signatures(self.db); + let function = bound_method.function(db); + let self_ty = bound_method.self_instance(db); + let bound_signatures = bound_method.bound_signatures(db); match bound_signatures.overloads.as_slice() { [signature] => { - let hide_unused_self = signature.should_hide_self_from_display(self.db); + let hide_unused_self = + signature.should_hide_self_from_display(db, self.env); let type_parameters = DisplayOptionalGenericContext { generic_context: signature.generic_context.as_ref(), - db: self.db, - settings: self.settings.clone(), + db, hide_unused_self, }; f.set_invalid_type_annotation(); f.write_str("bound method ")?; DisplayMaybeParenthesizedType { ty: self_ty, - db: self.db, + db, + env: self.env, settings: self.settings.singleline(), } .fmt_detailed(f)?; f.write_char('.')?; - f.with_type(self.ty).write_str(function.name(self.db))?; + f.with_type(self.ty).write_str(function.name(db))?; type_parameters.fmt_detailed(f)?; signature .display_with( self.db, + env, self.settings .disallow_signature_name() .name_already_written(), @@ -1394,7 +1521,11 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { let separator = if self.settings.multiline { "\n" } else { ", " }; let mut join = f.join(separator); for signature in signatures { - join.entry(&signature.display_with(self.db, self.settings.clone())); + join.entry(&signature.display_with( + db, + self.env, + self.settings.clone(), + )); } join.finish()?; if !self.settings.multiline { @@ -1412,44 +1543,44 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { "__get__", "function", Type::FunctionLiteral(function), - Some(&**function.name(self.db)), + Some(&**function.name(db)), ), KnownBoundMethodType::FunctionTypeDunderCall(function) => ( KnownClass::FunctionType, "__call__", "function", Type::FunctionLiteral(function), - Some(&**function.name(self.db)), + Some(&**function.name(db)), ), KnownBoundMethodType::PropertyDunderGet(property) => ( - property.instance_class(self.db), + property.instance_class(db), "__get__", - property_display_name(self.db, property), + property_display_name(db, property), Type::PropertyInstance(property), property - .getter(self.db) + .getter(db) .and_then(Type::as_function_literal) - .map(|getter| &**getter.name(self.db)), + .map(|getter| &**getter.name(db)), ), KnownBoundMethodType::PropertyDunderSet(property) => ( - property.instance_class(self.db), + property.instance_class(db), "__set__", - property_display_name(self.db, property), + property_display_name(db, property), Type::PropertyInstance(property), property - .setter(self.db) + .setter(db) .and_then(Type::as_function_literal) - .map(|setter| &**setter.name(self.db)), + .map(|setter| &**setter.name(db)), ), KnownBoundMethodType::PropertyDunderDelete(property) => ( - property.instance_class(self.db), + property.instance_class(db), "__delete__", - property_display_name(self.db, property), + property_display_name(db, property), Type::PropertyInstance(property), property - .deleter(self.db) + .deleter(db) .and_then(Type::as_function_literal) - .map(|deleter| &**deleter.name(self.db)), + .map(|deleter| &**deleter.name(db)), ), KnownBoundMethodType::StrStartswith(literal) => ( KnownClass::Property, @@ -1458,8 +1589,17 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { Type::LiteralValue(LiteralValueType::promotable( LiteralValueTypeKind::String(literal), )), - Some(literal.value(self.db)), + Some(literal.value(db)), ), + KnownBoundMethodType::ConstraintSetLowerBound => { + return f.write_str("bound method `ConstraintSet.lower_bound`"); + } + KnownBoundMethodType::ConstraintSetUpperBound => { + return f.write_str("bound method `ConstraintSet.upper_bound`"); + } + KnownBoundMethodType::ConstraintSetEquality => { + return f.write_str("bound method `ConstraintSet.equality`"); + } KnownBoundMethodType::ConstraintSetRange => { return f.write_str("bound method `ConstraintSet.range`"); } @@ -1475,6 +1615,9 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { KnownBoundMethodType::ConstraintSetSatisfies(_) => { return f.write_str("bound method `ConstraintSet.satisfies`"); } + KnownBoundMethodType::ConstraintSetExists(_) => { + return f.write_str("bound method `ConstraintSet.exists`"); + } KnownBoundMethodType::ConstraintSetForAll(_) => { return f.write_str("bound method `ConstraintSet.for_all`"); } @@ -1493,13 +1636,13 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { } }; - let class_ty = cls.to_class_literal(self.db); + let class_ty = cls.to_class_literal(db, self.env); f.write_char('<')?; - f.with_type(KnownClass::MethodWrapperType.to_class_literal(self.db)) + f.with_type(KnownClass::MethodWrapperType.to_class_literal(db, self.env)) .write_str("method-wrapper")?; f.write_str(" '")?; if let Place::Defined(DefinedPlace { ty: member_ty, .. }) = - class_ty.member(self.db, member_name).place + class_ty.member(db, self.env, member_name).place { f.with_type(member_ty).write_str(member_name)?; } else { @@ -1532,12 +1675,12 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { } }; f.write_char('<')?; - f.with_type(KnownClass::WrapperDescriptorType.to_class_literal(self.db)) + f.with_type(KnownClass::WrapperDescriptorType.to_class_literal(db, self.env)) .write_str("wrapper-descriptor")?; f.write_str(" '")?; f.write_str(method)?; f.write_str("' of '")?; - f.with_type(cls.to_class_literal(self.db)) + f.with_type(cls.to_class_literal(db, self.env)) .write_str(object)?; f.write_str("' objects>") } @@ -1550,25 +1693,26 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { f.write_str("") } Type::Union(union) => union - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f), Type::Intersection(intersection) => intersection - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f), Type::EnumComplement(complement) => { if let Some(literals) = - complement.remaining_literal_types_for_display(self.db, LITERAL_POLICY.max) + complement.remaining_literal_types_for_display(db, self.env, LITERAL_POLICY.max) { DisplayLiteralGroup { literals, - db: self.db, + db, + env: self.env, settings: self.settings.clone(), } .fmt_detailed(f) } else { complement - .to_intersection(self.db) - .display_with(self.db, self.settings.clone()) + .to_intersection(db, self.env) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f) } } @@ -1579,11 +1723,7 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { .write_str(if boolean { "True" } else { "False" }) } LiteralValueTypeKind::String(string) => { - write!( - f.with_type(self.ty), - "{}", - string.display_with(self.db, self.settings.clone()), - ) + write!(f.with_type(self.ty), "{}", string.display(db)) } // We used to return `str` as the type here because that feels generally more useful. // However, the inconsistency between the type shown in the inlay hint and its hover, and the @@ -1593,8 +1733,7 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { .with_type(Type::SpecialForm(SpecialFormType::LiteralString)) .write_str("LiteralString"), LiteralValueTypeKind::Bytes(bytes) => { - let escape = - AsciiEscape::with_preferred_quote(bytes.value(self.db), Quote::Double); + let escape = AsciiEscape::with_preferred_quote(bytes.value(db), Quote::Double); write!( f.with_type(self.ty), @@ -1604,14 +1743,14 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { } LiteralValueTypeKind::Enum(enum_literal) => { enum_literal - .enum_class(self.db) - .display_with(self.db, self.settings.clone()) + .enum_class(db) + .display_with(db, env, self.settings.clone()) .fmt_detailed(f)?; f.write_char('.')?; write!( f.with_type(Type::enum_literal(enum_literal)), "{}", - enum_literal.name(self.db) + enum_literal.name(db) ) } LiteralValueTypeKind::Float(v) => write!(f.with_type(self.ty), "{v}"), @@ -1631,8 +1770,8 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { f, "{}", bound_typevar - .identity(self.db) - .display_with(self.db, self.settings.clone()) + .identity(db) + .display_with(db, self.settings.clone()) ) } Type::AlwaysTruthy => f.with_type(self.ty).write_str("AlwaysTruthy"), @@ -1640,28 +1779,28 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { Type::BoundSuper(bound_super) => { f.set_invalid_type_annotation(); f.write_str("") } - Type::TypeIs(type_is) => fmt_type_guard_like(self.db, type_is, &self.settings, f), + Type::TypeIs(type_is) => fmt_type_guard_like(db, self.env, type_is, &self.settings, f), Type::TypeGuard(type_guard) => { - fmt_type_guard_like(self.db, type_guard, &self.settings, f) + fmt_type_guard_like(db, self.env, type_guard, &self.settings, f) } Type::TypeForm(typeform) => { f.with_type(Type::SpecialForm(SpecialFormType::TypeForm)) .write_str("TypeForm")?; f.write_char('[')?; typeform - .type_argument(self.db) - .display_with(self.db, self.settings.clone()) + .type_argument(db) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f)?; f.write_char(']') } @@ -1674,7 +1813,7 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { f.write_str(", ")?; } element - .display_with(self.db, self.settings.clone()) + .display_with(self.db, env, self.settings.clone()) .fmt_detailed(f)?; } f.write_char(']') @@ -1684,7 +1823,7 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { f.write_char(' ')?; restricted .type_argument(self.db) - .display_with(self.db, self.settings.clone()) + .display_with(self.db, env, self.settings.clone()) .fmt_detailed(f) } Type::Overlapping(overlapping) => { @@ -1693,7 +1832,7 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { f.write_char('[')?; overlapping .type_argument(self.db) - .display_with(self.db, self.settings.clone()) + .display_with(self.db, env, self.settings.clone()) .fmt_detailed(f)?; f.write_char(']') } @@ -1703,14 +1842,14 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { Type::Deferred(deferred) if deferred.is_checked(self.db) && !self.settings.reduce_symbolic_operations => { - fmt_deferred_operation(self.db, deferred, &self.settings, 0, f) + fmt_deferred_operation(self.db, env, deferred, &self.settings, 0, f) } // every other unspecialized operation displays as its reduced form (`T.a` shows // as the bound's `a`); once specialized it has folded to a concrete type and // this arm is not reached Type::Deferred(deferred) => deferred - .reduced(self.db) - .display_with(self.db, self.settings.clone()) + .reduced(self.db, env) + .display_with(self.db, env, self.settings.clone()) .fmt_detailed(f), Type::TypedDict(TypedDictType::Class(defining_class)) => { // basedpython: a dict-literal type reads back as the shape it was written as — @@ -1728,7 +1867,7 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { write!(f, "\"{name}\": ")?; field .declared_ty - .display_with(self.db, self.settings.clone()) + .display_with(self.db, env, self.settings.clone()) .fmt_detailed(f)?; wrote_any = true; } @@ -1737,7 +1876,7 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { f.write_str(", ")?; } f.write_str("**")?; - pack.display_with(self.db, self.settings.clone()) + pack.display_with(self.db, env, self.settings.clone()) .fmt_detailed(f)?; wrote_any = true; } @@ -1745,10 +1884,10 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { } match defining_class { ClassType::NonGeneric(class) => class - .display_with(self.db, self.settings.clone()) + .display_with(self.db, env, self.settings.clone()) .fmt_detailed(f), ClassType::Generic(alias) => alias - .display_with(self.db, self.settings.clone()) + .display_with(self.db, env, self.settings.clone()) .fmt_detailed(f), } } @@ -1765,7 +1904,7 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { ))) .write_str("TypedDict")?; f.write_str(" with items ")?; - let items = synthesized.items(self.db); + let items = synthesized.items(db); for (i, name) in items.keys().enumerate() { let is_last = i == items.len() - 1; write!(f, "'{name}'")?; @@ -1776,17 +1915,31 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { f.write_char('>') } Type::TypeAlias(alias) => { + let materialization_kind = alias.materialization_kind(db); + if let Some(kind) = materialization_kind { + let (name, form) = match kind { + MaterializationKind::Top => ("Top", SpecialFormType::Top), + MaterializationKind::Bottom => ("Bottom", SpecialFormType::Bottom), + }; + f.with_type(Type::SpecialForm(form)).write_str(name)?; + f.write_char('[')?; + } + alias - .display_with(self.db, self.settings.clone()) + .display_with(db, self.settings.clone()) .fmt_detailed(f)?; - match alias.specialization(self.db) { - None => Ok(()), - Some(specialization) => specialization - .display_short(self.db, TupleSpecialization::No, self.settings.clone()) - .fmt_detailed(f), + if let Some(specialization) = alias.specialization(db) { + specialization + .display_short(db, self.env, TupleSpecialization::No, self.settings.clone()) + .fmt_detailed(f)?; } + + if materialization_kind.is_some() { + f.write_char(']')?; + } + Ok(()) } - Type::NewTypeInstance(newtype) => f.with_type(self.ty).write_str(newtype.name(self.db)), + Type::NewTypeInstance(newtype) => f.with_type(self.ty).write_str(newtype.name(db)), } } } @@ -1849,11 +2002,13 @@ impl<'db> TupleSpec<'db> { fn display_with<'a>( &'a self, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, settings: DisplaySettings<'db>, ) -> DisplayTuple<'a, 'db> { DisplayTuple { tuple: self, db, + env, settings, } } @@ -1862,15 +2017,18 @@ impl<'db> TupleSpec<'db> { struct DisplayTuple<'a, 'db> { tuple: &'a TupleSpec<'db>, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, settings: DisplaySettings<'db>, } impl<'db> FmtDetailed<'db> for DisplayTuple<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { + let db = self.db; + let env = self.env; if basedpython_display_enabled() { return self.fmt_basedpython(f); } - f.with_type(KnownClass::Tuple.to_class_literal(self.db)) + f.with_type(KnownClass::Tuple.to_class_literal(self.db, env)) .write_str("tuple")?; f.write_char('[')?; match self.tuple { @@ -1880,7 +2038,7 @@ impl<'db> FmtDetailed<'db> for DisplayTuple<'_, 'db> { f.write_str("()")?; } else { elements - .display_with(self.db, self.settings.singleline()) + .display_with(db, self.env, self.settings.singleline()) .fmt_detailed(f)?; } } @@ -1888,7 +2046,7 @@ impl<'db> FmtDetailed<'db> for DisplayTuple<'_, 'db> { if !tuple.prefix_elements().is_empty() { tuple .prefix_elements() - .display_with(self.db, self.settings.singleline()) + .display_with(db, self.env, self.settings.singleline()) .fmt_detailed(f)?; f.write_str(", ")?; } @@ -1896,7 +2054,7 @@ impl<'db> FmtDetailed<'db> for DisplayTuple<'_, 'db> { VariableSegment::TypeVarTuple(typevar) => { f.write_char('*')?; Type::TypeVar(typevar) - .display_with(self.db, self.settings.singleline()) + .display_with(db, self.env, self.settings.singleline()) .fmt_detailed(f)?; } VariableSegment::Homogeneous(variable) => { @@ -1905,12 +2063,12 @@ impl<'db> FmtDetailed<'db> for DisplayTuple<'_, 'db> { { f.write_char('*')?; // Might as well link the type again here too - f.with_type(KnownClass::Tuple.to_class_literal(self.db)) + f.with_type(KnownClass::Tuple.to_class_literal(db, self.env)) .write_str("tuple")?; f.write_char('[')?; } variable - .display_with(self.db, self.settings.singleline()) + .display_with(db, self.env, self.settings.singleline()) .fmt_detailed(f)?; f.write_str(", ...")?; if !tuple.prefix_elements().is_empty() @@ -1924,7 +2082,7 @@ impl<'db> FmtDetailed<'db> for DisplayTuple<'_, 'db> { f.write_str(", ")?; tuple .suffix_elements() - .display_with(self.db, self.settings.singleline()) + .display_with(db, self.env, self.settings.singleline()) .fmt_detailed(f)?; } } @@ -1941,14 +2099,15 @@ impl<'db> DisplayTuple<'_, 'db> { /// tuple\[prefix, *tuple\[V, ...\], suffix\] → (prefix, *: V, suffix) /// tuple\[()\] → () fn fmt_basedpython(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { - f.with_type(KnownClass::Tuple.to_class_literal(self.db)) + let env = self.env; + f.with_type(KnownClass::Tuple.to_class_literal(self.db, env)) .write_char('(')?; match self.tuple { TupleSpec::Fixed(tuple) => { let elements = tuple.elements_slice(); if !elements.is_empty() { elements - .display_with(self.db, self.settings.singleline()) + .display_with(self.db, env, self.settings.singleline()) .fmt_detailed(f)?; if elements.len() == 1 { f.write_char(',')?; @@ -1963,7 +2122,7 @@ impl<'db> DisplayTuple<'_, 'db> { } first = false; prefix - .display_with(self.db, self.settings.singleline()) + .display_with(self.db, env, self.settings.singleline()) .fmt_detailed(f)?; } if !first { @@ -1972,16 +2131,16 @@ impl<'db> DisplayTuple<'_, 'db> { f.write_str("*: ")?; match tuple.variable() { VariableSegment::Homogeneous(variable) => variable - .display_with(self.db, self.settings.singleline()) + .display_with(self.db, env, self.settings.singleline()) .fmt_detailed(f)?, VariableSegment::TypeVarTuple(typevar) => Type::TypeVar(typevar) - .display_with(self.db, self.settings.singleline()) + .display_with(self.db, env, self.settings.singleline()) .fmt_detailed(f)?, } for suffix in tuple.suffix_elements() { f.write_str(", ")?; suffix - .display_with(self.db, self.settings.singleline()) + .display_with(self.db, env, self.settings.singleline()) .fmt_detailed(f)?; } } @@ -1999,47 +2158,56 @@ impl Display for DisplayTuple<'_, '_> { 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> { - Self::display_with(self, db, DisplaySettings::default()) + fn display<'env>( + self, + db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, + ) -> DisplayOverloadLiteral<'env, 'db> { + Self::display_with(self, db, env, DisplaySettings::default()) } - fn display_with( + fn display_with<'env>( self, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, settings: DisplaySettings<'db>, - ) -> DisplayOverloadLiteral<'db> { + ) -> DisplayOverloadLiteral<'env, 'db> { DisplayOverloadLiteral { literal: self, db, + env, settings, } } } -pub(crate) struct DisplayOverloadLiteral<'db> { +pub(crate) struct DisplayOverloadLiteral<'env, 'db> { literal: OverloadLiteral<'db>, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, settings: DisplaySettings<'db>, } -impl<'db> FmtDetailed<'db> for DisplayOverloadLiteral<'db> { +impl<'db> FmtDetailed<'db> for DisplayOverloadLiteral<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { - let signature = self.literal.signature(self.db); - let hide_unused_self = signature.should_hide_self_from_display(self.db); + let env = self.env; + let db = self.db; + let signature = self.literal.signature(db); + let hide_unused_self = signature.should_hide_self_from_display(db, self.env); let type_parameters = DisplayOptionalGenericContext { generic_context: signature.generic_context.as_ref(), - db: self.db, - settings: self.settings.clone(), + db, hide_unused_self, }; f.set_invalid_type_annotation(); f.write_str("def ")?; - write!(f, "{}", self.literal.name(self.db))?; + write!(f, "{}", self.literal.name(db))?; type_parameters.fmt_detailed(f)?; signature .display_with( self.db, + env, self.settings .disallow_signature_name() .name_already_written(), @@ -2048,44 +2216,49 @@ impl<'db> FmtDetailed<'db> for DisplayOverloadLiteral<'db> { } } -impl Display for DisplayOverloadLiteral<'_> { +impl Display for DisplayOverloadLiteral<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_detailed(&mut TypeWriter::Formatter(f)) } } impl<'db> FunctionType<'db> { - fn display_with( + fn display_with<'env>( self, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, settings: DisplaySettings<'db>, - ) -> DisplayFunctionType<'db> { + ) -> DisplayFunctionType<'env, 'db> { DisplayFunctionType { ty: self, db, + env, settings, } } } -struct DisplayFunctionType<'db> { +struct DisplayFunctionType<'env, 'db> { ty: FunctionType<'db>, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, settings: DisplaySettings<'db>, } -impl<'db> FmtDetailed<'db> for DisplayFunctionType<'db> { +impl<'db> FmtDetailed<'db> for DisplayFunctionType<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { // Detect self-referential function types to prevent infinite recursion, // and limit display depth for chains of different function types // (e.g. multiple redefinitions with `TypeOf[foo]` return types). const MAX_FUNCTION_TYPE_DISPLAY_DEPTH: usize = 4; + let env = self.env; + let db = self.db; if self.settings.visited_function_types.contains(&self.ty) || self.settings.visited_function_types.len() >= MAX_FUNCTION_TYPE_DISPLAY_DEPTH { f.set_invalid_type_annotation(); f.write_str("def ")?; - write!(f, "{}", self.ty.name(self.db))?; + write!(f, "{}", self.ty.name(db))?; return f.write_str("(...)"); } @@ -2094,25 +2267,25 @@ impl<'db> FmtDetailed<'db> for DisplayFunctionType<'db> { visited.insert(self.ty); settings.visited_function_types = Rc::new(visited); - let signature = self.ty.signature(self.db); + let signature = self.ty.signature(db); match signature.overloads.as_slice() { [signature] => { - let hide_unused_self = signature.should_hide_self_from_display(self.db); + let hide_unused_self = signature.should_hide_self_from_display(db, self.env); let type_parameters = DisplayOptionalGenericContext { generic_context: signature.generic_context.as_ref(), - db: self.db, - settings: settings.clone(), + db, hide_unused_self, }; f.set_invalid_type_annotation(); f.write_str("def ")?; - write!(f, "{}", self.ty.name(self.db))?; + write!(f, "{}", self.ty.name(db))?; type_parameters.fmt_detailed(f)?; signature .display_with( self.db, + env, settings.disallow_signature_name().name_already_written(), ) .fmt_detailed(f) @@ -2128,7 +2301,7 @@ impl<'db> FmtDetailed<'db> for DisplayFunctionType<'db> { let separator = if settings.multiline { "\n" } else { ", " }; let mut join = f.join(separator); for signature in signatures { - join.entry(&signature.display_with(self.db, settings.clone())); + join.entry(&signature.display_with(db, self.env, settings.clone())); } join.finish()?; if !settings.multiline { @@ -2140,43 +2313,52 @@ impl<'db> FmtDetailed<'db> for DisplayFunctionType<'db> { } } -impl Display for DisplayFunctionType<'_> { +impl Display for DisplayFunctionType<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_detailed(&mut TypeWriter::Formatter(f)) } } impl<'db> GenericAlias<'db> { - pub(crate) fn display(self, db: &'db dyn Db) -> DisplayGenericAlias<'db> { - self.display_with(db, DisplaySettings::default()) + pub(crate) fn display<'env>( + self, + db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, + ) -> DisplayGenericAlias<'env, 'db> { + self.display_with(db, env, DisplaySettings::default()) } - pub(crate) fn display_with( + pub(crate) fn display_with<'env>( self, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, settings: DisplaySettings<'db>, - ) -> DisplayGenericAlias<'db> { + ) -> DisplayGenericAlias<'env, 'db> { DisplayGenericAlias { origin: ClassLiteral::Static(self.origin(db)), specialization: self.specialization(db), db, + env, settings, } } } -pub(crate) struct DisplayGenericAlias<'db> { +pub(crate) struct DisplayGenericAlias<'env, 'db> { origin: ClassLiteral<'db>, specialization: Specialization<'db>, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, settings: DisplaySettings<'db>, } -impl<'db> FmtDetailed<'db> for DisplayGenericAlias<'db> { +impl<'db> FmtDetailed<'db> for DisplayGenericAlias<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { - if let Some(tuple) = self.specialization.tuple(self.db) { + let env = self.env; + let db = self.db; + if let Some(tuple) = self.specialization.tuple(db) { tuple - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f) } else { // basedpython surface syntax: per-typevar use-site variance @@ -2194,7 +2376,7 @@ impl<'db> FmtDetailed<'db> for DisplayGenericAlias<'db> { { use ruff_python_ast::helpers::UseSiteVariance; self.origin - .display_with(self.db, self.settings.clone()) + .display_with(self.db, env, self.settings.clone()) .fmt_detailed(f)?; let types = self.specialization.types(self.db); let projections = self.specialization.projections(self.db); @@ -2209,7 +2391,7 @@ impl<'db> FmtDetailed<'db> for DisplayGenericAlias<'db> { Some(UseSiteVariance::InOut) => f.write_str("in out ")?, None => {} } - ty.display_with(self.db, self.settings.clone()) + ty.display_with(self.db, env, self.settings.clone()) .fmt_detailed(f)?; } return f.write_char(']'); @@ -2226,7 +2408,7 @@ impl<'db> FmtDetailed<'db> for DisplayGenericAlias<'db> { ) { self.origin - .display_with(self.db, self.settings.clone()) + .display_with(self.db, env, self.settings.clone()) .fmt_detailed(f)?; let types = self.specialization.types(self.db); f.write_char('[')?; @@ -2237,7 +2419,7 @@ impl<'db> FmtDetailed<'db> for DisplayGenericAlias<'db> { if matches!(ty, Type::Dynamic(DynamicType::Any | DynamicType::Unknown)) { f.write_char('*')?; } else { - ty.display_with(self.db, self.settings.clone()) + ty.display_with(self.db, env, self.settings.clone()) .fmt_detailed(f)?; } } @@ -2248,7 +2430,7 @@ impl<'db> FmtDetailed<'db> for DisplayGenericAlias<'db> { Some(MaterializationKind::Top) => Some(("Top", SpecialFormType::Top)), Some(MaterializationKind::Bottom) => Some(("Bottom", SpecialFormType::Bottom)), }; - let suffix = match self.specialization.materialization_kind(self.db) { + let suffix = match self.specialization.materialization_kind(db) { None => "", Some(_) => "]", }; @@ -2257,12 +2439,13 @@ impl<'db> FmtDetailed<'db> for DisplayGenericAlias<'db> { f.write_char('[')?; } self.origin - .display_with(self.db, self.settings.clone()) + .display_with(db, env, self.settings.clone()) .fmt_detailed(f)?; self.specialization .display_short( - self.db, - TupleSpecialization::from_class(self.db, self.origin), + db, + self.env, + TupleSpecialization::from_class(db, self.origin), self.settings.clone(), ) .fmt_detailed(f)?; @@ -2271,7 +2454,7 @@ impl<'db> FmtDetailed<'db> for DisplayGenericAlias<'db> { } } -impl Display for DisplayGenericAlias<'_> { +impl Display for DisplayGenericAlias<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_detailed(&mut TypeWriter::Formatter(f)) } @@ -2279,29 +2462,19 @@ impl Display for DisplayGenericAlias<'_> { impl<'db> GenericContext<'db> { fn display<'a>(&'a self, db: &'db dyn Db) -> DisplayGenericContext<'a, 'db> { - Self::display_with(self, db, DisplaySettings::default()) - } - - fn display_full<'a>(&'a self, db: &'db dyn Db) -> DisplayGenericContext<'a, 'db> { DisplayGenericContext { generic_context: self, db, - settings: DisplaySettings::default(), - full: true, + full: false, hide_unused_self: false, } } - fn display_with<'a>( - &'a self, - db: &'db dyn Db, - settings: DisplaySettings<'db>, - ) -> DisplayGenericContext<'a, 'db> { + fn display_full<'a>(&'a self, db: &'db dyn Db) -> DisplayGenericContext<'a, 'db> { DisplayGenericContext { generic_context: self, db, - settings, - full: false, + full: true, hide_unused_self: false, } } @@ -2316,7 +2489,11 @@ enum SomeHoleBound<'db> { } /// basedpython: the bound of the `some` hole `ty` is, when it is one. -fn some_hole_bound<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option> { +fn some_hole_bound<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Option> { let Type::TypeVar(bound_typevar) = ty else { return None; }; @@ -2324,7 +2501,7 @@ fn some_hole_bound<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option SomeHoleBound::Bounded(bound), _ => SomeHoleBound::Unbounded, }) @@ -2332,17 +2509,19 @@ fn some_hole_bound<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option str)`. -struct DisplayInlineProtocol<'db> { +struct DisplayInlineProtocol<'env, 'db> { db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, interface: ProtocolInterface<'db>, settings: DisplaySettings<'db>, } -impl<'db> DisplayInlineProtocol<'db> { +impl<'env, 'db> DisplayInlineProtocol<'env, 'db> { /// `None` when some part of the interface has no inline spelling, so that the protocol is /// described rather than spelled wrongly. fn new( db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, interface: ProtocolInterface<'db>, settings: DisplaySettings<'db>, ) -> Option { @@ -2351,14 +2530,16 @@ impl<'db> DisplayInlineProtocol<'db> { .all(|member| member.inline_form().is_some()) .then_some(Self { db, + env, interface, settings, }) } } -impl<'db> FmtDetailed<'db> for DisplayInlineProtocol<'db> { +impl<'db> FmtDetailed<'db> for DisplayInlineProtocol<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { + let env = self.env; f.write_str("protocol(")?; let mut first = true; for member in self.interface.members(self.db) { @@ -2369,12 +2550,12 @@ impl<'db> FmtDetailed<'db> for DisplayInlineProtocol<'db> { Some(InlineProtocolMemberForm::Method(callable)) => { write!(f, "def {}", member.name())?; callable - .display_with(self.db, self.settings.clone()) + .display_with(self.db, env, self.settings.clone()) .fmt_detailed(f)?; } Some(InlineProtocolMemberForm::Attribute(ty)) => { write!(f, "{}: ", member.name())?; - ty.display_with(self.db, self.settings.clone()) + ty.display_with(self.db, env, self.settings.clone()) .fmt_detailed(f)?; } // `new` rejected an interface with such a member @@ -2385,7 +2566,7 @@ impl<'db> FmtDetailed<'db> for DisplayInlineProtocol<'db> { if !std::mem::take(&mut first) { f.write_str("; ")?; } - write!(f, "**{}", pack.display(self.db))?; + write!(f, "**{}", pack.display(self.db, env))?; } f.write_char(')') } @@ -2394,7 +2575,6 @@ impl<'db> FmtDetailed<'db> for DisplayInlineProtocol<'db> { struct DisplayOptionalGenericContext<'a, 'db> { generic_context: Option<&'a GenericContext<'db>>, db: &'db dyn Db, - settings: DisplaySettings<'db>, /// If true, hide `Self` type variables from the generic context prefix /// when they are not displayed in the signature body. hide_unused_self: bool, @@ -2406,7 +2586,6 @@ impl<'db> FmtDetailed<'db> for DisplayOptionalGenericContext<'_, 'db> { DisplayGenericContext { generic_context, db: self.db, - settings: self.settings.clone(), full: false, hide_unused_self: self.hide_unused_self, } @@ -2426,8 +2605,6 @@ impl Display for DisplayOptionalGenericContext<'_, '_> { struct DisplayGenericContext<'a, 'db> { generic_context: &'a GenericContext<'db>, db: &'db dyn Db, - #[expect(dead_code)] - settings: DisplaySettings<'db>, full: bool, /// If true, hide `Self` type variables from the generic context prefix. hide_unused_self: bool, @@ -2509,10 +2686,15 @@ impl Display for DisplayGenericContext<'_, '_> { } impl<'db> Specialization<'db> { - fn display_full(self, db: &'db dyn Db) -> DisplaySpecialization<'db> { + fn display_full<'env>( + self, + db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, + ) -> DisplaySpecialization<'env, 'db> { DisplaySpecialization { specialization: self, db, + env, tuple_specialization: TupleSpecialization::No, settings: DisplaySettings::default(), full: true, @@ -2520,15 +2702,17 @@ impl<'db> Specialization<'db> { } /// Renders the specialization as it would appear in a subscript expression, e.g. `[int, str]`. - fn display_short( + fn display_short<'env>( self, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, tuple_specialization: TupleSpecialization, settings: DisplaySettings<'db>, - ) -> DisplaySpecialization<'db> { + ) -> DisplaySpecialization<'env, 'db> { DisplaySpecialization { specialization: self, db, + env, tuple_specialization, settings, full: false, @@ -2536,31 +2720,34 @@ impl<'db> Specialization<'db> { } } -struct DisplaySpecialization<'db> { +struct DisplaySpecialization<'env, 'db> { specialization: Specialization<'db>, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, tuple_specialization: TupleSpecialization, settings: DisplaySettings<'db>, full: bool, } -impl<'db> DisplaySpecialization<'db> { +impl<'db> DisplaySpecialization<'_, 'db> { fn fmt_normal(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { + let env = self.env; + let db = self.db; f.write_char('[')?; let variables = self .specialization - .generic_context(self.db) - .variables(self.db) + .generic_context(db) + .variables(db) .collect::>(); - let types = self.specialization.types(self.db); + let types = self.specialization.types(db); let mut wrote_any = false; for (typevar, ty) in variables.iter().zip(types) { - if typevar.is_typevartuple(self.db) { - let Some(tuple) = ty.exact_tuple_instance_spec(self.db) else { + if typevar.is_typevartuple(db) { + let Some(tuple) = ty.exact_tuple_instance_spec(db) else { if wrote_any { f.write_str(", ")?; } - ty.display_with(self.db, self.settings.clone()) + ty.display_with(db, self.env, self.settings.clone()) .fmt_detailed(f)?; wrote_any = true; continue; @@ -2581,7 +2768,7 @@ impl<'db> DisplaySpecialization<'db> { f.write_str(", ")?; } element - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f)?; wrote_any = true; } @@ -2591,7 +2778,7 @@ impl<'db> DisplaySpecialization<'db> { f.write_str(", ")?; } f.write_char('*')?; - ty.display_with(self.db, self.settings.clone()) + ty.display_with(db, self.env, self.settings.clone()) .fmt_detailed(f)?; wrote_any = true; } @@ -2620,7 +2807,7 @@ impl<'db> DisplaySpecialization<'db> { } write!(f, "{name}=")?; field_type - .display_with(self.db, self.settings.clone()) + .display_with(self.db, env, self.settings.clone()) .fmt_detailed(f)?; wrote_any = true; } @@ -2635,7 +2822,7 @@ impl<'db> DisplaySpecialization<'db> { if self.settings.name_type_arguments && variables.len() > 1 { write!(f, "{}=", typevar.name(self.db))?; } - ty.display_with(self.db, self.settings.clone()) + ty.display_with(self.db, env, self.settings.clone()) .fmt_detailed(f)?; wrote_any = true; } @@ -2646,27 +2833,25 @@ impl<'db> DisplaySpecialization<'db> { } fn fmt_full(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { + let db = self.db; f.write_char('[')?; - let variables = self - .specialization - .generic_context(self.db) - .variables(self.db); - let types = self.specialization.types(self.db); + let variables = self.specialization.generic_context(db).variables(db); + let types = self.specialization.types(db); for (idx, (bound_typevar, ty)) in variables.zip(types).enumerate() { if idx > 0 { f.write_str(", ")?; } f.set_invalid_type_annotation(); - write!(f, "{}", bound_typevar.identity(self.db).display(self.db))?; + write!(f, "{}", bound_typevar.identity(db).display(db))?; f.write_str(" = ")?; - ty.display_with(self.db, self.settings.clone()) + ty.display_with(db, self.env, self.settings.clone()) .fmt_detailed(f)?; } f.write_char(']') } } -impl<'db> FmtDetailed<'db> for DisplaySpecialization<'db> { +impl<'db> FmtDetailed<'db> for DisplaySpecialization<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { if self.full { self.fmt_full(f) @@ -2676,7 +2861,7 @@ impl<'db> FmtDetailed<'db> for DisplaySpecialization<'db> { } } -impl Display for DisplaySpecialization<'_> { +impl Display for DisplaySpecialization<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_detailed(&mut TypeWriter::Formatter(f)) } @@ -2703,19 +2888,25 @@ impl TupleSpecialization { } impl<'db> CallableType<'db> { - pub(crate) fn display<'a>(&'a self, db: &'db dyn Db) -> DisplayCallableType<'a, 'db> { - Self::display_with(self, db, DisplaySettings::default()) + fn display<'a>( + &'a self, + db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, + ) -> DisplayCallableType<'a, 'db> { + Self::display_with(self, db, env, DisplaySettings::default()) } fn display_with<'a>( &'a self, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, settings: DisplaySettings<'db>, ) -> DisplayCallableType<'a, 'db> { DisplayCallableType { signatures: self.signatures(db), kind: self.kind(db), db, + env, settings, } } @@ -2725,11 +2916,13 @@ pub(crate) struct DisplayCallableType<'a, 'db> { signatures: &'a CallableSignature<'db>, kind: CallableTypeKind, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, settings: DisplaySettings<'db>, } impl<'db> FmtDetailed<'db> for DisplayCallableType<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { + let db = self.db; match self.signatures.overloads.as_slice() { [signature] => { if matches!(self.kind, CallableTypeKind::ParamSpecValue) { @@ -2738,14 +2931,14 @@ impl<'db> FmtDetailed<'db> for DisplayCallableType<'_, 'db> { } signature .parameters() - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f)?; if signature.parameters().is_top() { f.write_str("]")?; } } else { signature - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f)?; } } @@ -2760,7 +2953,7 @@ impl<'db> FmtDetailed<'db> for DisplayCallableType<'_, 'db> { let separator = if self.settings.multiline { "\n" } else { ", " }; let mut join = f.join(separator); for signature in signatures { - join.entry(&signature.display_with(self.db, self.settings.clone())); + join.entry(&signature.display_with(db, self.env, self.settings.clone())); } join.finish()?; if !self.settings.multiline { @@ -2780,13 +2973,41 @@ impl Display for DisplayCallableType<'_, '_> { } impl<'db> Signature<'db> { - pub(crate) fn display<'a>(&'a self, db: &'db dyn Db) -> DisplaySignature<'a, 'db> { - Self::display_with(self, db, DisplaySettings::default()) + /// Displays this signature with qualification inferred across all parameter and return types. + /// + /// For example, considering the annotations together keeps the two `float` classes distinct: + /// + /// ```python + /// import builtins + /// + /// class float: ... + /// + /// def f(value: builtins.float | float) -> None: ... + /// ``` + pub(crate) fn display<'a>( + &'a self, + db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, + ) -> DisplaySignature<'a, 'db> { + Self::display_with( + self, + db, + env, + DisplaySettings::from_possibly_ambiguous_types( + db, + env, + self.parameters() + .iter() + .map(Parameter::annotated_type) + .chain(std::iter::once(self.return_ty)), + ), + ) } pub(crate) fn display_with<'a>( &'a self, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, settings: DisplaySettings<'db>, ) -> DisplaySignature<'a, 'db> { DisplaySignature { @@ -2795,6 +3016,7 @@ impl<'db> Signature<'db> { parameters: self.parameters(), return_ty: self.return_ty, db, + env, settings, } } @@ -2806,10 +3028,35 @@ pub(crate) struct DisplaySignature<'a, 'db> { parameters: &'a Parameters<'db>, return_ty: Type<'db>, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, settings: DisplaySettings<'db>, } -impl<'db> DisplaySignature<'_, 'db> { +impl DisplaySignature<'_, '_> { + #[must_use] + pub(crate) fn multiline(self) -> Self { + Self { + settings: self.settings.multiline(), + ..self + } + } + + #[must_use] + pub(crate) fn disallow_name(self) -> Self { + Self { + settings: self.settings.disallow_signature_name(), + ..self + } + } + + #[must_use] + pub(crate) fn hide_return_type(self) -> Self { + Self { + settings: self.settings.hide_return_type(), + ..self + } + } + /// Get detailed display information including component ranges pub(crate) fn to_string_parts(&self) -> SignatureDisplayDetails { let mut f = TypeWriter::Details(TypeDetailsWriter::new()); @@ -2821,17 +3068,21 @@ impl<'db> DisplaySignature<'_, 'db> { } } - fn should_hide_self_from_display(&self, db: &'db dyn Db) -> bool { - !self.return_ty.contains_self(db) - && !self - .parameters - .iter() - .any(|p| p.should_annotation_be_displayed() && p.annotated_type().contains_self(db)) + fn should_hide_self_from_display(&self) -> bool { + let db = self.db; + let env = self.env; + + !self.return_ty.contains_self(db, env) + && !self.parameters.iter().any(|p| { + p.should_annotation_be_displayed() && p.annotated_type().contains_self(db, env) + }) } } impl<'db> FmtDetailed<'db> for DisplaySignature<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { + let env = self.env; + let db = self.db; // Immediately write a marker signaling we're starting a signature let _ = f.with_detail(TypeDetail::SignatureStart); f.set_invalid_type_annotation(); @@ -2850,16 +3101,14 @@ impl<'db> FmtDetailed<'db> for DisplaySignature<'_, 'db> { .signature_name_display .should_display(self.settings.multiline) && let Some(definition) = self.definition - && let Some(name) = definition.name(self.db) + && let Some(name) = definition.name(db) { f.write_str("def ")?; f.write_str(&name)?; is_declaration = true; } - let settings = self - .settings - .with_generic_context(self.db, self.generic_context); + let settings = self.settings.with_generic_context(db, self.generic_context); // Display type parameters if present, but only when the caller hasn't // already displayed them. @@ -2868,12 +3117,11 @@ impl<'db> FmtDetailed<'db> for DisplaySignature<'_, 'db> { .signature_name_display .allows_type_parameters() { - let hide_unused_self = self.should_hide_self_from_display(self.db); + let hide_unused_self = self.should_hide_self_from_display(); DisplayOptionalGenericContext { generic_context: self.generic_context, - db: self.db, - settings: settings.clone(), + db, hide_unused_self, } .fmt_detailed(&mut f)?; @@ -2887,7 +3135,7 @@ impl<'db> FmtDetailed<'db> for DisplaySignature<'_, 'db> { ..settings.clone() }; self.parameters - .display_with(self.db, param_settings) + .display_with(db, self.env, param_settings) .fmt_detailed(&mut f)?; // Return type. @@ -2900,13 +3148,14 @@ impl<'db> FmtDetailed<'db> for DisplaySignature<'_, 'db> { f.write_str(" -> ")?; let should_parenthesize_return_type = - should_parenthesize_callable_type(self.return_ty, self.db); + should_parenthesize_callable_type(self.return_ty, db); if should_parenthesize_return_type { f.write_char('(')?; } self.return_ty .display_with( self.db, + env, DisplaySettings { name_already_written: false, ..settings.singleline() @@ -2947,11 +3196,13 @@ impl<'db> Parameters<'db> { fn display_with<'a>( &'a self, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, settings: DisplaySettings<'db>, ) -> DisplayParameters<'a, 'db> { DisplayParameters { parameters: self, db, + env, settings, } } @@ -2960,6 +3211,7 @@ impl<'db> Parameters<'db> { struct DisplayParameters<'a, 'db> { parameters: &'a Parameters<'db>, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, settings: DisplaySettings<'db>, } @@ -2971,6 +3223,7 @@ impl<'db> FmtDetailed<'db> for DisplayParameters<'_, 'db> { parameters: &[Parameter<'db>], arg_separator: &str, ) -> fmt::Result { + let db = display.db; let mut star_added = false; let mut needs_slash = false; let mut after_synthetic_unpack = false; @@ -3012,7 +3265,7 @@ impl<'db> FmtDetailed<'db> for DisplayParameters<'_, 'db> { .map(|name| name.to_string()) .unwrap_or_default(); parameter - .display_with(display.db, display.settings.singleline()) + .display_with(db, display.env, display.settings.singleline()) .fmt_detailed(&mut f.with_detail(TypeDetail::Parameter(param_name)))?; after_synthetic_unpack |= is_synthetic_unpack; @@ -3028,6 +3281,7 @@ impl<'db> FmtDetailed<'db> for DisplayParameters<'_, 'db> { Ok(()) } + let db = self.db; // For `ParamSpec` kind, the parameters still contain `*args` and `**kwargs`, but we // display them as `**P` instead, so avoid multiline in that case. @@ -3078,11 +3332,11 @@ impl<'db> FmtDetailed<'db> for DisplayParameters<'_, 'db> { display_parameters(self, f, self.parameters.as_slice(), arg_separator)?; } ParametersKind::ParamSpec(typevar) => { - let parameter_name = format!("**{}", typevar.name(self.db)); + let parameter_name = format!("**{}", typevar.name(db)); let mut parameter = f.with_detail(TypeDetail::Parameter(parameter_name.clone())); write!(parameter, "{parameter_name}")?; - let binding_context = typevar.binding_context(self.db); - if let Some(binding_context_name) = binding_context.name(self.db) + let binding_context = typevar.binding_context(db); + if let Some(binding_context_name) = binding_context.name(db) && let Some(definition) = binding_context.definition() && !self.settings.active_scopes.contains(&definition) { @@ -3110,11 +3364,13 @@ impl<'db> Parameter<'db> { fn display_with<'a>( &'a self, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, settings: DisplaySettings<'db>, ) -> DisplayParameter<'a, 'db> { DisplayParameter { param: self, db, + env, settings, } } @@ -3123,11 +3379,14 @@ impl<'db> Parameter<'db> { struct DisplayParameter<'a, 'db> { param: &'a Parameter<'db>, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, settings: DisplaySettings<'db>, } impl<'db> FmtDetailed<'db> for DisplayParameter<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { + let env = self.env; + let db = self.db; if self.param.definition().is_none() && self.param.is_variadic() && self.param.has_starred_annotation() @@ -3135,7 +3394,7 @@ impl<'db> FmtDetailed<'db> for DisplayParameter<'_, 'db> { f.write_str("*")?; self.param .annotated_type() - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f)?; return Ok(()); } @@ -3147,18 +3406,18 @@ impl<'db> FmtDetailed<'db> for DisplayParameter<'_, 'db> { // basedpython: the hole this parameter opened is spelled where it was // opened — `some ` — rather than as a type parameter of its own. an // unbounded hole has nothing to say, so the parameter reads as unannotated - match some_hole_bound(self.db, annotated_type) { + match some_hole_bound(self.db, env, annotated_type) { Some(SomeHoleBound::Bounded(bound)) => { f.write_str(": some ")?; bound - .display_with(self.db, self.settings.clone()) + .display_with(self.db, env, self.settings.clone()) .fmt_detailed(f)?; } Some(SomeHoleBound::Unbounded) => {} None => { f.write_str(": ")?; annotated_type - .display_with(self.db, self.settings.clone()) + .display_with(self.db, env, self.settings.clone()) .fmt_detailed(f)?; } } @@ -3183,14 +3442,14 @@ impl<'db> FmtDetailed<'db> for DisplayParameter<'_, 'db> { { // For Literal types display the value without `Literal[..]` wrapping let representation = - default_type.representation(self.db, self.settings.clone()); + default_type.representation(db, self.env, self.settings.clone()); representation.fmt_detailed(f)?; } Type::NominalInstance(instance) => { // Some key default types like `None` are worth showing - let class = instance.class(self.db); + let class = instance.class(db, self.env); - match (class, class.known(self.db)) { + match (class, class.known(db)) { (_, Some(KnownClass::NoneType)) => { f.with_type(default_type).write_str("None")?; } @@ -3209,7 +3468,7 @@ impl<'db> FmtDetailed<'db> for DisplayParameter<'_, 'db> { // have something visible in the parameter slot. self.param .annotated_type() - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f)?; } Ok(()) @@ -3271,10 +3530,12 @@ impl<'db> UnionType<'db> { fn display_with<'a>( &'a self, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, settings: DisplaySettings<'db>, ) -> DisplayUnionType<'a, 'db> { DisplayUnionType { db, + env, ty: self, settings, } @@ -3284,67 +3545,82 @@ impl<'db> UnionType<'db> { struct DisplayUnionType<'a, 'db> { ty: &'a UnionType<'db>, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, settings: DisplaySettings<'db>, } +impl<'db> DisplayUnionType<'_, 'db> { + /// Return the literal types that can be folded into a displayed `Literal[...]` group. + /// + /// Plain literal types are returned as-is. Small enum complements are expanded to their + /// remaining enum literals so a type like `Color & ~Literal[Color.RED]` can be displayed + /// with the same condensation rules as explicit enum-literal unions. Large complements + /// stay compact to keep diagnostics readable. + /// + /// ```python + /// from enum import Enum + /// + /// class Color(Enum): + /// RED = 1 + /// BLUE = 2 + /// + /// # Color excluding RED displays through the literal-group path for BLUE. + /// ``` + fn condensable_literals(&self, ty: Type<'db>) -> Option>> { + // basedpython displays each union element separately in source order, so + // nothing is condensed into a `Literal[...]` group + if basedpython_display_enabled() { + return None; + } + match ty { + Type::LiteralValue(literal) + if matches!( + literal.kind(), + LiteralValueTypeKind::Int(_) + | LiteralValueTypeKind::String(_) + | LiteralValueTypeKind::Bytes(_) + | LiteralValueTypeKind::Bool(_) + | LiteralValueTypeKind::Enum(_) + ) => + { + Some(vec![ty]) + } + Type::EnumComplement(complement) => complement.remaining_literal_types_for_display( + self.db, + self.env, + LITERAL_POLICY.max, + ), + Type::Intersection(intersection) => { + intersection.finite_alternatives_for_display(self.db, self.env, LITERAL_POLICY.max) + } + _ => None, + } + } +} + const UNION_POLICY: TruncationPolicy = TruncationPolicy { max: 5, max_when_elided: 3, }; +fn subclass_of_known_class(db: &dyn Db, subclass_of: SubclassOfType<'_>) -> Option { + match subclass_of.subclass_of() { + SubclassOfInner::Class(class) => class.known(db), + _ => None, + } +} + impl<'db> FmtDetailed<'db> for DisplayUnionType<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { - /// Return the literal types that can be folded into a displayed `Literal[...]` group. - /// - /// Plain literal types are returned as-is. Small enum complements are expanded to their - /// remaining enum literals so a type like `Color & ~Literal[Color.RED]` can be displayed - /// with the same condensation rules as explicit enum-literal unions. Large complements - /// stay compact to keep diagnostics readable. - /// - /// ```python - /// from enum import Enum - /// - /// class Color(Enum): - /// RED = 1 - /// BLUE = 2 - /// - /// # Color excluding RED displays through the literal-group path for BLUE. - /// ``` - fn condensable_literals<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option>> { - // basedpython displays each union element separately in source order, so - // nothing is condensed into a `Literal[...]` group - if basedpython_display_enabled() { - return None; - } - match ty { - Type::LiteralValue(literal) - if matches!( - literal.kind(), - LiteralValueTypeKind::Int(_) - | LiteralValueTypeKind::String(_) - | LiteralValueTypeKind::Bytes(_) - | LiteralValueTypeKind::Bool(_) - | LiteralValueTypeKind::Enum(_) - ) => - { - Some(vec![ty]) - } - Type::EnumComplement(complement) => { - complement.remaining_literal_types_for_display(db, LITERAL_POLICY.max) - } - Type::Intersection(intersection) => { - intersection.finite_alternatives_for_display(db, LITERAL_POLICY.max) - } - _ => None, - } - } - fn singleline_union_element_label<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, element: Type<'db>, settings: &DisplaySettings<'db>, ) -> String { - element.display_with(db, settings.singleline()).to_string() + element + .display_with(db, env, settings.singleline()) + .to_string() } fn duplicate_ambiguous_labels(element_labels: &[Option]) -> FxHashSet<&str> { @@ -3359,8 +3635,18 @@ impl<'db> FmtDetailed<'db> for DisplayUnionType<'_, 'db> { .filter_map(|(label, count)| (count > 1).then_some(label)) .collect() } - - let elements = self.ty.elements(self.db); + let db = self.db; + + let elements = self.ty.elements(db); + let numeric_tower: Option = None; + let is_numeric_tower_element = |element: Type<'db>| { + numeric_tower.is_some_and(|group| { + element + .as_nominal_instance() + .and_then(|instance| instance.known_class(db)) + .is_some_and(|known_class| group.contains(known_class)) + }) + }; let mut condensed_types = vec![]; let mut condensed_element_count = 0usize; let mut subclass_of_types = vec![]; @@ -3368,14 +3654,16 @@ impl<'db> FmtDetailed<'db> for DisplayUnionType<'_, 'db> { .iter() .copied() .map(|element| { - (condensable_literals(self.db, element).is_none() && !element.is_subclass_of()) - .then(|| singleline_union_element_label(self.db, element, &self.settings)) + (self.condensable_literals(element).is_none() + && !element.is_subclass_of() + && !is_numeric_tower_element(element)) + .then(|| singleline_union_element_label(db, self.env, element, &self.settings)) }) .collect(); let duplicate_ambiguous_labels = duplicate_ambiguous_labels(&element_labels); for element in elements.iter().copied() { - if let Some(literals) = condensable_literals(self.db, element) { + if let Some(literals) = self.condensable_literals(element) { condensed_element_count += 1; for literal in literals { if !condensed_types.contains(&literal) { @@ -3387,7 +3675,16 @@ impl<'db> FmtDetailed<'db> for DisplayUnionType<'_, 'db> { } } - let total_entries = elements.len() - condensed_element_count - subclass_of_types.len() + let numeric_tower_element_count = elements + .iter() + .copied() + .filter(|element| is_numeric_tower_element(*element)) + .count(); + let total_entries = elements.len() + - numeric_tower_element_count + - condensed_element_count + - subclass_of_types.len() + + usize::from(numeric_tower.is_some()) + usize::from(!condensed_types.is_empty()) + usize::from(!subclass_of_types.is_empty()); @@ -3399,6 +3696,7 @@ impl<'db> FmtDetailed<'db> for DisplayUnionType<'_, 'db> { let display_limit = UNION_POLICY.display_limit(total_entries, self.settings.preserve_full_unions); + let mut numeric_tower = numeric_tower; let mut condensed_types = Some(condensed_types); let mut subclass_of_types = Some(subclass_of_types); let mut displayed_entries = 0usize; @@ -3408,21 +3706,33 @@ impl<'db> FmtDetailed<'db> for DisplayUnionType<'_, 'db> { break; } - if condensable_literals(self.db, *element).is_some() { - if let Some(condensed_types) = condensed_types.take() { + if is_numeric_tower_element(*element) { + if let Some(union) = numeric_tower.take() { + displayed_entries += 1; + join.entry(&DisplayKnownUnion { + union, + db, + env: self.env, + settings: self.settings.singleline(), + }); + } + } else if self.condensable_literals(*element).is_some() { + if let Some(literals) = condensed_types.take() { displayed_entries += 1; join.entry(&DisplayLiteralGroup { - literals: condensed_types, - db: self.db, + literals, + db, + env: self.env, settings: self.settings.singleline(), }); } } else if element.is_subclass_of() { - if let Some(subclass_of_types) = subclass_of_types.take() { + if let Some(types) = subclass_of_types.take() { displayed_entries += 1; join.entry(&DisplaySubclassOfGroup { - types: subclass_of_types, - db: self.db, + types, + db, + env: self.env, settings: self.settings.singleline(), }); } @@ -3438,7 +3748,8 @@ impl<'db> FmtDetailed<'db> for DisplayUnionType<'_, 'db> { }; join.entry(&DisplayMaybeParenthesizedType { ty: *element, - db: self.db, + db, + env: self.env, settings, }); } @@ -3458,6 +3769,31 @@ impl<'db> FmtDetailed<'db> for DisplayUnionType<'_, 'db> { } } +/// Displays a numeric-tower union through its canonical annotation class. +/// +/// Delegating to the class display preserves qualification and IDE navigation metadata. +struct DisplayKnownUnion<'env, 'db> { + union: KnownUnion, + db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, + settings: DisplaySettings<'db>, +} + +impl<'db> FmtDetailed<'db> for DisplayKnownUnion<'_, 'db> { + fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { + let env = self.env; + let class = self.union.annotation_class(); + if let Some(class_literal) = class.try_to_class_literal(self.db, self.env) { + ClassLiteral::Static(class_literal) + .display_with(self.db, env, self.settings.clone()) + .fmt_detailed(f) + } else { + f.with_type(class.to_instance(self.db, self.env)) + .write_str(class.name(self.env.python_version(self.db))) + } + } +} + impl Display for DisplayUnionType<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_detailed(&mut TypeWriter::Formatter(f)) @@ -3470,46 +3806,94 @@ impl fmt::Debug for DisplayUnionType<'_, '_> { } } -struct DisplaySubclassOfGroup<'db> { +struct DisplaySubclassOfGroup<'env, 'db> { types: Vec>, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, settings: DisplaySettings<'db>, } -impl<'db> FmtDetailed<'db> for DisplaySubclassOfGroup<'db> { +impl<'db> FmtDetailed<'db> for DisplaySubclassOfGroup<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { + let env = self.env; + let db = self.db; f.write_str("type[")?; - let total_entries = self.types.len(); + let numeric_tower: Option = None; + let is_numeric_tower_subclass = |subclass_of: SubclassOfType<'db>| { + numeric_tower.is_some_and(|group| { + subclass_of_known_class(self.db, subclass_of) + .is_some_and(|known_class| group.contains(known_class)) + }) + }; + let numeric_tower_element_count = self + .types + .iter() + .copied() + .filter(|subclass_of| is_numeric_tower_subclass(*subclass_of)) + .count(); + let total_entries = + self.types.len() - numeric_tower_element_count + usize::from(numeric_tower.is_some()); let display_limit = UNION_POLICY.display_limit(total_entries, self.settings.preserve_full_unions); let mut join = f.join(" | "); - for subclass_of in self.types.iter().take(display_limit) { + let mut numeric_tower = numeric_tower; + let mut displayed_entries = 0usize; + + for subclass_of in &self.types { + if displayed_entries >= display_limit { + break; + } + + if is_numeric_tower_subclass(*subclass_of) { + if let Some(union) = numeric_tower.take() { + displayed_entries += 1; + join.entry(&DisplayKnownUnion { + union, + db, + env: self.env, + settings: self.settings.singleline(), + }); + } + continue; + } + + displayed_entries += 1; + match subclass_of.subclass_of() { SubclassOfInner::Class(ClassType::NonGeneric(class)) => { - join.entry(&class.display_with(self.db, self.settings.singleline())); + join.entry(&class.display_with(db, env, self.settings.singleline())); } SubclassOfInner::Class(ClassType::Generic(alias)) => { - join.entry(&alias.display_with(self.db, self.settings.singleline())); + join.entry(&alias.display_with(db, self.env, self.settings.singleline())); } SubclassOfInner::Dynamic(dynamic) => { - let rep = - Type::Dynamic(dynamic).representation(self.db, self.settings.singleline()); + let rep = Type::Dynamic(dynamic).representation( + db, + self.env, + self.settings.singleline(), + ); join.entry(&rep); } SubclassOfInner::Protocol(protocol) => { - let rep = Type::ProtocolInstance(protocol) - .representation(self.db, self.settings.singleline()); + let rep = Type::ProtocolInstance(protocol).representation( + db, + self.env, + self.settings.singleline(), + ); join.entry(&rep); } SubclassOfInner::TypeVar(bound_typevar) => { - let rep = Type::TypeVar(bound_typevar) - .representation(self.db, self.settings.singleline()); + let rep = Type::TypeVar(bound_typevar).representation( + db, + self.env, + self.settings.singleline(), + ); join.entry(&rep); } } } if !self.settings.preserve_full_unions { - let omitted_entries = total_entries.saturating_sub(display_limit); + let omitted_entries = total_entries.saturating_sub(displayed_entries); if omitted_entries > 0 { join.entry(&DisplayOmitted { count: omitted_entries, @@ -3523,15 +3907,16 @@ impl<'db> FmtDetailed<'db> for DisplaySubclassOfGroup<'db> { } } -impl Display for DisplaySubclassOfGroup<'_> { +impl Display for DisplaySubclassOfGroup<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_detailed(&mut TypeWriter::Formatter(f)) } } -struct DisplayLiteralGroup<'db> { +struct DisplayLiteralGroup<'env, 'db> { literals: Vec>, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, settings: DisplaySettings<'db>, } @@ -3540,8 +3925,9 @@ const LITERAL_POLICY: TruncationPolicy = TruncationPolicy { max_when_elided: 5, }; -impl<'db> FmtDetailed<'db> for DisplayLiteralGroup<'db> { +impl<'db> FmtDetailed<'db> for DisplayLiteralGroup<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { + let db = self.db; f.with_type(Type::SpecialForm(SpecialFormType::Literal)) .write_str("Literal")?; f.write_char('[')?; @@ -3554,7 +3940,7 @@ impl<'db> FmtDetailed<'db> for DisplayLiteralGroup<'db> { let mut join = f.join(", "); for lit in self.literals.iter().take(display_limit) { - let rep = lit.representation(self.db, self.settings.singleline()); + let rep = lit.representation(db, self.env, self.settings.singleline()); join.entry(&rep); } @@ -3574,7 +3960,7 @@ impl<'db> FmtDetailed<'db> for DisplayLiteralGroup<'db> { } } -impl Display for DisplayLiteralGroup<'_> { +impl Display for DisplayLiteralGroup<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_detailed(&mut TypeWriter::Formatter(f)) } @@ -3584,10 +3970,12 @@ impl<'db> IntersectionType<'db> { fn display_with<'a>( &'a self, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, settings: DisplaySettings<'db>, ) -> DisplayIntersectionType<'a, 'db> { DisplayIntersectionType { db, + env, ty: self, settings, } @@ -3597,28 +3985,32 @@ impl<'db> IntersectionType<'db> { struct DisplayIntersectionType<'a, 'db> { ty: &'a IntersectionType<'db>, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, settings: DisplaySettings<'db>, } impl<'db> FmtDetailed<'db> for DisplayIntersectionType<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { + let db = self.db; let tys = self .ty - .positive(self.db) + .positive(db) .iter() .map(|&ty| DisplayMaybeNegatedType { ty, - db: self.db, + db, + env: self.env, settings: self.settings.singleline(), negated: false, }) .chain( self.ty - .negative(self.db) + .negative(db) .iter() .map(|&ty| DisplayMaybeNegatedType { ty, - db: self.db, + db, + env: self.env, settings: self.settings.singleline(), negated: true, }), @@ -3641,15 +4033,17 @@ impl fmt::Debug for DisplayIntersectionType<'_, '_> { } } -struct DisplayMaybeNegatedType<'db> { +struct DisplayMaybeNegatedType<'env, 'db> { ty: Type<'db>, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, negated: bool, settings: DisplaySettings<'db>, } -impl<'db> FmtDetailed<'db> for DisplayMaybeNegatedType<'db> { +impl<'db> FmtDetailed<'db> for DisplayMaybeNegatedType<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { + let db = self.db; if self.negated { // basedpython renders negation as `not T`; standard typing-spec // display uses `~T` @@ -3661,14 +4055,15 @@ impl<'db> FmtDetailed<'db> for DisplayMaybeNegatedType<'db> { } DisplayMaybeParenthesizedType { ty: self.ty, - db: self.db, + db, + env: self.env, settings: self.settings.clone(), } .fmt_detailed(f) } } -impl Display for DisplayMaybeNegatedType<'_> { +impl Display for DisplayMaybeNegatedType<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_detailed(&mut TypeWriter::Formatter(f)) } @@ -3690,90 +4085,106 @@ fn should_parenthesize_callable_type(ty: Type<'_>, db: &dyn Db) -> bool { } } -struct DisplayMaybeParenthesizedType<'db> { +struct DisplayMaybeParenthesizedType<'env, 'db> { ty: Type<'db>, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, settings: DisplaySettings<'db>, } -impl<'db> FmtDetailed<'db> for DisplayMaybeParenthesizedType<'db> { +impl<'db> FmtDetailed<'db> for DisplayMaybeParenthesizedType<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { + let db = self.db; let write_parentheses = |f: &mut TypeWriter<'_, '_, 'db>| { f.set_invalid_type_annotation(); f.write_char('(')?; self.ty - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f)?; f.write_char(')') }; match self.ty { - ty if should_parenthesize_callable_type(ty, self.db) => write_parentheses(f), - Type::KnownBoundMethod(_) - | Type::FunctionLiteral(_) - | Type::BoundMethod(_) - | Type::Union(_) => write_parentheses(f), - Type::Intersection(intersection) if !intersection.has_one_element(self.db) => { + ty if should_parenthesize_callable_type(ty, db) => write_parentheses(f), + Type::KnownBoundMethod(_) | Type::FunctionLiteral(_) | Type::BoundMethod(_) => { + write_parentheses(f) + } + Type::Union(union) + if matches!( + self.settings.numeric_tower_display, + NumericTowerDisplay::Expanded + ) || union.known(db).is_none() => + { + write_parentheses(f) + } + Type::Intersection(intersection) if !intersection.has_one_element(db) => { write_parentheses(f) } _ => self .ty - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f), } } } -impl Display for DisplayMaybeParenthesizedType<'_> { +impl Display for DisplayMaybeParenthesizedType<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_detailed(&mut TypeWriter::Formatter(f)) } } trait TypeArrayDisplay<'db> { - fn display_with( - &self, + fn display_with<'a>( + &'a self, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, settings: DisplaySettings<'db>, - ) -> DisplayTypeArray<'_, 'db>; + ) -> DisplayTypeArray<'a, 'db>; } impl<'db> TypeArrayDisplay<'db> for Box<[Type<'db>]> { - fn display_with( - &self, + fn display_with<'a>( + &'a self, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, settings: DisplaySettings<'db>, - ) -> DisplayTypeArray<'_, 'db> { + ) -> DisplayTypeArray<'a, 'db> { DisplayTypeArray { types: self, db, + env, settings, } } } impl<'db> TypeArrayDisplay<'db> for Vec> { - fn display_with( - &self, + fn display_with<'a>( + &'a self, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, settings: DisplaySettings<'db>, - ) -> DisplayTypeArray<'_, 'db> { + ) -> DisplayTypeArray<'a, 'db> { DisplayTypeArray { types: self, db, + env, settings, } } } impl<'db> TypeArrayDisplay<'db> for [Type<'db>] { - fn display_with( - &self, + fn display_with<'a>( + &'a self, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, settings: DisplaySettings<'db>, - ) -> DisplayTypeArray<'_, 'db> { + ) -> DisplayTypeArray<'a, 'db> { DisplayTypeArray { types: self, db, + env, settings, } } @@ -3782,16 +4193,18 @@ impl<'db> TypeArrayDisplay<'db> for [Type<'db>] { struct DisplayTypeArray<'b, 'db> { types: &'b [Type<'db>], db: &'db dyn Db, + env: &'b ProgramEnvironment<'db>, settings: DisplaySettings<'db>, } impl<'db> FmtDetailed<'db> for DisplayTypeArray<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { + let db = self.db; f.join(", ") .entries( self.types .iter() - .map(|ty| ty.display_with(self.db, self.settings.singleline())), + .map(|ty| ty.display_with(db, self.env, self.settings.singleline())), ) .finish() } @@ -3804,22 +4217,15 @@ impl Display for DisplayTypeArray<'_, '_> { } impl<'db> StringLiteralType<'db> { - fn display_with( - self, - db: &'db dyn Db, - settings: DisplaySettings<'db>, - ) -> DisplayStringLiteralType<'db> { + fn display(self, db: &'db dyn Db) -> DisplayStringLiteralType<'db> { DisplayStringLiteralType { string: self.value(db), - settings, } } } struct DisplayStringLiteralType<'db> { string: &'db str, - #[expect(dead_code)] - settings: DisplaySettings<'db>, } impl Display for DisplayStringLiteralType<'_> { @@ -3837,20 +4243,25 @@ impl Display for DisplayStringLiteralType<'_> { } } -pub(crate) struct DisplayKnownInstanceRepr<'db> { - pub(crate) known_instance: KnownInstanceType<'db>, - pub(crate) db: &'db dyn Db, - pub(crate) settings: DisplaySettings<'db>, +pub(crate) struct DisplayKnownInstanceRepr<'env, 'db> { + known_instance: KnownInstanceType<'db>, + db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, + settings: DisplaySettings<'db>, } /// If `ty` is a union that contains `None`, return the union of its remaining /// (non-`None`) members; otherwise `None`. Used to render the innermost layer /// of a wrapped optional (`int | None` -> base `int`) in `?` notation. -fn strip_none<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option> { +fn strip_none<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Option> { let Type::Union(union) = ty else { return None; }; - let none = Type::none(db); + let none = Type::none(db, env); let others: Vec> = union .elements(db) .iter() @@ -3860,31 +4271,35 @@ fn strip_none<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option> { if others.len() == union.elements(db).len() || others.is_empty() { return None; } - Some(UnionType::from_elements(db, others)) + Some(UnionType::from_elements(db, env, others)) } impl<'db> KnownInstanceType<'db> { - pub(crate) fn display_with( + pub(crate) fn display_with<'env>( self, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, settings: DisplaySettings<'db>, - ) -> DisplayKnownInstanceRepr<'db> { + ) -> DisplayKnownInstanceRepr<'env, 'db> { DisplayKnownInstanceRepr { known_instance: self, db, + env, settings, } } } -impl Display for DisplayKnownInstanceRepr<'_> { +impl Display for DisplayKnownInstanceRepr<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_detailed(&mut TypeWriter::Formatter(f)) } } -impl<'db> FmtDetailed<'db> for DisplayKnownInstanceRepr<'db> { +impl<'db> FmtDetailed<'db> for DisplayKnownInstanceRepr<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { + let env = self.env; + let db = self.db; let ty = Type::KnownInstance(self.known_instance); match self.known_instance { KnownInstanceType::SubscriptedProtocol(generic_context) => { @@ -3892,7 +4307,7 @@ impl<'db> FmtDetailed<'db> for DisplayKnownInstanceRepr<'db> { f.write_str("") } KnownInstanceType::SubscriptedGeneric(generic_context) => { @@ -3900,16 +4315,21 @@ impl<'db> FmtDetailed<'db> for DisplayKnownInstanceRepr<'db> { f.write_str("") } KnownInstanceType::TypeAliasType(alias) => { - if let Some(specialization) = alias.specialization(self.db) { + if let Some(specialization) = alias.specialization(db) { f.set_invalid_type_annotation(); f.write_str("") } else { @@ -3922,7 +4342,7 @@ impl<'db> FmtDetailed<'db> for DisplayKnownInstanceRepr<'db> { KnownInstanceType::TypeVar(typevar_instance) => { if typevar_instance.kind(self.db).is_parameter_pack() { f.with_type(ty).write_str("ParamSpec") - } else if typevar_instance.kind(self.db).is_typevartuple() { + } else if typevar_instance.kind(db).is_typevartuple() { f.with_type(ty).write_str("TypeVarTuple") } else { f.with_type(ty).write_str("TypeVar") @@ -3933,13 +4353,13 @@ impl<'db> FmtDetailed<'db> for DisplayKnownInstanceRepr<'db> { f.with_type(ty).write_str("dataclasses.Field")?; let field_type = field - .converter(self.db) + .converter(db) .map(|(_, converter_output)| converter_output) - .or(field.default_type(self.db)); + .or(field.default_type(db)); if let Some(field_ty) = field_type { f.write_char('[')?; - write!(f.with_type(field_ty), "{}", field_ty.display(self.db))?; + write!(f.with_type(field_ty), "{}", field_ty.display(db, self.env))?; f.write_char(']')?; } Ok(()) @@ -3947,12 +4367,12 @@ impl<'db> FmtDetailed<'db> for DisplayKnownInstanceRepr<'db> { KnownInstanceType::ConstraintSet(interned_set) => { f.with_type(ty).write_str("ConstraintSet")?; let constraints = ConstraintSetBuilder::new(); - let set = constraints.load(self.db, interned_set.constraints(self.db)); - if interned_set.detailed_display(self.db) { - write!(f, "[{}]", set.display(self.db)) - } else if set.is_always_satisfied(self.db) { + let set = constraints.load(db, self.env, interned_set.constraints(db)); + if interned_set.detailed_display(db) { + write!(f, "[{}]", set.display(db, self.env)) + } else if set.is_always_satisfied(db, self.env) { f.write_str("[Literal[True]]") - } else if set.is_never_satisfied(self.db) { + } else if set.is_never_satisfied(db, self.env) { f.write_str("[Literal[False]]") } else { f.write_str("[bool]") @@ -3961,14 +4381,14 @@ impl<'db> FmtDetailed<'db> for DisplayKnownInstanceRepr<'db> { KnownInstanceType::ConstraintSetSolution(solution) => { f.set_invalid_type_annotation(); f.with_type(ty).write_str("Solution[")?; - for (index, binding) in solution.bindings(self.db).iter().enumerate() { + for (index, binding) in solution.bindings(db).iter().enumerate() { if index > 0 { f.write_str(", ")?; } - write!(f, "{}=", binding.bound_typevar.name(self.db))?; + write!(f, "{}=", binding.bound_typevar.name(db))?; binding .solution - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f)?; } f.write_char(']') @@ -3976,23 +4396,23 @@ impl<'db> FmtDetailed<'db> for DisplayKnownInstanceRepr<'db> { KnownInstanceType::GenericContext(generic_context) => { f.with_type(ty) .write_str("ty_extensions._internal.GenericContext")?; - write!(f, "{}", generic_context.display_full(self.db)) + write!(f, "{}", generic_context.display_full(db)) } KnownInstanceType::Specialization(specialization) => { // Normalize for consistent output across CI platforms f.with_type(ty) .write_str("ty_extensions._internal.Specialization")?; - write!(f, "{}", specialization.display_full(self.db)) + write!(f, "{}", specialization.display_full(db, self.env)) } KnownInstanceType::UnionType(union) => { f.set_invalid_type_annotation(); f.write_char('<')?; - f.with_type(KnownClass::UnionType.to_class_literal(self.db)) + f.with_type(KnownClass::UnionType.to_class_literal(db, self.env)) .write_str("types.UnionType")?; f.write_str(" special-form")?; - if let Ok(ty) = union.union_type(self.db) { + if let Ok(ty) = union.union_type(db) { f.write_str(" '")?; - ty.display(self.db).fmt_detailed(f)?; + ty.display(db, self.env).fmt_detailed(f)?; f.write_char('\'')?; } f.write_char('>') @@ -4000,7 +4420,7 @@ impl<'db> FmtDetailed<'db> for DisplayKnownInstanceRepr<'db> { KnownInstanceType::Literal(inner) => { f.set_invalid_type_annotation(); f.write_str("") } KnownInstanceType::Annotated(inner) => { @@ -4009,7 +4429,7 @@ impl<'db> FmtDetailed<'db> for DisplayKnownInstanceRepr<'db> { f.with_type(Type::SpecialForm(SpecialFormType::Annotated)) .write_str("typing.Annotated")?; f.write_char('[')?; - inner.inner(self.db).display(self.db).fmt_detailed(f)?; + inner.inner(db).display(db, self.env).fmt_detailed(f)?; f.write_str(", ]'>") } KnownInstanceType::WrappedOptional(inner) => { @@ -4022,7 +4442,7 @@ impl<'db> FmtDetailed<'db> for DisplayKnownInstanceRepr<'db> { depth += 1; current = next.inner(self.db); } - let (base, extra) = match strip_none(self.db, current) { + let (base, extra) = match strip_none(self.db, env, current) { Some(base) => (base, 1), None => (current, 0), }; @@ -4030,7 +4450,7 @@ impl<'db> FmtDetailed<'db> for DisplayKnownInstanceRepr<'db> { if parenthesize { f.write_char('(')?; } - base.display(self.db).fmt_detailed(f)?; + base.display(self.db, env).fmt_detailed(f)?; if parenthesize { f.write_char(')')?; } @@ -4049,49 +4469,47 @@ impl<'db> FmtDetailed<'db> for DisplayKnownInstanceRepr<'db> { f.with_type(Type::SpecialForm(SpecialFormType::TypingCallable)) .write_str("Callable")?; f.write_str(" special-form '")?; - callable.display(self.db).fmt_detailed(f)?; + callable.display(db, self.env).fmt_detailed(f)?; f.write_str("'>") } KnownInstanceType::TypeGenericAlias(inner) => { f.set_invalid_type_annotation(); f.write_str("") } KnownInstanceType::LiteralStringAlias(_) => f - .with_type(KnownClass::Str.to_class_literal(self.db)) + .with_type(KnownClass::Str.to_class_literal(db, self.env)) .write_str("str"), KnownInstanceType::NewType(declaration) => { f.set_invalid_type_annotation(); f.write_char('<')?; - f.with_type(KnownClass::NewType.to_class_literal(self.db)) + f.with_type(KnownClass::NewType.to_class_literal(db, self.env)) .write_str("NewType")?; f.write_str(" pseudo-class '")?; - f.with_type(ty).write_str(declaration.name(self.db))?; + f.with_type(ty).write_str(declaration.name(db))?; f.write_str("'>") } KnownInstanceType::Sentinel(sentinel) => { - f.with_type(ty).write_str(sentinel.name(self.db).as_str()) + f.with_type(ty).write_str(sentinel.name(db).as_str()) } KnownInstanceType::NamedTupleSpec(_) => f.write_str("NamedTupleSpec"), KnownInstanceType::FunctoolsPartial(partial) => { f.write_str("partial[")?; - Type::Callable(partial.partial(self.db)) - .display_with(self.db, DisplaySettings::default().singleline()) + Type::Callable(partial.partial(db)) + .display_with(db, self.env, DisplaySettings::default().singleline()) .fmt_detailed(f)?; f.write_str("]") } KnownInstanceType::Range { .. } => f - .with_type(KnownClass::Range.to_class_literal(self.db)) + .with_type(KnownClass::Range.to_class_literal(db, self.env)) .write_str("range"), - KnownInstanceType::FunctoolsPartialCall(partial) => { - Type::Callable(partial.partial(self.db)) - .display_with(self.db, DisplaySettings::default().singleline()) - .fmt_detailed(f) - } + KnownInstanceType::FunctoolsPartialCall(partial) => Type::Callable(partial.partial(db)) + .display_with(db, self.env, DisplaySettings::default().singleline()) + .fmt_detailed(f), } } } @@ -4101,73 +4519,146 @@ mod tests { use insta::assert_snapshot; use ruff_python_ast::name::Name; - use crate::Db; - use crate::db::tests::setup_db; - use crate::types::{KnownClass, Parameter, Parameters, Signature, Type}; + use crate::db::tests::{TestDb, setup_db}; + use crate::types::{ + KnownClass, KnownUnion, Parameter, Parameters, Signature, Type, TypeDetail, UnionType, + }; #[test] fn string_literal_display() { let db = setup_db(); assert_eq!( - Type::string_literal(&db, r"\n").display(&db).to_string(), + Type::string_literal(&db, r"\n") + .display(&db, &db.program_environment()) + .to_string(), r#"Literal["\\n"]"# ); assert_eq!( - Type::string_literal(&db, "'").display(&db).to_string(), + Type::string_literal(&db, "'") + .display(&db, &db.program_environment()) + .to_string(), r#"Literal["'"]"# ); assert_eq!( - Type::string_literal(&db, r#"""#).display(&db).to_string(), + Type::string_literal(&db, r#"""#) + .display(&db, &db.program_environment()) + .to_string(), r#"Literal["\""]"# ); } + #[test] + fn numeric_tower_display() { + let db = setup_db(); + let env = db.program_environment(); + + let exact_float = KnownClass::Float.to_instance(&db, &env); + let exact_complex = KnownClass::Complex.to_instance(&db, &env); + let float_annotation = KnownUnion::Float.to_type(&db, &env); + let complex_annotation = KnownUnion::Complex.to_type(&db, &env); + + // a type is shown as what it is: the exact class is its own name, and the + // promoted annotation is the union it stands for. nothing is collapsed into a + // narrower-reading spelling, and nothing is marked with a `*` + assert_snapshot!(exact_float.display(&db, &env), @"float"); + assert_snapshot!(exact_complex.display(&db, &env), @"complex"); + assert_snapshot!(float_annotation.display(&db, &env), @"int | float"); + assert_snapshot!(complex_annotation.display(&db, &env), @"int | float | complex"); + assert_snapshot!(float_annotation.to_meta_type(&db, &env).display(&db, &env), @"type[int | float]"); + assert_snapshot!(complex_annotation.to_meta_type(&db, &env).display(&db, &env), @"type[int | float | complex]"); + + let list_of_float = + KnownClass::List.to_specialized_instance(&db, &env, &[float_annotation]); + assert_snapshot!(list_of_float.display(&db, &env), @"list[int | float]"); + + let string_or_float = UnionType::from_elements( + &db, + &env, + [KnownClass::Str.to_instance(&db, &env), float_annotation], + ); + assert_snapshot!(string_or_float.display(&db, &env), @"str | int | float"); + + // both spellings are ordinary type syntax now — neither needs a marker + assert!( + exact_float + .display(&db, &env) + .to_string_parts() + .is_valid_syntax + ); + assert!( + float_annotation + .display(&db, &env) + .to_string_parts() + .is_valid_syntax + ); + // the annotation renders as the union it stands for, so both members are + // navigable rather than only the one the collapsed spelling named + assert!(matches!( + float_annotation + .display(&db, &env) + .to_string_parts() + .details + .as_slice(), + [ + TypeDetail::Type(Type::ClassLiteral(int)), + TypeDetail::Type(Type::ClassLiteral(float)), + ] if int.known(&db) == Some(KnownClass::Int) + && float.known(&db) == Some(KnownClass::Float) + )); + } + fn display_signature<'db>( - db: &'db dyn Db, + db: &'db TestDb, parameters: impl IntoIterator>, return_ty: Option>, ) -> String { Signature::new( - Parameters::from_annotation(db, parameters), + Parameters::from_annotation(db, &db.program_environment(), parameters), return_ty.unwrap_or(Type::unknown()), ) - .display(db) + .display(db, &db.program_environment()) .to_string() } fn display_signature_multiline<'db>( - db: &'db dyn Db, + db: &'db TestDb, parameters: impl IntoIterator>, return_ty: Option>, ) -> String { Signature::new( - Parameters::from_annotation(db, parameters), + Parameters::from_annotation(db, &db.program_environment(), parameters), return_ty.unwrap_or(Type::unknown()), ) - .display_with(db, super::DisplaySettings::default().multiline()) + .display_with( + db, + &db.program_environment(), + super::DisplaySettings::default().multiline(), + ) .to_string() } #[test] fn signature_display() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); // Empty parameters with no return type. - assert_snapshot!(display_signature(&db, [], None), @"() -> Unknown"); + assert_snapshot!(display_signature(db, [], None), @"() -> Unknown"); // Empty parameters with a return type. assert_snapshot!( - display_signature(&db, [], Some(Type::none(&db))), + display_signature(db, [], Some(Type::none(db, &env))), @"() -> None" ); // Single parameter type (no name) with a return type. assert_snapshot!( display_signature( - &db, - [Parameter::positional_only(None).with_annotated_type(Type::none(&db))], - Some(Type::none(&db)) + db, + [Parameter::positional_only(None).with_annotated_type(Type::none(db, &env))], + Some(Type::none(db, &env)) ), @"(None, /) -> None" ); @@ -4175,15 +4666,15 @@ mod tests { // Two parameters where one has annotation and the other doesn't. assert_snapshot!( display_signature( - &db, + db, [ Parameter::positional_or_keyword(Name::new_static("x")) - .with_default_type(KnownClass::Int.to_instance(&db)), + .with_default_type(KnownClass::Int.to_instance(db, &env)), Parameter::positional_or_keyword(Name::new_static("y")) - .with_annotated_type(KnownClass::Str.to_instance(&db)) - .with_default_type(KnownClass::Str.to_instance(&db)), + .with_annotated_type(KnownClass::Str.to_instance(db, &env)) + .with_default_type(KnownClass::Str.to_instance(db, &env)), ], - Some(Type::none(&db)) + Some(Type::none(db, &env)) ), @"(x=..., y: str = ...) -> None" ); @@ -4191,12 +4682,12 @@ mod tests { // All positional only parameters. assert_snapshot!( display_signature( - &db, + db, [ Parameter::positional_only(Some(Name::new_static("x"))), Parameter::positional_only(Some(Name::new_static("y"))), ], - Some(Type::none(&db)) + Some(Type::none(db, &env)) ), @"(x, y, /) -> None" ); @@ -4204,12 +4695,12 @@ mod tests { // Positional-only parameters mixed with non-positional-only parameters. assert_snapshot!( display_signature( - &db, + db, [ Parameter::positional_only(Some(Name::new_static("x"))), Parameter::positional_or_keyword(Name::new_static("y")), ], - Some(Type::none(&db)) + Some(Type::none(db, &env)) ), @"(x, /, y) -> None" ); @@ -4217,12 +4708,12 @@ mod tests { // All keyword-only parameters. assert_snapshot!( display_signature( - &db, + db, [ Parameter::keyword_only(Name::new_static("x")), Parameter::keyword_only(Name::new_static("y")), ], - Some(Type::none(&db)) + Some(Type::none(db, &env)) ), @"(*, x, y) -> None" ); @@ -4230,12 +4721,12 @@ mod tests { // Keyword-only parameters mixed with non-keyword-only parameters. assert_snapshot!( display_signature( - &db, + db, [ Parameter::positional_or_keyword(Name::new_static("x")), Parameter::keyword_only(Name::new_static("y")), ], - Some(Type::none(&db)) + Some(Type::none(db, &env)) ), @"(x, *, y) -> None" ); @@ -4243,13 +4734,13 @@ mod tests { // '/' parameter must appear before '*' parameter assert_snapshot!( display_signature( - &db, + db, [ Parameter::positional_only(Some(Name::new_static("a"))), Parameter::keyword_only(Name::new_static("x")), Parameter::keyword_only(Name::new_static("y")), ], - Some(Type::none(&db)) + Some(Type::none(db, &env)) ), @"(a, /, *, x, y) -> None" ); @@ -4257,32 +4748,32 @@ mod tests { // A mix of all parameter kinds. assert_snapshot!( display_signature( - &db, + 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)), + .with_annotated_type(KnownClass::Int.to_instance(db, &env)), Parameter::positional_only(Some(Name::new_static("c"))) .with_default_type(Type::int_literal(1)), Parameter::positional_only(Some(Name::new_static("d"))) - .with_annotated_type(KnownClass::Int.to_instance(&db)) + .with_annotated_type(KnownClass::Int.to_instance(db, &env)) .with_default_type(Type::int_literal(2)), Parameter::positional_or_keyword(Name::new_static("e")) .with_default_type(Type::int_literal(3)), Parameter::positional_or_keyword(Name::new_static("f")) - .with_annotated_type(KnownClass::Int.to_instance(&db)) + .with_annotated_type(KnownClass::Int.to_instance(db, &env)) .with_default_type(Type::int_literal(4)), Parameter::variadic(Name::new_static("args")) .with_annotated_type(Type::object()), Parameter::keyword_only(Name::new_static("g")) .with_default_type(Type::int_literal(5)), Parameter::keyword_only(Name::new_static("h")) - .with_annotated_type(KnownClass::Int.to_instance(&db)) + .with_annotated_type(KnownClass::Int.to_instance(db, &env)) .with_default_type(Type::int_literal(6)), Parameter::keyword_variadic(Name::new_static("kwargs")) - .with_annotated_type(KnownClass::Str.to_instance(&db)), + .with_annotated_type(KnownClass::Str.to_instance(db, &env)), ], - Some(KnownClass::Bytes.to_instance(&db)) + Some(KnownClass::Bytes.to_instance(db, &env)) ), @"(a, b: int, c=1, d: int = 2, /, e=3, f: int = 4, *args: object, *, g=5, h: int = 6, **kwargs: str) -> bytes" ); @@ -4291,22 +4782,24 @@ mod tests { #[test] fn signature_display_multiline() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); // Empty parameters with no return type. - assert_snapshot!(display_signature_multiline(&db, [], None), @"() -> Unknown"); + assert_snapshot!(display_signature_multiline(db, [], None), @"() -> Unknown"); // Empty parameters with a return type. assert_snapshot!( - display_signature_multiline(&db, [], Some(Type::none(&db))), + display_signature_multiline(db, [], Some(Type::none(db, &env))), @"() -> None" ); // Single parameter type (no name) with a return type. assert_snapshot!( display_signature_multiline( - &db, - [Parameter::positional_only(None).with_annotated_type(Type::none(&db))], - Some(Type::none(&db)) + db, + [Parameter::positional_only(None).with_annotated_type(Type::none(db, &env))], + Some(Type::none(db, &env)) ), @"(None, /) -> None" ); @@ -4314,15 +4807,15 @@ mod tests { // Two parameters where one has annotation and the other doesn't. assert_snapshot!( display_signature_multiline( - &db, + db, [ Parameter::positional_or_keyword(Name::new_static("x")) - .with_default_type(KnownClass::Int.to_instance(&db)), + .with_default_type(KnownClass::Int.to_instance(db, &env)), Parameter::positional_or_keyword(Name::new_static("y")) - .with_annotated_type(KnownClass::Str.to_instance(&db)) - .with_default_type(KnownClass::Str.to_instance(&db)), + .with_annotated_type(KnownClass::Str.to_instance(db, &env)) + .with_default_type(KnownClass::Str.to_instance(db, &env)), ], - Some(Type::none(&db)) + Some(Type::none(db, &env)) ), @" ( @@ -4335,12 +4828,12 @@ mod tests { // All positional only parameters. assert_snapshot!( display_signature_multiline( - &db, + db, [ Parameter::positional_only(Some(Name::new_static("x"))), Parameter::positional_only(Some(Name::new_static("y"))), ], - Some(Type::none(&db)) + Some(Type::none(db, &env)) ), @" ( @@ -4354,12 +4847,12 @@ mod tests { // Positional-only parameters mixed with non-positional-only parameters. assert_snapshot!( display_signature_multiline( - &db, + db, [ Parameter::positional_only(Some(Name::new_static("x"))), Parameter::positional_or_keyword(Name::new_static("y")), ], - Some(Type::none(&db)) + Some(Type::none(db, &env)) ), @" ( @@ -4373,12 +4866,12 @@ mod tests { // All keyword-only parameters. assert_snapshot!( display_signature_multiline( - &db, + db, [ Parameter::keyword_only(Name::new_static("x")), Parameter::keyword_only(Name::new_static("y")), ], - Some(Type::none(&db)) + Some(Type::none(db, &env)) ), @" ( @@ -4392,12 +4885,12 @@ mod tests { // Keyword-only parameters mixed with non-keyword-only parameters. assert_snapshot!( display_signature_multiline( - &db, + db, [ Parameter::positional_or_keyword(Name::new_static("x")), Parameter::keyword_only(Name::new_static("y")), ], - Some(Type::none(&db)) + Some(Type::none(db, &env)) ), @" ( @@ -4411,32 +4904,32 @@ mod tests { // A mix of all parameter kinds. assert_snapshot!( display_signature_multiline( - &db, + 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)), + .with_annotated_type(KnownClass::Int.to_instance(db, &env)), Parameter::positional_only(Some(Name::new_static("c"))) .with_default_type(Type::int_literal(1)), Parameter::positional_only(Some(Name::new_static("d"))) - .with_annotated_type(KnownClass::Int.to_instance(&db)) + .with_annotated_type(KnownClass::Int.to_instance(db, &env)) .with_default_type(Type::int_literal(2)), Parameter::positional_or_keyword(Name::new_static("e")) .with_default_type(Type::int_literal(3)), Parameter::positional_or_keyword(Name::new_static("f")) - .with_annotated_type(KnownClass::Int.to_instance(&db)) + .with_annotated_type(KnownClass::Int.to_instance(db, &env)) .with_default_type(Type::int_literal(4)), Parameter::variadic(Name::new_static("args")) .with_annotated_type(Type::object()), Parameter::keyword_only(Name::new_static("g")) .with_default_type(Type::int_literal(5)), Parameter::keyword_only(Name::new_static("h")) - .with_annotated_type(KnownClass::Int.to_instance(&db)) + .with_annotated_type(KnownClass::Int.to_instance(db, &env)) .with_default_type(Type::int_literal(6)), Parameter::keyword_variadic(Name::new_static("kwargs")) - .with_annotated_type(KnownClass::Str.to_instance(&db)), + .with_annotated_type(KnownClass::Str.to_instance(db, &env)), ], - Some(KnownClass::Bytes.to_instance(&db)) + Some(KnownClass::Bytes.to_instance(db, &env)) ), @" ( diff --git a/crates/ty_python_semantic/src/types/enums.rs b/crates/ty_python_semantic/src/types/enums.rs index 710b1ab1cb..5a96a5c43f 100644 --- a/crates/ty_python_semantic/src/types/enums.rs +++ b/crates/ty_python_semantic/src/types/enums.rs @@ -1,3 +1,4 @@ +use crate::ProgramEnvironment; use compact_str::ToCompactString; use ruff_db::parsed::parsed_module; use ruff_python_ast::name::Name; @@ -61,9 +62,14 @@ impl KnownEnumDataTypeMixin { /// /// Literal conversions are preserved precisely, unions are normalized element-wise, and values /// whose conversion cannot be modeled precisely fall back to the mixin's instance type. - fn normalize_value<'db>(self, db: &'db dyn Db, value: Type<'db>) -> Type<'db> { + fn normalize_value<'db>( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + value: Type<'db>, + ) -> Type<'db> { if let Type::Union(union) = value { - return union.map(db, |element| self.normalize_value(db, *element)); + return union.map(db, env, |element| self.normalize_value(db, env, *element)); } match (self, value.as_literal_value_kind()) { @@ -78,8 +84,8 @@ impl KnownEnumDataTypeMixin { (Self::Str, Some(LiteralValueTypeKind::Bool(value))) => { Type::string_literal(db, if value { "True" } else { "False" }) } - (Self::Int, _) => KnownClass::Int.to_instance(db), - (Self::Str, _) => KnownClass::Str.to_instance(db), + (Self::Int, _) => KnownClass::Int.to_instance(db, env), + (Self::Str, _) => KnownClass::Str.to_instance(db, env), } } } @@ -160,13 +166,18 @@ impl<'db> EnumValueConstruction<'db> { /// Returns the payload after known built-in data-type construction, or `None` when the /// constructor may coerce it in a way that ty does not model. - fn normalize_value(self, db: &'db dyn Db, value: Type<'db>) -> Option> { + fn normalize_value( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + value: Type<'db>, + ) -> Option> { match self.data_type { InheritedEnumDataType::None => Some(value), InheritedEnumDataType::DeclaredValue(data_type) => { - value_has_exact_known_class(db, value, data_type).then_some(value) + value_has_exact_known_class(db, env, value, data_type).then_some(value) } - InheritedEnumDataType::Known(mixin) => Some(mixin.normalize_value(db, value)), + InheritedEnumDataType::Known(mixin) => Some(mixin.normalize_value(db, env, value)), InheritedEnumDataType::Opaque => None, } } @@ -182,6 +193,7 @@ impl<'db> EnumValueConstruction<'db> { fn alias_detection_value( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, value_ty: Type<'db>, is_auto: bool, ) -> Option> { @@ -197,11 +209,13 @@ impl<'db> EnumValueConstruction<'db> { } else if self.generate_next_value.is_opaque() { return None; } else if let Some(function) = self.generate_next_value.function() { - function.signature(db).overload_return_type_or_unknown(db) + function + .signature(db) + .overload_return_type_or_unknown(db, env) } else { value_ty }; - self.normalize_value(db, value) + self.normalize_value(db, env, value) } } @@ -224,7 +238,7 @@ impl<'db> EnumValueAnnotation<'db> { #[derive(Debug, PartialEq, Eq, salsa::SalsaValue)] pub(crate) struct EnumMetadata<'db> { pub(crate) members: FxIndexMap>, - pub(crate) aliases: FxHashMap, + aliases: FxHashMap, /// Whether alias detection was precise for every member declaration. pub(super) aliases_are_known: bool, @@ -243,6 +257,7 @@ impl get_size2::GetSize for EnumMetadata<'_> {} pub(super) fn class_defines_property<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: ClassLiteral<'db>, name: &str, ) -> bool { @@ -266,7 +281,12 @@ pub(super) fn class_defines_property<'db>( ) { return false; } - if let Some(member) = base.own_class_member(db, None, name).inner.place.raw_type() { + if let Some(member) = base + .own_class_member(db, env, None, name) + .inner + .place + .raw_type() + { return member.is_property_instance(); } } @@ -312,11 +332,16 @@ fn enum_class_literal<'db>( db: &'db dyn Db, class: ClassLiteral<'db>, ) -> Option> { + let env = ProgramEnvironment::from_file(class.program_file(db)); let metadata = enum_metadata(db, class)?; let members = metadata .members .keys() - .map(|name| metadata.value_type(db, name).map(|ty| (name.clone(), ty))) + .map(|name| { + metadata + .value_type(db, &env, name) + .map(|ty| (name.clone(), ty)) + }) .collect::>>()?; let mut aliases: Vec<_> = metadata .aliases @@ -325,7 +350,11 @@ fn enum_class_literal<'db>( .collect(); aliases.sort_unstable(); let members_are_exhaustive = !metadata.value_construction.metaclass_may_transform_values - && !Type::ClassLiteral(class).is_subtype_of(db, KnownClass::Flag.to_subclass_of(db)) + && !Type::ClassLiteral(class).is_subtype_of( + db, + &env, + KnownClass::Flag.to_subclass_of(db, &env), + ) && !enum_has_custom_missing(db, class) && !class.as_static().is_some_and(|static_class| { crate::types::class::based_enum_has_payload_variants(db, static_class) @@ -405,15 +434,16 @@ impl<'db> EnumClassLiteral<'db> { /// expand through the remaining literal union so descriptor lookup sees ordinary enum literals. pub(super) fn instance_member_for_enum_complement<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, complement: EnumComplement<'db>, name: &str, ) -> PlaceAndQualifiers<'db> { - if let Some(member) = special_member_for_enum_complement(db, complement, name) { + if let Some(member) = special_member_for_enum_complement(db, env, complement, name) { member } else { complement - .remaining_literal_union(db) - .instance_member(db, name) + .remaining_literal_union(db, env) + .instance_member(db, env, name) } } @@ -423,16 +453,17 @@ pub(super) fn instance_member_for_enum_complement<'db>( /// general member lookup so descriptor and class-variable policy is still applied. pub(super) fn member_lookup_for_enum_complement<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, complement: EnumComplement<'db>, name: &str, policy: MemberLookupPolicy, ) -> PlaceAndQualifiers<'db> { - if let Some(member) = special_member_for_enum_complement(db, complement, name) { + if let Some(member) = special_member_for_enum_complement(db, env, complement, name) { member } else { complement - .remaining_literal_union(db) - .member_lookup_with_policy(db, name, policy) + .remaining_literal_union(db, env) + .member_lookup_with_policy(db, env, name, policy) } } @@ -443,13 +474,14 @@ pub(super) fn member_lookup_for_enum_complement<'db>( /// directly from the remaining canonical members. fn special_member_for_enum_complement<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, complement: EnumComplement<'db>, name: &str, ) -> Option> { if matches!(name, "name" | "_name_" | "value" | "_value_") - && !class_defines_property(db, complement.enum_class(db), name) + && !class_defines_property(db, env, complement.enum_class(db), name) && complement.rest(db).iter().all(Type::is_dynamic) - && let Some(member_ty) = complement.member_type(db, name) + && let Some(member_ty) = complement.member_type(db, env, name) { Some(Place::bound(member_ty).into()) } else { @@ -464,6 +496,7 @@ fn special_member_for_enum_complement<'db>( /// are normalized to the annotated class by constructors such as `int.__new__`. fn known_constructor_preserves_value_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, value: Type<'db>, annotation: Type<'db>, ) -> bool { @@ -472,8 +505,8 @@ fn known_constructor_preserves_value_type<'db>( Type::Union(union) => union .elements(db) .iter() - .all(|element| known_constructor_preserves_value_type(db, *element, annotation)), - Type::LiteralValue(literal) => literal.fallback_instance(db) == annotation, + .all(|element| known_constructor_preserves_value_type(db, env, *element, annotation)), + Type::LiteralValue(literal) => literal.fallback_instance(db, env) == annotation, value => value == annotation, } } @@ -483,6 +516,7 @@ fn known_constructor_preserves_value_type<'db>( /// constructor may return an instance of the built-in base. fn value_has_exact_known_class<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, value: Type<'db>, data_type: KnownClass, ) -> bool { @@ -490,8 +524,8 @@ fn value_has_exact_known_class<'db>( Type::Union(union) => union .elements(db) .iter() - .all(|element| value_has_exact_known_class(db, *element, data_type)), - Type::LiteralValue(literal) => match literal.fallback_instance(db) { + .all(|element| value_has_exact_known_class(db, env, *element, data_type)), + Type::LiteralValue(literal) => match literal.fallback_instance(db, env) { Type::NominalInstance(instance) => instance.has_known_class(db, data_type), _ => false, }, @@ -519,7 +553,12 @@ impl<'db> EnumMetadata<'db> { /// data types normalize the value directly. A literal is preserved when its runtime class /// matches an inherited `_value_` annotation; otherwise, the annotation describes the /// normalized value. - pub(crate) fn value_type(&self, db: &'db dyn Db, member_name: &Name) -> Option> { + fn value_type( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + member_name: &Name, + ) -> Option> { if !self.members.contains_key(member_name) { return None; } @@ -527,12 +566,12 @@ impl<'db> EnumMetadata<'db> { if let Some(EnumValueAnnotation::UserDefined(annotation)) = self.value_annotation { return Some(annotation); } - let Some(value) = self.concrete_value_type(db, member_name) else { + let Some(value) = self.concrete_value_type(db, env, member_name) else { return Some(Type::Dynamic(DynamicType::Any)); }; if let Some(EnumValueAnnotation::StandardLibrary(annotation)) = self.value_annotation - && !known_constructor_preserves_value_type(db, value, annotation) + && !known_constructor_preserves_value_type(db, env, value, annotation) { Some(annotation) } else { @@ -547,6 +586,7 @@ impl<'db> EnumMetadata<'db> { pub(super) fn concrete_value_type( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, member_name: &Name, ) -> Option> { let declared_value = self.members.get(member_name).copied()?; @@ -560,11 +600,13 @@ impl<'db> EnumMetadata<'db> { .is_user_defined() && let Some(func_ty) = self.value_construction.generate_next_value.function() { - func_ty.signature(db).overload_return_type_or_unknown(db) + func_ty + .signature(db) + .overload_return_type_or_unknown(db, env) } else { declared_value }; - self.value_construction.normalize_value(db, value) + self.value_construction.normalize_value(db, env, value) } /// Return whether enum construction may replace the value declared for `member_name`. @@ -583,7 +625,11 @@ impl<'db> EnumMetadata<'db> { /// metaclass that may transform member values, returns `Any`. /// Otherwise, returns the union of each member's `value_type`, which /// applies `_generate_next_value_`'s return type to `auto()` members. - pub(crate) fn instance_value_type(&self, db: &'db dyn Db) -> Option> { + pub(crate) fn instance_value_type( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { if self.members.is_empty() { return None; } @@ -595,8 +641,8 @@ impl<'db> EnumMetadata<'db> { let union = self .members .keys() - .filter_map(|name| self.value_type(db, name)) - .fold(UnionBuilder::new(db), UnionBuilder::add) + .filter_map(|name| self.value_type(db, env, name)) + .fold(UnionBuilder::new(db, env), UnionBuilder::add) .build(); Some(union) } @@ -611,7 +657,11 @@ impl<'db> EnumMetadata<'db> { /// narrowed to a specific member (e.g. `x: MyEnum` where `MyEnum` has multiple members). /// /// Returns the union of all member name string literals. - pub(crate) fn instance_name_type(&self, db: &'db dyn Db) -> Option> { + pub(crate) fn instance_name_type( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { if self.members.is_empty() { return None; } @@ -619,7 +669,7 @@ impl<'db> EnumMetadata<'db> { .members .keys() .map(|name| Type::string_literal(db, name)) - .fold(UnionBuilder::new(db), UnionBuilder::add) + .fold(UnionBuilder::new(db, env), UnionBuilder::add) .build(); Some(union) } @@ -666,6 +716,7 @@ impl<'db> EnumComplementType<'db> { /// Recognize the compact enum-complement shape inside an intersection. pub(crate) fn from_intersection_parts( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, positive: &FxOrderSet>, negative: &NegativeIntersectionElements<'db>, ) -> Option { @@ -677,7 +728,8 @@ impl<'db> EnumComplementType<'db> { continue; }; - let Some(enum_class_literal) = instance.class_literal(db).into_enum_class(db) else { + let Some(enum_class_literal) = instance.class_literal(db, env).into_enum_class(db) + else { rest.push(*positive); continue; }; @@ -737,28 +789,24 @@ impl<'db> EnumComplementType<'db> { self.rest(db).is_empty() && self.remaining_member_count(db) == 1 } - /// Return `true` when this complement is a single value under equality narrowing. - /// - /// Enums that override equality are excluded because one remaining enum literal can still - /// compare equal to non-identical values. - pub(crate) fn is_single_valued(self, db: &'db dyn Db) -> bool { - self.is_singleton(db) - && !self - .enum_class(db) - .to_non_generic_instance(db) - .overrides_equality(db) - } - /// Expand this complement to the enum literals that remain possible. - pub fn remaining_literal_types(self, db: &'db dyn Db) -> Vec> { + pub fn remaining_literal_types( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Vec> { self.remaining_member_names(db) - .map(|name| self.remaining_literal_type(db, name)) + .map(|name| self.remaining_literal_type(db, env, name)) .collect() } /// Expand this complement to the union of enum literals that remain possible. - pub(crate) fn remaining_literal_union(self, db: &'db dyn Db) -> Type<'db> { - let alternatives = self.remaining_literal_types(db); + pub(crate) fn remaining_literal_union( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { + let alternatives = self.remaining_literal_types(db, env); match alternatives.as_slice() { [] => Type::Never, [single] => *single, @@ -774,16 +822,21 @@ impl<'db> EnumComplementType<'db> { } /// Build the type for one remaining canonical member, preserving any positive rest components. - fn remaining_literal_type(self, db: &'db dyn Db, name: &Name) -> Type<'db> { + fn remaining_literal_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &Name, + ) -> Type<'db> { let literal = Type::enum_literal(EnumLiteralType::new(db, self.enum_class_literal(db), name)); if self.rest(db).is_empty() { return literal; } - let mut builder = IntersectionBuilder::new(db).add_positive(literal); + let mut builder = IntersectionBuilder::new(db, env).add_positive(literal); for rest in self.rest(db) { - builder = builder.add_positive(*rest); + builder.add_positive_in_place(*rest); } builder.build() } @@ -795,6 +848,7 @@ impl<'db> EnumComplementType<'db> { pub(crate) fn remaining_literal_types_for_display( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, max_literals: usize, ) -> Option>> { if !self.rest(db).is_empty() { @@ -806,18 +860,26 @@ impl<'db> EnumComplementType<'db> { return None; } - Some(self.remaining_literal_types(db)) + Some(self.remaining_literal_types(db, env)) } /// Return the type of a member attribute for all enum literals remaining in this complement. /// /// This handles `.name`, `.value`, `._name_`, and `._value_` by unioning the corresponding /// attribute type from each remaining canonical enum member. - pub(crate) fn member_type(self, db: &'db dyn Db, member_name: &str) -> Option> { + fn member_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + member_name: &str, + ) -> Option> { let enum_class_literal = self.enum_class_literal(db); - let is_enum_subclass = Type::ClassLiteral(self.enum_class(db)) - .is_subtype_of(db, KnownClass::Enum.to_subclass_of(db)); - let mut builder = UnionBuilder::new(db); + let is_enum_subclass = Type::ClassLiteral(self.enum_class(db)).is_subtype_of( + db, + env, + KnownClass::Enum.to_subclass_of(db, env), + ); + let mut builder = UnionBuilder::new(db, env); let mut found_member = false; for name in self.remaining_member_names(db) { @@ -846,9 +908,13 @@ impl<'db> EnumComplementType<'db> { } /// Reconstruct the equivalent set-theoretic intersection. - pub(crate) fn to_intersection(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn to_intersection( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { let enum_class = self.enum_class(db); - let mut positive = FxOrderSet::from_iter([enum_class.to_non_generic_instance(db)]); + let mut positive = FxOrderSet::from_iter([enum_class.to_non_generic_instance(db, env)]); positive.extend(self.rest(db).iter().copied()); let mut negative = NegativeIntersectionElements::default(); @@ -875,7 +941,8 @@ pub(crate) fn enum_ignored_names<'db>(db: &'db dyn Db, scope_id: ScopeId<'db>) - }; let ignore_bindings = use_def_map.reachable_symbol_bindings(ignore); - let ignore_place = place_from_bindings(db, ignore_bindings).place; + let env = ProgramEnvironment::from_scope(scope_id); + let ignore_place = place_from_bindings(db, &env, ignore_bindings).place; match ignore_place { Place::Defined(DefinedPlace { ty, .. }) => ty @@ -928,6 +995,7 @@ pub(crate) fn enum_metadata<'db>( db: &'db dyn Db, class: ClassLiteral<'db>, ) -> Option> { + let env = &ProgramEnvironment::from_file(class.program_file(db)); let class = match class { ClassLiteral::Static(class) => class, ClassLiteral::Dynamic(..) => { @@ -948,8 +1016,9 @@ pub(crate) fn enum_metadata<'db>( if !spec.has_known_members(db) { return None; } + let env = ProgramEnvironment::from_scope(enum_lit.scope(db)); let value_construction = EnumValueConstruction { - data_type: inherited_enum_data_type(db, ClassLiteral::DynamicEnum(enum_lit)), + data_type: inherited_enum_data_type(db, &env, ClassLiteral::DynamicEnum(enum_lit)), ..EnumValueConstruction::default() }; let mut members = FxIndexMap::default(); @@ -957,7 +1026,7 @@ pub(crate) fn enum_metadata<'db>( let mut enum_values: FxHashMap, Name> = FxHashMap::default(); for (name, ty) in spec.members(db) { if value_construction - .alias_detection_value(db, *ty, false) + .alias_detection_value(db, &env, *ty, false) .and_then(|alias_value_ty| { try_register_alias(alias_value_ty, name, &mut enum_values, &mut aliases) // Identical raw literals remain aliases even when normalization widens. @@ -989,7 +1058,7 @@ pub(crate) fn enum_metadata<'db>( // on the surface rather than `NAME = value` assignments, so synthesize the // metadata directly (each member's value is an `auto()`-style int) if let Some(names) = crate::types::class::based_enum_unit_member_names(db, class) { - let int_ty = KnownClass::Int.to_instance(db); + let int_ty = KnownClass::Int.to_instance(db, env); let mut members = FxIndexMap::default(); let mut auto_members = FxHashSet::default(); for name in names { @@ -1017,7 +1086,9 @@ pub(crate) fn enum_metadata<'db>( return None; } - if !is_enum_class_by_inheritance(db, class) { + let env = ProgramEnvironment::from_file(class.program_file(db)); + + if !is_enum_class_by_inheritance(db, &env, class) { return None; } @@ -1035,26 +1106,27 @@ pub(crate) fn enum_metadata<'db>( // Look up custom construction methods, falling back to parent enum classes. An opaque binding // still shadows methods from classes later in the MRO. - let data_type = inherited_enum_data_type(db, ClassLiteral::Static(class)); + let data_type = inherited_enum_data_type(db, &env, ClassLiteral::Static(class)); let user_defined_init = custom_enum_method(db, scope_id, "__init__") - .or_else(|| inherited_user_defined_enum_method(db, class, "__init__")); + .or_else(|| inherited_user_defined_enum_method(db, &env, class, "__init__")); let init = resolve_enum_method(user_defined_init, || { - inherited_known_enum_method(db, class, "__init__") + inherited_known_enum_method(db, &env, class, "__init__") }); // CPython checks `__new_member__` and then `__new__` on each enum base before continuing // through the MRO or falling back to the data-type constructor. let user_defined_new = custom_enum_method(db, scope_id, "__new__") - .or_else(|| inherited_user_defined_enum_new(db, class)) + .or_else(|| inherited_user_defined_enum_new(db, &env, class)) .or_else(|| inherited_user_defined_mixin_new(db, class)); let new = resolve_enum_method(user_defined_new, || { - inherited_known_enum_method(db, class, "__new__") + inherited_known_enum_method(db, &env, class, "__new__") }); let metaclass_may_transform_values = enum_metaclass_may_transform_values(db, class); let user_defined_generate_next_value = - custom_enum_method(db, scope_id, "_generate_next_value_") - .or_else(|| inherited_user_defined_enum_method(db, class, "_generate_next_value_")); + custom_enum_method(db, scope_id, "_generate_next_value_").or_else(|| { + inherited_user_defined_enum_method(db, &env, class, "_generate_next_value_") + }); let generate_next_value = resolve_enum_method(user_defined_generate_next_value, || { - inherited_known_enum_method(db, class, "_generate_next_value_") + inherited_known_enum_method(db, &env, class, "_generate_next_value_") }); let value_construction = EnumValueConstruction { init, @@ -1086,7 +1158,7 @@ pub(crate) fn enum_metadata<'db>( return None; } - let inferred = place_from_bindings(db, bindings).place; + let inferred = place_from_bindings(db, &env, bindings).place; let mut explicit_member_wrapper = false; let value_ty = match inferred { @@ -1107,7 +1179,7 @@ pub(crate) fn enum_metadata<'db>( Some(KnownClass::Member) => { explicit_member_wrapper = true; Some( - ty.member(db, "value") + ty.member(db, &env, "value") .place .ignore_possibly_undefined() .unwrap_or(Type::unknown()), @@ -1122,7 +1194,11 @@ pub(crate) fn enum_metadata<'db>( // `StrEnum`s have different `auto()` behaviour to enums inheriting from `(str, Enum)` let auto_value_ty = if Type::ClassLiteral(ClassLiteral::Static(class)) - .is_subtype_of(db, KnownClass::StrEnum.to_subclass_of(db)) + .is_subtype_of( + db, + &env, + KnownClass::StrEnum.to_subclass_of(db, &env), + ) { Type::string_literal(db, &*name.to_lowercase()) } else { @@ -1134,7 +1210,8 @@ pub(crate) fn enum_metadata<'db>( .filter(|class| { !Type::from(*class).is_subtype_of( db, - KnownClass::Enum.to_subclass_of(db), + &env, + KnownClass::Enum.to_subclass_of(db, &env), ) }) .map(|class| class.known(db)) @@ -1153,7 +1230,7 @@ pub(crate) fn enum_metadata<'db>( [] | [Some(KnownClass::Int)] ) { if prev_value_was_non_literal_int { - KnownClass::Int.to_instance(db) + KnownClass::Int.to_instance(db, &env) } else if let Some(prev_bool_literal) = prev_bool_literal { @@ -1180,6 +1257,7 @@ pub(crate) fn enum_metadata<'db>( let dunder_get = ty .member_lookup_with_policy( db, + &env, "__get__", MemberLookupPolicy::NO_INSTANCE_FALLBACK, ) @@ -1210,7 +1288,7 @@ pub(crate) fn enum_metadata<'db>( declaration.kind(db), DefinitionKind::AnnotatedAssignment(assignment) if assignment - .value(&parsed_module(db, declaration.file(db)).load(db)) + .value(&parsed_module(db, declaration.python_file(db)).load(db)) .is_some() ) }) @@ -1222,7 +1300,7 @@ pub(crate) fn enum_metadata<'db>( // Track whether this member's value is a non-literal `int`, so a // following `auto()` knows to widen its result to `int`. prev_value_was_non_literal_int = value_ty.as_int_like_literal().is_none() - && value_ty.is_assignable_to(db, KnownClass::Int.to_instance(db)); + && value_ty.is_assignable_to(db, &env, KnownClass::Int.to_instance(db, &env)); prev_bool_literal = value_ty .as_literal_value_kind() @@ -1232,7 +1310,7 @@ pub(crate) fn enum_metadata<'db>( }); match value_construction - .alias_detection_value(db, value_ty, auto_members.contains(name)) + .alias_detection_value(db, &env, value_ty, auto_members.contains(name)) .and_then(|alias_value_ty| { try_register_alias(alias_value_ty, name, &mut enum_values, &mut aliases) }) { @@ -1253,12 +1331,12 @@ pub(crate) fn enum_metadata<'db>( return None; } - let value_annotation = custom_value_annotation(db, scope_id) - .or_else(|| inherited_user_defined_value_annotation(db, class)) + let value_annotation = custom_value_annotation(db, &env, scope_id) + .or_else(|| inherited_user_defined_value_annotation(db, &env, class)) .map(EnumValueAnnotation::UserDefined) .or_else(|| { (!metaclass_may_transform_values) - .then(|| inherited_value_annotation(db, class)) + .then(|| inherited_value_annotation(db, &env, class)) .flatten() .map(EnumValueAnnotation::StandardLibrary) }); @@ -1307,8 +1385,11 @@ fn enum_metaclass_may_transform_values<'db>( /// which declare `_value_` annotations that normally should be inherited. fn iter_parent_enum_classes<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: StaticClassLiteral<'db>, ) -> impl Iterator> + 'db { + let env = env.clone(); + class .iter_mro(db, None) .skip(1) @@ -1321,15 +1402,19 @@ fn iter_parent_enum_classes<'db>( KnownClass::IntEnum | KnownClass::Flag | KnownClass::IntFlag ) }); - (is_traversable && is_enum_class_by_inheritance(db, base)).then_some(base) + (is_traversable && is_enum_class_by_inheritance(db, &env, base)).then_some(base) }) } /// Returns the `_value_` annotation type if one is declared in the given scope. -fn custom_value_annotation<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Option> { +fn custom_value_annotation<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + scope: ScopeId<'db>, +) -> Option> { let symbol_id = place_table(db, scope).symbol_id("_value_")?; let declarations = use_def_map(db, scope).end_of_scope_symbol_declarations(symbol_id); - place_from_declarations(db, declarations) + place_from_declarations(db, env, declarations) .ignore_conflicting_declarations() .ignore_possibly_undefined() } @@ -1337,20 +1422,22 @@ fn custom_value_annotation<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Option< /// Looks up an inherited `_value_` annotation from parent enum classes in the MRO. fn inherited_value_annotation<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: StaticClassLiteral<'db>, ) -> Option> { - iter_parent_enum_classes(db, class) - .find_map(|base| custom_value_annotation(db, base.body_scope(db))) + iter_parent_enum_classes(db, env, class) + .find_map(|base| custom_value_annotation(db, env, base.body_scope(db))) } /// Looks up an inherited `_value_` annotation from user-defined parent enum classes in the MRO. fn inherited_user_defined_value_annotation<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: StaticClassLiteral<'db>, ) -> Option> { - iter_parent_enum_classes(db, class) + iter_parent_enum_classes(db, env, class) .filter(|base| base.known(db).is_none()) - .find_map(|base| custom_value_annotation(db, base.body_scope(db))) + .find_map(|base| custom_value_annotation(db, env, base.body_scope(db))) } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] @@ -1368,6 +1455,7 @@ enum InheritedEnumDataType { /// precisely when no user-defined non-enum base can affect member construction or attribute access. fn inherited_enum_data_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: ClassLiteral<'db>, ) -> InheritedEnumDataType { let mut selected = InheritedEnumDataType::None; @@ -1390,7 +1478,8 @@ fn inherited_enum_data_type<'db>( return InheritedEnumDataType::Opaque; }; - if base.known(db) == Some(KnownClass::Object) || is_enum_class_by_inheritance(db, base) + if base.known(db) == Some(KnownClass::Object) + || is_enum_class_by_inheritance(db, env, base) { continue; } @@ -1451,10 +1540,11 @@ fn custom_enum_method<'db>( /// Looks up the first user-defined enum method in the MRO. fn inherited_user_defined_enum_method<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: StaticClassLiteral<'db>, name: &str, ) -> Option> { - iter_parent_enum_classes(db, class) + iter_parent_enum_classes(db, env, class) .filter(|base| base.known(db).is_none()) .find_map(|base| custom_enum_method(db, base.body_scope(db), name)) } @@ -1462,9 +1552,10 @@ fn inherited_user_defined_enum_method<'db>( /// Looks up the first user-defined enum member constructor in the MRO. fn inherited_user_defined_enum_new<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: StaticClassLiteral<'db>, ) -> Option> { - iter_parent_enum_classes(db, class) + iter_parent_enum_classes(db, env, class) .filter(|base| base.known(db).is_none()) .find_map(|base| { let scope = base.body_scope(db); @@ -1494,10 +1585,11 @@ fn inherited_user_defined_mixin_new<'db>( /// Looks up a resolvable method inherited from a known enum class. fn inherited_known_enum_method<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: StaticClassLiteral<'db>, name: &str, ) -> Option> { - iter_parent_enum_classes(db, class) + iter_parent_enum_classes(db, env, class) .filter(|base| base.known(db).is_some()) .find_map( |base| match custom_enum_method(db, base.body_scope(db), name) { @@ -1566,23 +1658,27 @@ pub(crate) fn is_enum_class<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { /// instance of a metaclass), a tuple (a valid multi-target classinfo /// spelling), a bare `object` (which admits classes), and anything dynamic or /// unresolved lower to `isinstance` as usual -pub fn basedpython_is_keeps_identity<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { +pub fn basedpython_is_keeps_identity<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> bool { match ty { Type::Union(union) => union .elements(db) .iter() - .all(|element| basedpython_is_keeps_identity(db, *element)), + .all(|element| basedpython_is_keeps_identity(db, env, *element)), // literal values (enum members included) are never classes Type::LiteralValue(_) | Type::EnumComplement(_) => true, // a use-site modifier says nothing about whether the value is a class: // a unit enum variant is a `final _Shape_Point`, still an instance Type::Restricted(restricted) => { - basedpython_is_keeps_identity(db, restricted.value_type(db)) + basedpython_is_keeps_identity(db, env, restricted.value_type(db)) } Type::NominalInstance(instance) => { !instance.has_known_class(db, KnownClass::Object) - && !ty.is_assignable_to(db, KnownClass::Type.to_instance(db)) - && instance.tuple_spec(db).is_none() + && !ty.is_assignable_to(db, env, KnownClass::Type.to_instance(db, env)) + && instance.tuple_spec(db, env).is_none() } _ => false, } @@ -1595,13 +1691,16 @@ pub fn basedpython_is_keeps_identity<'db>(db: &'db dyn Db, ty: Type<'db>) -> boo /// verifies that the class has members. pub(crate) fn is_enum_class_by_inheritance<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: StaticClassLiteral<'db>, ) -> bool { - Type::ClassLiteral(ClassLiteral::Static(class)) - .is_subtype_of(db, KnownClass::Enum.to_subclass_of(db)) - || class - .metaclass(db) - .is_subtype_of(db, KnownClass::EnumType.to_subclass_of(db)) + Type::ClassLiteral(ClassLiteral::Static(class)).is_subtype_of( + db, + env, + KnownClass::Enum.to_subclass_of(db, env), + ) || class + .metaclass(db) + .is_subtype_of(db, env, KnownClass::EnumType.to_subclass_of(db, env)) } /// Extracts the inner value type from an `enum.nonmember()` wrapper. @@ -1610,11 +1709,15 @@ pub(crate) fn is_enum_class_by_inheritance<'db>( /// returns the inner value, not the `nonmember` wrapper. /// /// Returns `Some(value_type)` if the type is a `nonmember[T]`, otherwise `None`. -pub(crate) fn try_unwrap_nonmember_value<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option> { +pub(crate) fn try_unwrap_nonmember_value<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Option> { match ty { Type::NominalInstance(instance) if instance.has_known_class(db, KnownClass::Nonmember) => { Some( - ty.member(db, "value") + ty.member(db, env, "value") .place .ignore_possibly_undefined() .unwrap_or(Type::unknown()), diff --git a/crates/ty_python_semantic/src/types/equality.rs b/crates/ty_python_semantic/src/types/equality.rs index 36e370dce8..6459a61fec 100644 --- a/crates/ty_python_semantic/src/types/equality.rs +++ b/crates/ty_python_semantic/src/types/equality.rs @@ -5,19 +5,22 @@ //! methods. use rustc_hash::FxHashSet; +use ty_python_core::definition::Definition; -use crate::{AnalysisSettings, Db, place::PlaceAndQualifiers}; +use crate::{AnalysisSettings, Db, ProgramEnvironment, place::PlaceAndQualifiers}; use super::{ - EnumLiteralType, IntersectionBuilder, KnownBoundMethodType, KnownClass, LiteralValueType, - LiteralValueTypeKind, MemberLookupPolicy, Truthiness, Type, TypeVarBoundOrConstraints, - UnionBuilder, + CallArguments, EnumLiteralType, IntersectionBuilder, KnownBoundMethodType, KnownClass, + LiteralValueType, LiteralValueTypeKind, MemberLookupPolicy, Truthiness, Type, TypeContext, + TypeVarBoundOrConstraints, UnionBuilder, + bool::BoolError, + cyclic::ActiveRecursionDetector, enums::{enum_member_literals, enum_metadata}, }; mod enums; -use self::enums::evaluate_enum_domains; +use self::enums::evaluate_enum_comparison; /// The result of evaluating a runtime comparison between two types. /// @@ -125,85 +128,36 @@ impl<'db> ComparisonResult<'db> { /// constrain `left`. pub(super) fn evaluate_type_equality<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, left: Type<'db>, right: Type<'db>, is_positive: bool, soundness_policy: ComparisonSoundnessPolicy, ) -> Option> { - let right = right.resolve_type_alias(db); - - // Preserve the shared specialization of a constrained TypeVar. Expanding the TypeVar before - // comparing it with `left` would lose the correlation with other occurrences in the function. - if is_positive - && let Type::TypeVar(typevar) = right - && let Some(TypeVarBoundOrConstraints::Constraints(constraints)) = - typevar.typevar(db).bound_or_constraints(db) - && constraints.elements(db).iter().all(|constraint| { - evaluate_type_equality(db, left, *constraint, true, soundness_policy) - .is_some_and(|narrowed| narrowed.is_equivalent_to(db, *constraint)) - }) - { - return Some(right); - } - - let branch = ComparisonBranch::from(is_positive); - let condition_expects_equality = - ComparisonOperator::Equality.condition_expects_equality(branch); - enum_literal_constraint( + evaluate_type_comparison( db, + env, left, right, + is_positive, ComparisonOperator::Equality, - condition_expects_equality, + soundness_policy, ) - .or_else(|| { - builtin_literal_constraint( - db, - left, - right, - ComparisonOperator::Equality, - condition_expects_equality, - ) - }) - .or_else(|| { - evaluate_enum_domains(db, left, right, branch, ComparisonOperator::Equality) - .and_then(|result| result.constraint(branch)) - }) - .or_else(|| { - if comparison_domain( - db, - left, - right, - ComparisonOperator::Equality, - soundness_policy, - ) == ComparisonDomain::Known - { - ComparisonEvaluator::new(db, soundness_policy) - .evaluate(left, right, branch, ComparisonOperator::Equality) - .constraint(branch) - } else { - None - } - }) } /// Return a constraint excluding every value known to compare equal to `ty`. pub(super) fn equality_exclusion_constraint<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, + soundness_policy: ComparisonSoundnessPolicy, ) -> Option> { let ty = ty.resolve_type_alias(db); - builtin_literal_constraint(db, ty, ty, ComparisonOperator::Equality, false) - .or_else(|| ty.is_single_valued(db).then(|| ty.negate(db))) - .or_else(|| { - (ComparisonEvaluator::conservative(db).evaluate( - ty, - ty, - ComparisonBranch::Positive, - ComparisonOperator::Equality, - ) == ComparisonResult::AlwaysTrue) - .then(|| ty.negate(db)) - }) + builtin_literal_constraint(db, env, ty, ty, ComparisonOperator::Equality, false).or_else(|| { + let mut evaluator = ComparisonEvaluator::new(db, env, soundness_policy); + all_values_compare_equal(&mut evaluator, ty, ComparisonOperator::Equality) + .then(|| ty.negate(db, env)) + }) } /// Return a constraint for `left` in a branch where `left != right` has the given truthiness. @@ -228,50 +182,71 @@ pub(super) fn equality_exclusion_constraint<'db>( /// ``` pub(super) fn evaluate_type_inequality<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + left: Type<'db>, + right: Type<'db>, + is_positive: bool, + soundness_policy: ComparisonSoundnessPolicy, +) -> Option> { + evaluate_type_comparison( + db, + env, + left, + right, + is_positive, + ComparisonOperator::Inequality, + soundness_policy, + ) +} + +/// Return a constraint for `left` in the selected branch of an equality or inequality comparison. +fn evaluate_type_comparison<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, left: Type<'db>, right: Type<'db>, is_positive: bool, + operator: ComparisonOperator, soundness_policy: ComparisonSoundnessPolicy, ) -> Option> { let right = right.resolve_type_alias(db); + let branch = ComparisonBranch::from(is_positive); + let condition_expects_equality = operator.condition_expects_equality(branch); - // Preserve the shared specialization of a constrained TypeVar when `left != right` is false. - if !is_positive + // Preserve the shared specialization of a constrained TypeVar. Expanding it before comparing + // with `left` would lose the correlation with other occurrences in the function. + if condition_expects_equality && let Type::TypeVar(typevar) = right && let Some(TypeVarBoundOrConstraints::Constraints(constraints)) = - typevar.typevar(db).bound_or_constraints(db) + typevar.typevar(db).bound_or_constraints(db, env) && constraints.elements(db).iter().all(|constraint| { - evaluate_type_inequality(db, left, *constraint, false, soundness_policy) - .is_some_and(|narrowed| narrowed.is_equivalent_to(db, *constraint)) + evaluate_type_comparison( + db, + env, + left, + *constraint, + is_positive, + operator, + soundness_policy, + ) + .is_some_and(|narrowed| { + equality_truthiness(db, env, narrowed, *constraint, soundness_policy) + == Truthiness::AlwaysTrue + }) }) { return Some(right); } - let branch = ComparisonBranch::from(is_positive); - let condition_expects_equality = - ComparisonOperator::Inequality.condition_expects_equality(branch); - enum_literal_constraint( - db, - left, - right, - ComparisonOperator::Inequality, - condition_expects_equality, - ) - .or_else(|| { - builtin_literal_constraint( - db, - left, - right, - ComparisonOperator::Inequality, - condition_expects_equality, - ) - }) - .or_else(|| { - ComparisonEvaluator::new(db, soundness_policy) - .evaluate(left, right, branch, ComparisonOperator::Inequality) - .constraint(branch) - }) + enum_literal_constraint(db, env, left, right, operator, condition_expects_equality) + .or_else(|| { + builtin_literal_constraint(db, env, left, right, operator, condition_expects_equality) + }) + .or_else(|| { + ComparisonEvaluator::new(db, env, soundness_policy) + .evaluate(left, right, branch, operator) + .constraint(branch) + }) } /// Return the truthiness of `left == right` when it is known for every represented runtime value. @@ -279,12 +254,14 @@ pub(super) fn evaluate_type_inequality<'db>( /// A result that only permits narrowing remains ambiguous because it can still evaluate either way. pub(crate) fn equality_truthiness<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, left: Type<'db>, right: Type<'db>, soundness_policy: ComparisonSoundnessPolicy, ) -> Truthiness { comparison_truthiness( db, + env, left, right, ComparisonOperator::Equality, @@ -297,12 +274,14 @@ pub(crate) fn equality_truthiness<'db>( /// A result that only permits narrowing remains ambiguous because it can still evaluate either way. pub(super) fn inequality_truthiness<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, left: Type<'db>, right: Type<'db>, soundness_policy: ComparisonSoundnessPolicy, ) -> Truthiness { comparison_truthiness( db, + env, left, right, ComparisonOperator::Inequality, @@ -310,14 +289,64 @@ pub(super) fn inequality_truthiness<'db>( ) } +/// Evaluates tuple-element equality while reusing the active-comparison-set allocation across a +/// tuple walk. The set only detects recursive comparisons; results are not cached between +/// elements. +pub(super) struct TupleEqualityEvaluator<'db> { + evaluator: ComparisonEvaluator<'db>, +} + +impl<'db> TupleEqualityEvaluator<'db> { + pub(super) fn new( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + soundness_policy: ComparisonSoundnessPolicy, + ) -> Self { + Self { + evaluator: ComparisonEvaluator::for_truthiness(db, env, soundness_policy), + } + } + + pub(super) fn element_truthiness( + &mut self, + left: Type<'db>, + right: Type<'db>, + ) -> Result> { + let db = self.evaluator.db; + let truthiness = evaluate_tuple_element_equality(&mut self.evaluator, left, right); + if !truthiness.is_ambiguous() { + return Ok(truthiness); + } + + let Some(result) = Type::try_call_rich_comparison_dunder( + db, + &self.evaluator.env, + left, + right, + "__eq__", + "__eq__", + MemberLookupPolicy::default(), + ) else { + return Ok(Truthiness::Ambiguous); + }; + + // Identity can turn a false equality result true, but cannot turn a true result false. + Ok(match result.try_bool(db, &self.evaluator.env)? { + Truthiness::AlwaysTrue => Truthiness::AlwaysTrue, + Truthiness::AlwaysFalse | Truthiness::Ambiguous => Truthiness::Ambiguous, + }) + } +} + fn comparison_truthiness<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, left: Type<'db>, right: Type<'db>, operator: ComparisonOperator, soundness_policy: ComparisonSoundnessPolicy, ) -> Truthiness { - match ComparisonEvaluator::for_truthiness(db, soundness_policy).evaluate( + match ComparisonEvaluator::for_truthiness(db, env, soundness_policy).evaluate( left, right, ComparisonBranch::Positive, @@ -390,28 +419,35 @@ struct ComparisonKey<'db> { /// Tracks comparisons that are already in progress so recursive evaluation terminates. struct ComparisonEvaluator<'db> { db: &'db dyn Db, + env: ProgramEnvironment<'db>, active: FxHashSet>, goal: ComparisonGoal, soundness_policy: ComparisonSoundnessPolicy, } impl<'db> ComparisonEvaluator<'db> { - fn new(db: &'db dyn Db, soundness_policy: ComparisonSoundnessPolicy) -> Self { + fn new( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + soundness_policy: ComparisonSoundnessPolicy, + ) -> Self { Self { db, + env: env.clone(), active: FxHashSet::default(), goal: ComparisonGoal::Constraint, soundness_policy, } } - fn conservative(db: &'db dyn Db) -> Self { - Self::new(db, ComparisonSoundnessPolicy::CONSERVATIVE) - } - - fn for_truthiness(db: &'db dyn Db, soundness_policy: ComparisonSoundnessPolicy) -> Self { + fn for_truthiness( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + soundness_policy: ComparisonSoundnessPolicy, + ) -> Self { Self { db, + env: env.clone(), active: FxHashSet::default(), goal: ComparisonGoal::Truthiness, soundness_policy, @@ -423,7 +459,14 @@ impl<'db> ComparisonEvaluator<'db> { ty: Type<'db>, operator: ComparisonOperator, ) -> Option { - KnownComparisonSemantics::of_type_with_policy(self.db, ty, operator, self.soundness_policy) + let db = self.db; + KnownComparisonSemantics::of_type_with_policy( + db, + &self.env, + ty, + operator, + self.soundness_policy, + ) } /// Evaluate a comparison recursively, treating `left` as the operand being constrained. @@ -453,8 +496,9 @@ impl<'db> ComparisonEvaluator<'db> { branch: ComparisonBranch, operator: ComparisonOperator, ) -> ComparisonResult<'db> { - let left = left.resolve_type_alias(self.db); - let right = right.resolve_type_alias(self.db); + let db = self.db; + let left = left.resolve_type_alias(db); + let right = right.resolve_type_alias(db); let key = ComparisonKey { left, right, @@ -474,7 +518,10 @@ impl<'db> ComparisonEvaluator<'db> { } } -/// Evaluate a comparison whose aliases are resolved and whose key is registered as active. +/// Evaluate one comparison after resolving aliases and checking for recursion. +/// +/// Handle enums and dynamic values such as `Any` before checking individual enum members. +/// Otherwise, checking each member separately can incorrectly narrow `Any`. /// /// Recursive comparisons must use [`ComparisonEvaluator::evaluate`] so cycles are detected. fn evaluate_comparison_once<'db>( @@ -484,18 +531,81 @@ fn evaluate_comparison_once<'db>( branch: ComparisonBranch, operator: ComparisonOperator, ) -> ComparisonResult<'db> { - let db = evaluator.db; + evaluate_enum_comparison(evaluator, left, right, branch, operator) + .or_else(|| evaluate_dynamic_comparison(evaluator, left, right, branch, operator)) + .or_else(|| evaluate_finite_comparison(evaluator, left, right, branch, operator)) + .unwrap_or_else(|| evaluate_structural_comparison(evaluator, left, right, branch, operator)) +} - if let Some(result) = evaluate_enum_domains(db, left, right, branch, operator) { - return result; +/// Handle dynamic values such as `Any` before checking individual enum members. +/// +/// A one-member enum can exclude that member from `Any`. An enum with several members must not +/// exclude all of its members one at a time. +fn evaluate_dynamic_comparison<'db>( + evaluator: &mut ComparisonEvaluator<'db>, + left: Type<'db>, + right: Type<'db>, + branch: ComparisonBranch, + operator: ComparisonOperator, +) -> Option> { + let db = evaluator.db; + let env = evaluator.env.clone(); + match (left, right) { + (Type::Dynamic(_), other) + if !operator.condition_expects_equality(branch) + && all_values_compare_equal(evaluator, other, operator) => + { + let excluded = if other.is_enum(db, &env) + && let Some(alternatives) = finite_alternatives(db, &env, other, operator) + && let [alternative] = alternatives.as_slice() + { + *alternative + } else { + other + }; + Some(ComparisonResult::CanNarrow( + IntersectionBuilder::new(db, &env) + .add_positive(left) + .add_negative(excluded) + .build(), + )) + } + (Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => Some(ComparisonResult::Ambiguous), + _ => None, } +} - if let Some(alternatives) = finite_alternatives(db, left, operator) { - return evaluate_union_left(evaluator, &alternatives, right, branch, operator); - } - if let Some(alternatives) = finite_alternatives(db, right, operator) { - return evaluate_union_right(evaluator, left, &alternatives, branch, operator); - } +/// Compare finite sets of values after handling enums and dynamic values. +/// +/// Start with the side being narrowed so its restrictions are not applied to the other side. +fn evaluate_finite_comparison<'db>( + evaluator: &mut ComparisonEvaluator<'db>, + left: Type<'db>, + right: Type<'db>, + branch: ComparisonBranch, + operator: ComparisonOperator, +) -> Option> { + let db = evaluator.db; + let env = evaluator.env.clone(); + finite_alternatives(db, &env, left, operator) + .map(|alternatives| evaluate_union_left(evaluator, &alternatives, right, branch, operator)) + .or_else(|| { + finite_alternatives(db, &env, right, operator).map(|alternatives| { + evaluate_union_right(evaluator, left, &alternatives, branch, operator) + }) + }) +} + +/// Compare values not handled by the enum, dynamic, or finite-value stages. +fn evaluate_structural_comparison<'db>( + evaluator: &mut ComparisonEvaluator<'db>, + left: Type<'db>, + right: Type<'db>, + branch: ComparisonBranch, + operator: ComparisonOperator, +) -> ComparisonResult<'db> { + let db = evaluator.db; + let env = evaluator.env.clone(); match (left, right) { ( @@ -526,7 +636,7 @@ fn evaluate_comparison_once<'db>( && all_values_compare_equal(evaluator, other, operator) { ComparisonResult::CanNarrow( - IntersectionBuilder::new(db) + IntersectionBuilder::new(db, &env) .add_positive(left) .add_negative(other) .build(), @@ -537,24 +647,36 @@ fn evaluate_comparison_once<'db>( } (_, Type::Dynamic(_)) => ComparisonResult::Ambiguous, - (Type::TypeVar(var), other) => match var.typevar(db).bound_or_constraints(db) { + // A constrained TypeVar selects one constraint for the entire specialization, so each + // alternative can be checked independently without losing that correlation. + (Type::TypeVar(left_var), Type::TypeVar(right_var)) + if left_var.is_same_typevar_as(db, right_var) + && let Some(TypeVarBoundOrConstraints::Constraints(constraints)) = + left_var.typevar(db).bound_or_constraints(db, &env) + && constraints.elements(db).iter().all(|constraint| { + all_values_compare_equal(evaluator, *constraint, operator) + }) => + { + operator.result_from_equality(true) + } + (Type::TypeVar(var), other) => match var.typevar(db).bound_or_constraints(db, &env) { None => ComparisonResult::Ambiguous, Some(TypeVarBoundOrConstraints::UpperBound(_)) => { if !operator.condition_expects_equality(branch) && all_values_compare_equal(evaluator, other, operator) { - ComparisonResult::CanNarrow(other.negate(db)) + ComparisonResult::CanNarrow(other.negate(db, &env)) } else { ComparisonResult::Ambiguous } } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - evaluator.evaluate(constraints.as_type(db), other, branch, operator) + evaluator.evaluate(constraints.as_type(db, &env), other, branch, operator) } }, - (other, Type::TypeVar(var)) => match var.typevar(db).bound_or_constraints(db) { + (other, Type::TypeVar(var)) => match var.typevar(db).bound_or_constraints(db, &env) { Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - evaluator.evaluate(other, constraints.as_type(db), branch, operator) + evaluator.evaluate(other, constraints.as_type(db, &env), branch, operator) } None | Some(TypeVarBoundOrConstraints::UpperBound(_)) => ComparisonResult::Ambiguous, }, @@ -572,6 +694,23 @@ fn evaluate_comparison_once<'db>( (other, Type::Union(union)) => { evaluate_union_right(evaluator, other, union.elements(db), branch, operator) } + // An excluded string literal rules out its runtime value only when the intersection + // already proves that the string has literal origin. + (Type::Intersection(intersection), Type::LiteralValue(literal)) + | (Type::LiteralValue(literal), Type::Intersection(intersection)) + if literal.is_string() + && intersection + .positive(db) + .iter() + .any(|element| element.is_subtype_of(db, &env, Type::literal_string())) + && Type::Intersection(intersection).is_disjoint_from( + db, + &env, + Type::LiteralValue(literal), + ) => + { + operator.result_from_equality(false) + } (Type::Intersection(intersection), other) => evaluate_intersection_left( evaluator, Type::Intersection(intersection), @@ -582,10 +721,17 @@ fn evaluate_comparison_once<'db>( ), (Type::LiteralValue(left_literal), Type::LiteralValue(right_literal)) => { - match known_literal_equality(db, left_literal.kind(), right_literal.kind(), operator) { + match known_literal_equality( + db, + &env, + left_literal.kind(), + right_literal.kind(), + operator, + ) { Some(equal) => operator.result_from_equality(equal), None => narrow_literal_comparison( db, + &env, left, right, left_literal.kind(), @@ -625,11 +771,6 @@ fn evaluate_comparison_once<'db>( (Type::ModuleLiteral(left_module), Type::ModuleLiteral(right_module)) => { operator.result_from_equality(left_module.module(db) == right_module.module(db)) } - (Type::GenericAlias(left_alias), Type::GenericAlias(right_alias)) - if left_alias == right_alias => - { - operator.result_from_equality(true) - } (Type::WrapperDescriptor(left_descriptor), Type::WrapperDescriptor(right_descriptor)) if left_descriptor == right_descriptor => { @@ -643,16 +784,9 @@ fn evaluate_comparison_once<'db>( Type::KnownBoundMethod(KnownBoundMethodType::FunctionTypeDunderCall(left_function)), Type::KnownBoundMethod(KnownBoundMethodType::FunctionTypeDunderCall(right_function)), ) if left_function == right_function => operator.result_from_equality(true), - (Type::KnownInstance(left_instance), Type::KnownInstance(right_instance)) - if left_instance == right_instance - && left.is_single_valued(db) - && operator == ComparisonOperator::Equality => - { - ComparisonResult::AlwaysTrue - } (left, right) - if has_known_identity_comparison_semantics(db, left, operator) - && has_known_identity_comparison_semantics(db, right, operator) => + if has_known_identity_comparison_semantics(db, &env, left, operator) + && has_known_identity_comparison_semantics(db, &env, right, operator) => { operator.result_from_equality(left == right) } @@ -661,6 +795,15 @@ fn evaluate_comparison_once<'db>( compare_nominal_instances(evaluator, left_instance, right_instance, operator) } + (left, right) + if left.is_singleton(db, &env) + && left.is_equivalent_to(db, &env, right) + && KnownComparisonSemantics::of_type(db, &env, left, operator) + == Some(KnownComparisonSemantics::Object) => + { + operator.result_from_equality(true) + } + _ => ComparisonResult::Ambiguous, } } @@ -718,6 +861,7 @@ fn is_builtin_literal_type(db: &dyn Db, ty: Type) -> bool { /// both `Literal[0]` and `Literal[False]`, while `x != 1` excludes `Literal[1]` and `Literal[True]`. fn builtin_literal_constraint<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, left: Type<'db>, right: Type<'db>, operator: ComparisonOperator, @@ -727,17 +871,19 @@ fn builtin_literal_constraint<'db>( return None; }; - let equal_to_right = builtin_literals_equal_to(db, Type::LiteralValue(right), right.kind())?; + let equal_to_right = + builtin_literals_equal_to(db, env, Type::LiteralValue(right), right.kind())?; if !condition_expects_equality { let equal_to_right = add_equal_enum_literals( db, + env, left, right.kind(), operator, - UnionBuilder::new(db).add(equal_to_right), + UnionBuilder::new(db, env).add(equal_to_right), ); - return Some(equal_to_right.build().negate(db)); + return Some(equal_to_right.build().negate(db, env)); } match left.resolve_type_alias(db) { @@ -754,22 +900,23 @@ fn builtin_literal_constraint<'db>( /// Return the builtin literal values that compare equal to `literal_type`. fn builtin_literals_equal_to<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, literal_type: Type<'db>, literal: LiteralValueTypeKind<'db>, ) -> Option> { let builder = match literal { LiteralValueTypeKind::Int(value) => { - let mut builder = UnionBuilder::new(db).add(literal_type); + let mut builder = UnionBuilder::new(db, env).add(literal_type); if matches!(value.as_i64(), 0 | 1) { builder = builder.add(Type::bool_literal(value.as_i64() == 1)); } builder } - LiteralValueTypeKind::Bool(value) => UnionBuilder::new(db) + LiteralValueTypeKind::Bool(value) => UnionBuilder::new(db, env) .add(literal_type) .add(Type::int_literal(i64::from(value))), LiteralValueTypeKind::String(_) | LiteralValueTypeKind::Bytes(_) => { - UnionBuilder::new(db).add(literal_type) + UnionBuilder::new(db, env).add(literal_type) } LiteralValueTypeKind::LiteralString | LiteralValueTypeKind::Enum(_) => return None, // basedpython float/complex literals: equality spans int/float/complex @@ -782,6 +929,7 @@ fn builtin_literals_equal_to<'db>( /// Add finite enum members in `ty` that are known to compare equal to `right`. fn add_equal_enum_literals<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, right: LiteralValueTypeKind<'db>, operator: ComparisonOperator, @@ -790,19 +938,19 @@ fn add_equal_enum_literals<'db>( match ty.resolve_type_alias(db) { Type::Union(union) => { for element in union.elements(db) { - builder = add_equal_enum_literals(db, *element, right, operator, builder); + builder = add_equal_enum_literals(db, env, *element, right, operator, builder); } } Type::LiteralValue(literal) => { if matches!(literal.kind(), LiteralValueTypeKind::Enum(_)) - && known_literal_equality(db, literal.kind(), right, operator) == Some(true) + && known_literal_equality(db, env, literal.kind(), right, operator) == Some(true) { builder = builder.add(Type::LiteralValue(literal)); } } - ty if let Some(alternatives) = finite_alternatives(db, ty, operator) => { + ty if let Some(alternatives) = finite_alternatives(db, env, ty, operator) => { for alternative in alternatives { - builder = add_equal_enum_literals(db, alternative, right, operator, builder); + builder = add_equal_enum_literals(db, env, alternative, right, operator, builder); } } _ => {} @@ -832,6 +980,7 @@ fn add_equal_enum_literals<'db>( /// because those methods can change whether two members compare equal. fn enum_literal_constraint<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, left: Type<'db>, right: Type<'db>, operator: ComparisonOperator, @@ -843,9 +992,14 @@ fn enum_literal_constraint<'db>( let LiteralValueTypeKind::Enum(right) = right_literal.kind() else { return None; }; - if !is_same_enum_domain(db, left, right) - || KnownComparisonSemantics::of_instance(db, right.enum_class_instance(db), operator) - .is_none() + if !is_same_enum_domain(db, env, left, right) + || KnownComparisonSemantics::of_instance( + db, + env, + right.enum_class_instance(db, env), + operator, + ) + .is_none() { return None; } @@ -856,32 +1010,70 @@ fn enum_literal_constraint<'db>( EnumLiteralType::new(db, enum_class_literal, name), right_literal.is_promotable(), )); - Some(equal_to_right.negate_if(db, !condition_expects_equality)) + Some(equal_to_right.negate_if(db, env, !condition_expects_equality)) } /// Return whether every possible value of `ty` belongs to the same enum as `right`. pub(super) fn is_same_enum_domain<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, right: EnumLiteralType<'db>, ) -> bool { - match ty.resolve_type_alias(db) { - Type::LiteralValue(literal) => matches!( - literal.kind(), - LiteralValueTypeKind::Enum(left) - if left.enum_class(db) == right.enum_class(db) - ), - Type::Union(union) => union - .elements(db) - .iter() - .all(|element| is_same_enum_domain(db, *element, right)), - Type::NominalInstance(instance) => instance.class_literal(db) == right.enum_class(db), - Type::EnumComplement(complement) => complement.enum_class(db) == right.enum_class(db), - Type::Intersection(intersection) => intersection - .enum_complement(db) - .is_some_and(|complement| complement.enum_class(db) == right.enum_class(db)), - _ => false, + // A proof made while another alias is active can still be disproved by a later union arm, so + // completed visits must not be cached. + #[derive(Default)] + struct EnumDomainVisitor<'db> { + active_specializations: ActiveRecursionDetector>, + active_definitions: ActiveRecursionDetector>, + } + + fn visit<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + right: EnumLiteralType<'db>, + visitor: &EnumDomainVisitor<'db>, + ) -> bool { + match ty { + // The same specialization preserves the domain; different arguments can introduce + // values outside it even when the alias definition is the same. + Type::TypeAlias(alias) => visitor.active_specializations.visit( + &ty, + || true, + || { + visitor.active_definitions.visit( + &alias.definition(db), + || false, + || visit(db, env, alias.value_type(db), right, visitor), + ) + }, + ), + Type::LiteralValue(literal) => matches!( + literal.kind(), + LiteralValueTypeKind::Enum(left) + if left.enum_class(db) == right.enum_class(db) + ), + Type::Union(union) => union + .elements(db) + .iter() + .all(|&element| visit(db, env, element, right, visitor)), + Type::NewTypeInstance(newtype) => { + visit(db, env, newtype.concrete_base_type(db), right, visitor) + } + Type::NominalInstance(instance) => { + instance.class_literal(db, env) == right.enum_class(db) + } + Type::EnumComplement(complement) => complement.enum_class(db) == right.enum_class(db), + Type::Intersection(intersection) => intersection + .positive(db) + .iter() + .any(|&element| visit(db, env, element, right, visitor)), + _ => false, + } } + + visit(db, env, ty, right, &EnumDomainVisitor::default()) } /// Evaluate each alternative of the union being constrained and combine their branch results. @@ -892,6 +1084,7 @@ fn evaluate_union_left<'db>( branch: ComparisonBranch, operator: ComparisonOperator, ) -> ComparisonResult<'db> { + let db = evaluator.db; if evaluator.goal == ComparisonGoal::Truthiness { return combine_definite_truthiness( elements @@ -900,8 +1093,8 @@ fn evaluate_union_left<'db>( ); } - let db = evaluator.db; - evaluate_target_union(db, elements, branch, |element| { + let env = evaluator.env.clone(); + evaluate_target_union(db, &env, elements, branch, |element| { evaluator.evaluate(element, other, branch, operator) }) } @@ -912,6 +1105,7 @@ fn evaluate_union_left<'db>( /// negative constraints for removed arms so that the result still describes the branch predicate. fn evaluate_target_union<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, elements: &[Type<'db>], branch: ComparisonBranch, mut evaluate: impl FnMut(Type<'db>) -> ComparisonResult<'db>, @@ -923,7 +1117,7 @@ fn evaluate_target_union<'db>( let mut all_true = true; let mut all_false = true; let mut narrowed = Vec::with_capacity(elements.len()); - let mut removed = UnionBuilder::new(db); + let mut removed = UnionBuilder::new(db, env); let mut removed_any = false; for element in elements { @@ -969,13 +1163,13 @@ fn evaluate_target_union<'db>( } let removed = removed_any.then(|| removed.build()); - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); for narrowed in narrowed { let Some(mut narrowed) = narrowed else { continue; }; if let Some(removed) = removed { - narrowed = IntersectionBuilder::new(db) + narrowed = IntersectionBuilder::new(db, env) .add_positive(narrowed) .add_negative(removed) .build(); @@ -993,6 +1187,7 @@ fn evaluate_union_right<'db>( branch: ComparisonBranch, operator: ComparisonOperator, ) -> ComparisonResult<'db> { + let db = evaluator.db; if evaluator.goal == ComparisonGoal::Truthiness { return combine_definite_truthiness( elements @@ -1001,9 +1196,10 @@ fn evaluate_union_right<'db>( ); } - let db = evaluator.db; + let env = evaluator.env.clone(); evaluate_against_results( db, + &env, left, branch, elements @@ -1046,13 +1242,14 @@ fn combine_definite_truthiness<'db>( /// truthiness is reported only when every alternative agrees. fn evaluate_against_results<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, target: Type<'db>, branch: ComparisonBranch, results: impl IntoIterator>, ) -> ComparisonResult<'db> { let mut all_true = true; let mut all_false = true; - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); let mut any = false; for result in results { @@ -1103,6 +1300,7 @@ fn evaluate_intersection_left<'db>( branch: ComparisonBranch, operator: ComparisonOperator, ) -> ComparisonResult<'db> { + let db = evaluator.db; if evaluator.goal == ComparisonGoal::Truthiness { return combine_definite_truthiness( positive @@ -1111,20 +1309,31 @@ fn evaluate_intersection_left<'db>( ); } - let db = evaluator.db; let mut any_true = false; let mut any_false = false; let mut any_ambiguous = false; let mut any_narrowing = false; - let mut builder = IntersectionBuilder::new(db).add_positive(original); + let mut builder = IntersectionBuilder::new(db, &evaluator.env).add_positive(original); for element in positive { match evaluator.evaluate(*element, other, branch, operator) { ComparisonResult::AlwaysTrue => any_true = true, ComparisonResult::AlwaysFalse => any_false = true, ComparisonResult::CanNarrow(narrowed) => { + // Literal-string origin is a static proof, not a runtime object property. An + // untrusted string can therefore equal a literal even when their static types + // are disjoint. Keep its original proof instead of making that branch unreachable. + if operator.condition_expects_equality(branch) + && original.is_disjoint_from(db, &evaluator.env, narrowed) + && original + .identity_comparison_truthiness(db, &evaluator.env, narrowed) + .may_be_true() + { + return ComparisonResult::Ambiguous; + } + any_narrowing = true; - builder = builder.add_positive(narrowed); + builder.add_positive_in_place(narrowed); } ComparisonResult::Ambiguous => any_ambiguous = true, } @@ -1148,26 +1357,60 @@ fn evaluate_intersection_left<'db>( /// may compare equal to values outside the enum domain. fn finite_alternatives<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, operator: ComparisonOperator, ) -> Option>> { match ty { - Type::EnumComplement(complement) => KnownComparisonSemantics::of_type(db, ty, operator) - .is_some() - .then(|| complement.remaining_literal_types(db)), + Type::EnumComplement(complement) => { + KnownComparisonSemantics::of_type(db, env, ty, operator) + .is_some() + .then(|| complement.remaining_literal_types(db, env)) + } Type::Intersection(intersection) => { - let complement = intersection.enum_complement(db)?; - KnownComparisonSemantics::of_type(db, ty, operator) + let (comparison_type, complement) = if let Some(complement) = + intersection.enum_complement(db, env) + { + (ty, complement) + } else { + if !intersection.positive(db).iter().any(|positive| { + matches!(positive.resolve_type_alias(db), Type::NewTypeInstance(_)) + }) { + return None; + } + + let expanded = intersection.with_expanded_typevars_and_newtypes(db, env); + let complement = match expanded { + Type::LiteralValue(literal) if literal.is_enum() => { + return KnownComparisonSemantics::of_type(db, env, expanded, operator) + .is_some() + .then(|| vec![expanded]); + } + Type::EnumComplement(complement) => complement, + Type::Intersection(intersection) => intersection.enum_complement(db, env)?, + _ => return None, + }; + (expanded, complement) + }; + KnownComparisonSemantics::of_type(db, env, comparison_type, operator) .is_some() - .then(|| complement.remaining_literal_types(db)) + .then(|| complement.remaining_literal_types(db, env)) + } + Type::NewTypeInstance(newtype) => { + let base = newtype.concrete_base_type(db); + if base.is_enum(db, env) { + finite_alternatives(db, env, base, operator) + } else { + None + } } Type::NominalInstance(instance) if instance.has_known_class(db, KnownClass::Bool) => { Some(vec![Type::bool_literal(true), Type::bool_literal(false)]) } Type::NominalInstance(instance) - if KnownComparisonSemantics::of_type(db, ty, operator).is_some() => + if KnownComparisonSemantics::of_type(db, env, ty, operator).is_some() => { - enum_member_literals(db, instance.class_literal(db), None).map(Iterator::collect) + enum_member_literals(db, instance.class_literal(db, env), None).map(Iterator::collect) } _ => None, } @@ -1179,6 +1422,7 @@ fn finite_alternatives<'db>( /// or a string-valued enum member without having a single statically known runtime value. fn narrow_literal_comparison<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, left: Type<'db>, right: Type<'db>, left_literal: LiteralValueTypeKind<'db>, @@ -1187,16 +1431,16 @@ fn narrow_literal_comparison<'db>( ) -> ComparisonResult<'db> { match (left_literal, right_literal) { (LiteralValueTypeKind::LiteralString, LiteralValueTypeKind::String(_)) => { - ComparisonResult::CanNarrow(right.negate_if(db, !equality_is_positive)) + ComparisonResult::CanNarrow(right.negate_if(db, env, !equality_is_positive)) } (LiteralValueTypeKind::String(_), LiteralValueTypeKind::LiteralString) => { - ComparisonResult::CanNarrow(left.negate_if(db, !equality_is_positive)) + ComparisonResult::CanNarrow(left.negate_if(db, env, !equality_is_positive)) } (LiteralValueTypeKind::LiteralString, LiteralValueTypeKind::Enum(enum_literal)) => { - narrow_literal_string_against_enum(db, enum_literal, equality_is_positive) + narrow_literal_string_against_enum(db, env, enum_literal, equality_is_positive) } (LiteralValueTypeKind::Enum(enum_literal), LiteralValueTypeKind::LiteralString) => { - narrow_literal_string_against_enum(db, enum_literal, equality_is_positive) + narrow_literal_string_against_enum(db, env, enum_literal, equality_is_positive) } _ => ComparisonResult::Ambiguous, } @@ -1205,28 +1449,30 @@ fn narrow_literal_comparison<'db>( /// Narrow `LiteralString` against a string-valued enum member with inherited `str` semantics. fn narrow_literal_string_against_enum<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, enum_literal: EnumLiteralType<'db>, equality_is_positive: bool, ) -> ComparisonResult<'db> { if KnownComparisonSemantics::of_type( db, + env, Type::enum_literal(enum_literal), ComparisonOperator::Equality, ) != Some(KnownComparisonSemantics::Str) { return ComparisonResult::Ambiguous; } - let Some(value @ Type::LiteralValue(_)) = enum_literal_value(db, enum_literal) else { + let Some(value @ Type::LiteralValue(_)) = enum_literal_value(db, env, enum_literal) else { return ComparisonResult::Ambiguous; }; let Some(LiteralValueTypeKind::String(_)) = value.as_literal_value_kind() else { return ComparisonResult::Ambiguous; }; - let narrowed = UnionBuilder::new(db) + let narrowed = UnionBuilder::new(db, env) .add(value) .add(Type::enum_literal(enum_literal)) .build() - .negate_if(db, !equality_is_positive); + .negate_if(db, env, !equality_is_positive); ComparisonResult::CanNarrow(narrowed) } @@ -1261,6 +1507,7 @@ fn compare_literal_to_other<'db>( literal_operand: LiteralOperand, ) -> ComparisonResult<'db> { let db = evaluator.db; + let env = evaluator.env.clone(); if matches!(literal, LiteralValueTypeKind::LiteralString) { return match evaluator.comparison_semantics(other, operator) { @@ -1270,7 +1517,7 @@ fn compare_literal_to_other<'db>( }; } - let Some(literal_semantics) = KnownComparisonSemantics::of_literal(db, literal, operator) + let Some(literal_semantics) = KnownComparisonSemantics::of_literal(db, &env, literal, operator) else { return ComparisonResult::Ambiguous; }; @@ -1282,7 +1529,7 @@ fn compare_literal_to_other<'db>( if evaluator.soundness_policy.allow_unsafe_equality && condition_expects_equality && literal_operand == LiteralOperand::Other - && let Some(equal_to_literal) = builtin_literals_equal_to(db, literal_type, literal) + && let Some(equal_to_literal) = builtin_literals_equal_to(db, &env, literal_type, literal) && let Some(other_semantics) = unsafe_narrowable_builtin_semantics(db, other) { return if literal_semantics == other_semantics { @@ -1296,9 +1543,12 @@ fn compare_literal_to_other<'db>( Some(other_semantics) if literal_semantics != other_semantics => { ComparisonResult::from_bool(operator == ComparisonOperator::Inequality) } + // Object equality compares identity. `NewType` operands are evaluated using their concrete + // base before reaching this arm, so erased identities cannot make these types appear + // disjoint here. Some(KnownComparisonSemantics::Object) if literal_semantics == KnownComparisonSemantics::Object - && other.is_disjoint_from(db, literal_type) => + && other.is_disjoint_from(db, &env, literal_type) => { ComparisonResult::from_bool(operator == ComparisonOperator::Inequality) } @@ -1306,17 +1556,17 @@ fn compare_literal_to_other<'db>( // `int` subclass can compare equal to `1` despite being disjoint from `Literal[1]`. Some(_) if literal_operand == LiteralOperand::Other - && literal_type.is_single_valued(db) - && !other.is_disjoint_from(db, literal_type) => + && !other.is_disjoint_from(db, &env, literal_type) => { - ComparisonResult::CanNarrow(literal_type.negate_if(db, !condition_expects_equality)) + ComparisonResult::CanNarrow(literal_type.negate_if( + db, + &env, + !condition_expects_equality, + )) } Some(_) => ComparisonResult::Ambiguous, - None if literal_operand == LiteralOperand::Other - && !condition_expects_equality - && literal_type.is_single_valued(db) => - { - ComparisonResult::CanNarrow(literal_type.negate(db)) + None if literal_operand == LiteralOperand::Other && !condition_expects_equality => { + ComparisonResult::CanNarrow(literal_type.negate(db, &env)) } None => ComparisonResult::Ambiguous, } @@ -1325,14 +1575,15 @@ fn compare_literal_to_other<'db>( /// Compare nominal instances when their inherited comparison implementations are known. /// /// The result is definite only when the implementations cannot compare equal, or when both types -/// denote the same singleton. +/// denote the same singleton, or when their fixed tuple elements have a definite comparison. fn compare_nominal_instances<'db>( - evaluator: &ComparisonEvaluator<'db>, + evaluator: &mut ComparisonEvaluator<'db>, left_instance: super::NominalInstanceType<'db>, right_instance: super::NominalInstanceType<'db>, operator: ComparisonOperator, ) -> ComparisonResult<'db> { let db = evaluator.db; + let env = &evaluator.env; let left = Type::NominalInstance(left_instance); let right = Type::NominalInstance(right_instance); let Some(left_semantics) = evaluator.comparison_semantics(left, operator) else { @@ -1343,18 +1594,76 @@ fn compare_nominal_instances<'db>( }; if left_semantics != right_semantics - || (left_semantics == KnownComparisonSemantics::Object && left.is_disjoint_from(db, right)) + || (left_semantics == KnownComparisonSemantics::Object + && left.is_disjoint_from(db, env, right)) { return ComparisonResult::from_bool(operator == ComparisonOperator::Inequality); } - if left == right && left.is_singleton(db) { + if left == right && left.is_singleton(db, env) { ComparisonResult::from_bool(operator == ComparisonOperator::Equality) + } else if left_semantics == KnownComparisonSemantics::Tuple + && let Some(left_tuple) = left_instance.tuple_spec(db, env) + && let Some(right_tuple) = right_instance.tuple_spec(db, env) + && let Some(left_tuple) = left_tuple.as_fixed_length() + && let Some(right_tuple) = right_tuple.as_fixed_length() + { + let left_elements = left_tuple.all_elements(); + let right_elements = right_tuple.all_elements(); + if left_elements.len() != right_elements.len() { + return operator.result_from_equality(false); + } + + let mut all_equal = true; + for (&left, &right) in left_elements.iter().zip(right_elements) { + match evaluate_tuple_element_equality(evaluator, left, right) { + Truthiness::AlwaysTrue => {} + Truthiness::AlwaysFalse => return operator.result_from_equality(false), + Truthiness::Ambiguous => all_equal = false, + } + } + + if all_equal { + operator.result_from_equality(true) + } else { + ComparisonResult::Ambiguous + } } else { ComparisonResult::Ambiguous } } +fn evaluate_tuple_element_equality<'db>( + evaluator: &mut ComparisonEvaluator<'db>, + left: Type<'db>, + right: Type<'db>, +) -> Truthiness { + let db = evaluator.db; + if left == right && left.is_singleton(db, &evaluator.env) { + return Truthiness::AlwaysTrue; + } + + match evaluator.evaluate( + left, + right, + ComparisonBranch::Positive, + ComparisonOperator::Equality, + ) { + ComparisonResult::AlwaysTrue => Truthiness::AlwaysTrue, + // Known comparison semantics are reflexive, so a false result rules out shared runtime + // identity. Static disjointness alone is insufficient because `NewType` and similar + // wrappers can erase their distinction at runtime. + ComparisonResult::AlwaysFalse + if [left, right] + .into_iter() + .all(|ty| has_reflexive_equality_semantics(evaluator, ty)) => + { + Truthiness::AlwaysFalse + } + _ => Truthiness::Ambiguous, + } +} + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] enum ComparisonOperator { Equality, @@ -1404,35 +1713,48 @@ impl KnownComparisonSemantics { /// Determine the builtin comparison implementation inherited by `ty`. /// /// Returns `None` when dunder lookup finds custom or conflicting comparison behavior. - fn of_type<'db>(db: &'db dyn Db, ty: Type<'db>, operator: ComparisonOperator) -> Option { - Self::of_type_with_policy(db, ty, operator, ComparisonSoundnessPolicy::CONSERVATIVE) + fn of_type<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + operator: ComparisonOperator, + ) -> Option { + Self::of_type_with_policy( + db, + env, + ty, + operator, + ComparisonSoundnessPolicy::CONSERVATIVE, + ) } /// Determine comparison semantics, optionally assuming that subclasses do not override the /// inherited comparison method. fn of_type_with_policy<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, operator: ComparisonOperator, soundness_policy: ComparisonSoundnessPolicy, ) -> Option { match ty { - Type::LiteralValue(literal) => Self::of_literal(db, literal.kind(), operator), + Type::LiteralValue(literal) => Self::of_literal(db, env, literal.kind(), operator), Type::TypedDict(_) => Some(Self::Dict), Type::EnumComplement(complement) => Self::of_instance( db, - complement.enum_class(db).to_non_generic_instance(db), + env, + complement.enum_class(db).to_non_generic_instance(db, env), operator, ), Type::Intersection(intersection) - if let Some(complement) = intersection.enum_complement(db) => + if let Some(complement) = intersection.enum_complement(db, env) => { - let instance = complement.enum_class(db).to_non_generic_instance(db); - Self::of_instance(db, instance, operator) + let instance = complement.enum_class(db).to_non_generic_instance(db, env); + Self::of_instance(db, env, instance, operator) } Type::Intersection(intersection) => { let mut semantics = intersection.positive(db).iter().map(|element| { - Self::of_type_with_policy(db, *element, operator, soundness_policy) + Self::of_type_with_policy(db, env, *element, operator, soundness_policy) }); let first = semantics.next().flatten()?; semantics @@ -1440,14 +1762,29 @@ impl KnownComparisonSemantics { .then_some(first) } Type::NominalInstance(instance) - if instance.class(db).is_final(db) + if instance.class(db, env).is_final(db) || soundness_policy.allow_unsafe_equality - // `object` can contain values whose classes define their own comparison - // method, so treating it as exact would incorrectly eliminate those values. - && !instance.has_known_class(db, KnownClass::Object) => + && ( + // `object` can contain values whose classes define their own comparison + // method, so treating it as exact would incorrectly eliminate those values. + !instance.has_known_class(db, KnownClass::Object) + ) => { - Self::of_instance(db, ty, operator) + Self::of_instance(db, env, ty, operator) } + Type::SpecialForm(special_form) => KnownComparisonSemantics::of_type_with_policy( + db, + env, + special_form.instance_fallback(db, env), + operator, + soundness_policy, + ), + Type::KnownInstance(instance) => KnownComparisonSemantics::of_instance( + db, + env, + instance.instance_fallback(db, env), + operator, + ), _ => None, } } @@ -1455,6 +1792,7 @@ impl KnownComparisonSemantics { /// Return the builtin comparison implementation used by a literal value. fn of_literal<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, literal: LiteralValueTypeKind<'db>, operator: ComparisonOperator, ) -> Option { @@ -1465,7 +1803,7 @@ impl KnownComparisonSemantics { } LiteralValueTypeKind::Bytes(_) => Some(Self::Bytes), LiteralValueTypeKind::Enum(enum_literal) => { - Self::of_instance(db, enum_literal.enum_class_instance(db), operator) + Self::of_instance(db, env, enum_literal.enum_class_instance(db, env), operator) } // basedpython float/complex literals: their equality crosses numeric // types and NaN breaks reflexivity, so no builtin comparison shape @@ -1479,17 +1817,31 @@ impl KnownComparisonSemantics { /// Returns `None` when lookup finds custom comparison behavior. fn of_instance<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, instance: Type<'db>, operator: ComparisonOperator, ) -> Option { - let class = instance.to_meta_type(db); - let dunder = lookup_dunder(db, class, operator.dunder()); + instance.nominal_class(db, env)?; + let class = instance.to_meta_type(db, env); + let dunder = lookup_dunder(db, env, class, operator.dunder()); if dunder.place.is_undefined() { - if operator == ComparisonOperator::Inequality - && !lookup_dunder(db, class, "__eq__").place.is_undefined() - { - return None; + if operator == ComparisonOperator::Inequality { + let equality = lookup_dunder(db, env, class, "__eq__"); + // `tuple.__ne__` delegates to its builtin equality implementation. + if equality + == lookup_dunder( + db, + env, + KnownClass::Tuple.to_class_literal(db, env), + "__eq__", + ) + { + return Some(Self::Tuple); + } + if !equality.place.is_undefined() { + return None; + } } return Some(Self::Object); } @@ -1501,7 +1853,14 @@ impl KnownComparisonSemantics { (KnownClass::Tuple, Self::Tuple), (KnownClass::Dict, Self::Dict), ] { - if dunder == lookup_dunder(db, known_class.to_class_literal(db), operator.dunder()) { + if dunder + == lookup_dunder( + db, + env, + known_class.to_class_literal(db, env), + operator.dunder(), + ) + { return Some(semantics); } } @@ -1509,84 +1868,36 @@ impl KnownComparisonSemantics { } } -/// Whether the non-target operand has a comparison domain that can safely constrain the target. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -enum ComparisonDomain { - /// The operand may use comparison behavior that `ty` does not model. - Unknown, - /// The operand can be handled by `ty`'s equality-narrowing evaluator. - Known, -} - -/// Classify whether `ty` has comparison behavior that can constrain `target`. -/// -/// Unions only have a known domain if every arm does. Broad nominal types require full dunder -/// analysis, which is only useful here when it can eliminate an arm from a union target. -fn comparison_domain<'db>( - db: &'db dyn Db, - target: Type<'db>, +/// Return whether equality on `ty` is reflexive and therefore rules out shared identity when false. +fn has_reflexive_equality_semantics<'db>( + evaluator: &ComparisonEvaluator<'db>, ty: Type<'db>, - operator: ComparisonOperator, - soundness_policy: ComparisonSoundnessPolicy, -) -> ComparisonDomain { - let target = target.resolve_type_alias(db); - let ty = ty.resolve_type_alias(db); - - match ty { - Type::Union(union) => { - if union.elements(db).iter().all(|element| { - comparison_domain(db, target, *element, operator, soundness_policy) - == ComparisonDomain::Known - }) { - ComparisonDomain::Known - } else { - ComparisonDomain::Unknown - } - } - Type::LiteralValue(_) | Type::EnumComplement(_) | Type::TypedDict(_) => { - ComparisonDomain::Known - } - Type::Intersection(intersection) if intersection.enum_complement(db).is_some() => { - ComparisonDomain::Known - } - Type::NominalInstance(instance) => { - if instance.tuple_spec(db).is_some() - || ty.is_singleton(db) - || instance.has_known_class(db, KnownClass::Bool) - || target.is_union() - && KnownComparisonSemantics::of_type_with_policy( - db, - ty, - operator, - soundness_policy, - ) - .is_some() - { - ComparisonDomain::Known - } else { - ComparisonDomain::Unknown - } - } - _ if ty.is_single_valued(db) => ComparisonDomain::Known, - _ => ComparisonDomain::Unknown, - } +) -> bool { + evaluator + .comparison_semantics(ty, ComparisonOperator::Equality) + .is_some() } /// Return whether `ty` is a singleton whose comparison uses object identity semantics. fn has_known_identity_comparison_semantics<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, operator: ComparisonOperator, ) -> bool { match ty { - Type::FunctionLiteral(_) | Type::ModuleLiteral(_) | Type::SpecialForm(_) => true, + Type::FunctionLiteral(_) | Type::ModuleLiteral(_) => true, Type::ClassLiteral(class) => { - KnownComparisonSemantics::of_instance(db, class.metaclass_instance_type(db), operator) - == Some(KnownComparisonSemantics::Object) + KnownComparisonSemantics::of_instance( + db, + env, + class.metaclass_instance_type(db, env), + operator, + ) == Some(KnownComparisonSemantics::Object) } _ => { - ty.is_singleton(db) - && KnownComparisonSemantics::of_type(db, ty, operator) + ty.is_singleton(db, env) + && KnownComparisonSemantics::of_type(db, env, ty, operator) == Some(KnownComparisonSemantics::Object) } } @@ -1595,22 +1906,52 @@ fn has_known_identity_comparison_semantics<'db>( /// Look up a comparison method without falling back to `object`. fn lookup_dunder<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, name: &'static str, ) -> PlaceAndQualifiers<'db> { - ty.member_lookup_with_policy(db, name, MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK) + ty.member_lookup_with_policy(db, env, name, MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK) } /// Return the comparison result for two literals when their runtime values determine it. /// -/// This accounts for integer/boolean equality and enum aliases or enum values. `None` means custom -/// or insufficiently known comparison behavior prevents a definitive result. +/// This accounts for integer/boolean equality, enum aliases or enum values, and reflexive custom +/// enum comparison methods with a definite return type. `None` means comparison behavior is +/// insufficiently known to produce a definitive result. fn known_literal_equality<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, left: LiteralValueTypeKind<'db>, right: LiteralValueTypeKind<'db>, operator: ComparisonOperator, ) -> Option { + if let (LiteralValueTypeKind::Enum(left_enum), LiteralValueTypeKind::Enum(right_enum)) = + (left, right) + && same_enum_member(db, left_enum, right_enum) + && KnownComparisonSemantics::of_instance( + db, + env, + left_enum.enum_class_instance(db, env), + operator, + ) + .is_none() + && let Ok(bindings) = Type::enum_literal(left_enum).try_call_dunder_with_policy( + db, + env, + operator.dunder(), + &mut CallArguments::positional([Type::unknown()]), + TypeContext::default(), + MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK + | MemberLookupPolicy::MRO_NO_INT_OR_STR_LOOKUP, + ) + && let Some(result) = bindings + .return_type(db, env) + .as_literal_value() + .and_then(LiteralValueType::as_bool) + { + return Some(result == (operator == ComparisonOperator::Equality)); + } + match (left, right) { (LiteralValueTypeKind::Int(left), LiteralValueTypeKind::Int(right)) => { Some(left.as_i64() == right.as_i64()) @@ -1629,10 +1970,18 @@ fn known_literal_equality<'db>( Some(left.value(db) == right.value(db)) } (LiteralValueTypeKind::Enum(left), LiteralValueTypeKind::Enum(right)) => { - let left_semantics = - KnownComparisonSemantics::of_instance(db, left.enum_class_instance(db), operator)?; - let right_semantics = - KnownComparisonSemantics::of_instance(db, right.enum_class_instance(db), operator)?; + let left_semantics = KnownComparisonSemantics::of_instance( + db, + env, + left.enum_class_instance(db, env), + operator, + )?; + let right_semantics = KnownComparisonSemantics::of_instance( + db, + env, + right.enum_class_instance(db, env), + operator, + )?; if left_semantics != right_semantics { return Some(false); } @@ -1648,8 +1997,9 @@ fn known_literal_equality<'db>( } known_literal_equality( db, - enum_literal_value(db, left)?.as_literal_value_kind()?, - enum_literal_value(db, right)?.as_literal_value_kind()?, + env, + enum_literal_value(db, env, left)?.as_literal_value_kind()?, + enum_literal_value(db, env, right)?.as_literal_value_kind()?, ComparisonOperator::Equality, ) } @@ -1657,15 +2007,17 @@ fn known_literal_equality<'db>( | (other, LiteralValueTypeKind::Enum(enum_literal)) => { let enum_semantics = KnownComparisonSemantics::of_instance( db, - enum_literal.enum_class_instance(db), + env, + enum_literal.enum_class_instance(db, env), operator, )?; - if enum_semantics != KnownComparisonSemantics::of_literal(db, other, operator)? { + if enum_semantics != KnownComparisonSemantics::of_literal(db, env, other, operator)? { return Some(false); } known_literal_equality( db, - enum_literal_value(db, enum_literal)?.as_literal_value_kind()?, + env, + enum_literal_value(db, env, enum_literal)?.as_literal_value_kind()?, other, ComparisonOperator::Equality, ) @@ -1676,8 +2028,8 @@ fn known_literal_equality<'db>( ) | (LiteralValueTypeKind::String(_), LiteralValueTypeKind::LiteralString) => None, (left, right) => { - let left_semantics = KnownComparisonSemantics::of_literal(db, left, operator)?; - let right_semantics = KnownComparisonSemantics::of_literal(db, right, operator)?; + let left_semantics = KnownComparisonSemantics::of_literal(db, env, left, operator)?; + let right_semantics = KnownComparisonSemantics::of_literal(db, env, right, operator)?; (left_semantics != right_semantics).then_some(false) } } @@ -1686,11 +2038,15 @@ fn known_literal_equality<'db>( /// Return the statically known runtime value of an enum member. /// /// Custom enum construction can replace the declared value, so members of such enums return `None`. -fn enum_literal_value<'db>(db: &'db dyn Db, literal: EnumLiteralType<'db>) -> Option> { +fn enum_literal_value<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + literal: EnumLiteralType<'db>, +) -> Option> { let enum_class_literal = literal.enum_class_literal(db); let metadata = enum_metadata(db, enum_class_literal.class_literal(db))?; let name = enum_class_literal.resolve_member(db, literal.name(db))?; - metadata.concrete_value_type(db, name) + metadata.concrete_value_type(db, env, name) } /// Return whether two enum literals resolve to the same member, including aliases. diff --git a/crates/ty_python_semantic/src/types/equality/enums.rs b/crates/ty_python_semantic/src/types/equality/enums.rs index cae8937677..d7ce2ac6b5 100644 --- a/crates/ty_python_semantic/src/types/equality/enums.rs +++ b/crates/ty_python_semantic/src/types/equality/enums.rs @@ -9,36 +9,258 @@ use crate::types::{ EnumClassLiteral, EnumComplementType, EnumLiteralType, IntersectionBuilder, IntersectionType, LiteralValueType, LiteralValueTypeKind, Type, UnionBuilder, }; -use crate::{Db, FxOrderMap, FxOrderSet}; +use crate::{Db, FxOrderMap, FxOrderSet, ProgramEnvironment}; use super::{ - ComparisonBranch, ComparisonOperator, ComparisonResult, KnownComparisonSemantics, - enum_literal_value, + ComparisonBranch, ComparisonEvaluator, ComparisonGoal, ComparisonOperator, ComparisonResult, + KnownComparisonSemantics, combine_definite_truthiness, enum_literal_value, + evaluate_against_results, evaluate_target_union, }; -/// Compare two enum value domains without comparing every pair of members. +/// Compare enum values without checking every pair of members. /// -/// Any narrowing constraint produced here contains only enum-membership facts. In particular, -/// equality never transfers gradual or nominal intersection state from one operand to the other. -/// Same-class domains compare compact member sets directly, while comparisons spanning multiple -/// classes project their members onto runtime comparison keys. -pub(super) fn evaluate_enum_domains<'db>( +/// If either side also contains other values, compare those values normally. +/// +/// Return `None` when the enum comparison does not apply. +pub(super) fn evaluate_enum_comparison<'db>( + evaluator: &mut ComparisonEvaluator<'db>, + target: Type<'db>, + other: Type<'db>, + branch: ComparisonBranch, + operator: ComparisonOperator, +) -> Option> { + let db = evaluator.db; + let env = evaluator.env.clone(); + evaluate_enum_domains(db, &env, target, other, branch, operator).or_else(|| { + PartitionedEnumComparison::new(db, &env, target, other, branch, operator).map( + |comparison| match comparison.evaluate(evaluator, branch, operator) { + ComparisonResult::CanNarrow(narrowed) + if narrowed == target.resolve_type_alias(db) => + { + ComparisonResult::Ambiguous + } + result => result, + }, + ) + }) +} + +/// Compare values that are all enum members. +/// +/// Describe the result using enum members only. Do not copy other restrictions from one side to +/// the other. +fn evaluate_enum_domains<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, target: Type<'db>, other: Type<'db>, branch: ComparisonBranch, operator: ComparisonOperator, ) -> Option> { - let target = EnumDomainSet::from_type(db, target)?; - let other = EnumDomainSet::from_type(db, other)?; + let target = EnumDomainSet::from_type(db, env, target)?; + let other = EnumDomainSet::from_type(db, env, other)?; if let (Some(target), Some(other)) = (target.single(), other.single()) && target.enum_class == other.enum_class { return SameEnumComparison::new(db, target.clone(), other.clone(), operator) - .evaluate(db, branch, operator); + .evaluate(db, env, branch, operator); } - ProjectedEnumComparison::new(db, target, &other, operator)?.evaluate(db, branch, operator) + ProjectedEnumComparison::new(db, target, &other, operator)?.evaluate(db, env, branch, operator) +} + +/// Compare unions that contain enums and other values. +/// +/// Compare enum members together and compare other values normally. Values such as `None`, +/// `Any`, or a matching string can also affect which values match. +/// +/// Compare the enum members only once. +/// +/// ```python +/// from enum import StrEnum +/// +/// class Left(StrEnum): +/// SHARED = "shared" +/// LEFT = "left" +/// +/// class Right(StrEnum): +/// SHARED = "shared" +/// RIGHT = "right" +/// +/// def compare(left: Left | None, right: Right | None): +/// if left == right: +/// reveal_type(left) # Literal[Left.SHARED] | None +/// reveal_type(right) # Literal[Right.SHARED] | None +/// ``` +struct PartitionedEnumComparison<'db> { + target: EnumDomainPartition<'db>, + other: EnumDomainPartition<'db>, + other_type: Type<'db>, + enum_result: ComparisonResult<'db>, +} + +impl<'db> PartitionedEnumComparison<'db> { + /// Prepare a comparison when both sides contain enums and at least one side also contains + /// another value. + /// + /// Return `None` if either enum has unsupported comparison behavior. + fn new( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + target: Type<'db>, + other: Type<'db>, + branch: ComparisonBranch, + operator: ComparisonOperator, + ) -> Option { + if !matches!(target.resolve_type_alias(db), Type::Union(_)) + && !matches!(other.resolve_type_alias(db), Type::Union(_)) + { + return None; + } + + let target = EnumDomainPartition::from_type(db, env, target)?; + let other_type = other; + let other = EnumDomainPartition::from_type(db, env, other)?; + + if !target.has_other_values() && !other.has_other_values() { + return None; + } + + let enum_result = + evaluate_enum_domains(db, env, target.enum_type, other.enum_type, branch, operator)?; + + Some(Self { + target, + other, + other_type, + enum_result, + }) + } + + /// Reuse the saved enum result and compare all other values normally. + fn evaluate_pair( + &self, + evaluator: &mut ComparisonEvaluator<'db>, + target: Type<'db>, + other: Type<'db>, + branch: ComparisonBranch, + operator: ComparisonOperator, + ) -> ComparisonResult<'db> { + if target == self.target.enum_type && other == self.other.enum_type { + self.enum_result + } else { + evaluator.evaluate(target, other, branch, operator) + } + } + + /// Compare one possible value with the other side. + /// + /// Compare `Any` and `Unknown` with the whole union so neither is narrowed by separate values. + /// + /// Return whether the result is certain or which values can still match. + fn evaluate_against_other( + &self, + evaluator: &mut ComparisonEvaluator<'db>, + target: Type<'db>, + branch: ComparisonBranch, + operator: ComparisonOperator, + ) -> ComparisonResult<'db> { + let db = evaluator.db; + if let [other] = self.other.alternatives.as_slice() { + return self.evaluate_pair(evaluator, target, *other, branch, operator); + } + + if evaluator.goal == ComparisonGoal::Truthiness { + return combine_definite_truthiness( + self.other + .alternatives + .iter() + .map(|other| self.evaluate_pair(evaluator, target, *other, branch, operator)), + ); + } + + let env = evaluator.env.clone(); + if matches!(target.resolve_type_alias(db), Type::Dynamic(_)) { + return evaluator.evaluate(target, self.other_type, branch, operator); + } + + evaluate_against_results( + db, + &env, + target, + branch, + self.other + .alternatives + .iter() + .map(|other| self.evaluate_pair(evaluator, target, *other, branch, operator)), + ) + } + + /// Compare all possible values. + /// + /// If no member of an enum can match, exclude it from the other possible values. + /// + /// Return whether the result is certain or which values can still match. + fn evaluate( + &self, + evaluator: &mut ComparisonEvaluator<'db>, + branch: ComparisonBranch, + operator: ComparisonOperator, + ) -> ComparisonResult<'db> { + let db = evaluator.db; + if let [target] = self.target.alternatives.as_slice() { + return self.evaluate_against_other(evaluator, *target, branch, operator); + } + + if evaluator.goal == ComparisonGoal::Truthiness { + return combine_definite_truthiness( + self.target.alternatives.iter().map(|target| { + self.evaluate_against_other(evaluator, *target, branch, operator) + }), + ); + } + + let env = evaluator.env.clone(); + let mut narrowed_enum = None; + let result = evaluate_target_union(db, &env, &self.target.alternatives, branch, |target| { + let result = self.evaluate_against_other(evaluator, target, branch, operator); + if target == self.target.enum_type + && let ComparisonResult::CanNarrow(narrowed) = result + && narrowed != target + { + narrowed_enum = Some(narrowed); + } + result + }); + + if let ComparisonResult::CanNarrow(narrowed) = result + && let Some(narrowed_enum) = narrowed_enum + && let Some(domains) = EnumDomainSet::from_type(db, &env, self.target.enum_type) + { + let excluded = domains + .domains + .iter() + .fold(UnionBuilder::new(db, &env), |builder, domain| { + let domain_type = domain.restriction_type(db, &env); + if domain_type.is_disjoint_from(db, &env, narrowed_enum) { + builder.add(domain_type) + } else { + builder + } + }) + .build(); + if !excluded.is_never() { + return ComparisonResult::CanNarrow( + IntersectionBuilder::new(db, &env) + .add_positive(narrowed) + .add_negative(excluded) + .build(), + ); + } + } + + result + } } /// Two non-empty value domains from the same enum and the semantics used to compare them. @@ -94,6 +316,7 @@ impl<'db> SameEnumComparison<'db> { fn evaluate( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, branch: ComparisonBranch, operator: ComparisonOperator, ) -> Option> { @@ -103,15 +326,15 @@ impl<'db> SameEnumComparison<'db> { Truthiness::Ambiguous if !self.supports_domain_narrowing() => { Some(ComparisonResult::Ambiguous) } - Truthiness::Ambiguous if operator.condition_expects_equality(branch) => { - Some(ComparisonResult::CanNarrow(self.right.restriction_type(db))) - } + Truthiness::Ambiguous if operator.condition_expects_equality(branch) => Some( + ComparisonResult::CanNarrow(self.right.restriction_type(db, env)), + ), Truthiness::Ambiguous => Some(self.right.singleton_type(db).map_or( ComparisonResult::Ambiguous, |singleton| { ComparisonResult::CanNarrow( - IntersectionBuilder::new(db) - .add_positive(self.left.restriction_type(db)) + IntersectionBuilder::new(db, env) + .add_positive(self.left.restriction_type(db, env)) .add_negative(singleton) .build(), ) @@ -161,15 +384,17 @@ enum EnumValueSetMembers<'db> { impl<'db> EnumValueSet<'db> { /// Extract only structural enum membership facts from `ty`. /// - /// This deliberately does not use subtyping: a `NewType` over an enum is a subtype of the - /// enum but remains disjoint from the enum's literal members. + /// This deliberately does not use subtyping: extra nominal restrictions must not be + /// transferred to the other comparison operand. fn from_type( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, active_types: &mut FxHashSet>, ) -> Option { fn from_type_inner<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, active_types: &mut FxHashSet>, ) -> Option> { @@ -189,18 +414,21 @@ impl<'db> EnumValueSet<'db> { } } Type::NominalInstance(instance) => EnumValueSet { - enum_class: instance.class_literal(db).into_enum_class(db)?, + enum_class: instance.class_literal(db, env).into_enum_class(db)?, members: EnumValueSetMembers::All, }, + Type::NewTypeInstance(newtype) => { + EnumValueSet::from_type(db, env, newtype.concrete_base_type(db), active_types)? + } Type::EnumComplement(complement) => EnumValueSet { enum_class: complement.enum_class_literal(db), members: EnumValueSetMembers::AllExcept(complement), }, Type::Union(union) => { - EnumValueSet::from_union(db, union.elements(db), active_types)? + EnumValueSet::from_union(db, env, union.elements(db), active_types)? } Type::Intersection(intersection) => { - EnumValueSet::from_intersection(db, intersection, active_types)? + EnumValueSet::from_intersection(db, env, intersection, active_types)? } _ => return None, }; @@ -211,7 +439,7 @@ impl<'db> EnumValueSet<'db> { if !active_types.insert(ty) { return None; } - let value_set = from_type_inner(db, ty, active_types); + let value_set = from_type_inner(db, env, ty, active_types); active_types.remove(&ty); value_set } @@ -221,13 +449,14 @@ impl<'db> EnumValueSet<'db> { /// Whole-domain and complement arms are rejected because they are not exact included sets. fn from_union( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, elements: &[Type<'db>], active_types: &mut FxHashSet>, ) -> Option { let mut enum_class = None; let mut included = FxOrderMap::default(); for element in elements { - let value_set = Self::from_type(db, *element, active_types)?; + let value_set = Self::from_type(db, env, *element, active_types)?; if let Some(enum_class) = enum_class && enum_class != value_set.enum_class { @@ -288,11 +517,22 @@ impl<'db> EnumValueSet<'db> { /// Extract the enum restriction while discarding unrelated positive intersection state. fn from_intersection( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, intersection: IntersectionType<'db>, active_types: &mut FxHashSet>, ) -> Option { - if let Some(complement) = intersection.enum_complement(db) { - return Self::from_type(db, Type::EnumComplement(complement), active_types); + if let Some(complement) = intersection.enum_complement(db, env) { + return Self::from_type(db, env, Type::EnumComplement(complement), active_types); + } + + if intersection + .positive(db) + .iter() + .any(|positive| matches!(positive.resolve_type_alias(db), Type::NewTypeInstance(_))) + && let expanded = intersection.with_expanded_typevars_and_newtypes(db, env) + && let Some(value_set) = Self::from_type(db, env, expanded, active_types) + { + return Some(value_set); } // Other intersection components can only reduce the represented enum values. Ignoring @@ -300,7 +540,7 @@ impl<'db> EnumValueSet<'db> { let mut value_sets = intersection .positive(db) .iter() - .filter_map(|positive| Self::from_type(db, *positive, active_types)); + .filter_map(|positive| Self::from_type(db, env, *positive, active_types)); let value_set = value_sets.next()?; value_sets .all(|other| other.enum_class == value_set.enum_class) @@ -389,18 +629,18 @@ impl<'db> EnumValueSet<'db> { } /// Reconstruct a constraint containing only this enum value restriction. - fn restriction_type(&self, db: &'db dyn Db) -> Type<'db> { + fn restriction_type(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match &self.members { EnumValueSetMembers::All => self .enum_class .class_literal(db) - .to_non_generic_instance(db), + .to_non_generic_instance(db, env), EnumValueSetMembers::One { name, promotable } => { self.member_type(db, name, *promotable) } EnumValueSetMembers::Included(members) => members .iter() - .fold(UnionBuilder::new(db), |builder, (name, promotable)| { + .fold(UnionBuilder::new(db, env), |builder, (name, promotable)| { builder.add(self.member_type(db, name, *promotable)) }) .build(), @@ -449,20 +689,104 @@ impl<'db> EnumValueSet<'db> { } } +/// The enum members and other values on one side of a comparison. +/// +/// Place enum members at the first enum's position and keep other values in their original order. +struct EnumDomainPartition<'db> { + enum_type: Type<'db>, + alternatives: Vec>, +} + +impl<'db> EnumDomainPartition<'db> { + /// Combine enum values while keeping other values in their original order. + /// + /// Return `None` when there is no enum or a type alias refers to itself. + fn from_type(db: &'db dyn Db, env: &ProgramEnvironment<'db>, ty: Type<'db>) -> Option { + fn collect<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + enum_types: &mut Vec>, + alternatives: &mut Vec>, + enum_position: &mut Option, + active_types: &mut FxHashSet>, + ) -> Option<()> { + if EnumValueSet::from_type(db, env, ty, active_types).is_some() { + enum_position.get_or_insert(alternatives.len()); + enum_types.push(ty); + return Some(()); + } + + let Type::Union(union) = ty.resolve_type_alias(db) else { + alternatives.push(ty); + return Some(()); + }; + + if !active_types.insert(ty) { + return None; + } + + let result = union.elements(db).iter().try_for_each(|element| { + collect( + db, + env, + *element, + enum_types, + alternatives, + enum_position, + active_types, + ) + }); + active_types.remove(&ty); + result + } + + let mut enum_types = Vec::new(); + let mut alternatives = Vec::new(); + let mut enum_position = None; + let mut active_types = FxHashSet::default(); + collect( + db, + env, + ty, + &mut enum_types, + &mut alternatives, + &mut enum_position, + &mut active_types, + )?; + let enum_position = enum_position?; + let enum_type = enum_types + .into_iter() + .fold(UnionBuilder::new(db, env), UnionBuilder::add) + .build(); + alternatives.insert(enum_position, enum_type); + + Some(Self { + enum_type, + alternatives, + }) + } + + fn has_other_values(&self) -> bool { + self.alternatives.len() > 1 + } +} + /// One or more enum-class domains represented by an operand. struct EnumDomainSet<'db> { domains: Vec>, } impl<'db> EnumDomainSet<'db> { - fn from_type(db: &'db dyn Db, ty: Type<'db>) -> Option { + fn from_type(db: &'db dyn Db, env: &ProgramEnvironment<'db>, ty: Type<'db>) -> Option { fn collect<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, domains: &mut Vec>, active_types: &mut FxHashSet>, ) -> Option<()> { - if let Some(domain) = EnumValueSet::from_type(db, ty, active_types) { + if let Some(domain) = EnumValueSet::from_type(db, env, ty, active_types) { domains.push(domain); return Some(()); } @@ -470,13 +794,14 @@ impl<'db> EnumDomainSet<'db> { if !active_types.insert(ty) { return None; } - let result = collect_union(db, ty, domains, active_types); + let result = collect_union(db, env, ty, domains, active_types); active_types.remove(&ty); result } fn collect_union<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, domains: &mut Vec>, active_types: &mut FxHashSet>, @@ -485,14 +810,14 @@ impl<'db> EnumDomainSet<'db> { return None; }; for element in union.elements(db) { - collect(db, *element, domains, active_types)?; + collect(db, env, *element, domains, active_types)?; } Some(()) } let mut domains = Vec::new(); let mut active_types = FxHashSet::default(); - collect(db, ty, &mut domains, &mut active_types)?; + collect(db, env, ty, &mut domains, &mut active_types)?; (!domains.is_empty()).then_some(Self { domains }) } @@ -515,11 +840,11 @@ impl<'db> EnumDomainSet<'db> { Some(projection) } - fn restriction_type(&self, db: &'db dyn Db) -> Type<'db> { + fn restriction_type(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { self.domains .iter() - .fold(UnionBuilder::new(db), |builder, domain| { - builder.add(domain.restriction_type(db)) + .fold(UnionBuilder::new(db, env), |builder, domain| { + builder.add(domain.restriction_type(db, env)) }) .build() } @@ -527,17 +852,18 @@ impl<'db> EnumDomainSet<'db> { fn restrict_for_equality( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, operator: ComparisonOperator, other: &EnumKeyProjection<'db>, ) -> Option> { - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); for domain in &self.domains { let mut projection = EnumKeyProjection::default(); domain.add_keys_to_projection(db, operator, &mut projection)?; if projection.unknowns_may_overlap(other) { - builder = builder.add(domain.restriction_type(db)); + builder = builder.add(domain.restriction_type(db, env)); } else if let Some(retained) = domain.retain_keys(db, operator, &other.keys).ok()? { - builder = builder.add(retained.restriction_type(db)); + builder = builder.add(retained.restriction_type(db, env)); } } Some(builder.build()) @@ -547,10 +873,11 @@ impl<'db> EnumDomainSet<'db> { fn known_equal_type( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, operator: ComparisonOperator, other: &EnumKeyProjection<'db>, ) -> Option> { - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); for domain in &self.domains { let mut projection = EnumKeyProjection::default(); domain.add_keys_to_projection(db, operator, &mut projection)?; @@ -558,7 +885,7 @@ impl<'db> EnumDomainSet<'db> { continue; } if let Some(retained) = domain.retain_keys(db, operator, &other.keys).ok()? { - builder = builder.add(retained.restriction_type(db)); + builder = builder.add(retained.restriction_type(db, env)); } } Some(builder.build()) @@ -606,6 +933,7 @@ impl<'db> ProjectedEnumComparison<'db> { fn evaluate( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, branch: ComparisonBranch, operator: ComparisonOperator, ) -> Option> { @@ -615,16 +943,16 @@ impl<'db> ProjectedEnumComparison<'db> { Truthiness::Ambiguous if operator.condition_expects_equality(branch) => { Some(ComparisonResult::CanNarrow( self.left - .restrict_for_equality(db, operator, &self.right_projection)?, + .restrict_for_equality(db, env, operator, &self.right_projection)?, )) } Truthiness::Ambiguous if self.right_projection.single_key().is_some() => { let equal_left = self.left - .known_equal_type(db, operator, &self.right_projection)?; + .known_equal_type(db, env, operator, &self.right_projection)?; Some(ComparisonResult::CanNarrow( - IntersectionBuilder::new(db) - .add_positive(self.left.restriction_type(db)) + IntersectionBuilder::new(db, env) + .add_positive(self.left.restriction_type(db, env)) .add_negative(equal_left) .build(), )) @@ -793,9 +1121,13 @@ fn enum_class_key_profile<'db>( enum_class: EnumClassLiteral<'db>, operator: ComparisonOperator, ) -> EnumClassKeyProfile<'db> { + let env = ProgramEnvironment::from_file(enum_class.class_literal(db).program_file(db)); let semantics = KnownComparisonSemantics::of_instance( db, - enum_class.class_literal(db).to_non_generic_instance(db), + &env, + enum_class + .class_literal(db) + .to_non_generic_instance(db, &env), operator, ); let members: Box<[(Name, Option>)]> = enum_class @@ -805,7 +1137,7 @@ fn enum_class_key_profile<'db>( ( name.clone(), semantics.and_then(|semantics| { - enum_literal_value(db, EnumLiteralType::new(db, enum_class, name)) + enum_literal_value(db, &env, EnumLiteralType::new(db, enum_class, name)) .and_then(|value| enum_comparison_key(semantics, value)) }), ) diff --git a/crates/ty_python_semantic/src/types/exceptions.rs b/crates/ty_python_semantic/src/types/exceptions.rs index 98d12becd2..87374b4d22 100644 --- a/crates/ty_python_semantic/src/types/exceptions.rs +++ b/crates/ty_python_semantic/src/types/exceptions.rs @@ -46,6 +46,7 @@ use ty_python_core::definition::Definition; use ty_python_core::scope::ScopeId; use crate::Db; +use crate::types::ProgramEnvironment; use crate::types::context::InferContext; use crate::types::diagnostic::{ INVALID_RAISES_CLAUSE, OVERRIDE_RAISE, UNDECLARED_RAISE, UNHANDLED_EXCEPTION, @@ -112,10 +113,12 @@ pub(crate) fn raised_exceptions<'db>(db: &'db dyn Db, overload: OverloadLiteral< /// miss one that can. pub(crate) fn function_raised_exceptions<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, function: FunctionLiteral<'db>, ) -> Type<'db> { UnionType::from_elements( db, + env, function .iter_overloads_and_implementation(db) .map(|overload| raised_exceptions(db, overload)) @@ -140,7 +143,7 @@ pub(crate) fn declared_exceptions<'db>( if !file.source_type(db).is_basedpython() { return None; } - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, db.program_file(file).python_file(db)).load(db); let raises = overload.node(db, file, &module).raises.as_deref()?; if raises.is_ellipsis_literal_expr() { @@ -164,8 +167,10 @@ pub(crate) fn inferred_exceptions<'db>( db: &'db dyn Db, overload: OverloadLiteral<'db>, ) -> Type<'db> { + let env = &ProgramEnvironment::from_file(overload.program_file(db)); resolve_effects( db, + env, body_exception_effects(db, overload), Some(overload.body_scope(db)), ) @@ -181,26 +186,29 @@ pub(crate) fn body_exception_effects<'db>( db: &'db dyn Db, overload: OverloadLiteral<'db>, ) -> ExceptionEffects<'db> { + let env = &ProgramEnvironment::from_file(overload.program_file(db)); let file = overload.file(db); if !file.source_type(db).is_basedpython() { return ExceptionEffects::default(); } - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, db.program_file(file).python_file(db)).load(db); let node = overload.node(db, file, &module); let inference = infer_scope_types(db, overload.body_scope(db), TypeContext::default()); - collect_exception_effects(db, &node.body, |expr| inference.expression_type(expr)) + collect_exception_effects(db, env, &node.body, |expr| inference.expression_type(expr)) } /// Union the exceptions escaping `effects`, following each call into its callee. pub(crate) fn resolve_effects<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, effects: &ExceptionEffects<'db>, self_body_scope: Option>, ) -> Type<'db> { UnionType::from_elements( db, - escaping_sites(db, effects, self_body_scope, &[]) + env, + escaping_sites(db, env, effects, self_body_scope, &[]) .into_iter() .map(|(_, raised)| raised), ) @@ -215,6 +223,7 @@ pub(crate) fn resolve_effects<'db>( /// re-entered. pub(crate) fn escaping_sites<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, effects: &ExceptionEffects<'db>, self_body_scope: Option>, allowed: &[Type<'db>], @@ -222,7 +231,7 @@ pub(crate) fn escaping_sites<'db>( let direct = effects .direct .iter() - .filter_map(|raise| Some((raise.range, escaping(db, raise.raised, allowed)?))); + .filter_map(|raise| Some((raise.range, escaping(db, env, raise.raised, allowed)?))); let from_calls = effects .calls @@ -236,10 +245,11 @@ pub(crate) fn escaping_sites<'db>( .filter_map(|call| { let raised = escaping( db, - function_raised_exceptions(db, call.callee), + env, + function_raised_exceptions(db, env, call.callee), &call.caught, )?; - Some((call.range, escaping(db, raised, allowed)?)) + Some((call.range, escaping(db, env, raised, allowed)?)) }); direct.chain(from_calls).collect() @@ -253,6 +263,7 @@ pub(crate) fn escaping_sites<'db>( /// everything. pub(crate) fn escaping<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, raised: Type<'db>, caught: &[Type<'db>], ) -> Option> { @@ -262,6 +273,7 @@ pub(crate) fn escaping<'db>( let escaped = UnionType::from_elements( db, + env, union_elements(db, raised).into_iter().filter(|element| { // a dynamic member is an unknown exception, not a known one: it is // what `raises ...` declares, and what an unreadable `raise` leaves @@ -269,7 +281,7 @@ pub(crate) fn escaping<'db>( !element.is_dynamic() && !caught .iter() - .any(|caught| element.is_assignable_to(db, *caught)) + .any(|caught| element.is_assignable_to(db, env, *caught)) }), ); @@ -291,11 +303,13 @@ pub(crate) fn union_elements<'db>(db: &'db dyn Db, ty: Type<'db>) -> Vec( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, body: &[Stmt], expression_type: impl Fn(&Expr) -> Type<'db>, ) -> ExceptionEffects<'db> { let mut collector = EffectsCollector { db, + env: env.clone(), expression_type, caught: Vec::new(), handling: Vec::new(), @@ -312,6 +326,7 @@ pub(crate) fn collect_exception_effects<'db>( struct EffectsCollector<'db, F> { db: &'db dyn Db, + env: ProgramEnvironment<'db>, expression_type: F, /// the exception types caught by the `except` clauses currently enclosing /// the node being visited, innermost last @@ -328,6 +343,7 @@ where F: Fn(&Expr) -> Type<'db>, { fn visit_try(&mut self, try_stmt: &ast::StmtTry) { + let env = self.env.clone(); // an `except*` clause does not simply catch what it names — what escapes // it is a regrouped `ExceptionGroup` — so it is treated as catching // nothing rather than pretending either way @@ -361,7 +377,7 @@ where caught .get(index) .copied() - .unwrap_or_else(|| KnownClass::BaseException.to_instance(self.db)), + .unwrap_or_else(|| KnownClass::BaseException.to_instance(self.db, &env)), ); self.visit_body(&handler.body); self.handling.pop(); @@ -375,14 +391,16 @@ where /// The exception instance type an `except` clause catches. A bare `except:` /// catches everything, and so does a clause this analysis cannot read. fn caught_type(&self, type_: Option<&Expr>) -> Type<'db> { + let env = self.env.clone(); let Some(type_) = type_ else { - return KnownClass::BaseException.to_instance(self.db); + return KnownClass::BaseException.to_instance(self.db, &env); }; let caught = (self.expression_type)(type_); - if let Some(tuple) = caught.tuple_instance_spec(self.db) { + if let Some(tuple) = caught.tuple_instance_spec(self.db, &env) { return UnionType::from_elements( self.db, + &env, tuple .iter_element_types(self.db) .map(|element| self.exception_instance(element)) @@ -394,6 +412,7 @@ where } fn record_raise(&mut self, raise: &ast::StmtRaise) { + let env = self.env.clone(); let range = raise.range(); let Some(exception) = raise.exc.as_deref() else { // a bare `raise` re-raises what the enclosing handler caught; outside @@ -402,7 +421,7 @@ where .handling .last() .copied() - .unwrap_or_else(|| KnownClass::RuntimeError.to_instance(self.db)); + .unwrap_or_else(|| KnownClass::RuntimeError.to_instance(self.db, &env)); self.record_escaping(reraised, range); return; }; @@ -415,9 +434,14 @@ where /// Read `ty` as the exception instance it produces: `raise TypeError` names /// the class, `raise TypeError(...)` and `raise err` name an instance. fn exception_instance(&self, ty: Type<'db>) -> Type<'db> { - if ty.is_assignable_to(self.db, KnownClass::BaseException.to_subclass_of(self.db)) { - ty.to_instance_approximation(self.db) - .unwrap_or_else(|| KnownClass::BaseException.to_instance(self.db)) + let env = self.env.clone(); + if ty.is_assignable_to( + self.db, + &env, + KnownClass::BaseException.to_subclass_of(self.db, &env), + ) { + ty.to_instance_approximation(self.db, &env) + .unwrap_or_else(|| KnownClass::BaseException.to_instance(self.db, &env)) } else { ty } @@ -426,7 +450,8 @@ where /// Record `raised` as raised at `range`, minus whatever the enclosing /// handlers catch. fn record_escaping(&mut self, raised: Type<'db>, range: TextRange) { - if let Some(escaping) = escaping(self.db, raised, &self.caught) { + let env = self.env.clone(); + if let Some(escaping) = escaping(self.db, &env, raised, &self.caught) { self.direct.push(RaiseEffect { raised: escaping, range, @@ -449,6 +474,7 @@ where F: Fn(&Expr) -> Type<'db>, { fn visit_stmt(&mut self, stmt: &Stmt) { + let env = self.env.clone(); match stmt { // a nested function does not run where it is defined; its own body is // analysed when something calls it. its decorators and defaults do run @@ -475,7 +501,7 @@ where Stmt::Assert(assert) => { walk_stmt(self, stmt); self.record_escaping( - KnownClass::AssertionError.to_instance(self.db), + KnownClass::AssertionError.to_instance(self.db, &env), assert.range(), ); } @@ -555,6 +581,7 @@ pub(super) fn check_override_raises<'db>( superclass_function: FunctionType<'db>, superclass: ClassType<'db>, ) { + let env = context.program_environment(); let db = context.db(); // resolving both sets walks two call graphs, so do nothing at all unless the // strictness option asked for it @@ -562,9 +589,9 @@ pub(super) fn check_override_raises<'db>( return; } - let allowed = function_raised_exceptions(db, superclass_function.literal(db)); - let raised = function_raised_exceptions(db, subclass_function.literal(db)); - let Some(extra) = escaping(db, raised, &[allowed]) else { + let allowed = function_raised_exceptions(db, env, superclass_function.literal(db)); + let raised = function_raised_exceptions(db, env, subclass_function.literal(db)); + let Some(extra) = escaping(db, env, raised, &[allowed]) else { return; }; @@ -580,7 +607,7 @@ pub(super) fn check_override_raises<'db>( }; let mut diagnostic = builder.into_diagnostic(format_args!( "`{member}` can raise `{}`, which the method it overrides cannot", - extra.display(db) + extra.display(db, env) )); let base = superclass.name(db); let annotation = Annotation::secondary( @@ -595,7 +622,7 @@ pub(super) fn check_override_raises<'db>( } else { annotation.message(format_args!( "`{base}.{member}` raises only `{}`", - allowed.display(db) + allowed.display(db, env) )) }); } @@ -614,6 +641,7 @@ pub(super) fn check_function_exceptions<'db, 'ast>( definition: Definition<'db>, expression_type: impl Fn(&Expr) -> Type<'db>, ) { + let env = context.program_environment(); let db = context.db(); if !context.file().source_type(db).is_basedpython() { return; @@ -638,12 +666,12 @@ pub(super) fn check_function_exceptions<'db, 'ast>( None => return, }; - let effects = collect_exception_effects(db, &function.body, expression_type); + let effects = collect_exception_effects(db, env, &function.body, expression_type); if effects.is_empty() { return; } - for (range, escaped) in escaping_sites(db, &effects, Some(body_scope), &allowed) { + for (range, escaped) in escaping_sites(db, env, &effects, Some(body_scope), &allowed) { let name = &function.name.id; if declared.is_some() { let Some(builder) = context.report_lint(&UNDECLARED_RAISE, range) else { @@ -651,7 +679,7 @@ pub(super) fn check_function_exceptions<'db, 'ast>( }; builder.into_diagnostic(format_args!( "`{name}` can raise `{}`, which its `raises` clause does not include", - escaped.display(db) + escaped.display(db, env) )); } else { let Some(builder) = context.report_lint(&UNHANDLED_EXCEPTION, range) else { @@ -659,7 +687,7 @@ pub(super) fn check_function_exceptions<'db, 'ast>( }; builder.into_diagnostic(format_args!( "`{}` can escape `{name}`, the entry point", - escaped.display(db) + escaped.display(db, env) )); } } @@ -688,18 +716,19 @@ fn check_raises_clause_is_exceptions<'db, 'ast>( clause: &'ast Expr, declared: Type<'db>, ) { + let env = context.program_environment(); let db = context.db(); if declared.is_never() || declared.is_dynamic() { return; } - if !declared.is_disjoint_from(db, KnownClass::BaseException.to_instance(db)) { + if !declared.is_disjoint_from(db, env, KnownClass::BaseException.to_instance(db, env)) { return; } if let Some(builder) = context.report_lint(&INVALID_RAISES_CLAUSE, clause) { builder.into_diagnostic(format_args!( "`{}` contains no exception, so nothing can satisfy this `raises` clause", - declared.display(db) + declared.display(db, env) )); } } @@ -712,6 +741,7 @@ fn check_raises_clause_is_exceptions<'db, 'ast>( /// becomes the empty tuple, which no exception is an instance of. pub fn declared_raises_runtime_target<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: ruff_db::files::File, function: Type<'db>, ) -> Option { @@ -727,7 +757,7 @@ pub fn declared_raises_runtime_target<'db>( return Some("()".to_string()); } - crate::types::soundness::runtime_check_target(db, file, declared) + crate::types::soundness::runtime_check_target(db, env, file, declared) } /// Whether `function` is the module's entry point — a `main` defined directly at diff --git a/crates/ty_python_semantic/src/types/extensions.rs b/crates/ty_python_semantic/src/types/extensions.rs index 1ecae55fae..fe8c667cf5 100644 --- a/crates/ty_python_semantic/src/types/extensions.rs +++ b/crates/ty_python_semantic/src/types/extensions.rs @@ -30,6 +30,7 @@ use ty_python_core::{global_scope, place_table, semantic_index}; use crate::Db; use crate::place::{builtins_symbol, global_symbol}; +use crate::types::ProgramEnvironment; use crate::types::call::CallArguments; use crate::types::class::{ClassLiteral, ClassType, KnownClass, StaticClassLiteral}; use crate::types::class_base::ClassBase; @@ -41,6 +42,7 @@ use crate::types::generics::Specialization; use crate::types::member::class_member; use crate::types::typevar::{BoundTypeVarInstance, TypeVarBoundOrConstraints}; use crate::types::{MemberLookupPolicy, Type}; +use ty_module_resolver::ImportingFile; /// the symbol-name prefix the semantic index gives extension declarations pub(crate) const EXTENSION_SYMBOL_PREFIX: &str = " Option { let name = ModuleName::new_static(PRELUDE_MODULE)?; - resolve_module(db, from_file, &name)?.file(db) + resolve_module( + db, + ImportingFile::File( + from_file, + db.program_file(from_file).resolver_environment(db), + ), + &name, + )? + .file(db) } /// whether `extension` is declared in the basedpython prelude. the transpiler @@ -88,7 +98,7 @@ pub(crate) fn extensions_in_module(db: &dyn Db, file: File) -> Box<[StaticClassL if !file.source_type(db).is_basedpython() { return Box::default(); } - let global = global_scope(db, file); + let global = global_scope(db, db.program_file(file)); let mut extensions = Vec::new(); for symbol in place_table(db, global).symbols() { if !symbol.name().starts_with(EXTENSION_SYMBOL_PREFIX) { @@ -130,11 +140,15 @@ pub(crate) fn applicable_extensions(db: &dyn Db, file: File) -> Box<[StaticClass let mut extensions: Vec> = extensions_in_module(db, file).to_vec(); // `imported_modules` deliberately records only `import mod` (see its docs), // so the `from mod import X` forms are collected from the file's own statements - let imported = semantic_index(db, file) + let imported = semantic_index(db, db.program_file(file)) .imported_modules() .chain(crate::types::conversions::from_imported_modules(db, file)); for module_name in imported { - let Some(module) = resolve_module(db, file, module_name) else { + let Some(module) = resolve_module( + db, + ImportingFile::File(file, db.program_file(file).resolver_environment(db)), + module_name, + ) else { continue; }; let Some(module_file) = module.file(db) else { @@ -226,12 +240,17 @@ pub(crate) fn extended_class<'db>( db: &'db dyn Db, extension: StaticClassLiteral<'db>, ) -> Option> { + let env = &ProgramEnvironment::from_file(extension.program_file(db)); let name = extension.name(db); let file = extension.file(db); - let resolved = global_symbol(db, file, name) + let resolved = global_symbol(db, db.program_file(file), name) .place .ignore_possibly_undefined() - .or_else(|| builtins_symbol(db, name).place.ignore_possibly_undefined())?; + .or_else(|| { + builtins_symbol(db, env, name) + .place + .ignore_possibly_undefined() + })?; let literal = resolved.as_class_literal()?; // an extension of an extension makes no sense; the mangled binding makes // this unreachable in practice, but be explicit @@ -361,11 +380,12 @@ pub(crate) struct ExtensionMemberResolution<'db> { /// members — the caller only asks after normal lookup came up undefined pub(crate) fn resolve_extension_member<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, receiver: Type<'db>, name: &str, ) -> Option> { - let mut resolutions = resolve_extension_members(db, file, receiver, name).into_iter(); + let mut resolutions = resolve_extension_members(db, env, file, receiver, name).into_iter(); let mut resolved = resolutions.next()?; resolved.ambiguous_with = resolutions.next().map(|other| other.extension); Some(resolved) @@ -379,6 +399,7 @@ pub(crate) fn resolve_extension_member<'db>( /// extensions disagree — needs each one's own resolution pub(crate) fn resolve_extension_members<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, receiver: Type<'db>, name: &str, @@ -394,7 +415,7 @@ pub(crate) fn resolve_extension_members<'db>( // an instance receiver serves every member kind; a class-object receiver // serves only `static def` / `class def` members - let (receiver_class, instance) = if let Some(class) = receiver.nominal_class(db) { + let (receiver_class, instance) = if let Some(class) = receiver.nominal_class(db, env) { (class, Some(receiver)) } else { match receiver.to_class_type(db) { @@ -408,21 +429,24 @@ pub(crate) fn resolve_extension_members<'db>( // class is not in the interface's lattice let bind = |member: Type<'db>, conformed_as: Option>| { let bind_instance = match (conformed_as, instance) { - (Some(protocol), Some(_)) => Some(Type::instance(db, protocol)), + (Some(protocol), Some(_)) => Some(Type::instance(db, env, protocol)), (_, other) => other, }; let owner = match bind_instance { - Some(instance_ty) => instance_ty.to_meta_type(db), + Some(instance_ty) => instance_ty.to_meta_type(db, env), None => receiver, }; member - .try_call_dunder_get(db, bind_instance, owner) - .map_or(member, |(ty, _)| ty) + .try_call_dunder_get(db, env, bind_instance, owner) + .ok() + .flatten() + .map_or(member, |result| result.return_type) }; let mut resolved = Vec::new(); for &extension in candidates { - let Some(applicable) = applicable_member(db, file, extension, receiver_class, name) else { + let Some(applicable) = applicable_member(db, env, file, extension, receiver_class, name) + else { continue; }; let member = applicable.member; @@ -475,6 +499,7 @@ fn is_conformance<'db>(db: &'db dyn Db, extension: StaticClassLiteral<'db>) -> b /// nothing else does. pub(crate) fn extension_operator<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, receiver: Type<'db>, name: &str, @@ -483,13 +508,13 @@ pub(crate) fn extension_operator<'db>( // instance fallback — so an extension answers exactly where that finds // nothing if !receiver - .member_lookup_with_policy(db, name, MemberLookupPolicy::NO_INSTANCE_FALLBACK) + .member_lookup_with_policy(db, env, name, MemberLookupPolicy::NO_INSTANCE_FALLBACK) .place .is_undefined() { return None; } - let resolution = resolve_extension_member(db, file, receiver, name)?; + let resolution = resolve_extension_member(db, env, file, receiver, name)?; // an operator is invoked, so only a method can answer one — a computed // property named `__pos__` would be read, not called matches!(resolution.kind, ExtensionMemberKind::Method).then_some(resolution) @@ -518,19 +543,21 @@ impl<'db> ExtensionOperator<'db> { pub(crate) fn return_type( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, arguments: &CallArguments<'_, 'db>, ) -> Option> { self.resolution .ty - .try_call(db, arguments) + .try_call(db, env, arguments) .ok() - .map(|bindings| bindings.return_type(db)) + .map(|bindings| bindings.return_type(db, env)) } } /// The extension supplying `op`'s dunder for a unary operator, if any. pub(crate) fn unary_extension_operator<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, op: ast::UnaryOp, operand: Type<'db>, @@ -547,7 +574,7 @@ pub(crate) fn unary_extension_operator<'db>( | ast::UnaryOp::Force => return None, }; Some(ExtensionOperator { - resolution: extension_operator(db, file, operand, dunder)?, + resolution: extension_operator(db, env, file, operand, dunder)?, member: dunder, reflected: false, }) @@ -558,19 +585,20 @@ pub(crate) fn unary_extension_operator<'db>( /// reflected one — the order python itself resolves in. pub(crate) fn binary_extension_operator<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, left: Type<'db>, op: ast::Operator, right: Type<'db>, ) -> Option> { - extension_operator(db, file, left, op.dunder()) + extension_operator(db, env, file, left, op.dunder()) .map(|resolution| ExtensionOperator { resolution, member: op.dunder(), reflected: false, }) .or_else(|| { - extension_operator(db, file, right, op.reflected_dunder()).map(|resolution| { + extension_operator(db, env, file, right, op.reflected_dunder()).map(|resolution| { ExtensionOperator { resolution, member: op.reflected_dunder(), @@ -585,6 +613,7 @@ pub(crate) fn binary_extension_operator<'db>( /// `b.__contains__(a)` — and an identity test has no dunder at all. pub(crate) fn comparison_extension_operator<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, left: Type<'db>, op: ast::CmpOp, @@ -602,7 +631,7 @@ pub(crate) fn comparison_extension_operator<'db>( }; let receiver = if reflected { right } else { left }; Some(ExtensionOperator { - resolution: extension_operator(db, file, receiver, dunder)?, + resolution: extension_operator(db, env, file, receiver, dunder)?, member: dunder, reflected, }) @@ -646,6 +675,7 @@ impl<'db> ExtensionApplication<'db> { /// bound? pub(crate) fn extension_applies<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, extension: StaticClassLiteral<'db>, receiver_class: ClassType<'db>, ) -> Option> { @@ -657,13 +687,14 @@ pub(crate) fn extension_applies<'db>( ClassBase::Class(class) if class.class_literal(db) == target => Some(class), _ => None, })?; - applied_at(db, extension, target, target_class) + applied_at(db, env, extension, target, target_class) } /// [`extension_applies`] once the extended class has been located, with the /// specialization the receiver gives it pub(crate) fn applied_at<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, extension: StaticClassLiteral<'db>, target: ClassLiteral<'db>, target_class: ClassType<'db>, @@ -685,7 +716,7 @@ pub(crate) fn applied_at<'db>( let receiver_argument = receiver_specialization .and_then(|specialization| specialization.get(db, target_var)) .unwrap_or_else(Type::unknown); - if !satisfies_bound(db, receiver_argument, extension_var) { + if !satisfies_bound(db, env, receiver_argument, extension_var) { return None; } bracket_substitution.push(receiver_argument); @@ -724,12 +755,13 @@ struct ApplicableMember<'db> { /// reachable on a conforming type, exactly as they are on the protocol itself fn applicable_member<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, extension: StaticClassLiteral<'db>, receiver_class: ClassType<'db>, name: &str, ) -> Option> { - if let Some(application) = extension_applies(db, extension, receiver_class) { + if let Some(application) = extension_applies(db, env, extension, receiver_class) { let member = own_member(db, extension, name)?; return Some(ApplicableMember { member: application.apply(db, extension, member), @@ -737,8 +769,8 @@ fn applicable_member<'db>( }); } let target = extended_class(db, extension)?; - let conformed = conformance::conformance_for(db, file, receiver_class, target)?; - let application = applied_at(db, extension, target, conformed)?; + let conformed = conformance::conformance_for(db, env, file, receiver_class, target)?; + let application = applied_at(db, env, extension, target, conformed)?; let member = own_member(db, extension, name)?; Some(ApplicableMember { member: application.apply(db, extension, member), @@ -749,16 +781,19 @@ fn applicable_member<'db>( /// does the receiver's type argument satisfy a bracket typevar's bound? fn satisfies_bound<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, argument: Type<'db>, extension_var: BoundTypeVarInstance<'db>, ) -> bool { - match extension_var.typevar(db).bound_or_constraints(db) { + match extension_var.typevar(db).bound_or_constraints(db, env) { None => true, - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => argument.is_assignable_to(db, bound), + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + argument.is_assignable_to(db, env, bound) + } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints .elements(db) .iter() - .any(|constraint| argument.is_assignable_to(db, *constraint)), + .any(|constraint| argument.is_assignable_to(db, env, *constraint)), } } diff --git a/crates/ty_python_semantic/src/types/format.rs b/crates/ty_python_semantic/src/types/format.rs index 7c8fecfb43..aa06b49040 100644 --- a/crates/ty_python_semantic/src/types/format.rs +++ b/crates/ty_python_semantic/src/types/format.rs @@ -18,6 +18,7 @@ use ruff_python_literal::mini_language::{FormatSpecViolation, FormatTarget}; use ruff_python_literal::strftime::{self, DirectiveKind}; use ruff_text_size::{Ranged, TextRange, TextSize}; +use crate::types::ProgramEnvironment; use crate::types::class::{ClassLiteral, ClassType}; use crate::types::class_base::ClassBase; use crate::types::context::InferContext; @@ -83,10 +84,15 @@ pub enum SpecLanguage { /// this is decided by the class that *owns* `__format__`, not by the class of /// the value: a subclass of `int` that adds no `__format__` of its own still /// formats by `int`'s rules -pub fn spec_language<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option { +pub fn spec_language<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Option { let owner = owner_of( db, - ty.erase_restriction(db).nominal_class(db)?, + env, + ty.erase_restriction(db).nominal_class(db, env)?, "__format__", )?; let literal = owner.class_literal(db); @@ -108,19 +114,28 @@ pub fn spec_language<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option(db: &'db dyn Db, ty: Type<'db>) -> Option { - match spec_language(db, ty)? { +pub fn format_target<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Option { + match spec_language(db, env, ty)? { SpecLanguage::MiniLanguage(target) => Some(target), SpecLanguage::Strftime => None, } } /// the class in `class`'s MRO that defines `name` itself -fn owner_of<'db>(db: &'db dyn Db, class: ClassType<'db>, name: &str) -> Option> { +fn owner_of<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + class: ClassType<'db>, + name: &str, +) -> Option> { class .iter_mro(db) .filter_map(ClassBase::into_class) - .find(|base| !base.own_class_member(db, None, name).is_undefined()) + .find(|base| !base.own_class_member(db, env, None, name).is_undefined()) } /// the format spec written in a replacement field @@ -159,10 +174,10 @@ impl<'ast> WrittenSpec<'ast> { } /// the type the spec argument has at the `__format__` call - fn argument_type<'db>(&self, db: &'db dyn Db) -> Type<'db> { + fn argument_type<'db>(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match self.literal { Some(literal) => Type::string_literal(db, literal), - None => KnownClass::Str.to_instance(db), + None => KnownClass::Str.to_instance(db, env), } } @@ -182,11 +197,12 @@ pub(crate) fn check_interpolation<'db>( element: &ast::InterpolatedElement, value_ty: Type<'db>, ) { + let env = context.program_environment(); let db = context.db(); // a use-site modifier says nothing about how the value renders: `A()` is // inferred as `final A`, and it is `A` that has or lacks a `__format__` let value_ty = value_ty.erase_restriction(db); - let formatted = converted(db, value_ty, element.conversion); + let formatted = converted(db, env, value_ty, element.conversion); let spec = WrittenSpec::of(element); // the conversion does not excuse the value — `!r` is a request for the very @@ -214,8 +230,9 @@ pub(crate) fn check_interpolation<'db>( && formatted .try_call_dunder( db, + env, "__format__", - CallArguments::positional([spec.argument_type(db)]), + CallArguments::positional([spec.argument_type(db, env)]), TypeContext::default(), ) .is_err() @@ -310,16 +327,17 @@ pub(crate) fn check_stringifying_call<'db>( /// rather than by the value's own fn converted<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, value_ty: Type<'db>, conversion: ast::ConversionFlag, ) -> Type<'db> { match conversion { ast::ConversionFlag::None => value_ty, - ast::ConversionFlag::Str => value_ty.str(db), - ast::ConversionFlag::Repr => value_ty.repr(db), + ast::ConversionFlag::Str => value_ty.str(db, env), + ast::ConversionFlag::Repr => value_ty.repr(db, env), // `ascii` is `repr` with the non-ascii escaped, which cannot be read // off the type - ast::ConversionFlag::Ascii => KnownClass::Str.to_instance(db), + ast::ConversionFlag::Ascii => KnownClass::Str.to_instance(db, env), } } @@ -329,9 +347,12 @@ fn report_rejected_spec<'db>( spec: &WrittenSpec<'_>, formatted: Type<'db>, ) { + let env = context.program_environment(); let db = context.db(); // the rejection may be nothing but a stub's silence, which settles nothing - if inherits_object_format(db, formatted) && !declares_what_it_implements(db, formatted) { + if inherits_object_format(db, env, formatted) + && !declares_what_it_implements(db, env, formatted) + { return; } let Some(builder) = context.report_lint(&INVALID_FORMAT_SPEC, spec.range) else { @@ -340,13 +361,13 @@ fn report_rejected_spec<'db>( let written = spec.literal.unwrap_or_default(); let mut diagnostic = builder.into_diagnostic(format_args!( "`{written}` is not a valid format spec for `{}`", - formatted.display(db) + formatted.display(db, env) )); // the overwhelmingly common cause is a class that never opted in - if inherits_object_format(db, formatted) { + if inherits_object_format(db, env, formatted) { diagnostic.info(format_args!( "`{}` inherits `object.__format__`, which accepts only the empty spec", - formatted.display(db) + formatted.display(db, env) )); diagnostic.help("define `__format__` to give the class a format spec of its own"); } @@ -363,8 +384,12 @@ fn report_rejected_spec<'db>( /// /// (`implicit-object-repr` distrusts even the vendored stubs, because typeshed /// omits `__str__` and `__repr__` wholesale and we have not patched that) -fn declares_what_it_implements<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { - let Some(class) = ty.nominal_class(db) else { +fn declares_what_it_implements<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> bool { + let Some(class) = ty.nominal_class(db, env) else { return false; }; class @@ -378,9 +403,13 @@ fn declares_what_it_implements<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { } /// whether the `__format__` that would be called is `object`'s own -fn inherits_object_format<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { - ty.nominal_class(db) - .and_then(|class| owner_of(db, class, "__format__")) +fn inherits_object_format<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> bool { + ty.nominal_class(db, env) + .and_then(|class| owner_of(db, env, class, "__format__")) .is_some_and(|owner| owner.class_literal(db).known(db) == Some(KnownClass::Object)) } @@ -390,8 +419,9 @@ fn check_spec_content<'db>( spec: &WrittenSpec<'_>, formatted: Type<'db>, ) { + let env = context.program_environment(); let db = context.db(); - let (Some(written), Some(language)) = (spec.literal, spec_language(db, formatted)) else { + let (Some(written), Some(language)) = (spec.literal, spec_language(db, env, formatted)) else { return; }; let target = match language { @@ -504,6 +534,7 @@ pub(crate) fn check_implicit_object_repr<'db>( value_ty: Type<'db>, rendering: Rendering, ) { + let env = context.program_environment(); let db = context.db(); // a stub describes an interface and never runs, so no rendering of it // reaches anyone @@ -511,7 +542,7 @@ pub(crate) fn check_implicit_object_repr<'db>( return; } let settings = db.analysis_settings(context.file()); - let Some(class) = unrendered_class(db, settings, value_ty, rendering) else { + let Some(class) = unrendered_class(db, env, settings, value_ty, rendering) else { return; }; let Some(builder) = context.report_lint(&IMPLICIT_OBJECT_REPR, at) else { @@ -520,7 +551,7 @@ pub(crate) fn check_implicit_object_repr<'db>( let dunders = rendering.dunder_list(); // the class is named rather than the value, because it is the class that // is missing something — `print(some_function)` is about `FunctionType` - let named = Type::instance(db, class).display(db).to_string(); + let named = Type::instance(db, env, class).display(db, env).to_string(); let mut diagnostic = builder.into_diagnostic(format_args!("`{named}` has no {dunders} of its own")); diagnostic.info( @@ -538,9 +569,13 @@ pub(crate) fn check_implicit_object_repr<'db>( /// most values are instances, and their own class is the answer. a value that /// *is* a class or a function is an instance of its meta type — `type` and /// `types.FunctionType` — which is what decides how it prints -fn runtime_class<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option> { - ty.nominal_class(db) - .or_else(|| ty.to_meta_type(db).to_class_type(db)) +fn runtime_class<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Option> { + ty.nominal_class(db, env) + .or_else(|| ty.to_meta_type(db, env).to_class_type(db)) } /// whether `class` is one of the configured class names @@ -565,13 +600,14 @@ fn is_named<'db>( /// the value renders by whatever default the interpreter has fn unrendered_class<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, settings: &AnalysisSettings, ty: Type<'db>, rendering: Rendering, ) -> Option> { // `A()` is inferred as `final A`; it is `A` that has or lacks a rendering let ty = ty.erase_restriction(db); - let class = runtime_class(db, ty)?; + let class = runtime_class(db, env, ty)?; // `object` itself is the one class whose bare repr is not a mistake: it is // what the author asked for if class.class_literal(db).known(db) == Some(KnownClass::Object) { @@ -617,7 +653,7 @@ fn unrendered_class<'db>( // asked of an instance of the class, so a value that *is* a class or a // function is asked about `type` and `types.FunctionType` rather than about // its own members - let instance = Type::instance(db, class); + let instance = Type::instance(db, env, class); rendering .dunders() .iter() @@ -625,6 +661,7 @@ fn unrendered_class<'db>( instance .member_lookup_with_policy( db, + env, dunder, MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK | MemberLookupPolicy::NO_INSTANCE_FALLBACK, diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 475d0406d1..b018caf346 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -49,9 +49,11 @@ //! 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 std::{borrow::Cow, str::FromStr}; use bitflags::bitflags; +use itertools::Either; +use ruff_db::PythonFile; use ruff_db::diagnostic::{Annotation, DiagnosticId, Severity, Span}; use ruff_db::files::{File, FileRange}; use ruff_db::parsed::{ParsedModuleRef, parsed_module}; @@ -62,7 +64,7 @@ use ruff_python_ast::helpers::{last_bound_parameter, parameter_modifiers}; use ruff_python_ast::{self as ast, OperatorPrecedence, ParameterWithDefault}; use ruff_text_size::Ranged; use salsa::plumbing::AsId; -use ty_module_resolver::{KnownModule, ModuleName, file_to_module, resolve_module}; +use ty_module_resolver::{ImportingFile, KnownModule, ModuleName, file_to_module, resolve_module}; use crate::place::{DefinedPlace, Definedness, Place, place_from_bindings}; use crate::types::call::{Binding, CallArguments}; @@ -80,7 +82,7 @@ use crate::types::diagnostic::{ }; use crate::types::display::DisplaySettings; use crate::types::generics::{ApplySpecialization, GenericContext, typing_self}; -use crate::types::infer::{nearest_enclosing_class, original_class_type}; +use crate::types::infer::{infer_definition_types, nearest_enclosing_class, original_class_type}; use crate::types::inferred_signature::inferred_return_type; use crate::types::known_instance::DeprecatedInstance; use crate::types::list_members::all_members; @@ -95,13 +97,14 @@ use crate::types::{ CallableType, ClassBase, ClassLiteral, ClassType, FindLegacyTypeVarsVisitor, IntersectionBuilder, KnownClass, KnownInstanceType, MemberLookupPolicy, SpecialFormType, SubclassOfInner, SubclassOfType, Truthiness, Type, TypeContext, TypeMapping, - TypeVarBoundOrConstraints, UnionBuilder, UnionType, definition_expression_type, walk_signature, + TypeVarBoundOrConstraints, UnionBuilder, UnionType, binding_type, definition_expression_type, + walk_signature, }; -use crate::{Db, FxOrderSet}; +use crate::{Db, FxOrderSet, ProgramEnvironment}; use ty_python_core::ast_ids::HasScopedUseId; use ty_python_core::definition::Definition; use ty_python_core::scope::ScopeId; -use ty_python_core::{FileScopeId, SemanticIndex, semantic_index}; +use ty_python_core::{FileScopeId, ProgramFile, SemanticIndex, semantic_index}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] struct RecursiveTypeNormalizationKey { @@ -232,6 +235,7 @@ impl FunctionDecorators { /// user wrote `@typing.final` etc pub(crate) fn synthetic_decorator_target_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, decorator: &ast::Decorator, ) -> Option> { @@ -257,7 +261,7 @@ pub(crate) fn synthetic_decorator_target_type<'db>( ), _ => None, } { - let params = crate::types::DataclassParams::from_flags(db, flags); + let params = crate::types::DataclassParams::from_flags(db, env, flags); return Some(Type::DataclassDecorator(params)); } @@ -285,7 +289,7 @@ pub(crate) fn synthetic_decorator_target_type<'db>( _ => return None, }; modules.iter().find_map(|module| { - crate::place::known_module_symbol(db, *module, member) + crate::place::known_module_symbol(db, env, *module, member) .place .ignore_possibly_undefined() }) @@ -449,6 +453,14 @@ impl<'db> OverloadLiteral<'db> { self.body_scope(db).file(db) } + pub(crate) fn python_file(self, db: &'db dyn Db) -> PythonFile<'db> { + self.body_scope(db).python_file(db) + } + + pub(crate) fn program_file(self, db: &'db dyn Db) -> ProgramFile<'db> { + self.body_scope(db).program_file(db) + } + pub(crate) fn has_known_decorator(self, db: &dyn Db, decorator: FunctionDecorators) -> bool { self.decorators(db).contains(decorator) } @@ -471,8 +483,12 @@ impl<'db> OverloadLiteral<'db> { /// optional final implementation), so ty must see them as overloads here /// for type checks to align with the emitted Python output fn is_implicit_overload(self, db: &dyn Db) -> bool { - let module = parsed_module(db, self.file(db)).load(db); - let func_node = self.body_scope(db).node(db).expect_function().node(&module); + // the scope carries the program this query is running under; asking the db for + // the file's *default* program would resolve the node against a different python + // version when the same file is checked under several + let body_scope = self.body_scope(db); + let module = parsed_module(db, body_scope.python_file(db)).load(db); + let func_node = body_scope.node(db).expect_function().node(&module); let body_is_stub_shaped = func_node.body.is_empty() || matches!( func_node.body.as_slice(), @@ -482,7 +498,7 @@ impl<'db> OverloadLiteral<'db> { return false; } let scope = self.definition(db).scope(db); - let index = semantic_index(db, scope.file(db)); + let index = semantic_index(db, scope.program_file(db)); let use_def = index.use_def_map(scope.file_scope_id(db)); let Some(symbol_id) = index .place_table(scope.file_scope_id(db)) @@ -517,7 +533,7 @@ impl<'db> OverloadLiteral<'db> { if !source_type.is_basedpython() { return Box::default(); } - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, db.program_file(file).python_file(db)).load(db); let node = self.body_scope(db).node(db).expect_function().node(&module); let source = source_text(db, file); crate::reified::reified_type_param_names(source.as_str(), source_type, node) @@ -539,7 +555,7 @@ impl<'db> OverloadLiteral<'db> { return Box::default(); } let file = self.file(db); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, db.program_file(file).python_file(db)).load(db); let node = self.body_scope(db).node(db).expect_function().node(&module); let Some(type_params) = node.type_params.as_deref() else { return Box::default(); @@ -566,7 +582,7 @@ impl<'db> OverloadLiteral<'db> { db: &'db dyn Db, ) -> CallbackParameterModifiers { let file = self.file(db); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, db.program_file(file).python_file(db)).load(db); let parameters = &self.node(db, file, &module).parameters; let source = source_text(db, file); @@ -633,14 +649,14 @@ impl<'db> OverloadLiteral<'db> { /// Returns true if this overload is decorated with `@staticmethod`, or if it is implicitly a /// staticmethod. - pub(crate) fn is_staticmethod(self, db: &dyn Db) -> bool { + fn is_staticmethod(self, db: &dyn Db) -> bool { self.has_known_decorator(db, FunctionDecorators::STATICMETHOD) || is_implicit_staticmethod(self.name(db)) } /// Returns true if this overload is decorated with `@classmethod`, or if it is implicitly a /// classmethod. - pub(crate) fn is_classmethod(self, db: &dyn Db) -> bool { + fn is_classmethod(self, db: &dyn Db) -> bool { self.has_known_decorator(db, FunctionDecorators::CLASSMETHOD) || is_implicit_classmethod(self.name(db)) } @@ -688,24 +704,28 @@ impl<'db> OverloadLiteral<'db> { /// Iterate through the decorators on this function, returning the span of the first one /// that matches the given predicate. - pub(super) fn find_decorator_span( + fn find_decorator_span( self, db: &'db dyn Db, predicate: impl Fn(Type<'db>) -> bool, ) -> Option { let definition = self.definition(db); let file = definition.file(db); - self.node(db, file, &parsed_module(db, file).load(db)) - .decorator_list - .iter() - .find(|decorator| { - predicate(definition_expression_type( - db, - definition, - &decorator.expression, - )) - }) - .map(|decorator| Span::from(file).with_range(decorator.range)) + self.node( + db, + file, + &parsed_module(db, definition.python_file(db)).load(db), + ) + .decorator_list + .iter() + .find(|decorator| { + predicate(definition_expression_type( + db, + definition, + &decorator.expression, + )) + }) + .map(|decorator| Span::from(file).with_range(decorator.range)) } /// Iterate through the decorators on this function, returning the span of the first one @@ -744,7 +764,7 @@ impl<'db> OverloadLiteral<'db> { /// over-invalidation. pub(super) 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)); + let index = semantic_index(db, body_scope.program_file(db)); index.expect_single_definition(body_scope.node(db).expect_function()) } @@ -754,26 +774,38 @@ impl<'db> OverloadLiteral<'db> { // 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 module = parsed_module(db, self.python_file(db)).load(db); + let use_def = + semantic_index(db, scope.program_file(db)).use_def_map(scope.file_scope_id(db)); let use_id = self .body_scope(db) .node(db) .expect_function() .node(&module) .name - .scoped_use_id(db, self.file(db)); + .scoped_use_id(db, self.program_file(db)); + let env = ProgramEnvironment::from_scope(scope); let Place::Defined(DefinedPlace { - ty: Type::FunctionLiteral(previous_type), + ty: previous_type, definedness: Definedness::AlwaysDefined, + provenance, .. - }) = place_from_bindings(db, use_def.bindings_at_use(use_id)).place + }) = place_from_bindings(db, &env, use_def.bindings_at_use(use_id)).place else { return None; }; - let previous_literal = previous_type.literal(db); + let previous_literal = match previous_type { + Type::FunctionLiteral(previous_type) => previous_type.literal(db), + Type::Callable(_) => { + let definition = provenance.definition()?; + infer_definition_types(db, definition) + .function_type(definition)? + .literal(db) + } + _ => return None, + }; let previous_overload = previous_literal.last_definition; if !previous_overload.is_overload(db) { return None; @@ -803,22 +835,37 @@ impl<'db> OverloadLiteral<'db> { /// 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) -> Signature<'db> { - let mut signature = self.raw_signature(db, ReturnCallableTypeVarScope::Public); - let scope = self.body_scope(db); - let module = parsed_module(db, self.file(db)).load(db); + let program_file = self.program_file(db); + let python_file = program_file.python_file(db); + let mut signature = self.raw_signature(db, ReturnCallableTypeVarScope::Public); + let module = parsed_module(db, python_file).load(db); let function_node = scope.node(db).expect_function().node(&module); - let index = semantic_index(db, scope.file(db)); + let index = semantic_index(db, program_file); let file_scope_id = scope.file_scope_id(db); let is_generator = file_scope_id.is_generator_function(index); if function_node.is_async && !is_generator { - signature = signature.wrap_coroutine_return_type(db); + let env = ProgramEnvironment::from_file(program_file); + signature = signature.wrap_coroutine_return_type(db, &env); } signature } + /// Returns the effective signatures of this overload after applying decorators. + pub(crate) fn decorated_signatures( + self, + db: &'db dyn Db, + ) -> impl Iterator> + Clone + 'db { + match binding_type(db, self.definition(db)) { + Type::Callable(callable) => { + Either::Left(callable.signatures(db).overloads.iter().cloned()) + } + _ => Either::Right(std::iter::once(self.signature(db))), + } + } + /// Typed internally-visible "raw" signature for this function. /// That is, the return types of async functions are not wrapped in `CoroutineType[...]`. /// The `return_callable_typevar_scope` controls whether type variables that only appear in a @@ -890,11 +937,14 @@ impl<'db> OverloadLiteral<'db> { .is_some_and(|class| class.is_protocol(db)) } + let env = &ProgramEnvironment::from_scope(self.body_scope(db)); let scope = self.body_scope(db); - let module = parsed_module(db, self.file(db)).load(db); + let program_file = self.program_file(db); + let python_file = program_file.python_file(db); + let module = parsed_module(db, python_file).load(db); let function_stmt_node = scope.node(db).expect_function().node(&module); let definition = self.definition(db); - let index = semantic_index(db, scope.file(db)); + let index = semantic_index(db, program_file); let pep695_ctx = function_stmt_node.type_params.as_ref().map(|type_params| { GenericContext::from_type_params(db, index, definition, type_params) }); @@ -910,6 +960,7 @@ impl<'db> OverloadLiteral<'db> { let mut raw_signature = Signature::from_function( db, + env, pep695_ctx, definition, function_stmt_node, @@ -918,8 +969,10 @@ impl<'db> OverloadLiteral<'db> { ); let generic_context = raw_signature.generic_context; - raw_signature.add_implicit_self_annotation(db, || { - if self.is_staticmethod(db) { + raw_signature.add_implicit_self_annotation(db, env, || { + let is_staticmethod = self.is_staticmethod(db); + let is_dunder_new = self.name(db) == "__new__"; + if is_staticmethod && !is_dunder_new { return None; } @@ -944,9 +997,9 @@ impl<'db> OverloadLiteral<'db> { { let body_view = crate::types::extensions::body_view_class(db, static_literal)?; return Some(if self.takes_implicit_class_receiver(db) { - SubclassOfType::from(db, SubclassOfInner::Class(body_view)) + SubclassOfType::from(db, env, SubclassOfInner::Class(body_view)) } else { - Type::instance(db, body_view) + Type::instance(db, env, body_view) }); } @@ -974,7 +1027,7 @@ impl<'db> OverloadLiteral<'db> { if method_has_explicit_self || class_is_generic || class_is_fallback { let scope_id = definition.scope(db); let typevar_binding_context = Some(definition); - let index = semantic_index(db, scope_id.file(db)); + let index = semantic_index(db, scope_id.program_file(db)); let class = nearest_enclosing_class(db, index, scope_id).unwrap(); let typing_self = typing_self(db, scope_id, typevar_binding_context, class.into()) @@ -983,9 +1036,10 @@ impl<'db> OverloadLiteral<'db> { for an implicit self: Self annotation", ); - if self.takes_implicit_class_receiver(db) { + if self.takes_implicit_class_receiver(db) || is_dunder_new { Some(SubclassOfType::from( db, + env, SubclassOfInner::TypeVar(typing_self), )) } else { @@ -994,13 +1048,14 @@ impl<'db> OverloadLiteral<'db> { } else { // If skip creating the typevar, we use "instance of class" or "subclass of // class" as the implicit annotation instead. - if self.takes_implicit_class_receiver(db) { + if self.takes_implicit_class_receiver(db) || is_dunder_new { Some(SubclassOfType::from( db, + env, SubclassOfInner::Class(ClassType::NonGeneric(class_literal)), )) } else { - Some(class_literal.to_non_generic_instance(db)) + Some(class_literal.to_non_generic_instance(db, env)) } } }); @@ -1022,6 +1077,7 @@ impl<'db> OverloadLiteral<'db> { .collect(); raw_signature.inherit_unannotated_from_overloads( db, + env, &overload_sigs, function_stmt_node.returns.is_none(), ); @@ -1036,10 +1092,11 @@ impl<'db> OverloadLiteral<'db> { // than with what the base already declared if infers_unannotated_signatures(db, self.file(db)) && raw_signature.has_inherited_annotations_to_fill(function_stmt_node.returns.is_none()) - && let Some(base_signature) = self.overridden_signature(db) + && let Some(base_signature) = self.overridden_signature(db, env) { raw_signature.inherit_unannotated_from_overloads( db, + env, std::slice::from_ref(&base_signature), function_stmt_node.returns.is_none(), ); @@ -1051,7 +1108,8 @@ impl<'db> OverloadLiteral<'db> { && function_stmt_node.returns.is_none() && !function_stmt_node.is_asserts_return && raw_signature.return_ty.is_unknown() - && let OverriddenReturnType::Declared(base_return) = self.overridden_return_type(db) + && let OverriddenReturnType::Declared(base_return) = + self.overridden_return_type(db, env) { raw_signature.return_ty = base_return; } @@ -1061,6 +1119,7 @@ impl<'db> OverloadLiteral<'db> { if infers_unannotated_signatures(db, self.file(db)) { raw_signature.open_unannotated_parameter_holes( db, + env, definition, self.binds_first_parameter(db), ); @@ -1077,7 +1136,7 @@ impl<'db> OverloadLiteral<'db> { && function_stmt_node.returns.is_none() && !function_stmt_node.is_asserts_return && raw_signature.return_ty.is_unknown() - && self.recovers_return_type_from_body(db) + && self.recovers_return_type_from_body(db, env) { raw_signature.return_ty = inferred_return_type(db, self); } @@ -1099,6 +1158,7 @@ impl<'db> OverloadLiteral<'db> { pub(crate) fn return_type_without_annotation( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, from_body: impl FnOnce() -> Type<'db>, ) -> Type<'db> { let mut return_ty = Type::unknown(); @@ -1110,6 +1170,7 @@ impl<'db> OverloadLiteral<'db> { if !overload_list.is_empty() { return_ty = UnionType::from_elements( db, + env, overload_list.iter().map(|overload| { overload .raw_signature(db, ReturnCallableTypeVarScope::Public) @@ -1120,21 +1181,22 @@ impl<'db> OverloadLiteral<'db> { } if db.analysis_settings(self.file(db)).sound_types - && let Some(base_signature) = self.overridden_signature(db) + && let Some(base_signature) = self.overridden_signature(db, env) { return_ty = base_signature.return_ty; } if return_ty.is_unknown() && infers_unannotated_signatures(db, self.file(db)) - && let OverriddenReturnType::Declared(base_return) = self.overridden_return_type(db) + && let OverriddenReturnType::Declared(base_return) = + self.overridden_return_type(db, env) { return_ty = base_return; } if infers_unannotated_signatures(db, self.file(db)) && return_ty.is_unknown() - && self.recovers_return_type_from_body(db) + && self.recovers_return_type_from_body(db, env) { return_ty = from_body(); } @@ -1151,8 +1213,12 @@ impl<'db> OverloadLiteral<'db> { /// a base that declares one is still where a missing annotation should draw from: /// `def __len__(self): ...` under `Collection.__len__` returns `int`, not the `None` that /// running its placeholder body would give. - fn overridden_return_type(self, db: &'db dyn Db) -> OverriddenReturnType<'db> { - let Some(base) = self.overridden_method(db) else { + fn overridden_return_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> OverriddenReturnType<'db> { + let Some(base) = self.overridden_method(db, env) else { return OverriddenReturnType::NotOverridden; }; if base.signature(db).overloads.len() != 1 { @@ -1166,7 +1232,9 @@ impl<'db> OverloadLiteral<'db> { } // a type variable in the base's return type is bound to the base method's scope, so // copying it here would silently rebind it - if any_over_type(db, return_ty, false, |ty| matches!(ty, Type::TypeVar(_))) { + if any_over_type(db, env, return_ty, false, |ty| { + matches!(ty, Type::TypeVar(_)) + }) { return OverriddenReturnType::Inexpressible; } OverriddenReturnType::Declared(return_ty) @@ -1189,10 +1257,14 @@ impl<'db> OverloadLiteral<'db> { /// [`OverloadLiteral::raw_signature`] and [`OverloadLiteral::return_type_without_annotation`] /// both consult this, so the signature and the `redundant-return-annotation` lint cannot /// disagree about whether the body was the source. - fn recovers_return_type_from_body(self, db: &'db dyn Db) -> bool { + fn recovers_return_type_from_body( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> bool { self.name(db) != "__new__" && !matches!( - self.overridden_return_type(db), + self.overridden_return_type(db, env), OverriddenReturnType::Inexpressible ) } @@ -1205,8 +1277,12 @@ impl<'db> OverloadLiteral<'db> { /// name, when the base is overloaded, or when the base signature mentions a type variable. /// Type variables (including the implicit `Self`) are bound to the *base* method's scope, so /// copying them into this signature would silently rebind them. - fn overridden_signature(self, db: &'db dyn Db) -> Option> { - let base = self.overridden_method(db)?; + fn overridden_signature( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { + let base = self.overridden_method(db, env)?; if base.signature(db).overloads.len() != 1 { return None; @@ -1214,7 +1290,7 @@ impl<'db> OverloadLiteral<'db> { let signature = base_raw_signature(db, base); let mentions_typevar = - |ty: Type<'db>| any_over_type(db, ty, false, |ty| matches!(ty, Type::TypeVar(_))); + |ty: Type<'db>| any_over_type(db, env, ty, false, |ty| matches!(ty, Type::TypeVar(_))); if mentions_typevar(signature.return_ty) || signature .parameters() @@ -1229,10 +1305,14 @@ impl<'db> OverloadLiteral<'db> { /// basedpython: the method this method overrides, found by looking `self`'s name up in the /// enclosing class's MRO starting *after* the class itself — the same lookup `super()` performs. - fn overridden_method(self, db: &'db dyn Db) -> Option> { + fn overridden_method( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { let definition = self.definition(db); let file = definition.file(db); - let index = semantic_index(db, file); + let index = semantic_index(db, db.program_file(file)); let class_scope_id = definition.scope(db); let class_scope = index.scope(class_scope_id.file_scope_id(db)); @@ -1244,7 +1324,7 @@ impl<'db> OverloadLiteral<'db> { // skip the class itself, so we find what this method overrides rather than the method let mro = class_literal.iter_mro(db).skip(1); let member = - class_literal.class_member_from_mro(db, name, MemberLookupPolicy::default(), mro); + class_literal.class_member_from_mro(db, env, name, MemberLookupPolicy::default(), mro); match member.place.ignore_possibly_undefined()? { Type::FunctionLiteral(base) => Some(base), @@ -1259,7 +1339,7 @@ impl<'db> OverloadLiteral<'db> { ) -> (Span, Span) { let file = self.file(db); let span = Span::from(file); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, self.python_file(db)).load(db); let func_def = self.node(db, file, &module); let range = parameter_index .and_then(|parameter_index| { @@ -1278,7 +1358,7 @@ impl<'db> OverloadLiteral<'db> { pub(crate) fn spans(self, db: &'db dyn Db) -> FunctionSpans { let file = self.file(db); let span = Span::from(file); - let module = parsed_module(db, self.file(db)).load(db); + let module = parsed_module(db, self.python_file(db)).load(db); let func_def = self.node(db, file, &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); @@ -1313,6 +1393,36 @@ impl<'db> FunctionLiteral<'db> { } } + /// Ignore previous overloads when applying decorators to an individual definition. + pub(super) const fn without_overloads(self) -> Self { + Self { + overloaded: false, + ..self + } + } + + /// Preserve the overload set and last-definition identity while updating decorator metadata. + pub(super) fn with_last_definition_metadata( + self, + db: &'db dyn Db, + decorated: OverloadLiteral<'db>, + ) -> Self { + let definition = self.last_definition; + Self { + last_definition: OverloadLiteral::new( + db, + definition.name(db), + definition.known(db), + definition.body_scope(db), + definition.decorators(db), + decorated.deprecated(db), + decorated.dataclass_transformer_params(db), + definition.has_explicit_return_annotation(db), + ), + ..self + } + } + 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.name(db) @@ -1324,7 +1434,7 @@ impl<'db> FunctionLiteral<'db> { self.last_definition.known(db) } - fn has_known_decorator(self, db: &dyn Db, decorator: FunctionDecorators) -> bool { + fn has_known_decorator(self, db: &'db dyn Db, decorator: FunctionDecorators) -> bool { self.iter_overloads_and_implementation(db) .any(|overload| overload.decorators(db).contains(decorator)) } @@ -1393,9 +1503,8 @@ impl<'db> FunctionLiteral<'db> { (overloads.as_ref(), *implementation) } - fn has_separate_implementation(self, db: &'db dyn Db) -> bool { - !self.last_definition.is_overload(db) - && self.last_definition.previous_overload(db).is_some() + pub(super) fn has_separate_implementation(self, db: &'db dyn Db) -> bool { + self.overloaded && !self.last_definition.is_overload(db) } pub(super) fn iter_overloads_and_implementation( @@ -1427,7 +1536,22 @@ impl<'db> FunctionLiteral<'db> { return CallableSignature::single(implementation.signature(db)); } - CallableSignature::from_overloads(overloads.iter().map(|overload| overload.signature(db))) + CallableSignature::from_overloads(overloads.iter().enumerate().flat_map( + |(source_overload_index, overload)| { + // The last overload may still be inferred, so querying its binding would create a cycle. + if *overload == self.last_definition { + Either::Left(std::iter::once( + overload + .signature(db) + .with_source_overload_index(Some(source_overload_index)), + )) + } else { + Either::Right(overload.decorated_signatures(db).map(move |signature| { + signature.with_source_overload_index(Some(source_overload_index)) + })) + } + }, + )) } /// Typed externally-visible signature of the last overload or implementation of this function. @@ -1469,7 +1593,7 @@ impl<'db> FunctionLiteral<'db> { /// statements, or if it is a `Protocol` method that only has a docstring, /// or if it is a `Protocol` method whose body only consists of a single /// `raise NotImplementedError` statement. - pub(super) fn as_abstract_method( + fn as_abstract_method( self, db: &'db dyn Db, enclosing_class: ClassType<'db>, @@ -1503,10 +1627,13 @@ impl<'db> FunctionLiteral<'db> { implementation: OverloadLiteral<'db>, ) -> FunctionBodyKind { let definition = implementation.definition(db); - let file = definition.file(db); - let module = parsed_module(db, file).load(db); + let program_file = definition.program_file(db); + let python_file = program_file.python_file(db); + let env = ProgramEnvironment::from_file(program_file); + let file = python_file.file(db); + let module = parsed_module(db, python_file).load(db); let node = implementation.node(db, file, &module); - function_body_kind(db, node, |expr| { + function_body_kind(db, &env, node, |expr| { definition_expression_type(db, definition, expr) }) } @@ -1525,7 +1652,7 @@ impl<'db> FunctionLiteral<'db> { /// /// Methods defined in stub files are never considered to have trivial bodies, /// since stubs use `...` as a placeholder regardless of the runtime implementation. - pub(crate) fn has_trivial_body(self, db: &'db dyn Db) -> bool { + fn has_trivial_body(self, db: &'db dyn Db) -> bool { !self.definition(db).file(db).is_stub(db) && matches!( self.body_kind(db), @@ -1627,22 +1754,23 @@ pub struct UpdatedFunctionSignatures<'db> { /// See also: [`FunctionLiteral::signature`]. signature: Option>, - /// Contains a potentially modified signature for the implementation of an overloaded function, - /// in case certain operations (like type mappings) have been applied to it. + /// Contains the potentially modified callables for the implementation of an overloaded + /// function, in case decorators or type mappings have been applied to it. Each callable can + /// itself be overloaded. /// /// See also: [`FunctionLiteral::last_definition_signature`]. - implementation_signature: Option>, + implementation_callables: Option]>>, } impl<'db> UpdatedFunctionSignatures<'db> { fn new( signature: Option>, - implementation_signature: Option>, + implementation_callables: Option]>>, ) -> Option> { - (signature.is_some() || implementation_signature.is_some()).then(|| { + (signature.is_some() || implementation_callables.is_some()).then(|| { Box::new(Self { signature, - implementation_signature, + implementation_callables, }) }) } @@ -1672,8 +1800,10 @@ pub(super) fn walk_function_type<'db, V: super::visitor::TypeVisitor<'db> + ?Siz walk_signature(db, signature, visitor); } } - if let Some(signature) = function.updated_implementation_signature(db) { - walk_signature(db, signature, visitor); + if let Some(callables) = function.updated_implementation_callables(db) { + for callable in callables { + visitor.visit_callable_type(db, *callable); + } } } @@ -1686,9 +1816,48 @@ impl<'db> FunctionType<'db> { } fn updated_implementation_signature(self, db: &'db dyn Db) -> Option<&'db Signature<'db>> { + let [callable] = self.updated_implementation_callables(db)? else { + return None; + }; + let [signature] = callable.signatures(db).overloads.as_slice() else { + return None; + }; + Some(signature) + } + + fn updated_implementation_callables(self, db: &'db dyn Db) -> Option<&'db [CallableType<'db>]> { self.updated_signatures(db) .as_deref() - .and_then(|updated| updated.implementation_signature.as_ref()) + .and_then(|updated| updated.implementation_callables.as_deref()) + } + + /// Return all effective implementation callables, falling back to the raw implementation. + pub(super) fn implementation_callables(self, db: &'db dyn Db) -> Cow<'db, [CallableType<'db>]> { + self.updated_implementation_callables(db).map_or_else( + || { + Cow::Owned(vec![CallableType::single( + db, + self.last_definition_signature(db).clone(), + )]) + }, + Cow::Borrowed, + ) + } + + /// Retain decorated implementation callables without changing the caller-visible overloads. + pub(super) fn with_implementation_callables( + self, + db: &'db dyn Db, + implementation_callables: Box<[CallableType<'db>]>, + ) -> Self { + Self::new( + db, + self.literal(db), + UpdatedFunctionSignatures::new( + self.updated_signature(db).cloned(), + Some(implementation_callables), + ), + ) } pub(crate) fn with_inherited_generic_context( @@ -1700,17 +1869,27 @@ impl<'db> FunctionType<'db> { .signature(db) .with_inherited_generic_context(db, inherited_generic_context); let literal = self.literal(db); - let updated_implementation_signature = literal.has_separate_implementation(db).then(|| { - self.last_definition_signature(db) - .clone() - .with_inherited_generic_context(db, inherited_generic_context) + let updated_implementation_callables = literal.has_separate_implementation(db).then(|| { + self.implementation_callables(db) + .iter() + .map(|callable| { + CallableType::new( + db, + callable + .signatures(db) + .with_inherited_generic_context(db, inherited_generic_context), + callable.kind(db), + callable.provenance(db), + ) + }) + .collect() }); Self::new( db, literal, UpdatedFunctionSignatures::new( Some(updated_signature), - updated_implementation_signature, + updated_implementation_callables, ), ) } @@ -1720,12 +1899,12 @@ impl<'db> FunctionType<'db> { db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { // Returned-callable rescoping and type-alias specialization should not rebuild signatures from the // function literal; doing so can re-enter recursive `TypeOf` evaluation. let literal = self.literal(db); - let (updated_signature, updated_implementation_signature) = if matches!( + let (updated_signature, updated_implementation_callables) = if matches!( type_mapping, TypeMapping::ApplySpecialization( ApplySpecialization::ReturnCallables(_) | ApplySpecialization::TypeAlias(_) @@ -1739,8 +1918,13 @@ impl<'db> FunctionType<'db> { self.updated_signature(db).map(|signature| { signature.apply_type_mapping_impl(db, type_mapping, tcx, visitor) }), - self.updated_implementation_signature(db).map(|signature| { - signature.apply_type_mapping_impl(db, type_mapping, tcx, visitor) + self.updated_implementation_callables(db).map(|callables| { + callables + .iter() + .map(|callable| { + callable.apply_type_mapping_impl(db, type_mapping, tcx, visitor) + }) + .collect() }), ) } else { @@ -1750,23 +1934,23 @@ impl<'db> FunctionType<'db> { .apply_type_mapping_impl(db, type_mapping, tcx, visitor), ), literal.has_separate_implementation(db).then(|| { - self.last_definition_signature(db).apply_type_mapping_impl( - db, - type_mapping, - tcx, - visitor, - ) + self.implementation_callables(db) + .iter() + .map(|callable| { + callable.apply_type_mapping_impl(db, type_mapping, tcx, visitor) + }) + .collect() }), ) }; - if updated_signature.is_none() && updated_implementation_signature.is_none() { + if updated_signature.is_none() && updated_implementation_callables.is_none() { self } else { Self::new( db, literal, - UpdatedFunctionSignatures::new(updated_signature, updated_implementation_signature), + UpdatedFunctionSignatures::new(updated_signature, updated_implementation_callables), ) } } @@ -1808,6 +1992,14 @@ impl<'db> FunctionType<'db> { self.literal(db).last_definition.file(db) } + pub(crate) fn python_file(self, db: &'db dyn Db) -> PythonFile<'db> { + self.literal(db).last_definition.python_file(db) + } + + pub(crate) fn program_file(self, db: &'db dyn Db) -> ProgramFile<'db> { + self.literal(db).last_definition.program_file(db) + } + /// Returns the AST node for this function. pub(super) fn node<'ast>( self, @@ -1835,7 +2027,11 @@ impl<'db> FunctionType<'db> { /// 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 { + pub(crate) fn has_known_decorator( + self, + db: &'db dyn Db, + decorator: FunctionDecorators, + ) -> bool { self.literal(db).has_known_decorator(db, decorator) } @@ -2034,8 +2230,18 @@ impl<'db> FunctionType<'db> { /// would depend on the function's AST and rerun for every change in that file. #[salsa::tracked( returns(ref), - cycle_initial=|db, id, _| CallableSignature::cycle_initial(db, id), - cycle_fn=|db, cycle, previous, value: CallableSignature<'db>, _| value.cycle_normalized(db, previous, cycle), + cycle_initial=|db, id, function: FunctionType<'db>| { + let env = ProgramEnvironment::from_scope( + function.literal(db).last_definition.body_scope(db), + ); + CallableSignature::cycle_initial(db, &env, id) + }, + cycle_fn=|db, cycle, previous, value: CallableSignature<'db>, function: FunctionType<'db>| { + let env = ProgramEnvironment::from_scope( + function.literal(db).last_definition.body_scope(db), + ); + value.cycle_normalized(db, &env, previous, cycle) + }, heap_size=ruff_memory_usage::heap_size, )] pub(crate) fn signature(self, db: &'db dyn Db) -> CallableSignature<'db> { @@ -2058,7 +2264,8 @@ impl<'db> FunctionType<'db> { db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>, ) -> TypeVarVariance { - self.signature(db).variance_of(db, typevar) + let env = ProgramEnvironment::from_scope(self.literal(db).last_definition.body_scope(db)); + self.signature(db).variance_of(db, &env, typevar) } /// Typed externally-visible signature of the last overload or implementation of this function. @@ -2096,7 +2303,7 @@ impl<'db> FunctionType<'db> { cycle_initial=|_, _, _, _|Signature::bottom(), heap_size=ruff_memory_usage::heap_size, )] - pub(crate) fn last_definition_raw_signature( + pub(super) fn last_definition_raw_signature( self, db: &'db dyn Db, return_callable_typevar_scope: ReturnCallableTypeVarScope, @@ -2140,19 +2347,21 @@ impl<'db> FunctionType<'db> { pub(crate) fn find_legacy_typevars_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, ) { let signatures = self.signature(db); for signature in &signatures.overloads { - signature.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + signature.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } } pub(crate) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -2164,15 +2373,20 @@ impl<'db> FunctionType<'db> { let literal = self.literal(db); let updated_signature = match self.updated_signature(db) { Some(signature) => { - Some(signature.recursive_type_normalized_impl(db, div, nested)?) + Some(signature.recursive_type_normalized_impl(db, env, div, nested)?) } None => None, }; - let updated_implementation_signature = - match self.updated_implementation_signature(db) { - Some(signature) => { - Some(signature.recursive_type_normalized_impl(db, div, nested)?) - } + let updated_implementation_callables = + match self.updated_implementation_callables(db) { + Some(callables) => Some( + callables + .iter() + .map(|callable| { + callable.recursive_type_normalized_impl(db, env, div, nested) + }) + .collect::>>()?, + ), None => None, }; Some(Self::new( @@ -2180,7 +2394,7 @@ impl<'db> FunctionType<'db> { literal, UpdatedFunctionSignatures::new( updated_signature, - updated_implementation_signature, + updated_implementation_callables, ), )) }, @@ -2259,7 +2473,8 @@ fn check_classinfo_in_isinstance<'db>( let mut diagnostic = builder.into_diagnostic(format_args!( "`typing.Any` cannot be used with `isinstance()`" )); - diagnostic.set_primary_message("This call will raise `TypeError` at runtime"); + diagnostic + .set_primary_annotation_message("This call will raise `TypeError` at runtime"); } Type::KnownInstance(KnownInstanceType::UnionType(_)) => { report_invalid_union_type_elements( @@ -2271,7 +2486,9 @@ fn check_classinfo_in_isinstance<'db>( classinfo_expr, ); } - Type::NominalInstance(nominal) if let Some(tuple_spec) = nominal.tuple_spec(db) => { + Type::NominalInstance(nominal) + if let Some(tuple_spec) = nominal.tuple_spec(db, context.program_environment()) => + { let element_exprs = match classinfo_expr { Some(ast::Expr::Tuple(tuple_expr)) => Some(&tuple_expr.elts), _ => None, @@ -2305,6 +2522,7 @@ fn report_invalid_union_type_elements<'db>( ) { fn find_invalid_elements<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, function: KnownFunction, ty: Type<'db>, invalid_elements: &mut Vec>, @@ -2317,10 +2535,10 @@ fn report_invalid_union_type_elements<'db>( // `Any` can be used in `issubclass()` calls but not `isinstance()` calls Type::SpecialForm(SpecialFormType::Any) if function == KnownFunction::IsSubclass => {} Type::KnownInstance(KnownInstanceType::UnionType(instance)) => { - match instance.value_expression_types(db) { + match instance.value_expression_types(db, env) { Ok(value_expression_types) => { for element in value_expression_types { - find_invalid_elements(db, function, element, invalid_elements); + find_invalid_elements(db, env, function, element, invalid_elements); } } Err(_) => { @@ -2333,7 +2551,8 @@ fn report_invalid_union_type_elements<'db>( } let mut invalid_elements = vec![]; - find_invalid_elements(db, function, union_type, &mut invalid_elements); + let env = context.program_environment(); + find_invalid_elements(db, env, function, union_type, &mut invalid_elements); let Some((first_invalid_element, other_invalid_elements)) = invalid_elements.split_first() else { @@ -2361,10 +2580,11 @@ fn report_invalid_union_type_elements<'db>( // When we have a secondary annotation pointing at the UnionType expression, // "the union" is unambiguous. Otherwise, spell out the union type in the message. + let env = context.program_environment(); let union_suffix = match (&union_type_expr, union_type) { (None, Type::KnownInstance(KnownInstanceType::UnionType(instance))) => { match instance.union_type(db) { - Ok(ty) => format!(" `{}`", ty.display(db)), + Ok(ty) => format!(" `{}`", ty.display(db, env)), Err(_) => String::new(), } } @@ -2374,16 +2594,16 @@ fn report_invalid_union_type_elements<'db>( match other_invalid_elements { [] => diagnostic.info(format_args!( "Element `{}` in the union{union_suffix} is not a class object", - first_invalid_element.display(db) + first_invalid_element.display(db, env) )), [single] => diagnostic.info(format_args!( "Elements `{}` and `{}` in the union{union_suffix} are not class objects", - first_invalid_element.display(db), - single.display(db), + first_invalid_element.display(db, env), + single.display(db, env), )), _ => diagnostic.info(format_args!( "Element `{}` in the union{union_suffix}, and {} more elements, are not class objects", - first_invalid_element.display(db), + first_invalid_element.display(db, env), other_invalid_elements.len(), )), } @@ -2395,12 +2615,16 @@ fn report_invalid_union_type_elements<'db>( /// instead. fn is_instance_truthiness<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, class: ClassLiteral<'db>, ) -> Truthiness { let is_instance = |ty: &Type<'_>| { - ty.as_nominal_instance() - .is_some_and(|instance| instance.class(db).is_subtype_of_class_literal(db, class)) + ty.as_nominal_instance().is_some_and(|instance| { + instance + .class(db, env) + .is_subtype_of_class_literal(db, class) + }) }; let always_true_if = |test: bool| { @@ -2414,12 +2638,14 @@ fn is_instance_truthiness<'db>( match ty { // parameter-only marker; behaves as the type a body sees (bound of `Key`) Type::Overlapping(overlapping) => { - is_instance_truthiness(db, overlapping.value_type(db), class) + is_instance_truthiness(db, env, overlapping.value_type(db, env), class) } Type::Restricted(restricted) => { - is_instance_truthiness(db, restricted.value_type(db), class) + is_instance_truthiness(db, env, restricted.value_type(db), class) + } + Type::Deferred(deferred) => { + is_instance_truthiness(db, env, deferred.reduced(db, env), class) } - Type::Deferred(deferred) => is_instance_truthiness(db, deferred.reduced(db), class), Type::Union(..) => { // We do not handle unions specifically here, because something like `A | SubclassOfA` would // have been simplified to `A` anyway @@ -2435,19 +2661,19 @@ fn is_instance_truthiness<'db>( // Along the way, short-circuit to `AlwaysTrue` if we find any positive element // that is always true. Type::Intersection(intersection) => { - let mut effective = IntersectionBuilder::new(db); + let mut effective = IntersectionBuilder::new(db, env); let mut found_tvars_or_newtypes = false; for &positive in intersection.positive(db) { - if is_instance_truthiness(db, positive, class).is_always_true() { + if is_instance_truthiness(db, env, positive, class).is_always_true() { return Truthiness::AlwaysTrue; } else if let Type::TypeVar(tvar) = positive { - match tvar.typevar(db).bound_or_constraints(db) { + match tvar.typevar(db).bound_or_constraints(db, env) { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - effective = effective.add_positive(bound); + effective.add_positive_in_place(bound); } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - effective = effective.add_positive(constraints.as_type(db)); + effective.add_positive_in_place(constraints.as_type(db, env)); } // A typevar without bounds/constraints has `object` as its implicit upper bound, // and adding `object` to an intersection is a no-op @@ -2456,9 +2682,9 @@ fn is_instance_truthiness<'db>( found_tvars_or_newtypes = true; } else if let Type::NewTypeInstance(newtype) = positive { found_tvars_or_newtypes = true; - effective = effective.add_positive(newtype.concrete_base_type(db)); + effective.add_positive_in_place(newtype.concrete_base_type(db)); } else { - effective = effective.add_positive(positive); + effective.add_positive_in_place(positive); } } @@ -2467,10 +2693,10 @@ fn is_instance_truthiness<'db>( } for &negative in intersection.negative(db) { - if is_instance_truthiness(db, negative, class).is_always_true() { + if is_instance_truthiness(db, env, negative, class).is_always_true() { return Truthiness::AlwaysFalse; } - effective = effective.add_negative(negative); + effective.add_negative_in_place(negative); } let effective = effective.build(); @@ -2478,12 +2704,12 @@ fn is_instance_truthiness<'db>( if effective == ty { Truthiness::Ambiguous } else { - is_instance_truthiness(db, effective, class) + is_instance_truthiness(db, env, effective, class) } } Type::EnumComplement(complement) => { - is_instance_truthiness(db, complement.to_intersection(db), class) + is_instance_truthiness(db, env, complement.to_intersection(db, env), class) } Type::NominalInstance(..) => always_true_if(is_instance(&ty)), @@ -2494,28 +2720,32 @@ fn is_instance_truthiness<'db>( Type::LiteralValue(..) | Type::ModuleLiteral(..) | Type::FunctionLiteral(..) => { always_true_if( - ty.literal_fallback_instance(db) + ty.literal_fallback_instance(db, env) .as_ref() .is_some_and(is_instance), ) } - Type::ClassLiteral(..) => always_true_if(is_instance(&KnownClass::Type.to_instance(db))), + Type::ClassLiteral(..) => { + always_true_if(is_instance(&KnownClass::Type.to_instance(db, env))) + } - Type::TypeAlias(alias) => is_instance_truthiness(db, alias.value_type(db), class), + Type::TypeAlias(alias) => is_instance_truthiness(db, env, alias.value_type(db), class), - Type::TypeVar(bound_typevar) => match bound_typevar.typevar(db).bound_or_constraints(db) { - None => is_instance_truthiness(db, Type::object(), class), - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - is_instance_truthiness(db, bound, class) + Type::TypeVar(bound_typevar) => { + match bound_typevar.typevar(db).bound_or_constraints(db, env) { + None => is_instance_truthiness(db, env, Type::object(), class), + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + is_instance_truthiness(db, env, bound, class) + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => always_true_if( + constraints + .elements(db) + .iter() + .all(|c| is_instance_truthiness(db, env, *c, class).is_always_true()), + ), } - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => always_true_if( - constraints - .elements(db) - .iter() - .all(|c| is_instance_truthiness(db, *c, class).is_always_true()), - ), - }, + } Type::BoundMethod(..) | Type::KnownBoundMethod(..) @@ -2555,19 +2785,25 @@ fn is_instance_truthiness<'db>( /// if isinstance(x, (A, B)): /// return True /// ``` -fn is_instance_tuple_exhaustive<'db>(db: &'db dyn Db, ty: Type<'db>, classinfo: Type<'db>) -> bool { - let Some(tuple) = classinfo.tuple_instance_spec(db) else { +fn is_instance_tuple_exhaustive<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + classinfo: Type<'db>, +) -> bool { + let Some(tuple) = classinfo.tuple_instance_spec(db, env) else { return false; }; if tuple.is_variadic() { return false; } - is_instance_tuple_covers(db, &tuple, ty, &ActiveRecursionDetector::default()) + is_instance_tuple_covers(db, env, &tuple, ty, &ActiveRecursionDetector::default()) } fn is_instance_tuple_covers<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, tuple: &TupleSpec<'db>, ty: Type<'db>, recursion_guard: &ActiveRecursionDetector>, @@ -2576,32 +2812,32 @@ fn is_instance_tuple_covers<'db>( Type::TypeAlias(alias) => recursion_guard.visit( &ty, || true, - || is_instance_tuple_covers(db, tuple, alias.value_type(db), recursion_guard), + || is_instance_tuple_covers(db, env, tuple, alias.value_type(db), recursion_guard), ), Type::Union(union) => union .elements(db) .iter() - .all(|element| is_instance_tuple_covers(db, tuple, *element, recursion_guard)), + .all(|element| is_instance_tuple_covers(db, env, tuple, *element, recursion_guard)), Type::Intersection(intersection) => intersection .positive(db) .iter() - .any(|element| is_instance_tuple_covers(db, tuple, *element, recursion_guard)), - Type::TypeVar(typevar) => match typevar.typevar(db).bound_or_constraints(db) { + .any(|element| is_instance_tuple_covers(db, env, tuple, *element, recursion_guard)), + Type::TypeVar(typevar) => match typevar.typevar(db).bound_or_constraints(db, env) { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - is_instance_tuple_covers(db, tuple, bound, recursion_guard) + is_instance_tuple_covers(db, env, tuple, bound, recursion_guard) } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { constraints.elements(db).iter().all(|constraint| { - is_instance_tuple_covers(db, tuple, *constraint, recursion_guard) + is_instance_tuple_covers(db, env, tuple, *constraint, recursion_guard) }) } - None => is_instance_tuple_covers(db, tuple, Type::object(), recursion_guard), + None => is_instance_tuple_covers(db, env, tuple, Type::object(), recursion_guard), }, ty => tuple.fixed_elements().any(|element| { let Type::ClassLiteral(class) = element else { return false; }; - is_instance_truthiness(db, ty, *class).is_always_true() + is_instance_truthiness(db, env, ty, *class).is_always_true() }), } } @@ -2627,6 +2863,7 @@ pub(crate) fn function_has_stub_body(node: &ast::StmtFunctionDef) -> bool { /// the analysis is only done on the remaining statements if the first is a docstring. pub(super) fn function_body_kind<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, node: &ast::StmtFunctionDef, infer_type: impl Fn(&ast::Expr) -> Type<'db>, ) -> FunctionBodyKind { @@ -2644,16 +2881,19 @@ pub(super) fn function_body_kind<'db>( node_index: _, range: _, } = raise - && infer_type(exc).is_subtype_of( + { + if infer_type(exc).is_subtype_of( db, + env, UnionType::from_two_elements( db, - KnownClass::NotImplementedError.to_class_literal(db), - KnownClass::NotImplementedError.to_instance(db), + env, + KnownClass::NotImplementedError.to_class_literal(db, env), + KnownClass::NotImplementedError.to_instance(db, env), ), - ) - { - return FunctionBodyKind::AlwaysRaisesNotImplementedError; + ) { + return FunctionBodyKind::AlwaysRaisesNotImplementedError; + } } FunctionBodyKind::Regular @@ -2785,8 +3025,6 @@ pub enum KnownFunction { IsDisjointFrom, /// `ty_extensions._internal.is_singleton` IsSingleton, - /// `ty_extensions._internal.is_single_valued` - IsSingleValued, /// `ty_extensions._internal.generic_context` GenericContext, /// `ty_extensions._internal.into_callable` @@ -2849,7 +3087,9 @@ impl KnownFunction { let candidate = Self::from_str(name).ok()?; candidate - .check_module(file_to_module(db, definition.file(db))?.known(db)?) + .check_module( + file_to_module(db, definition.program_file(db).resolver_file(db))?.known(db)?, + ) .then_some(candidate) } @@ -2898,7 +3138,6 @@ impl KnownFunction { | Self::IsConstraintSetAssignableTo | Self::IsDisjointFrom | Self::IsEquivalentTo - | Self::IsSingleValued | Self::IsSingleton | Self::IsSubtypeOf | Self::GenericContext @@ -2931,16 +3170,18 @@ impl KnownFunction { overload: &mut Binding<'db>, call_arguments: &CallArguments<'_, 'db>, call_expression: &ast::ExprCall, - file: File, ) { let db = context.db(); let parameter_types = overload.parameter_types(); match self { KnownFunction::RevealType => { + let env = context.program_environment(); let revealed_type = overload .arguments_for_parameter(call_arguments, 0) - .fold(UnionBuilder::new(db), |builder, (_, ty)| builder.add(ty)) + .fold(UnionBuilder::new(db, env), |builder, (_, ty)| { + builder.add(ty) + }) .build(); report_revealed_type( context, @@ -2957,7 +3198,8 @@ impl KnownFunction { let Some(member) = literal.as_string() else { return; }; - let ty_members = all_members(db, *ty); + let env = context.program_environment(); + let ty_members = all_members(db, env, *ty); overload.set_return_type(Type::bool_literal( ty_members.iter().any(|m| m.name == member.value(db)), )); @@ -2967,20 +3209,22 @@ impl KnownFunction { let [Some(actual_ty), Some(asserted_ty)] = parameter_types else { return; }; - let asserted_ty = asserted_ty.project_type_form(db); - if actual_ty.is_equivalent_to(db, asserted_ty) { + let env = context.program_environment(); + let asserted_ty = asserted_ty.project_type_form(db, env); + if actual_ty.is_equivalent_to(db, env, asserted_ty) { return; } - let diagnostic = - if actual_ty.is_spellable(db) || !actual_ty.is_subtype_of(db, asserted_ty) { - &TYPE_ASSERTION_FAILURE - } else { - &ASSERT_TYPE_UNSPELLABLE_SUBTYPE - }; + let diagnostic = if actual_ty.is_spellable(db, env) + || !actual_ty.is_subtype_of(db, env, asserted_ty) + { + &TYPE_ASSERTION_FAILURE + } else { + &ASSERT_TYPE_UNSPELLABLE_SUBTYPE + }; if let Some(builder) = context.report_lint(diagnostic, call_expression) { let mut diagnostic = builder.into_diagnostic(format_args!( "Argument does not have asserted type `{}`", - asserted_ty.display(db), + asserted_ty.display(db, env), )); diagnostic.annotate( @@ -2990,27 +3234,30 @@ impl KnownFunction { .unwrap_or_else(|| ast::AnyNodeRef::from(call_expression)), ), ) - .message(format_args!("Inferred type is `{}`", actual_ty.display(db))), + .message(format_args!( + "Inferred type is `{}`", + actual_ty.display(db, env) + )), ); - if actual_ty.is_subtype_of(db, asserted_ty) { + if actual_ty.is_subtype_of(db, env, asserted_ty) { diagnostic.info(format_args!( "`{inferred_type}` is a subtype of `{asserted_type}`, but they are not equivalent", - asserted_type = asserted_ty.display(db), - inferred_type = actual_ty.display(db), + asserted_type = asserted_ty.display(db, env), + inferred_type = actual_ty.display(db, env), )); } else { 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), + asserted_type = asserted_ty.display(db, env), + inferred_type = actual_ty.display(db, env), )); } diagnostic.set_concise_message(format_args!( "Type `{}` does not match asserted type `{}`", - actual_ty.display(db), - asserted_ty.display(db), + actual_ty.display(db, env), + asserted_ty.display(db, env), )); } } @@ -3019,7 +3266,8 @@ impl KnownFunction { let [Some(actual_ty)] = parameter_types else { return; }; - if actual_ty.is_equivalent_to(db, Type::Never) { + let env = context.program_environment(); + if actual_ty.is_equivalent_to(db, env, Type::Never) { return; } if let Some(builder) = context.report_lint(&TYPE_ASSERTION_FAILURE, call_expression) @@ -3035,17 +3283,17 @@ impl KnownFunction { ) .message(format_args!( "Inferred type of argument is `{}`", - actual_ty.display(db) + actual_ty.display(db, env) )), ); diagnostic.info(format_args!( "`Never` and `{inferred_type}` are not equivalent types", - inferred_type = actual_ty.display(db), + inferred_type = actual_ty.display(db, env), )); diagnostic.set_concise_message(format_args!( "Type `{}` is not equivalent to `Never`", - actual_ty.display(db), + actual_ty.display(db, env), )); } } @@ -3054,7 +3302,8 @@ impl KnownFunction { let [Some(parameter_ty), message] = parameter_types else { return; }; - let truthiness = match parameter_ty.try_bool(db) { + let env = context.program_environment(); + let truthiness = match parameter_ty.try_bool(db, env) { Ok(truthiness) => truthiness, Err(err) => { err.report_diagnostic( @@ -3084,20 +3333,20 @@ impl KnownFunction { builder.into_diagnostic(format_args!( "Static assertion error: argument of type `{parameter_ty}` \ is always falsy", - parameter_ty = parameter_ty.display(db) + parameter_ty = parameter_ty.display(db, env) )) } 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) + parameter_ty = parameter_ty.display(db, env) )) }; if let Some(condition) = call_argument_node(call_expression, "condition", 0) { diagnostic.annotate( Annotation::secondary(context.span(condition)).message(format_args!( "Inferred type of argument is `{}`", - parameter_ty.display(db) + parameter_ty.display(db, env) )), ); } @@ -3108,14 +3357,15 @@ impl KnownFunction { let [Some(casted_type), Some(source_type)] = parameter_types else { return; }; - let casted_type = casted_type.project_type_form(db); - if source_type.is_equivalent_to(db, casted_type) - && non_any_dynamic_content(db, *source_type).is_absent() - && non_any_dynamic_content(db, casted_type).is_absent() + let env = context.program_environment(); + let casted_type = casted_type.project_type_form(db, env); + if source_type.is_equivalent_to(db, env, casted_type) + && non_any_dynamic_content(db, env, *source_type).is_absent() + && non_any_dynamic_content(db, env, casted_type).is_absent() { if let Some(builder) = context.report_lint(&REDUNDANT_CAST, call_expression) { - let source_display = source_type.display(db).to_string(); - let casted_display = casted_type.display(db).to_string(); + let source_display = source_type.display(db, env).to_string(); + let casted_display = casted_type.display(db, env).to_string(); let mut diagnostic = builder.into_diagnostic(format_args!( "Value is already of type `{casted_display}`", )); @@ -3137,7 +3387,7 @@ impl KnownFunction { let value_precedence = OperatorPrecedence::from_expr(value); OperatorPrecedence::from_expr_ref(parent) >= value_precedence }); - let value_text = &source_text(db, file)[value.range()]; + let value_text = &source_text(db, context.file())[value.range()]; let replacement = if needs_parens { format!("({value_text})") } else { @@ -3157,7 +3407,7 @@ impl KnownFunction { let [Some(Type::ClassLiteral(class))] = parameter_types else { return; }; - if class.is_protocol(db) { + if class.is_protocol(context.db()) { return; } report_bad_argument_to_get_protocol_members(context, call_expression, *class); @@ -3167,6 +3417,7 @@ impl KnownFunction { let [Some(param_type)] = parameter_types else { return; }; + let env = context.program_environment(); let Some(protocol_class) = param_type .to_class_type(db) .and_then(|class| class.into_protocol_class(db)) @@ -3188,7 +3439,7 @@ impl KnownFunction { ); diag.annotate(Annotation::primary(span).message(format_args!( "`{}`", - protocol_class.interface(db).display(db) + protocol_class.interface(db).display(db, env) ))); } } @@ -3233,7 +3484,7 @@ impl KnownFunction { }; let mut diagnostic = builder.into_diagnostic("Invalid argument to `reveal_mro`"); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Can only pass a class object, generic alias or a union thereof" )); return; @@ -3241,6 +3492,7 @@ impl KnownFunction { if let Some(builder) = context.report_diagnostic(DiagnosticId::RevealedType, Severity::Info) { + let env = context.program_environment(); let mut diag = builder.into_diagnostic("Revealed MRO"); let span = context.span( call_argument_node(call_expression, "cls", 0) @@ -3249,6 +3501,7 @@ impl KnownFunction { let mut message = String::new(); let display_settings = DisplaySettings::from_possibly_ambiguous_types( db, + env, classes .iter() .flat_map(|class| class.iter_mro(db)) @@ -3258,7 +3511,9 @@ impl KnownFunction { message.push('('); for class in class.iter_mro(db) { message.push_str( - &class.display_with(db, display_settings.clone()).to_string(), + &class + .display_with(db, env, display_settings.clone()) + .to_string(), ); // Omit the comma for the last element (which is always `object`) if class @@ -3299,26 +3554,35 @@ impl KnownFunction { ); if self == KnownFunction::IsInstance { + let env = context.program_environment(); let truthiness = match second_argument { - Type::ClassLiteral(class) => is_instance_truthiness(db, *first_arg, *class), + Type::ClassLiteral(class) => { + is_instance_truthiness(db, env, *first_arg, *class) + } Type::SpecialForm( SpecialFormType::TypingCallable | SpecialFormType::CollectionsAbcCallable, ) => { - let callable_top = - Type::Callable(CallableType::unknown(db)).top_materialization(db); - if first_arg.is_subtype_of(db, callable_top) { + let callable_top = Type::Callable(CallableType::unknown(db)) + .top_materialization(db, env); + if first_arg.is_subtype_of(db, env, callable_top) { Truthiness::AlwaysTrue } else { Truthiness::Ambiguous } } - _ if is_instance_tuple_exhaustive(db, *first_arg, *second_argument) => { + _ if is_instance_tuple_exhaustive( + db, + env, + *first_arg, + *second_argument, + ) => + { Truthiness::AlwaysTrue } _ => Truthiness::Ambiguous, }; - overload.set_return_type(Type::from_truthiness(db, truthiness)); + overload.set_return_type(Type::from_truthiness(db, env, truthiness)); } } @@ -3347,11 +3611,15 @@ impl KnownFunction { let Some(module_name) = ModuleName::new(module_name) else { return; }; - let Some(module) = resolve_module(db, file, &module_name) else { + let importing_file = ImportingFile::File( + context.file(), + context.program_environment().resolver_environment(db), + ); + let Some(module) = resolve_module(db, importing_file, &module_name) else { return; }; - overload.set_return_type(Type::module_literal(db, file, module)); + overload.set_return_type(Type::module_literal(db, context.program_file(), module)); } KnownFunction::TotalOrdering => { @@ -3391,15 +3659,14 @@ pub(super) fn report_revealed_type<'db>( revealed_type: Type<'db>, argument_node: impl Ranged, ) { + let db = context.db(); if let Some(builder) = context.report_diagnostic(DiagnosticId::RevealedType, Severity::Info) { + let env = context.program_environment(); let mut diag = builder.into_diagnostic("Revealed type"); diag.annotate( Annotation::primary(context.span(argument_node)).message(format_args!( "`{}`", - revealed_type.display_with( - context.db(), - DisplaySettings::default().preserve_long_unions() - ) + revealed_type.display(db, env).preserve_long_unions() )), ); } @@ -3468,7 +3735,6 @@ pub(crate) mod tests { | KnownFunction::DunderAllNames | KnownFunction::EnumMembers | KnownFunction::IsDisjointFrom - | KnownFunction::IsSingleValued | KnownFunction::IsAssignableTo | KnownFunction::IsConstraintSetAssignableTo | KnownFunction::IsEquivalentTo @@ -3488,11 +3754,12 @@ pub(crate) mod tests { continue; } - let function_definition = known_module_symbol(&db, module, function_name) - .place - .expect_type() - .expect_function_literal() - .definition(&db); + let function_definition = + known_module_symbol(&db, &db.program_environment(), module, function_name) + .place + .expect_type() + .expect_function_literal() + .definition(&db); assert_eq!( KnownFunction::try_from_definition_and_name( diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index b3cca143dc..6014dc72c8 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -1,9 +1,9 @@ +use crate::{Program, ProgramEnvironment}; use std::borrow::Cow; use std::cell::{Cell, RefCell}; use std::collections::hash_map::Entry; -use std::fmt::Display; -use itertools::{Either, Itertools}; +use itertools::Itertools; use ruff_db::parsed::parsed_module; use ruff_python_ast as ast; use rustc_hash::{FxHashMap, FxHashSet}; @@ -20,13 +20,15 @@ use crate::types::relation::{ DisjointnessChecker, HasRelationToVisitor, IsDisjointVisitor, TypeRelation, TypeRelationChecker, TypeVarEvaluation, }; -use crate::types::signatures::{CallableSignature, Parameters, SignatureRelationVisitor}; +use crate::types::signatures::{ + CallableSignature, Parameters, ReturnCallableTypeVarScope, SignatureRelationVisitor, +}; use crate::types::tuple::{ TupleSpec, TupleSpecBuilder, TupleType, VariableSegment, walk_tuple_type, }; use crate::types::type_alias::{walk_manual_pep_695_type_alias, walk_pep_695_type_alias}; use crate::types::typevar::{ - BoundTypeVarIdentity, PackBoundViolation, TypeVarIdentity, TypeVarInstance, + BoundTypeVarIdentity, PackBoundViolation, TypeVarIdentity, TypeVarInstance, TypeVarSet, pack_bound_violation, walk_type_var_bounds, }; use crate::types::visitor::{ @@ -93,6 +95,71 @@ pub(crate) fn bind_typevar<'db>( typevar_binding_context: Option>, typevar: TypeVarInstance<'db>, ) -> Option> { + find_typevar_binding( + db, + index, + containing_scope, + typevar, + ReturnCallableTypeVarScope::Public, + ) + .or_else(|| { + typevar_binding_context.map(|typevar_binding_context| { + typevar.with_binding_context(db, typevar_binding_context) + }) + }) +} + +/// Resolves a reference to a type variable that must already be bound. +/// +/// Unlike [`bind_typevar`], this function never introduces a binding in the current context. It +/// also uses the lexical form of enclosing function signatures, in which type variables moved to +/// a returned callable's public generic context are still visible within the function body. This +/// lets `P.args` and `P.kwargs` validation establish that an enclosing `ParamSpec` is in scope +/// without changing the binding selected for the current function's signature. +pub(crate) fn resolve_typevar_reference<'db>( + db: &'db dyn Db, + index: &SemanticIndex<'db>, + containing_scope: FileScopeId, + typevar: TypeVarInstance<'db>, +) -> Option> { + find_typevar_binding( + db, + index, + containing_scope, + typevar, + ReturnCallableTypeVarScope::Lexical, + ) +} + +/// Finds the nearest visible binding under the requested treatment of return-only callable type +/// variables. +/// +/// Captured `ParamSpec` bindings are recovered from component annotations because those bindings +/// are deliberately excluded from a nested function's own generic context. A binding owned by a +/// class is hidden after the search crosses a nested class boundary. +fn find_typevar_binding<'db>( + db: &'db dyn Db, + index: &SemanticIndex<'db>, + containing_scope: FileScopeId, + typevar: TypeVarInstance<'db>, + return_callable_typevar_scope: ReturnCallableTypeVarScope, +) -> Option> { + /// Returns whether a binding remains visible after crossing an inner class boundary. + /// + /// Class-owned bindings are hidden by the inner class; function-owned and synthetic bindings + /// remain visible. + fn is_visible_across_class_boundary<'db>( + db: &'db dyn Db, + bound: BoundTypeVarInstance<'db>, + crossed_class_scope: bool, + ) -> bool { + !crossed_class_scope + || !bound + .binding_context(db) + .definition() + .is_some_and(|definition| matches!(definition.kind(db), DefinitionKind::Class(_))) + } + // typing.Self is treated like a legacy typevar, but doesn't follow the same scoping rules. It // is always bound to the outermost method in the nearest enclosing class. The walk looks for a // (function, class) pair in the scope hierarchy. The caller (`typing_self`) is responsible for @@ -152,12 +219,39 @@ pub(crate) fn bind_typevar<'db>( return Some(typevar.with_binding_context(db, definition)); } - let generic_context = GenericContext::of_node(db, ancestor_scope.node(), index); + if typevar.is_paramspec(db) + && let NodeWithScopeKind::Function(function) = ancestor_scope.node() + { + let definition = index.expect_single_definition(function); + if let Some(function_ty) = + infer_definition_types(db, definition).function_type(definition) + { + let signature = function_ty + .last_definition_raw_signature(db, ReturnCallableTypeVarScope::Lexical); + if let Some(bound) = signature.paramspec_component_binding(db, typevar) + && bound.binding_context(db).definition() != Some(definition) + && is_visible_across_class_boundary(db, bound, crossed_class_scope) + { + return Some(bound); + } + } + } + let generic_context = match return_callable_typevar_scope { + ReturnCallableTypeVarScope::Lexical => { + GenericContext::lexical_of_node(db, ancestor_scope.node(), index) + } + ReturnCallableTypeVarScope::Public => { + GenericContext::of_node(db, ancestor_scope.node(), index) + } + }; // If we've already crossed a class boundary, skip class-scoped generic contexts. // This prevents inner classes from accessing type parameters of outer classes. + // An enclosing function's context can also retain a type variable originally bound by its + // enclosing class, so check the binding context as well as the ancestor node. if (!is_class_scope || !crossed_class_scope) && let Some(generic_context) = generic_context && let Some(bound) = generic_context.binds_typevar(db, typevar) + && is_visible_across_class_boundary(db, bound, crossed_class_scope) { return Some(bound); } @@ -168,7 +262,7 @@ pub(crate) fn bind_typevar<'db>( && let Some(class_ref) = ancestor_scope.node().as_class() { let definition = index.expect_single_definition(class_ref); - let module = parsed_module(db, definition.file(db)).load(db); + let module = parsed_module(db, definition.program_file(db).python_file(db)).load(db); if class_ref.node(&module).is_extension() && let Some(ClassLiteral::Static(extension)) = crate::types::infer::original_class_type(db, definition) @@ -186,26 +280,26 @@ pub(crate) fn bind_typevar<'db>( if is_class_scope && !ancestor_scope.node().as_class().is_some_and(|class_ref| { let definition = index.expect_single_definition(class_ref); - let module = parsed_module(db, definition.file(db)).load(db); + let module = + parsed_module(db, definition.program_file(db).python_file(db)).load(db); class_ref.node(&module).is_enum_variant() }) { crossed_class_scope = true; } } - typevar_binding_context - .map(|typevar_binding_context| typevar.with_binding_context(db, typevar_binding_context)) + None } /// Create a `typing.Self` type variable for a given class. pub(crate) fn typing_self<'db>( db: &'db dyn Db, - scope_id: ScopeId, + scope_id: ScopeId<'db>, typevar_binding_context: Option>, class: ClassLiteral<'db>, ) -> Option> { - let file = scope_id.file(db); - let index = semantic_index(db, file); + let env = ProgramEnvironment::from_scope(scope_id); + let index = semantic_index(db, scope_id.program_file(db)); // `Self` in a class's own type parameter list cannot be bounded by the class's *identity* // specialization: that names the very type parameters being declared, so `class C[T = Self]` // would define `T` through `T`. Bound it by the unspecialized class instead. @@ -237,16 +331,16 @@ pub(crate) fn typing_self<'db>( .and_then(|static_class| { if static_class.is_extension(db) { crate::types::extensions::body_view_class(db, static_class) - .map(|body_view| Type::instance(db, body_view)) + .map(|body_view| Type::instance(db, &env, body_view)) } else { crate::types::class::based_enum_variant_union(db, static_class) } }) .unwrap_or_else(|| { if in_own_type_params { - Type::instance(db, class.unknown_specialization(db)) + Type::instance(db, &env, class.unknown_specialization(db)) } else { - Type::instance(db, class.identity_specialization(db)) + Type::instance(db, &env, class.identity_specialization(db)) } }); let bounds = TypeVarBoundOrConstraints::UpperBound(self_bound); @@ -311,92 +405,6 @@ pub(crate) fn typing_self<'db>( ) } -/// The set of bound typevar occurrences that can be solved by the current inference context. -/// -/// Membership is keyed by [`BoundTypeVarIdentity`], including any freshness nonce. This lets a -/// fresh generic-callable occurrence be inferable without making the surrounding source-level -/// typevar inferable. -#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, get_size2::GetSize, salsa::SalsaValue)] -pub(crate) enum InferableTypeVars<'db> { - None, - Some(InferableTypeVarsInner<'db>), -} - -impl<'db> InferableTypeVars<'db> { - pub(crate) fn from_typevars( - db: &'db dyn Db, - mut typevars: FxOrderSet>, - ) -> Self { - if typevars.is_empty() { - return InferableTypeVars::None; - } - - typevars.shrink_to_fit(); - Self::Some(InferableTypeVarsInner::new_internal(db, typevars)) - } -} - -#[salsa::interned(debug, constructor=new_internal, heap_size=ruff_memory_usage::heap_size)] -pub(crate) struct InferableTypeVarsInner<'db> { - #[returns(ref)] - inferable: FxOrderSet>, -} - -// The Salsa heap is tracked separately. -impl get_size2::GetSize for InferableTypeVarsInner<'_> {} - -impl<'db> BoundTypeVarIdentity<'db> { - pub(crate) fn is_inferable(self, db: &'db dyn Db, inferable: InferableTypeVars<'db>) -> bool { - match inferable { - InferableTypeVars::None => false, - InferableTypeVars::Some(inner) => inner.inferable(db).contains(&self), - } - } -} - -impl<'db> BoundTypeVarInstance<'db> { - pub(crate) fn is_inferable(self, db: &'db dyn Db, inferable: InferableTypeVars<'db>) -> bool { - self.identity(db).is_inferable(db, inferable) - } -} - -#[salsa::tracked] -impl<'db> InferableTypeVars<'db> { - #[salsa::tracked(returns(copy), heap_size=ruff_memory_usage::heap_size)] - pub(crate) fn merge(self, db: &'db dyn Db, other: Self) -> Self { - match (self, other) { - (InferableTypeVars::None, other) | (other, InferableTypeVars::None) => other, - (InferableTypeVars::Some(self_inner), InferableTypeVars::Some(other_inner)) => { - let merged = self_inner.inferable(db) | other_inner.inferable(db); - Self::Some(InferableTypeVarsInner::new_internal(db, merged)) - } - } - } - - // This is not an IntoIterator implementation because I have no desire to try to name the - // iterator type. - pub(crate) fn iter( - self, - db: &'db dyn Db, - ) -> impl Iterator> + 'db { - match self { - InferableTypeVars::None => Either::Left(std::iter::empty()), - InferableTypeVars::Some(inner) => Either::Right(inner.inferable(db).iter().copied()), - } - } - - // Keep this around for debugging purposes - #[expect(dead_code)] - pub(crate) fn display(&self, db: &'db dyn Db) -> impl Display { - format!( - "[{}]", - self.iter(db) - .map(|identity| identity.display(db)) - .format(", ") - ) - } -} - /// A list of formal type variables for a generic function, class, type alias, or fresh callable /// occurrence. /// @@ -404,6 +412,9 @@ impl<'db> InferableTypeVars<'db> { /// generic context can coexist without collapsing into each other. #[salsa::interned(debug, constructor=new_internal, heap_size=ruff_memory_usage::heap_size)] pub struct GenericContext<'db> { + #[returns(copy)] + pub(crate) program: Program<'db>, + #[returns(ref)] variables_inner: FxOrderMap, BoundTypeVarInstance<'db>>, @@ -444,6 +455,7 @@ impl<'db> GenericContext<'db> { Self::new_internal( db, + binding_context.program(db), variables .map(|variable| (variable.identity(db), variable)) .collect::>(), @@ -479,13 +491,43 @@ impl<'db> GenericContext<'db> { } } + /// Returns the generic context visible while checking the scope introduced by `node`. + /// + /// For functions, this retains type variables that are moved to a returned callable in the + /// externally visible signature. Other scope kinds have identical lexical and public contexts. + fn lexical_of_node( + db: &'db dyn Db, + node: &NodeWithScopeKind, + index: &SemanticIndex<'db>, + ) -> Option { + if let NodeWithScopeKind::Function(function) = node { + let definition = index.expect_single_definition(function); + infer_definition_types(db, definition) + .function_type(definition)? + .last_definition_raw_signature(db, ReturnCallableTypeVarScope::Lexical) + .generic_context + } else { + Self::of_node(db, node, index) + } + } + /// Creates a generic context from a list of `BoundTypeVarInstance`s. pub(crate) fn from_typevar_instances( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + type_params: impl IntoIterator>, + ) -> Self { + Self::from_typevar_instances_in_program(db, env.program(db), type_params) + } + + fn from_typevar_instances_in_program( + db: &'db dyn Db, + program: Program<'db>, type_params: impl IntoIterator>, ) -> Self { Self::new_internal( db, + program, type_params .into_iter() .map(|variable| (variable.identity(db), variable)) @@ -507,8 +549,11 @@ impl<'db> GenericContext<'db> { /// Merge this generic context with another, returning a new generic context that /// contains type variables from both contexts. pub(crate) fn merge(self, db: &'db dyn Db, other: Self) -> Self { - Self::from_typevar_instances( + let program = self.program(db); + debug_assert_eq!(program, other.program(db)); + Self::from_typevar_instances_in_program( db, + program, self.variables_inner(db) .values() .chain(other.variables_inner(db).values()) @@ -539,8 +584,9 @@ impl<'db> GenericContext<'db> { generic_context: GenericContext<'db>, binding_context: Option>, ) -> GenericContext<'db> { - GenericContext::from_typevar_instances( + GenericContext::from_typevar_instances_in_program( db, + generic_context.program(db), generic_context.variables(db).filter(|bound_typevar| { !(bound_typevar.typevar(db).is_self(db) && binding_context.is_none_or(|binding_context| { @@ -565,14 +611,18 @@ impl<'db> GenericContext<'db> { /// In this example, `method`'s generic context binds `Self` and `T`, but its inferable set /// also includes `A@C`. This is needed because at each call site, we need to infer the /// specialized class instance type whose method is being invoked. - pub(crate) fn inferable_typevars(self, db: &'db dyn Db) -> InferableTypeVars<'db> { - #[derive(Default)] - struct CollectTypeVars<'db> { - typevars: RefCell>>, + pub(crate) fn inferable_typevars(self, db: &'db dyn Db) -> TypeVarSet<'db> { + struct CollectTypeVars<'a, 'db> { + env: &'a ProgramEnvironment<'db>, + typevars: RefCell, BoundTypeVarInstance<'db>>>, recursion_guard: TypeCollector<'db>, } - impl<'db> TypeVisitor<'db> for CollectTypeVars<'db> { + impl<'db> TypeVisitor<'db> for CollectTypeVars<'_, 'db> { + fn program_environment(&self) -> &ProgramEnvironment<'db> { + self.env + } + fn should_visit_lazy_type_attributes(&self) -> bool { false } @@ -584,9 +634,10 @@ impl<'db> GenericContext<'db> { ) { self.typevars .borrow_mut() - .insert(bound_typevar.identity(db)); + .entry(bound_typevar.identity(db)) + .or_insert(bound_typevar); let typevar = bound_typevar.typevar(db); - if let Some(bound_or_constraints) = typevar.bound_or_constraints(db) { + if let Some(bound_or_constraints) = typevar.bound_or_constraints(db, self.env) { walk_type_var_bounds(db, bound_or_constraints, self); } } @@ -598,18 +649,23 @@ impl<'db> GenericContext<'db> { #[salsa::tracked( returns(copy), - cycle_initial=|_, _, _| InferableTypeVars::None, + cycle_initial=|_, _, _| TypeVarSet::None, heap_size=ruff_memory_usage::heap_size, )] fn inferable_typevars_inner<'db>( db: &'db dyn Db, generic_context: GenericContext<'db>, - ) -> InferableTypeVars<'db> { - let visitor = CollectTypeVars::default(); + ) -> TypeVarSet<'db> { + let env = ProgramEnvironment::from_program(generic_context.program(db)); + let visitor = CollectTypeVars { + env: &env, + typevars: RefCell::default(), + recursion_guard: TypeCollector::default(), + }; for bound_typevar in generic_context.variables(db) { visitor.visit_bound_type_var_type(db, bound_typevar); } - InferableTypeVars::from_typevars(db, visitor.typevars.into_inner()) + TypeVarSet::from_typevars(db, visitor.typevars.into_inner().into_values()) } inferable_typevars_inner(db, self) @@ -697,22 +753,23 @@ impl<'db> GenericContext<'db> { parameters: &Parameters<'db>, return_type: Type<'db>, ) -> Option { + let env = ProgramEnvironment::from_definition(definition); // Find all of the legacy typevars mentioned in the function signature. let mut variables = FxOrderSet::default(); for param in parameters { param .annotated_type() - .find_legacy_typevars(db, Some(definition), &mut variables); + .find_legacy_typevars(db, &env, Some(definition), &mut variables); if let Some(ty) = param.default_type() { - ty.find_legacy_typevars(db, Some(definition), &mut variables); + ty.find_legacy_typevars(db, &env, Some(definition), &mut variables); } } - return_type.find_legacy_typevars(db, Some(definition), &mut variables); + return_type.find_legacy_typevars(db, &env, Some(definition), &mut variables); if variables.is_empty() { return None; } - Some(Self::from_typevar_instances(db, variables)) + Some(Self::from_typevar_instances(db, &env, variables)) } pub(crate) fn merge_pep695_and_legacy( @@ -721,17 +778,17 @@ impl<'db> GenericContext<'db> { legacy_generic_context: Option, ) -> Option { match (legacy_generic_context, pep695_generic_context) { - (Some(legacy_ctx), Some(ctx)) => { + (Some(legacy_ctx), Some(env)) => { if legacy_ctx .variables(db) .exactly_one() .is_ok_and(|bound_typevar| bound_typevar.typevar(db).is_self(db)) { - Some(legacy_ctx.merge(db, ctx)) + Some(legacy_ctx.merge(db, env)) } else { // Invalid mixes retained in the inferred signature are reported during // post-inference validation. - Some(ctx) + Some(env) } } (left, right) => left.or(right), @@ -745,14 +802,15 @@ impl<'db> GenericContext<'db> { definition: Definition<'db>, bases: impl Iterator>, ) -> Option { + let env = ProgramEnvironment::from_definition(definition); let mut variables = FxOrderSet::default(); for base in bases { - base.find_legacy_typevars(db, Some(definition), &mut variables); + base.find_legacy_typevars(db, &env, Some(definition), &mut variables); } if variables.is_empty() { return None; } - Some(Self::from_typevar_instances(db, variables)) + Some(Self::from_typevar_instances(db, &env, variables)) } pub(crate) fn remove_callable_only_typevars( @@ -788,6 +846,7 @@ impl<'db> GenericContext<'db> { FxHashSet>, FxHashMap, CallableType<'db>>, ) { + let env = ProgramEnvironment::from_definition(function_definition); let mut found_only_inside_callable_return = FxHashSet::default(); let replacements = self .found_inside_callable_return @@ -825,10 +884,11 @@ impl<'db> GenericContext<'db> { db, &TypeMapping::ApplySpecialization(apply), TypeContext::default(), - &ApplyTypeMappingVisitor::default(), + &ApplyTypeMappingVisitor::new(&env), ); let generic_context = GenericContext::from_typevar_instances( db, + &env, typevar_replacements.values().copied(), ); let signatures = @@ -850,15 +910,19 @@ impl<'db> GenericContext<'db> { /// A visitor that walks through the parameter and return type annotations, recording /// whether each typevar appears inside and/or outside of a return type `Callable`. - #[derive(Default)] - struct FindTypeVarLocations<'db> { + struct FindTypeVarLocations<'a, 'db> { + env: &'a ProgramEnvironment<'db>, locations: RefCell>, recursion_guard: TypeCollector<'db>, in_return_type: bool, in_callable_type: Cell>>, } - impl<'db> TypeVisitor<'db> for FindTypeVarLocations<'db> { + impl<'db> TypeVisitor<'db> for FindTypeVarLocations<'_, 'db> { + fn program_environment(&self) -> &ProgramEnvironment<'db> { + self.env + } + fn should_visit_lazy_type_attributes(&self) -> bool { false } @@ -925,9 +989,16 @@ impl<'db> GenericContext<'db> { let Some(generic_context) = generic_context else { return (None, return_type); }; + let env = ProgramEnvironment::from_definition(function_definition); // Find whether each typevar appears inside and/or outside a return type Callable. - let mut find_typevar_locations = FindTypeVarLocations::default(); + let mut find_typevar_locations = FindTypeVarLocations { + env: &env, + locations: RefCell::default(), + recursion_guard: TypeCollector::default(), + in_return_type: false, + in_callable_type: Cell::default(), + }; for param in parameters { find_typevar_locations.visit_type(db, param.annotated_type()); } @@ -941,7 +1012,8 @@ impl<'db> GenericContext<'db> { .into_inner() .finalize(db, function_definition); let type_mapping = TypeMapping::RescopeReturnCallables(&replacements); - let return_type = return_type.apply_type_mapping(db, &type_mapping, TypeContext::default()); + let return_type = + return_type.apply_type_mapping(db, &env, &type_mapping, TypeContext::default()); // And lastly remove those typevars from the function's generic context. let mut kept_typevars = generic_context @@ -951,7 +1023,11 @@ impl<'db> GenericContext<'db> { let generic_context = if kept_typevars.peek().is_none() { None } else { - Some(GenericContext::from_typevar_instances(db, kept_typevars)) + Some(GenericContext::from_typevar_instances( + db, + &env, + kept_typevars, + )) }; (generic_context, return_type) @@ -968,12 +1044,13 @@ impl<'db> GenericContext<'db> { ) -> Specialization<'db> { let partial = self.specialize_partial(db, std::iter::repeat_n(None, self.len(db))); if known_class == Some(KnownClass::Tuple) { + let env = ProgramEnvironment::from_program(self.program(db)); Specialization::new( db, self, partial.types(db), None, - Some(TupleType::homogeneous(db, Type::unknown())), + Some(TupleType::homogeneous(db, &env, Type::unknown())), Box::from([]), ) } else { @@ -997,13 +1074,23 @@ impl<'db> GenericContext<'db> { self.specialize(db, types) } - pub(crate) fn unknown_specialization(self, db: &'db dyn Db) -> Specialization<'db> { - self.specialize( + /// Specializes every type parameter to its unknown form. + /// + /// The built-in `tuple` also needs an explicit variable-length tuple shape so that + /// materialization can preserve its element type. + pub(crate) fn unknown_specialization( + self, + db: &'db dyn Db, + known_class: Option, + ) -> Specialization<'db> { + let env = ProgramEnvironment::from_program(self.program(db)); + Specialization::new( db, + self, self.variables(db) .map(|typevar| match typevar.kind(db) { TypeVarKind::LegacyTypeVarTuple | TypeVarKind::Pep695TypeVarTuple => { - Type::homogeneous_tuple(db, Type::unknown()) + Type::homogeneous_tuple(db, &env, Type::unknown()) } TypeVarKind::LegacyParamSpec | TypeVarKind::Pep695ParamSpec @@ -1012,7 +1099,11 @@ impl<'db> GenericContext<'db> { } _ => Type::unknown(), }) - .collect::>(), + .collect::>(), + None, + (known_class == Some(KnownClass::Tuple)) + .then(|| TupleType::homogeneous(db, &env, Type::unknown())), + Box::default(), ) } @@ -1083,6 +1174,7 @@ impl<'db> GenericContext<'db> { db: &'db dyn Db, mut types: Box<[Type<'db>]>, ) -> Specialization<'db> { + let env = ProgramEnvironment::from_program(self.program(db)); let len = types.len(); let variables = self.variables(db).collect_vec(); loop { @@ -1107,6 +1199,7 @@ impl<'db> GenericContext<'db> { }; let updated = types[i].apply_type_mapping( db, + &env, &TypeMapping::ApplySpecialization(specialization), TypeContext::default(), ); @@ -1144,6 +1237,7 @@ impl<'db> GenericContext<'db> { I: IntoIterator>>, I::IntoIter: ExactSizeIterator, { + let env = ProgramEnvironment::from_program(self.program(db)); let types = types.into_iter(); let variables = self.variables(db); assert_eq!(self.len(db), types.len()); @@ -1160,7 +1254,7 @@ impl<'db> GenericContext<'db> { for typevar in variables.clone() { expanded.push(match typevar.kind(db) { TypeVarKind::LegacyTypeVarTuple | TypeVarKind::Pep695TypeVarTuple => { - Type::homogeneous_tuple(db, Type::unknown()) + Type::homogeneous_tuple(db, &env, Type::unknown()) } TypeVarKind::LegacyParamSpec | TypeVarKind::Pep695ParamSpec @@ -1191,6 +1285,7 @@ impl<'db> GenericContext<'db> { }; let default = default.apply_type_mapping( db, + &env, &TypeMapping::ApplySpecialization(specialization), TypeContext::default(), ); @@ -1235,7 +1330,7 @@ pub struct Specialization<'db> { /// `Bottom[A[Any]]` is a subtype of all materializations of `A[Any]`, and is represented /// with `Some(MaterializationKind::Bottom)`. /// The `materialization_kind` field may be non-`None` only if the specialization contains - /// dynamic types in invariant positions. + /// dynamic types in invariant positions or positions with constrained type variables. #[returns(copy)] pub(crate) materialization_kind: Option, @@ -1415,10 +1510,11 @@ impl<'db> Specialization<'db> { return self; } + let env = ProgramEnvironment::from_program(self.generic_context(db).program(db)); Self::new( db, self.generic_context(db), - [tuple.tuple(db).homogeneous_element_type(db)].as_slice(), + [tuple.tuple(db).homogeneous_element_type(db, &env)].as_slice(), self.materialization_kind(db), None, self.projections(db).to_vec().into_boxed_slice(), @@ -1468,16 +1564,19 @@ impl<'db> Specialization<'db> { /// 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 env = &ProgramEnvironment::from_program(other.generic_context(db).program(db)); let new_specialization = self.apply_type_mapping( db, + env, &TypeMapping::ApplySpecialization(ApplySpecialization::Specialization(other)), ); match other.materialization_kind(db) { None => new_specialization, Some(materialization_kind) => new_specialization.materialize_impl( db, + env, materialization_kind, - &ApplyTypeMappingVisitor::default(), + &ApplyTypeMappingVisitor::new(env), ), } } @@ -1497,23 +1596,31 @@ impl<'db> Specialization<'db> { ) } - pub(crate) fn apply_type_mapping<'a>( + fn apply_type_mapping<'a>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'a, 'db>, ) -> Self { - self.apply_type_mapping_impl(db, type_mapping, &[], &ApplyTypeMappingVisitor::default()) + self.apply_type_mapping_impl( + db, + env, + type_mapping, + &[], + &ApplyTypeMappingVisitor::new(env), + ) } pub(crate) fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'a, 'db>, tcx: &[Type<'db>], - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { if let TypeMapping::Materialize(materialization_kind) = type_mapping { - return self.materialize_impl(db, *materialization_kind, visitor); + return self.materialize_impl(db, env, *materialization_kind, visitor); } let mut new_materialization_kind = self.materialization_kind(db); @@ -1527,6 +1634,7 @@ impl<'db> Specialization<'db> { materialization_kind, }, ) => { + let env = visitor.env; // An invariant type argument cannot be materialized in isolation. Keep the // specialized argument and record the materialization on this specialization. // Comparing both mappings distinguishes substituted gradual types from @@ -1534,17 +1642,19 @@ impl<'db> Specialization<'db> { // visitors because their transformation caches are keyed only by type. let specialized = ty.apply_type_mapping_impl( db, + env, &TypeMapping::ApplySpecialization(*specialization), tcx, - &ApplyTypeMappingVisitor::default(), + &ApplyTypeMappingVisitor::new(env), ); if new_materialization_kind.is_none() { let materialized = ty.apply_type_mapping_impl( db, + env, type_mapping, tcx, - &ApplyTypeMappingVisitor::default(), + &ApplyTypeMappingVisitor::new(env), ); if specialized != materialized { new_materialization_kind = Some(*materialization_kind); @@ -1566,6 +1676,7 @@ impl<'db> Specialization<'db> { }, ) => ty.apply_type_mapping_impl( db, + env, &TypeMapping::ProjectUseSiteVariance { specialization: *specialization, position: position.compose(variance), @@ -1582,6 +1693,7 @@ impl<'db> Specialization<'db> { { ty.apply_type_mapping_impl( db, + env, &TypeMapping::Promote( PromotionMode::On, PromotionKind::RegularKeepingLiterals, @@ -1591,15 +1703,15 @@ impl<'db> Specialization<'db> { ) } (variance, _) if variance.is_covariant() => { - ty.apply_type_mapping_impl(db, type_mapping, tcx, visitor) + ty.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor) } - _ => ty.apply_type_mapping_impl(db, &type_mapping.flip(), tcx, visitor), + _ => ty.apply_type_mapping_impl(db, env, &type_mapping.flip(), tcx, visitor), } }); let original_tuple_inner = self.tuple_inner(db); let tuple_inner = original_tuple_inner.and_then(|tuple| { - tuple.apply_type_mapping_impl(db, type_mapping, TypeContext::default(), visitor) + tuple.apply_type_mapping_impl(db, env, type_mapping, TypeContext::default(), visitor) }); // Keep this check in sync with every field that can be transformed above. @@ -1641,6 +1753,7 @@ impl<'db> Specialization<'db> { pub(crate) fn combine(self, db: &'db dyn Db, other: Self) -> Self { let generic_context = self.generic_context(db); assert_eq!(other.generic_context(db), generic_context); + let env = ProgramEnvironment::from_program(generic_context.program(db)); // 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. It's also wrong in case a typevar has a default, in which case it @@ -1652,7 +1765,7 @@ impl<'db> Specialization<'db> { .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_two_elements(db, *self_type, *other_type), + _ => UnionType::from_two_elements(db, &env, *self_type, *other_type), }) .collect(); // TODO: Combine the tuple specs too @@ -1670,25 +1783,26 @@ impl<'db> Specialization<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { let types = if nested { self.types(db) .iter() - .map(|ty| ty.recursive_type_normalized_impl(db, div, true)) + .map(|ty| ty.recursive_type_normalized_impl(db, env, div, true)) .collect::>>()? } else { self.types(db) .iter() .map(|ty| { - ty.recursive_type_normalized_impl(db, div, true) + ty.recursive_type_normalized_impl(db, env, div, true) .unwrap_or(div) }) .collect::>() }; let tuple_inner = match self.tuple_inner(db) { - Some(tuple) => Some(tuple.recursive_type_normalized_impl(db, div, nested)?), + Some(tuple) => Some(tuple.recursive_type_normalized_impl(db, env, div, nested)?), None => None, }; let context = self.generic_context(db); @@ -1705,34 +1819,60 @@ impl<'db> Specialization<'db> { pub(super) fn materialize_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, materialization_kind: MaterializationKind, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { // The top and bottom materializations are fully static types already, so materializing them // further does nothing. if self.materialization_kind(db).is_some() { return self; } - let mut has_dynamic_invariant_typevar = false; + let mut has_unsimplified_dynamic_typevar = false; let types = self.map_types(db, |_, bound_typevar, vartype| { - match specialization_variance(db, bound_typevar) { + let variance = specialization_variance(db, bound_typevar); + let top_materialization = + vartype.materialize(db, env, MaterializationKind::Top, visitor); + let has_dynamic_type = + !visitor.is_equivalent_to_materialization(db, vartype, top_materialization); + + match variance { TypeVarVariance::Bivariant => { // With bivariance, all specializations are subtypes of each other, // so any materialization is acceptable. - vartype.materialize(db, MaterializationKind::Top, visitor) + top_materialization } - TypeVarVariance::Covariant => { - vartype.materialize(db, materialization_kind, visitor) + TypeVarVariance::Covariant | TypeVarVariance::Contravariant + if has_dynamic_type && bound_typevar.typevar(db).is_constrained(db) => + { + has_unsimplified_dynamic_typevar = true; + vartype } - TypeVarVariance::Contravariant => { - vartype.materialize(db, materialization_kind.flip(), visitor) + TypeVarVariance::Covariant | TypeVarVariance::Contravariant => { + let effective_materialization_kind = if variance.is_covariant() { + materialization_kind + } else { + materialization_kind.flip() + }; + let materialized = + vartype.materialize(db, env, effective_materialization_kind, visitor); + + if has_dynamic_type + && effective_materialization_kind == MaterializationKind::Top + && let Some(upper_bound) = bound_typevar.top_materialized_upper_bound(db) + { + IntersectionType::from_two_elements( + db, + visitor.env, + materialized, + upper_bound, + ) + } else { + materialized + } } TypeVarVariance::Invariant => { - let top_materialization = - vartype.materialize(db, MaterializationKind::Top, visitor); - if !visitor.is_equivalent_to_materialization(db, vartype, top_materialization) { - has_dynamic_invariant_typevar = true; - } + has_unsimplified_dynamic_typevar |= has_dynamic_type; vartype } } @@ -1742,16 +1882,14 @@ impl<'db> Specialization<'db> { // Tuples are immutable, so tuple element types are always in covariant position. tuple.apply_type_mapping_impl( db, + env, &TypeMapping::Materialize(materialization_kind), TypeContext::default(), visitor, ) }); - let new_materialization_kind = if has_dynamic_invariant_typevar { - Some(materialization_kind) - } else { - None - }; + let new_materialization_kind = + has_unsimplified_dynamic_typevar.then_some(materialization_kind); // Keep this check in sync with every field that can be transformed above. let specialization_unchanged = matches!(&types, Cow::Borrowed(_)) && tuple_inner == original_tuple_inner @@ -1773,15 +1911,17 @@ impl<'db> Specialization<'db> { pub(crate) fn is_disjoint_from<'c>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, other: Self, constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, ) -> ConstraintSet<'db, 'c> { let relation_visitor = HasRelationToVisitor::default(constraints); let disjointness_visitor = IsDisjointVisitor::default(constraints); let signature_relation_visitor = SignatureRelationVisitor::default(); - let materialization_visitor = ApplyTypeMappingVisitor::default(); + let materialization_visitor = ApplyTypeMappingVisitor::new(env); let checker = DisjointnessChecker::new( + env, constraints, inferable, &relation_visitor, @@ -1789,21 +1929,22 @@ impl<'db> Specialization<'db> { &signature_relation_visitor, &materialization_visitor, ); - checker.check_specialization_pair(db, self, other) + checker.check_specialization_pair(db, env, self, other) } pub(crate) fn find_legacy_typevars_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, ) { if let Some(tuple) = self.tuple_inner(db) { - tuple.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + tuple.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } else { for ty in self.types(db) { - ty.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + ty.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } } } @@ -1827,6 +1968,72 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { return self.check_tuple_type_pair(db, source_tuple, target_tuple); } + let env = self.env; + + // A gradual specialization is a subtype of a fully static specialization when all its + // valid materializations are subtypes. Materializing the source applies declared bounds + // and constraints before comparing arguments. This establishes `C[Any] <: Top[C[Any]]` + // and lets negative `isinstance` narrowing exclude every specialization of `C`. + // + // This transformation is sound for directional subtyping and non-pure redundancy. + // Assignability and pure redundancy must retain the source's gradual semantics. + if matches!( + self.relation, + TypeRelation::Subtyping + | TypeRelation::SubtypingAssuming + | TypeRelation::Redundancy { pure: false } + ) && ( + // Explicitly materialized sources are already static and cannot advance further. + source.materialization_kind(db).is_none() + ) && ( + // Performance only: `source_top != source` below already handles unchanged + // arguments. Without expanding aliases, treat them as potentially gradual. + source.types(db).iter().any(|ty| { + any_over_type(db, env, *ty, false, |ty| { + ty.is_dynamic() || matches!(ty, Type::TypeAlias(_)) + }) + }) + ) && ( + // Avoid the `self.always()` type-variable shortcut in + // `check_subtyping_in_invariant_position`: it would incorrectly conclude + // that `Top[Inv[Any]] <: Inv[T]` for an unresolved `T`. + // TODO: remove this once that shortcut is removed. + target + .types(db) + .iter() + .all(|ty| !ty.has_typevar_or_typevar_instance(db, env)) + ) && ( + // Only non-pure redundancy needs a target already equal to its top. + // Materializing the source otherwise loses the bottom needed to + // simplify `Covariant[Any] | Covariant[Any | str]`. Comparing both + // top and bottom is a possible alternative, but it gets more complex + // due to the need to preserve Divergent markers. Also the fact that we currently + // simplify tuples containing `Never` to `Never` means that for + // `class C[T: tuple[int, int]]`, `C[tuple[Any, int]]` and `C[tuple[int, Any]]` + // have the same top and bottom but expose `Any` in different tuple positions. + // TODO: Try resolving the above issues so we can compare top/bottom subtyping here. + !matches!(self.relation, TypeRelation::Redundancy { pure: false }) + || target + == target.materialize_impl( + db, + env, + MaterializationKind::Top, + self.materialization_visitor, + ) + ) { + let source_top = source.materialize_impl( + db, + env, + MaterializationKind::Top, + self.materialization_visitor, + ); + // Dynamic arguments can still be unchanged by top materialization; retrying + // the same pair would recurse indefinitely. + if source_top != source { + return self.check_specialization_pair(db, source_top, target); + } + } + let source_materialization_kind = source.materialization_kind(db); let target_materialization_kind = target.materialization_kind(db); @@ -1876,16 +2083,51 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { match effective { TypeVarVariance::Invariant => self.check_relation_in_invariant_position( db, + env, *source_type, source_materialization_kind, *target_type, target_materialization_kind, ), - TypeVarVariance::Covariant => { - self.check_type_pair(db, *source_type, *target_type) - } - TypeVarVariance::Contravariant => { - self.check_type_pair(db, *target_type, *source_type) + TypeVarVariance::Covariant | TypeVarVariance::Contravariant => { + let ( + source_type, + source_materialization, + target_type, + target_materialization, + ) = if effective.is_covariant() { + ( + *source_type, + source_materialization_kind, + *target_type, + target_materialization_kind, + ) + } else { + ( + *target_type, + target_materialization_kind.map(MaterializationKind::flip), + *source_type, + source_materialization_kind.map(MaterializationKind::flip), + ) + }; + + self.check_type_pair( + db, + self.materialize_constrained_type_argument( + db, + env, + bound_typevar, + source_type, + source_materialization, + ), + self.materialize_constrained_type_argument( + db, + env, + bound_typevar, + target_type, + target_materialization, + ), + ) } TypeVarVariance::Bivariant => self.always(), } @@ -1893,12 +2135,107 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { ) } + /// Materializes a constrained covariant or contravariant argument for a relation check. + /// + /// A constrained type variable can only take one of its declared alternatives. For example, + /// replacing `Any` with `int | str` for `class C[T: (int, str)]` would create the invalid + /// specialization `C[int | str]`. The caller preserves the enclosing `Top[C[Any]]`; this + /// helper combines the reachable constraints into `int | str` only for the relation check, + /// without constructing `C[int | str]`. + fn materialize_constrained_type_argument( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + bound_typevar: BoundTypeVarInstance<'db>, + ty: Type<'db>, + materialization: Option, + ) -> Type<'db> { + let Some(materialization) = materialization else { + return ty; + }; + + // A lazy upper bound may refer back to the enclosing specialization. Check whether this + // type variable is constrained before evaluating its bounds or constraints. + + let typevar = bound_typevar.typevar(db); + if !typevar.is_constrained(db) { + return ty; + } + + let argument_top = ty.materialize( + db, + env, + MaterializationKind::Top, + self.materialization_visitor, + ); + if self + .materialization_visitor + .is_equivalent_to_materialization(db, ty, argument_top) + { + return ty; + } + let env = self.env; + let Some(constraints) = typevar.constraints(db, env) else { + return ty; + }; + let argument_bottom = ty.materialize( + db, + env, + MaterializationKind::Bottom, + self.materialization_visitor, + ); + + let viable_constraints = constraints.iter().filter_map(|constraint| { + let constraint_top = constraint.materialize( + db, + env, + MaterializationKind::Top, + self.materialization_visitor, + ); + + // A viable constraint must overlap the argument's upper materialization and contain + // its lower materialization. The upper check matters for `Intersection[int, Any]`, + // and the lower check matters for `Any | int`. + if argument_top.is_disjoint_from(db, env, constraint_top) + || !argument_bottom.is_subtype_of(db, env, constraint_top) + { + return None; + } + + Some(match materialization { + MaterializationKind::Top => constraint_top, + MaterializationKind::Bottom => constraint.materialize( + db, + env, + MaterializationKind::Bottom, + self.materialization_visitor, + ), + }) + }); + + match materialization { + MaterializationKind::Top => IntersectionType::from_two_elements( + db, + env, + argument_top, + UnionType::from_elements(db, env, viable_constraints), + ), + MaterializationKind::Bottom => UnionType::from_two_elements( + db, + env, + argument_bottom, + IntersectionType::from_elements(db, env, viable_constraints), + ), + } + } + /// Whether two types encountered in an invariant position /// have a relation (subtyping or assignability), taking into account /// that the two types may come from a top or bottom materialization. fn check_relation_in_invariant_position( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, source_type: Type<'db>, source_materialization: Option, target_type: Type<'db>, @@ -1913,6 +2250,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // is the same as assignability. (Some(source_mat), Some(target_mat), _) => self.check_subtyping_in_invariant_position( db, + env, source_type, source_mat, target_type, @@ -1941,18 +2279,31 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { && let (Type::TypeVar(typevar), ty) | (ty, Type::TypeVar(typevar)) = (source_type, target_type) && !ty.is_type_var() - // Preserve union distribution before constructing constraints. Storing the - // entire union as an exact bound makes solving common generic calls involving - // large unions significantly more expensive. - && !ty.is_union() + && ( + // Preserve union distribution before constructing constraints. Storing the + // entire union as an exact bound makes solving common generic calls involving + // large unions significantly more expensive. + !ty.is_union() + ) { let ty = ty.materialized_divergent_fallback().unwrap_or(ty); + let env = self.env; let (lower, upper) = if self.relation.is_subtyping() { - (ty.top_materialization(db), ty.bottom_materialization(db)) + ( + ty.top_materialization(db, env), + ty.bottom_materialization(db, env), + ) } else { (ty, ty) }; - ConstraintSet::constrain_typevar(db, self.constraints, typevar, lower, upper) + ConstraintSet::constrain_typevar( + db, + env, + self.constraints, + typevar, + lower, + upper, + ) } else { self.check_type_pair(db, target_type, source_type).and( db, @@ -1970,6 +2321,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { | TypeRelation::SubtypingAssuming, ) => self.check_subtyping_in_invariant_position( db, + env, source_type, MaterializationKind::Top, target_type, @@ -1983,6 +2335,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { | TypeRelation::SubtypingAssuming, ) => self.check_subtyping_in_invariant_position( db, + env, source_type, source_mat, target_type, @@ -1992,6 +2345,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { (None, Some(target_mat), TypeRelation::Assignability) => self .check_subtyping_in_invariant_position( db, + env, source_type, MaterializationKind::Bottom, target_type, @@ -2000,6 +2354,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { (Some(source_mat), None, TypeRelation::Assignability) => self .check_subtyping_in_invariant_position( db, + env, source_type, source_mat, target_type, @@ -2011,22 +2366,33 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { fn check_subtyping_in_invariant_position( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, source_type: Type<'db>, source_materialization: MaterializationKind, target_type: Type<'db>, target_materialization: MaterializationKind, ) -> ConstraintSet<'db, 'c> { - let source_top = - source_type.materialize(db, MaterializationKind::Top, self.materialization_visitor); + let source_top = source_type.materialize( + db, + env, + MaterializationKind::Top, + self.materialization_visitor, + ); let source_bottom = source_type.materialize( db, + env, MaterializationKind::Bottom, self.materialization_visitor, ); - let target_top = - target_type.materialize(db, MaterializationKind::Top, self.materialization_visitor); + let target_top = target_type.materialize( + db, + env, + MaterializationKind::Top, + self.materialization_visitor, + ); let target_bottom = target_type.materialize( db, + env, MaterializationKind::Bottom, self.materialization_visitor, ); @@ -2195,6 +2561,7 @@ impl<'c, 'db> DisjointnessChecker<'_, 'c, 'db> { pub(super) fn check_specialization_pair( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, left: Specialization<'db>, right: Specialization<'db>, ) -> ConstraintSet<'db, 'c> { @@ -2228,6 +2595,7 @@ impl<'c, 'db> DisjointnessChecker<'_, 'c, 'db> { self.as_relation_checker(TypeRelation::Subtyping) .check_subtyping_in_invariant_position( db, + env, left_type, MaterializationKind::Bottom, right_type, @@ -2347,11 +2715,13 @@ impl<'db> Type<'db> { pub(crate) fn substitute_one_typevar( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, bound_typevar: BoundTypeVarInstance<'db>, replacement: Type<'db>, ) -> Type<'db> { self.apply_type_mapping( db, + env, &TypeMapping::ApplySpecialization(ApplySpecialization::Single( bound_typevar, replacement, @@ -2365,8 +2735,9 @@ impl<'db> Type<'db> { /// specialization of a generic function. pub(crate) struct SpecializationBuilder<'db, 'c> { db: &'db dyn Db, + env: &'c ProgramEnvironment<'db>, constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, pending: ConstraintSet<'db, 'c>, types: FxHashMap, UnionAccumulator<'db>>, /// Typevars that were inferred only from bivariant positions, which contribute no bound to @@ -2394,7 +2765,21 @@ impl get_size2::GetSize for TypeVarInference<'_> {} impl<'db> TypeVarInference<'db> { /// Project this inference result into a closed specialization. pub(crate) fn specialization(self, db: &'db dyn Db) -> Specialization<'db> { - #[salsa::tracked(returns(copy))] + #[salsa::tracked( + returns(copy), + cycle_initial=|db, _, inference: TypeVarInference<'db>| { + inference.generic_context(db).unknown_specialization(db, None) + }, + cycle_fn=|db, cycle: &salsa::Cycle, previous: &Specialization<'db>, current: Specialization<'db>, inference: TypeVarInference<'db>| { + if cycle.iteration() <= crate::TAINTED_CYCLES { + current + } else { + current + .merge_cycle_recovery(db, *previous) + .unwrap_or_else(|| inference.generic_context(db).unknown_specialization(db, None)) + } + } + )] fn specialization_inner<'db>( db: &'db dyn Db, inference: TypeVarInference<'db>, @@ -2447,11 +2832,13 @@ enum ConstraintSetInferenceError<'db> { impl<'db, 'c> SpecializationBuilder<'db, 'c> { pub(crate) fn new( db: &'db dyn Db, + env: &'c ProgramEnvironment<'db>, constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, ) -> Self { Self { db, + env, constraints, inferable, pending: ConstraintSet::from_bool(constraints, true), @@ -2461,6 +2848,15 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { } } + /// Adds a constraint set to the pending specialization and projects its valid solutions into + /// the legacy type mappings. + pub(crate) fn add_constraint_set( + &mut self, + set: ConstraintSet<'db, 'c>, + ) -> Result<(), SpecializationError<'db>> { + self.infer_from_constraint_set(set) + } + /// Build a specialization, using a caller-provided hook to select the solution for each /// typevar. /// @@ -2476,12 +2872,13 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { generic_context: GenericContext<'db>, mut choose: impl FnMut(BoundTypeVarInstance<'db>, Option<&PathBound<'db>>) -> Option>, ) -> Specialization<'db> { + let db = self.db; let types = self .solve_pending_with(generic_context, &mut choose) .unwrap_or_else(|()| self.solve_hash_map_with(generic_context, &mut choose)); let specialization = generic_context - .variables_inner(self.db) + .variables_inner(db) .iter() .map(|(identity, variable)| { types @@ -2490,7 +2887,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { .or_else(|| choose(*variable, None)) }); - generic_context.specialize_recursive(self.db, specialization) + generic_context.specialize_recursive(db, specialization) } /// Build raw type-variable inference, preserving which type variables were left unsolved. @@ -2516,8 +2913,10 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { argument_relations: impl IntoIterator, Type<'db>)>, mut choose: impl FnMut(BoundTypeVarInstance<'db>, Option<&PathBound<'db>>) -> Option>, ) -> TypeVarInference<'db> { + let db = self.db; for (formal, actual) in argument_relations { - let when = actual.when_constraint_set_assignable_to(self.db, formal, self.constraints); + let when = + actual.when_constraint_set_assignable_to(db, self.env, formal, self.constraints); let _ = self.add_type_mappings_from_constraint_set(when); } @@ -2530,13 +2929,14 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { generic_context: GenericContext<'db>, types: &FxHashMap, Type<'db>>, ) -> TypeVarInference<'db> { + let db = self.db; let inferred: Box<[_]> = generic_context - .variables_inner(self.db) + .variables_inner(db) .keys() .map(|identity| types.get(identity).copied()) .collect(); - TypeVarInference::new(self.db, generic_context, inferred) + TypeVarInference::new(db, generic_context, inferred) } fn solve_pending_with( @@ -2544,9 +2944,10 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { generic_context: GenericContext<'db>, choose: &mut impl FnMut(BoundTypeVarInstance<'db>, Option<&PathBound<'db>>) -> Option>, ) -> Result, Type<'db>>, ()> { + let db = self.db; // TODO: Move `ParamSpec` and `TypeVarTuple` handling to the new constraint solver. if generic_context - .variables_inner(self.db) + .variables_inner(db) .values() .any(|typevar| typevar.is_parameter_pack(self.db) || typevar.is_typevartuple(self.db)) { @@ -2566,7 +2967,8 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // skipped projection changed precision in LiteralString tests. See the // `ty_micro[pydantic_core_schema_dict]` benchmark for a minimized reproducer. let solutions = match self.pending.solutions_with( - self.db, + db, + self.env, self.constraints, self.inferable, |_variance, path_bound| { @@ -2575,7 +2977,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { return Ok(Some(ty)); } - PathBounds::default_solve(self.db, self.constraints, path_bound) + PathBounds::default_solve(db, self.env, self.constraints, path_bound) }, ) { Solutions::Unsatisfiable => return Err(()), @@ -2588,12 +2990,12 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { let mut types = FxHashMap::default(); for solution in solutions { for binding in solution { - let identity = binding.bound_typevar.identity(self.db); + let identity = binding.bound_typevar.identity(db); types .entry(identity) .and_modify(|existing| { *existing = - UnionType::from_two_elements(self.db, *existing, binding.solution); + UnionType::from_two_elements(db, self.env, *existing, binding.solution); }) .or_insert(binding.solution); } @@ -2607,7 +3009,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // TODO: This is a solution-level projection. A more principled version would live in the // constraint-set solution extraction layer, taking an explicit domain of typevars to solve // for and existentially quantifying away the other typevars in that domain. - for (identity, variable) in generic_context.variables_inner(self.db) { + for (identity, variable) in generic_context.variables_inner(db) { if let Some(ty) = types.get_mut(identity) { *ty = self.remove_inferable_typevar_artifacts_from_solution(*variable, *ty); } @@ -2647,6 +3049,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { identity: BoundTypeVarIdentity<'db>, ty: Type<'db>, ) -> bool { + let db = self.db; match ty { // A bare `T = U` edge only replaces one typevar with another; it does not wrap the // replacement in additional structure and therefore cannot grow during repeated @@ -2655,18 +3058,18 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // Unions and intersections are flattened and deduplicated as they are constructed. // A cyclic reference directly inside one can add elements but cannot create // unbounded nesting. Keep looking inside its elements for a genuinely embedded edge. - Type::Union(union) => union.elements(self.db).iter().any(|element| { + Type::Union(union) => union.elements(db).iter().any(|element| { self.has_expanding_cycle(generic_context, types, identity, *element) }), Type::Intersection(intersection) => intersection - .iter_positive(self.db) - .chain(intersection.iter_negative(self.db)) + .iter_positive(db) + .chain(intersection.iter_negative(db)) .any(|element| self.has_expanding_cycle(generic_context, types, identity, element)), - _ => any_over_type(self.db, ty, false, |nested| { + _ => any_over_type(db, self.env, ty, false, |nested| { nested.as_typevar().is_some_and(|dependency| { - let dependency = dependency.identity(self.db); + let dependency = dependency.identity(db); dependency != identity - && generic_context.contains(self.db, dependency) + && generic_context.contains(db, dependency) && self.reaches_pending_typevar( generic_context, types, @@ -2687,6 +3090,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { target: BoundTypeVarIdentity<'db>, visited: &RefCell>>, ) -> bool { + let db = self.db; if identity == target { return true; } @@ -2695,13 +3099,13 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { } types.get(&identity).is_some_and(|ty| { - any_over_type(self.db, *ty, false, |nested| { + any_over_type(db, self.env, *ty, false, |nested| { nested.as_typevar().is_some_and(|dependency| { - let dependency = dependency.identity(self.db); + let dependency = dependency.identity(db); // Recursive specialization skips a typevar's own slot. Only references // through other mappings can recursively expand. dependency != identity - && generic_context.contains(self.db, dependency) + && generic_context.contains(db, dependency) && self.reaches_pending_typevar( generic_context, types, @@ -2719,14 +3123,19 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { target: BoundTypeVarInstance<'db>, ty: Type<'db>, ) -> bool { - let target_context = target.binding_context(self.db); + let db = self.db; + let target_context = target.binding_context(db); + let target_freshness = target.freshness(db); ty.as_typevar().is_some_and(|typevar| { // Relationships across binding contexts can intentionally remap one generic context - // onto another, as with constructor `self` annotations. Synthetic contexts do not - // identify a single source-level binding, so they are not safe to project either. - target_context != BindingContext::Synthetic - && typevar.is_inferable(self.db, self.inferable) - && typevar.binding_context(self.db) == target_context + // onto another, as with constructor `self` annotations. Relationships across fresh + // occurrences preserve an outer generic value through a recursive call. Synthetic + // contexts do not identify a single source-level binding, so they are not safe to + // project either. + !matches!(target_context, BindingContext::Synthetic(_)) + && typevar.is_inferable(db, self.inferable) + && typevar.binding_context(db) == target_context + && typevar.freshness(db) == target_freshness }) } @@ -2737,13 +3146,14 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { target: BoundTypeVarInstance<'db>, ty: Type<'db>, ) -> Type<'db> { + let db = self.db; match ty { Type::Intersection(intersection) if intersection - .iter_positive(self.db) + .iter_positive(db) .any(|element| !self.is_inferable_typevar_artifact(target, element)) => { - intersection.map_positive(self.db, |element| { + intersection.map_positive(db, self.env, |element| { if self.is_inferable_typevar_artifact(target, *element) { Type::object() } else { @@ -2753,11 +3163,11 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { } Type::Union(union) if union - .elements(self.db) + .elements(db) .iter() .any(|element| !self.is_inferable_typevar_artifact(target, *element)) => { - union.map(self.db, |element| { + union.map(db, self.env, |element| { if self.is_inferable_typevar_artifact(target, *element) { Type::Never } else { @@ -2774,8 +3184,9 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { generic_context: GenericContext<'db>, choose: &mut impl FnMut(BoundTypeVarInstance<'db>, Option<&PathBound<'db>>) -> Option>, ) -> FxHashMap, Type<'db>> { + let db = self.db; generic_context - .variables_inner(self.db) + .variables_inner(db) .iter() .filter_map(|(identity, variable)| { Some((*identity, self.mapped_type(*variable, choose)?)) @@ -2790,10 +3201,11 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { variable: BoundTypeVarInstance<'db>, choose: &mut impl FnMut(BoundTypeVarInstance<'db>, Option<&PathBound<'db>>) -> Option>, ) -> Option> { + let env = self.env; let mapped_ty = self .types .get_mut(&variable.identity(self.db)) - .map(|accumulator| accumulator.get_or_build(self.db)); + .map(|accumulator| accumulator.get_or_build(self.db, env)); match mapped_ty { Some(mapped_ty) => { @@ -2809,7 +3221,8 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { bound_typevar: BoundTypeVarInstance<'db>, ty: Type<'db>, ) { - let identity = bound_typevar.identity(self.db); + let db = self.db; + let identity = bound_typevar.identity(db); match self.types.entry(identity) { Entry::Occupied(mut entry) => { match bound_typevar.kind(self.db) { @@ -2829,28 +3242,28 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // candidates element-wise using unions. // https://typing.python.org/en/latest/spec/generics.html#type-variable-tuple-equality let accumulator = entry.get_mut(); - let existing = accumulator.get_or_build(self.db); + let existing = accumulator.get_or_build(db, self.env); if existing == ty { return; } - let Some(existing_tuple) = existing.exact_tuple_instance_spec(self.db) - else { + let Some(existing_tuple) = existing.exact_tuple_instance_spec(db) else { return; }; - let Some(new_tuple) = ty.exact_tuple_instance_spec(self.db) else { + let Some(new_tuple) = ty.exact_tuple_instance_spec(db) else { return; }; if existing_tuple.len() != new_tuple.len() { return; } let unioned = TupleSpecBuilder::from(existing_tuple.as_ref()) - .union(self.db, &new_tuple) + .union(db, self.env, &new_tuple) .build(); - *accumulator = - UnionAccumulator::new(Type::tuple(TupleType::new(self.db, &unioned))); + *accumulator = UnionAccumulator::new(Type::tuple(TupleType::new( + db, self.env, &unioned, + ))); } _ => { - entry.get_mut().add(self.db, ty); + entry.get_mut().add(db, self.env, ty); } } } @@ -2865,20 +3278,21 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { bound_typevar: BoundTypeVarInstance<'db>, bounds: ConstraintBounds<'db>, ) { + let db = self.db; let identity = bound_typevar.identity(self.db); if bound_typevar.is_parameter_pack(self.db) && !self.paramspec_seen.insert(identity) { return; } let constraint = ConstraintSet::constrain_typevar_with_bounds( - self.db, + db, + self.env, self.constraints, bound_typevar, bounds.lower, bounds.upper, ); - self.pending - .intersect(self.db, self.constraints, constraint); + self.pending.intersect(db, self.constraints, constraint); } pub(crate) fn inferred_type_is_assignable_to( @@ -2886,12 +3300,13 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { bound_typevar: BoundTypeVarIdentity<'db>, ty: Type<'db>, ) -> bool { + let db = self.db; self.types .get_mut(&bound_typevar) .is_some_and(|inferred_ty| { inferred_ty - .get_or_build(self.db) - .is_assignable_to(self.db, ty) + .get_or_build(db, self.env) + .is_assignable_to(db, self.env, ty) }) } @@ -2939,13 +3354,16 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { &mut self, set: ConstraintSet<'db, 'c>, ) -> Result<(), ConstraintSetInferenceError<'db>> { + let db = self.db; let mut first_error = None; let solutions = match set.solutions_with( - self.db, + db, + self.env, self.constraints, self.inferable, |_variance, path_bound| { - let solution = PathBounds::default_solve(self.db, self.constraints, path_bound); + let solution = + PathBounds::default_solve(db, self.env, self.constraints, path_bound); if solution.is_err() && first_error.is_none() { first_error = self.specialization_error_from_failed_bounds(path_bound); } @@ -2981,15 +3399,16 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { &self, path_bound: &PathBound<'db>, ) -> Option> { + let db = self.db; let bound_typevar = path_bound.bound_typevar; let argument = path_bound.lower?; match bound_typevar - .typevar(self.db) - .bound_or_constraints(self.db)? + .typevar(db) + .bound_or_constraints(db, self.env)? { TypeVarBoundOrConstraints::UpperBound(bound) => (!argument - .when_assignable_to(self.db, bound, self.constraints, self.inferable) - .is_always_satisfied(self.db)) + .when_assignable_to(db, self.env, bound, self.constraints, self.inferable) + .is_always_satisfied(db, self.env)) .then_some(SpecializationError::MismatchedBound { bound_typevar, argument, @@ -3011,27 +3430,60 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { &mut self, when: ConstraintSet<'db, 'c>, ) -> Result<(), SpecializationError<'db>> { + let db = self.db; let result = self.add_type_mappings_from_constraint_set(when); - self.pending.intersect(self.db, self.constraints, when); + self.pending.intersect(db, self.constraints, when); match result { Ok(()) | Err(ConstraintSetInferenceError::Unsatisfiable) => Ok(()), Err(ConstraintSetInferenceError::InvalidTypeVar(error)) => Err(error), } } - /// Returns common protocol constraints for a union containing only `TypedDict`s when every + /// Returns common protocol constraints for the `TypedDict` members of a union when every such /// member has the same constraints as their shared `Mapping[str, object]` fallback. fn common_typed_dict_protocol_constraints( &self, formal: Type<'db>, actual: UnionType<'db>, ) -> Option> { + fn is_string_keyed_mapping<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + ) -> bool { + let Type::NominalInstance(instance) = ty.resolve_type_alias(db) else { + return false; + }; + + matches!( + instance.class(db, env).known(db), + Some( + KnownClass::Dict + | KnownClass::Mapping + | KnownClass::MutableMapping + | KnownClass::DefaultDict + | KnownClass::ChainMap + | KnownClass::OrderedDict + ) + ) && instance + .class(db, env) + .into_generic_alias() + .is_some_and(|alias| { + matches!( + alias.specialization(db).types(db), + [key, _] if key.resolve_type_alias(db) == KnownClass::Str.to_instance(db, env) + ) + }) + } + fn collect_typed_dicts<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, resolving: &mut FxHashSet>, completed: &mut FxHashMap, bool>, typed_dicts: &mut FxHashSet>, + other_types: &mut FxOrderSet>, ) -> bool { let ty = ty.resolve_type_alias(db); if let Some(result) = completed.get(&ty) { @@ -3048,62 +3500,141 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { return false; } let result = union.elements(db).iter().all(|element| { - collect_typed_dicts(db, *element, resolving, completed, typed_dicts) + collect_typed_dicts( + db, + env, + *element, + resolving, + completed, + typed_dicts, + other_types, + ) }); resolving.remove(&ty); result } Type::Intersection(intersection) - if intersection - .iter_positive(db) - .any(|element| element.resolve_type_alias(db).is_typed_dict()) => + if intersection.negative(db).is_empty() + && intersection + .iter_positive(db) + .any(|element| element.resolve_type_alias(db).is_typed_dict()) + && intersection.iter_positive(db).all(|element| { + let element = element.resolve_type_alias(db); + element.is_typed_dict() + || element + == KnownClass::Dict + .to_instance_unknown(db, env) + .top_materialization(db, env) + }) => { // `isinstance(value, dict)` narrows a `TypedDict` to an intersection with - // `Top[dict[Unknown, Unknown]]`. Keep the full intersection so the normal - // constraint-equivalence check below remains authoritative. + // `Top[dict[Unknown, Unknown]]`. Other conjuncts may contribute gradual + // constraints that the shared mapping would erase. typed_dicts.insert(ty); true } + Type::Intersection(intersection) + if intersection.negative(db).is_empty() + && intersection + .iter_positive(db) + .any(|element| is_string_keyed_mapping(db, env, element)) + && intersection.iter_positive(db).all(|element| { + let element = element.resolve_type_alias(db); + is_string_keyed_mapping(db, env, element) + || element + == KnownClass::Dict + .to_instance_unknown(db, env) + .top_materialization(db, env) + }) => + { + // `isinstance(value, dict)` can also narrow a mapping to an intersection with + // `Top[dict[Unknown, Unknown]]`. Retain the full intersection so its original + // key and value constraints are preserved. + other_types.insert(ty); + true + } + Type::NominalInstance(_) if is_string_keyed_mapping(db, env, ty) => { + other_types.insert(ty); + true + } _ => false, }; completed.insert(ty, result); result } + let db = self.db; let mut resolving = FxHashSet::default(); let mut completed = FxHashMap::default(); let mut typed_dicts = FxHashSet::default(); - if !actual.elements(self.db).iter().all(|element| { + let mut other_types = FxOrderSet::default(); + let env = self.env; + + if !actual.elements(db).iter().all(|element| { collect_typed_dicts( - self.db, + db, + env, *element, &mut resolving, &mut completed, &mut typed_dicts, + &mut other_types, ) }) { return None; } + if typed_dicts.is_empty() { + return None; + } + // Other protocols can observe key-specific or gradual evidence that the shared mapping + // fallback erases; restrict mixed unions to the protocol used by dictionary constructors. + if !other_types.is_empty() + && !matches!(formal, Type::ProtocolInstance(protocol) + if protocol.class_origin(db).is_some_and(|class| { + class.is_known(db, KnownClass::SupportsKeysAndGetItem) + })) + { + return None; + } // Use the read-only `Mapping[str, object]` as the fallback rather than `dict[str, object]`. // The current constraint solver can consider mutable protocol constraints equivalent even // when a `TypedDict` preserves more precise correlations between its keys and values. - let spec = &[KnownClass::Str.to_instance(self.db), Type::object()]; - let mapping = KnownClass::Mapping.to_specialized_instance(self.db, spec); - let mapping_when = mapping.when_constraint_set_assignable_to_owned(self.db, formal); - let mapping_when = self.constraints.load(self.db, &mapping_when); - typed_dicts - .into_iter() - .all(|element| { - let element_when = self.constraints.load( - self.db, - &element.when_constraint_set_assignable_to_owned(self.db, formal), - ); - element_when - .iff(self.db, self.constraints, mapping_when) - .is_always_satisfied(self.db) - }) - .then_some(mapping_when) + let spec = &[KnownClass::Str.to_instance(db, env), Type::object()]; + let mapping = KnownClass::Mapping.to_specialized_instance(db, env, spec); + let mapping_when = mapping.when_constraint_set_assignable_to_owned(db, env, formal); + let mapping_when = self.constraints.load(db, env, &mapping_when); + // Logically equivalent constraints can still infer different solutions, such as `Any` + // instead of `object`; preserve the original constraints when gradual evidence differs. + let mapping_solutions = mapping_when.solutions(db, env, self.constraints, self.inferable); + if !typed_dicts.into_iter().all(|element| { + let element_when = self.constraints.load( + db, + env, + &element.when_constraint_set_assignable_to_owned(db, env, formal), + ); + element_when + .iff(db, self.constraints, mapping_when) + .is_always_satisfied(db, env) + && element_when.solutions(db, env, self.constraints, self.inferable) + == mapping_solutions + }) { + return None; + } + + // Reuse one constraint for all equivalent TypedDicts, but retain each mapping arm's + // original constraints. + Some(mapping_when.and(db, self.constraints, || { + other_types + .into_iter() + .when_all(db, self.constraints, |element| { + self.constraints.load( + db, + env, + &element.when_constraint_set_assignable_to_owned(db, env, formal), + ) + }) + })) } /// Infer type mappings by comparing formal callable signatures against actual callables. @@ -3115,19 +3646,26 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { formal_signature: &CallableSignature<'db>, actual_callables: &CallableTypes<'db>, ) -> Result<(), SpecializationError<'db>> { + let db = self.db; let formal_is_single_paramspec = formal_signature.is_single_paramspec().is_some(); for actual_callable in actual_callables.as_slice() { if formal_is_single_paramspec { let when = actual_callable - .signatures(self.db) - .when_constraint_set_assignable_to(self.db, formal_signature, self.constraints); + .signatures(db) + .when_constraint_set_assignable_to( + db, + self.env, + formal_signature, + self.constraints, + ); self.infer_from_constraint_set(when)?; } else { // An overloaded actual callable is compatible with the formal signature if at // least one of its overloads is. We collect type mappings from all satisfiable // overloads, and only report an error if none of them are satisfiable. - let db = self.db; + + let env = self.env.clone(); let constraints = self.constraints; let mut first_error = None; let combined = actual_callable @@ -3137,6 +3675,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { .filter_map(|actual_signature| { let when = actual_signature.when_constraint_set_assignable_to_signatures( db, + &env, formal_signature, constraints, ); @@ -3156,7 +3695,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { } return Ok(()); }; - self.pending.intersect(self.db, self.constraints, combined); + self.pending.intersect(db, self.constraints, combined); } } Ok(()) @@ -3183,6 +3722,8 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { polarity: TypeVarVariance, seen: &mut FxHashSet<(Type<'db>, Type<'db>)>, ) -> Result<(), SpecializationError<'db>> { + let env = self.env; + let db = self.db; // TODO: Eventually, the builder will maintain a constraint set, instead of a hash-map of // type mappings, to represent the specialization that we are building up. At that point, // this method will just need to compare `actual ≤ formal`, using constraint set @@ -3206,14 +3747,14 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // // For example, if `formal` is `list[T]` and `actual` is `list[int] | None`, we want to // specialize `T` to `int`, and so ignore the `None`. - let actual = actual.filter_disjoint_elements(self.db, formal, self.inferable); - let formal = formal.filter_disjoint_elements(self.db, actual, self.inferable); + let actual = actual.filter_disjoint_elements(db, self.env, formal, self.inferable); + let formal = formal.filter_disjoint_elements(db, self.env, actual, self.inferable); match (formal, actual) { // Expand PEP 695 type aliases in the formal type. // This is necessary for solving generics like `def head[T](my_list: MyList[T]) -> T`. (Type::TypeAlias(alias), _) => { - return self.infer_map_impl(alias.value_type(self.db), actual, polarity, seen); + return self.infer_map_impl(alias.value_type(db), actual, polarity, seen); } // basedpython: a use-site modifier constrains which values fit, not @@ -3256,8 +3797,8 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { (Type::TypeForm(formal_typeform), Type::TypeForm(actual_typeform)) => { let variance = TypeVarVariance::Covariant.compose(polarity); return self.infer_map_impl( - formal_typeform.type_argument(self.db), - actual_typeform.type_argument(self.db), + formal_typeform.type_argument(db), + actual_typeform.type_argument(db), variance, seen, ); @@ -3268,9 +3809,9 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { actual @ (Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_)), ) => { let variance = TypeVarVariance::Covariant.compose(polarity); - if let Some(actual_instance) = actual.to_instance_approximation(self.db) { + if let Some(actual_instance) = actual.to_instance_approximation(db, self.env) { return self.infer_map_impl( - formal_typeform.type_argument(self.db), + formal_typeform.type_argument(db), actual_instance, variance, seen, @@ -3279,11 +3820,11 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { } (Type::TypeForm(formal_typeform), Type::KnownInstance(actual_instance)) - if let Some(actual_argument) = actual_instance.type_form_argument(self.db) => + if let Some(actual_argument) = actual_instance.type_form_argument(db, self.env) => { let variance = TypeVarVariance::Covariant.compose(polarity); return self.infer_map_impl( - formal_typeform.type_argument(self.db), + formal_typeform.type_argument(db), actual_argument, variance, seen, @@ -3292,9 +3833,9 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { (Type::TypeForm(formal_typeform), Type::SpecialForm(actual_form)) => { let variance = TypeVarVariance::Covariant.compose(polarity); - if let Some(actual_argument) = actual_form.type_form_argument(self.db) { + if let Some(actual_argument) = actual_form.type_form_argument(db, self.env) { return self.infer_map_impl( - formal_typeform.type_argument(self.db), + formal_typeform.type_argument(db), actual_argument, variance, seen, @@ -3330,22 +3871,18 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // to prevent incorrect specialization: e.g. `T = int | list[int]` for `formal: T | list[T], actual: int | list[int]` // (the correct specialization is `T = int`). let types_have_typevars = formal_union - .elements(self.db) + .elements(db) .iter() - .filter(|ty| ty.has_typevar(self.db)); + .filter(|ty| ty.has_typevar(db, self.env)); let Ok(Type::TypeVar(formal_bound_typevar)) = types_have_typevars.exactly_one() else { return Ok(()); }; - if actual_union - .elements(self.db) - .iter() - .any(|ty| ty.is_type_var()) - { + if actual_union.elements(db).iter().any(|ty| ty.is_type_var()) { return Ok(()); } let remaining_actual = - actual_union.filter(self.db, |ty| !ty.is_subtype_of(self.db, formal)); + actual_union.filter(db, |ty| !ty.is_subtype_of(db, self.env, formal)); if remaining_actual.is_never() { return Ok(()); } @@ -3365,7 +3902,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // `ClassSelector[T]` with `ClassSelector[CT | None]`, descending into `None` // would map `T` to `None` before `CT` is solved from another argument. if let Type::TypeVar(actual_typevar) = actual - && actual_typevar.is_inferable(self.db, self.inferable) + && actual_typevar.is_inferable(db, self.inferable) && matches!(polarity, TypeVarVariance::Invariant) { self.add_type_mapping(actual_typevar, formal, polarity); @@ -3383,10 +3920,10 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // // without specializing `T` to `None`. if !actual.is_never() { - let assignable_elements = union_formal.elements(self.db).iter().filter(|ty| { + let assignable_elements = union_formal.elements(db).iter().filter(|ty| { actual - .when_subtype_of(self.db, **ty, self.constraints, self.inferable) - .is_always_satisfied(self.db) + .when_subtype_of(db, self.env, **ty, self.constraints, self.inferable) + .is_always_satisfied(db, self.env) }); if assignable_elements.exactly_one().is_ok() { return Ok(()); @@ -3394,7 +3931,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { } let mut bound_typevars = union_formal - .elements(self.db) + .elements(db) .iter() .filter_map(|ty| ty.as_typevar()); @@ -3414,7 +3951,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // ``` let mut first_error = None; let mut found_matching_element = false; - for formal_element in union_formal.elements(self.db) { + for formal_element in union_formal.elements(db) { let result = self.infer_map_impl(*formal_element, actual, polarity, seen); if let Err(err) = result { first_error.get_or_insert(err); @@ -3423,12 +3960,13 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // not assignable to the formal element. if !actual .when_assignable_to( - self.db, + db, + self.env, *formal_element, self.constraints, self.inferable, ) - .is_never_satisfied(self.db) + .is_never_satisfied(db, self.env) { found_matching_element = true; } @@ -3441,14 +3979,15 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { } (Type::TypeVar(bound_typevar), ty) | (ty, Type::TypeVar(bound_typevar)) - if bound_typevar.is_inferable(self.db, self.inferable) => + if bound_typevar.is_inferable(db, self.inferable) => { // basedpython: a variadic pack's bound describes its members, or its shape, and // never the pack's own value — so it is checked here rather than applied as an // ordinary upper bound, which would compare a tuple against an element type - if bound_typevar.typevar(self.db).has_pack_bound(self.db) { + if bound_typevar.typevar(self.db).has_pack_bound(self.db, env) { if let Some(violation) = pack_bound_violation( self.db, + env, bound_typevar, ty, self.constraints, @@ -3463,7 +4002,10 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { self.add_type_mapping(bound_typevar, ty, polarity); return Ok(()); } - match bound_typevar.typevar(self.db).bound_or_constraints(self.db) { + match bound_typevar + .typevar(self.db) + .bound_or_constraints(self.db, env) + { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { if polarity.is_contravariant() { // In a contravariant position, the formal type variable is a subtype of @@ -3474,14 +4016,20 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // check here. self.add_type_mapping( bound_typevar, - IntersectionType::from_two_elements(self.db, bound, ty), + IntersectionType::from_two_elements(db, self.env, bound, ty), polarity, ); return Ok(()); } if !ty - .when_assignable_to(self.db, bound, self.constraints, self.inferable) - .is_always_satisfied(self.db) + .when_assignable_to( + db, + self.env, + bound, + self.constraints, + self.inferable, + ) + .is_always_satisfied(db, self.env) { return Err(SpecializationError::MismatchedBound { bound_typevar, @@ -3492,7 +4040,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { } Some(TypeVarBoundOrConstraints::Constraints(typevar_constraints)) => { // Prefer an exact match first. - for constraint in typevar_constraints.elements(self.db) { + for constraint in typevar_constraints.elements(db) { if ty == *constraint { self.add_type_mapping(bound_typevar, ty, polarity); return Ok(()); @@ -3513,14 +4061,17 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // constraint. if let Type::TypeVar(actual_typevar) = ty && let Some(actual_constraints) = - actual_typevar.typevar(self.db).constraints(self.db) + actual_typevar.typevar(db).constraints(db, self.env) { let all_satisfied = actual_constraints.iter().all(|actual_constraint| { - typevar_constraints.elements(self.db).iter().any( + typevar_constraints.elements(db).iter().any( |formal_constraint| { - actual_constraint - .is_equivalent_to(self.db, *formal_constraint) + actual_constraint.is_equivalent_to( + db, + self.env, + *formal_constraint, + ) }, ) }); @@ -3530,24 +4081,26 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { } } - for constraint in typevar_constraints.elements(self.db) { + for constraint in typevar_constraints.elements(db) { let is_satisfied = if polarity.is_contravariant() { constraint .when_assignable_to( - self.db, + db, + self.env, ty, self.constraints, self.inferable, ) - .is_always_satisfied(self.db) + .is_always_satisfied(db, self.env) } else { ty.when_assignable_to( - self.db, + db, + self.env, *constraint, self.constraints, self.inferable, ) - .is_always_satisfied(self.db) + .is_always_satisfied(db, self.env) }; if is_satisfied { @@ -3577,7 +4130,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // 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_intersection.iter_positive(self.db) { + for positive in formal_intersection.iter_positive(db) { self.infer_map_impl(positive, actual, polarity, seen)?; } } @@ -3599,7 +4152,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // They don't all have to. let mut first_error = None; let mut found_matching_element = false; - for positive in actual_intersection.iter_positive(self.db) { + for positive in actual_intersection.iter_positive(db) { let result = self.infer_map_impl(formal, positive, polarity, seen); if let Err(err) = result { // TODO: `infer_map_impl` can have side effects even in the error case, so @@ -3611,8 +4164,14 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // The recursive call to `infer_map_impl` may succeed even if the actual // type is not assignable to the formal element. if !positive - .when_assignable_to(self.db, formal, self.constraints, self.inferable) - .is_never_satisfied(self.db) + .when_assignable_to( + db, + self.env, + formal, + self.constraints, + self.inferable, + ) + .is_never_satisfied(db, self.env) { found_matching_element = true; } @@ -3632,10 +4191,10 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { ) if let SubclassOfInner::Protocol(protocol) = formal_subclass.subclass_of() => { let formal_protocol = Type::ProtocolInstance(protocol); if let Type::Union(union) = actual { - for element in union.elements(self.db) { + for element in union.elements(db) { self.infer_map_impl( formal_protocol, - element.bindings(self.db).return_type(self.db), + element.bindings(db, self.env).return_type(db, self.env), polarity, seen, )?; @@ -3644,7 +4203,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { } return self.infer_map_impl( formal_protocol, - actual.bindings(self.db).return_type(self.db), + actual.bindings(db, self.env).return_type(db, self.env), polarity, seen, ); @@ -3652,7 +4211,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { (Type::SubclassOf(subclass_of), ty) | (ty, Type::SubclassOf(subclass_of)) if let Some(type_var) = subclass_of.into_type_var() - && let Some(actual_instance) = ty.to_instance_approximation(self.db) => + && let Some(actual_instance) = ty.to_instance_approximation(db, self.env) => { return self.infer_map_impl( Type::TypeVar(type_var), @@ -3668,7 +4227,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { ) => { // Retry specialization with the literal's fallback instance so literals can // contribute to generic inference for nominal and protocol formals. - let actual_instance = literal.fallback_instance(self.db); + let actual_instance = literal.fallback_instance(db, self.env); return self.infer_map_impl(formal, actual_instance, polarity, seen); } @@ -3680,19 +4239,48 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // ordinary `range` instance when inferring through generic nominal/protocol types. return self.infer_map_impl( formal, - known_instance.instance_fallback(self.db), + known_instance.instance_fallback(db, self.env), polarity, seen, ); } (formal, Type::ProtocolInstance(actual_protocol)) => { + if let Type::ProtocolInstance(formal_protocol) = formal + && let Some(actual_origin) = actual_protocol.materialized_origin(db) + && let Some(formal_origin) = formal_protocol.class_origin(db) + { + let nominally_inherited = actual_origin + .iter_mro(db) + .filter_map(ClassBase::into_class) + .any(|base| base.class_literal(db) == formal_origin.class_literal(db)); + let when = if nominally_inherited + || formal_protocol.interface(db).has_only_finite_members(db) + { + Some(actual.when_constraint_set_assignable_to_owned(db, self.env, formal)) + } else { + actual_protocol + .when_non_recursive_members_assignable_to_owned(db, formal_protocol) + .map(Cow::Borrowed) + }; + + // Materialized protocols cannot be replaced by their nominal origin: doing + // so would recover the original `Any` requirements. Infer from the complete + // interface when doing so is cycle-safe; otherwise use its nonrecursive + // requirements and leave full recursive compatibility to argument checking. + if let Some(when) = when { + let when = self.constraints.load(db, self.env, &when); + self.infer_from_constraint_set(when)?; + return Ok(()); + } + } + // TODO: This will only handle protocol classes that explicit inherit // from other generic protocol classes 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. - if let Some(actual_nominal) = actual_protocol.to_nominal_instance() { + if let Some(actual_nominal) = actual_protocol.nominal_origin_instance(db) { return self.infer_map_impl( formal, Type::NominalInstance(actual_nominal), @@ -3704,8 +4292,8 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // Special case: `formal` and `actual` are both tuples. (Type::NominalInstance(formal), Type::NominalInstance(actual)) - if let Some(formal_tuple) = formal.tuple_spec(self.db) - && let Some(actual_tuple) = actual.tuple_spec(self.db) => + if let Some(formal_tuple) = formal.tuple_spec(db, self.env) + && let Some(actual_tuple) = actual.tuple_spec(db, self.env) => { if let TupleSpec::Variable(formal_variable) = &*formal_tuple && let VariableSegment::TypeVarTuple(typevartuple) = formal_variable.variable() @@ -3726,7 +4314,8 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { ( &elements[..formal_prefix_len], Type::heterogeneous_tuple( - self.db, + db, + self.env, elements[formal_prefix_len..middle_end].iter().copied(), ), &elements[middle_end..], @@ -3745,7 +4334,8 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { ( &actual_prefix_elements[..formal_prefix_len], Type::tuple(TupleType::mixed_with_segment( - self.db, + db, + self.env, actual_prefix_elements[formal_prefix_len..].iter().copied(), actual.variable(), actual_suffix_elements[..suffix_start].iter().copied(), @@ -3773,15 +4363,17 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { else { return Ok(()); }; - let Ok(formal_tuple) = formal_tuple.resize(self.db, most_precise_length) else { + let Ok(formal_tuple) = formal_tuple.resize(db, self.env, most_precise_length) + else { return Ok(()); }; - let Ok(actual_tuple) = actual_tuple.resize(self.db, most_precise_length) else { + let Ok(actual_tuple) = actual_tuple.resize(db, self.env, most_precise_length) + else { return Ok(()); }; for (formal_element, actual_element) in formal_tuple - .iter_element_types(self.db) - .zip(actual_tuple.iter_element_types(self.db)) + .iter_element_types(db) + .zip(actual_tuple.iter_element_types(db)) { let variance = TypeVarVariance::Covariant.compose(polarity); self.infer_map_impl(formal_element, actual_element, variance, seen)?; @@ -3796,7 +4388,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // Extract formal_alias if this is a generic class let formal_alias = match formal { Type::NominalInstance(formal_nominal) => { - formal_nominal.class(self.db).into_generic_alias() + formal_nominal.class(db, self.env).into_generic_alias() } Type::ProtocolInstance(_) => { @@ -3804,8 +4396,9 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // will handle implicitly implemented protocols and generic protocols. We // eventually want this logic to be used for _all_ nominal instances // (replacing the logic below). - let when = actual.when_constraint_set_assignable_to_owned(self.db, formal); - let when = self.constraints.load(self.db, &when); + let when = + actual.when_constraint_set_assignable_to_owned(db, self.env, formal); + let when = self.constraints.load(db, self.env, &when); self.infer_from_constraint_set(when)?; return Ok(()); } @@ -3814,27 +4407,26 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { }; if let Some(formal_alias) = formal_alias { - let formal_origin = formal_alias.origin(self.db); - for base in actual_nominal.class(self.db).iter_mro(self.db) { + let formal_origin = formal_alias.origin(db); + for base in actual_nominal.class(db, self.env).iter_mro(db) { let ClassBase::Class(ClassType::Generic(base_alias)) = base else { continue; }; - if formal_origin != base_alias.origin(self.db) { + if formal_origin != base_alias.origin(db) { continue; } let generic_context = formal_alias - .specialization(self.db) - .generic_context(self.db) - .variables(self.db); - let formal_specialization = - formal_alias.specialization(self.db).types(self.db); - let base_specialization = base_alias.specialization(self.db).types(self.db); + .specialization(db) + .generic_context(db) + .variables(db); + let formal_specialization = formal_alias.specialization(db).types(db); + let base_specialization = base_alias.specialization(db).types(db); for (typevar, formal_ty, base_ty) in itertools::izip!( generic_context, formal_specialization, base_specialization ) { - let variance = typevar.variance_with_polarity(self.db, polarity); + let variance = typevar.variance_with_polarity(db, polarity); self.infer_map_impl(*formal_ty, *base_ty, variance, seen)?; } return Ok(()); @@ -3849,15 +4441,20 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { let when = self .common_typed_dict_protocol_constraints(formal, actual_union) .unwrap_or_else(|| { - actual.when_constraint_set_assignable_to(self.db, formal, self.constraints) + actual.when_constraint_set_assignable_to( + db, + self.env, + formal, + self.constraints, + ) }); self.infer_from_constraint_set(when)?; return Ok(()); } (formal @ Type::ProtocolInstance(_), actual @ Type::TypedDict(_)) => { - let when = actual.when_constraint_set_assignable_to_owned(self.db, formal); - let when = self.constraints.load(self.db, &when); + let when = actual.when_constraint_set_assignable_to_owned(db, self.env, formal); + let when = self.constraints.load(db, self.env, &when); self.infer_from_constraint_set(when)?; return Ok(()); } @@ -3866,26 +4463,26 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // from matching the actual type's callable signature against the protocol's `__call__` // method signature. (Type::ProtocolInstance(formal_protocol), _) => { - let Some(call_method) = formal_protocol.interface(self.db).call_method(self.db) + let Some(call_method) = formal_protocol.interface(db).call_method(db, self.env) else { return Ok(()); }; - let Some(actual_callables) = actual.try_upcast_to_callable(self.db) else { + let Some(actual_callables) = actual.try_upcast_to_callable(db, self.env) else { return Ok(()); }; // The protocol interface exposes the callable signature already bound for // instance access. - let formal_signature = call_method.signatures(self.db); + let formal_signature = call_method.signatures(db); self.infer_from_callable_signature(formal_signature, &actual_callables)?; } (Type::Callable(formal_callable), _) => { - let Some(actual_callables) = actual.try_upcast_to_callable(self.db) else { + let Some(actual_callables) = actual.try_upcast_to_callable(db, self.env) else { return Ok(()); }; - let formal_signature = formal_callable.signatures(self.db); + let formal_signature = formal_callable.signatures(db); self.infer_from_callable_signature(formal_signature, &actual_callables)?; } @@ -3935,3 +4532,40 @@ impl<'db> SpecializationError<'db> { } } } + +#[cfg(test)] +mod tests { + use super::*; + + use ruff_python_ast::name::Name; + + use crate::db::tests::setup_db; + + #[test] + fn generic_context_inferable_typevars_retain_instances_from_bounds() { + let db = setup_db(); + let db = &db; + let env = db.program_environment(); + let u = BoundTypeVarInstance::synthetic( + db, + &env, + Name::new_static("U"), + TypeVarVariance::Invariant, + ); + let t = BoundTypeVarInstance::synthetic( + db, + &env, + Name::new_static("T"), + TypeVarVariance::Invariant, + ) + .map_bound_or_constraints(db, |_| { + Some(TypeVarBoundOrConstraints::UpperBound(Type::TypeVar(u))) + }); + let context = GenericContext::from_typevar_instances(db, &env, [t]); + + let inferable = context.inferable_typevars(db); + assert_eq!(inferable.iter(db).collect::>(), [t, u]); + assert!(t.is_inferable(db, inferable)); + assert!(u.is_inferable(db, inferable)); + } +} diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 24feaf9f85..b2b505df34 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -1,9 +1,9 @@ -use std::collections::HashMap; +use std::collections::{HashMap, VecDeque}; use crate::FxIndexSet; use std::ops::ControlFlow; -use crate::place::{Place, builtins_module_scope, imported_symbol}; +use crate::place::{Place, implicit_builtins_symbol_scope, imported_symbol}; use crate::reachability::is_range_reachable; use crate::types::EnumLiteralType; use crate::types::call::bind::CheckTypesMode; @@ -20,9 +20,10 @@ use crate::types::overrides::is_constructor_like_method; use crate::types::signatures::{ParameterKind, ParametersKind, Signature}; use crate::types::{ CallDunderError, CallableTypes, ClassBase, ClassLiteral, ClassType, KnownClass, KnownFunction, - KnownUnion, SubclassOfInner, Type, TypeContext, TypeVarVariance, binding_type, + KnownUnion, PropertyAccessorRole, SubclassOfInner, Type, TypeContext, + TypeVarBoundOrConstraints, TypeVarVariance, binding_type, }; -use crate::{Db, DisplaySettings, HasDefinition, HasType, SemanticModel}; +use crate::{Db, HasDefinition, HasType, ProgramEnvironment, SemanticModel}; use itertools::Either; use ruff_db::files::{File, FileRange}; use ruff_db::parsed::parsed_module; @@ -31,9 +32,11 @@ use ruff_python_ast::{self as ast, AnyNodeRef, name::Name}; use ruff_python_stdlib::identifiers::is_mangled_private; use ruff_text_size::{Ranged, TextRange}; use rustc_hash::FxHashSet; -use ty_module_resolver::{Module, ModuleName, resolve_module_confident}; -use ty_python_core::definition::{Definition, DefinitionKind}; -use ty_python_core::{attribute_scopes, global_scope, semantic_index, use_def_map}; +use ty_module_resolver::{ + ImportingFile, Module, ModuleName, ResolverFile, resolve_module_confident, +}; +use ty_python_core::definition::{Definition, DefinitionKind, NestedBindingExecution}; +use ty_python_core::{ProgramFile, attribute_scopes, global_scope, semantic_index, use_def_map}; mod unreachable_code; #[path = "ide_support/unused_bindings.rs"] @@ -73,7 +76,8 @@ pub fn definitions_for_name<'db>( alias_resolution: ImportAliasResolution, ) -> Vec> { let db = model.db(); - let file = model.file(); + let env = model.program_environment(); + let file = model.program_file(); let index = semantic_index(db, file); // Get the scope for this name expression @@ -92,11 +96,31 @@ pub fn definitions_for_name<'db>( continue; // Name not found in this scope, try parent scope }; + let use_def_map = index.use_def_map(scope_id); + // Check if this place is marked as global or nonlocal let place_expr = place_table.symbol(symbol_id); let is_global = place_expr.is_global(); let is_nonlocal = place_expr.is_nonlocal(); + if is_global || is_nonlocal { + // Assignments in a forwarding scope remain valid navigation targets, including eager + // walrus bindings exported from comprehensions. + all_definitions.extend(user_visible_definitions( + db, + use_def_map + .reachable_symbol_bindings(symbol_id) + .filter_map(|binding| binding.binding.definition()) + .filter(|definition| match definition.kind(db) { + DefinitionKind::NamedExpression(_) => true, + DefinitionKind::NestedBindings(nested) => { + nested.execution == NestedBindingExecution::Eager + } + _ => false, + }), + )); + } + // TODO: The current algorithm doesn't return definitions or bindings // for other scopes that are outside of this scope hierarchy that target // this name using a nonlocal or global binding. The semantic analyzer @@ -110,7 +134,7 @@ pub fn definitions_for_name<'db>( if let Some(global_symbol_id) = global_place_table.symbol_id(name_str) { let global_use_def_map = ty_python_core::use_def_map(db, global_scope_id); - all_definitions.extend(reachable_definitions( + all_definitions.extend(user_visible_definitions( db, global_use_def_map .reachable_symbol_bindings(global_symbol_id) @@ -131,10 +155,8 @@ pub fn definitions_for_name<'db>( continue; } - let use_def_map = index.use_def_map(scope_id); - // Get all definitions (both bindings and declarations) for this place - all_definitions.extend(reachable_definitions( + all_definitions.extend(user_visible_definitions( db, use_def_map .reachable_symbol_bindings(symbol_id) @@ -156,13 +178,13 @@ pub fn definitions_for_name<'db>( let mut resolved_definitions = Vec::new(); for definition in &all_definitions { - let resolved = resolve_definition(db, *definition, Some(name_str), alias_resolution); + let resolved = resolve_definition(db, &env, *definition, Some(name_str), alias_resolution); resolved_definitions.extend(resolved); } // If we didn't find any definitions in scopes, fallback to builtins if resolved_definitions.is_empty() - && let Some(builtins_scope) = builtins_module_scope(db) + && let Some(builtins_scope) = implicit_builtins_symbol_scope(db, &env, name_str) { // Special cases for `float` and `complex` in type annotation positions. // We don't know whether we're in a type annotation position, so we'll just ask `Name`'s type, @@ -187,7 +209,7 @@ pub fn definitions_for_name<'db>( .rev() .filter_map(|ty| ty.as_nominal_instance()) .filter_map(|instance| { - let definition = instance.class_literal(db).definition(db)?; + let definition = instance.class_literal(db, &env).definition(db)?; Some(ResolvedDefinition::Definition(definition)) }) .collect(); @@ -199,6 +221,7 @@ pub fn definitions_for_name<'db>( .flat_map(|def| { resolve_definition( db, + &env, def, Some(name_str), ImportAliasResolution::ResolveAliases, @@ -227,25 +250,27 @@ pub fn definitions_for_attribute<'db>( let db = model.db(); let name_str = attribute.attr.as_str(); + let mut resolved = Vec::new(); + + // Determine the type of the LHS + let Some(lhs_ty) = attribute.value.inferred_type(model) else { + return resolved; + }; + + let env = model.program_environment(); + // A structural protocol meta-type still uses its nominal protocol declaration as the source // location for go-to-definition, even though the origin is not a nominal upper bound. let subclass_origin = |subclass_of: SubclassOfInner<'db>| { let class = match subclass_of { - SubclassOfInner::Protocol(protocol) => protocol.class_origin().map(|origin| *origin), - subclass_of => subclass_of.into_class(db), + SubclassOfInner::Protocol(protocol) => protocol.class_origin(db).map(|origin| *origin), + subclass_of => subclass_of.into_class(db, &env), }?; class .static_class_literal(db) .map(|(literal, _)| ClassLiteral::Static(literal)) }; - let mut resolved = Vec::new(); - - // Determine the type of the LHS - let Some(lhs_ty) = attribute.value.inferred_type(model) else { - return resolved; - }; - let tys = match lhs_ty { Type::Union(union) => union.elements(model.db()), _ => std::slice::from_ref(&lhs_ty), @@ -263,11 +288,16 @@ pub fn definitions_for_attribute<'db>( for ty in expanded_tys { // Handle modules if let Type::ModuleLiteral(module_literal) = ty { - if let Some(module_file) = module_literal.module(db).file(db) { + if let Some(module_file) = module_literal + .module(db) + .file(db) + .map(|file| ProgramFile::new(db, file, model.program_environment().program(db))) + { let module_scope = global_scope(db, module_file); for def in find_symbol_in_scope(db, module_scope, name_str) { resolved.extend(resolve_definition( db, + &env, def, Some(name_str), ImportAliasResolution::ResolveAliases, @@ -282,7 +312,7 @@ pub fn definitions_for_attribute<'db>( continue; } - let meta_type = ty.to_meta_type(db); + let meta_type = ty.to_meta_type(db, &env); // Look up the attribute first on the meta-type, unless it's already a class-like type. let lookup_type = match ty { @@ -352,6 +382,7 @@ pub fn definitions_for_django_lookup_root<'db>( call: &ast::ExprCall, name: TextRange, ) -> Vec> { + let env = &model.program_environment(); let db = model.db(); let file = model.file(); if !file.source_type(db).is_basedpython() { @@ -360,7 +391,7 @@ pub fn definitions_for_django_lookup_root<'db>( let Some(callee) = call.func.inferred_type(model) else { return Vec::new(); }; - let Some(queried) = django::lookup_call_model(db, callee) else { + let Some(queried) = django::lookup_call_model(db, env, callee) else { return Vec::new(); }; let Some(scope) = model.scope(AnyNodeRef::from(call)) else { @@ -368,8 +399,9 @@ pub fn definitions_for_django_lookup_root<'db>( }; let lookups = django::lookup_expressions( db, + env, file, - scope.to_scope_id(db, file), + scope.to_scope_id(db, db.program_file(file)), queried, &call.arguments, ); @@ -387,15 +419,330 @@ pub fn definitions_for_django_lookup_root<'db>( ) } +/// A prepared implementation search. +/// +/// Preparing the finder resolves the class roots and the implementations selected for those roots. +/// Candidate subclasses can then be scanned one file at a time. +pub struct ImplementationsFinder<'db> { + /// Definitions selected directly for the goto target's roots: + /// - Root class definitions for a class-family search + /// - Definitions found through each root's MRO for a member-family search. + initial_definitions: Vec>, + + /// Classes whose known subclasses should be scanned for additional implementations. + roots: FxHashSet>, + + /// Whether scanning should return subclass definitions or same-named members on subclasses. + kind: ImplementationsFinderKind, +} + +enum ImplementationsFinderKind { + ClassFamily, + MemberFamily { + name: Name, + accessor_role: Option, + }, +} + +impl<'db> ImplementationsFinder<'db> { + /// Creates a class-family finder from resolved class roots. + fn for_class_roots(db: &'db dyn Db, roots: Vec>) -> Self { + let mut initial_definitions = Vec::new(); + for root in &roots { + if let Some(definition) = root.definition(db) { + let resolved = ResolvedDefinition::Definition(definition); + if !initial_definitions.contains(&resolved) { + initial_definitions.push(resolved); + } + } + } + + Self { + initial_definitions, + roots: roots.into_iter().collect(), + kind: ImplementationsFinderKind::ClassFamily, + } + } + + /// Creates a member-family finder for roots that resolve the member through their MRO. + fn for_member_roots( + db: &'db dyn Db, + roots: Vec>, + member_name: Name, + accessor_role: Option, + ) -> Option { + let mut initial_definitions = Vec::new(); + let mut family_roots = FxHashSet::default(); + + for root in roots { + // Avoid scanning every known subclass when the member doesn't resolve on this root. + let Some(root_definitions) = + mro_member_definitions(db, root, member_name.as_str(), accessor_role) + else { + continue; + }; + + for definition in root_definitions { + if !initial_definitions.contains(&definition) { + initial_definitions.push(definition); + } + } + + family_roots.insert(root); + } + + if family_roots.is_empty() { + return None; + } + + Some(Self { + initial_definitions, + roots: family_roots, + kind: ImplementationsFinderKind::MemberFamily { + name: member_name, + accessor_role, + }, + }) + } + + /// Returns implementations contributed by classes defined in `file`. + pub fn implementations_for_file<'scan>( + &'scan self, + db: &'scan dyn Db, + file: ProgramFile<'scan>, + ) -> Vec> + where + 'db: 'scan, + { + let roots: &FxHashSet> = &self.roots; + match &self.kind { + ImplementationsFinderKind::ClassFamily => { + class_implementations_for_file(db, file, roots) + } + ImplementationsFinderKind::MemberFamily { + name, + accessor_role, + } => member_implementations_for_file(db, file, roots, name.as_str(), *accessor_role), + } + } + + /// Returns the definitions selected directly for the finder's roots. + pub fn into_initial_definitions(self) -> Vec> { + self.initial_definitions + } + + /// Creates an `ImplementationsFinder` for an attribute expression `x.y`. + /// + /// ```py + /// def f(animal: Animal): + /// animal.sound + /// ^^^^^ + /// ``` + /// + /// For a receiver of type `Animal`, this includes the member definition selected through + /// `Animal`'s MRO plus same-named definitions on known subclasses such as `Dog` or `Cat`. For a + /// receiver of type `Dog`, the root is `Dog`: inherited behavior resolves through `Dog`'s MRO, + /// and sibling classes such as `Cat` are not included. + /// + /// Both `def`-style methods and attribute definitions are returned, whether the attribute is + /// declared in the class body (`sound: str = ...`, `sound = ...`, or a bare `sound: str`) or + /// assigned to `self` in a method body (`self.sound = ...`). + pub fn for_attribute( + model: &SemanticModel<'db>, + attribute: &ast::ExprAttribute, + ) -> Option { + let db = model.db(); + let lhs_ty = attribute.value.inferred_type(model)?; + let env = model.program_environment(); + let mut roots = Vec::new(); + let mut seen = FxHashSet::default(); + collect_implementation_root_classes(db, &env, lhs_ty, &mut seen, &mut roots); + + let accessor_role = match attribute.ctx { + ast::ExprContext::Load => Some(PropertyAccessorRole::Getter), + ast::ExprContext::Store => Some(PropertyAccessorRole::Setter), + ast::ExprContext::Del => Some(PropertyAccessorRole::Deleter), + ast::ExprContext::Invalid => None, + }; + + ImplementationsFinder::for_member_roots(db, roots, attribute.attr.id.clone(), accessor_role) + } + + /// Creates an `ImplementationsFinder` for a method declaration. + /// + /// ```py + /// class Animal: + /// def speak(self): ... + /// ^^^^^ + /// + /// class Dog(Animal): + /// def speak(self): ... + /// ``` + /// + /// The containing class is used as the root. The method's implementation, if present, is returned + /// along with same-named methods defined on known transitive subclasses. This does not walk to + /// parent classes: on `Dog.speak`, the root is `Dog`, so `Animal.speak` is not included. + pub fn for_method(model: &SemanticModel<'db>, function: &ast::StmtFunctionDef) -> Option { + let db = model.db(); + let env = model.program_environment(); + let function_definition = function.definition(model); + if !is_reachable_implementation_definition(db, function_definition) { + return None; + } + + let containing_scope = function_definition.scope(db); + let accessor_role = function + .inferred_type(model) + .and_then(Type::as_property_instance) + .and_then(|property| property.accessor_role(db, function_definition)); + let class_node = containing_scope.node(db).as_class()?; + let class_definition = semantic_index(db, containing_scope.program_file(db)) + .expect_single_definition(class_node); + let class_ty = binding_type(db, class_definition); + let root = extract_class_literal(db, &env, class_ty)?; + + ImplementationsFinder::for_member_roots( + db, + vec![root], + function.name.id.clone(), + accessor_role, + ) + } + + /// Creates an `ImplementationsFinder` for a class declaration. + /// + /// ```py + /// class Animal: + /// ^^^^^^ + /// pass + /// + /// class Dog(Animal): ... + /// class Cat(Animal): ... + /// ``` + /// + /// The clicked class is the root and is returned first, followed by its known transitive + /// subclasses such as `Dog` and `Cat`. This walks down the hierarchy only: clicking a subclass + /// returns that class and its own subclasses, not its parents. + pub fn for_class(model: &SemanticModel<'db>, class: &ast::StmtClassDef) -> Option { + let db = model.db(); + let env = model.program_environment(); + let class_definition = class.definition(model); + if !is_reachable_implementation_definition(db, class_definition) { + return None; + } + let root = extract_class_literal(db, &env, binding_type(db, class_definition))?; + + Some(ImplementationsFinder::for_class_roots(db, vec![root])) + } + + /// Creates an `ImplementationsFinder` for classes referred to by `resolved`, covering class + /// references such as a base class, an annotation, or a constructor call. + /// + /// ```py + /// class Animal: ... + /// + /// class Dog(Animal): ... + /// ^^^^^^ + /// ``` + /// + /// The referenced class is the root and is returned first, followed by its known transitive + /// subclasses, just like clicking the class declaration. + /// + /// The resolved definitions' binding types are used rather than the reference's inferred value + /// type, because a class used as an annotation (`x: Animal`) infers as an instance of that class, + /// which is indistinguishable from an actual instance variable. The resolved definition's type is + /// a class object precisely when the reference refers to a class. + /// + /// Returns `None` when no binding refers to a class object or any binding refers to a non-class + /// object (for example an instance variable or method), so callers can fall back to member + /// handling. + pub fn for_class_reference( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + resolved_definitions: &[ResolvedDefinition<'db>], + ) -> Option { + let mut roots = Vec::new(); + let mut seen = FxHashSet::default(); + + for def in resolved_definitions { + let ResolvedDefinition::Definition(definition) = def else { + return None; + }; + + if !is_reachable_implementation_definition(db, *definition) { + continue; + } + + // Declaration-only definitions such as a bare `sound: str` annotation have no binding + // type and cannot refer to a class object. + if !def.category(db).is_binding() { + continue; + } + + // Only references that resolve to a class object (a base class, annotation, `Animal()`, or + // a name bound to a class) are class implementation requests; instances resolve to their + // own definitions, whose type is the instance rather than the class object. + let ty = binding_type(db, *definition); + + let root = match ty { + Type::ClassLiteral(_) | Type::SubclassOf(_) | Type::GenericAlias(_) => { + extract_class_literal(db, env, ty) + } + _ => None, + }; + + let root = root?; + + if seen.insert(root) { + roots.push(root); + } + } + + if roots.is_empty() { + return None; + } + + Some(ImplementationsFinder::for_class_roots(db, roots)) + } +} + +/// Finds subclasses of `roots` defined in `file`. +fn class_implementations_for_file<'db>( + db: &'db dyn Db, + file: ProgramFile<'db>, + roots: &FxHashSet>, +) -> Vec> { + if !contains_identifier(&source_text(db, file.file(db)), "class") { + return Vec::new(); + } + + let mut definitions = Vec::new(); + + for candidate in reachable_class_literals_in_file(db, file) { + if roots.contains(&candidate) || !class_mro_intersects(db, candidate, roots) { + continue; + } + if let Some(definition) = candidate.definition(db) { + let resolved = ResolvedDefinition::Definition(definition); + if !definitions.contains(&resolved) { + definitions.push(resolved); + } + } + } + + definitions +} + /// Returns the descriptor object type for an attribute expression `x.y`, without invoking the /// descriptor protocol. This corresponds to `inspect.getattr_static(x, "y")` at the type level. pub fn static_member_type_for_attribute<'db>( model: &SemanticModel<'db>, attribute: &ast::ExprAttribute, ) -> Option> { + let db = model.db(); let lhs_ty = attribute.value.inferred_type(model)?; lhs_ty - .static_member(model.db(), attribute.attr.as_str()) + .static_member(db, &model.program_environment(), attribute.attr.as_str()) .ignore_possibly_undefined() } @@ -405,6 +752,7 @@ fn definitions_for_attribute_in_class_hierarchy<'db>( attribute_name: &str, ) -> Vec> { let db = model.db(); + let env = model.program_environment(); let mut resolved = Vec::new(); 'scopes: for ancestor in class_literal .iter_mro(db) @@ -419,6 +767,7 @@ fn definitions_for_attribute_in_class_hierarchy<'db>( let use_def = use_def_map(db, class_scope); let resolved_in_scope = resolve_reachable_definitions( db, + &env, attribute_name, use_def .reachable_symbol_declarations(place_id) @@ -436,8 +785,7 @@ fn definitions_for_attribute_in_class_hierarchy<'db>( } // Look for instance attributes in method scopes (e.g., self.x = 1) - let file = class_scope.file(db); - let index = semantic_index(db, file); + let index = semantic_index(db, class_scope.program_file(db)); for function_scope_id in attribute_scopes(db, class_scope) { if let Some(place_id) = index @@ -447,6 +795,7 @@ fn definitions_for_attribute_in_class_hierarchy<'db>( let use_def = index.use_def_map(function_scope_id); let resolved_in_scope = resolve_reachable_definitions( db, + &env, attribute_name, use_def .reachable_member_declarations(place_id) @@ -468,26 +817,426 @@ fn definitions_for_attribute_in_class_hierarchy<'db>( resolved } -fn reachable_definitions<'db>( +/// Finds member implementations contributed by subclasses of `roots` defined in `file`. +fn member_implementations_for_file<'db>( + db: &'db dyn Db, + file: ProgramFile<'db>, + roots: &FxHashSet>, + member_name: &str, + accessor_role: Option, +) -> Vec> { + let mut definitions = Vec::new(); + + // A file can only contribute an override if it contains a class and spells the member name, + // whether as a method name, a class-body target, or a `self.member` assignment. + let source = source_text(db, file.file(db)); + if !contains_identifier(&source, "class") || !contains_identifier(&source, member_name) { + return definitions; + } + + for candidate in reachable_class_literals_in_file(db, file) { + // The implementations selected for the roots were collected during finder preparation. + if roots.contains(&candidate) { + continue; + } + + if !class_mro_intersects(db, candidate, roots) { + continue; + } + + for definition in + own_member_definitions(db, candidate, member_name, accessor_role).unwrap_or_default() + { + if !definitions.contains(&definition) { + definitions.push(definition); + } + } + } + + definitions +} + +/// Returns whether any class in `class`'s MRO is one of `roots`. +fn class_mro_intersects<'db>( + db: &'db dyn Db, + class: ClassLiteral<'db>, + roots: &FxHashSet>, +) -> bool { + class + .iter_mro(db) + .filter_map(ClassBase::into_class) + .any(|ancestor| roots.contains(&ancestor.class_literal(db))) +} + +/// Finds the member definitions selected by normal Python MRO lookup for `class`. +/// +/// This intentionally stops at the first class in the MRO that defines `member_name`; inherited +/// members should navigate to the definition that actually provides the behavior for the receiver. +/// The returned vector can be empty when the selected member has no implementation definition, +/// such as an overload-only method. +fn mro_member_definitions<'db>( + db: &'db dyn Db, + class: ClassLiteral<'db>, + member_name: &str, + accessor_role: Option, +) -> Option>> { + class + .iter_mro(db) + .filter_map(ClassBase::into_class) + .find_map(|class| { + own_member_definitions(db, class.class_literal(db), member_name, accessor_role) + }) +} + +/// Returns member definitions for `member_name` that are declared directly in `class`. +/// +/// ```py +/// class Animal: +/// def speak(self): ... +/// +/// class Dog(Animal): +/// pass +/// +/// class Cat(Animal): +/// def speak(self): ... +/// ``` +/// +/// For member `speak`, this returns nothing for `Dog` because it only inherits the member, but it +/// returns `Cat.speak` for `Cat` because it is defined directly in `Cat`. +/// +/// A class-body definition (method or attribute) takes priority and determines this class's +/// contribution when present, mirroring the goto-definition lookup in +/// [`definitions_for_attribute_in_class_hierarchy`]. Otherwise, instance attributes assigned in the +/// class's own method bodies (`self.member = ...`) are used. +/// +/// Subclasses that only inherit the member do not add a new implementation target. The inherited +/// definition is already represented by the ancestor that defines it; this only finds subclasses +/// that define a new method body or attribute. +/// +/// Returns `None` if `class` has no reachable user-visible definitions for `member_name`. Returns +/// `Some` with an empty vector if the class defines the symbol but none of its reachable definitions +/// produce a navigable implementation matching `accessor_role`. +fn own_member_definitions<'db>( + db: &'db dyn Db, + class: ClassLiteral<'db>, + member_name: &str, + accessor_role: Option, +) -> Option>> { + let class = class.as_static()?; + let class_scope = class.body_scope(db); + + let class_place_table = ty_python_core::place_table(db, class_scope); + if let Some(place_id) = class_place_table.symbol_id(member_name) { + let use_def = use_def_map(db, class_scope); + let definitions = reachable_implementation_definitions( + db, + use_def + .reachable_symbol_declarations(place_id) + .filter_map(|declaration| declaration.declaration.definition()) + .chain( + use_def + .reachable_symbol_bindings(place_id) + .filter_map(|binding| binding.binding.definition()), + ), + ); + if !definitions.is_empty() { + return Some( + definitions + .into_iter() + .filter(|definition| { + property_accessor_role_matches(db, *definition, accessor_role) + }) + .filter_map(|definition| member_implementation_definition(db, definition)) + .collect(), + ); + } + } + + let file = class_scope.program_file(db); + let index = semantic_index(db, file); + let mut instance_definitions = Vec::new(); + for function_scope_id in attribute_scopes(db, class_scope) { + let Some(place_id) = index + .place_table(function_scope_id) + .member_id_by_instance_attribute_name(member_name) + else { + continue; + }; + let use_def = index.use_def_map(function_scope_id); + instance_definitions.extend( + use_def + .reachable_member_declarations(place_id) + .filter_map(|declaration| declaration.declaration.definition()) + .chain( + use_def + .reachable_member_bindings(place_id) + .filter_map(|binding| binding.binding.definition()), + ), + ); + } + + let instance_definitions = reachable_implementation_definitions(db, instance_definitions); + if instance_definitions.is_empty() { + return None; + } + Some( + instance_definitions + .into_iter() + .filter_map(|definition| member_implementation_definition(db, definition)) + .collect(), + ) +} + +/// Returns whether `definition` is either not a property accessor or has the requested role. +fn property_accessor_role_matches<'db>( + db: &'db dyn Db, + definition: Definition<'db>, + requested_role: Option, +) -> bool { + if !matches!(definition.kind(db), DefinitionKind::Function(_)) { + return true; + } + + requested_role.is_none_or(|requested_role| { + binding_type(db, definition) + .as_property_instance() + .and_then(|property| property.accessor_role(db, definition)) + .is_none_or(|definition_role| definition_role == requested_role) + }) +} + +/// Normalize a member definition to the implementation target that should be navigated to. +fn member_implementation_definition<'db>( + db: &'db dyn Db, + definition: Definition<'db>, +) -> Option> { + match definition.kind(db) { + // `def` statements collapse overload declarations to their concrete implementation below. + DefinitionKind::Function(_) => {} + // Attribute definitions (`sound: str = ...`, `sound = ...`, a bare `sound: str` + // declaration, or `self.sound = ...`) are implementation targets as-is. + DefinitionKind::Assignment(_) | DefinitionKind::AnnotatedAssignment(_) => { + return Some(ResolvedDefinition::Definition(definition)); + } + // Other kinds (imports, comprehension targets, ...) can stop MRO lookup, but should not + // themselves become implementation targets. + _ => return None, + } + + // Use the inferred function type to collapse overload declarations to their concrete + // implementation. If inference cannot produce a function literal, keep the original `def` as a + // conservative fallback. + let Some(function) = binding_type(db, definition).as_function_literal() else { + return Some(ResolvedDefinition::Definition(definition)); + }; + + let (_, implementation) = function.overloads_and_implementation(db); + if implementation.is_some() { + return Some(ResolvedDefinition::Definition(function.last_definition(db))); + } + + // Stub overload declarations can still map to a real source implementation later. + if definition.file(db).is_stub(db) { + return Some(ResolvedDefinition::Definition(definition)); + } + + // Non-stub overload-only groups have no runtime implementation to navigate to. + None +} + +/// Normalizes a receiver type into the class roots used for implementation lookup. +fn collect_implementation_root_classes<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + seen: &mut FxHashSet>, + roots: &mut Vec>, +) { + match ty.resolve_type_alias(db) { + Type::Union(union) => { + // `pet: Dog | Cat` can dispatch through either `Dog` or `Cat`. + for element in union.elements(db) { + collect_implementation_root_classes(db, env, *element, seen, roots); + } + } + Type::Intersection(intersection) => { + // Finite intersections can stand for alternatives like `Dog` or `Cat`. + if let Some(alternatives) = intersection.finite_alternatives(db, env) { + for alternative in alternatives { + collect_implementation_root_classes(db, env, alternative, seen, roots); + } + } + } + Type::TypeVar(typevar) => match typevar.typevar(db).bound_or_constraints(db, env) { + // `T: Animal` can dispatch through the `Animal` bound. + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + collect_implementation_root_classes(db, env, bound, seen, roots); + } + // `T: (Dog, Cat)` can dispatch through either constraint. + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + collect_implementation_root_classes( + db, + env, + constraints.as_type(db, env), + seen, + roots, + ); + } + None => {} + }, + Type::SubclassOf(subclass_of) if subclass_of.is_type_var() => { + // Both `type[T]` and the implicit `cls` parameter of a classmethod are represented as + // `SubclassOf(TypeVar)`. Normalize them through the existing TypeVar handling above. + collect_implementation_root_classes( + db, + env, + subclass_of.to_instance(db, env), + seen, + roots, + ); + } + ty => { + // `dog: Dog` maps directly to the `Dog` class root. + let root = match ty { + Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_) => { + extract_class_literal(db, env, ty) + } + Type::NominalInstance(_) + | Type::ProtocolInstance(_) + | Type::KnownInstance(_) + | Type::LiteralValue(_) + | Type::TypedDict(_) + | Type::NewTypeInstance(_) => extract_class_literal(db, env, ty) + .or_else(|| extract_class_literal(db, env, ty.to_meta_type(db, env))), + _ => None, + }; + + if let Some(root) = root + && seen.insert(root) + { + roots.push(root); + } + } + } +} + +/// Returns the user-visible definitions represented by a use-def binding. +/// +/// Comprehension walruses are represented in the containing scope by synthetic eager bindings: +/// +/// ```python +/// [(last := item) for item in items] +/// print(last) # Go to definition should select `last := item` above. +/// ``` +/// +/// The binding for the use in `print` is synthetic, so follow it into the comprehension's +/// end-of-scope bindings. Nested comprehensions can produce a chain of these proxies. Only +/// follow sources that resolve to the same variable, so `global` and `nonlocal` writes do not +/// become definitions of each other. +fn user_visible_definitions<'db>( + db: &'db dyn Db, + definitions: impl IntoIterator>, +) -> FxIndexSet> { + let mut pending = definitions.into_iter().collect::>(); + let mut seen = FxHashSet::default(); + let mut result = FxIndexSet::default(); + + while let Some(definition) = pending.pop_front() { + if !seen.insert(definition) { + continue; + } + + match definition.kind(db) { + DefinitionKind::NestedBindings(nested) => { + let index = semantic_index(db, definition.program_file(db)); + let sources = nested + .visible_binding_sources(index, definition.file_scope(db)) + .flatten() + .filter_map(|binding| binding.binding.definition()); + // A lazy function proxy can lead to an eager comprehension proxy. Follow that + // proxy-only chain without exposing ordinary lazy nested assignments. + pending.extend(sources.filter(|source| { + nested.execution == NestedBindingExecution::Eager + || matches!(source.kind(db), DefinitionKind::NestedBindings(_)) + })); + } + kind if kind.is_user_visible() => { + result.insert(definition); + } + _ => {} + } + } + + result +} + +fn reachable_implementation_definitions<'db>( db: &'db dyn Db, definitions: impl IntoIterator>, ) -> FxIndexSet> { definitions .into_iter() .filter(|definition| definition.kind(db).is_user_visible()) + .filter(|definition| is_reachable_implementation_definition(db, *definition)) .collect() } +fn is_reachable_implementation_definition<'db>( + db: &'db dyn Db, + definition: Definition<'db>, +) -> bool { + let file = definition.program_file(db); + let parsed = parsed_module(db, file.python_file(db)).load(db); + is_range_reachable( + db, + semantic_index(db, file), + definition.file_scope(db), + definition.full_range(db, &parsed).range(), + ) +} + +/// Cheap text prefilter for identifier references before AST/semantic validation. +/// +/// Heuristically matches an ASCII approximation of `\b{name}\b`. +pub fn contains_identifier(source: &str, name: &str) -> bool { + if name.is_empty() { + return false; + } + + let bytes = source.as_bytes(); + let needle = name.as_bytes(); + + memchr::memmem::find_iter(bytes, needle).any(move |pos| { + let after = pos + needle.len(); + + // Skip this entry if it is within an identifier. E.g. skip + // this entry when searching for `x` and this is a match + // within `exclude = 10`. + let boundary_before = pos == 0 || !is_ascii_identifier_continue(bytes[pos - 1]); + let boundary_after = bytes + .get(after) + .is_none_or(|byte| !is_ascii_identifier_continue(*byte)); + + boundary_before && boundary_after + }) +} + +fn is_ascii_identifier_continue(byte: u8) -> bool { + byte.is_ascii_alphanumeric() || byte == b'_' +} + fn resolve_reachable_definitions<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, symbol_name: &str, definitions: impl IntoIterator>, ) -> Vec> { - reachable_definitions(db, definitions) + user_visible_definitions(db, definitions) .into_iter() .flat_map(|definition| { resolve_definition( db, + env, definition, Some(symbol_name), ImportAliasResolution::ResolveAliases, @@ -519,13 +1268,16 @@ pub fn typed_dict_key_hover<'db>( model: &SemanticModel<'db>, subscript: &ast::ExprSubscript, ) -> Option> { + let db = model.db(); let key = subscript .slice .as_string_literal_expr() .map(|literal| literal.value.to_str())?; let value_ty = subscript.value.inferred_type(model)?; let typed_dict = value_ty.as_typed_dict()?; - let owner = value_ty.display(model.db()).to_string(); + let owner = value_ty + .display(db, &model.program_environment()) + .to_string(); let field = typed_dict.items(model.db()).get(key)?; let docstring = field .first_declaration() @@ -557,9 +1309,10 @@ pub fn definitions_for_keyword_argument<'db>( let keyword_name_str = keyword_name.as_str(); let mut resolved_definitions = Vec::new(); + let env = &model.program_environment(); if let Some(callable_type) = func_type - .try_upcast_to_callable(db) + .try_upcast_to_callable(db, env) .and_then(CallableTypes::exactly_one) { let signatures = callable_type.signatures(db); @@ -591,9 +1344,11 @@ pub fn definitions_for_imported_symbol<'db>( alias_resolution: ImportAliasResolution, ) -> Vec> { let mut visited = FxHashSet::default(); + let env = model.program_environment(); resolve_definition::resolve_from_import_definitions( model.db(), - model.file(), + &env, + ImportingFile::File(model.file(), env.resolver_environment(model.db())), import_node, symbol_name, &mut visited, @@ -609,13 +1364,14 @@ pub fn definitions_and_overloads_for_function<'db>( model: &SemanticModel<'db>, function: &ast::StmtFunctionDef, ) -> Vec> { + let db = model.db(); if let Some(function_type) = function .inferred_type(model) .and_then(Type::as_function_literal) { function_type - .iter_overloads_and_implementation(model.db()) - .filter_map(|overload| overload.signature(model.db()).definition()) + .iter_overloads_and_implementation(db) + .filter_map(|overload| overload.signature(db).definition()) .map(ResolvedDefinition::Definition) .collect() } else { @@ -676,11 +1432,15 @@ pub struct CallSignatureParameter<'db> { } impl<'db> CallSignatureDetails<'db> { - fn from_binding(db: &'db dyn Db, binding: &crate::types::call::Binding<'db>) -> Self { + fn from_binding( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + binding: &crate::types::call::Binding<'db>, + ) -> Self { let argument_to_parameter_mapping = binding.argument_matches().to_vec(); - let specialization = binding.specialization(db); + let specialization = binding.specialization(db, env); let signature = binding.signature.clone(); - let display_details = signature.display(db).to_string_parts(); + let display_details = signature.display(db, env).to_string_parts(); let (parameters, parameter_to_displayed_parameter_mapping) = displayed_parameters_for_signature(db, &signature, &display_details, specialization); let argument_to_displayed_parameter_mapping = argument_to_parameter_mapping @@ -831,16 +1591,16 @@ pub fn call_signature_details<'db>( model: &SemanticModel<'db>, call_expr: &ast::ExprCall, ) -> Vec> { + let db = model.db(); let Some(func_type) = call_expr.func.inferred_type(model) else { return Vec::new(); }; - let db = model.db(); - // Use into_callable to handle all the complex type conversions + let env = &model.program_environment(); if let Some(callable_type) = func_type - .try_upcast_to_callable(db) - .map(|callables| callables.into_type(db)) + .try_upcast_to_callable(db, env) + .map(|callables| callables.into_type(db, env)) { // Use from_arguments_typed so that check_types can infer TypeVar // specializations from the actual argument types at this call site. @@ -850,9 +1610,10 @@ pub fn call_signature_details<'db>( .inferred_type(model) .unwrap_or(Type::unknown()) }); - let mut bindings = callable_type - .bindings(db) - .match_parameters(db, &call_arguments); + let mut bindings = + callable_type + .bindings(db, env) + .match_parameters(db, env, &call_arguments); // Run type checking to resolve TypeVar bindings from argument types. // For example, calling `dict[str, int].get("a")` resolves the `_KT` @@ -861,6 +1622,7 @@ pub fn call_signature_details<'db>( let constraints = ConstraintSetBuilder::new(); let _ = bindings.check_types_impl( db, + env, &constraints, &call_arguments, TypeContext::default(), @@ -872,7 +1634,7 @@ pub fn call_signature_details<'db>( bindings .iter_flat() .flatten() - .map(|binding| CallSignatureDetails::from_binding(db, binding)) + .map(|binding| CallSignatureDetails::from_binding(db, env, binding)) .collect() } else { // Type is not callable, return empty signatures @@ -888,7 +1650,8 @@ fn resolve_single_overload<'db>( call_expr: &ast::ExprCall, ) -> Option> { let db = model.db(); - let bindings = callable_type.bindings(db); + let env = &model.program_environment(); + let bindings = callable_type.bindings(db, env); let args = CallArguments::from_arguments_typed(&call_expr.arguments, |splatted_value| { splatted_value @@ -898,8 +1661,8 @@ fn resolve_single_overload<'db>( let constraints = ConstraintSetBuilder::new(); let mut resolved: Vec<_> = bindings - .match_parameters(db, &args) - .check_types(db, &constraints, &args, TypeContext::default(), &[]) + .match_parameters(db, env, &args) + .check_types(db, env, &constraints, &args, TypeContext::default(), &[]) .iter() .flat_map(super::call::bind::Bindings::iter_flat) .flat_map(|binding| { @@ -935,6 +1698,7 @@ fn full_type_bindings_for_call<'db>( call_expr: &ast::ExprCall, ) -> crate::types::call::Bindings<'db> { let db = model.db(); + let env = &model.program_environment(); let call_arguments = CallArguments::from_arguments_typed(&call_expr.arguments, |splatted_value| { splatted_value @@ -944,10 +1708,11 @@ fn full_type_bindings_for_call<'db>( let constraints = ConstraintSetBuilder::new(); func_type - .bindings(db) - .match_parameters(db, &call_arguments) + .bindings(db, env) + .match_parameters(db, env, &call_arguments) .check_types( db, + env, &constraints, &call_arguments, TypeContext::default(), @@ -1013,7 +1778,7 @@ pub fn call_argument_forms( // Ordinary callables have only value-form arguments for IDE purposes, so skip full binding. if !func_type - .bindings(db) + .bindings(db, &model.program_environment()) .iter_flat() .any(|binding| known_type_form_parameter_index(db, binding.callable_type).is_some()) { @@ -1085,21 +1850,20 @@ pub fn call_type_simplified_by_overloads( let db = model.db(); let func_type = call_expr.func.inferred_type(model)?; - let callable_type = func_type.try_upcast_to_callable(db)?.into_type(db); + let env = &model.program_environment(); + let callable_type = func_type + .try_upcast_to_callable(db, env)? + .into_type(db, env); // If the callable is trivial this analysis is useless, bail out - if let Some(binding) = callable_type.bindings(db).single_element() + if let Some(binding) = callable_type.bindings(db, env).single_element() && binding.overloads().len() < 2 { return None; } let signature = resolve_single_overload(model, callable_type, call_expr)?; - Some( - signature - .display_with(db, DisplaySettings::default().multiline()) - .to_string(), - ) + Some(signature.display(db, env).multiline().to_string()) } /// Returns the definitions of the binary operation along with its callable type. @@ -1107,13 +1871,15 @@ pub fn definitions_for_bin_op<'db>( model: &SemanticModel<'db>, binary_op: &ast::ExprBinOp, ) -> Option<(Vec>, Type<'db>)> { + let db = model.db(); let left_ty = binary_op.left.inferred_type(model)?; let right_ty = binary_op.right.inferred_type(model)?; - - let Ok(bindings) = Type::try_call_bin_op(model.db(), left_ty, binary_op.op, right_ty) else { + let env = &model.program_environment(); + let Ok(bindings) = Type::try_call_bin_op(db, env, left_ty, binary_op.op, right_ty) else { // basedpython: an applicable extension may supply the dunder let operator = crate::types::extensions::binary_extension_operator( model.db(), + env, model.file(), left_ty, binary_op.op, @@ -1131,7 +1897,7 @@ pub fn definitions_for_bin_op<'db>( )); }; - let callable_type = promote_for_self(model.db(), bindings.callable_type()); + let callable_type = promote_for_self(db, env, bindings.callable_type()); let definitions: Vec<_> = bindings .iter_flat() @@ -1151,6 +1917,7 @@ pub fn definitions_for_unary_op<'db>( model: &SemanticModel<'db>, unary_op: &ast::ExprUnaryOp, ) -> Option<(Vec>, Type<'db>)> { + let db = model.db(); let operand_ty = unary_op.operand.inferred_type(model)?; let unary_dunder_method = match unary_op.op { @@ -1162,8 +1929,10 @@ pub fn definitions_for_unary_op<'db>( ast::UnaryOp::Optional | ast::UnaryOp::Propagate | ast::UnaryOp::Force => return None, }; + let env = &model.program_environment(); let bindings = match operand_ty.try_call_dunder( - model.db(), + db, + env, unary_dunder_method, CallArguments::none(), TypeContext::default(), @@ -1172,7 +1941,8 @@ pub fn definitions_for_unary_op<'db>( Err(CallDunderError::MethodNotAvailable) if unary_op.op == ast::UnaryOp::Not => { // The runtime falls back to `__len__` for `not` if `__bool__` is not defined. match operand_ty.try_call_dunder( - model.db(), + db, + env, "__len__", CallArguments::none(), TypeContext::default(), @@ -1189,6 +1959,7 @@ pub fn definitions_for_unary_op<'db>( // basedpython: an applicable extension may supply the dunder let operator = crate::types::extensions::unary_extension_operator( model.db(), + env, model.file(), unary_op.op, operand_ty, @@ -1205,7 +1976,7 @@ pub fn definitions_for_unary_op<'db>( ) => *bindings, }; - let callable_type = promote_for_self(model.db(), bindings.callable_type()); + let callable_type = promote_for_self(db, env, bindings.callable_type()); let definitions = bindings .iter_flat() @@ -1229,14 +2000,15 @@ fn extension_operator_definitions<'db>( operator: crate::types::extensions::ExtensionOperator<'db>, arguments: &CallArguments<'_, 'db>, ) -> (Vec>, Type<'db>) { + let env = &model.program_environment(); let db = model.db(); // a call that does not check still names the member the operator resolved // to, which is what the IDE is being asked for - let bindings = match operator.resolution.ty.try_call(db, arguments) { + let bindings = match operator.resolution.ty.try_call(db, env, arguments) { Ok(bindings) => bindings, Err(error) => *error.into_bindings(), }; - let callable_type = promote_for_self(db, bindings.callable_type()); + let callable_type = promote_for_self(db, env, bindings.callable_type()); let definitions = bindings .iter_flat() .flatten() @@ -1252,14 +2024,22 @@ fn extension_operator_definitions<'db>( /// Promotes types in `self` positions. /// /// This is so that we show e.g. `int.__add__` instead of `Literal[4].__add__`. -fn promote_for_self<'db>(db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { +fn promote_for_self<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Type<'db> { match ty { Type::BoundMethod(method) => Type::BoundMethod(method.map_self_type(db, |self_ty| { - self_ty.literal_fallback_instance(db).unwrap_or(self_ty) + self_ty + .literal_fallback_instance(db, env) + .unwrap_or(self_ty) })), - Type::Union(elements) => elements.map(db, |ty| match ty { + Type::Union(elements) => elements.map(db, env, |ty| match ty { Type::BoundMethod(method) => Type::BoundMethod(method.map_self_type(db, |self_ty| { - self_ty.literal_fallback_instance(db).unwrap_or(self_ty) + self_ty + .literal_fallback_instance(db, env) + .unwrap_or(self_ty) })), _ => *ty, }), @@ -1318,7 +2098,10 @@ pub fn resolved_call_signature<'db>( ) -> Option> { let db = model.db(); let func_type = call_expr.func.inferred_type(model)?; - let callable_type = func_type.try_upcast_to_callable(db)?.into_type(db); + let env = &model.program_environment(); + let callable_type = func_type + .try_upcast_to_callable(db, env)? + .into_type(db, env); let args = CallArguments::from_arguments_typed(&call_expr.arguments, |splatted_value| { splatted_value @@ -1329,16 +2112,16 @@ pub fn resolved_call_signature<'db>( // Extract the `Bindings` regardless of whether type checking succeeded or failed. let constraints = ConstraintSetBuilder::new(); let bindings = callable_type - .bindings(db) - .match_parameters(db, &args) - .check_types(db, &constraints, &args, TypeContext::default(), &[]) + .bindings(db, env) + .match_parameters(db, env, &args) + .check_types(db, env, &constraints, &args, TypeContext::default(), &[]) .unwrap_or_else(|CallError(_, bindings)| *bindings); // First, try to find the matching overload after full type checking. let type_checked_details: Vec<_> = bindings .iter_flat() .flat_map(|binding| binding.matching_overloads().map(|(_, overload)| overload)) - .map(|binding| CallSignatureDetails::from_binding(db, binding)) + .map(|binding| CallSignatureDetails::from_binding(db, env, binding)) .collect(); if !type_checked_details.is_empty() { @@ -1352,7 +2135,7 @@ pub fn resolved_call_signature<'db>( let all_details: Vec<_> = bindings .iter_flat() .flatten() - .map(|binding| CallSignatureDetails::from_binding(db, binding)) + .map(|binding| CallSignatureDetails::from_binding(db, env, binding)) .collect(); if all_details.is_empty() { @@ -1409,8 +2192,7 @@ pub fn inlay_hint_call_argument_details<'db>( }; let parameter_label_offset = param.definition().map(|definition| { - let param_file = definition.file(db); - let module = parsed_module(db, param_file).load(db); + let module = parsed_module(db, definition.python_file(db)).load(db); definition.focus_range(db, &module) }); @@ -1444,7 +2226,7 @@ mod resolve_definition { } use indexmap::IndexSet; - use ruff_db::files::{File, FileRange, vendored_path_to_file}; + use ruff_db::files::{FileRange, vendored_path_to_file}; use ruff_db::parsed::{ParsedModuleRef, parsed_module}; use ruff_db::system::SystemPath; use ruff_db::vendored::VendoredPathBuf; @@ -1453,14 +2235,17 @@ mod resolve_definition { use ruff_text_size::TextRange; use rustc_hash::FxHashSet; use tracing::trace; - use ty_module_resolver::{ModuleName, file_to_module, resolve_module, resolve_real_module}; + use ty_module_resolver::{ + ImportingFile, ModuleName, file_to_module, resolve_module, resolve_real_module, + }; use crate::Db; + use crate::ProgramEnvironment; use crate::module_docstring; use crate::types::binding_type; use ty_python_core::definition::{Definition, DefinitionCategory, DefinitionKind}; use ty_python_core::scope::{NodeWithScopeKind, ScopeId}; - use ty_python_core::{global_scope, place_table, semantic_index, use_def_map}; + use ty_python_core::{ProgramFile, global_scope, place_table, semantic_index, use_def_map}; /// Represents the result of resolving an import to either a specific definition or /// a specific range within a file. @@ -1472,7 +2257,7 @@ mod resolve_definition { /// The import resolved to a specific definition within a module Definition(Definition<'db>), /// The import resolved to an entire module - Module(File), + Module(ProgramFile<'db>), /// The import resolved to a file with a specific range FileWithRange(FileRange), } @@ -1481,20 +2266,22 @@ mod resolve_definition { pub fn focus_range(&self, db: &dyn Db) -> FileRange { match self { ResolvedDefinition::Definition(definition) => { - let parsed = parsed_module(db, definition.file(db)).load(db); + let parsed = parsed_module(db, definition.python_file(db)).load(db); definition.focus_range(db, &parsed) } // For modules, navigate to the start of the file - ResolvedDefinition::Module(module) => FileRange::new(*module, TextRange::default()), + ResolvedDefinition::Module(module) => { + FileRange::new(module.file(db), TextRange::default()) + } ResolvedDefinition::FileWithRange(file_range) => *file_range, } } - pub fn category(&self, db: &dyn Db) -> DefinitionCategory { + pub(crate) fn category(&self, db: &dyn Db) -> DefinitionCategory { match self { ResolvedDefinition::Definition(definition) => { let file = definition.file(db); - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, definition.python_file(db)).load(db); definition.kind(db).category(file.is_stub(db), &parsed) } ResolvedDefinition::Module(_) | ResolvedDefinition::FileWithRange(_) => { @@ -1511,18 +2298,18 @@ mod resolve_definition { } } - fn file(&self, db: &'db dyn Db) -> File { - match self { - ResolvedDefinition::Definition(definition) => definition.file(db), - ResolvedDefinition::Module(file) => *file, - ResolvedDefinition::FileWithRange(file_range) => file_range.file(), + fn program_file(&self, db: &'db dyn Db) -> Option> { + match *self { + ResolvedDefinition::Definition(definition) => Some(definition.program_file(db)), + ResolvedDefinition::Module(file) => Some(file), + ResolvedDefinition::FileWithRange(_) => None, } } pub fn docstring(&self, db: &'db dyn Db) -> Option { match self { ResolvedDefinition::Definition(definition) => definition.docstring(db), - ResolvedDefinition::Module(file) => module_docstring(db, *file), + ResolvedDefinition::Module(file) => module_docstring(db, file.python_file(db)), ResolvedDefinition::FileWithRange(_) => None, } } @@ -1583,6 +2370,7 @@ mod resolve_definition { /// Always returns at least the original definition as a fallback if resolution fails. pub(crate) fn resolve_definition<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, definition: Definition<'db>, symbol_name: Option<&str>, alias_resolution: ImportAliasResolution, @@ -1590,6 +2378,7 @@ mod resolve_definition { let mut visited = FxHashSet::default(); let resolved = resolve_definition_recursive( db, + env, definition, &mut visited, symbol_name, @@ -1607,6 +2396,7 @@ mod resolve_definition { /// Helper function to resolve import definitions recursively. fn resolve_definition_recursive<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, definition: Definition<'db>, visited: &mut FxHashSet>, symbol_name: Option<&str>, @@ -1622,8 +2412,8 @@ mod resolve_definition { match kind { DefinitionKind::Import(import_def) => { - let file = definition.file(db); - let module = parsed_module(db, file).load(db); + let file = definition.program_file(db); + let module = parsed_module(db, file.python_file(db)).load(db); let alias = import_def.alias(&module); if alias.asname.is_some() @@ -1638,13 +2428,16 @@ mod resolve_definition { }; // Resolve the module to its file - let Some(resolved_module) = resolve_module(db, file, &module_name) else { + let importing_file = + ImportingFile::File(file.file(db), env.resolver_environment(db)); + let Some(resolved_module) = resolve_module(db, importing_file, &module_name) else { return Vec::new(); // Module not found, return empty list }; let Some(module_file) = resolved_module.file(db) else { return Vec::new(); // No file for module, return empty list }; + let module_file = ProgramFile::new(db, module_file, env.program(db)); // For simple imports like "import os", we want to navigate to the module itself. // Return the module file directly instead of trying to find definitions within it. @@ -1652,8 +2445,8 @@ mod resolve_definition { } DefinitionKind::ImportFrom(import_from_def) => { - let file = definition.file(db); - let module = parsed_module(db, file).load(db); + let file = definition.program_file(db); + let module = parsed_module(db, file.python_file(db)).load(db); let import_node = import_from_def.import(&module); let alias = import_from_def.alias(&module); @@ -1667,7 +2460,8 @@ mod resolve_definition { // (alias.name), not the local alias (symbol_name) resolve_from_import_definitions( db, - file, + env, + ImportingFile::File(file.file(db), env.resolver_environment(db)), import_node, &alias.name, visited, @@ -1677,15 +2471,16 @@ mod resolve_definition { // For star imports, try to resolve to the specific symbol being accessed DefinitionKind::StarImport(star_import_def) => { - let file = definition.file(db); - let module = parsed_module(db, file).load(db); + let file = definition.program_file(db); + let module = parsed_module(db, file.python_file(db)).load(db); let import_node = star_import_def.import(&module); // If we have a symbol name, use the helper to resolve it in the target module if let Some(symbol_name) = symbol_name { resolve_from_import_definitions( db, - file, + env, + ImportingFile::File(file.file(db), env.resolver_environment(db)), import_node, symbol_name, visited, @@ -1705,7 +2500,8 @@ mod resolve_definition { /// Helper function to resolve import definitions for `ImportFrom` and `StarImport` cases. pub(crate) fn resolve_from_import_definitions<'db>( db: &'db dyn Db, - file: File, + env: &ProgramEnvironment<'db>, + importing_file: ImportingFile<'db>, import_node: &ast::StmtImportFrom, symbol_name: &str, visited: &mut FxHashSet>, @@ -1716,7 +2512,7 @@ mod resolve_definition { if let Some(asname) = &alias.asname { if asname.as_str() == symbol_name { return vec![ResolvedDefinition::FileWithRange(FileRange::new( - file, + importing_file.file(db), asname.range, ))]; } @@ -1725,22 +2521,26 @@ mod resolve_definition { } // Resolve the module being imported from (handles both relative and absolute imports) - let Some(module_name) = ModuleName::from_import_statement(db, file, import_node).ok() + let Some(module_name) = + ModuleName::from_import_statement(db, importing_file, import_node).ok() else { return Vec::new(); }; - let Some(resolved_module) = resolve_module(db, file, &module_name) else { + let Some(resolved_module) = resolve_module(db, importing_file, &module_name) else { return Vec::new(); }; // Resolve the target module file - let module_file = resolved_module.file(db); + let module_file = resolved_module + .file(db) + .map(|file| ProgramFile::new(db, file, env.program(db))); let Some(module_file) = module_file else { // No file means this is a namespace package, try to import the submodule return Vec::from_iter(resolve_from_import_submodule_definitions( db, - file, + env, + importing_file, symbol_name, module_name, )); @@ -1753,8 +2553,14 @@ mod resolve_definition { // Recursively resolve any import definitions found in the target module let mut resolved_definitions = Vec::new(); for def in definitions_in_module { - let resolved = - resolve_definition_recursive(db, def, visited, Some(symbol_name), alias_resolution); + let resolved = resolve_definition_recursive( + db, + env, + def, + visited, + Some(symbol_name), + alias_resolution, + ); resolved_definitions.extend(resolved); } @@ -1767,7 +2573,8 @@ mod resolve_definition { // `child` has no binding in `pkg/__init__.py`. Vec::from_iter(resolve_from_import_submodule_definitions( db, - file, + env, + importing_file, symbol_name, module_name, )) @@ -1779,15 +2586,16 @@ mod resolve_definition { // Helper to resolve `from x.y import z` assuming `x.y.z` is a module. fn resolve_from_import_submodule_definitions<'db>( db: &'db dyn Db, - file: File, + env: &ProgramEnvironment<'db>, + importing_file: ImportingFile<'db>, symbol_name: &str, module_name: ModuleName, ) -> Option> { let submodule_name = ModuleName::new(symbol_name)?; let mut full_submodule_name = module_name; full_submodule_name.extend(&submodule_name); - let module = resolve_module(db, file, &full_submodule_name)?; - let file = module.file(db)?; + let module = resolve_module(db, importing_file, &full_submodule_name)?; + let file = ProgramFile::new(db, module.file(db)?, env.program(db)); Some(ResolvedDefinition::Module(file)) } @@ -1822,7 +2630,9 @@ mod resolve_definition { } } - definitions + super::user_visible_definitions(db, definitions) + .into_iter() + .collect() } /// Given a definition that may be in a stub file, find the "real" definition in a non-stub. @@ -1832,8 +2642,14 @@ mod resolve_definition { def: &ResolvedDefinition<'db>, cached_vendored_typeshed: Option<&SystemPath>, ) -> Option>> { + let Some(stub_program_file) = def.program_file(db) else { + trace!("Found arbitrary FileWithRange while stub mapping, giving up"); + return None; + }; + let env = ProgramEnvironment::from_file(stub_program_file); + // If the file isn't a stub, this is presumably the real definition - let stub_file = def.file(db); + let stub_file = stub_program_file.file(db); trace!("Stub mapping definition in: {}", stub_file.path(db)); if !stub_file.is_stub(db) { trace!("File isn't a stub, no stub mapping to do"); @@ -1850,7 +2666,7 @@ mod resolve_definition { // we're in typeshed to successfully stub-map to the Real Stdlib. So here we attempt // to do just that. The resulting file must not be used for anything other than // this module lookup, as the `ResolvedDefinition` we're handling isn't for that file. - let mut stub_file_for_module_lookup = stub_file; + let mut stub_file_for_module_lookup = stub_program_file; if let Some(vendored_typeshed) = cached_vendored_typeshed && let Some(stub_path) = stub_file.path(db).as_system_path() && let Ok(rel_path) = stub_path.strip_prefix(vendored_typeshed) @@ -1861,11 +2677,12 @@ mod resolve_definition { "Stub is cached vendored typeshed: {}", typeshed_file.path(db) ); - stub_file_for_module_lookup = typeshed_file; + stub_file_for_module_lookup = ProgramFile::new(db, typeshed_file, env.program(db)); } // It's definitely a stub, so now rerun module resolution but with stubs disabled. - let stub_module = file_to_module(db, stub_file_for_module_lookup)?; + let resolver_file = stub_file_for_module_lookup.resolver_file(db); + let stub_module = file_to_module(db, resolver_file)?; trace!("Found stub module: {}", stub_module.name(db)); // We need to pass an importing file to `resolve_real_module` which is a bit odd // here because there isn't really an importing file. However this `resolve_real_module` @@ -1876,13 +2693,17 @@ mod resolve_definition { // into the interpreter. In which case, all we have are stubs. // `resolve_real_module` will always return `None` for this case, but // it will emit false positive logs. And this saves us some work. - if is_builtin_module(db.python_version().minor, stub_module.name(db)) { + if is_builtin_module(stub_module.python_version(db).minor, stub_module.name(db)) { return None; } - let real_module = - resolve_real_module(db, stub_file_for_module_lookup, stub_module.name(db))?; + let real_module = resolve_real_module( + db, + ImportingFile::ResolverFile(resolver_file), + stub_module.name(db), + )?; trace!("Found real module: {}", real_module.name(db)); - let real_file = real_module.file(db)?; + let real_parse_file = ProgramFile::new(db, real_module.file(db)?, env.program(db)); + let real_file = real_parse_file.file(db); trace!("Found real file: {}", real_file.path(db)); // A definition has a "Definition Path" in a file made of nested definitions (~scopes): @@ -1903,7 +2724,7 @@ mod resolve_definition { let stub_ref; match *def { ResolvedDefinition::Definition(definition) => { - stub_parsed = parsed_module(db, stub_file); + stub_parsed = parsed_module(db, definition.python_file(db)); stub_ref = stub_parsed.load(db); // Get the leaf of the path (the definition itself) @@ -1915,7 +2736,7 @@ mod resolve_definition { path.push(leaf); // Get the ancestors of the path (all the definitions we're nested under) - let index = semantic_index(db, stub_file); + let index = semantic_index(db, definition.program_file(db)); for (_scope_id, scope) in index.ancestor_scopes(definition.file_scope(db)) { let node = scope.node(); let component = definition_path_component_for_node(&stub_ref, node) @@ -1935,7 +2756,7 @@ mod resolve_definition { stub_file.path(db), real_file.path(db) ); - return Some(vec![ResolvedDefinition::Module(real_file)]); + return Some(vec![ResolvedDefinition::Module(real_parse_file)]); } ResolvedDefinition::FileWithRange(_) => { // Not yet implemented -- in this case we want to recover something like a Definition @@ -1947,11 +2768,12 @@ mod resolve_definition { // Walk down the Definition Path in the real file let mut definitions = Vec::new(); - let index = semantic_index(db, real_file); - let real_parsed = parsed_module(db, real_file); + let index = semantic_index(db, real_parse_file); + let global_scope = global_scope(db, real_parse_file); + let real_parsed = parsed_module(db, global_scope.python_file(db)); let real_ref = real_parsed.load(db); // Start our search in the module (global) scope - let mut scopes = vec![global_scope(db, real_file)]; + let mut scopes = vec![global_scope]; while let Some(component) = path.pop() { trace!("Traversing definition path component: {}", component); // We're doing essentially a breadth-first traversal of the definitions. @@ -1967,6 +2789,7 @@ mod resolve_definition { .flat_map(|definition| { resolve_definition( db, + &env, definition, Some(component), ImportAliasResolution::ResolveAliases, @@ -1982,7 +2805,7 @@ mod resolve_definition { definition_path_component_for_node(&real_ref, scope_node) { if real_component == component { - scopes.push(child_scope_id.to_scope_id(db, real_file)); + scopes.push(child_scope_id.to_scope_id(db, real_parse_file)); } } scope.node(db); @@ -2045,6 +2868,18 @@ mod resolve_definition { let component = match definition.kind(db) { DefinitionKind::Function(func) => func.node(parsed).name.as_str(), DefinitionKind::Class(class) => class.node(parsed).name.as_str(), + DefinitionKind::Assignment(assignment) => { + let ast::Expr::Name(name) = assignment.target(parsed) else { + return Err(()); + }; + name.id.as_str() + } + DefinitionKind::AnnotatedAssignment(assignment) => { + let ast::Expr::Name(name) = assignment.target(parsed) else { + return Err(()); + }; + name.id.as_str() + } DefinitionKind::TypeAlias(_) | DefinitionKind::Import(_) | DefinitionKind::ImportFrom(_) @@ -2052,8 +2887,6 @@ mod resolve_definition { | DefinitionKind::StarImport(_) | DefinitionKind::NamedExpression(_) | DefinitionKind::StatementExpressionValue(_) - | DefinitionKind::Assignment(_) - | DefinitionKind::AnnotatedAssignment(_) | DefinitionKind::AugmentedAssignment(_) | DefinitionKind::DictKeyAssignment(_) | DefinitionKind::For(_) @@ -2080,11 +2913,11 @@ mod resolve_definition { /// Information about a class in the type hierarchy. #[derive(Debug, Clone)] -pub struct TypeHierarchyClass { +pub struct TypeHierarchyClass<'db> { /// The name of the class. pub name: Name, /// The file containing the class definition. - pub file: ruff_db::files::File, + pub file: ResolverFile<'db>, /// The range covering the full class definition header. pub full_range: TextRange, /// The range of the class name (for selection/focus). @@ -2099,8 +2932,12 @@ pub struct TypeHierarchyClass { /// This is meant to be used to "prepare" for a subtype or supertype request. /// That is, this effectively validates whether the given type can be used in /// subsequent requests for supertypes or subtypes. -pub fn type_hierarchy_prepare(db: &dyn Db, ty: Type<'_>) -> Option { - let class_literal = extract_class_literal(db, ty)?; +pub fn type_hierarchy_prepare<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Option> { + let class_literal = extract_class_literal(db, env, ty)?; Some(class_literal_to_hierarchy_info(db, class_literal)) } @@ -2110,18 +2947,22 @@ pub fn type_hierarchy_prepare(db: &dyn Db, ty: Type<'_>) -> Option) -> Vec { - let Some(class_literal) = extract_class_literal(db, ty) else { +pub fn type_hierarchy_supertypes<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Vec> { + let Some(class_literal) = extract_class_literal(db, env, ty) else { return vec![]; }; if class_literal.is_known(db, KnownClass::Object) { return vec![]; } - let mut supertypes: Vec = class_literal + let mut supertypes: Vec> = class_literal .explicit_bases(db) .into_iter() - .filter_map(|base| extract_class_literal(db, base)) + .filter_map(|base| extract_class_literal(db, env, base)) .map(|class_literal| class_literal_to_hierarchy_info(db, class_literal)) .collect(); // Every class implicitly inherits from `object` when no explicit @@ -2129,7 +2970,7 @@ pub fn type_hierarchy_supertypes(db: &dyn Db, ty: Type<'_>) -> Vec) -> Vec, - modules: &[Module<'_>], -) -> Vec { - let Some(target_class) = extract_class_literal(db, ty) else { +pub fn type_hierarchy_subtypes<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + modules: &[Module<'db>], +) -> Vec> { + let Some(target_class) = extract_class_literal(db, env, ty) else { return vec![]; }; + direct_subtypes(db, env, target_class, modules) + .into_iter() + .map(|class_literal| class_literal_to_hierarchy_info(db, class_literal)) + .collect() +} + +/// Finds classes that directly inherit from `target_class`. +/// +/// ```py +/// class Animal: ... +/// class Dog(Animal): ... +/// class LoudDog(Dog): ... +/// ``` +/// +/// For `Animal`, this returns `Dog`, but not `LoudDog`. +fn direct_subtypes<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + target_class: ClassLiteral<'db>, + modules: &[Module<'db>], +) -> Vec> { let target_name = target_class.name(db); let target_is_object = target_class.is_known(db, KnownClass::Object); let mut subtypes = vec![]; @@ -2168,37 +3031,25 @@ pub fn type_hierarchy_subtypes( continue; } - // Skip files that don't contain the class name. This avoids expensive - // semantic analysis for files that can't possibly contain a subclass - // of the target. We can't do this when looking for subtypes of - // `object` since `object` can be implicit. - if !target_is_object && !source_text(db, file).contains(target_name.as_str()) { + let source = source_text(db, file); + if !contains_identifier(&source, "class") { continue; } - let index = semantic_index(db, file); - for scope_id in index.scope_ids() { - let scope = scope_id.node(db); - let Some(class_node) = scope.as_class() else { - continue; - }; - - let def = index.expect_single_definition(class_node); - if !matches!(def.kind(db), DefinitionKind::Class(_)) { - continue; - } - - let file_scope_id = scope_id.file_scope_id(db); - let parsed = parsed_module(db, file).load(db); - if !is_range_reachable(db, index, file_scope_id, class_node.node(&parsed).range()) { - continue; - } - - let ty = crate::types::binding_type(db, def); - let Some(class_ty) = extract_class_literal(db, ty) else { - continue; - }; + // Keep the cheap name-based prefilter for non-first-party modules, which includes the + // vendored stdlib. First-party modules may inherit through local import aliases, e.g. + // `from a import Base as B; class Child(B): ...`, so they need semantic analysis even + // when they do not mention the target class's original name. + if is_non_first_party + && !target_is_object + && !contains_identifier(&source, target_name.as_str()) + { + continue; + } + let program_file = ProgramFile::new(db, file, env.program(db)); + let file_env = ProgramEnvironment::from_file(program_file); + for class_ty in reachable_class_literals_in_file(db, program_file) { let bases = class_ty.explicit_bases(db); let is_subtype = if target_is_object && bases.is_empty() @@ -2207,20 +3058,61 @@ pub fn type_hierarchy_subtypes( true } else { bases.iter().any(|base| { - extract_class_literal(db, *base) + extract_class_literal(db, &file_env, *base) .is_some_and(|base_literal| base_literal == target_class) }) }; if is_subtype { - subtypes.push(class_literal_to_hierarchy_info(db, class_ty)); + subtypes.push(class_ty); } } } subtypes } +/// Enumerates the reachable class definitions in `file`. +fn reachable_class_literals_in_file<'db>( + db: &'db dyn Db, + file: ProgramFile<'db>, +) -> Vec> { + let env = ProgramEnvironment::from_file(file); + let index = semantic_index(db, file); + let parsed = parsed_module(db, file.python_file(db)).load(db); + let mut classes = Vec::new(); + + for scope_id in index.scope_ids() { + let scope = scope_id.node(db); + let Some(class_node) = scope.as_class() else { + continue; + }; + + // Map AST class node to its definition in the semantic index. + let definition = index.expect_single_definition(class_node); + if !matches!(definition.kind(db), DefinitionKind::Class(_)) { + continue; + } + + // Drop classes in dead code — e.g. a class under if sys.version_info < (3, 9): on a newer Python. + let file_scope_id = scope_id.file_scope_id(db); + if !is_range_reachable(db, index, file_scope_id, class_node.node(&parsed).range()) { + continue; + } + + // Convert the definition's type into a ClassLiteral, dropping anything that doesn't produce a usable class object. + if let Some(class) = extract_class_literal(db, &env, binding_type(db, definition)) { + classes.push(class); + } + } + + classes +} + /// Extract a `ClassLiteral` from a `Type`, handling various type forms. -fn extract_class_literal<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option> { +fn extract_class_literal<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Option> { match ty { Type::ClassLiteral(class_literal) => Some(class_literal), Type::SubclassOf(subclass_of) => { @@ -2235,11 +3127,11 @@ fn extract_class_literal<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option Some(ClassLiteral::Static(generic_alias.origin(db))), - Type::NominalInstance(instance) => Some(instance.class(db).class_literal(db)), + Type::NominalInstance(instance) => Some(instance.class(db, env).class_literal(db)), Type::Union(union) => union .elements(db) .iter() - .find_map(|elem| extract_class_literal(db, *elem)), + .find_map(|elem| extract_class_literal(db, env, *elem)), _ => None, } @@ -2249,16 +3141,16 @@ fn extract_class_literal<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option, -) -> TypeHierarchyClass { +fn class_literal_to_hierarchy_info<'db>( + db: &'db dyn Db, + class_literal: ClassLiteral<'db>, +) -> TypeHierarchyClass<'db> { let name = class_literal.name(db).clone(); - let file = class_literal.file(db); + let file = class_literal.program_file(db).resolver_file(db); let (full_range, selection_range) = match class_literal { ClassLiteral::Static(static_class) => { - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, static_class.python_file(db)).load(db); let header_range = static_class.header_range(db); let body_scope = static_class.body_scope(db); @@ -2286,7 +3178,7 @@ fn class_literal_to_hierarchy_info( // (likely incorrectly) return the type hierarchy for `type` itself. ClassLiteral::Dynamic(dynamic_class) => { if let DynamicClassAnchor::Definition(definition) = dynamic_class.anchor(db) { - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, definition.python_file(db)).load(db); let kind = definition.kind(db); (kind.full_range(&parsed), kind.target_range(&parsed)) } else { @@ -2298,7 +3190,7 @@ fn class_literal_to_hierarchy_info( if let DynamicNamedTupleAnchor::CollectionsDefinition { definition, .. } | DynamicNamedTupleAnchor::TypingDefinition(definition) = namedtuple.anchor(db) { - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, definition.python_file(db)).load(db); let kind = definition.kind(db); (kind.full_range(&parsed), kind.target_range(&parsed)) } else { @@ -2312,7 +3204,7 @@ fn class_literal_to_hierarchy_info( } ClassLiteral::DynamicEnum(dynamic_enum) => { if let DynamicEnumAnchor::Definition { definition, .. } = dynamic_enum.anchor(db) { - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, definition.python_file(db)).load(db); let kind = definition.kind(db); (kind.full_range(&parsed), kind.target_range(&parsed)) } else { @@ -2334,21 +3226,21 @@ pub fn constructor_signature(model: &SemanticModel, call_expr: &ast::ExprCall) - let function_ty = call_expr.func.inferred_type(model)?; let db = model.db(); let class_name = function_ty.as_class_literal()?.name(db); + let env = &model.program_environment(); let display_sig = |signature: &Signature| { let params = signature - .display_with( - db, - DisplaySettings::default() - .multiline() - .disallow_signature_name() - .hide_return_type(), - ) + .display(db, env) + .multiline() + .disallow_name() + .hide_return_type() .to_string(); format!("class {class_name}{params}") }; - let callable_type = function_ty.try_upcast_to_callable(db)?.into_type(db); - let bindings = callable_type.bindings(db); + let callable_type = function_ty + .try_upcast_to_callable(db, env)? + .into_type(db, env); + let bindings = callable_type.bindings(db, env); if let Some(binding) = bindings.single_element() && binding.overloads().len() == 1 @@ -2381,7 +3273,11 @@ pub fn constructor_signature(model: &SemanticModel, call_expr: &ast::ExprCall) - /// /// The IDE surfaces this as an inlay hint, so a declared clause returns `None`: /// the source already says what it is. -pub fn inferred_raises<'db>(db: &'db dyn Db, function: Type<'db>) -> Option> { +pub fn inferred_raises<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + function: Type<'db>, +) -> Option> { let Type::FunctionLiteral(function) = function else { return None; }; @@ -2405,7 +3301,7 @@ pub fn inferred_raises<'db>(db: &'db dyn Db, function: Type<'db>) -> Option( /// hint appears exactly where writing `override` would silence that lint. pub fn inferred_override<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: Type<'db>, member: Type<'db>, name: &str, @@ -2464,7 +3361,7 @@ pub fn inferred_override<'db>( .filter_map(ClassBase::into_class) .find(|superclass| { !superclass - .own_class_member(db, None, name) + .own_class_member(db, env, None, name) .inner .place .is_undefined() @@ -2481,11 +3378,12 @@ pub fn inferred_override<'db>( /// its members — a union of enums contributes all of them. pub fn context_sensitive_members<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, target: Type<'db>, ) -> Vec<(Name, Type<'db>)> { let mut members = Vec::new(); let mut seen = FxHashSet::default(); - let _ = for_each_candidate(db, target, &mut |candidate| { + let _ = for_each_candidate(db, env, target, &mut |candidate| { for base in candidate.iter_mro(db).filter_map(ClassBase::into_class) { let Some(enum_class) = base.class_literal(db).into_enum_class(db) else { continue; @@ -2512,6 +3410,7 @@ pub fn context_sensitive_members<'db>( /// every applicable extension's body and asks about each name it declares. pub fn extension_members<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, receiver: Type<'db>, ) -> Vec<(Name, Type<'db>)> { @@ -2523,7 +3422,7 @@ pub fn extension_members<'db>( if !seen.insert(name.clone()) { continue; } - if let Some(resolution) = resolve_extension_member(db, file, receiver, &name) { + if let Some(resolution) = resolve_extension_member(db, env, file, receiver, &name) { members.push((name, resolution.ty)); } } @@ -2551,7 +3450,11 @@ pub struct OverridableMember<'db> { /// that offers the members can write the whole signature down — the one thing /// about an override that is not worth typing out. `object`'s members are left /// out: everything inherits them, and almost nothing means to override them. -pub fn overridable_members<'db>(db: &'db dyn Db, class: Type<'db>) -> Vec> { +pub fn overridable_members<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + class: Type<'db>, +) -> Vec> { let Type::ClassLiteral(class) = class else { return Vec::new(); }; @@ -2586,7 +3489,10 @@ pub fn overridable_members<'db>(db: &'db dyn Db, class: Type<'db>) -> Vec(db: &'db dyn Db, class: Type<'db>) -> Vec( // a block declares `it` only when its callback passes one, so read that off // the binding the block actually made rather than deciding it again here let binds_it = function.parameters.args.first().is_some_and(|it| { - semantic_index(db, model.file()) + semantic_index(db, db.program_file(model.file())) .try_definition(&it.parameter) .is_some() }); @@ -2671,7 +3581,7 @@ pub fn hintable_parameter_type<'db>( parameter: &ast::Parameter, ) -> Option> { let db = model.db(); - let definition = semantic_index(db, model.file()).try_definition(parameter)?; + let definition = semantic_index(db, db.program_file(model.file())).try_definition(parameter)?; match binding_type(db, definition) { Type::TypeVar(bound_typevar) if bound_typevar.typevar(db).is_self(db) => None, @@ -2720,8 +3630,16 @@ pub fn type_parameter_names<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option(db: &'db dyn Db, ty: Type<'db>) -> Option> { - Some(ty.try_iterate(db).ok()?.homogeneous_element_type(db)) +pub fn iterable_element_type<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Option> { + Some( + ty.try_iterate(db, env) + .ok()? + .homogeneous_element_type(db, env), + ) } /// Whether `ty` can be awaited. @@ -2729,8 +3647,8 @@ pub fn iterable_element_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option(db: &'db dyn Db, ty: Type<'db>) -> bool { - ty.try_await(db).is_ok() +pub fn is_awaitable<'db>(db: &'db dyn Db, env: &ProgramEnvironment<'db>, ty: Type<'db>) -> bool { + ty.try_await(db, env).is_ok() } /// What calling `ty` with no arguments gives, or `None` when it can't be called @@ -2741,10 +3659,14 @@ pub fn is_awaitable<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { /// the method, and `{% for book in author.book_set.all %}` iterates the queryset /// rather than the manager's method. Attribute access alone stops at the method /// and every one of those traversals dead-ends. -pub fn no_argument_call_return_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option> { - ty.try_call(db, &CallArguments::none()) +pub fn no_argument_call_return_type<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Option> { + ty.try_call(db, env, &CallArguments::none()) .ok() - .map(|bindings| bindings.return_type(db)) + .map(|bindings| bindings.return_type(db, env)) } /// How a callable takes one of its parameters. @@ -2791,10 +3713,11 @@ pub struct CallableParameter<'db> { /// answer for. pub fn callable_parameters<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, ) -> Option>> { let callable = ty - .try_upcast_to_callable(db) + .try_upcast_to_callable(db, env) .and_then(CallableTypes::exactly_one)?; let signatures = callable.signatures(db); let [signature] = signatures.overloads.as_slice() else { @@ -2838,13 +3761,25 @@ pub fn callable_parameters<'db>( /// Written for the web frameworks, which say what a route hands a view by naming /// the class — django's `` is a `uuid.UUID` — rather than by writing a /// type expression anywhere the type checker would evaluate one. -pub fn instance_of_class<'db>(db: &'db dyn Db, module: &str, name: &str) -> Option> { - let module = resolve_module_confident(db, &ModuleName::new(module)?)?; +pub fn instance_of_class<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + module: &str, + name: &str, +) -> Option> { + let module = + resolve_module_confident(db, env.resolver_environment(db), &ModuleName::new(module)?)?; - imported_symbol(db, module.file(db), name, None) - .place - .ignore_possibly_undefined()? - .to_instance_approximation(db) + imported_symbol( + db, + env, + module.file(db).map(|file| db.program_file(file)), + name, + None, + ) + .place + .ignore_possibly_undefined()? + .to_instance_approximation(db, env) } /// Whether `ty` is a function django would try to call and could not. @@ -2854,9 +3789,13 @@ pub fn instance_of_class<'db>(db: &'db dyn Db, module: &str, name: &str) -> Opti /// Only a function or a method is answered for: anything else that fails to call /// is something django's `callable()` test would not have called in the first /// place. -pub fn callable_needs_arguments<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { +pub fn callable_needs_arguments<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> bool { matches!(ty, Type::FunctionLiteral(_) | Type::BoundMethod(_)) - && no_argument_call_return_type(db, ty).is_none() + && no_argument_call_return_type(db, env, ty).is_none() } /// What django's template variable resolution does with the member a lookup @@ -2896,19 +3835,20 @@ pub enum TemplateLookup { /// contributes a return type rather than the member itself. pub fn template_lookup<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, receiver: Type<'db>, name: &str, member: Type<'db>, ) -> TemplateLookup { let declares_do_not_call = member - .member(db, "do_not_call_in_templates") + .member(db, env, "do_not_call_in_templates") .place .ignore_possibly_undefined() - .is_some_and(|flag| flag.bool(db).is_always_true()); + .is_some_and(|flag| flag.bool(db, env).is_always_true()); if declares_do_not_call { TemplateLookup::UsesUncalled - } else if django::refuses_template_call(db, receiver, name, member) { + } else if django::refuses_template_call(db, env, receiver, name, member) { TemplateLookup::Refuses } else { TemplateLookup::Calls @@ -2921,11 +3861,15 @@ pub fn template_lookup<'db>( /// apart from the several dozen members `models.Model` brings with it. The /// fields are what the template was written against; sorting them in among the /// framework's machinery is nearly the same as not offering them. -pub fn own_class_member_names<'db>(db: &'db dyn Db, ty: Type<'db>) -> FxIndexSet { +pub fn own_class_member_names<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> FxIndexSet { let Type::NominalInstance(instance) = ty else { return FxIndexSet::default(); }; - let Some((class, _)) = instance.class(db).static_class_literal(db) else { + let Some((class, _)) = instance.class(db, env).static_class_literal(db) else { return FxIndexSet::default(); }; @@ -2939,16 +3883,21 @@ pub fn own_class_member_names<'db>(db: &'db dyn Db, ty: Type<'db>) -> FxIndexSet /// /// `float` means `int | float` and `complex` means `int | float | complex` in a /// `.py` file. basedpython opts out, so a `.by` file promotes nothing. -pub fn numeric_promotion<'db>(db: &'db dyn Db, file: File, ty: Type<'db>) -> Option<&'static str> { +pub fn numeric_promotion<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + file: File, + ty: Type<'db>, +) -> Option<&'static str> { if file.source_type(db).is_basedpython() { return None; } // a type expression stores what it evaluated to, so the promotion is only // visible as the union it produced - if ty == KnownUnion::Float.to_type(db) { + if ty == KnownUnion::Float.to_type(db, env) { Some(" | int") - } else if ty == KnownUnion::Complex.to_type(db) { + } else if ty == KnownUnion::Complex.to_type(db, env) { Some(" | float | int") } else { None @@ -2957,11 +3906,23 @@ pub fn numeric_promotion<'db>(db: &'db dyn Db, file: File, ty: Type<'db>) -> Opt #[cfg(test)] mod tests { - use super::{CallArgumentForm, call_argument_forms}; + use super::{CallArgumentForm, call_argument_forms, contains_identifier}; use crate::SemanticModel; use crate::db::tests::TestDbBuilder; use ruff_db::files::system_path_to_file; use ruff_db::parsed::parsed_module; + use ty_python_core::ProgramFile; + + #[test] + fn source_candidate_prefilters_use_identifier_boundaries() { + for (source, name) in [("x = 1", "x"), ("obj.x", "x"), ("x()", "x")] { + assert!(contains_identifier(source, name)); + } + + for (source, name) in [("exclude = 10", "x"), ("Database", "Base"), ("", "x")] { + assert!(!contains_identifier(source, name)); + } + } #[test] fn keyword_call_argument_forms_follow_source_order() -> anyhow::Result<()> { @@ -2977,7 +3938,8 @@ cast(val="", typ=int) .build()?; let file = system_path_to_file(&db, "/src/foo.py").unwrap(); - let parsed = parsed_module(&db, file).load(&db); + let file = ProgramFile::new(&db, file, db.program_environment().program(&db)); + let parsed = parsed_module(&db, file.python_file(&db)).load(&db); let call = parsed .suite() .last() @@ -3017,7 +3979,8 @@ f(y="", x=1) .build()?; let file = system_path_to_file(&db, "/src/foo.py").unwrap(); - let parsed = parsed_module(&db, file).load(&db); + let file = ProgramFile::new(&db, file, db.program_environment().program(&db)); + let parsed = parsed_module(&db, file.python_file(&db)).load(&db); let call = parsed .suite() .last() @@ -3053,7 +4016,8 @@ f(val="", typ=int) .build()?; let file = system_path_to_file(&db, "/src/foo.py").unwrap(); - let parsed = parsed_module(&db, file).load(&db); + let file = ProgramFile::new(&db, file, db.program_environment().program(&db)); + let parsed = parsed_module(&db, file.python_file(&db)).load(&db); let call = parsed .suite() .last() @@ -3090,7 +4054,8 @@ f("", int) .build()?; let file = system_path_to_file(&db, "/src/foo.py").unwrap(); - let parsed = parsed_module(&db, file).load(&db); + let file = ProgramFile::new(&db, file, db.program_environment().program(&db)); + let parsed = parsed_module(&db, file.python_file(&db)).load(&db); let call = parsed .suite() .last() @@ -3130,7 +4095,8 @@ f(int, x) .build()?; let file = system_path_to_file(&db, "/src/foo.py").unwrap(); - let parsed = parsed_module(&db, file).load(&db); + let file = ProgramFile::new(&db, file, db.program_environment().program(&db)); + let parsed = parsed_module(&db, file.python_file(&db)).load(&db); let call = parsed .suite() .last() @@ -3174,7 +4140,8 @@ TypeAliasType("Alias", int) .build()?; let file = system_path_to_file(&db, "/src/foo.py").unwrap(); - let parsed = parsed_module(&db, file).load(&db); + let file = ProgramFile::new(&db, file, db.program_environment().program(&db)); + let parsed = parsed_module(&db, file.python_file(&db)).load(&db); let calls: Vec<_> = parsed .suite() .iter() @@ -3219,7 +4186,8 @@ cast(*args) .build()?; let file = system_path_to_file(&db, "/src/foo.py").unwrap(); - let parsed = parsed_module(&db, file).load(&db); + let file = ProgramFile::new(&db, file, db.program_environment().program(&db)); + let parsed = parsed_module(&db, file.python_file(&db)).load(&db); let call = parsed .suite() .last() diff --git a/crates/ty_python_semantic/src/types/ide_support/unreachable_code.rs b/crates/ty_python_semantic/src/types/ide_support/unreachable_code.rs index ac5b0d15a1..3da1ea8858 100644 --- a/crates/ty_python_semantic/src/types/ide_support/unreachable_code.rs +++ b/crates/ty_python_semantic/src/types/ide_support/unreachable_code.rs @@ -2,8 +2,8 @@ use crate::Db; use crate::reachability::is_reachable; use get_size2::GetSize; use itertools::Itertools; -use ruff_db::files::File; use ruff_text_size::TextRange; +use ty_python_core::ProgramFile; use ty_python_core::reachability_constraints::ScopedReachabilityConstraintId; use ty_python_core::semantic_index; @@ -45,10 +45,9 @@ pub enum UnreachableKind { /// `ALWAYS_FALSE` constraints are classified as unconditional; all others are /// unreachable only under the current analysis. #[salsa::tracked(returns(deref), heap_size=ruff_memory_usage::heap_size)] -pub fn unreachable_ranges(db: &dyn Db, file: File) -> Box<[UnreachableRange]> { +pub fn unreachable_ranges(db: &dyn Db, file: ProgramFile<'_>) -> Box<[UnreachableRange]> { let index = semantic_index(db, file); let mut unreachable = Vec::new(); - for scope_id in index.scope_ids() { let use_def = index.use_def_map(scope_id.file_scope_id(db)); unreachable.extend( @@ -93,7 +92,7 @@ fn merge_overlapping_ranges(mut ranges: Vec) -> Box<[Unreachab #[cfg(test)] mod tests { use super::{UnreachableKind, unreachable_ranges}; - use crate::db::tests::TestDbBuilder; + use crate::db::tests::{TestDb, TestDbBuilder}; use insta::assert_snapshot; use ruff_db::diagnostic::{ Annotation, Diagnostic, DiagnosticId, DisplayDiagnosticConfig, DisplayDiagnostics, Severity, @@ -101,6 +100,7 @@ mod tests { use ruff_db::files::{FileRange, system_path_to_file}; use ruff_python_ast::PythonVersion; use ruff_python_trivia::textwrap::dedent; + use ty_python_core::ProgramFile; use ty_python_core::platform::PythonPlatform; const TEST_PATH: &str = "/src/main.py"; @@ -145,25 +145,28 @@ mod tests { } } - fn render_unreachable_diagnostics(db: &crate::db::tests::TestDb, path: &str) -> String { + fn render_unreachable_diagnostics(db: &TestDb, path: &str) -> String { let file = system_path_to_file(db, path).unwrap(); - let diagnostics = unreachable_ranges(db, file) - .iter() - .map(|range| { - let mut diagnostic = Diagnostic::new( - DiagnosticId::lint("unreachable-code"), - Severity::Info, - match range.kind { - UnreachableKind::Unconditional => "Code is always unreachable", - UnreachableKind::CurrentAnalysis => "Code is unreachable", - }, - ); - diagnostic.annotate(Annotation::primary( - FileRange::new(file, range.range).into(), - )); - diagnostic - }) - .collect::>(); + let diagnostics = unreachable_ranges( + db, + ProgramFile::new(db, file, db.program_environment().program(db)), + ) + .iter() + .map(|range| { + let mut diagnostic = Diagnostic::new( + DiagnosticId::lint("unreachable-code"), + Severity::Info, + match range.kind { + UnreachableKind::Unconditional => "Code is always unreachable", + UnreachableKind::CurrentAnalysis => "Code is unreachable", + }, + ); + diagnostic.annotate(Annotation::primary( + FileRange::new(file, range.range).into(), + )); + diagnostic + }) + .collect::>(); DisplayDiagnostics::new( db, @@ -188,7 +191,6 @@ mod tests { | 4 | print("dead") | ^^^^^^^^^^^^^ - | "#); Ok(()) } @@ -208,7 +210,6 @@ mod tests { | 5 | print("dead") | ^^^^^^^^^^^^^ - | "#); Ok(()) } @@ -233,7 +234,6 @@ mod tests { | 7 | print("dead") | ^^^^^^^^^^^^^ - | "#); Ok(()) } @@ -254,7 +254,6 @@ mod tests { 4 | / print("dead") 5 | | print("still dead") | |_______________________^ - | "#); Ok(()) } @@ -273,7 +272,6 @@ mod tests { | 4 | print("dead") | ^^^^^^^^^^^^^ - | "#); Ok(()) } @@ -295,7 +293,6 @@ mod tests { | 5 | print("dead") | ^^^^^^^^^^^^^ - | "#); Ok(()) } @@ -314,7 +311,6 @@ mod tests { | 4 | print("dead") | ^^^^^^^^^^^^^ - | "#); Ok(()) } @@ -334,7 +330,6 @@ mod tests { | 5 | print("dead") | ^^^^^^^^^^^^^ - | "#); Ok(()) } @@ -354,7 +349,6 @@ mod tests { | 5 | print("dead") | ^^^^^^^^^^^^^ - | "#); Ok(()) } @@ -374,7 +368,6 @@ mod tests { | 5 | print("dead") | ^^^^^^^^^^^^^ - | "#); Ok(()) } @@ -392,11 +385,32 @@ mod tests { | 3 | print("dead") | ^^^^^^^^^^^^^ - | "#); Ok(()) } + #[test] + fn reports_impossible_typed_dict_key_membership() -> anyhow::Result<()> { + let source = r#" + from typing_extensions import TypedDict + + class Items(TypedDict, closed=True): + present: int + + def f(items: Items) -> None: + if "missing" in items: + print("missing") + if "present" not in items: + print("present") + "#; + + let diagnostics = UnreachableTest::new().render(source)?; + assert_eq!(diagnostics.matches("Code is unreachable").count(), 2); + assert!(diagnostics.contains("print(\"missing\")")); + assert!(diagnostics.contains("print(\"present\")")); + Ok(()) + } + #[test] fn reports_statically_empty_loop_bodies() -> anyhow::Result<()> { let source = r#" @@ -413,14 +427,12 @@ mod tests { | 3 | print("dead") | ^^^^^^^^^^^^^ - | info[unreachable-code]: Code is always unreachable --> src/main.py:6:5 | 6 | print("also dead") | ^^^^^^^^^^^^^^^^^^ - | "#); Ok(()) } @@ -438,7 +450,6 @@ mod tests { | 3 | print("dead") | ^^^^^^^^^^^^^ - | "#); Ok(()) } @@ -458,7 +469,6 @@ mod tests { | 5 | print("dead") | ^^^^^^^^^^^^^ - | "#); Ok(()) } @@ -478,7 +488,6 @@ mod tests { | 5 | print("dead") | ^^^^^^^^^^^^^ - | "#); Ok(()) } @@ -502,7 +511,6 @@ mod tests { | 4 | return | ^^^^^^ - | info[unreachable-code]: Code is always unreachable --> src/main.py:8:9 @@ -510,7 +518,6 @@ mod tests { 8 | / pass 9 | | print("dead") | |_________________^ - | "#); Ok(()) } @@ -530,7 +537,6 @@ mod tests { | 5 | print("dead") | ^^^^^^^^^^^^^ - | "#); Ok(()) } @@ -547,7 +553,6 @@ mod tests { | 2 | x = "yes" if True else "no" | ^^^^ - | "#); Ok(()) } @@ -568,14 +573,12 @@ mod tests { | 3 | x = 1 | ^^^^^ - | info[unreachable-code]: Code is always unreachable --> src/main.py:6:5 | 6 | y = 2 | ^^^^^ - | "); Ok(()) } @@ -593,7 +596,6 @@ mod tests { | 3 | x = lambda: 1 | ^^^^^^^^^^^^^ - | "); Ok(()) } @@ -613,7 +615,6 @@ mod tests { 3 | / def f(): 4 | | pass | |____________^ - | "); Ok(()) } @@ -633,7 +634,6 @@ mod tests { 3 | / class Foo: 4 | | pass | |____________^ - | "); Ok(()) } @@ -651,7 +651,6 @@ mod tests { | 3 | x = [i for i in range(10)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | "); Ok(()) } @@ -675,21 +674,18 @@ mod tests { | 3 | x = {k: v for k, v in {}.items()} | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | info[unreachable-code]: Code is always unreachable --> src/main.py:6:5 | 6 | y = {i for i in range(10)} | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | info[unreachable-code]: Code is always unreachable --> src/main.py:9:5 | 9 | z = (i for i in range(10)) | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | "); Ok(()) } @@ -710,7 +706,6 @@ mod tests { | 3 | type Alias[T] = list[T] | ^^^^^^^^^^^^^^^^^^^^^^^ - | "); Ok(()) } @@ -733,7 +728,6 @@ mod tests { | 5 | from typing import Self | ^^^^^^^^^^^^^^^^^^^^^^^ - | "); Ok(()) } @@ -756,7 +750,6 @@ mod tests { | 5 | import winreg | ^^^^^^^^^^^^^ - | "); Ok(()) } @@ -780,7 +773,6 @@ mod tests { | 9 | print("dead") | ^^^^^^^^^^^^^ - | "#); Ok(()) } @@ -826,7 +818,6 @@ mod tests { 5 | / if False: 6 | | x = lambda: 1 | |_____________________^ - | "); Ok(()) } diff --git a/crates/ty_python_semantic/src/types/ide_support/unused_bindings.rs b/crates/ty_python_semantic/src/types/ide_support/unused_bindings.rs index cb8b7625a5..3495215af5 100644 --- a/crates/ty_python_semantic/src/types/ide_support/unused_bindings.rs +++ b/crates/ty_python_semantic/src/types/ide_support/unused_bindings.rs @@ -10,7 +10,7 @@ use rustc_hash::FxHashSet; use ty_python_core::definition::{DefinitionCategory, DefinitionKind, DefinitionState}; use ty_python_core::place::ScopedPlaceId; use ty_python_core::scope::{FileScopeId, ScopeKind}; -use ty_python_core::{SemanticIndex, semantic_index}; +use ty_python_core::{ProgramFile, SemanticIndex, semantic_index}; /// Returns `true` for definition kinds that create user-facing bindings we consider for /// unused-binding diagnostics. @@ -48,6 +48,36 @@ fn should_consider_definition(kind: &DefinitionKind<'_>) -> bool { } } +/// Returns whether a comprehension walrus belongs to an enclosing function or lambda. +/// +/// ```python +/// def last_item(items): +/// [(last := item) for item in items] +/// return last +/// ``` +/// +/// A module-level walrus, or one declared `global` or `nonlocal` in its containing +/// function, is not a local binding and must not receive an unused-binding diagnostic. +fn comprehension_named_expression_is_local( + index: &SemanticIndex<'_>, + comprehension_scope: FileScopeId, + name: &str, +) -> bool { + index + .ancestor_scopes(comprehension_scope) + .skip(1) + .find(|(_, scope)| scope.kind() != ScopeKind::Comprehension) + .is_some_and(|(scope_id, scope)| { + matches!(scope.kind(), ScopeKind::Function | ScopeKind::Lambda) + && index + .place_table(scope_id) + .symbol_id(name) + .is_some_and(|symbol_id| { + index.place_table(scope_id).symbol(symbol_id).is_local() + }) + }) +} + fn function_scope_is_overload_declaration( db: &dyn Db, index: &SemanticIndex<'_>, @@ -76,11 +106,20 @@ pub struct UnusedBinding { /// without broader reference analysis. Bare local annotations (`x: int`) are also /// reported, but only if the symbol is neither bound nor used elsewhere in the scope. #[salsa::tracked(returns(deref), heap_size=ruff_memory_usage::heap_size)] -pub fn unused_bindings(db: &dyn Db, file: ruff_db::files::File) -> Box<[UnusedBinding]> { - let parsed = parsed_module(db, file).load(db); - let is_stub_file = file.is_stub(db); +pub fn unused_bindings(db: &dyn Db, file: ProgramFile<'_>) -> Box<[UnusedBinding]> { + let source_file = file.file(db); + let parsed = parsed_module(db, file.python_file(db)).load(db); + let is_stub_file = source_file.is_stub(db); let index = semantic_index(db, file); let mut unused = Vec::new(); + // A used synthetic definition counts as a use of the user-visible definitions it represents. + let used_definitions = index.scope_ids().flat_map(|scope_id| { + index + .use_def_map(scope_id.file_scope_id(db)) + .all_definitions_with_usage() + .filter_map(|(_, state, is_used)| is_used.then_some(state.definition()).flatten()) + }); + let used_user_visible_definitions = super::user_visible_definitions(db, used_definitions); for scope_id in index.scope_ids() { let file_scope_id = scope_id.file_scope_id(db); @@ -111,6 +150,7 @@ pub fn unused_bindings(db: &dyn Db, file: ruff_db::files::File) -> Box<[UnusedBi let DefinitionState::Defined(definition) = state else { continue; }; + let is_used = is_used || used_user_visible_definitions.contains(&definition); if is_used { let DefinitionKind::LoopHeader(loop_header_definition) = definition.kind(db) else { @@ -162,7 +202,12 @@ pub fn unused_bindings(db: &dyn Db, file: ruff_db::files::File) -> Box<[UnusedBi // Global and nonlocal assignments target bindings from outer scopes. // Treat them as externally managed to avoid false positives here. - if symbol.is_global() || symbol.is_nonlocal() { + let is_local_comprehension_named_expression = scope_kind == ScopeKind::Comprehension + && matches!(kind, DefinitionKind::NamedExpression(_)) + && comprehension_named_expression_is_local(index, file_scope_id, name); + if (symbol.is_global() || symbol.is_nonlocal()) + && !is_local_comprehension_named_expression + { continue; } @@ -204,6 +249,7 @@ mod tests { use ruff_python_ast::name::Name; use ruff_python_trivia::textwrap::dedent; use ruff_text_size::{TextRange, TextSize}; + use ty_python_core::ProgramFile; fn collect_unused_bindings_in_file( path: &str, @@ -211,7 +257,8 @@ mod tests { ) -> anyhow::Result> { let db = TestDbBuilder::new().with_file(path, source).build()?; let file = system_path_to_file(&db, path).unwrap(); - let mut bindings = unused_bindings(&db, file).to_vec(); + let program = db.program_environment().program(&db); + let mut bindings = unused_bindings(&db, ProgramFile::new(&db, file, program)).to_vec(); bindings.sort_unstable_by_key(|binding| (binding.range.start(), binding.range.end())); Ok(bindings) } @@ -285,11 +332,64 @@ mod tests { Ok(()) } + #[test] + fn or_pattern_captures_used_in_body_are_not_reported() -> anyhow::Result<()> { + let source = dedent( + " + def f(subject): + match subject: + case [first, second] | {\"first\": first, \"second\": second} | (first, second): + print(first, second) + ", + ); + + assert!(collect_unused_names(&source)?.is_empty()); + Ok(()) + } + + #[test] + fn nested_or_pattern_capture_used_in_guard_is_not_reported() -> anyhow::Result<()> { + let source = dedent( + " + def f(subject): + match subject: + case [[value] | {\"item\": value}] if value: + pass + ", + ); + + assert!(collect_unused_names(&source)?.is_empty()); + Ok(()) + } + + #[test] + fn or_pattern_captures_do_not_hide_other_unused_bindings() -> anyhow::Result<()> { + let source = dedent( + " + def f(subject): + value = 0 + match subject: + case [value] | {\"used\": value}: + print(value) + case {\"unused\": value} | {\"also_unused\": value}: + pass + ", + ); + + assert_eq!( + collect_unused_names(&source)?, + vec!["value", "value", "value"] + ); + Ok(()) + } + #[test] fn skips_module_and_class_scope_bindings() -> anyhow::Result<()> { let source = dedent( " module_dead = 1 + [(module_walrus := item) for item in [1]] + [[(nested_module_walrus := item) for item in [1]] for _ in [1]] class C: class_dead = 1 @@ -332,6 +432,7 @@ mod tests { def mutate_global(): global global_value global_value = 1 + [(global_value := item) for item in [1]] local_dead = 1 def outer(): @@ -340,6 +441,7 @@ mod tests { def inner(): nonlocal captured captured = 1 + [(captured := item) for item in [1]] inner() return captured @@ -351,6 +453,44 @@ mod tests { Ok(()) } + #[test] + fn tracks_comprehension_walruses_in_local_scopes() -> anyhow::Result<()> { + let source = dedent( + " + def used(items): + [(used_walrus := item) for item in items] + return used_walrus + + def unused(items): + [(unused_walrus := item) for item in items] + + def nested_used(items): + [[(nested_used_walrus := item) for item in items] for _ in [1]] + return nested_used_walrus + + def nested_unused(items): + [[(nested_unused_walrus := item) for item in items] for _ in [1]] + + used_lambda = lambda items: ( + [(used_lambda_walrus := item) for item in items], + used_lambda_walrus, + ) + unused_lambda = lambda items: [(unused_lambda_walrus := item) for item in items] + ", + ); + + let names = collect_unused_names(&source)?; + assert_eq!( + names, + vec![ + "nested_unused_walrus", + "unused_lambda_walrus", + "unused_walrus", + ] + ); + Ok(()) + } + #[test] fn reports_unused_parameter_for_overriding_method() -> anyhow::Result<()> { let source = dedent( diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index f98ee2ce85..e3b8c6d2bd 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -43,6 +43,7 @@ //! of iterations, so if we fail to converge, Salsa will eventually panic. (This should of course //! be considered a bug.) +use crate::ProgramEnvironment; use itertools::Either; use ruff_db::parsed::parsed_module; use ruff_python_ast as ast; @@ -132,7 +133,7 @@ fn extend_collection_use_constraints<'db>( cycle_initial=|db, id, definition: Definition<'db>| { DefinitionInference::cycle_initial(db, definition, Type::divergent(id)) }, - cycle_fn=|db, cycle, previous: &DefinitionInference<'db>, inference: DefinitionInference<'db>, definition| { + cycle_fn=|db: &'db dyn Db, cycle, previous: &DefinitionInference<'db>, inference: DefinitionInference<'db>, definition: Definition<'db>| { inference.cycle_normalized(db, previous, cycle, definition) }, heap_size=ruff_memory_usage::heap_size @@ -141,19 +142,30 @@ pub(crate) fn infer_definition_types<'db>( db: &'db dyn Db, definition: Definition<'db>, ) -> DefinitionInference<'db> { - let file = definition.file(db); - let module = parsed_module(db, file).load(db); + let program_file = definition.program_file(db); + let python_file = program_file.python_file(db); + let module = parsed_module(db, python_file).load(db); let _span = tracing::trace_span!( "infer_definition_types", range = ?definition.kind(db).target_range(&module), - ?file + ?python_file ) .entered(); - let index = semantic_index(db, file); + let index = semantic_index(db, program_file); - TypeInferenceBuilder::new(db, InferenceRegion::Definition(definition), index, &module) - .finish_definition(definition) + let env = ProgramEnvironment::from_file(program_file); + + TypeInferenceBuilder::new( + db, + &env, + InferenceRegion::Definition(definition), + python_file.file(db), + program_file, + index, + &module, + ) + .finish_definition(definition) } /// Returns `true` if the definition refers to a dictionary-key binding that should be discarded. @@ -173,7 +185,8 @@ pub(crate) fn is_discarded_dict_key_assignment<'db>( return false; }; - infer_definition_types(db, dict_key_assignment.assignment()).discards_dict_key_assignments() + let assignment = dict_key_assignment.assignment(); + infer_definition_types(db, assignment).discards_dict_key_assignments() } /// Infer decorator expression types for a function definition. @@ -191,13 +204,19 @@ pub(crate) fn function_known_decorators<'db>( db: &'db dyn Db, definition: Definition<'db>, ) -> FunctionDecoratorInference<'db> { - let file = definition.file(db); - let module = parsed_module(db, file).load(db); - let index = semantic_index(db, file); + let program_file = definition.program_file(db); + let python_file = program_file.python_file(db); + let module = parsed_module(db, python_file).load(db); + let index = semantic_index(db, program_file); + + let env = ProgramEnvironment::from_file(program_file); TypeInferenceBuilder::new( db, + &env, InferenceRegion::FunctionDecorators(definition), + python_file.file(db), + program_file, index, &module, ) @@ -236,23 +255,21 @@ impl<'db> FunctionDecoratorInference<'db> { self.expression_types.get(&expression.into()).copied() } - pub(crate) fn expression_types( + fn expression_types( &self, ) -> impl ExactSizeIterator)> + '_ { self.expression_types.iter().copied() } - pub(crate) fn bindings( - &self, - ) -> impl ExactSizeIterator, Type<'db>)> + '_ { + fn bindings(&self) -> impl ExactSizeIterator, Type<'db>)> + '_ { self.bindings.iter().copied() } - pub(crate) fn called_functions(&self) -> &[FunctionType<'db>] { + fn called_functions(&self) -> &[FunctionType<'db>] { &self.called_functions } - pub(crate) fn known_decorators(&self) -> FunctionDecorators { + fn known_decorators(&self) -> FunctionDecorators { self.known_decorators } @@ -261,7 +278,7 @@ impl<'db> FunctionDecoratorInference<'db> { self.trailing_lambda_return } - pub(crate) fn diagnostics(&self) -> &TypeCheckDiagnostics { + fn diagnostics(&self) -> &TypeCheckDiagnostics { &self.diagnostics } } @@ -275,7 +292,7 @@ impl<'db> FunctionDecoratorInference<'db> { cycle_initial=|db, id, definition: Definition<'db>| { DefinitionInference::cycle_initial(db, definition, Type::divergent(id)) }, - cycle_fn=|db, cycle, previous: &DefinitionInference<'db>, inference: DefinitionInference<'db>, definition| { + cycle_fn=|db: &'db dyn Db, cycle, previous: &DefinitionInference<'db>, inference: DefinitionInference<'db>, definition: Definition<'db>| { inference.cycle_normalized(db, previous, cycle, definition) }, heap_size=ruff_memory_usage::heap_size @@ -284,20 +301,31 @@ pub(crate) fn infer_deferred_types<'db>( db: &'db dyn Db, definition: Definition<'db>, ) -> DefinitionInference<'db> { - let file = definition.file(db); - let module = parsed_module(db, file).load(db); + let program_file = definition.program_file(db); + let python_file = program_file.python_file(db); + let module = parsed_module(db, python_file).load(db); let _span = tracing::trace_span!( "infer_deferred_types", definition = ?definition.as_id(), range = ?definition.kind(db).target_range(&module), - ?file + ?python_file ) .entered(); - let index = semantic_index(db, file); + let index = semantic_index(db, program_file); - TypeInferenceBuilder::new(db, InferenceRegion::Deferred(definition), index, &module) - .finish_definition(definition) + let env = ProgramEnvironment::from_file(program_file); + + TypeInferenceBuilder::new( + db, + &env, + InferenceRegion::Deferred(definition), + python_file.file(db), + program_file, + index, + &module, + ) + .finish_definition(definition) } /// Infer all types for a [`ScopeId`], including all definitions and expressions in that scope. @@ -313,13 +341,13 @@ pub(crate) fn infer_complete_scope_types<'db>( // Scopes that may require type context are inferred during the inference of // their outer scope. if scope.accepts_type_context(db) { - let file = scope.file(db); - let index = semantic_index(db, file); + let program_file = scope.program_file(db); + let index = semantic_index(db, program_file); if let Some(parent_scope) = index.parent_scope_id(scope.file_scope_id(db)) { // Note that nested lambdas or comprehensions may require recursing until we reach // an outer scope that is independent of any type context. - return infer_complete_scope_types(db, parent_scope.to_scope_id(db, file)); + return infer_complete_scope_types(db, parent_scope.to_scope_id(db, program_file)); } } @@ -345,8 +373,10 @@ pub(crate) fn infer_scope_types<'db>( #[salsa::tracked( returns(ref), cycle_initial=|_, id, _| ScopeInference::cycle_initial(Type::divergent(id)), - cycle_fn=|db, cycle, previous: &ScopeInference<'db>, inference: ScopeInference<'db>, _| { - inference.cycle_normalized(db, previous, cycle) + cycle_fn=|db, cycle, previous: &ScopeInference<'db>, inference: ScopeInference<'db>, input: InferScope<'db>| { + let (scope, _) = input.into_inner(db); + let env = ProgramEnvironment::from_scope(scope); + inference.cycle_normalized(db, &env, previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] @@ -355,16 +385,29 @@ pub(crate) fn infer_scope_types_impl<'db>( input: InferScope<'db>, ) -> ScopeInference<'db> { let (scope, tcx) = input.into_inner(db); - let file = scope.file(db); - let _span = tracing::trace_span!("infer_scope_types", scope=?scope.as_id(), ?file).entered(); + let program_file = scope.program_file(db); + let python_file = program_file.python_file(db); + let _span = + tracing::trace_span!("infer_scope_types", scope=?scope.as_id(), ?python_file).entered(); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, python_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); + let index = semantic_index(db, program_file); + + let env = ProgramEnvironment::from_file(program_file); - TypeInferenceBuilder::new(db, InferenceRegion::Scope(scope, tcx), index, &module).finish_scope() + TypeInferenceBuilder::new( + db, + &env, + InferenceRegion::Scope(scope, tcx), + python_file.file(db), + program_file, + index, + &module, + ) + .finish_scope() } /// Infer all types for an [`Expression`] (including sub-expressions). @@ -382,8 +425,10 @@ pub(crate) fn infer_expression_types<'db>( #[salsa::tracked( returns(ref), cycle_initial=expression_cycle_initial, - cycle_fn=|db, cycle, previous: &ExpressionInference<'db>, inference: ExpressionInference<'db>, _| { - inference.cycle_normalized(db, previous, cycle) + cycle_fn=|db, cycle, previous: &ExpressionInference<'db>, inference: ExpressionInference<'db>, input: InferExpression<'db>| { + let (expression, _) = input.into_inner(db); + let env = ProgramEnvironment::from_scope(expression.scope(db)); + inference.cycle_normalized(db, &env, previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] @@ -393,21 +438,27 @@ pub(super) fn infer_expression_types_impl<'db>( ) -> ExpressionInference<'db> { let (expression, tcx) = input.into_inner(db); - let file = expression.file(db); - let module = parsed_module(db, file).load(db); + let program_file = expression.program_file(db); + let python_file = program_file.python_file(db); + let module = parsed_module(db, python_file).load(db); let _span = tracing::trace_span!( "infer_expression_types", expression = ?expression.as_id(), range = ?expression.node_ref(db).node(&module).range(), - ?file + ?python_file ) .entered(); - let index = semantic_index(db, file); + let index = semantic_index(db, program_file); + + let env = ProgramEnvironment::from_file(program_file); TypeInferenceBuilder::new( db, + &env, InferenceRegion::Expression(expression, tcx), + python_file.file(db), + program_file, index, &module, ) @@ -456,8 +507,10 @@ pub(crate) fn infer_expression_type<'db>( #[salsa::tracked( returns(copy), cycle_initial=|_, id, _| Type::divergent(id), - cycle_fn=|db, cycle, previous: &Type<'db>, result: Type<'db>, _| { - result.cycle_normalized(db, *previous, cycle) + cycle_fn=|db, cycle, previous: &Type<'db>, result: Type<'db>, input: InferExpression<'db>| { + let (expression, _) = input.into_inner(db); + let env = ProgramEnvironment::from_scope(expression.scope(db)); + result.cycle_normalized(db, &env, *previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] @@ -496,8 +549,9 @@ pub(super) fn infer_statement_types<'db>( cycle_initial=|db, id, statement: StatementInner<'db>| { StatementInferenceInner::cycle_initial(statement.scope(db), Type::divergent(id)) }, - cycle_fn=|db, cycle, previous: &StatementInferenceInner<'db>, inference: StatementInferenceInner<'db>, _| { - inference.cycle_normalized(db, previous, cycle) + cycle_fn=|db, cycle, previous: &StatementInferenceInner<'db>, inference: StatementInferenceInner<'db>, statement: StatementInner<'db>| { + let env = ProgramEnvironment::from_file(statement.program_file(db)); + inference.cycle_normalized(db, &env, previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] @@ -505,20 +559,31 @@ fn infer_statement_types_impl<'db>( db: &'db dyn Db, statement: StatementInner<'db>, ) -> StatementInferenceInner<'db> { - let file = statement.file(db); - let module = parsed_module(db, file).load(db); + let program_file = statement.program_file(db); + let python_file = program_file.python_file(db); + let module = parsed_module(db, python_file).load(db); let _span = tracing::trace_span!( "infer_statement_types", statement = ?statement.as_id(), range = ?statement.node_ref(db).node(&module).range(), - ?file + ?python_file ) .entered(); - let index = semantic_index(db, file); + let index = semantic_index(db, program_file); - TypeInferenceBuilder::new(db, InferenceRegion::Statement(statement), index, &module) - .finish_statement() + let env = ProgramEnvironment::from_file(program_file); + + TypeInferenceBuilder::new( + db, + &env, + InferenceRegion::Statement(statement), + python_file.file(db), + program_file, + index, + &module, + ) + .finish_statement() } /// An `Expression` with an optional `TypeContext`. @@ -540,7 +605,7 @@ pub(super) struct ExpressionWithContext<'db> { } impl<'db> InferExpression<'db> { - pub(super) fn new( + fn new( db: &'db dyn Db, expression: Expression<'db>, tcx: TypeContext<'db>, @@ -579,11 +644,7 @@ pub(super) struct ScopeWithContext<'db> { } impl<'db> InferScope<'db> { - pub(super) fn new( - db: &'db dyn Db, - scope: ScopeId<'db>, - tcx: TypeContext<'db>, - ) -> InferScope<'db> { + fn new(db: &'db dyn Db, scope: ScopeId<'db>, tcx: TypeContext<'db>) -> InferScope<'db> { if tcx.annotation().is_some() { InferScope::WithContext(ScopeWithContext::new(db, scope, tcx)) } else { @@ -680,13 +741,14 @@ impl<'db> TypeContext<'db> { fn known_specialization( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, known_class: KnownClass, ) -> Option> { self.annotation() - .and_then(|ty| ty.known_specialization(db, known_class)) + .and_then(|ty| ty.known_specialization(db, env, known_class)) } - pub(crate) fn map(self, f: impl FnOnce(Type<'db>) -> Type<'db>) -> Self { + fn map(self, f: impl FnOnce(Type<'db>) -> Type<'db>) -> Self { Self { target: self.target.map(f), preserve_literals: self.preserve_literals, @@ -695,17 +757,21 @@ impl<'db> TypeContext<'db> { } } - pub(crate) fn is_typealias(&self) -> bool { + fn is_typealias(&self) -> bool { self.annotation() .is_some_and(|ty| ty.is_typealias_special_form()) } /// If the type annotation is a union, returns the target elements that it can be narrowed to. - pub(crate) fn narrow_targets(&self, db: &'db dyn Db) -> Option]>> { + fn narrow_targets( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option]>> { let union = self.annotation()?.as_union_like(db)?; let targets = if union.has_aliases(db) { - let expanded = union.expand_aliases(db); + let expanded = union.expand_aliases(db, env); if let Some(union) = expanded.as_union_like(db) { Cow::Borrowed(union.elements(db)) } else { @@ -737,18 +803,25 @@ impl<'db> From> for TypeContext<'db> { #[salsa::tracked( returns(ref), cycle_initial=|_, id, _| UnpackResult::cycle_initial(Type::divergent(id)), - cycle_fn=|db, cycle, previous: &UnpackResult<'db>, result: UnpackResult<'db>, _| { - result.cycle_normalized(db, previous, cycle) + cycle_fn=|db, cycle, previous: &UnpackResult<'db>, result: UnpackResult<'db>, unpack: Unpack<'db>| { + let env = ProgramEnvironment::from_file(unpack.program_file(db)); + result.cycle_normalized(db, &env, previous, cycle) }, heap_size=ruff_memory_usage::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 program_file = unpack.program_file(db); + let python_file = program_file.python_file(db); + let module = parsed_module(db, python_file).load(db); + let _span = tracing::trace_span!( + "infer_unpack_types", + range=?unpack.range(db, &module), + ?python_file + ) + .entered(); - let mut unpacker = Unpacker::new(db, unpack.target_scope(db), &module); + let env = ProgramEnvironment::from_file(program_file); + let mut unpacker = Unpacker::new(db, &env, unpack.target_scope(db), program_file, &module); unpacker.unpack(unpack.target(db, &module), unpack.value(db)); unpacker.finish() } @@ -936,11 +1009,12 @@ impl<'db> ScopeInference<'db> { fn cycle_normalized( mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, previous_inference: &ScopeInference<'db>, cycle: &salsa::Cycle, ) -> ScopeInference<'db> { self.expressions.map_values(|expr, ty| { - ty.cycle_normalized(db, previous_inference.expression_type(expr), cycle) + ty.cycle_normalized(db, env, previous_inference.expression_type(expr), cycle) }); if cycle.iteration() > crate::TAINTED_CYCLES @@ -1124,6 +1198,7 @@ impl<'db> DefinitionTypes<'db> { fn normalize_binding( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, previous: &DefinitionTypes<'db>, cycle: &salsa::Cycle, owner: Definition<'db>, @@ -1131,14 +1206,15 @@ impl<'db> DefinitionTypes<'db> { ty: Type<'db>, ) -> Type<'db> { if let Some(previous_ty) = previous.binding_type(owner, definition) { - ty.cycle_normalized(db, previous_ty, cycle) + ty.cycle_normalized(db, env, previous_ty, cycle) } else { - ty.recursive_type_normalized(db, cycle) + ty.recursive_type_normalized(db, env, cycle) } } fn normalize_declaration( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, previous: &DefinitionTypes<'db>, cycle: &salsa::Cycle, owner: Definition<'db>, @@ -1146,15 +1222,16 @@ impl<'db> DefinitionTypes<'db> { ty: TypeAndQualifiers<'db>, ) -> TypeAndQualifiers<'db> { if let Some(previous_ty) = previous.declaration_type(owner, definition) { - ty.map_type(|inner| inner.cycle_normalized(db, previous_ty.inner_type(), cycle)) + ty.map_type(|inner| inner.cycle_normalized(db, env, previous_ty.inner_type(), cycle)) } else { - ty.map_type(|inner| inner.recursive_type_normalized(db, cycle)) + ty.map_type(|inner| inner.recursive_type_normalized(db, env, cycle)) } } fn cycle_normalized( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, previous: &DefinitionTypes<'db>, cycle: &salsa::Cycle, owner: Definition<'db>, @@ -1162,22 +1239,30 @@ impl<'db> DefinitionTypes<'db> { match self { Self::Empty => Self::Empty, Self::Binding(ty) => Self::Binding(Self::normalize_binding( - db, previous, cycle, owner, owner, ty, + db, env, previous, cycle, owner, owner, ty, )), Self::Declaration(ty) => Self::Declaration(Self::normalize_declaration( - db, previous, cycle, owner, owner, ty, + db, env, previous, cycle, owner, owner, ty, )), Self::BindingAndDeclaration(declaration_ty) => { let binding_ty = Self::normalize_binding( db, + env, previous, cycle, owner, owner, declaration_ty.inner_type(), ); - let declaration_ty = - Self::normalize_declaration(db, previous, cycle, owner, owner, declaration_ty); + let declaration_ty = Self::normalize_declaration( + db, + env, + previous, + cycle, + owner, + owner, + declaration_ty, + ); if binding_ty == declaration_ty.inner_type() { Self::BindingAndDeclaration(declaration_ty) @@ -1190,10 +1275,19 @@ impl<'db> DefinitionTypes<'db> { } Self::Other(mut other) => { for (definition, ty) in &mut other.bindings { - *ty = Self::normalize_binding(db, previous, cycle, owner, *definition, *ty); + *ty = + Self::normalize_binding(db, env, previous, cycle, owner, *definition, *ty); } for (definition, ty) in &mut other.declarations { - *ty = Self::normalize_declaration(db, previous, cycle, owner, *definition, *ty); + *ty = Self::normalize_declaration( + db, + env, + previous, + cycle, + owner, + *definition, + *ty, + ); } match (&*other.bindings, &*other.declarations) { @@ -1420,12 +1514,15 @@ impl<'db> DefinitionInference<'db> { definition: Definition<'db>, cycle_recovery: Type<'db>, ) -> Self { + let env = ProgramEnvironment::from_definition(definition); let mut types = DefinitionTypes::Empty; // Eagerly store more precise types for collection literals to avoid an extra // cycle iteration, i.e., by inferring `list[Divergent]` instead of `Divergent`. if let DefinitionKind::Assignment(assignment) = definition.kind(db) { - let module = parsed_module(db, definition.file(db)).load(db); + let program_file = definition.program_file(db); + let python_file = program_file.python_file(db); + let module = parsed_module(db, python_file).load(db); let known_collection = match assignment.value(&module) { ast::Expr::Set(_) => Some(KnownClass::Set), ast::Expr::List(_) => Some(KnownClass::List), @@ -1433,15 +1530,49 @@ impl<'db> DefinitionInference<'db> { _ => None, }; - if let Some(collection_class) = known_collection - .and_then(|known_collection| known_collection.try_to_class_literal(db)) - { - let divergent_collection = collection_class - .apply_specialization(db, |generic_context| { - generic_context.repeat_specialization(db, cycle_recovery) - }); + if let Some(known_collection) = known_collection { + if let Some(collection_class) = known_collection.try_to_class_literal(db, &env) { + let divergent_collection = collection_class + .apply_specialization(db, |generic_context| { + generic_context.repeat_specialization(db, cycle_recovery) + }); - types = DefinitionTypes::Binding(Type::instance(db, divergent_collection)); + types = + DefinitionTypes::Binding(Type::instance(db, &env, divergent_collection)); + } + } + } else if let DefinitionKind::AnnotatedAssignment(assignment) = definition.kind(db) { + let program_file = definition.program_file(db); + let python_file = program_file.python_file(db); + let module = parsed_module(db, python_file).load(db); + let index = semantic_index(db, program_file); + + if assignment.value(&module).is_none() + && index + .use_def_map(definition.file_scope(db)) + .bindings_at_definition(definition) + .any(|binding| { + binding + .binding + .is_defined_and(|binding| binding.kind(db).is_loop_header()) + }) + { + // Loop-carried assignments need this annotation as context before validating + // the declaration can infer their binding types. + return TypeInferenceBuilder::new( + db, + &env, + InferenceRegion::Definition(definition), + python_file.file(db), + program_file, + index, + &module, + ) + .infer_annotated_assignment_cycle_initial( + definition, + assignment, + cycle_recovery, + ); } } @@ -1466,12 +1597,14 @@ impl<'db> DefinitionInference<'db> { cycle: &salsa::Cycle, definition: Definition<'db>, ) -> DefinitionInference<'db> { + let env = ProgramEnvironment::from_definition(definition); for (expr, ty) in &mut self.expressions { let previous_ty = previous_inference.expression_type(*expr); - *ty = ty.cycle_normalized(db, previous_ty, cycle); + *ty = ty.cycle_normalized(db, &env, previous_ty, cycle); } self.types = std::mem::take(&mut self.types).cycle_normalized( db, + &env, &previous_inference.types, cycle, definition, @@ -1503,18 +1636,19 @@ impl<'db> DefinitionInference<'db> { if let Some(fluid_creation) = &mut extra.fluid_creation // Only normalize a creation type that actually contains divergent parts: // normalization is lossy (e.g. it drops materializations). - && any_over_type(db, *fluid_creation, false, |ty| ty.is_divergent()) + && any_over_type(db, &env, *fluid_creation, false, |ty| ty.is_divergent()) { *fluid_creation = match previous_inference.fluid_creation() { Some(previous_creation) => { - fluid_creation.cycle_normalized(db, previous_creation, cycle) + fluid_creation.cycle_normalized(db, &env, previous_creation, cycle) } - None => fluid_creation.recursive_type_normalized(db, cycle), + None => fluid_creation.recursive_type_normalized(db, &env, cycle), }; } if let Some(fluid_timeline) = extra.fluid_timeline.take() { extra.fluid_timeline = Some(fluid_timeline.cycle_normalized( db, + &env, previous_inference.fluid_timeline(), cycle, )); @@ -1539,7 +1673,7 @@ impl<'db> DefinitionInference<'db> { .or_else(|| self.fallback_type()) } - pub(crate) fn collection_use_constraints( + fn collection_use_constraints( &self, collection_def: Definition<'db>, ) -> Option<&FxIndexSet>> { @@ -1655,14 +1789,14 @@ impl<'db> DefinitionInference<'db> { self.types.declaration_types() } - pub(crate) fn fallback_type(&self) -> Option> { + fn fallback_type(&self) -> Option> { match self.extra.as_deref() { Some(DefinitionInferenceExtra::Other(extra)) => extra.cycle_recovery, Some(_) | None => None, } } - pub(crate) fn discards_dict_key_assignments(&self) -> bool { + fn discards_dict_key_assignments(&self) -> bool { match self.extra.as_deref() { Some(DefinitionInferenceExtra::DiscardsDictKeyAssignments) => true, Some(DefinitionInferenceExtra::Other(extra)) => extra.discards_dict_key_assignments, @@ -1773,6 +1907,7 @@ impl<'db> ExpressionInference<'db> { fn cycle_normalized( mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, previous: &ExpressionInference<'db>, cycle: &salsa::Cycle, ) -> ExpressionInference<'db> { @@ -1784,31 +1919,35 @@ impl<'db> ExpressionInference<'db> { .iter() .find(|(previous_binding, _)| previous_binding == binding) }) { - *binding_ty = binding_ty.cycle_normalized(db, *previous_binding, cycle); + *binding_ty = binding_ty.cycle_normalized(db, env, *previous_binding, cycle); } else { - *binding_ty = binding_ty.recursive_type_normalized(db, cycle); + *binding_ty = binding_ty.recursive_type_normalized(db, env, cycle); } } if let Some(fluid_creation) = &mut extra.fluid_creation - && any_over_type(db, *fluid_creation, false, |ty| ty.is_divergent()) + && any_over_type(db, env, *fluid_creation, false, |ty| ty.is_divergent()) { *fluid_creation = match previous.fluid_creation() { Some(previous_creation) => { - fluid_creation.cycle_normalized(db, previous_creation, cycle) + fluid_creation.cycle_normalized(db, env, previous_creation, cycle) } - None => fluid_creation.recursive_type_normalized(db, cycle), + None => fluid_creation.recursive_type_normalized(db, env, cycle), }; } if let Some(fluid_timeline) = extra.fluid_timeline.take() { - extra.fluid_timeline = - Some(fluid_timeline.cycle_normalized(db, previous.fluid_timeline(), cycle)); + extra.fluid_timeline = Some(fluid_timeline.cycle_normalized( + db, + env, + previous.fluid_timeline(), + cycle, + )); } } for (expr, ty) in &mut self.expressions { let previous_ty = previous.expression_type(*expr); - *ty = ty.cycle_normalized(db, previous_ty, cycle); + *ty = ty.cycle_normalized(db, env, previous_ty, cycle); } if cycle.iteration() > crate::TAINTED_CYCLES @@ -1851,7 +1990,7 @@ impl<'db> ExpressionInference<'db> { .is_some_and(|extra| extra.unsolved_typevar_calls.contains(&expression.into())) } - pub(crate) fn collection_use_constraints( + fn collection_use_constraints( &self, collection_def: Definition<'db>, ) -> Option<&FxIndexSet>> { @@ -1900,7 +2039,7 @@ pub(crate) enum StatementInference<'db> { } impl<'db> StatementInference<'db> { - pub(crate) fn expression_type(&self, expression: impl Into) -> Type<'db> { + fn expression_type(&self, expression: impl Into) -> Type<'db> { match self { StatementInference::Expression(inference) => inference.expression_type(expression), StatementInference::Definition(_, inference) => inference.expression_type(expression), @@ -1908,7 +2047,7 @@ impl<'db> StatementInference<'db> { } } - pub(crate) fn collection_use_constraints( + fn collection_use_constraints( &self, collection_def: Definition<'db>, ) -> Option<&FxIndexSet>> { @@ -2015,12 +2154,13 @@ impl<'db> StatementInferenceInner<'db> { fn cycle_normalized( mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, previous_inference: &StatementInferenceInner<'db>, cycle: &salsa::Cycle, ) -> StatementInferenceInner<'db> { for (expr, ty) in &mut self.expressions { let previous_ty = previous_inference.expression_type(*expr); - *ty = ty.cycle_normalized(db, previous_ty, cycle); + *ty = ty.cycle_normalized(db, env, previous_ty, cycle); } for (binding, binding_ty) in &mut self.bindings { if let Some((_, previous_binding)) = previous_inference @@ -2028,9 +2168,9 @@ impl<'db> StatementInferenceInner<'db> { .iter() .find(|(previous_binding, _)| previous_binding == binding) { - *binding_ty = binding_ty.cycle_normalized(db, *previous_binding, cycle); + *binding_ty = binding_ty.cycle_normalized(db, env, *previous_binding, cycle); } else { - *binding_ty = binding_ty.recursive_type_normalized(db, cycle); + *binding_ty = binding_ty.recursive_type_normalized(db, env, cycle); } } for (declaration, declaration_ty) in &mut self.declarations { @@ -2040,11 +2180,11 @@ impl<'db> StatementInferenceInner<'db> { .find(|(previous_declaration, _)| previous_declaration == declaration) { *declaration_ty = declaration_ty.map_type(|decl_ty| { - decl_ty.cycle_normalized(db, previous_declaration.inner_type(), cycle) + decl_ty.cycle_normalized(db, env, previous_declaration.inner_type(), cycle) }); } else { - *declaration_ty = - declaration_ty.map_type(|decl_ty| decl_ty.recursive_type_normalized(db, cycle)); + *declaration_ty = declaration_ty + .map_type(|decl_ty| decl_ty.recursive_type_normalized(db, env, cycle)); } } @@ -2062,22 +2202,19 @@ impl<'db> StatementInferenceInner<'db> { self } - pub(crate) fn expression_type(&self, expression: impl Into) -> Type<'db> { + fn expression_type(&self, expression: impl Into) -> Type<'db> { self.try_expression_type(expression) .unwrap_or_else(Type::unknown) } - pub(crate) fn try_expression_type( - &self, - expression: impl Into, - ) -> Option> { + fn try_expression_type(&self, expression: impl Into) -> Option> { self.expressions .get(&expression.into()) .copied() .or_else(|| self.fallback_type()) } - pub(crate) fn collection_use_constraints( + fn collection_use_constraints( &self, collection_def: Definition<'db>, ) -> Option<&FxIndexSet>> { @@ -2105,7 +2242,7 @@ impl<'db> StatementInferenceInner<'db> { self.declarations.iter().copied() } - pub(crate) fn fallback_type(&self) -> Option> { + fn fallback_type(&self) -> Option> { self.extra.as_ref().and_then(|extra| extra.cycle_recovery) } } @@ -2197,6 +2334,9 @@ bitflags::bitflags! { /// This is the one position besides a `**kwargs` annotation where a `**` type expression /// means something: it bounds the pack as a whole rather than field by field. const IN_PACK_BOUND = 1 << 18; + + /// Whether the current method's explicit receiver annotation is incompatible with `Self`. + const HAS_INCOMPATIBLE_SELF_RECEIVER = 1 << 15; } } diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 15f659c9a5..98eea96fee 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -4,6 +4,7 @@ use std::rc::Rc; use compact_str::CompactString; use itertools::Itertools; +use ruff_db::diagnostic::Span; use ruff_db::files::File; use ruff_db::parsed::ParsedModuleRef; use ruff_db::source::source_text; @@ -21,7 +22,7 @@ use ruff_text_size::{Ranged, TextRange}; use rustc_hash::{FxHashMap, FxHashSet}; use smallvec::SmallVec; use strum::IntoEnumIterator; -use ty_module_resolver::{KnownModule, ModuleName, file_to_module, resolve_module}; +use ty_module_resolver::{ImportingFile, KnownModule, ModuleName, file_to_module, resolve_module}; use ty_python_core::ast_ids::HasScopedUseId; use ty_python_core::statement::StatementInner; @@ -36,12 +37,16 @@ use super::{ use crate::diagnostic::format_enumeration; use crate::place::{ ConsideredDefinitions, DefinedPlace, Definedness, LookupError, Place, PlaceAndQualifiers, - RequiresExplicitReExport, TypeOrigin, builtins_module_scope, builtins_symbol, - class_body_implicit_symbol, explicit_global_symbol, is_basedpython_implicit_typing_name, + RequiresExplicitReExport, TypeOrigin, builtins_module_scope, class_body_implicit_symbol, + explicit_global_symbol, implicit_builtins_symbol, is_basedpython_implicit_typing_name, known_module_symbol, loop_header_reachability, module_type_implicit_global_declaration, module_type_implicit_global_symbol, place_by_id, place_from_bindings_with_reachability_cache, place_from_declarations_with_reachability_cache, typing_extensions_symbol, typing_symbol, }; +use crate::place_load::{ + ImplicitPlaceLoad, PlaceExprPrefixLoad, PlaceExprPrefixLoads, PlaceLoadFailure, PlaceLoadMode, + PlaceLoadResolutionStep, PlaceLoadSource, PlaceLoadSourceKind, resolve_place_load, +}; use crate::reachability::{ ReachabilityEvaluationCache, analyze_pattern_predicate, evaluate_reachability, evaluate_reachability_with_cache, is_reachable, @@ -56,8 +61,8 @@ use crate::types::call::{Argument, Binding, Bindings, CallArguments, CallError, use crate::types::callable::{CallableFunctionProvenance, CallableTypeKind}; use crate::types::class::{ ClassLiteral, CodeGeneratorKind, DynamicNamedTupleAnchor, DynamicNamedTupleLiteral, - DynamicTypedDictAnchor, DynamicTypedDictLiteral, MethodDecorator, NamedTupleField, - NamedTupleSpec, StaticClassLiteral, + DynamicTypedDictAnchor, DynamicTypedDictLiteral, FrozenDataclassDispatch, MethodDecorator, + NamedTupleField, NamedTupleSpec, StaticClassLiteral, }; use crate::types::constraints::{ConstraintSetBuilder, PathBounds, Solutions}; use crate::types::context::InferContext; @@ -71,17 +76,18 @@ use crate::types::diagnostic::{ INVALID_DECLARATION, INVALID_ENUM_MEMBER_ANNOTATION, INVALID_FIELD_LOOKUP, INVALID_LEGACY_TYPE_VARIABLE, INVALID_NEWTYPE, INVALID_PARAMSPEC, INVALID_REGEX, INVALID_REIFIED_TYPE_PARAM, INVALID_TYPE_ALIAS_TYPE, INVALID_TYPE_FORM, - INVALID_TYPE_VARIABLE_CONSTRAINTS, INVALID_VARIANCE_DECLARATION, NARROWING_GUARD_AS_VALUE, - NON_EXHAUSTIVE_STATEMENT_EXPRESSION, NON_OVERLAPPING_CAST, NON_OVERLAPPING_TYPE_TEST, - OPTIONAL_OBJECT_CONVERSION, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_SUBMODULE, - REFUTABLE_DESTRUCTURING, TypeCheckDiagnostics, UNANNOTATED_MODEL_FIELD, - UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, - UNRESOLVED_GLOBAL, UNRESOLVED_REFERENCE, UNSPECIALIZED_REIFIED_GENERIC, UNSUPPORTED_OPERATOR, - UNUSED_AWAITABLE, hint_if_stdlib_attribute_exists_on_other_versions, - report_attempted_protocol_instantiation, report_bad_dunder_delattr_call, - report_bad_dunder_delete_call, report_bool_as_int, report_bool_as_int_assignment, - report_call_to_abstract_method, report_cannot_pop_required_field_on_typed_dict, - report_invalid_assignment, report_invalid_class_match_pattern, report_invalid_exception_caught, + INVALID_TYPE_VARIABLE_CONSTRAINTS, INVALID_TYPE_VARIABLE_DEFAULT, INVALID_VARIANCE_DECLARATION, + NARROWING_GUARD_AS_VALUE, NON_EXHAUSTIVE_STATEMENT_EXPRESSION, NON_OVERLAPPING_CAST, + NON_OVERLAPPING_TYPE_TEST, OPTIONAL_OBJECT_CONVERSION, POSSIBLY_MISSING_IMPLICIT_CALL, + POSSIBLY_MISSING_SUBMODULE, REFUTABLE_DESTRUCTURING, TypeCheckDiagnostics, + UNANNOTATED_MODEL_FIELD, UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS, UNDEFINED_REVEAL, + UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_REFERENCE, UNSOUND_YIELD, + UNSPECIALIZED_REIFIED_GENERIC, UNSUPPORTED_OPERATOR, UNUSED_AWAITABLE, YieldKind, + hint_if_stdlib_attribute_exists_on_other_versions, report_attempted_protocol_instantiation, + report_bad_dunder_delattr_call, report_bad_dunder_delete_call, report_bool_as_int, + report_bool_as_int_assignment, report_call_to_abstract_method, + report_cannot_pop_required_field_on_typed_dict, report_invalid_assignment, + report_invalid_class_match_pattern, report_invalid_exception_caught, report_invalid_exception_cause, report_invalid_exception_raised, report_invalid_exception_tuple_caught, report_invalid_generator_yield_type, report_invalid_key_on_typed_dict, report_invalid_match_args_type, @@ -89,8 +95,8 @@ use crate::types::diagnostic::{ report_match_pattern_against_non_runtime_checkable_protocol, report_match_pattern_against_typed_dict, report_mismatched_type_name, report_possibly_missing_attribute, report_possibly_unresolved_reference, - report_too_many_positional_patterns_for_class_pattern, report_unsupported_augmented_assignment, - report_unsupported_comparison, + report_too_many_positional_patterns_for_class_pattern, report_unsound_yield, + report_unsupported_augmented_assignment, report_unsupported_comparison, }; use crate::types::enums::{enum_ignored_names, is_enum_class_by_inheritance}; use crate::types::extensions; @@ -100,12 +106,10 @@ use crate::types::function::{ same_module_uncached_raw_signature, }; use crate::types::generics::{ - GenericContext, InferableTypeVars, Specialization, SpecializationBuilder, bind_typevar, - enclosing_binding_contexts, + GenericContext, Specialization, SpecializationBuilder, bind_typevar, enclosing_binding_contexts, }; use crate::types::infer::builder::named_tuple::NamedTupleKind; use crate::types::infer::builder::paramspec_validation::validate_paramspec_components; -use crate::types::infer::builder::typed_dict::TypedDictConstructorForm; use crate::types::infer::{ StatementInference, StatementInferenceInner, StatementInferenceInnerExtra, TypeAndRange, TypeExpressionFlags, infer_statement_types, nearest_enclosing_class, @@ -132,28 +136,31 @@ use crate::types::tuple::promotion::TupleSizePromotionConstraints; use crate::types::tuple::{Tuple, TupleLength, TupleSpecBuilder, TupleType, VariableSegment}; use crate::types::type_alias::{ManualPEP695TypeAliasType, PEP695TypeAliasType}; use crate::types::typed_dict::{TypedDictAssignmentKind, TypedDictKeyAssignment}; -use crate::types::typevar::{BoundTypeVarIdentity, TypeVarConstraints, TypeVarIdentity}; +use crate::types::typevar::{ + BoundTypeVarIdentity, TypeVarConstraints, TypeVarIdentity, TypeVarInstance, TypeVarSet, +}; use crate::types::unpacker::UnpackResult; use crate::types::{ - BoundTypeVarInstance, CallDunderError, CallableBinding, CallableType, CallableTypes, ClassType, - DeferredOperation, DeferredType, DynamicType, InferenceFlags, InstanceProjection, - InternedConstraintSet, InternedType, IntersectionBuilder, IntersectionType, KnownClass, - KnownInstanceType, KnownUnion, LiteralValueType, LiteralValueTypeKind, MemberLookupPolicy, - ParamSpecAttrKind, Parameter, Parameters, RestrictedType, SentinelInstance, Signature, - SpecialFormType, SubclassOfType, Type, TypeAliasType, TypeAndQualifiers, TypeContext, - TypeQualifiers, TypeVarBoundOrConstraints, TypeVarKind, TypeVarVariance, TypedDictModule, - TypedDictType, UnionAccumulator, UnionBuilder, UnionType, any_over_type, binding_type, - extract_fixed_length_iterable_element_types, infer_complete_scope_types, infer_scope_types, - is_discarded_dict_key_assignment, report_iteration_over_character, todo_type, + BindingContext, BoundTypeVarInstance, CallDunderError, CallableBinding, CallableType, + CallableTypes, ClassType, DeferredOperation, DeferredType, DynamicType, InferenceFlags, + InstanceProjection, InternedConstraintSet, InternedType, IntersectionBuilder, IntersectionType, + KnownClass, KnownInstanceType, KnownUnion, LiteralValueType, LiteralValueTypeKind, + MemberLookupPolicy, ParamSpecAttrKind, Parameter, Parameters, ProgramEnvironment, + RestrictedType, SentinelInstance, Signature, SpecialFormType, SubclassOfType, Type, + TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers, TypeVarBoundOrConstraints, + TypeVarKind, TypeVarVariance, TypedDictModule, TypedDictType, UnionAccumulator, UnionBuilder, + UnionType, any_over_type, binding_type, extract_fixed_length_iterable_element_types, + infer_complete_scope_types, infer_scope_types, is_discarded_dict_key_assignment, + report_iteration_over_character, todo_type, }; -use crate::{AnalysisSettings, Db, FxIndexSet, Program}; +use crate::{AnalysisSettings, Db, FxIndexSet, FxOrderSet}; use fluid::FluidTimeline; -use ty_python_core::ast_ids::ScopedUseId; use ty_python_core::definition::{ AnnotatedAssignmentDefinitionKind, AssignmentDefinitionKind, ComprehensionDefinitionKind, Definition, DefinitionKind, DefinitionNodeKey, DefinitionState, ExceptHandlerDefinitionKind, ForStmtDefinitionKind, LambdaParameterDefinitionNodeKind, LoopHeaderDefinitionKind, - NestedBindingsDefinitionKind, ParameterDefinitionNodeKind, TargetKind, WithItemDefinitionKind, + NestedBindingExecution, NestedBindingsDefinitionKind, ParameterDefinitionNodeKind, TargetKind, + WithItemDefinitionKind, }; use ty_python_core::expression::{Expression, ExpressionKind}; use ty_python_core::narrowing_constraints::ConstraintKey; @@ -161,9 +168,9 @@ use ty_python_core::node_key::NodeKey; use ty_python_core::place::{PlaceExpr, PlaceExprRef}; use ty_python_core::predicate::PatternPredicate; use ty_python_core::scope::{FileScopeId, NodeWithScopeKind, NodeWithScopeRef, ScopeId, ScopeKind}; -use ty_python_core::symbol::{ScopedSymbolId, Symbol}; +use ty_python_core::symbol::ScopedSymbolId; use ty_python_core::{ - ApplicableConstraints, EnclosingSnapshotResult, EvaluationMode, SemanticIndex, Truthiness, + ApplicableConstraints, EvaluationMode, ProgramFile, SemanticIndex, Truthiness, unpack::UnpackPosition, }; use ty_python_core::{ExpressionNodeKey, Statement}; @@ -195,7 +202,7 @@ mod typed_dict; mod typeguard; mod typevar; -use super::comparisons::{self, BinaryComparisonVisitor}; +use super::comparisons; /// A helper to track if we already know that declared and inferred types are the same. #[derive(Debug, Clone, PartialEq, Eq)] @@ -247,20 +254,24 @@ fn is_optional_value<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { /// Whether `ty` is the top type `object` (or `object | None`, the `object?` /// surface form) — a target that absorbs an optional's `None` arm without /// preserving it. Only such a target makes the widening both silent and lossy. -fn target_swallows_optional<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { +fn target_swallows_optional<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> bool { match ty { Type::NominalInstance(instance) => instance - .class(db) + .class(db, env) .class_literal(db) .is_known(db, KnownClass::Object), Type::Union(union) => { let elements = union.elements(db); elements .iter() - .any(|element| target_swallows_optional(db, *element)) - && elements - .iter() - .all(|element| element.is_none(db) || target_swallows_optional(db, *element)) + .any(|element| target_swallows_optional(db, env, *element)) + && elements.iter().all(|element| { + element.is_none(db) || target_swallows_optional(db, env, *element) + }) } _ => false, } @@ -488,6 +499,7 @@ pub(super) struct TypeInferenceBuilder<'db, 'ast> { fn transparent_callable_decorator_result<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, bindings: &Bindings<'db>, decorated_ty: Type<'db>, ) -> Option> { @@ -510,6 +522,7 @@ fn transparent_callable_decorator_result<'db>( fn callable_paramspec_and_return<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, ) -> Option<(BoundTypeVarInstance<'db>, TransparentCallableReturn<'db>)> { let callable = ty.resolve_type_alias(db).as_callable()?; @@ -523,9 +536,10 @@ fn transparent_callable_decorator_result<'db>( let return_typevar = if let Some(typevar) = signature.return_ty.as_typevar() { TransparentCallableReturn::TypeVar(typevar) } else { - let specialization = signature - .return_ty - .known_specialization(db, KnownClass::Awaitable)?; + let specialization = + signature + .return_ty + .known_specialization(db, env, KnownClass::Awaitable)?; let [inner] = specialization.types(db) else { return None; }; @@ -537,21 +551,22 @@ fn transparent_callable_decorator_result<'db>( if !matches!(decorated_ty, Type::FunctionLiteral(_) | Type::Callable(_)) { return None; } + let binding = bindings.single_element()?; let (_, overload) = binding.matching_overloads().exactly_one().ok()?; let decorator_signature = &overload.signature; let bound_signature = binding .bound_type - .map(|bound_type| decorator_signature.bind_self(db, Some(bound_type))); + .map(|bound_type| decorator_signature.bind_self(db, env, Some(bound_type))); let decorator_signature = bound_signature.as_ref().unwrap_or(decorator_signature); let [parameter] = decorator_signature.parameters().as_slice() else { return None; }; let (parameter_callable_paramspec, parameter_callable_return) = - callable_paramspec_and_return(db, parameter.annotated_type())?; + callable_paramspec_and_return(db, env, parameter.annotated_type())?; let (return_callable_paramspec, return_callable_return) = - callable_paramspec_and_return(db, decorator_signature.return_ty)?; + callable_paramspec_and_return(db, env, decorator_signature.return_ty)?; if !parameter_callable_paramspec.is_same_typevar_as(db, return_callable_paramspec) || !parameter_callable_return.matches(db, return_callable_return) { @@ -575,13 +590,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// Creates a new builder for inferring types in a region. pub(super) fn new( db: &'db dyn Db, + env: &'ast ProgramEnvironment<'db>, region: InferenceRegion<'db>, + file: File, + program_file: ProgramFile<'db>, index: &'db SemanticIndex<'db>, module: &'ast ParsedModuleRef, ) -> Self { let scope = region.scope(db); Self { - context: InferContext::new(db, scope, module), + context: InferContext::new(db, env, scope, file, program_file, module), index, region, scope, @@ -640,7 +658,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.cycle_recovery } - pub(super) fn recursive_type_expression_definition(&self) -> Option> { + fn recursive_type_expression_definition(&self) -> Option> { self.typevar_binding_context.or(match self.region { InferenceRegion::Definition(definition) | InferenceRegion::Deferred(definition) => { Some(definition) @@ -653,11 +671,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn extend_cycle_recovery(&mut self, other: Option>) { + let db = self.db(); if let Some(other) = other { match self.cycle_recovery { Some(existing) => { - self.cycle_recovery = - Some(UnionType::from_two_elements(self.db(), existing, other)); + self.cycle_recovery = Some(UnionType::from_two_elements( + db, + self.program_environment(), + existing, + other, + )); } None => { self.cycle_recovery = Some(other); @@ -725,7 +748,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { #[expect( clippy::iter_over_hash_type, - reason = "constraints for distinct collection definitions are merged independently" + reason = "constraints for distinct collection definitions are merged \ + independently" )] for (collection_def, constraints) in &extra.collection_use_constraints { self.collection_use_constraints @@ -899,6 +923,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.context.file() } + fn program_file(&self) -> ProgramFile<'db> { + self.context.program_file() + } + + #[inline] + fn program_environment(&self) -> &'ast ProgramEnvironment<'db> { + self.context.program_environment() + } + fn module(&self) -> &'ast ParsedModuleRef { self.context.module() } @@ -918,7 +951,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { fn bindings_for_call(&self, callable_type: Type<'db>) -> Bindings<'db> { let db = self.db(); callable_type - .bindings(db) + .bindings(db, self.program_environment()) .with_enclosing_binding_contexts(enclosing_binding_contexts( self.index, self.scope().file_scope_id(db), @@ -1010,7 +1043,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.index.has_future_annotations() || self.in_stub() || self.is_basedpython_file() - || Program::get(self.db()).python_version(self.db()) >= PythonVersion::PY314 + || self.program_environment().python_version(self.db()) >= PythonVersion::PY314 } /// Are we currently in a context where name resolution should be deferred @@ -1120,7 +1153,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// 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_scope = file_scope.to_scope_id(self.db(), self.program_file()); match self.region { InferenceRegion::Scope(scope, _) if scope == expr_scope => { self.expression_type(expression) @@ -1132,7 +1165,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// Get metadata for a type expression from any scope in the same file. fn file_type_expression_flags(&self, expression: &ast::Expr) -> TypeExpressionFlags { let file_scope = self.index.expression_scope_id(expression); - let expr_scope = file_scope.to_scope_id(self.db(), self.file()); + let expr_scope = file_scope.to_scope_id(self.db(), self.program_file()); match self.region { InferenceRegion::Scope(scope, _) if scope == expr_scope => { self.type_expression_flags(expression) @@ -1532,6 +1565,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// so argument-index lookups can't reach for the synthetic argument, /// which has no AST node. fn infer_trailing_lambda_marker(&mut self, function: &ast::StmtFunctionDef) { + let env = self.program_environment(); let Some(signature_callee) = function.trailing_lambda_callee() else { return; }; @@ -1592,11 +1626,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { )); let call_arguments: CallArguments<'_, 'db> = items.into_iter().collect(); - let return_ty = match callee_ty.try_call(self.db(), &call_arguments) { - Ok(bindings) => bindings.return_type(self.db()), + let return_ty = match callee_ty.try_call(self.db(), env, &call_arguments) { + Ok(bindings) => bindings.return_type(self.db(), env), Err(error) => { error.1.report_diagnostics(&self.context, decorator.into()); - error.return_type(self.db()) + error.return_type(self.db(), env) } }; if marker_call.is_some() { @@ -1695,8 +1729,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { (use_def.declarations_at_binding(binding), true) }; + let env = self.program_environment(); let (mut place_and_quals, conflicting) = place_from_declarations_with_reachability_cache( - self.db(), + db, + env, declarations, self.reachability_cache(), ) @@ -1708,7 +1744,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { 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))) + format_enumeration(conflicting.iter().map(|ty| ty.display(db, env))) )); } } @@ -1722,8 +1758,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if self.skip_non_global_scopes(file_scope_id, symbol_id) || self.scope.file_scope_id(self.db()).is_global() { - place_and_quals = place_and_quals.or_fall_back_to(self.db(), || { - module_type_implicit_global_declaration(self.db(), symbol.name()) + place_and_quals = place_and_quals.or_fall_back_to(db, env, || { + module_type_implicit_global_declaration(db, env, symbol.name()) }); } } @@ -1753,14 +1789,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// normal attribute or subscript lookup on its receiver. fn fallback_member_declared_type(&mut self, node: AnyNodeRef<'_>) -> Option> { let db = self.db(); - if let AnyNodeRef::ExprAttribute(ast::ExprAttribute { value, attr, .. }) = node { - let value_type = self.infer_maybe_standalone_expression(value, TypeContext::default()); + let value_type = self.try_expression_type(value).unwrap_or_else(|| { + self.infer_maybe_standalone_expression(value, TypeContext::default()) + }); if let Place::Defined(DefinedPlace { ty, definedness: Definedness::AlwaysDefined, .. - }) = value_type.member(db, attr).place + }) = value_type + .member(db, self.program_environment(), attr) + .place { // TODO: also consider qualifiers on the attribute Some(ty) @@ -1773,15 +1812,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }, ) = node { - let value_ty = self.infer_expression(value, TypeContext::default()); - let slice_ty = self.infer_expression(slice, TypeContext::default()); - Some(self.infer_subscript_expression_types( - subscript, - value_ty, - slice_ty, - *ctx, - TypeContext::default(), - )) + let value_ty = self.get_or_infer_expression(value, TypeContext::default()); + let slice_ty = self.get_or_infer_expression(slice, TypeContext::default()); + Some( + self.infer_subscript_expression_types( + subscript, + value_ty, + slice_ty, + *ctx, + TypeContext::default(), + ) + .unwrap_or_else(|recovery_ty| recovery_ty), + ) } else { None } @@ -1883,6 +1925,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { declaration: Definition<'db>, ty: TypeAndQualifiers<'db>, ) { + let db = self.db(); debug_assert!( declaration .kind(self.db()) @@ -1891,15 +1934,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); let use_def = self.index.use_def_map(declaration.file_scope(self.db())); let prior_bindings = use_def.bindings_at_definition(declaration); + let env = self.program_environment(); // unbound_ty is Never because for this check we don't care about unbound let inferred_ty = place_from_bindings_with_reachability_cache( - self.db(), + db, + env, prior_bindings, self.reachability_cache(), ) .place .with_qualifiers(TypeQualifiers::empty()) - .or_fall_back_to(self.db(), || { + .or_fall_back_to(db, env, || { // Fallback to bindings declared on `types.ModuleType` if it's a global symbol let scope = self.scope().file_scope_id(self.db()); let place = self @@ -1910,7 +1955,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let PlaceExprRef::Symbol(symbol) = &place && scope.is_global() { - module_type_implicit_global_symbol(self.db(), self.file(), symbol.name()) + module_type_implicit_global_symbol(db, self.program_file(), symbol.name()) } else { Place::Undefined.into() } @@ -1918,14 +1963,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .place .ignore_possibly_undefined() .unwrap_or(Type::Never); - let ty = if inferred_ty.is_assignable_to(self.db(), ty.inner_type()) { + let ty = if inferred_ty.is_assignable_to(db, env, 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()) + ty.inner_type().display(db, env), + inferred_ty.display(db, env) )); } TypeAndQualifiers::declared(Type::unknown()) @@ -1939,6 +1984,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { definition: Definition<'db>, declared_and_inferred_ty: &DeclaredAndInferredType<'db>, ) { + let db = self.db(); debug_assert!( definition .kind(self.db()) @@ -1960,39 +2006,48 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { declared_ty, inferred_ty, } => { + let env = self.program_environment(); 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(definition.place(self.db())); - let file = self.file(); if let Some(module_type_implicit_declaration) = place .as_symbol() .map(|symbol| { - module_type_implicit_global_symbol(self.db(), file, symbol.name()) + module_type_implicit_global_symbol( + db, + self.program_file(), + symbol.name(), + ) }) .and_then(|place| place.place.ignore_possibly_undefined()) { let declared_type = declared_ty.inner_type(); - if !declared_type - .is_assignable_to(self.db(), module_type_implicit_declaration) - { + if !declared_type.is_assignable_to( + db, + env, + 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()) + "Cannot shadow implicit global attribute `{place}` \ + with declaration of type `{}`", + declared_type.display(db, env) )); - diagnostic.info(format_args!("The global symbol `{}` must always have a type assignable to `{}`", + diagnostic.info(format_args!( + "The global symbol `{}` \ + must always have a type assignable to `{}`", place, - module_type_implicit_declaration.display(self.db()) + module_type_implicit_declaration.display(db, env) )); } } } } let declared_type = declared_ty.inner_type(); - if inferred_ty.is_assignable_to(self.db(), declared_type) { + if inferred_ty.is_assignable_to(db, env, declared_type) { report_bool_as_int_assignment( &self.context, node, @@ -2000,13 +2055,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { declared_type, inferred_ty, ); + // TODO We currently can't distinguish here between "no declared type" and + // "declared types is `Unknown` (e.g. due to a bad annotation, missing + // import, etc.)". Ideally we would still prefer `Unknown` declared type, + // but use inferred type if there is no declared type. if !should_preserve_inferred_binding_type(self.db(), inferred_ty) - // TODO We currently can't distinguish here between "no declared type" and - // "declared types is `Unknown` (e.g. due to a bad annotation, missing - // import, etc.)". Ideally we would still prefer `Unknown` declared type, - // but use inferred type if there is no declared type. && !matches!(declared_type, Type::Dynamic(DynamicType::Unknown)) - && declared_type.is_assignable_to(self.db(), inferred_ty) + && declared_type.is_assignable_to(db, env, inferred_ty) { (declared_ty, declared_type) } else { @@ -2074,6 +2129,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn infer_type_alias(&mut self, type_alias: &ast::StmtTypeAlias) { + let db = self.db(); let previous_check_unbound_typevars = self .context .inference_flags @@ -2120,7 +2176,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // type IntOrStr = int | StrOrInt # It's redundant, but OK // type StrOrInt = str | IntOrStr # It's redundant, but OK // ``` - let expanded = value_ty.expand_eagerly(self.db()); + let expanded = value_ty.expand_eagerly(db, self.program_environment()); if expanded.is_divergent() { if let Some(builder) = self .context @@ -2347,31 +2403,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { diagnostic::report_undeclared_protocol_attribute(&self.context, target, protocol); } - /// Returns the implicit `__class__` cell in the current direct method body or - /// lazy scope defined directly in a class body. - fn dunder_class_cell_type(&self) -> Option> { - let current_scope_id = self.scope().file_scope_id(self.db()); - let class_definition = - if let Some(definition) = self.index.class_definition_of_method(current_scope_id) { - definition - } else { - let current_scope = self.index.scope(current_scope_id); - if !matches!( - current_scope.node(), - NodeWithScopeKind::Lambda(_) | NodeWithScopeKind::GeneratorExpression(_) - ) { - return None; - } - let class = self - .index - .parent_scope(current_scope_id)? - .node() - .as_class()?; - self.index.expect_single_definition(class) - }; - original_class_type(self.db(), class_definition) - } - /// 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`. @@ -2438,6 +2469,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn infer_body(&mut self, suite: &[ast::Stmt]) { + let db = self.db(); for statement in suite { self.infer_maybe_standalone_statement(statement); @@ -2454,7 +2486,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { builder.into_diagnostic(format_args!( "Object of type `{}` is not awaited", - ty.display(self.db()), + ty.display(db, self.program_environment()), )); } } @@ -2535,11 +2567,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let rhs_scope = self .index .node_scope(NodeWithScopeRef::TypeAlias(type_alias)) - .to_scope_id(self.db(), self.file()); + .to_scope_id(self.db(), self.program_file()); let type_alias_ty = Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::PEP695( - PEP695TypeAliasType::new(self.db(), alias_name, rhs_scope, None), + PEP695TypeAliasType::new(self.db(), alias_name, rhs_scope, None, None), ))); self.store_expression_type(&type_alias.name, type_alias_ty); @@ -2605,6 +2637,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// block, but only one that diverges — control falling out of the block /// reaches the same unbound captures. fn check_destructure(&mut self, pattern: &ast::Pattern) { + let env = self.program_environment(); let Some(destructure) = self.index.destructure(NodeKey::from_node(pattern)) else { return; }; @@ -2634,14 +2667,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // that never said what it holds is not what this check is for. `Unknown` // is what an unannotated parameter, a bare `list` element and an // unresolved import all arrive as - if subject_ty.has_dynamic(self.db()) { + if subject_ty.has_dynamic(self.db(), env) { return; } if let Some(builder) = self.context.report_lint(&REFUTABLE_DESTRUCTURING, pattern) { builder.into_diagnostic(format_args!( "This pattern may not match `{}`, which would leave its captures unbound", - subject_ty.display(self.db()), + subject_ty.display(self.db(), env), )); } } @@ -2650,11 +2683,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// is a basedpython `if let := :` — the subject is matched /// against the pattern rather than tested for truthiness fn infer_if_condition(&mut self, pattern: Option<&ast::Pattern>, test: &ast::Expr) { + let env = self.program_environment(); let test_ty = self.infer_standalone_expression(test, TypeContext::default()); if let Some(pattern) = pattern { self.infer_match_pattern(pattern); - } else if let Err(err) = test_ty.try_bool(self.db()) { + } else if let Err(err) = test_ty.try_bool(self.db(), env) { err.report_diagnostic(&self.context, test); } else { self.check_condition(test); @@ -2702,6 +2736,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn infer_with_statement(&mut self, with_statement: &ast::StmtWith) { + let db = self.db(); let ast::StmtWith { range: _, node_index: _, @@ -2718,7 +2753,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // `with not_context_manager as a.x: ... builder .infer_standalone_expression(&item.context_expr, tcx) - .enter(builder.db()) + .enter(db, builder.program_environment()) }); } else { // Call into the context expression inference to validate that it evaluates @@ -2779,50 +2814,52 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { context_expression_type: Type<'db>, is_async: bool, ) -> Type<'db> { + let db = self.db(); let eval_mode = if is_async { EvaluationMode::Async } else { EvaluationMode::Sync }; + let env = self.program_environment(); context_expression_type - .try_enter_with_mode(self.db(), eval_mode) + .try_enter_with_mode(db, env, eval_mode) .unwrap_or_else(|err| { err.report_diagnostic( &self.context, context_expression_type, context_expression.into(), ); - err.fallback_enter_type(self.db()) + err.fallback_enter_type(db, env) }) } fn infer_exception(&mut self, node: Option<&ast::Expr>, is_star: bool) -> Type<'db> { + let db = self.db(); // 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, TypeContext::default()) }); - let type_base_exception = KnownClass::BaseException.to_subclass_of(self.db()); + let env = self.program_environment(); + let type_base_exception = KnownClass::BaseException.to_subclass_of(db, env); // 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 Some(tuple_spec) = node_ty.tuple_instance_spec(self.db()) { - let mut builder = UnionBuilder::new(self.db()); + let symbol_ty = if let Some(tuple_spec) = node_ty.tuple_instance_spec(db, env) { + let mut builder = UnionBuilder::new(db, env); let mut invalid_elements = vec![]; for (index, element) in tuple_spec.iter_element_types(self.db()).enumerate() { - builder = builder.add( - if element.is_assignable_to(self.db(), type_base_exception) { - element.to_instance_approximation(self.db()).expect( - "`Type::to_instance()` should always return `Some()` \ + builder.add_in_place(if element.is_assignable_to(db, env, type_base_exception) { + element.to_instance_approximation(db, env).expect( + "`Type::to_instance()` should always return `Some()` \ if called on a type assignable to `type[BaseException]`", - ) - } else { - invalid_elements.push((index, element)); - Type::unknown() - }, - ); + ) + } else { + invalid_elements.push((index, element)); + Type::unknown() + }); } if !invalid_elements.is_empty() @@ -2853,16 +2890,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { symbol_ty } else if node_ty.is_assignable_to( - self.db(), + db, + env, UnionType::from_two_elements( - self.db(), + db, + env, type_base_exception, - Type::homogeneous_tuple(self.db(), type_base_exception), + Type::homogeneous_tuple(db, env, type_base_exception), ), ) { // TODO: Handle valid handler expressions that are opaque to the structural helper // above, for example a type variable bounded by the full class-or-tuple union. - KnownClass::BaseException.to_instance(self.db()) + KnownClass::BaseException.to_instance(db, env) } else { if let Some(node) = node { report_invalid_exception_caught(&self.context, node, node_ty); @@ -2871,14 +2910,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; 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]) + let class = + if symbol_ty.is_subtype_of(db, env, KnownClass::Exception.to_instance(db, env)) { + KnownClass::ExceptionGroup + } else { + KnownClass::BaseExceptionGroup + }; + class.to_specialized_instance(db, env, &[symbol_ty]) } else { symbol_ty } @@ -2889,13 +2927,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ty: Type<'db>, type_base_exception: Type<'db>, ) -> Option> { - if let Some(tuple_spec) = ty.tuple_instance_spec(self.db()) { + let db = self.db(); + let env = self.program_environment(); + + if let Some(tuple_spec) = ty.tuple_instance_spec(db, env) { // `except (ValueError, TypeError) as e:` UnionType::try_from_elements( - self.db(), + db, + env, tuple_spec.iter_element_types(self.db()).map(|element| { - if element.is_assignable_to(self.db(), type_base_exception) { - Some(element.to_instance_approximation(self.db()).expect( + if element.is_assignable_to(db, env, type_base_exception) { + Some(element.to_instance_approximation(db, env).expect( "`Type::to_instance()` should always return `Some()` \ if called on a type assignable to `type[BaseException]`", )) @@ -2904,40 +2946,42 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } }), ) - } else if ty.is_assignable_to(self.db(), type_base_exception) { + } else if ty.is_assignable_to(db, env, type_base_exception) { // `except ValueError as e:` - Some(ty.to_instance_approximation(self.db()).expect( + Some(ty.to_instance_approximation(db, env).expect( "`Type::to_instance()` should always return `Some()` \ if called on a type assignable to `type[BaseException]`", )) } else if ty.is_assignable_to( - self.db(), - Type::homogeneous_tuple(self.db(), type_base_exception), + db, + env, + Type::homogeneous_tuple(db, env, type_base_exception), ) { // `except exception_types as e:`, where // `exception_types: tuple[type[ValueError], ...]` Some( - ty.tuple_instance_spec(self.db()) + ty.tuple_instance_spec(db, env) .and_then(|spec| { let specialization = spec - .homogeneous_element_type(self.db()) - .to_instance_approximation(self.db()); + .homogeneous_element_type(db, env) + .to_instance_approximation(db, env); debug_assert!(specialization.is_some_and(|specialization_type| { specialization_type.is_assignable_to( - self.db(), - KnownClass::BaseException.to_instance(self.db()), + db, + env, + KnownClass::BaseException.to_instance(db, env), ) })); specialization }) - .unwrap_or_else(|| KnownClass::BaseException.to_instance(self.db())), + .unwrap_or_else(|| KnownClass::BaseException.to_instance(db, env)), ) } else if let Type::Union(union) = ty { // `except exception_types as e:`, where // `exception_types: type[ValueError] | tuple[type[ValueError], ...]` - union.try_map(self.db(), |element| { + union.try_map(db, env, |element| { self.exception_handler_symbol_ty_from_valid_ty(*element, type_base_exception) }) } else { @@ -2975,9 +3019,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // This cutoff was chosen by benchmarking real isort to keep loop analysis // overhead minimal while preserving diagnostics. const MAX_EXACT_LOOP_HEADER_REACHABILITY_NODES: usize = 4096; - let db = self.db(); - let loop_header = loop_header_reachability(db, definition); + + let loop_header = loop_header_reachability(self.db(), definition); let use_def = self .index .use_def_map(self.scope().file_scope_id(self.db())); @@ -2993,14 +3037,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } let place = loop_header_kind.place(); - - let mut union = UnionBuilder::new(db).recursively_defined(RecursivelyDefined::Yes); + let env = self.program_environment(); + let mut union = UnionBuilder::new(db, env).recursively_defined(RecursivelyDefined::Yes); for reachable_binding in &loop_header.reachable_bindings { let binding_ty = binding_type(db, reachable_binding.definition); let narrowed_ty = use_def .narrowing_evaluator(reachable_binding.narrowing_constraint) - .narrow(db, binding_ty, place); + .narrow(db, env, binding_ty, place); union.add_in_place(narrowed_ty); } @@ -3016,63 +3060,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { const MAX_EXACT_NESTED_BINDING_REACHABILITY_NODES: usize = 2048; let db = self.db(); - let scope = definition.scope(db); - let scope_id = scope.file_scope_id(db); - let symbol_id = definition - .place(db) - .as_symbol() - .expect("nested bindings definition should be a symbol"); - let symbol = self.index.place_table(scope_id).symbol(symbol_id); - - // At the point where a nested bindings definition is first synthesized, we don't - // necessarily know whether the current scope will see global or nonlocal bindings. - // Consider this example: - // - // def outer(): - // def inner1(): - // global x - // x = 1 - // def inner2(): - // nonlocal x - // x = 2 - // # Nested bindings of both kinds are potentially visible here, but we can't - // # actually use both kinds in the same scope. If `print(x)` comes next, we should - // # only see 2. But if `global x; print(x)` comes next, we should only see 1. - // ... - // - // By the time we get here in type inference, though, we can ask the semantic index whether - // the symbol resolves to the global scope or not. (For free variables, this currently - // requires walking ancestor scopes.) If so, we see any nested `global` bindings that were - // recorded. If not, we see any nested `nonlocal` ones. - // - // Note that if `x` is a free variable in this scope, then this synthetic binding will not - // shadow `UNBOUND`, and `infer_place_load` will walk to the defining scope and see all the - // nested bindings from there via the symbol's public type. However, we still want to - // respect locally visible nested bindings in that case, because there might be narrowing - // constraints that apply to the public type but not these nested bindings. - let this_scope_sees_global_bindings = self - .index - .symbol_resolves_to_global_scope(symbol_id, scope_id); - - // If a function body binds `x`, it's interested in nested `nonlocal` bindings of `x` too, - // because those resolve to the same variable. But if a *class* body binds `x`, it does - // *not* want to consider nested bindings of `x`, because those do *not* resolve to the - // same variable. - let this_scope_sees_nonlocal_bindings = !(this_scope_sees_global_bindings - || (scope.scope(db).kind().is_class() && symbol.is_local())); - - let mut visible_nested_declarations = nested_bindings_kind - .nested_declarations - .iter() - .filter(|declaration| { - if declaration.is_global() { - this_scope_sees_global_bindings - } else { - this_scope_sees_nonlocal_bindings - } - }) + let scope_id = definition.file_scope(db); + let mut binding_sources = nested_bindings_kind + .visible_binding_sources(self.index, scope_id) .peekable(); - if visible_nested_declarations.peek().is_some() + if binding_sources.peek().is_some() && self .index .use_def_map(scope_id) @@ -3087,20 +3079,36 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { return; } - let mut union = UnionBuilder::new(db).recursively_defined(RecursivelyDefined::Yes); - for declaration in visible_nested_declarations { - assert!( - declaration.is_bound, - "nested declarations without bindings shouldn't be recorded here", - ); - let nested_place_table = self.index.place_table(declaration.file_scope_id); - let nested_symbol_id = nested_place_table - .symbol_id(&nested_bindings_kind.name) - .unwrap(); - let use_def = self.index.use_def_map(declaration.file_scope_id); + let recursively_defined = match nested_bindings_kind.execution { + NestedBindingExecution::Lazy => RecursivelyDefined::Yes, + NestedBindingExecution::Eager => RecursivelyDefined::No, + }; + let env = self.program_environment(); + let mut union = UnionBuilder::new(db, env).recursively_defined(recursively_defined); + for bindings in binding_sources { + if nested_bindings_kind.execution == NestedBindingExecution::Eager { + // A comprehension can execute repeatedly, so a source that is unreachable in the + // first modeled iteration may become reachable in a later one. Preserve each + // source's narrowed type and let the proxy's outer use-def state track boundness. + for binding in bindings { + let DefinitionState::Defined(source) = binding.binding else { + continue; + }; + let ty = binding_type(db, source); + union.add_in_place(binding.narrowing_constraint.narrow( + db, + env, + ty, + source.place(db), + )); + } + continue; + } + let Some(ty) = place_from_bindings_with_reachability_cache( db, - use_def.reachable_bindings(nested_symbol_id.into()), + env, + bindings, self.reachability_cache(), ) .place @@ -3109,10 +3117,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; union.add_in_place(ty); } - self.bindings.insert(definition, union.build()); + let ty = union.build(); + let ty = match nested_bindings_kind.execution { + NestedBindingExecution::Lazy => ty, + NestedBindingExecution::Eager => ty.promote(db, env), + }; + self.bindings.insert(definition, ty); } fn infer_match_statement(&mut self, match_statement: &ast::StmtMatch) { + let db = self.db(); let ast::StmtMatch { range: _, node_index: _, @@ -3135,7 +3149,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(guard) = guard.as_deref() { let guard_ty = self.infer_standalone_expression(guard, TypeContext::default()); - if let Err(err) = guard_ty.try_bool(self.db()) { + if let Err(err) = guard_ty.try_bool(db, self.program_environment()) { err.report_diagnostic(&self.context, guard); } else { self.check_condition(guard); @@ -3159,6 +3173,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn validate_class_pattern(&mut self, pattern: &ast::PatternMatchClass, cls_ty: Type<'db>) { + let db = self.db(); + let env = self.program_environment(); if let Type::SpecialForm(SpecialFormType::CollectionsAbcCallable) = cls_ty { if let Some(first_excess_pattern) = pattern.arguments.patterns.first() { report_too_many_positional_patterns_for_class_pattern( @@ -3190,7 +3206,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let positional_patterns = &pattern.arguments.patterns; if let [first_positional_pattern, ..] = positional_patterns.as_slice() - && let Some(result) = class_pattern_positional_result(self.db(), class) + && let Some(result) = class_pattern_positional_result(db, env, class) { match result { ClassPatternPositionalResult::Limit(limit) => { @@ -3200,7 +3216,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { first_excess_pattern, limit, positional_patterns.len(), - cls_ty.display(self.db()), + cls_ty.display(db, env), ); } } @@ -3214,7 +3230,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } } - } else if !cls_ty.is_assignable_to(self.db(), KnownClass::Type.to_instance(self.db())) { + } else if !cls_ty.is_assignable_to(db, env, KnownClass::Type.to_instance(db, env)) { report_invalid_class_match_pattern(&self.context, &*pattern.cls, cls_ty); } } @@ -3410,12 +3426,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// Returns `true` if `property_ty` is a property whose deleter returns `Never`/`NoReturn` /// when called for deletion on `object_ty`. fn property_deleter_returns_never(&self, property_ty: Type<'db>, object_ty: Type<'db>) -> bool { + let env = self.program_environment(); let db = self.db(); property_ty.as_property_instance().is_some_and(|property| { property.deleter(db).is_some_and(|deleter| { - match deleter.try_call(db, &CallArguments::positional([object_ty])) { - Ok(result) => result.return_type(db).is_never(), - Err(err) => err.return_type(db).is_never(), + match deleter.try_call(db, env, &CallArguments::positional([object_ty])) { + Ok(result) => result.return_type(db, env).is_never(), + Err(err) => err.return_type(db, env).is_never(), } }) }) @@ -3428,13 +3445,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { attribute: &str, emit_diagnostics: bool, ) -> bool { + let env = self.program_environment(); let db = self.db(); match object_ty { // parameter-only marker; behaves as the type a body sees (bound of `Key`) Type::Overlapping(overlapping) => self.validate_attribute_deletion( target, - overlapping.value_type(db), + overlapping.value_type(db, env), attribute, emit_diagnostics, ), @@ -3446,7 +3464,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ), Type::Deferred(deferred) => self.validate_attribute_deletion( target, - deferred.reduced(db), + deferred.reduced(db, env), attribute, emit_diagnostics, ), @@ -3496,7 +3514,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::EnumComplement(complement) => self.validate_attribute_deletion( target, - complement.remaining_literal_union(db), + complement.remaining_literal_union(db, env), attribute, emit_diagnostics, ), @@ -3535,17 +3553,70 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { | Type::TypeForm(_) | Type::TypedDict(_) | Type::NewTypeInstance(_) => { - let delattr_dunder_call_result = object_ty.try_call_dunder_with_policy( - db, - "__delattr__", - &mut CallArguments::positional([Type::string_literal(db, attribute)]), - TypeContext::default(), - MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, - ); + let frozen_dataclass_dispatch = object_ty + .nominal_class(db, env) + .and_then(|class| class.static_class_literal(db)) + .and_then(|(class, specialization)| { + class.inherited_frozen_dataclass_dispatch( + db, + specialization, + "__delattr__", + attribute, + ) + }); + + let delattr_receiver = frozen_dataclass_dispatch + .map_or(object_ty, |dispatch| dispatch.receiver(db, env, object_ty)); + + let mut delattr_arguments = + CallArguments::positional([Type::string_literal(db, attribute)]); + let delattr_dunder_call_result = if matches!(delattr_receiver, Type::BoundSuper(_)) + { + match delattr_receiver + .member_lookup_with_policy( + db, + env, + "__delattr__", + MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, + ) + .place + { + Place::Defined(DefinedPlace { + ty: delattr, + definedness, + provenance, + .. + }) => match delattr.try_call(db, env, &delattr_arguments) { + Ok(bindings) if definedness == Definedness::PossiblyUndefined => { + Err(CallDunderError::PossiblyUnbound { + bindings: Box::new(bindings), + unbound_on: None, + }) + } + Ok(bindings) => Ok(bindings), + Err(CallError(kind, bindings)) => { + Err(CallDunderError::CallError(kind, bindings, provenance)) + } + }, + Place::Undefined => Err(CallDunderError::MethodNotAvailable), + } + } else { + delattr_receiver.try_call_dunder_with_policy( + db, + env, + "__delattr__", + &mut delattr_arguments, + TypeContext::default(), + MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, + ) + }; - let returns_never = match &delattr_dunder_call_result { - Ok(result) => result.return_type(db).is_never(), - Err(err) => err.return_type(db).is_some_and(|ty| ty.is_never()), + let returns_never = matches!( + frozen_dataclass_dispatch, + Some(FrozenDataclassDispatch::FrozenField) + ) || match &delattr_dunder_call_result { + Ok(result) => result.return_type(db, env).is_never(), + Err(err) => err.return_type(db, env).is_some_and(|ty| ty.is_never()), }; if returns_never { if emit_diagnostics @@ -3554,14 +3625,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { builder.into_diagnostic(format_args!( "Cannot delete attribute `{attribute}` on type `{}` \ whose `__delattr__` method returns `Never`/`NoReturn`", - object_ty.display(db), + object_ty.display(db, env), )); } return false; } match delattr_dunder_call_result { - Ok(_) | Err(CallDunderError::PossiblyUnbound { .. }) => { + Ok(_) | Err(CallDunderError::PossiblyUnbound { .. }) + if !matches!( + frozen_dataclass_dispatch, + Some(FrozenDataclassDispatch::Delegate(_)) + ) => + { if self.validate_final_attribute_deletion( target, object_ty, @@ -3572,6 +3648,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } return true; } + Ok(_) | Err(CallDunderError::PossiblyUnbound { .. }) => {} Err(CallDunderError::CallError(kind, _bindings, _)) => { if emit_diagnostics { report_bad_dunder_delattr_call( @@ -3604,18 +3681,30 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .. }), .. - }) = assignment_attribute_members(db, object_ty, attribute) + }) = assignment_attribute_members(db, env, object_ty, attribute) .and_then(AssignmentAttributeMembers::type_member) { - let attr_ty = attr_ty.bind_self_typevars(db, object_ty); + let attr_ty = attr_ty.bind_self_typevars(db, env, object_ty); let delete_dunder_call_result = attr_ty.try_call_dunder( db, + env, "__delete__", CallArguments::positional([object_ty]), TypeContext::default(), ); - if self.property_deleter_returns_never(attr_ty, object_ty) { + // `Never` supports arbitrary operations only because there can be no runtime + // value to mutate; it is not a concrete descriptor with a terminal deleter. + let deleter_returns_never = !attr_ty.is_never() + && match &delete_dunder_call_result { + Ok(bindings) => bindings.return_type(db, env).is_never(), + Err(error) => { + error.return_type(db, env).is_some_and(|ty| ty.is_never()) + } + }; + if deleter_returns_never + || self.property_deleter_returns_never(attr_ty, object_ty) + { if emit_diagnostics && let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) @@ -3623,7 +3712,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { builder.into_diagnostic(format_args!( "Cannot delete attribute `{attribute}` on type `{}` \ whose `__delete__` method returns `Never`/`NoReturn`", - object_ty.display(db), + object_ty.display(db, env), )); } return false; @@ -3666,6 +3755,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { value: &ast::Expr, infer_assigned_ty: Option<&dyn Fn(&mut Self, TypeContext<'db>) -> Type<'db>>, ) { + let db = self.db(); match target { ast::Expr::Name(name) => { if let Some(infer_assigned_ty) = infer_assigned_ty { @@ -3684,8 +3774,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { | ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => { let assigned_ty = infer_assigned_ty.map(|f| f(self, TypeContext::default())); - if let Some(tuple_spec) = - assigned_ty.and_then(|ty| ty.tuple_instance_spec(self.db())) + if let Some(tuple_spec) = assigned_ty + .and_then(|ty| ty.tuple_instance_spec(db, self.program_environment())) { let assigned_tys = tuple_spec.iter_element_types(self.db()).collect::>(); @@ -3731,13 +3821,24 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } ast::Expr::Subscript(subscript_expr) => { if let Some(infer_assigned_ty) = infer_assigned_ty { + let object_ty = + self.infer_expression(&subscript_expr.value, TypeContext::default()); + let mut infer_slice_ty = |builder: &mut Self, tcx| { + builder.infer_expression(&subscript_expr.slice, tcx) + }; let infer_assigned_ty = &mut |builder: &mut Self, tcx| { let assigned_ty = infer_assigned_ty(builder, tcx); builder.store_expression_type(target, assigned_ty); assigned_ty }; - self.validate_subscript_assignment(subscript_expr, value, infer_assigned_ty); + self.validate_subscript_assignment( + subscript_expr, + value, + object_ty, + &mut infer_slice_ty, + infer_assigned_ty, + ); } } @@ -3844,6 +3945,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// that names no model, a `queryset` that doesn't trace to one, and a /// non-literal element are all left alone rather than guessed at. fn check_django_field_name_list(&mut self, target: &ast::Expr, value: &ast::Expr) { + let env = self.program_environment(); let ast::Expr::Name(name) = target else { return; }; @@ -3886,7 +3988,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; for (entry, range) in entries { if let django::FieldResolution::Unknown { model, segment } = - kind.resolve(db, model, entry) + kind.resolve(db, env, model, entry) && let Some(builder) = self.context.report_lint(&INVALID_FIELD_LOOKUP, range) { builder.into_diagnostic(format_args!( @@ -4068,7 +4170,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; if let Some(special_form) = target.as_name_expr().and_then(|name| { - SpecialFormType::try_from_file_and_name(self.db(), self.file(), &name.id) + let db = self.db(); + let importing_file = ImportingFile::File( + self.file(), + self.program_environment().resolver_environment(db), + ); + SpecialFormType::try_from_file_and_name(db, importing_file, &name.id) }) { target_ty = Type::SpecialForm(special_form); } @@ -4252,6 +4359,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn infer_assignment_deferred(&mut self, target: &ast::Expr, value: &'ast ast::Expr) { + let db = self.db(); + let env = self.program_environment(); // Infer deferred bounds/constraints/defaults of a legacy TypeVar / ParamSpec / NewType, // and field types for functional TypedDict. let ast::Expr::Call(ast::ExprCall { @@ -4303,7 +4412,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let constraint = self.infer_type_expression(arg); constraint_tys.push(constraint); - if constraint.has_typevar_or_typevar_instance(self.db()) + if constraint.has_typevar_or_typevar_instance(db, env) && let Some(builder) = self .context .report_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS, arg) @@ -4364,15 +4473,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Infer the deferred base type of a NewType. fn infer_newtype_assignment_deferred(&mut self, arguments: &ast::Arguments) { + let db = self.db(); + let env = self.program_environment(); let inferred = self.infer_type_expression(&arguments.args[1]); - if inferred.has_typevar_or_typevar_instance(self.db()) { + if inferred.has_typevar_or_typevar_instance(db, env) { if let Some(builder) = self .context .report_lint(&INVALID_NEWTYPE, &arguments.args[1]) { let mut diag = builder.into_diagnostic("invalid base for `typing.NewType`"); - diag.set_primary_message("A `NewType` base cannot be generic"); + diag.set_primary_annotation_message("A `NewType` base cannot be generic"); } return; } @@ -4398,7 +4509,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .report_lint(&INVALID_NEWTYPE, &arguments.args[1]) { let mut diag = builder.into_diagnostic("invalid base for `typing.NewType`"); - diag.set_primary_message(format!("type `{}`", inferred.display(self.db()))); + diag.set_primary_annotation_message(format!("type `{}`", inferred.display(db, env))); if matches!(inferred, Type::ProtocolInstance(_)) { diag.info("The base of a `NewType` is not allowed to be a protocol class."); } else if matches!(inferred, Type::TypedDict(_)) { @@ -4489,7 +4600,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.deferred.insert(definition); Type::KnownInstance(KnownInstanceType::TypeAliasType( - TypeAliasType::ManualPEP695(ManualPEP695TypeAliasType::new(db, name, definition)), + TypeAliasType::ManualPEP695(ManualPEP695TypeAliasType::new( + db, name, definition, None, None, + )), )) } @@ -4499,14 +4612,164 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { definition: Definition<'db>, arguments: &ast::Arguments, ) { + let db = self.db(); // Match the binding context used by eager assignment inference so legacy type variables // in the alias value are bound to the alias definition. let previous_context = self.typevar_binding_context.replace(definition); - self.infer_type_expression(&arguments.args[1]); + let value_ty = self.infer_type_expression(&arguments.args[1]); + let mut type_params = FxHashSet::default(); + let mut valid_type_params = true; // Infer keyword arguments (e.g. `type_params`) so their types are stored. for keyword in &arguments.keywords { self.infer_expression(&keyword.value, TypeContext::default()); + + if keyword.arg.as_deref() != Some("type_params") { + continue; + } + + let Some(tuple) = keyword.value.as_tuple_expr() else { + valid_type_params = false; + if let Some(builder) = self + .context + .report_lint(&INVALID_TYPE_ALIAS_TYPE, &keyword.value) + { + builder.into_diagnostic( + "The `type_params` argument to `TypeAliasType` must be a tuple literal", + ); + } + continue; + }; + + let db = self.db(); + let mut typevar_with_default = None; + let mut typevar_tuple: Option = None; + let mut reported_default_order_error = false; + + for element in &tuple.elts { + let bound_typevar = match self.expression_type(element) { + Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => bind_typevar( + self.db(), + self.index, + definition.file_scope(db), + Some(definition), + typevar, + ), + _ => None, + }; + let Some(bound_typevar) = bound_typevar else { + valid_type_params = false; + if let Some(builder) = + self.context.report_lint(&INVALID_TYPE_ALIAS_TYPE, element) + { + builder.into_diagnostic( + "Each `type_params` entry for `TypeAliasType` must be a type variable", + ); + } + continue; + }; + let typevar = bound_typevar.typevar(db); + + if bound_typevar.binding_context(db) != BindingContext::Definition(definition) { + valid_type_params = false; + if let Some(builder) = + self.context.report_lint(&INVALID_TYPE_ALIAS_TYPE, element) + { + builder.into_diagnostic(format_args!( + "Type parameter `{}` is bound in an outer scope \ + and cannot be used in `type_params`", + typevar.name(db), + )); + } + continue; + } + + if !type_params.insert(bound_typevar.identity(db)) { + valid_type_params = false; + if let Some(builder) = + self.context.report_lint(&INVALID_TYPE_ALIAS_TYPE, element) + { + builder.into_diagnostic(format_args!( + "Type parameter `{}` is duplicated in `type_params`", + typevar.name(db), + )); + } + } + + if typevar + .default_type(db, self.program_environment()) + .is_some() + { + if let Some(typevar_tuple) = typevar_tuple { + valid_type_params = false; + if let Some(builder) = self + .context + .report_lint(&INVALID_TYPE_VARIABLE_DEFAULT, element) + { + builder.into_diagnostic(format_args!( + "Type parameter `{}` with a default follows TypeVarTuple `{}`", + typevar.name(db), + typevar_tuple.name(db), + )); + } + } + typevar_with_default.get_or_insert(typevar); + } else if let Some(typevar_with_default) = typevar_with_default { + valid_type_params = false; + if !reported_default_order_error + && let Some(builder) = self + .context + .report_lint(&INVALID_TYPE_VARIABLE_DEFAULT, element) + { + reported_default_order_error = true; + builder.into_diagnostic(format_args!( + "Type parameter `{}` without a default \ + cannot follow earlier parameter `{}` with a default", + typevar.name(db), + typevar_with_default.name(db), + )); + } + } + + if typevar.is_typevartuple(db) { + if typevar_tuple.is_some() { + valid_type_params = false; + if let Some(builder) = + self.context.report_lint(&INVALID_TYPE_ALIAS_TYPE, element) + { + builder.into_diagnostic( + "Only one `TypeVarTuple` parameter is allowed in `type_params`", + ); + } + } else { + typevar_tuple = Some(typevar); + } + } + } + } + + if valid_type_params { + let mut value_typevars = FxOrderSet::default(); + value_ty.find_legacy_typevars( + db, + self.program_environment(), + Some(definition), + &mut value_typevars, + ); + + for typevar in value_typevars { + if !type_params.contains(&typevar.identity(self.db())) + && let Some(builder) = self + .context + .report_lint(&INVALID_TYPE_ALIAS_TYPE, &arguments.args[1]) + { + builder.into_diagnostic(format_args!( + "Type parameter `{}` used in the alias value \ + must be included in `type_params`", + typevar.name(self.db()), + )); + } + } } self.typevar_binding_context = previous_context; @@ -4524,6 +4787,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(value) = &assignment.value { self.check_django_field_name_list(&assignment.target, value); } + let db = self.db(); + let env = self.program_environment(); if assignment.target.is_name_expr() { self.infer_definition(assignment); } else { @@ -4577,7 +4842,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .report_lint(&INVALID_PARAMSPEC, annotation.as_ref()) { builder.into_diagnostic(format_args!( - "`{name}.{attr_name}` is only valid for annotating `{variadic}` function parameters", + "`{name}.{attr_name}` is only valid \ + for annotating `{variadic}` function parameters", )); } } else if let ast::Expr::Attribute(attr_expr) = annotation.as_ref() @@ -4599,7 +4865,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .report_lint(&INVALID_PARAMSPEC, annotation.as_ref()) { builder.into_diagnostic(format_args!( - "`{name}.{attr_name}` is only valid for annotating `{variadic}` function parameters", + "`{name}.{attr_name}` is only valid \ + for annotating `{variadic}` function parameters", )); } } @@ -4684,7 +4951,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // type, report an error and fall back to the annotated type. let target_ty = if let Some(value_ty) = value_ty { let declared_ty = annotated.inner_type(); - if value_ty.is_assignable_to(self.db(), declared_ty) { + if value_ty.is_assignable_to(db, env, declared_ty) { value_ty } else { if let Some(builder) = self @@ -4693,17 +4960,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { let mut diag = builder.into_diagnostic(format_args!( "Object of type `{}` is not assignable to `{}`", - value_ty.display(self.db()), - declared_ty.display(self.db()), + value_ty.display(db, env), + declared_ty.display(db, env), )); diag.annotate( self.context .secondary(annotation.as_ref()) .message("Declared type"), ); - diag.set_primary_message(format_args!( + diag.set_primary_annotation_message(format_args!( "Incompatible value of type `{}`", - value_ty.display(self.db()), + value_ty.display(db, env), )); } declared_ty @@ -4715,12 +4982,49 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } + /// Infer an annotated assignment's annotation using the file's deferred-annotation semantics. + fn infer_annotated_assignment_annotation( + &mut self, + assignment: &AnnotatedAssignmentDefinitionKind, + ) -> TypeAndQualifiers<'db> { + let annotation = assignment.annotation(self.module()); + + // PEP 681 lets a field specifier appear in the annotation's `Annotated` + // metadata (`x: Annotated[int, Field(default=0)]`), so recognize + // field-specifier calls while inferring the annotation, exactly as an + // r.h.s. value does. Only populated inside a dataclass-like class body; + // cleared immediately after so it does not leak into the value. + self.setup_dataclass_field_specifiers(); + let declared = self.infer_annotation_expression_allow_pep_613( + annotation, + DeferredExpressionState::from(self.defer_annotations()), + ); + self.dataclass_field_specifiers.clear(); + + declared + } + + /// Initialize a declaration cycle without discarding its annotation diagnostics or metadata. + pub(super) fn infer_annotated_assignment_cycle_initial( + mut self, + definition: Definition<'db>, + assignment: &AnnotatedAssignmentDefinitionKind, + cycle_recovery: Type<'db>, + ) -> DefinitionInference<'db> { + let declared = self.infer_annotated_assignment_annotation(assignment); + self.declarations.insert(definition, declared); + self.cycle_recovery = Some(cycle_recovery); + self.finish_inferred_definition(definition) + } + /// Infer the types in an annotated assignment definition. fn infer_annotated_assignment_definition( &mut self, assignment: &'db AnnotatedAssignmentDefinitionKind, definition: Definition<'db>, ) { + let db = self.db(); + let env = self.program_environment(); let target = assignment.target(self.module()); let value = assignment.value(self.module()); @@ -4773,7 +5077,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let base_ty = self.infer_type_expression(value_expr); let eager_base = match base_ty { Type::NominalInstance(nominal) => Some( - crate::types::newtype::NewTypeBase::ClassType(nominal.class(self.db())), + crate::types::newtype::NewTypeBase::ClassType(nominal.class(self.db(), env)), ), Type::NewTypeInstance(nt) => Some(crate::types::newtype::NewTypeBase::NewType(nt)), Type::Union(union) => match union.known(self.db()) { @@ -4878,17 +5182,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { return; } - // PEP 681 lets a field specifier appear in the annotation's `Annotated` - // metadata (`x: Annotated[int, Field(default=0)]`), so recognize - // field-specifier calls while inferring the annotation, exactly as the - // r.h.s. value below does. Only populated inside a dataclass-like class - // body; cleared immediately after so it does not leak into the value. - self.setup_dataclass_field_specifiers(); - let mut declared = self.infer_annotation_expression_allow_pep_613( - annotation, - DeferredExpressionState::from(self.defer_annotations()), - ); - self.dataclass_field_specifiers.clear(); + let mut declared = self.infer_annotated_assignment_annotation(assignment); // basedpython: `let x: T` declares read-only state in every scope, with or // without an initializer. the `__let__` marker only marks `FINAL` outside @@ -4926,7 +5220,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; if let Some(builder) = self.context.report_lint(&INVALID_PARAMSPEC, annotation) { builder.into_diagnostic(format_args!( - "`{name}.{attr_name}` is only valid for annotating `{variadic}` function parameters", + "`{name}.{attr_name}` is only valid \ + for annotating `{variadic}` function parameters", )); } } else if let ast::Expr::Attribute(attr_expr) = annotation @@ -4947,7 +5242,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; if let Some(builder) = self.context.report_lint(&INVALID_PARAMSPEC, annotation) { builder.into_diagnostic(format_args!( - "`{name}.{attr_name}` is only valid for annotating `{variadic}` function parameters", + "`{name}.{attr_name}` is only valid \ + for annotating `{variadic}` function parameters", )); } } @@ -5002,8 +5298,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { continue; } - let nearest_enclosing_class = - nearest_enclosing_class(self.db(), self.index, self.scope()); + let nearest_enclosing_class = nearest_enclosing_class(db, self.index, self.scope()); let class_kind = nearest_enclosing_class.and_then(|class| { CodeGeneratorKind::from_class(self.db(), ClassLiteral::Static(class)) }); @@ -5079,10 +5374,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .as_name_expr() .is_some_and(|name| &name.id == "TYPE_CHECKING") { - if !KnownClass::Bool - .to_instance(self.db()) - .is_assignable_to(self.db(), declared.inner_type()) - { + if !KnownClass::Bool.to_instance(db, env).is_assignable_to( + db, + env, + declared.inner_type(), + ) { // annotation not assignable from `bool` is an error report_invalid_type_checking_constant(&self.context, target.into()); } else if self.in_stub() @@ -5105,8 +5401,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Handle various singletons. if let Some(name_expr) = target.as_name_expr() - && let Some(special_form) = - SpecialFormType::try_from_file_and_name(self.db(), self.file(), &name_expr.id) + && let Some(special_form) = SpecialFormType::try_from_file_and_name( + self.db(), + ImportingFile::File( + self.file(), + self.program_environment().resolver_environment(self.db()), + ), + &name_expr.id, + ) { declared.inner = Type::SpecialForm(special_form); } @@ -5147,7 +5449,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::unknown() } Type::KnownInstance(KnownInstanceType::LiteralStringAlias(ty)) - if ty.inner(self.db()).contains_self(self.db()) => + if ty.inner(self.db()).contains_self(db, env) => { Type::KnownInstance(KnownInstanceType::LiteralStringAlias( InternedType::new(self.db(), Type::unknown()), @@ -5203,23 +5505,27 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(name_expr) = target.as_name_expr() && !name_expr.id.starts_with("__") && !matches!(name_expr.id.as_str(), "_ignore_" | "_value_" | "_name_") - // Not bare Final (bare Final is allowed on enum members) - && !(declared.qualifiers.contains(TypeQualifiers::FINAL) - && matches!(declared.inner_type(), Type::Dynamic(DynamicType::Unknown))) - // Value type would be an enum member at runtime (exclude callables, - // which are never members) - && !inferred_ty.is_subtype_of( - self.db(), - Type::Callable(CallableType::unknown(self.db())) - .top_materialization(self.db()), + && ( + // Not bare Final (bare Final is allowed on enum members) + !(declared.qualifiers.contains(TypeQualifiers::FINAL) + && matches!(declared.inner_type(), Type::Dynamic(DynamicType::Unknown))) + ) + && ( + // Value type would be an enum member at runtime (exclude callables, + // which are never members) + !inferred_ty.is_subtype_of( + db, + env, + Type::Callable(CallableType::unknown(self.db())) + .top_materialization(db, env), + ) ) { 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::Class - && let Some(class) = - nearest_enclosing_class(self.db(), self.index, self.scope()) - && is_enum_class_by_inheritance(self.db(), class) + && let Some(class) = nearest_enclosing_class(db, self.index, self.scope()) + && is_enum_class_by_inheritance(db, env, class) && !enum_ignored_names(self.db(), self.scope()).contains(&name_expr.id) && let Some(builder) = self .context @@ -5274,27 +5580,54 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.infer_definition(assignment); } else { // Non-name assignment targets are inferred as ordinary expressions, not definitions. - self.infer_augment_assignment(assignment); + if let Ok(result_ty) = self.infer_augment_assignment(assignment) { + let target = assignment.target.as_ref(); + match target { + ast::Expr::Attribute(attribute) => { + let object_ty = self.expression_type(&attribute.value); + self.validate_attribute_assignment( + attribute, + target, + object_ty, + attribute.attr.id(), + &mut |_, _| result_ty, + true, + ); + } + ast::Expr::Subscript(subscript) => { + let object_ty = self.expression_type(&subscript.value); + let slice_ty = self.expression_type(&subscript.slice); + self.validate_subscript_assignment( + subscript, + target, + object_ty, + &mut |_, _| slice_ty, + &mut |_, _| result_ty, + ); + } + _ => {} + } + } if let ast::Expr::Attribute(attr_expr) = assignment.target.as_ref() { - let object_ty = self.expression_type(&attr_expr.value); self.report_undeclared_protocol_attribute(attr_expr); - self.validate_final_attribute_assignment(attr_expr, object_ty, attr_expr.attr.id()); } } } + /// Infer an augmented operator, returning its recovery type if the operation fails. fn infer_augmented_op( &mut self, assignment: &ast::StmtAugAssign, target_type: Type<'db>, value_expr: &ast::Expr, infer_value_ty: &mut dyn FnMut(&mut Self, TypeContext<'db>) -> Type<'db>, - ) -> Type<'db> { + ) -> Result, Type<'db>> { + let db = self.db(); + let env = self.program_environment(); // 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(); // Fall back to non-augmented binary operator inference. let binary_return_ty = |builder: &mut Self, value_ty| { @@ -5311,7 +5644,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // here: an augmented assignment has no lowering to the backing // function (rewriting `a += b` re-evaluates the target), so // accepting it would put the checker and the runtime at odds - .unwrap_or_else(|| { + .ok_or_else(|| { report_unsupported_augmented_assignment( &builder.context, assignment, @@ -5330,14 +5663,27 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // equally applicable type contexts for each union member. infer_value_ty.infer_loud(self, TypeContext::default()); - union.map(db, |&elem_type| { - self.infer_augmented_op( + let mut operation_failed = false; + let result_ty = union.map(db, env, |&elem_type| { + match self.infer_augmented_op( assignment, elem_type, value_expr, &mut |builder, tcx| infer_value_ty.infer_silent(builder, tcx), - ) - }) + ) { + Ok(ty) => ty, + Err(recovery_ty) => { + operation_failed = true; + recovery_ty + } + } + }); + + if operation_failed { + Err(result_ty) + } else { + Ok(result_ty) + } } _ => { @@ -5349,14 +5695,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { infer_value_ty, ) { - return typed_dict_update_ty; + return Ok(typed_dict_update_ty); } let ast_arguments = [ArgOrKeyword::Arg(value_expr)]; let mut call_arguments = CallArguments::positional([Type::unknown()]); let call = self.infer_and_try_call_dunder( - db, target_type, op.in_place_dunder(), MemberLookupPolicy::NO_INSTANCE_FALLBACK, @@ -5365,9 +5710,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { &mut |builder, (_, _, tcx)| infer_value_ty(builder, tcx), TypeContext::default(), ); - match call { - Ok(outcome) => outcome.return_type(db), + Ok(outcome) => Ok(outcome.return_type(db, env)), Err(CallDunderError::MethodNotAvailable) => { let value_ty = infer_value_ty(self, TypeContext::default()); binary_return_ty(self, value_ty) @@ -5376,11 +5720,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { bindings: outcome, .. }) => { let value_ty = outcome.type_for_argument(&call_arguments, 0); - UnionType::from_two_elements( - db, - outcome.return_type(db), - binary_return_ty(self, value_ty), - ) + match binary_return_ty(self, value_ty) { + Ok(binary_ty) => Ok(UnionType::from_two_elements( + db, + env, + outcome.return_type(db, env), + binary_ty, + )), + Err(recovery_ty) => Err(UnionType::from_two_elements( + db, + env, + outcome.return_type(db, env), + recovery_ty, + )), + } } Err(CallDunderError::CallError(_, bindings, _)) => { let value_ty = bindings.type_for_argument(&call_arguments, 0); @@ -5390,7 +5743,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { target_type, value_ty, ); - bindings.return_type(db) + Err(bindings.return_type(db, env)) } } } @@ -5402,12 +5755,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { assignment: &'ast ast::StmtAugAssign, definition: Definition<'db>, ) { - let target_ty = self.infer_augment_assignment(assignment); - self.add_binding(assignment.into(), definition) + let target_ty = self + .infer_augment_assignment(assignment) + .unwrap_or_else(|recovery_ty| recovery_ty); + self.add_binding(assignment.target.as_ref().into(), definition) .insert(self, target_ty); } - fn infer_augment_assignment(&mut self, assignment: &ast::StmtAugAssign) -> Type<'db> { + fn infer_augment_assignment( + &mut self, + assignment: &ast::StmtAugAssign, + ) -> Result, Type<'db>> { let ast::StmtAugAssign { range: _, node_index: _, @@ -5417,28 +5775,37 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } = assignment; // Resolve the target type, assuming a load context. - let target_type = match &**target { + let target_result = match &**target { ast::Expr::Name(name) => { let previous_value = self.infer_name_load(name, TypeContext::default()); self.store_expression_type(target, previous_value); - previous_value + Ok(previous_value) } ast::Expr::Attribute(attr) => { - let previous_value = self.infer_attribute_load(attr); + let result = self.infer_attribute_load(attr); + let previous_value = result.unwrap_or_else(|recovery_ty| recovery_ty); self.store_expression_type(target, previous_value); - previous_value + result } ast::Expr::Subscript(subscript) => { - let previous_value = self.infer_subscript_load(subscript, TypeContext::default()); + let result = self.infer_subscript_load(subscript, TypeContext::default()); + let previous_value = result.unwrap_or_else(|recovery_ty| recovery_ty); self.store_expression_type(target, previous_value); - previous_value + result } - _ => self.infer_expression(target, TypeContext::default()), + _ => Ok(self.infer_expression(target, TypeContext::default())), }; - self.infer_augmented_op(assignment, target_type, value, &mut |builder, tcx| { - builder.infer_expression(value, tcx) - }) + let target_type = target_result.unwrap_or_else(|recovery_ty| recovery_ty); + let operation_result = + self.infer_augmented_op(assignment, target_type, value, &mut |builder, tcx| { + builder.infer_expression(value, tcx) + }); + + match (target_result, operation_result) { + (Ok(_), Ok(result_ty)) => Ok(result_ty), + (_, Ok(recovery_ty) | Err(recovery_ty)) => Err(recovery_ty), + } } fn infer_dict_key_assignment_definition( @@ -5462,20 +5829,24 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { iterable: &ast::Expr, expression_type: impl FnMut(&ast::Expr) -> Type<'db>, ) -> Option> { + let db = self.db(); + let env = self.program_environment(); let element_types = - extract_fixed_length_iterable_element_types(self.db(), iterable, expression_type)?; + extract_fixed_length_iterable_element_types(db, env, iterable, expression_type)?; if element_types.is_empty() { None } else { Some(UnionType::from_elements( - self.db(), + db, + env, element_types.iter().copied(), )) } } fn infer_for_statement(&mut self, for_statement: &ast::StmtFor) { + let db = self.db(); let ast::StmtFor { range: _, node_index: _, @@ -5498,9 +5869,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { element_type } else { + let env = builder.program_environment(); iterable_type - .iterate(builder.db()) - .homogeneous_element_type(builder.db()) + .iterate(db, env) + .homogeneous_element_type(db, env) } }); @@ -5519,6 +5891,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { for_stmt: &ForStmtDefinitionKind<'db>, definition: Definition<'db>, ) { + let db = self.db(); let iterable = for_stmt.iterable(self.module()); let target = for_stmt.target(self.module()); @@ -5545,15 +5918,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { element_type } else { + let env = self.program_environment(); iterable_type .try_iterate_with_mode( - self.db(), + db, + env, EvaluationMode::from_is_async(for_stmt.is_async()), ) - .map(|tuple| tuple.homogeneous_element_type(self.db())) + .map(|tuple| tuple.homogeneous_element_type(db, env)) .unwrap_or_else(|err| { err.report_diagnostic(&self.context, iterable_type, iterable.into()); - err.fallback_element_type(self.db()) + err.fallback_element_type(db, env) }) } } @@ -5565,6 +5940,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn infer_while_statement(&mut self, while_statement: &ast::StmtWhile) { + let db = self.db(); let ast::StmtWhile { range: _, node_index: _, @@ -5575,7 +5951,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let test_ty = self.infer_standalone_expression(test, TypeContext::default()); - if let Err(err) = test_ty.try_bool(self.db()) { + if let Err(err) = test_ty.try_bool(db, self.program_environment()) { err.report_diagnostic(&self.context, &**test); } else { self.check_condition(test); @@ -5586,6 +5962,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn infer_assert_statement(&mut self, assert: &ast::StmtAssert) { + let db = self.db(); let ast::StmtAssert { range: _, node_index: _, @@ -5595,7 +5972,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let test_ty = self.infer_standalone_expression(test, TypeContext::default()); - if let Err(err) = test_ty.try_bool(self.db()) { + if let Err(err) = test_ty.try_bool(db, self.program_environment()) { err.report_diagnostic(&self.context, &**test); } else { self.check_condition(test); @@ -5605,6 +5982,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn infer_raise_statement(&mut self, raise: &ast::StmtRaise) { + let db = self.db(); let ast::StmtRaise { range: _, node_index: _, @@ -5612,18 +5990,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { cause, } = raise; - let base_exception_type = KnownClass::BaseException.to_subclass_of(self.db()); - let base_exception_instance = KnownClass::BaseException.to_instance(self.db()); + let env = self.program_environment(); + let base_exception_type = KnownClass::BaseException.to_subclass_of(db, env); + let base_exception_instance = KnownClass::BaseException.to_instance(db, env); let can_be_raised = - UnionType::from_two_elements(self.db(), base_exception_type, base_exception_instance); + UnionType::from_two_elements(db, env, base_exception_type, base_exception_instance); let can_be_exception_cause = - UnionType::from_two_elements(self.db(), can_be_raised, Type::none(self.db())); + UnionType::from_two_elements(db, env, can_be_raised, Type::none(db, env)); if let Some(raised) = exc { let raised_type = self.infer_expression(raised, TypeContext::default()); - if !raised_type.is_assignable_to(self.db(), can_be_raised) { + if !raised_type.is_assignable_to(db, env, can_be_raised) { report_invalid_exception_raised(&self.context, raised, raised_type); } } @@ -5631,22 +6010,24 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(cause) = cause { let cause_type = self.infer_expression(cause, TypeContext::default()); - if !cause_type.is_assignable_to(self.db(), can_be_exception_cause) { + if !cause_type.is_assignable_to(db, env, can_be_exception_cause) { report_invalid_exception_cause(&self.context, cause, cause_type); } } } fn infer_return_statement(&mut self, ret: &ast::StmtReturn) { + let db = self.db(); + let env = self.program_environment(); let tcx = if ret.value.is_some() { - nearest_enclosing_function(self.db(), self.index, self.scope()) + nearest_enclosing_function(db, self.index, self.scope()) .map(|func| { // When inferring expressions within a function body, // the expected type passed should be the "raw" type, // i.e. type variables in the return type are non-inferable, // and the return types of async functions are not wrapped in `CoroutineType[...]`. let return_ty = same_module_uncached_raw_signature( - self.db(), + db, func, ReturnCallableTypeVarScope::Lexical, ) @@ -5658,7 +6039,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let file_scope_id = self.scope().file_scope_id(self.db()); let context_ty = if file_scope_id.is_generator_function(self.index) { return_ty - .generator_return_type(self.db()) + .generator_return_type(db, env) .unwrap_or(return_ty) } else { return_ty @@ -5677,7 +6058,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .map_or(ret.range(), |value| value.range()); self.record_return_type(ty, range); } else { - self.record_return_type(Type::none(self.db()), ret.range()); + self.record_return_type(Type::none(db, env), ret.range()); } } @@ -5723,7 +6104,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { continue; } } - if !module_type_implicit_global_symbol(self.db(), self.file(), name) + if !module_type_implicit_global_symbol(self.db(), self.program_file(), name) .place .is_undefined() { @@ -5737,10 +6118,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; let mut diag = builder.into_diagnostic(format_args!("Invalid global declaration of `{name}`")); - diag.set_primary_message(format_args!( + diag.set_primary_annotation_message(format_args!( "`{name}` has no declarations or bindings in the global scope" )); - diag.info("This limits ty's ability to make accurate inferences about the boundness and types of global-scope symbols"); + diag.info( + "This limits ty's ability to make accurate inferences \ + about the boundness and types of global-scope symbols", + ); diag.info(format_args!( "Consider adding a declaration to the global scope, e.g. `{name}: int`" )); @@ -5748,11 +6132,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn module_type_from_name(&self, module_name: &ModuleName) -> Option> { - resolve_module(self.db(), self.file(), module_name) - .map(|module| Type::module_literal(self.db(), self.file(), module)) + let db = self.db(); + let importing_file = ImportingFile::File( + self.file(), + self.program_environment().resolver_environment(db), + ); + resolve_module(db, importing_file, module_name) + .map(|module| Type::module_literal(self.db(), self.program_file(), module)) } fn infer_decorator(&mut self, decorator: &ast::Decorator) -> Type<'db> { + let env = self.program_environment(); let ast::Decorator { range: _, node_index: _, @@ -5765,6 +6155,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // downstream type checking treats them like the user wrote `@typing.final` if let Some(target) = crate::types::function::synthetic_decorator_target_type( self.db(), + env, self.file(), decorator, ) { @@ -5800,6 +6191,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { call_expression: &ast::ExprCall, return_ty: Type<'db>, ) -> Type<'db> { + let db = self.db(); + let env = self.program_environment(); let arguments = &call_expression.arguments; let [decorated_expression] = &arguments.args[..] else { return return_ty; @@ -5811,12 +6204,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let decorated_ty = self.get_or_infer_expression(decorated_expression, TypeContext::default()); let call_arguments = CallArguments::positional([decorated_ty]); - let Ok(bindings) = decorator_ty.try_call(self.db(), &call_arguments) else { + let Ok(bindings) = decorator_ty.try_call(db, env, &call_arguments) else { return return_ty; }; - transparent_callable_decorator_result(self.db(), &bindings, decorated_ty) - .unwrap_or(return_ty) + transparent_callable_decorator_result(db, env, &bindings, decorated_ty).unwrap_or(return_ty) } /// Apply a decorator to a function or class type and return the resulting type. @@ -5831,20 +6223,25 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) -> Type<'db> { fn propagate_callable_kind<'d>( db: &'d dyn Db, + env: &ProgramEnvironment<'d>, ty: Type<'d>, kind: CallableTypeKind, provenance: CallableFunctionProvenance, ) -> Option> { match ty { // parameter-only marker; behaves as the type a body sees (bound of `Key`) - Type::Overlapping(overlapping) => { - propagate_callable_kind(db, overlapping.value_type(db), kind, provenance) - } + Type::Overlapping(overlapping) => propagate_callable_kind( + db, + env, + overlapping.value_type(db, env), + kind, + provenance, + ), Type::Restricted(restricted) => { - propagate_callable_kind(db, restricted.value_type(db), kind, provenance) + propagate_callable_kind(db, env, restricted.value_type(db), kind, provenance) } Type::Deferred(deferred) => { - propagate_callable_kind(db, deferred.reduced(db), kind, provenance) + propagate_callable_kind(db, env, deferred.reduced(db, env), kind, provenance) } Type::Callable(callable) => Some(Type::Callable(CallableType::new( db, @@ -5852,11 +6249,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { kind, provenance, ))), - Type::Union(union) => union.try_map(db, |element| { - propagate_callable_kind(db, *element, kind, provenance) + Type::Union(union) => union.try_map(db, env, |element| { + propagate_callable_kind(db, env, *element, kind, provenance) }), Type::TypeAlias(alias) => { - propagate_callable_kind(db, alias.value_type(db), kind, provenance) + propagate_callable_kind(db, env, alias.value_type(db), kind, provenance) } // Intersections are currently not handled here because that would require // the decorator to be explicitly annotated as returning an intersection. @@ -5892,7 +6289,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { | Type::NewTypeInstance(_) => None, } } + let db = self.db(); + let env = self.program_environment(); // For FunctionLiteral, get the kind directly without computing the full signature. // This avoids a query cycle when the function has default parameter values, since // computing the signature requires evaluating those defaults which may trigger @@ -5905,7 +6304,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ), )), _ => decorated_ty - .try_upcast_to_callable(self.db()) + .try_upcast_to_callable(db, env) .and_then(CallableTypes::exactly_one) .and_then(|callable| match callable.kind(self.db()) { kind @ (CallableTypeKind::FunctionLike @@ -5918,20 +6317,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; let call_arguments = CallArguments::positional([decorated_ty]); - let (return_ty, decorator_bindings) = - match decorator_ty.try_call(self.db(), &call_arguments) { - Ok(bindings) => (bindings.return_type(self.db()), Some(bindings)), - Err(CallError(_, bindings)) => { - bindings.report_diagnostics(&self.context, decorator_node.into()); - (bindings.return_type(self.db()), None) - } - }; + let (return_ty, decorator_bindings) = match decorator_ty.try_call(db, env, &call_arguments) + { + Ok(bindings) => (bindings.return_type(db, env), Some(bindings)), + Err(CallError(_, bindings)) => { + bindings.report_diagnostics(&self.context, decorator_node.into()); + (bindings.return_type(db, env), None) + } + }; // TODO: Remove this special case once the new constraint solver can preserve // per-overload ParamSpec/return correlations for transparent callable decorators. if let Some(decorator_bindings) = decorator_bindings.as_ref() && let Some(result) = - transparent_callable_decorator_result(self.db(), decorator_bindings, decorated_ty) + transparent_callable_decorator_result(db, env, decorator_bindings, decorated_ty) { return result; } @@ -5943,7 +6342,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // extended explanation. propagatable_kind .and_then(|(kind, provenance)| { - propagate_callable_kind(self.db(), return_ty, kind, provenance) + propagate_callable_kind(db, env, return_ty, kind, provenance) }) .unwrap_or(return_ty) } @@ -5951,7 +6350,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { #[expect(clippy::too_many_arguments)] fn infer_and_try_call_dunder( &mut self, - db: &'db dyn Db, object: Type<'db>, name: &str, lookup_policy: MemberLookupPolicy, @@ -5960,8 +6358,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { infer_argument_ty: &mut dyn FnMut(&mut Self, ArgExpr<'db, '_>) -> Type<'db>, call_expression_tcx: TypeContext<'db>, ) -> Result, CallDunderError<'db>> { + let db = self.db(); + let env = self.program_environment(); match object - .member_lookup_with_policy(db, name, lookup_policy) + .member_lookup_with_policy(db, env, name, lookup_policy) .place { Place::Defined(DefinedPlace { @@ -5970,9 +6370,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { provenance, .. }) => { - let mut bindings = self - .bindings_for_call(dunder_callable) - .match_parameters(db, argument_types); + let mut bindings = self.bindings_for_call(dunder_callable).match_parameters( + db, + env, + argument_types, + ); if let Err(call_error) = self.infer_and_check_argument_types( ast_arguments, @@ -6011,6 +6413,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let db = self.db(); let constraints = ConstraintSetBuilder::new(); let initial_argument_types = argument_types.clone(); + let env = self.program_environment(); // Keep track of which arguments match generic parameters. let mut generic_arguments = SmallVec::<[bool; 8]>::with_capacity(argument_types.len()); @@ -6024,7 +6427,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // typevar occurrences across all overload candidates. Note that the set of overload candidates // stays stable across all iterations. bindings.visit_type_context_callables(&mut |binding| { - let candidate_overload_indices = binding.candidate_overload_indices(db, argument_types); + let candidate_overload_indices = + binding.candidate_overload_indices(db, env, argument_types); has_generic_context |= candidate_overload_indices.iter().any(|&overload_index| { binding.overloads()[overload_index] @@ -6045,8 +6449,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { continue; } - let typevar_occurrences = - overload.typevar_occurrences_for_parameter(db, binding, argument_index); + let typevar_occurrences = overload.typevar_occurrences_for_parameter( + db, + env, + binding, + argument_index, + ); *is_generic |= typevar_occurrences > 0; overload_typevar_occurrences += typevar_occurrences; } @@ -6074,7 +6482,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // If the type context is a union, attempt to narrow to a specific element. let narrow_targets = call_expression_tcx - .narrow_targets(db) + .narrow_targets(db, env) // We only need to attempt narrowing on generic calls, otherwise the type // context has no effect. .filter(|_| has_generic_context) @@ -6087,12 +6495,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .signature .generic_context .map(|generic_context| generic_context.inferable_typevars(db)) - .unwrap_or(InferableTypeVars::None); + .unwrap_or(TypeVarSet::None); !overload .return_ty - .when_assignable_to(db, narrowed_ty, &constraints, inferable) - .is_never_satisfied(db) + .when_assignable_to(db, env, narrowed_ty, &constraints, inferable) + .is_never_satisfied(db, env) }) { return None; } @@ -6143,8 +6551,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // cases where the constraint solver is not smart enough to solve complex unions. // We should see revisit this after the new constraint solver is implemented. if !speculative_bindings - .return_type(db) - .is_assignable_to(db, narrowed_ty) + .return_type(db, env) + .is_assignable_to(db, env, narrowed_ty) { return None; } @@ -6164,10 +6572,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { for narrowed_ty in std::iter::chain( narrow_targets .iter() - .filter(|ty| ty.may_prefer_declared_type(db)), + .filter(|ty| ty.may_prefer_declared_type(db, env)), narrow_targets .iter() - .filter(|ty| !ty.may_prefer_declared_type(db)), + .filter(|ty| !ty.may_prefer_declared_type(db, env)), ) { if let Some(result) = try_narrow(*narrowed_ty) { if teardown_expression_cache { @@ -6228,6 +6636,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { call_expression_tcx: TypeContext<'db>, candidates: &OverloadSet, ) -> Result<(), CallErrorKind> { + let db = self.db(); + let env = self.program_environment(); let requires_overload_evaluation = requires_overload_evaluation(candidates); let arguments_tcx = self.collect_call_arguments_type_context( baseline_argument_types, @@ -6249,7 +6659,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); return bindings.check_types_impl( - self.db(), + db, + env, constraints, argument_types, call_expression_tcx, @@ -6272,7 +6683,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); let result = bindings.check_types_impl( - self.db(), + db, + env, constraints, argument_types, call_expression_tcx, @@ -6367,6 +6779,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { next_bindings = bindings.clone(); let _ = next_bindings.check_types_impl( db, + self.program_environment(), constraints, &next_argument_types, call_expression_tcx, @@ -6407,6 +6820,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Discard any non-matching constructors overloads now that the inferred types have converged. let result = next_bindings.finalize_argument_inference( db, + self.program_environment(), &converged_argument_types, &self.dataclass_field_specifiers, ); @@ -6460,6 +6874,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { fn add_overloads_from_binding<'a, 'db>( db: &'db dyn Db, file: ruff_db::files::File, + env: &ProgramEnvironment<'db>, overloads_with_binding: &mut OverloadsWithBinding<'a, 'db>, binding: &'a CallableBinding<'db>, constraints: &ConstraintSetBuilder<'db>, @@ -6471,6 +6886,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let specialization = overload.argument_type_context_specialization( db, file, + env, constraints, call_expression_tcx, ); @@ -6481,6 +6897,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let specialization = overload.argument_type_context_specialization( db, file, + env, constraints, call_expression_tcx, ); @@ -6490,10 +6907,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { overloads_with_binding.push((overload, binding, specialization)); } } - let db = self.db(); let file = self.file(); + let env = self.program_environment(); + // Collect the set of candidate overloads and bindings. let mut overloads_with_binding: OverloadsWithBinding = Vec::new(); if let Some(candidates) = candidates { @@ -6501,6 +6919,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let specialization = overload.argument_type_context_specialization( db, file, + env, constraints, call_expression_tcx, ); @@ -6512,6 +6931,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { add_overloads_from_binding( db, file, + env, &mut overloads_with_binding, binding, constraints, @@ -6531,6 +6951,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { |overload: &Binding<'db>, binding: &CallableBinding<'db>, specialization| { overload.argument_type_context( db, + env, constraints, binding, argument_types, @@ -6712,7 +7133,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { fn infer_expression(&mut self, expression: &ast::Expr, tcx: TypeContext<'db>) -> 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." + "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, tcx) @@ -6909,11 +7332,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { expression: &ast::Expr, tcx: TypeContext<'db>, ) -> Type<'db> { + let db = self.db(); let mut ty = match expression { ast::Expr::NoneLiteral(ast::ExprNoneLiteral { range: _, node_index: _, - }) => Type::none(self.db()), + }) => Type::none(db, self.program_environment()), 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, tcx), @@ -7007,13 +7431,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// Applies the provided type context to an already inferred type. fn apply_type_context(&mut self, mut ty: Type<'db>, tcx: TypeContext<'db>) -> Type<'db> { + let db = self.db(); + let env = self.program_environment(); // Avoid promoting explicitly annotated literal values. if let Type::LiteralValue(literal) = ty && let Some(tcx) = tcx.annotation() && let literal_tcx @ (Type::Union(_) | Type::LiteralValue(_)) = tcx - .resolve_type_alias(self.db()) - .filter_union(self.db(), |ty| ty.as_literal_value().is_some()) - && ty.is_assignable_to(self.db(), literal_tcx) + .resolve_type_alias(db) + .filter_union(db, |ty| ty.as_literal_value().is_some()) + && ty.is_assignable_to(db, env, literal_tcx) { ty = Type::LiteralValue(literal.to_unpromotable()); } @@ -7027,6 +7453,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// /// This lets `list` in a `Callable[[], list[str]]` context be treated as `list[str]`. fn specialize_generic_class_from_context(&self, ty: Type<'db>, target: Type<'db>) -> Type<'db> { + let env = self.program_environment(); // TODO: The constraint-set assignability rules should already be // able to determine that `list` (coerced into a callable) is assignable // to `Callable[[], list[str]]` when `_T@list = str`. However, when @@ -7080,7 +7507,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let Some(class_generic_context) = class.generic_context(db) else { return ty; }; - let Some(source_callable) = ty.try_upcast_to_callable(db) else { + let Some(source_callable) = ty.try_upcast_to_callable(db, env) else { return ty; }; // The callable relation existentially solves variables bound by each signature. Keep @@ -7099,7 +7526,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { variables .peek() .is_some() - .then(|| GenericContext::from_typevar_instances(db, variables)) + .then(|| GenericContext::from_typevar_instances(db, env, variables)) }); Signature::new_generic( signature_generic_context, @@ -7114,9 +7541,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let inferable = class_generic_context.inferable_typevars(db); let constraints = ConstraintSetBuilder::new(); let path_bounds = source_callable - .into_type(db) - .assignable_solutions_with_inferable(db, Type::Callable(target_callable), inferable); - let Solutions::Constrained(solutions) = path_bounds.solve(db, &constraints) else { + .into_type(db, env) + .assignable_solutions_with_inferable( + db, + env, + Type::Callable(target_callable), + inferable, + ); + let Solutions::Constrained(solutions) = path_bounds.solve(db, env, &constraints) else { return ty; }; @@ -7126,14 +7558,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { for binding in solution { let inferred_ty = binding .solution - .filter_union(db, |ty| !ty.has_unspecialized_type_var(db)); - if inferred_ty.has_unspecialized_type_var(db) { + .filter_union(db, |ty| !ty.has_unspecialized_type_var(db, env)); + if inferred_ty.has_unspecialized_type_var(db, env) { continue; } type_context_mappings .entry(binding.bound_typevar.identity(db)) - .and_modify(|existing| existing.add(db, inferred_ty)) + .and_modify(|existing| existing.add(db, env, inferred_ty)) .or_insert_with(|| UnionAccumulator::new(inferred_ty)); } } @@ -7145,7 +7577,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let type_context_mappings: FxHashMap, Type<'db>> = type_context_mappings .into_iter() - .map(|(identity, accumulator)| (identity, accumulator.into_type(db))) + .map(|(identity, accumulator)| (identity, accumulator.into_type(db, env))) .collect(); let specialized = Type::from(class.apply_specialization(db, |generic_context| { generic_context.specialize_recursive( @@ -7155,7 +7587,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .map(|typevar| type_context_mappings.get(&typevar.identity(db)).copied()), ) })); - if specialized.is_assignable_to(db, Type::Callable(target_callable)) { + if specialized.is_assignable_to(db, env, Type::Callable(target_callable)) { specialized } else { ty @@ -7211,12 +7643,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn union_expected_types(&mut self, expected_types: &FxHashMap>) { + let db = self.db(); + let env = self.program_environment(); // Non-empty only if the producing inference collected, i.e. the file is open if expected_types.is_empty() { return; } - let db = self.db(); #[expect( clippy::iter_over_hash_type, reason = "expected types for distinct expressions are unioned independently" @@ -7224,36 +7657,38 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { for (expression, ty) in expected_types { self.expected_types .entry(*expression) - .and_modify(|existing| *existing = UnionType::from_two_elements(db, *existing, *ty)) + .and_modify(|existing| { + *existing = UnionType::from_two_elements(db, env, *existing, *ty); + }) .or_insert(*ty); } } fn infer_number_literal_expression(&self, literal: &ast::ExprNumberLiteral) -> Type<'db> { + let db = self.db(); + let env = self.program_environment(); let ast::ExprNumberLiteral { range: _, node_index: _, value, } = literal; - let db = self.db(); - match value { ast::Number::Int(n) => n .as_i64() .map(Type::int_literal) - .unwrap_or_else(|| KnownClass::Int.to_instance(db)), + .unwrap_or_else(|| KnownClass::Int.to_instance(db, env)), ast::Number::Float(v) => { if self.is_basedpython_file() { Type::float_literal(*v) } else { - KnownClass::Float.to_instance(db) + KnownClass::Float.to_instance(db, env) } } ast::Number::Complex { real, imag } => { if self.is_basedpython_file() { Type::complex_literal(db, *real, *imag) } else { - KnownClass::Complex.to_instance(db) + KnownClass::Complex.to_instance(db, env) } } } @@ -7300,6 +7735,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn infer_fstring_expression(&mut self, fstring: &ast::ExprFString) -> Type<'db> { + let db = self.db(); + let env = self.program_environment(); let ast::ExprFString { range: _, node_index: _, @@ -7348,11 +7785,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { collector.add_non_literal_string_expression(); } else { - let str_ty = ty.str(self.db()); + let str_ty = ty.str(db, env); if let Some(literal) = str_ty.as_string_literal() { collector.push_str(literal.value(self.db())); - } else if str_ty - .is_subtype_of(self.db(), Type::literal_string()) + } else if str_ty.is_subtype_of(db, env, Type::literal_string()) { collector.add_literal_string_expression(); } else { @@ -7368,10 +7804,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } } - collector.string_type(self.db()) + collector.string_type(&self.context) } fn infer_tstring_expression(&mut self, tstring: &ast::ExprTString) -> Type<'db> { + let db = self.db(); let ast::ExprTString { value, .. } = tstring; for tstring in value { for element in &tstring.elements { @@ -7395,14 +7832,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } } - KnownClass::Template.to_instance(self.db()) + KnownClass::Template.to_instance(db, self.program_environment()) } fn infer_ellipsis_literal_expression( &mut self, _literal: &ast::ExprEllipsisLiteral, ) -> Type<'db> { - KnownClass::EllipsisType.to_instance(self.db()) + let db = self.db(); + KnownClass::EllipsisType.to_instance(db, self.program_environment()) } /// Build a synthesized `typing.NamedTuple` class for an anonymous named @@ -7449,6 +7887,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { _ => Kind::Fixed, } } + let env = self.program_environment(); let kinds: Vec = tuple.elts.iter().map(classify).collect(); let has_variadic = kinds.iter().any(|k| matches!(k, Kind::Variadic)); if !has_variadic { @@ -7490,7 +7929,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // additional variadic merges into the existing // variable-length type via union (degraded form) let existing = variable.take().unwrap(); - variable = Some(crate::types::UnionType::from_elements(db, [existing, ty])); + variable = Some(crate::types::UnionType::from_elements( + db, + env, + [existing, ty], + )); } else { variable = Some(ty); } @@ -7503,9 +7946,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let variable = variable.unwrap_or_else(Type::object); let tt = if prefix.is_empty() && suffix.is_empty() { - Some(TupleType::homogeneous(db, variable)) + Some(TupleType::homogeneous(db, env, variable)) } else { - TupleType::mixed(db, prefix, variable, suffix) + TupleType::mixed(db, env, prefix, variable, suffix) }; Some(Type::tuple(tt)) } @@ -7548,6 +7991,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { use crate::types::typed_dict::{TypedDictFieldBuilder, TypedDictSchema}; use ty_python_core::global_scope; + let env = self.program_environment(); if dict.items.is_empty() { return None; @@ -7574,7 +8018,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // basedpython `{**Kwargs}`: the pack contributes no fields until it is // specialized, so it is carried on the anchor and spliced in by the type mapping if let Some(pack) = self.keyword_pack_reference(&item.value) { - pack.display(db).to_string().hash(&mut hasher); + pack.display(db, env).to_string().hash(&mut hasher); packs.push(pack); continue; } @@ -7586,7 +8030,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let name = Name::new(s.value.to_str()); let field_ty = self.infer_type_expression(&item.value); name.as_str().hash(&mut hasher); - field_ty.display(db).to_string().hash(&mut hasher); + field_ty.display(db, env).to_string().hash(&mut hasher); schema.insert( name, TypedDictFieldBuilder::new(field_ty).required(true).build(), @@ -7597,7 +8041,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let truncated = hasher.finish() as u32; let class_name = Name::new(format!("_TypedDict_{truncated:08x}")); - let module_scope = global_scope(db, self.file()); + let module_scope = global_scope(db, db.program_file(self.file())); let anchor = DynamicTypedDictAnchor::Synthesized { scope: module_scope, range: dict.range(), @@ -7605,7 +8049,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { packs: packs.into_boxed_slice(), }; let td = DynamicTypedDictLiteral::new(db, class_name, anchor, TypedDictModule::Typing); - Type::ClassLiteral(ClassLiteral::DynamicTypedDict(td)).to_instance_approximation(db) + Type::ClassLiteral(ClassLiteral::DynamicTypedDict(td)).to_instance_approximation(db, env) } /// basedpython: synthesize the protocol type an inline `protocol(...)` type expression @@ -7616,6 +8060,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// duplicate member name is rejected by the parser, so the last binding for a name wins here. fn synthesize_inline_protocol(&mut self, protocol: &ast::ExprProtocolType) -> Type<'db> { use crate::types::protocol_class::InlineProtocolMember; + let env = self.program_environment(); let mut members: Vec<(Name, InlineProtocolMember<'db>)> = Vec::with_capacity(protocol.members.len()); @@ -7642,7 +8087,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { builder.into_diagnostic(format_args!( "Only a keyword-variadic pack can be unpacked into an inline \ protocol, not `{}`", - ty.display(self.db()) + ty.display(self.db(), env) )); } } @@ -7673,14 +8118,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { builder.into_diagnostic(format_args!( "`{}` is not a valid inline protocol member; expected `name: T`, \ `def name(...) -> T`, or `**Pack`", - ty.display(self.db()) + ty.display(self.db(), env) )); } } } } - Type::inline_protocol(self.db(), members, packs.into_boxed_slice()) + Type::inline_protocol(self.db(), env, members, packs.into_boxed_slice()) } fn synthesize_anon_named_tuple_class( @@ -7692,6 +8137,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { use std::hash::{Hash, Hasher}; use ty_python_core::global_scope; + let env = self.program_environment(); let db = self.db(); let mut fields: Vec> = Vec::with_capacity(tuple.elts.len()); @@ -7741,7 +8187,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut hasher = DefaultHasher::new(); for field in spec.fields(db) { field.name.as_str().hash(&mut hasher); - field.ty.display(db).to_string().hash(&mut hasher); + field.ty.display(db, env).to_string().hash(&mut hasher); } #[expect(clippy::cast_possible_truncation)] let truncated = hasher.finish() as u32; @@ -7752,7 +8198,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // identity (driven entirely by `spec`) determines the synthesized // class. Different shapes produce different specs; identical shapes // unify across the file. - let module_scope = global_scope(db, self.file()); + let module_scope = global_scope(db, db.program_file(self.file())); let anchor = DynamicNamedTupleAnchor::ScopeOffset { scope: module_scope, offset: 0, @@ -7772,6 +8218,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// we promote `Literal` types when inferring the elements of the tuple. /// This provides a huge speedup on files that have very large unannotated tuple literals. const MAX_TUPLE_LENGTH_FOR_UNANNOTATED_LITERAL_INFERENCE: usize = 64; + let env = self.program_environment(); + let db = self.db(); // basedpython anonymous named tuple type literal in value position. // E.g. `a = (name: str, age: int)` is a type-alias-like expression @@ -7789,7 +8237,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let class_lit = self.synthesize_anon_named_tuple_class(tuple, /* is_type_form = */ false); let literal_instance = class_lit - .to_instance_approximation(self.db()) + .to_instance_approximation(self.db(), env) .unwrap_or(class_lit); // an expected anonymous-named-tuple shape wins over the literal one, the // same coercion the plain-tuple spelling gets below: `b: P = (name="a")` @@ -7798,8 +8246,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // the runtime accepts if let Some(target) = tcx .annotation() - .and_then(|annotation| find_anon_nt_target(self.db(), annotation)) - && literal_instance.is_assignable_to(self.db(), target) + .and_then(|annotation| find_anon_nt_target(self.db(), env, annotation)) + && literal_instance.is_assignable_to(self.db(), env, target) { return target; } @@ -7832,18 +8280,22 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // follows what the annotation means rather than how it is spelled — // which is also what the transpiler wraps on. #[expect(clippy::items_after_statements, reason = "helper colocated with use")] - fn find_anon_nt_target<'db>(db: &'db dyn crate::Db, ty: Type<'db>) -> Option> { + fn find_anon_nt_target<'db>( + db: &'db dyn crate::Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + ) -> Option> { let ty = ty.resolve_type_alias(db); if let Type::NominalInstance(instance) = ty && let crate::types::class::ClassLiteral::DynamicNamedTuple(nt) = - instance.class(db).class_literal(db) + instance.class(db, env).class_literal(db) && nt.name(db).as_str().starts_with("_AnonNamedTuple_") { return Some(ty); } if let Type::Union(union) = ty { for member in union.elements(db).iter().copied() { - if let Some(found) = find_anon_nt_target(db, member) { + if let Some(found) = find_anon_nt_target(db, env, member) { return Some(found); } } @@ -7852,10 +8304,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } if let Some(target_instance) = tcx .annotation() - .and_then(|a| find_anon_nt_target(self.db(), a)) + .and_then(|a| find_anon_nt_target(self.db(), env, a)) && let Type::NominalInstance(instance) = target_instance && let crate::types::class::ClassLiteral::DynamicNamedTuple(nt) = - instance.class(self.db()).class_literal(self.db()) + instance.class(self.db(), env).class_literal(self.db()) && nt.name(self.db()).as_str().starts_with("_AnonNamedTuple_") { let spec = match nt.anchor(self.db()) { @@ -7883,7 +8335,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut all_assignable = true; for (elt, field) in tuple.elts.iter().zip(fields.iter()) { let elt_ty = self.infer_expression(elt, TypeContext::new(Some(field.ty))); - if !elt_ty.is_assignable_to(self.db(), field.ty) { + if !elt_ty.is_assignable_to(self.db(), env, field.ty) { all_assignable = false; } elt_tys.push(elt_ty); @@ -7891,11 +8343,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if all_assignable { return target_instance; } - return Type::heterogeneous_tuple(self.db(), elt_tys); + return Type::heterogeneous_tuple(self.db(), env, elt_tys); } } } + let env = self.program_environment(); let ast::ExprTuple { range: _, node_index: _, @@ -7911,13 +8364,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Remove any union elements of the annotation that are unrelated to the tuple type. let tcx = tcx.map(|annotation| { let inferable = KnownClass::Tuple - .try_to_class_literal(self.db()) - .and_then(|class| class.generic_context(self.db())) - .map(|generic_context| generic_context.inferable_typevars(self.db())) - .unwrap_or(InferableTypeVars::None); + .try_to_class_literal(db, env) + .and_then(|class| class.generic_context(db)) + .map(|generic_context| generic_context.inferable_typevars(db)) + .unwrap_or(TypeVarSet::None); annotation.filter_disjoint_elements( - self.db(), - Type::homogeneous_tuple(self.db(), Type::unknown()), + db, + env, + Type::homogeneous_tuple(db, env, Type::unknown()), inferable, ) }); @@ -7925,7 +8379,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut is_homogeneous_tuple_annotation = false; let annotated_tuple = tcx - .known_specialization(self.db(), KnownClass::Tuple) + .known_specialization(db, env, KnownClass::Tuple) .and_then(|specialization| { let spec = specialization .tuple(self.db()) @@ -7939,7 +8393,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { is_homogeneous_tuple_annotation = true; } - spec.resize(self.db(), TupleLength::Fixed(elts.len())).ok() + spec.resize(db, env, TupleLength::Fixed(elts.len())).ok() }); // TODO: this is a simplification for now. @@ -7956,14 +8410,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .unwrap_or_default(); let mut annotated_elt_tys = annotated_elt_tys.into_iter(); - let db = self.db(); - let mut infer_element = |elt: &ast::Expr| { let annotated_elt_ty = annotated_elt_tys.by_ref().next(); - let ctx = if can_use_type_context { + let element_tcx = if can_use_type_context { let expected = if elt.is_starred_expr() { let expected_element = annotated_elt_ty.unwrap_or_else(Type::object); - Some(KnownClass::Iterable.to_specialized_instance(db, &[expected_element])) + Some(KnownClass::Iterable.to_specialized_instance(db, env, &[expected_element])) } else { annotated_elt_ty }; @@ -7974,9 +8426,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if tuple.len() > MAX_TUPLE_LENGTH_FOR_UNANNOTATED_LITERAL_INFERENCE { // Promote literals for very large unannotated tuples, // to avoid pathological performance issues - self.infer_expression(elt, ctx).promote(db) + self.infer_expression(elt, element_tcx).promote(db, env) } else { - self.infer_expression(elt, ctx) + self.infer_expression(elt, element_tcx) } }; @@ -7988,7 +8440,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Fine to use `iterate` rather than `try_iterate` here: // errors from iterating over something not iterable will have been // emitted in the `infer_element` call above. - let mut spec = element_type.iterate(db).into_owned(); + let mut spec = element_type.iterate(db, env).into_owned(); let known_length = match &*starred.value { ast::Expr::List(ast::ExprList { elts, .. }) @@ -8005,20 +8457,21 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(known_length) = known_length { spec = spec - .resize(db, TupleLength::Fixed(known_length)) + .resize(db, env, TupleLength::Fixed(known_length)) .unwrap_or(spec); } - builder = builder.concat(db, &spec); + builder = builder.concat(db, env, &spec); } else { builder.push(infer_element(element)); } } - Type::tuple(TupleType::new(db, &builder.build())) + Type::tuple(TupleType::new(db, env, &builder.build())) } fn infer_list_expression(&mut self, list: &ast::ExprList, tcx: TypeContext<'db>) -> Type<'db> { + let db = self.db(); let ast::ExprList { range: _, node_index: _, @@ -8037,10 +8490,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { &mut infer_elt_ty, tcx, ) - .unwrap_or_else(|| KnownClass::List.to_specialized_instance(self.db(), &[Type::unknown()])) + .unwrap_or_else(|| { + KnownClass::List.to_specialized_instance( + db, + self.program_environment(), + &[Type::unknown()], + ) + }) } fn infer_set_expression(&mut self, set: &ast::ExprSet, tcx: TypeContext<'db>) -> Type<'db> { + let db = self.db(); let ast::ExprSet { range: _, node_index: _, @@ -8061,7 +8521,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { &mut infer_elt_ty, tcx, ) - .unwrap_or_else(|| KnownClass::Set.to_specialized_instance(self.db(), &[Type::unknown()])) + .unwrap_or_else(|| { + KnownClass::Set.to_specialized_instance( + db, + self.program_environment(), + &[Type::unknown()], + ) + }) } /// Infers a set element, optionally with a fallback context for an incomplete `TypedDict` key. @@ -8075,6 +8541,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { elt_tcx: TypeContext<'db>, fallback_tcx: TypeContext<'db>, ) -> Type<'db> { + let db = self.db(); let inference_tcx = if elt_tcx.annotation().is_some() { elt_tcx } else { @@ -8089,7 +8556,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { self.store_expected_type( elt, - UnionType::from_two_elements(self.db(), elt_ty, fallback_ty), + UnionType::from_two_elements(db, self.program_environment(), elt_ty, fallback_ty), ); } @@ -8120,6 +8587,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn infer_dict_expression(&mut self, dict: &ast::ExprDict, tcx: TypeContext<'db>) -> Type<'db> { + let db = self.db(); + let env = self.program_environment(); let ast::ExprDict { range: _, node_index: _, @@ -8146,7 +8615,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut has_dict_compatible_fallback = false; for element in union_elements { - let element = element.resolve_type_alias(self.db()); + let element = element.resolve_type_alias(db); if let Some(typed_dict) = element.as_typed_dict() { typed_dicts.push(typed_dict); @@ -8156,7 +8625,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut speculative_builder = self.speculate_without_diagnostics(); has_dict_compatible_fallback = speculative_builder .infer_dict_expression(dict, TypeContext::new(Some(element))) - .is_assignable_to(self.db(), element); + .is_assignable_to(db, env, element); } } @@ -8205,7 +8674,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Successfully narrowed to a subset of typed dicts. if !narrowed_tys.is_empty() { - return UnionType::from_elements(self.db(), narrowed_tys); + return UnionType::from_elements(db, env, narrowed_tys); } } } @@ -8235,7 +8704,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { tcx, ) .unwrap_or_else(|| { - KnownClass::Dict.to_specialized_instance(self.db(), &[Type::unknown(), Type::unknown()]) + KnownClass::Dict.to_specialized_instance(db, env, &[Type::unknown(), Type::unknown()]) }) } @@ -8249,7 +8718,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { tcx: TypeContext<'db>, ) -> Option> { let db = self.db(); - + let env = self.program_environment(); let mut try_narrow = |narrowed_ty| { let mut speculative_builder = self.speculate(); @@ -8263,7 +8732,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { )?; // Ensure the inferred return type is assignable to the narrowed declared type. - if !inferred_ty.is_assignable_to(db, narrowed_ty) { + if !inferred_ty.is_assignable_to(db, env, narrowed_ty) { return None; } @@ -8274,11 +8743,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // If the type context is a union, attempt to narrow to a specific element. for narrowed_ty in tcx - .narrow_targets(db) + .narrow_targets(db, env) .as_deref() .into_iter() .flatten() - .filter(|ty| ty.class_specialization(db).is_some()) + .filter(|ty| ty.class_specialization(db, env).is_some()) { if let Some(result) = try_narrow(*narrowed_ty) { return Some(result); @@ -8303,11 +8772,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { infer_elt_expression: &mut dyn FnMut(&mut Self, ArgExpr<'db, 'expr>) -> Type<'db>, tcx: TypeContext<'db>, ) -> Option> { + let db = self.db(); + let env = self.program_environment(); + // Extract the type variable `T` from `list[T]` in typeshed. let elt_tys = |collection_class: KnownClass| { let collection_alias = collection_class - .try_to_class_literal(self.db())? - .identity_specialization(self.db()) + .try_to_class_literal(db, env)? + .identity_specialization(db) .into_generic_alias()?; let generic_context = collection_alias @@ -8332,17 +8804,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; let constraints = ConstraintSetBuilder::new(); - let inferable = generic_context.inferable_typevars(self.db()); - let identity_instance = Type::instance(self.db(), ClassType::Generic(collection_alias)); - let mut builder = SpecializationBuilder::new(self.db(), &constraints, inferable); + let inferable = generic_context.inferable_typevars(db); + let identity_instance = Type::instance(db, env, ClassType::Generic(collection_alias)); + let mut builder = SpecializationBuilder::new(db, env, &constraints, inferable); // Remove any union elements of that are unrelated to the collection type. // // For example, we only want the `list[int]` from `annotation: list[int] | None` if // `collection_ty` is `list`. let tcx = tcx.map(|annotation| { - let collection_ty = collection_class.to_instance(self.db()); - annotation.filter_disjoint_elements(self.db(), collection_ty, inferable) + let collection_ty = collection_class.to_instance(db, env); + annotation.filter_disjoint_elements(db, env, collection_ty, inferable) }); // Collect type constraints from the declared element types. @@ -8362,13 +8834,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .annotation() .map(|tcx| tcx.resolve_type_alias(self.db())) && matches!(tcx, Type::NominalInstance(_)) - && let Some(specialization) = tcx.known_specialization(self.db(), collection_class) + && let Some(specialization) = tcx.known_specialization(db, env, collection_class) && specialization.generic_context(self.db()) == generic_context && generic_context.variables(self.db()).all(|typevar| { !typevar.is_paramspec(self.db()) && typevar .typevar(self.db()) - .bound_or_constraints(self.db()) + .bound_or_constraints(db, env) .is_none() }) { @@ -8380,33 +8852,33 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .zip(specialization.types(self.db())) { let inferred_ty = inferred_ty - .filter_union(self.db(), |ty| { + .filter_union(db, |ty| { !ty.as_typevar() .is_some_and(|tv| tv.is_inferable(self.db(), inferable)) }) - .filter_union(self.db(), |ty| !ty.has_unspecialized_type_var(self.db())); - if inferred_ty.has_unspecialized_type_var(self.db()) { + .filter_union(db, |ty| !ty.has_unspecialized_type_var(db, env)); + if inferred_ty.has_unspecialized_type_var(db, env) { continue; } let identity = typevar.identity(self.db()); elt_tcx_constraints.insert(identity, UnionAccumulator::new(inferred_ty)); - elt_tcx_variance.insert(identity, typevar.variance(self.db())); + elt_tcx_variance.insert(identity, typevar.variance(db)); } } else if let Some(tcx) = tcx.annotation() - && tcx.class_specialization(self.db()).is_some() + && tcx.class_specialization(self.db(), env).is_some() { let db = self.db(); let path_bounds = - identity_instance.assignable_solutions_with_inferable(db, tcx, inferable); + identity_instance.assignable_solutions_with_inferable(db, env, tcx, inferable); let solutions = path_bounds.solve_with(|variance, path_bound| { let identity = path_bound.bound_typevar.identity(db); elt_tcx_variance .entry(identity) .and_modify(|current| *current = current.join(variance)) .or_insert(variance); - PathBounds::default_solve(db, &constraints, path_bound) + PathBounds::default_solve(db, env, &constraints, path_bound) }); match solutions { @@ -8435,15 +8907,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // type context from an outer generic call. If the type context is // a union, we try to keep any concrete elements. let inferred_ty = inferred_ty - .filter_union(db, |ty| !ty.has_unspecialized_type_var(db)); - if inferred_ty.has_unspecialized_type_var(db) { + .filter_union(db, |ty| !ty.has_unspecialized_type_var(db, env)); + if inferred_ty.has_unspecialized_type_var(db, env) { continue; } let identity = binding.bound_typevar.identity(db); elt_tcx_constraints .entry(identity) - .and_modify(|existing| existing.add(db, inferred_ty)) + .and_modify(|existing| { + existing.add(db, env, inferred_ty); + }) .or_insert_with(|| UnionAccumulator::new(inferred_ty)); } } @@ -8457,11 +8931,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - let db = self.db(); let elt_tcx_constraints: FxHashMap, Type<'db>> = elt_tcx_constraints .into_iter() - .map(|(identity, accumulator)| (identity, accumulator.into_type(db))) + .map(|(identity, accumulator)| (identity, accumulator.into_type(db, env))) .collect(); (elt_tcx_constraints, elt_tcx_variance) @@ -8512,7 +8985,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { let Some(elt) = elt else { continue }; let elt_tcx = if elt.is_starred_expr() && collection_class != KnownClass::Dict { - Type::homogeneous_tuple(self.db(), elt_tcx) + Type::homogeneous_tuple(db, env, elt_tcx) } else { elt_tcx }; @@ -8520,7 +8993,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { infer_elt_expression(self, (i, elt, TypeContext::new(Some(elt_tcx)))); inferred_elt_tys[i] = Some(inferred_elt_ty); - if !inferred_elt_ty.is_assignable_to(self.db(), elt_tcx) { + if !inferred_elt_ty.is_assignable_to(db, env, elt_tcx) { compatible = false; } } @@ -8529,13 +9002,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if compatible { let class_type = collection_alias.origin(self.db()).apply_specialization( - self.db(), + db, |generic_context| { generic_context - .specialize_recursive(self.db(), specialization.into_iter().map(Some)) + .specialize_recursive(db, specialization.into_iter().map(Some)) }, ); - return Type::from(class_type).to_instance_approximation(self.db()); + return Type::from(class_type).to_instance_approximation(db, env); } pre_inferred_elt_tys = Some(inferred_elts); @@ -8611,7 +9084,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let unpack_ty = infer_elt_expression(self, (1, value_expr, tcx)); let Some((unpacked_key_ty, unpacked_value_ty)) = - unpack_ty.unpack_keys_and_items(self.db()) + unpack_ty.unpack_keys_and_items(db, env) else { if let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, value_expr) @@ -8619,9 +9092,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut diag = builder .into_diagnostic("Argument expression after ** must be a mapping type"); - diag.set_primary_message(format_args!( + diag.set_primary_annotation_message(format_args!( "Found `{}`", - unpack_ty.display(self.db()) + unpack_ty.display(db, env) )); } @@ -8631,14 +9104,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut elt_tys = elt_tys.clone(); if let Some((key_ty, value_ty)) = elt_tys.next_tuple() { tuple_size_promotion_constraints.record_unpromotable_type( - self.db(), + db, + env, key_ty.identity(self.db()), - unpacked_key_ty.promote_in(self.db(), self.file()), + unpacked_key_ty.promote_in(self.db(), env, self.file()), ); tuple_size_promotion_constraints.record_unpromotable_type( - self.db(), + db, + env, value_ty.identity(self.db()), - unpacked_value_ty.promote_in(self.db(), self.file()), + unpacked_value_ty.promote_in(self.db(), env, self.file()), ); builder.infer(Type::TypeVar(key_ty), unpacked_key_ty).ok()?; @@ -8668,7 +9143,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .copied() .map(|tcx| { if elt.is_starred_expr() && collection_class != KnownClass::Dict { - Type::homogeneous_tuple(self.db(), tcx) + Type::homogeneous_tuple(db, env, tcx) } else { tcx } @@ -8684,7 +9159,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Simplify the inference based on a non-covariant declared type. if let Some(elt_tcx) = elt_tcx.filter(|_| !elt_tcx_variance[&elt_ty_identity].is_covariant()) - && inferred_elt_ty.is_assignable_to(self.db(), elt_tcx) + && inferred_elt_ty.is_assignable_to(db, env, elt_tcx) { continue; } @@ -8699,19 +9174,35 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // from, so a module that asked for strict numerics has to get one // here too — otherwise appending a `float` infers `list[int | float]` // and the buffer is lost - inferred_elt_ty.promote_in(self.db(), self.file()) + // + // A covariant context is an upper bound, so promotion must not widen an + // otherwise compatible element beyond that bound. In particular, promoting + // an exact float introduces `int`, which is not assignable to an + // exact-float context. + let promoted_elt_ty = inferred_elt_ty.promote_in(self.db(), env, self.file()); + if let Some(elt_tcx) = elt_tcx + && elt_tcx_variance[&elt_ty_identity].is_covariant() + && promoted_elt_ty != inferred_elt_ty + && !promoted_elt_ty.is_assignable_to(db, env, elt_tcx) + && inferred_elt_ty.is_assignable_to(db, env, elt_tcx) + { + inferred_elt_ty + } else { + promoted_elt_ty + } }; let inferred_type_for_typevar = if elt.is_starred_expr() { inferred_elt_ty - .iterate(self.db()) - .homogeneous_element_type(self.db()) + .iterate(db, env) + .homogeneous_element_type(db, env) } else { inferred_elt_ty }; tuple_size_promotion_constraints.record_inferred_expression_type( - self.db(), + db, + env, elt_ty_identity, elt, inferred_type_for_typevar, @@ -8725,7 +9216,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let class_type = collection_alias .origin(self.db()) - .apply_specialization(self.db(), |_| { + .apply_specialization(db, |_| { builder.build_with(generic_context, |current_typevar, bounds| { let Some(lower) = bounds.and_then(|bounds| bounds.lower) else { // In fluid mode, an element typevar with no constraints comes from an @@ -8750,7 +9241,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // position unless an explicit annotation made them unpromotable — and, // like them, follow the file's numeric model, or a `float` element // widens back to `int | float` and the buffer is lost - lower.promote_in(self.db(), self.file()) + lower.promote_in(self.db(), env, self.file()) } else { lower }; @@ -8758,7 +9249,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let lower = if tuple_size_promotion_constraints .allow(current_typevar.identity(self.db())) { - lower.promote_tuple_size_in_union(self.db()) + lower.promote_tuple_size_in_union(db, env) } else { lower }; @@ -8767,7 +9258,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { lower // Promote singleton types to `T | Unknown` in inferred type parameters, // so that e.g. `[None]` is inferred as `list[None | Unknown]`. - .promote_singletons_recursively(self.db()) + .promote_singletons_recursively(db, env) } else { lower }; @@ -8776,7 +9267,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }) }); - let creation = Type::from(class_type).to_instance_approximation(self.db())?; + let creation = Type::from(class_type).to_instance_approximation(self.db(), env)?; if let Some(fluid_def) = fluid_def { // Combine the creation-time solution with the constraining events of the @@ -8815,32 +9306,35 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { tcx: TypeContext<'db>, evaluation_mode: EvaluationMode, ) -> TypeContext<'db> { + let db = self.db(); + let env = self.program_environment(); let Some(annotation) = tcx.annotation() else { return TypeContext::default(); }; - let db = self.db(); let yield_typevar = BoundTypeVarInstance::synthetic( db, + env, Name::new_static("_GeneratorYieldT"), TypeVarVariance::Covariant, ); let yield_ty = Type::TypeVar(yield_typevar); - let none = Type::none(db); + let none = Type::none(db, env); let generator_ty = if evaluation_mode.is_async() { - KnownClass::AsyncGeneratorType.to_specialized_instance(db, &[yield_ty, none]) + KnownClass::AsyncGeneratorType.to_specialized_instance(db, env, &[yield_ty, none]) } else { - KnownClass::GeneratorType.to_specialized_instance(db, &[yield_ty, none, none]) + KnownClass::GeneratorType.to_specialized_instance(db, env, &[yield_ty, none, none]) }; - let generic_context = GenericContext::from_typevar_instances(db, [yield_typevar]); + let generic_context = GenericContext::from_typevar_instances(db, env, [yield_typevar]); let path_bounds = generator_ty.assignable_solutions_with_inferable( db, + env, annotation, generic_context.inferable_typevars(db), ); let constraints = ConstraintSetBuilder::new(); - let Solutions::Constrained(solutions) = path_bounds.solve(db, &constraints) else { + let Solutions::Constrained(solutions) = path_bounds.solve(db, env, &constraints) else { return TypeContext::default(); }; @@ -8851,13 +9345,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { continue; } match &mut yield_tcx { - Some(accumulator) => accumulator.add(db, binding.solution), + Some(accumulator) => { + accumulator.add(db, env, binding.solution); + } None => yield_tcx = Some(UnionAccumulator::new(binding.solution)), } } } - TypeContext::new(yield_tcx.map(|accumulator| accumulator.into_type(db))) + TypeContext::new(yield_tcx.map(|accumulator| accumulator.into_type(db, env))) } fn infer_generator_expression( @@ -8865,6 +9361,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { generator: &ast::ExprGenerator, tcx: TypeContext<'db>, ) -> Type<'db> { + let db = self.db(); + let env = self.program_environment(); let ast::ExprGenerator { range: _, node_index: _, @@ -8884,18 +9382,22 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let evaluation_mode = EvaluationMode::from_is_async(scope_id.is_async_comprehension(self.index)); let yield_tcx = self.generator_yield_type_context(tcx, evaluation_mode); - let scope = scope_id.to_scope_id(self.db(), self.file()); + let scope = scope_id.to_scope_id(self.db(), self.program_file()); let inference = infer_scope_types(self.db(), scope, yield_tcx); self.extend_scope(inference); let yield_type = self.comprehension_element_type(elt, inference); if evaluation_mode.is_async() { - KnownClass::AsyncGeneratorType - .to_specialized_instance(self.db(), &[yield_type, Type::none(self.db())]) + KnownClass::AsyncGeneratorType.to_specialized_instance( + db, + env, + &[yield_type, Type::none(db, env)], + ) } else { KnownClass::GeneratorType.to_specialized_instance( - self.db(), - &[yield_type, Type::none(self.db()), Type::none(self.db())], + db, + env, + &[yield_type, Type::none(db, env), Type::none(db, env)], ) } } @@ -8905,11 +9407,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { element: &ast::Expr, inference: &ScopeInference<'db>, ) -> Type<'db> { + let db = self.db(); + let env = self.program_environment(); let element_type = inference.expression_type(element); if element.is_starred_expr() { element_type - .iterate(self.db()) - .homogeneous_element_type(self.db()) + .iterate(db, env) + .homogeneous_element_type(db, env) } else { element_type } @@ -8942,6 +9446,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { listcomp: &ast::ExprListComp, tcx: TypeContext<'db>, ) -> Type<'db> { + let db = self.db(); let ast::ExprListComp { range: _, node_index: _, @@ -8957,7 +9462,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { else { return Type::unknown(); }; - let scope = scope_id.to_scope_id(self.db(), self.file()); + let scope = scope_id.to_scope_id(self.db(), self.program_file()); let inference = infer_scope_types(self.db(), scope, tcx); self.extend_scope(inference); @@ -8968,7 +9473,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { inference, tcx, ) - .unwrap_or_else(|| KnownClass::List.to_specialized_instance(self.db(), &[Type::unknown()])) + .unwrap_or_else(|| { + KnownClass::List.to_specialized_instance( + db, + self.program_environment(), + &[Type::unknown()], + ) + }) } fn infer_set_comprehension_expression( @@ -8976,6 +9487,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { setcomp: &ast::ExprSetComp, tcx: TypeContext<'db>, ) -> Type<'db> { + let db = self.db(); let ast::ExprSetComp { range: _, node_index: _, @@ -8991,7 +9503,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { else { return Type::unknown(); }; - let scope = scope_id.to_scope_id(self.db(), self.file()); + let scope = scope_id.to_scope_id(self.db(), self.program_file()); let inference = infer_scope_types(self.db(), scope, tcx); self.extend_scope(inference); @@ -9002,7 +9514,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { inference, tcx, ) - .unwrap_or_else(|| KnownClass::Set.to_specialized_instance(self.db(), &[Type::unknown()])) + .unwrap_or_else(|| { + KnownClass::Set.to_specialized_instance( + db, + self.program_environment(), + &[Type::unknown()], + ) + }) } fn infer_dict_comprehension_expression( @@ -9010,6 +9528,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { dictcomp: &ast::ExprDictComp, tcx: TypeContext<'db>, ) -> Type<'db> { + let db = self.db(); let ast::ExprDictComp { range: _, node_index: _, @@ -9026,7 +9545,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { else { return Type::unknown(); }; - let scope = scope_id.to_scope_id(self.db(), self.file()); + let scope = scope_id.to_scope_id(self.db(), self.program_file()); let inference = infer_scope_types(self.db(), scope, tcx); self.extend_scope(inference); @@ -9038,7 +9557,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { tcx, ) .unwrap_or_else(|| { - KnownClass::Dict.to_specialized_instance(self.db(), &[Type::unknown(), Type::unknown()]) + KnownClass::Dict.to_specialized_instance( + db, + self.program_environment(), + &[Type::unknown(), Type::unknown()], + ) }) } @@ -9047,6 +9570,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { generator: &ast::ExprGenerator, tcx: TypeContext<'db>, ) { + let db = self.db(); let ast::ExprGenerator { range: _, node_index: _, @@ -9056,7 +9580,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } = generator; let elt_tcx = if elt.is_starred_expr() { - tcx.map(|yield_ty| KnownClass::Iterable.to_specialized_instance(self.db(), &[yield_ty])) + tcx.map(|yield_ty| { + KnownClass::Iterable.to_specialized_instance( + db, + self.program_environment(), + &[yield_ty], + ) + }) } else { tcx }; @@ -9168,6 +9698,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn infer_comprehension(&mut self, comprehension: &ast::Comprehension, is_first: bool) { + let db = self.db(); + let env = self.program_environment(); let ast::Comprehension { range: _, node_index: _, @@ -9186,17 +9718,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } else { builder.infer_maybe_standalone_expression(iter, tcx) } - .iterate(builder.db()) - .homogeneous_element_type(builder.db()) + .iterate(db, env) + .homogeneous_element_type(db, env) }); for expr in ifs { let guard_ty = self.infer_maybe_standalone_expression(expr, TypeContext::default()); - // Same shape as every other condition site: a guard whose type has no usable - // `__bool__` is skipped. Unlike the others this does not *report* that — ty has never - // reported `unsupported-bool-conversion` for a comprehension guard. - if guard_ty.try_bool(self.db()).is_ok() { - self.check_condition(expr); + // a guard whose type has no usable `__bool__` is reported like any other condition + // site, and the basedpython condition lints are skipped for it — there is no + // truthiness to reason about + match guard_ty.try_bool(self.db(), env) { + Ok(_) => self.check_condition(expr), + Err(err) => err.report_diagnostic(&self.context, expr), } } } @@ -9206,6 +9739,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { comprehension: &ComprehensionDefinitionKind<'db>, definition: Definition<'db>, ) { + let db = self.db(); let iterable = comprehension.iterable(self.module()); let target = comprehension.target(self.module()); @@ -9251,15 +9785,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(element_type) = element_type { element_type } else { + let env = self.program_environment(); iterable_type .try_iterate_with_mode( - self.db(), + db, + env, EvaluationMode::from_is_async(comprehension.is_async()), ) - .map(|tuple| tuple.homogeneous_element_type(self.db())) + .map(|tuple| tuple.homogeneous_element_type(db, env)) .unwrap_or_else(|err| { err.report_diagnostic(&self.context, iterable_type, iterable.into()); - err.fallback_element_type(self.db()) + err.fallback_element_type(db, env) }) } } @@ -9321,6 +9857,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// a value that is possibly undefined at this read means some path completes /// the statement without producing one. fn infer_statement_expression(&mut self, statement: &ast::ExprStatement) -> Type<'db> { + let env = self.program_environment(); // basedpython: a trailing lambda block's value is the call it stands for, // not a union of tail expressions, so it is neither collected nor subject // to the exhaustiveness check. the call is checked in the block's @@ -9344,9 +9881,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let db = self.db(); let file_scope_id = self.scope().file_scope_id(db); let use_def = self.index.use_def_map(file_scope_id); - let use_id = ast::ExprRef::Statement(statement).scoped_use_id(db, self.file()); + let use_id = + ast::ExprRef::Statement(statement).scoped_use_id(db, db.program_file(self.file())); let place = place_from_bindings_with_reachability_cache( db, + env, use_def.bindings_at_use(use_id), self.reachability_cache(), ) @@ -9412,6 +9951,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if_expression: &ast::ExprIf, tcx: TypeContext<'db>, ) -> Type<'db> { + let db = self.db(); + let env = self.program_environment(); let ast::ExprIf { range: _, node_index: _, @@ -9441,7 +9982,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { (body_ty, orelse_ty) }; - let truthiness = match test_ty.try_bool(self.db()) { + let truthiness = match test_ty.try_bool(self.db(), env) { Ok(truthiness) => { self.check_condition(test); truthiness @@ -9455,7 +9996,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { match truthiness { Truthiness::AlwaysTrue => body_ty, Truthiness::AlwaysFalse => orelse_ty, - Truthiness::Ambiguous => UnionType::from_two_elements(self.db(), body_ty, orelse_ty), + Truthiness::Ambiguous => UnionType::from_two_elements(db, env, body_ty, orelse_ty), } } @@ -9468,6 +10009,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { lambda_expression: &ast::ExprLambda, tcx: TypeContext<'db>, ) -> Type<'db> { + let db = self.db(); + let env = self.program_environment(); let ast::ExprLambda { range: _, node_index: _, @@ -9480,12 +10023,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let in_stub = self.in_stub(); let previous_deferred_state = std::mem::replace(&mut self.deferred_state, in_stub.into()); + // TODO: We could perform multi-inference here if there are multiple `Callable` annotations + // in the union/intersection. let callable_tcx = if let Some(tcx) = tcx.annotation() - // TODO: We could perform multi-inference here if there are multiple `Callable` annotations - // in the union/intersection. - && let Some(callable) = tcx - .filter_union(self.db(), Type::is_callable_type) - .as_callable() + && let Some(callable) = tcx.filter_union(db, Type::is_callable_type).as_callable() { match callable.signatures(self.db()).overloads.as_slice() { [signature] => Some(signature), @@ -9530,7 +10071,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // basedpython: mirrors the unannotated function parameter rule, so that a // lambda's own signature is checked at its call sites. a lambda body is a // single expression, so there is nothing else to read and no hole is opened - default_ty.map(|default_ty| default_ty.promote(builder.db())) + default_ty.map(|default_ty| default_ty.promote(builder.db(), env)) } else { None } @@ -9544,7 +10085,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let ctx_ty = parameter_types.next(); let default_ty = param.default().map(|default_expr| { self.infer_expression(default_expr, TypeContext::default()) - .replace_parameter_defaults(self.db()) + .replace_parameter_defaults(self.db(), env) }); let parameter_base = Parameter::positional_only(Some(param.name().id.clone())) .with_optional_default_type(default_ty); @@ -9562,7 +10103,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let ctx_ty = parameter_types.next(); let default_ty = param.default().map(|default_expr| { self.infer_expression(default_expr, TypeContext::default()) - .replace_parameter_defaults(self.db()) + .replace_parameter_defaults(self.db(), env) }); let parameter_base = Parameter::positional_or_keyword(param.name().id.clone()) .with_optional_default_type(default_ty); @@ -9588,7 +10129,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .map(|param| { let default_ty = param.default().map(|default_expr| { self.infer_expression(default_expr, TypeContext::default()) - .replace_parameter_defaults(self.db()) + .replace_parameter_defaults(self.db(), env) }); let parameter_base = Parameter::keyword_only(param.name().id.clone()) .with_optional_default_type(default_ty); @@ -9611,7 +10152,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .chain(keyword_only) .chain(keyword_variadic); - Parameters::from_annotation(self.db(), parameters) + Parameters::from_annotation(db, env, parameters) } else { Parameters::empty() }; @@ -9625,7 +10166,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { return Type::unknown(); }; - let scope = scope_id.to_scope_id(self.db(), self.file()); + let scope = scope_id.to_scope_id(self.db(), self.program_file()); // explicit `-> return_type` annotation takes priority over Callable context let declared_return_ty = if let Some(returns_expr) = returns { @@ -9680,6 +10221,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { argument_type: Type<'db>, argument: &'ast ast::ArgOrKeyword, ) -> Option> { + let env = self.program_environment(); let db = self.db(); let file_scope_id = self.scope().file_scope_id(db); let use_def = self.index.use_def_map(file_scope_id); @@ -9710,10 +10252,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Collect the types of each distinct key. let mut elements: Vec<(&str, Type<'db>)> = Vec::new(); - - for bindings in use_def.multi_bindings_at_use(keyword.scoped_use_id(db, self.file())) { + for bindings in + use_def.multi_bindings_at_use(keyword.scoped_use_id(db, self.program_file())) + { let place = place_from_bindings_with_reachability_cache( db, + env, bindings.clone(), self.reachability_cache(), ); @@ -9749,6 +10293,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let getitem_protocol = Type::protocol_with_methods( db, + env, [( "__getitem__", CallableType::new( @@ -9764,6 +10309,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // as it may contain keys that were not explicitly assigned to. Some(IntersectionType::from_elements( db, + env, [argument_type, getitem_protocol], )) } @@ -9774,6 +10320,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { &mut self, arguments: &'a ast::Arguments, ) -> CallArguments<'a, 'db> { + let db = self.db(); + let env = self.program_environment(); let call_arguments = CallArguments::from_arguments(arguments, |arg_or_keyword, splatted_value| { let ty = self.get_or_infer_expression(splatted_value, TypeContext::default()); @@ -9796,7 +10344,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { iterable_type, value.as_ref().into(), ); - if let Err(err) = iterable_type.try_iterate(self.db()) { + if let Err(err) = iterable_type.try_iterate(self.db(), env) { err.report_diagnostic(&self.context, iterable_type, value.as_ref().into()); } } @@ -9810,7 +10358,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mapping_type = self.expression_type(&keyword.value); if mapping_type.as_paramspec_typevar(self.db()).is_some() - || mapping_type.unpack_keys_and_items(self.db()).is_some() + || mapping_type.unpack_keys_and_items(db, env).is_some() { continue; } @@ -9824,7 +10372,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { builder .into_diagnostic("Argument expression after ** must be a mapping type") - .set_primary_message(format_args!("Found `{}`", mapping_type.display(self.db()))); + .set_primary_annotation_message(format_args!( + "Found `{}`", + mapping_type.display(db, env) + )); } call_arguments @@ -9839,7 +10390,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { receiver_generic_context: Option>, call_specialization: Specialization<'db>, ) -> Option> { - let constraint = identity_instance.apply_specialization(self.db(), call_specialization); + let db = self.db(); + let env = self.program_environment(); + let constraint = identity_instance.apply_specialization(db, call_specialization); let Some(receiver_generic_context) = receiver_generic_context else { return Some(constraint); }; @@ -9848,7 +10401,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // types learned for the collection. Until collection-use constraints are represented as // projected constraint sets, avoid leaking those method-local typevars into the inferred // collection literal type. - if any_over_type(self.db(), constraint, false, |ty| { + if any_over_type(db, env, constraint, false, |ty| { ty.as_typevar().is_some_and(|typevar| { !receiver_generic_context.contains(self.db(), typevar.identity(self.db())) }) @@ -9876,10 +10429,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { value_ty: Type<'db>, target: Type<'db>, ) { + let env = self.program_environment(); // a statically-proven upcast (`B[int]() cast list[int]`) verifies // nothing at runtime, so no argument claim is dropped and the lint // would be a false positive - if cast_is_redundant(self.db(), value_ty, target) { + if cast_is_redundant(self.db(), env, value_ty, target) { return; } let db = self.db(); @@ -9887,10 +10441,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // decide in full assumes nothing: a reified type parameter compares its // runtime cell (`def f[T](x: list[T])` casting to `list[int]` lowers to // `T == int`), and a static fold needs no check at all - if let Some(alias) = crate::types::reified_infer::parametric_cast_target(db, target) + if let Some(alias) = crate::types::reified_infer::parametric_cast_target(db, env, target) && matches!( crate::types::reified_infer::classify_parametric_is( db, + env, self.file(), value_ty, alias, @@ -9907,20 +10462,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // member whose specialized type has no runtime spelling (a callable // attribute) does, and it has no runtime residue — the cast degrades to // an unchecked `typing.cast` - if cast_target_is_unverifiable_protocol(db, self.file(), target) { + if cast_target_is_unverifiable_protocol(db, env, self.file(), target) { let Some(builder) = self.context.report_lint(&ERASED_CAST_ARGUMENT, type_arg) else { return; }; let mut diagnostic = builder.into_diagnostic(format_args!( "`{}` cannot be checked at runtime", - target.display(db) + target.display(db, env) )); diagnostic.info( "a protocol member with no runtime spelling has no residue; the cast is unchecked", ); return; } - if !erases_type_arguments(db, self.file(), target) { + if !erases_type_arguments(db, env, self.file(), target) { return; } let Some(builder) = self.context.report_lint(&ERASED_CAST_ARGUMENT, type_arg) else { @@ -9928,9 +10483,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; let mut diagnostic = builder.into_diagnostic(format_args!( "Type arguments of `{}` are erased at runtime", - target.display(db) + target.display(db, env) )); - match runtime_check_target(db, self.file(), target) { + match runtime_check_target(db, env, self.file(), target) { Some(shallow) => diagnostic.info(format_args!( "a runtime check can only test `{shallow}`; the type arguments are assumed" )), @@ -9947,8 +10502,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { value_ty: Type<'db>, target: Type<'db>, ) { + let env = self.program_environment(); let db = self.db(); - if !value_ty.is_disjoint_from(db, target) { + if !value_ty.is_disjoint_from(db, env, target) { return; } let Some(builder) = self.context.report_lint(&NON_OVERLAPPING_CAST, value_arg) else { @@ -9956,8 +10512,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; builder.into_diagnostic(format_args!( "Cast from `{}` to `{}` is between non-overlapping types", - value_ty.display(db), - target.display(db) + value_ty.display(db, env), + target.display(db, env) )); } @@ -9969,6 +10525,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// so this use site is where the loss becomes observable. `!` (unwrap) or /// `cast object` (make it explicit) are the intended alternatives. fn report_optional_object_arguments(&mut self, call: &ast::ExprCall, bindings: &Bindings<'db>) { + let env = self.program_environment(); if !self.is_basedpython_file() { return; } @@ -9982,7 +10539,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let Some(parameter_type) = parameter_type else { continue; }; - if !target_swallows_optional(db, parameter_type) { + if !target_swallows_optional(db, env, parameter_type) { continue; } let value = argument.value(); @@ -9995,8 +10552,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; let mut diagnostic = builder.into_diagnostic(format_args!( "Optional `{}` is implicitly widened to `{}`", - argument_type.display(db), - parameter_type.display(db) + argument_type.display(db, env), + parameter_type.display(db, env) )); diagnostic.help("Unwrap it with `!`, or convert explicitly with `cast object`"); } @@ -10032,6 +10589,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { call_expression: &ast::ExprCall, tcx: TypeContext<'db>, ) -> Type<'db> { + let env = self.program_environment(); // basedpython ` cast ` parses as `ExprCall { is_cast: true, // func: Name("cast"), arguments: [type, value] }`. The synthetic `cast` // name is unresolved by design, so dispatch on the flag: infer the @@ -10055,7 +10613,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let target = self.infer_type_expression(type_arg); self.report_erased_cast_argument(type_arg, value_ty, target); self.report_non_overlapping_cast(value_arg, value_ty, target); - return UnionType::from_elements(self.db(), [target, Type::none(self.db())]); + return UnionType::from_elements(self.db(), env, [target, Type::none(self.db(), env)]); } // basedpython carries the call's expected type into the callee so a bare @@ -10100,6 +10658,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { callable_type: Type<'db>, return_type: Type<'db>, ) -> Type<'db> { + let env = self.program_environment(); if !self.is_basedpython_file() || !call_expression.arguments.keywords.is_empty() || call_expression @@ -10136,6 +10695,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } let deferred = DeferredType::build( self.db(), + env, &DeferredOperation::Call, operands.into_boxed_slice(), ); @@ -10164,6 +10724,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { callable_type: Type<'db>, return_type: Type<'db>, ) -> Type<'db> { + let env = self.program_environment(); if !self.is_basedpython_file() { return return_type; } @@ -10176,10 +10737,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let Type::NominalInstance(instance) = return_type else { return return_type; }; - if instance.class(db).class_literal(db) != constructed { + if instance.class(db, env).class_literal(db) != constructed { return return_type; } - RestrictedType::from_type_expression(db, TypeModifier::Final, return_type) + RestrictedType::from_type_expression(db, env, TypeModifier::Final, return_type) } fn infer_empty_list_or_set_constructor( @@ -10324,14 +10885,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { callable_type: Type<'db>, arguments: &ast::Arguments, ) -> bool { + let env = self.program_environment(); if !self.is_basedpython_file() { return false; } let db = self.db(); - let Some(model) = django::lookup_call_model(db, callable_type) else { + let Some(model) = django::lookup_call_model(db, env, callable_type) else { return false; }; - let lookups = django::lookup_expressions(db, self.file(), self.scope(), model, arguments); + let lookups = + django::lookup_expressions(db, env, self.file(), self.scope(), model, arguments); if lookups.is_empty() { return false; } @@ -10341,7 +10904,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { match node { ast::Expr::Attribute(attribute) if index > 0 => { ty = ty - .member(db, attribute.attr.as_str()) + .member(db, env, attribute.attr.as_str()) .place .ignore_possibly_undefined() .unwrap_or_else(Type::unknown); @@ -10359,7 +10922,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.store_expression_type(node, ty); } self.infer_maybe_standalone_expression(lookup.value, TypeContext::default()); - self.store_expression_type(lookup.argument, KnownClass::Bool.to_instance(db)); + self.store_expression_type(lookup.argument, KnownClass::Bool.to_instance(db, env)); } true } @@ -10373,12 +10936,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { bound_method: crate::types::BoundMethodType<'db>, call_expression: &ast::ExprCall, ) { + let env = self.program_environment(); let db = self.db(); let method_name = bound_method.function(db).name(db); let Some(kind) = django::queryset_method_kind(method_name.as_str()) else { return; }; - let Some(model) = django::queryset_or_manager_model(db, bound_method.self_instance(db)) + let Some(model) = + django::queryset_or_manager_model(db, env, bound_method.self_instance(db)) else { return; }; @@ -10410,9 +10975,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { continue; } let resolution = if is_create { - django::resolve_create_kwarg(db, model, key) + django::resolve_create_kwarg(db, env, model, key) } else { - django::resolve_lookup(db, model, key) + django::resolve_lookup(db, env, model, key) }; match resolution { django::FieldResolution::Unknown { model, segment } => { @@ -10426,19 +10991,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let operand = if is_create { operand } else { - UnionType::from_two_elements(db, operand, Type::none(db)) + UnionType::from_two_elements(db, env, operand, Type::none(db, env)) }; let value_ty = self.expression_type(&keyword.value); - if !value_ty.is_assignable_to(db, operand) { + if !value_ty.is_assignable_to(db, env, operand) { if let Some(builder) = self.context.report_lint(&INVALID_FIELD_LOOKUP, keyword) { builder.into_diagnostic(format_args!( "Value for `{key}` has type `{}`, \ but `{}` expects `{}`", - value_ty.display(db), + value_ty.display(db, env), model_name, - operand.display(db), + operand.display(db, env), )); } } @@ -10452,31 +11017,36 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if !is_create && self.is_basedpython_file() { for lookup in django::lookup_expressions( db, + env, self.file(), self.scope(), model, &call_expression.arguments, ) { let range = lookup.argument.range(); - match django::resolve_lookup(db, model, &lookup.key) { + match django::resolve_lookup(db, env, model, &lookup.key) { django::FieldResolution::Unknown { model, segment } => { report_unknown(range, &model, &segment, &lookup.key); } django::FieldResolution::Resolved { operand: Some(operand), } => { - let operand = - UnionType::from_two_elements(db, operand, Type::none(db)); + let operand = UnionType::from_two_elements( + db, + env, + operand, + Type::none(db, env), + ); let value_ty = self.expression_type(lookup.value); - if !value_ty.is_assignable_to(db, operand) + if !value_ty.is_assignable_to(db, env, operand) && let Some(builder) = self.context.report_lint(&INVALID_FIELD_LOOKUP, range) { builder.into_diagnostic(format_args!( "Value for `{}` has type `{}`, but `{model_name}` expects `{}`", lookup.key, - value_ty.display(db), - operand.display(db), + value_ty.display(db, env), + operand.display(db, env), )); } } @@ -10492,7 +11062,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; let name = literal.value.to_str(); if let django::FieldResolution::Unknown { model, segment } = - django::resolve_field_name(db, model, name) + django::resolve_field_name(db, env, model, name) { report_unknown(arg.range(), &model, &segment, &segment); } @@ -10510,8 +11080,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { overload: &mut Binding<'db>, call_expression: &ast::ExprCall, ) { + let env = self.program_environment(); let db = self.db(); - if file_to_module(db, function.file(db)).and_then(|module| module.known(db)) + if file_to_module(db, function.program_file(db).resolver_file(db)) + .and_then(|module| module.known(db)) != Some(KnownModule::Re) { return; @@ -10526,12 +11098,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // an already-compiled pattern brought its groups with it let (groups, any_str) = if let Some(groups) = regex::groups_of(db, pattern_ty) { - let Some(any_str) = regex::any_str_of(db, pattern_ty) else { + let Some(any_str) = regex::any_str_of(db, env, pattern_ty) else { return; }; (groups, any_str) } else { - let Some((text, any_str)) = regex::pattern_source(db, pattern_ty) else { + let Some((text, any_str)) = regex::pattern_source(db, env, pattern_ty) else { return; }; let flags = overload @@ -10567,6 +11139,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { overload.set_return_type(regex::refined_return( db, + env, call, groups, any_str, @@ -10582,11 +11155,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { overload: &mut Binding<'db>, call_expression: &ast::ExprCall, ) { + let env = self.program_environment(); let db = self.db(); let receiver = bound_method.self_instance(db); let (Some(groups), Some(any_str)) = ( regex::groups_of(db, receiver), - regex::any_str_of(db, receiver), + regex::any_str_of(db, env, receiver), ) else { return; }; @@ -10596,6 +11170,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(call) = regex::RegexCall::from_name(name) { overload.set_return_type(regex::refined_return( db, + env, call, groups, any_str, @@ -10626,7 +11201,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let Some(key) = self.regex_group_key(argument) else { return; }; - let Ok(ty) = regex::group_type(db, groups, any_str, key) else { + let Ok(ty) = regex::group_type(db, env, groups, any_str, key) else { self.report_no_such_regex_group(argument.into(), key); overload.set_return_type(Type::unknown()); return; @@ -10635,23 +11210,23 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } overload.set_return_type(match types[..] { [single] => single, - _ => Type::heterogeneous_tuple(db, types), + _ => Type::heterogeneous_tuple(db, env, types), }); } regex::MatchMember::Groups => { let unset = arguments.args.first().map(|it| self.expression_type(it)); - overload.set_return_type(regex::groups_type(db, groups, any_str, unset)); + overload.set_return_type(regex::groups_type(db, env, groups, any_str, unset)); } regex::MatchMember::GroupDict => { let unset = arguments.args.first().map(|it| self.expression_type(it)); - if let Some(ty) = regex::group_dict_type(db, groups, any_str, unset) { + if let Some(ty) = regex::group_dict_type(db, env, groups, any_str, unset) { overload.set_return_type(ty); } } regex::MatchMember::Position => { if let Some(argument) = arguments.args.first() && let Some(key) = self.regex_group_key(argument) - && regex::group_type(db, groups, any_str, key).is_err() + && regex::group_type(db, env, groups, any_str, key).is_err() { self.report_no_such_regex_group(argument.into(), key); } @@ -10683,6 +11258,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { callable_type: Type<'db>, arguments: &ast::Arguments, ) -> Option> { + let env = self.program_environment(); let db = self.db(); let is_substitution = |name: &str| regex::RegexCall::from_name(name) == Some(regex::RegexCall::Substitute); @@ -10695,7 +11271,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } Type::FunctionLiteral(function) if is_substitution(function.name(db).as_str()) - && file_to_module(db, function.file(db)) + && file_to_module(db, function.program_file(db).resolver_file(db)) .and_then(|module| module.known(db)) == Some(KnownModule::Re) => { @@ -10706,7 +11282,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(groups) = regex::groups_of(db, pattern_ty) { return Some(groups); } - let (text, _) = regex::pattern_source(db, pattern_ty)?; + let (text, _) = regex::pattern_source(db, env, pattern_ty)?; // no signature has been matched yet, so the `flags` parameter is // located by its position in `re.sub`/`re.subn` directly let verbose = self.regex_verbose_flag(arguments.find_argument_value("flags", 4))?; @@ -10769,18 +11345,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) -> Type<'db> { fn report_missing_implicit_constructor_call<'db>( context: &InferContext<'db, '_>, - db: &'db dyn Db, callable_type: Type<'db>, call_expression: &ast::ExprCall, bindings: &Bindings<'db>, ) { + let db = context.db(); + let env = context.program_environment(); if bindings.has_implicit_dunder_new_is_possibly_unbound() { if let Some(builder) = context.report_lint(&POSSIBLY_MISSING_IMPLICIT_CALL, call_expression) { builder.into_diagnostic(format_args!( "Method `__new__` on type `{}` may be missing.", - callable_type.display(db), + callable_type.display(db, env), )); } } @@ -10791,14 +11368,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { builder.into_diagnostic(format_args!( "Method `__init__` on type `{}` may be missing.", - callable_type.display(db), + callable_type.display(db, env), )); } } } + let db = self.db(); + let env = self.program_environment(); let ast::ExprCall { - range: _, + range_start: _, node_index: _, func, arguments, @@ -10897,17 +11476,31 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { 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(self.db()), + Type::SubclassOf(subclass) => subclass.subclass_of().into_class(db, env), _ => None, }; + if let Some(class) = class + && class.is_typed_dict(db) + { + return self.infer_typed_dict_constructor( + callable_type, + class, + call_expression, + call_expression_tcx, + ); + } + + // basedpython: a django lookup written as an expression names fields + // rather than values, so its own inference has to happen before the + // arguments are inferred as ordinary expressions // Prepare `TypedDict` constructor calls before variadic argument setup so field-directed // value inference becomes canonical before `**kwargs` expressions are inferred. let has_prepared_typed_dict_constructor = class .filter(|class| class.is_typed_dict(self.db())) .map(|class| { let typed_dict = TypedDictType::new(class); - let form = TypedDictConstructorForm::from_arguments(arguments); + let form = typed_dict::TypedDictConstructorForm::from_arguments(arguments); self.prepare_typed_dict_constructor( typed_dict, form, @@ -10917,9 +11510,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }) .is_some(); - // basedpython: a django lookup written as an expression names fields - // rather than values, so its own inference has to happen before the - // arguments are inferred as ordinary expressions let has_django_lookup_expressions = self.prepare_django_lookup_expressions(callable_type, arguments); @@ -10936,13 +11526,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Type::TypedDict(typed_dict_ty) = value_type && matches!(method_name, "get" | "pop" | "setdefault") && !arguments.args.is_empty() - - // Validate the key argument for `TypedDict` methods - && let Some(first_arg) = arguments.args.first() + && let Some(first_arg) = ( + // Validate the key argument for `TypedDict` methods + arguments.args.first() + ) && let Some(key) = (match first_arg { ast::Expr::StringLiteral(ast::ExprStringLiteral { - value: key_literal, - .. + value: key_literal, .. }) => Some(key_literal.to_str()), _ => self .speculate_without_diagnostics() @@ -10977,10 +11567,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { TypeContext::new(Some(field.declared_ty)), ) } else { - Type::none(self.db()) + Type::none(db, env) }; return UnionType::from_two_elements( - self.db(), + db, + env, field.declared_ty, default_ty, ); @@ -11006,8 +11597,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.context.report_lint(&INVALID_ARGUMENT_TYPE, first_arg) { builder.into_diagnostic(format_args!( - "Cannot {action} read-only extra item \"{key}\" {preposition} TypedDict `{}`", - Type::TypedDict(typed_dict_ty).display(self.db()), + "Cannot {action} read-only extra item \ + \"{key}\" {preposition} TypedDict `{}`", + Type::TypedDict(typed_dict_ty).display(db, env), )); } return Type::unknown(); @@ -11026,7 +11618,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { field.declared_ty, |default| { UnionType::from_two_elements( - self.db(), + db, + env, field.declared_ty, self.get_or_infer_expression( default, @@ -11099,7 +11692,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .report_lint(&INEFFECTIVE_FINAL, call_expression) { let mut diagnostic = builder.into_diagnostic( - "Type checkers will not prevent subclassing when `final()` is called as a function", + "Type checkers will not prevent subclassing \ + when `final()` is called as a function", ); diagnostic.info("Use `@final` as a decorator on a class or method instead"); } @@ -11110,10 +11704,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { match callable_type { Type::BoundMethod(bound_method) => { let function = bound_method.function(self.db()); - if let Some(class) = bound_method - .self_instance(self.db()) - .to_class_type(self.db()) - { + if let Some(class) = bound_method.self_instance(self.db()).to_class_type(db) { if function.is_classmethod(self.db()) && function.as_abstract_method(self.db(), class).is_some() && function.has_trivial_body(self.db()) @@ -11130,7 +11721,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::FunctionLiteral(function) if function.is_staticmethod(self.db()) => { if let ast::Expr::Attribute(ast::ExprAttribute { value, .. }) = func.as_ref() { let value_type = self.expression_type(value); - if let Some(class) = value_type.to_class_type(self.db()) { + if let Some(class) = value_type.to_class_type(db) { if function.as_abstract_method(self.db(), class).is_some() && function.has_trivial_body(self.db()) { @@ -11220,10 +11811,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { _ => {} } } - - let mut bindings = self - .bindings_for_call(callable_type) - .match_parameters(self.db(), &call_arguments); + let mut bindings = + self.bindings_for_call(callable_type) + .match_parameters(db, env, &call_arguments); // basedpython: fill unmatched `context` parameters from the `context` // declarations visible at this call site, before check/report. gated @@ -11236,6 +11826,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) { bindings.resolve_context_arguments( self.db(), + env, self.scope(), call_expression.range().start(), ); @@ -11243,7 +11834,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { report_missing_implicit_constructor_call( &self.context, - self.db(), callable_type, call_expression, &bindings, @@ -11259,7 +11849,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { &mut call_arguments, &mut |builder, (_, expr, tcx)| { let tcx = match substitution_groups { - Some(groups) => tcx.map(|ty| regex::attach_groups(builder.db(), ty, groups)), + Some(groups) => { + tcx.map(|ty| regex::attach_groups(builder.db(), env, ty, groups)) + } None => tcx, }; if has_prepared_typed_dict_constructor || has_django_lookup_expressions { @@ -11284,10 +11876,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let value_type = self.expression_type(value); if let Some(collection_def) = self.index.fluid_candidate_binding(value) - && let Some((collection_literal, _)) = value_type.class_specialization(self.db()) + && let Some((collection_literal, _)) = + value_type.class_specialization(self.db(), env) { let identity_instance = Type::instance( self.db(), + env, collection_literal.identity_specialization(self.db()), ); let collection_generic_context = collection_literal.generic_context(self.db()); @@ -11299,8 +11893,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut identity_bindings = self .speculate_without_diagnostics() .infer_attribute_load_impl(attribute, identity_instance) - .bindings(self.db()) - .match_parameters(self.db(), &call_arguments) + .unwrap_or_else(|recovery_ty| recovery_ty) + .bindings(db, env) + .match_parameters(db, env, &call_arguments) // Perform inference against the type variables on the receiver's generic context. .with_generic_context(self.db(), collection_generic_context); @@ -11325,7 +11920,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { for call_specialization in identity_bindings .iter_flat() .flat_map(CallableBinding::matching_overloads) - .filter_map(|(_, identity_overload)| identity_overload.specialization(db)) + .filter_map(|(_, identity_overload)| { + identity_overload.specialization(db, env) + }) { // Record the constraints on the receiver's generic context formed by // the arguments to this bound method call. @@ -11350,7 +11947,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Ok(()) => bindings, Err(_) => { bindings.report_diagnostics(&self.context, call_expression.into()); - let return_ty = bindings.return_type(self.db()); + let return_ty = bindings.return_type(self.db(), env); self.record_unsolved_typevar_call(call_expression, return_ty, &bindings); return return_ty; } @@ -11399,6 +11996,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } else { reified_infer::inferred_call_type_arguments( self.db(), + env, self.file(), callable_type, function, @@ -11432,7 +12030,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { diagnostic.info(format_args!( "inferred type `{}` for type parameter `{parameter}` has \ no runtime spelling — specialize with `{name}[...]`", - ty.display(self.db()), + ty.display(self.db(), env), )); } Some(ReifiedInferenceError::NoBinding) => { @@ -11463,7 +12061,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { overload, &call_arguments, call_expression, - self.file(), ); } self.check_regex_function_call(function_literal, overload, call_expression); @@ -11509,7 +12106,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(instance_ty) = self.infer_builtin_range_instance_type(callable_type, arguments, &call_arguments) { - bindings = bindings.with_constructed_instance_type(self.db(), instance_ty); + bindings = bindings.with_constructed_instance_type(db, instance_ty); } // basedpython: `float(...)` over literal arguments constructs a known float, and @@ -11521,11 +12118,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } let db = self.db(); - let return_ty = bindings.return_type(db); + let return_ty = bindings.return_type(db, env); let return_ty = match collection_initializer_class { Some(collection_class @ (KnownClass::List | KnownClass::Set)) if return_ty - .class_specialization(db) + .class_specialization(db, env) .is_some_and(|(class, _)| class.is_known(db, collection_class)) => { self.infer_empty_list_or_set_constructor( @@ -11605,6 +12202,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { starred: &ast::ExprStarred, tcx: TypeContext<'db>, ) -> Type<'db> { + let env = self.program_environment(); let ast::ExprStarred { range: _, node_index: _, @@ -11619,7 +12217,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if typevar.is_typevartuple(db) => { bind_typevar( - db, + self.db(), self.index, self.scope().file_scope_id(db), self.typevar_binding_context, @@ -11632,43 +12230,45 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(typevartuple) = typevartuple { return Type::tuple(TupleType::new( db, + env, &TupleSpecBuilder::with_capacity(0) - .concat_variadic_typevar(db, typevartuple) + .concat_variadic_typevar(db, env, typevartuple) .build(), )); } report_iteration_over_character(&self.context, iterable_type, value.as_ref().into()); iterable_type - .try_iterate(db) - .map(|spec| Type::tuple(TupleType::new(db, &spec))) + .try_iterate(db, env) + .map(|spec| Type::tuple(TupleType::new(db, env, &spec))) .unwrap_or_else(|err| { err.report_diagnostic(&self.context, iterable_type, value.as_ref().into()); - Type::homogeneous_tuple(db, err.fallback_element_type(db)) + Type::homogeneous_tuple(db, env, err.fallback_element_type(db, env)) }) } fn infer_yield_expression(&mut self, yield_expression: &ast::ExprYield) -> Type<'db> { + let db = self.db(); + let env = self.program_environment(); let ast::ExprYield { range: _, node_index: _, value, } = yield_expression; - let Some(enclosing_function) = - nearest_enclosing_function(self.db(), self.index, self.scope()) + let Some(enclosing_function) = nearest_enclosing_function(db, self.index, self.scope()) else { let _ = self.infer_optional_expression(value.as_deref(), TypeContext::default()); return Type::unknown(); }; let declared_return_ty = same_module_uncached_raw_signature( - self.db(), + db, enclosing_function, ReturnCallableTypeVarScope::Public, ) .return_ty; let return_type_span = enclosing_function.spans(self.db()).return_type; - let Some(generator_type_params) = declared_return_ty.generator_types(self.db()) else { + let Some(generator_type_params) = declared_return_ty.generator_types(db, env) else { let _ = self.infer_optional_expression(value.as_deref(), TypeContext::default()); return Type::unknown(); }; @@ -11677,21 +12277,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let tcx = TypeContext::new(expected_yield_ty); let yielded_ty = self .infer_optional_expression(value.as_deref(), tcx) - .unwrap_or_else(|| Type::none(self.db())); + .unwrap_or_else(|| Type::none(db, env)); let diagnostic_node: AnyNodeRef = value .as_deref() .map_or_else(|| yield_expression.into(), AnyNodeRef::from); - if let Some(expected_yield_ty) = expected_yield_ty - && !yielded_ty.is_assignable_to(self.db(), expected_yield_ty) - { - report_invalid_generator_yield_type( - &self.context, + if let Some(expected_yield_ty) = expected_yield_ty { + self.validate_generator_yield_type( diagnostic_node, + YieldKind::Yield, return_type_span, expected_yield_ty, yielded_ty, - GeneratorMismatchKind::YieldType, ); } @@ -11699,64 +12296,64 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn infer_yield_from_expression(&mut self, yield_from: &ast::ExprYieldFrom) -> Type<'db> { + let db = self.db(); + let env = self.program_environment(); let ast::ExprYieldFrom { range: _, node_index: _, value, } = yield_from; - let Some(enclosing_function) = - nearest_enclosing_function(self.db(), self.index, self.scope()) + let Some(enclosing_function) = nearest_enclosing_function(db, self.index, self.scope()) else { let _ = self.infer_expression(value, TypeContext::default()); return Type::unknown(); }; let annotated_return_ty = same_module_uncached_raw_signature( - self.db(), + db, enclosing_function, ReturnCallableTypeVarScope::Public, ) .return_ty; - let Some(outer_expected) = annotated_return_ty.generator_types(self.db()) else { + let Some(outer_expected) = annotated_return_ty.generator_types(db, env) else { let _ = self.infer_expression(value, TypeContext::default()); return Type::unknown(); }; let return_type_span = enclosing_function.spans(self.db()).return_type; let tcx = TypeContext::new(outer_expected.yield_ty.map(|yielded_ty| { - KnownClass::Iterable.to_specialized_instance(self.db(), &[yielded_ty]) + KnownClass::Iterable.to_specialized_instance(db, env, &[yielded_ty]) })); let iterable_type = self.infer_expression(value, tcx); report_iteration_over_character(&self.context, iterable_type, value.as_ref().into()); - let inner_yield_ty = iterable_type - .try_iterate(self.db()) - .map(|tuple| tuple.homogeneous_element_type(self.db())) - .unwrap_or_else(|err| { - err.report_diagnostic(&self.context, iterable_type, value.as_ref().into()); - err.fallback_element_type(self.db()) - }); + let known_inner_yield_type = match iterable_type.try_iterate(db, env) { + Ok(tuple) => Some(tuple.homogeneous_element_type(db, env)), + Err(err) => { + err.report_diagnostic(&self.context, iterable_type, AnyNodeRef::from(&**value)); + err.element_type(db, env) + } + }; if let Some(outer_yield_ty) = outer_expected.yield_ty - && !inner_yield_ty.is_assignable_to(self.db(), outer_yield_ty) + && let Some(known_inner_yield_type) = known_inner_yield_type { - report_invalid_generator_yield_type( - &self.context, - value.as_ref(), + self.validate_generator_yield_type( + &**value, + YieldKind::YieldFrom, return_type_span.clone(), outer_yield_ty, - inner_yield_ty, - GeneratorMismatchKind::YieldType, + known_inner_yield_type, ); } if let Some(outer_send_ty) = outer_expected.send_ty { let inner_send_ty = iterable_type - .generator_send_type(self.db()) - .unwrap_or_else(|| Type::none(self.db())); - if !outer_send_ty.is_assignable_to(self.db(), inner_send_ty) { + .generator_send_type(db, env) + .unwrap_or_else(|| Type::none(db, env)); + if !outer_send_ty.is_assignable_to(db, env, inner_send_ty) { report_invalid_generator_yield_type( &self.context, value.as_ref(), @@ -11769,15 +12366,54 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } iterable_type - .generator_return_type(self.db()) + .generator_return_type(db, env) .unwrap_or_else(Type::unknown) } + fn validate_generator_yield_type( + &self, + yielded_value: impl Ranged, + yield_kind: YieldKind, + return_type_span: Option, + expected_yield_ty: Type<'db>, + yielded_ty: Type<'db>, + ) { + let db = self.db(); + let env = self.program_environment(); + + if !yielded_ty.is_assignable_to(db, env, expected_yield_ty) { + report_invalid_generator_yield_type( + &self.context, + yielded_value, + return_type_span, + expected_yield_ty, + yielded_ty, + GeneratorMismatchKind::YieldType, + ); + } else if self.context.is_lint_enabled(&UNSOUND_YIELD) + && expected_yield_ty.is_fully_static(db, env) + && !yielded_ty.is_pure_redundant_with(db, env, expected_yield_ty) + { + // N.B. the implementation here is the ~same as for `UNSOUND_RETURN_STATEMENT`; + // update that too if updating this! + report_unsound_yield( + &self.context, + yielded_value, + yield_kind, + return_type_span, + expected_yield_ty, + yielded_ty, + ); + } + } + fn infer_await_expression( &mut self, await_expression: &ast::ExprAwait, tcx: TypeContext<'db>, ) -> Type<'db> { + let db = self.db(); + let env = self.program_environment(); let ast::ExprAwait { range: _, node_index: _, @@ -11787,10 +12423,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let expr_type = self.infer_expression( value, - tcx.map(|tcx| KnownClass::Awaitable.to_specialized_instance(self.db(), &[tcx])), + tcx.map(|tcx| KnownClass::Awaitable.to_specialized_instance(db, env, &[tcx])), ); - expr_type.try_await(self.db()).unwrap_or_else(|err| { + expr_type.try_await(db, env).unwrap_or_else(|err| { err.report_diagnostic(&self.context, expr_type, value.as_ref().into()); Type::unknown() }) @@ -11804,6 +12440,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { constraint_keys: &[(FileScopeId, ConstraintKey)], ) -> Type<'db> { let db = self.db(); + let env = self.program_environment(); 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); @@ -11816,7 +12453,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.index, ) { ApplicableConstraints::UnboundBinding(constraint) => { - ty = constraint.narrow(db, ty, place); + ty = constraint.narrow(db, env, ty, place); } // Performs narrowing based on constrained bindings. // This handling must be performed even if narrowing is attempted and failed using `infer_place_load`. @@ -11836,7 +12473,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ApplicableConstraints::ConstrainedBindings(bindings) => { let reachability_constraints = bindings.reachability_constraints(); let predicates = bindings.predicates(); - let mut union = UnionBuilder::new(db); + let mut union = UnionBuilder::new(db, env); for binding in bindings { let static_reachability = evaluate_reachability_with_cache( db, @@ -11853,15 +12490,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if !is_discarded_dict_key_assignment(db, definition) => { let binding_ty = binding_type(db, definition); - union = union.add( - binding.narrowing_constraint.narrow(db, binding_ty, place), + union.add_in_place( + binding + .narrowing_constraint + .narrow(db, env, binding_ty, place), ); } DefinitionState::Defined(_) | DefinitionState::Undefined | DefinitionState::Deleted => { - union = - union.add(binding.narrowing_constraint.narrow(db, ty, place)); + union.add_in_place( + binding.narrowing_constraint.narrow(db, env, ty, place), + ); } } } @@ -11892,7 +12532,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut diag = builder.into_diagnostic(format_args!(r#"The class `{class_name}` is deprecated"#)); if let Some(message) = deprecated.message { - diag.set_primary_message(message.value(self.db())); + diag.set_primary_annotation_message(message.value(self.db())); } diag.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Deprecated); return; @@ -11924,70 +12564,29 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut diag = builder.into_diagnostic(format_args!(r#"The function `{func_name}` is deprecated"#)); if let Some(message) = deprecated.message { - diag.set_primary_message(message.value(self.db())); + diag.set_primary_annotation_message(message.value(self.db())); } diag.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Deprecated); } fn infer_name_load(&mut self, name_node: &ast::ExprName, tcx: TypeContext<'db>) -> Type<'db> { - let ast::ExprName { - range: _, - node_index: _, - id: symbol_name, - ctx: _, - } = name_node; - let expr = PlaceExpr::from_expr_name(name_node); + let symbol_name = &name_node.id; let db = self.db(); + let expr = PlaceExpr::from_expr_name(name_node); - let (resolved, constraint_keys) = - self.infer_place_load(PlaceExprRef::from(&expr), ast::ExprRef::Name(name_node)); + let (resolved, _) = self.infer_place_load(expr, ast::ExprRef::Name(name_node)); + let env = self.program_environment(); let resolved_after_fallback = 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, self.file(), symbol_name).map_type(|ty| { - self.narrow_place_with_applicable_constraints( - PlaceExprRef::from(&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::Undefined.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::Undefined.into() - } - }) // basedpython only: `typing` members are implicitly available // and emitted as `from typing import …` by the transpiler. // version-gated names (e.g. `Self`, `LiteralString`) aren't in // the older-version typing stub — fall through to // `typing_extensions` so the implicit name still resolves - .or_fall_back_to(db, || { + .or_fall_back_to(db, env, || { if self.is_basedpython_file() && is_basedpython_implicit_typing_name(symbol_name) { - typing_symbol(db, symbol_name) - .or_fall_back_to(db, || typing_extensions_symbol(db, symbol_name)) + typing_symbol(db, env, symbol_name) + .or_fall_back_to(db, env, || typing_extensions_symbol(db, env, symbol_name)) } else { Place::Undefined.into() } @@ -11999,14 +12598,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // value-position `dynamic` stays an ordinary identifier; only // reached when otherwise unbound, so a local `dynamic = …` binding // still shadows it - .or_fall_back_to(db, || { + .or_fall_back_to(db, env, || { if self.is_basedpython_file() && symbol_name == "dynamic" && self .inference_flags() .contains(InferenceFlags::IN_TYPE_EXPRESSION) { - typing_symbol(db, "Any") + typing_symbol(db, env, "Any") } else { Place::Undefined.into() } @@ -12016,14 +12615,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // and friends). resolve the bare name in a type expression so it // doesn't require an import there. gated on type-expression position // so a value-position `Overlapping` stays an ordinary identifier - .or_fall_back_to(db, || { + .or_fall_back_to(db, env, || { if self.is_basedpython_file() && symbol_name == "Overlapping" && self .inference_flags() .contains(InferenceFlags::IN_TYPE_EXPRESSION) { - known_module_symbol(db, KnownModule::TyExtensions, "Overlapping") + known_module_symbol(db, env, KnownModule::TyExtensions, "Overlapping") } else { Place::Undefined.into() } @@ -12033,14 +12632,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // the matching `from ty_extensions import Character`. gated on // type-expression position and only reached when otherwise // unbound, so a local `Character = …` binding still shadows it - .or_fall_back_to(db, || { + .or_fall_back_to(db, env, || { if self.is_basedpython_file() && symbol_name == "Character" && self .inference_flags() .contains(InferenceFlags::IN_TYPE_EXPRESSION) { - known_module_symbol(db, KnownModule::TyExtensions, "Character") + known_module_symbol(db, env, KnownModule::TyExtensions, "Character") } else { Place::Undefined.into() } @@ -12052,10 +12651,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // otherwise unbound, so a local `Some = …` binding still shadows it. // It takes exactly one value (so `Some()` / `Some(1, 2)` are arity // errors) and produces the wrapped optional of that value's type - .or_fall_back_to(db, || { + .or_fall_back_to(db, env, || { if self.is_basedpython_file() && symbol_name == "Some" { let value_typevar = BoundTypeVarInstance::synthetic( db, + env, Name::new_static("_SomeT"), TypeVarVariance::Covariant, ); @@ -12063,8 +12663,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let value = Parameter::positional_only(Some(Name::new_static("value"))) .with_annotated_type(value_ty); let signature = Signature::new_generic( - Some(GenericContext::from_typevar_instances(db, [value_typevar])), - Parameters::from_annotation(db, [value]), + Some(GenericContext::from_typevar_instances( + db, + env, + [value_typevar], + )), + Parameters::from_annotation(db, env, [value]), Type::KnownInstance(KnownInstanceType::WrappedOptional(InternedType::new( db, value_ty, ))), @@ -12082,7 +12686,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // typevar *object* — `bind_typevar` recognises extension bodies as // binding the extended class's parameters, exactly as a class body // binds its own - .or_fall_back_to(db, || { + .or_fall_back_to(db, env, || { if self.is_basedpython_file() && let Some(extension) = self.enclosing_extension() && let Some(typevar) = @@ -12101,10 +12705,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // `self` and its members are in scope unqualified. reached last, so a // name bound anywhere in the lexical chain — or a builtin — keeps its // ordinary meaning - .or_fall_back_to(db, || { + .or_fall_back_to(db, env, || { if self.is_basedpython_file() && let Some(resolved) = receivers::implicit_receiver_name( db, + env, self.file(), self.scope(), symbol_name, @@ -12120,10 +12725,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // nothing else is looked up as a member of that type — `Red` in a // `Color` context means `Color.Red`. reached last of all, so it is // purely additive: nothing that resolves today changes meaning - .or_fall_back_to(db, || { + .or_fall_back_to(db, env, || { if self.is_basedpython_file() && let Some(member) = context_sensitive::resolve_in_context( db, + env, self.file(), self.scope(), tcx, @@ -12136,8 +12742,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } }); - let ty = - resolved_after_fallback.unwrap_with_diagnostic(db, |lookup_error| match lookup_error { + let ty = resolved_after_fallback.unwrap_with_diagnostic(db, env, |lookup_error| { + match lookup_error { LookupError::Undefined(qualifiers) => { self.report_unresolved_reference(name_node, tcx); TypeAndQualifiers::new(Type::unknown(), TypeOrigin::Inferred, qualifiers) @@ -12146,7 +12752,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { report_possibly_unresolved_reference(&self.context, name_node); type_when_bound } - }); + } + }); let ty = ty.inner_type(); @@ -12183,469 +12790,233 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { let kind = typevar.kind(db); if kind.is_typevartuple() { - return Type::homogeneous_tuple(db, KnownClass::Type.to_instance(db)); + return Type::homogeneous_tuple(db, env, KnownClass::Type.to_instance(db, env)); } if kind.is_keyword_variadic() { return KnownClass::Dict.to_specialized_instance( db, + env, &[ - KnownClass::Str.to_instance(db), - KnownClass::Type.to_instance(db), + KnownClass::Str.to_instance(db, env), + KnownClass::Type.to_instance(db, env), ], ); } - return Type::TypeVar(bound_typevar).to_meta_type(db); + return Type::TypeVar(bound_typevar).to_meta_type(db, env); } ty } - fn infer_local_place_load( + /// Infer the type of a place expression from its ordered load sources. + /// + /// This also returns the [`ConstraintKey`]s used by expression-level narrowing. + fn infer_place_load( &self, - expr: PlaceExprRef, + place_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, look them up from end-of-scope. - if self.is_deferred() { - let place = if let Some(place_id) = place_table.place_id(expr) { - place_from_bindings_with_reachability_cache( - db, - use_def.reachable_bindings(place_id), - self.reachability_cache(), - ) - .place - } else { - assert!( - self.in_string_annotation(), - "Expected the place table to create a place for every valid PlaceExpr node" - ); - Place::Undefined - }; - (place, None) + ) -> (PlaceAndQualifiers<'db>, Vec<(FileScopeId, ConstraintKey)>) { + let env = self.program_environment(); + let mode = if self.is_deferred() && self.in_string_annotation() { + PlaceLoadMode::StringAnnotation + } else if self.is_deferred() { + PlaceLoadMode::Deferred } else { - if expr_ref - .as_name_expr() - .is_some_and(|name| name.is_invalid()) - { - return (Place::Undefined, None); - } - - // A named expression can show up here when resolving the parent place of something - // like `(foo := bar()).baz`. It binds `foo`, but it is not a normal load site and - // therefore has no `ScopedUseId`, so resolve it from its binding definition instead. - if let ast::ExprRef::Named(named) = expr_ref { - let place = if named.target.is_name_expr() { - let definition = self.index.expect_single_definition(named); - Place::bound(binding_type(db, definition)).with_definition(definition) - } else { - Place::Undefined - }; - return (place, None); - } - - let use_id = expr_ref.scoped_use_id(db, self.file()); - let place = place_from_bindings_with_reachability_cache( - db, - use_def.bindings_at_use(use_id), - self.reachability_cache(), - ) - .place; - - (place, Some(use_id)) - } - } - - /// Resolve a load that has fallen through to the module's explicit global scope. - /// - /// For eager nested scopes, this uses the global enclosing snapshot instead of the completed - /// module scope, so a class body cannot see a class name that is bound only after the body - /// finishes: - /// - /// ```python - /// class A: - /// A = A - /// ``` - /// - /// `symbol_name` is only needed when no snapshot is available: snapshots can resolve complex - /// places like `a.x`, but the fallback global query only works for bare symbols. `assume_bound` - /// preserves the class-body fallback behavior for names that are also local to the class body. - fn infer_explicit_global_symbol_load( - &self, - place_expr: PlaceExprRef, - symbol_name: Option<&str>, - current_scope_id: FileScopeId, - constraint_keys: &mut Vec<(FileScopeId, ConstraintKey)>, - assume_bound: bool, - ) -> PlaceAndQualifiers<'db> { - let db = self.db(); - - if current_scope_id.is_global() { - return Place::Undefined.into(); - } - - if !self.is_deferred() { - match self - .index - .enclosing_snapshot(FileScopeId::global(), place_expr, current_scope_id) - { - EnclosingSnapshotResult::FoundConstraint(constraint) => { - constraint_keys.push(( - FileScopeId::global(), - ConstraintKey::NarrowingConstraint(constraint), - )); - // Reaching here means that no bindings are found in any scope. - // Since `explicit_global_symbol` may return a cycle initial value, we return `Place::Undefined` here. - return Place::Undefined.into(); - } - EnclosingSnapshotResult::FoundBindings(bindings) => { - let mut place_and_qualifiers = place_from_bindings_with_reachability_cache( - db, - bindings, - self.reachability_cache(), - ); - if assume_bound && let Place::Defined(defined) = place_and_qualifiers.place { - place_and_qualifiers.place = - Place::Defined(defined.with_definedness(Definedness::AlwaysDefined)); + PlaceLoadMode::AtExpression(expr_ref) + }; + let mut resolution = + resolve_place_load(self.db(), self.index, self.scope(), place_expr, mode); + let mut place = PlaceAndQualifiers::from(Place::Undefined); + let mut failure = None; + let mut checked_deprecated = false; + + while let Some(step) = resolution.next() { + match step { + PlaceLoadResolutionStep::Source(source) => { + if !checked_deprecated && source.is_post_lexical() { + // Deprecation diagnostics apply to the result of lexical name resolution, + // before it is combined with implicit module globals or builtins. Hence, we + // check for deprecation here when the first post-lexical source is yielded. + // If resolution stops before this, then the check after the resolution loop + // handles the final lexical result instead. + if let Some(ty) = place.place.ignore_possibly_undefined() { + self.check_deprecated(expr_ref, ty); + } + checked_deprecated = true; } - let place = place_and_qualifiers.place.map_type(|ty| { - self.narrow_place_with_applicable_constraints( - place_expr, - ty, - constraint_keys, + let narrowing_constraints = resolution.narrowing_constraints_for(&source); + place = place.or_fall_back_to(self.db(), env, || { + self.infer_place_load_source( + resolution.place_expr(), + source, + narrowing_constraints, ) }); - constraint_keys.push(( - FileScopeId::global(), - ConstraintKey::NestedScope(current_scope_id), - )); - return place.into(); + if place.place.is_definitely_bound() { + break; + } } - // There are no visible bindings / constraint here. - EnclosingSnapshotResult::NotFound => { - return Place::Undefined.into(); + PlaceLoadResolutionStep::MemberResolutionCondition(prefix_loads) => { + if self.has_bound_place_expr_prefix(&prefix_loads) { + failure = Some(PlaceLoadFailure::NotFound); + break; + } + } + PlaceLoadResolutionStep::Exhausted(exhaustion_failure) => { + failure = Some(exhaustion_failure); + break; } - EnclosingSnapshotResult::NoLongerInEagerContext => {} } } - let Some(symbol_name) = symbol_name else { - return Place::Undefined.into(); + if !checked_deprecated && let Some(ty) = place.place.ignore_possibly_undefined() { + self.check_deprecated(expr_ref, ty); + } + + let place = if failure == Some(PlaceLoadFailure::NotFound) { + place.or_fall_back_to(self.db(), env, || { + self.infer_unimported_reveal_type_fallback(expr_ref) + }) + } else { + place }; - explicit_global_symbol(db, self.file(), symbol_name).map_type(|ty| { - self.narrow_place_with_applicable_constraints(place_expr, ty, constraint_keys) - }) + let constraint_keys = resolution.into_constraints(); + + (place, constraint_keys) } - /// 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( + fn infer_place_load_source( &self, place_expr: PlaceExprRef, - expr_ref: ast::ExprRef, - ) -> (PlaceAndQualifiers<'db>, Vec<(FileScopeId, ConstraintKey)>) { + source: PlaceLoadSource<'db>, + narrowing_constraints: &[(FileScopeId, ConstraintKey)], + ) -> PlaceAndQualifiers<'db> { 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 env = self.program_environment(); + let is_class_body_global_fallback = source.is_class_body_global_fallback(); - let mut constraint_keys = vec![]; - let (local_scope_place, use_id) = self.infer_local_place_load(place_expr, expr_ref); - if let Some(use_id) = use_id { - constraint_keys.push((file_scope_id, ConstraintKey::UseId(use_id))); - } + let place = match source.kind { + PlaceLoadSourceKind::Bindings(bindings) => { + let mut place = place_from_bindings_with_reachability_cache( + db, + env, + bindings, + self.reachability_cache(), + ) + .place; - let place = PlaceAndQualifiers::from(local_scope_place).or_fall_back_to(db, || { - let mut symbol_resolves_locally = false; - if let Some(symbol) = place_expr.as_symbol() - && let Some(symbol_id) = place_table.symbol_id(symbol.name()) - { - // Footgun: `place_expr` and `symbol` were probably constructed with all-zero - // flags. We need to read the place table to get correct flags. - symbol_resolves_locally = place_table.symbol(symbol_id).is_local(); - // If we try to access a variable in a class before it has been defined, the - // lookup will fall back to global. See the comment on `Symbol::is_local`. - let fallback_to_global = - scope.node(db).scope_kind().is_class() && symbol_resolves_locally; - if self.skip_non_global_scopes(file_scope_id, symbol_id) || fallback_to_global { - return self.infer_explicit_global_symbol_load( - place_expr, - Some(symbol.name()), - file_scope_id, - &mut constraint_keys, - fallback_to_global, - ); + // Compatibility policy: ty historically treats a possibly-bound module snapshot + // reached through a class-body global fallback as definitely bound. At runtime, + // an unbound snapshot would continue to builtins or produce a name error. + if is_class_body_global_fallback && let Place::Defined(defined) = place { + place = Place::Defined(defined.with_definedness(Definedness::AlwaysDefined)); } - } - - // Symbols that are bound or declared in the local scope, and not marked `nonlocal` or - // `global`, never refer to an enclosing scope. (If you reference such a symbol before - // it's bound, you get an `UnboundLocalError`.) Short-circuit instead of walking - // enclosing scopes in this case. The one exception to this rule is the global fallback - // in class bodies, which we already handled above. - if symbol_resolves_locally { - return Place::Undefined.into(); - } - if let PlaceExprRef::Symbol(symbol) = place_expr - && symbol.name() == "__class__" - && let Some(class) = self.dunder_class_cell_type() - { - return Place::bound(class).into(); + place.into() } - - for parent_id in place_table.parents(place_expr) { - let parent_expr = place_table.place(parent_id); - let mut expr_ref = expr_ref; - for _ in 0..(place_expr.num_member_segments() - parent_expr.num_member_segments()) { - 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!(), + PlaceLoadSourceKind::DefinitionsFromOwningScope { scope, id } => place_by_id( + db, + scope, + id, + RequiresExplicitReExport::No, + ConsideredDefinitions::AllReachable, + ), + PlaceLoadSourceKind::Implicit(implicit) => match implicit { + ImplicitPlaceLoad::DunderClass(definition) => original_class_type(db, definition) + .map_or_else( + || Place::Undefined.into(), + |class| Place::bound(class).into(), + ), + ImplicitPlaceLoad::ClassBodySymbol(name) => { + let implicit = class_body_implicit_symbol(db, env, &name); + if implicit.place.is_definitely_bound() { + implicit + } else { + Place::Undefined.into() } } - let (parent_place, _use_id) = self.infer_local_place_load(parent_expr, expr_ref); - if let Place::Defined(_) = parent_place { - return Place::Undefined.into(); + ImplicitPlaceLoad::ExplicitGlobalSymbol { file, name } => { + explicit_global_symbol(db, file, &name) } - } - - // Walk enclosing scopes to resolve a free-variable load (`LOAD_DEREF` at runtime). - // There are two main ways we try to model these loads: - // - // 1. "Snapshots" record the bindings/constraints in the enclosing scope at the point - // just before a nested scope begins. For variables that aren't modified after that - // point, that's the only value that the nested scope can see. If a variable is - // reassigned later, lazy snapshots for that variable can be updated or swept. - // - // 2. Otherwise, we keep walking until we get to the variable's original defining - // scope, and we use its "public type" there, which respects all reachable bindings, - // not just end-of-scope bindings. That includes the synthetic `NestedBindings` - // definitions that we install after each nested scope is closed, so it has - // a complete view of the nested `global` and `nonlocal` writes beneath it. - // - // This walk only resolves free variables and explicit `nonlocal`s. A symbol that is - // local to the current scope never falls back to an enclosing scope, even if it's only - // possibly bound at the current use: Python would raise `UnboundLocalError` instead. - // - // Note that we only get to this walk via `or_fall_back_to` above. In other words, for - // definitely-locally-bound variables, we defer to the current scope's bindings instead - // of looking at enclosing scopes. Concretely: - // - // def f(): - // x = None - // - // def g(): - // nonlocal x - // if flag: - // x = 42 - // - // # `x` is possibly unbound here, so we walk enclosing scopes and see the - // # public type in `f`. - // reveal_type(x) # revealed: Literal[42, 99] | None - // - // x = 99 - // # But now `x` is definitely bound, so we don't do the walk. - // reveal_type(x) # revealed: Literal[99] - // - // Importantly, this approach isn't generally sound. The public type could include - // nested bindings from sibling scopes, which really could run at any time, and in some - // cases we're being too deferential to local bindings. Unfortunately the fully sound - // treatment would reveal `Literal[42, 99] | None` even immediately after `x = 99`, - // which is too frustrating for users in practice. - for (enclosing_scope_file_id, _) in self.index.ancestor_scopes(file_scope_id).skip(1) { - // If the current enclosing scope is global, no place lookup is performed here, - // instead falling back to the module's explicit global lookup below. - if enclosing_scope_file_id.is_global() { - break; + ImplicitPlaceLoad::ModuleImplicitGlobal { file, name } => { + module_type_implicit_global_symbol(db, file, &name) } - - // 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: annotation scopes can see - // names defined in an immediately-enclosing class scope. - let enclosing_scope = self.index.scope(enclosing_scope_file_id); - - let is_immediately_enclosing_scope = scope.is_annotation(db) - && scope - .scope(db) - .parent() - .is_some_and(|parent| parent == enclosing_scope_file_id); - - let has_root_place_been_reassigned = || { - let enclosing_place_table = self.index.place_table(enclosing_scope_file_id); - enclosing_place_table - .parents(place_expr) - .any(|enclosing_root_place_id| { - enclosing_place_table - .place(enclosing_root_place_id) - .is_bound() - }) - }; - - // 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.) - let mut eagerly_resolved_place = None; - if !self.is_deferred() { - match self.index.enclosing_snapshot( - enclosing_scope_file_id, - place_expr, - file_scope_id, - ) { - EnclosingSnapshotResult::FoundConstraint(constraint) => { - constraint_keys.push(( - enclosing_scope_file_id, - ConstraintKey::NarrowingConstraint(constraint), - )); - // If the current scope is eager, it is certain that the place is undefined in the current scope. - // Do not call the `place` query below as a fallback. - if scope.scope(db).is_eager() { - eagerly_resolved_place = Some(Place::Undefined.into()); - } - } - EnclosingSnapshotResult::FoundBindings(bindings) => { - let place = place_from_bindings_with_reachability_cache( - db, - bindings, - self.reachability_cache(), - ) - .place - .map_type(|ty| { - self.narrow_place_with_applicable_constraints( - place_expr, - ty, - &constraint_keys, - ) - }); - constraint_keys.push(( - enclosing_scope_file_id, - ConstraintKey::NestedScope(file_scope_id), - )); - return place.into(); - } - // There are no visible bindings / constraint here. - // Don't fall back to non-eager place resolution. - EnclosingSnapshotResult::NotFound => { - if has_root_place_been_reassigned() { - return Place::Undefined.into(); - } - continue; - } - EnclosingSnapshotResult::NoLongerInEagerContext => { - if has_root_place_been_reassigned() { - return Place::Undefined.into(); - } - } + ImplicitPlaceLoad::Builtin(name) => { + if Some(self.scope()) == builtins_module_scope(db, env) { + Place::Undefined.into() + } else { + implicit_builtins_symbol(db, env, &name) } } + }, + }; - if !enclosing_scope.kind().is_function_like() && !is_immediately_enclosing_scope { - continue; - } + if narrowing_constraints.is_empty() { + place + } else { + place.map_type(|ty| { + self.narrow_place_with_applicable_constraints(place_expr, ty, narrowing_constraints) + }) + } + } - let enclosing_place_table = self.index.place_table(enclosing_scope_file_id); - let Some(enclosing_place_id) = enclosing_place_table.place_id(place_expr) else { - continue; - }; + /// Applies ty's convenience fallback for an unimported `reveal_type`. + fn infer_unimported_reveal_type_fallback( + &self, + expr_ref: ast::ExprRef, + ) -> PlaceAndQualifiers<'db> { + let Some(name) = expr_ref + .as_name_expr() + .filter(|name| name.id == "reveal_type") + else { + return Place::Undefined.into(); + }; - let enclosing_place = enclosing_place_table.place(enclosing_place_id); + if !self.in_stub() + && !self.is_in_type_checking_block(self.scope(), name) + && let Some(builder) = self.context.report_lint(&UNDEFINED_REVEAL, name) + { + 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"); + } - // Reads of "free" or `nonlocal` variables terminate at any enclosing scope that - // marks the variable `global`, whether or not that scope actually binds the - // variable. If we see a `global` declaration, stop walking scopes and proceed to - // the global handling below. (If we're walking from a prior/inner scope where this - // variable is `nonlocal`, then this is a semantic syntax error, but we don't - // enforce that here. See `SemanticIndexBuilder::pop_scope`.) - if enclosing_place.as_symbol().is_some_and(Symbol::is_global) { - break; - } + typing_extensions_symbol(self.db(), self.program_environment(), "reveal_type") + } - // Keep walking until we reach the defining scope of the variable. The synthetic - // nested bindings definitions installed there will see everything below it. - if enclosing_place.as_symbol().is_some_and(Symbol::is_nonlocal) { - continue; - } - if !(enclosing_place.is_bound() || enclosing_place.is_declared()) { - // Note that this check includes members like `x.y` and `x[0]`, which aren't - // symbols and can't be explicitly `nonlocal`. - continue; - } + /// Returns whether any tracked place-expression prefix has a definite or possible binding in + /// this scope. + fn has_bound_place_expr_prefix(&self, prefix_loads: &PlaceExprPrefixLoads<'db>) -> bool { + let db = self.db(); + let env = self.program_environment(); + let file_scope_id = prefix_loads.scope().file_scope_id(db); + let use_def = self.index.use_def_map(file_scope_id); - // We've reached the defining scope of the variable. Infer its public type. - debug_assert!(enclosing_place.is_bound() || enclosing_place.is_declared()); - let enclosing_scope_id = enclosing_scope_file_id.to_scope_id(db, self.file()); - return eagerly_resolved_place.unwrap_or_else(|| { - place_by_id( + prefix_loads.iter().any(|prefix| { + let place = match prefix { + PlaceExprPrefixLoad::AtUse(use_id) => { + place_from_bindings_with_reachability_cache( db, - enclosing_scope_id, - enclosing_place_id, - RequiresExplicitReExport::No, - ConsideredDefinitions::AllReachable, + env, + use_def.bindings_at_use(use_id), + self.reachability_cache(), ) - .map_type(|ty| { - self.narrow_place_with_applicable_constraints( - place_expr, - ty, - &constraint_keys, - ) - }) - }); - } - - PlaceAndQualifiers::default() - // If we're in a class body, check for implicit class body symbols first. - // These take precedence over globals. - .or_fall_back_to(db, || { - if scope.node(db).scope_kind().is_class() - && let Some(symbol) = place_expr.as_symbol() - { - let implicit = class_body_implicit_symbol(db, symbol.name()); - if implicit.place.is_definitely_bound() { - return implicit.map_type(|ty| { - self.narrow_place_with_applicable_constraints( - place_expr, - ty, - &constraint_keys, - ) - }); - } - } - Place::Undefined.into() - }) - // 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, || { - self.infer_explicit_global_symbol_load( - place_expr, - place_expr.as_symbol().map(|symbol| symbol.name().as_str()), - file_scope_id, - &mut constraint_keys, - false, + .place + } + PlaceExprPrefixLoad::AllReachable(place_id) => { + place_from_bindings_with_reachability_cache( + db, + env, + use_def.reachable_bindings(place_id), + self.reachability_cache(), ) - }) - }); - - if let Some(ty) = place.place.ignore_possibly_undefined() { - self.check_deprecated(expr_ref, ty); - } + .place + } + PlaceExprPrefixLoad::DefinitelyBound => return true, + }; - (place, constraint_keys) + !place.is_undefined() + }) } pub(super) fn report_unresolved_reference( @@ -12653,6 +13024,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { expr_name_node: &ast::ExprName, tcx: TypeContext<'db>, ) { + let db = self.db(); + let env = self.program_environment(); let Some(builder) = self .context .report_lint(&UNRESOLVED_REFERENCE, expr_name_node) @@ -12671,7 +13044,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // === if self.is_basedpython_file() && let Some(miss) = - context_sensitive::explain_miss(self.db(), self.file(), self.scope(), tcx, id) + context_sensitive::explain_miss(self.db(), env, self.file(), self.scope(), tcx, id) { let db = self.db(); match miss { @@ -12702,7 +13075,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { "`{id}` was added as a builtin in Python 3.{version_added_to_builtins}" )); add_inferred_python_version_hint_to_diagnostic( - self.db(), + db, + self.file(), &mut diagnostic, "resolving types", ); @@ -12713,9 +13087,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // === // We don't need to check for typing_extensions.Type, // because it's already caught by typing.Type. - if Program::get(self.db()).python_version(self.db()) >= PythonVersion::PY39 { + if self.program_environment().python_version(db) >= PythonVersion::PY39 { if let Some(("", builtin_name)) = as_pep_585_generic("typing", id) { - diagnostic.set_primary_message(format_args!("Did you mean `{builtin_name}`?")); + diagnostic + .set_primary_annotation_message(format_args!("Did you mean `{builtin_name}`?")); } } @@ -12750,12 +13125,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; let attribute_exists = match MethodDecorator::try_from_fn_type(self.db(), function_type) { - Some(MethodDecorator::ClassMethod) => !Type::instance(self.db(), class) - .class_member(self.db(), id) + Some(MethodDecorator::ClassMethod) => !Type::instance(db, env, class) + .class_member(db, env, id) .place .is_undefined(), - Some(MethodDecorator::None) => !Type::instance(self.db(), class) - .member(self.db(), id) + Some(MethodDecorator::None) => !Type::instance(db, env, class) + .member(db, env, id) .place .is_undefined(), Some(MethodDecorator::StaticMethod) | None => false, @@ -12826,6 +13201,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// match the sugar form, when there is no enclosing class, or when the /// supplied target class is not in the enclosing class' MRO. fn basedpython_super_value_type(&mut self, value: &ast::Expr) -> Option> { + let env = self.program_environment(); let db = self.db(); let target_class: Option> = match value { @@ -12880,7 +13256,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { return Some(Type::unknown()); }; let enclosing_class = ClassLiteral::Static(enclosing).default_specialization(db); - let owner_type = Type::instance(db, enclosing_class); + let owner_type = Type::instance(db, env, enclosing_class); let pivot_class_type = match target_class { None => Type::from(enclosing_class), @@ -12901,7 +13277,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } }; - crate::types::bound_super::BoundSuperType::build(db, pivot_class_type, owner_type).ok() + crate::types::bound_super::BoundSuperType::build(db, env, pivot_class_type, owner_type).ok() } /// Resolve `receiver` to the type the next link of a basedpython optional chain must be @@ -12939,28 +13315,35 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { present: Type<'db>, short_circuits: bool, ) -> Type<'db> { + let env = self.program_environment(); if !short_circuits { return present; } self.basedpython_chain_present.insert(link.into(), present); - UnionType::from_two_elements(self.db(), present, Type::none(self.db())) + // the present type comes first so the chain reads the way the field was declared + // (`str | None`), rather than leading with the arm the chain only takes when it + // short-circuits + UnionType::from_two_elements(self.db(), env, present, Type::none(self.db(), env)) } - /// Infer the type of a [`ast::ExprAttribute`] expression, assuming a load context. - fn infer_attribute_load(&mut self, attribute: &ast::ExprAttribute) -> Type<'db> { + /// Infer an attribute load, returning its recovery type if lookup fails. + fn infer_attribute_load( + &mut self, + attribute: &ast::ExprAttribute, + ) -> Result, Type<'db>> { let value_type = self.infer_maybe_standalone_expression(&attribute.value, TypeContext::default()); let (value_type, in_chain) = self.basedpython_chain_receiver(&attribute.value, value_type); self.infer_attribute_load_chained(attribute, value_type, in_chain) } - /// Infer the type of a [`ast::ExprAttribute`] expression, assuming a load context and a - /// receiver that does not continue a basedpython optional chain. + /// Infer an attribute load on a known receiver that does not continue a basedpython + /// optional chain, returning its recovery type if lookup fails. fn infer_attribute_load_impl( &mut self, attribute: &ast::ExprAttribute, value_type: Type<'db>, - ) -> Type<'db> { + ) -> Result, Type<'db>> { self.infer_attribute_load_chained(attribute, value_type, false) } @@ -12973,22 +13356,24 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { attribute: &ast::ExprAttribute, mut value_type: Type<'db>, in_chain: bool, - ) -> Type<'db> { + ) -> Result, Type<'db>> { fn union_elements_missing_attribute<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, attr_name: &str, missing_types: &mut FxIndexSet>, ) { if let Some(union) = ty.as_union_like(db) { for element in union.elements(db) { - union_elements_missing_attribute(db, *element, attr_name, missing_types); + union_elements_missing_attribute(db, env, *element, attr_name, missing_types); } - } else if ty.member(db, attr_name).place.is_undefined() { + } else if ty.member(db, env, attr_name).place.is_undefined() { missing_types.insert(ty); } } + let env = self.program_environment(); let ast::ExprAttribute { value, attr, .. } = attribute; let db = self.db(); @@ -13007,19 +13392,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { value_type = inner.inner(db); none_chain_was_optional = true; } - let none = Type::none(db); + let none = Type::none(db, env); let narrowed = match value_type { - Type::Union(u) => u.map(db, |elem| { - if elem.is_subtype_of(db, none) { + Type::Union(u) => u.map(db, env, |elem| { + if elem.is_subtype_of(db, env, none) { Type::Never } else { *elem } }), - ty if ty.is_subtype_of(db, none) => Type::Never, + ty if ty.is_subtype_of(db, env, none) => Type::Never, ty => ty, }; - if !narrowed.is_equivalent_to(db, value_type) { + if !narrowed.is_equivalent_to(db, env, value_type) { value_type = narrowed; none_chain_was_optional = true; } @@ -13039,14 +13424,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // was declared with, and through any receiver but the class's own that type is erased to // what such a view actually knows — the parameter's bound if let Some(view) = - crate::types::safe_variance::private_member_view(db, value_type, attr.as_str()) + crate::types::safe_variance::private_member_view(db, env, value_type, attr.as_str()) { let read_type = if self.is_own_receiver_attribute(attribute) { view.declared_ty } else { - view.read_type(db) + view.read_type(db, env) }; - return self.basedpython_chain_result(attribute, read_type, none_chain_was_optional); + return Ok(self.basedpython_chain_result( + attribute, + read_type, + none_chain_was_optional, + )); } // basedpython: `expr.N` is tuple-member dot access. the parser only @@ -13057,9 +13446,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { && attr.id.as_str().bytes().all(|b| b.is_ascii_digit()) && let Ok(index) = attr.id.as_str().parse::() && let Some(spec) = value_type.exact_tuple_instance_spec(db) - && let Ok(element_ty) = (&*spec).py_index(db, index) + && let Ok(element_ty) = (&*spec).py_index(db, env, index) { - return self.basedpython_chain_result(attribute, element_ty, none_chain_was_optional); + return Ok(self.basedpython_chain_result( + attribute, + element_ty, + none_chain_was_optional, + )); } // basedpython: `T.a` in a type expression is an *attribute type* — the type of @@ -13095,10 +13488,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut assigned_type = None; if let Some(place_expr) = PlaceExpr::try_from_expr(attribute) { - let (resolved, keys) = self.infer_place_load( - PlaceExprRef::from(&place_expr), - ast::ExprRef::Attribute(attribute), - ); + let (resolved, keys) = + self.infer_place_load(place_expr, ast::ExprRef::Attribute(attribute)); constraint_keys.extend(keys); if let Place::Defined(DefinedPlace { ty, @@ -13109,9 +13500,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { assigned_type = Some(ty); } } - let mut fallback_place = value_type.member(db, &attr.id).map_type(|ty| { - self.narrow_expr_with_applicable_constraints(attribute, ty, &constraint_keys) - }); + + let mut fallback_place = value_type + .try_member_lookup(db, env, &attr.id) + .unwrap_or_else(|error| { + error.report_diagnostic(&self.context, value_type, attribute, assigned_type); + error.fallback_member(db) + }) + .map_type(|ty| { + self.narrow_expr_with_applicable_constraints(attribute, ty, &constraint_keys) + }); // basedpython: an attribute that resolves to no declared member may be // supplied by an `extension` in scope (this module's, or one from any @@ -13119,7 +13517,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // declared members — this only runs after normal lookup came up empty if self.is_basedpython_file() && fallback_place.place.is_undefined() { if let Some(resolution) = - extensions::resolve_extension_member(db, self.file(), value_type, &attr.id) + extensions::resolve_extension_member(db, env, self.file(), value_type, &attr.id) { if let Some(other) = resolution.ambiguous_with && let Some(builder) = self @@ -13144,6 +13542,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { && fallback_place.place.is_undefined() && let Some(bound) = receivers::resolve_receiver_attribute( db, + env, self.file(), self.scope(), value_type, @@ -13154,8 +13553,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } let attr_name = &attr.id; - let resolved_type = - fallback_place.unwrap_with_diagnostic(db, |lookup_err| match lookup_err { + let lookup_result = fallback_place.into_lookup_result(db, env); + let resolved_type = lookup_result.unwrap_or_else(|lookup_err| { + match lookup_err { LookupError::Undefined(_) => { let fallback = || { TypeAndQualifiers::new( @@ -13167,12 +13567,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let bound_on_instance = match value_type { Type::ClassLiteral(class) => { - !class.instance_member(db, None, attr).is_undefined() + !class.instance_member(db, env, None, attr).is_undefined() } Type::SubclassOf(subclass_of @ SubclassOfType { .. }) => { match subclass_of.subclass_of() { SubclassOfInner::Class(class) => { - !class.instance_member(db, attr).is_undefined() + !class.instance_member(db, env, attr).is_undefined() } SubclassOfInner::Dynamic(_) => unreachable!( "Attribute lookup on a dynamic `SubclassOf` type \ @@ -13193,7 +13593,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { let mut maybe_submodule_name = module_name.clone(); maybe_submodule_name.extend(&relative_submodule); - if resolve_module(db, self.file(), &maybe_submodule_name).is_some() { + if resolve_module( + db, + ImportingFile::File( + self.file(), + self.program_environment().resolver_environment(db), + ), + &maybe_submodule_name, + ) + .is_some() + { if let Some(builder) = self .context .report_lint(&POSSIBLY_MISSING_SUBMODULE, attribute) @@ -13221,8 +13630,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { db, self.scope(), self.typevar_binding_context, - self.inference_flags() - ) && !defined_type.member(db, attr_name).place.is_undefined() + self.inference_flags(), + ) && !defined_type.member(db, env, attr_name).place.is_undefined() { diag.help(format_args!( "Objects with type `{ty}` have a{maybe_n} `{attr_name}` \ @@ -13233,7 +13642,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } else { "" }, - ty = defined_type.display(self.db()) + ty = defined_type.display(db, env) )); if is_dotted_name(value) { let source = @@ -13258,7 +13667,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { builder.into_diagnostic(format_args!( "Attribute `{attr_name}` can only be accessed on instances, \ not on the class object `{}` itself.", - value_type.display(db) + value_type.display(db, env) )); return fallback(); } @@ -13274,7 +13683,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { )), Type::GenericAlias(alias) => builder.into_diagnostic(format_args!( "Class `{}` has no attribute `{attr_name}`", - alias.display(db), + alias.display(db, env), )), Type::FunctionLiteral(function) => builder.into_diagnostic(format_args!( "Function `{}` has no attribute `{attr_name}`", @@ -13282,14 +13691,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { )), _ => builder.into_diagnostic(format_args!( "Object of type `{}` has no attribute `{attr_name}`", - value_type.display(db), + value_type.display(db, env), )), }; if value_type.is_callable_type() && KnownClass::FunctionType - .to_instance(db) - .member(db, attr_name) + .to_instance(db, env) + .member(db, env, attr_name) .place .is_definitely_bound() { @@ -13318,6 +13727,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } else { hint_if_stdlib_attribute_exists_on_other_versions( db, + self.program_file(), diagnostic, value_type, attr_name, @@ -13346,7 +13756,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Attribute lookup on a bounded type variable delegates to its upper bound, so // use that bound here too when determining whether the lookup was on a union. let union_like_type = if let Type::TypeVar(typevar) = value_type - && let Some(bound) = typevar.typevar(db).upper_bound(db) + && let Some(bound) = typevar.typevar(db).upper_bound(db, env) { bound } else { @@ -13358,6 +13768,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { for element in union.elements(db) { union_elements_missing_attribute( db, + env, *element, attr_name, &mut elements_missing_the_attribute, @@ -13370,14 +13781,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { let missing_types = elements_missing_the_attribute .iter() - .map(|ty| format!("`{}`", ty.display(db))) + .map(|ty| format!("`{}`", ty.display(db, env))) .collect::>() .join(", "); builder.into_diagnostic(format_args!( - "Attribute `{attr_name}` is not defined on {} in union `{union_like_type}`", + "Attribute `{attr_name}` is not defined on {} \ + in union `{union_like_type}`", missing_types, - union_like_type = union_like_type.display(db), + union_like_type = union_like_type.display(db, env), )); } return type_when_bound; @@ -13393,7 +13805,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { type_when_bound } - }); + } + }); let resolved_type = resolved_type.inner_type(); @@ -13406,23 +13819,29 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(receiver) = attribute_type_receiver { let attribute_type = DeferredType::build( db, + env, &DeferredOperation::Attribute(attr.id.clone()), Box::from([receiver]), ); - return self.basedpython_chain_result( + return Ok(self.basedpython_chain_result( attribute, attribute_type, none_chain_was_optional, - ); + )); } // Even if we can obtain the attribute type based on the assignments, we still perform default type inference // (to report errors). - let final_type = assigned_type.unwrap_or(resolved_type); + let inferred_type = assigned_type.unwrap_or(resolved_type); // basedpython `?.`: short-circuit returns None on a None receiver, so // the overall expression type is the attribute type unioned with None - self.basedpython_chain_result(attribute, final_type, none_chain_was_optional) + let inferred_type = + self.basedpython_chain_result(attribute, inferred_type, none_chain_was_optional); + + lookup_result + .map(|_| inferred_type) + .map_err(|_| inferred_type) } fn infer_attribute_expression(&mut self, attribute: &ast::ExprAttribute) -> Type<'db> { @@ -13436,13 +13855,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } = attribute; match ctx { - ExprContext::Load => self.infer_attribute_load(attribute), + ExprContext::Load => self + .infer_attribute_load(attribute) + .unwrap_or_else(|recovery_ty| recovery_ty), ExprContext::Store => { self.infer_expression(value, TypeContext::default()); Type::Never } ExprContext::Del => { - self.infer_attribute_load(attribute); + let _ = self.infer_attribute_load(attribute); self.validate_attribute_deletion( attribute, self.expression_type(value), @@ -13466,13 +13887,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { unary_dunder_method: &str, error: Option<&CallDunderError<'db>>, ) { + let db = self.db(); + let env = self.program_environment(); let Some(builder) = self.context.report_lint(&UNSUPPORTED_OPERATOR, unary) else { return; }; let mut diagnostic = builder.into_diagnostic(format_args!( "Unary operator `{op}` is not supported for object of type `{}`", - operand_type.display(self.db()), + operand_type.display(db, env), )); if let Some(CallDunderError::PossiblyUnbound { @@ -13483,7 +13906,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { for ty in unbound_on.iter().copied() { diagnostic.info(format_args!( "`{}` does not implement `{unary_dunder_method}`", - ty.display(self.db()) + ty.display(db, env) )); } } @@ -13499,12 +13922,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { op: ast::UnaryOp, operand: Type<'db>, ) -> Option> { + let env = self.program_environment(); if !self.is_basedpython_file() { return None; } let db = self.db(); - extensions::unary_extension_operator(db, self.file(), op, operand)? - .return_type(db, &CallArguments::none()) + extensions::unary_extension_operator(db, env, self.file(), op, operand)?.return_type( + db, + env, + &CallArguments::none(), + ) } /// basedpython: the type a binary operator evaluates to when an applicable @@ -13515,13 +13942,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { op: ast::Operator, right: Type<'db>, ) -> Option> { + let env = self.program_environment(); if !self.is_basedpython_file() { return None; } let db = self.db(); - let operator = extensions::binary_extension_operator(db, self.file(), left, op, right)?; + let operator = + extensions::binary_extension_operator(db, env, self.file(), left, op, right)?; let argument = if operator.reflected { left } else { right }; - operator.return_type(db, &CallArguments::positional([argument])) + operator.return_type(db, env, &CallArguments::positional([argument])) } /// basedpython: the type a comparison evaluates to when an applicable @@ -13534,15 +13963,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { op: ast::CmpOp, right: Type<'db>, ) -> Option> { + let env = self.program_environment(); if !self.is_basedpython_file() { return None; } let db = self.db(); - let operator = extensions::comparison_extension_operator(db, self.file(), left, op, right)?; + let operator = + extensions::comparison_extension_operator(db, env, self.file(), left, op, right)?; let argument = if operator.reflected { left } else { right }; - let returned = operator.return_type(db, &CallArguments::positional([argument]))?; + let returned = operator.return_type(db, env, &CallArguments::positional([argument]))?; Some(match op { - ast::CmpOp::In | ast::CmpOp::NotIn => KnownClass::Bool.to_instance(db), + ast::CmpOp::In | ast::CmpOp::NotIn => KnownClass::Bool.to_instance(db, env), _ => returned, }) } @@ -13566,6 +13997,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { operand_type: Type<'db>, unary: &ast::ExprUnaryOp, ) -> Type<'db> { + let db = self.db(); + let env = self.program_environment(); let fallback_unary_expression_type = || { let unary_dunder_method = match op { ast::UnaryOp::Invert => "__invert__", @@ -13580,12 +14013,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; match operand_type.try_call_dunder( - self.db(), + db, + env, unary_dunder_method, CallArguments::none(), TypeContext::default(), ) { - Ok(outcome) => outcome.return_type(self.db()), + Ok(outcome) => outcome.return_type(db, env), Err(e) => { // basedpython: an applicable extension may supply the dunder if let Some(ty) = self.try_unary_extension_operator(op, operand_type) { @@ -13598,14 +14032,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { unary_dunder_method, Some(&e), ); - e.fallback_return_type(self.db()) + e.fallback_return_type(db, env) } } }; match (op, operand_type) { // parameter-only marker; behaves as the type a body sees (bound of `Key`) - (_, Type::Overlapping(overlapping)) => overlapping.value_type(self.db()), + (_, Type::Overlapping(overlapping)) => overlapping.value_type(self.db(), env), (_, Type::Restricted(restricted)) => { self.infer_unary_expression_type(op, restricted.value_type(self.db()), unary) } @@ -13616,21 +14050,22 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if self.is_basedpython_file() && DeferredOperation::Unary(op).is_checked_arithmetic() && is_symbolic_operand(operand_type) - && is_integer_operand(self.db(), operand_type) => + && is_integer_operand(self.db(), env, operand_type) => { DeferredType::build( self.db(), + env, &DeferredOperation::Unary(op), Box::new([operand_type]), ) } - (_, Type::Deferred(deferred)) => deferred.reduced(self.db()), + (_, Type::Deferred(deferred)) => deferred.reduced(self.db(), env), (ast::UnaryOp::Invert | ast::UnaryOp::UAdd | ast::UnaryOp::USub, Type::Dynamic(_)) | (_, Type::Divergent(_)) => operand_type, (_, Type::Never) => Type::Never, (_, Type::TypeAlias(alias)) => { - self.infer_unary_expression_type(op, alias.value_type(self.db()), unary) + self.infer_unary_expression_type(op, alias.value_type(db), unary) } // basedpython postfix `!` force-unwrap and `^` propagate both peel @@ -13643,12 +14078,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::KnownInstance(KnownInstanceType::WrappedOptional(inner)), ) => inner.inner(self.db()), (ast::UnaryOp::Force | ast::UnaryOp::Propagate, Type::Union(union)) => { - let none = Type::none(self.db()); - let base_exception = KnownClass::BaseException.to_instance(self.db()); + let none = Type::none(self.db(), env); + let base_exception = KnownClass::BaseException.to_instance(self.db(), env); let is_absent = |element: Type<'db>| { element == none || (!element.is_dynamic() - && element.is_subtype_of(self.db(), base_exception)) + && element.is_subtype_of(self.db(), env, base_exception)) }; let present: Vec> = union .elements(self.db()) @@ -13660,7 +14095,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // nothing to unwrap — unwrap of a non-optional union todo_type!("basedpython unwrap of non-optional") } else { - UnionType::from_elements(self.db(), present) + UnionType::from_elements(self.db(), env, present) } } @@ -13674,13 +14109,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ( ast::UnaryOp::UAdd | ast::UnaryOp::USub | ast::UnaryOp::Invert, Type::LiteralValue(literal), - ) => binary_expressions::literal_unary_op(self.db(), op, literal) + ) => binary_expressions::literal_unary_op(self.db(), env, op, literal) .unwrap_or_else(fallback_unary_expression_type), (ast::UnaryOp::Invert, Type::KnownInstance(KnownInstanceType::ConstraintSet(set))) => { let constraints = ConstraintSetBuilder::new(); let result = constraints.into_owned(|constraints| { - let set = constraints.load(self.db(), set.constraints(self.db())); + let set = constraints.load(db, env, set.constraints(self.db())); set.negate(self.db(), constraints) }); Type::KnownInstance(KnownInstanceType::ConstraintSet( @@ -13689,8 +14124,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } (ast::UnaryOp::Not, ty) => Type::from_truthiness( - self.db(), - ty.try_bool(self.db()) + db, + env, + ty.try_bool(db, env) .unwrap_or_else(|err| { err.report_diagnostic(&self.context, unary); err.fallback_truthiness() @@ -13715,22 +14151,23 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } }; - match tvar.typevar(self.db()).bound_or_constraints(self.db()) { + match tvar.typevar(self.db()).bound_or_constraints(db, env) { Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - let db = self.db(); match Self::map_constrained_typevar_constraints( db, + env, operand_type, constraints, |constraint| { constraint .try_call_dunder( db, + env, unary_dunder_method, CallArguments::none(), TypeContext::default(), ) - .map(|outcome| outcome.return_type(db)) + .map(|outcome| outcome.return_type(db, env)) .ok() }, ) { @@ -13747,13 +14184,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { operand_type .try_call_dunder( db, + env, unary_dunder_method, CallArguments::none(), TypeContext::default(), ) .map_or_else( - |e| e.fallback_return_type(db), - |b| b.return_type(db), + |e| e.fallback_return_type(db, env), + |b| b.return_type(db, env), ) } } @@ -13764,24 +14202,27 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.infer_unary_expression_type(op, bound, unary) } // For unconstrained TypeVars, fall through to default handling. - None => match operand_type.try_call_dunder( - self.db(), - unary_dunder_method, - CallArguments::none(), - TypeContext::default(), - ) { - Ok(outcome) => outcome.return_type(self.db()), - Err(e) => { - self.report_unsupported_unary_operator( - unary, - op, - operand_type, - unary_dunder_method, - Some(&e), - ); - e.fallback_return_type(self.db()) + None => { + match operand_type.try_call_dunder( + db, + env, + unary_dunder_method, + CallArguments::none(), + TypeContext::default(), + ) { + Ok(outcome) => outcome.return_type(db, env), + Err(e) => { + self.report_unsupported_unary_operator( + unary, + op, + operand_type, + unary_dunder_method, + Some(&e), + ); + e.fallback_return_type(db, env) + } } - }, + } } } @@ -13877,9 +14318,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { NeedsPeerType: Fn(&Item) -> bool, InferType: Fn(&mut Self, Item, Option>) -> (Type<'db>, TextRange), { + let db = self.db(); + let env = self.program_environment(); let mut done = false; let mut peer_types: Option> = None; - let db = self.db(); let elements = operations .into_iter() @@ -13890,7 +14332,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } else { peer_types .as_mut() - .map(|peer_types| peer_types.get_or_build(db)) + .map(|peer_types| peer_types.get_or_build(db, env)) }; let (ty, range) = infer_ty(self, item, peer_ty); @@ -13899,7 +14341,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if is_last { if done { Type::Never } else { ty } } else { - let truthiness = ty.try_bool(self.db()).unwrap_or_else(|err| { + let truthiness = ty.try_bool(db, env).unwrap_or_else(|err| { err.report_diagnostic(&self.context, range); err.fallback_truthiness() }); @@ -13921,11 +14363,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { (Truthiness::Ambiguous, _) => { if track_peer_types { match &mut peer_types { - Some(peer_types) => peer_types.add(db, ty), + Some(peer_types) => peer_types.add(db, env, ty), None => peer_types = Some(UnionAccumulator::new(ty)), } } - IntersectionBuilder::new(db) + IntersectionBuilder::new(db, env) .add_positive(ty) .add_negative(match op { ast::BoolOp::And => Type::AlwaysTruthy, @@ -13937,10 +14379,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } }); - UnionType::from_elements(db, elements) + UnionType::from_elements(db, env, elements) } fn infer_compare_expression(&mut self, compare: &ast::ExprCompare) -> Type<'db> { + let db = self.db(); let ast::ExprCompare { range: _, node_index: _, @@ -14000,7 +14443,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { *op, right_ty, range, - &BinaryComparisonVisitor::new(Ok(Type::bool_literal(true))), ) // basedpython: an applicable extension may supply the dunder. // only for a lone comparison — a chain is two calls joined by a @@ -14029,7 +14471,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { 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()) + KnownClass::Bool.to_instance(db, builder.program_environment()) } // Other operators can return arbitrary types _ => Type::unknown(), @@ -14064,17 +14506,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// instance type of the class on the right, or the union of the arms' instance /// types for a union target. `None` for a target with no instance form. fn is_test_target_instance(&self, right: &ast::Expr, right_ty: Type<'db>) -> Option> { + let env = self.program_environment(); // an over-approximating projection is the safe direction here: a wider // target can only overlap more, so it never invents a disjointness let db = self.db(); let Some(arms) = union_target_arms(right) else { - return right_ty.to_instance(db).map(InstanceProjection::into_inner); + return right_ty + .to_instance(db, env) + .map(InstanceProjection::into_inner); }; let mut instances = Vec::with_capacity(arms.len()); for arm in arms { - instances.push(self.expression_type(arm).to_instance(db)?.into_inner()); + instances.push(self.expression_type(arm).to_instance(db, env)?.into_inner()); } - Some(UnionType::from_elements(db, instances)) + Some(UnionType::from_elements(db, env, instances)) } /// Warn when a keyword-form `is`/`is not` tests a value against a type it can @@ -14090,12 +14535,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { op: ast::CmpOp, decision: IsTestDecision, ) { + let env = self.program_environment(); let db = self.db(); let Some(target) = self.is_test_target_instance(right, right_ty) else { return; }; let never_holds = match decision { - IsTestDecision::Instance => left_ty.is_disjoint_from(db, target), + IsTestDecision::Instance => left_ty.is_disjoint_from(db, env, target), IsTestDecision::ParametricNeverHolds => true, IsTestDecision::Undecided => false, }; @@ -14113,8 +14559,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; builder.into_diagnostic(format_args!( "`{}` and `{}` are non-overlapping types, so this test is always `{always}`", - left_ty.display(db), - target.display(db), + left_ty.display(db, env), + target.display(db, env), )); } @@ -14133,6 +14579,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { right_ty: Type<'db>, op: ast::CmpOp, ) -> Option<(Type<'db>, IsTestDecision)> { + let env = self.program_environment(); if !matches!(op, ast::CmpOp::Is | ast::CmpOp::IsNot) || !self.is_basedpython_file() { return None; } @@ -14145,7 +14592,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if !crate::reified::is_keyword_comparison(source.as_str(), op, left, right) { return None; } - let bool_ty = KnownClass::Bool.to_instance(self.db()); + let bool_ty = KnownClass::Bool.to_instance(self.db(), env); // a union target `a is T1 | T2` tests each arm (`type(a) <: Ti` for any // arm). an erased arm can't be checked at runtime and, unlike a @@ -14156,6 +14603,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { for arm in arms { let Some(alias) = crate::types::reified_infer::parametric_is_target( self.db(), + env, self.expression_type(arm), ) else { continue; @@ -14167,6 +14615,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let crate::types::reified_infer::ParametricIsPlan::ErasedTarget(_) = crate::types::reified_infer::classify_parametric_is( self.db(), + env, self.file(), left_ty, alias, @@ -14182,11 +14631,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // a plain-value rhs (an enum member, an instance of a non-type class) // keeps python identity semantics — the transpiler leaves `is`/`is not` // untouched, so ty types it as an ordinary identity comparison too - if crate::types::basedpython_is_keeps_identity(self.db(), right_ty) { + if crate::types::basedpython_is_keeps_identity(self.db(), env, right_ty) { return None; } - let Some(alias) = crate::types::reified_infer::parametric_is_target(self.db(), right_ty) + let Some(alias) = + crate::types::reified_infer::parametric_is_target(self.db(), env, right_ty) else { // a bare class / dynamic rhs (`x is int`, `x is SomeClass`) is an // instance check that lowers to `isinstance`, so it always yields a @@ -14196,6 +14646,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; let plan = crate::types::reified_infer::classify_parametric_is( self.db(), + env, self.file(), left_ty, alias, @@ -14453,7 +14904,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if bindings.len() > 20 { tracing::debug!( - "Inferred statement region `{:?}` contains {} bindings. Lookups by linear scan might be slow.", + "Inferred statement region `{:?}` contains {} bindings. \ + Lookups by linear scan might be slow.", self.region, bindings.len(), ); @@ -14461,7 +14913,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if declarations.len() > 20 { tracing::debug!( - "Inferred statement region `{:?}` contains {} declarations. Lookups by linear scan might be slow.", + "Inferred statement region `{:?}` contains {} declarations. \ + Lookups by linear scan might be slow.", self.region, declarations.len(), ); @@ -14556,7 +15009,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { definition: Definition<'db>, ) -> DefinitionInference<'db> { self.infer_region(); + self.finish_inferred_definition(definition) + } + fn finish_inferred_definition(self, definition: Definition<'db>) -> DefinitionInference<'db> { let Self { context, expressions, @@ -14676,7 +15132,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if bindings.len() > 20 { tracing::debug!( - "Inferred definition region `{:?}` contains {} bindings. Lookups by linear scan might be slow.", + "Inferred definition region `{:?}` contains {} bindings. \ + Lookups by linear scan might be slow.", self.region, bindings.len(), ); @@ -14684,7 +15141,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if declarations.len() > 20 { tracing::debug!( - "Inferred declaration region `{:?}` contains {} declarations. Lookups by linear scan might be slow.", + "Inferred declaration region `{:?}` contains {} declarations. \ + Lookups by linear scan might be slow.", self.region, declarations.len(), ); @@ -14787,6 +15245,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// The inference results can be merged into the current inference region using /// [`TypeInferenceBuilder::extend`]. fn speculate(&self) -> Self { + let db = self.db(); let Self { region, index, @@ -14823,7 +15282,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { type_expression_flags: _, } = *self; - let mut builder = TypeInferenceBuilder::new(self.db(), region, index, self.module()); + let mut builder = TypeInferenceBuilder::new( + db, + self.program_environment(), + region, + self.file(), + self.program_file(), + index, + self.module(), + ); // Speculated builders are often discarded immediately. builder.context.defuse(); @@ -15091,7 +15558,8 @@ impl<'db> FullExpressionCacheEntry<'db> { .then(|| { if self.bindings.len() > 20 { tracing::debug!( - "Inferred expression region `{:?}` contains {} bindings. Lookups by linear scan might be slow.", + "Inferred expression region `{:?}` contains {} bindings. \ + Lookups by linear scan might be slow.", region, self.bindings.len() ); @@ -15506,9 +15974,10 @@ impl StringPartsCollector { self.contains_non_literal_str = true; } - fn string_type(self, db: &dyn Db) -> Type<'_> { + fn string_type<'db>(self, context: &InferContext<'db, '_>) -> Type<'db> { + let db = context.db(); if self.contains_non_literal_str { - KnownClass::Str.to_instance(db) + KnownClass::Str.to_instance(db, context.program_environment()) } else if let Some(concatenated) = self.concatenated { Type::string_literal(db, &concatenated) } else { @@ -15647,13 +16116,7 @@ where self.0.push(value); } -} -impl VecSet -where - V: Eq, - V: std::fmt::Debug, -{ #[inline] fn extend>(&mut self, iter: T) { if cfg!(debug_assertions) { @@ -15700,6 +16163,7 @@ impl<'db, 'ast> AddBinding<'db, 'ast> { builder: &mut TypeInferenceBuilder<'db, 'ast>, inferred_ty: Type<'db>, ) -> Type<'db> { + let env = builder.program_environment(); let declared_ty = self.declared_ty.unwrap_or(Type::unknown()); let db = builder.db(); @@ -15726,7 +16190,7 @@ impl<'db, 'ast> AddBinding<'db, 'ast> { "Reassignment of `Final` symbol `{place}` is not allowed" )); - diagnostic.set_primary_message("Reassignment of `Final` symbol"); + diagnostic.set_primary_annotation_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 @@ -15753,14 +16217,15 @@ impl<'db, 'ast> AddBinding<'db, 'ast> { .message("Symbol declared as `Final` here"), ); } - diagnostic.set_primary_message("Symbol later reassigned here"); + diagnostic + .set_primary_annotation_message("Symbol later reassigned here"); } } } } } - if bound_ty.is_assignable_to(db, declared_ty) { + if bound_ty.is_assignable_to(db, env, declared_ty) { report_bool_as_int_assignment( &builder.context, self.node, @@ -15787,11 +16252,10 @@ impl<'db, 'ast> AddBinding<'db, 'ast> { builder.infer_maybe_standalone_expression(value, TypeContext::default()) }); // 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) - .place - .ignore_possibly_undefined() - .is_some_and(|ty| ty.may_be_data_descriptor(db)) + if assignment_attribute_members(db, env, value_ty, &attr.id) + .and_then(AssignmentAttributeMembers::type_member) + .and_then(|member| member.place.ignore_possibly_undefined()) + .is_some_and(|ty| ty.may_be_data_descriptor(db, env)) { builder.discard_dict_key_assignments_for(self.binding); bound_ty = declared_ty; @@ -15801,7 +16265,7 @@ impl<'db, 'ast> AddBinding<'db, 'ast> { .try_expression_type(value) .unwrap_or_else(|| builder.infer_expression(value, TypeContext::default())); - if !value_ty.is_typed_dict() && !Self::is_safe_mutable_class(db, value_ty) { + if !value_ty.is_typed_dict() && !Self::is_safe_mutable_class(db, env, value_ty) { builder.discard_dict_key_assignments_for(self.binding); bound_ty = declared_ty; } @@ -15821,7 +16285,11 @@ impl<'db, 'ast> AddBinding<'db, 'ast> { /// 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. - fn is_safe_mutable_class(db: &'db dyn Db, ty: Type<'db>) -> bool { + fn is_safe_mutable_class( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + ) -> bool { const SAFE_MUTABLE_CLASSES: &[KnownClass] = &[ KnownClass::List, KnownClass::Dict, @@ -15835,12 +16303,12 @@ impl<'db, 'ast> AddBinding<'db, 'ast> { SAFE_MUTABLE_CLASSES .iter() - .map(|class| class.to_instance(db)) + .map(|class| class.to_instance(db, env)) .any(|safe_mutable_class| { - ty.is_equivalent_to(db, safe_mutable_class) + ty.is_equivalent_to(db, env, safe_mutable_class) || ty - .generic_origin(db) - .zip(safe_mutable_class.generic_origin(db)) + .generic_origin(db, env) + .zip(safe_mutable_class.generic_origin(db, env)) .is_some_and(|(l, r)| l == r) }) } @@ -15860,6 +16328,7 @@ enum BoundOrConstraintsNodes<'ast> { /// `Never`, so any value assignment fails. fn attribute_has_covariant_projected_typevar<'db>( db: &'db dyn crate::Db, + env: &ProgramEnvironment<'db>, object_ty: Type<'db>, attribute: &str, ) -> bool { @@ -15868,7 +16337,7 @@ fn attribute_has_covariant_projected_typevar<'db>( let Some(instance) = object_ty.as_nominal_instance() else { return false; }; - let crate::types::ClassType::Generic(alias) = instance.class(db) else { + let crate::types::ClassType::Generic(alias) = instance.class(db, env) else { return false; }; let specialization = alias.specialization(db); @@ -15905,14 +16374,14 @@ fn attribute_has_covariant_projected_typevar<'db>( // typevar references in the field declaration are preserved. let identity_class_type = class_literal.identity_specialization(db); let unspecialized_member = identity_class_type - .instance_member(db, attribute) + .instance_member(db, env, attribute) .place .ignore_possibly_undefined(); let Some(declared_ty) = unspecialized_member else { return false; }; - crate::types::any_over_type(db, declared_ty, false, |ty| { + crate::types::any_over_type(db, env, declared_ty, false, |ty| { if let Type::TypeVar(typevar) = ty { let identity = typevar.identity(db); target_typevar_identities.contains(&identity) diff --git a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs index 8447b39d29..36e531293e 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs @@ -176,6 +176,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { AnnotationExpressionInference::new(annotation_ty) } } + let db = self.db(); // basedpython annotation markers — `let x = v`, `final x: T`, `class a = v`, // `[modifiers] a = v`, `newtype X = T`, `abstract a: T`, `private a: T`, @@ -197,6 +198,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { return result; } + let env = self.program_environment(); // https://typing.python.org/en/latest/spec/annotations.html#grammar-token-expression-grammar-annotation_expression let inferred = match annotation { // String annotations: https://typing.python.org/en/latest/spec/annotations.html#string-annotations @@ -293,12 +295,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ); let in_type_expression = inferred .inner_type() - .in_type_expression( - self.db(), - self.scope(), - None, - self.inference_flags(), - ) + .in_type_expression(db, self.scope(), None, self.inference_flags()) .unwrap_or_else(|err| { err.into_fallback_type( &self.context, @@ -362,7 +359,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if qualifier == TypeQualifier::ClassVar && type_and_qualifiers .inner_type() - .has_non_self_typevar(self.db()) + .has_non_self_typevar(db, env) && let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { diff --git a/crates/ty_python_semantic/src/types/infer/builder/attribute_assignment.rs b/crates/ty_python_semantic/src/types/infer/builder/attribute_assignment.rs index bfd8876d59..621304fd45 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/attribute_assignment.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/attribute_assignment.rs @@ -8,7 +8,9 @@ use crate::types::attribute_write::{ FallbackAttributeWriteRequirement, InstanceAttributeWriteMember, ProtocolMemberWriteRequirement, attribute_write_requirement, property_setter_returns_never, }; -use crate::types::call::{Bindings, CallArguments, CallError}; +use crate::types::call::{Bindings, CallArguments, CallDiagnosticOverride, CallError}; +use crate::types::class::FrozenDataclassDispatch; +use crate::types::dedicated::pydantic; use crate::types::diagnostic::{ INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, UNRESOLVED_ATTRIBUTE, report_bad_dunder_set_call, report_bool_as_int, report_invalid_attribute_assignment, report_possibly_missing_attribute, @@ -31,6 +33,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { infer_value_ty: &mut dyn FnMut(&mut Self, TypeContext<'db>) -> Type<'db>, emit_diagnostics: bool, ) -> bool { + let env = self.program_environment(); let db = self.db(); // basedpython use-site variance: writes to an attribute typed with a @@ -38,7 +41,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // attribute's declared type on the unspecialized class is inspected // for any covariantly-projected typevar; if found, the projected T // would substitute to `Never` here, so no value can satisfy the write. - if super::attribute_has_covariant_projected_typevar(db, object_ty, attribute) { + if super::attribute_has_covariant_projected_typevar(db, env, object_ty, attribute) { let value_ty = infer_value_ty(self, TypeContext::default()); if emit_diagnostics && let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) @@ -46,8 +49,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { builder.into_diagnostic(format_args!( "Cannot assign value of type `{}` to attribute `{attribute}` on \ covariantly-projected object of type `{}`", - value_ty.display(db), - object_ty.display(db), + value_ty.display(db, env), + object_ty.display(db, env), )); } return false; @@ -60,12 +63,13 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // setters working. Every other receiver is a widened view, and `attribute_write_requirement` // erases those itself let write_receiver = if self.is_own_receiver_attribute(target) { - private_member_view(db, object_ty, attribute).map_or(object_ty, |view| view.own_view) + private_member_view(db, env, object_ty, attribute) + .map_or(object_ty, |view| view.own_view) } else { object_ty }; - let requirement = attribute_write_requirement(db, write_receiver, attribute); + let requirement = attribute_write_requirement(db, env, write_receiver, attribute); let mut evaluator = AssignmentAttributeWriteEvaluator { builder: self, target, @@ -90,10 +94,15 @@ enum AssignmentAttributeWriteDiagnostic<'db> { is_setattr_synthesized: bool, }, TerminalDescriptor, - BadDunderSet(CallError<'db>), + BadDunderSet { + failure: CallError<'db>, + descriptor_ty: Type<'db>, + includes_descriptor_argument: bool, + }, PossiblyMissing, BadSetAttr { value_ty: Type<'db>, + failure: CallError<'db>, }, Unresolved { with_period: bool, @@ -156,11 +165,16 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { ast::ArgOrKeyword::Arg(self.value), ]; let mut call_arguments = CallArguments::positional([name_ty, Type::unknown()]); + // A bound `super` must use its own MRO lookup rather than the normal instance fallback. + let lookup_policy = if matches!(object_ty, Type::BoundSuper(_)) { + MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK + } else { + MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK | MemberLookupPolicy::NO_INSTANCE_FALLBACK + }; let setattr_result = self.builder.infer_and_try_call_dunder( - db, object_ty, "__setattr__", - MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK | MemberLookupPolicy::NO_INSTANCE_FALLBACK, + lookup_policy, ArgumentsIter::synthesized(&ast_arguments), &mut call_arguments, &mut |builder, (argument_index, _, tcx)| { @@ -181,6 +195,8 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { requirement: &AttributeWriteRequirement<'db>, emit_diagnostics: bool, ) -> bool { + let db = self.builder.db(); + let env = self.builder.program_environment(); match requirement { AttributeWriteRequirement::All { object_ty, @@ -190,7 +206,7 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { let mut valid = true; for element_ty in *element_tys { let requirement = - attribute_write_requirement(self.builder.db(), *element_ty, self.attribute); + attribute_write_requirement(db, env, *element_ty, self.attribute); if !self.evaluate(&requirement, false) { valid = false; break; @@ -218,7 +234,7 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { let mut valid = false; for element_ty in element_tys { let requirement = - attribute_write_requirement(self.builder.db(), *element_ty, self.attribute); + attribute_write_requirement(db, env, *element_ty, self.attribute); if self.evaluate(&requirement, false) { valid = true; break; @@ -282,6 +298,11 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { TypeContext::new(Some(domain.unwrap_or_else(Type::unknown))), emit_diagnostics, ); + if let Some(domain) = domain + && !self.check_type_pair(value_ty, *domain, emit_diagnostics) + { + return false; + } self.evaluate_protocol_descriptor_write( *descriptor_ty, *receiver_ty, @@ -323,8 +344,9 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { target_ty: Type<'db>, emit_diagnostics: bool, ) -> bool { + let env = self.builder.program_environment(); let db = self.builder.db(); - if value_ty.is_assignable_to(db, target_ty) { + if value_ty.is_assignable_to(db, env, target_ty) { if emit_diagnostics { report_bool_as_int(&self.builder.context, self.value, value_ty, target_ty); } @@ -335,19 +357,21 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { // declared, and a conversion dunder materializes the call. both sides ask // `value_conversions`, so neither can accept what the other cannot let file = self.builder.file(); - let model = crate::SemanticModel::new(db, file); + let model = crate::SemanticModel::new(db, db.program_file(file)); // the transpiler recovers the target type by looking the attribute up on the // object. accept a conversion only when that agrees with the type actually // being enforced here — for a descriptor or property the two can differ, and // a wrap built from the wrong one would be worse than the plain error let conversions = if self .object_ty - .member(db, self.attribute) + .member(db, env, self.attribute) .place .ignore_possibly_undefined() == Some(target_ty) { - crate::types::conversions::value_conversions(db, file, &model, self.value, target_ty) + crate::types::conversions::value_conversions( + db, env, file, &model, self.value, target_ty, + ) } else { Vec::new() }; @@ -409,13 +433,33 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { emit_diagnostics: bool, ) -> bool { let db = self.builder.db(); + let env = self.builder.program_environment(); + + let frozen_dataclass_dispatch = object_ty + .nominal_class(db, env) + .and_then(|class| class.static_class_literal(db)) + .and_then(|(class, specialization)| { + class.inherited_frozen_dataclass_dispatch( + db, + specialization, + "__setattr__", + self.attribute, + ) + }); + let setattr_receiver = frozen_dataclass_dispatch + .map_or(object_ty, |dispatch| dispatch.receiver(db, env, object_ty)); + let (setattr_result, value_ty) = if matches!(member, InstanceAttributeWriteMember::SetAttr) - { - self.infer_and_try_call_setattr(object_ty, emit_diagnostics) + || matches!( + frozen_dataclass_dispatch, + Some(FrozenDataclassDispatch::Delegate(_)) + ) { + self.infer_and_try_call_setattr(setattr_receiver, emit_diagnostics) } else { let value_ty = self.infer_value(TypeContext::default(), emit_diagnostics); - let setattr_result = object_ty.try_call_dunder_with_policy( + let setattr_result = setattr_receiver.try_call_dunder_with_policy( db, + env, "__setattr__", &mut CallArguments::positional([ Type::string_literal(db, self.attribute), @@ -428,14 +472,30 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { }; // A terminal `__setattr__` blocks even explicitly declared attributes. - let setattr_returns_never = match &setattr_result { - Ok(bindings) => bindings.return_type(db).is_never(), - Err(error) => error.return_type(db).is_some_and(|ty| ty.is_never()), + let setattr_returns_never = matches!( + frozen_dataclass_dispatch, + Some(FrozenDataclassDispatch::FrozenField) + ) || match &setattr_result { + Ok(bindings) => bindings.return_type(db, env).is_never(), + Err(error) => error.return_type(db, env).is_some_and(|ty| ty.is_never()), }; - if setattr_returns_never { + + // We could also model this more precisely by synthesizing a `__setattr__`overload set + // that only disallows mutation on non-private fields, but for now, we just suppress the + // diagnostic here. This is much easier and faster. + let is_private_pydantic_attribute = + matches!(member, InstanceAttributeWriteMember::Explicit { .. }) + && pydantic::is_private_attribute(self.attribute) + && pydantic::is_model_instance(db, env, object_ty); + + if setattr_returns_never && !is_private_pydantic_attribute { if emit_diagnostics { - let is_setattr_synthesized = match object_ty.class_member_with_policy( + let is_setattr_synthesized = !matches!( + frozen_dataclass_dispatch, + Some(FrozenDataclassDispatch::Delegate(_)) + ) && match object_ty.class_member_with_policy( db, + env, "__setattr__", MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, ) { @@ -445,7 +505,10 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { } => ty.is_callable_type(), _ => false, }; - let member_exists = !object_ty.member(db, self.attribute).place.is_undefined(); + let member_exists = !object_ty + .member(db, env, self.attribute) + .place + .is_undefined(); self.report(AssignmentAttributeWriteDiagnostic::TerminalSetAttr { member_exists, is_setattr_synthesized, @@ -466,6 +529,19 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { { return false; } + if matches!( + frozen_dataclass_dispatch, + Some(FrozenDataclassDispatch::Delegate(_)) + ) && let Err(CallDunderError::CallError(kind, bindings, _)) = setattr_result + { + if emit_diagnostics { + self.report(AssignmentAttributeWriteDiagnostic::BadSetAttr { + value_ty, + failure: CallError(kind, bindings), + }); + } + return false; + } let member_valid = self.evaluate_explicit_member(object_ty, member, value_ty, emit_diagnostics); if let Some(fallback) = fallback { @@ -481,12 +557,23 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { } InstanceAttributeWriteMember::SetAttr => match setattr_result { Ok(_) | Err(CallDunderError::PossiblyUnbound { .. }) => true, - Err(CallDunderError::CallError(..)) => { + Err(CallDunderError::CallError(kind, bindings, _)) => { if emit_diagnostics { - self.report(AssignmentAttributeWriteDiagnostic::BadSetAttr { value_ty }); + self.report(AssignmentAttributeWriteDiagnostic::BadSetAttr { + value_ty, + failure: CallError(kind, bindings), + }); } false } + Err(CallDunderError::MethodNotAvailable) + if matches!( + frozen_dataclass_dispatch, + Some(FrozenDataclassDispatch::Delegate(_)) + ) => + { + true + } Err(CallDunderError::MethodNotAvailable) => { if emit_diagnostics { self.report(AssignmentAttributeWriteDiagnostic::Unresolved { @@ -505,6 +592,8 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { member: &ClassAttributeWriteMember<'db>, emit_diagnostics: bool, ) -> bool { + let db = self.builder.db(); + let env = self.builder.program_environment(); match member { ClassAttributeWriteMember::Explicit { member, fallback } => { if !self.final_assignment_is_valid(object_ty, member.qualifiers(), emit_diagnostics) @@ -536,12 +625,11 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { ClassAttributeWriteMember::Unresolved { has_instance_attribute, } => { - let db = self.builder.db(); let (setattr_result, value_ty) = self.infer_and_try_call_setattr(object_ty, emit_diagnostics); let setattr_returns_never = match &setattr_result { - Ok(bindings) => bindings.return_type(db).is_never(), - Err(error) => error.return_type(db).is_some_and(|ty| ty.is_never()), + Ok(bindings) => bindings.return_type(db, env).is_never(), + Err(error) => error.return_type(db, env).is_some_and(|ty| ty.is_never()), }; if setattr_returns_never { if emit_diagnostics { @@ -555,10 +643,11 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { match setattr_result { Ok(_) | Err(CallDunderError::PossiblyUnbound { .. }) => true, - Err(CallDunderError::CallError(..)) => { + Err(CallDunderError::CallError(kind, bindings, _)) => { if emit_diagnostics { self.report(AssignmentAttributeWriteDiagnostic::BadSetAttr { value_ty, + failure: CallError(kind, bindings), }); } false @@ -611,6 +700,7 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { value_ty: Type<'db>, emit_diagnostics: bool, ) -> bool { + let env = self.builder.program_environment(); let db = self.builder.db(); let descriptor_ty = descriptor_ty.resolve_type_alias(db); if let Type::Union(union) = descriptor_ty { @@ -635,7 +725,7 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { return true; } - if property_setter_returns_never(db, descriptor_ty, receiver_ty, value_ty) { + if property_setter_returns_never(db, env, descriptor_ty, receiver_ty, value_ty) { if emit_diagnostics { self.report(AssignmentAttributeWriteDiagnostic::TerminalDescriptor); } @@ -644,6 +734,7 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { match descriptor_ty.try_call_dunder_with_policy( db, + env, "__set__", &mut CallArguments::positional([receiver_ty, value_ty]), TypeContext::default(), @@ -652,9 +743,11 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { Ok(_) => true, Err(CallDunderError::CallError(kind, bindings, _)) => { if emit_diagnostics { - self.report(AssignmentAttributeWriteDiagnostic::BadDunderSet(CallError( - kind, bindings, - ))); + self.report(AssignmentAttributeWriteDiagnostic::BadDunderSet { + failure: CallError(kind, bindings), + descriptor_ty, + includes_descriptor_argument: false, + }); } false } @@ -676,21 +769,37 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { emit_diagnostics: bool, ) -> bool { let db = self.builder.db(); - if property_setter_returns_never(db, descriptor_ty, object_ty, value_ty) { + let env = self.builder.program_environment(); + let setter_result = setter_ty.try_call( + db, + env, + &CallArguments::positional([descriptor_ty, object_ty, value_ty]), + ); + // `Never` supports arbitrary operations only because there can be no runtime value to + // mutate; it is not a concrete descriptor with a terminal setter. + let setter_returns_never = !descriptor_ty.is_never() + && match &setter_result { + Ok(bindings) => bindings.return_type(db, env).is_never(), + Err(error) => error.return_type(db, env).is_never(), + }; + if setter_returns_never + || property_setter_returns_never(db, env, descriptor_ty, object_ty, value_ty) + { if emit_diagnostics { self.report(AssignmentAttributeWriteDiagnostic::TerminalDescriptor); } return false; } - match setter_ty.try_call( - db, - &CallArguments::positional([descriptor_ty, object_ty, value_ty]), - ) { + match setter_result { Ok(_) => true, Err(error) => { if emit_diagnostics { - self.report(AssignmentAttributeWriteDiagnostic::BadDunderSet(error)); + self.report(AssignmentAttributeWriteDiagnostic::BadDunderSet { + failure: error, + descriptor_ty, + includes_descriptor_argument: true, + }); } false } @@ -761,6 +870,7 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { fn report(&mut self, diagnostic: AssignmentAttributeWriteDiagnostic<'db>) { let db = self.builder.db(); + let env = self.builder.program_environment(); match diagnostic { AssignmentAttributeWriteDiagnostic::InvalidCompositeAssignment { object_ty, @@ -773,9 +883,9 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { { builder.into_diagnostic(format_args!( "Object of type `{}` is not assignable to attribute `{}` on type `{}`", - value_ty.display(db), + value_ty.display(db, env), self.attribute, - object_ty.display(db), + object_ty.display(db, env), )); } } @@ -788,7 +898,7 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { builder.into_diagnostic(format_args!( "Cannot assign to attribute `{}` on type `{}`", self.attribute, - self.object_ty.display(db), + self.object_ty.display(db, env), )); } } @@ -801,7 +911,7 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { builder.into_diagnostic(format_args!( "Cannot assign to ClassVar `{}` from an instance of type `{}`", self.attribute, - self.object_ty.display(db), + self.object_ty.display(db, env), )); } } @@ -818,19 +928,19 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { format!( "Cannot assign to unresolved attribute `{}` on type `{}`", self.attribute, - self.object_ty.display(db) + self.object_ty.display(db, env) ) } else if is_setattr_synthesized { format!( "Property `{}` defined in `{}` is read-only", self.attribute, - self.object_ty.display(db) + self.object_ty.display(db, env) ) } else { format!( "Cannot assign to attribute `{}` on type `{}` whose `__setattr__` method returns `Never`/`NoReturn`", self.attribute, - self.object_ty.display(db) + self.object_ty.display(db, env) ) }; builder.into_diagnostic(message); @@ -845,17 +955,23 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { builder.into_diagnostic(format_args!( "Cannot assign to attribute `{}` on type `{}` whose `__set__` method returns `Never`/`NoReturn`", self.attribute, - self.object_ty.display(db), + self.object_ty.display(db, env), )); } } - AssignmentAttributeWriteDiagnostic::BadDunderSet(failure) => { + AssignmentAttributeWriteDiagnostic::BadDunderSet { + failure, + descriptor_ty, + includes_descriptor_argument, + } => { report_bad_dunder_set_call( &self.builder.context, &failure, - self.attribute, self.object_ty, + descriptor_ty, + includes_descriptor_argument, self.target, + self.value, ); } AssignmentAttributeWriteDiagnostic::PossiblyMissing => { @@ -866,19 +982,22 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { self.object_ty, ); } - AssignmentAttributeWriteDiagnostic::BadSetAttr { value_ty } => { - if let Some(builder) = self - .builder - .context - .report_lint(&UNRESOLVED_ATTRIBUTE, self.target) - { - builder.into_diagnostic(format_args!( - "Cannot assign object of type `{}` to attribute `{}` on type `{}` with custom `__setattr__` method.", - value_ty.display(db), - self.attribute, - self.object_ty.display(db) - )); - } + AssignmentAttributeWriteDiagnostic::BadSetAttr { value_ty, failure } => { + failure.report_diagnostics_with_override( + &self.builder.context, + self.target.into(), + &CallDiagnosticOverride { + lint: &INVALID_ASSIGNMENT, + message: format!( + "Cannot assign object of type `{}` to attribute `{}` on type `{}`", + value_ty.display(db, env), + self.attribute, + self.object_ty.display(db, env) + ), + info: "This assignment implicitly calls a custom `__setattr__` method", + argument_ranges: &[self.target.range(), self.value.range()], + }, + ); } AssignmentAttributeWriteDiagnostic::Unresolved { with_period } => { if let Some(builder) = self @@ -890,13 +1009,13 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { builder.into_diagnostic(format_args!( "Unresolved attribute `{}` on type `{}`.", self.attribute, - self.object_ty.display(db) + self.object_ty.display(db, env) )); } else { builder.into_diagnostic(format_args!( "Unresolved attribute `{}` on type `{}`", self.attribute, - self.object_ty.display(db) + self.object_ty.display(db, env) )); } } @@ -910,7 +1029,7 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { builder.into_diagnostic(format_args!( "Cannot assign to instance attribute `{}` from the class object `{}`", self.attribute, - self.object_ty.display(db) + self.object_ty.display(db, env) )); } } diff --git a/crates/ty_python_semantic/src/types/infer/builder/binary_expressions.rs b/crates/ty_python_semantic/src/types/infer/builder/binary_expressions.rs index 3f3e9d29e6..144f17cd7c 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/binary_expressions.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/binary_expressions.rs @@ -1,8 +1,9 @@ +use crate::Db; +use crate::ProgramEnvironment; use compact_str::CompactString; use ruff_python_ast::{self as ast, AnyNodeRef}; use super::TypeInferenceBuilder; -use crate::Db; use crate::types::call::CallArguments; use crate::types::constraints::ConstraintSetBuilder; use crate::types::cyclic::CycleDetector; @@ -35,6 +36,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { binary: &ast::ExprBinOp, tcx: TypeContext<'db>, ) -> Type<'db> { + let env = self.program_environment(); if tcx.is_typealias() { return self.infer_pep_604_union_type_alias(binary, tcx); } @@ -53,13 +55,13 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let db = self.db(); let left_ty = self.infer_expression(left, tcx); let right_ty = self.infer_expression(right, tcx); - let none = Type::none(db); - let left_non_none = if left_ty.is_subtype_of(db, none) { + let none = Type::none(db, env); + let left_non_none = if left_ty.is_subtype_of(db, env, none) { Type::Never } else { match left_ty { - Type::Union(u) => u.map(db, |elem| { - if elem.is_subtype_of(db, none) { + Type::Union(u) => u.map(db, env, |elem| { + if elem.is_subtype_of(db, env, none) { Type::Never } else { *elem @@ -68,10 +70,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { _ => left_ty, } }; - if left_non_none.is_equivalent_to(db, right_ty) { + if left_non_none.is_equivalent_to(db, env, right_ty) { return left_non_none; } - return UnionType::from_two_elements(db, left_non_none, right_ty); + return UnionType::from_two_elements(db, env, left_non_none, right_ty); } let (left_ty, right_ty) = @@ -96,6 +98,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { tcx: TypeContext<'db>, ) -> Type<'db> { let db = self.db(); + let env = self.program_environment(); let ast::ExprBinOp { left, op, @@ -118,7 +121,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // `TypeAlias`, which uses `X | Y` syntax, where the returned type is not actually a union. // And attempting to enforce this more tightly showed a lot of potential false positives in // the ecosystem. - if left_ty.is_equivalent_to(db, right_ty) { + if left_ty.is_equivalent_to(db, env, right_ty) { left_ty } else { UnionTypeInstance::from_value_expression_types( @@ -140,6 +143,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { right: &ast::Expr, tcx: TypeContext<'db>, ) -> BinaryExpressionOperandTypes<'db> { + let db = self.db(); // As a special case, pass `tcx` to binary operands that are collection literals/displays. // Note that it's not correct to pass it to all binary operands, for example: // ``` @@ -172,7 +176,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if let Type::TypedDict(typed_dict) = right_ty && let Some(ty) = self.try_typed_dict_pep_584_dunder( left, - typed_dict.to_partial(self.db()), + typed_dict.to_partial(db), typed_dict, "__ror__", ) @@ -194,7 +198,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { && matches!(right, ast::Expr::Dict(_)) && let Some(ty) = self.try_typed_dict_pep_584_dunder( right, - typed_dict.to_partial(self.db()), + typed_dict.to_partial(db), typed_dict, "__or__", ) @@ -216,21 +220,22 @@ impl<'db> TypeInferenceBuilder<'db, '_> { dunder_name: &str, ) -> Option> { let db = self.db(); - let update_ty = self.speculate_without_diagnostics().infer_expression( update, TypeContext::new(Some(Type::TypedDict(update_context_typed_dict))), ); + let env = self.program_environment(); Type::TypedDict(result_typed_dict) .try_call_dunder( db, + env, dunder_name, CallArguments::positional([update_ty]), TypeContext::default(), ) .ok() - .map(|bindings| bindings.return_type(db)) + .map(|bindings| bindings.return_type(db, env)) } /// Handle `TypedDict |= value` before the normal `__ior__` path runs. @@ -247,6 +252,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { value_expr: &ast::Expr, infer_value_ty: &mut dyn FnMut(&mut Self, TypeContext<'db>) -> Type<'db>, ) -> Option> { + let db = self.db(); if assignment.op != ast::Operator::BitOr { return None; } @@ -268,7 +274,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } // Subset updates use the mutation-safe patch as context. - let update_patch = typed_dict.to_update_patch(self.db()); + let update_patch = typed_dict.to_update_patch(db); if self .try_typed_dict_pep_584_dunder(value_expr, update_patch, typed_dict, "__ior__") .is_some() @@ -290,16 +296,17 @@ impl<'db> TypeInferenceBuilder<'db, '_> { /// otherwise returns the union of all results. pub(super) fn map_constrained_typevar_constraints( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typevar: Type<'db>, constraints: TypeVarConstraints<'db>, mut op: impl FnMut(Type<'db>) -> Option>, ) -> Option> { - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); let mut any_different = false; for constraint in constraints.elements(db) { let result = op(*constraint)?; - if !result.is_equivalent_to(db, *constraint) { + if !result.is_equivalent_to(db, env, *constraint) { any_different = true; } builder = builder.add(result); @@ -343,6 +350,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { visitor: &BinaryExpressionVisitor<'db>, tcx: TypeContext<'db>, ) -> Option> { + let env = self.program_environment(); let db = self.db(); // Check for division by zero; this doesn't change the inferred type for the expression, but @@ -366,7 +374,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { self.infer_binary_expression_type_impl( node, emitted_division_by_zero_diagnostic, - overlapping.value_type(db), + overlapping.value_type(db, env), right_ty, op, visitor, @@ -407,7 +415,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { node, emitted_division_by_zero_diagnostic, left_ty, - overlapping.value_type(db), + overlapping.value_type(db, env), op, visitor, tcx, @@ -419,14 +427,15 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // dunder against the hole instead lets the *other* operand decide the result — // `int * ` reads as `int`, which `scale(3, 1.5)` disproves at runtime (left, right, _) - if gradual_hole(db, left).is_some() || gradual_hole(db, right).is_some() => + if gradual_hole(db, env, left).is_some() + || gradual_hole(db, env, right).is_some() => { visitor.visit(db, (left_ty, op, right_ty), || { self.infer_binary_expression_type_impl( node, emitted_division_by_zero_diagnostic, - gradual_hole(db, left).unwrap_or(left), - gradual_hole(db, right).unwrap_or(right), + gradual_hole(db, env, left).unwrap_or(left), + gradual_hole(db, env, right).unwrap_or(right), op, visitor, tcx, @@ -442,11 +451,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if self.is_basedpython_file() && DeferredOperation::Binary(op).is_checked_arithmetic() && (is_symbolic_operand(left) || is_symbolic_operand(right)) - && is_integer_operand(db, left) - && is_integer_operand(db, right) => + && is_integer_operand(db, env, left) + && is_integer_operand(db, env, right) => { Some(DeferredType::build( db, + env, &DeferredOperation::Binary(op), Box::new([left, right]), )) @@ -455,7 +465,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { self.infer_binary_expression_type_impl( node, emitted_division_by_zero_diagnostic, - deferred.reduced(db), + deferred.reduced(db, env), right_ty, op, visitor, @@ -467,13 +477,13 @@ impl<'db> TypeInferenceBuilder<'db, '_> { node, emitted_division_by_zero_diagnostic, left_ty, - deferred.reduced(db), + deferred.reduced(db, env), op, visitor, tcx, ) }), - (Type::Union(lhs_union), rhs, _) => lhs_union.try_map(db, |lhs_element| { + (Type::Union(lhs_union), rhs, _) => lhs_union.try_map(db, env, |lhs_element| { self.infer_binary_expression_type_impl( node, emitted_division_by_zero_diagnostic, @@ -484,7 +494,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { tcx, ) }), - (lhs, Type::Union(rhs_union), _) => rhs_union.try_map(db, |rhs_element| { + (lhs, Type::Union(rhs_union), _) => rhs_union.try_map(db, env, |rhs_element| { self.infer_binary_expression_type_impl( node, emitted_division_by_zero_diagnostic, @@ -561,13 +571,13 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }), (Type::TypedDict(left_typed_dict), rhs, ast::Operator::BitOr) - if rhs.is_assignable_to(db, Type::TypedDict(left_typed_dict)) => + if rhs.is_assignable_to(db, env, Type::TypedDict(left_typed_dict)) => { Some(Type::TypedDict(left_typed_dict)) } (lhs, Type::TypedDict(right_typed_dict), ast::Operator::BitOr) - if lhs.is_assignable_to(db, Type::TypedDict(right_typed_dict)) => + if lhs.is_assignable_to(db, env, Type::TypedDict(right_typed_dict)) => { Some(Type::TypedDict(right_typed_dict)) } @@ -611,10 +621,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> { (Type::TypeVar(left_tvar), Type::TypeVar(right_tvar), _) if left_tvar.identity(db) == right_tvar.identity(db) => { - match left_tvar.typevar(db).bound_or_constraints(db) { + match left_tvar.typevar(db).bound_or_constraints(db, env) { Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { Self::map_constrained_typevar_constraints( db, + env, left_ty, constraints, |constraint| { @@ -630,7 +641,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ) } // For bounded TypeVars or unconstrained TypeVars, fall through to the default handling. - _ => Type::try_call_bin_op_return_type(db, left_ty, op, right_ty), + _ => Type::try_call_bin_op_return_type(db, env, left_ty, op, right_ty), } } @@ -642,10 +653,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // TODO: We expect to replace this with more general support once we migrate to the new // solver. (Type::TypeVar(left_tvar), rhs, _) if !rhs.is_type_var() => { - match left_tvar.typevar(db).bound_or_constraints(db) { + match left_tvar.typevar(db).bound_or_constraints(db, env) { Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { Self::map_constrained_typevar_constraints( db, + env, left_ty, constraints, |constraint| { @@ -662,17 +674,18 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ) } // For bounded TypeVars or unconstrained TypeVars, fall through to the default handling. - _ => Type::try_call_bin_op_return_type(db, left_ty, op, right_ty), + _ => Type::try_call_bin_op_return_type(db, env, left_ty, op, right_ty), } } // When the right operand is a constrained TypeVar and the left operand is not a TypeVar, // we check if each constraint supports the operation with the left operand. (lhs, Type::TypeVar(right_tvar), _) if !lhs.is_type_var() => { - match right_tvar.typevar(db).bound_or_constraints(db) { + match right_tvar.typevar(db).bound_or_constraints(db, env) { Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { Self::map_constrained_typevar_constraints( db, + env, right_ty, constraints, |constraint| { @@ -689,7 +702,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ) } // For bounded TypeVars or unconstrained TypeVars, fall through to the default handling. - _ => Type::try_call_bin_op_return_type(db, left_ty, op, right_ty), + _ => Type::try_call_bin_op_return_type(db, env, left_ty, op, right_ty), } } @@ -700,7 +713,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // positional arguments get. In those cases we need to explicitly delegate to the base // type, so that it hits the `Type::Union` branches above. (Type::NewTypeInstance(newtype), rhs, _) => { - Type::try_call_bin_op_return_type(db, left_ty, op, right_ty).or_else(|| { + Type::try_call_bin_op_return_type(db, env, left_ty, op, right_ty).or_else(|| { self.infer_binary_expression_type_impl( node, emitted_division_by_zero_diagnostic, @@ -713,7 +726,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }) } (lhs, Type::NewTypeInstance(newtype), _) => { - Type::try_call_bin_op_return_type(db, left_ty, op, right_ty).or_else(|| { + Type::try_call_bin_op_return_type(db, env, left_ty, op, right_ty).or_else(|| { self.infer_binary_expression_type_impl( node, emitted_division_by_zero_diagnostic, @@ -740,7 +753,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { RecursivelyDefined::No }; let result = - literal_binary_op(db, left_ty, right_ty, op, self.is_basedpython_file()); + literal_binary_op(db, env, left_ty, right_ty, op, self.is_basedpython_file()); result.map(|result| match result { Type::LiteralValue(literal) => { @@ -757,8 +770,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ) => { let constraints = ConstraintSetBuilder::new(); let result = constraints.into_owned(|constraints| { - let left = constraints.load(db, left.constraints(db)); - let right = constraints.load(db, right.constraints(db)); + let left = constraints.load(db, env, left.constraints(db)); + let right = constraints.load(db, env, right.constraints(db)); left.and(db, constraints, || right) }); Some(Type::KnownInstance(KnownInstanceType::ConstraintSet( @@ -773,8 +786,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ) => { let constraints = ConstraintSetBuilder::new(); let result = constraints.into_owned(|constraints| { - let left = constraints.load(db, left.constraints(db)); - let right = constraints.load(db, right.constraints(db)); + let left = constraints.load(db, env, left.constraints(db)); + let right = constraints.load(db, env, right.constraints(db)); left.or(db, constraints, || right) }); Some(Type::KnownInstance(KnownInstanceType::ConstraintSet( @@ -814,7 +827,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ), ast::Operator::BitOr, ) => { - if left_ty.is_equivalent_to(db, right_ty) { + if left_ty.is_equivalent_to(db, env, right_ty) { Some(left_ty) } else { Some(UnionTypeInstance::from_value_expression_types( @@ -871,6 +884,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ast::Operator::BitOr, ) => Type::try_call_bin_op_with_policy( db, + env, left_ty, ast::Operator::BitOr, right_ty, @@ -878,7 +892,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, ) .ok() - .map(|binding| binding.return_type(db)), + .map(|binding| binding.return_type(db, env)), // fold `(a, b) * n` (and `n * (a, b)`) into a fixed-length tuple with the // elements repeated `n` times, matching the runtime behaviour of @@ -887,23 +901,23 @@ impl<'db> TypeInferenceBuilder<'db, '_> { (Type::NominalInstance(_), _, ast::Operator::Mult) if right_ty.as_int_like_literal().is_some() => { - fold_tuple_repeat(db, left_ty, right_ty).or_else(|| { - Type::try_call_bin_op_return_type_with_tcx(db, left_ty, op, right_ty, tcx) + fold_tuple_repeat(db, env, left_ty, right_ty).or_else(|| { + Type::try_call_bin_op_return_type_with_tcx(db, env, left_ty, op, right_ty, tcx) }) } (_, Type::NominalInstance(_), ast::Operator::Mult) if left_ty.as_int_like_literal().is_some() => { - fold_tuple_repeat(db, right_ty, left_ty).or_else(|| { - Type::try_call_bin_op_return_type_with_tcx(db, left_ty, op, right_ty, tcx) + fold_tuple_repeat(db, env, right_ty, left_ty).or_else(|| { + Type::try_call_bin_op_return_type_with_tcx(db, env, left_ty, op, right_ty, tcx) }) } // fold `(a, b) + (c,)` into `(a, b, c)`. as with `*`, typeshed's `tuple.__add__` // otherwise widens the concatenation to `tuple[T, ...]` (Type::NominalInstance(_), Type::NominalInstance(_), ast::Operator::Add) => { - fold_tuple_concat(db, left_ty, right_ty).or_else(|| { - Type::try_call_bin_op_return_type_with_tcx(db, left_ty, op, right_ty, tcx) + fold_tuple_concat(db, env, left_ty, right_ty).or_else(|| { + Type::try_call_bin_op_return_type_with_tcx(db, env, left_ty, op, right_ty, tcx) }) } @@ -965,7 +979,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { | Type::TypeForm(_) | Type::TypedDict(_), op, - ) => Type::try_call_bin_op_return_type_with_tcx(db, left_ty, op, right_ty, tcx), + ) => Type::try_call_bin_op_return_type_with_tcx(db, env, left_ty, op, right_ty, tcx), } } @@ -1005,7 +1019,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { 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(db) + left.display(db, self.program_environment()) )); } @@ -1129,6 +1143,7 @@ fn complex_binary_op_result( /// `MAX_LENGTH`. A non-positive multiplier folds to the empty tuple. pub(crate) fn fold_tuple_repeat<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, tuple_ty: Type<'db>, multiplier: Type<'db>, ) -> Option> { @@ -1152,7 +1167,7 @@ pub(crate) fn fold_tuple_repeat<'db>( for _ in 0..factor { repeated.extend_from_slice(elements); } - Some(Type::heterogeneous_tuple(db, repeated)) + Some(Type::heterogeneous_tuple(db, env, repeated)) } /// Fold `left + right` into a single fixed-length tuple concatenating their elements. @@ -1161,6 +1176,7 @@ pub(crate) fn fold_tuple_repeat<'db>( /// both operands are exact fixed-length tuples. pub(crate) fn fold_tuple_concat<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, left_ty: Type<'db>, right_ty: Type<'db>, ) -> Option> { @@ -1171,6 +1187,7 @@ pub(crate) fn fold_tuple_concat<'db>( }; Some(Type::heterogeneous_tuple( db, + env, left.all_elements() .iter() .chain(right.all_elements()) @@ -1184,6 +1201,7 @@ pub(crate) fn fold_tuple_concat<'db>( /// between value inference and the deferred type-operation path. pub(crate) fn literal_unary_op<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, op: ast::UnaryOp, literal: crate::types::LiteralValueType<'db>, ) -> Option> { @@ -1199,7 +1217,7 @@ pub(crate) fn literal_unary_op<'db>( .as_i64() .checked_neg() .map(Type::int_literal) - .unwrap_or_else(|| KnownClass::Int.to_instance(db)), + .unwrap_or_else(|| KnownClass::Int.to_instance(db, env)), ), (ast::UnaryOp::USub, LiteralValueTypeKind::Bool(value)) => { Some(Type::int_literal(-i64::from(value))) @@ -1228,6 +1246,7 @@ pub(crate) fn literal_unary_op<'db>( /// re-evaluates to `Literal[6]` pub(crate) fn literal_binary_op<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, left_ty: Type<'db>, right_ty: Type<'db>, op: ast::Operator, @@ -1241,21 +1260,21 @@ pub(crate) fn literal_binary_op<'db>( n.as_i64() .checked_add(m.as_i64()) .map(Type::int_literal) - .unwrap_or_else(|| KnownClass::Int.to_instance(db)), + .unwrap_or_else(|| KnownClass::Int.to_instance(db, env)), ), (LiteralValueTypeKind::Int(n), LiteralValueTypeKind::Int(m), ast::Operator::Sub) => Some( n.as_i64() .checked_sub(m.as_i64()) .map(Type::int_literal) - .unwrap_or_else(|| KnownClass::Int.to_instance(db)), + .unwrap_or_else(|| KnownClass::Int.to_instance(db, env)), ), (LiteralValueTypeKind::Int(n), LiteralValueTypeKind::Int(m), ast::Operator::Mult) => Some( n.as_i64() .checked_mul(m.as_i64()) .map(Type::int_literal) - .unwrap_or_else(|| KnownClass::Int.to_instance(db)), + .unwrap_or_else(|| KnownClass::Int.to_instance(db, env)), ), (LiteralValueTypeKind::Int(n), LiteralValueTypeKind::Int(m), ast::Operator::Div) => Some({ @@ -1268,7 +1287,7 @@ pub(crate) fn literal_binary_op<'db>( } else { None }; - computed.unwrap_or_else(|| KnownClass::Float.to_instance(db)) + computed.unwrap_or_else(|| KnownClass::Float.to_instance(db, env)) }), (LiteralValueTypeKind::Int(n), LiteralValueTypeKind::Int(m), ast::Operator::FloorDiv) => { @@ -1281,7 +1300,7 @@ pub(crate) fn literal_binary_op<'db>( q = q.map(|q| q - 1); } q.map(Type::int_literal) - .unwrap_or_else(|| KnownClass::Int.to_instance(db)) + .unwrap_or_else(|| KnownClass::Int.to_instance(db, env)) }) } @@ -1294,18 +1313,18 @@ pub(crate) fn literal_binary_op<'db>( r = r.map(|x| x + m.as_i64()); } r.map(Type::int_literal) - .unwrap_or_else(|| KnownClass::Int.to_instance(db)) + .unwrap_or_else(|| KnownClass::Int.to_instance(db, env)) }), (LiteralValueTypeKind::Int(n), LiteralValueTypeKind::Int(m), ast::Operator::Pow) => Some({ if m.as_i64() < 0 { - KnownClass::Float.to_instance(db) + KnownClass::Float.to_instance(db, env) } else { u32::try_from(m.as_i64()) .ok() .and_then(|m| n.as_i64().checked_pow(m)) .map(Type::int_literal) - .unwrap_or_else(|| KnownClass::Int.to_instance(db)) + .unwrap_or_else(|| KnownClass::Int.to_instance(db, env)) } }), @@ -1409,6 +1428,7 @@ pub(crate) fn literal_binary_op<'db>( op, ) => literal_binary_op( db, + env, Type::int_literal(i64::from(b1)), right_ty, op, @@ -1417,6 +1437,7 @@ pub(crate) fn literal_binary_op<'db>( (LiteralValueTypeKind::Int(_), LiteralValueTypeKind::Bool(b2), op) => literal_binary_op( db, + env, left_ty, Type::int_literal(i64::from(b2)), op, @@ -1452,7 +1473,7 @@ pub(crate) fn literal_binary_op<'db>( .filter(|&m| m <= headroom) .and_then(|m| n.checked_shl(m)) .map(Type::int_literal) - .unwrap_or_else(|| KnownClass::Int.to_instance(db)), + .unwrap_or_else(|| KnownClass::Int.to_instance(db, env)), ) } @@ -1461,7 +1482,7 @@ pub(crate) fn literal_binary_op<'db>( let result = match u32::try_from(m.as_i64()) { Ok(m) => Type::int_literal(n >> m.clamp(0, 63)), Err(_) if m.as_i64() > 0 => Type::int_literal(if n >= 0 { 0 } else { -1 }), - Err(_) => KnownClass::Int.to_instance(db), + Err(_) => KnownClass::Int.to_instance(db, env), }; Some(result) } @@ -1504,14 +1525,14 @@ pub(crate) fn literal_binary_op<'db>( // which are diagnosed before the type is used, and the deferred // path, which already passes `is_basedpython: true` let widened = if complex_involved { - crate::types::set_theoretic::KnownUnion::Complex.to_type(db) + crate::types::set_theoretic::KnownUnion::Complex.to_type(db, env) } else { - crate::types::set_theoretic::KnownUnion::Float.to_type(db) + crate::types::set_theoretic::KnownUnion::Float.to_type(db, env) }; Some(widened) } Some(LiteralArithOutcome::Unsupported) | None => { - Type::try_call_bin_op_return_type(db, left_ty, op, right_ty) + Type::try_call_bin_op_return_type(db, env, left_ty, op, right_ty) } } } diff --git a/crates/ty_python_semantic/src/types/infer/builder/class.rs b/crates/ty_python_semantic/src/types/infer/builder/class.rs index b168b1f34c..1e7d2214ee 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/class.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/class.rs @@ -1,3 +1,5 @@ +use crate::Db; +use crate::ProgramEnvironment; use crate::place::Place; use crate::types::{ CallArguments, ClassLiteralFlags, DataclassFlags, DataclassParams, KnownClass, @@ -14,7 +16,7 @@ use crate::types::{ special_form::TypeQualifier, }; use ruff_python_ast::{self as ast, helpers::any_over_expr}; -use ty_module_resolver::{KnownModule, file_to_module}; +use ty_module_resolver::{ImportingFile, KnownModule, file_to_module}; use ty_python_core::{definition::Definition, scope::NodeWithScopeRef}; impl<'db> TypeInferenceBuilder<'db, '_> { @@ -84,6 +86,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { class_node: &ast::StmtClassDef, definition: Definition<'db>, ) { + let env = self.program_environment(); let ast::StmtClassDef { range: _, node_index: _, @@ -142,11 +145,15 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let body_scope = self .index .node_scope(NodeWithScopeRef::Class(class_node)) - .to_scope_id(db, self.file()); + .to_scope_id(db, self.program_file()); - let maybe_known_class = KnownClass::try_from_file_and_name(db, self.file(), name); + let file = self.program_file(); + let importing_file = ImportingFile::File(file.file(db), env.resolver_environment(db)); + let maybe_known_class = KnownClass::try_from_file_and_name(db, importing_file, name); - let known_module = || file_to_module(db, self.file()).and_then(|module| module.known(db)); + let known_module = || { + file_to_module(db, importing_file.resolver_file(db)).and_then(|module| module.known(db)) + }; let in_typing_module = || { matches!( known_module(), @@ -166,6 +173,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if class_node.has_synthetic_marker("variant_tuple") { dataclass_params = Some(DataclassParams::from_flags( db, + env, DataclassFlags::default() | DataclassFlags::FROZEN, )); } @@ -252,7 +260,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .as_function_literal() .is_some_and(|function| function.is_known(db, KnownFunction::Dataclass)) { - dataclass_params = Some(DataclassParams::default_params(db)); + dataclass_params = Some(DataclassParams::default_params(db, env)); continue; } @@ -344,21 +352,22 @@ impl<'db> TypeInferenceBuilder<'db, '_> { dataclass_transformer_params, total_ordering, ); - let decorator_result = apply_class_decorator(db, decorator_ty, original_class_ty); + let decorator_result = apply_class_decorator(db, env, decorator_ty, original_class_ty); let decorated_ty = match &decorator_result { Ok(return_ty) => *return_ty, - Err(error) => error.return_type(db), + Err(error) => error.return_type(db, env), }; if is_unknown_decorator_result(db, decorated_ty) { if !preserve_binding_for_unknown_result( db, + env, decorator_ty, decorator_call_ty(decorator), decorated_ty, ) { metadata_applies_to_original_class = false; } - } else if !type_retains_original_class(db, original_class_ty, decorated_ty) { + } else if !type_retains_original_class(db, env, original_class_ty, decorated_ty) { metadata_applies_to_original_class = false; } @@ -392,13 +401,13 @@ impl<'db> TypeInferenceBuilder<'db, '_> { { decorator_result } - _ => apply_class_decorator(db, decorator_ty, inferred_ty), + _ => apply_class_decorator(db, env, decorator_ty, inferred_ty), }; let decorated_ty = match decorator_result { Ok(return_ty) => return_ty, Err(CallError(_, bindings)) => { bindings.report_diagnostics(&self.context, decorator_node.into()); - bindings.return_type(db) + bindings.return_type(db, env) } }; let decorated_ty = match decorated_ty { @@ -410,15 +419,22 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let should_preserve_binding = is_unknown_decorator_result(db, decorated_ty) && preserve_binding_for_unknown_result( db, + env, decorator_ty, decorator_call_ty(decorator_node), decorated_ty, ); inferred_ty = if should_preserve_binding { inferred_ty - } else if class_decorator_preserves_class_binding(db, original_class_ty, decorated_ty) { + } else if class_decorator_preserves_class_binding( + db, + env, + original_class_ty, + decorated_ty, + ) { merge_class_preserving_decorator_result( db, + env, original_class_ty, inferred_ty, decorated_ty, @@ -533,14 +549,15 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } fn apply_class_decorator<'db>( - db: &'db dyn crate::Db, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, decorator_ty: Type<'db>, decorated_ty: Type<'db>, ) -> Result, CallError<'db>> { let call_arguments = CallArguments::positional([decorated_ty]); decorator_ty - .try_call(db, &call_arguments) - .map(|bindings| bindings.return_type(db)) + .try_call(db, env, &call_arguments) + .map(|bindings| bindings.return_type(db, env)) } /// Return true if a decorator result still binds the name to the original class. @@ -557,7 +574,8 @@ fn apply_class_decorator<'db>( /// This also accepts metaclass-shaped results such as `type[C]`, because those still describe the /// original class object even if the decorator call produced a `SubclassOf` type internally. fn class_decorator_preserves_class_binding<'db>( - db: &'db dyn crate::Db, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, original_class: Type<'db>, decorated_class: Type<'db>, ) -> bool { @@ -574,26 +592,26 @@ fn class_decorator_preserves_class_binding<'db>( } Type::SubclassOf(subclass_of) => subclass_of .subclass_of() - .into_class(db) + .into_class(db, env) .is_some_and(|class| class == original_literal.default_specialization(db)), Type::Divergent(_) => true, - Type::Union(union) => union - .elements(db) - .iter() - .all(|element| class_decorator_preserves_class_binding(db, original_class, *element)), + Type::Union(union) => union.elements(db).iter().all(|element| { + class_decorator_preserves_class_binding(db, env, original_class, *element) + }), Type::TypeAlias(alias) => { - class_decorator_preserves_class_binding(db, original_class, alias.value_type(db)) + class_decorator_preserves_class_binding(db, env, original_class, alias.value_type(db)) } - _ => SubclassOfType::try_from_type(db, original_class).is_some_and(|original_meta_type| { - decorated_class.is_equivalent_to(db, original_meta_type) - }), + _ => SubclassOfType::try_from_type(db, env, original_class).is_some_and( + |original_meta_type| decorated_class.is_equivalent_to(db, env, original_meta_type), + ), } } /// Return true if a type still contains the original class object, even if it also carries extra /// intersection members. fn type_retains_original_class<'db>( - db: &'db dyn crate::Db, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, original_class: Type<'db>, decorated_class: Type<'db>, ) -> bool { @@ -601,15 +619,15 @@ fn type_retains_original_class<'db>( Type::Intersection(intersection) => intersection .positive(db) .iter() - .any(|element| type_retains_original_class(db, original_class, *element)), + .any(|element| type_retains_original_class(db, env, original_class, *element)), Type::Union(union) => union .elements(db) .iter() - .all(|element| type_retains_original_class(db, original_class, *element)), + .all(|element| type_retains_original_class(db, env, original_class, *element)), Type::TypeAlias(alias) => { - type_retains_original_class(db, original_class, alias.value_type(db)) + type_retains_original_class(db, env, original_class, alias.value_type(db)) } - _ => class_decorator_preserves_class_binding(db, original_class, decorated_class), + _ => class_decorator_preserves_class_binding(db, env, original_class, decorated_class), } } @@ -632,21 +650,22 @@ fn type_retains_original_class<'db>( /// `decorator_factory` carries the static information that tells us whether an unknown result can /// be preserved. fn preserve_binding_for_unknown_result<'db>( - db: &'db dyn crate::Db, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, decorator_ty: Type<'db>, decorator_call_ty: Option>, decorator_result_ty: Type<'db>, ) -> bool { - ClassDecoratorUnknownResultPolicy::from_decorator(db, decorator_ty, decorator_result_ty) + ClassDecoratorUnknownResultPolicy::from_decorator(db, env, decorator_ty, decorator_result_ty) == ClassDecoratorUnknownResultPolicy::PreserveBinding || decorator_call_ty.is_some_and(|ty| { - ClassDecoratorUnknownResultPolicy::from_decorator(db, ty, decorator_result_ty) + ClassDecoratorUnknownResultPolicy::from_decorator(db, env, ty, decorator_result_ty) == ClassDecoratorUnknownResultPolicy::PreserveBinding }) } /// Return true if applying a class decorator produced no useful replacement type. -fn is_unknown_decorator_result<'db>(db: &'db dyn crate::Db, ty: Type<'db>) -> bool { +fn is_unknown_decorator_result<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { ty.is_unknown() || is_unknown_class_object_decorator_result(db, ty) } @@ -663,7 +682,7 @@ fn is_unknown_decorator_result<'db>(db: &'db dyn crate::Db, ty: Type<'db>) -> bo /// @decorator /// class C: ... /// ``` -fn is_unknown_class_object_decorator_result<'db>(db: &'db dyn crate::Db, ty: Type<'db>) -> bool { +fn is_unknown_class_object_decorator_result<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { let Type::SubclassOf(subclass_of) = ty.resolve_type_alias(db) else { return false; }; @@ -695,7 +714,8 @@ impl ClassDecoratorUnknownResultPolicy { /// application result is unknown. Explicit return annotations are trusted as replacement /// intent. fn from_decorator<'db>( - db: &'db dyn crate::Db, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, decorator_ty: Type<'db>, decorator_result_ty: Type<'db>, ) -> Self { @@ -703,7 +723,7 @@ impl ClassDecoratorUnknownResultPolicy { return Self::ReplaceBinding; } - Self::known_from_decorator(db, decorator_ty, decorator_result_ty) + Self::known_from_decorator(db, env, decorator_ty, decorator_result_ty) .unwrap_or(Self::ReplaceBinding) } @@ -725,7 +745,8 @@ impl ClassDecoratorUnknownResultPolicy { /// Callable instances and protocols delegate the decision to their `__call__` member, because /// the decorator value itself is not the function that receives the class. fn known_from_decorator<'db>( - db: &'db dyn crate::Db, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, decorator_ty: Type<'db>, decorator_result_ty: Type<'db>, ) -> Option { @@ -748,6 +769,7 @@ impl ClassDecoratorUnknownResultPolicy { let call_symbol = decorator_ty .member_lookup_with_policy( db, + env, "__call__", MemberLookupPolicy::NO_INSTANCE_FALLBACK, ) @@ -757,7 +779,7 @@ impl ClassDecoratorUnknownResultPolicy { && place.is_definitely_defined() { Some( - Self::known_from_decorator(db, place.ty, decorator_result_ty) + Self::known_from_decorator(db, env, place.ty, decorator_result_ty) .unwrap_or(Self::ReplaceBinding), ) } else { @@ -766,7 +788,7 @@ impl ClassDecoratorUnknownResultPolicy { } Type::Union(union) => Some( if union.elements(db).iter().all(|element| { - Self::known_from_decorator(db, *element, decorator_result_ty) + Self::known_from_decorator(db, env, *element, decorator_result_ty) == Some(Self::PreserveBinding) }) { Self::PreserveBinding @@ -775,7 +797,7 @@ impl ClassDecoratorUnknownResultPolicy { }, ), Type::TypeAlias(alias) => Some( - Self::known_from_decorator(db, alias.value_type(db), decorator_result_ty) + Self::known_from_decorator(db, env, alias.value_type(db), decorator_result_ty) .unwrap_or(Self::ReplaceBinding), ), Type::Callable(callable) => Some(match callable.provenance(db) { @@ -830,13 +852,14 @@ impl ClassDecoratorUnknownResultPolicy { /// members instead of collapsing back to the undecorated class when a later decorator simply /// returns the original class object again. fn merge_class_preserving_decorator_result<'db>( - db: &'db dyn crate::Db, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, original_class: Type<'db>, current_binding: Type<'db>, decorated_binding: Type<'db>, ) -> Type<'db> { if current_binding == original_class - || type_retains_original_class(db, original_class, current_binding) + || type_retains_original_class(db, env, original_class, current_binding) { current_binding } else { diff --git a/crates/ty_python_semantic/src/types/infer/builder/conditions.rs b/crates/ty_python_semantic/src/types/infer/builder/conditions.rs index a363448f43..e071a80906 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/conditions.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/conditions.rs @@ -6,6 +6,7 @@ use ty_module_resolver::KnownModule; use ty_python_core::{Truthiness, place::PlaceExpr}; use crate::place::Place; +use crate::types::ProgramEnvironment; use crate::types::{ ClassLiteral, IntersectionBuilder, KnownClass, Type, diagnostic::{OVERLAPPING_CONDITION, REDUNDANT_BOOLEAN_COMPARISON, REDUNDANT_CONDITION}, @@ -159,13 +160,14 @@ impl<'db> TypeInferenceBuilder<'db, '_> { root: &ast::Expr, polarity: ConditionPolarity, ) { + let env = self.program_environment(); // Only a value *read* can have its outcome fixed by its own type. A comparison or a call // computes a fresh value, and ty folding that one is the statically-known-branch // machinery doing its job — `elif isinstance(x, B):` closing an exhaustive chain is // deliberate, not a conditional that failed to be conditional. let is_place = PlaceExpr::try_from_expr(root).is_some(); let truthiness = ConditionTruthiness::classify( - self.expression_type(root).bool(self.db()), + self.expression_type(root).bool(self.db(), env), polarity, || is_place && self.is_artificial(root), ); @@ -189,6 +191,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { root: &ast::Expr, truthiness: ConditionTruthiness, ) { + let env = self.program_environment(); let Some((outcome, adjective)) = truthiness.constant_outcome() else { return; }; @@ -199,7 +202,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { builder.into_diagnostic(format_args!("This condition is always {outcome}")); diagnostic.info(format_args!( "`{}` is always {adjective}", - self.expression_type(root).display(self.db()) + self.expression_type(root).display(self.db(), env) )); } @@ -257,6 +260,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { root: &ast::Expr, polarity: ConditionPolarity, ) { + let env = self.program_environment(); if !self.context.is_lint_enabled(&OVERLAPPING_CONDITION) { return; } @@ -264,7 +268,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let Some(tested) = self.try_expression_type(root) else { return; }; - let selected = selected_branch(db, tested, polarity, db.analysis_settings(self.file())); + let selected = + selected_branch(db, env, tested, polarity, db.analysis_settings(self.file())); if !selected.conflates() { return; } @@ -277,16 +282,16 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }; let leading = leading .iter() - .map(|kind| format!("`{}`", kind.part.display(db))) + .map(|kind| format!("`{}`", kind.part.display(db, env))) .collect::>() .join(", "); let mut diagnostic = builder.into_diagnostic(format_args!( "This condition does not distinguish between {leading} and `{}`", - last.part.display(db) + last.part.display(db, env) )); diagnostic.info(format_args!( "`{}` is tested for {}", - tested.display(db), + tested.display(db, env), polarity.noun() )); diagnostic.help("Compare against the specific value instead of testing truthiness"); @@ -303,6 +308,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { op: ast::CmpOp, range: TextRange, ) { + let env = self.program_environment(); if !self.context.is_lint_enabled(&REDUNDANT_BOOLEAN_COMPARISON) { return; } @@ -323,7 +329,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { _ => return, }; let db = self.db(); - if !operand_ty.is_subtype_of(db, KnownClass::Bool.to_instance(db)) { + if !operand_ty.is_subtype_of(db, env, KnownClass::Bool.to_instance(db, env)) { return; } @@ -339,7 +345,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { )); diagnostic.info(format_args!( "`{}` already is the value this comparison produces", - operand_ty.display(db) + operand_ty.display(db, env) )); if is_equality == literal { diagnostic.help("Test the operand directly"); @@ -386,6 +392,7 @@ impl SelectedBranch<'_> { /// nobody expected it to. fn selected_branch<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, tested: Type<'db>, polarity: ConditionPolarity, settings: &AnalysisSettings, @@ -395,19 +402,19 @@ fn selected_branch<'db>( has_whole: false, has_partial: false, }; - for_each_arm(db, tested, &mut |arm| { - let Some(class) = arm_class(db, arm) else { + for_each_arm(db, env, tested, &mut |arm| { + let Some(class) = arm_class(db, env, arm) else { return; }; if is_exempt(db, arm, class, &settings.overlapping_condition_exempt_types) { return; } - let truthiness = arm_truthiness(db, arm, settings); + let truthiness = arm_truthiness(db, env, arm, settings); let whole = !truthiness.is_ambiguous(); let part = match truthiness { Truthiness::AlwaysTrue if polarity == ConditionPolarity::Falsy => return, Truthiness::AlwaysFalse if polarity == ConditionPolarity::Truthy => return, - Truthiness::Ambiguous => IntersectionBuilder::new(db) + Truthiness::Ambiguous => IntersectionBuilder::new(db, env) .add_positive(arm) .add_negative(polarity.rejects()) .build(), @@ -424,11 +431,11 @@ fn selected_branch<'db>( match selected .kinds .iter_mut() - .find(|kind| same_kind(db, kind.class, class)) + .find(|kind| same_kind(db, env, kind.class, class)) { // keep the most general class of the group, so which arm the message names does not // depend on the order the arms happen to be in - Some(kind) if derives(db, kind.class, class) => { + Some(kind) if derives(db, env, kind.class, class) => { kind.class = class; kind.part = part; } @@ -442,7 +449,12 @@ fn selected_branch<'db>( /// Visit the union arms of `tested`, or `tested` itself when it has only the one. /// /// An enum complement and an intersection with a finite alternative are unions in all but name. -fn for_each_arm<'db>(db: &'db dyn Db, tested: Type<'db>, visit: &mut impl FnMut(Type<'db>)) { +fn for_each_arm<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + tested: Type<'db>, + visit: &mut impl FnMut(Type<'db>), +) { if let Some(union) = tested.as_union_like(db) { for arm in union.elements(db) { visit(*arm); @@ -450,22 +462,27 @@ fn for_each_arm<'db>(db: &'db dyn Db, tested: Type<'db>, visit: &mut impl FnMut( return; } let alternatives = match tested { - Type::EnumComplement(complement) => Some(complement.remaining_literal_union(db)), - Type::Intersection(intersection) => intersection.finite_alternative_union(db), + Type::EnumComplement(complement) => Some(complement.remaining_literal_union(db, env)), + Type::Intersection(intersection) => intersection.finite_alternative_union(db, env), _ => None, }; match alternatives { // the equality guard keeps a type that describes itself from recursing forever - Some(alternatives) if alternatives != tested => for_each_arm(db, alternatives, visit), + Some(alternatives) if alternatives != tested => for_each_arm(db, env, alternatives, visit), _ => visit(tested), } } -fn arm_truthiness<'db>(db: &'db dyn Db, arm: Type<'db>, settings: &AnalysisSettings) -> Truthiness { - let truthiness = arm.bool(db); +fn arm_truthiness<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + arm: Type<'db>, + settings: &AnalysisSettings, +) -> Truthiness { + let truthiness = arm.bool(db, env); if truthiness.is_ambiguous() && settings.overlapping_condition_assume_truthy_instances - && defines_no_truthiness(db, arm) + && defines_no_truthiness(db, env, arm) { return Truthiness::AlwaysTrue; } @@ -476,11 +493,15 @@ fn arm_truthiness<'db>(db: &'db dyn Db, arm: Type<'db>, settings: &AnalysisSetti /// /// Such an instance is truthy unless a subclass says otherwise, which is why ty calls it /// ambiguous; `overlapping-condition-assume-truthy-instances` takes it at face value instead. -fn defines_no_truthiness<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { +fn defines_no_truthiness<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> bool { matches!(ty, Type::NominalInstance(_) | Type::ProtocolInstance(_)) && ["__bool__", "__len__"] .iter() - .all(|dunder| matches!(ty.member(db, dunder).place, Place::Undefined)) + .all(|dunder| matches!(ty.member(db, env, dunder).place, Place::Undefined)) } /// The class whose instances a union arm holds, if it has one. @@ -490,16 +511,20 @@ fn defines_no_truthiness<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { /// intersection is a remnant of some earlier narrowing whose truthiness ty models only /// approximately: `str & ~AlwaysFalsy` cannot be falsy, but ty still calls it ambiguous, so /// counting it would report a falsy branch that only ever holds `None`. -fn arm_class<'db>(db: &'db dyn Db, arm: Type<'db>) -> Option> { +fn arm_class<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + arm: Type<'db>, +) -> Option> { if arm.is_intersection() { return None; } - match arm.to_meta_type(db) { + match arm.to_meta_type(db, env) { Type::ClassLiteral(class) => Some(class), Type::GenericAlias(alias) => Some(ClassLiteral::Static(alias.origin(db))), Type::SubclassOf(subclass_of) => subclass_of .subclass_of() - .into_class(db) + .into_class(db, env) .map(|class| class.class_literal(db)), _ => None, } @@ -510,16 +535,28 @@ fn arm_class<'db>(db: &'db dyn Db, arm: Type<'db>) -> Option> /// /// Type arguments are deliberately out of scope: `list[A]` and `list[B]` are both lists, and a /// truthiness test sees only that one of them is empty. -fn same_kind<'db>(db: &'db dyn Db, left: ClassLiteral<'db>, right: ClassLiteral<'db>) -> bool { - left == right || derives(db, left, right) || derives(db, right, left) +fn same_kind<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + left: ClassLiteral<'db>, + right: ClassLiteral<'db>, +) -> bool { + left == right || derives(db, env, left, right) || derives(db, env, right, left) } /// Whether `subclass` derives `base`, ignoring type arguments. -fn derives<'db>(db: &'db dyn Db, subclass: ClassLiteral<'db>, base: ClassLiteral<'db>) -> bool { +fn derives<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + subclass: ClassLiteral<'db>, + base: ClassLiteral<'db>, +) -> bool { subclass != base - && subclass - .default_specialization(db) - .is_subclass_of(db, base.default_specialization(db)) + && subclass.default_specialization(db).is_subclass_of( + db, + env, + base.default_specialization(db), + ) } /// Whether the user has told us not to count this arm as distinct. diff --git a/crates/ty_python_semantic/src/types/infer/builder/dict.rs b/crates/ty_python_semantic/src/types/infer/builder/dict.rs index 067f39a109..fecb2c838e 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/dict.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/dict.rs @@ -17,6 +17,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { collection_expr: Option>, call_expression_tcx: TypeContext<'db>, ) -> Option> { + let db = self.db(); if !arguments.args.is_empty() { return None; } @@ -41,19 +42,16 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // back. let supports_typed_dict_context = { let mut speculative_builder = self.speculate_without_diagnostics(); + let env = speculative_builder.program_environment(); infer_unpacked_keyword_types(arguments, |expr, tcx| { speculative_builder.infer_expression(expr, tcx) }) .into_iter() .flatten() .all(|keyword_ty| { - keyword_ty - .is_assignable_to(speculative_builder.db(), Type::TypedDict(typed_dict)) - || extract_unpacked_typed_dict_keys_from_value_type( - speculative_builder.db(), - keyword_ty, - ) - .is_some() + keyword_ty.is_assignable_to(db, env, Type::TypedDict(typed_dict)) + || extract_unpacked_typed_dict_keys_from_value_type(db, env, keyword_ty) + .is_some() }) }; diff --git a/crates/ty_python_semantic/src/types/infer/builder/dynamic_class.rs b/crates/ty_python_semantic/src/types/infer/builder/dynamic_class.rs index 1f34f5cd2c..2eae58737a 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/dynamic_class.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/dynamic_class.rs @@ -48,28 +48,29 @@ impl<'db> TypeInferenceBuilder<'db, '_> { kind: DynamicClassKind, ) -> Option]>> { let db = self.db(); + let env = self.program_environment(); let fn_name = kind.function_name(); let formal_parameter_type = match kind { - DynamicClassKind::TypeCall => Type::homogeneous_tuple(db, Type::object()), + DynamicClassKind::TypeCall => Type::homogeneous_tuple(db, env, Type::object()), DynamicClassKind::NewClass => { - KnownClass::Iterable.to_specialized_instance(db, &[Type::object()]) + KnownClass::Iterable.to_specialized_instance(db, env, &[Type::object()]) } }; - if !bases_type.is_assignable_to(db, formal_parameter_type) + if !bases_type.is_assignable_to(db, env, formal_parameter_type) && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, bases_node) { let mut diagnostic = builder.into_diagnostic(format_args!( "Invalid argument to parameter 2 (`bases`) of `{fn_name}`" )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Expected `{}`, found `{}`", - formal_parameter_type.display(db), - bases_type.display(db) + formal_parameter_type.display(db, env), + bases_type.display(db, env) )); } - extract_fixed_length_iterable_element_types(db, bases_node, |expr| { + extract_fixed_length_iterable_element_types(db, env, bases_node, |expr| { self.expression_type(expr) }) } @@ -95,13 +96,14 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .map(|tuple| tuple.elts.as_slice()); let mut disjoint_bases = IncompatibleBases::default(); let fn_name = kind.function_name(); + let env = self.context.program_environment(); for (idx, base) in bases.iter().enumerate() { let diagnostic_node = bases_tuple_elts .and_then(|elts| elts.get(idx)) .unwrap_or(bases_node); - let Some(class_base) = ClassBase::try_from_type(db, *base, None) else { + let Some(class_base) = ClassBase::try_from_type(db, env, *base, None) else { continue; }; @@ -112,8 +114,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut diagnostic = builder.into_diagnostic(format_args!( "Invalid base for class created via `{fn_name}`" )); - diagnostic - .set_primary_message(format_args!("Has type `{}`", base.display(db))); + diagnostic.set_primary_annotation_message(format_args!( + "Has type `{}`", + base.display(db, env) + )); match class_base { ClassBase::Generic => { diagnostic.info(format_args!( @@ -143,8 +147,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut diagnostic = builder.into_diagnostic(format_args!( "Unsupported base for class created via `{fn_name}`" )); - diagnostic - .set_primary_message(format_args!("Has type `{}`", base.display(db))); + diagnostic.set_primary_annotation_message(format_args!( + "Has type `{}`", + base.display(db, env) + )); diagnostic.info(format_args!( "Classes created via `{fn_name}` cannot be protocols", )); @@ -172,16 +178,16 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if kind == DynamicClassKind::TypeCall && let Some((static_class, _)) = class_type.static_class_literal(db) - && is_enum_class_by_inheritance(db, static_class) + && is_enum_class_by_inheritance(db, env, static_class) { if let Some(builder) = self.context.report_lint(&INVALID_BASE, diagnostic_node) { let mut diagnostic = builder .into_diagnostic("Invalid base for class created via `type()`"); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Has type `{}`", - base.display(db) + base.display(db, env) )); diagnostic.info("Creating an enum class via `type()` is not supported"); diagnostic.info(format_args!( @@ -216,14 +222,14 @@ pub(super) fn report_dynamic_mro_errors<'db>( bases: &ast::Expr, ) -> bool { let db = context.db(); + let env = context.program_environment(); let Err(error) = dynamic_class.try_mro(db) else { return true; }; - let bases_display = dynamic_class .explicit_bases(db) .iter() - .map(|base| base.display(db)) + .map(|base| base.display(db, env)) .join(", "); report_mro_error_kind( context, @@ -247,7 +253,7 @@ pub(super) fn report_inconsistent_dynamic_generic_bases<'db>( bases: &ast::Expr, ) { let db = context.db(); - let explicit_bases = dynamic_class.explicit_bases(db); + let explicit_bases = dynamic_class.explicit_bases(context.db()); let base_nodes = bases .as_tuple_expr() .map(|tuple| tuple.elts.as_slice()) @@ -288,22 +294,23 @@ pub(super) fn report_mro_error_kind<'db>( let Some(bases) = bases_expr else { return; }; + let env = context.program_environment(); let bases_tuple_elts = bases.as_tuple_expr().map(|tuple| tuple.elts.as_slice()); for (idx, base_type) in invalid_bases { - let instance_of_type = KnownClass::Type.to_instance(db); + let instance_of_type = KnownClass::Type.to_instance(db, env); let specific_base = bases_tuple_elts.and_then(|elts| elts.get(*idx)); let diagnostic_range = specific_base .map(ast::Expr::range) .unwrap_or_else(|| bases.range()); - if base_type.is_assignable_to(db, instance_of_type) { + if base_type.is_assignable_to(db, env, instance_of_type) { if let Some(builder) = context.report_lint(&UNSUPPORTED_DYNAMIC_BASE, diagnostic_range) { let mut diagnostic = builder.into_diagnostic("Unsupported class base"); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Has type `{}`", - base_type.display(db) + base_type.display(db, env) )); diagnostic.info(format_args!( "ty cannot determine a MRO for class `{class_name}` due to this base", @@ -313,7 +320,7 @@ pub(super) fn report_mro_error_kind<'db>( } else if let Some(builder) = context.report_lint(&INVALID_BASE, diagnostic_range) { let mut diagnostic = builder.into_diagnostic(format_args!( "Invalid class base with type `{}`", - base_type.display(db) + base_type.display(db, env) )); if specific_base.is_none() { diagnostic @@ -329,12 +336,13 @@ pub(super) fn report_mro_error_kind<'db>( } DynamicMroErrorKind::DuplicateBases(duplicates) => { if let Some(builder) = context.report_lint(&DUPLICATE_BASE, call_expr) { + let env = context.program_environment(); builder.into_diagnostic(format_args!( "Duplicate base class{maybe_s} {dupes} in class `{class_name}`", maybe_s = if duplicates.len() == 1 { "" } else { "es" }, dupes = duplicates .iter() - .map(|base: &ClassBase<'_>| base.display(db)) + .map(|base: &ClassBase<'_>| base.display(db, env)) .join(", "), )); } diff --git a/crates/ty_python_semantic/src/types/infer/builder/enum_call.rs b/crates/ty_python_semantic/src/types/infer/builder/enum_call.rs index 04d8ea70fa..39cf9db71e 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/enum_call.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/enum_call.rs @@ -6,11 +6,12 @@ use rustc_hash::FxHashSet; use ty_python_core::definition::Definition; use crate::{ - Db, Program, + Db, ProgramEnvironment, types::{ ClassLiteral, KnownClass, Type, TypeContext, UnionType, class::{DynamicEnumAnchor, DynamicEnumLiteral, EnumSpec}, constraints::ConstraintSetBuilder, + context::InferContext, diagnostic::{ INVALID_ARGUMENT_TYPE, INVALID_BASE, MISSING_ARGUMENT, PARAMETER_ALREADY_ASSIGNED, TOO_MANY_POSITIONAL_ARGUMENTS, UNKNOWN_ARGUMENT, report_mismatched_type_name, @@ -137,16 +138,20 @@ fn enum_functional_call_keyword_is_valid(name: &str, python_version: PythonVersi /// /// This includes the string form, iterables of strings, iterables of /// iterable-like `(name, value)` pairs, and mappings from `str` to values. -fn enum_names_type(db: &dyn Db) -> Type<'_> { - let str_type = KnownClass::Str.to_instance(db); - let iterable_str = KnownClass::Iterable.to_specialized_instance(db, &[str_type]); - let iterable_object = KnownClass::Iterable.to_specialized_instance(db, &[Type::object()]); +fn enum_names_type<'db>(db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + let str_type = KnownClass::Str.to_instance(db, env); + let iterable_str = KnownClass::Iterable.to_specialized_instance(db, env, &[str_type]); + let iterable_object = KnownClass::Iterable.to_specialized_instance(db, env, &[Type::object()]); let iterable_iterable_object = - KnownClass::Iterable.to_specialized_instance(db, &[iterable_object]); - let mapping_str_object = KnownClass::Mapping - .to_specialized_instance(db, &[KnownClass::Str.to_instance(db), Type::object()]); + KnownClass::Iterable.to_specialized_instance(db, env, &[iterable_object]); + let mapping_str_object = KnownClass::Mapping.to_specialized_instance( + db, + env, + &[KnownClass::Str.to_instance(db, env), Type::object()], + ); UnionType::from_elements( db, + env, [ str_type, iterable_str, @@ -161,16 +166,18 @@ fn enum_names_type(db: &dyn Db) -> Type<'_> { /// `StrEnum` ignores `start` and uses the lowercased member name. Other enum kinds use the /// literal `start` value when available, and widen to `int` when `start` is a non-literal int. fn first_enum_auto_value<'db>( - db: &'db dyn Db, + context: &InferContext<'db, '_>, base_class: KnownClass, name: &str, start: EnumStart, ) -> Type<'db> { + let db = context.db(); + let env = context.program_environment(); match base_class { KnownClass::StrEnum => Type::string_literal(db, &*name.to_lowercase()), _ => match start { EnumStart::Literal(start) => Type::int_literal(start), - EnumStart::DynamicInt => KnownClass::Int.to_instance(db), + EnumStart::DynamicInt => KnownClass::Int.to_instance(db, env), }, } } @@ -183,16 +190,18 @@ fn first_enum_auto_value<'db>( /// - `Flag`/`IntFlag`: next highest power of two /// - Others: `last_value + 1` fn next_auto_value<'db>( - db: &'db dyn Db, + context: &InferContext<'db, '_>, base_class: KnownClass, name: &str, last_int_value: Option, ) -> Type<'db> { + let db = context.db(); + let env = context.program_environment(); match base_class { KnownClass::StrEnum => Type::string_literal(db, &*name.to_lowercase()), _ => { let Some(last) = last_int_value else { - return KnownClass::Int.to_instance(db); + return KnownClass::Int.to_instance(db, env); }; match base_class { KnownClass::Flag | KnownClass::IntFlag => { @@ -205,32 +214,32 @@ fn next_auto_value<'db>( .checked_shl(shift) .and_then(|value| i64::try_from(value).ok()) .map(Type::int_literal) - .unwrap_or_else(|| KnownClass::Int.to_instance(db)) + .unwrap_or_else(|| KnownClass::Int.to_instance(db, env)) } } _ => last .checked_add(1) .map(Type::int_literal) - .unwrap_or_else(|| KnownClass::Int.to_instance(db)), + .unwrap_or_else(|| KnownClass::Int.to_instance(db, env)), } } } } -fn enum_members_from_names( - db: &dyn Db, +fn enum_members_from_names<'db>( + context: &InferContext<'db, '_>, names: Vec, start: EnumStart, base_class: KnownClass, -) -> Vec<(Name, Type<'_>)> { +) -> Vec<(Name, Type<'db>)> { let mut members = Vec::with_capacity(names.len()); let mut last_int_value = None; for (index, name) in names.into_iter().enumerate() { let value = if index == 0 { - first_enum_auto_value(db, base_class, name.as_str(), start) + first_enum_auto_value(context, base_class, name.as_str(), start) } else { - next_auto_value(db, base_class, name.as_str(), last_int_value) + next_auto_value(context, base_class, name.as_str(), last_int_value) }; last_int_value = value.as_int_literal(); members.push((name, value)); @@ -247,57 +256,39 @@ fn enum_members_from_names( /// Returns `None` when the mixin is not a supported builtin or when the generated values are not /// compatible with the corresponding builtin conversion. fn apply_generated_type_mixin_member_values<'db>( - db: &'db dyn Db, + context: &InferContext<'db, '_>, mixin_type: Type<'_>, members: Vec<(Name, Type<'db>)>, ) -> Option)>> { + let db = context.db(); let Type::ClassLiteral(ClassLiteral::Static(class)) = mixin_type else { return None; }; - match class.known(db) { - Some(KnownClass::Str) => Some( - members - .into_iter() - .map(|(name, value)| { - let value = if let Some(literal) = value.as_int_literal() { - Type::string_literal(db, literal.to_compact_string()) - } else if value.is_assignable_to(db, KnownClass::Int.to_instance(db)) { - KnownClass::Str.to_instance(db) - } else { - return None; - }; - Some((name, value)) - }) - .collect::>>()?, - ), - Some(KnownClass::Bytes) => Some( - members - .into_iter() - .map(|(name, value)| { - let value = if value.is_assignable_to(db, KnownClass::Int.to_instance(db)) { - KnownClass::Bytes.to_instance(db) - } else { - return None; - }; - Some((name, value)) - }) - .collect::>>()?, - ), - Some(KnownClass::Float) => Some( - members - .into_iter() - .map(|(name, value)| { - if value.is_assignable_to(db, KnownClass::Int.to_instance(db)) { - Some((name, KnownClass::Float.to_instance(db))) - } else { - None - } - }) - .collect::>>()?, - ), - _ => None, - } + let mixin_class @ (KnownClass::Str | KnownClass::Bytes | KnownClass::Float) = + class.known(db)? + else { + return None; + }; + + let env = context.program_environment(); + members + .into_iter() + .map(|(name, value)| { + if !value.is_assignable_to(db, env, KnownClass::Int.to_instance(db, env)) { + return None; + } + + let value = if mixin_class == KnownClass::Str + && let Some(literal) = value.as_int_literal() + { + Type::string_literal(db, literal.to_compact_string()) + } else { + mixin_class.to_instance(db, env) + }; + Some((name, value)) + }) + .collect() } impl<'db> TypeInferenceBuilder<'db, '_> { @@ -315,16 +306,18 @@ impl<'db> TypeInferenceBuilder<'db, '_> { node_index: _, } = &call_expr.arguments; - let base_name = base_class.name(db); - let python_version = Program::get(db).python_version(db); - for kw in keywords { - if let Some(name) = &kw.arg - && !enum_functional_call_keyword_is_valid(name.as_str(), python_version) + let Some(name) = &kw.arg else { + continue; + }; + let env = self.program_environment(); + let python_version = env.python_version(db); + if !enum_functional_call_keyword_is_valid(name.as_str(), python_version) && let Some(builder) = self.context.report_lint(&UNKNOWN_ARGUMENT, kw) { builder.into_diagnostic(format_args!( "Argument `{name}` does not match any known parameter of function `{base_name}`", + base_name = base_class.name(env.python_version(db)), )); } } @@ -343,7 +336,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .report_lint(&PARAMETER_ALREADY_ASSIGNED, keyword) { builder.into_diagnostic(format_args!( - "Multiple values provided for parameter `value` of `{base_name}()`" + "Multiple values provided for parameter `value` of `{base_name}()`", + base_name = base_class.name(self.program_environment().python_version(db)), )); } if args.len() >= 2 @@ -353,7 +347,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .report_lint(&PARAMETER_ALREADY_ASSIGNED, keyword) { builder.into_diagnostic(format_args!( - "Multiple values provided for parameter `names` of `{base_name}()`" + "Multiple values provided for parameter `names` of `{base_name}()`", + base_name = base_class.name(self.program_environment().python_version(db)), )); } @@ -364,6 +359,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }; let name_arg = name_arg?; + let env = self.program_environment(); let Some(names_arg) = names_arg else { for arg in args { @@ -381,13 +377,15 @@ impl<'db> TypeInferenceBuilder<'db, '_> { self.infer_enum_mixin_argument(&keyword.value, base_class); } + let python_version = self.program_environment().python_version(db); if let Some(builder) = self.context.report_lint(&MISSING_ARGUMENT, call_expr) { builder.into_diagnostic(format_args!( - "Missing required argument `names` to `{base_name}()`" + "Missing required argument `names` to `{base_name}()`", + base_name = base_class.name(python_version), )); } - return Some(base_class.to_instance(db)); + return Some(base_class.to_instance(db, env)); }; for arg in args { @@ -415,6 +413,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { builder.into_diagnostic(format_args!( "Too many positional arguments to function `{base_name}`: expected 2, got {}", args.len(), + base_name = base_class.name(self.program_environment().python_version(db)), )); } @@ -423,7 +422,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .as_string_literal() .map(|name_literal| name_literal.value(db)); - if (name.is_some() || name_ty.is_assignable_to(db, KnownClass::Str.to_instance(db))) + if (name.is_some() + || name_ty.is_assignable_to(db, env, KnownClass::Str.to_instance(db, env))) && let Some(definition) = definition && let Some(assigned_name) = definition.name(db) && Some(assigned_name.as_str()) != name @@ -431,7 +431,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { report_mismatched_type_name( &self.context, name_arg, - base_name, + base_class.name(self.program_environment().python_version(db)), &assigned_name, name, name_ty, @@ -449,7 +449,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // Non-literal names use the ordinary `type[EnumSubclass]` overload result // instead of synthesizing a `DynamicEnumLiteral`. let Some(name) = self.infer_enum_name_argument(name_arg, base_class) else { - return SubclassOfType::try_from_type(db, base_class.to_class_literal(db)); + return SubclassOfType::try_from_type(db, env, base_class.to_class_literal(db, env)); }; let anchor = self.create_dynamic_enum_anchor(call_expr, definition, spec); @@ -473,19 +473,20 @@ impl<'db> TypeInferenceBuilder<'db, '_> { base_class: KnownClass, ) -> Option<&'db str> { let db = self.db(); - let base_name = base_class.name(db); let name_type = self.expression_type(name_arg); let Some(name_literal) = name_type.as_string_literal() else { - if !name_type.is_assignable_to(db, KnownClass::Str.to_instance(db)) + let env = self.program_environment(); + if !name_type.is_assignable_to(db, env, KnownClass::Str.to_instance(db, env)) && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, name_arg) { let mut diagnostic = builder.into_diagnostic(format_args!( - "Invalid argument to parameter `value` of `{base_name}()`" + "Invalid argument to parameter `value` of `{base_name}()`", + base_name = base_class.name(env.python_version(db)) )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Expected `str`, found `{}`", - name_type.display(db) + name_type.display(db, env) )); } return None; @@ -501,14 +502,15 @@ impl<'db> TypeInferenceBuilder<'db, '_> { return EnumStart::Literal(literal); } - if ty.is_assignable_to(db, KnownClass::Int.to_instance(db)) { + let env = self.program_environment(); + if ty.is_assignable_to(db, env, KnownClass::Int.to_instance(db, env)) { return EnumStart::DynamicInt; } if let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, value) { builder.into_diagnostic(format_args!( "Expected `int` for `start` argument, got `{}`", - ty.display(db), + ty.display(db, env), )); } @@ -522,13 +524,14 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ) -> (Option>, bool) { let db = self.db(); let ty = self.expression_type(value); + let env = self.program_environment(); if let Some(class_lit) = ty.as_class_literal() { if class_lit.is_typed_dict(db) && let Some(builder) = self.context.report_lint(&INVALID_BASE, value) { builder.into_diagnostic(format_args!( "TypedDict class `{}` cannot be used as an enum mixin", - ty.display(db), + ty.display(db, env), )); return (None, false); } @@ -536,17 +539,17 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let Some(mixin_class) = ty.to_class_type(db) else { return (Some(ty), true); }; - let Some(enum_base) = base_class.to_class_literal(db).to_class_type(db) else { + let Some(enum_base) = base_class.to_class_literal(db, env).to_class_type(db) else { return (Some(ty), true); }; let constraints = ConstraintSetBuilder::new(); - if !mixin_class.could_coexist_in_mro_with(db, enum_base, &constraints) + if !mixin_class.could_coexist_in_mro_with(db, env, enum_base, &constraints) && let Some(builder) = self.context.report_lint(&INVALID_BASE, value) { builder.into_diagnostic(format_args!( "Class `{}` cannot be used as an enum mixin with `{}`", mixin_class.name(db), - base_class.name(db), + base_class.name(self.program_environment().python_version(db)), )); return (None, false); } @@ -560,7 +563,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, value) { builder.into_diagnostic(format_args!( "Expected a class for `type` argument, got `{}`", - ty.display(db), + ty.display(db, env), )); } @@ -599,7 +602,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { TypeMixinMemberBehavior::Precise => (known_members.members, true), TypeMixinMemberBehavior::ConvertedValues => { match apply_generated_type_mixin_member_values( - db, + &self.context, mixin_type, known_members.members, ) { @@ -677,7 +680,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .filter(|s| !s.is_empty()) .map(Name::new) .collect(); - let members = enum_members_from_names(db, names, start, base_class); + let members = enum_members_from_names(&self.context, names, start, base_class); return EnumMembersArgParseResult::Known(KnownEnumMembers { members, value_form: EnumMemberValueForm::Generated, @@ -697,7 +700,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { return self.parse_enum_members_from_dict(dict, base_class); } - if ty.is_dynamic() || ty.is_assignable_to(db, enum_names_type(db)) { + let env = self.program_environment(); + if ty.is_dynamic() || ty.is_assignable_to(db, env, enum_names_type(db, env)) { EnumMembersArgParseResult::Unknown } else { EnumMembersArgParseResult::Invalid @@ -759,7 +763,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if matches!(form, Some(SequenceEnumMemberForm::Names)) { return EnumMembersArgParseResult::Known(KnownEnumMembers { - members: enum_members_from_names(db, names, start, base_class), + members: enum_members_from_names(&self.context, names, start, base_class), value_form: EnumMemberValueForm::Generated, }); } @@ -776,7 +780,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut last_int_value = Some(0); for (name, value) in explicit_members { let value = if value.is_instance_of(db, KnownClass::Auto) { - next_auto_value(db, base_class, name.as_str(), last_int_value) + next_auto_value(&self.context, base_class, name.as_str(), last_int_value) } else { value }; @@ -803,6 +807,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut members = Vec::with_capacity(dict.items.len()); let mut last_int_value = Some(0); let mut has_opaque_keys = false; + let env = self.program_environment(); for item in &dict.items { let Some(key) = &item.key else { return EnumMembersArgParseResult::Invalid; @@ -810,7 +815,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let key_ty = self.expression_type(key); let Some(string_lit) = key_ty.as_string_literal() else { if key_ty.is_dynamic() - || key_ty.is_assignable_to(db, KnownClass::Str.to_instance(db)) + || key_ty.is_assignable_to(db, env, KnownClass::Str.to_instance(db, env)) { has_opaque_keys = true; continue; @@ -820,7 +825,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let name = Name::new(string_lit.value(db)); let raw_value = self.expression_type(&item.value); let value = if raw_value.is_instance_of(db, KnownClass::Auto) { - next_auto_value(db, base_class, name.as_str(), last_int_value) + next_auto_value(&self.context, base_class, name.as_str(), last_int_value) } else { raw_value }; @@ -859,6 +864,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { /// This is used when the name position is not a known string literal, but /// is still compatible with `str`. fn is_potential_explicit_enum_member(&mut self, elt: &ast::Expr) -> bool { + let db = self.db(); let pair = match elt { ast::Expr::Tuple(tup) => &tup.elts, ast::Expr::List(list) => &list.elts, @@ -867,9 +873,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let [name_expr, _value_expr] = &**pair else { return false; }; - let db = self.db(); let name_ty = self.expression_type(name_expr); - name_ty.is_dynamic() || name_ty.is_assignable_to(db, KnownClass::Str.to_instance(db)) + let env = self.program_environment(); + name_ty.is_dynamic() + || name_ty.is_assignable_to(db, env, KnownClass::Str.to_instance(db, env)) } /// Classifies one element from a sequence-form `names` argument. @@ -885,7 +892,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if let Some((name, value)) = self.parse_explicit_enum_member(elt) { return SequenceEnumMember::PairKnown(name, value); } - if ty.is_dynamic() || ty.is_assignable_to(db, KnownClass::Str.to_instance(db)) { + let env = self.program_environment(); + if ty.is_dynamic() || ty.is_assignable_to(db, env, KnownClass::Str.to_instance(db, env)) { return SequenceEnumMember::NameOpaque; } if self.is_potential_explicit_enum_member(elt) { @@ -900,16 +908,17 @@ impl<'db> TypeInferenceBuilder<'db, '_> { base_class: KnownClass, ) { let db = self.db(); - let base_name = base_class.name(db); + let base_name = base_class.name(self.program_environment().python_version(db)); let names_ty = self.expression_type(names_arg); if let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, names_arg) { + let env = self.program_environment(); let mut diagnostic = builder.into_diagnostic(format_args!( "Invalid argument to parameter `names` of `{base_name}()`" )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Expected `{}`, found `{}`", - enum_names_type(db).display(db), - names_ty.display(db), + enum_names_type(db, env).display(db, env), + names_ty.display(db, env), )); } } diff --git a/crates/ty_python_semantic/src/types/infer/builder/final_attribute.rs b/crates/ty_python_semantic/src/types/infer/builder/final_attribute.rs index fb95d0d161..2e1307440b 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/final_attribute.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/final_attribute.rs @@ -26,7 +26,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ) { let db = self.db(); let file = declaration.file(db); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, declaration.python_file(db)).load(db); let range = match declaration.kind(db) { DefinitionKind::AnnotatedAssignment(assignment) => { assignment.annotation(&module).range() @@ -50,7 +50,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { attribute: &str, ) -> Option> { let db = self.db(); - let class_ty = object_ty.nominal_class(db)?; + let env = self.program_environment(); + let class_ty = object_ty.nominal_class(db, env)?; for base in class_ty.iter_mro(db) { let Some(class) = base.into_class() else { @@ -62,16 +63,18 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let class_body_scope = class_literal.body_scope(db); let class_scope_id = class_body_scope.file_scope_id(db); - let class_index = semantic_index(db, class_body_scope.file(db)); + let class_index = semantic_index(db, class_body_scope.program_file(db)); let place_table = class_index.place_table(class_scope_id); let Some(symbol_id) = place_table.symbol_id(attribute) else { continue; }; let use_def = class_index.use_def_map(class_scope_id); - - let place_and_quals_result = - place_from_declarations(db, use_def.end_of_scope_symbol_declarations(symbol_id)); + let place_and_quals_result = place_from_declarations( + db, + env, + use_def.end_of_scope_symbol_declarations(symbol_id), + ); let Some(declaration) = place_and_quals_result.first_declaration else { continue; @@ -98,7 +101,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { /// expression refers to the first parameter of the enclosing method and has not been shadowed /// in intermediate scopes. We additionally check that the nearest enclosing function has an /// implicit receiver, since static methods also have a first parameter. - pub(super) fn is_instance_attribute_assignment(&self, target: &ast::ExprAttribute) -> bool { + fn is_instance_attribute_assignment(&self, target: &ast::ExprAttribute) -> bool { let Some(place_expr) = PlaceExpr::try_from_expr(target) else { return false; }; @@ -195,11 +198,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> { attribute: &str, qualifiers: TypeQualifiers, ) -> bool { + let env = self.program_environment(); + let db = self.db(); if !qualifiers.contains(TypeQualifiers::FINAL) { return false; } - - let db = self.db(); let final_declaration = self.precise_final_attribute_declaration(object_ty, attribute); // TODO: Use the full assignment statement range for these diagnostics instead of @@ -211,10 +214,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let report_not_in_init = || { let is_dataclass_like = object_ty - .nominal_class(db) + .nominal_class(db, env) .or_else(|| object_ty.to_class_type(db)) .and_then(|cls| cls.static_class_literal(db)) - .is_some_and(|(class_literal, _)| class_literal.is_dataclass_like(db)); + .is_some_and(|(class_literal, _)| class_literal.is_dataclass_like(self.db())); let Some(builder) = self .context .report_lint(&INVALID_ASSIGNMENT, target.range()) @@ -223,9 +226,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }; let mut diagnostic = builder.into_diagnostic(format_args!( "Cannot assign to final attribute `{attribute}` on type `{}`", - object_ty.display(db) + object_ty.display(db, env) )); - diagnostic.set_primary_message(if is_dataclass_like { + diagnostic.set_primary_annotation_message(if is_dataclass_like { "`Final` attributes can only be assigned in the class body, `__init__`, or `__post_init__` on dataclass-like classes" } else { "`Final` attributes can only be assigned in the class body or `__init__`" @@ -250,10 +253,14 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // that happens to have the right type. let is_self_parameter = self.is_instance_attribute_assignment(target); - let class_instance_ty = Type::instance(db, class_ty).top_materialization(db); - let object_instance_ty = object_ty.bind_self_typevars(db, class_instance_ty); - let is_current_class_instance = - is_self_parameter && object_instance_ty.is_subtype_of(db, class_instance_ty); + // Final ownership is nominal: checking structural protocol requirements can + // incorrectly reject the declaring class's own receiver. + let is_current_class_instance = is_self_parameter + && object_ty + .nominal_class(db, env) + .is_some_and(|object_class| { + object_class.is_subtype_of_class_literal(db, class_ty.class_literal(db)) + }); if !is_current_class_instance { report_not_in_init(); return true; @@ -262,7 +269,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if let Some((class_literal, _)) = class_ty.static_class_literal(db) { let class_body_scope = class_literal.body_scope(db); let class_scope_id = class_body_scope.file_scope_id(db); - let class_index = semantic_index(db, class_body_scope.file(db)); + let class_index = semantic_index(db, class_body_scope.program_file(db)); let pt = class_index.place_table(class_scope_id); if let Some(symbol) = pt.symbol_by_name(attribute) @@ -274,7 +281,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { { let mut diagnostic = diag_builder.into_diagnostic("Invalid assignment to final attribute"); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "`{attribute}` already has a value in the class body" )); if let Some(final_declaration) = final_declaration { @@ -297,12 +304,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> { qualifiers: TypeQualifiers, emit_diagnostics: bool, ) -> bool { + let db = self.db(); if !qualifiers.contains(TypeQualifiers::FINAL) { return false; } if emit_diagnostics { - let db = self.db(); let final_declaration = self.precise_final_attribute_declaration(object_ty, attribute); if let Some(builder) = self @@ -311,9 +318,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> { { let mut diagnostic = builder.into_diagnostic(format_args!( "Cannot delete final attribute `{attribute}` on type `{}`", - object_ty.display(db) + object_ty.display(db, self.program_environment()) )); - diagnostic.set_primary_message("`Final` attributes cannot be deleted"); + diagnostic.set_primary_annotation_message("`Final` attributes cannot be deleted"); if let Some(final_declaration) = final_declaration { self.annotate_final_declaration(&mut diagnostic, final_declaration); } @@ -329,7 +336,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { object_ty: Type<'db>, attribute: &str, ) { - let Some(members) = assignment_attribute_members(self.db(), object_ty, attribute) else { + let db = self.db(); + let Some(members) = + assignment_attribute_members(db, self.program_environment(), object_ty, attribute) + else { return; }; @@ -352,7 +362,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { attribute: &str, emit_diagnostics: bool, ) -> bool { - let Some(members) = assignment_attribute_members(self.db(), object_ty, attribute) else { + let db = self.db(); + let Some(members) = + assignment_attribute_members(db, self.program_environment(), object_ty, attribute) + else { return false; }; diff --git a/crates/ty_python_semantic/src/types/infer/builder/fluid.rs b/crates/ty_python_semantic/src/types/infer/builder/fluid.rs index 117161962a..e6afc2eb58 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/fluid.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/fluid.rs @@ -45,6 +45,7 @@ use ty_python_core::fluid::FluidUseRole; use super::TypeInferenceBuilder; use crate::Db; +use crate::types::ProgramEnvironment; use crate::types::any_over_type; use crate::types::binding_type; use crate::types::constraints::ConstraintSetBuilder; @@ -146,6 +147,7 @@ impl<'db> FluidTimeline<'db> { pub(crate) fn cycle_normalized( mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, previous: Option<&FluidTimeline<'db>>, cycle: &salsa::Cycle, ) -> Self { @@ -173,14 +175,14 @@ impl<'db> FluidTimeline<'db> { // every iteration is another, and one a type built entirely out of `Unknown` // never acquires a marker for. skipping either stores the value raw, which // un-widens whatever the last iteration had settled - if !any_over_type(db, *ty, false, |inner| inner.is_divergent()) - && !ty.is_deeply_nested(db) + if !any_over_type(db, env, *ty, false, |inner| inner.is_divergent()) + && !ty.is_deeply_nested(db, env) { return; } *ty = match previous_ty { - Some(previous_ty) => ty.cycle_normalized(db, previous_ty, cycle), - None => ty.recursive_type_normalized(db, cycle), + Some(previous_ty) => ty.cycle_normalized(db, env, previous_ty, cycle), + None => ty.recursive_type_normalized(db, env, cycle), }; }; @@ -240,6 +242,7 @@ impl<'db> FluidFold<'db, '_> { fn record( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, anchor: ExpressionNodeKey, kind: FluidEventKind, constraint: Option>, @@ -255,7 +258,10 @@ impl<'db> FluidFold<'db, '_> { let (solution, solution_promoted) = if self.poisoned { (None, None) } else { - (Some(self.build(db, false)), Some(self.build(db, true))) + ( + Some(self.build(db, env, false)), + Some(self.build(db, env, true)), + ) }; self.events.push(FluidEvent { anchor, @@ -268,7 +274,12 @@ impl<'db> FluidFold<'db, '_> { /// build the cumulative solution, matching the promotion policy of /// [`TypeInferenceBuilder::solve_fluid_specialization`] - fn build(&mut self, db: &'db dyn Db, promote: bool) -> Type<'db> { + fn build( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + promote: bool, + ) -> Type<'db> { let file = self.file; let specialization = self .builder @@ -277,8 +288,8 @@ impl<'db> FluidFold<'db, '_> { Some(if promote && typevar.widens_literal_solutions(db) { // see the note in `solve_fluid_specialization` lower - .promote_in(db, file) - .promote_singletons_recursively(db) + .promote_in(db, env, file) + .promote_singletons_recursively(db, env) } else { lower }) @@ -360,6 +371,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { callable_type: Type<'db>, tcx: TypeContext<'db>, ) -> Type<'db> { + let env = self.program_environment(); let fluid_def = if tcx.annotation().is_none() && callable_type.is_class_literal() { self.fluid_candidate_definition(ast::ExprRef::Call(call_expr)) } else { @@ -373,11 +385,14 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let ty = self.infer_call_expression_impl(call_expr, callable_type, tcx); if let Some(fluid_def) = fluid_def - && let Some((class_literal, _)) = ty.class_specialization(self.db()) + && let Some((class_literal, _)) = ty.class_specialization(self.db(), env) && let Some(generic_context) = class_literal.generic_context(self.db()) { - let identity_instance = - Type::instance(self.db(), class_literal.identity_specialization(self.db())); + let identity_instance = Type::instance( + self.db(), + env, + class_literal.identity_specialization(self.db()), + ); return self.fluid_eventual_type(fluid_def, identity_instance, generic_context, ty); } @@ -394,6 +409,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { arguments: &ast::Arguments, bindings: &mut crate::types::Bindings<'db>, ) { + let env = self.program_environment(); if !self.fluid_specializations_enabled() { return; } @@ -441,7 +457,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { continue; } - let Some(specialization) = overload.specialization(db) else { + let Some(specialization) = overload.specialization(db, env) else { continue; }; let Some(matched) = overload @@ -465,10 +481,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // if a typevar solved from this parameter occurs in the return // type. let shared_variance = std::cell::Cell::new(TypeVarVariance::Bivariant); - let shares_typevar = any_over_type(db, parameter_ty, false, |ty| { + let shares_typevar = any_over_type(db, env, parameter_ty, false, |ty| { ty.as_typevar().is_some_and(|typevar| { let shared = std::cell::Cell::new(false); - return_ty.visit_specialization(db, |return_part, variance| { + return_ty.visit_specialization(db, env, |return_part, variance| { if return_part.as_typevar().is_some_and(|return_typevar| { return_typevar.identity(db) == typevar.identity(db) }) { @@ -499,7 +515,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let constraints = ConstraintSetBuilder::new(); let inferable = signature_context.inferable_typevars(db); let mut builder = - SpecializationBuilder::new(db, &constraints, inferable); + SpecializationBuilder::new(db, env, &constraints, inferable); if builder.infer(parameter_ty, eventual).is_ok() { let eventual_specialization = builder.build_with(signature_context, |_, bounds| { @@ -537,6 +553,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { generic_context: GenericContext<'db>, upto: ExpressionNodeKey, ) -> FluidConstraints<'db> { + let env = self.program_environment(); let db = self.db(); let uses = self.index.fluid_uses(candidate_def); @@ -614,7 +631,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if use_.role == FluidUseRole::TypeContextual { if let Some(adoption) = statement_use_types.fluid_adoption(use_.use_expression) - && !adoption.has_unspecialized_type_var(db) + && !adoption.has_unspecialized_type_var(db, env) && self.fluid_constraint_binds_typevars( identity_instance, generic_context, @@ -647,7 +664,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .iter() .copied() .filter(|constraint| { - !constraint.has_unspecialized_type_var(db) + !constraint.has_unspecialized_type_var(db, env) && self.fluid_constraint_binds_typevars( identity_instance, generic_context, @@ -698,6 +715,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { generic_context: GenericContext<'db>, creation: Type<'db>, ) -> FluidTimeline<'db> { + let env = self.program_environment(); let db = self.db(); let uses = self.index.fluid_uses(candidate_def); @@ -707,7 +725,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let constraint_sets = ConstraintSetBuilder::new(); let inferable = generic_context.inferable_typevars(db); let mut fold = FluidFold { - builder: SpecializationBuilder::new(db, &constraint_sets, inferable), + builder: SpecializationBuilder::new(db, env, &constraint_sets, inferable), identity_instance, generic_context, file: self.file(), @@ -729,7 +747,13 @@ impl<'db> TypeInferenceBuilder<'db, '_> { FluidUseRole::Read => {} FluidUseRole::Escape => { - fold.record(db, use_.use_expression, FluidEventKind::EscapeLock, None); + fold.record( + db, + env, + use_.use_expression, + FluidEventKind::EscapeLock, + None, + ); break; } @@ -739,7 +763,13 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let Some(statement) = use_.statement else { // Constraint-bearing roles always carry a statement; be // conservative if one is somehow missing. - fold.record(db, use_.use_expression, FluidEventKind::EscapeLock, None); + fold.record( + db, + env, + use_.use_expression, + FluidEventKind::EscapeLock, + None, + ); break; }; @@ -754,6 +784,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { generic_context.repeat_specialization(db, Type::Divergent(divergent)); fold.record( db, + env, use_.use_expression, FluidEventKind::Constrain, Some( @@ -771,7 +802,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if use_.role == FluidUseRole::TypeContextual { if let Some(adoption) = statement_use_types.fluid_adoption(use_.use_expression) - && !adoption.has_unspecialized_type_var(db) + && !adoption.has_unspecialized_type_var(db, env) && self.fluid_constraint_binds_typevars( identity_instance, generic_context, @@ -780,6 +811,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { { fold.record( db, + env, use_.use_expression, FluidEventKind::AdoptLock, Some(adoption), @@ -803,7 +835,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // literals. if seen_statements.insert(statement) { for constraint in use_constraints.iter().copied() { - if constraint.has_unspecialized_type_var(db) + if constraint.has_unspecialized_type_var(db, env) || !self.fluid_constraint_binds_typevars( identity_instance, generic_context, @@ -824,6 +856,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .unwrap_or(constraint); fold.record( db, + env, use_.use_expression, FluidEventKind::Constrain, Some(resolved), @@ -953,10 +986,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> { generic_context: GenericContext<'db>, constraint: Type<'db>, ) -> bool { + let env = self.program_environment(); let db = self.db(); let constraints = ConstraintSetBuilder::new(); let inferable = generic_context.inferable_typevars(db); - let mut builder = SpecializationBuilder::new(db, &constraints, inferable); + let mut builder = SpecializationBuilder::new(db, env, &constraints, inferable); if builder.infer(identity_instance, constraint).is_err() { // An incompatible context still hands the value to another observer. @@ -983,10 +1017,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> { constraint_instances: impl IntoIterator>, promote: bool, ) -> Option> { + let env = self.program_environment(); let db = self.db(); let constraints = ConstraintSetBuilder::new(); let inferable = generic_context.inferable_typevars(db); - let mut builder = SpecializationBuilder::new(db, &constraints, inferable); + let mut builder = SpecializationBuilder::new(db, env, &constraints, inferable); for constraint in constraint_instances { builder.infer(identity_instance, constraint).ok()?; @@ -1004,8 +1039,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // element type, which is what a layout is chosen from, so a module that // asked for strict numerics has to get one here too. lower - .promote_in(db, file) - .promote_singletons_recursively(db) + .promote_in(db, env, file) + .promote_singletons_recursively(db, env) } else { lower }) @@ -1025,6 +1060,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { generic_context: GenericContext<'db>, creation: Type<'db>, ) -> Type<'db> { + let env = self.program_environment(); self.fluid_creation = Some(creation); let timeline = @@ -1047,9 +1083,13 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // re-solve can lose structure that the constructor inference produced (e.g. // the `Top[...]` materialization of a ParamSpec specialization). if eventual.is_none() - && !any_over_type(self.db(), creation, false, |ty| { - ty.as_literal_value().is_some() || ty.is_singleton(self.db()) - }) + && !any_over_type( + self.db(), + self.program_environment(), + creation, + false, + |ty| ty.as_literal_value().is_some() || ty.is_singleton(self.db(), env), + ) { // A fluid empty collection is `Never`-specialized, which is the precise type // for flow-sensitive uses (recorded above as `fluid_creation`). Its public @@ -1081,8 +1121,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> { generic_context: GenericContext<'db>, creation: Type<'db>, ) -> Type<'db> { + let env = self.program_environment(); let db = self.db(); - let Some((_, specialization)) = creation.class_specialization(db) else { + let Some((_, specialization)) = creation.class_specialization(db, env) else { return creation; }; if !specialization.types(db).iter().all(Type::is_never) { @@ -1119,6 +1160,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { fallback: Type<'db>, tcx: TypeContext<'db>, ) -> Type<'db> { + let env = self.program_environment(); let db = self.db(); if !self.is_fluid_candidate(candidate_def) { @@ -1144,7 +1186,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { return fallback; }; - let Some((class_literal, _)) = creation.class_specialization(db) else { + let Some((class_literal, _)) = creation.class_specialization(db, env) else { // The creation type contains a cycle-recovery placeholder; fall back // until the fixpoint converges. return fallback; @@ -1152,7 +1194,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let Some(generic_context) = class_literal.generic_context(db) else { return fallback; }; - let identity_instance = Type::instance(db, class_literal.identity_specialization(db)); + let identity_instance = Type::instance(db, env, class_literal.identity_specialization(db)); let use_key = ExpressionNodeKey::from(use_expr); let view = @@ -1191,8 +1233,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // places no requirement on the specialization and observes the narrow // type as-is; a structured one (`def f[T](t: list[T])`) solves its // typevars against the promoted view. - if annotation.has_unspecialized_type_var(db) - && annotation.class_specialization(db).is_some() + if annotation.has_unspecialized_type_var(db, env) + && annotation.class_specialization(db, env).is_some() { if let (Some(timeline), Some(index)) = (timeline, snapshot) { return timeline.solution(index, true).unwrap_or(fallback); diff --git a/crates/ty_python_semantic/src/types/infer/builder/function.rs b/crates/ty_python_semantic/src/types/infer/builder/function.rs index efda199556..1ea715a73f 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/function.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/function.rs @@ -1,19 +1,21 @@ use crate::types::any_over_type; use crate::{ - Db, + Db, ProgramEnvironment, types::{ KnownClass, KnownInstanceType, ParamSpecAttrKind, SubclassOfInner, SubclassOfType, Type, TypeContext, TypeVarKind, UnionType, class::ClassLiteral, + constraints::ConstraintSetBuilder, dedicated::pytest, diagnostic::{ - FINAL_ON_NON_METHOD, INVALID_FIXTURE_TYPE, INVALID_PARAMETER_DEFAULT, - INVALID_PARAMETRIZE, INVALID_PARAMSPEC, INVALID_TYPE_FORM, REDUNDANT_RETURN_ANNOTATION, - REIFIED_CLASSMETHOD, TRAILING_LAMBDA_PARAMETERS, TRAILING_LAMBDA_RETURN_TYPE, - UNKNOWN_FIXTURE, USELESS_OVERLOAD_BODY, add_type_expression_reference_link, + ABSTRACT_AND_FINAL_METHOD, FINAL_ON_NON_METHOD, INVALID_FIXTURE_TYPE, + INVALID_PARAMETER_DEFAULT, INVALID_PARAMETRIZE, INVALID_PARAMSPEC, INVALID_TYPE_FORM, + REDUNDANT_RETURN_ANNOTATION, REIFIED_CLASSMETHOD, TRAILING_LAMBDA_PARAMETERS, + TRAILING_LAMBDA_RETURN_TYPE, UNKNOWN_FIXTURE, UNSOUND_RETURN_STATEMENT, + USELESS_OVERLOAD_BODY, add_type_expression_reference_link, is_invalid_typed_dict_literal, report_bool_as_int, report_implicit_return_type, report_invalid_generator_function_return_type, report_invalid_return_type, - report_shadowed_type_variable, + report_shadowed_type_variable, report_unsound_return_statement, }, extensions, function::{ @@ -29,18 +31,20 @@ use crate::{ DeclaredAndInferredType, DeferredExpressionState, TypeAndRange, TypeParamReification, validate_paramspec_components, }, - function_known_decorators, infer_statement_types, nearest_enclosing_function, - original_class_type, + function_known_decorator_flags, function_known_decorators, infer_statement_types, + nearest_enclosing_function, original_class_type, }, infer_definition_types, infer_expression_types, infer_scope_types, inferred_signature::{can_implicitly_return_none, return_type_from_body}, lifetimes::InheritedBorrow, + relation::TypeRelation, signatures::ReturnCallableTypeVarScope, trailing_lambda::{ UnbindableParameters, trailing_lambda_it_borrow, trailing_lambda_it_type, }, tuple::{TupleSpecBuilder, TupleType}, typed_dict::extract_unpacked_typed_dict_keys_from_kwargs_annotation, + typevar::TypeVarSet, }, }; use ty_python_core::{ @@ -69,46 +73,102 @@ fn parameters_have_annotations(parameters: &ast::Parameters) -> bool { .is_some_and(|param| param.annotation.is_some()) } +/// Whether a non-static method receives an instance or the class itself. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum MethodReceiverKind { + Instance, + Class, +} + +impl MethodReceiverKind { + /// Classifies methods by their decorators and implicit class-receiver rules. + /// + /// Free functions and ordinary static methods have no receiver; `__new__` receives the class. + /// + /// ```python + /// class Example: + /// def instance(self): ... + /// @classmethod + /// def class_method(cls): ... + /// @staticmethod + /// def static_method(): ... + /// ``` + fn from_function<'db>( + db: &'db dyn Db, + definition: Definition<'db>, + function: &ast::StmtFunctionDef, + ) -> Option { + if !definition.scope(db).scope(db).kind().is_class() { + return None; + } + + let decorators = function_known_decorator_flags(db, definition); + if decorators.contains(FunctionDecorators::STATICMETHOD) && function.name.id != "__new__" { + return None; + } + + if decorators.contains(FunctionDecorators::CLASSMETHOD) + || is_implicit_classmethod(&function.name) + || function.name.id == "__new__" + { + Some(Self::Class) + } else { + Some(Self::Instance) + } + } + + /// Accepts only `Self` for an instance receiver and `type[Self]` for a class receiver. + fn accepts_annotation(self, db: &dyn Db, annotation: Type<'_>) -> bool { + match (self, annotation) { + (Self::Instance, Type::TypeVar(typevar)) => typevar.typevar(db).is_self(db), + (Self::Class, Type::SubclassOf(subclass)) => { + matches!( + subclass.subclass_of(), + SubclassOfInner::TypeVar(typevar) if typevar.typevar(db).is_self(db) + ) + } + _ => false, + } + } +} + /// Return type policy for checking explicit `return` statements in a function body. #[derive(Debug, Copy, Clone)] struct ExpectedReturnType<'db> { /// The externally-visible return type. public: Type<'db>, - /// The lexical return type, if it differs for a generic PEP 695 function. - lexical: Option>, + /// The return type as seen from inside the function body. + lexical: Type<'db>, } impl<'db> ExpectedReturnType<'db> { - /// Creates the expected return type policy for `function_node`. - fn from_function( - db: &'db dyn Db, - function: FunctionType<'db>, - function_node: &ast::StmtFunctionDef, - ) -> Self { + /// Creates the expected return type policy for `function`. + fn from_function(db: &'db dyn Db, function: FunctionType<'db>) -> Self { /// Normalizes special return annotations to the type actually returned by expressions. - fn normalize<'db>(db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { - match ty { - Type::TypeIs(_) | Type::TypeGuard(_) => KnownClass::Bool.to_instance(db), - ty => ty, + fn normalize<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + ) -> Type<'db> { + match ty.resolve_type_alias(db) { + Type::TypeIs(_) | Type::TypeGuard(_) => KnownClass::Bool.to_instance(db, env), + _ => ty, } } + let env = ProgramEnvironment::from_file(function.program_file(db)); let public = normalize( db, + &env, same_module_uncached_raw_signature(db, function, ReturnCallableTypeVarScope::Public) .return_ty, ); - let lexical = function_node.type_params.is_some().then(|| { - normalize( - db, - same_module_uncached_raw_signature( - db, - function, - ReturnCallableTypeVarScope::Lexical, - ) + let lexical = normalize( + db, + &env, + same_module_uncached_raw_signature(db, function, ReturnCallableTypeVarScope::Lexical) .return_ty, - ) - }); + ); Self { public, lexical } } @@ -120,16 +180,27 @@ impl<'db> ExpectedReturnType<'db> { /// Returns `true` if `ty` is accepted by either the public return type or the lexical return /// type. - fn accepts(self, db: &'db dyn Db, ty: Type<'db>) -> bool { - ty.is_assignable_to(db, self.public) - || self - .lexical - .is_some_and(|lexical| ty.is_assignable_to(db, lexical)) + fn accepts( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + relation: TypeRelation, + ) -> bool { + let builder = ConstraintSetBuilder::new(); + + let check = + |target| ty.has_relation_to(db, env, target, &builder, TypeVarSet::None, relation); + + check(self.public) + .or(db, &builder, || check(self.lexical)) + .is_always_satisfied(db, env) } } impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { pub(super) fn infer_function_body(&mut self, function: &ast::StmtFunctionDef) { + let env = self.program_environment(); let db = self.db(); // Parameters are odd: they are Definitions in the function body scope, but have no @@ -157,7 +228,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - validate_paramspec_components(&self.context, &function.parameters, |expr| { + validate_paramspec_components(&self.context, self.index, &function.parameters, |expr| { self.file_expression_type(expr) }); self.validate_unpacked_typed_dict_kwargs(&function.parameters); @@ -255,7 +326,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if overloads.is_empty() || implementation.is_none() { return None; } - if function_body_kind(db, function, |expr| self.expression_type(expr)) + if function_body_kind(db, env, function, |expr| self.expression_type(expr)) == FunctionBodyKind::Stub { return None; @@ -267,7 +338,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(returns_range) = inherited_return_range { let has_empty_body = self.return_types_and_ranges.is_empty() - && function_body_kind(db, function, |expr| self.expression_type(expr)) + && function_body_kind(db, env, function, |expr| self.expression_type(expr)) == FunctionBodyKind::Stub; let mut enclosing_class_context = None; @@ -311,8 +382,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ReturnCallableTypeVarScope::Public, ) .return_ty; - let expected_return = - ExpectedReturnType::from_function(db, enclosing_function, function); + let expected_return = ExpectedReturnType::from_function(db, enclosing_function); let expected_ty = expected_return.public(); let scope_id = self.index.node_scope(NodeWithScopeRef::Function(function)); @@ -330,10 +400,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } else { KnownClass::GeneratorType }; - if !inferred_return - .to_instance_unknown(db) - .is_assignable_to(db, expected_ty) + .to_instance_unknown(db, env) + .is_assignable_to(db, env, expected_ty) { report_invalid_generator_function_return_type( &self.context, @@ -343,7 +412,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); } - if let Some(expected_return_ty) = declared_ty.generator_return_type(db) { + if let Some(expected_return_ty) = declared_ty.generator_return_type(db, env) { for returned in self.return_types_and_ranges.iter().copied() { report_bool_as_int( &self.context, @@ -352,27 +421,42 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { expected_return_ty, ); } - for invalid in - self.return_types_and_ranges - .iter() - .copied() - .filter(|actual_return_ty| { - !actual_return_ty.ty.is_assignable_to(db, expected_return_ty) - }) - { - report_invalid_return_type( - &self.context, - invalid.range, - returns_range, - expected_return_ty, - invalid.ty, - ); + for &return_statement in &self.return_types_and_ranges { + if !return_statement + .ty + .is_assignable_to(db, env, expected_return_ty) + { + report_invalid_return_type( + &self.context, + return_statement.range, + returns_range, + expected_return_ty, + return_statement.ty, + ); + } else if self.context.is_lint_enabled(&UNSOUND_RETURN_STATEMENT) + && expected_return_ty.is_fully_static(db, env) + && !return_statement.ty.is_pure_redundant_with( + db, + env, + expected_return_ty, + ) + { + // N.B. the implementation here is the ~same as for `UNSOUND_YIELD`; + // update that too if updating this! + report_unsound_return_statement( + &self.context, + return_statement.range, + returns_range, + expected_return_ty, + return_statement.ty, + ); + } } let use_def = self.index.use_def_map(scope_id); if can_implicitly_return_none(db, use_def) - && !Type::none(db).is_assignable_to(db, expected_return_ty) + && !Type::none(db, env).is_assignable_to(db, env, expected_return_ty) { let no_return = self.return_types_and_ranges.is_empty(); report_implicit_return_type( @@ -393,21 +477,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { report_bool_as_int(&self.context, returned.range, returned.ty, declared_ty); } - 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(db, |ty| !ty.is_notimplemented(db)), - range: ty_range.range, - }), - ty if ty.is_notimplemented(db) => None, - _ => Some(ty_range), - }) - .filter(|ty_range| !expected_return.accepts(db, ty_range.ty)) + for return_statement 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(db, |ty| !ty.is_notimplemented(db)), + range: ty_range.range, + }), + ty if ty.is_notimplemented(db) => None, + _ => Some(ty_range), + }) { // basedpython: a `return` is a conversion site — an in-scope // a conformance or a conversion dunder makes the value @@ -424,15 +507,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) == Some(declared_ty) && crate::types::conversions::repair_conversion( db, + env, self.file(), - invalid.ty, + return_statement.ty, declared_ty, - crate::types::conversions::returned_value_at(function, invalid.range), + crate::types::conversions::returned_value_at( + function, + return_statement.range, + ), ) .is_some_and(|repair| { crate::types::conversions::report_ambiguous_conversion( &self.context, - invalid.range, + return_statement.range, &repair, ); true @@ -440,17 +527,43 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { continue; } - report_invalid_return_type( - &self.context, - invalid.range, - returns_range, - declared_ty, - invalid.ty, - ); + if !expected_return.accepts( + db, + env, + return_statement.ty, + TypeRelation::Assignability, + ) { + report_invalid_return_type( + &self.context, + return_statement.range, + returns_range, + declared_ty, + return_statement.ty, + ); + } else if self.context.is_lint_enabled(&UNSOUND_RETURN_STATEMENT) + && expected_return.public.is_fully_static(db, env) + && !expected_return.accepts( + db, + env, + return_statement.ty, + TypeRelation::Redundancy { pure: true }, + ) + { + // N.B. the implementation here is the ~same as for `UNSOUND_YIELD`; + // update that too if updating this! + report_unsound_return_statement( + &self.context, + return_statement.range, + returns_range, + declared_ty, + return_statement.ty, + ); + } } + let use_def = self.index.use_def_map(scope_id); if can_implicitly_return_none(db, use_def) - && !Type::none(db).is_assignable_to(db, expected_ty) + && !Type::none(db, env).is_assignable_to(db, env, expected_ty) { let no_return = self.return_types_and_ranges.is_empty(); report_implicit_return_type( @@ -474,6 +587,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// `None`: a generator hands back a generator, a body that always raises hands back `Never`, /// and an override or an overload implementation hands back whatever it inherits. fn check_redundant_return_annotation(&self, function: &ast::StmtFunctionDef) { + let env = self.program_environment(); let db = self.db(); let Some(returns) = function.returns.as_deref() else { @@ -510,9 +624,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let without_annotation = function_type .literal(db) .last_definition - .return_type_without_annotation(db, || { + .return_type_without_annotation(db, env, || { return_type_from_body( db, + env, function, scope_id.is_generator_function(self.index), can_implicitly_return_none(db, self.index.use_def_map(scope_id)), @@ -539,6 +654,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// against the function's signature. A function pytest does not manage is /// left untouched. fn check_pytest_function(&self, function_node: &ast::StmtFunctionDef) { + let env = self.program_environment(); let db = self.db(); let Some(function) = self.current_function_type() else { return; @@ -575,7 +691,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { continue; }; - match pytest::resolve_fixture(db, file, name.as_str()) { + match pytest::resolve_fixture(db, env, file, name.as_str()) { Some(fixture) => { // only an explicitly annotated parameter can disagree with // its fixture; an unannotated one adopts the fixture's type, @@ -589,7 +705,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { continue; }; let declared = parameter.annotated_type(); - if provided.is_assignable_to(db, declared) { + if provided.is_assignable_to(db, env, declared) { continue; } let Some(builder) = self.context.report_lint(&INVALID_FIXTURE_TYPE, range) @@ -598,12 +714,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; let mut diagnostic = builder.into_diagnostic(format_args!( "Fixture `{name}` provides `{}`, but the parameter is annotated `{}`", - provided.display(db), - declared.display(db), + provided.display(db, env), + declared.display(db, env), )); if let Some(fixture_definition) = fixture.definition { let fixture_module = - parsed_module(db, fixture_definition.file(db)).load(db); + parsed_module(db, fixture_definition.program_file(db).python_file(db)) + .load(db); let span = Span::from(fixture_definition.focus_range(db, &fixture_module)); diagnostic .annotate(Annotation::secondary(span).message("fixture defined here")); @@ -623,6 +740,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// name against the function's parameters, and each value row's length /// against the number of names. fn check_parametrize(&self, function_node: &ast::StmtFunctionDef, function: FunctionType<'db>) { + let env = self.program_environment(); let db = self.db(); let callable = function.signature(db); let parameter_names: FxHashSet<&ast::name::Name> = callable @@ -638,7 +756,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .unwrap_or_default(); for decorator in &function_node.decorator_list { - let Some(marker) = pytest::parametrize_marker(db, function, decorator) else { + let Some(marker) = pytest::parametrize_marker(db, env, function, decorator) else { continue; }; @@ -714,7 +832,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let db = self.db(); let decorator_inference = - (!decorator_list.is_empty()).then(|| function_known_decorators(db, definition)); + (!decorator_list.is_empty()).then(|| function_known_decorators(self.db(), definition)); if let Some(decorator_inference) = decorator_inference.as_ref() { self.context.extend(decorator_inference.diagnostics()); self.expressions @@ -886,6 +1004,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } + if function_decorators + .contains(FunctionDecorators::ABSTRACT_METHOD | FunctionDecorators::FINAL) + && self + .index + .scope(self.scope().file_scope_id(db)) + .kind() + .is_class() + && let Some(builder) = self.context.report_lint(&ABSTRACT_AND_FINAL_METHOD, name) + { + builder.into_diagnostic(format_args!( + "Method `{name}` cannot be both `@abstractmethod` and `@final`", + )); + } + let has_defaults = parameters .iter_non_variadic_params() .any(|param| param.default.is_some()); @@ -913,7 +1045,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let body_scope = self .index .node_scope(NodeWithScopeRef::Function(function)) - .to_scope_id(db, self.file()); + .to_scope_id(db, self.program_file()); let overload_literal = OverloadLiteral::new( db, @@ -926,8 +1058,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { function.returns.is_some(), ); let function_literal = FunctionLiteral::new(db, overload_literal); - - let mut inferred_ty = Type::FunctionLiteral(FunctionType::new(db, function_literal, None)); + let function_type = FunctionType::new(db, function_literal, None); + let is_decorated_overload_implementation = !decorator_types_and_nodes.is_empty() + && function_literal.has_separate_implementation(db); + let is_decorated_overload = + !decorator_types_and_nodes.is_empty() && overload_literal.is_overload(db); + + let mut inferred_ty = Type::FunctionLiteral( + if is_decorated_overload_implementation || is_decorated_overload { + FunctionType::new(db, function_literal.without_overloads(), None) + } else { + function_type + }, + ); if !decorator_list.is_empty() { self.undecorated_type = Some(inferred_ty); } @@ -938,7 +1081,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let current_scope = self.scope().file_scope_id(db); for type_param in type_params.iter() { let param_name = type_param.name(); - for enclosing in enclosing_generic_contexts(db, self.index, current_scope) { + for enclosing in enclosing_generic_contexts(self.db(), self.index, current_scope) { if let Some(other_typevar) = enclosing.binds_named_typevar(db, ¶m_name.id) { let kind = match type_param { ast::TypeParam::TypeVar(_) => TypeVarKind::Pep695TypeVar, @@ -972,6 +1115,34 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; } + if is_decorated_overload_implementation { + let function_type = if let Type::FunctionLiteral(function) = inferred_ty { + FunctionType::new( + db, + function_literal + .with_last_definition_metadata(db, function.literal(db).last_definition), + None, + ) + } else { + function_type + }; + let implementation_callables = inferred_ty + .try_upcast_to_callable(db, self.program_environment()) + .map_or_else(Box::default, |callables| { + callables.iter().copied().collect() + }); + inferred_ty = Type::FunctionLiteral( + function_type.with_implementation_callables(db, implementation_callables), + ); + } else if is_decorated_overload && let Type::FunctionLiteral(function) = inferred_ty { + inferred_ty = Type::FunctionLiteral(FunctionType::new( + db, + function_literal + .with_last_definition_metadata(db, function.literal(db).last_definition), + None, + )); + } + self.add_declaration_with_binding( function.into(), definition, @@ -999,7 +1170,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { "Useless body for `@overload`-decorated function `{}`", function.name )); - diagnostic.set_primary_message("This statement will never be executed"); + diagnostic.set_primary_annotation_message("This statement will never be executed"); diagnostic.info( "`@overload`-decorated functions are solely for type checkers \ and must be overwritten at runtime by a non-`@overload`-decorated implementation", @@ -1044,9 +1215,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let previous_typevar_binding_context = self.typevar_binding_context.replace(definition); if !has_type_params { - self.infer_return_type_annotation(function); + self.infer_function_signature_annotations(function, definition); self.infer_raises_clause(function); - self.infer_parameters(function.parameters.as_ref()); } if has_defaults { @@ -1061,9 +1231,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let type_params_scope = self .index .node_scope(NodeWithScopeRef::FunctionTypeParameters(function)) - .to_scope_id(db, self.file()); + .to_scope_id(db, self.program_file()); let type_params_inference = - infer_scope_types(db, type_params_scope, TypeContext::default()); + infer_scope_types(self.db(), type_params_scope, TypeContext::default()); for param_with_default in function.parameters.iter_non_variadic_params() { let Some(default) = param_with_default.default.as_deref() else { @@ -1173,29 +1343,89 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } pub(super) 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"); - let binding_context = self.index.expect_single_definition(function); let previous_typevar_binding_context = self.typevar_binding_context.replace(binding_context); - self.infer_return_type_annotation(function); + self.infer_function_signature_annotations(function, binding_context); self.infer_raises_clause(function); - // basedpython: a `type def` is not a runtime function — the transpiler erases the - // declaration, so there is no closure for the specialization step to rebuild - let reification = if ast::helpers::is_type_def(function) { - TypeParamReification::TypeDef - } else { - TypeParamReification::Function - }; - self.infer_type_parameters(type_params, reification); - self.infer_parameters(&function.parameters); self.typevar_binding_context = previous_typevar_binding_context; } - fn infer_parameters(&mut self, parameters: &ast::Parameters) { + /// Infer an annotated method receiver before the rest of its signature so `Self` can be + /// validated where it occurs, including inside parsed string annotations. + /// + /// ```python + /// class Example: + /// def method(self: object) -> "Self | object": ... + /// ``` + fn infer_function_signature_annotations( + &mut self, + function: &ast::StmtFunctionDef, + definition: Definition<'db>, + ) { + let receiver_is_incompatible = self.infer_method_receiver_annotation(function, definition); + let previous_incompatible_receiver = self.context.inference_flags.replace( + InferenceFlags::HAS_INCOMPATIBLE_SELF_RECEIVER, + receiver_is_incompatible == Some(true), + ); + + self.infer_return_type_annotation(function); + if let Some(type_params) = function.type_params.as_deref() { + // basedpython: a `type def` is not a runtime function — the transpiler erases the + // declaration, so there is no closure for the specialization step to rebuild + let reification = if ast::helpers::is_type_def(function) { + TypeParamReification::TypeDef + } else { + TypeParamReification::Function + }; + self.infer_type_parameters(type_params, reification); + } + self.infer_parameters(&function.parameters, receiver_is_incompatible.is_some()); + + self.context.inference_flags.set( + InferenceFlags::HAS_INCOMPATIBLE_SELF_RECEIVER, + previous_incompatible_receiver, + ); + } + + /// Infers an explicitly annotated method receiver before the rest of its signature. + /// + /// Returns whether the annotation is incompatible with `Self`, or `None` for functions without + /// an annotated instance or class receiver. + fn infer_method_receiver_annotation( + &mut self, + function: &ast::StmtFunctionDef, + definition: Definition<'db>, + ) -> Option { + let receiver = function + .parameters + .posonlyargs + .first() + .or_else(|| function.parameters.args.first())?; + let annotation = receiver.parameter.annotation.as_deref()?; + let receiver_kind = MethodReceiverKind::from_function(self.db(), definition, function)?; + + let previously_in_parameter_annotation = self + .context + .inference_flags + .replace(InferenceFlags::IN_PARAMETER_ANNOTATION, true); + let annotation_type = self.infer_type_expression_with_state( + annotation, + DeferredExpressionState::from(self.defer_annotations()), + ); + self.context.inference_flags.set( + InferenceFlags::IN_PARAMETER_ANNOTATION, + previously_in_parameter_annotation, + ); + + Some(!receiver_kind.accepts_annotation(self.db(), annotation_type)) + } + + fn infer_parameters( + &mut self, + parameters: &ast::Parameters, + first_annotation_already_inferred: bool, + ) { let ast::Parameters { range: _, node_index: _, @@ -1207,7 +1437,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } = parameters; self.context.inference_flags |= InferenceFlags::IN_PARAMETER_ANNOTATION; - for param_with_default in parameters.iter_non_variadic_params() { + for param_with_default in parameters + .iter_non_variadic_params() + .skip(usize::from(first_annotation_already_inferred)) + { self.infer_parameter_with_default(param_with_default); } if let Some(vararg) = vararg { @@ -1239,6 +1472,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { parameters: &ast::Parameters, kwargs_annotation: &ast::Expr, ) -> bool { + let env = self.program_environment(); let Type::TypeVar(typevar) = annotated_type else { return false; }; @@ -1248,13 +1482,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .filter_map(ruff_python_ast::AnyParameterRef::annotation) .filter(|annotation| !std::ptr::eq(*annotation, kwargs_annotation)) .any(|annotation| { - any_over_type(self.db(), self.file_expression_type(annotation), false, |ty| { + any_over_type(self.db(), env, self.file_expression_type(annotation), false, |ty| { matches!(ty, Type::TypeVar(other) if other.identity(self.db()) == identity) }) }) } fn validate_unpacked_typed_dict_kwargs(&mut self, parameters: &ast::Parameters) { + let db = self.db(); + let env = self.program_environment(); let Some(kwargs) = parameters.kwarg.as_ref() else { return; }; @@ -1279,13 +1515,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // arguments; there are no keys to check against the other parameters until then. This // only works while the keywords are the sole source of the type variable -- if another // parameter also mentions it, fall through and report the unpacked value as invalid. - if annotated_type.is_typed_dict_bounded_typevar(self.db()) + if annotated_type.is_typed_dict_bounded_typevar(self.db(), env) && !self.typevar_used_by_another_parameter(annotated_type, parameters, annotation) { return; } let Some(unpacked_keys) = extract_unpacked_typed_dict_keys_from_kwargs_annotation( - self.db(), + db, annotated_type, annotation_flags, ) else { @@ -1294,7 +1530,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { let diag = builder.into_diagnostic(format_args!( "Unpacked value for `**kwargs` must be a TypedDict, not `{}`", - annotated_type.display(self.db()) + annotated_type.display(db, env) )); add_type_expression_reference_link(diag); } @@ -1412,6 +1648,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { parameter_with_default: &'ast ast::ParameterWithDefault, definition: Definition<'db>, ) { + let env = self.program_environment(); let ast::ParameterWithDefault { parameter, default, @@ -1426,7 +1663,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // `Overlapping[Key]` is a call-binder-only marker; inside the body the // parameter is seen as `Key`'s upper bound so it can never be written // back into `Key`-typed covariant storage - let declared_ty = self.file_expression_type(annotation).erase_overlapping(db); + let declared_ty = self + .file_expression_type(annotation) + .erase_overlapping(db, env); // P.args and P.kwargs are only valid as annotations on *args and **kwargs, // not on regular parameters. basedpython has no source spelling for them at all, @@ -1460,7 +1699,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Avoid duplicate diagnostics: invalid TypedDict literals already emit specific errors. let suppress_invalid_default = is_invalid_typed_dict_literal(db, declared_ty, default_expr.into()); - if !default_ty.is_assignable_to(db, declared_ty) + if !default_ty.is_assignable_to(db, env, declared_ty) && !suppress_invalid_default && !((self.in_stub() || self.in_function_overload_or_abstractmethod() @@ -1479,8 +1718,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { builder.into_diagnostic(format_args!( "Default value of type `{}` is not assignable \ to annotated parameter type `{}`", - default_ty.display(db), - declared_ty.display(db) + default_ty.display(db, env), + declared_ty.display(db, env) )); } } @@ -1517,15 +1756,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let ty = if let Some(default_expr) = default_expr { let default_ty = self.file_expression_type(default_expr); if let Some(base) = inherited { - if default_ty.is_assignable_to(db, base) { + if default_ty.is_assignable_to(db, env, base) { base } else { - UnionType::from_two_elements(db, base, default_ty) + UnionType::from_two_elements(db, env, base, default_ty) } } else if let Some(hole) = hole { hole } else { - UnionType::from_two_elements(db, Type::unknown(), default_ty) + UnionType::from_two_elements(db, env, Type::unknown(), default_ty) } } else if let Some(ty) = self.special_first_method_parameter_type(parameter) { ty @@ -1551,9 +1790,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// /// [`check_pytest_function`]: Self::check_pytest_function fn pytest_fixture_parameter_type(&self, parameter: &ast::Parameter) -> Option> { + let env = self.program_environment(); let db = self.db(); let function = nearest_enclosing_function(db, self.index, self.scope())?; - pytest::injected_parameter_type(db, function, parameter.name.as_str()) + pytest::injected_parameter_type(db, env, function, parameter.name.as_str()) } /// basedpython: for an unannotated parameter, look up the type that @@ -1622,8 +1862,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { Type::tuple(TupleType::new( db, + self.program_environment(), &TupleSpecBuilder::with_capacity(0) - .concat_variadic_typevar(db, typevar) + .concat_variadic_typevar(db, self.program_environment(), typevar) .build(), )) } @@ -1647,22 +1888,22 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut diag = builder.into_diagnostic(format_args!( "`{name}.kwargs` is valid only in `**kwargs` annotation", )); - diag.set_primary_message(format_args!( + diag.set_primary_annotation_message(format_args!( "Did you mean `{name}.args`?" )); add_type_expression_reference_link(diag); } - Type::homogeneous_tuple(db, Type::unknown()) + Type::homogeneous_tuple(db, self.program_environment(), Type::unknown()) } // `*args: P` None => { // The diagnostic for this case is handled in `in_type_expression`. - Type::homogeneous_tuple(db, Type::unknown()) + Type::homogeneous_tuple(db, self.program_environment(), Type::unknown()) } } } - _ => Type::homogeneous_tuple(db, annotated_type), + _ => Type::homogeneous_tuple(db, self.program_environment(), annotated_type), }; self.add_declaration_with_binding( @@ -1671,7 +1912,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { &DeclaredAndInferredType::are_the_same_type(ty), ); } else { - let inferred_ty = Type::homogeneous_tuple(db, Type::unknown()); + let inferred_ty = + Type::homogeneous_tuple(db, self.program_environment(), Type::unknown()); self.add_binding(parameter.into(), definition) .insert(self, inferred_ty); } @@ -1756,12 +1998,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// `x.fn` where `fn` names a receiver callable in scope rather than a member /// of `x`, which the transpiler lowers to `fn(x)` fn is_implicit_receiver_attribute(&self, attribute: &ast::ExprAttribute) -> bool { + let env = self.program_environment(); self.is_basedpython_file() && self .try_expression_type(&attribute.value) .is_some_and(|receiver_ty| { crate::types::receivers::is_implicit_receiver_attribute( self.db(), + env, self.file(), self.scope(), attribute, @@ -1810,6 +2054,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// block), so its callback must be declared to return `None`. Report a /// callback with any other return type — those are not yet supported. fn check_trailing_lambda_callback_returns_none(&self, function: &ast::StmtFunctionDef) { + let env = self.program_environment(); let db = self.db(); let Some(callee) = function.trailing_lambda_callee() else { return; @@ -1824,7 +2069,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; // the block returns `None`; a declared return type that accepts `None` // (`None`, `int | None`, `object`, …) is satisfiable, anything else is not - if Type::none(db).is_assignable_to(db, return_ty) { + if Type::none(db, env).is_assignable_to(db, env, return_ty) { return; } if let Some(builder) = self @@ -1834,7 +2079,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { builder.into_diagnostic(format_args!( "a trailing-lambda callback must return `None`, not `{}` \ (other return types are not yet supported)", - return_ty.display(db) + return_ty.display(db, env) )); } } @@ -1844,8 +2089,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { &mut self, parameter: &ast::Parameter, ) -> Option> { + let env = self.program_environment(); let db = self.db(); - let file = self.file(); + let file = self.program_file(); let function_scope_id = self.scope(); let function_scope = function_scope_id.scope(db); @@ -1888,7 +2134,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .as_class_literal() .and_then(|class| class.known(db)) { - if known_class == KnownClass::Staticmethod { + // `__new__` is implicitly a static method, so spelling the decorator out + // changes nothing: its first parameter is still the class being constructed + if known_class == KnownClass::Staticmethod && function_name != "__new__" { return None; } @@ -1913,18 +2161,23 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { let body_view = extensions::body_view_class(db, static_literal)?; return Some(if is_classmethod || function_name == "__new__" { - SubclassOfType::from(db, SubclassOfInner::Class(body_view)) + SubclassOfType::from(db, env, SubclassOfInner::Class(body_view)) } else { - Type::instance(db, body_view) + Type::instance(db, env, body_view) }); } let typing_self = typing_self(db, self.scope(), Some(method_definition), class_literal); - if is_classmethod || function_name == "__new__" { - typing_self - .map(|typing_self| SubclassOfType::from(db, SubclassOfInner::TypeVar(typing_self))) + let receiver_kind = if is_classmethod || function_name == "__new__" { + MethodReceiverKind::Class } else { - typing_self.map(Type::TypeVar) + MethodReceiverKind::Instance + }; + match receiver_kind { + MethodReceiverKind::Class => typing_self.map(|typing_self| { + SubclassOfType::from(db, env, SubclassOfInner::TypeVar(typing_self)) + }), + MethodReceiverKind::Instance => typing_self.map(Type::TypeVar), } } @@ -1940,6 +2193,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { parameter: &'ast ast::Parameter, definition: Definition<'db>, ) { + let env = self.program_environment(); let db = self.db(); if let Some(annotation) = parameter.annotation() { @@ -1961,12 +2215,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut diag = builder.into_diagnostic(format_args!( "`{name}.args` is valid only in `*args` annotation", )); - diag.set_primary_message(format_args!("Did you mean `{name}.kwargs`?")); + diag.set_primary_annotation_message(format_args!( + "Did you mean `{name}.kwargs`?" + )); add_type_expression_reference_link(diag); } KnownClass::Dict.to_specialized_instance( db, - &[KnownClass::Str.to_instance(db), Type::unknown()], + env, + &[KnownClass::Str.to_instance(db, env), Type::unknown()], ) } @@ -1978,7 +2235,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // The diagnostic for this case is handled in `in_type_expression`. KnownClass::Dict.to_specialized_instance( db, - &[KnownClass::Str.to_instance(db), Type::unknown()], + env, + &[KnownClass::Str.to_instance(db, env), Type::unknown()], ) } } @@ -1991,8 +2249,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { annotated_type } else { - KnownClass::Dict - .to_specialized_instance(db, &[KnownClass::Str.to_instance(db), annotated_type]) + KnownClass::Dict.to_specialized_instance( + db, + env, + &[KnownClass::Str.to_instance(db, env), annotated_type], + ) }; self.add_declaration_with_binding( parameter.into(), @@ -2000,8 +2261,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { &DeclaredAndInferredType::are_the_same_type(ty), ); } else { - let inferred_ty = KnownClass::Dict - .to_specialized_instance(db, &[KnownClass::Str.to_instance(db), Type::unknown()]); + let inferred_ty = KnownClass::Dict.to_specialized_instance( + db, + env, + &[KnownClass::Str.to_instance(db, env), Type::unknown()], + ); self.add_binding(parameter.into(), definition) .insert(self, inferred_ty); @@ -2024,6 +2288,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { node_index: _, } = parameter_with_default; + let env = &self.program_environment(); let default_expr = default.as_ref(); let ty = if let Some(parameter_type) = self.annotated_lambda_parameter_type(index, lambda) { parameter_type @@ -2032,9 +2297,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if self.settings().sound_types { // basedpython: same rule as an unannotated function parameter with a default — // the parameter takes the default's promoted type instead of folding in `Unknown` - default_ty.promote(self.db()) + default_ty.promote(self.db(), env) } else { - UnionType::from_two_elements(self.db(), Type::unknown(), default_ty) + UnionType::from_two_elements(self.db(), env, Type::unknown(), default_ty) } } else { Type::unknown() @@ -2068,12 +2333,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { lambda: &'ast ast::ExprLambda, definition: Definition<'db>, ) { + let db = self.db(); // Note that this currently always returns `None` because we do not support `Unpack` // annotations for callable types. let ty = if let Some(parameter_type) = self.annotated_lambda_parameter_type(index, lambda) { parameter_type } else { - Type::homogeneous_tuple(self.db(), Type::unknown()) + Type::homogeneous_tuple(db, self.program_environment(), Type::unknown()) }; // see `infer_lambda_parameter_definition` — annotated `*args` is a // `DeclarationAndBinding`, which doesn't populate @@ -2097,9 +2363,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { parameter: &'ast ast::Parameter, definition: Definition<'db>, ) { + let db = self.db(); + let env = self.program_environment(); let inferred_ty = KnownClass::Dict.to_specialized_instance( - self.db(), - &[KnownClass::Str.to_instance(self.db()), Type::unknown()], + db, + env, + &[KnownClass::Str.to_instance(db, env), Type::unknown()], ); if parameter.annotation.is_some() { @@ -2121,6 +2390,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { index: u32, lambda: &'ast ast::ExprLambda, ) -> Option> { + let db = self.db(); let enclosing_stmt = infer_statement_types( self.db(), self.index.enclosing_lambda_statement(lambda.into())?, @@ -2132,7 +2402,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; let parameter_type = signature.parameters().as_slice()[index as usize].annotated_type(); - if parameter_type.is_unknown() || parameter_type.has_unspecialized_type_var(self.db()) { + if parameter_type.is_unknown() + || parameter_type.has_unspecialized_type_var(db, self.program_environment()) + { None } else { Some(parameter_type) diff --git a/crates/ty_python_semantic/src/types/infer/builder/imports.rs b/crates/ty_python_semantic/src/types/infer/builder/imports.rs index 481b907ffa..ab27f8e9d2 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/imports.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/imports.rs @@ -1,12 +1,12 @@ use ruff_python_ast as ast; use ruff_text_size::{Ranged, TextRange}; use ty_module_resolver::{ - KnownModule, Module, ModuleName, ModuleNameResolutionError, ModuleResolveMode, resolve_module, - search_paths, + ImportingFile, KnownModule, Module, ModuleName, ModuleNameResolutionError, ModuleResolveMode, + resolve_module, search_paths, }; use crate::{ - Program, TypeQualifiers, add_inferred_python_version_hint_to_diagnostic, + TypeQualifiers, add_inferred_python_version_hint_to_diagnostic, dependencies::{self, GroupName, ImportStanding}, place::{ DefinedPlace, Definedness, Place, PlaceAndQualifiers, TypeOrigin, @@ -78,14 +78,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if level == 0 { if let Some(module_name) = module_name { - let program = Program::get(db); - let typeshed_versions = program.search_paths(db).typeshed_versions(); + let resolver_environment = self.program_environment().program(db); + let typeshed_versions = resolver_environment.search_paths(db).typeshed_versions(); // Loop over ancestors in case we have info on the parent module but not submodule for module_name in module_name.ancestors() { 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(db); + let python_version = self.program_environment().python_version(db); if !version_range.contains(python_version) { // ...But not on *this* Python version. diagnostic.info(format_args!( @@ -94,6 +94,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { )); add_inferred_python_version_hint_to_diagnostic( db, + self.file(), &mut diagnostic, "resolving modules", ); @@ -105,13 +106,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } } else { + let importing_file = ImportingFile::File( + self.file(), + self.program_environment().resolver_environment(db), + ); if let Some(better_level) = (0..level).rev().find(|reduced_level| { let Ok(module_name) = - ModuleName::from_identifier_parts(db, self.file(), module, *reduced_level) + ModuleName::from_identifier_parts(db, importing_file, module, *reduced_level) else { return false; }; - resolve_module(db, self.file(), &module_name).is_some() + resolve_module(db, importing_file, &module_name).is_some() }) { diagnostic .help("The module can be resolved if the number of leading dots is reduced"); @@ -130,7 +135,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Add search paths information to the diagnostic // Use the same search paths function that is used in actual module resolution let verbose = db.verbose(); - let search_paths = search_paths(db, ModuleResolveMode::Typing); + let search_paths = search_paths( + db, + self.program_environment().resolver_environment(db), + ModuleResolveMode::Typing, + ); diagnostic.info(format_args!( "Searched in the following paths during module resolution:" @@ -248,7 +257,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { for alias in names { for definition in self.index.definitions(alias) { - let inferred = infer_definition_types(db, *definition); + let inferred = infer_definition_types(self.db(), *definition); // Check non-star imports for deprecations if definition.kind(db).as_star_import().is_none() { // In the initial cycle, `declaration_types()` is empty, so no deprecation check is performed. @@ -281,7 +290,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { format_import_from_module(*level, module), self.file().path(db), ); - let module_name = ModuleName::from_import_statement(db, self.file(), import_from); + let importing_file = ImportingFile::File( + self.file(), + self.program_environment().resolver_environment(db), + ); + let module_name = ModuleName::from_import_statement(db, importing_file, import_from); let module_name = match module_name { Ok(module_name) => module_name, @@ -310,7 +323,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } }; - if resolve_module(db, self.file(), &module_name).is_none() { + if resolve_module(db, importing_file, &module_name).is_none() { self.report_unresolved_import(module_ref.range(), *level, module, Some(&module_name)); } else { self.check_framework_stubs(module_ref.range(), &module_name); @@ -327,6 +340,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { module: Module<'db>, name: &str, ) -> Option> { + let env = self.program_environment(); let db = self.db(); let redirected = if module.is_known(db, KnownModule::Typing) { basedpython_typing_added_in(name).is_some() @@ -338,7 +352,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if !redirected { return None; } - let place = typing_extensions_symbol(db, name); + let place = typing_extensions_symbol(db, env, name); (!place.place.is_undefined()).then_some(place) } @@ -353,7 +367,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { fn check_dependency_declaration(&self, range: TextRange, module_name: &ModuleName) { let db = self.db(); - let Some(module) = resolve_module(db, self.file(), module_name) else { + let Some(module) = resolve_module( + db, + ImportingFile::File( + self.file(), + db.program_file(self.file()).resolver_environment(db), + ), + module_name, + ) else { return; }; @@ -407,14 +428,22 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let Some(package_name) = ModuleName::new(framework.package) else { return; }; - let Some(module) = resolve_module(db, self.file(), &package_name) else { + let Some(module) = resolve_module( + db, + ImportingFile::File( + self.file(), + db.program_file(self.file()).resolver_environment(db), + ), + &package_name, + ) else { return; }; - // A first-party module that happens to share the framework's name is - // not the framework. + // A module that happens to share the framework's name is not the framework unless it + // was installed as one — this diagnostic asks the user to install a stubs distribution, + // which only makes sense for a package they installed in the first place. if !module .search_path(db) - .is_some_and(ty_module_resolver::SearchPath::is_third_party) + .is_some_and(ty_module_resolver::SearchPath::is_installed_distribution) { return; } @@ -480,9 +509,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { alias: &ast::Alias, definition: Definition<'db>, ) { + let env = self.program_environment(); let db = self.db(); - let Ok(module_name) = ModuleName::from_import_statement(db, self.file(), import_from) + let importing_file = ImportingFile::File( + self.file(), + self.program_environment().resolver_environment(db), + ); + let Ok(module_name) = ModuleName::from_import_statement(db, importing_file, import_from) else { self.add_unknown_declaration_with_binding(alias.into(), definition); return; @@ -502,7 +536,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { return; } - let Some(module) = resolve_module(db, self.file(), &module_name) else { + let Some(module) = resolve_module(db, importing_file, &module_name) else { self.add_unknown_declaration_with_binding(alias.into(), definition); return; }; @@ -510,7 +544,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let module_literal = ModuleLiteralType::new( db, module, - module.kind(db).is_package().then_some(self.file()), + module.kind(db).is_package().then_some(self.program_file()), ); let module_ty = Type::ModuleLiteral(module_literal); @@ -541,7 +575,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // First try loading the requested attribute from the module. if !skip_self_referential_member_lookup { - let mut member = module_literal.static_member(db, name); + let mut member = module_literal.static_member(db, env, name); // basedpython: version-gated `typing`/`warnings` members are always // available. When the member is missing at the target Python version, // fall back to `typing_extensions`, mirroring the transpiler's import @@ -682,6 +716,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(full_submodule_name) = full_submodule_name { submodule_hint_added = hint_if_stdlib_submodule_exists_on_other_versions( db, + self.file(), + self.program_environment(), &mut diagnostic, &full_submodule_name, module, @@ -691,6 +727,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if !submodule_hint_added { hint_if_stdlib_attribute_exists_on_other_versions( db, + self.program_file(), diagnostic, module_ty, name, @@ -718,15 +755,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { definition: Definition<'db>, ) { let db = self.db(); + let importing_file = ImportingFile::File( + self.file(), + self.program_environment().resolver_environment(db), + ); // Get this package's absolute module name by resolving `.`, and make sure it exists - let Ok(thispackage_name) = ModuleName::package_for_file(db, self.file()) else { + let Ok(thispackage_name) = ModuleName::package_for_file(db, importing_file) else { self.add_binding(import_from.into(), definition) .insert(self, Type::unknown()); return; }; - let Some(module) = resolve_module(db, self.file(), &thispackage_name) else { + let Some(module) = resolve_module(db, importing_file, &thispackage_name) else { self.add_binding(import_from.into(), definition) .insert(self, Type::unknown()); return; @@ -738,7 +779,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // First we normalize to `whatever.thispackage.x.y` let Some(final_part) = ModuleName::from_identifier_parts( db, - self.file(), + importing_file, import_from.module.as_deref(), import_from.level, ) @@ -798,7 +839,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { )); hint_if_stdlib_submodule_exists_on_other_versions( - db, + self.db(), + self.file(), + self.program_environment(), &mut diagnostic, &full_submodule_name, module, diff --git a/crates/ty_python_semantic/src/types/infer/builder/named_tuple.rs b/crates/ty_python_semantic/src/types/infer/builder/named_tuple.rs index 16dcda2c89..febb1b79f6 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/named_tuple.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/named_tuple.rs @@ -33,6 +33,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { definition: Option>, kind: NamedTupleKind, ) -> Type<'db> { + let env = self.program_environment(); let db = self.db(); // The fallback type reflects the fact that if the call were successful, @@ -45,9 +46,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let fallback = || { IntersectionType::from_elements( db, + env, [ - Type::homogeneous_tuple(db, Type::unknown()).to_meta_type(db), - KnownClass::NamedTupleLike.to_subclass_of(db), + Type::homogeneous_tuple(db, env, Type::unknown()).to_meta_type(db, env), + KnownClass::NamedTupleLike.to_subclass_of(db, env), Type::unknown(), ], ) @@ -219,7 +221,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { "defaults" if kind.is_collections() => { defaults_kw = Some(kw); if let Some(element_types) = - extract_fixed_length_iterable_element_types(db, &kw.value, |expr| { + extract_fixed_length_iterable_element_types(db, env, &kw.value, |expr| { self.expression_type(expr) }) { @@ -234,18 +236,19 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } // Emit diagnostic for invalid types (not Iterable[Any] | None). let iterable_any = - KnownClass::Iterable.to_specialized_instance(db, &[Type::any()]); - let valid_type = UnionType::from_two_elements(db, iterable_any, Type::none(db)); - if !kw_type.is_assignable_to(db, valid_type) + KnownClass::Iterable.to_specialized_instance(db, env, &[Type::any()]); + let valid_type = + UnionType::from_two_elements(db, env, iterable_any, Type::none(db, env)); + if !kw_type.is_assignable_to(db, env, valid_type) && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, &kw.value) { let mut diagnostic = builder.into_diagnostic(format_args!( "Invalid argument to parameter `defaults` of `namedtuple()`" )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Expected `Iterable[Any] | None`, found `{}`", - kw_type.display(db) + kw_type.display(db, env) )); } } @@ -253,16 +256,16 @@ impl<'db> TypeInferenceBuilder<'db, '_> { rename_type = Some(kw_type); // Emit diagnostic for non-bool types. - if !kw_type.is_assignable_to(db, KnownClass::Bool.to_instance(db)) + if !kw_type.is_assignable_to(db, env, KnownClass::Bool.to_instance(db, env)) && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, &kw.value) { let mut diagnostic = builder.into_diagnostic(format_args!( "Invalid argument to parameter `rename` of `namedtuple()`" )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Expected `bool`, found `{}`", - kw_type.display(db) + kw_type.display(db, env) )); } } @@ -270,19 +273,20 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // Emit diagnostic for invalid types (not str | None). let valid_type = UnionType::from_two_elements( db, - KnownClass::Str.to_instance(db), - Type::none(db), + env, + KnownClass::Str.to_instance(db, env), + Type::none(db, env), ); - if !kw_type.is_assignable_to(db, valid_type) + if !kw_type.is_assignable_to(db, env, valid_type) && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, &kw.value) { let mut diagnostic = builder.into_diagnostic(format_args!( "Invalid argument to parameter `module` of `namedtuple()`" )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Expected `str | None`, found `{}`", - kw_type.display(db) + kw_type.display(db, env) )); } } @@ -329,15 +333,15 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .map(|literal| literal.value(db)); if name.is_none() - && !name_type.is_assignable_to(db, KnownClass::Str.to_instance(db)) + && !name_type.is_assignable_to(db, env, KnownClass::Str.to_instance(db, env)) && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, name_arg) { let mut diagnostic = builder.into_diagnostic(format_args!( "Invalid argument to parameter `typename` of `{kind}()`" )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Expected `str`, found `{}`", - name_type.display(db) + name_type.display(db, env) )); } else if let Some(actual_name) = name && let Some(definition) = definition @@ -418,6 +422,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { default_types: &[Type<'db>], defaults_kw: Option<&ast::Keyword>, ) -> NamedTupleSpec<'db> { + let env = self.program_environment(); let db = self.db(); // `collections.namedtuple`: `field_names` is a list or tuple of strings, or a space or @@ -425,7 +430,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // Check for `rename=True`. Use `is_always_true()` to handle truthy values // (e.g., `rename=1`), though we'd still want a diagnostic for non-bool types. - let rename = rename_type.is_some_and(|ty| ty.bool(db).is_always_true()); + let rename = rename_type.is_some_and(|ty| ty.bool(db, env).is_always_true()); let fields_type = self.infer_expression(fields_arg, TypeContext::default()); @@ -442,7 +447,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .collect(), ) } else { - extract_fixed_length_iterable_element_types(db, fields_arg, |expr| { + extract_fixed_length_iterable_element_types(db, env, fields_arg, |expr| { self.expression_type(expr) }) .and_then(|field_types| { @@ -455,18 +460,23 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if maybe_field_names.is_none() { // Emit diagnostic if the type is outright invalid (not str | Iterable[str]). - let iterable_str = KnownClass::Iterable.to_specialized_instance(db, &[Type::any()]); - let valid_type = - UnionType::from_two_elements(db, KnownClass::Str.to_instance(db), iterable_str); - if !fields_type.is_assignable_to(db, valid_type) + let iterable_str = + KnownClass::Iterable.to_specialized_instance(db, env, &[Type::any()]); + let valid_type = UnionType::from_two_elements( + db, + env, + KnownClass::Str.to_instance(db, env), + iterable_str, + ); + if !fields_type.is_assignable_to(db, env, valid_type) && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, fields_arg) { let mut diagnostic = builder.into_diagnostic(format_args!( "Invalid argument to parameter `field_names` of `namedtuple()`" )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Expected `str` or an iterable of strings, found `{}`", - fields_type.display(db) + fields_type.display(db, env) )); } } @@ -511,7 +521,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { { let mut diagnostic = builder.into_diagnostic(format_args!("Too many defaults for `namedtuple()`")); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Got {defaults_count} default values but only {num_fields} field names" )); diagnostic.info("This will raise `TypeError` at runtime"); @@ -552,6 +562,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { Tuple, } + let env = self.program_environment(); let db = self.db(); // Get the elements from the list or tuple literal. @@ -564,7 +575,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut diagnostic = builder.into_diagnostic( "Invalid argument to parameter `fields` of `NamedTuple()`", ); - diagnostic.set_primary_message("`fields` must be a literal list or tuple"); + diagnostic + .set_primary_annotation_message("`fields` must be a literal list or tuple"); } return NamedTupleSpec::unknown(db); } @@ -586,12 +598,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> { SequenceKind::List => { self.store_expression_type( fields_arg, - KnownClass::List.to_instance(db), + KnownClass::List.to_instance(db, env), ); } SequenceKind::Tuple => self.store_expression_type( fields_arg, - Type::homogeneous_tuple(db, Type::unknown()), + Type::homogeneous_tuple(db, env, Type::unknown()), ), } if let Some(builder) = @@ -600,7 +612,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut diagnostic = builder.into_diagnostic( "Invalid argument to parameter `fields` of `NamedTuple()`", ); - diagnostic.set_primary_message( + diagnostic.set_primary_annotation_message( "`fields` must be a sequence of literal lists or tuples", ); } @@ -615,18 +627,21 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } match field_arg_kind { SequenceKind::List => { - self.store_expression_type(fields_arg, KnownClass::List.to_instance(db)); + self.store_expression_type( + fields_arg, + KnownClass::List.to_instance(db, env), + ); } SequenceKind::Tuple => self.store_expression_type( fields_arg, - Type::homogeneous_tuple(db, Type::unknown()), + Type::homogeneous_tuple(db, env, Type::unknown()), ), } if let Some(builder) = self.context.report_lint(&INVALID_NAMED_TUPLE, fields_arg) { let mut diagnostic = builder.into_diagnostic( "Invalid argument to parameter `fields` of `NamedTuple()`", ); - diagnostic.set_primary_message( + diagnostic.set_primary_annotation_message( "Each element in `fields` must be a length-2 tuple or list", ); } @@ -637,10 +652,18 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let declared_type = self.infer_type_expression(declaration_expr); let element_type = match field_spec_kind { - SequenceKind::Tuple => Type::heterogeneous_tuple(db, [name_type, declared_type]), + SequenceKind::Tuple => { + Type::heterogeneous_tuple(db, env, [name_type, declared_type]) + } SequenceKind::List => KnownClass::List.to_specialized_instance( db, - &[UnionType::from_two_elements(db, name_type, declared_type)], + env, + &[UnionType::from_two_elements( + db, + env, + name_type, + declared_type, + )], ), }; @@ -652,19 +675,22 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } match field_arg_kind { SequenceKind::List => { - self.store_expression_type(fields_arg, KnownClass::List.to_instance(db)); + self.store_expression_type( + fields_arg, + KnownClass::List.to_instance(db, env), + ); } SequenceKind::Tuple => self.store_expression_type( fields_arg, - Type::homogeneous_tuple(db, Type::unknown()), + Type::homogeneous_tuple(db, env, Type::unknown()), ), } if let Some(builder) = self.context.report_lint(&INVALID_NAMED_TUPLE, name_expr) { let mut diagnostic = builder.into_diagnostic("Invalid `NamedTuple` field name definition"); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Expected a string literal for the field name, found `{}`", - name_type.display(db) + name_type.display(db, env) )); } return NamedTupleSpec::unknown(db); @@ -707,7 +733,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut diagnostic = builder.into_diagnostic(format_args!( "Duplicate field name `{field_name}` in `{kind}()`" )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Field `{field_name}` already defined; will raise `ValueError` at runtime" )); } @@ -718,21 +744,21 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut diagnostic = builder.into_diagnostic(format_args!( "Field name `{field_name}` in `{kind}()` cannot start with an underscore" )); - diagnostic.set_primary_message("Will raise `ValueError` at runtime"); + diagnostic.set_primary_annotation_message("Will raise `ValueError` at runtime"); } else if is_keyword(field_name) && let Some(builder) = self.context.report_lint(&INVALID_NAMED_TUPLE, fields_arg) { let mut diagnostic = builder.into_diagnostic(format_args!( "Field name `{field_name}` in `{kind}()` cannot be a Python keyword" )); - diagnostic.set_primary_message("Will raise `ValueError` at runtime"); + diagnostic.set_primary_annotation_message("Will raise `ValueError` at runtime"); } else if !is_identifier(field_name) && let Some(builder) = self.context.report_lint(&INVALID_NAMED_TUPLE, fields_arg) { let mut diagnostic = builder.into_diagnostic(format_args!( "Field name `{field_name}` in `{kind}()` is not a valid identifier" )); - diagnostic.set_primary_message("Will raise `ValueError` at runtime"); + diagnostic.set_primary_annotation_message("Will raise `ValueError` at runtime"); } } } diff --git a/crates/ty_python_semantic/src/types/infer/builder/new_class.rs b/crates/ty_python_semantic/src/types/infer/builder/new_class.rs index efdfecfe08..ab3ae0b03e 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/new_class.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/new_class.rs @@ -27,6 +27,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { call_expr: &ast::ExprCall, definition: Option>, ) -> Type<'db> { + let env = self.program_environment(); let db = self.db(); let ast::Arguments { @@ -74,15 +75,15 @@ impl<'db> TypeInferenceBuilder<'db, '_> { literal.value(db) } else { if let Some(name_node) = name_node - && !name_type.is_assignable_to(db, KnownClass::Str.to_instance(db)) + && !name_type.is_assignable_to(db, env, KnownClass::Str.to_instance(db, env)) && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, name_node) { let mut diagnostic = builder.into_diagnostic( "Invalid argument to parameter 1 (`name`) of `types.new_class()`", ); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Expected `str`, found `{}`", - name_type.display(db) + name_type.display(db, env) )); } "" @@ -189,9 +190,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> { call_expr.into(), dynamic_class.name(db), metaclass1, - base1.display(db), + base1.display(db, env), metaclass2, - base2.display(db), + base2.display(db, env), ); } } @@ -215,7 +216,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }; // Get the already-inferred class type from the initial pass. - let inferred_type = definition_expression_type(db, definition, call_expr); + let inferred_type = definition_expression_type(self.db(), definition, call_expr); let Type::ClassLiteral(ClassLiteral::Dynamic(dynamic_class)) = inferred_type else { return; }; @@ -256,20 +257,23 @@ impl<'db> TypeInferenceBuilder<'db, '_> { definition: Option>, ) { let db = self.db(); + let env = self.program_environment(); let callable_type = self.expression_type(call_expr.func.as_ref()); - let iterable_object = KnownClass::Iterable.to_specialized_instance(db, &[Type::object()]); + let iterable_object = + KnownClass::Iterable.to_specialized_instance(db, env, &[Type::object()]); let mut call_arguments = self.prepare_call_arguments(&call_expr.arguments); - let mut bindings = callable_type - .bindings(db) - .match_parameters(db, &call_arguments); + let mut bindings = + callable_type + .bindings(db, env) + .match_parameters(db, env, &call_arguments); let bindings_result = self.infer_and_check_argument_types( ArgumentsIter::from_ast(&call_expr.arguments), &mut call_arguments, &mut |builder, (_, expr, tcx)| { if name_node.is_some_and(|name| std::ptr::eq(expr, name)) { let _ = builder.infer_expression(expr, tcx); - KnownClass::Str.to_instance(builder.db()) + KnownClass::Str.to_instance(db, env) } else if bases_arg.is_some_and(|bases| std::ptr::eq(expr, bases)) { if definition.is_none() { let _ = builder.infer_expression(expr, tcx); diff --git a/crates/ty_python_semantic/src/types/infer/builder/paramspec_validation.rs b/crates/ty_python_semantic/src/types/infer/builder/paramspec_validation.rs index b84b666882..9c2fff7dca 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/paramspec_validation.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/paramspec_validation.rs @@ -1,6 +1,15 @@ -use crate::types::{ParamSpecAttrKind, Type, context::InferContext, diagnostic::INVALID_PARAMSPEC}; +use crate::{ + FxOrderSet, + types::{ + ParamSpecAttrKind, Type, + context::InferContext, + diagnostic::{INVALID_PARAMSPEC, UNBOUND_TYPE_VARIABLE}, + generics::resolve_typevar_reference, + }, +}; use ruff_python_ast as ast; use ruff_text_size::Ranged; +use ty_python_core::SemanticIndex; /// How a parameter pack's positional or keyword half is written in `context`'s file: /// `*args: *P` in basedpython, `*args: P.args` in python. @@ -27,13 +36,16 @@ fn component( /// This enforces several rules from the typing spec: /// - `P.args` and `P.kwargs` must always be used together /// - When `*args: P.args` is present, `**kwargs: P.kwargs` must also be present (same P) +/// - `P` must already be in scope /// - No keyword-only parameters are allowed between `*args: P.args` and `**kwargs: P.kwargs` pub(super) fn validate_paramspec_components<'db>( context: &'db InferContext<'db, '_>, + index: &SemanticIndex<'db>, parameters: &ast::Parameters, infer_type: impl Fn(&ast::Expr) -> Type<'db>, ) { let db = context.db(); + let env = context.program_environment(); // Extract ParamSpec info from *args annotation let args_paramspec = parameters.vararg.as_deref().and_then(|vararg| { @@ -68,7 +80,7 @@ pub(super) fn validate_paramspec_components<'db>( match (args_paramspec, kwargs_paramspec) { // Both *args: P.args and **kwargs: P.kwargs present - (Some((args_tv, _args_annotation)), Some((kwargs_tv, kwargs_annotation))) => { + (Some((args_tv, args_annotation)), Some((kwargs_tv, kwargs_annotation))) => { // Check they refer to the same ParamSpec if !args_tv.is_same_typevar_as(db, kwargs_tv) { let name = args_tv.name(db); @@ -82,6 +94,48 @@ pub(super) fn validate_paramspec_components<'db>( )); } } else { + let paramspec_is_bound_by_parameter = parameters + .iter() + .filter_map(ast::AnyParameterRef::annotation) + .map(&infer_type) + .filter(|ty| { + !matches!( + ty, + Type::TypeVar(typevar) + if typevar.is_paramspec(db) + && typevar.paramspec_attr(db).is_some() + ) + }) + .any(|ty| { + let mut typevars = FxOrderSet::default(); + ty.find_legacy_typevars(db, env, None, &mut typevars); + typevars + .iter() + .any(|typevar| typevar.is_same_typevar_as(db, args_tv)) + }); + let paramspec_is_in_scope = paramspec_is_bound_by_parameter + || index + .scope(context.scope().file_scope_id(db)) + .parent() + .is_some_and(|parent_scope| { + resolve_typevar_reference(db, index, parent_scope, args_tv.typevar(db)) + .is_some() + }); + if !paramspec_is_in_scope + && let Some(builder) = + context.report_lint(&UNBOUND_TYPE_VARIABLE, args_annotation) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "ParamSpec `{}` is not in scope", + args_tv.name(db), + )); + diagnostic.annotate( + context + .secondary(kwargs_annotation) + .message("This component uses the same out-of-scope ParamSpec"), + ); + } + // Same ParamSpec - check no keyword-only params between them if !parameters.kwonlyargs.is_empty() { let name = args_tv.name(db); diff --git a/crates/ty_python_semantic/src/types/infer/builder/post_inference/dynamic_class.rs b/crates/ty_python_semantic/src/types/infer/builder/post_inference/dynamic_class.rs index 292d4e424f..7ebd5b17cd 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/post_inference/dynamic_class.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/post_inference/dynamic_class.rs @@ -24,7 +24,7 @@ pub(crate) fn check_dynamic_class_definition<'db>( return; }; - let ty = binding_type(db, definition); + let ty = binding_type(context.db(), definition); // Check if it's a dynamic class with a Definition anchor. let Type::ClassLiteral(ClassLiteral::Dynamic(dynamic_class)) = ty else { @@ -46,6 +46,8 @@ pub(crate) fn check_dynamic_class_definition<'db>( return; }; + let env = context.program_environment(); + // Check for MRO errors. if report_dynamic_mro_errors(context, dynamic_class, call_expr, bases) { report_inconsistent_dynamic_generic_bases(context, dynamic_class, bases); @@ -54,7 +56,11 @@ pub(crate) fn check_dynamic_class_definition<'db>( let mut disjoint_bases = IncompatibleBases::default(); let bases_tuple_elts = bases.as_tuple_expr().map(|tuple| tuple.elts.as_slice()); - for (idx, base_type) in dynamic_class.explicit_bases(db).iter().enumerate() { + for (idx, base_type) in dynamic_class + .explicit_bases(context.db()) + .iter() + .enumerate() + { // Convert to ClassType to access nearest_disjoint_base. if let Some(class_type) = base_type.to_class_type(db) && let Some(disjoint_base) = class_type.nearest_disjoint_base(db) @@ -87,9 +93,9 @@ pub(crate) fn check_dynamic_class_definition<'db>( call_expr.into(), dynamic_class.name(db), metaclass1, - base1.display(db), + base1.display(db, env), metaclass2, - base2.display(db), + base2.display(db, env), ); } } diff --git a/crates/ty_python_semantic/src/types/infer/builder/post_inference/final_variable.rs b/crates/ty_python_semantic/src/types/infer/builder/post_inference/final_variable.rs index f1ad602a35..a3ef01a8ea 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/post_inference/final_variable.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/post_inference/final_variable.rs @@ -27,9 +27,10 @@ pub(crate) fn check_final_without_value<'db>( let use_def = index.use_def_map(file_scope_id); let place_table = index.place_table(file_scope_id); + let env = context.program_environment(); for (symbol_id, declarations) in use_def.all_end_of_scope_symbol_declarations() { - let result = place_from_declarations(db, declarations); + let result = place_from_declarations(db, env, declarations); let first_declaration = result.first_declaration; let (place_and_quals, _) = result.into_place_and_conflicting_declarations(); @@ -46,7 +47,7 @@ pub(crate) fn check_final_without_value<'db>( // Check if the symbol has any bindings in the current scope. let bindings = use_def.end_of_scope_symbol_bindings(symbol_id); - let binding_place = place_from_bindings(db, bindings); + let binding_place = place_from_bindings(db, env, bindings); if !binding_place.place.is_undefined() { continue; diff --git a/crates/ty_python_semantic/src/types/infer/builder/post_inference/function.rs b/crates/ty_python_semantic/src/types/infer/builder/post_inference/function.rs index e077088c70..8a88629cfe 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/post_inference/function.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/post_inference/function.rs @@ -31,7 +31,8 @@ pub(crate) fn check_function_definition<'db>( ) { let db = context.db(); - let Some(function_type) = infer_definition_types(db, definition).function_type(definition) + let Some(function_type) = + infer_definition_types(context.db(), definition).function_type(definition) else { return; }; @@ -59,10 +60,10 @@ fn check_pep695_function_legacy_typevars<'db>( let Some(type_params) = node.type_params.as_deref() else { return; }; - + let env = context.program_environment(); let mut has_legacy_default = false; for default in type_params.iter().filter_map(ast::TypeParam::default) { - let Some(typevar) = find_over_type(db, file_expression_type(default), false, |ty| { + let Some(typevar) = find_over_type(db, env, file_expression_type(default), false, |ty| { if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = ty && matches!( typevar.kind(db), @@ -162,7 +163,7 @@ fn check_legacy_positional_only_convention<'db>( "Invalid use of the legacy convention \ for positional-only parameters", ); - diagnostic.set_primary_message( + diagnostic.set_primary_annotation_message( "Parameter name begins with `__` but will not be treated as positional-only", ); diagnostic.info( @@ -199,6 +200,8 @@ fn check_legacy_typevar_defaults<'db>( return; }; + let env = context.program_environment(); + let typevars = generic_context .variables(db) .map(|bound_tvar| bound_tvar.typevar(db)); @@ -216,11 +219,11 @@ fn check_legacy_typevar_defaults<'db>( continue; } - let Some(default_ty) = typevar.default_type(db) else { + let Some(default_ty) = typevar.default_type(db, env) else { continue; }; - let first_bad_tvar = find_over_type(db, default_ty, false, |t| { + let first_bad_tvar = find_over_type(db, env, default_ty, false, |t| { let tvar = match t { Type::TypeVar(tvar) => tvar.typevar(db), Type::KnownInstance(KnownInstanceType::TypeVar(tvar)) => tvar, @@ -253,7 +256,7 @@ fn check_legacy_typevar_defaults<'db>( )); if is_later_in_list { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Default of `{typevar_name}` references later type parameter `{}`", bad_typevar.name(db), )); @@ -263,7 +266,7 @@ fn check_legacy_typevar_defaults<'db>( bad_typevar.name(db) )); } else { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Default of `{typevar_name}` references out-of-scope type variable `{}`", bad_typevar.name(db), )); @@ -275,11 +278,11 @@ fn check_legacy_typevar_defaults<'db>( } if let Some(typevar_definition) = typevar.definition(db) { - let file = typevar_definition.file(db); diagnostic.annotate( - Annotation::secondary(Span::from( - typevar_definition.full_range(db, &parsed_module(db, file).load(db)), - )) + Annotation::secondary(Span::from(typevar_definition.full_range( + db, + &parsed_module(db, typevar_definition.python_file(db)).load(db), + ))) .message(format_args!("`{typevar_name}` defined here")), ); } @@ -295,13 +298,14 @@ fn find_typevar_annotation_range<'db>( file_expression_type: impl Fn(&ast::Expr) -> Type<'db>, ) -> TextRange { let db = context.db(); + let env = context.program_environment(); let typevar_id = typevar.identity(db); node.parameters .iter() .filter_map(ast::AnyParameterRef::annotation) .chain(node.returns.as_deref()) - .find(|ann| file_expression_type(ann).references_typevar(db, typevar_id)) + .find(|ann| file_expression_type(ann).references_typevar(db, env, typevar_id)) .map(Ranged::range) .unwrap_or_else(|| node.name.range()) } @@ -328,6 +332,8 @@ fn check_legacy_typevar_ordering<'db>( return; }; + let env = context.program_environment(); + let mut state: Option> = None; for bound_typevar in generic_context.variables(db) { @@ -344,7 +350,7 @@ fn check_legacy_typevar_ordering<'db>( continue; } - let has_default = typevar.default_type(db).is_some(); + let has_default = typevar.default_type(db, env).is_some(); if let Some(state) = state.as_mut() { if !has_default { @@ -392,14 +398,14 @@ fn check_legacy_typevar_ordering<'db>( )); if let [single_typevar] = &*state.invalid_later_tvars { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Type variable `{}` does not have a default", single_typevar.name(db), )); } else { let later_typevars = format_enumeration(state.invalid_later_tvars.iter().map(|tv| tv.name(db))); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Type variables {later_typevars} do not have defaults", )); } @@ -419,10 +425,9 @@ fn check_legacy_typevar_ordering<'db>( let Some(definition) = tvar.definition(db) else { continue; }; - let file = definition.file(db); diagnostic.annotate( Annotation::secondary(Span::from( - definition.full_range(db, &parsed_module(db, file).load(db)), + definition.full_range(db, &parsed_module(db, definition.python_file(db)).load(db)), )) .message(format_args!("`{}` defined here", tvar.name(db))), ); diff --git a/crates/ty_python_semantic/src/types/infer/builder/post_inference/overloaded_function.rs b/crates/ty_python_semantic/src/types/infer/builder/post_inference/overloaded_function.rs index d0b6481bd9..563cbd8753 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/post_inference/overloaded_function.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/post_inference/overloaded_function.rs @@ -9,7 +9,7 @@ use crate::{ Db, place::{DefinedPlace, Definedness, Place, place_from_bindings}, types::{ - KnownClass, Type, + CallableType, KnownClass, Type, context::InferContext, diagnostic::INVALID_OVERLOAD, function::{FunctionDecorators, FunctionType, KnownFunction, OverloadLiteral}, @@ -46,6 +46,7 @@ pub(crate) fn check_overloaded_function<'db>( }; let db = context.db(); + let env = context.program_environment(); if function.file(db) != context.file() { // If the function is not in this file, we don't need to check it. @@ -67,6 +68,7 @@ pub(crate) fn check_overloaded_function<'db>( .. }) = place_from_bindings( db, + env, use_def.end_of_scope_symbol_bindings(place.as_symbol().unwrap()), ) .place @@ -101,8 +103,15 @@ pub(crate) fn check_overloaded_function<'db>( if let Some(implementation) = implementation && binding_decorator_inconsistencies.is_empty() + && context.is_lint_enabled(&INVALID_OVERLOAD) { - check_non_generic_overload_implementation_consistency(context, overloads, implementation); + let implementation_callables = function.implementation_callables(db); + check_non_generic_overload_implementation_consistency( + context, + overloads, + implementation, + &implementation_callables, + ); } // Check that the overloaded function has at least two overloads @@ -113,9 +122,9 @@ pub(crate) fn check_overloaded_function<'db>( "Overloaded function `{}` requires at least two overloads", function_node.name )); - diagnostic.set_primary_message("Only one overload defined here"); + diagnostic.set_primary_annotation_message("Only one overload defined here"); if let Some(decorator) = - single_overload.find_known_decorator_span(db, KnownFunction::Overload) + single_overload.find_known_decorator_span(context.db(), KnownFunction::Overload) { diagnostic.annotate(Annotation::secondary(decorator)); } @@ -142,11 +151,15 @@ pub(crate) fn check_overloaded_function<'db>( ) { if class.is_protocol(db) - || (Type::ClassLiteral(class) - .is_subtype_of(db, KnownClass::ABCMeta.to_instance(db)) - && overloads.iter().all(|overload| { - overload.has_known_decorator(db, FunctionDecorators::ABSTRACT_METHOD) - })) + || ({ + Type::ClassLiteral(class).is_subtype_of( + db, + env, + KnownClass::ABCMeta.to_instance(db, env), + ) + } && overloads.iter().all(|overload| { + overload.has_known_decorator(db, FunctionDecorators::ABSTRACT_METHOD) + })) { implementation_required = false; } @@ -192,7 +205,7 @@ pub(crate) fn check_overloaded_function<'db>( .message(format_args!("Missing here")), ); if let Some(decorator) = - function.find_known_decorator_span(db, KnownFunction::Overload) + function.find_known_decorator_span(context.db(), KnownFunction::Overload) { diagnostic.annotate(Annotation::secondary(decorator)); } @@ -220,7 +233,8 @@ pub(crate) fn check_overloaded_function<'db>( name = known_function.name() )); for known_function in [known_function, KnownFunction::Overload] { - if let Some(decorator) = overload.find_known_decorator_span(db, known_function) + if let Some(decorator) = + overload.find_known_decorator_span(context.db(), known_function) { diagnostic.annotate(Annotation::secondary(decorator)); } @@ -250,11 +264,13 @@ pub(crate) fn check_overloaded_function<'db>( first overload", name = known_function.name() )); - if let Some(decorator) = overload.find_known_decorator_span(db, known_function) { + if let Some(decorator) = + overload.find_known_decorator_span(context.db(), known_function) + { diagnostic.annotate(Annotation::secondary(decorator)); } let file = function.file(db); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, first_overload.python_file(db)).load(db); let node = first_overload.node(db, file, &module); let span = if node.body.len() == 1 { Span::from(file).with_range(node.range()) @@ -272,15 +288,29 @@ pub(crate) fn check_overloaded_function<'db>( /// Check non-generic overload signatures against their implementation. /// -/// This is the first, deliberately narrow pass at overload implementation consistency. It reports -/// only when the overloads and implementation are all non-generic; generic signatures require -/// careful treatment of type-variable domains. +/// This is the first, deliberately narrow pass at overload implementation consistency. Signature +/// compatibility is checked only when the overloads and implementation are all non-generic; +/// generic signatures require careful treatment of type-variable domains. Each callable +/// alternative of the implementation must contain a signature consistent with each overload. fn check_non_generic_overload_implementation_consistency<'db>( context: &InferContext<'db, '_>, overloads: &'db [OverloadLiteral<'db>], implementation: OverloadLiteral<'db>, + implementation_callables: &[CallableType<'db>], ) { - if !context.is_lint_enabled(&INVALID_OVERLOAD) { + let db = context.db(); + let env = context.program_environment(); + if implementation_callables.is_empty() + || implementation_callables + .iter() + .any(|callable| callable.signatures(db).overloads.is_empty()) + { + let function_node = implementation.node(db, context.file(), context.module()); + if let Some(builder) = context.report_lint(&INVALID_OVERLOAD, &function_node.name) { + builder.into_diagnostic(format_args!( + "Overload implementation is not callable after applying decorators" + )); + } return; } @@ -292,12 +322,13 @@ fn check_non_generic_overload_implementation_consistency<'db>( // basedpython: an unannotated parameter's hole is erased first — it makes the signature // generic without there being any type-variable domain to reason about, and skipping on it // would silently retire this check for every unannotated implementation - let Some(implementation_signature) = implementation + if implementation .signature(db) .without_inferred_parameter_holes(db) - else { + .is_none() + { return; - }; + } let Some(overload_signatures) = overloads .iter() @@ -315,10 +346,51 @@ fn check_non_generic_overload_implementation_consistency<'db>( for (overload, overload_signature) in overload_signatures { let function_node = overload.node(db, context.file(), context.module()); - let parameter_consistency = implementation_signature - .non_generic_implementation_parameters_consistency_with(db, &overload_signature); - let return_type_consistency = implementation_signature - .non_generic_implementation_return_type_consistency_with(db, &overload_signature); + let Some((implementation_signature, parameter_consistency, return_type_consistency)) = + implementation_callables.iter().find_map(|callable| { + let mut inconsistency = None; + for implementation_signature in &callable.signatures(db).overloads { + // basedpython: erase an unannotated parameter's hole before comparing — + // it makes the signature generic without there being any type-variable + // domain to reason about, and skipping on it would silently retire this + // check for every unannotated implementation + let Some(implementation_signature) = + implementation_signature.without_inferred_parameter_holes(db) + else { + continue; + }; + let parameter_consistency = implementation_signature + .non_generic_implementation_parameters_consistency_with( + db, + env, + &overload_signature, + ); + let return_type_consistency = implementation_signature + .non_generic_implementation_return_type_consistency_with( + db, + env, + &overload_signature, + ); + if matches!( + (¶meter_consistency, &return_type_consistency), + ( + ParameterConsistency::Consistent, + ReturnTypeConsistency::Consistent + ) + ) { + return None; + } + inconsistency = Some(( + implementation_signature, + parameter_consistency, + return_type_consistency, + )); + } + inconsistency + }) + else { + continue; + }; let (parameter_error_context, return_type_error_context, message) = match (parameter_consistency, return_type_consistency) { @@ -356,18 +428,18 @@ fn check_non_generic_overload_implementation_consistency<'db>( if let Some(error_context) = parameter_error_context { diagnostic.info(format_args!( "Implementation signature `{}` is not assignable to overload signature `{}`", - implementation_signature.display(db), - overload_signature.display(db), + implementation_signature.display(db, env), + overload_signature.display(db, env), )); - error_context.attach_to(db, &mut diagnostic); + error_context.attach_to(db, env, &mut diagnostic); } if let Some(error_context) = return_type_error_context { diagnostic.info(format_args!( "Overload returns `{}`, which is not assignable to implementation return type `{}`", - overload_signature.return_ty.display(db), - implementation_signature.return_ty.display(db), + overload_signature.return_ty.display(db, env), + implementation_signature.return_ty.display(db, env), )); - error_context.attach_to(db, &mut diagnostic); + error_context.attach_to(db, env, &mut diagnostic); } diagnostic.annotate( context diff --git a/crates/ty_python_semantic/src/types/infer/builder/post_inference/static_class.rs b/crates/ty_python_semantic/src/types/infer/builder/post_inference/static_class.rs index 3b2ef9c423..fcc4eaf4ba 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/post_inference/static_class.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/post_inference/static_class.rs @@ -1,3 +1,4 @@ +use crate::Db; use itertools::Itertools; use ruff_db::{ diagnostic::{Annotation, SubDiagnostic, SubDiagnosticSeverity}, @@ -9,7 +10,7 @@ use ruff_text_size::{Ranged, TextRange, TextSize}; use rustc_hash::FxHashSet; use crate::{ - Db, Program, TypeQualifiers, + TypeQualifiers, diagnostic::format_enumeration, place::{DefinedPlace, Place, TypeOrigin, place_from_bindings, place_from_declarations}, types::{ @@ -18,7 +19,7 @@ use crate::{ SpecialFormType, StaticClassLiteral, Type, TypeVarVariance, TypedDictModule, binding_type, call::Argument, class::{ - AbstractMethod, CodeGeneratorKind, FieldKind, MetaclassErrorKind, + AbstractMethod, CodeGeneratorKind, Field, FieldKind, MetaclassErrorKind, expanded_class_base_entries, }, conformance, @@ -34,10 +35,10 @@ use crate::{ UNKNOWN_ARGUMENT, report_bad_frozen_dataclass_inheritance, report_conflicting_metaclass_from_bases, report_duplicate_bases, report_inconsistent_generic_bases, report_instance_layout_conflict, - report_invalid_attribute_assignment, report_invalid_or_unsupported_base, - report_invalid_total_ordering, report_invalid_type_param_order, - report_invalid_typevar_default_reference, report_missing_type_arguments, - report_named_tuple_field_with_leading_underscore, + report_invalid_attribute_assignment, report_invalid_named_tuple_field_qualifier, + report_invalid_or_unsupported_base, report_invalid_total_ordering, + report_invalid_type_param_order, report_invalid_typevar_default_reference, + report_missing_type_arguments, report_named_tuple_field_with_leading_underscore, report_namedtuple_field_without_default_after_field_with_default, report_shadowed_type_variable, report_subclass_of_class_with_non_callable_init_subclass, report_unsupported_base, @@ -50,6 +51,7 @@ use crate::{ infer_definition_types, mro::StaticMroErrorKind, overrides, + special_form::TypeQualifier, tuple::Tuple, typevar::{TypeVarInstance, TypeVarKind}, variance::VarianceInferable, @@ -104,7 +106,7 @@ pub(crate) fn check_static_class_definitions<'db>( crate::types::conversions::validate_conversion_dunders(context, class, class_node); // Check that the class does not have a cyclic definition - if let Some(inheritance_cycle) = class.inheritance_cycle(db) { + if let Some(inheritance_cycle) = class.inheritance_cycle(context.db()) { if inheritance_cycle.is_participant() && let Some(builder) = context.report_lint(&CYCLIC_CLASS_DEFINITION, class_node) { @@ -119,8 +121,10 @@ pub(crate) fn check_static_class_definitions<'db>( return; } + let env = context.program_environment(); + // Check that the class is not an enum and generic - if is_enum_class_by_inheritance(db, class) && class.generic_context(db).is_some() { + if is_enum_class_by_inheritance(db, env, class) && class.generic_context(db).is_some() { if let Some(builder) = context.report_lint(&INVALID_GENERIC_ENUM, class_node) { builder.into_diagnostic(format_args!( "Enum class `{}` cannot be generic", @@ -134,6 +138,27 @@ pub(crate) fn check_static_class_definitions<'db>( // If it's a `NamedTuple` class, check that no field without a default value // appears after a field with a default value. if class_kind == Some(CodeGeneratorKind::NamedTuple) { + // `ClassVar` and `Final` fields have to be checked against the class body's annotations + // rather than against `own_fields`, since `own_fields` drops `ClassVar` declarations and + // does not retain the `Final` qualifier for the fields that it does keep. + // + // A field carrying both qualifiers is reported once per qualifier, since each qualifier + // independently violates the restriction on `NamedTuple` fields. + for (field_name, qualifiers, declaration) in class.own_annotated_qualifiers(db) { + let invalid_qualifiers = [TypeQualifier::ClassVar, TypeQualifier::Final] + .into_iter() + .filter(|qualifier| qualifiers.contains(TypeQualifiers::from(*qualifier))); + + for qualifier in invalid_qualifiers { + report_invalid_named_tuple_field_qualifier( + context, + &field_name, + qualifier, + declaration, + ); + } + } + let mut field_with_default_encountered = None; for (field_name, field) in class.own_fields(db, None, CodeGeneratorKind::NamedTuple) { @@ -211,7 +236,7 @@ pub(crate) fn check_static_class_definitions<'db>( "An exception will often be raised when instantiating the class at runtime", ); } - } else if is_enum_class_by_inheritance(db, class) { + } else if is_enum_class_by_inheritance(db, env, class) { if let Some(builder) = context.report_lint(&INVALID_DATACLASS, class.header_range(db)) { let mut diagnostic = builder.into_diagnostic(format_args!( "Enum class `{}` cannot be decorated with `@dataclass`", @@ -338,7 +363,7 @@ pub(crate) fn check_static_class_definitions<'db>( return None; } let required_variance = - base_alias.variance_of(db, typevar.identity(db)); + base_alias.variance_of(db, env, typevar.identity(db)); if declared_variance.join(required_variance) != declared_variance { Some((typevar, declared_variance, required_variance)) } else { @@ -389,7 +414,7 @@ pub(crate) fn check_static_class_definitions<'db>( "TypedDict class `{}` can only inherit from TypedDict classes", class.name(db), )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "`{}` is not a `TypedDict` class", base_class.name(db) )); @@ -418,9 +443,15 @@ pub(crate) fn check_static_class_definitions<'db>( // emit diagnostics for), so subclassing a sealed base that lives in a // non-first-party file (a dependency) is forbidden. if base_class.is_sealed(db) - && !file_to_module(db, base_class.class_literal(db).file(db)) - .and_then(|module| module.search_path(db)) - .is_some_and(SearchPath::is_first_party) + && !file_to_module( + db, + base_class + .class_literal(db) + .program_file(db) + .resolver_file(db), + ) + .and_then(|module| module.search_path(db)) + .is_some_and(SearchPath::is_first_party) && let Some(builder) = context.report_lint(&SUBCLASS_OF_SEALED_CLASS, source_node) { builder.into_diagnostic(format_args!( @@ -471,7 +502,7 @@ pub(crate) fn check_static_class_definitions<'db>( for base in class_node.bases() { if let ast::Expr::Starred(starred) = base && let starred_ty = definition_expression_type(db, class_definition, &starred.value) - && let Some(tuple_spec) = starred_ty.tuple_instance_spec(db) + && let Some(tuple_spec) = starred_ty.tuple_instance_spec(db, env) && !matches!(tuple_spec.as_ref(), Tuple::Fixed(_)) { report_unsupported_base(context, base, starred_ty, class); @@ -504,7 +535,10 @@ pub(crate) fn check_static_class_definitions<'db>( "Cannot create a consistent method resolution order (MRO) \ for class `{}` with bases list `[{}]`", class.name(db), - bases_list.iter().map(|base| base.display(db)).join(", ") + bases_list + .iter() + .map(|base| base.display(db, env)) + .join(", ") )); let can_rewrite_bases = bases_list.len() == class_node.bases().len() && !class_node.bases().iter().any(ast::Expr::is_starred_expr); @@ -562,7 +596,7 @@ pub(crate) fn check_static_class_definitions<'db>( ); } - let explicit_bases = class.explicit_bases(db); + let explicit_bases = class.explicit_bases(context.db()); let base_nodes = (class_node.bases().len() == explicit_bases.len() && !class_node.bases().iter().any(ast::Expr::is_starred_expr)) .then_some(class_node.bases()); @@ -615,7 +649,7 @@ pub(crate) fn check_static_class_definitions<'db>( { builder.into_diagnostic(format_args!( "Metaclass type `{}` is not callable", - ty.display(db) + ty.display(db, env) )); } } @@ -625,7 +659,7 @@ pub(crate) fn check_static_class_definitions<'db>( { builder.into_diagnostic(format_args!( "Metaclass type `{}` is partly not callable", - ty.display(db) + ty.display(db, env) )); } } @@ -677,7 +711,7 @@ pub(crate) fn check_static_class_definitions<'db>( if class_kind == Some(CodeGeneratorKind::TypedDict) { let supports_pep_728 = context.in_stub() || class.typed_dict_module(db) == Some(TypedDictModule::TypingExtensions) - || Program::get(db).python_version(db) >= PythonVersion::PY315; + || env.python_version(db) >= PythonVersion::PY315; for keyword in &args.keywords { if !supports_pep_728 @@ -700,9 +734,9 @@ pub(crate) fn check_static_class_definitions<'db>( "Invalid argument to parameter `{arg_name}` \ in `TypedDict` definition", )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Expected either `True` or `False`, got object of type `{}`", - passed_type.display(db) + passed_type.display(db, env) )); } } @@ -759,6 +793,7 @@ pub(crate) fn check_static_class_definitions<'db>( let init_subclass_type = class .class_member_from_mro( db, + env, "__init_subclass__", MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, // skip(1) to skip the current class and only consider base classes. @@ -768,7 +803,7 @@ pub(crate) fn check_static_class_definitions<'db>( if let Some(init_subclass) = init_subclass_type { let call_args = call_args.with_self(Some(Type::from(class))); - if let Err(call_error) = init_subclass.try_call(db, &call_args) { + if let Err(call_error) = init_subclass.try_call(db, env, &call_args) { report_subclass_of_class_with_non_callable_init_subclass( context, call_error, class, class_node, ); @@ -836,7 +871,7 @@ pub(crate) fn check_static_class_definitions<'db>( for bound_typevar in generic_context.variables(db) { let typevar = bound_typevar.typevar(db); - let has_default = typevar.default_type(db).is_some(); + let has_default = typevar.default_type(db, env).is_some(); if let Some(state) = state.as_mut() { if !has_default { @@ -874,11 +909,11 @@ pub(crate) fn check_static_class_definitions<'db>( // `variables` should be fairly cheap to clone; it's just several cheap wrappers around // a `std::slice::Iter` under the hood. for (i, typevar) in typevars.clone().enumerate() { - let Some(default_ty) = typevar.default_type(db) else { + let Some(default_ty) = typevar.default_type(db, env) else { continue; }; - let first_bad_tvar = find_over_type(db, default_ty, false, |t| { + let first_bad_tvar = find_over_type(db, env, default_ty, false, |t| { let tvar = match t { Type::TypeVar(tvar) => tvar.typevar(db), Type::KnownInstance(KnownInstanceType::TypeVar(tvar)) => tvar, @@ -964,17 +999,16 @@ pub(crate) fn check_static_class_definitions<'db>( { let specialization = None; let class_init = class.has_dataclass_param(db, field_policy, DataclassFlags::INIT); + let own_fields = class.own_fields(db, specialization, field_policy); - let mut kw_only_sentinel_fields = vec![]; - let mut required_after_default_field_names = vec![]; - let mut has_seen_default_field = false; - - for (name, field) in class.own_fields(db, specialization, field_policy) { - if field.is_kw_only_sentinel(db) { - kw_only_sentinel_fields.push(name); - continue; - } + let kw_only_sentinel_fields: Vec<_> = own_fields + .iter() + .filter_map(|(name, field)| field.is_kw_only_sentinel(db).then_some(name)) + .collect(); + let mut field_order_violations = vec![]; + let mut previous_default_field = None; + for (name, field) in class.fields(db, specialization, field_policy) { // Extract dataclass field properties let FieldKind::Dataclass { default_ty, @@ -992,9 +1026,9 @@ pub(crate) fn check_static_class_definitions<'db>( } if default_ty.is_some() { - has_seen_default_field = true; - } else if has_seen_default_field { - required_after_default_field_names.push(name); + previous_default_field = Some((name, field)); + } else if let Some((default_name, default_field)) = previous_default_field { + field_order_violations.push((default_name, default_field, name, field)); } } @@ -1015,36 +1049,52 @@ pub(crate) fn check_static_class_definitions<'db>( } } - if !required_after_default_field_names.is_empty() { - // Report field ordering violations + if !field_order_violations.is_empty() { let body_scope = class.body_scope(db).file_scope_id(db); let use_def_map = index.use_def_map(body_scope); let place_table = index.place_table(body_scope); - for name in required_after_default_field_names { - let Some(symbol_id) = place_table.symbol_id(name.as_str()) else { - continue; - }; - for decl_with_constraints in use_def_map.end_of_scope_symbol_declarations(symbol_id) + for (default_name, default_field, name, field) in field_order_violations { + if !own_fields.contains_key(default_name) + && !own_fields.contains_key(name) + && has_inherited_dataclass_field_order_violation( + db, + class, + default_name, + default_field, + name, + field, + ) { - let Some(definition) = decl_with_constraints.declaration.definition() else { - continue; - }; - let DefinitionKind::AnnotatedAssignment(ann_assign) = definition.kind(db) - else { - continue; - }; - let Some(builder) = context - .report_lint(&DATACLASS_FIELD_ORDER, ann_assign.target(context.module())) - else { - continue; + continue; + } + + let report = |range: TextRange| { + let Some(builder) = context.report_lint(&DATACLASS_FIELD_ORDER, range) else { + return false; }; builder.into_diagnostic(format_args!( - "Required field `{name}` cannot be defined \ - after fields with default values", + "Required field `{name}` cannot be defined after fields with default values", )); + true + }; - break; + if !own_fields.contains_key(name) { + report(class_node.name.range()); + continue; + } + + let Some(symbol_id) = place_table.symbol_id(name.as_str()) else { + continue; + }; + for decl_with_constraints in use_def_map.end_of_scope_symbol_declarations(symbol_id) + { + if let Some(definition) = decl_with_constraints.declaration.definition() + && let DefinitionKind::AnnotatedAssignment(ann_assign) = definition.kind(db) + && report(ann_assign.target(context.module()).range()) + { + break; + } } } } @@ -1140,6 +1190,64 @@ fn check_declared_variance_usage<'db>( } } +/// Returns whether the same default-before-required field pair already violates an ancestor's +/// generated constructor ordering. +/// +/// ```python +/// from dataclasses import dataclass +/// +/// @dataclass +/// class Base: +/// optional: int = 1 +/// required: int +/// +/// @dataclass +/// class Child(Base): +/// pass +/// ``` +/// +/// `Child` inherits the existing error and should not report it again. Comparing declaration +/// provenance preserves diagnostics when a subclass redeclares either field. +fn has_inherited_dataclass_field_order_violation<'db>( + db: &'db dyn Db, + class: StaticClassLiteral<'db>, + default_name: &Name, + default_field: &Field<'db>, + required_name: &Name, + required_field: &Field<'db>, +) -> bool { + class + .iter_mro(db, None) + .skip(1) + .filter_map(ClassBase::into_class) + .filter_map(|ancestor| ancestor.static_class_literal(db)) + .any(|(ancestor, specialization)| { + let Some(field_policy @ CodeGeneratorKind::DataclassLike(_)) = + CodeGeneratorKind::from_class(db, ancestor.into()) + else { + return false; + }; + if !ancestor.has_dataclass_param(db, field_policy, DataclassFlags::INIT) { + return false; + } + + let fields = ancestor.fields(db, specialization, field_policy); + let Some((default_index, _, inherited_default_field)) = fields.get_full(default_name) + else { + return false; + }; + let Some((required_index, _, inherited_required_field)) = + fields.get_full(required_name) + else { + return false; + }; + + default_index < required_index + && inherited_default_field.first_declaration == default_field.first_declaration + && inherited_required_field.first_declaration == required_field.first_declaration + }) +} + /// Check compatibility between class namespace values and attributes populated by its metaclass. /// /// A binding in a class body is passed through the namespace used to construct the class object @@ -1154,12 +1262,13 @@ fn check_class_namespace_against_metaclass_members<'db>( index: &SemanticIndex<'db>, ) { let db = context.db(); + let env = context.program_environment(); let metaclass = class.metaclass(db); - if metaclass == KnownClass::Type.to_class_literal(db) { + if metaclass == KnownClass::Type.to_class_literal(db, env) { return; } - let Some(metaclass_instance) = metaclass.to_instance_approximation(db) else { + let Some(metaclass_instance) = metaclass.to_instance_approximation(db, env) else { return; }; @@ -1181,7 +1290,7 @@ fn check_class_namespace_against_metaclass_members<'db>( .filter_map(|class| class.static_class_literal(db).map(|(literal, _)| literal)) { let body_scope = metaclass.body_scope(db); - let metaclass_index = semantic_index(db, body_scope.file(db)); + let metaclass_index = semantic_index(db, body_scope.program_file(db)); let body_scope_id = body_scope.file_scope_id(db); let metaclass_table = metaclass_index.place_table(body_scope_id); let metaclass_use_def = metaclass_index.use_def_map(body_scope_id); @@ -1224,7 +1333,9 @@ fn check_class_namespace_against_metaclass_members<'db>( ty: metaclass_member_ty, origin, .. - }) = metaclass_instance.instance_member(db, name.as_str()).place + }) = metaclass_instance + .instance_member(db, env, name.as_str()) + .place else { continue; }; @@ -1241,7 +1352,7 @@ fn check_class_namespace_against_metaclass_members<'db>( } let assigned_ty = binding_type(db, definition); - if !assigned_ty.is_assignable_to(db, metaclass_member_ty) { + if !assigned_ty.is_assignable_to(db, env, metaclass_member_ty) { reported_incompatible_binding = true; report_invalid_attribute_assignment( context, @@ -1265,7 +1376,7 @@ fn check_class_namespace_against_metaclass_members<'db>( } let result = - place_from_declarations(db, use_def.end_of_scope_symbol_declarations(symbol_id)); + place_from_declarations(db, env, use_def.end_of_scope_symbol_declarations(symbol_id)); let Some(definition) = result.first_declaration else { continue; }; @@ -1282,7 +1393,7 @@ fn check_class_namespace_against_metaclass_members<'db>( if !matches!(definition_kind, DefinitionKind::AnnotatedAssignment(_)) { continue; } - if !metaclass_member_ty.is_assignable_to(db, class_declared_ty) { + if !metaclass_member_ty.is_assignable_to(db, env, class_declared_ty) { report_invalid_attribute_assignment( context, definition_kind.target_range(context.module()), @@ -1326,7 +1437,6 @@ fn check_final_class_abstract_methods<'db>( class_node: &ast::StmtClassDef, ) { let db = context.db(); - // Only check if the class is final. if !class.is_final(db) { return; @@ -1340,6 +1450,8 @@ fn check_final_class_abstract_methods<'db>( return; } + let env = context.program_environment(); + let class_type = class.identity_specialization(db); let abstract_methods = class_type.abstract_methods(db); @@ -1383,7 +1495,8 @@ fn check_final_class_abstract_methods<'db>( "Final class `{class_name}` has unimplemented abstract method \ `{first_method_name}`", )); - diagnostic.set_primary_message(format_args!("`{first_method_name}` is unimplemented")); + diagnostic + .set_primary_annotation_message(format_args!("`{first_method_name}` is unimplemented")); } else { let verbose = db.verbose(); let max_abstract_methods_to_print = if verbose { num_abstract_methods } else { 3 }; @@ -1391,7 +1504,7 @@ fn check_final_class_abstract_methods<'db>( format_enumeration(abstract_methods.keys().take(max_abstract_methods_to_print)); if num_abstract_methods > max_abstract_methods_to_print { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "{num_abstract_methods} abstract methods are unimplemented, \ including {formatted_methods}", )); @@ -1408,7 +1521,7 @@ fn check_final_class_abstract_methods<'db>( "Final class `{class_name}` has unimplemented \ abstract methods {formatted_methods}", )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Abstract methods {formatted_methods} are unimplemented" )); } @@ -1461,14 +1574,14 @@ fn check_final_class_abstract_methods<'db>( if kind.is_implicit_due_to_stub_body() && db.should_check_file(definition.file(db)) { let function_type_as_callable = infer_definition_types(db, *definition) .binding_type(*definition) - .try_upcast_to_callable(db); + .try_upcast_to_callable(db, env); if let Some(callables) = function_type_as_callable && Type::function_like_callable( db, - Signature::new(Parameters::gradual_form(), Type::none(db)), + Signature::new(Parameters::gradual_form(), Type::none(db, env)), ) - .is_assignable_to(db, callables.into_type(db)) + .is_assignable_to(db, env, callables.into_type(db, env)) { diagnostic.help(format_args!( "Change the body of `{first_method_name}` to `return` \ @@ -1492,6 +1605,7 @@ fn check_class_final_without_value<'db>( } let db = context.db(); + let env = context.program_environment(); let body_scope = class.body_scope(db); let body_scope_id = body_scope.file_scope_id(db); let use_def = index.use_def_map(body_scope_id); @@ -1506,7 +1620,7 @@ fn check_class_final_without_value<'db>( } for (symbol_id, declarations) in use_def.all_end_of_scope_symbol_declarations() { - let result = place_from_declarations(db, declarations); + let result = place_from_declarations(db, env, declarations); let first_declaration = result.first_declaration; let (place_and_quals, _) = result.into_place_and_conflicting_declarations(); @@ -1516,7 +1630,7 @@ fn check_class_final_without_value<'db>( // Check if the symbol has any bindings at class level. let bindings = use_def.end_of_scope_symbol_bindings(symbol_id); - let binding_place = place_from_bindings(db, bindings); + let binding_place = place_from_bindings(db, env, bindings); if !binding_place.place.is_undefined() { continue; diff --git a/crates/ty_python_semantic/src/types/infer/builder/post_inference/type_param_validation.rs b/crates/ty_python_semantic/src/types/infer/builder/post_inference/type_param_validation.rs index 75b3910de3..e11416ef88 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/post_inference/type_param_validation.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/post_inference/type_param_validation.rs @@ -49,7 +49,7 @@ pub(crate) fn check_single_typevar_tuple_pep695( "{owner_kind} `{owner_name}` cannot have multiple `TypeVarTuple` type parameters" )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "`{}` is an additional TypeVarTuple", typevar_tuple.name )); @@ -80,6 +80,7 @@ pub(crate) fn check_declared_alias_variance<'db>( alias: TypeAliasType<'db>, type_params: &ast::TypeParams, ) { + let env = context.program_environment(); let db = context.db(); let Some(generic_context) = alias.generic_context(db) else { return; @@ -95,7 +96,7 @@ pub(crate) fn check_declared_alias_variance<'db>( continue; }; - let required = alias.variance_of(db, bound_typevar.identity(db)); + let required = alias.variance_of(db, env, bound_typevar.identity(db)); if declared.join(required) == declared { continue; } @@ -169,7 +170,7 @@ pub(crate) fn check_no_default_after_typevar_tuple_pep695( typevar_tuple.name )); - diagnostic.set_primary_message(format_args!("`{single_name}` has a default")); + diagnostic.set_primary_annotation_message(format_args!("`{single_name}` has a default")); } else { let names = format_enumeration(params_with_defaults.iter().map(|p| p.name())); @@ -178,7 +179,7 @@ pub(crate) fn check_no_default_after_typevar_tuple_pep695( typevar_tuple.name )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "`{}` has a default", params_with_defaults[0].name() )); diff --git a/crates/ty_python_semantic/src/types/infer/builder/post_inference/typed_dict.rs b/crates/ty_python_semantic/src/types/infer/builder/post_inference/typed_dict.rs index ec861a367c..441d3753d2 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/post_inference/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/post_inference/typed_dict.rs @@ -1,8 +1,9 @@ +use crate::ProgramEnvironment; use ruff_db::{ diagnostic::{Annotation, Diagnostic, Span, SubDiagnostic, SubDiagnosticSeverity}, parsed::parsed_module, }; -use ruff_python_ast as ast; +use ruff_python_ast::{self as ast}; use ruff_text_size::Ranged; use rustc_hash::FxHashSet; @@ -102,6 +103,7 @@ fn validate_typed_dict_field_overrides<'db>( direct_bases: &[ClassType<'db>], ) { let db = context.db(); + let env = context.program_environment(); let child_fields = TypedDictType::new(class.identity_specialization(db)).items(db); let own_fields = class.own_fields(db, None, CodeGeneratorKind::TypedDict); let mut reported_fields = FxHashSet::default(); @@ -113,7 +115,7 @@ fn validate_typed_dict_field_overrides<'db>( }; let Some(reason) = - TypedDictFieldOverrideReason::from_fields(db, child_field, base_field) + TypedDictFieldOverrideReason::from_fields(db, env, child_field, base_field) else { continue; }; @@ -134,7 +136,7 @@ fn validate_typed_dict_field_overrides<'db>( context, class, field_name.as_str(), - reason, + &reason, base.name(db), base_field.first_declaration(), own_field_definition, @@ -151,6 +153,7 @@ fn validate_typed_dict_openness<'db>( direct_bases: &[ClassType<'db>], ) { let db = context.db(); + let env = context.program_environment(); let child = TypedDictType::new(class.identity_specialization(db)); let child_openness = child.openness(db); let child_items = child.items(db); @@ -217,17 +220,18 @@ fn validate_typed_dict_openness<'db>( } TypedDictOpenness::Closed => {} TypedDictOpenness::Extra(child_extra_items) => { - if !child_extra_items - .declared_ty - .is_assignable_to(db, base_extra_items.declared_ty) - { + if !child_extra_items.declared_ty.is_assignable_to( + db, + env, + base_extra_items.declared_ty, + ) { report_invalid_typed_dict_openness( context, class, format_args!( "Extra items type `{}` is not assignable to `{}` from base `{}`", - child_extra_items.declared_ty.display(db), - base_extra_items.declared_ty.display(db), + child_extra_items.declared_ty.display(db, env), + base_extra_items.declared_ty.display(db, env), base.name(db), ), ); @@ -238,17 +242,19 @@ fn validate_typed_dict_openness<'db>( if let Some((field_name, field)) = child_items.iter().find(|(field_name, field)| { !base_items.contains_key(*field_name) - && !field - .declared_ty - .is_assignable_to(db, base_extra_items.declared_ty) + && !field.declared_ty.is_assignable_to( + db, + env, + base_extra_items.declared_ty, + ) }) { report_invalid_typed_dict_openness( context, class, format_args!( "Item `{field_name}` of type `{}` is not assignable to extra items type `{}` from base `{}`", - field.declared_ty.display(db), - base_extra_items.declared_ty.display(db), + field.declared_ty.display(db, env), + base_extra_items.declared_ty.display(db, env), base.name(db), ), ); @@ -269,12 +275,16 @@ fn validate_typed_dict_openness<'db>( }; if child_extra_items.is_read_only() - || !child_extra_items - .declared_ty - .is_assignable_to(db, base_extra_items.declared_ty) - || !base_extra_items - .declared_ty - .is_assignable_to(db, child_extra_items.declared_ty) + || !child_extra_items.declared_ty.is_assignable_to( + db, + env, + base_extra_items.declared_ty, + ) + || !base_extra_items.declared_ty.is_assignable_to( + db, + env, + child_extra_items.declared_ty, + ) { report_invalid_typed_dict_openness( context, @@ -282,7 +292,7 @@ fn validate_typed_dict_openness<'db>( format_args!( "TypedDict `{}` must preserve mutable extra items type `{}` from base `{}`", class.name(db), - base_extra_items.declared_ty.display(db), + base_extra_items.declared_ty.display(db, env), base.name(db), ), ); @@ -293,19 +303,23 @@ fn validate_typed_dict_openness<'db>( !base_items.contains_key(*field_name) && (field.is_required() || field.is_read_only() - || !field - .declared_ty - .is_assignable_to(db, base_extra_items.declared_ty) - || !base_extra_items - .declared_ty - .is_assignable_to(db, field.declared_ty)) + || !field.declared_ty.is_assignable_to( + db, + env, + base_extra_items.declared_ty, + ) + || !base_extra_items.declared_ty.is_assignable_to( + db, + env, + field.declared_ty, + )) }) { report_invalid_typed_dict_openness( context, class, format_args!( "Item `{field_name}` must be mutable, not required, and consistent with extra items type `{}` from base `{}`", - base_extra_items.declared_ty.display(db), + base_extra_items.declared_ty.display(db, env), base.name(db), ), ); @@ -327,7 +341,7 @@ fn report_invalid_typed_dict_openness( } } -#[derive(Clone, Copy)] +#[derive(Clone)] enum TypedDictFieldOverrideReason<'db> { /// A required inherited field was relaxed to `NotRequired`. RequiredFieldMadeNotRequired, @@ -338,12 +352,14 @@ enum TypedDictFieldOverrideReason<'db> { /// A read-only inherited field's new type is not assignable to the base type. ReadOnlyTypeNotAssignable { db: &'db dyn Db, + env: ProgramEnvironment<'db>, child_ty: Type<'db>, base_ty: Type<'db>, }, /// A mutable inherited field's new type is not mutually assignable with the base type. MutableTypeIncompatible { db: &'db dyn Db, + env: ProgramEnvironment<'db>, child_ty: Type<'db>, base_ty: Type<'db>, }, @@ -372,23 +388,25 @@ impl std::fmt::Display for TypedDictFieldOverrideReason<'_> { } Self::ReadOnlyTypeNotAssignable { db, + env, child_ty, base_ty, } => write!( f, "Inherited read-only field type `{}` is not assignable from `{}`", - base_ty.display(*db), - child_ty.display(*db), + base_ty.display(*db, env), + child_ty.display(*db, env), ), Self::MutableTypeIncompatible { db, + env, child_ty, base_ty, } => write!( f, "Inherited mutable field type `{}` is incompatible with `{}`", - base_ty.display(*db), - child_ty.display(*db), + base_ty.display(*db, env), + child_ty.display(*db, env), ), } } @@ -397,6 +415,7 @@ impl std::fmt::Display for TypedDictFieldOverrideReason<'_> { impl<'db> TypedDictFieldOverrideReason<'db> { fn from_fields( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, child_field: &TypedDictField<'db>, base_field: &TypedDictField<'db>, ) -> Option { @@ -417,14 +436,14 @@ impl<'db> TypedDictFieldOverrideReason<'db> { let types_are_compatible = if base_field.is_read_only() { child_field .declared_ty - .is_assignable_to(db, base_field.declared_ty) + .is_assignable_to(db, env, base_field.declared_ty) } else { child_field .declared_ty - .is_assignable_to(db, base_field.declared_ty) + .is_assignable_to(db, env, base_field.declared_ty) && base_field .declared_ty - .is_assignable_to(db, child_field.declared_ty) + .is_assignable_to(db, env, child_field.declared_ty) }; if types_are_compatible { @@ -434,12 +453,14 @@ impl<'db> TypedDictFieldOverrideReason<'db> { Some(if base_field.is_read_only() { Self::ReadOnlyTypeNotAssignable { db, + env: env.clone(), child_ty: child_field.declared_ty, base_ty: base_field.declared_ty, } } else { Self::MutableTypeIncompatible { db, + env: env.clone(), child_ty: child_field.declared_ty, base_ty: base_field.declared_ty, } @@ -452,7 +473,7 @@ fn report_typed_dict_field_override<'db>( context: &InferContext<'db, '_>, class: StaticClassLiteral<'db>, field_name: &str, - reason: TypedDictFieldOverrideReason<'db>, + reason: &TypedDictFieldOverrideReason<'db>, base_name: &str, base_definition: Option>, own_field_definition: Option>, @@ -481,7 +502,7 @@ fn report_typed_dict_field_override<'db>( )) }; - diagnostic.set_primary_message(format_args!("{reason}")); + diagnostic.set_primary_annotation_message(format_args!("{reason}")); if own_field_definition.is_none() { add_definition_subdiagnostic( @@ -511,7 +532,7 @@ fn add_definition_subdiagnostic<'db>( }; let file = definition.file(db); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, definition.python_file(db)).load(db); let mut sub = SubDiagnostic::new(SubDiagnosticSeverity::Info, "Field declaration"); sub.annotate( Annotation::secondary( diff --git a/crates/ty_python_semantic/src/types/infer/builder/post_inference/typeguard.rs b/crates/ty_python_semantic/src/types/infer/builder/post_inference/typeguard.rs index 74f4b70b95..6c51098d43 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/post_inference/typeguard.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/post_inference/typeguard.rs @@ -25,6 +25,7 @@ pub(crate) fn check_type_guard_definition<'db>( }; let db = context.db(); + let env = context.program_environment(); let overload = function.literal(db).last_definition; let signature = overload.signature(db); @@ -101,15 +102,15 @@ pub(crate) fn check_type_guard_definition<'db>( // type it replaced and every narrowing fits it let param_ty = narrowed_param.annotated_type(); let param_ty = - crate::types::inferred_signature::gradual_hole(db, param_ty).unwrap_or(param_ty); - if !narrowed_ty.is_assignable_to(db, param_ty) + crate::types::inferred_signature::gradual_hole(db, env, param_ty).unwrap_or(param_ty); + if !narrowed_ty.is_assignable_to(db, env, param_ty) && let Some(builder) = context.report_lint(&INVALID_TYPE_GUARD_DEFINITION, returns_expr) { builder.into_diagnostic(format_args!( "Narrowed type `{narrowed}` is not assignable \ to the declared parameter type `{param}`", - narrowed = narrowed_ty.display(db), - param = param_ty.display(db) + narrowed = narrowed_ty.display(db, env), + param = param_ty.display(db, env) )); } } @@ -135,13 +136,13 @@ fn check_guard_place_exists<'db>( // nothing — the name has to be bound or declared somewhere the guard can see let db = context.db(); let file = context.file(); - let index = semantic_index(db, file); + let index = semantic_index(db, db.program_file(file)); let root = PlaceExpr::from_symbol_with_members(&guard.name, &[]); let resolves = root.is_some_and(|root| { index .ancestor_scopes(context.scope().file_scope_id(db)) .any(|(scope_id, _)| { - let places = place_table(db, scope_id.to_scope_id(db, file)); + let places = place_table(db, scope_id.to_scope_id(db, db.program_file(file))); places.place_id(&root).is_some_and(|place_id| { let place = places.place(place_id); place.is_bound() || place.is_declared() diff --git a/crates/ty_python_semantic/src/types/infer/builder/subscript.rs b/crates/ty_python_semantic/src/types/infer/builder/subscript.rs index 8122bc9627..61c7d2b52c 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/subscript.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/subscript.rs @@ -17,7 +17,7 @@ use crate::types::diagnostic::{ TypedDictDeleteErrorKind, report_cannot_delete_typed_dict_key, report_invalid_arguments_to_annotated, report_not_subscriptable, }; -use crate::types::generics::{GenericContext, InferableTypeVars, bind_typevar}; +use crate::types::generics::{GenericContext, bind_typevar}; use crate::types::infer::builder::annotation_expression::PEP613Policy; use crate::types::infer::builder::type_expression::{ resolve_use_site_variance_class, use_site_variance_slice_elements, @@ -31,7 +31,10 @@ use crate::types::subscript::{ DunderMethod, LegacyGenericOrigin, SubscriptError, SubscriptErrorKind, }; use crate::types::tuple::{Tuple, TupleSpecBuilder, TupleType, VariableSegment}; -use crate::types::typed_dict::{TypedDictAssignmentKind, TypedDictKeyAssignment}; +use crate::types::typed_dict::{ + TypedDictAssignmentKind, TypedDictExtraItems, TypedDictKeyAssignment, +}; +use crate::types::typevar::TypeVarSet; use crate::types::typevar::pack_bound_violation; use crate::types::{ BoundTypeVarInstance, CallArguments, CallDunderError, CallableBinding, CycleDetector, @@ -40,9 +43,9 @@ use crate::types::{ TypeAliasType, TypeAndQualifiers, TypeContext, TypeVarBoundOrConstraints, UnionType, UnionTypeInstance, any_over_type, todo_type, }; -use crate::{Db, FxOrderSet}; +use crate::{Db, FxOrderSet, ProgramEnvironment}; use ty_python_core::definition::Definition; -use ty_python_core::place::{PlaceExpr, PlaceExprRef}; +use ty_python_core::place::PlaceExpr; use ty_python_core::scope::FileScopeId; use ty_python_core::{SemanticIndex, place_table}; @@ -96,7 +99,7 @@ fn add_typevar_definition<'db>( return; }; let file = definition.file(db); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, db.program_file(file).python_file(db)).load(db); let range = definition.focus_range(db, &module).range(); diagnostic.annotate( Annotation::secondary(Span::from(file).with_range(range)) @@ -113,20 +116,22 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { provided: Type<'db>, node: impl Ranged, ) -> bool { + let env = self.program_environment(); let db = self.db(); let Some(violation) = pack_bound_violation( db, + env, typevar, provided, &ConstraintSetBuilder::new(), - InferableTypeVars::None, + TypeVarSet::None, ) else { return false; }; if let Some(builder) = self.context.report_lint(&INVALID_TYPE_ARGUMENTS, node) { - let mut diagnostic = builder.into_diagnostic(violation.message(db, typevar)); + let mut diagnostic = builder.into_diagnostic(violation.message(db, env, typevar)); add_typevar_definition(db, &mut diagnostic, typevar); - violation.attach_context(db, typevar, &mut diagnostic); + violation.attach_context(db, env, typevar, &mut diagnostic); } true } @@ -138,45 +143,52 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { fn imp<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, visitor: &TypedDictKeyExpectedTypeVisitor<'db>, ) -> Option> { match ty { Type::TypedDict(typed_dict) => { if typed_dict.explicit_extra_items(db).is_some() { - return Some(KnownClass::Str.to_instance(db)); + return Some(KnownClass::Str.to_instance(db, env)); } let keys = typed_dict .items(db) .keys() .map(|key| Type::string_literal(db, key)) .collect_vec(); - (!keys.is_empty()).then(|| UnionType::from_elements(db, keys)) + (!keys.is_empty()).then(|| UnionType::from_elements(db, env, keys)) } Type::Union(union) => { let keys = union .elements(db) .iter() - .filter_map(|element| imp(db, *element, visitor)) + .filter_map(|element| imp(db, env, *element, visitor)) .collect_vec(); - (!keys.is_empty()).then(|| UnionType::from_elements(db, keys)) + (!keys.is_empty()).then(|| UnionType::from_elements(db, env, keys)) } Type::Intersection(intersection) => { let keys = intersection .positive(db) .iter() - .filter_map(|element| imp(db, *element, visitor)) + .filter_map(|element| imp(db, env, *element, visitor)) .collect_vec(); - (!keys.is_empty()).then(|| UnionType::from_elements(db, keys)) + (!keys.is_empty()).then(|| UnionType::from_elements(db, env, keys)) } Type::TypeAlias(alias) => { - visitor.visit(db, ty, || imp(db, alias.value_type(db), visitor)) + visitor.visit(db, ty, || imp(db, env, alias.value_type(db), visitor)) } _ => None, } } + let db = self.db(); - imp(self.db(), ty, &TypedDictKeyExpectedTypeVisitor::default()) + imp( + db, + self.program_environment(), + ty, + &TypedDictKeyExpectedTypeVisitor::default(), + ) } fn store_typed_dict_key_expected_type(&mut self, slice: &ast::Expr, value_ty: Type<'db>) { @@ -214,12 +226,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } match ctx { - ExprContext::Load => self.infer_subscript_load(subscript, tcx), + ExprContext::Load => self + .infer_subscript_load(subscript, tcx) + .unwrap_or_else(|recovery_ty| recovery_ty), ExprContext::Store => { let value_ty = self.infer_expression(value, TypeContext::default()); self.store_typed_dict_key_expected_type(slice, value_ty); let slice_ty = self.infer_expression(slice, TypeContext::default()); - self.infer_subscript_expression_types( + let _ = self.infer_subscript_expression_types( subscript, value_ty, slice_ty, @@ -238,7 +252,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ExprContext::Invalid => { let value_ty = self.infer_expression(value, TypeContext::default()); let slice_ty = self.infer_expression(slice, TypeContext::default()); - self.infer_subscript_expression_types( + let _ = self.infer_subscript_expression_types( subscript, value_ty, slice_ty, @@ -250,11 +264,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } + /// Infer a subscript load, returning its inferred type when the subscription succeeds. + /// + /// If the subscription fails, report the error and return the type that should be used to + /// continue inference. This recovery type may be `Unknown` or, for example, the return type of + /// `__getitem__` when its arguments are invalid. Keeping it separate from a successful result + /// lets augmented assignments check their right-hand side without attempting a failed store. pub(super) fn infer_subscript_load( &mut self, subscript: &ast::ExprSubscript, tcx: TypeContext<'db>, - ) -> Type<'db> { + ) -> Result, Type<'db>> { let value_ty = self.infer_expression(&subscript.value, TypeContext::default()); // basedpython: `F[int]` where `F` is a `type def` is a *type* expression form. @@ -270,7 +290,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { "a `type def` can only be applied in a type expression, not used as a value", ); } - return Type::unknown(); + return Err(Type::unknown()); } // basedpython `a?.b[0]`: the `?.` short-circuit covers the subscript too, matching the @@ -281,11 +301,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // If we have an implicit type alias like `MyList = list[T]`, and if `MyList` is being // used in another implicit type alias like `Numbers = MyList[int]`, then we infer the // right hand side as a value expression, and need to handle the specialization here. - let ty = if value_ty.is_generic_alias() { - self.infer_explicit_type_alias_specialization(subscript, value_ty, false) - } else { - self.infer_subscript_load_impl(value_ty, subscript, tcx) - }; + if value_ty.is_generic_alias() { + return Ok(self.infer_explicit_type_alias_specialization(subscript, value_ty, false)); + } + let loaded = self.infer_subscript_load_impl(value_ty, subscript, tcx); + let ty = loaded.unwrap_or_else(|recovery_ty| recovery_ty); // `m[1]` goes through `Match.__getitem__`, whose stub can only say // `AnyStr | None`; the pattern says which of the two it is @@ -293,7 +313,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .refine_regex_subscript(value_ty, subscript) .unwrap_or(ty); - self.basedpython_chain_result(subscript, ty, in_chain) + let ty = self.basedpython_chain_result(subscript, ty, in_chain); + loaded.map(|_| ty).map_err(|_| ty) } /// The type of `m[key]` for a `re.Match` whose capture groups are known. @@ -302,11 +323,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { value_ty: Type<'db>, subscript: &ast::ExprSubscript, ) -> Option> { + let env = self.program_environment(); let db = self.db(); let groups = regex::groups_of(db, value_ty)?; - let any_str = regex::any_str_of(db, value_ty)?; + let any_str = regex::any_str_of(db, env, value_ty)?; let key = self.regex_group_key(&subscript.slice)?; - match regex::group_type(db, groups, any_str, key) { + match regex::group_type(db, env, groups, any_str, key) { Ok(ty) => Some(ty), Err(_) => { self.report_no_such_regex_group((&*subscript.slice).into(), key); @@ -315,12 +337,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - pub(super) fn infer_subscript_load_impl( + fn infer_subscript_load_impl( &mut self, value_ty: Type<'db>, subscript: &ast::ExprSubscript, tcx: TypeContext<'db>, - ) -> Type<'db> { + ) -> Result, Type<'db>> { + let env = self.program_environment(); let db = self.db(); let ast::ExprSubscript { @@ -328,7 +351,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { node_index: _, value: _, slice, - ctx, + ctx: _, is_typeof: _, } = subscript; @@ -341,10 +364,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // (mirroring kotlin's `Container` where reads give `Any?`); the // returned `object` then forces narrower-typed targets to fail by // normal assignability rules. - if instance_has_contravariant_projection(db, value_ty) { + if instance_has_contravariant_projection(db, env, value_ty) { // still infer the slice to surface other diagnostics let _ = self.infer_expression(slice, TypeContext::default()); - return Type::object(); + return Err(Type::object()); } let mut constraint_keys = vec![]; @@ -352,10 +375,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // If `value` is a valid reference, we attempt type narrowing by assignment. if !value_ty.is_unknown() { if let Some(expr) = PlaceExpr::try_from_expr(subscript) { - let (place, keys) = self.infer_place_load( - PlaceExprRef::from(&expr), - ast::ExprRef::Subscript(subscript), - ); + let (place, keys) = self.infer_place_load(expr, ast::ExprRef::Subscript(subscript)); constraint_keys.extend(keys); if let Place::Defined(DefinedPlace { ty, @@ -366,20 +386,22 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // 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, TypeContext::default()); - self.infer_subscript_expression_types( - subscript, - value_ty, - slice_ty, - *ctx, - TypeContext::default(), - ); - return ty; + return self + .infer_subscript_expression_types( + subscript, + value_ty, + slice_ty, + ExprContext::Load, + TypeContext::default(), + ) + .map(|_| ty) + .map_err(|_| ty); } } } - let tuple_generic_alias = |db: &'db dyn Db, tuple: Option>| { - let tuple = tuple.unwrap_or_else(|| TupleType::homogeneous(db, Type::unknown())); + let tuple_generic_alias = |env: &ProgramEnvironment<'db>, tuple: Option>| { + let tuple = tuple.unwrap_or_else(|| TupleType::homogeneous(db, env, Type::unknown())); Type::from(tuple.to_class_type(db)) }; @@ -398,7 +420,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { resolve_use_site_variance_class(db, value_ty, &elements, |elt| { self.infer_type_expression(elt) }); - return class_type.map_or_else(Type::unknown, Type::from); + return Ok(class_type.map_or_else(Type::unknown, Type::from)); } // HACK ALERT: If we are subscripting a generic class, short-circuit the rest of the @@ -408,23 +430,26 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // updating all of the subscript logic below to use custom callables for all of the _other_ // special cases, too. if class.is_tuple(db) { - return tuple_generic_alias(db, self.infer_tuple_type_expression(subscript)); + return Ok(tuple_generic_alias( + env, + self.infer_tuple_type_expression(subscript), + )); } else if class.is_known(db, KnownClass::Type) { let argument_ty = self.infer_type_expression(slice); - return Type::KnownInstance(KnownInstanceType::TypeGenericAlias( + return Ok(Type::KnownInstance(KnownInstanceType::TypeGenericAlias( InternedType::new(db, argument_ty), - )); + ))); } if let Some(generic_context) = class.generic_context(db) && let Some(class) = class.as_static() { - return self.infer_explicit_class_specialization( + return Ok(self.infer_explicit_class_specialization( subscript, value_ty, class, generic_context, - ); + )); } } Type::FunctionLiteral(function) => { @@ -432,11 +457,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(overload) = signature.overloads.first() && let Some(generic_context) = overload.generic_context { - return self.infer_explicit_function_specialization( + return Ok(self.infer_explicit_function_specialization( subscript, value_ty, generic_context, - ); + )); } } // basedpython: a reified generic *method* is specialized through @@ -454,44 +479,34 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { && let Some(overload) = signature.overloads.first() && let Some(generic_context) = overload.generic_context { - return self.infer_explicit_function_specialization( + return Ok(self.infer_explicit_function_specialization( subscript, value_ty, generic_context, - ); + )); } } - Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::ManualPEP695( - _, - ))) => { - let slice_ty = self.infer_expression(slice, TypeContext::default()); - let mut variables = FxOrderSet::default(); - slice_ty.bind_and_find_all_legacy_typevars( - db, - self.typevar_binding_context, - &mut variables, - ); - let generic_context = GenericContext::from_typevar_instances(db, variables); - return Type::Dynamic(DynamicType::UnknownGeneric(generic_context)); - } Type::KnownInstance(KnownInstanceType::TypeAliasType(type_alias)) => { if let Some(generic_context) = type_alias.generic_context(db) { - return self.infer_explicit_type_alias_type_specialization( + return Ok(self.infer_explicit_type_alias_type_specialization( subscript, value_ty, type_alias, generic_context, - ); + )); } } Type::SpecialForm(special_form) => match special_form { SpecialFormType::Tuple => { - return tuple_generic_alias(db, self.infer_tuple_type_expression(subscript)); + return Ok(tuple_generic_alias( + env, + self.infer_tuple_type_expression(subscript), + )); } SpecialFormType::Literal => match self.infer_literal_parameter_type(slice) { Ok(result) => { - return Type::KnownInstance(KnownInstanceType::Literal(InternedType::new( - db, result, + return Ok(Type::KnownInstance(KnownInstanceType::Literal( + InternedType::new(db, result), ))); } Err(nodes) => { @@ -502,19 +517,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; builder.into_diagnostic( "Type arguments for `Literal` must be `None`, \ - a literal value (int, bool, str, or bytes), or an enum member", + a literal value (int, bool, str, or bytes), \ + or an enum member", ); } - return Type::unknown(); + return Ok(Type::unknown()); } }, SpecialFormType::Annotated => { - return self + return Ok(self .parse_subscription_of_annotated_special_form( subscript, AnnotatedExprContext::TypeExpression, ) - .inner_type(); + .inner_type()); } SpecialFormType::Optional => { if matches!(**slice, ast::Expr::Tuple(_)) @@ -530,16 +546,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // `Optional[None]` is equivalent to `None`: if ty.is_none(db) { - return ty; + return Ok(ty); } - - return Type::KnownInstance(KnownInstanceType::UnionType( + return Ok(Type::KnownInstance(KnownInstanceType::UnionType( UnionTypeInstance::new( db, None, - Ok(UnionType::from_two_elements(db, ty, Type::none(db))), + Ok(UnionType::from_two_elements( + db, + env, + ty, + Type::none(db, env), + )), ), - )); + ))); } SpecialFormType::Union => match **slice { ast::Expr::Tuple(ref tuple) => { @@ -549,7 +569,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { UnionTypeInstance::new( db, None, - Ok(UnionType::from_elements(db, elements)), + Ok(UnionType::from_elements(db, env, elements)), ), )); @@ -562,18 +582,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); } - return union_type; + return Ok(union_type); } _ => { - return self.infer_expression(slice, TypeContext::default()); + return Ok(self.infer_expression(slice, TypeContext::default())); } }, SpecialFormType::Type => { // Similar to the branch above that handles `type[…]`, handle `typing.Type[…]` let argument_ty = self.infer_type_expression(slice); - return Type::KnownInstance(KnownInstanceType::TypeGenericAlias( + return Ok(Type::KnownInstance(KnownInstanceType::TypeGenericAlias( InternedType::new(db, argument_ty), - )); + ))); } SpecialFormType::TypingCallable | SpecialFormType::CollectionsAbcCallable => { let callable = self @@ -581,7 +601,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .as_callable() .expect("always returns Type::Callable"); - return Type::KnownInstance(KnownInstanceType::Callable(callable)); + return Ok(Type::KnownInstance(KnownInstanceType::Callable(callable))); } SpecialFormType::Unpack => { self.store_type_expression_flags( @@ -599,19 +619,21 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { previously_in_unpack_type_argument, ); - return if matches!( - inner_ty, - Type::TypeVar(typevar) if typevar.is_typevartuple(db) - ) || inner_ty.exact_tuple_instance_spec(db).is_some() - { - inner_ty - } else { - self.store_type_expression_flags( - ast::ExprRef::from(subscript), - TypeExpressionFlags::INVALID_UNPACK, - ); - Type::unknown() - }; + return Ok( + if matches!( + inner_ty, + Type::TypeVar(typevar) if typevar.is_typevartuple(db) + ) || inner_ty.exact_tuple_instance_spec(db).is_some() + { + inner_ty + } else { + self.store_type_expression_flags( + ast::ExprRef::from(subscript), + TypeExpressionFlags::INVALID_UNPACK, + ); + Type::unknown() + }, + ); } SpecialFormType::LegacyStdlibAlias(alias) => { let AliasSpec { @@ -647,10 +669,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .map(|arg| self.infer_type_expression(arg)) .collect(); - return class - .to_specialized_class_type(db, arg_types) + return Ok(class + .to_specialized_class_type(db, env, arg_types) .map(Type::from) - .unwrap_or_else(Type::unknown); + .unwrap_or_else(Type::unknown)); } _ => {} }, @@ -661,26 +683,35 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { | KnownInstanceType::Callable(_) | KnownInstanceType::TypeGenericAlias(_), ) => { - return self.infer_explicit_type_alias_specialization(subscript, value_ty, false); + return Ok( + self.infer_explicit_type_alias_specialization(subscript, value_ty, false) + ); } Type::Dynamic(DynamicType::Unknown) => { let slice_ty = self.infer_expression(slice, TypeContext::default()); let mut variables = FxOrderSet::default(); slice_ty.bind_and_find_all_legacy_typevars( db, + env, self.typevar_binding_context, &mut variables, ); - let generic_context = GenericContext::from_typevar_instances(db, variables); - return Type::Dynamic(DynamicType::UnknownGeneric(generic_context)); + let generic_context = GenericContext::from_typevar_instances(db, env, variables); + return Ok(Type::Dynamic(DynamicType::UnknownGeneric(generic_context))); } _ => {} } let slice_ty = self.infer_expression(slice, TypeContext::default()); - let result_ty = - self.infer_subscript_expression_types(subscript, value_ty, slice_ty, *ctx, tcx); - self.narrow_expr_with_applicable_constraints(subscript, result_ty, &constraint_keys) + self.infer_subscript_expression_types(subscript, value_ty, slice_ty, ExprContext::Load, tcx) + .map(|ty| self.narrow_expr_with_applicable_constraints(subscript, ty, &constraint_keys)) + .map_err(|recovery_ty| { + self.narrow_expr_with_applicable_constraints( + subscript, + recovery_ty, + &constraint_keys, + ) + }) } pub(super) fn infer_explicit_class_specialization( @@ -690,6 +721,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { generic_class: StaticClassLiteral<'db>, generic_context: GenericContext<'db>, ) -> Type<'db> { + let env = self.program_environment(); let db = self.db(); let specialize = &|types: &[Option>]| { Type::from(generic_class.apply_specialization(db, |_| { @@ -709,7 +741,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .is_some_and(|protocol| { protocol .interface(db) - .includes_generic_writable_instance_member(db, "__class__", generic_context) + .includes_generic_writable_instance_member( + db, + env, + "__class__", + generic_context, + ) }); let previously_disabled_int_float_special_case = disable_int_float_special_case.then(|| { @@ -752,7 +789,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(builder) = self.context.report_lint(&NOT_SUBSCRIPTABLE, subscript) { let mut diagnostic = builder.into_diagnostic("Cannot specialize non-generic type alias"); - diagnostic.set_primary_message("Double specialization is not allowed"); + diagnostic.set_primary_annotation_message("Double specialization is not allowed"); } return Type::unknown(); } @@ -836,7 +873,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { result } - pub(super) fn infer_explicit_callable_specialization_impl( + fn infer_explicit_callable_specialization_impl( &mut self, subscript: &ast::ExprSubscript, value_ty: Type<'db>, @@ -855,6 +892,23 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { NonGeneric, } + fn add_typevar_definition<'db>( + db: &'db dyn Db, + diagnostic: &mut Diagnostic, + typevar: BoundTypeVarInstance<'db>, + ) { + let Some(definition) = typevar.typevar(db).definition(db) else { + return; + }; + let file = definition.file(db); + let module = parsed_module(db, definition.python_file(db)).load(db); + let range = definition.focus_range(db, &module).range(); + diagnostic.annotate( + Annotation::secondary(Span::from(file).with_range(range)) + .message("Type variable defined here"), + ); + } + /// A type argument after expanding any allowed `Unpack[tuple[...]]` syntax. #[derive(Clone, Copy)] struct TypeArgument<'ast, 'db> { @@ -882,6 +936,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Default, } + let env = self.program_environment(); let db = self.db(); let constraints = ConstraintSetBuilder::new(); let slice_node = subscript.slice.as_ref(); @@ -1129,12 +1184,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .contains(TypeExpressionFlags::UNPACK) && let Some(tuple) = provided_type.exact_tuple_instance_spec(db) { - tuple_builder = tuple_builder.concat(db, &tuple); + tuple_builder = tuple_builder.concat(db, env, &tuple); } else { tuple_builder.push(provided_type); } } - let provided_type = Type::tuple(TupleType::new(db, &tuple_builder.build())); + let provided_type = + Type::tuple(TupleType::new(db, env, &tuple_builder.build())); self.check_pack_bound(*typevar, provided_type, subscript); specialization_types.push(Some(provided_type)); } @@ -1304,7 +1360,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { && variable.suffix_elements().is_empty() && let Some(variable_type) = variable.variable().homogeneous_type() { - tuple_builder = tuple_builder.concat(db, &tuple); + tuple_builder = tuple_builder.concat(db, env, &tuple); packed.push(TypeArgument { ty: Some(variable_type), ..*type_argument @@ -1357,12 +1413,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .type_expression_flags(type_argument.node) .contains(TypeExpressionFlags::UNPACK); if is_unpack && let Some(tuple) = provided_type.exact_tuple_instance_spec(db) { - tuple_builder = tuple_builder.concat(db, &tuple); + tuple_builder = tuple_builder.concat(db, env, &tuple); } else if is_unpack && let Type::TypeVar(typevar) = provided_type && typevar.is_typevartuple(db) { - tuple_builder = tuple_builder.concat_variadic_typevar(db, typevar); + tuple_builder = tuple_builder.concat_variadic_typevar(db, env, typevar); } else { tuple_builder.push(provided_type); } @@ -1384,7 +1440,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { && variable.suffix_elements().is_empty() && let Some(variable_type) = variable.variable().homogeneous_type() { - tuple_builder = tuple_builder.concat(db, &tuple); + tuple_builder = tuple_builder.concat(db, env, &tuple); packed_suffix.push(TypeArgument { ty: Some(variable_type), ..*type_argument @@ -1422,7 +1478,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { node: expanded_type_arguments .get(typevartuple_index) .map_or(slice_node, |argument| argument.node), - ty: Some(Type::tuple(TupleType::new(db, &tuple_builder.build()))), + ty: Some(Type::tuple(TupleType::new(db, env, &tuple_builder.build()))), source_index: expanded_type_arguments .get(typevartuple_index) .map_or(0, |argument| argument.source_index), @@ -1503,7 +1559,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { continue; }; let file = definition.file(db); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, definition.python_file(db)).load(db); let range = definition.focus_range(db, &module).range(); diagnostic.annotate( Annotation::secondary(Span::from(file).with_range(range)) @@ -1525,11 +1581,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { && lower_bound .when_assignable_to( db, + env, provided_type, &constraints, - InferableTypeVars::None, + TypeVarSet::None, ) - .is_never_satisfied(db) + .is_never_satisfied(db, env) { if let Some(builder) = self .context @@ -1538,14 +1595,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut diagnostic = builder.into_diagnostic(format_args!( "Type `{}` does not satisfy lower bound `{}` \ of type variable `{}`", - provided_type.display(db), - lower_bound.display(db), + provided_type.display(db, env), + lower_bound.display(db, env), typevar.identity(db).display(db), )); add_typevar_definition(db, &mut diagnostic, typevar); lower_bound - .assignability_error_context(db, provided_type) - .attach_to(db, &mut diagnostic); + .assignability_error_context(db, env, provided_type) + .attach_to(db, env, &mut diagnostic); } error = Some(ExplicitSpecializationError::UnsatisfiedBound); specialization_types.push(Some(Type::unknown())); @@ -1555,7 +1612,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // basedpython: a pack's bound reads element-wise or whole-pack depending on // its star count, and neither is the ordinary check below — that one would // compare the packed tuple against an element bound and always fail - if typevar.is_typevartuple(db) && typevar.typevar(db).has_pack_bound(db) { + if typevar.is_typevartuple(db) && typevar.typevar(db).has_pack_bound(db, env) { if self.check_pack_bound(typevar, provided_type, type_argument.node) { error = Some(ExplicitSpecializationError::UnsatisfiedBound); } @@ -1570,16 +1627,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // against bounds/constraints, but recording the expression for deferred // checking at end of scope. This would avoid a lot of cycles caused by eagerly // doing assignment checks here. - match typevar.typevar(db).bound_or_constraints(db) { + match typevar.typevar(db).bound_or_constraints(db, env) { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { if provided_type - .when_assignable_to( - db, - bound, - &constraints, - InferableTypeVars::None, - ) - .is_never_satisfied(db) + .when_assignable_to(db, env, bound, &constraints, TypeVarSet::None) + .is_never_satisfied(db, env) { if let Some(builder) = self .context @@ -1588,14 +1640,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut diagnostic = builder.into_diagnostic(format_args!( "Type `{}` is not assignable to upper bound `{}` \ of type variable `{}`", - provided_type.display(db), - bound.display(db), + provided_type.display(db, env), + bound.display(db, env), typevar.identity(db).display(db), )); add_typevar_definition(db, &mut diagnostic, typevar); provided_type - .assignability_error_context(db, bound) - .attach_to(db, &mut diagnostic); + .assignability_error_context(db, env, bound) + .attach_to(db, env, &mut diagnostic); } error = Some(ExplicitSpecializationError::UnsatisfiedBound); specialization_types.push(Some(Type::unknown())); @@ -1611,11 +1663,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if provided_type .when_assignable_to( db, - typevar_constraints.as_type(db), + env, + typevar_constraints.as_type(db, env), &constraints, - InferableTypeVars::None, + TypeVarSet::None, ) - .is_never_satisfied(db) + .is_never_satisfied(db, env) { if let Some(builder) = self .context @@ -1624,11 +1677,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut diagnostic = builder.into_diagnostic(format_args!( "Type `{}` does not satisfy constraints `{}` \ of type variable `{}`", - provided_type.display(db), + provided_type.display(db, env), typevar_constraints .elements(db) .iter() - .map(|c| c.display(db)) + .map(|c| c.display(db, env)) .format("`, `"), typevar.identity(db).display(db), )); @@ -1684,12 +1737,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(builder) = self.context.report_lint(&NOT_SUBSCRIPTABLE, subscript) { let mut diagnostic = builder.into_diagnostic(format_args!( "Cannot subscript non-generic type `{}`", - value_ty.display(db) + value_ty.display(db, env) )); let already_specialized = match value_ty { Type::GenericAlias(_) => true, Type::KnownInstance(KnownInstanceType::UnionType(union)) => union - .value_expression_types(db) + .value_expression_types(db, env) .is_ok_and(|mut tys| tys.any(|ty| ty.is_generic_alias())), _ => false, }; @@ -1732,6 +1785,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { slice_node, Type::heterogeneous_tuple( db, + env, inferred_type_arguments .into_iter() .map(|ty| ty.unwrap_or(Type::unknown())), @@ -1751,7 +1805,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Some(if typevar.is_paramspec(db) { Type::paramspec_value_callable(db, Parameters::unknown()) } else if typevar.is_typevartuple(db) { - Type::homogeneous_tuple(db, Type::unknown()) + Type::homogeneous_tuple(db, env, Type::unknown()) } else { Type::unknown() }) @@ -1776,6 +1830,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { expr: &ast::Expr, exactly_one_paramspec: bool, ) -> Result, ()> { + let env = self.program_environment(); let db = self.db(); match expr { @@ -1826,7 +1881,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // TODO: `Unpack` Parameters::todo() } else { - Parameters::from_annotation(db, params) + Parameters::from_annotation(db, env, params) }; return Ok(Type::paramspec_value_callable(db, parameters)); @@ -1896,6 +1951,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { db, Parameters::from_annotation( db, + env, [ Parameter::positional_only(None) .with_annotated_type(param_type), @@ -1925,6 +1981,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } else { Parameters::from_annotation( db, + env, [Parameter::positional_only(None) .with_annotated_type(param_type)], ) @@ -1973,6 +2030,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Err(()) } + /// Infer a subscription and report failures while preserving their recovery types. pub(super) fn infer_subscript_expression_types( &self, subscript: &ast::ExprSubscript, @@ -1980,7 +2038,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { slice_ty: Type<'db>, expr_context: ExprContext, tcx: TypeContext<'db>, - ) -> Type<'db> { + ) -> Result, Type<'db>> { + let env = self.program_environment(); let db = self.db(); if let Some(origin) = match value_ty { @@ -2039,7 +2098,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { SubscriptErrorKind::MultipleTypeVarTuples { origin }, ); error.report_diagnostics(&self.context, subscript); - return error.result_type(); + return Err(error.result_type()); } if has_invalid_unpack_argument { let error = SubscriptError::new( @@ -2050,7 +2109,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }, ); error.report_diagnostics(&self.context, subscript); - return error.result_type(); + return Err(error.result_type()); } } @@ -2060,6 +2119,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let subscript_result = match value_ty { Type::SpecialForm(SpecialFormType::Generic) => infer_legacy_generic_subscript( db, + env, self.index, self.scope().file_scope_id(db), self.typevar_binding_context, @@ -2069,6 +2129,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ), Type::SpecialForm(SpecialFormType::Protocol) => infer_legacy_generic_subscript( db, + env, self.index, self.scope().file_scope_id(db), self.typevar_binding_context, @@ -2081,10 +2142,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut variables = FxOrderSet::default(); slice_ty.bind_and_find_all_legacy_typevars( db, + env, self.typevar_binding_context, &mut variables, ); - let generic_context = GenericContext::from_typevar_instances(db, variables); + let generic_context = GenericContext::from_typevar_instances(db, env, variables); Ok(Type::Dynamic(DynamicType::UnknownGeneric(generic_context))) } // basedpython: a keyword subscript is a `__getitem__` call carrying @@ -2095,12 +2157,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { _ if self.is_basedpython_file() && keyword_subscript_elements(subscript).is_some() => { self.infer_keyword_subscript(subscript, value_ty, slice_ty, tcx) } - _ => value_ty.subscript(db, slice_ty, expr_context, tcx), + _ => value_ty.subscript(db, env, slice_ty, expr_context, tcx), }; - subscript_result.unwrap_or_else(|e| { - e.report_diagnostics(&self.context, subscript); - e.result_type() + subscript_result.map_err(|error| { + error.report_diagnostics(&self.context, subscript); + error.result_type() }) } @@ -2114,9 +2176,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { slice_ty: Type<'db>, tcx: TypeContext<'db>, ) -> Result, SubscriptError<'db>> { + let env = self.program_environment(); let db = self.db(); let Some(elements) = keyword_subscript_elements(subscript) else { - return value_ty.subscript(db, slice_ty, ast::ExprContext::Load, tcx); + return value_ty.subscript(db, env, slice_ty, ast::ExprContext::Load, tcx); }; let arguments: CallArguments<'_, 'db> = elements .iter() @@ -2131,17 +2194,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { _ => (Argument::Positional, Some(self.expression_type(element))), }) .collect(); - match value_ty.try_call_dunder(db, "__getitem__", arguments, tcx) { - Ok(outcome) => Ok(outcome.return_type(db)), + match value_ty.try_call_dunder(db, env, "__getitem__", arguments, tcx) { + Ok(outcome) => Ok(outcome.return_type(db, env)), Err(CallDunderError::PossiblyUnbound { bindings, .. }) => Err(SubscriptError::new( - bindings.return_type(db), + bindings.return_type(db, env), SubscriptErrorKind::DunderPossiblyUnbound { method: DunderMethod::GetItem, value_ty, }, )), Err(CallDunderError::CallError(_, bindings, _)) => Err(SubscriptError::new( - bindings.return_type(db), + bindings.return_type(db, env), SubscriptErrorKind::KeywordSubscriptCallError { bindings }, )), Err(CallDunderError::MethodNotAvailable) => Err(SubscriptError::new( @@ -2156,7 +2219,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { pub(super) fn infer_slice_expression(&mut self, slice: &ast::ExprSlice) -> Type<'db> { let db = self.db(); - + let env = self.program_environment(); let ast::ExprSlice { range: _, node_index: _, @@ -2171,10 +2234,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { KnownClass::Slice.to_specialized_instance( db, + env, &[ - ty_lower.unwrap_or_else(|| Type::none(db)), - ty_upper.unwrap_or_else(|| Type::none(db)), - ty_step.unwrap_or_else(|| Type::none(db)), + ty_lower.unwrap_or_else(|| Type::none(db, env)), + ty_upper.unwrap_or_else(|| Type::none(db, env)), + ty_step.unwrap_or_else(|| Type::none(db, env)), ], ) } @@ -2184,8 +2248,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { &mut self, target: &ast::ExprSubscript, rhs_value: &ast::Expr, + object_ty: Type<'db>, + infer_slice_ty: &mut dyn FnMut(&mut Self, TypeContext<'db>) -> Type<'db>, infer_rhs_value: &mut dyn FnMut(&mut Self, TypeContext<'db>) -> Type<'db>, ) -> bool { + let env = self.program_environment(); let ast::ExprSubscript { range: _, node_index: _, @@ -2197,7 +2264,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let db = self.db(); - let object_ty = self.infer_expression(object, TypeContext::default()); self.store_typed_dict_key_expected_type(slice, object_ty); // basedpython use-site variance: `Container[out T]` rejects writes @@ -2205,7 +2271,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // contravariant and projects to `Never` under an `out` projection. // Emit a focused diagnostic and short-circuit before calling the // dunder. - if instance_has_covariant_projection(self.db(), object_ty) { + if instance_has_covariant_projection(self.db(), env, object_ty) { let slice_ty = self.infer_expression(slice, TypeContext::default()); let rhs_ty = infer_rhs_value(self, TypeContext::default()); if let Some(builder) = self @@ -2215,21 +2281,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { builder.into_diagnostic(format_args!( "Invalid subscript assignment with key of type `{}` and value of \ type `{}` on object of type `{}`", - slice_ty.display(self.db()), - rhs_ty.display(self.db()), - object_ty.display(self.db()), + slice_ty.display(self.db(), env), + rhs_ty.display(self.db(), env), + object_ty.display(self.db(), env), )); } return false; } - let mut infer_slice_ty = |builder: &mut Self, tcx| builder.infer_expression(slice, tcx); - let is_valid_assignment = self.validate_subscript_assignment_impl( target, None, object_ty, - &mut infer_slice_ty, + infer_slice_ty, rhs_value, infer_rhs_value, true, @@ -2240,9 +2304,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if is_valid_assignment && self.fluid_specializations_enabled() && let Some(collection_def) = self.index.fluid_candidate_binding(object) - && let Some((class_literal, _)) = object_ty.class_specialization(db) + && let Some((class_literal, _)) = object_ty.class_specialization(db, env) { - let identity_instance = Type::instance(db, class_literal.identity_specialization(db)); + let identity_instance = + Type::instance(db, env, class_literal.identity_specialization(db)); let collection_generic_context = class_literal.generic_context(db); let ast_arguments = [ @@ -2259,14 +2324,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }) = identity_instance .member_lookup_with_policy( db, + env, "__setitem__", MemberLookupPolicy::NO_INSTANCE_FALLBACK, ) .place { let mut identity_bindings = dunder_callable - .bindings(db) - .match_parameters(db, &call_arguments) + .bindings(db, env) + .match_parameters(db, env, &call_arguments) // Perform inference against the type variables on the receiver's generic context. .with_generic_context(db, collection_generic_context); @@ -2292,7 +2358,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { for call_specialization in identity_bindings .iter_flat() .flat_map(CallableBinding::matching_overloads) - .filter_map(|(_, identity_overload)| identity_overload.specialization(db)) + .filter_map(|(_, identity_overload)| { + identity_overload.specialization(db, env) + }) { // Record the constraints on the receiver's generic context formed by // the arguments to this dunder call. @@ -2327,13 +2395,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { infer_rhs_value: &mut dyn FnMut(&mut Self, TypeContext<'db>) -> Type<'db>, emit_diagnostic: bool, ) -> bool { + let env = self.program_environment(); let db = self.db(); let attach_original_type_info = |diagnostic: &mut LintDiagnosticGuard| { if let Some(full_object_ty) = full_object_ty { diagnostic.info(format_args!( "The full type of the subscripted object is `{}`", - full_object_ty.display(db) + full_object_ty.display(db, env) )); } }; @@ -2409,7 +2478,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::EnumComplement(complement) => self.validate_subscript_assignment_impl( target, full_object_ty, - complement.remaining_literal_union(db), + complement.remaining_literal_union(db, env), infer_slice_ty, rhs_value_node, infer_rhs_value, @@ -2433,12 +2502,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { return true; } - if slice_ty.is_assignable_to(db, KnownClass::Str.to_instance(db)) - && let Some(expected_ty) = typed_dict.arbitrary_key_mutation_type(db) + if slice_ty.is_assignable_to(db, env, KnownClass::Str.to_instance(db, env)) + && let Some(expected_ty) = typed_dict.arbitrary_key_mutation_type(db, env) { let rhs_value_ty = infer_rhs_value(self, TypeContext::new(Some(expected_ty))); - if rhs_value_ty.is_assignable_to(db, expected_ty) { + if rhs_value_ty.is_assignable_to(db, env, expected_ty) { return true; } @@ -2448,14 +2517,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .report_lint(&INVALID_ASSIGNMENT, rhs_value_node) { let mut diagnostic = builder.into_diagnostic(format_args!( - "Cannot assign value of type `{}` to key of type `{}` on TypedDict `{}`", - rhs_value_ty.display(db), - slice_ty.display(db), - object_ty.display(db), + "Cannot assign value of type `{}` to key of type `{}` \ + on TypedDict `{}`", + rhs_value_ty.display(db, env), + slice_ty.display(db, env), + object_ty.display(db, env), )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Expected value assignable to `{}`", - expected_ty.display(db) + expected_ty.display(db, env) )); attach_original_type_info(&mut diagnostic); } @@ -2463,19 +2533,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } let rhs_value_ty = infer_rhs_value(self, TypeContext::default()); - let assigned_d = rhs_value_ty.display(db); - let value_d = object_ty.display(db); + let assigned_d = rhs_value_ty.display(db, env); + let value_d = object_ty.display(db, env); - if slice_ty.is_assignable_to(db, Type::literal_string()) - && !slice_ty.is_equivalent_to(db, Type::literal_string()) + if slice_ty.is_assignable_to(db, env, Type::literal_string()) + && !slice_ty.is_equivalent_to(db, env, Type::literal_string()) { if let Some(builder) = self .context .report_lint(&INVALID_ASSIGNMENT, target.slice.as_ref()) { let mut diagnostic = builder.into_diagnostic(format_args!( - "Cannot assign value of type `{assigned_d}` to key of type `{}` on TypedDict `{value_d}`", - slice_ty.display(db) + "Cannot assign value of type `{assigned_d}` to key of type `{}` \ + on TypedDict `{value_d}`", + slice_ty.display(db, env) )); attach_original_type_info(&mut diagnostic); } @@ -2485,8 +2556,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .report_lint(&INVALID_KEY, target.slice.as_ref()) { let mut diagnostic = builder.into_diagnostic(format_args!( - "TypedDict `{value_d}` can only be subscripted with a string literal key, got key of type `{}`.", - slice_ty.display(db) + "TypedDict `{value_d}` can only be subscripted \ + with a string literal key, got key of type `{}`.", + slice_ty.display(db, env) )); attach_original_type_info(&mut diagnostic); } @@ -2554,7 +2626,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; let Err(call_dunder_err) = self.infer_and_try_call_dunder( - db, object_ty, "__setitem__", MemberLookupPolicy::NO_INSTANCE_FALLBACK, @@ -2575,7 +2646,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { let mut diagnostic = builder.into_diagnostic(format_args!( "Method `__setitem__` of type `{}` may be missing", - object_ty.display(db), + object_ty.display(db, env), )); attach_original_type_info(&mut diagnostic); } @@ -2594,8 +2665,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut diagnostic = builder.into_diagnostic(format_args!( "Method `__setitem__` of type `{}` is not callable \ on object of type `{}`", - bindings.callable_type().display(db), - object_ty.display(db), + bindings.callable_type().display(db, env), + object_ty.display(db, env), )); attach_original_type_info(&mut diagnostic); } @@ -2625,42 +2696,46 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { target.range.cover(rhs_value_node.range()), ) { - let assigned_d = rhs_value_ty.display(db); - let object_d = object_ty.display(db); + let assigned_d = rhs_value_ty.display(db, env); + let object_d = object_ty.display(db, env); let mut diagnostic = builder.into_diagnostic(format_args!( - "Invalid subscript assignment with key of type `{}` and value of \ - type `{assigned_d}` on object of type `{object_d}`", - slice_ty.display(db), - )); + "Invalid subscript assignment with key of type `{}` \ + and value of type `{assigned_d}` \ + on object of type `{object_d}`", + slice_ty.display(db, env), + )); // Special diagnostic for dictionaries if let Some([expected_key_ty, expected_value_ty]) = object_ty - .known_specialization(db, KnownClass::Dict) + .known_specialization(db, env, KnownClass::Dict) .map(|s| s.types(db)) { - if !slice_ty.is_assignable_to(db, *expected_key_ty) { + if !slice_ty.is_assignable_to(db, env, *expected_key_ty) + { diagnostic.annotate( self.context .secondary(target.slice.as_ref()) .message(format_args!( "Expected key of type `{}`, got `{}`", - expected_key_ty.display(db), - slice_ty.display(db), + expected_key_ty.display(db, env), + slice_ty.display(db, env), )), ); } - if !rhs_value_ty - .is_assignable_to(db, *expected_value_ty) - { + if !rhs_value_ty.is_assignable_to( + db, + env, + *expected_value_ty, + ) { diagnostic.annotate( self.context.secondary(rhs_value_node).message( format_args!( "Expected value of type `{}`, got `{}`", - expected_value_ty.display(db), - rhs_value_ty.display(db), + expected_value_ty.display(db, env), + rhs_value_ty.display(db, env), ), ), ); @@ -2677,10 +2752,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.context.report_lint(&CALL_NON_CALLABLE, target) { let mut diagnostic = builder.into_diagnostic(format_args!( - "Method `__setitem__` of type `{}` may not be callable on object of type `{}`", - bindings.callable_type().display(db), - object_ty.display(db), - )); + "Method `__setitem__` of type `{}` may not be callable \ + on object of type `{}`", + bindings.callable_type().display(db, env), + object_ty.display(db, env), + )); attach_original_type_info(&mut diagnostic); } } @@ -2694,28 +2770,31 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { let mut diagnostic = builder.into_diagnostic(format_args!( "Cannot assign to a subscript on an object of type `{}`", - object_ty.display(db), + object_ty.display(db, env), )); attach_original_type_info(&mut diagnostic); // If it's a user-defined class, suggest adding a `__setitem__` method. if object_ty .as_nominal_instance() - .and_then(|instance| instance.class(db).static_class_literal(db)) + .and_then(|instance| { + instance.class(db, env).static_class_literal(db) + }) .and_then(|(class_literal, _)| { - file_to_module(db, class_literal.file(db)) + let file = class_literal.program_file(db); + file_to_module(db, file.resolver_file(db)) }) .and_then(|module| module.search_path(db)) .is_some_and(ty_module_resolver::SearchPath::is_first_party) { diagnostic.help(format_args!( "Consider adding a `__setitem__` method to `{}`.", - object_ty.display(db), + object_ty.display(db, env), )); } else { diagnostic.info(format_args!( "`{}` does not have a `__setitem__` method.", - object_ty.display(db), + object_ty.display(db, env), )); } } @@ -2743,13 +2822,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { object_ty: Type<'db>, slice_ty: Type<'db>, ) { + let env = self.program_environment(); let db = self.db(); let attach_original_type_info = |diagnostic: &mut LintDiagnosticGuard| { if let Some(full_object_ty) = full_object_ty { diagnostic.info(format_args!( "The full type of the subscripted object is `{}`", - full_object_ty.display(db) + full_object_ty.display(db, env) )); } }; @@ -2791,7 +2871,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::EnumComplement(complement) => self.validate_subscript_deletion_impl( target, full_object_ty, - complement.remaining_literal_union(db), + complement.remaining_literal_union(db, env), slice_ty, ), @@ -2807,148 +2887,145 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { && string_literal_values(db, slice_ty).is_some_and(|mut literals| { literals.all(|literal| !typed_dict.items(db).contains_key(literal)) }); - let can_delete_arbitrary_key = slice_ty - .is_assignable_to(db, KnownClass::Str.to_instance(db)) - && typed_dict.supports_arbitrary_key_deletion(db); + let can_delete_arbitrary_key = + slice_ty.is_assignable_to(db, env, KnownClass::Str.to_instance(db, env)) + && typed_dict.supports_arbitrary_key_deletion(db); if can_delete_extra_literals || can_delete_arbitrary_key { return; } } - match object_ty.try_call_dunder( + let Err(err) = object_ty.try_call_dunder( db, + env, "__delitem__", CallArguments::positional([slice_ty]), TypeContext::default(), - ) { - Ok(_) => {} - Err(err) => match err { - CallDunderError::PossiblyUnbound { .. } => { - if let Some(builder) = self - .context - .report_lint(&POSSIBLY_MISSING_IMPLICIT_CALL, target) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Method `__delitem__` of type `{}` may be missing", - object_ty.display(db), - )); - attach_original_type_info(&mut diagnostic); - } + ) else { + return; + }; + + match err { + CallDunderError::PossiblyUnbound { .. } => { + if let Some(builder) = self + .context + .report_lint(&POSSIBLY_MISSING_IMPLICIT_CALL, target) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Method `__delitem__` of type `{}` may be missing", + object_ty.display(db, env), + )); + attach_original_type_info(&mut diagnostic); } - CallDunderError::CallError(call_error_kind, bindings, _) => { - match call_error_kind { - CallErrorKind::NotCallable => { - if let Some(builder) = - self.context.report_lint(&CALL_NON_CALLABLE, target) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Method `__delitem__` of type `{}` is not callable \ - on object of type `{}`", - bindings.callable_type().display(db), - object_ty.display(db), - )); - attach_original_type_info(&mut diagnostic); - } + } + CallDunderError::CallError(call_error_kind, bindings, _) => { + match call_error_kind { + CallErrorKind::NotCallable => { + if let Some(builder) = + self.context.report_lint(&CALL_NON_CALLABLE, target) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Method `__delitem__` of type `{}` \ + is not callable on object of type `{}`", + bindings.callable_type().display(db, env), + object_ty.display(db, env), + )); + attach_original_type_info(&mut diagnostic); } - CallErrorKind::BindingError => { - // For deletions of string literal keys on `TypedDict`, provide - // a more detailed diagnostic. - if let Some(typed_dict) = object_ty.as_typed_dict() { - if let Some(string_literal) = slice_ty.as_string_literal() { - let key = string_literal.value(db); - let items = typed_dict.items(db); - - if let Some(field) = items.get(key) { - // Key exists but is required (i.e., can't be deleted). - report_cannot_delete_typed_dict_key( - &self.context, - (&*target.slice).into(), - typed_dict, - key, - Some(field), - TypedDictDeleteErrorKind::RequiredKey, - ); - } else if typed_dict - .explicit_extra_items(db) - .is_some_and(|extra_items| { - extra_items.is_read_only() - }) - { - report_cannot_delete_typed_dict_key( - &self.context, - (&*target.slice).into(), - typed_dict, - key, - None, - TypedDictDeleteErrorKind::ReadOnlyExtraItem, - ); - } else { - // Key doesn't exist. - report_cannot_delete_typed_dict_key( - &self.context, - (&*target.slice).into(), - typed_dict, - key, - None, - TypedDictDeleteErrorKind::UnknownKey, - ); - } + } + CallErrorKind::BindingError => { + // For deletions of string literal keys on `TypedDict`, provide + // a more detailed diagnostic. + if let Some(typed_dict) = object_ty.as_typed_dict() { + if let Some(string_literal) = slice_ty.as_string_literal() { + let key = string_literal.value(db); + let items = typed_dict.items(db); + + if let Some(field) = items.get(key) { + // Key exists but is required (i.e., can't be deleted). + report_cannot_delete_typed_dict_key( + &self.context, + (&*target.slice).into(), + typed_dict, + key, + Some(field), + TypedDictDeleteErrorKind::RequiredKey, + ); + } else if typed_dict + .explicit_extra_items(db) + .is_some_and(TypedDictExtraItems::is_read_only) + { + report_cannot_delete_typed_dict_key( + &self.context, + (&*target.slice).into(), + typed_dict, + key, + None, + TypedDictDeleteErrorKind::ReadOnlyExtraItem, + ); } else { - // Non-string-literal key on `TypedDict`. - if let Some(builder) = self - .context - .report_lint(&INVALID_ARGUMENT_TYPE, target) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Method `__delitem__` of type `{}` cannot be called \ - with key of type `{}` on object of type `{}`", - bindings.callable_type().display(db), - slice_ty.display(db), - object_ty.display(db), - )); - attach_original_type_info(&mut diagnostic); - } + // Key doesn't exist. + report_cannot_delete_typed_dict_key( + &self.context, + (&*target.slice).into(), + typed_dict, + key, + None, + TypedDictDeleteErrorKind::UnknownKey, + ); } } else { - // Non-`TypedDict` object + // Non-string-literal key on `TypedDict`. if let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, target) { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Method `__delitem__` of type `{}` cannot be called \ - with key of type `{}` on object of type `{}`", - bindings.callable_type().display(db), - slice_ty.display(db), - object_ty.display(db), - )); + let mut diagnostic = + builder.into_diagnostic(format_args!( + "Method `__delitem__` of type `{}` \ + cannot be called with key of type \ + `{}` on object of type `{}`", + bindings.callable_type().display(db, env), + slice_ty.display(db, env), + object_ty.display(db, env), + )); attach_original_type_info(&mut diagnostic); } } - } - CallErrorKind::PossiblyNotCallable => { + } else { + // Non-`TypedDict` object if let Some(builder) = - self.context.report_lint(&CALL_NON_CALLABLE, target) + self.context.report_lint(&INVALID_ARGUMENT_TYPE, target) { let mut diagnostic = builder.into_diagnostic(format_args!( - "Method `__delitem__` of type `{}` may not be callable \ - on object of type `{}`", - bindings.callable_type().display(db), - object_ty.display(db), + "Method `__delitem__` of type `{}` cannot \ + be called with key of type `{}` on \ + object of type `{}`", + bindings.callable_type().display(db, env), + slice_ty.display(db, env), + object_ty.display(db, env), )); attach_original_type_info(&mut diagnostic); } } } + CallErrorKind::PossiblyNotCallable => { + if let Some(builder) = + self.context.report_lint(&CALL_NON_CALLABLE, target) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Method `__delitem__` of type `{}` may not be \ + callable on object of type `{}`", + bindings.callable_type().display(db, env), + object_ty.display(db, env), + )); + attach_original_type_info(&mut diagnostic); + } + } } - CallDunderError::MethodNotAvailable => { - report_not_subscriptable( - &self.context, - target, - object_ty, - "__delitem__", - ); - } - }, + } + CallDunderError::MethodNotAvailable => { + report_not_subscriptable(&self.context, target, object_ty, "__delitem__"); + } } } } @@ -2960,6 +3037,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { object_ty .try_call_dunder( db, + self.program_environment(), "__delitem__", CallArguments::positional([slice_ty]), TypeContext::default(), @@ -3018,7 +3096,10 @@ enum LegacyGenericContextError<'db> { /// A duplicate typevar was provided. DuplicateTypevar(&'db str), /// A `TypeVarTuple` was provided but not unpacked. - TypeVarTupleMustBeUnpacked, + /// + /// The generic context is available when the argument is a bound `TypeVarTuple` and is used + /// to avoid cascading errors during recovery. + TypeVarTupleMustBeUnpacked(Option>), } impl<'db> LegacyGenericContextError<'db> { @@ -3027,7 +3108,7 @@ impl<'db> LegacyGenericContextError<'db> { LegacyGenericContextError::InvalidArgument(_) | LegacyGenericContextError::VariadicTupleArguments | LegacyGenericContextError::DuplicateTypevar(_) - | LegacyGenericContextError::TypeVarTupleMustBeUnpacked => Type::unknown(), + | LegacyGenericContextError::TypeVarTupleMustBeUnpacked(_) => Type::unknown(), LegacyGenericContextError::NotYetSupported => { todo_type!("ParamSpecs and TypeVarTuples") } @@ -3037,8 +3118,10 @@ impl<'db> LegacyGenericContextError<'db> { /// Validate the type arguments to `Generic[...]` or `Protocol[...]`, returning /// either the resulting [`GenericContext`] or a [`SubscriptError`]. +#[expect(clippy::too_many_arguments)] fn infer_legacy_generic_subscript<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, index: &'db SemanticIndex<'db>, file_scope_id: FileScopeId, typevar_binding_context: Option>, @@ -3046,8 +3129,14 @@ fn infer_legacy_generic_subscript<'db>( origin: LegacyGenericOrigin, wrap_ok: impl FnOnce(GenericContext<'db>) -> KnownInstanceType<'db>, ) -> Result, SubscriptError<'db>> { - match legacy_generic_class_context(db, index, file_scope_id, typevar_binding_context, slice_ty) - { + match legacy_generic_class_context( + db, + env, + index, + file_scope_id, + typevar_binding_context, + slice_ty, + ) { Ok(context) => Ok(Type::KnownInstance(wrap_ok(context))), Err(LegacyGenericContextError::InvalidArgument(argument_ty)) => Err(SubscriptError::new( Type::unknown(), @@ -3063,10 +3152,14 @@ fn infer_legacy_generic_subscript<'db>( typevar_name, }, )), - Err(LegacyGenericContextError::TypeVarTupleMustBeUnpacked) => Err(SubscriptError::new( - Type::unknown(), - SubscriptErrorKind::TypeVarTupleNotUnpacked { origin }, - )), + Err(LegacyGenericContextError::TypeVarTupleMustBeUnpacked(generic_context)) => { + Err(SubscriptError::new( + generic_context.map_or(Type::unknown(), |generic_context| { + Type::KnownInstance(wrap_ok(generic_context)) + }), + SubscriptErrorKind::TypeVarTupleNotUnpacked { origin }, + )) + } Err( error @ (LegacyGenericContextError::NotYetSupported | LegacyGenericContextError::VariadicTupleArguments), @@ -3078,6 +3171,7 @@ fn infer_legacy_generic_subscript<'db>( /// that each argument is a type variable. fn legacy_generic_class_context<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, index: &'db SemanticIndex<'db>, file_scope_id: FileScopeId, typevar_binding_context: Option>, @@ -3113,7 +3207,10 @@ fn legacy_generic_class_context<'db>( let bound = bind_typevar(db, index, file_scope_id, typevar_binding_context, typevar) .ok_or(LegacyGenericContextError::InvalidArgument(argument_ty))?; if bound.is_typevartuple(db) { - return Err(LegacyGenericContextError::TypeVarTupleMustBeUnpacked); + validated_typevars.insert(bound); + return Err(LegacyGenericContextError::TypeVarTupleMustBeUnpacked(Some( + GenericContext::from_typevar_instances(db, env, validated_typevars), + ))); } if !validated_typevars.insert(bound) { return Err(LegacyGenericContextError::DuplicateTypevar( @@ -3132,8 +3229,8 @@ fn legacy_generic_class_context<'db>( Some(KnownClass::TypeVarTuple | KnownClass::ExtensionsTypeVarTuple) ) { - return Err(LegacyGenericContextError::TypeVarTupleMustBeUnpacked); - } else if any_over_type(db, argument_ty, true, |inner_ty| match inner_ty { + return Err(LegacyGenericContextError::TypeVarTupleMustBeUnpacked(None)); + } else if any_over_type(db, env, argument_ty, true, |inner_ty| match inner_ty { Type::NominalInstance(nominal) => matches!( nominal.known_class(db), Some(KnownClass::TypeVarTuple | KnownClass::ExtensionsTypeVarTuple) @@ -3147,6 +3244,7 @@ fn legacy_generic_class_context<'db>( } Ok(GenericContext::from_typevar_instances( db, + env, validated_typevars, )) } @@ -3191,13 +3289,17 @@ impl AnnotatedExprContext { /// writes — a covariantly-projected typevar appears in the `__setitem__` /// value parameter (a contravariant position), and projects to `Never`, /// so no assignment can succeed. -fn instance_has_covariant_projection<'db>(db: &'db dyn Db, object_ty: Type<'db>) -> bool { +fn instance_has_covariant_projection<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + object_ty: Type<'db>, +) -> bool { use ruff_python_ast::helpers::UseSiteVariance; let Some(instance) = object_ty.as_nominal_instance() else { return false; }; - let crate::types::ClassType::Generic(alias) = instance.class(db) else { + let crate::types::ClassType::Generic(alias) = instance.class(db, env) else { return false; }; alias @@ -3209,13 +3311,17 @@ fn instance_has_covariant_projection<'db>(db: &'db dyn Db, object_ty: Type<'db>) /// Symmetric helper for contravariant (`in`) projection — used by subscript /// READS to reject calls to `__getitem__`. -fn instance_has_contravariant_projection<'db>(db: &'db dyn Db, object_ty: Type<'db>) -> bool { +fn instance_has_contravariant_projection<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + object_ty: Type<'db>, +) -> bool { use ruff_python_ast::helpers::UseSiteVariance; let Some(instance) = object_ty.as_nominal_instance() else { return false; }; - let crate::types::ClassType::Generic(alias) = instance.class(db) else { + let crate::types::ClassType::Generic(alias) = instance.class(db, env) else { return false; }; alias diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_call.rs b/crates/ty_python_semantic/src/types/infer/builder/type_call.rs index ea4532f886..41c4fb9030 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_call.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_call.rs @@ -31,6 +31,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { call_expr: &ast::ExprCall, definition: Option>, ) -> Type<'db> { + let env = self.program_environment(); let db = self.db(); let ast::Arguments { @@ -49,7 +50,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let arg_type = self.infer_expression(single, TypeContext::default()); return if keywords.is_empty() { - arg_type.dunder_class(db) + arg_type.dunder_class(db, env) } else { if keywords.iter().any(|keyword| keyword.arg.is_some()) && let Some(builder) = @@ -173,20 +174,26 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }; if !matches!(namespace_type, Type::TypedDict(_)) - && !namespace_type.is_assignable_to( - db, - KnownClass::Dict - .to_specialized_instance(db, &[KnownClass::Str.to_instance(db), Type::any()]), - ) + && { + !namespace_type.is_assignable_to( + db, + env, + KnownClass::Dict.to_specialized_instance( + db, + env, + &[KnownClass::Str.to_instance(db, env), Type::any()], + ), + ) + } && let Some(builder) = self .context .report_lint(&INVALID_ARGUMENT_TYPE, namespace_arg) { let mut diagnostic = builder .into_diagnostic("Invalid argument to parameter 3 (`namespace`) of `type()`"); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Expected `dict[str, Any]`, found `{}`", - namespace_type.display(db) + namespace_type.display(db, env) )); } @@ -194,14 +201,14 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let name = if let Some(literal) = name_type.as_string_literal() { literal.value(db) } else { - if !name_type.is_assignable_to(db, KnownClass::Str.to_instance(db)) + if !name_type.is_assignable_to(db, env, KnownClass::Str.to_instance(db, env)) && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, name_arg) { let mut diagnostic = builder.into_diagnostic("Invalid argument to parameter 1 (`name`) of `type()`"); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Expected `str`, found `{}`", - name_type.display(db) + name_type.display(db, env) )); } "" @@ -295,9 +302,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> { call_expr.into(), dynamic_class.name(db), metaclass1, - base1.display(db), + base1.display(db, env), metaclass2, - base2.display(db), + base2.display(db, env), ); } } @@ -321,7 +328,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }; // Get the already-inferred class type from the initial pass. - let inferred_type = definition_expression_type(db, definition, call_expr); + let inferred_type = definition_expression_type(self.db(), definition, call_expr); let Type::ClassLiteral(ClassLiteral::Dynamic(dynamic_class)) = inferred_type else { return; }; diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index face723b7c..e58c136522 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -30,19 +30,20 @@ use crate::types::type_fn::{ }; use ty_python_core::scope::ScopeKind; +use crate::types::ProgramEnvironment; use crate::types::{ BindingContext, BoundTypeVarInstance, CallableType, DeferredOperation, DeferredType, DynamicType, GenericContext, InternedType, IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, LintDiagnosticGuard, LiteralValueTypeKind, OverlappingType, ParamSpecAttrKind, Parameter, Parameters, RestrictedType, SpecialFormType, SubclassOfType, - Type, TypeAliasType, TypeContext, TypeFormType, TypeGuardType, TypeIsType, TypeMapping, - TypeVarKind, UnionBuilder, UnionType, UnsafeUnionType, any_over_type, todo_type, + Type, TypeContext, TypeFormType, TypeGuardType, TypeIsType, TypeMapping, TypeVarKind, + UnionBuilder, UnionType, UnsafeUnionType, any_over_type, todo_type, }; -use crate::{FxOrderSet, Program, add_inferred_python_version_hint_to_diagnostic}; +use crate::{FxOrderSet, add_inferred_python_version_hint_to_diagnostic}; /// Type expressions impl<'db> TypeInferenceBuilder<'db, '_> { - pub(super) const fn type_expression_context(&self) -> &'static str { + const fn type_expression_context(&self) -> &'static str { self.inference_flags().type_expression_context() } @@ -125,20 +126,22 @@ impl<'db> TypeInferenceBuilder<'db, '_> { /// A receiver that still mentions a type parameter (`X[T].x`) keeps the whole /// thing symbolic; a ground one folds here and now. fn infer_attribute_type_expression(&mut self, attribute: &ast::ExprAttribute) -> Type<'db> { + let env = self.program_environment(); let receiver = self.infer_type_expression(&attribute.value); let db = self.db(); let member = &attribute.attr.id; - if receiver.member(db, member).place.is_undefined() { + if receiver.member(db, env, member).place.is_undefined() { if let Some(builder) = self.context.report_lint(&UNRESOLVED_ATTRIBUTE, attribute) { builder.into_diagnostic(format_args!( "Object of type `{}` has no attribute `{member}`", - receiver.display(db), + receiver.display(db, env), )); } return Type::unknown(); } DeferredType::build( db, + env, &DeferredOperation::Attribute(member.clone()), Box::from([receiver]), ) @@ -171,6 +174,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // a dotted name whose lookup already produced a type names that type directly; // there is no value whose type-expression meaning still has to be taken. that // covers `P.args` / `P.kwargs` and basedpython's `T.a` attribute types + let db = self.db(); + let env = self.program_environment(); if annotation.is_attribute_expr() && match ty { Type::TypeVar(tvar) => tvar.paramspec_attr(self.db()).is_some(), @@ -192,9 +197,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } report_missing_type_arguments(&self.context, ty, annotation); let result_ty = ty - .default_specialize(self.db()) + .default_specialize(db, env) .in_type_expression( - self.db(), + db, self.scope(), self.typevar_binding_context, self.inference_flags(), @@ -232,9 +237,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { {attr} and is not valid in a `.by` file", )); if let Some(suggestion) = suggestion { - diagnostic.set_primary_message(format_args!("Did you mean `{suggestion}`?")); + diagnostic + .set_primary_annotation_message(format_args!("Did you mean `{suggestion}`?")); } else { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Keyword-variadic pack `{name}` has no positional parameters" )); } @@ -271,6 +277,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { /// Infer the type of a type expression without storing the result. pub(super) fn infer_type_expression_no_store(&mut self, expression: &ast::Expr) -> Type<'db> { + let db = self.db(); + let env = self.program_environment(); let ignore_runtime_errors = |builder: &Self| { builder.deferred_state.is_deferred() || builder.in_stub() @@ -285,7 +293,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // before the generic subscript arm reads it as a subscription if let Some((modifier, inner)) = type_modifier_marker(expression) { let inner_ty = self.infer_type_expression(inner); - return RestrictedType::from_type_expression(self.db(), modifier, inner_ty); + return RestrictedType::from_type_expression(self.db(), env, modifier, inner_ty); } // https://typing.python.org/en/latest/spec/annotations.html#grammar-token-expression-grammar-type_expression @@ -340,6 +348,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // the normal attribute load with the type we // already inferred, so it isn't inferred twice self.infer_attribute_load_impl(attribute_expression, receiver) + .unwrap_or_else(|recovery_ty| recovery_ty) } else { self.infer_dotted_type_expression(attribute_expression) }; @@ -369,7 +378,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } } - ast::Expr::NoneLiteral(_literal) => Type::none(self.db()), + ast::Expr::NoneLiteral(_literal) => Type::none(db, env), // https://typing.python.org/en/latest/spec/annotations.html#string-annotations ast::Expr::StringLiteral(string) => self.infer_string_type_expression(string), @@ -398,6 +407,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let value_ty = self.infer_expression(value, TypeContext::default()); return resolve_use_site_variance( self.db(), + env, value_ty, &slice_elements, |elt| self.infer_type_expression(elt), @@ -417,7 +427,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if class_literal.is_known(self.db(), KnownClass::Tuple) { // tuple has variadic typevars — treat any marker // as a homogeneous-Any tuple regardless of mix - Type::homogeneous_tuple(self.db(), Type::any()) + Type::homogeneous_tuple(self.db(), env, Type::any()) } else { let db = self.db(); let arg_types: Vec> = elts @@ -444,12 +454,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .specialize(db, vec![Type::any(); n].as_slice()) } }); - Type::instance(db, class_type) + Type::instance(db, env, class_type) } } _ => value_ty, }; - return inner_ty.top_materialization(self.db()); + return inner_ty.top_materialization(self.db(), env); } // basedpython `typeof X` desugars to `ty_extensions.TypeOf[X]` @@ -473,7 +483,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { { let inner = self.infer_subscript_type_expression_no_store(subscript, slice, value_ty); - return inner.top_materialization(self.db()); + return inner.top_materialization(self.db(), env); } if *is_typeof || is_dotted_name(value) { @@ -536,7 +546,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .infer_expression(&binary.right, TypeContext::default()); let dunder_fails = Type::try_call_bin_op( - self.db(), + db, + env, left_type_value, ast::Operator::BitOr, right_type_value, @@ -553,8 +564,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let literal = match (left_type_value, right_type_value) { (Type::ClassLiteral(class), Type::LiteralValue(literal)) | (Type::LiteralValue(literal), Type::ClassLiteral(class)) - if class.metaclass(self.db()) - == KnownClass::Type.to_class_literal(self.db()) => + if class.metaclass(db) + == KnownClass::Type.to_class_literal(db, env) => { Some(literal) } @@ -574,15 +585,15 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut diagnostic = builder.into_diagnostic("Unsupported `|` operation"); - if left_type_value.is_equivalent_to(self.db(), right_type_value) { - diagnostic.set_primary_message(format_args!( + if left_type_value.is_equivalent_to(db, env, right_type_value) { + diagnostic.set_primary_annotation_message(format_args!( "Both operands have type `{}`", - left_type_value.display(self.db()) + left_type_value.display(db, env) )); diagnostic.set_concise_message(format_args!( "Operator `|` is unsupported between \ two objects of type `{}`", - left_type_value.display(self.db()) + left_type_value.display(db, env) )); } else { for (operand, ty) in [ @@ -592,15 +603,15 @@ impl<'db> TypeInferenceBuilder<'db, '_> { diagnostic.annotate( self.context.secondary(operand).message(format_args!( "Has type `{}`", - ty.display(self.db()) + ty.display(db, env) )), ); } diagnostic.set_concise_message(format_args!( "Operator `|` is unsupported between \ objects of type `{}` and `{}`", - left_type_value.display(self.db()), - right_type_value.display(self.db()) + left_type_value.display(db, env), + right_type_value.display(db, env) )); } @@ -618,7 +629,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ), _ => { let python_version = - Program::get(self.db()).python_version(self.db()); + self.program_environment().python_version(db); if python_version < PythonVersion::PY314 { diagnostic.info(format_args!( @@ -627,7 +638,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { self.type_expression_context() )); add_inferred_python_version_hint_to_diagnostic( - self.db(), + db, + self.file(), &mut diagnostic, "inferring types", ); @@ -645,7 +657,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } } - UnionType::from_elements_leave_aliases(self.db(), [left_ty, right_ty]) + UnionType::from_elements_leave_aliases(db, env, [left_ty, right_ty]) } // basedpython: `A & B` in a type annotation is an // intersection type. core syntax in `.by` / `.byi`, @@ -672,7 +684,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let right_value = speculative_builder .infer_expression(&binary.right, TypeContext::default()); if Type::try_call_bin_op( - self.db(), + db, + env, left_value, ast::Operator::BitAnd, right_value, @@ -689,7 +702,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } } - IntersectionType::from_two_elements(self.db(), left_ty, right_ty) + IntersectionType::from_two_elements(db, env, left_ty, right_ty) } // anything else is an invalid annotation: op => { @@ -709,9 +722,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // specialized. keep it symbolic so `Array[Dim + 1]` // re-evaluates to `Array[6]` at the call site let operands = [left_ty, right_ty]; - if DeferredType::is_deferred(db, &operands) { + if DeferredType::is_deferred(db, env, &operands) { return DeferredType::build( db, + env, &DeferredOperation::Binary(op), Box::new(operands), ); @@ -777,7 +791,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if let Some(single_element) = bytes.as_single_part_bytestring() && let Ok(valid_string) = String::from_utf8(single_element.value.to_vec()) { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Did you mean `typing.Literal[b\"{valid_string}\"]`?" )); } @@ -794,7 +808,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if let Some(int) = int.as_i64() { return Type::int_literal(int); } - return KnownClass::Int.to_instance(self.db()); + return KnownClass::Int.to_instance(self.db(), env); } if let Some(mut diagnostic) = self.report_invalid_type_expression( expression, @@ -804,7 +818,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ), ) { if let Some(int) = int.as_i64() { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Did you mean `typing.Literal[{int}]`?" )); } @@ -861,7 +875,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { self.type_expression_context() ), ) { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Did you mean `typing.Literal[{}]`?", if bool_value.value { "True" } else { "False" } )); @@ -870,8 +884,6 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } ast::Expr::List(list) => { - let db = self.db(); - if !self.in_string_annotation() { self.infer_list_expression(list, TypeContext::default()); } @@ -887,13 +899,13 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut speculative_builder = self.speculate_without_diagnostics(); let inner_type = speculative_builder.infer_type_expression(single_element); - if inner_type.is_hintable(self.db()) { + if inner_type.is_hintable(self.db(), env) { let hinted_type = - KnownClass::List.to_specialized_instance(db, &[inner_type]); + KnownClass::List.to_specialized_instance(db, env, &[inner_type]); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Did you mean `{}`?", - hinted_type.display(self.db()), + hinted_type.display(db, env), )); } } @@ -911,7 +923,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let class_lit = self .synthesize_anon_named_tuple_class(tuple, /* is_type_form = */ true); return class_lit - .to_instance_approximation(self.db()) + .to_instance_approximation(self.db(), env) .unwrap_or(class_lit); } @@ -942,12 +954,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> { _ => self.infer_type_expression(e), }) .collect(); - return Type::heterogeneous_tuple(self.db(), elt_tys); + return Type::heterogeneous_tuple(self.db(), env, elt_tys); } let class_lit = self .synthesize_anon_named_tuple_class(tuple, /* is_type_form = */ true); return class_lit - .to_instance_approximation(self.db()) + .to_instance_approximation(self.db(), env) .unwrap_or(class_lit); } @@ -961,7 +973,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { tuple.range(), /* specialization = */ false, ); - return Type::tuple(TupleType::new(self.db(), &spec)); + return Type::tuple(TupleType::new(self.db(), env, &spec)); } if tuple.parenthesized { @@ -985,11 +997,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .map(|element| speculative.infer_type_expression(element)) .collect(); - if inner_types.iter().all(|ty| ty.is_hintable(self.db())) { - let hinted_type = Type::heterogeneous_tuple(self.db(), inner_types); - diagnostic.set_primary_message(format_args!( + if inner_types.iter().all(|ty| ty.is_hintable(self.db(), env)) { + let hinted_type = Type::heterogeneous_tuple(db, env, inner_types); + diagnostic.set_primary_annotation_message(format_args!( "Did you mean `{}`?", - hinted_type.display(self.db()), + hinted_type.display(db, env), )); } } @@ -1014,9 +1026,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .collect(); return match bool_op.op { ast::BoolOp::Or => { - UnionType::from_elements_leave_aliases(self.db(), elements) + UnionType::from_elements_leave_aliases(self.db(), env, elements) + } + ast::BoolOp::And => { + IntersectionType::from_elements(self.db(), env, elements) } - ast::BoolOp::And => IntersectionType::from_elements(self.db(), elements), }; } if !self.in_string_annotation() { @@ -1078,7 +1092,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .speculate_without_diagnostics() .infer_expression(operand, TypeContext::default()); if let Err(error) = operand_value.try_call_dunder( - self.db(), + db, + env, "__invert__", CallArguments::none(), TypeContext::default(), @@ -1093,7 +1108,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } } - operand_ty.negate(self.db()) + operand_ty.negate(db, env) } ast::Expr::UnaryOp(unary) => { @@ -1102,7 +1117,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // inner type and apply the Not special form via inference if matches!(unary.op, ast::UnaryOp::Not) && self.is_basedpython_file() { let inner = self.infer_type_expression(&unary.operand); - return inner.negate(self.db()); + return inner.negate(self.db(), env); } // basedpython: `-float.inf` is the negative-infinity float // literal (`-float.nan` stays nan). the exact `-float.` @@ -1158,7 +1173,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } let decomposition = UnionType::from_elements_leave_aliases( self.db(), - [inner, Type::none(self.db())], + env, + [inner, Type::none(self.db(), env)], ); if matches!(inner, Type::TypeVar(_)) { return Type::KnownInstance(KnownInstanceType::WrappedOptional( @@ -1181,9 +1197,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let db = self.db(); let operand_ty = self.infer_type_expression(&unary.operand); let operands = [operand_ty]; - if DeferredType::is_deferred(db, &operands) { + if DeferredType::is_deferred(db, env, &operands) { return DeferredType::build( db, + env, &DeferredOperation::Unary(unary.op), Box::new(operands), ); @@ -1259,12 +1276,17 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut speculative = self.speculate_without_diagnostics(); let key_type = speculative.infer_type_expression(key); let value_type = speculative.infer_type_expression(value); - if key_type.is_hintable(self.db()) && value_type.is_hintable(self.db()) { - let hinted_type = KnownClass::Dict - .to_specialized_instance(self.db(), &[key_type, value_type]); - diagnostic.set_primary_message(format_args!( + if key_type.is_hintable(self.db(), env) + && value_type.is_hintable(self.db(), env) + { + let hinted_type = KnownClass::Dict.to_specialized_instance( + db, + env, + &[key_type, value_type], + ); + diagnostic.set_primary_annotation_message(format_args!( "Did you mean `{}`?", - hinted_type.display(self.db()), + hinted_type.display(db, env), )); } } @@ -1286,13 +1308,13 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut speculative_builder = self.speculate_without_diagnostics(); let inner_type = speculative_builder.infer_type_expression(single_element); - if inner_type.is_hintable(self.db()) { + if inner_type.is_hintable(self.db(), env) { let hinted_type = - KnownClass::Set.to_specialized_instance(self.db(), &[inner_type]); + KnownClass::Set.to_specialized_instance(db, env, &[inner_type]); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Did you mean `{}`?", - hinted_type.display(self.db()), + hinted_type.display(db, env), )); } } @@ -1417,7 +1439,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // type so other passes don't choke let _ = compare.left.as_ref(); let narrowed = self.infer_type_expression(target); - let expanded = narrowed.expand_eagerly(self.db()); + let expanded = narrowed.expand_eagerly(self.db(), env); if expanded.is_divergent() { return expanded; } @@ -1445,6 +1467,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let right_ty = self.infer_type_expression(comparator); return DeferredType::build( db, + env, &DeferredOperation::Compare(compare.ops[0]), Box::new([left_ty, right_ty]), ); @@ -1512,7 +1535,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let db = self.db(); let receiver_ty = self.infer_type_expression(&method.value); let callee_ty = receiver_ty - .member(db, method.attr.as_str()) + .member(db, env, method.attr.as_str()) .ignore_possibly_undefined() .unwrap_or_else(Type::unknown); // record a type for the method-access node so the expression map @@ -1525,6 +1548,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } return DeferredType::build( db, + env, &DeferredOperation::Call, operands.into_boxed_slice(), ); @@ -1674,6 +1698,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { callable: &ast::ExprCallableType, receiver: Option<&ast::ExprName>, ) -> Type<'db> { + let env = self.program_environment(); let db = self.db(); let receiver_offset = usize::from(receiver.is_some()); let args = &callable.args[receiver_offset..]; @@ -1770,7 +1795,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { |index| callable.parameter_borrow(index + receiver_offset), )) .collect(); - let parameters = Parameters::from_annotation(db, params); + let parameters = Parameters::from_annotation(db, env, params); let return_type = self.infer_type_expression(&callable.returns); let previous = self .inference_flags() @@ -1818,6 +1843,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { star: Option, borrow_at: impl Fn(usize) -> ParameterBorrow, ) -> Vec> { + let env = self.program_environment(); let db = self.db(); let mut params: Vec> = Vec::with_capacity(elements.len()); for (index, element) in elements.iter().enumerate() { @@ -1841,7 +1867,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { )) .with_annotated_type(ty); params.push(if unpacks { - parameter.with_unpacked_kwargs(db) + parameter.with_unpacked_kwargs(db, env) } else { parameter }); @@ -1889,7 +1915,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let parameter = Parameter::keyword_variadic(Name::new_static("kwargs")) .with_annotated_type(ty); let parameter = if unpacks || inner.value.is_name_expr() { - parameter.with_unpacked_kwargs(db) + parameter.with_unpacked_kwargs(db, env) } else { parameter }; @@ -2020,6 +2046,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } fn infer_starred_type_expression(&mut self, starred: &ast::ExprStarred) -> Type<'db> { + let env = self.program_environment(); + let db = self.db(); let ast::ExprStarred { range: _, node_index: _, @@ -2133,7 +2161,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ), )); } - return Type::homogeneous_tuple(self.db(), Type::unknown()); + return Type::homogeneous_tuple(self.db(), env, Type::unknown()); } if let Some(target) = unpack_target(self.db(), starred_type) { @@ -2150,7 +2178,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { builder.into_diagnostic("`*` can only unpack a tuple type or `TypeVarTuple`"), ); } - Type::homogeneous_tuple(self.db(), Type::unknown()) + Type::homogeneous_tuple(db, self.program_environment(), Type::unknown()) } } @@ -2160,6 +2188,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { slice: &ast::Expr, value_ty: Type<'db>, ) -> Type<'db> { + let env = self.program_environment(); // basedpython: track `ty_extensions.Top` / `Bottom` appearing in nested // type-position inside this subscript's slice. the Name/Attribute arms // set `slice_materialization` when they encounter one; on exit the @@ -2186,10 +2215,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .set(InferenceFlags::IN_SUBSCRIPT_SLICE, prev_flag); match kind { Some(crate::types::MaterializationKind::Top) => { - return result.top_materialization(self.db()); + return result.top_materialization(self.db(), env); } Some(crate::types::MaterializationKind::Bottom) => { - return result.bottom_materialization(self.db()); + return result.bottom_materialization(self.db(), env); } None => {} } @@ -2203,6 +2232,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { slice: &ast::Expr, value_ty: Type<'db>, ) -> Type<'db> { + let env = self.program_environment(); // basedpython use-site variance also fires here — the annotation // expression path enters `infer_subscript_type_expression_no_store` // directly for `list[in T]` / `Container[out T]` annotations on @@ -2211,7 +2241,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if self.is_basedpython_file() && let Some(slice_elements) = use_site_variance_slice_elements(slice) { - return resolve_use_site_variance(self.db(), value_ty, &slice_elements, |elt| { + return resolve_use_site_variance(self.db(), env, value_ty, &slice_elements, |elt| { self.infer_type_expression(elt) }); } @@ -2244,6 +2274,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { slice: &ast::Expr, function: FunctionType<'db>, ) -> Type<'db> { + let env = self.program_environment(); let db = self.db(); let arguments: Vec> = match slice { ast::Expr::Tuple(tuple) if !tuple.parenthesized => tuple @@ -2272,15 +2303,15 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // a bound is a precondition: it is checked before the function runs, so an // impossible argument costs no interpreter. it is also the only check that // works on a symbolic argument, so it happens before the deferral below - if let Some((index, argument, bound)) = first_bound_violation(db, function, &arguments) + if let Some((index, argument, bound)) = first_bound_violation(db, env, function, &arguments) && let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { builder.into_diagnostic(format_args!( "argument {} to `{}` is `{}`, which is not assignable to its bound `{}`", index + 1, function.name(db), - argument.display(db), - bound.display(db), + argument.display(db, env), + bound.display(db, env), )); return Type::unknown(); } @@ -2289,12 +2320,13 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // `F[T]` inside a generic function is only knowable once `T` is // substituted. keep the application symbolic; `DeferredType` re-runs it on // specialization, and until then it behaves as the declared return type - if DeferredType::is_deferred(db, &arguments) { + if DeferredType::is_deferred(db, env, &arguments) { let mut operands = Vec::with_capacity(arguments.len() + 1); operands.push(Type::FunctionLiteral(function)); operands.extend_from_slice(&arguments); return DeferredType::build( db, + env, &DeferredOperation::TypeFn, operands.into_boxed_slice(), ); @@ -2390,6 +2422,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { report_at: TextRange, specialization: bool, ) -> TupleSpec<'db> { + let env = self.program_environment(); let mut element_types = TupleSpecBuilder::with_capacity(0); let mut first_unpacked_variadic_tuple = None; @@ -2397,7 +2430,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if specialization && element.is_ellipsis_literal_expr() { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, report_at) { let mut diagnostic = builder.into_diagnostic("Invalid `tuple` specialization"); - diagnostic.set_primary_message( + diagnostic.set_primary_annotation_message( "`...` can only be used as the second element \ in a two-element `tuple` specialization", ); @@ -2451,7 +2484,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }; if let Some(inner_tuple) = element_ty.exact_tuple_instance_spec(self.db()) { - element_types = element_types.concat(self.db(), &inner_tuple); + element_types = element_types.concat(self.db(), env, &inner_tuple); if inner_tuple.is_variadic() { report_too_many_unpacked_tuples(); @@ -2460,7 +2493,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { && typevar.is_typevartuple(self.db()) { report_too_many_unpacked_tuples(); - element_types = element_types.concat_variadic_typevar(self.db(), typevar); + element_types = element_types.concat_variadic_typevar(self.db(), env, typevar); } else { // TODO: emit a diagnostic } @@ -2480,6 +2513,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { &mut self, tuple: &ast::ExprSubscript, ) -> Option> { + let db = self.db(); + let env = self.program_environment(); match &*tuple.slice { ast::Expr::Tuple(elements) => { if let [element, ellipsis @ ast::Expr::EllipsisLiteral(_)] = &*elements.elts { @@ -2500,10 +2535,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> { { let mut diagnostic = builder.into_diagnostic("Invalid `tuple` specialization"); - diagnostic - .set_primary_message("`...` cannot be used after an unpacked element"); + diagnostic.set_primary_annotation_message( + "`...` cannot be used after an unpacked element", + ); } - let result = TupleType::homogeneous(self.db(), element_ty); + let result = TupleType::homogeneous(db, env, element_ty); self.store_expression_type(&tuple.slice, Type::tuple(Some(result))); return Some(result); } @@ -2514,7 +2550,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { /* specialization = */ true, ); - let ty = TupleType::new(self.db(), &element_types); + let ty = TupleType::new(self.db(), env, &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 @@ -2528,13 +2564,13 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, tuple) { let mut diagnostic = builder.into_diagnostic("Invalid `tuple` specialization"); - diagnostic.set_primary_message( + diagnostic.set_primary_annotation_message( "`...` can only be used as the second element \ in a two-element `tuple` specialization", ); } self.store_expression_type(single_element, Type::unknown()); - return TupleType::heterogeneous(self.db(), std::iter::once(Type::unknown())); + return TupleType::heterogeneous(db, env, std::iter::once(Type::unknown())); } let previously_in_valid_unpack_context = self .context @@ -2556,25 +2592,28 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if let Some(inner_tuple) = single_element_ty.exact_tuple_instance_spec(self.db()) { - return TupleType::new(self.db(), &inner_tuple); + return TupleType::new(db, env, &inner_tuple); } else if let Type::TypeVar(typevar) = single_element_ty && typevar.is_typevartuple(self.db()) { return TupleType::new( - self.db(), + db, + env, &TupleSpecBuilder::with_capacity(0) - .concat_variadic_typevar(self.db(), typevar) + .concat_variadic_typevar(db, env, typevar) .build(), ); } } - TupleType::heterogeneous(self.db(), std::iter::once(single_element_ty)) + TupleType::heterogeneous(db, env, 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> { + let db = self.db(); + let env = self.program_environment(); let invalid_type_argument = |builder: &Self, slice: &ast::Expr| { builder.report_invalid_type_expression( slice, @@ -2584,18 +2623,16 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }; let subclass_of_type_argument = |builder: &Self, slice: &ast::Expr, slice_ty: Type<'db>| { - let slice_ty = slice_ty.resolve_type_alias(builder.db()); + let slice_ty = slice_ty.resolve_type_alias(db); let slice_ty = match slice_ty { Type::Union(union) if union.has_aliases(builder.db()) => { - union.expand_aliases(builder.db()) + union.expand_aliases(db, env) } _ => slice_ty, }; - SubclassOfType::try_from_instance(builder.db(), slice_ty).unwrap_or_else(|| { - match slice_ty { - Type::Callable(_) => invalid_type_argument(builder, slice), - _ => todo_type!("unsupported type[X] special form"), - } + SubclassOfType::try_from_instance(db, env, slice_ty).unwrap_or_else(|| match slice_ty { + Type::Callable(_) => invalid_type_argument(builder, slice), + _ => todo_type!("unsupported type[X] special form"), }) }; @@ -2622,7 +2659,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } ast::Expr::NoneLiteral(_) => { self.infer_expression(slice, TypeContext::default()); - KnownClass::NoneType.to_subclass_of(self.db()) + KnownClass::NoneType.to_subclass_of(db, env) + } + ast::Expr::Subscript(ast::ExprSubscript { value, .. }) if !is_dotted_name(value) => { + infer_type_argument(self, slice) } ast::Expr::Subscript( subscript @ ast::ExprSubscript { @@ -2635,7 +2675,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { Type::SpecialForm(SpecialFormType::Union) => match &**parameters { ast::Expr::Tuple(tuple) => { let ty = UnionType::from_elements_leave_aliases( - self.db(), + db, + env, tuple .iter() .map(|element| self.infer_subclass_of_type_expression(element)), @@ -2650,26 +2691,25 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let class_type = self .infer_tuple_type_expression(subscript) .map(|tuple_type| tuple_type.to_class_type(self.db())) - .unwrap_or_else(|| class_literal.default_specialization(self.db())); - SubclassOfType::from(self.db(), class_type) + .unwrap_or_else(|| class_literal.default_specialization(db)); + SubclassOfType::from(db, env, class_type) } else { - match class_literal.generic_context(self.db()) { + match class_literal.generic_context(db) { Some(generic_context) => { - let db = self.db(); let specialize = &|types: &[Option>]| { let class = class_literal.apply_specialization(db, |_| { generic_context .specialize_partial(db, types.iter().copied()) }); if class_literal.is_protocol(db) { - match Type::instance(db, class) { + match Type::instance(db, env, class) { Type::ProtocolInstance(protocol) => { SubclassOfType::from_protocol(protocol) } - _ => SubclassOfType::from(db, class), + _ => SubclassOfType::from(db, env, class), } } else { - SubclassOfType::from(db, class) + SubclassOfType::from(db, env, class) } }; self.infer_explicit_callable_specialization( @@ -2680,9 +2720,16 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ) } None => { - // TODO: emit a diagnostic if you try to specialize a non-generic class. self.infer_expression(parameters, TypeContext::default()); - todo_type!("specialized non-generic class") + if let Some(builder) = + self.context.report_lint(&NOT_SUBSCRIPTABLE, subscript) + { + builder.into_diagnostic(format_args!( + "Cannot subscript non-generic type `{}`", + value_ty.display(db, self.program_environment()) + )); + } + Type::unknown() } } } @@ -2697,9 +2744,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ); invalid_type_argument(self, slice) } - value_ty @ Type::KnownInstance(KnownInstanceType::TypeAliasType( - TypeAliasType::PEP695(_), - )) => { + value_ty @ (Type::SpecialForm( + SpecialFormType::Top | SpecialFormType::Bottom | SpecialFormType::Annotated, + ) + | Type::KnownInstance(_) + | Type::GenericAlias(_) + | Type::Callable(_)) => { let slice_ty = self.infer_subscript_type_expression(subscript, value_ty); subclass_of_type_argument(self, slice, slice_ty) } @@ -2725,6 +2775,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { mut value_ty: Type<'db>, in_type_expression: bool, ) -> Type<'db> { + let env = self.program_environment(); let db = self.db(); if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = value_ty @@ -2732,14 +2783,15 @@ impl<'db> TypeInferenceBuilder<'db, '_> { { value_ty = value_ty.apply_type_mapping( db, + env, &TypeMapping::BindLegacyTypevars(BindingContext::Definition(definition)), TypeContext::default(), ); } let mut variables = FxOrderSet::default(); - value_ty.find_legacy_typevars(db, None, &mut variables); - let generic_context = GenericContext::from_typevar_instances(db, variables); + value_ty.find_legacy_typevars(db, env, None, &mut variables); + let generic_context = GenericContext::from_typevar_instances(db, env, variables); let scope_id = self.scope(); let current_typevar_binding_context = self.typevar_binding_context; @@ -2753,7 +2805,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // instead of two. So until we properly support these, specialize all remaining type // variables with a `@Todo` type (since we don't know which of the type arguments // belongs to the remaining type variables). - if any_over_type(self.db(), value_ty, true, |ty| ty.is_divergent()) { + if any_over_type(db, env, value_ty, true, |ty| ty.is_divergent()) { let value_ty = value_ty.apply_specialization( db, generic_context.specialize( @@ -2807,11 +2859,13 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ) } - pub(super) fn infer_subscript_type_expression( + fn infer_subscript_type_expression( &mut self, subscript: &ast::ExprSubscript, value_ty: Type<'db>, ) -> Type<'db> { + let db = self.db(); + let env = self.program_environment(); let ast::ExprSubscript { range: _, node_index: _, @@ -2830,7 +2884,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if self.is_basedpython_file() && let Some(slice_elements) = use_site_variance_slice_elements(slice) { - return resolve_use_site_variance(self.db(), value_ty, &slice_elements, |elt| { + return resolve_use_site_variance(self.db(), env, value_ty, &slice_elements, |elt| { self.infer_type_expression(elt) }); } @@ -2947,7 +3001,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } Type::unknown() } - KnownInstanceType::TypeAliasType(type_alias @ TypeAliasType::PEP695(_)) => { + KnownInstanceType::TypeAliasType(type_alias) => { match type_alias.generic_context(self.db()) { Some(generic_context) => { let specialized_type_alias = self @@ -2960,7 +3014,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { specialized_type_alias .in_type_expression( - self.db(), + db, self.scope(), self.typevar_binding_context, self.inference_flags(), @@ -2983,12 +3037,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if value_type.is_specialized_generic(self.db()) { diagnostic.annotate(secondary.message(format_args!( "Alias to `{}`, which is already specialized", - value_type.display(self.db()) + value_type.display(db, env) ))); } else { diagnostic.annotate(secondary.message(format_args!( "Alias to `{}`, which is not generic", - value_type.display(self.db()) + value_type.display(db, env) ))); } } @@ -2997,19 +3051,6 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } } } - KnownInstanceType::TypeAliasType(TypeAliasType::ManualPEP695(_)) => { - // TODO: support generic "manual" PEP 695 type aliases - let slice_ty = self.infer_expression(slice, TypeContext::default()); - let mut variables = FxOrderSet::default(); - slice_ty.bind_and_find_all_legacy_typevars( - self.db(), - self.typevar_binding_context, - &mut variables, - ); - let generic_context = - GenericContext::from_typevar_instances(self.db(), variables); - Type::Dynamic(DynamicType::UnknownGeneric(generic_context)) - } KnownInstanceType::Literal(ty) => { if !self.in_string_annotation() { self.infer_expression(slice, TypeContext::default()); @@ -3017,7 +3058,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { builder.into_diagnostic(format_args!( "`{ty}` is not a generic class", - ty = ty.inner(self.db()).display(self.db()) + ty = ty.inner(self.db()).display(db, env) )); } Type::unknown() @@ -3145,7 +3186,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { specialized_class .in_type_expression( - self.db(), + db, self.scope(), self.typevar_binding_context, self.inference_flags(), @@ -3153,9 +3194,16 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .unwrap_or(Type::unknown()) } _ => { - // TODO: emit a diagnostic if you try to specialize a non-generic class. self.infer_expression(slice, TypeContext::default()); - todo_type!("specialized non-generic class") + if let Some(builder) = + self.context.report_lint(&NOT_SUBSCRIPTABLE, subscript) + { + builder.into_diagnostic(format_args!( + "Cannot subscript non-generic type `{}`", + value_ty.display(db, self.program_environment()) + )); + } + Type::unknown() } } } @@ -3170,7 +3218,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { Type::Union(union) => { let db = self.db(); let mut union_builder = - UnionBuilder::new(db).recursively_defined(union.recursively_defined(db)); + UnionBuilder::new(db, env).recursively_defined(union.recursively_defined(db)); for (index, element) in union.elements(db).iter().enumerate() { let mut speculative_builder = self.speculate(); @@ -3193,7 +3241,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { builder.into_diagnostic(format_args!( "Invalid subscript of object of type `{}` in a {}", - value_ty.display(self.db()), + value_ty.display(db, env), self.type_expression_context() )); } @@ -3207,6 +3255,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { subscript_node: &ast::ExprSubscript, alias: LegacyStdlibAlias, ) -> Type<'db> { + let db = self.db(); let arguments = &*subscript_node.slice; let args = if let ast::Expr::Tuple(t) = arguments && !t.is_anon_named_tuple @@ -3236,7 +3285,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } } let ty = class.to_specialized_instance( - self.db(), + db, + self.program_environment(), args.iter() .map(|node| self.infer_type_expression(node)) .collect::>(), @@ -3294,9 +3344,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> { "`[...]` is not a valid parameter list for `Callable`", ) { if let Some(returns) = return_type { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Did you mean `Callable[..., {}]`?", - returns.display(db) + returns.display(db, builder.program_environment()) )); } } @@ -3362,6 +3412,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { subscript: &ast::ExprSubscript, special_form: SpecialFormType, ) -> Type<'db> { + let env = self.program_environment(); let db = self.db(); let arguments_slice = &*subscript.slice; match special_form { @@ -3371,7 +3422,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { AnnotatedExprContext::TypeExpression, ) .inner_type() - .in_type_expression(self.db(), self.scope(), None, self.inference_flags()) + .in_type_expression(db, self.scope(), None, self.inference_flags()) .unwrap_or_else(|err| { err.into_fallback_type(&self.context, subscript, self.inference_flags()) }), @@ -3393,7 +3444,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }, SpecialFormType::Optional => { let param_type = self.infer_type_expression(arguments_slice); - UnionType::from_elements_leave_aliases(db, [param_type, Type::none(db)]) + UnionType::from_elements_leave_aliases(db, env, [param_type, Type::none(db, env)]) } SpecialFormType::Union => { // TODO: Support the union of a `TypeVarTuple`'s elements. Until then, reject @@ -3406,32 +3457,26 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut has_unpacked_typevartuple = false; let union_ty = UnionType::from_elements_leave_aliases( db, + env, arguments.iter().map(|argument| { let ty = self.infer_type_expression(argument); if self .type_expression_flags(argument) .contains(TypeExpressionFlags::UNPACK) { - let is_typevartuple = matches!( - ty, - Type::TypeVar(typevar) if typevar.is_typevartuple(db) - ) || if let ast::Expr::Subscript(subscript) = argument { - let previously_in_unpack_type_argument = self - .context - .inference_flags - .replace(InferenceFlags::IN_UNPACK_TYPE_ARGUMENT, true); - let inner_ty = self.infer_type_expression(&subscript.slice); - self.context.inference_flags.set( - InferenceFlags::IN_UNPACK_TYPE_ARGUMENT, - previously_in_unpack_type_argument, - ); + let is_typevartuple = matches!( - inner_ty, + ty, Type::TypeVar(typevar) if typevar.is_typevartuple(db) - ) - } else { - false - }; + ) || if let ast::Expr::Subscript(subscript) = argument { + matches!( + self.expression_type(&subscript.slice), + Type::TypeVar(typevar) if typevar.is_typevartuple(db) + ) + } else { + false + }; + if is_typevartuple { has_unpacked_typevartuple = true; if !ty.is_unknown() @@ -3440,7 +3485,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { { diagnostic::add_type_expression_reference_link( builder.into_diagnostic( - "Unpacking a `TypeVarTuple` in `Union` is not supported", + "Unpacking a `TypeVarTuple` in `Union` \ + is not supported", ), ); } @@ -3474,7 +3520,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }; let num_arguments = arguments.len(); let negated_type = if num_arguments == 1 { - self.infer_type_expression(&arguments[0]).negate(db) + self.infer_type_expression(&arguments[0]).negate(db, env) } else { if !self.in_string_annotation() { for argument in arguments { @@ -3502,7 +3548,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }; let ty = elements - .fold(IntersectionBuilder::new(db), |builder, element| { + .fold(IntersectionBuilder::new(db, env), |builder, element| { builder.add_positive(self.infer_type_expression(element)) }) .build(); @@ -3588,7 +3634,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ); Type::unknown() }; - arg.top_materialization(db) + arg.top_materialization(db, env) } SpecialFormType::Bottom => { let arguments = if let ast::Expr::Tuple(tuple) = arguments_slice @@ -3616,7 +3662,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ); Type::unknown() }; - arg.bottom_materialization(db) + arg.bottom_materialization(db, env) } SpecialFormType::TypeOf => { let arguments = if let ast::Expr::Tuple(tuple) = arguments_slice @@ -3712,19 +3758,19 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } let argument_type = self.infer_expression(&arguments[0], TypeContext::default()); - let Some(callable_type) = argument_type .try_upcast_to_callable_with_recursive_fallback( db, + env, self.recursive_type_expression_definition(), ) .map(|callables| { if special_form == SpecialFormType::RegularCallableTypeOf { callables .map(|callable| callable.into_regular(db)) - .into_type(db) + .into_type(db, env) } else { - callables.into_type(db) + callables.into_type(db, env) } }) else { @@ -3736,7 +3782,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { "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) + actual_type = argument_type.display(db, env) )); } if arguments_slice.is_tuple_expr() { @@ -3794,7 +3840,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } _ => { let narrowed = self.infer_type_expression(arguments_slice); - let expanded = narrowed.expand_eagerly(self.db()); + let expanded = narrowed.expand_eagerly(db, env); if expanded.is_divergent() { expanded @@ -3818,12 +3864,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { Type::unknown() } - _ => TypeGuardType::unbound( - self.db(), - // Unlike `TypeIs`, don't use top materialization, because - // `TypeGuard` clobbering behavior makes it counterintuitive - self.infer_type_expression(arguments_slice), - ), + _ => TypeGuardType::unbound(self.db(), self.infer_type_expression(arguments_slice)), }, SpecialFormType::Concatenate => { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { @@ -3884,10 +3925,40 @@ impl<'db> TypeInferenceBuilder<'db, '_> { TypeExpressionFlags::UNPACK, ); - if self - .inference_flags() - .contains(InferenceFlags::IN_UNPACK_TYPE_ARGUMENT) + let inference_flags = self.inference_flags(); + let is_nested_unpack = + inference_flags.contains(InferenceFlags::IN_UNPACK_TYPE_ARGUMENT); + let is_nested_kwargs = inference_flags + .contains(InferenceFlags::IN_KWARG_ANNOTATION) + && inference_flags.contains(InferenceFlags::IN_NESTED_TYPE_EXPRESSION); + let is_invalid_context = !inference_flags.intersects( + InferenceFlags::IN_VARARG_ANNOTATION + | InferenceFlags::IN_KWARG_ANNOTATION + | InferenceFlags::IN_VALID_UNPACK_CONTEXT, + ); + + let previously_in_unpack_type_argument = self + .context + .inference_flags + .replace(InferenceFlags::IN_UNPACK_TYPE_ARGUMENT, true); + let inner_ty = if self.in_string_annotation() + && (is_nested_unpack || is_nested_kwargs || is_invalid_context) { + // Invalid string annotations never execute, so their operands must not + // produce runtime errors even though their inferred types are still needed. + let mut speculative = self.speculate_without_diagnostics(); + let inner_ty = speculative.infer_type_expression(arguments_slice); + self.extend(speculative); + inner_ty + } else { + self.infer_type_expression(arguments_slice) + }; + self.context.inference_flags.set( + InferenceFlags::IN_UNPACK_TYPE_ARGUMENT, + previously_in_unpack_type_argument, + ); + + if is_nested_unpack { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { diagnostic::add_type_expression_reference_link( builder.into_diagnostic("`Unpack` cannot be nested"), @@ -3896,13 +3967,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { return Type::unknown(); } - if self - .inference_flags() - .contains(InferenceFlags::IN_KWARG_ANNOTATION) - && self - .inference_flags() - .contains(InferenceFlags::IN_NESTED_TYPE_EXPRESSION) - { + if is_nested_kwargs { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { diagnostic::add_type_expression_reference_link(builder.into_diagnostic( "`Unpack` is only valid as the top-level `**kwargs` annotation form", @@ -3911,11 +3976,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { return Type::unknown(); } - if !self.inference_flags().intersects( - InferenceFlags::IN_VARARG_ANNOTATION - | InferenceFlags::IN_KWARG_ANNOTATION - | InferenceFlags::IN_VALID_UNPACK_CONTEXT, - ) { + if is_invalid_context { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { diagnostic::add_type_expression_reference_link(builder.into_diagnostic( format_args!( @@ -3927,16 +3988,6 @@ impl<'db> TypeInferenceBuilder<'db, '_> { return Type::unknown(); } - let previously_in_unpack_type_argument = self - .context - .inference_flags - .replace(InferenceFlags::IN_UNPACK_TYPE_ARGUMENT, true); - let inner_ty = self.infer_type_expression(arguments_slice); - self.context.inference_flags.set( - InferenceFlags::IN_UNPACK_TYPE_ARGUMENT, - previously_in_unpack_type_argument, - ); - if self .inference_flags() .contains(InferenceFlags::IN_KWARG_ANNOTATION) @@ -3961,7 +4012,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { "`Unpack` can only unpack a tuple type or `TypeVarTuple`", )); } - Type::homogeneous_tuple(self.db(), Type::unknown()) + Type::homogeneous_tuple(db, env, Type::unknown()) } } SpecialFormType::NoReturn @@ -4065,6 +4116,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { &mut self, parameters: &'param ast::Expr, ) -> Result, Vec<&'param ast::Expr>> { + let db = self.db(); + let env = self.program_environment(); let ty = match parameters { ast::Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => { let value_ty = self.infer_expression(value, TypeContext::default()); @@ -4084,7 +4137,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } ast::Expr::Tuple(tuple) if !tuple.parenthesized => { let mut errors = vec![]; - let mut builder = UnionBuilder::new(self.db()); + let mut builder = UnionBuilder::new(db, env); for elt in tuple { match self.infer_literal_parameter_type(elt) { Ok(ty) => { @@ -4139,8 +4192,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { match subscript_ty { // type aliases to literal types Type::KnownInstance(KnownInstanceType::TypeAliasType(type_alias)) => { - let value_ty = type_alias.value_type(self.db()); - if value_ty.is_literal_or_union_of_literals(self.db()) { + let value_ty = type_alias.value_type(db); + if value_ty.is_literal_or_union_of_literals(db, env) { return Ok(value_ty); } } @@ -4154,7 +4207,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } // `Literal[SingletonEnum.Member]`, where `SingletonEnum.Member` simplifies to // just `SingletonEnum`. - Type::NominalInstance(_) if subscript_ty.is_enum(self.db()) => { + Type::NominalInstance(_) if subscript_ty.is_enum(db, env) => { return Ok(subscript_ty); } // suppress false positives for e.g. members of functional-syntax enums @@ -4186,10 +4239,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> { /// /// It returns `None` if the argument is invalid i.e., not a list of types, parameter /// specification, `typing.Concatenate`, or `...`. - pub(super) fn infer_callable_parameter_types( + fn infer_callable_parameter_types( &mut self, parameters: &ast::Expr, ) -> Option> { + let env = self.program_environment(); + let db = self.db(); match parameters { ast::Expr::EllipsisLiteral(ast::ExprEllipsisLiteral { .. }) => { return Some(Parameters::gradual_form()); @@ -4285,7 +4340,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } } } - return Some(Parameters::from_annotation(self.db(), params)); + return Some(Parameters::from_annotation(self.db(), env, params)); } ast::Expr::List(ast::ExprList { elts: params, .. }) => { if let [ast::Expr::EllipsisLiteral(_)] = ¶ms[..] { @@ -4335,7 +4390,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { previously_in_valid_unpack_context, ); - return Some(Parameters::from_annotation(self.db(), parameters)); + return Some(Parameters::from_annotation(db, env, parameters)); } ast::Expr::Subscript(subscript) => { let value_ty = self.infer_expression(&subscript.value, TypeContext::default()); @@ -4371,7 +4426,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if let Type::TypeVar(tvar) = parameters_type && tvar.is_paramspec(self.db()) { - return Some(Parameters::paramspec(self.db(), tvar)); + return Some(Parameters::paramspec(db, tvar)); } if parameters_type == Type::Dynamic(DynamicType::InvalidConcatenateUnknown) { // Avoid emitting a confusing error here saying that the first argument to @@ -4422,6 +4477,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { &mut self, subscript: &ast::ExprSubscript, ) -> Parameters<'db> { + let db = self.db(); let previous_concatenate_context = self .context .inference_flags @@ -4496,7 +4552,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let parameters = self .infer_concatenate_tail(last_arg) - .map(|tail| Parameters::concatenate(self.db(), prefix_params, tail)); + .map(|tail| Parameters::concatenate(db, prefix_params, tail)); if arguments_slice.is_tuple_expr() { // TODO: What type to store for the argument slice in `Concatenate` because @@ -4591,11 +4647,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { /// /// Returns `Unknown` as a fallback if the type variable is unbound, otherwise returns the /// original type unchanged. - pub(super) fn check_for_unbound_type_variable( - &self, - expression: &ast::Expr, - ty: Type<'db>, - ) -> Type<'db> { + fn check_for_unbound_type_variable(&self, expression: &ast::Expr, ty: Type<'db>) -> Type<'db> { if !self .inference_flags() .contains(InferenceFlags::CHECK_UNBOUND_TYPEVARS) @@ -4722,12 +4774,13 @@ pub(super) fn resolve_use_site_variance_class<'db, 'ast>( /// Falls back to `Unknown` if the outer is not a class. fn resolve_use_site_variance<'db, 'ast>( db: &'db dyn crate::Db, + env: &ProgramEnvironment<'db>, value_ty: Type<'db>, elements: &[VarianceSliceElement<'ast>], infer_inner: impl FnMut(&'ast ast::Expr) -> Type<'db>, ) -> Type<'db> { match resolve_use_site_variance_class(db, value_ty, elements, infer_inner) { - Some(class_type) => Type::instance(db, class_type), + Some(class_type) => Type::instance(db, env, class_type), None => Type::unknown(), } } diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_form.rs b/crates/ty_python_semantic/src/types/infer/builder/type_form.rs index 57157e9ef7..88dfa725aa 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_form.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_form.rs @@ -18,15 +18,18 @@ impl<'db> TypeInferenceBuilder<'db, '_> { expression: &ast::Expr, target: Type<'db>, ) -> Option> { - let non_type_form_fallback = match target.resolve_type_alias(self.db()) { + let db = self.db(); + let env = self.program_environment(); + let non_type_form_fallback = match target.resolve_type_alias(db) { Type::TypeForm(_) => None, Type::Union(union) - if union.elements(self.db()).iter().any(|element| { - matches!(element.resolve_type_alias(self.db()), Type::TypeForm(_)) - }) => + if union + .elements(self.db()) + .iter() + .any(|element| matches!(element.resolve_type_alias(db), Type::TypeForm(_))) => { - Some(target.filter_union(self.db(), |element| { - !matches!(element.resolve_type_alias(self.db()), Type::TypeForm(_)) + Some(target.filter_union(db, |element| { + !matches!(element.resolve_type_alias(db), Type::TypeForm(_)) })) } _ => return None, @@ -37,10 +40,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let value_ty = self .speculate_without_diagnostics() .infer_maybe_standalone_expression(expression, TypeContext::default()); - if matches!(value_ty.resolve_type_alias(self.db()), Type::Never) + if matches!(value_ty.resolve_type_alias(db), Type::Never) || self.contains_type_form_value(expression, value_ty) || non_type_form_fallback - .is_some_and(|alternative| value_ty.is_assignable_to(self.db(), alternative)) + .is_some_and(|alternative| value_ty.is_assignable_to(db, env, alternative)) { return None; } @@ -56,7 +59,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let contextual_ty = self .speculate_without_diagnostics() .infer_value_expression_impl(expression, TypeContext::new(Some(target))); - if contextual_ty.is_assignable_to(self.db(), target) { + if contextual_ty.is_assignable_to(db, env, target) { return None; } } @@ -78,6 +81,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ty: Type<'db>, visitor: &ContainsTypeFormValueVisitor<'db>, ) -> bool { + let db = builder.db(); + let env = builder.program_environment(); match ty { Type::TypeForm(_) | Type::SubclassOf(_) => true, // A bare class object is valid type-expression syntax and should still be @@ -99,18 +104,18 @@ impl<'db> TypeInferenceBuilder<'db, '_> { Type::Intersection(intersection) => intersection .iter_positive(builder.db()) .any(|element| imp(builder, expression, element, visitor)), - Type::TypeAlias(alias) => visitor.visit(builder.db(), ty, || { - imp(builder, expression, alias.value_type(builder.db()), visitor) + Type::TypeAlias(alias) => visitor.visit(db, ty, || { + imp(builder, expression, alias.value_type(db), visitor) }), - Type::TypeVar(typevar) => visitor.visit(builder.db(), ty, || { + Type::TypeVar(typevar) => visitor.visit(db, ty, || { typevar .typevar(builder.db()) - .bound_or_constraints(builder.db()) + .bound_or_constraints(db, env) .is_some_and(|bound_or_constraints| { imp( builder, expression, - bound_or_constraints.as_type(builder.db()), + bound_or_constraints.as_type(db, env), visitor, ) }) diff --git a/crates/ty_python_semantic/src/types/infer/builder/typed_dict.rs b/crates/ty_python_semantic/src/types/infer/builder/typed_dict.rs index e786503ad1..fdadf7c8ee 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/typed_dict.rs @@ -4,8 +4,9 @@ use rustc_hash::FxHashMap; use smallvec::SmallVec; use strum::IntoEnumIterator; -use super::TypeInferenceBuilder; +use super::{ArgumentsIter, TypeInferenceBuilder}; use crate::types::class::{ClassLiteral, DynamicTypedDictAnchor, DynamicTypedDictLiteral}; +use crate::types::cyclic::ActiveRecursionDetector; use crate::types::diagnostic::{ INVALID_ARGUMENT_TYPE, INVALID_TYPE_FORM, MISSING_ARGUMENT, TOO_MANY_POSITIONAL_ARGUMENTS, UNKNOWN_ARGUMENT, report_mismatched_type_name, @@ -18,12 +19,37 @@ use crate::types::typed_dict::{ validate_typed_dict_constructor, validate_typed_dict_dict_literal, }; use crate::types::{ - IntersectionType, KnownClass, Type, TypeAndQualifiers, TypeContext, TypedDictModule, - TypedDictType, + ClassType, IntersectionType, KnownClass, Type, TypeAndQualifiers, TypeContext, TypedDictModule, + TypedDictType, any_over_type, }; -use crate::{Program, TypeQualifiers}; +use crate::{Db, ProgramEnvironment, TypeQualifiers}; use ty_python_core::definition::Definition; +/// Returns whether a field type contains a `TypedDict` with unresolved type variables. +/// +/// Structural wrappers and type aliases are traversed. Revisiting an alias definition counts as a +/// match so aliases that grow with every specialization cannot recurse indefinitely: +/// +/// ```python +/// type Growing[T] = T | Growing[list[T]] +/// ``` +fn contains_generic_typed_dict<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + active_aliases: &ActiveRecursionDetector>, +) -> bool { + any_over_type(db, env, ty, false, |nested| match nested { + Type::TypedDict(_) => nested.has_typevar(db, env), + Type::TypeAlias(alias) => active_aliases.visit( + &alias.definition(db), + || true, + || contains_generic_typed_dict(db, env, alias.value_type(db), active_aliases), + ), + _ => false, + }) +} + /// The shape of a `TypedDict` constructor call that affects how we prepare it for inference. #[derive(Debug, Clone, Copy)] pub(super) enum TypedDictConstructorForm<'expr> { @@ -75,8 +101,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { definition: Option>, typed_dict_module: TypedDictModule, ) -> Type<'db> { + let env = self.program_environment(); let db = self.db(); - let ast::Arguments { args, keywords, @@ -93,9 +119,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // it would return a class that is a subclass of `Mapping[str, object]` // with an unknown set of fields. let fallback = || { - let spec = &[KnownClass::Str.to_instance(db), Type::object()]; - let str_object_map = KnownClass::Mapping.to_specialized_subclass_of(db, spec); - IntersectionType::from_two_elements(db, str_object_map, Type::unknown()) + let spec = &[KnownClass::Str.to_instance(db, env), Type::object()]; + let str_object_map = KnownClass::Mapping.to_specialized_subclass_of(db, env, spec); + IntersectionType::from_two_elements(db, env, str_object_map, Type::unknown()) }; // Emit diagnostic for unsupported variadic arguments. @@ -148,7 +174,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut extra_items = None; let supports_pep_728 = self.in_stub() || typed_dict_module == TypedDictModule::TypingExtensions - || Program::get(db).python_version(db) >= PythonVersion::PY315; + || self.program_environment().python_version(db) >= PythonVersion::PY315; for kw in keywords { let Some(arg) = &kw.arg else { @@ -174,20 +200,20 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut diagnostic = builder.into_diagnostic(format_args!( "Invalid argument to parameter `{arg_name}` of `TypedDict()`" )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Expected either `True` or `False`, got object of type `{}`", - kw_type.display(db) + kw_type.display(db, env) )); } if arg_name == "total" { - if kw_type.bool(db).is_always_false() { + if kw_type.bool(db, env).is_always_false() { total = false; - } else if !kw_type.bool(db).is_always_true() { + } else if !kw_type.bool(db, env).is_always_true() { total = true; } } else { - closed = kw_type.bool(db).is_always_true(); + closed = kw_type.bool(db, env).is_always_true(); } } "extra_items" => { @@ -269,15 +295,15 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .map(|literal| literal.value(db)); if name.is_none() - && !name_type.is_assignable_to(db, KnownClass::Str.to_instance(db)) + && !name_type.is_assignable_to(db, env, KnownClass::Str.to_instance(db, env)) && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, name_arg) { let mut diagnostic = builder.into_diagnostic(format_args!( "Invalid argument to parameter `typename` of `TypedDict()`" )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Expected `str`, found `{}`", - name_type.display(db) + name_type.display(db, env) )); } else if let Some(definition) = definition && let Some(assigned_name) = definition.name(db) @@ -338,6 +364,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { typed_dict: TypedDictType<'db>, item_types: &mut FxHashMap>, ) -> Option> { + let db = self.db(); + let env = self.program_environment(); let ast::ExprDict { range: _, node_index: _, @@ -358,12 +386,14 @@ impl<'db> TypeInferenceBuilder<'db, '_> { && let Some(field) = typed_dict.item(self.db(), key.value(self.db())) { self.infer_expression(&item.value, TypeContext::new(Some(field.declared_ty))) - } else if key_ty.is_some_and(|key_ty| { - key_ty.is_assignable_to(self.db(), KnownClass::Str.to_instance(self.db())) - }) && let Some(value_ty) = - typed_dict.arbitrary_key_initialization_type(self.db()) - { - self.infer_expression(&item.value, TypeContext::new(Some(value_ty))) + } else if let Some(key_ty) = key_ty { + if key_ty.is_assignable_to(db, env, KnownClass::Str.to_instance(db, env)) + && let Some(value_ty) = typed_dict.arbitrary_key_initialization_type(db, env) + { + self.infer_expression(&item.value, TypeContext::new(Some(value_ty))) + } else { + self.infer_expression(&item.value, TypeContext::default()) + } } else { self.infer_expression(&item.value, TypeContext::default()) }; @@ -390,6 +420,178 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .map(|_| Type::TypedDict(typed_dict)) } + /// Infers and validates a `TypedDict` constructor through one call-binding pipeline. + /// + /// Bare generic constructors infer from direct keyword arguments. Other forms use the existing + /// field-directed validation and bind against the class's default specialization: + /// + /// ```python + /// Box(value=1) # Box[int] + /// Box({"value": 1}) # Box[Unknown] + /// ``` + pub(super) fn infer_typed_dict_constructor<'expr>( + &mut self, + callable_type: Type<'db>, + class: ClassType<'db>, + call_expression: &'expr ast::ExprCall, + call_expression_tcx: TypeContext<'db>, + ) -> Type<'db> { + let db = self.db(); + let env = self.program_environment(); + let typed_dict = TypedDictType::new(class); + let arguments = &call_expression.arguments; + let form = TypedDictConstructorForm::from_arguments(arguments); + let error_node: AnyNodeRef = call_expression.func.as_ref().into(); + let fallback_ty = callable_type + .to_instance_approximation(db, env) + .unwrap_or_else(Type::unknown); + let is_generic = matches!( + callable_type, + Type::ClassLiteral(class_literal) if class_literal.generic_context(db).is_some() + ); + if is_generic && arguments.args.is_empty() { + for keyword in &arguments.keywords { + if keyword.arg.is_none() && !keyword.value.is_dict_expr() { + self.get_or_infer_expression(&keyword.value, TypeContext::default()); + } + } + } + let can_infer = is_generic + && self.can_infer_generic_typed_dict_constructor(class, arguments, call_expression_tcx); + + if !can_infer { + self.prepare_typed_dict_constructor(typed_dict, form, arguments, error_node); + } + + let mut call_arguments = self.prepare_call_arguments(arguments); + let binding_callable = if is_generic && !can_infer { + class.class_literal(db).default_specialization(db).into() + } else { + callable_type + }; + let mut bindings = + self.bindings_for_call(binding_callable) + .match_parameters(db, env, &call_arguments); + + if can_infer && !bindings.satisfies(|_| true) { + self.prepare_typed_dict_constructor(typed_dict, form, arguments, error_node); + return fallback_ty; + } + + let result = self.infer_and_check_argument_types( + ArgumentsIter::from_ast(arguments), + &mut call_arguments, + &mut |builder, (_, expr, tcx)| { + if can_infer { + builder.infer_expression(expr, tcx) + } else { + builder.get_or_infer_expression(expr, tcx) + } + }, + &mut bindings, + call_expression_tcx, + ); + + if result.is_err() { + if can_infer + && arguments.keywords.iter().any(|keyword| { + keyword + .arg + .as_ref() + .and_then(|name| typed_dict.item(db, name.id.as_str())) + .is_some_and(|field| { + !self.expression_type(&keyword.value).is_assignable_to( + db, + env, + field.declared_ty, + ) + }) + }) + { + validate_typed_dict_constructor( + &self.context, + typed_dict, + arguments, + error_node, + |expr, _| self.expression_type(expr), + ); + return fallback_ty; + } + + bindings.report_diagnostics(&self.context, call_expression.into()); + } + + bindings.return_type(db, env) + } + + /// Returns whether constructor arguments can safely constrain a generic `TypedDict`. + /// + /// Mapping arguments, unresolved nested `TypedDict` fields, and gradual expected types remain + /// on the field-directed path so sibling arguments cannot force an unsound specialization: + /// + /// ```python + /// Outer(inner=Inner(value=1), marker="x") # Outer[Unknown] + /// ``` + /// + /// TODO: Remove this gate once ordinary generic call inference can safely handle mapping + /// arguments, nested `TypedDict` fields, and unresolved contextual type arguments. + fn can_infer_generic_typed_dict_constructor( + &self, + class: ClassType<'db>, + arguments: &ast::Arguments, + call_expression_tcx: TypeContext<'db>, + ) -> bool { + let db = self.db(); + let env = self.program_environment(); + let class_literal = class.class_literal(db); + + // An inner `Node(value=1)` must retain `Node[Unknown]` when its enclosing + // `Node(child=...)` cannot infer through the recursive field. + let has_gradual_class_context = class_literal + .as_static() + .zip(call_expression_tcx.annotation()) + .is_some_and(|(class_literal, annotation)| { + any_over_type(db, env, annotation.resolve_type_alias(db), false, |ty| { + ty.resolve_type_alias(db) + .specialization_of(db, env, class_literal) + .is_some_and(|specialization| { + specialization + .types(db) + .iter() + .any(|ty| ty.is_unknown() || ty.has_typevar(db, env)) + }) + }) + }); + let typed_dict = TypedDictType::new(class_literal.identity_specialization(db)); + + arguments.args.is_empty() + && !has_gradual_class_context + && arguments.keywords.iter().all(|keyword| { + let permits_field_inference = |name: &str| { + typed_dict.item(db, name).is_none_or(|field| { + !contains_generic_typed_dict( + db, + env, + field.declared_ty, + &ActiveRecursionDetector::default(), + ) + }) + }; + + if let Some(name) = keyword.arg.as_ref() { + return permits_field_inference(name.id.as_str()); + } + + self.try_expression_type(&keyword.value) + .and_then(|ty| ty.resolve_type_alias(db).as_typed_dict()) + .is_some_and(|unpacked| { + unpacked.items(db).iter().all(|(name, field)| { + field.is_required() && permits_field_inference(name.as_str()) + }) + }) + }) + } + /// Prepare a `TypedDict` constructor call before general argument inference. /// /// This gives constructor values the declared field type as context, then validates the full @@ -404,6 +606,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { arguments: &'expr ast::Arguments, error_node: AnyNodeRef<'expr>, ) { + let db = self.db(); match form { TypedDictConstructorForm::LiteralOnly(argument) => { let target_ty = Type::TypedDict(typed_dict); @@ -420,14 +623,14 @@ impl<'db> TypeInferenceBuilder<'db, '_> { self.get_or_infer_expression(expr, tcx) }); let keyword_keys = collect_guaranteed_keyword_keys( - self.db(), + db, + self.program_environment(), typed_dict, arguments, &unpacked_keyword_types, &mut |expr, tcx| self.get_or_infer_expression(expr, tcx), ); - let positional_target = - typed_dict_with_relaxed_keys(self.db(), typed_dict, &keyword_keys); + let positional_target = typed_dict_with_relaxed_keys(db, typed_dict, &keyword_keys); let target_ty = Type::TypedDict(positional_target); self.get_or_infer_expression(&arguments.args[0], TypeContext::new(Some(target_ty))); } @@ -485,6 +688,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { typed_dict: TypedDictType<'db>, dict_expr: &ast::ExprDict, ) { + let db = self.db(); + let env = self.program_environment(); let key_tcx = TypeContext::new(self.typed_dict_key_expected_type(Type::TypedDict(typed_dict))); @@ -497,10 +702,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> { && let Some(field) = typed_dict.item(self.db(), key.value(self.db())) { TypeContext::new(Some(field.declared_ty)) - } else if key_ty.is_some_and(|key_ty| { - key_ty.is_assignable_to(self.db(), KnownClass::Str.to_instance(self.db())) - }) { - TypeContext::new(typed_dict.arbitrary_key_initialization_type(self.db())) + } else if let Some(key_ty) = key_ty { + if key_ty.is_assignable_to(db, env, KnownClass::Str.to_instance(db, env)) { + TypeContext::new(typed_dict.arbitrary_key_initialization_type(db, env)) + } else { + TypeContext::default() + } } else { TypeContext::default() }; @@ -638,7 +845,6 @@ impl<'db> TypeInferenceBuilder<'db, '_> { /// themselves. fn validate_fields_arg(&mut self, fields_arg: &ast::Expr) { let db = self.db(); - if let ast::Expr::Dict(dict_expr) = fields_arg { for ast::DictItem { key, value } in dict_expr { if let Some(key) = key { @@ -650,8 +856,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { "Expected a string-literal key \ in the `fields` dict of `TypedDict()`", ); - diagnostic - .set_primary_message(format_args!("Found `{}`", key_type.display(db))); + diagnostic.set_primary_annotation_message(format_args!( + "Found `{}`", + key_type.display(db, self.program_environment()) + )); } } else { self.infer_expression(value, TypeContext::default()); diff --git a/crates/ty_python_semantic/src/types/infer/builder/typevar.rs b/crates/ty_python_semantic/src/types/infer/builder/typevar.rs index f60f4e64a7..067504305e 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/typevar.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/typevar.rs @@ -1,5 +1,4 @@ use crate::{ - Program, reachability::is_reachable, types::{ BindingContext, KnownClass, KnownInstanceType, LintDiagnosticGuard, Truthiness, Type, @@ -188,6 +187,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } pub(super) fn infer_typevar_deferred(&mut self, node: &'ast ast::TypeParamTypeVar) { + let env = self.program_environment(); let ast::TypeParamTypeVar { range: _, node_index: _, @@ -238,7 +238,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .iter() .map(|expr| { let constraint = self.infer_type_expression(expr); - if constraint.has_typevar_or_typevar_instance(db) + if constraint.has_typevar_or_typevar_instance(db, env) && let Some(builder) = self .context .report_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS, expr) @@ -252,7 +252,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // the tuple holding the set is a node of its own, and so needs a type; a mapping // written as a bare type *is* its single member, already inferred above if let Some(bound_expr @ ast::Expr::Tuple(_)) = bound_node { - let tuple_ty = Type::heterogeneous_tuple(db, constraint_tys.clone()); + let tuple_ty = Type::heterogeneous_tuple(db, env, constraint_tys.clone()); self.store_expression_type(bound_expr, tuple_ty); } // Mirror the `< 2` guard from `infer_typevar_definition` to avoid @@ -278,7 +278,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .iter() .map(|expr| self.infer_type_expression(expr)) .collect(); - let bound_ty = Type::heterogeneous_tuple(db, elem_tys); + let bound_ty = Type::heterogeneous_tuple(db, env, elem_tys); self.store_expression_type(bound_expr, bound_ty); Some(TypeVarBoundOrConstraints::UpperBound(bound_ty)) } @@ -294,15 +294,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let lower_bound_ty = lower_bound.as_deref().map(|lower_expr| { let lower_ty = self.infer_type_variable_bound_end(lower_expr, "lower"); if let Some(TypeVarBoundOrConstraints::UpperBound(upper_ty)) = bound_or_constraints - && !lower_ty.is_assignable_to(db, upper_ty) + && !lower_ty.is_assignable_to(db, env, upper_ty) && let Some(builder) = self .context .report_lint(&INVALID_TYPE_VARIABLE_BOUND, lower_expr) { let mut diagnostic = builder.into_diagnostic(format_args!( "TypeVar lower bound `{lower}` is not assignable to its upper bound `{upper}`", - lower = lower_ty.display(db), - upper = upper_ty.display(db), + lower = lower_ty.display(db, env), + upper = upper_ty.display(db, env), )); diagnostic.info( "no type satisfies this bound range, so the type variable cannot be specialized", @@ -315,7 +315,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // the default is a specialization like any other, so it has to sit above the lower // end too — `validate_typevar_default` only knows about the upper end if let Some((lower_expr, lower_ty)) = lower_bound_ty - && !lower_ty.is_assignable_to(db, default_ty) + && !lower_ty.is_assignable_to(db, env, default_ty) && let Some(builder) = self .context .report_lint(&INVALID_TYPE_VARIABLE_DEFAULT, default_expr) @@ -357,6 +357,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// Infer one end of a type variable's bound, named by `end` in any diagnostic. basedpython /// bound ranges `T: Lower..Upper` have two ends; every other form has only an upper bound. fn infer_type_variable_bound_end(&mut self, bound: &ast::Expr, end: &str) -> Type<'db> { + let env = self.program_environment(); let previously_in_type_variable_bound = self .context .inference_flags @@ -367,7 +368,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { previously_in_type_variable_bound, ); - if bound_ty.has_non_self_typevar_or_typevar_instance(self.db()) + if bound_ty.has_non_self_typevar_or_typevar_instance(self.db(), env) && let Some(builder) = self .context .report_lint(&INVALID_TYPE_VARIABLE_BOUND, bound) @@ -387,6 +388,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { default_node: &ast::Expr, bound_or_constraints_nodes: Option>, ) { + let env = self.program_environment(); let Some(bound_or_constraints) = bound_or_constraints else { return; }; @@ -456,11 +458,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Annotate the diagnostic with the definition span of the default TypeVar. let annotate_default_definition = |diagnostic: &mut LintDiagnosticGuard<'_, '_>| { if let Some(definition) = default_typevar.definition(db) { - let file = definition.file(db); diagnostic.annotate( - Annotation::secondary(Span::from( - definition.full_range(db, &parsed_module(db, file).load(db)), - )) + Annotation::secondary(Span::from(definition.full_range( + db, + &parsed_module(db, definition.python_file(db)).load(db), + ))) .message(format_args!("`{default_name}` defined here")), ); } @@ -471,17 +473,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Default TypeVar's upper bound must be assignable to outer's bound. // If the default has constraints, all constraints must be assignable // to the outer bound. - if let Some(default_constraints) = default_typevar.constraints(db) { + if let Some(default_constraints) = default_typevar.constraints(db, env) { for constraint in default_constraints { - if !constraint.is_assignable_to(db, outer_bound) { + if !constraint.is_assignable_to(db, env, outer_bound) { if let Some(mut diagnostic) = not_assignable_to_upper_bound() { annotate_default_definition(&mut diagnostic); if let Some(name) = name { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Constraint `{constraint}` of default \ `{default_name}` is not assignable to upper \ bound of `{name}`", - constraint = constraint.display(db), + constraint = constraint.display(db, env), )); diagnostic.set_concise_message(format_args!( "Default `{default_name}` of TypeVar `{name}` \ @@ -489,23 +491,23 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { of `{name}` because constraint `{constraint}` \ of `{default_name}` is not assignable to \ `{bound}`", - bound = outer_bound.display(db), - constraint = constraint.display(db), + bound = outer_bound.display(db, env), + constraint = constraint.display(db, env), )); } else { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Constraint `{constraint}` of `{default_name}` is \ not assignable to upper bound `{bound}` of \ outer TypeVar", - constraint = constraint.display(db), - bound = outer_bound.display(db), + constraint = constraint.display(db, env), + bound = outer_bound.display(db, env), )); diagnostic.set_concise_message(format_args!( "Default of TypeVar is not assignable its upper \ bound `{bound}` because constraint `{constraint}` \ of `{default_name}` is not assignable to `{bound}`", - bound = outer_bound.display(db), - constraint = constraint.display(db), + bound = outer_bound.display(db, env), + constraint = constraint.display(db, env), )); } } @@ -513,17 +515,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } } else { - let default_bound = - default_typevar.upper_bound(db).unwrap_or_else(Type::object); - if !default_bound.is_assignable_to(db, outer_bound) { + let default_bound = default_typevar + .upper_bound(db, env) + .unwrap_or_else(Type::object); + if !default_bound.is_assignable_to(db, env, outer_bound) { if let Some(mut diagnostic) = not_assignable_to_upper_bound() { annotate_default_definition(&mut diagnostic); if let Some(name) = name { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Upper bound `{default_bound}` of default \ `{default_name}` is not assignable to upper \ bound of `{name}`", - default_bound = default_bound.display(db), + default_bound = default_bound.display(db, env), )); diagnostic.set_concise_message(format_args!( "Default `{default_name}` of TypeVar `{name}` \ @@ -531,15 +534,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { of `{name}` because its upper bound \ `{default_bound}` is not assignable to \ `{bound}`", - bound = outer_bound.display(db), - default_bound = default_bound.display(db), + bound = outer_bound.display(db, env), + default_bound = default_bound.display(db, env), )); } else { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Upper bound `{default_bound}` of default \ `{default_name}` is not assignable to upper \ bound of outer TypeVar", - default_bound = default_bound.display(db), + default_bound = default_bound.display(db, env), )); diagnostic.set_concise_message(format_args!( "TypeVar default `{default_name}` is not \ @@ -547,8 +550,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { because upper bound of `{default_name}` (`{default_bound}`) is not assignable to `{bound}`", - bound = outer_bound.display(db), - default_bound = default_bound.display(db), + bound = outer_bound.display(db, env), + default_bound = default_bound.display(db, env), )); } } @@ -558,21 +561,21 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { TypeVarBoundOrConstraints::Constraints(outer_constraints) => { // TypeVar default with constrained outer. let outer = outer_constraints.elements(db); - if let Some(default_constraints) = default_typevar.constraints(db) { + if let Some(default_constraints) = default_typevar.constraints(db, env) { // Default has constraints: outer constraints must be a superset. for default_constraint in default_constraints { if !outer .iter() - .any(|o| default_constraint.is_equivalent_to(db, *o)) + .any(|o| default_constraint.is_equivalent_to(db, env, *o)) { if let Some(mut diagnostic) = inconsistent_with_constraints() { annotate_default_definition(&mut diagnostic); if let Some(name) = name { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Constraint `{constraint}` of default \ `{default_name}` is not one of the constraints \ of `{name}`", - constraint = default_constraint.display(db), + constraint = default_constraint.display(db, env), )); diagnostic.set_concise_message(format_args!( "Default `{default_name}` of TypeVar `{name}` \ @@ -580,14 +583,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { `{name}` because constraint `{constraint}` of \ `{default_name}` is not one of the constraints \ of `{name}`", - constraint = default_constraint.display(db), + constraint = default_constraint.display(db, env), )); } else { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Constraint `{constraint}` of outer TypeVar default \ `{default_name}` is not one of the constraints \ of the outer TypeVar", - constraint = default_constraint.display(db), + constraint = default_constraint.display(db, env), )); diagnostic.set_concise_message(format_args!( "Default `{default_name}` of outer TypeVar is \ @@ -595,7 +598,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { TypeVar because constraint `{constraint}` of \ default `{default_name}` is not one of the \ constraints of the outer TypeVar", - constraint = default_constraint.display(db), + constraint = default_constraint.display(db, env), )); } } @@ -607,17 +610,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // incompatible with a constrained outer TypeVar per the typing spec. if let Some(mut diagnostic) = inconsistent_with_constraints() { annotate_default_definition(&mut diagnostic); - if let Some(default_bound) = default_typevar.upper_bound(db) { - diagnostic.set_primary_message( + if let Some(default_bound) = default_typevar.upper_bound(db, env) { + diagnostic.set_primary_annotation_message( "Bounded TypeVar cannot be used as the default \ for a constrained TypeVar", ); diagnostic.info(format_args!( "`{default_name}` has bound `{default_bound}` but is not constrained", - default_bound = default_bound.display(db), + default_bound = default_bound.display(db, env), )); } else { - diagnostic.set_primary_message( + diagnostic.set_primary_annotation_message( "Unbounded TypeVar cannot be used as the default \ for a constrained TypeVar", ); @@ -635,12 +638,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Concrete default type checks. match bound_or_constraints { TypeVarBoundOrConstraints::UpperBound(bound) => { - if !default_ty.is_assignable_to(db, bound) { + if !default_ty.is_assignable_to(db, env, bound) { if let Some(mut diagnostic) = not_assignable_to_upper_bound() { if let Some(name) = name { - diagnostic.set_primary_message(format_args!("Default of `{name}`")); + diagnostic.set_primary_annotation_message(format_args!( + "Default of `{name}`" + )); } else { - diagnostic.set_primary_message("TypeVar default"); + diagnostic.set_primary_annotation_message("TypeVar default"); } diagnostic.set_concise_message(not_assignable_message); } @@ -651,18 +656,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { && !constraints .elements(db) .iter() - .any(|c| default_ty.is_equivalent_to(db, *c)) + .any(|c| default_ty.is_equivalent_to(db, env, *c)) { if let Some(mut diagnostic) = inconsistent_with_constraints() { if let Some(name) = name { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "`{default}` is not one of the constraints of `{name}`", - default = default_ty.display(db), + default = default_ty.display(db, env), )); } else { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "`{default}` is not one of the constraints", - default = default_ty.display(db), + default = default_ty.display(db, env), )); } } @@ -702,7 +707,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; let expected_binding = BindingContext::Definition(expected_binding_def); - let outer_tv = find_over_type(db, default_ty, false, |ty| { + let outer_tv = find_over_type(db, self.program_environment(), default_ty, false, |ty| { if let Type::TypeVar(bound_tv) = ty && bound_tv.binding_context(db) != expected_binding { @@ -726,7 +731,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut diagnostic = builder.into_diagnostic(format_args!( "Invalid default for type parameter `{typevar_name}`" )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "`{outer_name}` is a type parameter bound in an outer scope" )); diagnostic.set_concise_message(format_args!( @@ -734,10 +739,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { outer-scope type parameter `{outer_name}` as its default" )); if let Some(definition) = outer_typevar.definition(db) { - let file = definition.file(db); diagnostic.annotate( Annotation::secondary(Span::from( - definition.full_range(db, &parsed_module(db, file).load(db)), + definition + .full_range(db, &parsed_module(db, definition.python_file(db)).load(db)), )) .message(format_args!("`{outer_name}` defined here")), ); @@ -793,6 +798,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } pub(super) fn infer_paramspec_deferred(&mut self, node: &ast::TypeParamParamSpec) { + let env = self.program_environment(); let ast::TypeParamParamSpec { range: _, node_index: _, @@ -819,7 +825,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { builder.into_diagnostic(format_args!( "The whole-pack bound of a keyword-variadic pack must be a dict literal \ type or a `TypedDict`, not `{}`", - bound_ty.display(self.db()), + bound_ty.display(self.db(), env), )); } } @@ -889,7 +895,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); // N.B. We cannot represent a heterogeneous list of types in our type system, so we // use a heterogeneous tuple type to represent the list of types instead. - self.store_expression_type(default_expr, Type::heterogeneous_tuple(db, types)); + let ty = Type::heterogeneous_tuple(db, self.program_environment(), types); + self.store_expression_type(default_expr, ty); return; } ast::Expr::Name(_) => { @@ -1069,19 +1076,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { message: impl std::fmt::Display, node: impl Ranged, ) -> Type<'db> { + let db = context.db(); if let Some(builder) = context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, node) { builder.into_diagnostic(message); } - KnownClass::TypeVarTuple.to_instance(context.db()) + KnownClass::TypeVarTuple.to_instance(db, context.program_environment()) } + let env = self.program_environment(); let db = self.db(); let arguments = &call_expr.arguments; let is_typing_extensions = known_class == KnownClass::ExtensionsTypeVarTuple; let assume_all_features = self.in_stub() || is_typing_extensions; - let python_version = Program::get(db).python_version(db); - let have_features_from = - |version: PythonVersion| assume_all_features || python_version >= version; let mut default = None; let mut covariant = false; @@ -1128,7 +1134,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Some(self.infer_expression(&kwarg.value, TypeContext::default())); } "default" => { - if !have_features_from(PythonVersion::PY313) { + if !assume_all_features + && self.program_environment().python_version(db) < PythonVersion::PY313 + { error( &self.context, "The `default` parameter of `typing.TypeVarTuple` was added in Python 3.13", @@ -1138,7 +1146,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { default = Some(TypeVarDefaultEvaluation::Lazy); } "bound" => { - if !have_features_from(PythonVersion::PY315) { + if !assume_all_features + && self.program_environment().python_version(db) < PythonVersion::PY315 + { return error( &self.context, "The `bound` parameter of `typing.TypeVarTuple` was added in Python 3.15", @@ -1152,7 +1162,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); } "covariant" => { - if !have_features_from(PythonVersion::PY315) { + if !assume_all_features + && self.program_environment().python_version(db) < PythonVersion::PY315 + { error( &self.context, "The `covariant` parameter of `typing.TypeVarTuple` was added in Python 3.15", @@ -1161,7 +1173,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } match self .infer_expression(&kwarg.value, TypeContext::default()) - .bool(db) + .bool(db, env) { Truthiness::AlwaysTrue => covariant = true, Truthiness::AlwaysFalse => {} @@ -1176,7 +1188,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } "contravariant" => { - if !have_features_from(PythonVersion::PY315) { + if !assume_all_features + && self.program_environment().python_version(db) < PythonVersion::PY315 + { error( &self.context, "The `contravariant` parameter of `typing.TypeVarTuple` was added in Python 3.15", @@ -1185,7 +1199,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } match self .infer_expression(&kwarg.value, TypeContext::default()) - .bool(db) + .bool(db, env) { Truthiness::AlwaysTrue => contravariant = true, Truthiness::AlwaysFalse => {} @@ -1200,7 +1214,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } "infer_variance" => { - if !have_features_from(PythonVersion::PY315) { + if !assume_all_features + && self.program_environment().python_version(db) < PythonVersion::PY315 + { error( &self.context, "The `infer_variance` parameter of `typing.TypeVarTuple` was added in Python 3.15", @@ -1209,7 +1225,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } match self .infer_expression(&kwarg.value, TypeContext::default()) - .bool(db) + .bool(db, env) { Truthiness::AlwaysTrue => infer_variance = true, Truthiness::AlwaysFalse => {} @@ -1329,21 +1345,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { message: impl std::fmt::Display, node: impl Ranged, ) -> Type<'db> { + let db = context.db(); if let Some(builder) = context.report_lint(&INVALID_PARAMSPEC, node) { builder.into_diagnostic(message); } // If the call doesn't create a valid paramspec, we'll emit diagnostics and fall back to // just creating a regular instance of `typing.ParamSpec`. - KnownClass::ParamSpec.to_instance(context.db()) + KnownClass::ParamSpec.to_instance(db, context.program_environment()) } + let env = self.program_environment(); let db = self.db(); let arguments = &call_expr.arguments; let is_typing_extensions = known_class == KnownClass::ExtensionsParamSpec; let assume_all_features = self.in_stub() || is_typing_extensions; - let python_version = Program::get(db).python_version(db); - let have_features_from = - |version: PythonVersion| assume_all_features || python_version >= version; let mut default = None; let mut covariant = false; @@ -1399,7 +1414,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); } "infer_variance" => { - if !have_features_from(PythonVersion::PY312) { + if !assume_all_features + && self.program_environment().python_version(db) < PythonVersion::PY312 + { error( &self.context, "The `infer_variance` parameter of `typing.ParamSpec` was added in Python 3.12", @@ -1408,7 +1425,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } match self .infer_expression(&kwarg.value, TypeContext::default()) - .bool(db) + .bool(db, env) { Truthiness::AlwaysTrue => infer_variance = true, Truthiness::AlwaysFalse => {} @@ -1425,7 +1442,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { "covariant" => { match self .infer_expression(&kwarg.value, TypeContext::default()) - .bool(db) + .bool(db, env) { Truthiness::AlwaysTrue => covariant = true, Truthiness::AlwaysFalse => {} @@ -1442,7 +1459,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { "contravariant" => { match self .infer_expression(&kwarg.value, TypeContext::default()) - .bool(db) + .bool(db, env) { Truthiness::AlwaysTrue => contravariant = true, Truthiness::AlwaysFalse => {} @@ -1457,7 +1474,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } "default" => { - if !have_features_from(PythonVersion::PY313) { + if !assume_all_features + && self.program_environment().python_version(db) < PythonVersion::PY313 + { // We don't return here; this error is informational since this will error // at runtime, but the user's intent is plain, we may as well respect it. error( @@ -1576,21 +1595,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { message: impl std::fmt::Display, node: impl Ranged, ) -> Type<'db> { + let db = context.db(); if let Some(builder) = context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, node) { builder.into_diagnostic(message); } // If the call doesn't create a valid typevar, we'll emit diagnostics and fall back to // just creating a regular instance of `typing.TypeVar`. - KnownClass::TypeVar.to_instance(context.db()) + KnownClass::TypeVar.to_instance(db, context.program_environment()) } + let env = self.program_environment(); let db = self.db(); let arguments = &call_expr.arguments; let is_typing_extensions = known_class == KnownClass::ExtensionsTypeVar; let assume_all_features = self.in_stub() || is_typing_extensions; - let python_version = Program::get(db).python_version(db); - let have_features_from = - |version: PythonVersion| assume_all_features || python_version >= version; let mut has_bound = false; let mut default = None; @@ -1635,7 +1653,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { "covariant" => { match self .infer_expression(&kwarg.value, TypeContext::default()) - .bool(db) + .bool(db, env) { Truthiness::AlwaysTrue => covariant = true, Truthiness::AlwaysFalse => {} @@ -1652,7 +1670,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { "contravariant" => { match self .infer_expression(&kwarg.value, TypeContext::default()) - .bool(db) + .bool(db, env) { Truthiness::AlwaysTrue => contravariant = true, Truthiness::AlwaysFalse => {} @@ -1667,7 +1685,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } "default" => { - if !have_features_from(PythonVersion::PY313) { + if !assume_all_features + && self.program_environment().python_version(db) < PythonVersion::PY313 + { // We don't return here; this error is informational since this will error // at runtime, but the user's intent is plain, we may as well respect it. error( @@ -1680,7 +1700,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { default = Some(TypeVarDefaultEvaluation::Lazy); } "infer_variance" => { - if !have_features_from(PythonVersion::PY312) { + if !assume_all_features + && self.program_environment().python_version(db) < PythonVersion::PY312 + { // We don't return here; this error is informational since this will error // at runtime, but the user's intent is plain, we may as well respect it. error( @@ -1691,7 +1713,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } match self .infer_expression(&kwarg.value, TypeContext::default()) - .bool(db) + .bool(db, env) { Truthiness::AlwaysTrue => infer_variance = true, Truthiness::AlwaysFalse => {} @@ -1785,7 +1807,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { name_param_ty, ); } - let previous_definition_in = |scope, place, before| { let use_def = self.index.use_def_map(scope); use_def diff --git a/crates/ty_python_semantic/src/types/infer/comparisons.rs b/crates/ty_python_semantic/src/types/infer/comparisons.rs index e7ceb98806..0148426d50 100644 --- a/crates/ty_python_semantic/src/types/infer/comparisons.rs +++ b/crates/ty_python_semantic/src/types/infer/comparisons.rs @@ -1,23 +1,173 @@ +use crate::Db; use ruff_python_ast as ast; use ruff_text_size::TextRange; use smallvec::SmallVec; -use crate::Db; +use crate::ProgramEnvironment; use crate::types::call::{CallArguments, CallDunderError}; use crate::types::constraints::ConstraintSetBuilder; use crate::types::context::InferContext; use crate::types::cyclic::CycleDetector; use crate::types::equality::{ - ComparisonSoundnessPolicy, equality_truthiness, inequality_truthiness, + ComparisonSoundnessPolicy, TupleEqualityEvaluator, equality_truthiness, inequality_truthiness, }; -use crate::types::tuple::TupleSpec; +use crate::types::tuple::{Tuple, TupleSpec}; use crate::types::{ DynamicType, IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, - LiteralValueType, LiteralValueTypeKind, MemberLookupPolicy, Type, TypeContext, + LiteralValueType, LiteralValueTypeKind, MemberLookupPolicy, Type, TypeContext, TypeTransformer, TypeVarBoundOrConstraints, UnionBuilder, }; use ty_python_core::Truthiness; +impl<'db> Type<'db> { + /// Upcast `self` to a type that conservatively describes its possible runtime objects in an + /// identity comparison. + /// + /// A `NewType` constructor returns its argument unchanged, so its tag can differ between two + /// views of the same object: upcast a `NewType` to its concrete base. In contrast, preserve + /// invariant generic arguments because the same mutable object cannot satisfy incompatible + /// commitments such as `list[int]` and `list[str]` without some other code already being + /// unsound. + /// + /// Preserve negations that constrain the object itself, such as `~None`, `~SomeClass`, and + /// `~Literal[1]`. A `NewType` tag, type-variable selection, type-guard proof, or literal-string + /// origin can differ between views. A negated string literal excludes its runtime value only + /// when another constraint already establishes that the string has literal origin. + /// + /// A type variable can also hide a `NewType` tag: even a variable bounded by `int` can be + /// instantiated as an integer `NewType`. Expand variables to their upcast bounds or constraints + /// instead of transferring that potentially tagged relationship; an unbounded variable becomes + /// `object`. + /// + /// Use this upcast both to decide whether identity is possible and to narrow the other + /// operand when it succeeds. Each operand retains its own existing tags and type-variable + /// relationships when the resulting constraint is applied. + pub(crate) fn identity_comparison_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { + struct IdentityComparisonUpcasting; + + fn upcast<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + visitor: &TypeTransformer<'db, IdentityComparisonUpcasting>, + ) -> Type<'db> { + match ty { + Type::TypeAlias(alias) => { + visitor.visit_type(db, ty, || upcast(db, env, alias.value_type(db), visitor)) + } + Type::NewTypeInstance(newtype) => { + upcast(db, env, newtype.concrete_base_type(db), visitor) + } + Type::TypeVar(typevar) => visitor.visit_type(db, ty, || { + match typevar.typevar(db).bound_or_constraints(db, env) { + Some(bound_or_constraints) => { + upcast(db, env, bound_or_constraints.as_type(db, env), visitor) + } + None => KnownClass::Object.to_instance(db, env), + } + }), + Type::Union(union) => { + union.map(db, env, |element| upcast(db, env, *element, visitor)) + } + Type::Intersection(intersection) => { + let has_literal_string_origin = intersection + .positive(db) + .iter() + .any(|element| element.is_subtype_of(db, env, Type::literal_string())); + let mut builder = IntersectionBuilder::new(db, env); + for element in intersection.positive(db) { + builder = builder.add_positive(upcast(db, env, *element, visitor)); + } + for element in intersection.negative(db) { + // Static tags, predicate proofs, and literal-string origin can differ + // between views. Once literal origin is known, an excluded string literal + // also excludes its runtime value and must be preserved. + match element.resolve_type_alias(db) { + Type::NewTypeInstance(_) + | Type::TypeVar(_) + | Type::TypeIs(_) + | Type::TypeGuard(_) => continue, + Type::LiteralValue(literal) + if literal.is_literal_string() + || literal.is_string() && !has_literal_string_origin => + { + continue; + } + _ => builder.add_negative_in_place(*element), + } + } + builder.build() + } + _ => ty, + } + } + + upcast( + db, + env, + self, + &TypeTransformer::::default(), + ) + } + + /// Return whether values of these types always, never, or possibly identify the same object. + pub(crate) fn identity_comparison_truthiness( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + other: Type<'db>, + ) -> Truthiness { + let is_singleton_or_intersection_with_singleton = |ty: Type<'db>| { + ty.is_singleton(db, env) + || ty + .resolve_type_alias(db) + .as_intersection() + .is_some_and(|intersection| { + intersection + .positive(db) + .iter() + .any(|ty| ty.is_singleton(db, env)) + }) + }; + + // Two occurrences of the same constrained `TypeVar` require separate handling. Although + // different specializations can choose different singleton constraints, every occurrence in + // one specialization shares the same selected constraint and therefore the same object. + if let Type::TypeVar(left) = self.resolve_type_alias(db) + && let Type::TypeVar(right) = other.resolve_type_alias(db) + && left.is_same_typevar_as(db, right) + && is_singleton_or_intersection_with_singleton(Type::TypeVar(left)) + { + return Truthiness::AlwaysTrue; + } + + // `NewType` instances are identity functions at runtime, so distinct static types can still + // identify the same object. Compare the types of their possible runtime objects instead. + let left_identity = self.identity_comparison_type(db, env); + let right_identity = other.identity_comparison_type(db, env); + + // Non-disjoint singleton types do not necessarily identify the same object: disjointness can + // be inconclusive, for example when aliases between enum members cannot be determined. + // Require one singleton type to be a subtype of the other before concluding that they are + // definitely identical. + if left_identity.is_disjoint_from(db, env, right_identity) { + Truthiness::AlwaysFalse + } else if is_singleton_or_intersection_with_singleton(left_identity) + && is_singleton_or_intersection_with_singleton(right_identity) + && (left_identity.is_subtype_of(db, env, right_identity) + || right_identity.is_subtype_of(db, env, left_identity)) + { + Truthiness::AlwaysTrue + } else { + Truthiness::Ambiguous + } + } +} + /// Whether the intersection type is on the left or right side of the comparison. #[derive(Debug, Clone, Copy)] enum IntersectionOn { @@ -26,15 +176,15 @@ enum IntersectionOn { } /// A [`CycleDetector`] that is used in [`infer_binary_type_comparison`]. -pub(super) type BinaryComparisonVisitor<'db> = CycleDetector< +type BinaryComparisonVisitor<'db> = CycleDetector< 'db, ast::CmpOp, - (Type<'db>, ast::CmpOp, Type<'db>), + (Type<'db>, NonIdentityOperator, Type<'db>), Result, UnsupportedComparisonError<'db>>, 1, >; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] enum RichCompareOperator { Eq, Ne, @@ -83,17 +233,42 @@ impl RichCompareOperator { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum MembershipTestCompareOperator { +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum MembershipOperator { In, NotIn, } -impl From for ast::CmpOp { - fn from(value: MembershipTestCompareOperator) -> Self { +impl MembershipOperator { + const fn is_in(self) -> bool { + matches!(self, MembershipOperator::In) + } + + const fn is_not_in(self) -> bool { + matches!(self, MembershipOperator::NotIn) + } +} + +impl From for ast::CmpOp { + fn from(value: MembershipOperator) -> Self { match value { - MembershipTestCompareOperator::In => ast::CmpOp::In, - MembershipTestCompareOperator::NotIn => ast::CmpOp::NotIn, + MembershipOperator::In => ast::CmpOp::In, + MembershipOperator::NotIn => ast::CmpOp::NotIn, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum NonIdentityOperator { + Rich(RichCompareOperator), + Membership(MembershipOperator), +} + +impl From for ast::CmpOp { + fn from(value: NonIdentityOperator) -> Self { + match value { + NonIdentityOperator::Rich(rich_op) => rich_op.into(), + NonIdentityOperator::Membership(membership_op) => membership_op.into(), } } } @@ -128,6 +303,7 @@ pub(crate) struct UnsupportedComparisonError<'db> { /// directly, but the rich-comparison results are identical. pub(crate) fn deferred_comparison<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, left: Type<'db>, op: ast::CmpOp, right: Type<'db>, @@ -148,7 +324,15 @@ pub(crate) fn deferred_comparison<'db>( return Some(folded); } - infer_rich_comparison(db, left, right, rich, MemberLookupPolicy::default()).ok() + Type::try_call_rich_comparison_dunder( + db, + env, + left, + right, + rich.dunder(), + rich.reflect().dunder(), + MemberLookupPolicy::default(), + ) } /// Fold a rich comparison of two literal operands to a `Literal[bool]`, mirroring the @@ -202,77 +386,132 @@ pub(super) fn infer_binary_type_comparison<'db>( op: ast::CmpOp, right: Type<'db>, range: TextRange, +) -> Result, UnsupportedComparisonError<'db>> { + let db = context.db(); + let env = &context.program_environment(); + + let op = match op { + ast::CmpOp::Is | ast::CmpOp::IsNot => { + let truthiness = left + .identity_comparison_truthiness(db, env, right) + .negate_if(op == ast::CmpOp::IsNot); + return Ok(Type::from_truthiness(db, env, truthiness)); + } + ast::CmpOp::Eq => NonIdentityOperator::Rich(RichCompareOperator::Eq), + ast::CmpOp::NotEq => NonIdentityOperator::Rich(RichCompareOperator::Ne), + ast::CmpOp::Lt => NonIdentityOperator::Rich(RichCompareOperator::Lt), + ast::CmpOp::LtE => NonIdentityOperator::Rich(RichCompareOperator::Le), + ast::CmpOp::Gt => NonIdentityOperator::Rich(RichCompareOperator::Gt), + ast::CmpOp::GtE => NonIdentityOperator::Rich(RichCompareOperator::Ge), + ast::CmpOp::In => NonIdentityOperator::Membership(MembershipOperator::In), + ast::CmpOp::NotIn => NonIdentityOperator::Membership(MembershipOperator::NotIn), + }; + + infer_binary_type_comparison_inner( + context, + left, + op, + right, + range, + &BinaryComparisonVisitor::new(Ok(Type::bool_literal(true))), + ) +} + +fn infer_binary_type_comparison_inner<'db>( + context: &InferContext<'db, '_>, + left: Type<'db>, + op: NonIdentityOperator, + right: Type<'db>, + range: TextRange, visitor: &BinaryComparisonVisitor<'db>, ) -> Result, UnsupportedComparisonError<'db>> { let db = context.db(); + let env = &context.program_environment(); - // 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 let try_dunder = |policy: MemberLookupPolicy| { - let rich_comparison = |op| infer_rich_comparison(db, left, right, op, policy); + let rich_comparison = |op| infer_rich_comparison(context, left, right, op, policy); let membership_test_comparison = |op, range: TextRange| { infer_membership_test_comparison(context, 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(db, right) { - Ok(Type::bool_literal(false)) - } else if left.is_singleton(db) && left.is_equivalent_to(db, right) { - Ok(Type::bool_literal(true)) - } else { - Ok(KnownClass::Bool.to_instance(db)) - } - } - ast::CmpOp::IsNot => { - if left.is_disjoint_from(db, right) { - Ok(Type::bool_literal(true)) - } else if left.is_singleton(db) && left.is_equivalent_to(db, right) { - Ok(Type::bool_literal(false)) - } else { - Ok(KnownClass::Bool.to_instance(db)) - } + NonIdentityOperator::Rich(rich_op) => rich_comparison(rich_op), + NonIdentityOperator::Membership(membership_op) => { + membership_test_comparison(membership_op, range) } } }; let soundness_policy = ComparisonSoundnessPolicy::from_analysis_settings(db.analysis_settings(context.file())); + + if let NonIdentityOperator::Rich(rich_op) = op + && let Some(left_tuple) = left.tuple_instance_spec(db, env) + && let Some(right_tuple) = right.tuple_instance_spec(db, env) + { + return visitor.visit(db, (left, op, right), || { + infer_tuple_rich_comparison(context, &left_tuple, rich_op, &right_tuple, range, visitor) + }); + } + + if let NonIdentityOperator::Membership(op) = op + && let Some(right_tuple) = right.tuple_instance_spec(db, env) + && let Tuple::Fixed(right_tuple) = &*right_tuple + { + let mut any_eq = false; + let mut any_ambiguous = false; + let mut equality = TupleEqualityEvaluator::new(db, env, soundness_policy); + + for &element_ty in right_tuple.elements_slice() { + // 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`. + match equality + .element_truthiness(element_ty, left) + .unwrap_or_else(|error| error.fallback_truthiness()) + { + Truthiness::AlwaysTrue => any_eq = true, + Truthiness::AlwaysFalse => (), + Truthiness::Ambiguous => any_ambiguous = true, + } + } + + return Ok(if any_eq { + Type::bool_literal(op.is_in()) + } else if !any_ambiguous { + Type::bool_literal(op.is_not_in()) + } else { + KnownClass::Bool.to_instance(db, env) + }); + } + let comparison_truthiness = match op { - ast::CmpOp::Eq => equality_truthiness(db, left, right, soundness_policy), - ast::CmpOp::NotEq => inequality_truthiness(db, left, right, soundness_policy), + NonIdentityOperator::Rich(RichCompareOperator::Eq) => { + equality_truthiness(db, env, left, right, soundness_policy) + } + NonIdentityOperator::Rich(RichCompareOperator::Ne) => { + inequality_truthiness(db, env, left, right, soundness_policy) + } _ => Truthiness::Ambiguous, }; if comparison_truthiness != Truthiness::Ambiguous { - return Ok(Type::from_truthiness(db, comparison_truthiness)); + return Ok(Type::from_truthiness(db, env, comparison_truthiness)); } let comparison_result = match (left, right) { - (Type::EnumComplement(complement), right) => Some(infer_binary_type_comparison( + (Type::EnumComplement(complement), right) => Some(infer_binary_type_comparison_inner( context, - complement.remaining_literal_union(db), + complement.remaining_literal_union(db, env), op, right, range, visitor, )), - (left, Type::EnumComplement(complement)) => Some(infer_binary_type_comparison( + (left, Type::EnumComplement(complement)) => Some(infer_binary_type_comparison_inner( context, left, op, - complement.remaining_literal_union(db), + complement.remaining_literal_union(db, env), range, visitor, )), @@ -282,10 +521,9 @@ pub(super) fn infer_binary_type_comparison<'db>( // overlaps it, so it must not be split into per-member `__contains__` // calls (which would reject a member that is disjoint from the element, // e.g. `"a"` in `x: Literal[1, "a"]` tested against a `tuple[Literal[1]]`) - (Type::Union(_), other) if matches!(op, ast::CmpOp::In | ast::CmpOp::NotIn) => { - let membership_op = match op { - ast::CmpOp::NotIn => MembershipTestCompareOperator::NotIn, - _ => MembershipTestCompareOperator::In, + (Type::Union(_), other) if matches!(op, NonIdentityOperator::Membership(_)) => { + let NonIdentityOperator::Membership(membership_op) = op else { + unreachable!("the guard above matched a membership operator") }; Some(infer_membership_test_comparison( context, @@ -297,18 +535,18 @@ pub(super) fn infer_binary_type_comparison<'db>( } (Type::Union(union), other) => { - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); for element in union.elements(db) { - builder = builder.add(infer_binary_type_comparison( + builder = builder.add(infer_binary_type_comparison_inner( context, *element, op, other, range, visitor, )?); } Some(Ok(builder.build())) } (other, Type::Union(union)) => { - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); for element in union.elements(db) { - builder = builder.add(infer_binary_type_comparison( + builder = builder.add(infer_binary_type_comparison_inner( context, other, op, *element, range, visitor, )?); } @@ -322,9 +560,9 @@ pub(super) fn infer_binary_type_comparison<'db>( .copied() .any(Type::is_type_var) => { - Some(infer_binary_type_comparison( + Some(infer_binary_type_comparison_inner( context, - intersection.with_expanded_typevars_and_newtypes(db), + intersection.with_expanded_typevars_and_newtypes(db, env), op, right, range, @@ -338,11 +576,11 @@ pub(super) fn infer_binary_type_comparison<'db>( .copied() .any(Type::is_type_var) => { - Some(infer_binary_type_comparison( + Some(infer_binary_type_comparison_inner( context, left, op, - intersection.with_expanded_typevars_and_newtypes(db), + intersection.with_expanded_typevars_and_newtypes(db, env), range, visitor, )) @@ -359,7 +597,7 @@ pub(super) fn infer_binary_type_comparison<'db>( visitor, ) .map_err(|err| UnsupportedComparisonError { - op, + op: op.into(), left_ty: left, right_ty: err.right_ty, }), @@ -375,18 +613,32 @@ pub(super) fn infer_binary_type_comparison<'db>( visitor, ) .map_err(|err| UnsupportedComparisonError { - op, + op: op.into(), left_ty: err.left_ty, right_ty: right, }), ), (Type::TypeAlias(alias), right) => Some(visitor.visit(db, (left, op, right), || { - infer_binary_type_comparison(context, alias.value_type(db), op, right, range, visitor) + infer_binary_type_comparison_inner( + context, + alias.value_type(db), + op, + right, + range, + visitor, + ) })), (left, Type::TypeAlias(alias)) => Some(visitor.visit(db, (left, op, right), || { - infer_binary_type_comparison(context, left, op, alias.value_type(db), range, visitor) + infer_binary_type_comparison_inner( + context, + left, + op, + alias.value_type(db), + range, + visitor, + ) })), // `try_dunder` works for almost all `NewType`s, but not for `NewType`s of `float` and @@ -398,7 +650,7 @@ pub(super) fn infer_binary_type_comparison<'db>( (Type::NewTypeInstance(newtype), right) => { Some(try_dunder(MemberLookupPolicy::default()).or_else(|_| { visitor.visit(db, (left, op, right), || { - infer_binary_type_comparison( + infer_binary_type_comparison_inner( context, newtype.concrete_base_type(db), op, @@ -412,7 +664,7 @@ pub(super) fn infer_binary_type_comparison<'db>( (left, Type::NewTypeInstance(newtype)) => { Some(try_dunder(MemberLookupPolicy::default()).or_else(|_| { visitor.visit(db, (left, op, right), || { - infer_binary_type_comparison( + infer_binary_type_comparison_inner( context, left, op, @@ -432,19 +684,21 @@ pub(super) fn infer_binary_type_comparison<'db>( (Type::TypeVar(left_tvar), Type::TypeVar(right_tvar)) if left_tvar.identity(db) == right_tvar.identity(db) => { - match left_tvar.typevar(db).bound_or_constraints(db) { + match left_tvar.typevar(db).bound_or_constraints(db, env) { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { Some(try_dunder(MemberLookupPolicy::default()).or_else(|_| { visitor.visit(db, (left, op, right), || { - infer_binary_type_comparison(context, bound, op, bound, range, visitor) + infer_binary_type_comparison_inner( + context, bound, op, bound, range, visitor, + ) }) })) } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { // For constrained TypeVars, check each constraint paired with itself. - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); for &constraint in constraints.elements(db) { - builder = builder.add(infer_binary_type_comparison( + builder = builder.add(infer_binary_type_comparison_inner( context, constraint, op, constraint, range, visitor, )?); } @@ -453,46 +707,29 @@ pub(super) fn infer_binary_type_comparison<'db>( None => None, // Fall through to default handling } } - // When the left operand is a bounded TypeVar and the right is not a TypeVar, - // delegate to the bound type. - (Type::TypeVar(left_tvar), right) if !right.is_type_var() => { - match left_tvar.typevar(db).bound_or_constraints(db) { - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - Some(try_dunder(MemberLookupPolicy::default()).or_else(|_| { - visitor.visit(db, (left, op, right), || { - infer_binary_type_comparison(context, bound, op, right, range, visitor) - }) - })) - } - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - let mut builder = UnionBuilder::new(db); - for &constraint in constraints.elements(db) { - builder = builder.add(infer_binary_type_comparison( - context, constraint, op, right, range, visitor, - )?); - } - Some(Ok(builder.build())) - } - None => None, - } - } - // When the right operand is a bounded TypeVar and the left is not a TypeVar, - // delegate to the bound type. - (left, Type::TypeVar(right_tvar)) if !left.is_type_var() => { - match right_tvar.typevar(db).bound_or_constraints(db) { + // A bounded or constrained TypeVar on either side delegates to its concrete alternatives. + (Type::TypeVar(typevar), other) | (other, Type::TypeVar(typevar)) + if !other.is_type_var() => + { + let compare_replacement = |replacement| { + let (left, right) = if left.is_type_var() { + (replacement, right) + } else { + (left, replacement) + }; + infer_binary_type_comparison_inner(context, left, op, right, range, visitor) + }; + + match typevar.typevar(db).bound_or_constraints(db, env) { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { Some(try_dunder(MemberLookupPolicy::default()).or_else(|_| { - visitor.visit(db, (left, op, right), || { - infer_binary_type_comparison(context, left, op, bound, range, visitor) - }) + visitor.visit(db, (left, op, right), || compare_replacement(bound)) })) } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); for &constraint in constraints.elements(db) { - builder = builder.add(infer_binary_type_comparison( - context, left, op, constraint, range, visitor, - )?); + builder = builder.add(compare_replacement(constraint)?); } Some(Ok(builder.build())) } @@ -504,31 +741,27 @@ pub(super) fn infer_binary_type_comparison<'db>( match (left_literal.kind(), right_literal.kind()) { (LiteralValueTypeKind::Int(n), LiteralValueTypeKind::Int(m)) => { Some(match op { - ast::CmpOp::Eq => Ok(Type::bool_literal(n == m)), - ast::CmpOp::NotEq => Ok(Type::bool_literal(n != m)), - ast::CmpOp::Lt => Ok(Type::bool_literal(n < m)), - ast::CmpOp::LtE => Ok(Type::bool_literal(n <= m)), - ast::CmpOp::Gt => Ok(Type::bool_literal(n > m)), - ast::CmpOp::GtE => Ok(Type::bool_literal(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(db)) - } else { - Ok(Type::bool_literal(false)) - } + NonIdentityOperator::Rich(RichCompareOperator::Eq) => { + Ok(Type::bool_literal(n == m)) } - ast::CmpOp::IsNot => { - if n == m { - Ok(KnownClass::Bool.to_instance(db)) - } else { - Ok(Type::bool_literal(true)) - } + NonIdentityOperator::Rich(RichCompareOperator::Ne) => { + Ok(Type::bool_literal(n != m)) + } + NonIdentityOperator::Rich(RichCompareOperator::Lt) => { + Ok(Type::bool_literal(n < m)) + } + NonIdentityOperator::Rich(RichCompareOperator::Le) => { + Ok(Type::bool_literal(n <= m)) + } + NonIdentityOperator::Rich(RichCompareOperator::Gt) => { + Ok(Type::bool_literal(n > m)) + } + NonIdentityOperator::Rich(RichCompareOperator::Ge) => { + Ok(Type::bool_literal(n >= m)) } // Undefined for (int, int) - ast::CmpOp::In | ast::CmpOp::NotIn => Err(UnsupportedComparisonError { - op, + NonIdentityOperator::Membership(_) => Err(UnsupportedComparisonError { + op: op.into(), left_ty: left, right_ty: right, }), @@ -536,7 +769,7 @@ pub(super) fn infer_binary_type_comparison<'db>( } // Booleans are coded as integers (False = 0, True = 1) (LiteralValueTypeKind::Int(n), LiteralValueTypeKind::Bool(b)) => Some( - infer_binary_type_comparison( + infer_binary_type_comparison_inner( context, Type::int_literal(n.as_i64()), op, @@ -545,13 +778,13 @@ pub(super) fn infer_binary_type_comparison<'db>( visitor, ) .map_err(|_| UnsupportedComparisonError { - op, + op: op.into(), left_ty: left, right_ty: right, }), ), (LiteralValueTypeKind::Bool(b), LiteralValueTypeKind::Int(m)) => Some( - infer_binary_type_comparison( + infer_binary_type_comparison_inner( context, Type::int_literal(i64::from(b)), op, @@ -560,13 +793,13 @@ pub(super) fn infer_binary_type_comparison<'db>( visitor, ) .map_err(|_| UnsupportedComparisonError { - op, + op: op.into(), left_ty: left, right_ty: right, }), ), (LiteralValueTypeKind::Bool(a), LiteralValueTypeKind::Bool(b)) => Some( - infer_binary_type_comparison( + infer_binary_type_comparison_inner( context, Type::int_literal(i64::from(a)), op, @@ -575,7 +808,7 @@ pub(super) fn infer_binary_type_comparison<'db>( visitor, ) .map_err(|_| UnsupportedComparisonError { - op, + op: op.into(), left_ty: left, right_ty: right, }), @@ -588,27 +821,29 @@ pub(super) fn infer_binary_type_comparison<'db>( let s1 = salsa_s1.value(db); let s2 = salsa_s2.value(db); let result = match op { - ast::CmpOp::Eq => Type::bool_literal(s1 == s2), - ast::CmpOp::NotEq => Type::bool_literal(s1 != s2), - ast::CmpOp::Lt => Type::bool_literal(s1 < s2), - ast::CmpOp::LtE => Type::bool_literal(s1 <= s2), - ast::CmpOp::Gt => Type::bool_literal(s1 > s2), - ast::CmpOp::GtE => Type::bool_literal(s1 >= s2), - ast::CmpOp::In => Type::bool_literal(s2.contains(s1)), - ast::CmpOp::NotIn => Type::bool_literal(!s2.contains(s1)), - ast::CmpOp::Is => { - if s1 == s2 { - KnownClass::Bool.to_instance(db) - } else { - Type::bool_literal(false) - } + NonIdentityOperator::Rich(RichCompareOperator::Eq) => { + Type::bool_literal(s1 == s2) } - ast::CmpOp::IsNot => { - if s1 == s2 { - KnownClass::Bool.to_instance(db) - } else { - Type::bool_literal(true) - } + NonIdentityOperator::Rich(RichCompareOperator::Ne) => { + Type::bool_literal(s1 != s2) + } + NonIdentityOperator::Rich(RichCompareOperator::Lt) => { + Type::bool_literal(s1 < s2) + } + NonIdentityOperator::Rich(RichCompareOperator::Le) => { + Type::bool_literal(s1 <= s2) + } + NonIdentityOperator::Rich(RichCompareOperator::Gt) => { + Type::bool_literal(s1 > s2) + } + NonIdentityOperator::Rich(RichCompareOperator::Ge) => { + Type::bool_literal(s1 >= s2) + } + NonIdentityOperator::Membership(MembershipOperator::In) => { + Type::bool_literal(s2.contains(s1)) + } + NonIdentityOperator::Membership(MembershipOperator::NotIn) => { + Type::bool_literal(!s2.contains(s1)) } }; Some(Ok(result)) @@ -618,31 +853,29 @@ pub(super) fn infer_binary_type_comparison<'db>( let b1 = salsa_b1.value(db); let b2 = salsa_b2.value(db); let result = match op { - ast::CmpOp::Eq => Type::bool_literal(b1 == b2), - ast::CmpOp::NotEq => Type::bool_literal(b1 != b2), - ast::CmpOp::Lt => Type::bool_literal(b1 < b2), - ast::CmpOp::LtE => Type::bool_literal(b1 <= b2), - ast::CmpOp::Gt => Type::bool_literal(b1 > b2), - ast::CmpOp::GtE => Type::bool_literal(b1 >= b2), - ast::CmpOp::In => { - Type::bool_literal(memchr::memmem::find(b2, b1).is_some()) + NonIdentityOperator::Rich(RichCompareOperator::Eq) => { + Type::bool_literal(b1 == b2) } - ast::CmpOp::NotIn => { - Type::bool_literal(memchr::memmem::find(b2, b1).is_none()) + NonIdentityOperator::Rich(RichCompareOperator::Ne) => { + Type::bool_literal(b1 != b2) } - ast::CmpOp::Is => { - if b1 == b2 { - KnownClass::Bool.to_instance(db) - } else { - Type::bool_literal(false) - } + NonIdentityOperator::Rich(RichCompareOperator::Lt) => { + Type::bool_literal(b1 < b2) } - ast::CmpOp::IsNot => { - if b1 == b2 { - KnownClass::Bool.to_instance(db) - } else { - Type::bool_literal(true) - } + NonIdentityOperator::Rich(RichCompareOperator::Le) => { + Type::bool_literal(b1 <= b2) + } + NonIdentityOperator::Rich(RichCompareOperator::Gt) => { + Type::bool_literal(b1 > b2) + } + NonIdentityOperator::Rich(RichCompareOperator::Ge) => { + Type::bool_literal(b1 >= b2) + } + NonIdentityOperator::Membership(MembershipOperator::In) => { + Type::bool_literal(memchr::memmem::find(b2, b1).is_some()) + } + NonIdentityOperator::Membership(MembershipOperator::NotIn) => { + Type::bool_literal(memchr::memmem::find(b2, b1).is_none()) } }; Some(Ok(result)) @@ -673,8 +906,11 @@ pub(super) fn infer_binary_type_comparison<'db>( | LiteralValueTypeKind::Bool(_) | LiteralValueTypeKind::Bytes(_), LiteralValueTypeKind::LiteralString, - ) if matches!(op, ast::CmpOp::Eq | ast::CmpOp::NotEq) => { - Some(Ok(Type::bool_literal(op == ast::CmpOp::NotEq))) + ) if let NonIdentityOperator::Rich( + rich @ (RichCompareOperator::Eq | RichCompareOperator::Ne), + ) = op => + { + Some(Ok(Type::bool_literal(rich == RichCompareOperator::Ne))) } _ => None, } @@ -685,95 +921,20 @@ pub(super) fn infer_binary_type_comparison<'db>( Type::KnownInstance(KnownInstanceType::ConstraintSet(right)), ) => { let constraints = ConstraintSetBuilder::new(); - let left = constraints.load(db, left.constraints(db)); - let right = constraints.load(db, right.constraints(db)); - let result = left.iff(db, &constraints, right); - let equivalent = result.is_always_satisfied(db); + let left = constraints.load(db, env, left.constraints(db)); + let right = constraints.load(db, env, right.constraints(db)); + let equivalent = left + .iff(db, &constraints, right) + .is_always_satisfied(db, env); match op { - ast::CmpOp::Eq => Some(Ok(Type::bool_literal(equivalent))), - ast::CmpOp::NotEq => Some(Ok(Type::bool_literal(!equivalent))), - _ => None, - } - } - - (Type::NominalInstance(nominal1), Type::NominalInstance(nominal2)) - if let Some(lhs_tuple) = nominal1.tuple_spec(db) - && let Some(rhs_tuple) = nominal2.tuple_spec(db) => - { - let tuple_rich_comparison = |rich_op| { - visitor.visit(db, (left, op, right), || { - infer_tuple_rich_comparison( - context, &lhs_tuple, rich_op, &rhs_tuple, range, visitor, - ) - }) - }; - - let result = 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.iter_element_types(db) { - let eq_result = infer_binary_type_comparison( - context, - left, - ast::CmpOp::Eq, - ty, - range, - visitor, - ) - .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(db) { - Truthiness::AlwaysTrue => any_eq = true, - Truthiness::AlwaysFalse => (), - Truthiness::Ambiguous => any_ambiguous = true, - }, - } - } - - if any_eq { - Ok(Type::bool_literal(op.is_in())) - } else if !any_ambiguous { - Ok(Type::bool_literal(op.is_not_in())) - } else { - Ok(KnownClass::Bool.to_instance(db)) - } + NonIdentityOperator::Rich(RichCompareOperator::Eq) => { + Some(Ok(Type::bool_literal(equivalent))) } - 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(db) { - Truthiness::AlwaysFalse => Type::bool_literal(op.is_is_not()), - _ => KnownClass::Bool.to_instance(db), - }, - }) + NonIdentityOperator::Rich(RichCompareOperator::Ne) => { + Some(Ok(Type::bool_literal(!equivalent))) } - }; - - Some(result) + _ => None, + } } _ => None, @@ -790,7 +951,7 @@ pub(super) fn infer_binary_type_comparison<'db>( fn infer_binary_intersection_type_comparison<'db>( context: &InferContext<'db, '_>, intersection: IntersectionType<'db>, - op: ast::CmpOp, + op: NonIdentityOperator, other: Type<'db>, intersection_on: IntersectionOn, range: TextRange, @@ -807,14 +968,15 @@ fn infer_binary_intersection_type_comparison<'db>( } let db = context.db(); + let env = &context.program_environment(); - if let Some(alternatives) = intersection.finite_alternative_union(db) { + if let Some(alternatives) = intersection.finite_alternative_union(db, env) { return match intersection_on { IntersectionOn::Left => { - infer_binary_type_comparison(context, alternatives, op, other, range, visitor) + infer_binary_type_comparison_inner(context, alternatives, op, other, range, visitor) } IntersectionOn::Right => { - infer_binary_type_comparison(context, other, op, alternatives, range, visitor) + infer_binary_type_comparison_inner(context, other, op, alternatives, range, visitor) } }; } @@ -825,10 +987,10 @@ fn infer_binary_intersection_type_comparison<'db>( for pos in intersection.positive(db) { let result = match intersection_on { IntersectionOn::Left => { - infer_binary_type_comparison(context, *pos, op, other, range, visitor) + infer_binary_type_comparison_inner(context, *pos, op, other, range, visitor) } IntersectionOn::Right => { - infer_binary_type_comparison(context, other, op, *pos, range, visitor) + infer_binary_type_comparison_inner(context, other, op, *pos, range, visitor) } }; @@ -841,30 +1003,6 @@ fn infer_binary_intersection_type_comparison<'db>( } } - // 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(db) { - let result = match intersection_on { - IntersectionOn::Left => { - infer_binary_type_comparison(context, *neg, op, other, range, visitor).ok() - } - IntersectionOn::Right => { - infer_binary_type_comparison(context, other, op, *neg, range, visitor).ok() - } - } - .and_then(Type::as_literal_value_kind); - - match (op, result) { - (ast::CmpOp::Is, Some(LiteralValueTypeKind::Bool(true))) => { - return Ok(Type::bool_literal(false)); - } - (ast::CmpOp::IsNot, Some(LiteralValueTypeKind::Bool(false))) => { - return Ok(Type::bool_literal(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 // @@ -903,26 +1041,26 @@ fn infer_binary_intersection_type_comparison<'db>( // // we would get a result type `Literal[True]` which is too narrow. // - let mut builder = IntersectionBuilder::new(db); + let mut builder = IntersectionBuilder::new(db, env); - builder = builder.add_positive(KnownClass::Bool.to_instance(db)); + builder.add_positive_in_place(KnownClass::Bool.to_instance(db, env)); let mut state = State::NoPositiveElements; for pos in intersection.positive(db) { let result = match intersection_on { IntersectionOn::Left => { - infer_binary_type_comparison(context, *pos, op, other, range, visitor) + infer_binary_type_comparison_inner(context, *pos, op, other, range, visitor) } IntersectionOn::Right => { - infer_binary_type_comparison(context, other, op, *pos, range, visitor) + infer_binary_type_comparison_inner(context, other, op, *pos, range, visitor) } }; match result { Ok(ty) => { state = State::Supported; - builder = builder.add_positive(ty); + builder.add_positive_in_place(ty); } Err(error) => { match state { @@ -950,12 +1088,22 @@ fn infer_binary_intersection_type_comparison<'db>( State::NoPositiveElements => { // We didn't see any positive elements, check if the operation is supported on `object`: match intersection_on { - IntersectionOn::Left => { - infer_binary_type_comparison(context, Type::object(), op, other, range, visitor) - } - IntersectionOn::Right => { - infer_binary_type_comparison(context, other, op, Type::object(), range, visitor) - } + IntersectionOn::Left => infer_binary_type_comparison_inner( + context, + Type::object(), + op, + other, + range, + visitor, + ), + IntersectionOn::Right => infer_binary_type_comparison_inner( + context, + other, + op, + Type::object(), + range, + visitor, + ), } } State::UnsupportedOnAllElements(error) => Err(error), @@ -967,32 +1115,23 @@ fn infer_binary_intersection_type_comparison<'db>( /// This function performs rich comparison between two types and returns the resulting type. /// see `` fn infer_rich_comparison<'db>( - db: &'db dyn Db, + context: &InferContext<'db, '_>, left: Type<'db>, right: Type<'db>, op: RichCompareOperator, policy: MemberLookupPolicy, ) -> Result, UnsupportedComparisonError<'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_with_policy( - db, - op.dunder(), - &mut CallArguments::positional([right]), - TypeContext::default(), - policy, - ) - .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)) - } + let db = context.db(); + let env = &context.program_environment(); + Type::try_call_rich_comparison_dunder( + db, + env, + left, + right, + op.dunder(), + op.reflect().dunder(), + policy, + ) .or_else(|| { // When no appropriate method returns any value other than NotImplemented, // the `==` and `!=` operators will fall back to `is` and `is not`, respectively. @@ -1002,7 +1141,7 @@ fn infer_rich_comparison<'db>( // on `object`, so it does not apply if we skip looking up attributes on `object`. && !policy.mro_no_object_fallback() { - Some(KnownClass::Bool.to_instance(db)) + Some(KnownClass::Bool.to_instance(db, env)) } else { None } @@ -1022,23 +1161,35 @@ fn infer_membership_test_comparison<'db>( context: &InferContext<'db, '_>, left: Type<'db>, right: Type<'db>, - op: MembershipTestCompareOperator, + op: MembershipOperator, range: TextRange, ) -> Result, UnsupportedComparisonError<'db>> { let db = context.db(); + let env = &context.program_environment(); + + if let Some(key) = left.as_string_literal() + && let Some(typed_dict) = right.as_typed_dict() + { + let truthiness = typed_dict + .key_membership_truthiness(db, key.value(db)) + .negate_if(op.is_not_in()); + return Ok(Type::from_truthiness(db, env, truthiness)); + } + let compare_result_opt = match right.try_call_dunder( db, + env, "__contains__", CallArguments::positional([left]), TypeContext::default(), ) { // If `__contains__` is available, it is used directly for the membership test. - Ok(bindings) => Some(bindings.return_type(db)), + Ok(bindings) => Some(bindings.return_type(db, env)), // If `__contains__` is not available or possibly unbound, // fall back to iteration-based membership test. Err(CallDunderError::MethodNotAvailable | CallDunderError::PossiblyUnbound { .. }) => right - .try_iterate(db) - .map(|_| KnownClass::Bool.to_instance(db)) + .try_iterate(db, env) + .map(|_| KnownClass::Bool.to_instance(db, env)) .ok(), // `__contains__` exists but can't be called with the given arguments. Err(CallDunderError::CallError(..)) => None, @@ -1050,16 +1201,14 @@ fn infer_membership_test_comparison<'db>( return ty; } - let truthiness = ty.try_bool(db).unwrap_or_else(|err| { + let truthiness = ty.try_bool(db, env).unwrap_or_else(|err| { err.report_diagnostic(context, range); err.fallback_truthiness() }); match op { - MembershipTestCompareOperator::In => Type::from_truthiness(db, truthiness), - MembershipTestCompareOperator::NotIn => { - Type::from_truthiness(db, truthiness.negate()) - } + MembershipOperator::In => Type::from_truthiness(db, env, truthiness), + MembershipOperator::NotIn => Type::from_truthiness(db, env, truthiness.negate()), } }) .ok_or_else(|| UnsupportedComparisonError { @@ -1083,31 +1232,30 @@ fn infer_tuple_rich_comparison<'db>( visitor: &BinaryComparisonVisitor<'db>, ) -> Result, UnsupportedComparisonError<'db>> { let db = context.db(); + let env = &context.program_environment(); match (left, right) { // Both fixed-length: perform full lexicographic comparison. (TupleSpec::Fixed(left), TupleSpec::Fixed(right)) => { let left_iter = left.iter_all_elements(); let right_iter = right.iter_all_elements(); - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); + let soundness_policy = ComparisonSoundnessPolicy::from_analysis_settings( + db.analysis_settings(context.file()), + ); + let mut equality = TupleEqualityEvaluator::new(db, env, soundness_policy); for (l_ty, r_ty) in left_iter.zip(right_iter) { - let pairwise_eq_result = infer_binary_type_comparison( - context, - l_ty, - ast::CmpOp::Eq, - r_ty, - range, - visitor, - ) - .expect("infer_binary_type_comparison should never return None for `CmpOp::Eq`"); - - match pairwise_eq_result.try_bool(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(context, range); - err.fallback_truthiness() - }) { + let eq_truthiness = equality + .element_truthiness(l_ty, r_ty) + .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(context, range); + Truthiness::Ambiguous + }); + + match eq_truthiness { // - AlwaysTrue : Continue to the next pair for lexicographic comparison Truthiness::AlwaysTrue => continue, // - AlwaysFalse: @@ -1122,15 +1270,16 @@ fn infer_tuple_rich_comparison<'db>( RichCompareOperator::Lt | RichCompareOperator::Le | RichCompareOperator::Gt - | RichCompareOperator::Ge => infer_binary_type_comparison( + | RichCompareOperator::Ge => infer_binary_type_comparison_inner( context, l_ty, - op.into(), + NonIdentityOperator::Rich(op), r_ty, range, visitor, )?, - // For `==` and `!=`, we already figure out the result from `pairwise_eq_result` + // For `==` and `!=`, the equality evaluator has already determined + // that these elements may differ. // 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::bool_literal(false), @@ -1173,7 +1322,7 @@ fn infer_tuple_rich_comparison<'db>( (TupleSpec::Variable(_), _) | (_, TupleSpec::Variable(_)) if matches!(op, RichCompareOperator::Eq | RichCompareOperator::Ne) => { - Ok(KnownClass::Bool.to_instance(db)) + Ok(KnownClass::Bool.to_instance(db, env)) } // At least one variable-length: check all elements that could potentially be compared. @@ -1181,10 +1330,10 @@ fn infer_tuple_rich_comparison<'db>( (left @ TupleSpec::Variable(_), right) | (left, right @ TupleSpec::Variable(_)) => { let mut results = SmallVec::<[Type<'db>; 8]>::new(); left.try_for_each_element_pair(db, right, |l_ty, r_ty| { - results.push(infer_binary_type_comparison( + results.push(infer_binary_type_comparison_inner( context, l_ty, - op.into(), + NonIdentityOperator::Rich(op), r_ty, range, visitor, @@ -1192,12 +1341,12 @@ fn infer_tuple_rich_comparison<'db>( Ok::<_, UnsupportedComparisonError<'db>>(()) })?; - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); for result in results { builder = builder.add(result); } // Length comparison (when all elements are equal) returns bool. - builder = builder.add(KnownClass::Bool.to_instance(db)); + builder = builder.add(KnownClass::Bool.to_instance(db, env)); Ok(builder.build()) } diff --git a/crates/ty_python_semantic/src/types/infer/tests.rs b/crates/ty_python_semantic/src/types/infer/tests.rs index 926b942ff3..f85ca23ecf 100644 --- a/crates/ty_python_semantic/src/types/infer/tests.rs +++ b/crates/ty_python_semantic/src/types/infer/tests.rs @@ -1,18 +1,32 @@ use super::builder::TypeInferenceBuilder; -use crate::db::tests::{TestDb, setup_db}; +use crate::db::tests::{TestDb, TestDbBuilder, setup_db}; use crate::place::symbol; -use crate::place::{ConsideredDefinitions, Place, global_symbol}; +use crate::place::{ConsideredDefinitions, Place, PlaceAndQualifiers}; use crate::types::{KnownClass, KnownInstanceType, check_types}; use ruff_db::diagnostic::{Diagnostic, DiagnosticId}; 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 ruff_python_ast::PythonVersion; +use salsa::plumbing::AsId; use ty_python_core::definition::Definition; +use ty_python_core::program::{Program, ProgramSettings}; use ty_python_core::scope::FileScopeId; -use ty_python_core::{global_scope, place_table, semantic_index, use_def_map}; +use ty_python_core::{ + ProgramFile, TestProgramDb as _, global_scope, place_table, semantic_index, use_def_map, +}; +use ty_site_packages::{PythonVersionSource, PythonVersionWithSource}; use super::*; +fn program_file(db: &TestDb, file: File) -> ProgramFile<'_> { + ProgramFile::new(db, file, db.program_environment().program(db)) +} + +fn global_symbol<'db>(db: &'db TestDb, file: File, name: &str) -> PlaceAndQualifiers<'db> { + crate::place::global_symbol(db, program_file(db, file), name) +} + #[track_caller] fn get_symbol<'db>( db: &'db TestDb, @@ -21,7 +35,8 @@ fn get_symbol<'db>( 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 file = program_file(db, file); + let module = parsed_module(db, file.python_file(db)).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); @@ -42,7 +57,7 @@ fn get_symbol<'db>( fn assert_diagnostic_messages(diagnostics: &[Diagnostic], expected: &[&str]) { let messages: Vec<&str> = diagnostics .iter() - .map(Diagnostic::primary_message) + .map(Diagnostic::headline_message) .collect(); assert_eq!(&messages, expected); } @@ -50,7 +65,7 @@ fn assert_diagnostic_messages(diagnostics: &[Diagnostic], expected: &[&str]) { #[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); + let diagnostics = check_types(db, program_file(db, file)); assert_diagnostic_messages(&diagnostics, expected); } @@ -58,7 +73,7 @@ fn assert_file_diagnostics(db: &TestDb, filename: &str, expected: &[&str]) { #[track_caller] fn assert_revealed_type(db: &TestDb, filename: &str, expected: &str) { let file = system_path_to_file(db, filename).unwrap(); - let diagnostics = check_types(db, file); + let diagnostics = check_types(db, program_file(db, file)); assert_eq!(diagnostics.len(), 1, "{diagnostics:#?}"); let diagnostic = &diagnostics[0]; @@ -72,6 +87,151 @@ fn assert_revealed_type(db: &TestDb, filename: &str, expected: &str) { ); } +#[test] +fn same_file_at_different_python_versions() -> anyhow::Result<()> { + let mut db = TestDbBuilder::new() + .with_python_version(PythonVersion::PY311) + .build()?; + db.write_dedented( + "src/main.py", + r#" + import sys + + from typing import reveal_type + from zipfile._path import Path + + if sys.version_info >= (3, 12): + from py312_dependency import value + else: + from py311_dependency import value + + type Alias = int + + reveal_type(value) + "#, + )?; + db.write_dedented("src/py311_dependency.py", "value: str = 'py311'")?; + db.write_dedented("src/py312_dependency.py", "value: int = 312")?; + + let file = system_path_to_file(&db, "src/main.py").expect("file to exist"); + let default_program = db.program(); + let search_paths = default_program.search_paths(&db).clone(); + let python_platform = default_program.python_platform(&db).clone(); + let py311 = ProgramFile::new( + &db, + file, + Program::from_settings( + &db, + ProgramSettings { + python_version: PythonVersionWithSource { + version: PythonVersion::PY311, + source: PythonVersionSource::Default, + }, + python_platform: python_platform.clone(), + search_paths: search_paths.clone(), + }, + ), + ); + let py312 = ProgramFile::new( + &db, + file, + Program::from_settings( + &db, + ProgramSettings { + python_version: PythonVersionWithSource { + version: PythonVersion::PY312, + source: PythonVersionSource::Default, + }, + python_platform, + search_paths, + }, + ), + ); + + let check = |file, expected_type, expect_invalid_syntax, expect_unresolved_import| { + let diagnostics = crate::check_file_unwrap(&db, file); + + assert_eq!( + diagnostics + .iter() + .any(|diagnostic| diagnostic.id() == DiagnosticId::InvalidSyntax), + expect_invalid_syntax, + "{diagnostics:#?}" + ); + assert_eq!( + diagnostics + .iter() + .any(|diagnostic| diagnostic.headline_message().contains("zipfile._path")), + expect_unresolved_import, + "{diagnostics:#?}" + ); + + let revealed = diagnostics + .iter() + .find(|diagnostic| diagnostic.id() == DiagnosticId::RevealedType) + .and_then(Diagnostic::primary_annotation) + .and_then(|annotation| annotation.get_message()); + assert_eq!(revealed, Some(expected_type), "{diagnostics:#?}"); + assert_eq!( + diagnostics.len(), + 1 + usize::from(expect_invalid_syntax) + usize::from(expect_unresolved_import), + "{diagnostics:#?}" + ); + }; + + check(py311, "`str`", true, true); + check(py312, "`int`", false, false); + check(py311, "`str`", true, true); + + Ok(()) +} + +#[test] +fn program_file_changes_with_python_version() -> anyhow::Result<()> { + let db = TestDbBuilder::new() + .with_python_version(PythonVersion::PY311) + .with_file("src/main.py", "type Alias = int") + .build()?; + let file = system_path_to_file(&db, "src/main.py").expect("file to exist"); + let program = db.program(); + let (program_file_id, py311) = { + let program_file = program.program_file(&db, file); + (program_file.as_id(), program_file.python_file(&db).as_id()) + }; + + let equivalent_program = Program::from_settings( + &db, + ProgramSettings { + python_version: db.program_settings().python_version.clone(), + python_platform: program.python_platform(&db).clone(), + search_paths: program.search_paths(&db).clone(), + }, + ); + assert_eq!(program, equivalent_program); + assert_eq!( + program_file_id, + equivalent_program.program_file(&db, file).as_id() + ); + + let py312_program = Program::from_settings( + &db, + ProgramSettings { + python_version: PythonVersionWithSource { + version: PythonVersion::PY312, + source: PythonVersionSource::Default, + }, + python_platform: program.python_platform(&db).clone(), + search_paths: program.search_paths(&db).clone(), + }, + ); + + let program_file = py312_program.program_file(&db, file); + assert_ne!(program_file_id, program_file.as_id()); + assert_eq!(program_file.python_version(&db), PythonVersion::PY312); + assert_ne!(py311, program_file.python_file(&db).as_id()); + Ok(()) +} + #[test] fn expected_types_are_collected_only_for_open_files() -> anyhow::Result<()> { let has_expected_type = |open_file: bool| -> anyhow::Result { @@ -90,7 +250,7 @@ fn expected_types_are_collected_only_for_open_files() -> anyhow::Result<()> { db.open_file(file); } - let module = parsed_module(&db, file).load(&db); + let module = parsed_module(&db, program_file(&db, file).python_file(&db)).load(&db); let assignment = module.syntax().body[1] .as_ann_assign_stmt() .expect("annotated assignment"); @@ -100,7 +260,7 @@ fn expected_types_are_collected_only_for_open_files() -> anyhow::Result<()> { .expect("annotated assignment to have a value") .as_string_literal_expr() .expect("string literal value"); - let scope = global_scope(&db, file); + let scope = global_scope(&db, program_file(&db, file)); Ok(infer_complete_scope_types(&db, scope) .try_expected_type(ruff_python_ast::ExprRef::from(string_expr)) @@ -130,12 +290,12 @@ fn compact_definition_types_omit_owner() -> anyhow::Result<()> { )?; let file = system_path_to_file(&db, "/src/definitions.py").unwrap(); - let module = parsed_module(&db, file).load(&db); + let module = parsed_module(&db, program_file(&db, file).python_file(&db)).load(&db); let first_assignment = module.syntax().body[0].as_assign_stmt().unwrap(); let second_assignment = module.syntax().body[1].as_assign_stmt().unwrap(); - let first = semantic_index(&db, file) + let first = semantic_index(&db, program_file(&db, file)) .expect_single_definition(first_assignment.targets[0].as_name_expr().unwrap()); - let second = semantic_index(&db, file) + let second = semantic_index(&db, program_file(&db, file)) .expect_single_definition(second_assignment.targets[0].as_name_expr().unwrap()); let owner_type = Type::unknown(); @@ -283,17 +443,18 @@ fn pep695_type_params() { ) .unwrap(); + let env = db.program_environment(); let check_typevar = |var: &'static str, display: &'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(), display); + assert_eq!(var_ty.display(&db, &env).to_string(), display); 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 name_ty = var_ty.member(&db, &env, "__name__").place.expect_type(); + assert_eq!(name_ty.display(&db, &env).to_string(), expected_name_ty); let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = var_ty else { panic!("expected TypeVar"); @@ -301,14 +462,14 @@ fn pep695_type_params() { assert_eq!( typevar - .upper_bound(&db) - .map(|ty| ty.display(&db).to_string()), + .upper_bound(&db, &env) + .map(|ty| ty.display(&db, &env).to_string()), upper_bound.map(std::borrow::ToOwned::to_owned) ); assert_eq!( - typevar.constraints(&db).map(|tys| tys + typevar.constraints(&db, &env).map(|tys| tys .iter() - .map(|ty| ty.display(&db).to_string()) + .map(|ty| ty.display(&db, &env).to_string()) .collect::>()), constraints.map(|strings| strings .iter() @@ -317,8 +478,8 @@ fn pep695_type_params() { ); assert_eq!( typevar - .default_type(&db) - .map(|ty| ty.display(&db).to_string()), + .default_type(&db, &env) + .map(|ty| ty.display(&db, &env).to_string()), default.map(std::borrow::ToOwned::to_owned) ); }; @@ -356,11 +517,20 @@ fn pep695_type_params_based() { constraints: Option<&[&'static str]>, default: Option<&'static str>| { let var_ty = get_symbol(&db, "src/a.by", &["f"], var).expect_type(); - assert_eq!(var_ty.display(&db).to_string(), display); + assert_eq!( + var_ty.display(&db, &db.program_environment()).to_string(), + display + ); 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 name_ty = var_ty + .member(&db, &db.program_environment(), "__name__") + .place + .expect_type(); + assert_eq!( + name_ty.display(&db, &db.program_environment()).to_string(), + expected_name_ty + ); let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = var_ty else { panic!("expected TypeVar"); @@ -368,15 +538,17 @@ fn pep695_type_params_based() { assert_eq!( typevar - .upper_bound(&db) - .map(|ty| ty.display(&db).to_string()), + .upper_bound(&db, &db.program_environment()) + .map(|ty| ty.display(&db, &db.program_environment()).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::>()), + typevar + .constraints(&db, &db.program_environment()) + .map(|tys| tys + .iter() + .map(|ty| ty.display(&db, &db.program_environment()).to_string()) + .collect::>()), constraints.map(|strings| strings .iter() .map(std::string::ToString::to_string) @@ -384,8 +556,8 @@ fn pep695_type_params_based() { ); assert_eq!( typevar - .default_type(&db) - .map(|ty| ty.display(&db).to_string()), + .default_type(&db, &db.program_environment()) + .map(|ty| ty.display(&db, &db.program_environment()).to_string()), default.map(std::borrow::ToOwned::to_owned) ); }; @@ -400,6 +572,29 @@ fn pep695_type_params_based() { check_typevar("Y", "TypeVar", None, None, None); } +#[test] +fn simple_assignment_does_not_enter_salsa_cycle() { + let mut db = setup_db(); + db.write_dedented("src/a.py", "x = 1; y = x + 1").unwrap(); + + 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::>() + }); + assert_eq!(cycles, Vec::::new()); +} + /// Test that a symbol known to be unbound in a scope does not still trigger cycle-causing /// reachability-constraint checks in that scope. #[test] @@ -408,14 +603,6 @@ fn unbound_symbol_no_reachability_constraint_check() { // this is about let mut db = setup_db().without_inferred_signatures(); - // First, type-check a random other file so that we cache a result for the `module_type_symbols` - // query (which often encounters cycles due to `types.pyi` importing `typing_extensions` and - // `typing_extensions.pyi` importing `types`). Clear the events afterwards so that unrelated - // cycles from that query don't interfere with our test. - db.write_dedented("src/wherever.py", "print(x)").unwrap(); - assert_file_diagnostics(&db, "src/wherever.py", &["Name `x` used when not defined"]); - db.clear_salsa_events(); - // 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 @@ -642,7 +829,7 @@ class Form(Ui): // 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); + let scope = global_scope(db, program_file(db, file)); use_def_map(db, scope) .end_of_scope_symbol_bindings(place_table(db, scope).symbol_id(name).unwrap()) .find_map(|b| b.binding.definition()) @@ -661,7 +848,10 @@ fn dependency_public_symbol_type_change() -> anyhow::Result<()> { 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"); + assert_eq!( + x_ty.display(&db, &db.program_environment()).to_string(), + "int" + ); // Change `x` to a different value db.write_file("/src/foo.py", "x: bool = True\ndef foo(): ...")?; @@ -670,7 +860,10 @@ fn dependency_public_symbol_type_change() -> anyhow::Result<()> { let x_ty_2 = global_symbol(&db, a, "x").place.expect_type(); - assert_eq!(x_ty_2.display(&db).to_string(), "bool"); + assert_eq!( + x_ty_2.display(&db, &db.program_environment()).to_string(), + "bool" + ); Ok(()) } @@ -687,7 +880,10 @@ fn dependency_internal_symbol_change() -> anyhow::Result<()> { 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"); + assert_eq!( + x_ty.display(&db, &db.program_environment()).to_string(), + "int" + ); db.write_file("/src/foo.py", "x: int = 10\ndef foo(): pass")?; @@ -697,7 +893,10 @@ fn dependency_internal_symbol_change() -> anyhow::Result<()> { let x_ty_2 = global_symbol(&db, a, "x").place.expect_type(); - assert_eq!(x_ty_2.display(&db).to_string(), "int"); + assert_eq!( + x_ty_2.display(&db, &db.program_environment()).to_string(), + "int" + ); let events = db.take_salsa_events(); @@ -723,7 +922,10 @@ fn dependency_unrelated_symbol() -> anyhow::Result<()> { 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"); + assert_eq!( + x_ty.display(&db, &db.program_environment()).to_string(), + "int" + ); db.write_file("/src/foo.py", "x: int = 10\ny: bool = False")?; @@ -733,7 +935,10 @@ fn dependency_unrelated_symbol() -> anyhow::Result<()> { let x_ty_2 = global_symbol(&db, a, "x").place.expect_type(); - assert_eq!(x_ty_2.display(&db).to_string(), "int"); + assert_eq!( + x_ty_2.display(&db, &db.program_environment()).to_string(), + "int" + ); let events = db.take_salsa_events(); @@ -750,12 +955,12 @@ fn dependency_unrelated_symbol() -> anyhow::Result<()> { 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); + let ast = parsed_module(db, program_file(db, file_main).python_file(db)).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); + let index = semantic_index(db, program_file(db, file_main)); index.expression(x_rhs_node.as_ref()) } @@ -780,7 +985,10 @@ fn dependency_implicit_instance_attribute() -> anyhow::Result<()> { 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(), "int | None"); + assert_eq!( + attr_ty.display(&db, &db.program_environment()).to_string(), + "int | None" + ); // Change the type of `attr` to `str | None`; this should trigger the type of `x` to be re-inferred db.write_dedented( @@ -795,7 +1003,10 @@ fn dependency_implicit_instance_attribute() -> anyhow::Result<()> { 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(), "str | None"); + assert_eq!( + attr_ty.display(&db, &db.program_environment()).to_string(), + "str | None" + ); db.take_salsa_events() }; assert_function_query_was_run( @@ -819,7 +1030,10 @@ fn dependency_implicit_instance_attribute() -> anyhow::Result<()> { 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(), "str | None"); + assert_eq!( + attr_ty.display(&db, &db.program_environment()).to_string(), + "str | None" + ); db.take_salsa_events() }; @@ -839,12 +1053,12 @@ fn dependency_implicit_instance_attribute() -> anyhow::Result<()> { 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); + let ast = parsed_module(db, program_file(db, file_main).python_file(db)).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); + let index = semantic_index(db, program_file(db, file_main)); index.expression(x_rhs_node.as_ref()) } @@ -871,7 +1085,10 @@ fn dependency_own_instance_member() -> anyhow::Result<()> { 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(), "int | None"); + assert_eq!( + attr_ty.display(&db, &db.program_environment()).to_string(), + "int | None" + ); // Change the type of `attr` to `str | None`; this should trigger the type of `x` to be re-inferred db.write_dedented( @@ -888,7 +1105,10 @@ fn dependency_own_instance_member() -> anyhow::Result<()> { 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(), "str | None"); + assert_eq!( + attr_ty.display(&db, &db.program_environment()).to_string(), + "str | None" + ); db.take_salsa_events() }; assert_function_query_was_run( @@ -914,7 +1134,10 @@ fn dependency_own_instance_member() -> anyhow::Result<()> { 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(), "str | None"); + assert_eq!( + attr_ty.display(&db, &db.program_environment()).to_string(), + "str | None" + ); db.take_salsa_events() }; @@ -932,12 +1155,12 @@ fn dependency_own_instance_member() -> anyhow::Result<()> { 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); + let ast = parsed_module(db, program_file(db, file_main).python_file(db)).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); + let index = semantic_index(db, program_file(db, file_main)); index.expression(x_rhs_node.as_ref()) } @@ -967,7 +1190,10 @@ fn dependency_implicit_class_member() -> anyhow::Result<()> { 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(), "int"); + assert_eq!( + attr_ty.display(&db, &db.program_environment()).to_string(), + "int" + ); // Change the type of `class_attr` to `str`; this should trigger the type of `x` to be re-inferred db.write_dedented( @@ -986,7 +1212,10 @@ fn dependency_implicit_class_member() -> anyhow::Result<()> { 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(), "str"); + assert_eq!( + attr_ty.display(&db, &db.program_environment()).to_string(), + "str" + ); db.take_salsa_events() }; assert_function_query_was_run( @@ -1014,7 +1243,10 @@ fn dependency_implicit_class_member() -> anyhow::Result<()> { 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(), "str"); + assert_eq!( + attr_ty.display(&db, &db.program_environment()).to_string(), + "str" + ); db.take_salsa_events() }; @@ -1054,12 +1286,15 @@ fn call_type_doesnt_rerun_when_only_callee_changed() -> anyhow::Result<()> { let bar = system_path_to_file(&db, "src/bar.py")?; let a = global_symbol(&db, bar, "a").place; - assert_eq!(a.expect_type(), KnownClass::Int.to_instance(&db)); + assert_eq!( + a.expect_type(), + KnownClass::Int.to_instance(&db, &db.program_environment()) + ); let events = db.take_salsa_events(); - let module = parsed_module(&db, bar).load(&db); + let module = parsed_module(&db, program_file(&db, bar).python_file(&db)).load(&db); let call = &*module.syntax().body[1].as_assign_stmt().unwrap().value; - let foo_call = semantic_index(&db, bar).expression(call); + let foo_call = semantic_index(&db, program_file(&db, bar)).expression(call); assert_function_query_was_run( &db, @@ -1082,12 +1317,15 @@ fn call_type_doesnt_rerun_when_only_callee_changed() -> anyhow::Result<()> { let a = global_symbol(&db, bar, "a").place; - assert_eq!(a.expect_type(), KnownClass::Int.to_instance(&db)); + assert_eq!( + a.expect_type(), + KnownClass::Int.to_instance(&db, &db.program_environment()) + ); let events = db.take_salsa_events(); - let module = parsed_module(&db, bar).load(&db); + let module = parsed_module(&db, program_file(&db, bar).python_file(&db)).load(&db); let call = &*module.syntax().body[1].as_assign_stmt().unwrap().value; - let foo_call = semantic_index(&db, bar).expression(call); + let foo_call = semantic_index(&db, program_file(&db, bar)).expression(call); assert_function_query_was_not_run( &db, diff --git a/crates/ty_python_semantic/src/types/inferred_signature.rs b/crates/ty_python_semantic/src/types/inferred_signature.rs index 3dfd8b1182..97e95e4a3c 100644 --- a/crates/ty_python_semantic/src/types/inferred_signature.rs +++ b/crates/ty_python_semantic/src/types/inferred_signature.rs @@ -31,6 +31,7 @@ use ty_python_core::{UseDefMap, semantic_index, use_def_map}; use crate::Db; use crate::reachability::ReachabilityConstraintsExtension; +use crate::types::ProgramEnvironment; use crate::types::call::CallArguments; use crate::types::callable::CallableType; use crate::types::constraints::ConstraintSetBuilder; @@ -61,8 +62,9 @@ use crate::types::{ #[salsa::tracked( returns(copy), cycle_initial = |_, id, _| Type::divergent(id), - cycle_fn = |db, cycle, previous: &Type<'db>, value: Type<'db>, _| { - value.cycle_normalized(db, *previous, cycle) + cycle_fn = |db, cycle, previous: &Type<'db>, value: Type<'db>, overload: OverloadLiteral<'db>| { + let env = &ProgramEnvironment::from_file(overload.program_file(db)); + value.cycle_normalized(db, env, *previous, cycle) }, heap_size = ruff_memory_usage::heap_size, )] @@ -70,16 +72,18 @@ pub(crate) fn inferred_return_type<'db>( db: &'db dyn Db, overload: OverloadLiteral<'db>, ) -> Type<'db> { + let env = &ProgramEnvironment::from_file(overload.program_file(db)); let file = overload.file(db); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, db.program_file(file).python_file(db)).load(db); let node = overload.node(db, file, &module); let body_scope = overload.body_scope(db); - let index = semantic_index(db, file); + let index = semantic_index(db, db.program_file(file)); let file_scope_id = body_scope.file_scope_id(db); let inference = infer_scope_types(db, body_scope, TypeContext::default()); return_type_from_body( db, + env, node, file_scope_id.is_generator_function(index), can_implicitly_return_none(db, index.use_def_map(file_scope_id)), @@ -96,6 +100,7 @@ pub(crate) fn inferred_return_type<'db>( /// that advice silently change the function's type. pub(crate) fn return_type_from_body<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, node: &ast::StmtFunctionDef, is_generator: bool, can_implicitly_return_none: bool, @@ -103,6 +108,7 @@ pub(crate) fn return_type_from_body<'db>( ) -> Type<'db> { let mut collector = BodyValueCollector { db, + env: env.clone(), expression_type, returns: Vec::new(), yields: Vec::new(), @@ -111,11 +117,12 @@ pub(crate) fn return_type_from_body<'db>( let returned = UnionType::from_elements( db, + env, collector .returns .iter() .copied() - .chain(can_implicitly_return_none.then(|| Type::none(db))), + .chain(can_implicitly_return_none.then(|| Type::none(db, env))), ); if !is_generator { @@ -125,11 +132,15 @@ pub(crate) fn return_type_from_body<'db>( // what a generator's caller receives is the generator, not what the body // returns; the send type is the one thing the body does not determine, since // it is what the caller passes back in - let yielded = UnionType::from_elements(db, collector.yields.iter().copied()); + let yielded = UnionType::from_elements(db, env, collector.yields.iter().copied()); if node.is_async { - KnownClass::AsyncGeneratorType.to_specialized_instance(db, &[yielded, Type::unknown()]) + KnownClass::AsyncGeneratorType.to_specialized_instance(db, env, &[yielded, Type::unknown()]) } else { - KnownClass::GeneratorType.to_specialized_instance(db, &[yielded, Type::unknown(), returned]) + KnownClass::GeneratorType.to_specialized_instance( + db, + env, + &[yielded, Type::unknown(), returned], + ) } } @@ -173,7 +184,11 @@ pub(crate) fn inferred_parameter_typevar<'db>( /// Anywhere that reads a *structure* out of a type rather than relating it to another — a class to /// subclass, a pivot for `super()` — has to see through it, or recovering the signature would /// report what the gradual type never did. -pub(crate) fn gradual_hole<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option> { +pub(crate) fn gradual_hole<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Option> { let Type::TypeVar(bound_typevar) = ty else { return None; }; @@ -181,7 +196,7 @@ pub(crate) fn gradual_hole<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option( parameter: Definition<'db>, ) -> Option> { let scope = parameter.scope(db); - let index = semantic_index(db, scope.file(db)); + let index = semantic_index(db, scope.program_file(db)); let function = index.scope(scope.file_scope_id(db)).node().as_function()?; Some(index.expect_single_definition(function)) } @@ -220,12 +235,13 @@ pub(crate) fn inferred_parameter_default<'db>( db: &'db dyn Db, parameter: Definition<'db>, ) -> Option> { + let env = &ProgramEnvironment::from_definition(parameter); let DefinitionKind::Parameter(ParameterDefinitionNodeKind::Parameter(node)) = parameter.kind(db) else { return None; }; - let module = parsed_module(db, parameter.file(db)).load(db); + let module = parsed_module(db, parameter.program_file(db).python_file(db)).load(db); let default = node.node(&module).default.as_deref()?; let function = parameter_function_definition(db, parameter)?; @@ -234,7 +250,7 @@ pub(crate) fn inferred_parameter_default<'db>( Some( infer_deferred_types(db, function) .expression_type(default) - .replace_parameter_defaults(db), + .replace_parameter_defaults(db, env), ) } @@ -263,17 +279,18 @@ pub(crate) fn inferred_parameter_bound<'db>( db: &'db dyn Db, parameter: Definition<'db>, ) -> Type<'db> { + let env = &ProgramEnvironment::from_definition(parameter); // `None` is the sentinel every optional parameter is spelled with — it says the argument // may be left out, not that `None` is the kind of thing that belongs there. bounding by it // would reject every call that supplies one, which is what `def f(x=None)` exists for let from_default = inferred_parameter_default(db, parameter) .filter(|default| !default.is_none(db)) - .map(|default| default.promote(db)); + .map(|default| default.promote(db, env)); let from_body = parameter_function_definition(db, parameter) .map(|function| body_parameter_constraints(db, function).get(parameter)) .unwrap_or_default(); - let mut bound = IntersectionBuilder::new(db); + let mut bound = IntersectionBuilder::new(db, env); let mut constrained = false; for constraint in from_default.into_iter().chain(from_body) { constrained = true; @@ -306,16 +323,17 @@ pub(crate) fn body_parameter_constraints<'db>( db: &'db dyn Db, function: Definition<'db>, ) -> ParameterConstraints<'db> { + let env = &ProgramEnvironment::from_definition(function); let file = function.file(db); - let module = parsed_module(db, file).load(db); - let index = semantic_index(db, file); + let module = parsed_module(db, db.program_file(file).python_file(db)).load(db); + let index = semantic_index(db, db.program_file(file)); let DefinitionKind::Function(function_kind) = function.kind(db) else { return ParameterConstraints::default(); }; let node = function_kind.node(&module); let Some(body_scope) = index .try_node_scope(NodeWithScopeRef::Function(node)) - .map(|scope| scope.to_scope_id(db, file)) + .map(|scope| scope.to_scope_id(db, db.program_file(file))) else { return ParameterConstraints::default(); }; @@ -339,6 +357,7 @@ pub(crate) fn body_parameter_constraints<'db>( let mut collector = UseCollector { db, + env: env.clone(), file, use_def: use_def_map(db, body_scope), expression_type: |expr: &Expr| inference.expression_type(expr), @@ -353,10 +372,18 @@ pub(crate) fn body_parameter_constraints<'db>( collector.visit_body(&node.body); let mut uses = collector.uses; - apply_asserted_local_types(db, index, body_scope, node, &collector.locals, &mut uses); + apply_asserted_local_types( + db, + env, + index, + body_scope, + node, + &collector.locals, + &mut uses, + ); - let mut entries = path_bounds(db, uses); - entries.extend(asserted_parameter_types(db, index, body_scope, node)); + let mut entries = path_bounds(db, env, uses); + entries.extend(asserted_parameter_types(db, env, index, body_scope, node)); // a parameter a nested scope captured keeps nothing: that body is checked against this // bound, and this walk never saw what it does with the name @@ -399,6 +426,7 @@ impl<'db> ParameterConstraints<'db> { /// statement later. fn apply_asserted_local_types<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, index: &ty_python_core::SemanticIndex<'db>, body_scope: ScopeId<'db>, node: &ast::StmtFunctionDef, @@ -414,7 +442,7 @@ fn apply_asserted_local_types<'db>( let Some(place_id) = place_table.symbol_id(local.as_str()) else { continue; }; - for asserted in asserted_types(db, index, node, place_id.into()) { + for asserted in asserted_types(db, env, index, node, place_id.into()) { uses.entry(path.clone()).or_default().value.push(asserted); } } @@ -423,6 +451,7 @@ fn apply_asserted_local_types<'db>( /// The types an `assert` at the top level of `node`'s body narrows `place` to. fn asserted_types<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, index: &ty_python_core::SemanticIndex<'db>, node: &ast::StmtFunctionDef, place: ScopedPlaceId, @@ -436,14 +465,14 @@ fn asserted_types<'db>( node: PredicateNode::Expression(expression), is_positive: true, }; - let (Some(constraint), _) = infer_narrowing_constraints(db, predicate, place) else { + let (Some(constraint), _) = infer_narrowing_constraints(db, env, predicate, place) else { continue; }; // narrowing `object` rather than the place's own type keeps a hole out of its own bound let narrowed = NarrowingConstraint::intersection(Type::object()) .merge_constraint_and(constraint) - .evaluate_constraint_type(db); - if !narrowed.is_object() && !narrowed.is_never() && !narrowed.has_typevar(db) { + .evaluate_constraint_type(db, env); + if !narrowed.is_object() && !narrowed.is_never() && !narrowed.has_typevar(db, env) { asserted.push(narrowed); } } @@ -458,6 +487,7 @@ fn asserted_types<'db>( /// to be reachable. fn asserted_parameter_types<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, index: &ty_python_core::SemanticIndex<'db>, body_scope: ScopeId<'db>, node: &ast::StmtFunctionDef, @@ -489,14 +519,14 @@ fn asserted_parameter_types<'db>( is_positive: true, }; for (place_id, definition) in ¶meters { - let (Some(constraint), _) = infer_narrowing_constraints(db, predicate, *place_id) + let (Some(constraint), _) = infer_narrowing_constraints(db, env, predicate, *place_id) else { continue; }; // narrowing `object` rather than the hole keeps the hole out of its own bound let narrowed = NarrowingConstraint::intersection(Type::object()) .merge_constraint_and(constraint) - .evaluate_constraint_type(db); + .evaluate_constraint_type(db, env); if !narrowed.is_object() && !narrowed.is_never() { asserted.push((*definition, narrowed)); } @@ -558,6 +588,7 @@ struct PathUses<'db> { /// bound intersects its protocol with the types it was forwarded into. fn path_bounds<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, uses: FxHashMap, PathUses<'db>>, ) -> Vec<(Definition<'db>, Type<'db>)> { let mut uses: Vec<_> = uses.into_iter().collect(); @@ -566,7 +597,7 @@ fn path_bounds<'db>( let mut resolved: FxHashMap, Type<'db>> = FxHashMap::default(); let mut entries = Vec::new(); for (path, path_uses) in uses { - let mut bound = IntersectionBuilder::new(db); + let mut bound = IntersectionBuilder::new(db, env); let mut constrained = false; if !path_uses.members.is_empty() { @@ -589,7 +620,7 @@ fn path_bounds<'db>( (name, member) }) .collect(); - bound = bound.add_positive(Type::recovered_protocol(db, members)); + bound = bound.add_positive(Type::recovered_protocol(db, env, members)); constrained = true; } for value in path_uses.value { @@ -616,7 +647,7 @@ fn parameter_definition_name<'db>(db: &'db dyn Db, parameter: Definition<'db>) - else { return None; }; - let module = parsed_module(db, parameter.file(db)).load(db); + let module = parsed_module(db, parameter.program_file(db).python_file(db)).load(db); Some(node.node(&module).parameter.name.id.clone()) } @@ -649,6 +680,7 @@ fn record_captured_names_in_expr(expr: &Expr, into: &mut FxHashSet) { /// that value, and is it narrowed — are asked of its bindings instead. struct UseCollector<'db, F> { db: &'db dyn Db, + env: ProgramEnvironment<'db>, file: File, use_def: &'db UseDefMap<'db>, expression_type: F, @@ -698,10 +730,11 @@ where /// *program* states, written as `protocol(...)` or established by a narrowing, is a /// requirement like any other and stays. fn portable(&self, ty: Type<'db>) -> Option> { - (!ty.has_typevar_or_typevar_instance(self.db) + let env = self.env.clone(); + (!ty.has_typevar_or_typevar_instance(self.db, &env) && !ty.is_dynamic() && !ty.is_object() - && !ty.mentions_recovered_protocol(self.db)) + && !ty.mentions_recovered_protocol(self.db, &env)) .then_some(ty) } @@ -783,6 +816,7 @@ where /// The value a *name* stands for: the parameter's own hole, or a local a value at some path /// was assigned to. fn name_path(&self, expr: &Expr) -> Option> { + let db = self.db; if let Some(parameter) = self.hole(expr) { return Some(MemberPath::parameter(parameter)); } @@ -798,7 +832,7 @@ where // the body being inferred would change from one round of this analysis to the next let mut bindings = self .use_def - .bindings_at_use(name.scoped_use_id(self.db, self.file)); + .bindings_at_use(name.scoped_use_id(self.db, db.program_file(self.file))); let binding = bindings.next()?; (bindings.next().is_none() && binding.binding.definition().is_some() @@ -833,6 +867,7 @@ where /// That parameter type serves twice: an argument that *is* a hole has to fit it, and an /// argument that reads a member off a hole makes that member's value have to fit it. fn visit_call_arguments(&mut self, call: &ast::ExprCall) { + let env = self.env.clone(); // a splatted argument hands the callee its *elements*, or its values under their own // names, so the parameter it lands on says nothing about the argument itself. taking // that parameter as a requirement would bound a hole by what it is expected to contain @@ -854,7 +889,7 @@ where .any(|(argument, splatted)| !splatted && self.path(argument).is_some()); let parameter_types = if constrains_a_hole { let callee = (self.expression_type)(&call.func); - call_parameter_types(self.db, callee, &call.arguments, |expr| { + call_parameter_types(self.db, &env, callee, &call.arguments, |expr| { (self.expression_type)(expr) }) .unwrap_or_default() @@ -984,16 +1019,18 @@ where /// argument is well-defined. fn call_parameter_types<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, callee: Type<'db>, arguments: &ast::Arguments, expression_type: impl Fn(&Expr) -> Type<'db>, ) -> Option>>> { let call_arguments = CallArguments::from_arguments_typed(arguments, expression_type); let bindings = callee - .bindings(db) - .match_parameters(db, &call_arguments) + .bindings(db, env) + .match_parameters(db, env, &call_arguments) .check_types( db, + env, &ConstraintSetBuilder::new(), &call_arguments, TypeContext::default(), @@ -1022,6 +1059,7 @@ pub(crate) fn can_implicitly_return_none<'db>(db: &'db dyn Db, use_def: &UseDefM /// this never descends into one. A class body is skipped for the same reason. struct BodyValueCollector<'db, F> { db: &'db dyn Db, + env: ProgramEnvironment<'db>, expression_type: F, returns: Vec>, yields: Vec>, @@ -1032,6 +1070,7 @@ where F: Fn(&Expr) -> Type<'db>, { fn visit_stmt(&mut self, stmt: &Stmt) { + let env = self.env.clone(); match stmt { // a nested scope returns and yields on its own account. its // decorators, defaults and bases do run here, but none of them can @@ -1044,7 +1083,7 @@ where self.visit_expr(value); (self.expression_type)(value) } - None => Type::none(self.db), + None => Type::none(self.db, &env), }; self.returns.push(returned); } @@ -1054,6 +1093,7 @@ where } fn visit_expr(&mut self, expr: &Expr) { + let env = self.env.clone(); match expr { // each of these opens a scope of its own, which cannot contain a // `return` and (since 3.8) cannot contain a `yield` either @@ -1066,7 +1106,7 @@ where Expr::Yield(yield_expr) => { self.yields.push(match yield_expr.value.as_deref() { Some(value) => (self.expression_type)(value), - None => Type::none(self.db), + None => Type::none(self.db, &env), }); } @@ -1074,8 +1114,8 @@ where Expr::YieldFrom(yield_from) => { self.yields.push( (self.expression_type)(&yield_from.value) - .iterate(self.db) - .homogeneous_element_type(self.db), + .iterate(self.db, &env) + .homogeneous_element_type(self.db, &env), ); } diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index 0ccc0b8407..3b20365602 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -1,5 +1,6 @@ //! Instance types: both nominal and structural. +use crate::ProgramEnvironment; use std::borrow::Cow; use std::cell::Cell; use std::marker::PhantomData; @@ -7,7 +8,7 @@ use std::marker::PhantomData; use ruff_python_ast::name::Name; use ty_module_resolver::{ModuleName, file_to_module}; -use super::protocol_class::{InlineProtocolMember, ProtocolInterface}; +use super::protocol_class::{InlineProtocolMember, ProtocolInterface, ProtocolInterfaceView}; use super::{ BoundTypeVarIdentity, BoundTypeVarInstance, ClassType, DivergentType, KnownClass, MaterializationKind, SubclassOfType, Type, TypeAliasType, TypeVarVariance, @@ -15,10 +16,10 @@ use super::{ use crate::place::PlaceAndQualifiers; use crate::types::class::DynamicNamedTupleAnchor; use crate::types::constraints::{ - ConstraintSet, ConstraintSetBuilder, IteratorConstraintsExtension, + ConstraintSet, ConstraintSetBuilder, IteratorConstraintsExtension, OwnedConstraintSet, }; use crate::types::enums::is_single_member_enum; -use crate::types::generics::{InferableTypeVars, walk_specialization}; +use crate::types::generics::walk_specialization; use crate::types::protocol_class::{ ProtocolClass, has_all_protocol_members_defined, walk_protocol_instance_member, walk_protocol_interface, @@ -30,6 +31,7 @@ use crate::types::relation::{ }; use crate::types::signatures::SignatureRelationVisitor; use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type}; +use crate::types::typevar::TypeVarSet; use crate::types::visitor::{TypeCollector, TypeVisitor, walk_type_with_recursion_guard}; use crate::types::{ ApplyTypeMappingVisitor, CallableType, ClassBase, ClassLiteral, ErrorContext, @@ -55,7 +57,11 @@ impl<'db> Type<'db> { ) } - pub(crate) fn instance(db: &'db dyn Db, class: ClassType<'db>) -> Self { + pub(crate) fn instance( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + class: ClassType<'db>, + ) -> Self { match class.class_literal(db) { // Dynamic classes created via `type()` don't have special instance types. ClassLiteral::Dynamic(_) @@ -70,6 +76,7 @@ impl<'db> Type<'db> { match class_literal.known(db) { Some(KnownClass::Tuple) => Type::tuple(TupleType::new( db, + env, specialization .and_then(|spec| Some(Cow::Borrowed(spec.tuple(db)?))) .unwrap_or_else(|| Cow::Owned(TupleSpec::homogeneous(Type::unknown()))) @@ -117,23 +124,32 @@ impl<'db> Type<'db> { Type::tuple_instance(tuple) } - pub fn homogeneous_tuple(db: &'db dyn Db, element: Type<'db>) -> Self { - Type::tuple_instance(TupleType::homogeneous(db, element)) + pub fn homogeneous_tuple( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + element: Type<'db>, + ) -> Self { + Type::tuple_instance(TupleType::homogeneous(db, env, element)) } - pub(crate) fn heterogeneous_tuple(db: &'db dyn Db, elements: I) -> Self + pub(crate) fn heterogeneous_tuple( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + elements: I, + ) -> Self where I: IntoIterator, T: Into>, { Type::tuple(TupleType::heterogeneous( db, + env, elements.into_iter().map(Into::into), )) } - pub(crate) fn empty_tuple(db: &'db dyn Db) -> Self { - Type::tuple_instance(TupleType::empty(db)) + pub(crate) fn empty_tuple(db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Self { + Type::tuple_instance(TupleType::empty(db, env)) } /// **Private** helper function to create a `Type::NominalInstance` from a tuple. @@ -162,31 +178,39 @@ impl<'db> Type<'db> { /// Return `true` if `self` is a nominal instance of the given known class. pub(crate) fn is_instance_of(self, db: &'db dyn Db, known_class: KnownClass) -> bool { match self { - Type::NominalInstance(instance) => instance.class(db).is_known(db, known_class), + Type::NominalInstance(instance) => instance.has_known_class(db, known_class), _ => false, } } /// 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 + pub(super) fn protocol_with_readonly_members<'a, M>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + members: M, + ) -> Self where M: IntoIterator)>, { Self::ProtocolInstance(ProtocolInstanceType::synthesized( SynthesizedProtocolType::stated( db, - ProtocolInterface::with_property_members(db, members), + ProtocolInterface::with_property_members(db, env, members), ), )) } /// Synthesize a protocol instance type with a given set of methods. - pub(super) fn protocol_with_methods<'a, M>(db: &'db dyn Db, methods: M) -> Self + pub(super) fn protocol_with_methods<'a, M>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + methods: M, + ) -> Self where M: IntoIterator)>, { Self::ProtocolInstance(ProtocolInstanceType::synthesized( - SynthesizedProtocolType::stated(db, ProtocolInterface::with_methods(db, methods)), + SynthesizedProtocolType::stated(db, ProtocolInterface::with_methods(db, env, methods)), )) } @@ -196,8 +220,12 @@ impl<'db> Type<'db> { /// Such a shape is the bound of a hole that analysis is in the middle of writing, so it says /// nothing a call site could fail. Every other structural protocol — one written as /// `protocol(...)`, or one a narrowing established — is a requirement like any other. - pub(super) fn mentions_recovered_protocol(self, db: &'db dyn Db) -> bool { - super::visitor::any_over_type(db, self, false, |ty| { + pub(super) fn mentions_recovered_protocol( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> bool { + super::visitor::any_over_type(db, env, self, false, |ty| { matches!( ty, Type::ProtocolInstance(ProtocolInstanceType { @@ -213,14 +241,19 @@ impl<'db> Type<'db> { /// /// `packs` holds the `**Kwargs` keyword-variadic packs that are not specialized yet; each one /// contributes an attribute member per field once it is. - pub(super) fn inline_protocol(db: &'db dyn Db, members: M, packs: Box<[Type<'db>]>) -> Self + pub(super) fn inline_protocol( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + members: M, + packs: Box<[Type<'db>]>, + ) -> Self where M: IntoIterator)>, { Self::ProtocolInstance(ProtocolInstanceType::synthesized( SynthesizedProtocolType::stated( db, - ProtocolInterface::with_inline_members(db, members, packs), + ProtocolInterface::with_inline_members(db, env, members, packs), ), )) } @@ -230,14 +263,18 @@ impl<'db> Type<'db> { /// program states. /// /// See [`Type::mentions_recovered_protocol`], which is the only thing the mark is read by. - pub(super) fn recovered_protocol(db: &'db dyn Db, members: M) -> Self + pub(super) fn recovered_protocol( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + members: M, + ) -> Self where M: IntoIterator)>, { Self::ProtocolInstance(ProtocolInstanceType::synthesized( SynthesizedProtocolType::recovered( db, - ProtocolInterface::with_inline_members(db, members, Box::default()), + ProtocolInterface::with_inline_members(db, env, members, Box::default()), ), )) } @@ -261,7 +298,9 @@ pub(super) fn walk_nominal_instance_type<'db, V: super::visitor::TypeVisitor<'db walk_tuple_type(db, tuple, visitor); } NominalInstanceInner::Object => {} - NominalInstanceInner::NonTuple(class) => visitor.visit_type(db, class.class(db).into()), + NominalInstanceInner::NonTuple(class) => { + visitor.visit_type(db, class.class(db).into()); + } NominalInstanceInner::SysVersionInfo => {} NominalInstanceInner::Regex(regex) => visitor.visit_type(db, regex.class(db).into()), } @@ -289,8 +328,8 @@ impl<'db> NominalInstanceType<'db> { /// As of 2026-02-16, this method is not used in any crates in the Ruff /// repo, but is exposed as a public API for external users of /// `ty_python_semantic`. - pub fn class_name(&self, db: &'db dyn Db) -> &'db Name { - self.class(db).name(db) + pub fn class_name(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> &'db Name { + self.class(db, env).name(db) } /// Returns the fully qualified module name of the module in which the class @@ -303,26 +342,34 @@ impl<'db> NominalInstanceType<'db> { /// As of 2026-02-16, this method is not used in any crates in the Ruff /// repo, but is exposed as a public API for external users of /// `ty_python_semantic`. - pub fn class_module_name(&self, db: &'db dyn Db) -> Option<&'db ModuleName> { - let file = self.class(db).class_literal(db).file(db); - file_to_module(db, file).map(|module| module.name(db)) + pub fn class_module_name( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option<&'db ModuleName> { + let class = self.class(db, env).class_literal(db); + file_to_module(db, class.program_file(db).resolver_file(db)).map(|module| module.name(db)) } - pub(crate) fn class(&self, db: &'db dyn Db) -> ClassType<'db> { + pub(crate) fn class(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> ClassType<'db> { match self.0 { NominalInstanceInner::ExactTuple(tuple) => tuple.to_class_type(db), NominalInstanceInner::NonTuple(class) => class.class(db), NominalInstanceInner::SysVersionInfo => { - sys_version_info_class(db).unwrap_or_else(|| ClassType::object(db)) + sys_version_info_class(db, env).unwrap_or_else(|| ClassType::object(db, env)) } - NominalInstanceInner::Object => ClassType::object(db), + NominalInstanceInner::Object => ClassType::object(db, env), NominalInstanceInner::Regex(regex) => regex.class(db), } } /// Returns the class literal for this instance. - pub(super) fn class_literal(&self, db: &'db dyn Db) -> ClassLiteral<'db> { - self.class(db).class_literal(db) + pub(super) fn class_literal( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> ClassLiteral<'db> { + self.class(db, env).class_literal(db) } /// Returns the [`KnownClass`] that this is a nominal instance of, or `None` if it is not an @@ -359,11 +406,15 @@ impl<'db> NominalInstanceType<'db> { /// /// I.e., for the type `tuple[int, str]`, this will return the tuple spec `[int, str]`. /// For a subclass of `tuple[int, str]`, it will return the same tuple spec. - pub(crate) fn tuple_spec(&self, db: &'db dyn Db) -> Option>> { + pub(crate) fn tuple_spec( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option>> { match self.0 { NominalInstanceInner::ExactTuple(tuple) => Some(Cow::Borrowed(tuple.tuple(db))), NominalInstanceInner::SysVersionInfo => { - Some(Cow::Owned(TupleSpec::version_info_spec(db))) + Some(Cow::Owned(TupleSpec::version_info_spec(db, env))) } NominalInstanceInner::Object | NominalInstanceInner::Regex(_) => None, NominalInstanceInner::NonTuple(class) => { @@ -474,13 +525,14 @@ impl<'db> NominalInstanceType<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { match self.0 { NominalInstanceInner::ExactTuple(tuple) => { Some(Self(NominalInstanceInner::ExactTuple( - tuple.recursive_type_normalized_impl(db, div, nested)?, + tuple.recursive_type_normalized_impl(db, env, div, nested)?, ))) } NominalInstanceInner::SysVersionInfo => { @@ -490,7 +542,7 @@ impl<'db> NominalInstanceType<'db> { NominalInstanceInner::NonTuple(class) => { let transformed = class .class(db) - .recursive_type_normalized_impl(db, div, nested)?; + .recursive_type_normalized_impl(db, env, div, nested)?; Some(Self(NominalInstanceInner::NonTuple( class.with_class(db, transformed), ))) @@ -498,7 +550,7 @@ impl<'db> NominalInstanceType<'db> { NominalInstanceInner::Regex(regex) => { let transformed = regex .class(db) - .recursive_type_normalized_impl(db, div, nested)?; + .recursive_type_normalized_impl(db, env, div, nested)?; Some(Self(NominalInstanceInner::Regex(RegexInstanceClass::new( db, transformed, @@ -527,34 +579,21 @@ impl<'db> NominalInstanceType<'db> { } } - pub(super) fn is_single_valued(self, db: &'db dyn Db) -> bool { - match self.0 { - NominalInstanceInner::ExactTuple(tuple) => tuple.is_single_valued(db), - NominalInstanceInner::Object | NominalInstanceInner::Regex(_) => false, - NominalInstanceInner::SysVersionInfo => true, - NominalInstanceInner::NonTuple(class) => class - .class(db) - .known(db) - .and_then(KnownClass::is_single_valued) - .or_else(|| Some(self.tuple_spec(db)?.is_single_valued(db))) - .unwrap_or_else(|| is_single_member_enum(db, class.class(db).class_literal(db))), - } - } - - pub(super) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { - SubclassOfType::from(db, self.class(db)) + pub(super) fn to_meta_type(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + SubclassOfType::from(db, env, self.class(db, env)) } pub(super) fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Type<'db> { match self.0 { NominalInstanceInner::ExactTuple(tuple) => { - Type::tuple(tuple.apply_type_mapping_impl(db, type_mapping, tcx, visitor)) + Type::tuple(tuple.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor)) } NominalInstanceInner::SysVersionInfo => Type::NominalInstance(self), NominalInstanceInner::Object => Type::object(), @@ -569,7 +608,7 @@ impl<'db> NominalInstanceType<'db> { return Type::regex_instance(db, mapped_class, *groups); } let transformed = - mapped_class.apply_type_mapping_impl(db, type_mapping, tcx, visitor); + mapped_class.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor); Type::NominalInstance(Self(NominalInstanceInner::NonTuple( class.with_class(db, transformed), ))) @@ -578,7 +617,7 @@ impl<'db> NominalInstanceType<'db> { let transformed = regex .class(db) - .apply_type_mapping_impl(db, type_mapping, tcx, visitor); + .apply_type_mapping_impl(db, env, type_mapping, tcx, visitor); let groups = match type_mapping { // a later, more precise pattern replaces an earlier one TypeMapping::AttachRegexGroups(groups) => *groups, @@ -592,24 +631,33 @@ impl<'db> NominalInstanceType<'db> { pub(super) fn find_legacy_typevars_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, ) { match self.0 { NominalInstanceInner::ExactTuple(tuple) => { - tuple.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + tuple.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } NominalInstanceInner::SysVersionInfo | NominalInstanceInner::Object => {} NominalInstanceInner::NonTuple(class) => { - class - .class(db) - .find_legacy_typevars_impl(db, binding_context, typevars, visitor); + class.class(db).find_legacy_typevars_impl( + db, + env, + binding_context, + typevars, + visitor, + ); } NominalInstanceInner::Regex(regex) => { - regex - .class(db) - .find_legacy_typevars_impl(db, binding_context, typevars, visitor); + regex.class(db).find_legacy_typevars_impl( + db, + env, + binding_context, + typevars, + visitor, + ); } } } @@ -629,16 +677,36 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { ty: Type<'db>, protocol: ProtocolInstanceType<'db>, ) -> ConstraintSet<'db, 'c> { - // `ty` might satisfy the protocol nominally, if `protocol` is a class-based protocol and - // `ty` has the protocol class in its MRO. This is a much cheaper check than the - // structural check we perform below, so we do it first to avoid the structural check when - // we can. + // Explicit protocol inheritance is nominal, but materializing a protocol can change + // the requirements represented by that same class. The nominal shortcut is therefore + // valid only when materialization leaves the target's members unchanged. let mut result = self.never(); + let source_protocol = ty.as_protocol_instance(); + + // Every gradual type lies between its bottom and top materializations. Comparing the + // exact same class specialization can therefore settle these directions without expanding + // a recursive protocol's members or confusing opposite materialization requirements. + if let Some(source) = source_protocol + && matches!( + ( + source.materialization_kind(db), + protocol.materialization_kind(db) + ), + ( + None | Some(MaterializationKind::Bottom), + Some(MaterializationKind::Top) + ) | (Some(MaterializationKind::Bottom), None) + ) + && let (Some(source_origin), Some(target_origin)) = + (source.class_origin(db), protocol.class_origin(db)) + && source_origin == target_origin + { + return self.always(); + } - if let Some(nominal_instance) = protocol.to_nominal_instance() { - let source_protocol_as_nominal = ty - .as_protocol_instance() - .and_then(ProtocolInstanceType::to_nominal_instance); + let source_protocol_as_nominal = + source_protocol.and_then(|source| source.nominal_origin_instance(db)); + if let Some(nominal_instance) = protocol.nominal_origin_instance(db) { // if `ty` and `protocol` are *both* protocols, we also need to treat `ty` as if it // were a nominal type, or we won't consider a protocol `P` that explicitly inherits // from a protocol `Q` to be a subtype of `Q` to be a subtype of `Q` if it overrides @@ -650,61 +718,70 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { let nominally_satisfied = self.check_type_pair(db, type_to_test, Type::NominalInstance(nominal_instance)); - if result - .union(db, self.constraints, nominally_satisfied) - .is_always_satisfied(db) - { - return result; - } - - // `Generator` special case: compare the type parameters nominally. Prior to 3.13, - // its return type does not appear non-recursively in the protocol; from 3.13 onward, - // structurally inferring through `close() -> ReturnT | None` can spuriously infer - // `None`. - // TODO: Remove the Python 3.13+ extension of this special case once + // `Generator` parameters must be compared nominally. The class specialization + // already materializes each parameter according to its variance, while structural + // inference through `close() -> ReturnT | None` can infer a spurious `None` on + // Python 3.13 and newer. + // TODO: Remove the Python 3.13+ extension once // https://github.com/astral-sh/ty/issues/3596 is fixed. - if let Some(source_protocol) = ty.as_protocol_instance() - && let Protocol::FromClass(source_class) = source_protocol.inner - && let Protocol::FromClass(proto_class) = protocol.inner - && source_class.is_known(db, KnownClass::Generator) - && proto_class.is_known(db, KnownClass::Generator) + if nominal_instance.has_known_class(db, KnownClass::Generator) + && source_protocol_as_nominal + .is_some_and(|source| source.has_known_class(db, KnownClass::Generator)) { - return result; + return nominally_satisfied; } - if let Some(structurally_satisfied) = self.try_check_non_recursive_protocol_members( - db, - ty, - protocol, - source_protocol_as_nominal, - nominal_instance, - ) { - return result.or(db, self.constraints, || structurally_satisfied); - } + let env = self.env; + // A nominal relation that cannot succeed cannot bypass any materialized requirement. + // Check that inexpensive case first: comparing every requirement of an unrelated + // recursive protocol can expand its interface before structural member ordering gets + // a chance to reject an incompatible finite member. + let nominal_is_safe = nominally_satisfied.is_never_satisfied(db, env) + || (!protocol.materialization_changes_requirements(db, env, protocol) + && !source_protocol.is_some_and(|source| { + source.materialization_changes_requirements(db, env, protocol) + })); + + if nominal_is_safe { + if result + .union(db, self.constraints, nominally_satisfied) + .is_trivially_always_satisfied() + { + return result; + } + + if let Some(structurally_satisfied) = self.try_check_non_recursive_protocol_members( + db, + ty, + protocol, + source_protocol_as_nominal, + nominal_instance, + ) { + return result.or(db, self.constraints, || structurally_satisfied); + } - // For union simplification, failing the nominal relation between two - // specializations of the same protocol class is enough to keep both union elements. - // Falling back to the structural relation can recursively compare every protocol - // member even though a failed redundancy check only means that we preserve a - // potentially redundant union arm. - if matches!(self.relation, TypeRelation::Redundancy { pure: false }) - && ty - .as_protocol_instance() - .and_then(ProtocolInstanceType::to_nominal_instance) - .is_some_and(|source_instance| { - source_instance.class(db).class_literal(db) - == nominal_instance.class(db).class_literal(db) + // For union simplification, failing the nominal relation between two + // specializations of the same protocol class is enough to keep both union elements. + // Falling back to the structural relation can recursively compare every protocol + // member even though a failed redundancy check only means that we preserve a + // potentially redundant union arm. + if matches!(self.relation, TypeRelation::Redundancy { pure: false }) + && source_protocol_as_nominal.is_some_and(|source_instance| { + source_instance.class(db, env).class_literal(db) + == nominal_instance.class(db, env).class_literal(db) }) - { - return nominally_satisfied; + { + return nominally_satisfied; + } } } // Fast path: skip expensive per-member type comparisons when members are plainly // missing. When collecting error context, we continue and let the structural check // below report per-member errors instead. + let env = self.env; if !self.is_context_collection_enabled() - && !has_all_protocol_members_defined(db, ty, protocol) + && !has_all_protocol_members_defined(db, env, ty, protocol) { return result; } @@ -718,7 +795,6 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { ) } else { protocol - .inner .interface(db) .members(db) .when_all(db, self.constraints, |member| { @@ -726,7 +802,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { }) }; if let Some(context) = self.report_context() - && structurally_satisfied.is_never_satisfied(db) + && structurally_satisfied.is_never_satisfied(db, env) { context.push(ErrorContext::TypeNotCompatibleWithProtocol { ty, @@ -758,9 +834,11 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { return None; }; let source_instance = source_protocol_as_nominal?; - let (ClassType::Generic(source_alias), ClassType::Generic(target_alias)) = - (source_instance.class(db), nominal_instance.class(db)) - else { + let env = self.env; + let (ClassType::Generic(source_alias), ClassType::Generic(target_alias)) = ( + source_instance.class(db, env), + nominal_instance.class(db, env), + ) else { return None; }; if source_alias.origin(db) != target_alias.origin(db) { @@ -774,19 +852,32 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { let source_interface = source_protocol.interface(db); let target_interface = protocol.interface(db); let source_non_recursive = - non_recursive_protocol_interface(db, source_interface, identity_protocol, ty); + non_recursive_protocol_interface(db, source_interface.base(), identity_protocol, ty); let target_non_recursive = non_recursive_protocol_interface( db, - target_interface, + target_interface.base(), identity_protocol, Type::ProtocolInstance(protocol), ); - if source_non_recursive == source_interface && target_non_recursive == target_interface { + if source_non_recursive == source_interface.base() + && target_non_recursive == target_interface.base() + { return None; } - Some(self.check_protocol_interface_pair(db, ty, source_non_recursive, target_non_recursive)) + Some(self.check_protocol_interface_pair( + db, + ty, + ProtocolInterfaceView::new( + source_non_recursive, + source_interface.materialization_kind(), + ), + ProtocolInterfaceView::new( + target_non_recursive, + target_interface.materialization_kind(), + ), + )) } /// Return whether a class-object type inhabits `type[protocol]`. @@ -804,12 +895,13 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { meta_ty: Type<'db>, protocol: ProtocolInstanceType<'db>, ) -> ConstraintSet<'db, 'c> { + let env = self.env; debug_assert!(matches!( meta_ty, Type::ClassLiteral(_) | Type::SubclassOf(_) | Type::GenericAlias(_) )); - let constructed_ty = meta_ty.bindings(db).return_type(db); + let constructed_ty = meta_ty.bindings(db, env).return_type(db, env); self.check_type_pair(db, constructed_ty, Type::ProtocolInstance(protocol)) .and(db, self.constraints, || { self.check_meta_protocol_members(db, constructed_ty, meta_ty, protocol) @@ -819,6 +911,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { pub(super) fn check_nominal_instance_pair( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, source: NominalInstanceType<'db>, target: NominalInstanceType<'db>, ) -> ConstraintSet<'db, 'c> { @@ -829,12 +922,15 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { NominalInstanceInner::ExactTuple(target_tuple), ) => self.check_tuple_type_pair(db, source_tuple, target_tuple), _ => { - if let Some(result) = - self.check_anon_named_tuple_pair(db, source.class(db), target.class(db)) - { + if let Some(result) = self.check_anon_named_tuple_pair( + db, + env, + source.class(db, env), + target.class(db, env), + ) { return result; } - self.check_class_pair(db, source.class(db), target.class(db)) + self.check_class_pair(db, source.class(db, env), target.class(db, env)) } } } @@ -848,6 +944,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { fn check_anon_named_tuple_pair( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, source: ClassType<'db>, target: ClassType<'db>, ) -> Option> { @@ -891,7 +988,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { result = result.and(db, self.constraints, || { self.check_type_pair(db, sf.ty, tf.ty) }); - if result.is_never_satisfied(db) { + if result.is_never_satisfied(db, env) { return Some(result); } } @@ -916,13 +1013,18 @@ fn non_recursive_protocol_interface<'db>( protocol: ProtocolClass<'db>, receiver_ty: Type<'db>, ) -> ProtocolInterface<'db> { - struct ProtocolReferenceFinder<'db> { + struct ProtocolReferenceFinder<'a, 'db> { + env: &'a ProgramEnvironment<'db>, origin: ClassLiteral<'db>, found: Cell, recursion_guard: TypeCollector<'db>, } - impl<'db> TypeVisitor<'db> for ProtocolReferenceFinder<'db> { + impl<'db> TypeVisitor<'db> for ProtocolReferenceFinder<'_, 'db> { + fn program_environment(&self) -> &ProgramEnvironment<'db> { + self.env + } + fn should_visit_lazy_type_attributes(&self) -> bool { false } @@ -938,8 +1040,10 @@ fn non_recursive_protocol_interface<'db>( if ty .as_protocol_instance() - .and_then(ProtocolInstanceType::to_nominal_instance) - .is_some_and(|instance| instance.class_literal(db) == self.origin) + .and_then(|protocol| protocol.nominal_origin_instance(db)) + .is_some_and(|instance| { + instance.class_literal(db, self.program_environment()) == self.origin + }) { self.found.set(true); return; @@ -949,8 +1053,10 @@ fn non_recursive_protocol_interface<'db>( } } + let env = ProgramEnvironment::from_file(protocol.class_literal(db).program_file(db)); interface.filter_members(db, |member| { let visitor = ProtocolReferenceFinder { + env: &env, origin: protocol.class_literal(db), found: Cell::new(false), recursion_guard: TypeCollector::default(), @@ -960,6 +1066,45 @@ fn non_recursive_protocol_interface<'db>( }) } +/// Infers protocol constraints without expanding recursive member requirements. +/// +/// The target view retains its materialization, so readable and writable members are still +/// materialized in their respective variance positions. The complete target protocol must be +/// checked separately after generic inference. +#[salsa::tracked( + returns(ref), + cycle_initial = |_, _, _, _| OwnedConstraintSet::always(), + heap_size = ruff_memory_usage::heap_size, +)] +fn non_recursive_protocol_constraints<'db>( + db: &'db dyn Db, + source: ProtocolInstanceType<'db>, + target: ProtocolInterfaceView<'db>, +) -> OwnedConstraintSet<'db> { + let env = ProgramEnvironment::from_program(target.base().program(db)); + let constraints = ConstraintSetBuilder::new(); + constraints.into_owned(|constraints| { + let relation_visitor = HasRelationToVisitor::default(constraints); + let disjointness_visitor = IsDisjointVisitor::default(constraints); + let signature_relation_visitor = SignatureRelationVisitor::default(); + let materialization_visitor = ApplyTypeMappingVisitor::new(&env); + let checker = TypeRelationChecker::constraint_set_assignability( + &env, + constraints, + &relation_visitor, + &disjointness_visitor, + &signature_relation_visitor, + &materialization_visitor, + ); + checker.check_protocol_interface_pair( + db, + Type::ProtocolInstance(source), + source.interface(db), + target, + ) + }) +} + impl<'c, 'db> DisjointnessChecker<'_, 'c, 'db> { /// Return `true` if this protocol type is disjoint from the protocol `other`. /// @@ -984,13 +1129,14 @@ impl<'c, 'db> DisjointnessChecker<'_, 'c, 'db> { if left.is_object() || right.is_object() { return result; } - if let Some(left_spec) = left.tuple_spec(db) - && let Some(right_spec) = right.tuple_spec(db) + let env = self.env; + if let Some(left_spec) = left.tuple_spec(db, env) + && let Some(right_spec) = right.tuple_spec(db, env) { let compatible = self.check_tuple_spec_pair(db, &left_spec, &right_spec); if result .union(db, self.constraints, compatible) - .is_always_satisfied(db) + .is_trivially_always_satisfied() { return result; } @@ -1000,8 +1146,13 @@ impl<'c, 'db> DisjointnessChecker<'_, 'c, 'db> { ConstraintSet::from_bool( self.constraints, !left - .class(db) - .could_coexist_in_mro_with_disjointness_checker(db, right.class(db), self), + .class(db, env) + .could_coexist_in_mro_with_disjointness_checker( + db, + env, + right.class(db, env), + self, + ), ) }) } @@ -1103,9 +1254,12 @@ struct RegexInstanceClass<'db> { // The Salsa heap is tracked separately. impl get_size2::GetSize for RegexInstanceClass<'_> {} -fn sys_version_info_class(db: &dyn Db) -> Option> { +fn sys_version_info_class<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, +) -> Option> { KnownClass::VersionInfo - .try_to_class_literal(db) + .try_to_class_literal(db, env) .map(|class| class.default_specialization(db)) } @@ -1116,8 +1270,13 @@ pub(crate) struct SliceLiteral { } impl<'db> VarianceInferable<'db> for NominalInstanceType<'db> { - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { - self.class(db).variance_of(db, typevar) + fn variance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { + self.class(db, env).variance_of(db, env, typevar) } } @@ -1139,16 +1298,23 @@ pub(super) fn walk_protocol_instance_type<'db, V: super::visitor::TypeVisitor<'d visitor: &V, ) { if visitor.should_visit_lazy_type_attributes() { - walk_protocol_interface(db, protocol.inner.interface(db), visitor); + walk_protocol_interface(db, protocol.interface(db), visitor); } else { match protocol.inner { - Protocol::FromClass(class) => { - if let Some((_, Some(specialization))) = class.static_class_literal(db) { + Protocol::FromClass(_) | Protocol::Materialized(_) => { + if let Some((_, Some(specialization))) = protocol + .class_origin(db) + .and_then(|class| class.static_class_literal(db)) + { walk_specialization(db, specialization, visitor); } } Protocol::Synthesized(synthesized) => { - walk_protocol_interface(db, synthesized.interface(db), visitor); + walk_protocol_interface( + db, + ProtocolInterfaceView::new(synthesized.interface(db), None), + visitor, + ); } } } @@ -1157,8 +1323,8 @@ pub(super) fn walk_protocol_instance_type<'db, V: super::visitor::TypeVisitor<'d impl<'db> ProtocolInstanceType<'db> { /// Return `true` if this is the standard-library `Hashable` protocol. pub(super) fn is_hashable(self, db: &'db dyn Db) -> bool { - self.to_nominal_instance() - .is_some_and(|instance| instance.class(db).is_known(db, KnownClass::Hashable)) + self.class_origin(db) + .is_some_and(|class| class.is_known(db, KnownClass::Hashable)) } // Keep this method private, so that the only way of constructing `ProtocolInstanceType` @@ -1179,40 +1345,125 @@ impl<'db> ProtocolInstanceType<'db> { } } - /// Return the class backing a class-based protocol instance. - pub(super) fn as_class_based(self) -> Option> { + /// Preserves a class-based protocol and the polarity of its pending materialization. + /// + /// Member requirements are materialized only when an operation observes them. + fn materialized( + db: &'db dyn Db, + origin: ProtocolClass<'db>, + materialization_kind: MaterializationKind, + ) -> Self { + Self { + inner: Protocol::Materialized(MaterializedProtocolType::new( + db, + origin, + materialization_kind, + )), + _phantom: PhantomData, + } + } + + /// Returns the nominal instance of a protocol's origin without asserting nominal subtyping. + pub(super) fn nominal_origin_instance( + self, + db: &'db dyn Db, + ) -> Option> { + self.class_origin(db).map(|origin| { + NominalInstanceType(NominalInstanceInner::NonTuple(NominalInstanceClass::Plain( + *origin, + ))) + }) + } + + /// Return the class that defines this protocol, if it is class-backed. + pub(super) fn class_origin(self, db: &'db dyn Db) -> Option> { match self.inner { Protocol::FromClass(class) => Some(class), Protocol::Synthesized(_) => None, + Protocol::Materialized(materialized) => Some(materialized.origin(db)), } } - /// If this is a class-based protocol, convert the protocol-instance into a nominal instance. - /// - /// If this is a synthesized protocol that does not correspond to a class definition - /// in source code, return `None`. These are "pure" abstract types, that cannot be - /// treated in a nominal way. - pub(super) fn to_nominal_instance(self) -> Option> { + /// Returns the pending materialization of a class-based protocol, if any. + pub(super) fn materialization_kind(self, db: &'db dyn Db) -> Option { match self.inner { - Protocol::FromClass(class) => Some(NominalInstanceType( - NominalInstanceInner::NonTuple(NominalInstanceClass::Plain(*class)), - )), - Protocol::Synthesized(_) => None, + Protocol::Materialized(materialized) => Some(materialized.materialization_kind(db)), + Protocol::FromClass(_) | Protocol::Synthesized(_) => None, } } - /// Return the class that defines this protocol, if it is class-backed. - pub(super) const fn class_origin(self) -> Option> { + /// Returns the class origin of a protocol with a pending materialization. + pub(super) fn materialized_origin(self, db: &'db dyn Db) -> Option> { match self.inner { - Protocol::FromClass(class) => Some(class), - Protocol::Synthesized(_) => None, + Protocol::Materialized(materialized) => Some(materialized.origin(db)), + Protocol::FromClass(_) | Protocol::Synthesized(_) => None, + } + } + + /// Returns the nominal origin when a materialized requirement is a property descriptor. + /// + /// Descriptor lookup needs the original property object even though ordinary reads expose + /// its lazily materialized value. + pub(super) fn materialized_origin_property( + self, + db: &'db dyn Db, + name: &str, + ) -> Option> { + self.materialized_origin(db) + .filter(|_| self.interface(db).member_is_property(db, name)) + } + + /// Returns whether a materialization changes any member required by `target`. + /// + /// An unrelated changed member must not prevent an explicitly inherited protocol from + /// satisfying its base nominally. + fn materialization_changes_requirements( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + target: ProtocolInstanceType<'db>, + ) -> bool { + self.materialization_kind(db).is_some() + && self + .interface(db) + .differs_for_members_required_by(db, env, target.interface(db)) + } + + /// Returns the materialization wrapper needed for displaying this protocol. + /// + /// Fully static requirements need no wrapper. A generic specialization can already display + /// its materialization, in which case adding another wrapper would duplicate `Top` or + /// `Bottom`. + pub(super) fn display_materialization_kind( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option { + let Protocol::Materialized(materialized) = self.inner else { + return None; + }; + let origin = materialized.origin(db); + if origin + .static_class_literal(db) + .and_then(|(_, specialization)| specialization) + .and_then(|specialization| specialization.materialization_kind(db)) + .is_some() + { + return None; } + + let interface = self.interface(db); + interface + .differs_for_members_required_by(db, env, interface) + .then_some(materialized.materialization_kind(db)) } /// Return the structural meta-type of this protocol-instance type. - pub(super) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { + pub(super) fn to_meta_type(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match self.inner { - Protocol::FromClass(_) => SubclassOfType::from_protocol(self), + Protocol::FromClass(_) | Protocol::Materialized(_) => { + SubclassOfType::from_protocol(self) + } // TODO: we can and should do better here. // @@ -1227,16 +1478,20 @@ impl<'db> ProtocolInstanceType<'db> { // 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), + Protocol::Synthesized(_) => KnownClass::Type.to_instance(db, env), } } /// Return the nominal meta-type used for internal class-member lookup on a protocol instance. - pub(super) fn to_nominal_meta_type(self, db: &'db dyn Db) -> Type<'db> { - match self.inner { - Protocol::FromClass(class) => SubclassOfType::from(db, *class), - Protocol::Synthesized(_) => self.to_meta_type(db), - } + pub(super) fn to_nominal_meta_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { + self.class_origin(db).map_or_else( + || self.to_meta_type(db, env), + |origin| SubclassOfType::from(db, env, *origin), + ) } /// Return `true` if this protocol is a supertype of `object`. @@ -1252,14 +1507,25 @@ impl<'db> ProtocolInstanceType<'db> { protocol: ProtocolInstanceType<'db>, _: (), ) -> bool { + let interface = protocol.interface(db); + + // Hashability is not preserved by inheritance: subclasses can replace + // `object.__hash__` with `None`. A protocol that explicitly requires `__hash__` + // therefore does not describe every object, despite `object` defining that method. + if interface.includes_member(db, "__hash__") { + return false; + } + + let env = ProgramEnvironment::from_program(interface.base().program(db)); let constraints = ConstraintSetBuilder::new(); let relation_visitor = HasRelationToVisitor::default(&constraints); let disjointness_visitor = IsDisjointVisitor::default(&constraints); let signature_relation_visitor = SignatureRelationVisitor::default(); - let materialization_visitor = ApplyTypeMappingVisitor::default(); + let materialization_visitor = ApplyTypeMappingVisitor::new(&env); let checker = TypeRelationChecker::subtyping( + &env, &constraints, - InferableTypeVars::None, + TypeVarSet::None, &relation_visitor, &disjointness_visitor, &signature_relation_visitor, @@ -1267,7 +1533,7 @@ impl<'db> ProtocolInstanceType<'db> { ); checker .check_type_satisfies_protocol(db, Type::object(), protocol) - .is_always_satisfied(db) + .is_always_satisfied(db, &env) } is_equivalent_to_object_inner(db, self, ()) @@ -1276,126 +1542,248 @@ impl<'db> ProtocolInstanceType<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { Some(Self { - inner: self.inner.recursive_type_normalized_impl(db, div, nested)?, + inner: self + .inner + .recursive_type_normalized_impl(db, env, div, nested)?, _phantom: PhantomData, }) } - pub(crate) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + /// Returns an effective materialized member without applying the nominal class fallback. + fn materialized_interface_member( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> Option> { + self.materialization_kind(db)?; + let interface = self.interface(db); + interface + .includes_member(db, name) + .then(|| interface.instance_member(db, env, name)) + } + + pub(crate) fn instance_member( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> PlaceAndQualifiers<'db> { match self.inner { - Protocol::FromClass(class) => class.instance_member(db, name), + Protocol::FromClass(class) => class.instance_member(db, env, name), Protocol::Synthesized(synthesized) => { - synthesized.interface(db).instance_member(db, name) + synthesized.interface(db).instance_member(db, env, name) } + Protocol::Materialized(materialized) => self + .materialized_interface_member(db, env, name) + .unwrap_or_else(|| materialized.origin(db).instance_member(db, env, name)), } } pub(super) fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { match self.inner { Protocol::FromClass(class) => { - Self::from_class(class.apply_type_mapping_impl(db, type_mapping, tcx, visitor)) + let mapped_class = + class.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor); + if let TypeMapping::Materialize(materialization_kind) = type_mapping { + Self::materialized(db, mapped_class, *materialization_kind) + } else { + Self::from_class(mapped_class) + } } Protocol::Synthesized(synthesized) => Self::synthesized( - synthesized.apply_type_mapping_impl(db, type_mapping, tcx, visitor), + synthesized.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor), ), + Protocol::Materialized(materialized) => { + if matches!(type_mapping, TypeMapping::Materialize(_)) { + self + } else { + Self::materialized( + db, + materialized.origin(db).apply_type_mapping_impl( + db, + env, + type_mapping, + tcx, + visitor, + ), + materialized.materialization_kind(db), + ) + } + } } } pub(super) fn find_legacy_typevars_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, ) { match self.inner { Protocol::FromClass(class) => { - class.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + class.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } Protocol::Synthesized(synthesized) => { - synthesized.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + synthesized.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); + } + Protocol::Materialized(materialized) => { + materialized.origin(db).find_legacy_typevars_impl( + db, + env, + binding_context, + typevars, + visitor, + ); } } } - pub(super) fn interface(self, db: &'db dyn Db) -> ProtocolInterface<'db> { + pub(super) fn interface(self, db: &'db dyn Db) -> ProtocolInterfaceView<'db> { self.inner.interface(db) } + + /// Returns constraints inferred from the nonrecursive requirements of `target`. + /// + /// Recursive requirements are omitted only while inferring a generic specialization. The + /// eventual argument check must still compare against the complete protocol interface. + pub(super) fn when_non_recursive_members_assignable_to_owned( + self, + db: &'db dyn Db, + target: Self, + ) -> Option<&'db OwnedConstraintSet<'db>> { + let origin = target.class_origin(db)?; + let interface = target.interface(db); + let non_recursive = non_recursive_protocol_interface( + db, + interface.base(), + origin, + Type::ProtocolInstance(target), + ); + let target = ProtocolInterfaceView::new(non_recursive, interface.materialization_kind()); + if target.member_count(db) == 0 { + return None; + } + + Some(non_recursive_protocol_constraints(db, self, target)) + } } impl<'db> VarianceInferable<'db> for ProtocolInstanceType<'db> { - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { - self.inner.variance_of(db, typevar) + fn variance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { + self.inner.variance_of(db, env, typevar) } } -/// 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. +/// A class-backed protocol materialization whose member requirements remain lazy. +#[salsa::interned(debug, heap_size = ruff_memory_usage::heap_size)] +pub(super) struct MaterializedProtocolType<'db> { + #[returns(copy)] + pub(super) origin: ProtocolClass<'db>, + #[returns(copy)] + pub(super) materialization_kind: MaterializationKind, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for MaterializedProtocolType<'_> {} + +/// A class-backed, synthesized, or lazily materialized protocol. #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize, salsa::SalsaValue)] pub(super) enum Protocol<'db> { FromClass(ProtocolClass<'db>), Synthesized(SynthesizedProtocolType<'db>), + Materialized(MaterializedProtocolType<'db>), } impl<'db> Protocol<'db> { /// Return the members of this protocol type - fn interface(self, db: &'db dyn Db) -> ProtocolInterface<'db> { + fn interface(self, db: &'db dyn Db) -> ProtocolInterfaceView<'db> { match self { - Self::FromClass(class) => class.interface(db), - Self::Synthesized(synthesized) => synthesized.interface(db), + Self::FromClass(class) => ProtocolInterfaceView::new(class.interface(db), None), + Self::Synthesized(synthesized) => { + ProtocolInterfaceView::new(synthesized.interface(db), None) + } + Self::Materialized(materialized) => ProtocolInterfaceView::new( + materialized.origin(db).unmaterialized_interface(db), + Some(materialized.materialization_kind(db)), + ), } } fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { match self { Self::FromClass(class) => Some(Self::FromClass( - class.recursive_type_normalized_impl(db, div, nested)?, + class.recursive_type_normalized_impl(db, env, div, nested)?, )), Self::Synthesized(synthesized) => Some(Self::Synthesized( - synthesized.recursive_type_normalized_impl(db, div, nested)?, + synthesized.recursive_type_normalized_impl(db, env, div, nested)?, )), + Self::Materialized(materialized) => { + Some(Self::Materialized(MaterializedProtocolType::new( + db, + materialized + .origin(db) + .recursive_type_normalized_impl(db, env, div, nested)?, + materialized.materialization_kind(db), + ))) + } } } - - pub(super) const fn is_synthesized(self) -> bool { - matches!(self, Self::Synthesized(_)) - } } impl<'db> VarianceInferable<'db> for Protocol<'db> { - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { + fn variance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { match self { - Protocol::FromClass(class_type) => class_type.variance_of(db, typevar), + Protocol::FromClass(class_type) => class_type.variance_of(db, env, typevar), Protocol::Synthesized(synthesized_protocol_type) => { - synthesized_protocol_type.variance_of(db, typevar) + synthesized_protocol_type.variance_of(db, env, typevar) + } + Protocol::Materialized(materialized) => { + materialized.origin(db).variance_of(db, env, typevar) } } } } mod synthesized_protocol { + use crate::types::protocol_class::ProtocolInterface; use crate::types::{ ApplyTypeMappingVisitor, BoundTypeVarIdentity, BoundTypeVarInstance, FindLegacyTypeVarsVisitor, Type, TypeContext, TypeMapping, TypeVarVariance, VarianceInferable, }; - use crate::{Db, FxOrderSet}; + use crate::{Db, FxOrderSet, ProgramEnvironment}; use ty_python_core::definition::Definition; /// A "synthesized" protocol type that is dissociated from a class definition in source code. @@ -1434,31 +1822,39 @@ mod synthesized_protocol { pub(super) fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { self.with_interface( db, self.interface(db) - .apply_type_mapping_impl(db, type_mapping, tcx, visitor), + .apply_type_mapping_impl(db, env, type_mapping, tcx, visitor), ) } pub(super) fn find_legacy_typevars_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, ) { - self.interface(db) - .find_legacy_typevars_impl(db, binding_context, typevars, visitor); + self.interface(db).find_legacy_typevars_impl( + db, + env, + binding_context, + typevars, + visitor, + ); } pub(in crate::types) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -1466,7 +1862,7 @@ mod synthesized_protocol { self.with_interface( db, self.interface(db) - .recursive_type_normalized_impl(db, div, nested)?, + .recursive_type_normalized_impl(db, env, div, nested)?, ), ) } @@ -1476,9 +1872,10 @@ mod synthesized_protocol { fn variance_of( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typevar: BoundTypeVarIdentity<'db>, ) -> TypeVarVariance { - self.interface(db).variance_of(db, typevar) + self.interface(db).variance_of(db, env, typevar) } } } diff --git a/crates/ty_python_semantic/src/types/iteration.rs b/crates/ty_python_semantic/src/types/iteration.rs index 16edc19a12..9670b8eb0f 100644 --- a/crates/ty_python_semantic/src/types/iteration.rs +++ b/crates/ty_python_semantic/src/types/iteration.rs @@ -1,15 +1,14 @@ -use crate::{ - Db, - types::{ - AwaitError, Bindings, CallArguments, CallDunderError, KnownClass, LintDiagnosticGuard, - LintDiagnosticGuardBuilder, LiteralValueTypeKind, Type, TypeContext, - TypeVarBoundOrConstraints, UnionType, - call::CallErrorKind, - context::InferContext, - diagnostic::{ITERATION_OVER_CHARACTER, NOT_ITERABLE}, - todo_type, - tuple::{TupleSpec, TupleSpecBuilder}, - }, +use crate::Db; +use crate::ProgramEnvironment; +use crate::types::{ + AwaitError, Bindings, CallArguments, CallDunderError, KnownClass, LintDiagnosticGuard, + LintDiagnosticGuardBuilder, LiteralValueTypeKind, Type, TypeContext, TypeVarBoundOrConstraints, + UnionType, + call::CallErrorKind, + context::InferContext, + diagnostic::{ITERATION_OVER_CHARACTER, NOT_ITERABLE}, + todo_type, + tuple::{TupleSpec, TupleSpecBuilder}, }; use compact_str::ToCompactString; use ruff_python_ast as ast; @@ -22,11 +21,13 @@ use ty_python_core::EvaluationMode; /// recursively unpacking starred elements whose iterables are also fixed-length. pub(crate) fn extract_fixed_length_iterable_element_types<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, iterable: &ast::Expr, mut expression_type: impl FnMut(&ast::Expr) -> Type<'db>, ) -> Option]>> { fn extend_fixed_length_iterable<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, iterable: &ast::Expr, expression_type: &mut impl FnMut(&ast::Expr) -> Type<'db>, element_types: &mut Vec>, @@ -42,6 +43,7 @@ pub(crate) fn extract_fixed_length_iterable_element_types<'db>( if let ast::Expr::Starred(starred) = element { extend_fixed_length_iterable( db, + env, starred.value.as_ref(), expression_type, element_types, @@ -54,14 +56,14 @@ pub(crate) fn extract_fixed_length_iterable_element_types<'db>( } let iterable_type = expression_type(iterable); - let spec = iterable_type.try_iterate(db).ok()?; + let spec = iterable_type.try_iterate(db, env).ok()?; let tuple = spec.as_fixed_length()?; element_types.extend(tuple.all_elements().iter().copied()); Some(()) } let mut element_types = Vec::new(); - extend_fixed_length_iterable(db, iterable, &mut expression_type, &mut element_types)?; + extend_fixed_length_iterable(db, env, iterable, &mut expression_type, &mut element_types)?; Some(element_types.into_boxed_slice()) } @@ -77,11 +79,12 @@ pub(crate) fn report_iteration_over_character<'db>( iterable_type: Type<'db>, iterable_node: ast::AnyNodeRef, ) { + let env = context.program_environment(); let db = context.db(); let Type::NominalInstance(instance) = iterable_type else { return; }; - if !instance.class(db).is_known(db, KnownClass::Character) { + if !instance.class(db, env).is_known(db, KnownClass::Character) { return; } if let Some(builder) = context.report_lint(&ITERATION_OVER_CHARACTER, iterable_node) { @@ -97,9 +100,14 @@ impl<'db> Type<'db> { /// /// 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. - pub(super) fn iterate(self, db: &'db dyn Db) -> Cow<'db, TupleSpec<'db>> { - self.try_iterate(db) - .unwrap_or_else(|err| Cow::Owned(TupleSpec::homogeneous(err.fallback_element_type(db)))) + pub(super) fn iterate( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Cow<'db, TupleSpec<'db>> { + self.try_iterate(db, env).unwrap_or_else(|err| { + Cow::Owned(TupleSpec::homogeneous(err.fallback_element_type(db, env))) + }) } /// Given the type of an object that is iterated over in some way, @@ -113,17 +121,20 @@ impl<'db> Type<'db> { pub(super) fn try_iterate( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ) -> Result>, IterationError<'db>> { - self.try_iterate_with_mode(db, EvaluationMode::Sync) + self.try_iterate_with_mode(db, env, EvaluationMode::Sync) } pub(super) fn try_iterate_with_mode( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, mode: EvaluationMode, ) -> Result>, IterationError<'db>> { fn non_async_special_case<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, ) -> Option>> { // We will not infer precise heterogeneous tuple specs for literals with lengths above this threshold. @@ -133,16 +144,19 @@ impl<'db> Type<'db> { const MAX_TUPLE_LENGTH: usize = 128; match ty { - Type::NominalInstance(nominal) => nominal.tuple_spec(db), - Type::NewTypeInstance(newtype) => non_async_special_case(db, newtype.concrete_base_type(db)), - Type::Overlapping(overlapping) => non_async_special_case(db, overlapping.value_type(db)), - Type::Restricted(restricted) => non_async_special_case(db, restricted.value_type(db)), - Type::Deferred(deferred) => non_async_special_case(db, deferred.reduced(db)), + Type::NominalInstance(nominal) => nominal.tuple_spec(db, env), + Type::NewTypeInstance(newtype) => non_async_special_case(db, env, newtype.concrete_base_type(db)), + Type::Overlapping(overlapping) => non_async_special_case(db, env, overlapping.value_type(db, env)), + Type::Restricted(restricted) => non_async_special_case(db, env, restricted.value_type(db)), + Type::Deferred(deferred) => non_async_special_case(db, env, deferred.reduced(db, env)), Type::GenericAlias(alias) if alias.origin(db).is_tuple(db) => { Some(Cow::Owned(TupleSpec::homogeneous(todo_type!( "*tuple[] annotations" )))) } + Type::GenericAlias(alias) if alias.origin(db).is_tuple(db) => Some(Cow::Owned( + TupleSpec::homogeneous(todo_type!("*tuple[] annotations")), + )), Type::LiteralValue(literal) => match literal.kind() { LiteralValueTypeKind::Bytes(bytes) => { let bytes_literal = bytes.value(db); @@ -150,13 +164,13 @@ impl<'db> Type<'db> { TupleSpec::heterogeneous( bytes_literal .iter() - .map(|b| Type::int_literal( i64::from(*b))), + .map(|b| Type::int_literal(i64::from(*b))), ) } else { - TupleSpec::homogeneous(KnownClass::Int.to_instance(db)) + TupleSpec::homogeneous(KnownClass::Int.to_instance(db, env)) }; Some(Cow::Owned(spec)) - }, + } LiteralValueTypeKind::String(string_literal_ty) => { let string_literal = string_literal_ty.value(db); let spec = if string_literal.len() < MAX_TUPLE_LENGTH { @@ -174,8 +188,8 @@ impl<'db> Type<'db> { LiteralValueTypeKind::LiteralString => { Some(Cow::Owned(TupleSpec::homogeneous(ty))) } - _ => None - } + _ => None, + }, Type::Never => { // The dunder logic below would have us return `tuple[Never, ...]`, which eagerly // simplifies to `tuple[()]`. That will will cause us to emit false positives if we @@ -184,23 +198,32 @@ impl<'db> Type<'db> { // diagnostic in unreachable code. Some(Cow::Owned(TupleSpec::homogeneous(Type::unknown()))) } - Type::TypeAlias(alias) => { - non_async_special_case(db, alias.value_type(db)) - } - Type::TypeVar(tvar) => match tvar.typevar(db).bound_or_constraints(db)? { + Type::TypeAlias(alias) => non_async_special_case(db, env, alias.value_type(db)), + Type::TypeVar(tvar) => match tvar.typevar(db).bound_or_constraints(db, env)? { TypeVarBoundOrConstraints::UpperBound(bound) => { - non_async_special_case(db, bound) + non_async_special_case(db, env, bound) + } + TypeVarBoundOrConstraints::Constraints(constraints) => { + non_async_special_case(db, env, constraints.as_type(db, env)) } - TypeVarBoundOrConstraints::Constraints(constraints) => non_async_special_case(db, constraints.as_type(db)), }, Type::Union(union) => { let elements = union.elements(db); if elements.len() < MAX_TUPLE_LENGTH { let mut elements_iter = elements.iter(); - let first_element_spec = elements_iter.next()?.try_iterate_with_mode(db, EvaluationMode::Sync).ok()?; + let first_element_spec = elements_iter + .next()? + .try_iterate_with_mode(db, env, EvaluationMode::Sync) + .ok()?; let mut builder = TupleSpecBuilder::from(&*first_element_spec); for element in elements_iter { - builder = builder.union(db, &*element.try_iterate_with_mode(db, EvaluationMode::Sync).ok()?); + builder = builder.union( + db, + env, + &*element + .try_iterate_with_mode(db, env, EvaluationMode::Sync) + .ok()?, + ); } Some(Cow::Owned(builder.build())) } else { @@ -221,20 +244,24 @@ impl<'db> Type<'db> { // - A simpler type (if it fully simplified). // // We then iterate over the flattened type. - let flattened = ty.flatten_typevars(db); + let flattened = ty.flatten_typevars(db, env); // If flattening didn't change anything, iterate the intersection directly. if flattened == ty { - let mut specs_iter = intersection.positive_elements_or_object(db).filter_map( - |element| element.try_iterate_with_mode(db, EvaluationMode::Sync).ok(), - ); + let mut specs_iter = intersection + .positive_elements_or_object(db) + .filter_map(|element| { + element + .try_iterate_with_mode(db, env, EvaluationMode::Sync) + .ok() + }); let first_spec = specs_iter.next()?; let mut builder = TupleSpecBuilder::from(&*first_spec); for spec in specs_iter { // Two tuples cannot have incompatible specs unless the tuples themselves // are disjoint. `IntersectionBuilder` eagerly simplifies such // intersections to `Never`, so this should always return `Some`. - let Some(intersected) = builder.intersect(db, &spec) else { + let Some(intersected) = builder.intersect(db, env, &spec) else { return Some(Cow::Owned(TupleSpec::homogeneous(Type::unknown()))); }; builder = intersected; @@ -243,10 +270,10 @@ impl<'db> Type<'db> { } // Flattening changed the type; recursively iterate the flattened result. - flattened.try_iterate(db).ok() + flattened.try_iterate(db, env).ok() } Type::EnumComplement(complement) => { - non_async_special_case(db, complement.remaining_literal_union(db)) + non_async_special_case(db, env, complement.remaining_literal_union(db, env)) } // N.B. This special case isn't strictly necessary, it's just an obvious optimization Type::Dynamic(_) => Some(Cow::Owned(TupleSpec::homogeneous(ty))), @@ -261,11 +288,6 @@ impl<'db> Type<'db> { | Type::DataclassTransformer(_) | Type::Callable(_) | Type::ModuleLiteral(_) - // We could infer a precise tuple spec for enum classes with members, - // but it's not clear whether that's worth the added complexity: - // you'd have to check that `EnumMeta.__iter__` is not overridden for it to be sound - // (enums can have `EnumMeta` subclasses as their metaclasses). - | Type::ClassLiteral(_) | Type::SubclassOf(_) | Type::ProtocolInstance(_) | Type::SpecialForm(_) @@ -277,6 +299,7 @@ impl<'db> Type<'db> { | Type::TypeIs(_) | Type::TypeGuard(_) | Type::TypeForm(_) + | Type::ClassLiteral(_) | Type::TypedDict(_) // No fast path: fall through to `__iter__`/`__getitem__` resolution, which // already looks members up across the materializations. @@ -286,9 +309,9 @@ impl<'db> Type<'db> { if mode.is_async() { if let Type::Intersection(_) = self { - let flattened = self.flatten_typevars(db); + let flattened = self.flatten_typevars(db, env); if flattened != self { - return flattened.try_iterate_with_mode(db, mode); + return flattened.try_iterate_with_mode(db, env, mode); } } @@ -299,21 +322,25 @@ impl<'db> Type<'db> { iterator .try_call_dunder( db, + env, "__anext__", CallArguments::none(), TypeContext::default(), ) - .map(|dunder_anext_outcome| dunder_anext_outcome.return_type(db).try_await(db)) + .map(|dunder_anext_outcome| { + dunder_anext_outcome.return_type(db, env).try_await(db, env) + }) }; return match self.try_call_dunder( db, + env, "__aiter__", CallArguments::none(), TypeContext::default(), ) { Ok(dunder_aiter_bindings) => { - let iterator = dunder_aiter_bindings.return_type(db); + let iterator = dunder_aiter_bindings.return_type(db, env); match try_call_dunder_anext_on_iterator(iterator) { Ok(Ok(result)) => Ok(Cow::Owned(TupleSpec::homogeneous(result))), Ok(Err(AwaitError::InvalidReturnType(..))) => { @@ -332,7 +359,7 @@ impl<'db> Type<'db> { bindings: dunder_aiter_bindings, .. }) => { - let iterator = dunder_aiter_bindings.return_type(db); + let iterator = dunder_aiter_bindings.return_type(db, env); match try_call_dunder_anext_on_iterator(iterator) { Ok(_) => Err(IterationError::IterCallError { kind: CallErrorKind::PossiblyNotCallable, @@ -359,39 +386,42 @@ impl<'db> Type<'db> { }; } - if let Some(special_case) = non_async_special_case(db, self) { + if let Some(special_case) = non_async_special_case(db, env, self) { return Ok(special_case); } let try_call_dunder_getitem = || { self.try_call_dunder( db, + env, "__getitem__", - CallArguments::positional([KnownClass::Int.to_instance(db)]), + CallArguments::positional([KnownClass::Int.to_instance(db, env)]), TypeContext::default(), ) - .map(|dunder_getitem_outcome| dunder_getitem_outcome.return_type(db)) + .map(|dunder_getitem_outcome| dunder_getitem_outcome.return_type(db, env)) }; let try_call_dunder_next_on_iterator = |iterator: Type<'db>| { iterator .try_call_dunder( db, + env, "__next__", CallArguments::none(), TypeContext::default(), ) - .map(|dunder_next_outcome| dunder_next_outcome.return_type(db)) + .map(|dunder_next_outcome| dunder_next_outcome.return_type(db, env)) }; let dunder_iter_result = self .try_call_dunder( db, + env, "__iter__", CallArguments::none(), TypeContext::default(), ) - .map(|dunder_iter_outcome| dunder_iter_outcome.return_type(db)); + .map(|dunder_iter_outcome| dunder_iter_outcome.return_type(db, env)); match dunder_iter_result { Ok(iterator) => { @@ -413,7 +443,7 @@ impl<'db> Type<'db> { bindings: dunder_iter_outcome, unbound_on: unbound_on_iter, }) => { - let iterator = dunder_iter_outcome.return_type(db); + let iterator = dunder_iter_outcome.return_type(db, env); match try_call_dunder_next_on_iterator(iterator) { Ok(dunder_next_return) => { @@ -428,6 +458,7 @@ impl<'db> Type<'db> { // No diagnostic is emitted; iteration will always succeed! Cow::Owned(TupleSpec::homogeneous(UnionType::from_two_elements( db, + env, dunder_next_return, dunder_getitem_return_type, ))) @@ -521,24 +552,32 @@ pub(super) enum IterationError<'db> { } impl<'db> IterationError<'db> { - pub(super) fn fallback_element_type(&self, db: &'db dyn Db) -> Type<'db> { - self.element_type(db).unwrap_or(Type::unknown()) + pub(super) fn fallback_element_type( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { + self.element_type(db, env).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> { + pub(super) fn element_type( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { let return_type = |result: Result, CallDunderError<'db>>| { result - .map(|outcome| Some(outcome.return_type(db))) - .unwrap_or_else(|call_error| call_error.return_type(db)) + .map(|outcome| Some(outcome.return_type(db, env))) + .unwrap_or_else(|call_error| call_error.return_type(db, env)) }; match self { Self::IterReturnsInvalidIterator { dunder_error, mode, .. - } => dunder_error.return_type(db).and_then(|ty| { + } => dunder_error.return_type(db, env).and_then(|ty| { if mode.is_async() { - ty.try_await(db).ok() + ty.try_await(db, env).ok() } else { Some(ty) } @@ -550,16 +589,18 @@ impl<'db> IterationError<'db> { mode, } => { if mode.is_async() { - return_type(dunder_iter_bindings.return_type(db).try_call_dunder( + return_type(dunder_iter_bindings.return_type(db, env).try_call_dunder( db, + env, "__anext__", CallArguments::none(), TypeContext::default(), )) - .and_then(|ty| ty.try_await(db).ok()) + .and_then(|ty| ty.try_await(db, env).ok()) } else { - return_type(dunder_iter_bindings.return_type(db).try_call_dunder( + return_type(dunder_iter_bindings.return_type(db, env).try_call_dunder( db, + env, "__next__", CallArguments::none(), TypeContext::default(), @@ -578,16 +619,18 @@ impl<'db> IterationError<'db> { .. } => Some(UnionType::from_two_elements( db, + env, *dunder_next_return, - dunder_getitem_outcome.return_type(db), + dunder_getitem_outcome.return_type(db, env), )), CallDunderError::CallError(CallErrorKind::NotCallable, _, _) => { Some(*dunder_next_return) } CallDunderError::CallError(_, dunder_getitem_bindings, _) => { - let dunder_getitem_return = dunder_getitem_bindings.return_type(db); + let dunder_getitem_return = dunder_getitem_bindings.return_type(db, env); Some(UnionType::from_two_elements( db, + env, *dunder_next_return, dunder_getitem_return, )) @@ -596,7 +639,7 @@ impl<'db> IterationError<'db> { Self::UnboundIterAndGetitemError { dunder_getitem_error, - } => dunder_getitem_error.return_type(db), + } => dunder_getitem_error.return_type(db, env), Self::UnboundAiterError => None, } @@ -628,14 +671,15 @@ impl<'db> IterationError<'db> { /// A little helper type for emitting a diagnostic /// based on the variant of iteration error. - struct Reporter<'a> { + struct Reporter<'env, 'a> { db: &'a dyn Db, + env: &'env ProgramEnvironment<'a>, builder: LintDiagnosticGuardBuilder<'a, 'a>, iterable_type: Type<'a>, mode: EvaluationMode, } - impl<'a> Reporter<'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. @@ -645,22 +689,23 @@ impl<'db> IterationError<'db> { because: impl std::fmt::Display, error_context: ErrorContext, ) -> LintDiagnosticGuard<'a, 'a> { + let db = self.db; let mut diag = self.builder.into_diagnostic(format_args!( "Object of type `{iterable_type}` is not {maybe_async}iterable", - iterable_type = self.iterable_type.display(self.db), + iterable_type = self.iterable_type.display(db, self.env), maybe_async = if self.mode.is_async() { "async-" } else { "" } )); diag.info(because); if let ErrorContext::Enabled = error_context { let target = if self.mode.is_async() { - KnownClass::TyExtensionsAsyncIterable.to_instance_unknown(self.db) + KnownClass::TyExtensionsAsyncIterable.to_instance_unknown(db, self.env) } else { - KnownClass::TyExtensionsIterable.to_instance_unknown(self.db) + KnownClass::TyExtensionsIterable.to_instance_unknown(db, self.env) }; self.iterable_type - .assignability_error_context(self.db, target) - .attach_to(self.db, &mut diag); + .assignability_error_context(db, self.env, target) + .attach_to(db, self.env, &mut diag); } diag @@ -674,35 +719,38 @@ impl<'db> IterationError<'db> { because: impl std::fmt::Display, error_context: ErrorContext, ) -> LintDiagnosticGuard<'a, 'a> { + let db = self.db; let mut diag = self.builder.into_diagnostic(format_args!( "Object of type `{iterable_type}` may not be {maybe_async}iterable", - iterable_type = self.iterable_type.display(self.db), + iterable_type = self.iterable_type.display(db, self.env), maybe_async = if self.mode.is_async() { "async-" } else { "" } )); diag.info(because); if let ErrorContext::Enabled = error_context { let target = if self.mode.is_async() { - KnownClass::TyExtensionsAsyncIterable.to_instance_unknown(self.db) + KnownClass::TyExtensionsAsyncIterable.to_instance_unknown(db, self.env) } else { - KnownClass::TyExtensionsIterable.to_instance_unknown(self.db) + KnownClass::TyExtensionsIterable.to_instance_unknown(db, self.env) }; self.iterable_type - .assignability_error_context(self.db, target) - .attach_to(self.db, &mut diag); + .assignability_error_context(db, self.env, target) + .attach_to(db, self.env, &mut diag); } diag } } + let db = context.db(); let Some(builder) = context.report_lint(&NOT_ITERABLE, iterable_node) else { return; }; - let db = context.db(); + let env = context.program_environment(); let mode = self.mode(); let reporter = Reporter { db, + env, builder, iterable_type, mode, @@ -725,17 +773,21 @@ impl<'db> IterationError<'db> { match kind { CallErrorKind::NotCallable => { - reporter.is_not(format_args!( - "Its `{method}` attribute has type `{dunder_iter_type}`, which is not callable", - dunder_iter_type = bindings.callable_type().display(db), - ), ErrorContext::Disabled); + reporter.is_not( + format_args!( + "Its `{method}` attribute has type `{dunder_iter_type}`, \ + which is not callable", + dunder_iter_type = bindings.callable_type().display(db, env), + ), + ErrorContext::Disabled, + ); } CallErrorKind::PossiblyNotCallable => { reporter.may_not( format_args!( "Its `{method}` attribute (with type `{dunder_iter_type}`) \ may not be callable", - dunder_iter_type = bindings.callable_type().display(db), + dunder_iter_type = bindings.callable_type().display(db, env), ), ErrorContext::Disabled, ); @@ -755,7 +807,7 @@ impl<'db> IterationError<'db> { ); diag.info(format_args!( "Type of `{method}` is `{dunder_iter_type}`", - dunder_iter_type = bindings.callable_type().display(db), + dunder_iter_type = bindings.callable_type().display(db, env), )); diag.info(format_args!( "Expected signature for `{method}` is `def {method}(self): ...`", @@ -782,52 +834,83 @@ impl<'db> IterationError<'db> { }; match dunder_next_error { CallDunderError::MethodNotAvailable => { - reporter.is_not(format_args!( - "Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \ - which has no `{dunder_next_name}` method", - iterator_type = iterator.display(db), - ), ErrorContext::Disabled); + reporter.is_not( + format_args!( + "Its `{dunder_iter_name}` method returns an object of type \ + `{iterator_type}`, which has no `{dunder_next_name}` method", + iterator_type = iterator.display(db, env), + ), + ErrorContext::Disabled, + ); } CallDunderError::PossiblyUnbound { .. } => { - reporter.may_not(format_args!( - "Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \ - which may not have a `{dunder_next_name}` method", - iterator_type = iterator.display(db), - ), ErrorContext::Enabled); + reporter.may_not( + format_args!( + "Its `{dunder_iter_name}` method returns \ + an object of type `{iterator_type}`, \ + which may not have a `{dunder_next_name}` method", + iterator_type = iterator.display(db, env), + ), + ErrorContext::Enabled, + ); } CallDunderError::CallError(CallErrorKind::NotCallable, _, _) => { - reporter.is_not(format_args!( - "Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \ - which has a `{dunder_next_name}` attribute that is not callable", - iterator_type = iterator.display(db), - ), ErrorContext::Disabled); + reporter.is_not( + format_args!( + "Its `{dunder_iter_name}` method returns \ + an object of type `{iterator_type}`, \ + which has a `{dunder_next_name}` attribute \ + that is not callable", + iterator_type = iterator.display(db, env), + ), + ErrorContext::Disabled, + ); } CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, _, _) => { - reporter.may_not(format_args!( - "Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \ - which has a `{dunder_next_name}` attribute that may not be callable", - iterator_type = iterator.display(db), - ), ErrorContext::Enabled); + reporter.may_not( + format_args!( + "Its `{dunder_iter_name}` method returns \ + an object of type `{iterator_type}`, \ + which has a `{dunder_next_name}` attribute \ + that may not be callable", + iterator_type = iterator.display(db, env), + ), + ErrorContext::Enabled, + ); } CallDunderError::CallError(CallErrorKind::BindingError, bindings, _) if bindings.is_single() => { reporter - .is_not(format_args!( - "Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \ - which has an invalid `{dunder_next_name}` method", - iterator_type = iterator.display(db), - ), ErrorContext::Enabled) - .info(format_args!("Expected signature for `{dunder_next_name}` is `def {dunder_next_name}(self): ...`")); + .is_not( + format_args!( + "Its `{dunder_iter_name}` method returns \ + an object of type `{iterator_type}`, \ + which has an invalid `{dunder_next_name}` method", + iterator_type = iterator.display(db, env), + ), + ErrorContext::Enabled, + ) + .info(format_args!( + "Expected signature for `{dunder_next_name}` is \ + `def {dunder_next_name}(self): ...`" + )); } CallDunderError::CallError(CallErrorKind::BindingError, _, _) => { reporter - .may_not(format_args!( - "Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \ - which may have an invalid `{dunder_next_name}` method", - iterator_type = iterator.display(db), - ), ErrorContext::Enabled) - .info(format_args!("Expected signature for `{dunder_next_name}` is `def {dunder_next_name}(self): ...`")); + .may_not( + format_args!( + "Its `{dunder_iter_name}` method returns an object \ + of type `{iterator_type}`, which may have \ + an invalid `{dunder_next_name}` method", + iterator_type = iterator.display(db, env), + ), + ErrorContext::Enabled, + ) + .info(format_args!( + "Expected signature for `{dunder_next_name}` is \ + `def {dunder_next_name}(self): ...`" + )); } } } @@ -853,7 +936,7 @@ impl<'db> IterationError<'db> { "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), + dunder_getitem_type = bindings.callable_type().display(db, env), ), ErrorContext::Disabled, ), @@ -870,9 +953,10 @@ impl<'db> IterationError<'db> { 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), + and its `__getitem__` attribute \ + (with type `{dunder_getitem_type}`) \ + may not be callable", + dunder_getitem_type = bindings.callable_type().display(db, env), ), ErrorContext::Disabled, ) @@ -897,9 +981,11 @@ impl<'db> IterationError<'db> { let mut diag = 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), + 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, env), ), ErrorContext::Disabled, ); @@ -915,7 +1001,7 @@ impl<'db> IterationError<'db> { for ty in unbound_on.iter().copied() { diag.info(format_args!( "`{}` does not implement `__iter__`", - ty.display(db) + ty.display(db, env) )); } } @@ -940,9 +1026,9 @@ impl<'db> IterationError<'db> { 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), + its `__getitem__` attribute has type `{dunder_getitem_type}`, \ + which is not callable", + dunder_getitem_type = bindings.callable_type().display(db, env), ), ErrorContext::Disabled, ); @@ -957,13 +1043,16 @@ impl<'db> IterationError<'db> { ); } CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, bindings, _) => { - reporter.may_not( - "It has no `__iter__` method and its `__getitem__` attribute is invalid", - ErrorContext::Disabled, - ).info(format_args!( - "`__getitem__` has type `{dunder_getitem_type}`, which is not callable", - dunder_getitem_type = bindings.callable_type().display(db), - )); + reporter + .may_not( + "It has no `__iter__` method \ + and its `__getitem__` attribute is invalid", + ErrorContext::Disabled, + ) + .info(format_args!( + "`__getitem__` has type `{dunder_getitem_type}`, which is not callable", + dunder_getitem_type = bindings.callable_type().display(db, env), + )); } CallDunderError::CallError(CallErrorKind::BindingError, bindings, _) if bindings.is_single() => @@ -986,9 +1075,11 @@ impl<'db> IterationError<'db> { .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), + 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, env), ), ErrorContext::Disabled, ) diff --git a/crates/ty_python_semantic/src/types/known_instance.rs b/crates/ty_python_semantic/src/types/known_instance.rs index c45a5c754c..86a44602d6 100644 --- a/crates/ty_python_semantic/src/types/known_instance.rs +++ b/crates/ty_python_semantic/src/types/known_instance.rs @@ -1,3 +1,4 @@ +use crate::ProgramEnvironment; use itertools::Either; use ruff_python_ast::name::Name; @@ -241,10 +242,15 @@ pub(super) fn walk_known_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Size } impl<'db> VarianceInferable<'db> for KnownInstanceType<'db> { - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { + fn variance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { match self { KnownInstanceType::TypeAliasType(type_alias) => { - type_alias.raw_value_type(db).variance_of(db, typevar) + type_alias.raw_value_type(db).variance_of(db, env, typevar) } _ => TypeVarVariance::Bivariant, } @@ -255,6 +261,7 @@ impl<'db> KnownInstanceType<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -269,45 +276,45 @@ impl<'db> KnownInstanceType<'db> { Self::TypeVar(typevar) => Some(Self::TypeVar(typevar)), Self::TypeAliasType(type_alias) => Some(Self::TypeAliasType(type_alias)), Self::Field(field) => field - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Self::Field), Self::UnionType(union_type) => union_type - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Self::UnionType), Self::Literal(ty) => ty - .recursive_type_normalized_impl(db, div, true) + .recursive_type_normalized_impl(db, env, div, true) .map(Self::Literal), Self::Annotated(ty) => ty - .recursive_type_normalized_impl(db, div, true) + .recursive_type_normalized_impl(db, env, div, true) .map(Self::Annotated), Self::WrappedOptional(ty) => ty - .recursive_type_normalized_impl(db, div, true) + .recursive_type_normalized_impl(db, env, div, true) .map(Self::WrappedOptional), Self::TypeGenericAlias(ty) => ty - .recursive_type_normalized_impl(db, div, true) + .recursive_type_normalized_impl(db, env, div, true) .map(Self::TypeGenericAlias), Self::LiteralStringAlias(ty) => ty - .recursive_type_normalized_impl(db, div, true) + .recursive_type_normalized_impl(db, env, div, true) .map(Self::LiteralStringAlias), Self::Callable(callable) => callable - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Self::Callable), Self::NewType(newtype) => newtype - .recursive_type_normalized_impl(db, div, true) + .recursive_type_normalized_impl(db, env, div, true) .map(Self::NewType), Self::Sentinel(sentinel) => Some(Self::Sentinel(sentinel)), Self::GenericContext(generic) => Some(Self::GenericContext(generic)), Self::Specialization(specialization) => specialization - .recursive_type_normalized_impl(db, div, true) + .recursive_type_normalized_impl(db, env, div, true) .map(Self::Specialization), Self::NamedTupleSpec(spec) => spec - .recursive_type_normalized_impl(db, div, true) + .recursive_type_normalized_impl(db, env, div, true) .map(Self::NamedTupleSpec), Self::FunctoolsPartial(partial) => partial - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Self::FunctoolsPartial), Self::FunctoolsPartialCall(partial) => partial - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Self::FunctoolsPartialCall), } } @@ -324,7 +331,7 @@ impl<'db> KnownInstanceType<'db> { KnownClass::TypeVarTuple } Self::TypeVar(_) => KnownClass::TypeVar, - Self::TypeAliasType(TypeAliasType::PEP695(alias)) if alias.is_specialized(db) => { + Self::TypeAliasType(alias) if alias.specialization(db).is_some() => { KnownClass::GenericAlias } Self::TypeAliasType(_) => KnownClass::TypeAliasType, @@ -352,31 +359,39 @@ impl<'db> KnownInstanceType<'db> { } } - pub(super) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { - self.class(db).to_class_literal(db) + pub(super) fn to_meta_type(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + self.class(db).to_class_literal(db, env) } /// 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)` + /// `typing.TypeAliasType`, so `KnownInstanceType::TypeAliasType(_).instance_fallback(db, python_version)` /// returns `Type::NominalInstance(NominalInstanceType { class: })`. - pub(super) fn instance_fallback(self, db: &'db dyn Db) -> Type<'db> { - self.class(db).to_instance(db) + pub(super) fn instance_fallback( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { + self.class(db).to_instance(db, env) } /// Return the type denoted by this retained runtime type-expression object. /// /// This is the scope-independent subset of `Type::in_type_expression` used when a value /// reaches a `TypeForm` position after it has already been inferred in value context. - pub(crate) fn type_form_argument(self, db: &'db dyn Db) -> Option> { + pub(crate) fn type_form_argument( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { match self { Self::TypeAliasType(alias) => Some(Type::TypeAlias(alias)), Self::UnionType(instance) => instance.union_type(db).as_ref().ok().copied(), Self::Literal(ty) | Self::Annotated(ty) | Self::LiteralStringAlias(ty) => { Some(ty.inner(db)) } - Self::TypeGenericAlias(instance) => Some(instance.inner(db).to_meta_type(db)), + Self::TypeGenericAlias(instance) => Some(instance.inner(db).to_meta_type(db, env)), Self::Callable(callable) => Some(Type::Callable(callable)), Self::NewType(newtype) => Some(Type::NewTypeInstance(newtype)), Self::Sentinel(sentinel) => { @@ -403,21 +418,31 @@ impl<'db> KnownInstanceType<'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(db).is_subclass_of(db, class) + pub(super) fn is_instance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + class: ClassType, + ) -> bool { + self.class(db).is_subclass_of(db, env, class) } /// Return the repr of the symbol at runtime - pub(super) fn repr(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db { - self.display_with(db, DisplaySettings::default()) + pub(super) fn repr<'env>( + self, + db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, + ) -> impl std::fmt::Display + 'env { + self.display_with(db, env, DisplaySettings::default()) } pub(super) fn apply_type_mapping_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'_, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Type<'db> { match self { KnownInstanceType::TypeVar(typevar) => match type_mapping { @@ -445,21 +470,21 @@ impl<'db> KnownInstanceType<'db> { }, KnownInstanceType::UnionType(instance) => { Type::KnownInstance(KnownInstanceType::UnionType( - instance.apply_type_mapping_impl(db, type_mapping, tcx, visitor), + instance.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor), )) } KnownInstanceType::Annotated(ty) => { Type::KnownInstance(KnownInstanceType::Annotated(InternedType::new( db, ty.inner(db) - .apply_type_mapping_impl(db, type_mapping, tcx, visitor), + .apply_type_mapping_impl(db, env, type_mapping, tcx, visitor), ))) } KnownInstanceType::WrappedOptional(ty) => { Type::KnownInstance(KnownInstanceType::WrappedOptional(InternedType::new( db, ty.inner(db) - .apply_type_mapping_impl(db, type_mapping, tcx, visitor), + .apply_type_mapping_impl(db, env, type_mapping, tcx, visitor), ))) } KnownInstanceType::Callable(callable_type) => { @@ -469,33 +494,33 @@ impl<'db> KnownInstanceType<'db> { } KnownInstanceType::FunctoolsPartial(partial) => { Type::KnownInstance(KnownInstanceType::FunctoolsPartial( - partial.apply_type_mapping_impl(db, type_mapping, tcx, visitor), + partial.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor), )) } KnownInstanceType::Range { .. } => match type_mapping { TypeMapping::Promote( PromotionMode::On, PromotionKind::Regular | PromotionKind::RegularKeepingLiterals, - ) => self.instance_fallback(db), + ) => self.instance_fallback(db, env), _ => Type::KnownInstance(self), }, KnownInstanceType::FunctoolsPartialCall(partial) => { Type::KnownInstance(KnownInstanceType::FunctoolsPartialCall( - partial.apply_type_mapping_impl(db, type_mapping, tcx, visitor), + partial.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor), )) } KnownInstanceType::TypeGenericAlias(ty) => { Type::KnownInstance(KnownInstanceType::TypeGenericAlias(InternedType::new( db, ty.inner(db) - .apply_type_mapping_impl(db, type_mapping, tcx, visitor), + .apply_type_mapping_impl(db, env, type_mapping, tcx, visitor), ))) } KnownInstanceType::LiteralStringAlias(ty) => { Type::KnownInstance(KnownInstanceType::LiteralStringAlias(InternedType::new( db, ty.inner(db) - .apply_type_mapping_impl(db, type_mapping, tcx, visitor), + .apply_type_mapping_impl(db, env, type_mapping, tcx, visitor), ))) } @@ -591,29 +616,32 @@ impl<'db> FieldInstance<'db> { fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { let default_type = match self.default_type(db) { - Some(default) if nested => Some(default.recursive_type_normalized_impl(db, div, true)?), + Some(default) if nested => { + Some(default.recursive_type_normalized_impl(db, env, div, true)?) + } Some(default) => Some( default - .recursive_type_normalized_impl(db, div, true) + .recursive_type_normalized_impl(db, env, div, true) .unwrap_or(div), ), None => None, }; let converter = match self.converter(db) { Some((input_ty, output_ty)) if nested => Some(( - input_ty.recursive_type_normalized_impl(db, div, true)?, - output_ty.recursive_type_normalized_impl(db, div, true)?, + input_ty.recursive_type_normalized_impl(db, env, div, true)?, + output_ty.recursive_type_normalized_impl(db, env, div, true)?, )), Some((input_ty, output_ty)) => Some(( input_ty - .recursive_type_normalized_impl(db, div, true) + .recursive_type_normalized_impl(db, env, div, true) .unwrap_or(div), output_ty - .recursive_type_normalized_impl(db, div, true) + .recursive_type_normalized_impl(db, env, div, true) .unwrap_or(div), )), None => None, @@ -666,9 +694,11 @@ impl<'db> UnionTypeInstance<'db> { typevar_binding_context: Option>, inference_flags: InferenceFlags, ) -> Type<'db> { - let mut builder = UnionBuilder::new(db); + let env = ProgramEnvironment::from_scope(scope_id); + let mut builder = UnionBuilder::new(db, &env); for ty in &value_expr_types { - match ty.in_type_expression(db, scope_id, typevar_binding_context, inference_flags) { + match ty.in_type_expression_impl(db, scope_id, typevar_binding_context, inference_flags) + { Ok(ty) => builder.add_in_place(ty), Err(error) => { return Type::KnownInstance(KnownInstanceType::UnionType( @@ -699,18 +729,19 @@ impl<'db> UnionTypeInstance<'db> { ))) } - pub(super) fn apply_type_mapping_impl( + fn apply_type_mapping_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'_, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { if let Ok(union_type) = self.union_type(db) { UnionTypeInstance::new( db, self._value_expr_types(db), - Ok(union_type.apply_type_mapping_impl(db, type_mapping, tcx, visitor)), + Ok(union_type.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor)), ) } else { self @@ -726,12 +757,14 @@ impl<'db> UnionTypeInstance<'db> { pub(crate) fn value_expression_types( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ) -> Result> + 'db, InvalidTypeExpressionError<'db>> { - let to_class_literal = |ty: Type<'db>| { + let env = env.clone(); + let to_class_literal = move |ty: Type<'db>| { ty.as_nominal_instance() .and_then(|instance| { instance - .class(db) + .class(db, &env) .static_class_literal(db) .map(|(lit, _)| Type::ClassLiteral(lit.into())) }) @@ -755,6 +788,7 @@ impl<'db> UnionTypeInstance<'db> { fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -762,23 +796,23 @@ impl<'db> UnionTypeInstance<'db> { // See `UnionType::recursive_type_normalized_impl` for details. let value_expr_types = match self._value_expr_types(db).as_ref() { Some([first, second]) if nested => Some([ - first.recursive_type_normalized_impl(db, div, nested)?, - second.recursive_type_normalized_impl(db, div, nested)?, + first.recursive_type_normalized_impl(db, env, div, nested)?, + second.recursive_type_normalized_impl(db, env, div, nested)?, ]), Some([first, second]) => Some([ first - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .unwrap_or(div), second - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .unwrap_or(div), ]), None => None, }; let union_type = match self.union_type(db).clone() { - Ok(ty) if nested => Ok(ty.recursive_type_normalized_impl(db, div, nested)?), + Ok(ty) if nested => Ok(ty.recursive_type_normalized_impl(db, env, div, nested)?), Ok(ty) => Ok(ty - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .unwrap_or(div)), Err(err) => Err(err), }; @@ -792,6 +826,7 @@ impl<'db> FunctoolsPartialInstance<'db> { fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -801,10 +836,10 @@ impl<'db> FunctoolsPartialInstance<'db> { db, self.wrapped(db) .inner(db) - .recursive_type_normalized_impl(db, div, nested)?, + .recursive_type_normalized_impl(db, env, div, nested)?, ), self.partial(db) - .recursive_type_normalized_impl(db, div, nested)?, + .recursive_type_normalized_impl(db, env, div, nested)?, )) } @@ -812,17 +847,22 @@ impl<'db> FunctoolsPartialInstance<'db> { fn apply_type_mapping_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'_, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { Self::new( db, InternedType::new( db, - self.wrapped(db) - .inner(db) - .apply_type_mapping_impl(db, type_mapping, tcx, visitor), + self.wrapped(db).inner(db).apply_type_mapping_impl( + db, + env, + type_mapping, + tcx, + visitor, + ), ), self.partial(db) .apply_type_mapping_impl(db, type_mapping, tcx, visitor), @@ -843,15 +883,16 @@ impl<'db> InternedType<'db> { fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { let inner = if nested { self.inner(db) - .recursive_type_normalized_impl(db, div, nested)? + .recursive_type_normalized_impl(db, env, div, nested)? } else { self.inner(db) - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .unwrap_or(div) }; Some(InternedType::new(db, inner)) diff --git a/crates/ty_python_semantic/src/types/list_members.rs b/crates/ty_python_semantic/src/types/list_members.rs index 7b5108be53..5016cc8bb3 100644 --- a/crates/ty_python_semantic/src/types/list_members.rs +++ b/crates/ty_python_semantic/src/types/list_members.rs @@ -11,19 +11,20 @@ use ruff_python_ast::name::Name; use rustc_hash::FxHashSet; use crate::{ - Db, NameKind, + Db, place::{ DefinedPlace, Place, PlaceWithDefinition, imported_symbol, place_from_bindings, place_from_declarations, }, types::{ - ClassBase, ClassLiteral, KnownClass, KnownInstanceType, StaticClassLiteral, + ClassBase, ClassLiteral, KnownClass, ProgramEnvironment, StaticClassLiteral, SubclassOfInner, Type, TypeVarBoundOrConstraints, class::CodeGeneratorKind, + exists_at_runtime, }, }; use ty_python_core::{ - attribute_scopes, definition::Definition, global_scope, place_table, scope::ScopeId, - semantic_index, use_def_map, + ProgramFile, attribute_scopes, definition::Definition, global_scope, place_table, + scope::ScopeId, semantic_index, use_def_map, }; /// Iterate over all declarations and bindings that exist at the end @@ -32,13 +33,16 @@ pub(crate) fn all_end_of_scope_members<'db>( db: &'db dyn Db, scope_id: ScopeId<'db>, ) -> impl Iterator> + 'db { + let env = ProgramEnvironment::from_scope(scope_id); + let use_def_map = use_def_map(db, scope_id); let table = place_table(db, scope_id); + let bindings_ctx = env.clone(); use_def_map .all_end_of_scope_symbol_declarations() .filter_map(move |(symbol_id, declarations)| { - let place_result = place_from_declarations(db, declarations); + let place_result = place_from_declarations(db, &env, declarations); let first_reachable_definition = place_result.first_declaration?; let ty = place_result .ignore_conflicting_declarations() @@ -48,6 +52,7 @@ pub(crate) fn all_end_of_scope_members<'db>( let member = Member { name: symbol.name().clone(), ty, + is_type_check_only: false, }; Some(MemberWithDefinition { member, @@ -59,7 +64,7 @@ pub(crate) fn all_end_of_scope_members<'db>( let PlaceWithDefinition { place, first_definition, - } = place_from_bindings(db, bindings); + } = place_from_bindings(db, &bindings_ctx, bindings); let first_reachable_definition = first_definition?; let ty = place.ignore_possibly_undefined()?; @@ -68,6 +73,7 @@ pub(crate) fn all_end_of_scope_members<'db>( let member = Member { name: symbol.name().clone(), ty, + is_type_check_only: false, }; Some(MemberWithDefinition { member, @@ -83,6 +89,8 @@ pub(crate) fn all_reachable_members<'db>( db: &'db dyn Db, scope_id: ScopeId<'db>, ) -> impl Iterator> + 'db { + let env = ProgramEnvironment::from_scope(scope_id); + let use_def_map = use_def_map(db, scope_id); let table = place_table(db, scope_id); @@ -91,7 +99,7 @@ pub(crate) fn all_reachable_members<'db>( .flat_map(move |(symbol_id, declarations, bindings)| { let symbol = table.symbol(symbol_id); - let declaration_place_result = place_from_declarations(db, declarations); + let declaration_place_result = place_from_declarations(db, &env, declarations); let declaration = declaration_place_result .first_declaration @@ -103,6 +111,7 @@ pub(crate) fn all_reachable_members<'db>( let member = Member { name: symbol.name().clone(), ty, + is_type_check_only: false, }; Some(MemberWithDefinition { member, @@ -110,7 +119,7 @@ pub(crate) fn all_reachable_members<'db>( }) }); - let place_with_definition = place_from_bindings(db, bindings); + let place_with_definition = place_from_bindings(db, &env, bindings); let binding = place_with_definition .first_definition @@ -119,6 +128,7 @@ pub(crate) fn all_reachable_members<'db>( let member = Member { name: symbol.name().clone(), ty, + is_type_check_only: false, }; Some(MemberWithDefinition { member, @@ -153,25 +163,25 @@ struct AllMembers<'db> { } impl<'db> AllMembers<'db> { - fn of(db: &'db dyn Db, ty: Type<'db>) -> Self { + fn of(db: &'db dyn Db, env: &ProgramEnvironment<'db>, ty: Type<'db>) -> Self { let mut all_members = Self { members: FxHashSet::default(), }; - all_members.extend_with_type(db, ty); + all_members.extend_with_type(db, env, ty); all_members } - fn extend_with_type(&mut self, db: &'db dyn Db, ty: Type<'db>) { + fn extend_with_type(&mut self, db: &'db dyn Db, env: &ProgramEnvironment<'db>, ty: Type<'db>) { match ty { // parameter-only marker; behaves as the type a body sees (bound of `Key`) Type::Overlapping(overlapping) => { - self.extend_with_type(db, overlapping.value_type(db)); + self.extend_with_type(db, env, overlapping.value_type(db, env)); } Type::Restricted(restricted) => { - self.extend_with_type(db, restricted.value_type(db)); + self.extend_with_type(db, env, restricted.value_type(db)); } Type::Deferred(deferred) => { - self.extend_with_type(db, deferred.reduced(db)); + self.extend_with_type(db, env, deferred.reduced(db, env)); } Type::Union(union) => { fn is_dynamic(db: &dyn Db, ty: Type<'_>) -> bool { @@ -189,13 +199,13 @@ impl<'db> AllMembers<'db> { let union = match union.filter(db, |&ty| !is_dynamic(db, ty)) { Type::Union(union) => union, - ty => return self.extend_with_type(db, ty), + ty => return self.extend_with_type(db, env, ty), }; self.members.extend( union .elements(db) .iter() - .map(|ty| AllMembers::of(db, *ty).members) + .map(|ty| AllMembers::of(db, env, *ty).members) .reduce(|acc, members| acc.intersection(&members).cloned().collect()) .unwrap_or_default(), ); @@ -205,7 +215,7 @@ impl<'db> AllMembers<'db> { intersection .positive(db) .iter() - .map(|ty| AllMembers::of(db, *ty).members) + .map(|ty| AllMembers::of(db, env, *ty).members) .reduce(|acc, members| acc.union(&members).cloned().collect()) .unwrap_or_default(), ), @@ -216,84 +226,117 @@ impl<'db> AllMembers<'db> { unsafe_union .elements(db) .iter() - .map(|ty| AllMembers::of(db, *ty).members) + .map(|ty| AllMembers::of(db, env, *ty).members) .reduce(|acc, members| acc.union(&members).cloned().collect()) .unwrap_or_default(), ), Type::EnumComplement(complement) => { - self.extend_with_type(db, complement.to_intersection(db)); + self.extend_with_type(db, env, complement.to_intersection(db, env)); } Type::NominalInstance(instance) => { - let class = instance.class(db); + let class = instance.class(db, env); if let Some((class_literal, _)) = class.static_class_literal(db) { - self.extend_with_instance_members(db, ty, class_literal); - self.extend_with_synthetic_members(db, ty, ClassLiteral::Static(class_literal)); + self.extend_with_instance_members(db, env, ty, class_literal); + self.extend_with_synthetic_members( + db, + env, + ty, + ClassLiteral::Static(class_literal), + ); } else { // For dynamic classes, we can't enumerate instance members (requires body scope), // but we can still add synthetic members for dataclass-like classes. - self.extend_with_synthetic_members(db, ty, class.class_literal(db)); + self.extend_with_synthetic_members(db, env, ty, class.class_literal(db)); } } Type::NewTypeInstance(newtype) => { - self.extend_with_type(db, newtype.concrete_base_type(db)); + self.extend_with_type(db, env, newtype.concrete_base_type(db)); } Type::ClassLiteral(class_literal) if class_literal.is_typed_dict(db) => { - self.extend_with_type(db, KnownClass::TypedDictFallback.to_class_literal(db)); + self.extend_with_type( + db, + env, + KnownClass::TypedDictFallback.to_class_literal(db, env), + ); } Type::GenericAlias(generic_alias) if generic_alias.is_typed_dict(db) => { - self.extend_with_type(db, KnownClass::TypedDictFallback.to_class_literal(db)); + self.extend_with_type( + db, + env, + KnownClass::TypedDictFallback.to_class_literal(db, env), + ); } - Type::SubclassOf(subclass_of_type) if subclass_of_type.is_typed_dict(db) => { - self.extend_with_type(db, KnownClass::TypedDictFallback.to_class_literal(db)); + Type::SubclassOf(subclass_of_type) if subclass_of_type.is_typed_dict(db, env) => { + self.extend_with_type( + db, + env, + KnownClass::TypedDictFallback.to_class_literal(db, env), + ); } Type::ClassLiteral(class_literal) => { - self.extend_with_class_members(db, ty, class_literal); - self.extend_with_synthetic_members(db, ty, class_literal); - self.extend_with_metaclass_members(db, ty, class_literal.metaclass(db)); + self.extend_with_class_members(db, env, ty, class_literal); + self.extend_with_synthetic_members(db, env, ty, class_literal); + self.extend_with_metaclass_members(db, env, ty, class_literal.metaclass(db)); } Type::GenericAlias(generic_alias) => { let class_literal = generic_alias.origin(db); - self.extend_with_class_members(db, ty, ClassLiteral::Static(class_literal)); - self.extend_with_synthetic_members(db, ty, ClassLiteral::Static(class_literal)); - self.extend_with_metaclass_members(db, ty, class_literal.metaclass(db)); + self.extend_with_class_members(db, env, ty, ClassLiteral::Static(class_literal)); + self.extend_with_synthetic_members( + db, + env, + ty, + ClassLiteral::Static(class_literal), + ); + self.extend_with_metaclass_members(db, env, ty, class_literal.metaclass(db)); } Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() { SubclassOfInner::Dynamic(_) => { - self.extend_with_type(db, KnownClass::Type.to_instance(db)); + self.extend_with_type(db, env, KnownClass::Type.to_instance(db, env)); } SubclassOfInner::Protocol(protocol) => { if let Some((class_literal, _)) = protocol - .class_origin() + .class_origin(db) .and_then(|origin| origin.static_class_literal(db)) { - self.extend_with_class_members(db, ty, ClassLiteral::Static(class_literal)); + self.extend_with_class_members( + db, + env, + ty, + ClassLiteral::Static(class_literal), + ); self.extend_with_synthetic_members( db, + env, ty, ClassLiteral::Static(class_literal), ); } // A structural implementation can use any metaclass, so only members of // `type` itself are guaranteed in addition to the protocol interface. - self.extend_with_type(db, KnownClass::Type.to_instance(db)); + self.extend_with_type(db, env, KnownClass::Type.to_instance(db, env)); } _ => { - if let Some(class_type) = subclass_of_type.subclass_of().into_class(db) + if let Some(class_type) = subclass_of_type.subclass_of().into_class(db, env) && let Some((class_literal, _)) = class_type.static_class_literal(db) { let static_class = ClassLiteral::Static(class_literal); - self.extend_with_class_members(db, ty, static_class); - self.extend_with_synthetic_members(db, ty, static_class); - self.extend_with_metaclass_members(db, ty, class_literal.metaclass(db)); + self.extend_with_class_members(db, env, ty, static_class); + self.extend_with_synthetic_members(db, env, ty, static_class); + self.extend_with_metaclass_members( + db, + env, + ty, + class_literal.metaclass(db), + ); } } }, @@ -304,25 +347,27 @@ impl<'db> AllMembers<'db> { | Type::AlwaysTruthy | Type::AlwaysFalsy | Type::TypeForm(_) => { - self.extend_with_type(db, Type::object()); + self.extend_with_type(db, env, Type::object()); } - Type::TypeAlias(alias) => self.extend_with_type(db, alias.value_type(db)), + Type::TypeAlias(alias) => { + self.extend_with_type(db, env, alias.value_type(db)); + } Type::TypeVar(bound_typevar) => { - match bound_typevar.typevar(db).bound_or_constraints(db) { + match bound_typevar.typevar(db).bound_or_constraints(db, env) { None => { - self.extend_with_type(db, Type::object()); + self.extend_with_type(db, env, Type::object()); } Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - self.extend_with_type(db, bound); + self.extend_with_type(db, env, bound); } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { self.members.extend( constraints .elements(db) .iter() - .map(|ty| AllMembers::of(db, *ty).members) + .map(|ty| AllMembers::of(db, env, *ty).members) .reduce(|acc, members| { acc.intersection(&members).cloned().collect() }) @@ -346,106 +391,88 @@ impl<'db> AllMembers<'db> { | Type::KnownInstance(_) | Type::BoundSuper(_) | Type::TypeIs(_) - | Type::TypeGuard(_) => match ty.to_meta_type(db) { + | Type::TypeGuard(_) => match ty.to_meta_type(db, env) { Type::ClassLiteral(class_literal) => { - self.extend_with_class_members(db, ty, class_literal); + self.extend_with_class_members(db, env, ty, class_literal); } Type::SubclassOf(subclass_of) => { - if let Some(class) = subclass_of.subclass_of().into_class(db) + if let Some(class) = subclass_of.subclass_of().into_class(db, env) && let Some((class_literal, _)) = class.static_class_literal(db) { - self.extend_with_class_members(db, ty, ClassLiteral::Static(class_literal)); + self.extend_with_class_members( + db, + env, + ty, + ClassLiteral::Static(class_literal), + ); } } Type::GenericAlias(generic_alias) => { let class_literal = generic_alias.origin(db); - self.extend_with_class_members(db, ty, ClassLiteral::Static(class_literal)); + self.extend_with_class_members( + db, + env, + ty, + ClassLiteral::Static(class_literal), + ); } _ => {} }, Type::TypedDict(_) => { - if let Type::ClassLiteral(class_literal) = ty.to_meta_type(db) { - self.extend_with_class_members(db, ty, class_literal); + if let Type::ClassLiteral(class_literal) = ty.to_meta_type(db, env) { + self.extend_with_class_members(db, env, ty, class_literal); } if let Type::ClassLiteral(ClassLiteral::Static(class)) = - KnownClass::TypedDictFallback.to_class_literal(db) + KnownClass::TypedDictFallback.to_class_literal(db, env) { - self.extend_with_instance_members(db, ty, class); + self.extend_with_instance_members(db, env, ty, class); } } Type::ModuleLiteral(literal) => { + let module = literal.module(db); // Looking up `__file__` on `types.ModuleType` will not give as precise a type // as we infer in type inference, but it's confusing if autocomplete etc. // shows a different type in the tooltip to the one inferred by the type checker. - let dunder_file_type = if literal.module(db).file(db).is_some() { - KnownClass::Str.to_instance(db) + let dunder_file_type = if module.file(db).is_some() { + KnownClass::Str.to_instance(db, env) } else { - Type::none(db) + Type::none(db, env) }; self.members.insert(Member { name: Name::new_static("__file__"), ty: dunder_file_type, + is_type_check_only: false, }); - self.extend_with_type(db, KnownClass::ModuleType.to_instance(db)); - let module = literal.module(db); + self.extend_with_type(db, env, KnownClass::ModuleType.to_instance(db, env)); let Some(file) = module.file(db) else { return; }; + let program_file = ProgramFile::new(db, file, env.program(db)); - let module_scope = global_scope(db, file); + let module_scope = global_scope(db, program_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_symbol_declarations() { let symbol_name = place_table.symbol(symbol_id).name(); - let Place::Defined(DefinedPlace { ty, .. }) = - imported_symbol(db, Some(file), symbol_name, None).place + let Place::Defined(defined) = + imported_symbol(db, env, Some(program_file), symbol_name, None).place else { continue; }; - // Filter private symbols from stubs if they appear to be internal types - let is_stub_file = matches!(file.path(db).extension(), Some("pyi" | "byi")); - 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.known_class(db), - Some( - KnownClass::TypeVar - | KnownClass::TypeVarTuple - | KnownClass::ExtensionsTypeVarTuple - | KnownClass::ParamSpec - | KnownClass::UnionType - ) - ) => - { - continue; - } - Type::ClassLiteral(class) if class.is_protocol(db) => continue, - Type::KnownInstance( - KnownInstanceType::TypeVar(_) - | KnownInstanceType::TypeAliasType(_) - | KnownInstanceType::UnionType(_) - | KnownInstanceType::Literal(_) - | KnownInstanceType::Annotated(_), - ) => continue, - _ => {} - } - } - self.members.insert(Member { name: symbol_name.clone(), - ty, + ty: defined.ty, + is_type_check_only: defined + .provenance + .definition() + .is_some_and(|definition| !exists_at_runtime(db, definition)), }); } @@ -454,7 +481,11 @@ impl<'db> AllMembers<'db> { |submodule_name| { let ty = literal.resolve_submodule(db, &submodule_name)?; let name = submodule_name.clone(); - Some(Member { name, ty }) + Some(Member { + name, + ty, + is_type_check_only: false, + }) }, )); } @@ -485,6 +516,7 @@ impl<'db> AllMembers<'db> { fn extend_with_class_members( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, class_literal: ClassLiteral<'db>, ) { @@ -495,13 +527,14 @@ impl<'db> AllMembers<'db> { { let parent_scope = parent.body_scope(db); for memberdef in all_end_of_scope_members(db, parent_scope) { - let result = ty.member(db, memberdef.member.name.as_str()); + let result = ty.member(db, env, memberdef.member.name.as_str()); let Some(ty) = result.place.ignore_possibly_undefined() else { continue; }; self.members.insert(Member { name: memberdef.member.name, ty, + is_type_check_only: memberdef.member.is_type_check_only, }); } } @@ -514,6 +547,7 @@ impl<'db> AllMembers<'db> { fn extend_with_metaclass_members( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, metaclass: Type<'db>, ) { @@ -521,9 +555,9 @@ impl<'db> AllMembers<'db> { return; }; - self.extend_with_class_members(db, ty, metaclass.class_literal(db)); + self.extend_with_class_members(db, env, ty, metaclass.class_literal(db)); if let Some((metaclass, _)) = metaclass.static_class_literal(db) { - self.extend_with_instance_members(db, ty, metaclass); + self.extend_with_instance_members(db, env, ty, metaclass); } } @@ -531,24 +565,26 @@ impl<'db> AllMembers<'db> { fn extend_with_instance_members_for_class( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, class_literal: StaticClassLiteral<'db>, ) { let class_body_scope = class_literal.body_scope(db); - let file = class_body_scope.file(db); - let index = semantic_index(db, file); + let program_file = class_body_scope.program_file(db); + let index = semantic_index(db, program_file); for function_scope_id in attribute_scopes(db, class_body_scope) { for place_expr in index.place_table(function_scope_id).members() { let Some(name) = place_expr.as_instance_attribute() else { continue; }; - let result = ty.member(db, name); + let result = ty.member(db, env, name); let Some(ty) = result.place.ignore_possibly_undefined() else { continue; }; self.members.insert(Member { name: Name::new(name), ty, + is_type_check_only: false, }); } } @@ -559,13 +595,14 @@ impl<'db> AllMembers<'db> { // member, e.g., `SomeClass.__delattr__` is not a bound // method, but `instance_of_SomeClass.__delattr__` is. for memberdef in all_end_of_scope_members(db, class_body_scope) { - let result = ty.member(db, memberdef.member.name.as_str()); + let result = ty.member(db, env, memberdef.member.name.as_str()); let Some(ty) = result.place.ignore_possibly_undefined() else { continue; }; self.members.insert(Member { name: memberdef.member.name, ty, + is_type_check_only: memberdef.member.is_type_check_only, }); } } @@ -574,6 +611,7 @@ impl<'db> AllMembers<'db> { fn extend_with_instance_members( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, class_literal: StaticClassLiteral<'db>, ) { @@ -582,7 +620,7 @@ impl<'db> AllMembers<'db> { .filter_map(ClassBase::into_class) { if let Some((class_literal, _)) = class.static_class_literal(db) { - self.extend_with_instance_members_for_class(db, ty, class_literal); + self.extend_with_instance_members_for_class(db, env, ty, class_literal); } } } @@ -590,51 +628,65 @@ impl<'db> AllMembers<'db> { fn extend_with_synthetic_members( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, class_literal: ClassLiteral<'db>, ) { match CodeGeneratorKind::from_class(db, class_literal) { Some(CodeGeneratorKind::NamedTuple) => { if ty.is_nominal_instance() { - self.extend_with_type(db, KnownClass::NamedTupleFallback.to_instance(db)); + self.extend_with_type( + db, + env, + KnownClass::NamedTupleFallback.to_instance(db, env), + ); } else { - self.extend_with_type(db, KnownClass::NamedTupleFallback.to_class_literal(db)); + self.extend_with_type( + db, + env, + KnownClass::NamedTupleFallback.to_class_literal(db, env), + ); } } Some(CodeGeneratorKind::TypedDict) => {} - Some(CodeGeneratorKind::DataclassLike(_)) => { - for attr in SYNTHETIC_DATACLASS_ATTRIBUTES { + Some(kind @ (CodeGeneratorKind::DataclassLike(_) | CodeGeneratorKind::Pydantic(_))) => { + let synthetic_attributes: &[&str] = if kind.is_pydantic() { + &["__replace__"] + } else { + SYNTHETIC_DATACLASS_ATTRIBUTES + }; + + for attr in synthetic_attributes { if let Place::Defined(DefinedPlace { ty: synthetic_member, .. - }) = ty.member(db, attr).place + }) = ty.member(db, env, attr).place { self.members.insert(Member { name: Name::from(*attr), ty: synthetic_member, + is_type_check_only: false, }); } } } - Some(CodeGeneratorKind::Pydantic(_)) => { - // Pydantic's special attributes are declared on and inherited from `BaseModel`. - } Some(field_policy @ CodeGeneratorKind::Django) => { // django's synthesized model attributes (`id`, `pk`, fk attnames) // resolve by name only; surface them for completions if let Some(class) = class_literal.as_static() { let fields = class.fields(db, None, field_policy); - for name in - crate::types::dedicated::django::synthesized_member_names(db, class, fields) - { + for name in crate::types::dedicated::django::synthesized_member_names( + db, env, class, fields, + ) { if let Place::Defined(DefinedPlace { ty: synthetic_member, .. - }) = ty.member(db, name.as_str()).place + }) = ty.member(db, env, name.as_str()).place { self.members.insert(Member { name, ty: synthetic_member, + is_type_check_only: false, }); } } @@ -653,13 +705,13 @@ impl<'db> AllMembers<'db> { /// A member of a type or scope, with the first reachable definition of that member. #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub struct MemberWithDefinition<'db> { - pub member: Member<'db>, - pub first_reachable_definition: Definition<'db>, + pub(crate) member: Member<'db>, + pub(crate) first_reachable_definition: Definition<'db>, } /// A member of a type or scope. /// -/// In the context of the [`all_members`] routine, this represents +/// In the context of the `all_members` routine, this represents /// a single item in (ideally) the list returned by `dir(object)`. /// /// The equality, comparison and hashing traits implemented for @@ -674,6 +726,8 @@ pub struct MemberWithDefinition<'db> { pub struct Member<'db> { pub name: Name, pub ty: Type<'db>, + /// Whether this member is known to exist only during type checking. + pub is_type_check_only: bool, } impl std::hash::Hash for Member<'_> { @@ -704,6 +758,10 @@ impl<'db> PartialOrd for Member<'db> { /// 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 +pub fn all_members<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> FxHashSet> { + AllMembers::of(db, env, ty).members } diff --git a/crates/ty_python_semantic/src/types/literal.rs b/crates/ty_python_semantic/src/types/literal.rs index ad56f8ca14..3c8ca7e54b 100644 --- a/crates/ty_python_semantic/src/types/literal.rs +++ b/crates/ty_python_semantic/src/types/literal.rs @@ -1,3 +1,4 @@ +use crate::ProgramEnvironment; use bitflags::bitflags; use compact_str::CompactString; use ruff_python_ast::name::Name; @@ -263,17 +264,21 @@ impl<'db> LiteralValueType<'db> { matches!(self.kind(), LiteralValueTypeKind::Bytes(..)) } - pub(crate) fn fallback_instance(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn fallback_instance( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { match self.kind() { LiteralValueTypeKind::String(_) | LiteralValueTypeKind::LiteralString => { - KnownClass::Str.to_instance(db) + KnownClass::Str.to_instance(db, env) } - LiteralValueTypeKind::Bool(_) => KnownClass::Bool.to_instance(db), - LiteralValueTypeKind::Int(_) => KnownClass::Int.to_instance(db), - LiteralValueTypeKind::Bytes(_) => KnownClass::Bytes.to_instance(db), - LiteralValueTypeKind::Enum(literal) => literal.enum_class_instance(db), - LiteralValueTypeKind::Float(_) => KnownClass::Float.to_instance(db), - LiteralValueTypeKind::Complex(_) => KnownClass::Complex.to_instance(db), + LiteralValueTypeKind::Bool(_) => KnownClass::Bool.to_instance(db, env), + LiteralValueTypeKind::Int(_) => KnownClass::Int.to_instance(db, env), + LiteralValueTypeKind::Bytes(_) => KnownClass::Bytes.to_instance(db, env), + LiteralValueTypeKind::Enum(literal) => literal.enum_class_instance(db, env), + LiteralValueTypeKind::Float(_) => KnownClass::Float.to_instance(db, env), + LiteralValueTypeKind::Complex(_) => KnownClass::Complex.to_instance(db, env), } } } @@ -509,8 +514,12 @@ impl<'db> EnumLiteralType<'db> { self.enum_class_literal(db).class_literal(db) } - pub(crate) fn enum_class_instance(self, db: &'db dyn Db) -> Type<'db> { - self.enum_class(db).to_non_generic_instance(db) + pub(crate) fn enum_class_instance( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { + self.enum_class(db).to_non_generic_instance(db, env) } pub(crate) fn definition(self, db: &'db dyn Db) -> Option> { diff --git a/crates/ty_python_semantic/src/types/match_pattern.rs b/crates/ty_python_semantic/src/types/match_pattern.rs index 2ba529af57..a0d62fa38c 100644 --- a/crates/ty_python_semantic/src/types/match_pattern.rs +++ b/crates/ty_python_semantic/src/types/match_pattern.rs @@ -1,3 +1,5 @@ +use crate::Db; +use crate::ProgramEnvironment; use ruff_python_ast as ast; use ruff_python_ast::name::Name; use ty_python_core::Truthiness; @@ -6,7 +8,6 @@ use ty_python_core::predicate::{ SequencePatternPredicateKind, }; -use crate::Db; use crate::place::{DefinedPlace, Place}; use crate::types::callable::{CallableFunctionProvenance, CallableTypeKind}; use crate::types::equality::{ @@ -22,22 +23,34 @@ use crate::types::{ infer_same_file_expression_type, }; -pub(crate) fn singleton_pattern_type(db: &dyn Db, singleton: ast::Singleton) -> Type<'_> { +pub(crate) fn singleton_pattern_type<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + singleton: ast::Singleton, +) -> Type<'db> { let ty = match singleton { - ast::Singleton::None => Type::none(db), + ast::Singleton::None => Type::none(db, env), ast::Singleton::True => Type::bool_literal(true), ast::Singleton::False => Type::bool_literal(false), }; - debug_assert!(ty.is_singleton(db)); + debug_assert!(ty.is_singleton(db, env)); ty } -pub(crate) fn mapping_pattern_type(db: &dyn Db) -> Type<'_> { - KnownClass::Mapping.to_instance(db).top_materialization(db) +pub(crate) fn mapping_pattern_type<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, +) -> Type<'db> { + KnownClass::Mapping + .to_instance(db, env) + .top_materialization(db, env) } -pub(crate) fn callable_pattern_type(db: &dyn Db) -> Type<'_> { - Type::Callable(CallableType::unknown(db)).top_materialization(db) +pub(crate) fn callable_pattern_type<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, +) -> Type<'db> { + Type::Callable(CallableType::unknown(db)).top_materialization(db, env) } /// Return whether every runtime value represented by a `TypedDict` satisfies `class`. @@ -45,61 +58,85 @@ pub(crate) fn callable_pattern_type(db: &dyn Db) -> Type<'_> { /// `TypedDict` is not a nominal subtype of `dict` in the static type system, but every runtime /// value is a dictionary. A `TypedDict` therefore matches class patterns such as `dict()`, /// `Mapping()`, and `MutableMapping()`. -pub(crate) fn typed_dict_matches_class_pattern(db: &dyn Db, class: ClassLiteral<'_>) -> bool { - let Some(dict) = KnownClass::Dict.to_class_literal(db).as_class_literal() else { +pub(crate) fn typed_dict_matches_class_pattern<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + class: ClassLiteral<'db>, +) -> bool { + let Some(dict) = KnownClass::Dict + .to_class_literal(db, env) + .as_class_literal() + else { return false; }; - Type::instance(db, dict.top_materialization(db)) - .is_subtype_of(db, Type::instance(db, class.top_materialization(db))) + Type::instance(db, env, dict.top_materialization(db)).is_subtype_of( + db, + env, + Type::instance(db, env, class.top_materialization(db)), + ) } /// Return whether every value in `ty` belongs to a `TypedDict` domain accepted by `predicate`. fn typed_dict_pattern_domain_satisfies<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, predicate: &impl Fn(TypedDictType<'db>) -> bool, ) -> bool { match ty.resolve_type_alias(db) { Type::TypedDict(typed_dict) => predicate(typed_dict), - Type::TypeVar(typevar) => match typevar.typevar(db).bound_or_constraints(db) { + Type::TypeVar(typevar) => match typevar.typevar(db).bound_or_constraints(db, env) { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - typed_dict_pattern_domain_satisfies(db, bound, predicate) + typed_dict_pattern_domain_satisfies(db, env, bound, predicate) + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + constraints.elements(db).iter().all(|constraint| { + typed_dict_pattern_domain_satisfies(db, env, *constraint, predicate) + }) } - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints - .elements(db) - .iter() - .all(|constraint| typed_dict_pattern_domain_satisfies(db, *constraint, predicate)), None => false, }, Type::Union(union) => union .elements(db) .iter() - .all(|element| typed_dict_pattern_domain_satisfies(db, *element, predicate)), + .all(|element| typed_dict_pattern_domain_satisfies(db, env, *element, predicate)), Type::Intersection(intersection) => intersection .positive(db) .iter() - .any(|element| typed_dict_pattern_domain_satisfies(db, *element, predicate)), + .any(|element| typed_dict_pattern_domain_satisfies(db, env, *element, predicate)), _ => false, } } /// Return whether every value in `ty` is represented by a `TypedDict` schema at runtime. -fn is_typed_dict_pattern_domain(db: &dyn Db, ty: Type<'_>) -> bool { - typed_dict_pattern_domain_satisfies(db, ty, &|_| true) +pub(super) fn is_typed_dict_runtime_domain( + db: &dyn Db, + env: &ProgramEnvironment<'_>, + ty: Type<'_>, +) -> bool { + typed_dict_pattern_domain_satisfies(db, env, ty, &|_| true) } -pub(crate) fn sequence_pattern_type_builder(db: &dyn Db) -> IntersectionBuilder<'_> { - IntersectionBuilder::new(db) - .add_positive(KnownClass::Sequence.to_instance(db).top_materialization(db)) +pub(crate) fn sequence_pattern_type_builder<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, +) -> IntersectionBuilder<'db> { + IntersectionBuilder::new(db, env) + .add_positive( + KnownClass::Sequence + .to_instance(db, env) + .top_materialization(db, env), + ) // `str`, `bytes`, and `bytearray` are sequences, but Python sequence // patterns explicitly do not match them or their subclasses. - .add_negative(KnownClass::Str.to_instance(db)) - .add_negative(KnownClass::Bytes.to_instance(db)) - .add_negative(KnownClass::Bytearray.to_instance(db)) + .add_negative(KnownClass::Str.to_instance(db, env)) + .add_negative(KnownClass::Bytes.to_instance(db, env)) + .add_negative(KnownClass::Bytearray.to_instance(db, env)) } fn sequence_pattern_getitem_method<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, indexed_element_types: impl IntoIterator)>, fallback_return_type: Option>, ) -> CallableType<'db> { @@ -122,7 +159,7 @@ fn sequence_pattern_getitem_method<'db>( Parameters::standard([ self_parameter(), Parameter::positional_only(Some(Name::new_static("index"))) - .with_annotated_type(KnownClass::Int.to_instance(db)), + .with_annotated_type(KnownClass::Int.to_instance(db, env)), ]), fallback_return_type, ) @@ -151,16 +188,17 @@ fn sequence_pattern_getitem_method<'db>( /// and element types. pub(crate) fn exact_sequence_pattern_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, element_types: impl ExactSizeIterator>, ) -> Type<'db> { let Ok(length) = i64::try_from(element_types.len()) else { - return sequence_pattern_type_builder(db).build(); + return sequence_pattern_type_builder(db, env).build(); }; // `False == 0` and `True == 1`, so the protocol must accept both literals. let length_type = match length { - 0 => UnionType::from_two_elements(db, Type::int_literal(0), Type::bool_literal(false)), - 1 => UnionType::from_two_elements(db, Type::int_literal(1), Type::bool_literal(true)), + 0 => UnionType::from_two_elements(db, env, Type::int_literal(0), Type::bool_literal(false)), + 1 => UnionType::from_two_elements(db, env, Type::int_literal(1), Type::bool_literal(true)), _ => Type::int_literal(length), }; @@ -172,16 +210,17 @@ pub(crate) fn exact_sequence_pattern_type<'db>( let getitem_method = (element_types.len() > 0).then(|| { ( "__getitem__", - sequence_pattern_getitem_method(db, (0..length).zip(element_types), None), + sequence_pattern_getitem_method(db, env, (0..length).zip(element_types), None), ) }); let protocol = Type::protocol_with_methods( db, + env, std::iter::once(("__len__", len_method)).chain(getitem_method), ); - sequence_pattern_type_builder(db) + sequence_pattern_type_builder(db, env) .add_positive(protocol) .build() } @@ -192,25 +231,26 @@ pub(crate) fn exact_sequence_pattern_type<'db>( /// negative indices. Other integer indices retain the sequence's element type. pub(crate) fn starred_sequence_pattern_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, prefix_element_types: impl ExactSizeIterator>, suffix_element_types: impl ExactSizeIterator>, ) -> Type<'db> { if prefix_element_types.len() == 0 && suffix_element_types.len() == 0 { - return sequence_pattern_type_builder(db).build(); + return sequence_pattern_type_builder(db, env).build(); } let Ok(suffix_length) = i64::try_from(suffix_element_types.len()) else { - return sequence_pattern_type_builder(db).build(); + return sequence_pattern_type_builder(db, env).build(); }; let indexed_element_types = (0_i64..) .zip(prefix_element_types) .chain((-suffix_length..0).zip(suffix_element_types)); let getitem_method = - sequence_pattern_getitem_method(db, indexed_element_types, Some(Type::object())); - let protocol = Type::protocol_with_methods(db, [("__getitem__", getitem_method)]); + sequence_pattern_getitem_method(db, env, indexed_element_types, Some(Type::object())); + let protocol = Type::protocol_with_methods(db, env, [("__getitem__", getitem_method)]); - sequence_pattern_type_builder(db) + sequence_pattern_type_builder(db, env) .add_positive(protocol) .build() } @@ -231,14 +271,15 @@ pub(crate) fn starred_sequence_pattern_type<'db>( /// ``` fn class_pattern_is_exhaustive( db: &dyn Db, + env: &ProgramEnvironment<'_>, class: ClassLiteral<'_>, subject_ty: Type<'_>, kind: &ClassPatternPredicateKind<'_>, ) -> bool { - let class_instance_ty = Type::instance(db, class.top_materialization(db)); - let is_typed_dict_match = - is_typed_dict_pattern_domain(db, subject_ty) && typed_dict_matches_class_pattern(db, class); - if !is_typed_dict_match && !subject_ty.is_subtype_of(db, class_instance_ty) { + let class_instance_ty = Type::instance(db, env, class.top_materialization(db)); + let is_typed_dict_match = is_typed_dict_runtime_domain(db, env, subject_ty) + && typed_dict_matches_class_pattern(db, env, class); + if !is_typed_dict_match && !subject_ty.is_subtype_of(db, env, class_instance_ty) { return false; } @@ -247,21 +288,22 @@ fn class_pattern_is_exhaustive( } if !kind.keywords.iter().all(|keyword| { - member_pattern_is_exhaustive(db, subject_ty, keyword.attr.as_str(), &keyword.pattern) + member_pattern_is_exhaustive(db, env, subject_ty, keyword.attr.as_str(), &keyword.pattern) }) { return false; } - let positional_sources = class_pattern_positional_sources(db, class, kind.positional.len()); + let positional_sources = + class_pattern_positional_sources(db, env, class, kind.positional.len()); kind.positional .iter() .zip(positional_sources) .all(|(pattern, source)| match source { ClassPatternPositionalSource::MatchSelf => { - pattern_is_exhaustive_for_subject(db, pattern, subject_ty) + pattern_is_exhaustive_for_subject(db, env, pattern, subject_ty) } ClassPatternPositionalSource::Attribute(name) => { - member_pattern_is_exhaustive(db, subject_ty, name.as_str(), pattern) + member_pattern_is_exhaustive(db, env, subject_ty, name.as_str(), pattern) } ClassPatternPositionalSource::Unknown => false, }) @@ -302,8 +344,15 @@ pub(crate) enum ClassPatternPositionalSource { /// attributes, inferred assignments retain their literal binding type while an explicit annotation /// remains authoritative. `PossiblyUndefined` is distinct from `Undefined` because only a truly /// absent `__match_args__` enables match-self behavior. -fn class_match_args_type<'db>(db: &'db dyn Db, class: ClassLiteral<'db>) -> ClassMatchArgs<'db> { - match Type::ClassLiteral(class).member(db, "__match_args__").place { +fn class_match_args_type<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + class: ClassLiteral<'db>, +) -> ClassMatchArgs<'db> { + match Type::ClassLiteral(class) + .member(db, env, "__match_args__") + .place + { Place::Defined( place @ DefinedPlace { ty, @@ -366,9 +415,10 @@ pub(crate) enum ClassPatternPositionalResult<'db> { /// Validate positional subpatterns against a statically known `__match_args__` type. pub(crate) fn class_pattern_positional_result<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: ClassLiteral<'db>, ) -> Option> { - match class_match_args_type(db, class) { + match class_match_args_type(db, env, class) { ClassMatchArgs::Undefined if class_has_match_self_flag(db, class) => { Some(ClassPatternPositionalResult::Limit(1)) } @@ -390,7 +440,7 @@ pub(crate) fn class_pattern_positional_result<'db>( Some(ClassPatternPositionalResult::Limit(limit)) } else { match_args - .is_disjoint_from(db, Type::homogeneous_tuple(db, Type::unknown())) + .is_disjoint_from(db, env, Type::homogeneous_tuple(db, env, Type::unknown())) .then_some(ClassPatternPositionalResult::InvalidType(match_args)) } } @@ -420,10 +470,11 @@ pub(crate) fn class_pattern_positional_result<'db>( /// ``` pub(crate) fn class_pattern_positional_sources( db: &dyn Db, + env: &ProgramEnvironment<'_>, class: ClassLiteral<'_>, positional_count: usize, ) -> Vec { - let fixed = match class_match_args_type(db, class) { + let fixed = match class_match_args_type(db, env, class) { ClassMatchArgs::Undefined if class_has_match_self_flag(db, class) => { return (0..positional_count) .map(|index| { @@ -461,26 +512,29 @@ pub(crate) fn class_pattern_positional_sources( /// Return whether `name` is definitely bound and `pattern` consumes its entire static member type. fn member_pattern_is_exhaustive( db: &dyn Db, + env: &ProgramEnvironment<'_>, instance_ty: Type<'_>, name: &str, pattern: &PatternPredicateKind<'_>, ) -> bool { - let place = instance_ty.member(db, name).place; + let place = instance_ty.member(db, env, name).place; place.is_definitely_bound() && place .raw_type() - .is_some_and(|member_ty| pattern_is_exhaustive_for_subject(db, pattern, member_ty)) + .is_some_and(|member_ty| pattern_is_exhaustive_for_subject(db, env, pattern, member_ty)) } /// Return whether `pattern` is statically guaranteed to match every value in `subject_ty`. fn pattern_is_exhaustive_for_subject( db: &dyn Db, + env: &ProgramEnvironment<'_>, pattern: &PatternPredicateKind<'_>, subject_ty: Type<'_>, ) -> bool { subject_ty.is_subtype_of( db, - definite_match_pattern_type_for_subject(db, pattern, subject_ty), + env, + definite_match_pattern_type_for_subject(db, env, pattern, subject_ty), ) } @@ -491,10 +545,11 @@ fn pattern_is_exhaustive_for_subject( /// guarantee that a particular key is present. fn mapping_pattern_is_exhaustive( db: &dyn Db, + env: &ProgramEnvironment<'_>, kind: &MappingPatternPredicateKind<'_>, subject_ty: Type<'_>, ) -> bool { - typed_dict_pattern_domain_satisfies(db, subject_ty, &|typed_dict| { + typed_dict_pattern_domain_satisfies(db, env, subject_ty, &|typed_dict| { kind.entries.iter().all(|entry| { let key_ty = infer_same_file_expression_type(db, entry.key, TypeContext::default()); let Some(key) = key_ty.as_string_literal() else { @@ -502,7 +557,7 @@ fn mapping_pattern_is_exhaustive( }; typed_dict.item(db, key.value(db)).is_some_and(|field| { field.is_required() - && pattern_is_exhaustive_for_subject(db, &entry.pattern, field.declared_ty) + && pattern_is_exhaustive_for_subject(db, env, &entry.pattern, field.declared_ty) }) }) }) @@ -514,10 +569,11 @@ fn mapping_pattern_is_exhaustive( /// tuple element's actual static type. fn sequence_pattern_is_exhaustive_for_subject( db: &dyn Db, + env: &ProgramEnvironment<'_>, kind: &SequencePatternPredicateKind<'_>, subject_ty: Type<'_>, ) -> bool { - if !subject_ty.is_subtype_of(db, sequence_pattern_type_builder(db).build()) { + if !subject_ty.is_subtype_of(db, env, sequence_pattern_type_builder(db, env).build()) { return false; } @@ -539,7 +595,7 @@ fn sequence_pattern_is_exhaustive_for_subject( .iter() .zip(kind.patterns.iter()) .all(|(element, pattern)| { - pattern_is_exhaustive_for_subject(db, pattern, *element) + pattern_is_exhaustive_for_subject(db, env, pattern, *element) }); }; if elements.len() < prefix.len() + suffix.len() { @@ -550,7 +606,7 @@ fn sequence_pattern_is_exhaustive_for_subject( .iter() .zip(prefix) .chain(elements.iter().rev().zip(suffix.iter().rev())) - .all(|(element, pattern)| pattern_is_exhaustive_for_subject(db, pattern, *element)) + .all(|(element, pattern)| pattern_is_exhaustive_for_subject(db, env, pattern, *element)) } /// Return the values that are statically guaranteed to match `kind`, using `subject_ty` when the @@ -584,10 +640,12 @@ fn sequence_pattern_is_exhaustive_for_subject( /// ``` pub(crate) fn definite_match_pattern_type_for_subject<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, kind: &PatternPredicateKind<'db>, subject_ty: Type<'db>, ) -> Type<'db> { - if let Some(subject_independent_ty) = subject_independent_definite_match_pattern_type(db, kind) + if let Some(subject_independent_ty) = + subject_independent_definite_match_pattern_type(db, env, kind) { return subject_independent_ty; } @@ -596,10 +654,11 @@ pub(crate) fn definite_match_pattern_type_for_subject<'db>( if let Type::Union(union) = resolved_subject_ty { return UnionType::from_elements( db, + env, union .elements(db) .iter() - .map(|element| definite_match_pattern_type_for_subject(db, kind, *element)), + .map(|element| definite_match_pattern_type_for_subject(db, env, kind, *element)), ); } @@ -608,6 +667,7 @@ pub(crate) fn definite_match_pattern_type_for_subject<'db>( let value_ty = infer_same_file_expression_type(db, *value, TypeContext::default()); if equality_truthiness( db, + env, resolved_subject_ty, value_ty, ComparisonSoundnessPolicy::from_analysis_settings( @@ -622,9 +682,9 @@ pub(crate) fn definite_match_pattern_type_for_subject<'db>( let class_ty = infer_same_file_expression_type(db, kind.class, TypeContext::default()); match class_ty { Type::ClassLiteral(class) => { - if class_pattern_is_exhaustive(db, class, resolved_subject_ty, kind) { - let top_subject_ty = resolved_subject_ty.top_materialization(db); - if !class_pattern_is_exhaustive(db, class, top_subject_ty, kind) { + if class_pattern_is_exhaustive(db, env, class, resolved_subject_ty, kind) { + let top_subject_ty = resolved_subject_ty.top_materialization(db, env); + if !class_pattern_is_exhaustive(db, env, class, top_subject_ty, kind) { return subject_ty; } return top_subject_ty; @@ -632,8 +692,8 @@ pub(crate) fn definite_match_pattern_type_for_subject<'db>( } Type::SpecialForm(SpecialFormType::CollectionsAbcCallable) if kind.is_empty() - && let callable_pattern_ty = callable_pattern_type(db) - && subject_ty.is_subtype_of(db, callable_pattern_ty) => + && let callable_pattern_ty = callable_pattern_type(db, env) + && subject_ty.is_subtype_of(db, env, callable_pattern_ty) => { return callable_pattern_ty; } @@ -641,25 +701,25 @@ pub(crate) fn definite_match_pattern_type_for_subject<'db>( } } PatternPredicateKind::Sequence(kind) => { - if !sequence_pattern_is_exhaustive_for_subject(db, kind, resolved_subject_ty) { + if !sequence_pattern_is_exhaustive_for_subject(db, env, kind, resolved_subject_ty) { // A nested subject-dependent pattern rejected the context-free approximation. // Reusing that approximation for the surrounding sequence would reintroduce the // values that the recursive analysis deliberately excluded. return Type::Never; } - let top_subject_ty = resolved_subject_ty.top_materialization(db); - return if sequence_pattern_is_exhaustive_for_subject(db, kind, top_subject_ty) { + let top_subject_ty = resolved_subject_ty.top_materialization(db, env); + return if sequence_pattern_is_exhaustive_for_subject(db, env, kind, top_subject_ty) { top_subject_ty } else { subject_ty }; } PatternPredicateKind::Mapping(kind) => { - if !mapping_pattern_is_exhaustive(db, kind, resolved_subject_ty) { + if !mapping_pattern_is_exhaustive(db, env, kind, resolved_subject_ty) { return Type::Never; } - let top_subject_ty = resolved_subject_ty.top_materialization(db); - return if mapping_pattern_is_exhaustive(db, kind, top_subject_ty) { + let top_subject_ty = resolved_subject_ty.top_materialization(db, env); + return if mapping_pattern_is_exhaustive(db, env, kind, top_subject_ty) { top_subject_ty } else { subject_ty @@ -668,20 +728,21 @@ pub(crate) fn definite_match_pattern_type_for_subject<'db>( PatternPredicateKind::Or(patterns) => { return UnionType::from_elements( db, + env, patterns.iter().map(|pattern| { - definite_match_pattern_type_for_subject(db, pattern, subject_ty) + definite_match_pattern_type_for_subject(db, env, pattern, subject_ty) }), ); } PatternPredicateKind::As(Some(pattern), _) => { - return definite_match_pattern_type_for_subject(db, pattern, subject_ty); + return definite_match_pattern_type_for_subject(db, env, pattern, subject_ty); } _ => return Type::Never, } - IntersectionBuilder::new(db) + IntersectionBuilder::new(db, env) .add_positive(subject_ty) - .add_positive(definite_match_pattern_type(db, kind)) + .add_positive(definite_match_pattern_type(db, env, kind)) .build() } @@ -700,8 +761,9 @@ pub(crate) fn definite_match_pattern_type_for_subject<'db>( /// case other: /// reveal_type(other) # Literal[2] /// ``` -pub(crate) fn pattern_fallthrough_type<'db>( +fn pattern_fallthrough_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, kind: &PatternPredicateKind<'db>, subject_ty: Type<'db>, ) -> Type<'db> { @@ -712,9 +774,10 @@ pub(crate) fn pattern_fallthrough_type<'db>( // matches. This includes narrowed intersections containing `Self` or another type variable // whose upper bound is that enum. if let Some(enum_literal) = value_ty.as_enum_literal() - && is_same_enum_pattern_domain(db, subject_ty, enum_literal) + && is_same_enum_pattern_domain(db, env, subject_ty, enum_literal) && equality_truthiness( db, + env, value_ty, value_ty, ComparisonSoundnessPolicy::from_analysis_settings( @@ -722,29 +785,30 @@ pub(crate) fn pattern_fallthrough_type<'db>( ), ) == Truthiness::AlwaysTrue { - return IntersectionBuilder::new(db) + return IntersectionBuilder::new(db, env) .add_positive(subject_ty) .add_negative(value_ty) .build(); } if let Some(constraint) = evaluate_type_equality( db, + env, subject_ty, value_ty, false, ComparisonSoundnessPolicy::from_analysis_settings(db.analysis_settings(value.file(db))), ) { - return IntersectionBuilder::new(db) + return IntersectionBuilder::new(db, env) .add_positive(subject_ty) .add_positive(constraint) .build(); } } - IntersectionBuilder::new(db) + IntersectionBuilder::new(db, env) .add_positive(subject_ty) .add_negative(definite_match_pattern_type_for_subject( - db, kind, subject_ty, + db, env, kind, subject_ty, )) .build() } @@ -770,12 +834,14 @@ pub(crate) fn pattern_fallthrough_type<'db>( /// ``` pub(crate) fn pattern_binding_fallthrough_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, kind: &PatternPredicateKind<'db>, subject_ty: Type<'db>, ) -> Type<'db> { let mut budget = ExactTuplePatternExpansionBudget::default(); - try_pattern_binding_fallthrough_type(db, kind, subject_ty, &mut budget) - .unwrap_or_else(|()| conservative_pattern_binding_fallthrough_type(db, kind, subject_ty)) + try_pattern_binding_fallthrough_type(db, env, kind, subject_ty, &mut budget).unwrap_or_else( + |()| conservative_pattern_binding_fallthrough_type(db, env, kind, subject_ty), + ) } /// Compute binding fallthrough while charging every nested exact-tuple expansion to `budget`. @@ -784,23 +850,24 @@ pub(crate) fn pattern_binding_fallthrough_type<'db>( /// complete pattern conservatively. fn try_pattern_binding_fallthrough_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, kind: &PatternPredicateKind<'db>, subject_ty: Type<'db>, budget: &mut ExactTuplePatternExpansionBudget, ) -> Result, ()> { match kind { PatternPredicateKind::Sequence(sequence) => { - try_sequence_pattern_binding_fallthrough_type(db, sequence, subject_ty, budget) + try_sequence_pattern_binding_fallthrough_type(db, env, sequence, subject_ty, budget) } PatternPredicateKind::Or(patterns) => { patterns.iter().try_fold(subject_ty, |remaining, pattern| { - try_pattern_binding_fallthrough_type(db, pattern, remaining, budget) + try_pattern_binding_fallthrough_type(db, env, pattern, remaining, budget) }) } PatternPredicateKind::As(Some(pattern), _) => { - try_pattern_binding_fallthrough_type(db, pattern, subject_ty, budget) + try_pattern_binding_fallthrough_type(db, env, pattern, subject_ty, budget) } - _ => Ok(pattern_fallthrough_type(db, kind, subject_ty)), + _ => Ok(pattern_fallthrough_type(db, env, kind, subject_ty)), } } @@ -810,19 +877,20 @@ fn try_pattern_binding_fallthrough_type<'db>( /// used when the precise traversal exceeds its expansion budget. fn conservative_pattern_binding_fallthrough_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, kind: &PatternPredicateKind<'db>, subject_ty: Type<'db>, ) -> Type<'db> { match kind { PatternPredicateKind::Or(patterns) => { patterns.iter().fold(subject_ty, |remaining, pattern| { - conservative_pattern_binding_fallthrough_type(db, pattern, remaining) + conservative_pattern_binding_fallthrough_type(db, env, pattern, remaining) }) } PatternPredicateKind::As(Some(pattern), _) => { - conservative_pattern_binding_fallthrough_type(db, pattern, subject_ty) + conservative_pattern_binding_fallthrough_type(db, env, pattern, subject_ty) } - _ => pattern_fallthrough_type(db, kind, subject_ty), + _ => pattern_fallthrough_type(db, env, kind, subject_ty), } } @@ -832,6 +900,7 @@ fn conservative_pattern_binding_fallthrough_type<'db>( /// expansion cannot exceed the configured limits. fn try_sequence_pattern_binding_fallthrough_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, kind: &SequencePatternPredicateKind<'db>, subject_ty: Type<'db>, budget: &mut ExactTuplePatternExpansionBudget, @@ -839,14 +908,14 @@ fn try_sequence_pattern_binding_fallthrough_type<'db>( let resolved = subject_ty.resolve_type_alias(db); let narrowed = match resolved { Type::Union(union) => union - .try_map(db, |element| { - try_sequence_pattern_binding_fallthrough_type(db, kind, *element, budget).ok() + .try_map(db, env, |element| { + try_sequence_pattern_binding_fallthrough_type(db, env, kind, *element, budget).ok() }) .ok_or(())?, Type::Intersection(intersection) => { let mut failed = false; - let narrowed = intersection.map_positive(db, |element| { - try_sequence_pattern_binding_fallthrough_type(db, kind, *element, budget) + let narrowed = intersection.map_positive(db, env, |element| { + try_sequence_pattern_binding_fallthrough_type(db, env, kind, *element, budget) .unwrap_or_else(|()| { failed = true; *element @@ -858,18 +927,27 @@ fn try_sequence_pattern_binding_fallthrough_type<'db>( narrowed } Type::TypeVar(typevar) - if typevar.typevar(db).upper_bound(db).is_some_and(|bound| { - pattern_fallthrough_type(db, &PatternPredicateKind::Sequence(kind.clone()), bound) + if typevar + .typevar(db) + .upper_bound(db, env) + .is_some_and(|bound| { + pattern_fallthrough_type( + db, + env, + &PatternPredicateKind::Sequence(kind.clone()), + bound, + ) .is_never() - }) => + }) => { Type::Never } _ if resolved.exact_tuple_instance_spec(db).is_some() => { - exact_tuple_sequence_pattern_fallthrough_type(db, kind, resolved, budget)? + exact_tuple_sequence_pattern_fallthrough_type(db, env, kind, resolved, budget)? .unwrap_or_else(|| { pattern_fallthrough_type( db, + env, &PatternPredicateKind::Sequence(kind.clone()), resolved, ) @@ -877,9 +955,9 @@ fn try_sequence_pattern_binding_fallthrough_type<'db>( } // An irrefutable sequence pattern can only fail if the subject is not eligible for sequence // matching. Unlike length and indexed-element facts, eligibility is unaffected by mutation. - _ if kind.is_irrefutable() => IntersectionBuilder::new(db) + _ if kind.is_irrefutable() => IntersectionBuilder::new(db, env) .add_positive(resolved) - .add_negative(sequence_pattern_type_builder(db).build()) + .add_negative(sequence_pattern_type_builder(db, env).build()) .build(), _ => resolved, }; @@ -922,6 +1000,7 @@ impl ExactTuplePatternExpansionBudget { /// representation used by the general fallthrough path. fn exact_tuple_sequence_pattern_fallthrough_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, kind: &SequencePatternPredicateKind<'db>, subject_ty: Type<'db>, budget: &mut ExactTuplePatternExpansionBudget, @@ -939,7 +1018,7 @@ fn exact_tuple_sequence_pattern_fallthrough_type<'db>( if tuple .all_elements() .iter() - .any(|element| any_over_type(db, *element, true, |ty| ty.is_dynamic())) + .any(|element| any_over_type(db, env, *element, true, |ty| ty.is_dynamic())) { return Ok(None); } @@ -952,7 +1031,7 @@ fn exact_tuple_sequence_pattern_fallthrough_type<'db>( .zip(kind.patterns.iter()) .enumerate() { - let remaining = try_pattern_binding_fallthrough_type(db, pattern, element, budget)?; + let remaining = try_pattern_binding_fallthrough_type(db, env, pattern, element, budget)?; if remaining == element { return Ok(Some(subject_ty)); } @@ -963,36 +1042,37 @@ fn exact_tuple_sequence_pattern_fallthrough_type<'db>( budget.add_alternative(tuple.len())?; let mut elements = tuple.all_elements().to_vec(); elements[index] = remaining; - alternatives.push(Type::heterogeneous_tuple(db, elements)); + alternatives.push(Type::heterogeneous_tuple(db, env, elements)); } - Ok(Some(UnionType::from_elements(db, alternatives))) + Ok(Some(UnionType::from_elements(db, env, alternatives))) } /// Return whether every possible value of `ty` belongs to the same enum as `right`, including /// bounded type variables nested inside unions or intersections. fn is_same_enum_pattern_domain<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, right: EnumLiteralType<'db>, ) -> bool { - if is_same_enum_domain(db, ty, right) { + if is_same_enum_domain(db, env, ty, right) { return true; } match ty.resolve_type_alias(db) { Type::TypeVar(typevar) => typevar .typevar(db) - .upper_bound(db) - .is_some_and(|bound| is_same_enum_domain(db, bound, right)), + .upper_bound(db, env) + .is_some_and(|bound| is_same_enum_domain(db, env, bound, right)), Type::Union(union) => union .elements(db) .iter() - .all(|element| is_same_enum_pattern_domain(db, *element, right)), + .all(|element| is_same_enum_pattern_domain(db, env, *element, right)), Type::Intersection(intersection) => intersection .positive(db) .iter() - .any(|element| is_same_enum_pattern_domain(db, *element, right)), + .any(|element| is_same_enum_pattern_domain(db, env, *element, right)), _ => false, } } @@ -1004,47 +1084,48 @@ fn is_same_enum_pattern_domain<'db>( /// the static subject type. fn subject_independent_definite_match_pattern_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, kind: &PatternPredicateKind<'db>, ) -> Option> { match kind { PatternPredicateKind::Class(kind) => { match infer_same_file_expression_type(db, kind.class, TypeContext::default()) { Type::ClassLiteral(class) if kind.is_empty() => { - let class_instance_ty = Type::instance(db, class.top_materialization(db)); + let class_instance_ty = Type::instance(db, env, class.top_materialization(db)); let typed_dict_adds_runtime_matches = - typed_dict_matches_class_pattern(db, class) - && !Type::object().is_subtype_of(db, class_instance_ty); + typed_dict_matches_class_pattern(db, env, class) + && !Type::object().is_subtype_of(db, env, class_instance_ty); (!typed_dict_adds_runtime_matches).then_some(class_instance_ty) } Type::ClassLiteral(_) => None, Type::SpecialForm(SpecialFormType::CollectionsAbcCallable) if kind.is_empty() => { - Some(callable_pattern_type(db)) + Some(callable_pattern_type(db, env)) } _ => Some(Type::Never), } } PatternPredicateKind::Sequence(kind) => { - build_definite_sequence_pattern_type(db, kind, |pattern| { - subject_independent_definite_match_pattern_type(db, pattern) + build_definite_sequence_pattern_type(db, env, kind, |pattern| { + subject_independent_definite_match_pattern_type(db, env, pattern) }) } PatternPredicateKind::Mapping(kind) => { if kind.is_irrefutable() { - Some(mapping_pattern_type(db)) + Some(mapping_pattern_type(db, env)) } else { None } } PatternPredicateKind::Or(patterns) => patterns .iter() - .map(|pattern| subject_independent_definite_match_pattern_type(db, pattern)) + .map(|pattern| subject_independent_definite_match_pattern_type(db, env, pattern)) .collect::>>() - .map(|types| UnionType::from_elements(db, types)), + .map(|types| UnionType::from_elements(db, env, types)), PatternPredicateKind::As(Some(pattern), _) => { - subject_independent_definite_match_pattern_type(db, pattern) + subject_independent_definite_match_pattern_type(db, env, pattern) } PatternPredicateKind::Value(_) => None, - _ => Some(definite_match_pattern_type(db, kind)), + _ => Some(definite_match_pattern_type(db, env, kind)), } } @@ -1053,10 +1134,11 @@ fn subject_independent_definite_match_pattern_type<'db>( /// Reachability and negative narrowing can only subtract this under-approximation. pub(crate) fn definite_match_pattern_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, kind: &PatternPredicateKind<'db>, ) -> Type<'db> { match kind { - PatternPredicateKind::Singleton(singleton) => singleton_pattern_type(db, *singleton), + PatternPredicateKind::Singleton(singleton) => singleton_pattern_type(db, env, *singleton), PatternPredicateKind::Value(value) => { let ty = infer_same_file_expression_type(db, *value, TypeContext::default()); // Only return the type if it's guaranteed to match itself. @@ -1064,7 +1146,7 @@ pub(crate) fn definite_match_pattern_type<'db>( let policy = ComparisonSoundnessPolicy::from_analysis_settings( db.analysis_settings(value.file(db)), ); - if equality_truthiness(db, ty, ty, policy) == Truthiness::AlwaysTrue { + if equality_truthiness(db, env, ty, ty, policy) == Truthiness::AlwaysTrue { ty } else { Type::Never @@ -1073,39 +1155,40 @@ pub(crate) fn definite_match_pattern_type<'db>( PatternPredicateKind::Class(kind) => { match infer_same_file_expression_type(db, kind.class, TypeContext::default()) { Type::ClassLiteral(class) if kind.is_empty() => { - Type::instance(db, class.top_materialization(db)) + Type::instance(db, env, class.top_materialization(db)) } Type::SpecialForm(SpecialFormType::CollectionsAbcCallable) if kind.is_empty() => { - callable_pattern_type(db) + callable_pattern_type(db, env) } _ => Type::Never, } } PatternPredicateKind::Mapping(kind) => { if kind.is_irrefutable() { - mapping_pattern_type(db) + mapping_pattern_type(db, env) } else { Type::Never } } - PatternPredicateKind::Sequence(kind) => definite_sequence_pattern_type(db, kind), + PatternPredicateKind::Sequence(kind) => definite_sequence_pattern_type(db, env, kind), PatternPredicateKind::Or(predicates) => UnionType::from_elements( db, + env, predicates .iter() - .map(|p| definite_match_pattern_type(db, p)), + .map(|p| definite_match_pattern_type(db, env, p)), ), // basedpython: a value matches a conjunction only when every conjunct // matches it PatternPredicateKind::And(predicates) => predicates .iter() - .fold(IntersectionBuilder::new(db), |builder, p| { - builder.add_positive(definite_match_pattern_type(db, p)) + .fold(IntersectionBuilder::new(db, env), |builder, p| { + builder.add_positive(definite_match_pattern_type(db, env, p)) }) .build(), PatternPredicateKind::As(pattern, _) => pattern .as_deref() - .map(|p| definite_match_pattern_type(db, p)) + .map(|p| definite_match_pattern_type(db, env, p)) .unwrap_or_else(Type::object), PatternPredicateKind::Star(_) => Type::object(), } @@ -1114,21 +1197,23 @@ pub(crate) fn definite_match_pattern_type<'db>( /// Return the values that are guaranteed to match a sequence pattern. fn definite_sequence_pattern_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, kind: &SequencePatternPredicateKind<'db>, ) -> Type<'db> { - build_definite_sequence_pattern_type(db, kind, |pattern| { - Some(definite_match_pattern_type(db, pattern)) + build_definite_sequence_pattern_type(db, env, kind, |pattern| { + Some(definite_match_pattern_type(db, env, pattern)) }) .unwrap_or(Type::Never) } fn build_definite_sequence_pattern_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, kind: &SequencePatternPredicateKind<'db>, mut element_type: impl FnMut(&PatternPredicateKind<'db>) -> Option>, ) -> Option> { if kind.is_irrefutable() { - return Some(sequence_pattern_type_builder(db).build()); + return Some(sequence_pattern_type_builder(db, env).build()); } if let Some((prefix, suffix)) = kind.split_around_star() { @@ -1142,6 +1227,7 @@ fn build_definite_sequence_pattern_type<'db>( .collect::>>()?; return Some(Type::tuple(TupleType::mixed( db, + env, prefix_types, Type::object(), suffix_types, @@ -1157,6 +1243,10 @@ fn build_definite_sequence_pattern_type<'db>( if element_types.iter().any(Type::is_never) { Some(Type::Never) } else { - Some(exact_sequence_pattern_type(db, element_types.into_iter())) + Some(exact_sequence_pattern_type( + db, + env, + element_types.into_iter(), + )) } } diff --git a/crates/ty_python_semantic/src/types/match_type.rs b/crates/ty_python_semantic/src/types/match_type.rs index 527507d1bb..06bbb57830 100644 --- a/crates/ty_python_semantic/src/types/match_type.rs +++ b/crates/ty_python_semantic/src/types/match_type.rs @@ -29,6 +29,7 @@ use ruff_python_ast as ast; use ruff_db::parsed::parsed_module; use crate::Db; +use crate::types::ProgramEnvironment; use crate::types::deferred::{DeferredOperation, DeferredType}; use crate::types::generics::{ApplySpecialization, GenericContext}; use crate::types::tuple::{Tuple, TupleType}; @@ -51,6 +52,7 @@ use ty_python_core::semantic_index; /// Returns `None` for an ordinary type alias. pub(crate) fn match_type_application<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, alias: TypeAliasType<'db>, ) -> Option> { let pep695 = alias.as_pep_695_type_alias()?; @@ -70,6 +72,7 @@ pub(crate) fn match_type_application<'db>( Some(DeferredType::build( db, + env, &DeferredOperation::MatchType, operands.into_boxed_slice(), )) @@ -135,29 +138,30 @@ fn evaluate_match_type_cached<'db>( db: &'db dyn Db, alias: PEP695TypeAliasType<'db>, ) -> MatchTypeOutcome<'db> { + let env = &ProgramEnvironment::from_scope(alias.rhs_scope(db)); let scope = alias.rhs_scope(db); let file = scope.file(db); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, db.program_file(file).python_file(db)).load(db); let node = scope.node(db).expect_type_alias().node(&module); let definition = alias.definition(db); let subject = alias.apply_own_specialization( db, - subject_type(db, definition, &node.value).unwrap_or_else(Type::unknown), + subject_type(db, env, definition, &node.value).unwrap_or_else(Type::unknown), ); // a subject that still mentions a type parameter cannot pick a case: `()` and // `(Dim, *Rest)` are both still possible - if subject.has_typevar(db) { + if subject.has_typevar(db, env) { return MatchTypeOutcome::Unresolved; } - if exceeds_budget(db, subject) { + if exceeds_budget(db, env, subject) { return MatchTypeOutcome::TooLarge; } for case in &node.cases { let mut bindings = Bindings::default(); - match match_pattern(db, file, subject, &case.pattern, &mut bindings) { + match match_pattern(db, env, file, subject, &case.pattern, &mut bindings) { PatternMatch::NoMatch => continue, // an undecidable pattern stops the whole match: falling through to the next case // would let it answer a question this one could still have claimed @@ -169,12 +173,12 @@ fn evaluate_match_type_cached<'db>( }; let body = alias.apply_own_specialization(db, definition_expression_type(db, definition, body)); - let value = bindings.apply(db, body); + let value = bindings.apply(db, env, body); // a body that names a capture the pattern did not bind — an or-pattern whose // alternatives bind different names, say — would otherwise leak that capture's type // variable into the alias's value, where it means nothing. the malformed pattern is // reported where the alias is written; here it simply has no value - if mentions_capture_of(db, value, definition) { + if mentions_capture_of(db, env, value, definition) { return MatchTypeOutcome::Unresolved; } return MatchTypeOutcome::Matched(value); @@ -187,9 +191,9 @@ fn evaluate_match_type_cached<'db>( /// /// The walk short-circuits as soon as the budget runs out, so this costs at most the budget /// however large `ty` actually is. -fn exceeds_budget<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { +fn exceeds_budget<'db>(db: &'db dyn Db, env: &ProgramEnvironment<'db>, ty: Type<'db>) -> bool { let remaining = std::cell::Cell::new(MAX_SUBJECT_NODES); - any_over_type(db, ty, false, |_| match remaining.get() { + any_over_type(db, env, ty, false, |_| match remaining.get() { 0 => true, budget => { remaining.set(budget - 1); @@ -204,10 +208,11 @@ fn exceeds_budget<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { /// a `case` capture that escaped its pattern. fn mentions_capture_of<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, alias_definition: Definition<'db>, ) -> bool { - any_over_type(db, ty, false, |ty| match ty { + any_over_type(db, env, ty, false, |ty| match ty { Type::TypeVar(bound_typevar) => { bound_typevar.binding_context(db) == BindingContext::Definition(alias_definition) } @@ -232,6 +237,7 @@ fn case_body(case: &ast::MatchCase) -> Option<&ast::Expr> { /// pack spreads into. Any other subject is matched as itself. fn subject_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, definition: Definition<'db>, subject: &ast::Expr, ) -> Option> { @@ -247,6 +253,7 @@ fn subject_type<'db>( let bound_typevar = unpacked.as_typevar()?; Some(Type::tuple(Some(TupleType::unpacked_typevartuple( db, + env, bound_typevar, )))) } @@ -308,12 +315,13 @@ impl<'db> Bindings<'db> { } /// Substitutes the captures into a case body's type. - fn apply(&self, db: &'db dyn Db, body: Type<'db>) -> Type<'db> { + fn apply(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>, body: Type<'db>) -> Type<'db> { if self.captures.is_empty() { return body; } let generic_context = GenericContext::from_typevar_instances( db, + env, self.captures.iter().map(|(typevar, _)| *typevar), ); let specialization = generic_context.specialize( @@ -326,9 +334,10 @@ impl<'db> Bindings<'db> { ); body.apply_type_mapping_impl( db, + env, &TypeMapping::ApplySpecialization(ApplySpecialization::TypeAlias(specialization)), TypeContext::default(), - &ApplyTypeMappingVisitor::default(), + &ApplyTypeMappingVisitor::new(env), ) } } @@ -338,6 +347,7 @@ impl<'db> Bindings<'db> { /// `bindings` is only meaningful when the result is [`PatternMatch::Matched`]. fn match_pattern<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: ruff_db::files::File, subject: Type<'db>, pattern: &ast::Pattern, @@ -351,7 +361,7 @@ fn match_pattern<'db>( .. }) => { if let Some(inner) = inner.as_deref() { - match match_pattern(db, file, subject, inner, bindings) { + match match_pattern(db, env, file, subject, inner, bindings) { PatternMatch::Matched => {} other => return other, } @@ -369,7 +379,7 @@ fn match_pattern<'db>( let mut undecidable = false; for pattern in patterns { let checkpoint = bindings.checkpoint(); - match match_pattern(db, file, subject, pattern, bindings) { + match match_pattern(db, env, file, subject, pattern, bindings) { PatternMatch::Matched => return PatternMatch::Matched, PatternMatch::Undecidable => undecidable = true, PatternMatch::NoMatch => {} @@ -386,7 +396,7 @@ fn match_pattern<'db>( } ast::Pattern::MatchSequence(ast::PatternMatchSequence { patterns, .. }) => { - match_sequence(db, file, subject, patterns, bindings) + match_sequence(db, env, file, subject, patterns, bindings) } // `case 2:` — a literal in a shape. `Literal[2]` is what the subject element is, so @@ -399,16 +409,16 @@ fn match_pattern<'db>( // not a literal type at all — reported where the alias is written return PatternMatch::Undecidable; }; - decide(subject, subject.is_equivalent_to(db, expected)) + decide(subject, subject.is_equivalent_to(db, env, expected)) } ast::Pattern::MatchSingleton(ast::PatternMatchSingleton { value, .. }) => { let expected = match value { - ast::Singleton::None => Type::none(db), + ast::Singleton::None => Type::none(db, env), ast::Singleton::True => Type::bool_literal(true), ast::Singleton::False => Type::bool_literal(false), }; - decide(subject, subject.is_equivalent_to(db, expected)) + decide(subject, subject.is_equivalent_to(db, env, expected)) } // a class or mapping pattern destructures a *value*; there is nothing at the type @@ -425,7 +435,7 @@ fn match_pattern<'db>( let checkpoint = bindings.checkpoint(); let mut undecidable = false; for pattern in patterns { - match match_pattern(db, file, subject, pattern, bindings) { + match match_pattern(db, env, file, subject, pattern, bindings) { PatternMatch::Matched => {} PatternMatch::Undecidable => undecidable = true, PatternMatch::NoMatch => { @@ -461,6 +471,7 @@ fn decide(subject: Type<'_>, matches: bool) -> PatternMatch { /// Matches a sequence pattern — `()`, `(A, B)`, `(A, *Rest)` — against a tuple subject. fn match_sequence<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: ruff_db::files::File, subject: Type<'db>, patterns: &[ast::Pattern], @@ -482,7 +493,7 @@ fn match_sequence<'db>( if elements.len() != patterns.len() { return PatternMatch::NoMatch; } - return match_all(db, file, patterns, elements, bindings); + return match_all(db, env, file, patterns, elements, bindings); }; let suffix_len = patterns.len() - star_index - 1; @@ -492,11 +503,11 @@ fn match_sequence<'db>( let (prefix, rest) = elements.split_at(star_index); let (starred, suffix) = rest.split_at(rest.len() - suffix_len); - match match_all(db, file, &patterns[..star_index], prefix, bindings) { + match match_all(db, env, file, &patterns[..star_index], prefix, bindings) { PatternMatch::Matched => {} other => return other, } - match match_all(db, file, &patterns[star_index + 1..], suffix, bindings) { + match match_all(db, env, file, &patterns[star_index + 1..], suffix, bindings) { PatternMatch::Matched => {} other => return other, } @@ -513,7 +524,7 @@ fn match_sequence<'db>( bindings.push( db, typevar, - Type::heterogeneous_tuple(db, starred.iter().copied()), + Type::heterogeneous_tuple(db, env, starred.iter().copied()), ) } @@ -521,13 +532,14 @@ fn match_sequence<'db>( /// that does not match or cannot be decided. fn match_all<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: ruff_db::files::File, patterns: &[ast::Pattern], elements: &[Type<'db>], bindings: &mut Bindings<'db>, ) -> PatternMatch { for (pattern, element) in std::iter::zip(patterns, elements) { - match match_pattern(db, file, *element, pattern, bindings) { + match match_pattern(db, env, file, *element, pattern, bindings) { PatternMatch::Matched => {} other => return other, } @@ -571,7 +583,7 @@ fn capture_typevar<'db>( file: ruff_db::files::File, name: &ast::Identifier, ) -> Option> { - let index = semantic_index(db, file); + let index = semantic_index(db, db.program_file(file)); let definition = index.try_definition(name)?; let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = binding_type(db, definition) else { diff --git a/crates/ty_python_semantic/src/types/member.rs b/crates/ty_python_semantic/src/types/member.rs index 6f75d70b84..8f13081198 100644 --- a/crates/ty_python_semantic/src/types/member.rs +++ b/crates/ty_python_semantic/src/types/member.rs @@ -3,7 +3,7 @@ use crate::place::{ ConsideredDefinitions, DefinedPlace, Place, PlaceAndQualifiers, RequiresExplicitReExport, place_by_id, place_from_bindings, }; -use crate::types::Type; +use crate::types::{ProgramEnvironment, Type}; use ty_python_core::{place_table, scope::ScopeId, use_def_map}; /// The return type of certain member-lookup operations. Contains information @@ -85,7 +85,8 @@ pub(super) fn class_member<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str // 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_symbol_bindings(symbol_id); - let inferred = place_from_bindings(db, bindings).place; + let env = ProgramEnvironment::from_scope(scope); + let inferred = place_from_bindings(db, &env, bindings).place; // 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 diff --git a/crates/ty_python_semantic/src/types/method.rs b/crates/ty_python_semantic/src/types/method.rs index d69e91c3f5..3e1454889c 100644 --- a/crates/ty_python_semantic/src/types/method.rs +++ b/crates/ty_python_semantic/src/types/method.rs @@ -1,3 +1,4 @@ +use crate::ProgramEnvironment; use itertools::Either; use ruff_python_ast::name::Name; @@ -51,9 +52,12 @@ impl<'db> BoundMethodType<'db> { /// a `@classmethod`, then it should be an instance of that bound-instance type. pub(crate) fn typing_self_type(self, db: &'db dyn Db) -> Type<'db> { let mut self_instance = self.self_instance(db); - if self.function(db).is_classmethod(db) { + let function = self.function(db); + if function.is_classmethod(db) { + let env = + ProgramEnvironment::from_scope(function.literal(db).last_definition.body_scope(db)); self_instance = self_instance - .to_instance_approximation(db) + .to_instance_approximation(db, &env) .unwrap_or_else(Type::unknown); } self_instance @@ -74,7 +78,6 @@ impl<'db> BoundMethodType<'db> { )] pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> CallableType<'db> { let function = self.function(db); - CallableType::new( db, self.bound_signatures(db), @@ -89,6 +92,7 @@ impl<'db> BoundMethodType<'db> { pub(crate) fn into_callable_type_with_receiver( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, receiver_type: Type<'db>, typing_self_type: Type<'db>, ) -> CallableType<'db> { @@ -96,7 +100,7 @@ impl<'db> BoundMethodType<'db> { CallableType::new( db, - self.bound_signatures_with_receiver(db, receiver_type, typing_self_type), + self.bound_signatures_with_receiver(db, env, receiver_type, typing_self_type), CallableTypeKind::FunctionLike, CallableFunctionProvenance::from_function_return_annotation( function.has_explicit_return_annotation(db), @@ -106,15 +110,19 @@ impl<'db> BoundMethodType<'db> { #[salsa::tracked(returns(ref), cycle_initial=|_, _, _| CallableSignature::bottom(), heap_size=ruff_memory_usage::heap_size)] pub(crate) fn bound_signatures(self, db: &'db dyn Db) -> CallableSignature<'db> { + let function = self.function(db); + let env = + ProgramEnvironment::from_scope(function.literal(db).last_definition.body_scope(db)); let typing_self_type = self.typing_self_type(db); let receiver_type = self.self_instance(db); - self.bound_signatures_with_receiver(db, receiver_type, typing_self_type) + self.bound_signatures_with_receiver(db, &env, receiver_type, typing_self_type) } fn bound_signatures_with_receiver( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, receiver_type: Type<'db>, typing_self_type: Type<'db>, ) -> CallableSignature<'db> { @@ -130,6 +138,7 @@ impl<'db> BoundMethodType<'db> { |signature| { signature.bind_self_with_receiver( db, + env, Some(receiver_type), Some(typing_self_type), ) @@ -138,22 +147,15 @@ impl<'db> BoundMethodType<'db> { } return CallableSignature::from_overloads( - function_signature - .overloads - .iter() - .filter(|signature| signature.can_bind_self_to(db, receiver_type)) - .map(|signature| { - signature.bind_self_with_receiver( - db, - Some(receiver_type), - Some(typing_self_type), - ) - }), + function_signature.overloads.iter().filter_map(|signature| { + signature.bind_self_if_compatible(db, env, receiver_type, typing_self_type) + }), ); }; CallableSignature::single(signature.bind_self_with_receiver( db, + env, Some(receiver_type), Some(typing_self_type), )) @@ -162,15 +164,16 @@ impl<'db> BoundMethodType<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { Some(Self::new( db, self.function(db) - .recursive_type_normalized_impl(db, div, nested)?, + .recursive_type_normalized_impl(db, env, div, nested)?, self.self_instance(db) - .recursive_type_normalized_impl(db, div, true)?, + .recursive_type_normalized_impl(db, env, div, true)?, )) } } @@ -217,11 +220,15 @@ pub enum KnownBoundMethodType<'db> { StrStartswith(StringLiteralType<'db>), // ConstraintSet methods + ConstraintSetLowerBound, + ConstraintSetUpperBound, + ConstraintSetEquality, ConstraintSetRange, ConstraintSetAlways, ConstraintSetNever, ConstraintSetImpliesSubtypeOf(InternedConstraintSet<'db>), ConstraintSetSatisfies(InternedConstraintSet<'db>), + ConstraintSetExists(InternedConstraintSet<'db>), ConstraintSetForAll(InternedConstraintSet<'db>), ConstraintSetSatisfiedByAllTypeVars(InternedConstraintSet<'db>), ConstraintSetSolutionsFor(InternedConstraintSet<'db>), @@ -256,11 +263,15 @@ pub(super) fn walk_method_wrapper_type<'db, V: visitor::TypeVisitor<'db> + ?Size LiteralValueType::promotable(LiteralValueTypeKind::String(string_literal)).into(), ); } - KnownBoundMethodType::ConstraintSetRange + KnownBoundMethodType::ConstraintSetLowerBound + | KnownBoundMethodType::ConstraintSetUpperBound + | KnownBoundMethodType::ConstraintSetEquality + | KnownBoundMethodType::ConstraintSetRange | KnownBoundMethodType::ConstraintSetAlways | KnownBoundMethodType::ConstraintSetNever | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) | KnownBoundMethodType::ConstraintSetSatisfies(_) + | KnownBoundMethodType::ConstraintSetExists(_) | KnownBoundMethodType::ConstraintSetForAll(_) | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) | KnownBoundMethodType::ConstraintSetSolutionsFor(_) @@ -273,41 +284,46 @@ impl<'db> KnownBoundMethodType<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { match self { KnownBoundMethodType::FunctionTypeDunderGet(function) => { Some(KnownBoundMethodType::FunctionTypeDunderGet( - function.recursive_type_normalized_impl(db, div, nested)?, + function.recursive_type_normalized_impl(db, env, div, nested)?, )) } KnownBoundMethodType::FunctionTypeDunderCall(function) => { Some(KnownBoundMethodType::FunctionTypeDunderCall( - function.recursive_type_normalized_impl(db, div, nested)?, + function.recursive_type_normalized_impl(db, env, div, nested)?, )) } KnownBoundMethodType::PropertyDunderGet(property) => { Some(KnownBoundMethodType::PropertyDunderGet( - property.recursive_type_normalized_impl(db, div, nested)?, + property.recursive_type_normalized_impl(db, env, div, nested)?, )) } KnownBoundMethodType::PropertyDunderSet(property) => { Some(KnownBoundMethodType::PropertyDunderSet( - property.recursive_type_normalized_impl(db, div, nested)?, + property.recursive_type_normalized_impl(db, env, div, nested)?, )) } KnownBoundMethodType::PropertyDunderDelete(property) => { Some(KnownBoundMethodType::PropertyDunderDelete( - property.recursive_type_normalized_impl(db, div, nested)?, + property.recursive_type_normalized_impl(db, env, div, nested)?, )) } KnownBoundMethodType::StrStartswith(_) + | KnownBoundMethodType::ConstraintSetLowerBound + | KnownBoundMethodType::ConstraintSetUpperBound + | KnownBoundMethodType::ConstraintSetEquality | KnownBoundMethodType::ConstraintSetRange | KnownBoundMethodType::ConstraintSetAlways | KnownBoundMethodType::ConstraintSetNever | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) | KnownBoundMethodType::ConstraintSetSatisfies(_) + | KnownBoundMethodType::ConstraintSetExists(_) | KnownBoundMethodType::ConstraintSetForAll(_) | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) | KnownBoundMethodType::ConstraintSetSolutionsFor(_) @@ -325,11 +341,15 @@ impl<'db> KnownBoundMethodType<'db> { | KnownBoundMethodType::PropertyDunderSet(_) | KnownBoundMethodType::PropertyDunderDelete(_) => KnownClass::MethodWrapperType, KnownBoundMethodType::StrStartswith(_) => KnownClass::BuiltinFunctionType, - KnownBoundMethodType::ConstraintSetRange + KnownBoundMethodType::ConstraintSetLowerBound + | KnownBoundMethodType::ConstraintSetUpperBound + | KnownBoundMethodType::ConstraintSetEquality + | KnownBoundMethodType::ConstraintSetRange | KnownBoundMethodType::ConstraintSetAlways | KnownBoundMethodType::ConstraintSetNever | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) | KnownBoundMethodType::ConstraintSetSatisfies(_) + | KnownBoundMethodType::ConstraintSetExists(_) | KnownBoundMethodType::ConstraintSetForAll(_) | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) | KnownBoundMethodType::ConstraintSetSolutionsFor(_) @@ -343,7 +363,11 @@ impl<'db> KnownBoundMethodType<'db> { /// Return the signatures of this bound method type. /// /// If the bound method type is overloaded, it may have multiple signatures. - pub(super) fn signatures(self, db: &'db dyn Db) -> impl Iterator> { + pub(super) fn signatures( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> impl Iterator> { let object_type_form = || TypeFormType::from_type_expression(db, Type::object()); match self { @@ -372,9 +396,9 @@ impl<'db> KnownBoundMethodType<'db> { Signature::new( Parameters::standard([ Parameter::positional_only(Some(Name::new_static("instance"))) - .with_annotated_type(Type::none(db)), + .with_annotated_type(Type::none(db, env)), Parameter::positional_only(Some(Name::new_static("owner"))) - .with_annotated_type(KnownClass::Type.to_instance(db)), + .with_annotated_type(KnownClass::Type.to_instance(db, env)), ]), Type::unknown(), ), @@ -385,10 +409,11 @@ impl<'db> KnownBoundMethodType<'db> { Parameter::positional_only(Some(Name::new_static("owner"))) .with_annotated_type(UnionType::from_two_elements( db, - KnownClass::Type.to_instance(db), - Type::none(db), + env, + KnownClass::Type.to_instance(db, env), + Type::none(db, env), )) - .with_default_type(Type::none(db)), + .with_default_type(Type::none(db, env)), ]), Type::unknown(), ), @@ -424,25 +449,68 @@ impl<'db> KnownBoundMethodType<'db> { Parameter::positional_only(Some(Name::new_static("prefix"))) .with_annotated_type(UnionType::from_two_elements( db, - KnownClass::Str.to_instance(db), - Type::homogeneous_tuple(db, KnownClass::Str.to_instance(db)), + env, + KnownClass::Str.to_instance(db, env), + Type::homogeneous_tuple( + db, + env, + KnownClass::Str.to_instance(db, env), + ), )), Parameter::positional_only(Some(Name::new_static("start"))) .with_annotated_type(UnionType::from_two_elements( db, - KnownClass::SupportsIndex.to_instance(db), - Type::none(db), + env, + KnownClass::SupportsIndex.to_instance(db, env), + Type::none(db, env), )) - .with_default_type(Type::none(db)), + .with_default_type(Type::none(db, env)), Parameter::positional_only(Some(Name::new_static("end"))) .with_annotated_type(UnionType::from_two_elements( db, - KnownClass::SupportsIndex.to_instance(db), - Type::none(db), + env, + KnownClass::SupportsIndex.to_instance(db, env), + Type::none(db, env), )) - .with_default_type(Type::none(db)), + .with_default_type(Type::none(db, env)), + ]), + KnownClass::Bool.to_instance(db, env), + ))) + } + + KnownBoundMethodType::ConstraintSetLowerBound => { + Either::Right(std::iter::once(Signature::new( + Parameters::standard([ + Parameter::positional_only(Some(Name::new_static("lower_bound"))) + .with_annotated_type(object_type_form()), + Parameter::positional_only(Some(Name::new_static("typevar"))) + .with_annotated_type(object_type_form()), + ]), + KnownClass::ConstraintSet.to_instance(db, env), + ))) + } + + KnownBoundMethodType::ConstraintSetUpperBound => { + Either::Right(std::iter::once(Signature::new( + Parameters::standard([ + Parameter::positional_only(Some(Name::new_static("typevar"))) + .with_annotated_type(object_type_form()), + Parameter::positional_only(Some(Name::new_static("upper_bound"))) + .with_annotated_type(object_type_form()), + ]), + KnownClass::ConstraintSet.to_instance(db, env), + ))) + } + + KnownBoundMethodType::ConstraintSetEquality => { + Either::Right(std::iter::once(Signature::new( + Parameters::standard([ + Parameter::positional_only(Some(Name::new_static("typevar"))) + .with_annotated_type(object_type_form()), + Parameter::positional_only(Some(Name::new_static("value"))) + .with_annotated_type(object_type_form()), ]), - KnownClass::Bool.to_instance(db), + KnownClass::ConstraintSet.to_instance(db, env), ))) } @@ -456,7 +524,7 @@ impl<'db> KnownBoundMethodType<'db> { Parameter::positional_only(Some(Name::new_static("upper_bound"))) .with_annotated_type(object_type_form()), ]), - KnownClass::ConstraintSet.to_instance(db), + KnownClass::ConstraintSet.to_instance(db, env), ))) } @@ -464,7 +532,7 @@ impl<'db> KnownBoundMethodType<'db> { | KnownBoundMethodType::ConstraintSetNever => { Either::Right(std::iter::once(Signature::new( Parameters::empty(), - KnownClass::ConstraintSet.to_instance(db), + KnownClass::ConstraintSet.to_instance(db, env), ))) } @@ -476,7 +544,7 @@ impl<'db> KnownBoundMethodType<'db> { Parameter::positional_only(Some(Name::new_static("of"))) .with_annotated_type(object_type_form()), ]), - KnownClass::ConstraintSet.to_instance(db), + KnownClass::ConstraintSet.to_instance(db, env), ))) } @@ -485,21 +553,22 @@ impl<'db> KnownBoundMethodType<'db> { Parameters::standard([Parameter::positional_only(Some(Name::new_static( "other", ))) - .with_annotated_type(KnownClass::ConstraintSet.to_instance(db))]), - KnownClass::ConstraintSet.to_instance(db), + .with_annotated_type(KnownClass::ConstraintSet.to_instance(db, env))]), + KnownClass::ConstraintSet.to_instance(db, env), ))) } - KnownBoundMethodType::ConstraintSetForAll(_) => { + KnownBoundMethodType::ConstraintSetExists(_) + | KnownBoundMethodType::ConstraintSetForAll(_) => { Either::Right(std::iter::once(Signature::new( Parameters::standard([Parameter::positional_only(Some(Name::new_static( "typevars", ))) .with_annotated_type(TypeFormType::from_type_expression( db, - Type::homogeneous_tuple(db, Type::object()), + Type::homogeneous_tuple(db, env, Type::object()), ))]), - KnownClass::ConstraintSet.to_instance(db), + KnownClass::ConstraintSet.to_instance(db, env), ))) } @@ -508,14 +577,15 @@ impl<'db> KnownBoundMethodType<'db> { Parameters::standard([Parameter::keyword_only(Name::new_static("inferable")) .with_annotated_type(UnionType::from_two_elements( db, + env, TypeFormType::from_type_expression( db, - Type::homogeneous_tuple(db, Type::object()), + Type::homogeneous_tuple(db, env, Type::object()), ), - Type::none(db), + Type::none(db, env), )) - .with_default_type(Type::none(db))]), - KnownClass::Bool.to_instance(db), + .with_default_type(Type::none(db, env))]), + KnownClass::Bool.to_instance(db, env), ))) } @@ -527,17 +597,19 @@ impl<'db> KnownBoundMethodType<'db> { Parameter::keyword_only(Name::new_static("inferable")).with_annotated_type( TypeFormType::from_type_expression( db, - Type::homogeneous_tuple(db, Type::object()), + Type::homogeneous_tuple(db, env, Type::object()), ), ), ]), UnionType::from_two_elements( db, + env, Type::homogeneous_tuple( db, - KnownClass::ConstraintSetSolution.to_instance(db), + env, + KnownClass::ConstraintSetSolution.to_instance(db, env), ), - Type::none(db), + Type::none(db, env), ), ))) } @@ -547,15 +619,17 @@ impl<'db> KnownBoundMethodType<'db> { Parameters::standard([Parameter::keyword_only(Name::new_static("inferable")) .with_annotated_type(TypeFormType::from_type_expression( db, - Type::homogeneous_tuple(db, Type::object()), + Type::homogeneous_tuple(db, env, Type::object()), ))]), UnionType::from_two_elements( db, + env, Type::homogeneous_tuple( db, - KnownClass::ConstraintSetSolution.to_instance(db), + env, + KnownClass::ConstraintSetSolution.to_instance(db, env), ), - Type::none(db), + Type::none(db, env), ), ))) } @@ -563,7 +637,7 @@ impl<'db> KnownBoundMethodType<'db> { KnownBoundMethodType::ConstraintSetWithDetailedDisplay(_) => { Either::Right(std::iter::once(Signature::new( Parameters::empty(), - KnownClass::ConstraintSet.to_instance(db), + KnownClass::ConstraintSet.to_instance(db, env), ))) } } @@ -606,6 +680,18 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } ( + KnownBoundMethodType::ConstraintSetLowerBound, + KnownBoundMethodType::ConstraintSetLowerBound, + ) + | ( + KnownBoundMethodType::ConstraintSetUpperBound, + KnownBoundMethodType::ConstraintSetUpperBound, + ) + | ( + KnownBoundMethodType::ConstraintSetEquality, + KnownBoundMethodType::ConstraintSetEquality, + ) + | ( KnownBoundMethodType::ConstraintSetRange, KnownBoundMethodType::ConstraintSetRange, ) @@ -625,6 +711,10 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { KnownBoundMethodType::ConstraintSetSatisfies(_), KnownBoundMethodType::ConstraintSetSatisfies(_), ) + | ( + KnownBoundMethodType::ConstraintSetExists(_), + KnownBoundMethodType::ConstraintSetExists(_), + ) | ( KnownBoundMethodType::ConstraintSetForAll(_), KnownBoundMethodType::ConstraintSetForAll(_), @@ -653,11 +743,15 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { | KnownBoundMethodType::PropertyDunderSet(_) | KnownBoundMethodType::PropertyDunderDelete(_) | KnownBoundMethodType::StrStartswith(_) + | KnownBoundMethodType::ConstraintSetLowerBound + | KnownBoundMethodType::ConstraintSetUpperBound + | KnownBoundMethodType::ConstraintSetEquality | KnownBoundMethodType::ConstraintSetRange | KnownBoundMethodType::ConstraintSetAlways | KnownBoundMethodType::ConstraintSetNever | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) | KnownBoundMethodType::ConstraintSetSatisfies(_) + | KnownBoundMethodType::ConstraintSetExists(_) | KnownBoundMethodType::ConstraintSetForAll(_) | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) | KnownBoundMethodType::ConstraintSetSolutionsFor(_) @@ -669,11 +763,15 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { | KnownBoundMethodType::PropertyDunderSet(_) | KnownBoundMethodType::PropertyDunderDelete(_) | KnownBoundMethodType::StrStartswith(_) + | KnownBoundMethodType::ConstraintSetLowerBound + | KnownBoundMethodType::ConstraintSetUpperBound + | KnownBoundMethodType::ConstraintSetEquality | KnownBoundMethodType::ConstraintSetRange | KnownBoundMethodType::ConstraintSetAlways | KnownBoundMethodType::ConstraintSetNever | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) | KnownBoundMethodType::ConstraintSetSatisfies(_) + | KnownBoundMethodType::ConstraintSetExists(_) | KnownBoundMethodType::ConstraintSetForAll(_) | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) | KnownBoundMethodType::ConstraintSetSolutionsFor(_) @@ -698,7 +796,11 @@ pub enum WrapperDescriptorKind { } impl WrapperDescriptorKind { - pub(super) fn signatures(self, db: &dyn Db) -> impl Iterator> { + pub(super) fn signatures<'db>( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> impl Iterator> { /// Similar to what we do in [`KnownBoundMethod::signatures`], /// 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` @@ -707,10 +809,14 @@ impl WrapperDescriptorKind { /// TODO: Consider merging these synthesized signatures with the ones in /// [`KnownBoundMethod::signatures`], since that one is just this signature /// with the `self` parameters removed. - fn dunder_get_signatures(db: &dyn Db, class: KnownClass) -> [Signature<'_>; 2] { - let type_instance = KnownClass::Type.to_instance(db); - let none = Type::none(db); - let descriptor = class.to_instance(db); + fn dunder_get_signatures<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + class: KnownClass, + ) -> [Signature<'db>; 2] { + let type_instance = KnownClass::Type.to_instance(db, env); + let none = Type::none(db, env); + let descriptor = class.to_instance(db, env); [ Signature::new( Parameters::standard([ @@ -732,6 +838,7 @@ impl WrapperDescriptorKind { Parameter::positional_only(Some(Name::new_static("owner"))) .with_annotated_type(UnionType::from_two_elements( db, + env, type_instance, none, )) @@ -744,17 +851,17 @@ impl WrapperDescriptorKind { match self { WrapperDescriptorKind::FunctionTypeDunderGet => { - Either::Left(dunder_get_signatures(db, KnownClass::FunctionType).into_iter()) + Either::Left(dunder_get_signatures(db, env, KnownClass::FunctionType).into_iter()) } WrapperDescriptorKind::PropertyDunderGet => { - Either::Left(dunder_get_signatures(db, KnownClass::Property).into_iter()) + Either::Left(dunder_get_signatures(db, env, KnownClass::Property).into_iter()) } WrapperDescriptorKind::PropertyDunderSet => { let object = Type::object(); Either::Right(std::iter::once(Signature::new( Parameters::standard([ Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(KnownClass::Property.to_instance(db)), + .with_annotated_type(KnownClass::Property.to_instance(db, env)), Parameter::positional_only(Some(Name::new_static("instance"))) .with_annotated_type(object), Parameter::positional_only(Some(Name::new_static("value"))) @@ -767,7 +874,7 @@ impl WrapperDescriptorKind { Either::Right(std::iter::once(Signature::new( Parameters::standard([ Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(KnownClass::Property.to_instance(db)), + .with_annotated_type(KnownClass::Property.to_instance(db, env)), Parameter::positional_only(Some(Name::new_static("instance"))) .with_annotated_type(Type::object()), ]), diff --git a/crates/ty_python_semantic/src/types/mro.rs b/crates/ty_python_semantic/src/types/mro.rs index 659dff017b..69d7fa00b9 100644 --- a/crates/ty_python_semantic/src/types/mro.rs +++ b/crates/ty_python_semantic/src/types/mro.rs @@ -1,10 +1,11 @@ +use crate::Db; +use crate::ProgramEnvironment; use std::collections::VecDeque; use std::ops::Deref; use indexmap::IndexMap; use rustc_hash::{FxBuildHasher, FxHashSet}; -use crate::Db; use crate::types::class::{DynamicClassLiteral, DynamicEnumLiteral}; use crate::types::class_base::ClassBase; use crate::types::generics::Specialization; @@ -87,6 +88,7 @@ impl<'db> Mro<'db> { resolved_bases.push(ClassBase::Generic); } + let env = &ProgramEnvironment::from_scope(class_literal.body_scope(db)); let class = class_literal.apply_optional_specialization(db, specialization); let original_bases = class_literal.explicit_bases(db); @@ -118,10 +120,13 @@ impl<'db> Mro<'db> { Ok(Self::from([ ClassBase::Class(class), ClassBase::Generic, - ClassBase::object(db), + ClassBase::object(db, env), ])) } else { - Ok(Self::from([ClassBase::Class(class), ClassBase::object(db)])) + Ok(Self::from([ + ClassBase::Class(class), + ClassBase::object(db, env), + ])) } } @@ -143,6 +148,7 @@ impl<'db> Mro<'db> { { ClassBase::try_from_explicit_base( db, + env, *single_base, Some(ClassLiteral::Static(class_literal)), ) @@ -158,12 +164,12 @@ impl<'db> Mro<'db> { Err(StaticMroErrorKind::InheritanceCycle) } else { Ok(std::iter::once(ClassBase::Class(class)) - .chain(single_base.mro(db, specialization)) + .chain(single_base.mro(db, env, specialization)) .collect()) } }, ) - .map_err(|err| err.into_mro_error(db, class)) + .map_err(|err| err.into_mro_error(db, env, class)) } // The class has multiple explicit bases. @@ -189,6 +195,7 @@ impl<'db> Mro<'db> { } else { match ClassBase::try_from_explicit_base( db, + env, *base, Some(ClassLiteral::Static(class_literal)), ) { @@ -201,7 +208,7 @@ impl<'db> Mro<'db> { if !invalid_bases.is_empty() { return Err( StaticMroErrorKind::InvalidBases(invalid_bases.into_boxed_slice()) - .into_mro_error(db, class), + .into_mro_error(db, env, class), ); } @@ -214,9 +221,11 @@ impl<'db> Mro<'db> { let mut seqs = vec![VecDeque::from([ClassBase::Class(class)])]; for base in &resolved_bases { if base.has_cyclic_mro(db) { - return Err(StaticMroErrorKind::InheritanceCycle.into_mro_error(db, class)); + return Err( + StaticMroErrorKind::InheritanceCycle.into_mro_error(db, env, class) + ); } - seqs.push(base.mro(db, specialization).collect()); + seqs.push(base.mro(db, env, specialization).collect()); } seqs.push( resolved_bases @@ -243,7 +252,7 @@ impl<'db> Mro<'db> { }) { return Err(StaticMroErrorKind::Pep695ClassWithGenericInheritance - .into_mro_error(db, class)); + .into_mro_error(db, env, class)); } let mut duplicate_dynamic_bases = false; @@ -264,6 +273,7 @@ impl<'db> Mro<'db> { for (index, base) in original_bases.iter().enumerate() { let Some(base) = ClassBase::try_from_explicit_base( db, + env, *base, Some(ClassLiteral::Static(class_literal)), ) else { @@ -306,33 +316,38 @@ impl<'db> Mro<'db> { if duplicate_bases.is_empty() { if duplicate_dynamic_bases { - Ok(Mro::from_error(db, class)) + Ok(Mro::from_error(db, env, class)) } else { Err(StaticMroErrorKind::UnresolvableMro { bases_list: original_bases.iter().copied().collect(), generic_index: check_generic_reorder_fixes_mro( db, + env, resolved_bases.as_slice(), original_bases, ), } - .into_mro_error(db, class)) + .into_mro_error(db, env, class)) } } else { Err( StaticMroErrorKind::DuplicateBases(duplicate_bases.into_boxed_slice()) - .into_mro_error(db, class), + .into_mro_error(db, env, class), ) } } } } - pub(super) fn from_error(db: &'db dyn Db, class: ClassType<'db>) -> Self { + pub(super) fn from_error( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + class: ClassType<'db>, + ) -> Self { Self::from([ ClassBase::Class(class), ClassBase::unknown(), - ClassBase::object(db), + ClassBase::object(db, env), ]) } @@ -343,6 +358,7 @@ impl<'db> Mro<'db> { db: &'db dyn Db, dynamic: DynamicClassLiteral<'db>, ) -> Result> { + let env = &ProgramEnvironment::from_scope(dynamic.scope(db)); let original_bases = dynamic.explicit_bases(db); // Convert Types to ClassBases, tracking any that fail conversion. @@ -350,7 +366,7 @@ impl<'db> Mro<'db> { let mut invalid_bases = Vec::new(); for (i, base_type) in original_bases.iter().enumerate() { - match ClassBase::try_from_explicit_base(db, *base_type, None) { + match ClassBase::try_from_explicit_base(db, env, *base_type, None) { Some(class_base) => resolved_bases.push(class_base), None => invalid_bases.push((i, *base_type)), } @@ -360,7 +376,7 @@ impl<'db> Mro<'db> { if !invalid_bases.is_empty() { return Err( DynamicMroErrorKind::InvalidBases(invalid_bases.into_boxed_slice()) - .into_error(db, dynamic), + .into_error(db, env, dynamic), ); } @@ -373,16 +389,16 @@ impl<'db> Mro<'db> { // Handle empty bases case: MRO is just [self, object]. if resolved_bases.is_empty() { - return Ok(Self::from([self_base, ClassBase::object(db)])); + return Ok(Self::from([self_base, ClassBase::object(db, env)])); } // Build MRO sequences and check for inheritance cycles. let mut seqs = vec![VecDeque::from([self_base])]; for base in &resolved_bases { if base.has_cyclic_mro(db) { - return Err(DynamicMroErrorKind::InheritanceCycle.into_error(db, dynamic)); + return Err(DynamicMroErrorKind::InheritanceCycle.into_error(db, env, dynamic)); } - seqs.push(base.mro(db, None).collect()); + seqs.push(base.mro(db, env, None).collect()); } seqs.push(resolved_bases.iter().copied().collect()); @@ -412,15 +428,15 @@ impl<'db> Mro<'db> { if !duplicates.is_empty() { return Err( DynamicMroErrorKind::DuplicateBases(duplicates.into_boxed_slice()) - .into_error(db, dynamic), + .into_error(db, env, dynamic), ); } // No duplicate concrete bases. If there are dynamic bases, use fallback MRO. if has_dynamic_bases || has_duplicate_dynamic_bases { - Ok(Self::dynamic_fallback(db, dynamic)) + Ok(Self::dynamic_fallback(db, env, dynamic)) } else { - Err(DynamicMroErrorKind::UnresolvableMro.into_error(db, dynamic)) + Err(DynamicMroErrorKind::UnresolvableMro.into_error(db, env, dynamic)) } } @@ -434,6 +450,7 @@ impl<'db> Mro<'db> { db: &'db dyn Db, dynamic_enum: DynamicEnumLiteral<'db>, ) -> Result> { + let env = &ProgramEnvironment::from_scope(dynamic_enum.scope(db)); let self_base = ClassBase::Class(ClassType::NonGeneric(dynamic_enum.into())); // Convert the functional enum bases (`type=` mixin first, enum base second) @@ -442,7 +459,7 @@ impl<'db> Mro<'db> { let original_bases = dynamic_enum.explicit_bases(db); let mut resolved_bases: Vec> = Vec::with_capacity(original_bases.len()); for base_type in original_bases.iter().copied() { - if let Some(base) = ClassBase::try_from_explicit_base(db, base_type, None) { + if let Some(base) = ClassBase::try_from_explicit_base(db, env, base_type, None) { resolved_bases.push(base); } } @@ -464,7 +481,7 @@ impl<'db> Mro<'db> { let mut seen = FxHashSet::default(); seen.insert(self_base); for base in &resolved_bases { - for item in base.mro(db, None) { + for item in base.mro(db, env, None) { if seen.insert(item) { result.push(item); } @@ -484,7 +501,7 @@ impl<'db> Mro<'db> { fallback_mro: fallback_mro(), }); } - seqs.push(base.mro(db, None).collect()); + seqs.push(base.mro(db, env, None).collect()); } seqs.push(resolved_bases.iter().copied().collect()); @@ -497,7 +514,11 @@ impl<'db> Mro<'db> { /// Compute a fallback MRO for a dynamic class when `of_dynamic_class` fails. /// /// Iterates over base MROs sequentially with deduplication. - pub(super) fn dynamic_fallback(db: &'db dyn Db, dynamic: DynamicClassLiteral<'db>) -> Self { + fn dynamic_fallback( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + dynamic: DynamicClassLiteral<'db>, + ) -> Self { let self_base = ClassBase::Class(ClassType::NonGeneric(dynamic.into())); let mut result = vec![self_base]; let mut seen = FxHashSet::default(); @@ -505,10 +526,10 @@ impl<'db> Mro<'db> { for base_type in dynamic.explicit_bases(db) { // Convert `Type` to `ClassBase`, falling back to `Unknown` if conversion fails. - let base = ClassBase::try_from_explicit_base(db, *base_type, None) + let base = ClassBase::try_from_explicit_base(db, env, *base_type, None) .unwrap_or_else(ClassBase::unknown); - for item in base.mro(db, None) { + for item in base.mro(db, env, None) { if seen.insert(item) { result.push(item); } @@ -600,10 +621,11 @@ impl<'db> MroIterator<'db> { } fn first_element(&self) -> ClassBase<'db> { + let db = self.db; match self.class { - ClassLiteral::Static(literal) => ClassBase::Class( - literal.apply_optional_specialization(self.db, self.specialization), - ), + ClassLiteral::Static(literal) => { + ClassBase::Class(literal.apply_optional_specialization(db, self.specialization)) + } ClassLiteral::Dynamic(literal) => { ClassBase::Class(ClassType::NonGeneric(literal.into())) } @@ -622,13 +644,14 @@ impl<'db> MroIterator<'db> { /// 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) -> &mut std::slice::Iter<'db, ClassBase<'db>> { + let db = self.db; self.subsequent_elements .get_or_insert_with(|| match self.class { ClassLiteral::Static(literal) => { let specialization = self.specialization.map(|specialization| { - specialization.tuple_runtime_element_specialization(self.db) + specialization.tuple_runtime_element_specialization(db) }); - let mut full_mro_iter = match literal.try_mro(self.db, specialization) { + let mut full_mro_iter = match literal.try_mro(db, specialization) { Ok(mro) => mro.iter(), Err(error) => error.fallback_mro().iter(), }; @@ -636,7 +659,7 @@ impl<'db> MroIterator<'db> { full_mro_iter } ClassLiteral::Dynamic(literal) => { - let mut full_mro_iter = match literal.try_mro(self.db) { + let mut full_mro_iter = match literal.try_mro(db) { Ok(mro) => mro.iter(), Err(error) => error.fallback_mro().iter(), }; @@ -644,17 +667,17 @@ impl<'db> MroIterator<'db> { full_mro_iter } ClassLiteral::DynamicNamedTuple(literal) => { - let mut full_mro_iter = literal.mro(self.db).iter(); + let mut full_mro_iter = literal.mro(db).iter(); full_mro_iter.next(); full_mro_iter } ClassLiteral::DynamicTypedDict(literal) => { - let mut full_mro_iter = literal.mro(self.db).iter(); + let mut full_mro_iter = literal.mro(db).iter(); full_mro_iter.next(); full_mro_iter } ClassLiteral::DynamicEnum(literal) => { - let mut full_mro_iter = match literal.try_mro(self.db) { + let mut full_mro_iter = match literal.try_mro(db) { Ok(mro) => mro.iter(), Err(error) => error.fallback_mro().iter(), }; @@ -703,8 +726,12 @@ pub(super) struct StaticMroError<'db> { impl<'db> StaticMroError<'db> { /// Construct an MRO error of kind `InheritanceCycle`. - pub(super) fn cycle(db: &'db dyn Db, class: ClassType<'db>) -> Self { - StaticMroErrorKind::InheritanceCycle.into_mro_error(db, class) + pub(super) fn cycle( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + class: ClassType<'db>, + ) -> Self { + StaticMroErrorKind::InheritanceCycle.into_mro_error(db, env, class) } pub(super) fn is_cycle(&self) -> bool { @@ -718,7 +745,7 @@ impl<'db> StaticMroError<'db> { /// 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> { + fn fallback_mro(&self) -> &Mro<'db> { &self.fallback_mro } } @@ -763,14 +790,15 @@ pub(super) enum StaticMroErrorKind<'db> { } impl<'db> StaticMroErrorKind<'db> { - pub(super) fn into_mro_error( + fn into_mro_error( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: ClassType<'db>, ) -> StaticMroError<'db> { StaticMroError { kind: self, - fallback_mro: Mro::from_error(db, class), + fallback_mro: Mro::from_error(db, env, class), } } } @@ -838,6 +866,7 @@ fn c3_merge(mut sequences: Vec>) -> Option { /// the `Generic[]` base. If not, this function will return `None`. fn check_generic_reorder_fixes_mro<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, resolved_bases: &[ClassBase<'db>], original_bases: &[Type<'db>], ) -> Option { @@ -869,7 +898,7 @@ fn check_generic_reorder_fixes_mro<'db>( if base.has_cyclic_mro(db) { return None; } - seqs.push(base.mro(db, None).collect()); + seqs.push(base.mro(db, env, None).collect()); } seqs.push(reordered); c3_merge(seqs)?; @@ -892,7 +921,7 @@ impl<'db> DynamicMroError<'db> { } /// Return the fallback MRO to use for type inference. - pub(crate) fn fallback_mro(&self) -> &Mro<'db> { + fn fallback_mro(&self) -> &Mro<'db> { &self.fallback_mro } } @@ -922,11 +951,12 @@ impl<'db> DynamicMroErrorKind<'db> { fn into_error( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class_literal: DynamicClassLiteral<'db>, ) -> DynamicMroError<'db> { DynamicMroError { kind: self, - fallback_mro: Mro::dynamic_fallback(db, class_literal), + fallback_mro: Mro::dynamic_fallback(db, env, class_literal), } } } diff --git a/crates/ty_python_semantic/src/types/name_fallback.rs b/crates/ty_python_semantic/src/types/name_fallback.rs index 473354d0cc..1488042855 100644 --- a/crates/ty_python_semantic/src/types/name_fallback.rs +++ b/crates/ty_python_semantic/src/types/name_fallback.rs @@ -36,6 +36,7 @@ use crate::Db; use crate::place::{ builtins_symbol, is_basedpython_implicit_typing_name, module_type_implicit_global_symbol, }; +use crate::types::ProgramEnvironment; /// whether an ordinary lexical lookup owns `name`: a binding or a declaration /// anywhere in the visible scope chain, or a builtin @@ -50,13 +51,14 @@ use crate::place::{ /// nested scope), matching the free-variable walk of a name load pub(crate) fn claimed_by_lexical_scope( db: &dyn Db, + env: &ProgramEnvironment<'_>, file: File, scope: ScopeId<'_>, name: &str, ) -> bool { - let index = semantic_index(db, file); + let index = semantic_index(db, db.program_file(file)); for (ancestor_id, _) in index.visible_ancestor_scopes(scope.file_scope_id(db)) { - let ancestor_scope = ancestor_id.to_scope_id(db, file); + let ancestor_scope = ancestor_id.to_scope_id(db, db.program_file(file)); if place_table(db, ancestor_scope) .symbol_by_name(name) .is_some_and(|symbol| symbol.is_bound() || symbol.is_declared()) @@ -64,7 +66,7 @@ pub(crate) fn claimed_by_lexical_scope( return true; } } - !builtins_symbol(db, name).place.is_undefined() + !builtins_symbol(db, env, name).place.is_undefined() } /// [`claimed_by_lexical_scope`], and additionally every name ty resolves with no @@ -77,14 +79,15 @@ pub(crate) fn claimed_by_lexical_scope( /// lowered it as a receiver member or a lookup path pub(crate) fn claimed_by_name_resolution( db: &dyn Db, + env: &ProgramEnvironment<'_>, file: File, scope: ScopeId<'_>, name: &str, ) -> bool { - claimed_by_lexical_scope(db, file, scope, name) + claimed_by_lexical_scope(db, env, file, scope, name) // states the intent directly rather than leaning on the fact that the // builtins lookup above happens to fall back to `types.ModuleType` too - || !module_type_implicit_global_symbol(db, file, name) + || !module_type_implicit_global_symbol(db, db.program_file(file), name) .place .is_undefined() || is_basedpython_implicit_typing_name(name) @@ -109,19 +112,28 @@ mod tests { let mut db = setup_db(); db.write_file("/src/a.by", "x = 1\n").unwrap(); let file = system_path_to_file(&db, "/src/a.by").unwrap(); - let scope = global_scope(&db, file); + let scope = global_scope(&db, crate::Db::program_file(&db, file)); // a binding and a builtin: both gates own these for name in ["x", "int"] { - assert!(claimed_by_lexical_scope(&db, file, scope, name), "{name}"); - assert!(claimed_by_name_resolution(&db, file, scope, name), "{name}"); + assert!( + claimed_by_lexical_scope(&db, &db.program_environment(), file, scope, name), + "{name}" + ); + assert!( + claimed_by_name_resolution(&db, &db.program_environment(), file, scope, name), + "{name}" + ); } // a name nothing claims: both gates leave it for the fallbacks for name in ["nonesuch", "Red"] { - assert!(!claimed_by_lexical_scope(&db, file, scope, name), "{name}"); assert!( - !claimed_by_name_resolution(&db, file, scope, name), + !claimed_by_lexical_scope(&db, &db.program_environment(), file, scope, name), + "{name}" + ); + assert!( + !claimed_by_name_resolution(&db, &db.program_environment(), file, scope, name), "{name}" ); } @@ -130,14 +142,23 @@ mod tests { // builtins lookup already falls back to `types.ModuleType`, so the wider // gate's own module-global check only ever repeats an answer it has for name in ["__name__", "__spec__", "__debug__", "__file__"] { - assert!(claimed_by_lexical_scope(&db, file, scope, name), "{name}"); + assert!( + claimed_by_lexical_scope(&db, &db.program_environment(), file, scope, name), + "{name}" + ); } // the whole of the difference: the basedpython implicit `typing` names, and // the two implicit names that have no stub to resolve through for name in ["Optional", "Self", "Sequence", "Character", "Some"] { - assert!(!claimed_by_lexical_scope(&db, file, scope, name), "{name}"); - assert!(claimed_by_name_resolution(&db, file, scope, name), "{name}"); + assert!( + !claimed_by_lexical_scope(&db, &db.program_environment(), file, scope, name), + "{name}" + ); + assert!( + claimed_by_name_resolution(&db, &db.program_environment(), file, scope, name), + "{name}" + ); } } } diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 3f33990add..9e64b77fb1 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -1,7 +1,6 @@ use std::borrow::Cow; use std::collections::{BTreeMap, btree_map::Entry as BTreeEntry, hash_map::Entry}; -use crate::Db; use crate::reachability::{narrow_type_by_constraint, type_narrowed_by_previous_patterns}; use crate::subscript::PyIndex; use crate::types::callable::CallableTypes; @@ -11,9 +10,7 @@ use crate::types::narrowing_guards::{GuardRoot, guard_root, narrowed_place, narr use crate::types::signatures::NarrowingGuardKind; use crate::types::special_form::TypeQualifier; use crate::types::tuple::{TupleLength, TupleSpec, TupleSpecBuilder, TupleType, TupleUnpacker}; -use crate::types::typed_dict::{ - TypedDictField, TypedDictFieldBuilder, TypedDictSchema, TypedDictType, -}; +use crate::types::typed_dict::{TypedDictFieldBuilder, TypedDictSchema, TypedDictType}; use crate::types::{ CallableType, ClassBase, ClassLiteral, ClassPatternPositionalSource, ClassType, IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, LiteralValueTypeKind, @@ -24,6 +21,7 @@ use crate::types::{ mapping_pattern_type, pattern_binding_fallthrough_type, sequence_pattern_type_builder, singleton_pattern_type, starred_sequence_pattern_type, typed_dict_matches_class_pattern, }; +use crate::{Db, ProgramEnvironment}; use ty_python_core::expression::Expression; use ty_python_core::frozen::FrozenMap; use ty_python_core::place::{PlaceExpr, PlaceTable, ScopedPlaceId}; @@ -46,6 +44,7 @@ use super::equality::{ ComparisonSoundnessPolicy, equality_exclusion_constraint, equality_truthiness, evaluate_type_equality, evaluate_type_inequality, }; +use super::match_pattern::is_typed_dict_runtime_domain; use super::variance::TypeVarVariance; use itertools::Itertools; use ruff_python_ast as ast; @@ -75,6 +74,7 @@ use self::containment::{elements_of, narrow_string_membership}; /// constraint is applied to that symbol, so we'd just return `(None, None)`. pub(crate) fn infer_narrowing_constraints<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, predicate: Predicate<'db>, place: ScopedPlaceId, ) -> ( @@ -118,7 +118,7 @@ pub(crate) fn infer_narrowing_constraints<'db>( .filter(|(target, _)| *target == place) .map(|(_, ty)| *ty) .reduce(|left, right| { - IntersectionBuilder::new(db) + IntersectionBuilder::new(db, env) .add_positive(left) .add_positive(right) .build() @@ -130,6 +130,7 @@ pub(crate) fn infer_narrowing_constraints<'db>( } PredicateNode::IsNonTerminalCall(_) | PredicateNode::IsNonEmptyIterable(_) + | PredicateNode::OrPatternAlternative(_) | PredicateNode::StarImportPlaceholder(_) => (None, None), }; @@ -168,6 +169,7 @@ fn asserts_guard_targets<'db>( callable: Expression<'db>, call_expr: Expression<'db>, ) -> Box<[(ScopedPlaceId, Type<'db>)]> { + let env = &ProgramEnvironment::from_scope(callable.scope(db)); let scope = callable.scope(db); // `asserts` is basedpython surface syntax, so this lookup costs other files nothing if !scope.file(db).source_type(db).is_basedpython() { @@ -179,7 +181,7 @@ fn asserts_guard_targets<'db>( return Box::default(); } let Some(callable_ty) = callable_ty - .try_upcast_to_callable(db) + .try_upcast_to_callable(db, env) .and_then(CallableTypes::exactly_one) else { return Box::default(); @@ -191,7 +193,7 @@ fn asserts_guard_targets<'db>( return Box::default(); } - let module = parsed_module(db, call_expr.file(db)).load(db); + let module = parsed_module(db, call_expr.program_file(db).python_file(db)).load(db); let call = match call_expr.node_ref(db).node(&module) { ast::Expr::Await(await_expr) => await_expr.value.as_call_expr(), node => node.as_call_expr(), @@ -207,16 +209,18 @@ fn asserts_guard_targets<'db>( let narrowed_to = match guard.kind { NarrowingGuardKind::Asserts { is_positive } => { if is_positive { - Type::AlwaysFalsy.negate(db) + Type::AlwaysFalsy.negate(db, env) } else { - Type::AlwaysTruthy.negate(db) + Type::AlwaysTruthy.negate(db, env) } } // a guard type that still mentions a type variable isn't resolved against the // call's specialization here, so it says nothing about the argument - NarrowingGuardKind::AssertsType { ty, .. } if ty.has_typevar(db) => return None, + NarrowingGuardKind::AssertsType { ty, .. } if ty.has_typevar(db, env) => { + return None; + } NarrowingGuardKind::AssertsType { is_positive, ty } => { - ty.negate_if(db, !is_positive) + ty.negate_if(db, env, !is_positive) } NarrowingGuardKind::Predicate => return None, }; @@ -264,8 +268,12 @@ 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() + let program_file = pattern.program_file(db); + let python_file = program_file.python_file(db); + let env = ProgramEnvironment::from_file(program_file); + let module = parsed_module(db, python_file).load(db); + NarrowingConstraintsBuilder::new(db, &env, &module, PredicateNode::Pattern(pattern), true) + .finish() } #[salsa::tracked( @@ -277,11 +285,14 @@ fn all_narrowing_constraints_for_expression<'db>( db: &'db dyn Db, expression: Expression<'db>, ) -> ExpressionNarrowingConstraints<'db> { - let module = parsed_module(db, expression.file(db)).load(db); + let program_file = expression.program_file(db); + let python_file = program_file.python_file(db); + let env = ProgramEnvironment::from_file(program_file); + let module = parsed_module(db, python_file).load(db); let predicate = PredicateNode::Expression(expression); ExpressionNarrowingConstraints { - positive: NarrowingConstraintsBuilder::new(db, &module, predicate, true).finish(), - negative: NarrowingConstraintsBuilder::new(db, &module, predicate, false).finish(), + positive: NarrowingConstraintsBuilder::new(db, &env, &module, predicate, true).finish(), + negative: NarrowingConstraintsBuilder::new(db, &env, &module, predicate, false).finish(), } } @@ -290,8 +301,12 @@ 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() + let program_file = pattern.program_file(db); + let python_file = program_file.python_file(db); + let env = ProgramEnvironment::from_file(program_file); + let module = parsed_module(db, python_file).load(db); + NarrowingConstraintsBuilder::new(db, &env, &module, PredicateNode::Pattern(pattern), false) + .finish() } #[salsa::tracked(returns(as_ref), heap_size=ruff_memory_usage::heap_size)] @@ -300,9 +315,13 @@ fn all_narrowing_constraints_for_subject_element_pattern<'db>( pattern: PatternPredicate<'db>, target: ExpressionNodeKey, ) -> Option> { - let module = parsed_module(db, pattern.file(db)).load(db); + let program_file = pattern.program_file(db); + let python_file = program_file.python_file(db); + let env = ProgramEnvironment::from_file(program_file); + let module = parsed_module(db, python_file).load(db); NarrowingConstraintsBuilder::new( db, + &env, &module, PredicateNode::SubjectElementPattern(SubjectElementPatternPredicate { pattern, target }), true, @@ -343,13 +362,19 @@ impl<'db> PatternSuccessTypes<'db> { } } - fn cycle_normalized(mut self, db: &'db dyn Db, previous: &Self, cycle: &salsa::Cycle) -> Self { + fn cycle_normalized( + mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + previous: &Self, + cycle: &salsa::Cycle, + ) -> Self { for (place, ty) in &mut self.bindings { - *ty = ty.cycle_normalized(db, previous.binding_type(*place), cycle); + *ty = ty.cycle_normalized(db, env, previous.binding_type(*place), cycle); } self.missing_binding_ty = self.missing_binding_ty - .cycle_normalized(db, previous.missing_binding_ty, cycle); + .cycle_normalized(db, env, previous.missing_binding_ty, cycle); self } } @@ -439,8 +464,8 @@ impl<'db> PatternBindingTypes<'db> { } /// Return the union of all contributions to this binding. - fn ty(&self, db: &'db dyn Db) -> Type<'db> { - UnionType::from_elements(db, self.contributions.iter().map(|binding| binding.ty)) + fn ty(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + UnionType::from_elements(db, env, self.contributions.iter().map(|binding| binding.ty)) } /// Mark every contribution as referring to a value extracted from the current subject. @@ -456,9 +481,10 @@ impl<'db> PatternBindingTypes<'db> { } /// Return the union of the contributions that alias the current subject. - fn subject_ty(&self, db: &'db dyn Db) -> Type<'db> { + fn subject_ty(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { UnionType::from_elements( db, + env, self.contributions .iter() .filter(|binding| binding.aliases_subject) @@ -509,6 +535,7 @@ enum PatternValueSource { /// preserve type variables. struct PatternSuccessAnalyzer<'db> { db: &'db dyn Db, + env: ProgramEnvironment<'db>, scope: ScopeId<'db>, } @@ -544,8 +571,9 @@ pub(crate) fn pattern_subject_type<'db>( #[salsa::tracked( returns(ref), cycle_initial=|_, id, _| PatternSuccessTypes::cycle_initial(Type::divergent(id)), - cycle_fn=|db, cycle, previous: &PatternSuccessTypes<'db>, result: PatternSuccessTypes<'db>, _| { - result.cycle_normalized(db, previous, cycle) + cycle_fn=|db: &'db dyn Db, cycle, previous: &PatternSuccessTypes<'db>, result: PatternSuccessTypes<'db>, pattern: PatternPredicate<'db>| { + let env = ProgramEnvironment::from_scope(pattern.scope(db)); + result.cycle_normalized(db, &env, previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] @@ -553,7 +581,9 @@ pub(crate) fn pattern_success_types<'db>( db: &'db dyn Db, pattern: PatternPredicate<'db>, ) -> PatternSuccessTypes<'db> { - let incoming_subject_ty = pattern_subject_type(db, pattern.subject(db)); + let subject = pattern.subject(db); + let env = ProgramEnvironment::from_scope(pattern.scope(db)); + let incoming_subject_ty = pattern_subject_type(db, subject); let incoming_subject_ty = type_narrowed_by_previous_patterns(db, pattern, incoming_subject_ty); let analyzer = PatternSuccessAnalyzer::new(db, pattern.scope(db)); let result = analyzer.analyze_successful_pattern(pattern.kind(db), incoming_subject_ty); @@ -561,7 +591,7 @@ pub(crate) fn pattern_success_types<'db>( bindings: result .bindings .into_iter() - .map(|(place, binding)| (place, binding.ty(db))) + .map(|(place, binding)| (place, binding.ty(db, &env))) .collect(), missing_binding_ty: if result.matched_subject_ty.is_never() { Type::Never @@ -591,31 +621,56 @@ impl ClassInfoConstraintFunction { fn generate_constraint<'db>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, classinfo: Type<'db>, is_positive: bool, + use_generic_filtering: bool, ) -> Option> { - let constraint_from_class_literal = |class: ClassLiteral<'db>| match self { - ClassInfoConstraintFunction::IsInstance => { - Type::instance(db, class.top_materialization(db)) - } - ClassInfoConstraintFunction::IsSubclass => { - SubclassOfType::from(db, class.top_materialization(db)) + let constraint_from_class_literal = |class: ClassLiteral<'db>| { + let specialization = if use_generic_filtering { + class.unknown_specialization(db) + } else { + // A negative result excludes every specialization of the class. + class.top_materialization(db) + }; + + match self { + ClassInfoConstraintFunction::IsInstance => Type::instance(db, env, specialization), + ClassInfoConstraintFunction::IsSubclass => { + SubclassOfType::from(db, env, specialization) + } } }; match classinfo { - Type::TypeAlias(alias) => { - self.generate_constraint(db, alias.value_type(db), is_positive) - } - Type::Overlapping(overlapping) => { - self.generate_constraint(db, overlapping.value_type(db), is_positive) - } - Type::Restricted(restricted) => { - self.generate_constraint(db, restricted.value_type(db), is_positive) - } - Type::Deferred(deferred) => { - self.generate_constraint(db, deferred.reduced(db), is_positive) - } + Type::TypeAlias(alias) => self.generate_constraint( + db, + env, + alias.value_type(db), + is_positive, + use_generic_filtering, + ), + Type::Overlapping(overlapping) => self.generate_constraint( + db, + env, + overlapping.value_type(db, env), + is_positive, + use_generic_filtering, + ), + Type::Restricted(restricted) => self.generate_constraint( + db, + env, + restricted.value_type(db), + is_positive, + use_generic_filtering, + ), + Type::Deferred(deferred) => self.generate_constraint( + db, + env, + deferred.reduced(db, env), + is_positive, + use_generic_filtering, + ), Type::ClassLiteral(class_literal) => Some(constraint_from_class_literal(class_literal)), Type::SubclassOf(subclass_of_ty) => { // We can't narrow negatively from a `SubclassOf` type. `if !isinstance(x, y)` @@ -655,7 +710,7 @@ impl ClassInfoConstraintFunction { Type::Dynamic(_) | Type::Divergent(_) => Some(classinfo), Type::Intersection(intersection) => { if intersection.negative(db).is_empty() { - let mut builder = IntersectionBuilder::new(db); + let mut builder = IntersectionBuilder::new(db, env); let mut any_member = false; for element in intersection.positive(db) { // A member that yields no constraint (e.g. a parametrized @@ -663,8 +718,14 @@ impl ClassInfoConstraintFunction { // target) should be SKIPPED, not abort narrowing on the // whole intersection. Narrowing on the remaining members // is still sound. - if let Some(c) = self.generate_constraint(db, *element, is_positive) { - builder = builder.add_positive(c); + if let Some(c) = self.generate_constraint( + db, + env, + *element, + is_positive, + use_generic_filtering, + ) { + builder.add_positive_in_place(c); any_member = true; } } @@ -678,22 +739,31 @@ impl ClassInfoConstraintFunction { None } } - Type::Union(union) => union.try_map(db, |element| { - self.generate_constraint(db, *element, is_positive) + Type::Union(union) => union.try_map(db, env, |element| { + self.generate_constraint(db, env, *element, is_positive, use_generic_filtering) }), // Any materialization could be the actual class-info argument, so narrowing must // allow for all of them: the union face. - Type::UnsafeUnion(unsafe_union) => { - self.generate_constraint(db, unsafe_union.to_union(db), is_positive) - } + Type::UnsafeUnion(unsafe_union) => self.generate_constraint( + db, + env, + unsafe_union.to_union(db, env), + is_positive, + use_generic_filtering, + ), Type::TypeVar(bound_typevar) => { - match bound_typevar.typevar(db).bound_or_constraints(db)? { + match bound_typevar.typevar(db).bound_or_constraints(db, env)? { TypeVarBoundOrConstraints::UpperBound(bound) => { - self.generate_constraint(db, bound, is_positive) - } - TypeVarBoundOrConstraints::Constraints(constraints) => { - self.generate_constraint(db, constraints.as_type(db), is_positive) + self.generate_constraint(db, env, bound, is_positive, use_generic_filtering) } + TypeVarBoundOrConstraints::Constraints(constraints) => self + .generate_constraint( + db, + env, + constraints.as_type(db, env), + is_positive, + use_generic_filtering, + ), } } @@ -701,56 +771,88 @@ impl ClassInfoConstraintFunction { // e.g. `isinstance(x, list[int])` fails at runtime. Type::GenericAlias(_) => None, - Type::NominalInstance(nominal) if let Some(tuple) = nominal.tuple_spec(db) => { + Type::NominalInstance(nominal) if let Some(tuple) = nominal.tuple_spec(db, env) => { UnionType::try_from_elements( db, - tuple - .iter_element_types(db) - .map(|element| self.generate_constraint(db, element, is_positive)), + env, + tuple.iter_element_types(db).map(|element| { + self.generate_constraint( + db, + env, + element, + is_positive, + use_generic_filtering, + ) + }), ) } Type::KnownInstance(KnownInstanceType::UnionType(instance)) => { UnionType::try_from_elements( db, - instance.value_expression_types(db).ok()?.map(|element| { - // A special case is made for `None` at runtime - // (it's implicitly converted to `NoneType` in `int | None`) - // which means that `isinstance(x, int | None)` works even though - // `None` is not a class literal. - if element.is_none(db) { - self.generate_constraint( - db, - KnownClass::NoneType.to_class_literal(db), - is_positive, - ) - } else { - self.generate_constraint(db, element, is_positive) - } - }), + env, + instance + .value_expression_types(db, env) + .ok()? + .map(|element| { + // A special case is made for `None` at runtime + // (it's implicitly converted to `NoneType` in `int | None`) + // which means that `isinstance(x, int | None)` works even though + // `None` is not a class literal. + if element.is_none(db) { + self.generate_constraint( + db, + env, + KnownClass::NoneType.to_class_literal(db, env), + is_positive, + use_generic_filtering, + ) + } else { + self.generate_constraint( + db, + env, + element, + is_positive, + use_generic_filtering, + ) + } + }), ) } Type::SpecialForm(form) => match form { SpecialFormType::LegacyStdlibAlias(alias) => self.generate_constraint( db, - alias.aliased_class().to_class_literal(db), + env, + alias.aliased_class().to_class_literal(db, env), is_positive, + use_generic_filtering, ), SpecialFormType::Tuple => self.generate_constraint( db, - KnownClass::Tuple.to_class_literal(db), + env, + KnownClass::Tuple.to_class_literal(db, env), is_positive, + use_generic_filtering, + ), + SpecialFormType::Type => self.generate_constraint( + db, + env, + KnownClass::Type.to_class_literal(db, env), + is_positive, + use_generic_filtering, ), - SpecialFormType::Type => { - self.generate_constraint(db, KnownClass::Type.to_class_literal(db), is_positive) - } - // We don't have a good meta-type for `Callable`s right now, // so only apply `isinstance()` narrowing, not `issubclass()` - SpecialFormType::TypingCallable | SpecialFormType::CollectionsAbcCallable => (self - == ClassInfoConstraintFunction::IsInstance) - .then(|| Type::Callable(CallableType::unknown(db)).top_materialization(db)), + SpecialFormType::TypingCallable | SpecialFormType::CollectionsAbcCallable => { + (self == ClassInfoConstraintFunction::IsInstance).then(|| { + if use_generic_filtering { + Type::Callable(CallableType::unknown(db)) + } else { + callable_pattern_type(db, env) + } + }) + } // `InitVar` is a class at runtime, so can be used in `isinstance()`, // but we can't represent internally the type that we should narrow to after an `isinstance()` check, @@ -787,20 +889,50 @@ impl ClassInfoConstraintFunction { } } +#[derive(Hash, PartialEq, Debug, Eq, Clone, Copy, get_size2::GetSize, salsa::SalsaValue)] +enum NarrowingOperation<'db> { + /// Narrow the subject by intersecting it directly with this type. + Intersection(Type<'db>), + /// Narrow to this generic type while preserving type arguments already known about the subject. + GenericFiltering(Type<'db>), +} + +impl<'db> NarrowingOperation<'db> { + const fn ty(self) -> Type<'db> { + match self { + Self::Intersection(ty) | Self::GenericFiltering(ty) => ty, + } + } +} + #[derive(Hash, PartialEq, Debug, Eq, Clone, get_size2::GetSize, salsa::SalsaValue)] struct Conjunctions<'db> { - conjuncts: SmallVec<[Type<'db>; 2]>, + conjuncts: SmallVec<[NarrowingOperation<'db>; 2]>, } impl<'db> Conjunctions<'db> { fn singleton(ty: Type<'db>) -> Self { Self { - conjuncts: smallvec![ty], + conjuncts: smallvec![NarrowingOperation::Intersection(ty)], + } + } + + fn generic_filtering(ty: Type<'db>) -> Self { + Self { + conjuncts: smallvec![NarrowingOperation::GenericFiltering(ty)], } } fn and_with(mut self, other: Self) -> Self { - if self.conjuncts.iter().any(Type::is_never) || other.conjuncts.iter().any(Type::is_never) { + if self + .conjuncts + .iter() + .any(|conjunct| conjunct.ty().is_never()) + || other + .conjuncts + .iter() + .any(|conjunct| conjunct.ty().is_never()) + { return Self::singleton(Type::Never); } @@ -812,20 +944,293 @@ impl<'db> Conjunctions<'db> { self } - fn evaluate_constraint_type(self, db: &'db dyn Db) -> Type<'db> { + fn evaluate_constraint_type(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { if self.conjuncts.len() == 1 { - return self.conjuncts[0]; + return self.conjuncts[0].ty(); } // Collapse shared union arms before distributing the next constraint over them. self.conjuncts .into_iter() - .fold(Type::object(), |accumulated, conjunct| { - IntersectionType::from_two_elements(db, accumulated, conjunct) + .fold(Type::object(), |accumulated, conjunct| match conjunct { + NarrowingOperation::Intersection(ty) => { + IntersectionType::from_two_elements(db, env, accumulated, ty) + } + NarrowingOperation::GenericFiltering(ty) => { + filter_generic_narrowing_constraint(db, env, accumulated, ty) + } }) } } +/// Preserve known generic arguments when narrowing a specialized base to one of its subclasses. +/// +/// For example, filtering `Sequence[int]` with `list[Unknown]` first infers `list[int]` from +/// the target class's specialized `Sequence` base. Unrelated union arms and intersection elements +/// are still intersected with the original unknown-specialized target. +fn filter_generic_narrowing_constraint<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + subject: Type<'db>, + target: Type<'db>, +) -> Type<'db> { + match (subject, target) { + (Type::Union(union), target) => union.map(db, env, |element| { + filter_generic_narrowing_constraint(db, env, *element, target) + }), + (subject, Type::Union(union)) => union.map(db, env, |element| { + filter_generic_narrowing_constraint(db, env, subject, *element) + }), + (subject @ Type::Callable(_), Type::Callable(_)) => subject, + (subject, target) + if is_typed_dict_runtime_domain(db, env, subject) + && target.nominal_class(db, env).is_some_and(|class| { + !class.is_protocol(db) + && typed_dict_matches_class_pattern(db, env, class.class_literal(db)) + }) => + { + // A TypedDict is a dictionary at runtime, but intersecting it with the target would + // expose dict's unrestricted mutations and discard its required-key guarantees. + subject + } + (Type::Intersection(intersection), target) => { + let specialized_target = + specialize_narrowing_target_from_intersection(db, env, intersection, target) + .or_else(|| { + intersection.positive(db).iter().find_map(|element| { + specialize_narrowing_target(db, env, *element, target) + }) + }) + .unwrap_or(target); + IntersectionType::from_two_elements(db, env, subject, specialized_target) + } + (subject, target) => { + let specialized_target = + specialize_narrowing_target(db, env, subject, target).unwrap_or(target); + IntersectionType::from_two_elements(db, env, subject, specialized_target) + } + } +} + +/// Combine the constraints contributed by multiple specialized bases in an intersection. +/// +/// For example, if `Both[L, R]` inherits from `Left[L]` and `Right[R]`, narrowing +/// `Left[int] & Right[str]` to `Both` must infer `Both[int, str]`. +fn specialize_narrowing_target_from_intersection<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + intersection: IntersectionType<'db>, + target: Type<'db>, +) -> Option> { + let target_class = target.nominal_class(db, env)?.class_literal(db); + let generic_context = target_class.generic_context(db)?; + let target_identity = target_class.identity_specialization(db); + + let compatible_bases: SmallVec<[(ClassType<'db>, ClassType<'db>); 2]> = intersection + .positive(db) + .iter() + .filter_map(|element| { + let subject_class = element.nominal_class(db, env)?; + subject_class.static_class_literal(db)?.1?; + let target_base = target_identity + .iter_mro(db) + .filter_map(ClassBase::into_class) + .find(|base| base.class_literal(db) == subject_class.class_literal(db))?; + Some((target_base, subject_class)) + }) + .collect(); + + if compatible_bases.len() < 2 { + return None; + } + + let constraints = ConstraintSetBuilder::new(); + let mut base_constraints = compatible_bases + .into_iter() + .map(|(target_base, subject_class)| { + Type::instance(db, env, target_base).when_constraint_set_assignable_to( + db, + env, + Type::instance(db, env, subject_class), + &constraints, + ) + }); + let mut combined_constraints = base_constraints.next()?; + for base_constraint in base_constraints { + combined_constraints.intersect(db, &constraints, base_constraint); + } + + let solutions = combined_constraints.solutions( + db, + env, + &constraints, + generic_context.inferable_typevars(db), + ); + let specialized_class = + specialize_generic_class_from_solutions(db, env, target_class, solutions)?; + Some(Type::instance(db, env, specialized_class)) +} + +fn specialize_narrowing_target<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + subject: Type<'db>, + target: Type<'db>, +) -> Option> { + if let Type::TypeVar(typevar) = subject { + let bound = match typevar.typevar(db).bound_or_constraints(db, env)? { + TypeVarBoundOrConstraints::UpperBound(bound) => bound, + TypeVarBoundOrConstraints::Constraints(constraints) => constraints.as_type(db, env), + }; + + return match bound { + Type::Union(union) => { + let mut candidates = UnionBuilder::new(db, env); + for element in union.elements(db) { + if let Some(specialized) = + specialize_narrowing_target(db, env, *element, target) + { + candidates.add_in_place(specialized); + } + } + (!candidates.is_empty()).then(|| candidates.build()) + } + Type::Intersection(intersection) => intersection + .positive(db) + .iter() + .find_map(|element| specialize_narrowing_target(db, env, *element, target)), + bound if bound != subject => specialize_narrowing_target(db, env, bound, target), + _ => None, + }; + } + + let (target_class, subject_class, is_subclass) = match target { + Type::SubclassOf(target) => { + let SubclassOfInner::Class(target_class) = target.subclass_of() else { + return None; + }; + let Type::SubclassOf(subject) = subject else { + return None; + }; + let SubclassOfInner::Class(subject_class) = subject.subclass_of() else { + return None; + }; + (target_class, subject_class, true) + } + _ => ( + target.nominal_class(db, env)?, + subject.nominal_class(db, env)?, + false, + ), + }; + + // An unspecialized class cannot contribute type arguments to the narrowing target. + subject_class.static_class_literal(db)?.1?; + + let target_class = + if subject_class.is_subtype_of_class_literal(db, target_class.class_literal(db)) { + subject_class + } else { + specialize_generic_class_for_subject( + db, + env, + target_class.class_literal(db), + subject_class, + )? + }; + + Some(if is_subclass { + SubclassOfType::from(db, env, target_class) + } else { + Type::instance(db, env, target_class) + }) +} + +/// Infer a generic subclass specialization from a specialized base class. +/// +/// For example, if `target_class` is `list` and `subject_class` is `Sequence[int]`, +/// this returns the specialized class `list[int]`. +fn specialize_generic_class_for_subject<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + target_class: ClassLiteral<'db>, + subject_class: ClassType<'db>, +) -> Option> { + let generic_context = target_class.generic_context(db)?; + let target_identity = target_class.identity_specialization(db); + let target_base = target_identity + .iter_mro(db) + .filter_map(ClassBase::into_class) + .find(|base| base.class_literal(db) == subject_class.class_literal(db)); + + let (source, target) = if let Some(target_base) = target_base { + (target_base, subject_class) + } else if target_class.is_protocol(db) { + (subject_class, target_identity) + } else if subject_class.is_protocol(db) { + (target_identity, subject_class) + } else { + return None; + }; + + let constraints = ConstraintSetBuilder::new(); + let solutions = Type::instance(db, env, source) + .assignable_solutions_with_inferable( + db, + env, + Type::instance(db, env, target), + generic_context.inferable_typevars(db), + ) + .solve(db, env, &constraints); + + specialize_generic_class_from_solutions(db, env, target_class, solutions) +} + +fn specialize_generic_class_from_solutions<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + target_class: ClassLiteral<'db>, + solutions: Solutions<'db>, +) -> Option> { + let generic_context = target_class.generic_context(db)?; + let Solutions::Constrained(solutions) = solutions else { + return None; + }; + let [solution] = solutions.as_slice() else { + return None; + }; + + let typevars = generic_context.variables(db); + let unknown_specialization = generic_context.unknown_specialization(db, target_class.known(db)); + let types = typevars + .clone() + .map(|typevar| { + solution + .iter() + .find(|binding| binding.bound_typevar == typevar) + .map(|binding| binding.solution) + .or_else(|| unknown_specialization.get(db, typevar)) + }) + .collect::>>()?; + if types.iter().any(|ty| { + typevars + .clone() + .any(|typevar| ty.references_typevar(db, env, typevar.typevar(db).identity(db))) + }) { + return None; + } + + let specialization = if target_class.is_known(db, KnownClass::Tuple) + && let [element] = types.as_slice() + { + generic_context.specialize_tuple(db, *element, TupleType::homogeneous(db, env, *element)) + } else { + generic_context.specialize(db, types) + }; + + Some(target_class.apply_specialization(db, |_| specialization)) +} + /// Represents narrowing constraints in Disjunctive Normal Form (DNF). /// /// This is a disjunction (OR) of conjunctions (AND) of constraints. @@ -871,6 +1276,15 @@ impl<'db> NarrowingConstraint<'db> { } } + /// Create an intersection constraint that preserves generic arguments already known about + /// the subject when narrowing it to a subclass. + fn generic_filtering(constraint: Type<'db>) -> Self { + Self { + intersection_disjuncts: smallvec_inline![Conjunctions::generic_filtering(constraint)], + replacement_disjuncts: smallvec![], + } + } + /// Create a "replacement" constraint: the previous type will be /// replaced wholesale with this constraint fn replacement(constraint: Type<'db>) -> Self { @@ -944,14 +1358,18 @@ impl<'db> NarrowingConstraint<'db> { /// Evaluate the type this effectively constrains to /// /// Forgets whether each constraint originated from a `replacement` disjunct or not - pub(crate) fn evaluate_constraint_type(self, db: &'db dyn Db) -> Type<'db> { - let mut union = UnionBuilder::new(db); + pub(crate) fn evaluate_constraint_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { + let mut union = UnionBuilder::new(db, env); for conjunctions in self .replacement_disjuncts .into_iter() .chain(self.intersection_disjuncts) { - union = union.add(conjunctions.evaluate_constraint_type(db)); + union.add_in_place(conjunctions.evaluate_constraint_type(db, env)); } union.build() } @@ -1100,17 +1518,30 @@ fn merge_constraints_or<'db>( /// value of `PatternClass` may be a subclass of `A`. fn positive_class_pattern_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class_expression_ty: Type<'db>, + use_generic_filtering: bool, ) -> Option> { match class_expression_ty { Type::SpecialForm(SpecialFormType::CollectionsAbcCallable) => { - Some(callable_pattern_type(db)) + Some(if use_generic_filtering { + Type::Callable(CallableType::unknown(db)) + } else { + callable_pattern_type(db, env) + }) } - _ if class_expression_ty.is_assignable_to(db, KnownClass::Type.to_instance(db)) => { + _ if class_expression_ty.is_assignable_to( + db, + env, + KnownClass::Type.to_instance(db, env), + ) => + { ClassInfoConstraintFunction::IsInstance.generate_constraint( db, + env, class_expression_ty, true, + use_generic_filtering, ) } _ => None, @@ -1137,6 +1568,7 @@ fn positive_class_pattern_type<'db>( /// ``` fn refine_exact_tuple_for_sequence_pattern<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, subject_ty: Type<'db>, pattern_element_types: &[Type<'db>], ) -> Option> { @@ -1144,9 +1576,9 @@ fn refine_exact_tuple_for_sequence_pattern<'db>( let pattern_tuple = TupleSpec::heterogeneous(pattern_element_types.iter().copied()); Some( TupleSpecBuilder::from(tuple.as_ref()) - .intersect(db, &pattern_tuple) + .intersect(db, env, &pattern_tuple) .map_or(Type::Never, |refined| { - Type::tuple(TupleType::new(db, &refined.build())) + Type::tuple(TupleType::new(db, env, &refined.build())) }), ) } @@ -1170,33 +1602,37 @@ fn refine_exact_tuple_for_sequence_pattern<'db>( /// every value that does. fn necessary_match_pattern_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, pattern: &PatternPredicateKind<'db>, ) -> Type<'db> { match pattern { - PatternPredicateKind::Singleton(singleton) => singleton_pattern_type(db, *singleton), + PatternPredicateKind::Singleton(singleton) => singleton_pattern_type(db, env, *singleton), PatternPredicateKind::Class(kind) => positive_class_pattern_type( db, + env, infer_same_file_expression_type(db, kind.class, TypeContext::default()), + false, ) .unwrap_or_else(Type::object), - PatternPredicateKind::Mapping(_) => mapping_pattern_type(db), - PatternPredicateKind::Sequence(kind) => necessary_sequence_pattern_type(db, kind), + PatternPredicateKind::Mapping(_) => mapping_pattern_type(db, env), + PatternPredicateKind::Sequence(kind) => necessary_sequence_pattern_type(db, env, kind), PatternPredicateKind::Or(predicates) => UnionType::from_elements( db, + env, predicates .iter() - .map(|predicate| necessary_match_pattern_type(db, predicate)), + .map(|predicate| necessary_match_pattern_type(db, env, predicate)), ), // basedpython: a value that matches a conjunction matches every conjunct PatternPredicateKind::And(predicates) => predicates .iter() - .fold(IntersectionBuilder::new(db), |builder, predicate| { - builder.add_positive(necessary_match_pattern_type(db, predicate)) + .fold(IntersectionBuilder::new(db, env), |builder, predicate| { + builder.add_positive(necessary_match_pattern_type(db, env, predicate)) }) .build(), PatternPredicateKind::As(pattern, _) => pattern .as_deref() - .map(|pattern| necessary_match_pattern_type(db, pattern)) + .map(|pattern| necessary_match_pattern_type(db, env, pattern)) .unwrap_or_else(Type::object), PatternPredicateKind::Value(_) | PatternPredicateKind::Star(_) => Type::object(), } @@ -1205,28 +1641,36 @@ fn necessary_match_pattern_type<'db>( /// Preserve the sequence element constraints that can be addressed at fixed indices. fn necessary_sequence_pattern_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, kind: &SequencePatternPredicateKind<'db>, ) -> Type<'db> { if let Some((prefix_patterns, suffix_patterns)) = kind.split_around_star() { let prefix_element_types = prefix_patterns .iter() - .map(|pattern| necessary_match_pattern_type(db, pattern)); + .map(|pattern| necessary_match_pattern_type(db, env, pattern)); let suffix_element_types = suffix_patterns .iter() - .map(|pattern| necessary_match_pattern_type(db, pattern)); + .map(|pattern| necessary_match_pattern_type(db, env, pattern)); - starred_sequence_pattern_type(db, prefix_element_types, suffix_element_types) + starred_sequence_pattern_type(db, env, prefix_element_types, suffix_element_types) } else { let element_types = kind .patterns .iter() - .map(|pattern| necessary_match_pattern_type(db, pattern)); - exact_sequence_pattern_type(db, element_types) + .map(|pattern| necessary_match_pattern_type(db, env, pattern)); + exact_sequence_pattern_type(db, env, element_types) } } +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +enum NominalAttributeComparison { + Equality, + Identity, +} + struct NarrowingConstraintsBuilder<'db, 'ast> { db: &'db dyn Db, + env: ProgramEnvironment<'db>, module: &'ast ParsedModuleRef, predicate: PredicateNode<'db>, is_positive: bool, @@ -1235,12 +1679,14 @@ struct NarrowingConstraintsBuilder<'db, 'ast> { impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { fn new( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, module: &'ast ParsedModuleRef, predicate: PredicateNode<'db>, is_positive: bool, ) -> Self { Self { db, + env: env.clone(), module, predicate, is_positive, @@ -1260,6 +1706,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { } PredicateNode::AssertsCall(_) | PredicateNode::IsNonTerminalCall(_) => return None, PredicateNode::IsNonEmptyIterable(_) => return None, + PredicateNode::OrPatternAlternative(_) => return None, PredicateNode::StarImportPlaceholder(_) => return None, }; @@ -1271,7 +1718,8 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { expression: Expression<'db>, is_positive: bool, ) -> Option> { - let expression_node = expression.node_ref(self.db).node(self.module); + let db = self.db; + let expression_node = expression.node_ref(db).node(self.module); self.evaluate_expression_node_predicate(expression_node, expression, is_positive) } @@ -1281,10 +1729,10 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { expression: Expression<'db>, is_positive: bool, ) -> Option> { + let db = self.db; match expression_node { ast::Expr::Name(_) => { - let file = expression.file(self.db); - let index = semantic_index(self.db, file); + let index = semantic_index(db, expression.program_file(db)); let constraints = self.evaluate_simple_expr(expression_node, is_positive); if let Some(alias_predicate) = index.narrowing_alias_predicate(expression_node) { let aliased_constraints = @@ -1300,7 +1748,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { } ast::Expr::Attribute(attribute) => { let constraints = self.evaluate_simple_expr(expression_node, is_positive); - let inference = infer_expression_types(self.db, expression, TypeContext::default()); + let inference = infer_expression_types(db, expression, TypeContext::default()); let nominal_constraints = self .narrow_nominal_attribute_by_truthiness( inference.expression_type(&*attribute.value), @@ -1316,7 +1764,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { } ast::Expr::Subscript(subscript) => { let constraints = self.evaluate_simple_expr(expression_node, is_positive); - let inference = infer_expression_types(self.db, expression, TypeContext::default()); + let inference = infer_expression_types(db, expression, TypeContext::default()); let typeddict_constraints = self .narrow_typeddict_subscript_by_truthiness( inference.expression_type(&*subscript.value), @@ -1382,9 +1830,10 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { expression: Expression<'db>, is_positive: bool, ) -> Option> { - let test_truthiness = infer_expression_types(self.db, expression, TypeContext::default()) + let db = self.db; + let test_truthiness = infer_expression_types(db, expression, TypeContext::default()) .expression_type(&expr_if.test) - .bool(self.db); + .bool(db, &self.env); match test_truthiness { Truthiness::AlwaysTrue => { @@ -1455,6 +1904,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { pattern: PatternPredicate<'db>, is_positive: bool, ) -> Option> { + let db = self.db; let kind = pattern.kind(self.db); // basedpython: a destructuring binder holds a value nothing else can // name, so matching it narrows no place @@ -1467,7 +1917,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { .into_constraints(); } - let subject_node = subject.node_ref(self.db).node(self.module); + let subject_node = subject.node_ref(db).node(self.module); let expression_constraints = self .evaluate_positive_pattern_related_expressions(kind, subject, subject_node) .into_constraints(); @@ -1476,7 +1926,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { return expression_constraints; }; let place = self.expect_place(&subject_place); - let subject_ty = infer_same_file_expression_type(self.db, subject, TypeContext::default()); + let subject_ty = infer_same_file_expression_type(db, subject, TypeContext::default()); let mut constraints = expression_constraints.unwrap_or_default(); constraints.remove(&place); if let Some(subject_constraint) = self.positive_subject_constraint(kind, subject_ty) { @@ -1538,15 +1988,15 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { pattern: &PatternPredicateKind<'db>, subject_ty: Type<'db>, ) -> Option> { + let db = self.db; match pattern { PatternPredicateKind::Value(value) => { - let value_ty = - infer_same_file_expression_type(self.db, *value, TypeContext::default()); + let value_ty = infer_same_file_expression_type(db, *value, TypeContext::default()); self.evaluate_expr_compare_op(subject_ty, value_ty, ast::CmpOp::Eq, true) .map(NarrowingConstraint::intersection) } PatternPredicateKind::Singleton(singleton) => Some(NarrowingConstraint::intersection( - singleton_pattern_type(self.db, *singleton), + singleton_pattern_type(db, &self.env, *singleton), )), PatternPredicateKind::As(Some(pattern), _) => { self.positive_subject_constraint(pattern, subject_ty) @@ -1564,9 +2014,9 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { Some(constraint) } _ => { - let matched_subject_ty = PatternSuccessAnalyzer::new(self.db, self.scope()) + let matched_subject_ty = PatternSuccessAnalyzer::new(db, self.scope()) .matched_subject_type(pattern, subject_ty); - (!matched_subject_ty.is_equivalent_to(self.db, subject_ty)) + (!matched_subject_ty.is_equivalent_to(db, &self.env, subject_ty)) .then(|| NarrowingConstraint::intersection(matched_subject_ty)) } } @@ -1575,13 +2025,22 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { impl<'db> PatternSuccessAnalyzer<'db> { fn new(db: &'db dyn Db, scope: ScopeId<'db>) -> Self { - Self { db, scope } + Self { + db, + env: ProgramEnvironment::from_scope(scope), + scope, + } } fn comparison_soundness_policy(&self) -> ComparisonSoundnessPolicy { - ComparisonSoundnessPolicy::from_analysis_settings( - self.db.analysis_settings(self.scope.file(self.db)), - ) + let db = self.db; + ComparisonSoundnessPolicy::from_analysis_settings(db.analysis_settings(self.scope.file(db))) + } + + fn use_generic_filtering(&self) -> bool { + let db = self.db; + !db.analysis_settings(self.scope.file(db)) + .strict_generic_narrowing } fn merge_binding( @@ -1634,6 +2093,7 @@ impl<'db> PatternSuccessAnalyzer<'db> { pattern: &PatternPredicateKind<'db>, subject_ty: Type<'db>, ) -> PatternSuccessResult<'db> { + let db = self.db; match pattern { PatternPredicateKind::Class(kind) => { self.analyze_successful_class_pattern(kind, subject_ty) @@ -1695,8 +2155,10 @@ impl<'db> PatternSuccessAnalyzer<'db> { } } PatternPredicateKind::Singleton(_) => { - let matched_subject_ty = self - .intersect_types(subject_ty, necessary_match_pattern_type(self.db, pattern)); + let matched_subject_ty = self.intersect_types( + subject_ty, + necessary_match_pattern_type(db, &self.env, pattern), + ); PatternSuccessResult { matched_subject_ty, binding_subject_ty: matched_subject_ty, @@ -1716,6 +2178,7 @@ impl<'db> PatternSuccessAnalyzer<'db> { pattern: &PatternPredicateKind<'db>, subject_ty: Type<'db>, ) -> Type<'db> { + let db = self.db; match pattern { PatternPredicateKind::Class(kind) => { self.matched_class_pattern_subject_type(kind, subject_ty) @@ -1742,9 +2205,10 @@ impl<'db> PatternSuccessAnalyzer<'db> { PatternPredicateKind::Value(value) => { self.match_value_pattern_subject_type(*value, subject_ty) } - PatternPredicateKind::Singleton(_) => { - self.intersect_types(subject_ty, necessary_match_pattern_type(self.db, pattern)) - } + PatternPredicateKind::Singleton(_) => self.intersect_types( + subject_ty, + necessary_match_pattern_type(db, &self.env, pattern), + ), } } @@ -1753,12 +2217,14 @@ impl<'db> PatternSuccessAnalyzer<'db> { patterns: &[PatternPredicateKind<'db>], subject_ty: Type<'db>, ) -> Type<'db> { + let db = self.db; self.analyze_matched_subject_arms( subject_ty, OriginalSubjectPreservation::TypeVariablesOnly, |analyzer, _, subject_ty| { Some(UnionType::from_elements( - analyzer.db, + db, + &analyzer.env, patterns .iter() .map(|pattern| analyzer.matched_subject_type(pattern, subject_ty)), @@ -1785,9 +2251,11 @@ impl<'db> PatternSuccessAnalyzer<'db> { value: Expression<'db>, subject_ty: Type<'db>, ) -> Type<'db> { - let value_ty = infer_same_file_expression_type(self.db, value, TypeContext::default()); + let db = self.db; + let value_ty = infer_same_file_expression_type(db, value, TypeContext::default()); evaluate_type_equality( - self.db, + db, + &self.env, subject_ty, value_ty, true, @@ -1841,6 +2309,7 @@ impl<'db> PatternSuccessAnalyzer<'db> { patterns: &[PatternPredicateKind<'db>], subject_ty: Type<'db>, ) -> PatternSuccessResult<'db> { + let db = self.db; let mut patterns = patterns.iter(); let Some(first_pattern) = patterns.next() else { return PatternSuccessResult { @@ -1850,9 +2319,9 @@ impl<'db> PatternSuccessAnalyzer<'db> { }; }; let first = self.analyze_successful_pattern(first_pattern, subject_ty); - let mut matched_subject_types = UnionBuilder::new(self.db); + let mut matched_subject_types = UnionBuilder::new(db, &self.env); matched_subject_types.add_in_place(first.matched_subject_ty); - let mut binding_subject_types = UnionBuilder::new(self.db); + let mut binding_subject_types = UnionBuilder::new(db, &self.env); binding_subject_types.add_in_place(first.binding_subject_ty); // All alternatives bind the same names. Merge by logical place so the case body sees the // union even though the semantic walk visits the definitions in order. @@ -1861,8 +2330,12 @@ impl<'db> PatternSuccessAnalyzer<'db> { let mut previous_pattern = first_pattern; for pattern in patterns { - remaining_subject_ty = - pattern_binding_fallthrough_type(self.db, previous_pattern, remaining_subject_ty); + remaining_subject_ty = pattern_binding_fallthrough_type( + db, + &self.env, + previous_pattern, + remaining_subject_ty, + ); let alternative = self.analyze_successful_pattern(pattern, remaining_subject_ty); binding_subject_types.add_in_place(alternative.binding_subject_ty); Self::merge_bindings(&mut bindings, alternative.bindings); @@ -1898,40 +2371,53 @@ impl<'db> PatternSuccessAnalyzer<'db> { class_ty: Type<'db>, subject_ty: Type<'db>, ) -> Type<'db> { + let db = self.db; + let intersect = |subject_ty| { + if self.use_generic_filtering() { + filter_generic_narrowing_constraint(db, &self.env, subject_ty, class_ty) + } else { + self.intersect_types(subject_ty, class_ty) + } + }; match subject_ty { Type::TypeAlias(alias) => { - self.filter_class_pattern_subject_type(class, class_ty, alias.value_type(self.db)) + self.filter_class_pattern_subject_type(class, class_ty, alias.value_type(db)) } - Type::Union(union) => union.map(self.db, |element| { + Type::Union(union) => union.map(db, &self.env, |element| { self.filter_class_pattern_subject_type(class, class_ty, *element) }), - Type::Intersection(intersection) if intersection.positive(self.db).is_empty() => { - self.intersect_types(subject_ty, class_ty) + Type::Intersection(intersection) if intersection.positive(db).is_empty() => { + intersect(subject_ty) + } + Type::Intersection(intersection) => { + intersection.map_positive(db, &self.env, |positive| { + self.filter_class_pattern_subject_type(class, class_ty, *positive) + }) } - Type::Intersection(intersection) => intersection.map_positive(self.db, |positive| { - self.filter_class_pattern_subject_type(class, class_ty, *positive) - }), Type::NominalInstance(instance) => { let Some(class) = class else { - return self.intersect_types(subject_ty, class_ty); + return intersect(subject_ty); }; - let subject_class = instance.class(self.db); - if subject_class.is_subtype_of_class_literal(self.db, class) { + let subject_class = instance.class(db, &self.env); + if subject_class.is_subtype_of_class_literal(db, class) { subject_ty - } else if subject_ty.is_disjoint_from(self.db, class_ty) { + } else if subject_ty.is_disjoint_from(db, &self.env, class_ty) { Type::Never } else { - self.intersect_types(subject_ty, class_ty) + intersect(subject_ty) } } Type::TypedDict(_) - if class.is_some_and(|class| typed_dict_matches_class_pattern(self.db, class)) => + if class.is_some_and(|class| { + typed_dict_matches_class_pattern(db, &self.env, class) + }) => { subject_ty } - _ if subject_ty.is_subtype_of(self.db, class_ty) => subject_ty, - _ if subject_ty.is_disjoint_from(self.db, class_ty) => Type::Never, - _ => self.intersect_types(subject_ty, class_ty), + Type::Callable(_) if matches!(class_ty, Type::Callable(_)) => subject_ty, + _ if subject_ty.is_subtype_of(db, &self.env, class_ty) => subject_ty, + _ if subject_ty.is_disjoint_from(db, &self.env, class_ty) => Type::Never, + _ => intersect(subject_ty), } } @@ -1943,67 +2429,94 @@ impl<'db> PatternSuccessAnalyzer<'db> { filtering_subject_ty: Type<'db>, subject_ty: Type<'db>, ) -> Option>> { + let db = self.db; let subject_is_final = subject_ty - .nominal_class(self.db) - .is_some_and(|class| class.is_final(self.db)); - let specialized_pattern_class = - if context.positional_sources.is_empty() && kind.keywords.is_empty() { - None - } else { - context - .class - .zip(filtering_subject_ty.nominal_class(self.db)) - .and_then(|(pattern_class, subject_class)| { - self.specialize_pattern_class_for_subject(pattern_class, subject_class) - }) - }; + .nominal_class(db, &self.env) + .is_some_and(|class| class.is_final(db)); + let specialized_pattern_class = if context.positional_sources.is_empty() + && kind.keywords.is_empty() + { + None + } else if self.use_generic_filtering() { + context + .class + .filter(|pattern_class| pattern_class.generic_context(db).is_some()) + .and_then(|pattern_class| { + subject_ty + .nominal_class(db, &self.env) + .filter(|subject_class| subject_class.class_literal(db) == pattern_class) + .or_else(|| { + if let Type::Intersection(intersection) = subject_ty { + intersection.positive(db).iter().find_map(|element| { + element + .nominal_class(db, &self.env) + .filter(|class| class.class_literal(db) == pattern_class) + }) + } else { + None + } + }) + }) + } else { + context + .class + .zip(filtering_subject_ty.nominal_class(db, &self.env)) + .and_then(|(pattern_class, subject_class)| { + self.specialize_pattern_class_for_subject(pattern_class, subject_class) + }) + }; let member_type = |name: &Name| { let original_member_ty = original_subject_ty - .member(self.db, name.as_str()) + .member(db, &self.env, name.as_str()) .place .ignore_possibly_undefined(); - let place = subject_ty.member(self.db, name.as_str()).place; + let place = subject_ty.member(db, &self.env, name.as_str()).place; let mut member_ty = place.ignore_possibly_undefined(); - if let Some(specialized_pattern_class) = specialized_pattern_class { - member_ty = Type::instance(self.db, specialized_pattern_class) - .member(self.db, name.as_str()) - .place - .ignore_possibly_undefined(); + if let Some(specialized_pattern_class) = specialized_pattern_class + && let Some(specialized_member_ty) = + Type::instance(db, &self.env, specialized_pattern_class) + .member(db, &self.env, name.as_str()) + .place + .ignore_possibly_undefined() + && !specialized_member_ty.is_unknown() + { + member_ty = Some(specialized_member_ty); } else if let Some(pattern_class) = context.class && pattern_class - .generic_context(self.db) + .generic_context(db) .and_then(|generic_context| { pattern_class .instance_member( - self.db, - Some(generic_context.identity_specialization(self.db)), + db, + &self.env, + Some(generic_context.identity_specialization(db)), name.as_str(), ) .place .ignore_possibly_undefined() }) - .is_some_and(|ty| ty.has_typevar(self.db)) + .is_some_and(|ty| ty.has_typevar(db, &self.env)) { - let unknown_pattern_class = pattern_class.unknown_specialization(self.db); - let unknown_pattern_member_ty = Type::instance(self.db, unknown_pattern_class) - .member(self.db, name.as_str()) - .place - .ignore_possibly_undefined(); + let unknown_pattern_class = pattern_class.unknown_specialization(db); + let unknown_pattern_member_ty = + Type::instance(db, &self.env, unknown_pattern_class) + .member(db, &self.env, name.as_str()) + .place + .ignore_possibly_undefined(); // For example, `Child[int]` and `Base[T]` share a generic hierarchy, so a `Base` // pattern can reuse `int` from the subject. This is also the conservative fallback // when the subject does not determine one exact specialization of the pattern // subclass. if original_subject_ty - .nominal_class(self.db) + .nominal_class(db, &self.env) .is_some_and(|original_class| { - unknown_pattern_class.is_subtype_of_class_literal( - self.db, - original_class.class_literal(self.db), - ) || original_class.is_subtype_of_class_literal( - self.db, - unknown_pattern_class.class_literal(self.db), - ) + unknown_pattern_class + .is_subtype_of_class_literal(db, original_class.class_literal(db)) + || original_class.is_subtype_of_class_literal( + db, + unknown_pattern_class.class_literal(db), + ) }) { // The pattern class's unknown specialization loses type arguments known @@ -2018,7 +2531,8 @@ impl<'db> PatternSuccessAnalyzer<'db> { // Unrelated classes can overlap through multiple inheritance, so retain the // generic pattern class's member as a possible runtime value. member_ty = Some(UnionType::from_elements( - self.db, + db, + &self.env, member_ty.into_iter().chain([pattern_member_ty]), )); } @@ -2062,7 +2576,8 @@ impl<'db> PatternSuccessAnalyzer<'db> { /// the existing conservative member type. /// /// ```python - /// class Base[T]: ... + /// class Base[T]: + /// value: T /// /// class Child[T](Base[T]): /// item: T @@ -2077,30 +2592,32 @@ impl<'db> PatternSuccessAnalyzer<'db> { pattern_class: ClassLiteral<'db>, subject_class: ClassType<'db>, ) -> Option> { - let generic_context = pattern_class.generic_context(self.db)?; + let db = self.db; + let generic_context = pattern_class.generic_context(db)?; let pattern_base = pattern_class - .identity_specialization(self.db) - .iter_mro(self.db) + .identity_specialization(db) + .iter_mro(db) .filter_map(ClassBase::into_class) - .find(|base| base.class_literal(self.db) == subject_class.class_literal(self.db))?; + .find(|base| base.class_literal(db) == subject_class.class_literal(db))?; let constraints = ConstraintSetBuilder::new(); - let solutions = Type::instance(self.db, pattern_base) + let solutions = Type::instance(db, &self.env, pattern_base) .assignable_solutions_with_inferable( - self.db, - Type::instance(self.db, subject_class), - generic_context.inferable_typevars(self.db), + db, + &self.env, + Type::instance(db, &self.env, subject_class), + generic_context.inferable_typevars(db), ) .solve_with(|variance, path_bound| { let Some(lower) = path_bound.lower else { return Ok(None); }; if variance != TypeVarVariance::Invariant - || path_bound.upper.materialize_exact(self.db) != lower + || path_bound.upper.materialize_exact(db, &self.env) != lower { return Ok(None); } - PathBounds::default_solve(self.db, &constraints, path_bound) + PathBounds::default_solve(db, &self.env, &constraints, path_bound) }); let Solutions::Constrained(solutions) = solutions else { return None; @@ -2109,7 +2626,7 @@ impl<'db> PatternSuccessAnalyzer<'db> { return None; }; - let typevars = generic_context.variables(self.db); + let typevars = generic_context.variables(db); let types = typevars .clone() .map(|typevar| { @@ -2121,43 +2638,48 @@ impl<'db> PatternSuccessAnalyzer<'db> { .collect::>>()?; if types.iter().any(|ty| { typevars.clone().any(|typevar| { - ty.references_typevar(self.db, typevar.typevar(self.db).identity(self.db)) + ty.references_typevar(db, &self.env, typevar.typevar(db).identity(db)) }) }) { return None; } - Some( - pattern_class - .apply_specialization(self.db, |_| generic_context.specialize(self.db, types)), - ) + Some(pattern_class.apply_specialization(db, |_| generic_context.specialize(db, types))) } fn class_pattern_contexts( &self, kind: &ClassPatternPredicateKind<'db>, ) -> SmallVec<[ClassPatternContext<'db>; 2]> { - let class_expr_ty = - infer_same_file_expression_type(self.db, kind.class, TypeContext::default()) - .resolve_type_alias(self.db); + let db = self.db; + let class_expr_ty = infer_same_file_expression_type(db, kind.class, TypeContext::default()) + .resolve_type_alias(db); + let use_generic_filtering = self.use_generic_filtering(); let context = |class_expr_ty: Type<'db>| { let class = class_expr_ty.as_class_literal(); ClassPatternContext { class, - class_ty: positive_class_pattern_type(self.db, class_expr_ty) - .unwrap_or_else(Type::object), + class_ty: positive_class_pattern_type( + db, + &self.env, + class_expr_ty, + use_generic_filtering, + ) + .unwrap_or_else(Type::object), positional_sources: class.map_or_else( || vec![ClassPatternPositionalSource::Unknown; kind.positional.len()], - |class| class_pattern_positional_sources(self.db, class, kind.positional.len()), + |class| { + class_pattern_positional_sources( + db, + &self.env, + class, + kind.positional.len(), + ) + }, ), } }; match class_expr_ty { - Type::Union(union) => union - .elements(self.db) - .iter() - .copied() - .map(context) - .collect(), + Type::Union(union) => union.elements(db).iter().copied().map(context).collect(), _ => smallvec![context(class_expr_ty)], } } @@ -2189,8 +2711,10 @@ impl<'db> PatternSuccessAnalyzer<'db> { kind: &ClassPatternPredicateKind<'db>, subject_ty: Type<'db>, ) -> Type<'db> { + let db = self.db; UnionType::from_elements( - self.db, + db, + &self.env, self.class_pattern_contexts(kind).iter().map(|context| { self.matched_class_pattern_subject_type_for_context(kind, context, subject_ty) }), @@ -2234,8 +2758,9 @@ impl<'db> PatternSuccessAnalyzer<'db> { kind: &ClassPatternPredicateKind<'db>, subject_ty: Type<'db>, ) -> PatternSuccessResult<'db> { - let mut matched_subject_types = UnionBuilder::new(self.db); - let mut binding_subject_types = UnionBuilder::new(self.db); + let db = self.db; + let mut matched_subject_types = UnionBuilder::new(db, &self.env); + let mut binding_subject_types = UnionBuilder::new(db, &self.env); let mut bindings = BTreeMap::new(); for context in self.class_pattern_contexts(kind) { let result = @@ -2299,31 +2824,32 @@ impl<'db> PatternSuccessAnalyzer<'db> { subject_ty: Type<'db>, key_ty: Type<'db>, ) -> Option> { - if let Type::TypedDict(typed_dict) = subject_ty.resolve_type_alias(self.db) { - let key_ty = key_ty.resolve_type_alias(self.db); - let typed_dict_key_ty = typed_dict.key_type(self.db); + let db = self.db; + if let Type::TypedDict(typed_dict) = subject_ty.resolve_type_alias(db) { + let key_ty = key_ty.resolve_type_alias(db); + let typed_dict_key_ty = typed_dict.key_type(db, &self.env); let policy = self.comparison_soundness_policy(); if typed_dict_key_ty.is_never() - || equality_truthiness(self.db, typed_dict_key_ty, key_ty, policy) + || equality_truthiness(db, &self.env, typed_dict_key_ty, key_ty, policy) == Truthiness::AlwaysFalse { return None; } if let Some(key) = key_ty.as_string_literal() { return typed_dict - .item(self.db, key.value(self.db)) + .item(db, key.value(db)) .map(|field| field.declared_ty) .or_else(|| { typed_dict - .openness(self.db) + .openness(db) .is_implicitly_open() .then_some(Type::object()) }); } - return Some(typed_dict.value_type(self.db)); + return Some(typed_dict.value_type(db, &self.env)); } - let Some((_, mapping_value_ty)) = subject_ty.unpack_keys_and_items(self.db) else { + let Some((_, mapping_value_ty)) = subject_ty.unpack_keys_and_items(db, &self.env) else { return Some(Type::unknown()); }; // For a standard `dict`/`Mapping` `get`, the captured value is simply the @@ -2336,7 +2862,7 @@ impl<'db> PatternSuccessAnalyzer<'db> { return Some(mapping_value_ty); } let Some(get_method) = subject_ty - .member(self.db, "get") + .member(db, &self.env, "get") .place .ignore_possibly_undefined() else { @@ -2345,19 +2871,21 @@ impl<'db> PatternSuccessAnalyzer<'db> { Some( get_method .try_call( - self.db, + db, + &self.env, &CallArguments::positional([key_ty, Type::object()]), ) - .map(|bindings| bindings.return_type(self.db)) - .unwrap_or_else(|error| error.return_type(self.db)), + .map(|bindings| bindings.return_type(db, &self.env)) + .unwrap_or_else(|error| error.return_type(db, &self.env)), ) } fn mapping_pattern_uses_standard_get(&self, subject_ty: Type<'db>) -> bool { - let Some(class) = subject_ty.nominal_class(self.db) else { + let db = self.db; + let Some(class) = subject_ty.nominal_class(db, &self.env) else { return false; }; - for base in class.iter_mro(self.db) { + for base in class.iter_mro(db) { let class = match base { ClassBase::Class(class) => class, ClassBase::Generic | ClassBase::Protocol => continue, @@ -2368,14 +2896,20 @@ impl<'db> PatternSuccessAnalyzer<'db> { return false; } }; - if !class.own_instance_member(self.db, "get").is_undefined() { + if !class + .own_instance_member(db, &self.env, "get") + .is_undefined() + { return false; } - if class.own_class_member(self.db, None, "get").is_undefined() { + if class + .own_class_member(db, &self.env, None, "get") + .is_undefined() + { continue; } return matches!( - class.known(self.db), + class.known(db), Some(KnownClass::Dict | KnownClass::Mapping) ); } @@ -2383,11 +2917,10 @@ impl<'db> PatternSuccessAnalyzer<'db> { } fn mapping_pattern_key_types(&self, kind: &MappingPatternPredicateKind<'db>) -> Vec> { + let db = self.db; kind.entries .iter() - .map(|entry| { - infer_same_file_expression_type(self.db, entry.key, TypeContext::default()) - }) + .map(|entry| infer_same_file_expression_type(db, entry.key, TypeContext::default())) .collect() } @@ -2396,7 +2929,9 @@ impl<'db> PatternSuccessAnalyzer<'db> { subject_ty: Type<'db>, key_types: &[Type<'db>], ) -> Option<(Type<'db>, Vec>)> { - let narrowed_subject_ty = self.intersect_types(subject_ty, mapping_pattern_type(self.db)); + let db = self.db; + let narrowed_subject_ty = + self.intersect_types(subject_ty, mapping_pattern_type(db, &self.env)); if narrowed_subject_ty.is_never() { return None; } @@ -2478,13 +3013,14 @@ impl<'db> PatternSuccessAnalyzer<'db> { } fn mapping_pattern_rest_type_for_arm(&self, subject_ty: Type<'db>) -> Type<'db> { - let (key_ty, value_ty) = match subject_ty.resolve_type_alias(self.db) { - Type::TypedDict(_) => (KnownClass::Str.to_instance(self.db), Type::object()), + let db = self.db; + let (key_ty, value_ty) = match subject_ty.resolve_type_alias(db) { + Type::TypedDict(_) => (KnownClass::Str.to_instance(db, &self.env), Type::object()), _ => subject_ty - .unpack_keys_and_items(self.db) + .unpack_keys_and_items(db, &self.env) .unwrap_or_else(|| (Type::unknown(), Type::unknown())), }; - KnownClass::Dict.to_specialized_instance(self.db, &[key_ty, value_ty]) + KnownClass::Dict.to_specialized_instance(db, &self.env, &[key_ty, value_ty]) } fn matched_sequence_pattern_subject_type( @@ -2492,8 +3028,9 @@ impl<'db> PatternSuccessAnalyzer<'db> { kind: &SequencePatternPredicateKind<'db>, subject_ty: Type<'db>, ) -> Type<'db> { + let db = self.db; let target_len = Self::sequence_pattern_target_len(kind); - let sequence_ty = sequence_pattern_type_builder(self.db).build(); + let sequence_ty = sequence_pattern_type_builder(db, &self.env).build(); self.analyze_matched_subject_arms( subject_ty, OriginalSubjectPreservation::TypeVariablesOnly, @@ -2538,8 +3075,9 @@ impl<'db> PatternSuccessAnalyzer<'db> { kind: &SequencePatternPredicateKind<'db>, subject_ty: Type<'db>, ) -> PatternSuccessResult<'db> { + let db = self.db; let target_len = Self::sequence_pattern_target_len(kind); - let sequence_ty = sequence_pattern_type_builder(self.db).build(); + let sequence_ty = sequence_pattern_type_builder(db, &self.env).build(); self.analyze_pattern_subject_arms( subject_ty, OriginalSubjectPreservation::TypeVariablesOnly, @@ -2591,9 +3129,14 @@ impl<'db> PatternSuccessAnalyzer<'db> { narrowed_subject_ty: Type<'db>, matched_element_types: &[Type<'db>], ) -> Type<'db> { + let db = self.db; if kind.split_around_star().is_none() - && let Some(refined) = - refine_exact_tuple_for_sequence_pattern(self.db, subject_ty, matched_element_types) + && let Some(refined) = refine_exact_tuple_for_sequence_pattern( + db, + &self.env, + subject_ty, + matched_element_types, + ) { return refined; } @@ -2616,20 +3159,28 @@ impl<'db> PatternSuccessAnalyzer<'db> { subject_ty: Type<'db>, binding_element_types: &[Type<'db>], ) -> Type<'db> { + let db = self.db; if kind.split_around_star().is_none() - && let Some(refined) = - refine_exact_tuple_for_sequence_pattern(self.db, subject_ty, binding_element_types) + && let Some(refined) = refine_exact_tuple_for_sequence_pattern( + db, + &self.env, + subject_ty, + binding_element_types, + ) { return refined; } - if subject_ty.exact_tuple_instance_spec(self.db).is_some() { + if subject_ty.exact_tuple_instance_spec(db).is_some() { self.intersect_types( subject_ty, self.successful_sequence_pattern_type(kind, binding_element_types), ) } else { - self.intersect_types(subject_ty, sequence_pattern_type_builder(self.db).build()) + self.intersect_types( + subject_ty, + sequence_pattern_type_builder(db, &self.env).build(), + ) } } @@ -2638,15 +3189,16 @@ impl<'db> PatternSuccessAnalyzer<'db> { kind: &SequencePatternPredicateKind<'db>, matched_element_types: &[Type<'db>], ) -> Type<'db> { + let db = self.db; if let Some((prefix, suffix)) = kind.split_around_star() { let prefix_types = matched_element_types.iter().copied().take(prefix.len()); let suffix_types = matched_element_types .iter() .copied() .skip(matched_element_types.len().saturating_sub(suffix.len())); - starred_sequence_pattern_type(self.db, prefix_types, suffix_types) + starred_sequence_pattern_type(db, &self.env, prefix_types, suffix_types) } else { - exact_sequence_pattern_type(self.db, matched_element_types.iter().copied()) + exact_sequence_pattern_type(db, &self.env, matched_element_types.iter().copied()) } } @@ -2661,22 +3213,25 @@ impl<'db> PatternSuccessAnalyzer<'db> { target_len: TupleLength, sequence_ty: Type<'db>, ) -> Option<(Type<'db>, Vec>)> { + let db = self.db; let narrowed_subject_ty = self.intersect_types(subject_ty, sequence_ty); if narrowed_subject_ty.is_never() { return None; } - let tuple = subject_ty.try_iterate(self.db).unwrap_or_else(|error| { - let fallback_element_ty = error.fallback_element_type(self.db); - Cow::Owned(TupleSpec::homogeneous( - if fallback_element_ty.is_unknown() { - Type::object() - } else { - fallback_element_ty - }, - )) - }); - let mut unpacker = TupleUnpacker::new(self.db, target_len); + let tuple = subject_ty + .try_iterate(db, &self.env) + .unwrap_or_else(|error| { + let fallback_element_ty = error.fallback_element_type(db, &self.env); + Cow::Owned(TupleSpec::homogeneous( + if fallback_element_ty.is_unknown() { + Type::object() + } else { + fallback_element_ty + }, + )) + }); + let mut unpacker = TupleUnpacker::new(db, &self.env, target_len); unpacker.unpack_tuple(tuple.as_ref()).ok()?; Some((narrowed_subject_ty, unpacker.into_types().collect())) } @@ -2687,15 +3242,17 @@ impl<'db> PatternSuccessAnalyzer<'db> { preservation: OriginalSubjectPreservation, analyze_arm: impl Fn(&Self, Type<'db>, Type<'db>) -> Option>, ) -> Type<'db> { + let db = self.db; let subject_arms = self.match_pattern_subject_arms(subject_ty); let grouped_arms = subject_arms .into_iter() .chunk_by(|(original_subject_ty, _)| *original_subject_ty); - let mut matched_subject_types = UnionBuilder::new(self.db); + let mut matched_subject_types = UnionBuilder::new(db, &self.env); for (original_subject_ty, arms) in &grouped_arms { let matched_types = UnionType::from_elements( - self.db, + db, + &self.env, arms.filter_map(|(_, filtering_subject_ty)| { analyze_arm(self, original_subject_ty, filtering_subject_ty) }), @@ -2716,17 +3273,18 @@ impl<'db> PatternSuccessAnalyzer<'db> { preservation: OriginalSubjectPreservation, analyze_arm: impl Fn(&Self, Type<'db>, Type<'db>) -> Option>, ) -> PatternSuccessResult<'db> { + let db = self.db; let subject_arms = self.match_pattern_subject_arms(subject_ty); let grouped_arms = subject_arms .into_iter() .chunk_by(|(original_subject_ty, _)| *original_subject_ty); - let mut matched_subject_types = UnionBuilder::new(self.db); - let mut binding_subject_types = UnionBuilder::new(self.db); + let mut matched_subject_types = UnionBuilder::new(db, &self.env); + let mut binding_subject_types = UnionBuilder::new(db, &self.env); let mut bindings = BTreeMap::new(); for (original_subject_ty, arms) in &grouped_arms { - let mut matched_types = UnionBuilder::new(self.db); - let mut binding_types = UnionBuilder::new(self.db); + let mut matched_types = UnionBuilder::new(db, &self.env); + let mut binding_types = UnionBuilder::new(db, &self.env); let mut arm_bindings = BTreeMap::new(); for (_, filtering_subject_ty) in arms { @@ -2738,7 +3296,7 @@ impl<'db> PatternSuccessAnalyzer<'db> { } for binding in arm_bindings.values_mut() { - let subject_ty = binding.subject_ty(self.db); + let subject_ty = binding.subject_ty(db, &self.env); if !subject_ty.is_never() { binding.restore_subject(self.preserve_original_subject_type( original_subject_ty, @@ -2775,13 +3333,14 @@ impl<'db> PatternSuccessAnalyzer<'db> { filtered_ty: Type<'db>, preservation: OriginalSubjectPreservation, ) -> Type<'db> { + let db = self.db; let filtering_ty = self.pattern_filtering_type(original_subject_ty); - if filtered_ty.is_equivalent_to(self.db, filtering_ty) + if filtered_ty.is_equivalent_to(db, &self.env, filtering_ty) && (matches!(preservation, OriginalSubjectPreservation::EquivalentTypes) - || original_subject_ty.has_typevar(self.db)) + || original_subject_ty.has_typevar(db, &self.env)) { original_subject_ty - } else if original_subject_ty.has_typevar(self.db) { + } else if original_subject_ty.has_typevar(db, &self.env) { self.intersect_types(original_subject_ty, filtered_ty) } else { filtered_ty @@ -2797,14 +3356,15 @@ impl<'db> PatternSuccessAnalyzer<'db> { &self, subject_ty: Type<'db>, ) -> SmallVec<[(Type<'db>, Type<'db>); 2]> { - let subject_ty = subject_ty.resolve_type_alias(self.db); + let db = self.db; + let subject_ty = subject_ty.resolve_type_alias(db); let mut arms = SmallVec::new(); let mut add_arm = |original_subject_ty: Type<'db>| { let filtering_subject_ty = self.pattern_filtering_type(original_subject_ty); match filtering_subject_ty { Type::Union(union) => arms.extend( union - .elements(self.db) + .elements(db) .iter() .map(|element| (original_subject_ty, *element)), ), @@ -2813,11 +3373,7 @@ impl<'db> PatternSuccessAnalyzer<'db> { }; match subject_ty { - Type::Union(union) => union - .elements(self.db) - .iter() - .copied() - .for_each(&mut add_arm), + Type::Union(union) => union.elements(db).iter().copied().for_each(&mut add_arm), _ => add_arm(subject_ty), } @@ -2825,11 +3381,12 @@ impl<'db> PatternSuccessAnalyzer<'db> { } fn pattern_filtering_type(&self, ty: Type<'db>) -> Type<'db> { - let ty = ty.resolve_type_alias(self.db); + let db = self.db; + let ty = ty.resolve_type_alias(db); if let Type::TypeVar(typevar) = ty - && let Some(bound) = typevar.typevar(self.db).upper_bound(self.db) + && let Some(bound) = typevar.typevar(db).upper_bound(db, &self.env) { - bound.resolve_type_alias(self.db) + bound.resolve_type_alias(db) } else { ty } @@ -2844,14 +3401,16 @@ impl<'db> PatternSuccessAnalyzer<'db> { } fn intersect_types(&self, left: Type<'db>, right: Type<'db>) -> Type<'db> { - IntersectionBuilder::new(self.db) + let db = self.db; + IntersectionBuilder::new(db, &self.env) .add_positive(left) .add_positive(right) .build() } fn places(&self) -> &'db PlaceTable { - place_table(self.db, self.scope) + let db = self.db; + place_table(db, self.scope) } } @@ -2860,6 +3419,7 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { &mut self, subject_element: SubjectElementPatternPredicate<'db>, ) -> Option> { + let db = self.db; let pattern = subject_element.pattern; // a subject element predicate only ever comes from a sequence-display // subject, which is an expression the source wrote @@ -2870,35 +3430,39 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { self.evaluate_match_pattern_for_subject_element( subject_expression, subject, - pattern.kind(self.db), + pattern.kind(db), Some(subject_element.target), ) .into_constraints() } fn places(&self) -> &'db PlaceTable { - place_table(self.db, self.scope()) + let db = self.db; + place_table(db, self.scope()) } fn scope(&self) -> ScopeId<'db> { + let db = self.db; match self.predicate { - PredicateNode::Expression(expression) => expression.scope(self.db), - PredicateNode::Pattern(pattern) => pattern.scope(self.db), + PredicateNode::Expression(expression) => expression.scope(db), + PredicateNode::Pattern(pattern) => pattern.scope(db), + PredicateNode::OrPatternAlternative(scope) => scope, PredicateNode::SubjectElementPattern(subject_element) => { - subject_element.pattern.scope(self.db) + subject_element.pattern.scope(db) } PredicateNode::IsNonTerminalCall(CallableAndCallExpr { callable, .. }) | PredicateNode::AssertsCall(CallableAndCallExpr { callable, .. }) => { callable.scope(self.db) } - PredicateNode::IsNonEmptyIterable(expression) => expression.scope(self.db), - PredicateNode::StarImportPlaceholder(definition) => definition.scope(self.db), + PredicateNode::IsNonEmptyIterable(expression) => expression.scope(db), + PredicateNode::StarImportPlaceholder(definition) => definition.scope(db), } } fn comparison_soundness_policy(&self) -> ComparisonSoundnessPolicy { + let db = self.db; ComparisonSoundnessPolicy::from_analysis_settings( - self.db.analysis_settings(self.scope().file(self.db)), + db.analysis_settings(self.scope().file(db)), ) } @@ -2921,9 +3485,13 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { /// and much of our special-casing for tuples elsewhere depends on this assumption). /// - Arbitrary user types that return `Literal` types from both `__len__` and `__bool__`, /// where the returned `Literal` types are mutually consistent in their truthiness. - fn is_base_type_narrowable_by_len(db: &'db dyn Db, ty: Type<'db>) -> bool { + fn is_base_type_narrowable_by_len( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + ) -> bool { match ty { - Type::NominalInstance(instance) if instance.tuple_spec(db).is_some() => true, + Type::NominalInstance(instance) if instance.tuple_spec(db, env).is_some() => true, Type::LiteralValue(literal) if matches!( literal.kind(), @@ -2934,9 +3502,9 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { { true } - _ => ty.len(db).is_some_and(|len_ty| { - let len_ty_bool = len_ty.bool(db); - len_ty_bool != Truthiness::Ambiguous && len_ty_bool == ty.bool(db) + _ => ty.len(db, env).is_some_and(|len_ty| { + let len_ty_bool = len_ty.bool(db, env); + len_ty_bool != Truthiness::Ambiguous && len_ty_bool == ty.bool(db, env) }), } } @@ -2947,7 +3515,12 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { /// `~AlwaysTruthy` (negative). For non-narrowable types, we return them unchanged. /// /// Returns `None` if no part of the type is narrowable. - fn narrow_type_by_len(db: &'db dyn Db, ty: Type<'db>, is_positive: bool) -> Option> { + fn narrow_type_by_len( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + is_positive: bool, + ) -> Option> { match ty { Type::Union(union) => { let mut has_narrowable = false; @@ -2955,7 +3528,8 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { .elements(db) .iter() .map(|element| { - if let Some(narrowed) = Self::narrow_type_by_len(db, *element, is_positive) + if let Some(narrowed) = + Self::narrow_type_by_len(db, env, *element, is_positive) { has_narrowable = true; narrowed @@ -2967,7 +3541,7 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { .collect(); if has_narrowable { - Some(UnionType::from_elements(db, narrowed_elements)) + Some(UnionType::from_elements(db, env, narrowed_elements)) } else { None } @@ -2977,27 +3551,27 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { let positive = intersection.positive(db); let has_narrowable = positive .iter() - .any(|element| Self::is_base_type_narrowable_by_len(db, *element)); + .any(|element| Self::is_base_type_narrowable_by_len(db, env, *element)); if has_narrowable { // Apply the narrowing constraint to the whole intersection. - let mut builder = IntersectionBuilder::new(db).add_positive(ty); + let mut builder = IntersectionBuilder::new(db, env).add_positive(ty); if is_positive { - builder = builder.add_negative(Type::AlwaysFalsy); + builder.add_negative_in_place(Type::AlwaysFalsy); } else { - builder = builder.add_negative(Type::AlwaysTruthy); + builder.add_negative_in_place(Type::AlwaysTruthy); } Some(builder.build()) } else { None } } - _ if Self::is_base_type_narrowable_by_len(db, ty) => { - let mut builder = IntersectionBuilder::new(db).add_positive(ty); + _ if Self::is_base_type_narrowable_by_len(db, env, ty) => { + let mut builder = IntersectionBuilder::new(db, env).add_positive(ty); if is_positive { - builder = builder.add_negative(Type::AlwaysFalsy); + builder.add_negative_in_place(Type::AlwaysFalsy); } else { - builder = builder.add_negative(Type::AlwaysTruthy); + builder.add_negative_in_place(Type::AlwaysTruthy); } Some(builder.build()) } @@ -3012,6 +3586,7 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { /// an observed length would become stale after mutation. fn narrow_type_by_exact_len( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, length: usize, is_equality: bool, @@ -3019,28 +3594,35 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { let resolved = ty.resolve_type_alias(db); let narrowed = match resolved { - Type::Union(union) => union.map(db, |element| { - Self::narrow_type_by_exact_len(db, *element, length, is_equality) + Type::Union(union) => union.map(db, env, |element| { + Self::narrow_type_by_exact_len(db, env, *element, length, is_equality) }), - Type::Intersection(intersection) => intersection.map_positive(db, |element| { - Self::narrow_type_by_exact_len(db, *element, length, is_equality) + Type::Intersection(intersection) => intersection.map_positive(db, env, |element| { + Self::narrow_type_by_exact_len(db, env, *element, length, is_equality) }), Type::TypeVar(typevar) => { - let Some(bound_or_constraints) = typevar.typevar(db).bound_or_constraints(db) + let Some(bound_or_constraints) = typevar.typevar(db).bound_or_constraints(db, env) else { return ty; }; - let upper_bound = bound_or_constraints.as_type(db); + let upper_bound = bound_or_constraints.as_type(db, env); let narrowed_upper_bound = match bound_or_constraints { TypeVarBoundOrConstraints::UpperBound(bound) => { - Self::narrow_type_by_exact_len(db, bound, length, is_equality) + Self::narrow_type_by_exact_len(db, env, bound, length, is_equality) } TypeVarBoundOrConstraints::Constraints(constraints) => { UnionType::from_elements( db, + env, constraints.elements(db).iter().map(|constraint| { - Self::narrow_type_by_exact_len(db, *constraint, length, is_equality) + Self::narrow_type_by_exact_len( + db, + env, + *constraint, + length, + is_equality, + ) }), ) } @@ -3049,17 +3631,17 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { if narrowed_upper_bound == upper_bound { resolved } else { - IntersectionType::from_two_elements(db, resolved, narrowed_upper_bound) + IntersectionType::from_two_elements(db, env, resolved, narrowed_upper_bound) } } _ => { if is_equality && let Some(tuple) = resolved.exact_tuple_instance_spec(db) { - match tuple.resize(db, TupleLength::Fixed(length)) { - Ok(tuple) => Type::tuple(TupleType::new(db, &tuple)), + match tuple.resize(db, env, TupleLength::Fixed(length)) { + Ok(tuple) => Type::tuple(TupleType::new(db, env, &tuple)), Err(_) => Type::Never, } } else { - let tuple_length = resolved.tuple_instance_spec(db).map(|spec| spec.len()); + let tuple_length = resolved.tuple_instance_spec(db, env).map(|spec| spec.len()); let satisfies_comparison = |length_type: Type<'db>| { length_type .as_int_literal() @@ -3067,7 +3649,7 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { .is_some_and(|actual| (actual == length) == is_equality) }; let comparison_possible = resolved - .len(db) + .len(db, env) .map(|length_type| match length_type { Type::Union(union) => union .elements(db) @@ -3103,13 +3685,14 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { expr: &ast::Expr, is_positive: bool, ) -> Option> { + let db = self.db; let target = PlaceExpr::try_from_expr(expr)?; let place = self.expect_place(&target); let ty = if is_positive { - Type::AlwaysFalsy.negate(self.db) + Type::AlwaysFalsy.negate(db, &self.env) } else { - Type::AlwaysTruthy.negate(self.db) + Type::AlwaysTruthy.negate(db, &self.env) }; Some(NarrowingConstraints::from_iter([( @@ -3154,31 +3737,30 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { } fn evaluate_expr_in(&self, lhs_ty: Type<'db>, rhs_ty: Type<'db>) -> Option> { - let rhs_ty = rhs_ty.resolve_type_alias(self.db); + let db = self.db; + let rhs_ty = rhs_ty.resolve_type_alias(db); // The supported containers compare against their iterated elements, so union arms can be // combined. String membership also accepts multi-character substrings, so evaluate literal // haystacks separately, including when they occur in a union. if let Some(haystack) = rhs_ty.as_string_literal() { - return narrow_string_membership(self.db, lhs_ty, haystack.value(self.db), true); + return narrow_string_membership(db, &self.env, lhs_ty, haystack.value(db), true); } if let Type::Union(union) = rhs_ty - && union.elements(self.db).iter().any(|element| { - element - .resolve_type_alias(self.db) - .as_string_literal() - .is_some() - }) + && union + .elements(db) + .iter() + .any(|element| element.resolve_type_alias(db).as_string_literal().is_some()) { - let mut builder = UnionBuilder::new(self.db); - for element in union.elements(self.db) { + let mut builder = UnionBuilder::new(db, &self.env); + for element in union.elements(db) { builder = builder.add(self.evaluate_expr_in(lhs_ty, *element)?); } let narrowed = builder.build(); return (narrowed != lhs_ty).then_some(narrowed); } - let membership_type = elements_of(self.db, rhs_ty)?; - let iterable = membership_type.try_iterate(self.db).ok()?; + let membership_type = elements_of(db, &self.env, rhs_ty)?; + let iterable = membership_type.try_iterate(db, &self.env).ok()?; if iterable .as_fixed_length() @@ -3187,28 +3769,35 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { return Some(Type::Never); } evaluate_type_equality( - self.db, + db, + &self.env, lhs_ty, - iterable.homogeneous_element_type(self.db), + iterable.homogeneous_element_type(db, &self.env), true, self.comparison_soundness_policy(), ) } fn evaluate_expr_not_in(&self, lhs_ty: Type<'db>, rhs_ty: Type<'db>) -> Option> { - if let Some(haystack) = rhs_ty.resolve_type_alias(self.db).as_string_literal() { - return narrow_string_membership(self.db, lhs_ty, haystack.value(self.db), false); + let db = self.db; + if let Some(haystack) = rhs_ty.resolve_type_alias(db).as_string_literal() { + return narrow_string_membership(db, &self.env, lhs_ty, haystack.value(db), false); } - let membership_type = elements_of(self.db, rhs_ty)?; - let iterable = membership_type.try_iterate(self.db).ok()?; + let membership_type = elements_of(db, &self.env, rhs_ty)?; + let iterable = membership_type.try_iterate(db, &self.env).ok()?; let fixed_length = iterable.as_fixed_length()?; - let mut builder = IntersectionBuilder::new(self.db); + let mut builder = IntersectionBuilder::new(db, &self.env); let mut constrained = false; // `not in` negates equality with every element; it does not use `__ne__`. Only add an // exclusion when every value represented by a slot is known to compare equal. for element_ty in fixed_length.all_elements().iter().copied() { - if let Some(constraint) = equality_exclusion_constraint(self.db, element_ty) { + if let Some(constraint) = equality_exclusion_constraint( + db, + &self.env, + element_ty, + self.comparison_soundness_policy(), + ) { builder = builder.add_positive(constraint); constrained = true; } @@ -3226,6 +3815,7 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { rhs: &ast::Expr, inference: &ExpressionInference<'db>, ) -> Option> { + let db = self.db; let elements = match rhs.expression_value() { ast::Expr::List(list) => &list.elts, ast::Expr::Set(set) => &set.elts, @@ -3237,7 +3827,8 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { } Some(Type::heterogeneous_tuple( - self.db, + db, + &self.env, elements .iter() .map(|element| inference.expression_type(element)), @@ -3251,9 +3842,11 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { op: ast::CmpOp, is_positive: bool, ) -> Option> { + let db = self.db; if op == ast::CmpOp::Eq { return evaluate_type_equality( - self.db, + db, + &self.env, lhs_ty, rhs_ty, is_positive, @@ -3262,7 +3855,8 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { } if op == ast::CmpOp::NotEq { return evaluate_type_inequality( - self.db, + db, + &self.env, lhs_ty, rhs_ty, is_positive, @@ -3274,14 +3868,82 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { match op { ast::CmpOp::IsNot => { - if rhs_ty.is_singleton(self.db) { - Some(rhs_ty.negate(self.db)) + let rhs_identity_ty = rhs_ty.identity_comparison_type(db, &self.env); + let rhs_constraint = if rhs_identity_ty.is_singleton(db, &self.env) { + rhs_identity_ty + } else if matches!(rhs_ty.resolve_type_alias(db), Type::TypeVar(_)) + && rhs_ty.is_singleton(db, &self.env) + { + rhs_ty } else { - // Non-singletons cannot be safely narrowed using `is not` - None + return None; + }; + Some(rhs_constraint.negate(db, &self.env)) + } + ast::CmpOp::Is => { + let rhs_identity_ty = rhs_ty.identity_comparison_type(db, &self.env); + // Identity transfers the runtime type, not a `NewType` tag or type-variable + // selection belonging to the other operand. + let mut builder = UnionBuilder::new(db, &self.env).add(rhs_identity_ty); + let rhs_resolved = rhs_ty.resolve_type_alias(db); + let add_runtime_overlap = |builder: UnionBuilder<'db>, element: Type<'db>| { + let overlaps_only_at_runtime = |rhs_element| { + element.is_disjoint_from(db, &self.env, rhs_element) + && element + .identity_comparison_truthiness(db, &self.env, rhs_element) + .may_be_true() + }; + let has_runtime_only_overlap = match rhs_resolved { + Type::Union(union) => union + .elements(db) + .iter() + .copied() + .any(overlaps_only_at_runtime), + Type::TypeVar(typevar) => { + match typevar.typevar(db).bound_or_constraints(db, &self.env) { + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + overlaps_only_at_runtime(bound) + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + constraints + .elements(db) + .iter() + .copied() + .any(overlaps_only_at_runtime) + } + None => overlaps_only_at_runtime(rhs_ty), + } + } + rhs_ty => overlaps_only_at_runtime(rhs_ty), + }; + if !has_runtime_only_overlap { + return builder; + } + + let runtime_overlap = IntersectionType::from_two_elements( + db, + &self.env, + element, + rhs_identity_ty, + ); + builder.add(if runtime_overlap.is_never() { + element + } else { + runtime_overlap + }) + }; + + if let Type::Union(union) = lhs_ty.resolve_type_alias(db) { + builder = union + .elements(db) + .iter() + .copied() + .fold(builder, add_runtime_overlap); + } else { + builder = add_runtime_overlap(builder, lhs_ty); } + Some(builder.build()) } - ast::CmpOp::Is => Some(rhs_ty), ast::CmpOp::In => self.evaluate_expr_in(lhs_ty, rhs_ty), ast::CmpOp::NotIn => self.evaluate_expr_not_in(lhs_ty, rhs_ty), _ => None, @@ -3322,22 +3984,28 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { /// at runtime). Similarly, we return `None` for `type[Y[int]]`, type variables /// bound to `type[Y[int]]`, and type aliases where the underlying value is a /// generic class. - fn find_underlying_class<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option> { + fn find_underlying_class<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + ) -> Option> { match ty { Type::ClassLiteral(class) => Some(class), Type::SubclassOf(subclass_of) => { - match subclass_of.subclass_of().with_transposed_type_var(db) { + match subclass_of.subclass_of().with_transposed_type_var(db, env) { SubclassOfInner::Class(ClassType::NonGeneric(class)) => Some(class), SubclassOfInner::Class(ClassType::Generic(_)) | SubclassOfInner::Dynamic(_) | SubclassOfInner::Protocol(_) => None, SubclassOfInner::TypeVar(tvar) => { - find_underlying_class(db, tvar.typevar(db).upper_bound(db)?) + find_underlying_class(db, env, tvar.typevar(db).upper_bound(db, env)?) } } } - Type::TypeVar(tvar) => find_underlying_class(db, tvar.typevar(db).upper_bound(db)?), - Type::TypeAlias(alias) => find_underlying_class(db, alias.value_type(db)), + Type::TypeVar(tvar) => { + find_underlying_class(db, env, tvar.typevar(db).upper_bound(db, env)?) + } + Type::TypeAlias(alias) => find_underlying_class(db, env, alias.value_type(db)), _ => None, } } @@ -3375,6 +4043,8 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { _ => None, } } + let env = self.env.clone(); + let db = self.db; let ast::ExprCompare { range: _, @@ -3401,7 +4071,7 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { return None; } - let inference = infer_expression_types(self.db, expression, TypeContext::default()); + let inference = infer_expression_types(db, expression, TypeContext::default()); let comparator_tuples = std::iter::once(&**left) .chain(comparators) @@ -3414,30 +4084,26 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { // if t[0] is not None: // reveal_type(t) # tuple[int, int] if matches!(&**ops, [ast::CmpOp::Is | ast::CmpOp::IsNot]) + && let is_positive_check = is_positive == (ops[0] == ast::CmpOp::Is) && let ast::Expr::Subscript(subscript) = left.expression_value() && let Type::Union(union) = inference .expression_type(&*subscript.value) - .resolve_type_alias(self.db) + .resolve_type_alias(db) && let Some(subscript_place_expr) = PlaceExpr::try_from_expr(&subscript.value) && let Some(index) = inference .expression_type(&*subscript.slice) .as_int_literal() && let Ok(index) = i32::try_from(index) && let rhs_ty = inference.expression_type(&comparators[0]) - && rhs_ty.is_singleton(self.db) { - let is_positive_check = is_positive == (ops[0] == ast::CmpOp::Is); - let filtered = union.filter(self.db, |elem| { - elem.tuple_instance_spec(self.db) - .and_then(|spec| spec.py_index(self.db, index).ok()) + let filtered = union.filter(db, |elem| { + elem.tuple_instance_spec(db, &self.env) + .and_then(|spec| spec.py_index(db, &self.env, index).ok()) .is_none_or(|el_ty| { - if is_positive_check { - // `is X` context: keep tuples where element could be X - !el_ty.is_disjoint_from(self.db, rhs_ty) - } else { - // `is not X` context: keep tuples where element is not always X - !el_ty.is_subtype_of(self.db, rhs_ty) - } + el_ty + .identity_comparison_truthiness(db, &self.env, rhs_ty) + .negate_if(!is_positive_check) + .may_be_true() }) }); if filtered != Type::Union(union) { @@ -3468,7 +4134,7 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { else { return; }; - if function_type.known(self.db) != Some(KnownFunction::Len) + if function_type.known(db) != Some(KnownFunction::Len) || !call.arguments.keywords.is_empty() { return; @@ -3476,9 +4142,7 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { let [arg] = &*call.arguments.args else { return; }; - let Some(length_literal) = length_type - .resolve_type_alias(self.db) - .as_int_like_literal() + let Some(length_literal) = length_type.resolve_type_alias(db).as_int_like_literal() else { return; }; @@ -3491,7 +4155,7 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { let arg_type = inference.expression_type(arg); let narrowed = - Self::narrow_type_by_exact_len(self.db, arg_type, length, is_equality); + Self::narrow_type_by_exact_len(db, &self.env, arg_type, length, is_equality); if narrowed != arg_type { insert_narrowing_constraint( &mut constraints, @@ -3541,6 +4205,19 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { if let ast::Expr::Subscript(subscript) = comparators[0].expression_value() { narrow_subscript(subscript, inference.expression_type(&**left)); } + } + + if let [ + operator @ (ast::CmpOp::Eq | ast::CmpOp::NotEq | ast::CmpOp::Is | ast::CmpOp::IsNot), + ] = &**ops + { + let comparison = if matches!(operator, ast::CmpOp::Is | ast::CmpOp::IsNot) { + NominalAttributeComparison::Identity + } else { + NominalAttributeComparison::Equality + }; + let is_positive_comparison = + is_positive == matches!(operator, ast::CmpOp::Eq | ast::CmpOp::Is); let mut narrow_attribute = |attribute: &ast::ExprAttribute, other_type: Type<'db>| { let value_type = inference.expression_type(&*attribute.value); @@ -3550,13 +4227,19 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { &attribute.value, attribute.attr.id(), other_type, - is_equality, + comparison, + is_positive_comparison, ) { insert_narrowing_constraint(&mut constraints, place, constraint); } }; - if let ast::Expr::Attribute(attribute) = &**left { + if let ast::Expr::Attribute(attribute) = &**left + && comparators[0].as_named_expr().is_none_or(|named| { + PlaceExpr::try_from_expr(&named.target) + != PlaceExpr::try_from_expr(&attribute.value) + }) + { narrow_attribute(attribute, inference.expression_type(&comparators[0])); } @@ -3581,9 +4264,9 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { && let Some(key) = inference.expression_type(&**left).as_string_literal() && let rhs_expr = comparators[0].expression_value() && let rhs_type = inference.expression_type(&comparators[0]) - && is_or_contains_typeddict(self.db, rhs_type) + && is_or_contains_typeddict(db, &env, rhs_type) { - let key = key.value(self.db); + let key = key.value(db); let apply_constraint = |constraints: &mut NarrowingConstraints<'db>, constraint: NarrowingConstraint<'db>| { @@ -3604,17 +4287,15 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { if is_positive == (ops[0] == ast::CmpOp::In) { let narrowed = self.narrow_with_present_key(rhs_type, key); - if narrowed != rhs_type.resolve_type_alias(self.db) { + if narrowed != rhs_type.resolve_type_alias(db) { apply_constraint(&mut constraints, NarrowingConstraint::replacement(narrowed)); } } else { let requires_key = |td: TypedDictType<'db>| -> bool { - td.items(self.db) - .get(key) - .is_some_and(TypedDictField::is_required) + td.key_membership_truthiness(db, key).is_always_true() }; - let resolved_rhs_type = rhs_type.resolve_type_alias(self.db); + let resolved_rhs_type = rhs_type.resolve_type_alias(db); let narrowed = match resolved_rhs_type { Type::TypedDict(td) => { @@ -3626,7 +4307,7 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { } Type::Intersection(intersection) => { if intersection - .positive(self.db) + .positive(db) .iter() .copied() .filter_map(Type::as_typed_dict) @@ -3639,10 +4320,10 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { } Type::Union(union) => { // remove all members of the union that would require the key - union.filter(self.db, |ty| match ty { + union.filter(db, |ty| match ty { Type::TypedDict(td) => !requires_key(*td), Type::Intersection(intersection) => !intersection - .positive(self.db) + .positive(db) .iter() .copied() .filter_map(Type::as_typed_dict) @@ -3697,7 +4378,9 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { let trimmed = between.trim(); !trimmed.starts_with("===") && !trimmed.starts_with("!==") } - }) && !basedpython_is_keeps_identity(self.db, rhs_ty); + }) && !basedpython_is_keeps_identity( + self.db, &env, rhs_ty, + ); // Narrowing for: // - `if type(x) is Y` @@ -3713,8 +4396,8 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { // - `if x.__class__ is y.__class__` // - `if x.__class__ is not y.__class__` let exact_class_checks = match ( - exact_class_narrowing_target(self.db, inference, left), - exact_class_narrowing_target(self.db, inference, right), + exact_class_narrowing_target(db, inference, left), + exact_class_narrowing_target(db, inference, right), ) { (Some(left_target), Some(right_target)) => { [Some((left_target, rhs_ty)), Some((right_target, lhs_ty))] @@ -3735,17 +4418,19 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { if let Some(is_positive) = is_positive && let Some(target) = PlaceExpr::try_from_expr(target_expr) - && let Some(other_class) = find_underlying_class(self.db, other) + && let Some(other_class) = + find_underlying_class(db, &self.env, other, + ) // `else`-branch narrowing for `if type(x) is Y` can only be done // if `Y` is a final class - && (is_positive || other_class.is_final(self.db)) + && (is_positive || other_class.is_final(db)) { let place = self.expect_place(&target); constraints.insert( place, NarrowingConstraint::intersection( - Type::instance(self.db, other_class.top_materialization(self.db)) - .negate_if(self.db, !is_positive), + Type::instance(db, &self.env, other_class.top_materialization(db)) + .negate_if(db, &self.env, !is_positive), ), ); } @@ -3763,15 +4448,15 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { // negative narrowing: an unreified or witness-less (empty) // value answers `False` even when it *is* one statically let constraint = if let Type::GenericAlias(alias) = rhs_ty { - positive.then(|| Type::instance(self.db, ClassType::Generic(alias))) + positive.then(|| Type::instance(self.db, &env, ClassType::Generic(alias))) } else { ClassInfoConstraintFunction::IsInstance - .generate_constraint(self.db, rhs_ty, positive) + .generate_constraint(self.db, &env, rhs_ty, positive, false) }; if let Some(constraint_ty) = constraint { let place = self.expect_place(&narrowable); let constraint = NarrowingConstraint::intersection( - constraint_ty.negate_if(self.db, !positive), + constraint_ty.negate_if(self.db, &env, !positive), ); constraints .entry(place) @@ -3828,7 +4513,9 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { .or_insert(constraint); // Use the narrowed type for subsequent comparisons in a chain. - last_rhs_ty = Some(IntersectionType::from_two_elements(self.db, rhs_ty, ty)); + last_rhs_ty = Some(IntersectionType::from_two_elements( + db, &self.env, rhs_ty, ty, + )); } else { last_rhs_ty = Some(rhs_ty); } @@ -3842,7 +4529,8 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { expression: Expression<'db>, is_positive: bool, ) -> Option> { - let inference = infer_expression_types(self.db, expression, TypeContext::default()); + let db = self.db; + let inference = infer_expression_types(db, expression, TypeContext::default()); // basedpython ` cast ` narrows the value place to the target // type wholesale: a checked cast is an assertion that overrides the @@ -3879,13 +4567,15 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { Type::FunctionLiteral(function_type) if expr_call.arguments.args.len() == 1 && expr_call.arguments.keywords.is_empty() - && function_type.known(self.db) == Some(KnownFunction::Len) => + && function_type.known(db) == Some(KnownFunction::Len) => { let arg = &expr_call.arguments.args[0]; let arg_ty = inference.expression_type(arg); // Narrow only the parts of the type that are safe to narrow based on len(). - if let Some(narrowed_ty) = Self::narrow_type_by_len(self.db, arg_ty, is_positive) { + if let Some(narrowed_ty) = + Self::narrow_type_by_len(db, &self.env, arg_ty, is_positive) + { let target = PlaceExpr::try_from_expr(arg)?; let place = self.expect_place(&target); Some(NarrowingConstraints::from_iter([( @@ -3901,14 +4591,14 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { return None; }; let first_arg = PlaceExpr::try_from_expr(first_arg)?; - let function = function_type.known(self.db)?; + let function = function_type.known(db)?; let place = self.expect_place(&first_arg); if function == KnownFunction::HasAttr { let attr = inference .expression_type(second_arg) .as_string_literal()? - .value(self.db); + .value(db); if !is_identifier(attr) { return None; @@ -3916,14 +4606,19 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { // 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())]); + let constraint = Type::protocol_with_readonly_members( + db, + &self.env, + [(attr, Type::object())], + ); return Some(NarrowingConstraints::from_iter([( place, - NarrowingConstraint::intersection( - constraint.negate_if(self.db, !is_positive), - ), + NarrowingConstraint::intersection(constraint.negate_if( + db, + &self.env, + !is_positive, + )), )])); } @@ -3931,14 +4626,31 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { let class_info_ty = inference.expression_type(second_arg); + let use_generic_filtering = is_positive + && !self + .db + .analysis_settings(self.scope().file(self.db)) + .strict_generic_narrowing; function - .generate_constraint(self.db, class_info_ty, is_positive) + .generate_constraint( + db, + &self.env, + class_info_ty, + is_positive, + use_generic_filtering, + ) .map(|constraint| { NarrowingConstraints::from_iter([( place, - NarrowingConstraint::intersection( - constraint.negate_if(self.db, !is_positive), - ), + if use_generic_filtering { + NarrowingConstraint::generic_filtering(constraint) + } else { + NarrowingConstraint::intersection(constraint.negate_if( + db, + &self.env, + !is_positive, + )) + }, )]) }) } @@ -3946,7 +4658,7 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { 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) => + && class_type.is_known(db, KnownClass::Bool) => { self.evaluate_expression_node_predicate( &expr_call.arguments.args[0], @@ -3966,26 +4678,27 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { expr_call: &ast::ExprCall, is_positive: bool, ) -> Option> { + let db = self.db; let return_ty = inference.expression_type(expr_call); let place_and_constraint = match return_ty { Type::TypeIs(type_is) => { - let (_, place) = type_is.place_info(self.db)?; + let (_, place) = type_is.place_info(db)?; Some(( place, - NarrowingConstraint::intersection( - type_is - .return_type(self.db) - .negate_if(self.db, !is_positive), - ), + NarrowingConstraint::intersection(type_is.return_type(db).negate_if( + db, + &self.env, + !is_positive, + )), )) } // TypeGuard only narrows in the positive case Type::TypeGuard(type_guard) if is_positive => { - let (_, place) = type_guard.place_info(self.db)?; + let (_, place) = type_guard.place_info(db)?; Some(( place, - NarrowingConstraint::replacement(type_guard.return_type(self.db)), + NarrowingConstraint::replacement(type_guard.return_type(db)), )) } _ => None, @@ -3999,10 +4712,11 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { subject: Expression<'db>, singleton: ast::Singleton, ) -> Option> { - let subject = PlaceExpr::try_from_expr(subject.node_ref(self.db).node(self.module))?; + let db = self.db; + let subject = PlaceExpr::try_from_expr(subject.node_ref(db).node(self.module))?; let place = self.expect_place(&subject); - let ty = singleton_pattern_type(self.db, singleton).negate(self.db); + let ty = singleton_pattern_type(db, &self.env, singleton).negate(db, &self.env); Some(NarrowingConstraints::from_iter([( place, NarrowingConstraint::intersection(ty), @@ -4014,18 +4728,19 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { subject: Expression<'db>, pattern: &PatternPredicateKind<'db>, ) -> Option> { - let subject_place = PlaceExpr::try_from_expr(subject.node_ref(self.db).node(self.module))?; + let db = self.db; + let subject_place = PlaceExpr::try_from_expr(subject.node_ref(db).node(self.module))?; let place = self.expect_place(&subject_place); - let subject_ty = infer_same_file_expression_type(self.db, subject, TypeContext::default()); + let subject_ty = infer_same_file_expression_type(db, subject, TypeContext::default()); let definitely_matched = - definite_match_pattern_type_for_subject(self.db, pattern, subject_ty); + definite_match_pattern_type_for_subject(db, &self.env, pattern, subject_ty); if definitely_matched.is_never() { return None; } Some(NarrowingConstraints::from_iter([( place, - NarrowingConstraint::intersection(definitely_matched.negate(self.db)), + NarrowingConstraint::intersection(definitely_matched.negate(db, &self.env)), )])) } @@ -4035,7 +4750,8 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { kind: &SequencePatternPredicateKind<'db>, pattern: &PatternPredicateKind<'db>, ) -> PatternNarrowingResult<'db> { - let subject_node = subject.node_ref(self.db).node(self.module); + let db = self.db; + let subject_node = subject.node_ref(db).node(self.module); // A tuple or list expression has no place that can be narrowed as a whole. For example: // @@ -4053,8 +4769,8 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { return PatternNarrowingResult::Possible(None); }; - let subject_ty = infer_same_file_expression_type(self.db, subject, TypeContext::default()); - let narrowed_ty = pattern_binding_fallthrough_type(self.db, pattern, subject_ty); + let subject_ty = infer_same_file_expression_type(db, subject, TypeContext::default()); + let narrowed_ty = pattern_binding_fallthrough_type(db, &self.env, pattern, subject_ty); if narrowed_ty == subject_ty { return PatternNarrowingResult::Possible(None); } @@ -4136,6 +4852,7 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { pattern: &PatternPredicateKind<'db>, target: Option, ) -> PatternNarrowingResult<'db> { + let db = self.db; if let Some(elements) = Self::sequence_expression_elements(subject) { return match pattern { PatternPredicateKind::Sequence(kind) => self @@ -4172,15 +4889,14 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { let Some(subject) = PlaceExpr::try_from_expr(subject_expr) else { return PatternNarrowingResult::Possible(None); }; - let subject_ty = - infer_expression_types(self.db, subject_expression, TypeContext::default()) - .expression_type(subject_expr); + let subject_ty = infer_expression_types(db, subject_expression, TypeContext::default()) + .expression_type(subject_expr); let Some(constraint) = self.positive_subject_constraint(pattern, subject_ty) else { return PatternNarrowingResult::Possible(None); }; if NarrowingConstraint::intersection(subject_ty) .merge_constraint_and(constraint.clone()) - .evaluate_constraint_type(self.db) + .evaluate_constraint_type(db, &self.env) .is_never() { return PatternNarrowingResult::Impossible; @@ -4202,13 +4918,14 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { value: Expression<'db>, is_positive: bool, ) -> Option> { - let subject_node = subject.node_ref(self.db).node(self.module); + let db = self.db; + let subject_node = subject.node_ref(db).node(self.module); let place = { let subject = PlaceExpr::try_from_expr(subject_node)?; self.expect_place(&subject) }; - let subject_ty = infer_same_file_expression_type(self.db, subject, TypeContext::default()); - let value_ty = infer_same_file_expression_type(self.db, value, TypeContext::default()); + let subject_ty = infer_same_file_expression_type(db, subject, TypeContext::default()); + let value_ty = infer_same_file_expression_type(db, value, TypeContext::default()); let mut constraints = self .evaluate_expr_compare_op(subject_ty, value_ty, ast::CmpOp::Eq, is_positive) @@ -4230,7 +4947,7 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { // // Like in the `if` statement case, we're constraining `union` itself, not `union["tag"]`. if let ast::Expr::Subscript(subscript) = subject_node { - let inference = infer_expression_types(self.db, subject, TypeContext::default()); + let inference = infer_expression_types(db, subject, TypeContext::default()); if let Some((place, constraint)) = self.narrow_typeddict_subscript( inference.expression_type(&*subscript.value), &subscript.value, @@ -4251,12 +4968,13 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { constraints.insert(place, constraint); } } else if let ast::Expr::Attribute(attribute) = subject_node { - let inference = infer_expression_types(self.db, subject, TypeContext::default()); + let inference = infer_expression_types(db, subject, TypeContext::default()); if let Some((place, constraint)) = self.narrow_nominal_attribute( inference.expression_type(&*attribute.value), &attribute.value, attribute.attr.id(), value_ty, + NominalAttributeComparison::Equality, is_positive, ) { constraints.insert(place, constraint); @@ -4272,13 +4990,15 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { expression: Expression<'db>, is_positive: bool, ) -> Option> { - let inference = infer_expression_types(self.db, expression, TypeContext::default()); + let db = self.db; + let inference = infer_expression_types(db, expression, TypeContext::default()); + let env = self.env.clone(); let sub_constraints = expr_bool_op .values .iter() // filter our arms with statically known truthiness .filter(|expr| { - inference.expression_type(*expr).bool(self.db) + inference.expression_type(*expr).bool(db, &env) != match expr_bool_op.op { BoolOp::And => Truthiness::AlwaysTrue, BoolOp::Or => Truthiness::AlwaysFalse, @@ -4331,8 +5051,10 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { rhs_type: Type<'db>, is_equality: bool, ) -> Option<(ScopedPlaceId, NarrowingConstraint<'db>)> { + let env = self.env.clone(); + let db = self.db; // Check preconditions: we need a TypedDict, a string key, and a supported tag literal. - if !is_or_contains_typeddict(self.db, subscript_value_type) { + if !is_or_contains_typeddict(db, &env, subscript_value_type) { return None; } let subscript_place_expr = PlaceExpr::try_from_expr(subscript_value_expr)?; @@ -4350,21 +5072,21 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { // literal type" without worrying about what other types might be present. if is_equality && !all_matching_typeddict_fields_have_literal_types( - self.db, + db, + &self.env, subscript_value_type, - key_literal.value(self.db), + key_literal.value(db), ) { return None; } - - let field_name = Name::from(key_literal.value(self.db)); + let field_name = Name::from(key_literal.value(db)); // To avoid excluding non-`TypedDict` types, our constraints are always expressed // as a negative intersection (i.e. "you're *not* this kind of `TypedDict`"). If // `is_equality` is true, the whole constraint is going to be a double // negative, i.e. "you're *not* a `TypedDict` *without* this literal field". As the // first step of building that, we negate the right hand side. - let field_type = rhs_type.negate_if(self.db, is_equality); + let field_type = rhs_type.negate_if(db, &self.env, is_equality); // Create the synthesized `TypedDict` with that (possibly negated) field. We don't // want to constrain the mutability or required-ness of the field, so the most // compatible form is not-required and read-only. @@ -4373,9 +5095,9 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { .read_only(true) .build(); let schema = TypedDictSchema::from_iter([(field_name, field)]); - let synthesized_typeddict = TypedDictType::from_schema_items(self.db, schema); + let synthesized_typeddict = TypedDictType::from_schema_items(db, schema); // As mentioned above, the synthesized `TypedDict` is always negated. - let intersection = Type::TypedDict(synthesized_typeddict).negate(self.db); + let intersection = Type::TypedDict(synthesized_typeddict).negate(db, &self.env); let place = self.expect_place(&subscript_place_expr); Some((place, NarrowingConstraint::intersection(intersection))) } @@ -4388,7 +5110,9 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { subscript_key_type: Type<'db>, is_positive: bool, ) -> Option<(ScopedPlaceId, NarrowingConstraint<'db>)> { - if !is_or_contains_typeddict(self.db, subscript_value_type) { + let env = self.env.clone(); + let db = self.db; + if !is_or_contains_typeddict(db, &env, subscript_value_type) { return None; } let subscript_place_expr = PlaceExpr::try_from_expr(subscript_value_expr)?; @@ -4403,9 +5127,9 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { .required(false) .read_only(true) .build(); - let schema = TypedDictSchema::from_iter([(Name::from(key_literal.value(self.db)), field)]); - let synthesized_typeddict = TypedDictType::from_schema_items(self.db, schema); - let intersection = Type::TypedDict(synthesized_typeddict).negate(self.db); + let schema = TypedDictSchema::from_iter([(Name::from(key_literal.value(db)), field)]); + let synthesized_typeddict = TypedDictType::from_schema_items(db, schema); + let intersection = Type::TypedDict(synthesized_typeddict).negate(db, &self.env); let place = self.expect_place(&subscript_place_expr); Some((place, NarrowingConstraint::intersection(intersection))) } @@ -4414,23 +5138,31 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { // `NarrowingConstraint::intersection` at the call site instead of constructing a replacement // type here. fn narrow_with_present_key(&self, ty: Type<'db>, key: &str) -> Type<'db> { + let env = self.env.clone(); let db = self.db; let constrain = |ty, key_presence_constraint| { - IntersectionType::from_two_elements(db, ty, key_presence_constraint) + IntersectionType::from_two_elements(db, &self.env, ty, key_presence_constraint) }; - match ty.resolve_type_alias(self.db) { - Type::Union(union) => union.map(self.db, |element| { + match ty.resolve_type_alias(db) { + Type::Union(union) => union.map(db, &self.env, |element| { self.narrow_with_present_key(*element, key) }), - resolved if typeddict_declares_key(self.db, resolved, key) => resolved, + Type::TypedDict(typed_dict) + if typed_dict + .key_membership_truthiness(db, key) + .is_always_false() => + { + Type::Never + } + resolved if typeddict_declares_key(db, resolved, key) => resolved, // TODO: Extend this to subtypes of `Mapping[str, object]` whose membership and // subscript operations obey the `Mapping` contract. - resolved if is_or_contains_typeddict(self.db, resolved) => constrain( + resolved if is_or_contains_typeddict(db, &env, resolved) => constrain( ty, - Type::TypedDict(required_typeddict_key(self.db, key, Type::object())), + Type::TypedDict(required_typeddict_key(db, key, Type::object())), ), - _ => constrain(ty, key_membership_contains_protocol(self.db, key)), + _ => constrain(ty, key_membership_contains_protocol(db, &self.env, key)), } } @@ -4456,8 +5188,9 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { rhs_type: Type<'db>, is_equality: bool, ) -> Option<(ScopedPlaceId, NarrowingConstraint<'db>)> { + let db = self.db; // We need a union type for narrowing to be useful. - let Type::Union(union) = subscript_value_type.resolve_type_alias(self.db) else { + let Type::Union(union) = subscript_value_type.resolve_type_alias(db) else { return None; }; @@ -4471,30 +5204,31 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { } let subscript_place_expr = PlaceExpr::try_from_expr(subscript_value_expr)?; - // Skip narrowing if any tuple in the union has an out-of-bounds index. // A diagnostic will be emitted elsewhere for the out-of-bounds access. - if any_tuple_has_out_of_bounds_index(self.db, union, index) { + if any_tuple_has_out_of_bounds_index(db, &self.env, union, index) { return None; } // For equality constraints, all matching elements must have literal types to safely narrow. // For inequality constraints, we can narrow even with non-literal element types. - if is_equality && !all_matching_tuple_elements_have_literal_types(self.db, union, index) { + if is_equality + && !all_matching_tuple_elements_have_literal_types(db, &self.env, union, index) + { return None; } // Filter the union based on whether each tuple element at the index could match the rhs. - let filtered = union.filter(self.db, |elem| { - elem.tuple_instance_spec(self.db) - .and_then(|spec| spec.py_index(self.db, index).ok()) + let filtered = union.filter(db, |elem| { + elem.tuple_instance_spec(db, &self.env) + .and_then(|spec| spec.py_index(db, &self.env, index).ok()) .is_none_or(|el_ty| { if is_equality { // Keep tuples where element could be equal to rhs. - !el_ty.is_disjoint_from(self.db, rhs_type) + !el_ty.is_disjoint_from(db, &self.env, rhs_type) } else { // Keep tuples where element is not always equal to rhs. - !el_ty.is_subtype_of(self.db, rhs_type) + !el_ty.is_subtype_of(db, &self.env, rhs_type) } }) }); @@ -4514,24 +5248,38 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { attribute_value_expr: &ast::Expr, attribute_name: &str, rhs_type: Type<'db>, - is_equality: bool, + comparison: NominalAttributeComparison, + is_positive: bool, ) -> Option<(ScopedPlaceId, NarrowingConstraint<'db>)> { - let Type::Union(union) = attribute_value_type.resolve_type_alias(self.db) else { + let db = self.db; + let Type::Union(union) = attribute_value_type.resolve_type_alias(db) else { return None; }; - if !is_supported_tag_literal(rhs_type) { + + if comparison == NominalAttributeComparison::Equality && !is_supported_tag_literal(rhs_type) + { return None; } - let narrowed = union.filter(self.db, |element| { - nominal_attribute_type(self.db, *element, attribute_name).is_none_or(|attribute_type| { - if is_equality { - !is_supported_tag_literal(attribute_type) - || !attribute_type.is_disjoint_from(self.db, rhs_type) - } else { - !attribute_type.is_subtype_of(self.db, rhs_type) - } - }) + let narrowed = union.filter(db, |element| { + element + .resolve_type_alias(db) + .member(db, &self.env, attribute_name) + .place + .ignore_possibly_undefined() + .is_none_or(|attribute_type| match (comparison, is_positive) { + (NominalAttributeComparison::Equality, true) => { + !is_supported_tag_literal(attribute_type) + || !attribute_type.is_disjoint_from(db, &self.env, rhs_type) + } + (NominalAttributeComparison::Equality, false) => { + !attribute_type.is_subtype_of(db, &self.env, rhs_type) + } + (NominalAttributeComparison::Identity, is_positive) => attribute_type + .identity_comparison_truthiness(db, &self.env, rhs_type) + .negate_if(!is_positive) + .may_be_true(), + }) }); if narrowed == Type::Union(union) { @@ -4550,19 +5298,25 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { attribute_name: &str, is_positive: bool, ) -> Option<(ScopedPlaceId, NarrowingConstraint<'db>)> { - let Type::Union(union) = attribute_value_type.resolve_type_alias(self.db) else { + let db = self.db; + let Type::Union(union) = attribute_value_type.resolve_type_alias(db) else { return None; }; - let narrowed = union.filter(self.db, |element| { - nominal_attribute_type(self.db, *element, attribute_name).is_none_or(|attribute_type| { - let truthiness = attribute_type.bool(self.db); - if is_positive { - !truthiness.is_always_false() - } else { - !truthiness.is_always_true() - } - }) + let narrowed = union.filter(db, |element| { + element + .resolve_type_alias(db) + .member(db, &self.env, attribute_name) + .place + .ignore_possibly_undefined() + .is_none_or(|attribute_type| { + let truthiness = attribute_type.bool(db, &self.env); + if is_positive { + !truthiness.is_always_false() + } else { + !truthiness.is_always_true() + } + }) }); if narrowed == Type::Union(union) { @@ -4577,25 +5331,37 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { // Return true if the given type is a `TypedDict` or a union or intersection that includes at least // one `TypedDict` (even if other types are also present), or a type alias to such a type. -fn is_or_contains_typeddict<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { +fn is_or_contains_typeddict<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> bool { match ty { - Type::Overlapping(overlapping) => is_or_contains_typeddict(db, overlapping.value_type(db)), - Type::Restricted(restricted) => is_or_contains_typeddict(db, restricted.value_type(db)), - Type::Deferred(deferred) => is_or_contains_typeddict(db, deferred.reduced(db)), + Type::Overlapping(overlapping) => { + is_or_contains_typeddict(db, env, overlapping.value_type(db, env)) + } + Type::Restricted(restricted) => { + is_or_contains_typeddict(db, env, restricted.value_type(db)) + } + Type::Deferred(deferred) => is_or_contains_typeddict(db, env, deferred.reduced(db, env)), Type::TypedDict(_) => true, - Type::Intersection(intersection) => intersection - .positive(db) - .iter() - .any(|intersection_element_ty| is_or_contains_typeddict(db, *intersection_element_ty)), + Type::Intersection(intersection) => { + intersection + .positive(db) + .iter() + .any(|intersection_element_ty| { + is_or_contains_typeddict(db, env, *intersection_element_ty) + }) + } Type::Union(union) => union .elements(db) .iter() - .any(|union_member_ty| is_or_contains_typeddict(db, *union_member_ty)), + .any(|union_member_ty| is_or_contains_typeddict(db, env, *union_member_ty)), Type::UnsafeUnion(unsafe_union) => unsafe_union .elements(db) .iter() - .any(|element| is_or_contains_typeddict(db, *element)), - Type::TypeAlias(alias) => is_or_contains_typeddict(db, alias.value_type(db)), + .any(|element| is_or_contains_typeddict(db, env, *element)), + Type::TypeAlias(alias) => is_or_contains_typeddict(db, env, alias.value_type(db)), Type::Dynamic(_) | Type::Divergent(_) @@ -4690,7 +5456,11 @@ fn required_typeddict_key<'db>( /// /// Non-`TypedDict` union arms therefore receive this `__contains__` protocol instead of the /// synthesized `TypedDict` used for `TypedDict` arms. -fn key_membership_contains_protocol<'db>(db: &'db dyn Db, key: &str) -> Type<'db> { +fn key_membership_contains_protocol<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + key: &str, +) -> Type<'db> { let signature = Signature::new( Parameters::standard([ Parameter::positional_only(Some(Name::new_static("self"))), @@ -4702,6 +5472,7 @@ fn key_membership_contains_protocol<'db>(db: &'db dyn Db, key: &str) -> Type<'db Type::protocol_with_methods( db, + env, [("__contains__", CallableType::function_like(db, signature))], ) } @@ -4718,28 +5489,13 @@ fn is_supported_tag_literal(ty: Type) -> bool { ) } -fn nominal_attribute_type<'db>( - db: &'db dyn Db, - ty: Type<'db>, - attribute_name: &str, -) -> Option> { - let resolved_ty = ty.resolve_type_alias(db); - if resolved_ty.is_nominal_instance() { - resolved_ty - .member(db, attribute_name) - .place - .ignore_possibly_undefined() - } else { - None - } -} - // Return true if the given type is a `TypedDict` whose `field_name` field has a supported tag literal // type, or a union in which all elements that are `TypedDict`s have a supported tag literal type // for that field, or an intersection in which all positive elements that are `TypedDict`s have a // supported tag literal type for that field, or a type alias to such a type. fn all_matching_typeddict_fields_have_literal_types<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, field_name: &str, ) -> bool { @@ -4754,41 +5510,51 @@ fn all_matching_typeddict_fields_have_literal_types<'db>( match ty { Type::TypedDict(td) => matching_field_is_literal(&td), Type::Union(union) => union.elements(db).iter().all(|union_member_ty| { - !is_or_contains_typeddict(db, *union_member_ty) + !is_or_contains_typeddict(db, env, *union_member_ty) || all_matching_typeddict_fields_have_literal_types( db, + env, *union_member_ty, field_name, ) }), Type::UnsafeUnion(unsafe_union) => unsafe_union.elements(db).iter().all(|element| { - !is_or_contains_typeddict(db, *element) - || all_matching_typeddict_fields_have_literal_types(db, *element, field_name) + !is_or_contains_typeddict(db, env, *element) + || all_matching_typeddict_fields_have_literal_types(db, env, *element, field_name) }), Type::Overlapping(overlapping) => all_matching_typeddict_fields_have_literal_types( db, - overlapping.value_type(db), + env, + overlapping.value_type(db, env), field_name, ), Type::Restricted(restricted) => all_matching_typeddict_fields_have_literal_types( db, + env, restricted.value_type(db), field_name, ), - Type::Deferred(deferred) => { - all_matching_typeddict_fields_have_literal_types(db, deferred.reduced(db), field_name) - } - Type::TypeAlias(alias) => { - all_matching_typeddict_fields_have_literal_types(db, alias.value_type(db), field_name) - } + Type::Deferred(deferred) => all_matching_typeddict_fields_have_literal_types( + db, + env, + deferred.reduced(db, env), + field_name, + ), + Type::TypeAlias(alias) => all_matching_typeddict_fields_have_literal_types( + db, + env, + alias.value_type(db), + field_name, + ), Type::Intersection(intersection) => { intersection .positive(db) .iter() .all(|intersection_member_ty| { - !is_or_contains_typeddict(db, *intersection_member_ty) + !is_or_contains_typeddict(db, env, *intersection_member_ty) || all_matching_typeddict_fields_have_literal_types( db, + env, *intersection_member_ty, field_name, ) @@ -4828,7 +5594,7 @@ fn all_matching_typeddict_fields_have_literal_types<'db>( | Type::NewTypeInstance(_) => { unreachable!( "invalid type {} in all_matching_typeddict_fields_have_literal_types", - ty.display(db) + ty.display(db, env) ) } } @@ -4840,12 +5606,13 @@ fn all_matching_typeddict_fields_have_literal_types<'db>( /// since a diagnostic will be emitted elsewhere for the out-of-bounds access. fn any_tuple_has_out_of_bounds_index<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, union: UnionType<'db>, index: i32, ) -> bool { union.elements(db).iter().any(|elem| { - elem.tuple_instance_spec(db) - .is_some_and(|spec| spec.py_index(db, index).is_err()) + elem.tuple_instance_spec(db, env) + .is_some_and(|spec| spec.py_index(db, env, index).is_err()) }) } @@ -4857,24 +5624,38 @@ fn any_tuple_has_out_of_bounds_index<'db>( /// `__eq__` in unexpected ways. fn all_matching_tuple_elements_have_literal_types<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, union: UnionType<'db>, index: i32, ) -> bool { union.elements(db).iter().all(|elem| { - elem.tuple_instance_spec(db) - .and_then(|spec| spec.py_index(db, index).ok()) + elem.tuple_instance_spec(db, env) + .and_then(|spec| spec.py_index(db, env, index).ok()) .is_none_or(is_supported_tag_literal) }) } pub(crate) trait NarrowingEvaluatorExtension<'db> { - fn narrow(&self, db: &'db dyn Db, base_type: Type<'db>, place: ScopedPlaceId) -> Type<'db>; + fn narrow( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + base_type: Type<'db>, + place: ScopedPlaceId, + ) -> Type<'db>; } impl<'db> NarrowingEvaluatorExtension<'db> for NarrowingEvaluator<'_, 'db> { - fn narrow(&self, db: &'db dyn Db, base_type: Type<'db>, place: ScopedPlaceId) -> Type<'db> { + fn narrow( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + base_type: Type<'db>, + place: ScopedPlaceId, + ) -> Type<'db> { narrow_type_by_constraint( db, + env, self.narrowing_constraints(), self.predicates(), self.constraint(), diff --git a/crates/ty_python_semantic/src/types/narrow/containment.rs b/crates/ty_python_semantic/src/types/narrow/containment.rs index 9f39b85e36..8af46e2086 100644 --- a/crates/ty_python_semantic/src/types/narrow/containment.rs +++ b/crates/ty_python_semantic/src/types/narrow/containment.rs @@ -1,7 +1,6 @@ -use crate::{ - Db, - types::{ClassBase, IntersectionBuilder, KnownClass, Type, UnionBuilder}, -}; +use crate::Db; +use crate::ProgramEnvironment; +use crate::types::{ClassBase, IntersectionBuilder, KnownClass, Type, UnionBuilder}; enum ContainmentBehavior<'db> { /// Membership compares against the elements yielded by the wrapped type. Callers use @@ -14,7 +13,11 @@ enum ContainmentBehavior<'db> { } /// Return the containment behavior known for this type. -fn containment_behavior<'db>(db: &'db dyn Db, ty: Type<'db>) -> ContainmentBehavior<'db> { +fn containment_behavior<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> ContainmentBehavior<'db> { let ty = ty.resolve_type_alias(db); match ty { @@ -23,10 +26,10 @@ fn containment_behavior<'db>(db: &'db dyn Db, ty: Type<'db>) -> ContainmentBehav // through wrappers such as type variables. Positive unions that contain string // literals are distributed in `evaluate_expr_in` instead because substring semantics // depend on the value of each literal haystack. - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); let mut has_unknown_behavior = false; for element in union.elements(db) { - match containment_behavior(db, *element) { + match containment_behavior(db, env, *element) { ContainmentBehavior::ElementsOf(elements_of) => { builder = builder.add(elements_of); } @@ -40,13 +43,15 @@ fn containment_behavior<'db>(db: &'db dyn Db, ty: Type<'db>) -> ContainmentBehav ContainmentBehavior::ElementsOf(builder.build()) } } - Type::TypeVar(type_var) => type_var - .typevar(db) - .bound_or_constraints(db) - .map_or(ContainmentBehavior::Unknown, |bound_or_constraints| { - containment_behavior(db, bound_or_constraints.as_type(db)) - }), - Type::NewTypeInstance(newtype) => containment_behavior(db, newtype.concrete_base_type(db)), + Type::TypeVar(type_var) => type_var.typevar(db).bound_or_constraints(db, env).map_or( + ContainmentBehavior::Unknown, + |bound_or_constraints| { + containment_behavior(db, env, bound_or_constraints.as_type(db, env)) + }, + ), + Type::NewTypeInstance(newtype) => { + containment_behavior(db, env, newtype.concrete_base_type(db)) + } Type::Intersection(intersection) => { // Preserve the narrowing already supported on main for unsimplified intersections // such as `Iterable[T] & tuple[object, ...]`. Replacing the component that establishes @@ -59,8 +64,8 @@ fn containment_behavior<'db>(db: &'db dyn Db, ty: Type<'db>) -> ContainmentBehav // https://github.com/astral-sh/ruff/pull/26365 let mut has_elements_of = false; let mut has_custom_behavior = false; - let elements_of = - intersection.map_positive(db, |element| match containment_behavior(db, *element) { + let elements_of = intersection.map_positive(db, env, |element| { + match containment_behavior(db, env, *element) { ContainmentBehavior::ElementsOf(elements_of) => { has_elements_of = true; elements_of @@ -70,7 +75,8 @@ fn containment_behavior<'db>(db: &'db dyn Db, ty: Type<'db>) -> ContainmentBehav *element } ContainmentBehavior::Unknown => *element, - }); + } + }); if has_custom_behavior { ContainmentBehavior::Custom } else if has_elements_of { @@ -83,7 +89,7 @@ fn containment_behavior<'db>(db: &'db dyn Db, ty: Type<'db>) -> ContainmentBehav Type::NominalInstance(instance) => { // Walk the MRO until we find either a visible override or a supported built-in // implementation. - for base in instance.class(db).iter_mro(db) { + for base in instance.class(db, env).iter_mro(db) { let class = match base { ClassBase::Class(class) => class, ClassBase::Generic | ClassBase::Protocol => continue, @@ -113,16 +119,16 @@ fn containment_behavior<'db>(db: &'db dyn Db, ty: Type<'db>) -> ContainmentBehav // takes precedence over `__iter__` for containment checks, but this is only // relevant to us for built-ins, since user types with `__contains__` have // containment behavior that we can't understand and don't try to model.) - return ContainmentBehavior::ElementsOf(Type::instance(db, class)); + return ContainmentBehavior::ElementsOf(Type::instance(db, env, class)); } if !class - .own_class_member(db, None, "__contains__") + .own_class_member(db, env, None, "__contains__") .is_undefined() { return ContainmentBehavior::Custom; } } - if instance.class(db).is_final(db) { + if instance.class(db, env).is_final(db) { ContainmentBehavior::ElementsOf(ty) } else { ContainmentBehavior::Unknown @@ -133,8 +139,12 @@ fn containment_behavior<'db>(db: &'db dyn Db, ty: Type<'db>) -> ContainmentBehav } /// Return the type whose iterated elements may satisfy membership for `ty`. -pub(super) fn elements_of<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option> { - match containment_behavior(db, ty) { +pub(super) fn elements_of<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Option> { + match containment_behavior(db, env, ty) { ContainmentBehavior::ElementsOf(elements_of) => Some(elements_of), ContainmentBehavior::Custom | ContainmentBehavior::Unknown => None, } @@ -146,18 +156,20 @@ const MAX_STRING_MEMBERSHIP_EXCLUSIONS: usize = 128; /// Narrow membership in a known string literal using substring semantics. pub(super) fn narrow_string_membership<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, lhs_ty: Type<'db>, haystack: &str, is_contained: bool, ) -> Option> { let lhs_ty = lhs_ty.resolve_type_alias(db); - let flattened_lhs_ty = lhs_ty.flatten_typevars(db); + let flattened_lhs_ty = lhs_ty.flatten_typevars(db, env); let keep = |element: &Type<'db>| { let element = element.resolve_type_alias(db); if let Some(needle) = element.as_string_literal() { haystack.contains(needle.value(db)) == is_contained } else { - !(is_contained && element.is_disjoint_from(db, KnownClass::Str.to_instance(db))) + !(is_contained + && element.is_disjoint_from(db, env, KnownClass::Str.to_instance(db, env))) } }; @@ -173,9 +185,9 @@ pub(super) fn narrow_string_membership<'db>( .nth(MAX_STRING_MEMBERSHIP_EXCLUSIONS) .is_none() { - let mut builder = IntersectionBuilder::new(db).add_positive(narrowed); + let mut builder = IntersectionBuilder::new(db, env).add_positive(narrowed); for character in haystack.chars() { - builder = builder.add_negative(Type::single_char_string_literal(db, character)); + builder.add_negative_in_place(Type::single_char_string_literal(db, character)); } narrowed = builder.build(); } diff --git a/crates/ty_python_semantic/src/types/newtype.rs b/crates/ty_python_semantic/src/types/newtype.rs index d061bfc80c..40e7703fc3 100644 --- a/crates/ty_python_semantic/src/types/newtype.rs +++ b/crates/ty_python_semantic/src/types/newtype.rs @@ -1,9 +1,10 @@ use crate::Db; +use crate::ProgramEnvironment; use crate::types::constraints::ConstraintSet; use crate::types::relation::{DisjointnessChecker, TypeRelation, TypeRelationChecker}; use crate::types::{ClassType, KnownUnion, Type, definition_expression_type, visitor}; use ruff_db::parsed::parsed_module; -use ruff_python_ast as ast; +use ruff_python_ast::{self as ast}; use rustc_hash::FxHashSet; use ty_python_core::definition::{Definition, DefinitionKind}; @@ -54,16 +55,22 @@ impl<'db> NewType<'db> { #[salsa::tracked( returns(copy), - cycle_initial=|db, _, _| NewTypeBase::ClassType(ClassType::object(db)), + cycle_initial=|db, _, self_: NewType<'db>| NewTypeBase::ClassType(ClassType::object( + db, + &ProgramEnvironment::from_definition(self_.definition(db)), + )), heap_size=ruff_memory_usage::heap_size )] fn lazy_base(self, db: &'db dyn Db) -> NewTypeBase<'db> { // `TypeInferenceBuilder` emits diagnostics for invalid `NewType` definitions that show up // in assignments, but invalid definitions still get here, and also `NewType` might show up // in places that aren't definitions at all. Fall back to `object` in all error cases. - let object_fallback = NewTypeBase::ClassType(ClassType::object(db)); let definition = self.definition(db); - let module = parsed_module(db, definition.file(db)).load(db); + let program_file = definition.program_file(db); + let python_file = program_file.python_file(db); + let env = ProgramEnvironment::from_file(program_file); + let object_fallback = NewTypeBase::ClassType(ClassType::object(db, &env)); + let module = parsed_module(db, python_file).load(db); let DefinitionKind::Assignment(assignment) = definition.kind(db) else { return object_fallback; }; @@ -75,7 +82,7 @@ impl<'db> NewType<'db> { }; match definition_expression_type(db, definition, second_arg) { Type::NominalInstance(nominal_instance_type) => { - NewTypeBase::ClassType(nominal_instance_type.class(db)) + NewTypeBase::ClassType(nominal_instance_type.class(db, &env)) } Type::NewTypeInstance(newtype) => NewTypeBase::NewType(newtype), // There are exactly two union types allowed as bases for NewType: `int | float` and @@ -105,13 +112,16 @@ impl<'db> NewType<'db> { for base in self.iter_bases(db) { match base { NewTypeBase::NewType(_) => continue, - concrete => return concrete.instance_type(db), + concrete => { + let env = ProgramEnvironment::from_definition(self.definition(db)); + return concrete.instance_type(db, &env); + } } } Type::object() } - pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { + fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { // Two instances of the "same" `NewType` won't compare == if one of them has an eagerly // evaluated base (or a normalized base, etc.) and the other doesn't, so we only check for // equality of the `definition`. @@ -121,7 +131,7 @@ impl<'db> NewType<'db> { /// Create a new `NewType` by mapping the underlying `ClassType`. This descends through any /// number of nested `NewType` layers and rebuilds the whole chain. In the rare case of cyclic /// `NewType`s with no underlying `ClassType`, this has no effect and does not call `f`. - pub(crate) fn try_map_base_class_type( + fn try_map_base_class_type( self, db: &'db dyn Db, f: impl FnOnce(ClassType<'db>) -> Option>, @@ -182,11 +192,12 @@ impl<'db> NewType<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { let eager_base = match self.eager_base(db) { - Some(base) => Some(base.recursive_type_normalized_impl(db, div, nested)?), + Some(base) => Some(base.recursive_type_normalized_impl(db, env, div, nested)?), None => None, }; @@ -233,6 +244,7 @@ impl<'c, 'db> DisjointnessChecker<'_, 'c, 'db> { // Two NewTypes are disjoint if they're not equal and neither inherits from the other. // NewTypes have single inheritance, and a regular class can't inherit from a NewType, so // it's not possible for some third type to multiply-inherit from both. + let relation_checker = self.as_relation_checker(TypeRelation::Subtyping); relation_checker .check_newtype_pair(db, left, right) @@ -254,7 +266,7 @@ pub(crate) fn walk_newtype_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Si newtype.eager_base(db) }; if let Some(base) = base { - visitor.visit_type(db, base.instance_type(db)); + visitor.visit_type(db, base.instance_type(db, visitor.program_environment())); } } @@ -272,27 +284,28 @@ pub enum NewTypeBase<'db> { } impl<'db> NewTypeBase<'db> { - pub fn instance_type(self, db: &'db dyn Db) -> Type<'db> { + pub fn instance_type(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match self { - NewTypeBase::ClassType(class_type) => Type::instance(db, class_type), + NewTypeBase::ClassType(class_type) => Type::instance(db, env, class_type), NewTypeBase::NewType(newtype) => Type::NewTypeInstance(newtype), - NewTypeBase::Float => KnownUnion::Float.to_type(db), - NewTypeBase::Complex => KnownUnion::Complex.to_type(db), + NewTypeBase::Float => KnownUnion::Float.to_type(db, env), + NewTypeBase::Complex => KnownUnion::Complex.to_type(db, env), } } fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { match self { NewTypeBase::ClassType(class_type) => class_type - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(NewTypeBase::ClassType), NewTypeBase::NewType(newtype) => newtype - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(NewTypeBase::NewType), NewTypeBase::Float | NewTypeBase::Complex => Some(self), } diff --git a/crates/ty_python_semantic/src/types/overlapping.rs b/crates/ty_python_semantic/src/types/overlapping.rs index 2214929534..227f8d3633 100644 --- a/crates/ty_python_semantic/src/types/overlapping.rs +++ b/crates/ty_python_semantic/src/types/overlapping.rs @@ -21,6 +21,7 @@ use super::variance::VarianceInferable; use super::{BoundTypeVarIdentity, Type, TypeVarVariance, visitor}; use crate::Db; +use crate::types::ProgramEnvironment; #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub struct OverlappingType<'db> { @@ -48,12 +49,12 @@ impl<'db> OverlappingType<'db> { /// method body*: the upper bound of the wrapped type argument. A covariant /// `Key` is thereby erased to its bound (`object` when unbounded), so it can /// never be written back into `Key`-typed storage. - pub(crate) fn value_type(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn value_type(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match self.type_argument(db) { Type::TypeVar(typevar) => typevar .typevar(db) - .bound_or_constraints(db) - .map(|bound_or_constraints| bound_or_constraints.as_type(db)) + .bound_or_constraints(db, env) + .map(|bound_or_constraints| bound_or_constraints.as_type(db, env)) .unwrap_or_else(Type::object), other => other, } @@ -65,9 +66,13 @@ impl<'db> Type<'db> { /// such a parameter (`Key`'s upper bound). Any other type is returned /// unchanged. Used when binding a parameter's declared type inside the body, /// where the marker has no place — it exists only for the call binder. - pub(crate) fn erase_overlapping(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn erase_overlapping( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { match self { - Type::Overlapping(overlapping) => overlapping.value_type(db), + Type::Overlapping(overlapping) => overlapping.value_type(db, env), _ => self, } } @@ -78,7 +83,12 @@ impl<'db> VarianceInferable<'db> for OverlappingType<'db> { // neither direction, matching the escape-hatch semantics (its whole purpose // is to let a covariant typevar appear in an input position without forcing // invariance). - fn variance_of(self, _db: &'db dyn Db, _typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { + fn variance_of( + self, + _db: &'db dyn Db, + _env: &ProgramEnvironment<'db>, + _typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { TypeVarVariance::Bivariant } } diff --git a/crates/ty_python_semantic/src/types/overrides.rs b/crates/ty_python_semantic/src/types/overrides.rs index bbb100fb37..78f74a9f65 100644 --- a/crates/ty_python_semantic/src/types/overrides.rs +++ b/crates/ty_python_semantic/src/types/overrides.rs @@ -14,7 +14,7 @@ use ruff_python_stdlib::identifiers::is_mangled_private; use rustc_hash::FxHashSet; use crate::{ - Db, Program, + Db, ProgramEnvironment, lint::LintId, place::{DefinedPlace, Place, PlaceAndQualifiers, TypeOrigin}, reachability::ReachabilityConstraintsExtension, @@ -85,7 +85,6 @@ pub(super) fn check_class<'db>( if configuration.check_method_liskov_violations() && !inconsistent_generic_bases { check_inherited_method_conflicts(context, class, class_specialized, &own_class_members); } - let enum_info = enum_metadata(db, class.into()); #[expect( @@ -129,10 +128,11 @@ fn check_inherited_method_conflicts<'db>( own_class_members: &FxHashSet>, ) { let db = context.db(); + let env = &context.program_environment(); let mut direct_bases = Vec::new(); for base in class.explicit_bases(db) { - match ClassBase::try_from_explicit_base(db, *base, Some(class.into())) { + match ClassBase::try_from_explicit_base(db, env, *base, Some(class.into())) { Some(ClassBase::Class(base)) if base.static_class_literal(db).is_some() => { direct_bases.push(base); } @@ -154,7 +154,7 @@ fn check_inherited_method_conflicts<'db>( if direct_bases.iter().enumerate().any(|(index, left)| { direct_bases[index + 1..] .iter() - .any(|right| !left.could_coexist_in_mro_with(db, *right, &constraints)) + .any(|right| !left.could_coexist_in_mro_with(db, env, *right, &constraints)) }) { return; } @@ -172,7 +172,7 @@ fn check_inherited_method_conflicts<'db>( ClassBase::TypedDict(_) | ClassBase::Class(_) => return, } } - let receiver = Type::instance(db, class_specialized); + let receiver = Type::instance(db, env, class_specialized); let mut seen_names: FxHashSet<_> = own_class_members .iter() .map(|member| member.member.name.clone()) @@ -216,24 +216,24 @@ fn check_inherited_method_conflicts<'db>( continue; } let Some((selected_decorator, selected_ty)) = - source_method_contract(db, owner, receiver, name) + source_method_contract(db, env, owner, receiver, name) else { continue; }; for contract_owner in mro[index + 1..].iter().copied() { let Some((contract_decorator, contract_ty)) = - source_method_contract(db, contract_owner, receiver, name) + source_method_contract(db, env, contract_owner, receiver, name) else { continue; }; let Some((selected_ty, contract_ty)) = - method_override_types(db, selected_ty, contract_ty) + method_override_types(db, env, selected_ty, contract_ty) else { continue; }; if selected_decorator == contract_decorator - && selected_ty.is_assignable_to(db, contract_ty) + && selected_ty.is_assignable_to(db, env, contract_ty) { continue; } @@ -270,19 +270,23 @@ fn check_inherited_method_conflicts<'db>( .filter_map(ClassBase::into_class) .find(|ancestor| ancestor.class_literal(db) == contract_owner.class_literal(db)) { - let parent_receiver = Type::instance(db, owner); + let parent_receiver = Type::instance(db, env, owner); let Some((parent_decorator, parent_ty)) = - source_method_contract(db, owner, parent_receiver, name) + source_method_contract(db, env, owner, parent_receiver, name) else { continue; }; - let Some((ancestor_decorator, ancestor_ty)) = - source_method_contract(db, parent_contract_owner, parent_receiver, name) - else { + let Some((ancestor_decorator, ancestor_ty)) = source_method_contract( + db, + env, + parent_contract_owner, + parent_receiver, + name, + ) else { continue; }; if parent_decorator != ancestor_decorator - || !is_assignable_method_override(db, parent_ty, ancestor_ty) + || !is_assignable_method_override(db, env, parent_ty, ancestor_ty) { continue; } @@ -306,7 +310,7 @@ fn check_inherited_method_conflicts<'db>( name, (owner, member.first_reachable_definition, selected_decorator), (contract_owner, contract_definition, contract_decorator), - || selected_ty.assignability_error_context(db, contract_ty), + || selected_ty.assignability_error_context(db, env, contract_ty), ); continue 'members; } @@ -317,6 +321,7 @@ fn check_inherited_method_conflicts<'db>( /// Returns a source-defined method bound to the class whose MRO is being checked. fn source_method_contract<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, owner: ClassType<'db>, receiver: Type<'db>, name: &Name, @@ -335,7 +340,7 @@ fn source_method_contract<'db>( // class Conflict(ReturnsStr, ReturnsInt): ... // ``` let Type::FunctionLiteral(function) = owner - .own_class_member(db, None, name) + .own_class_member(db, env, None, name) .inner .place .raw_type()? @@ -343,8 +348,9 @@ fn source_method_contract<'db>( return None; }; let ty = Type::FunctionLiteral(function) - .try_call_dunder_get(db, Some(receiver), receiver.to_meta_type(db))? - .0; + .try_call_dunder_get(db, env, Some(receiver), receiver.to_meta_type(db, env)) + .unwrap_or_else(|error| Some(error.fallback()))? + .return_type; Some((MethodDecorator::try_from_fn_type(db, function)?, ty)) } @@ -360,7 +366,8 @@ fn enum_class_creation_manages_conflict<'db>( selected_owner: ClassType<'db>, contract_owner: ClassType<'db>, ) -> bool { - if !is_enum_class_by_inheritance(db, class) { + let env = ProgramEnvironment::from_scope(class.body_scope(db)); + if !is_enum_class_by_inheritance(db, &env, class) { return false; } @@ -372,8 +379,12 @@ fn enum_class_creation_manages_conflict<'db>( || contract_owner.is_known(db, KnownClass::Enum); } - Program::get(db).python_version(db) >= PythonVersion::PY311 - && Type::ClassLiteral(class.into()).is_subtype_of(db, KnownClass::Flag.to_subclass_of(db)) + env.python_version(db) >= PythonVersion::PY311 + && Type::ClassLiteral(class.into()).is_subtype_of( + db, + &env, + KnownClass::Flag.to_subclass_of(db, &env), + ) && matches!( name.as_str(), "__or__" | "__and__" | "__xor__" | "__ror__" | "__rand__" | "__rxor__" | "__invert__" @@ -430,15 +441,16 @@ fn check_class_declaration<'db>( member: &MemberWithDefinition<'db>, ) { let db = context.db(); + let env = &context.program_environment(); let MemberWithDefinition { member, first_reachable_definition, } = member; - let instance_of_class = Type::instance(db, class); + let instance_of_class = Type::instance(db, env, class); - let subclass_instance_member = instance_of_class.member(db, &member.name); + let subclass_instance_member = instance_of_class.member(db, env, &member.name); let Place::Defined(DefinedPlace { ty: type_on_subclass_instance, .. @@ -586,7 +598,7 @@ fn check_class_declaration<'db>( .can_validate_with_value_annotation() && let Some(expected_type) = enum_info.value_annotation_type() { - if !member_value_type.is_assignable_to(db, expected_type) { + if !member_value_type.is_assignable_to(db, env, expected_type) { if let Some(builder) = context.report_lint( &INVALID_ASSIGNMENT, first_reachable_definition.focus_range(db, context.module()), @@ -597,8 +609,8 @@ fn check_class_declaration<'db>( )); diagnostic.info(format_args!( "Expected `{}`, got `{}`", - expected_type.display(db), - member_value_type.display(db) + expected_type.display(db, env), + member_value_type.display(db, env) )); } } @@ -661,7 +673,7 @@ fn check_class_declaration<'db>( } } else { if superclass_literal - .own_synthesized_member(db, superclass_specialization, None, &member.name) + .own_synthesized_member(db, env, superclass_specialization, None, &member.name) .is_none() { continue; @@ -672,7 +684,7 @@ fn check_class_declaration<'db>( } let superclass_instance_member = - Type::instance(db, superclass).member(db, &member.name); + Type::instance(db, env, superclass).member(db, env, &member.name); let Place::Defined(DefinedPlace { ty: superclass_type, .. @@ -708,7 +720,7 @@ fn check_class_declaration<'db>( || (configuration.check_final_variable_overridden() && overridden_final_variable.is_none()) { - let own_class_member = superclass.own_class_member(db, None, &member.name); + let own_class_member = superclass.own_class_member(db, env, None, &member.name); if configuration.check_final_method_overridden() { overridden_final_method = overridden_final_method.or_else(|| { @@ -791,7 +803,8 @@ fn check_class_declaration<'db>( let subclass_kind = *subclass_variable_kind.get_or_insert_with(|| { variable_kind( db, - class.own_class_member(db, None, &member.name).inner, + env, + class.own_class_member(db, env, None, &member.name).inner, subclass_instance_member, ) }); @@ -817,7 +830,7 @@ fn check_class_declaration<'db>( if let Some((immediate_parent, immediate_parent_kind)) = immediate_parent_variable_kind && immediate_parent != superclass - && immediate_parent.is_subclass_of(db, superclass) + && immediate_parent.is_subclass_of(db, env, superclass) && immediate_parent_kind != superclass_variable_kind { continue; @@ -873,6 +886,7 @@ fn check_class_declaration<'db>( if let Some(superclass_function) = superclass_function && let Some(error) = crate::types::reified_infer::reified_override_error( db, + env, superclass_function, subclass_function, ) @@ -908,12 +922,12 @@ fn check_class_declaration<'db>( } let Some((subclass_override_type, superclass_override_type)) = - method_override_types(db, type_on_subclass_instance, superclass_type) + method_override_types(db, env, type_on_subclass_instance, superclass_type) else { continue; }; - if subclass_override_type.is_assignable_to(db, superclass_override_type) { + if subclass_override_type.is_assignable_to(db, env, superclass_override_type) { continue; } @@ -927,7 +941,12 @@ fn check_class_declaration<'db>( // The immediate parent already defines this method and is different from the // current ancestor we're checking. Check if the immediate parent's method // is also incompatible with this ancestor. - if !is_assignable_method_override(db, immediate_parent_type, superclass_type) { + if !is_assignable_method_override( + db, + env, + immediate_parent_type, + superclass_type, + ) { // The immediate parent already has an LSP violation with this ancestor. // Don't report the same violation for the child. continue; @@ -944,7 +963,13 @@ fn check_class_declaration<'db>( superclass, superclass_type, method_kind, - || subclass_override_type.assignability_error_context(db, superclass_override_type), + || { + subclass_override_type.assignability_error_context( + db, + env, + superclass_override_type, + ) + }, ); liskov_diagnostic_emitted = true; @@ -954,8 +979,8 @@ fn check_class_declaration<'db>( if !subclass_overrides_superclass_declaration && !has_dynamic_superclass { if has_typeddict_in_mro { if !KnownClass::TypedDictFallback - .to_instance(db) - .member(db, &member.name) + .to_instance(db, env) + .member(db, env, &member.name) .place .is_undefined() { @@ -963,8 +988,8 @@ fn check_class_declaration<'db>( } } else if class_kind == Some(CodeGeneratorKind::NamedTuple) { if !KnownClass::NamedTupleFallback - .to_instance(db) - .member(db, &member.name) + .to_instance(db, env) + .member(db, env, &member.name) .place .is_undefined() { @@ -981,9 +1006,11 @@ fn check_class_declaration<'db>( if !subclass_overrides_superclass_declaration && !has_dynamic_superclass - // accessing `.kind()` here is fine as `definition` - // will always be a definition in the file currently being checked - && first_reachable_definition.kind(db).is_function_def() + && ( + // accessing `.kind()` here is fine as `definition` + // will always be a definition in the file currently being checked + first_reachable_definition.kind(db).is_function_def() + ) { check_explicit_overrides(context, member, class_scope, class); } @@ -1031,16 +1058,18 @@ fn check_class_declaration<'db>( /// ``` fn is_assignable_method_override<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, subclass_type: Type<'db>, superclass_type: Type<'db>, ) -> bool { - method_override_types(db, subclass_type, superclass_type).is_some_and( - |(subclass_type, superclass_type)| subclass_type.is_assignable_to(db, superclass_type), + method_override_types(db, env, subclass_type, superclass_type).is_some_and( + |(subclass_type, superclass_type)| subclass_type.is_assignable_to(db, env, superclass_type), ) } fn method_override_types<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, subclass_type: Type<'db>, superclass_type: Type<'db>, ) -> Option<(Type<'db>, Type<'db>)> { @@ -1061,19 +1090,22 @@ fn method_override_types<'db>( receiver.map_or((subclass_type, superclass_type), |receiver| { let typing_self_type = subclass_method.typing_self_type(db); - let receiver = receiver.bind_self_typevars(db, typing_self_type); + let receiver = receiver.bind_self_typevars(db, env, typing_self_type); let receiver = IntersectionType::from_elements( db, + env, [subclass_method.self_instance(db), receiver], ); ( Type::Callable(subclass_method.into_callable_type_with_receiver( db, + env, receiver, typing_self_type, )), Type::Callable(superclass_method.into_callable_type_with_receiver( db, + env, receiver, typing_self_type, )), @@ -1082,14 +1114,14 @@ fn method_override_types<'db>( } _ => (subclass_type, superclass_type), }; - let superclass_callable = superclass_type.try_upcast_to_callable(db)?; + let superclass_callable = superclass_type.try_upcast_to_callable(db, env)?; - Some((subclass_type, superclass_callable.into_type(db))) + Some((subclass_type, superclass_callable.into_type(db, env))) } /// Whether an attribute declaration is a class variable or an instance variable. #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, get_size2::GetSize)] -enum VariableKind { +pub(super) enum VariableKind { /// A variable annotated with `ClassVar`. Class, /// An instance variable, including an unannotated class-body assignment. @@ -1130,7 +1162,8 @@ fn superclass_variable_kind<'db>( return None; } - variable_kind(db, class_member, instance_member) + let env = ProgramEnvironment::from_scope(superclass_scope); + variable_kind(db, &env, class_member, instance_member) } /// Returns the variable kind for a superclass member, preserving inherited `ClassVar` declarations @@ -1153,11 +1186,12 @@ fn superclass_variable_kind<'db>( /// ``` #[allow(clippy::needless_pass_by_value)] #[salsa::tracked(returns(copy), heap_size=ruff_memory_usage::heap_size)] -fn effective_superclass_variable_kind<'db>( +pub(super) fn effective_superclass_variable_kind<'db>( db: &'db dyn Db, superclass: ClassType<'db>, name: Name, ) -> Option { + let env = &ProgramEnvironment::from_file(superclass.class_literal(db).program_file(db)); let inherited_variable_kind = || { superclass .iter_mro(db) @@ -1176,7 +1210,7 @@ fn effective_superclass_variable_kind<'db>( superclass_symbol.is_bound() || superclass_symbol.is_declared() } else { superclass_literal - .own_synthesized_member(db, superclass_specialization, None, &name) + .own_synthesized_member(db, env, superclass_specialization, None, &name) .is_some() }; @@ -1185,8 +1219,8 @@ fn effective_superclass_variable_kind<'db>( db, superclass_scope, superclass_symbol_id, - superclass.own_class_member(db, None, &name).inner, - Type::instance(db, superclass).member(db, &name), + superclass.own_class_member(db, env, None, &name).inner, + superclass.own_instance_member(db, env, &name).inner, ); if superclass_variable_kind == Some(VariableKind::Instance) @@ -1258,7 +1292,7 @@ fn is_function_definition<'db>( /// it inspects the declaration's AST node, which might live in another module. #[salsa::tracked(heap_size=ruff_memory_usage::heap_size)] fn is_let_declaration<'db>(db: &'db dyn Db, scope: ScopeId<'db>, symbol: ScopedSymbolId) -> bool { - let module = parsed_module(db, scope.file(db)).load(db); + let module = parsed_module(db, scope.program_file(db).python_file(db)).load(db); use_def_map(db, scope) .end_of_scope_symbol_declarations(symbol) .filter_map(|declaration| declaration.declaration.definition()) @@ -1287,6 +1321,7 @@ fn is_let_marker(annotation: &ast::Expr) -> bool { /// Returns the variable kind for an attribute if it should participate in `ClassVar` override checks. fn variable_kind<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class_member: PlaceAndQualifiers<'db>, instance_member: PlaceAndQualifiers<'db>, ) -> Option { @@ -1334,7 +1369,7 @@ fn variable_kind<'db>( .. }) = class_member.place && class_member_ty - .class_member(db, "__get__") + .class_member(db, env, "__get__") .place .ignore_possibly_undefined() .is_some() @@ -1388,7 +1423,7 @@ fn report_invalid_attribute_override<'db>( let mut diagnostic = builder.into_diagnostic(format_args!("Invalid override of attribute `{member}`")); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "{subclass_kind} cannot override {superclass_kind} `{superclass_member}`" )); diagnostic.info("This violates the Liskov Substitution Principle"); @@ -1652,7 +1687,15 @@ fn check_missing_overrides<'db>( "Method `{}` overrides `{superclass_member}` but is not decorated with `@override`", member.name )); - diagnostic.info("Decorate the method with `@typing.override` to make the override explicit"); + let override_module = + if context.program_environment().python_version(db) >= PythonVersion::PY312 { + "typing" + } else { + "typing_extensions" + }; + diagnostic.info(format_args!( + "Decorate the method with `@{override_module}.override` to make the override explicit" + )); if let Some(superclass_definition) = superclass_definition && superclass_definition.file(db) == context.file() @@ -1705,7 +1748,6 @@ fn extract_local_override_definitions<'db>( extract_member_functions_from_type(db, member.ty, &member.name, subclass_scope); let mut candidates = smallvec::smallvec![]; let mut seen_function_types = smallvec::SmallVec::<[FunctionType<'db>; 1]>::new(); - for definition in end_of_scope_function_definitions(db, subclass_scope, &member.name) { let function = member_functions .iter() @@ -1753,7 +1795,6 @@ fn end_of_scope_function_definitions<'db>( let use_def = use_def_map(db, subclass_scope); let predicates = use_def.predicates(); let reachability_constraints = use_def.reachability_constraints(); - use_def .end_of_scope_symbol_bindings(symbol_id) .filter_map(|binding| { @@ -1820,7 +1861,7 @@ fn is_local_member_function<'db>( member_name: &Name, member_scope: ScopeId<'db>, ) -> bool { - function.file(db) == member_scope.file(db) + function.python_file(db) == member_scope.python_file(db) && function.definition(db).scope(db) == member_scope && function.name(db) == member_name } @@ -1881,6 +1922,7 @@ fn check_post_init_signature<'db>( let Some((static_class, spec)) = class.static_class_literal(db) else { return; }; + let env = &context.program_environment(); let init_var_fields = static_class .fields(db, spec, policy) @@ -1896,7 +1938,7 @@ fn check_post_init_signature<'db>( }); let first_parameter = Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(Type::instance(db, class)); + .with_annotated_type(Type::instance(db, env, class)); let following_parameters = init_var_fields.map(|(name, field)| { Parameter::positional_only(Some(name.clone())).with_annotated_type(field.declared_ty) @@ -1909,7 +1951,7 @@ fn check_post_init_signature<'db>( if member .ty - .is_assignable_to(db, Type::Callable(expected_signature)) + .is_assignable_to(db, env, Type::Callable(expected_signature)) { return; } @@ -1962,12 +2004,13 @@ fn check_enum_member_against_constructor_method<'db>( method: EnumConstructorMethod, ) { let db = context.db(); + let env = &context.program_environment(); // The enum metaclass unpacks tuple values as positional args: // MEMBER = (a, b, c) → __new__(cls, a, b, c) / __init__(self, a, b, c) // MEMBER = x → __new__(cls, x) / __init__(self, x) let args: Vec> = if let Type::NominalInstance(instance) = member_value_type - && let Some(spec) = instance.tuple_spec(db) + && let Some(spec) = instance.tuple_spec(db, env) { if let Tuple::Fixed(fixed) = &*spec { fixed.all_elements().to_vec() @@ -1984,9 +2027,16 @@ fn check_enum_member_against_constructor_method<'db>( let constraints = ConstraintSetBuilder::new(); let result = Type::FunctionLiteral(function) - .bindings(db) - .match_parameters(db, &call_args) - .check_types(db, &constraints, &call_args, TypeContext::default(), &[]); + .bindings(db, env) + .match_parameters(db, env, &call_args) + .check_types( + db, + env, + &constraints, + &call_args, + TypeContext::default(), + &[], + ); if result.is_err() { if let Some(builder) = context.report_lint( @@ -1999,7 +2049,7 @@ fn check_enum_member_against_constructor_method<'db>( )); diagnostic.info(format_args!( "Expected compatible arguments for `{}`", - Type::FunctionLiteral(function).display(db), + Type::FunctionLiteral(function).display(db, env), )); } } diff --git a/crates/ty_python_semantic/src/types/property_tests.rs b/crates/ty_python_semantic/src/types/property_tests.rs index cf7d63fcbb..04e9a744f0 100644 --- a/crates/ty_python_semantic/src/types/property_tests.rs +++ b/crates/ty_python_semantic/src/types/property_tests.rs @@ -30,8 +30,8 @@ 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 +/// The `$test_name` identifier specifies the name of the test function. The `$env` identifier +/// is used to refer to the semantic context in the property to be tested. The actual property is /// specified using the syntax: /// /// forall types t1, t2, ..., tn . ` @@ -39,34 +39,36 @@ use type_generation::{intersection, union}; /// 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) => { + ($test_name:ident, $db:ident, $env: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);)+ + fn $test_name($($types: Ty),+) -> bool { + let $db = &get_cached_db(); + let $env = &$db.program_environment(); + $(let $types = $types.into_type($db, $env);)+ let result = $property; if !result { println!("\nFailing types were:"); - $(println!("{}", $types.display($db));)+ + $(println!("{}", $types.display($db, $env));)+ } result } }; - ($test_name:ident, $db:ident, forall fully_static_types $($types:ident),+ . $property:expr) => { + ($test_name:ident, $db:ident, $env: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);)+ + fn $test_name($($types: FullyStaticTy),+) -> bool { + let $db = &get_cached_db(); + let $env = &$db.program_environment(); + $(let $types = $types.into_type($db, $env);)+ let result = $property; if !result { println!("\nFailing types were:"); - $(println!("{}", $types.display($db));)+ + $(println!("{}", $types.display($db, $env));)+ } result @@ -74,157 +76,155 @@ macro_rules! type_property_test { }; // 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)); + ($name:ident, $db:ident, $env:ident, forall $typekind:ident $($types:ident),+ . $premise:expr => $conclusion:expr) => { + type_property_test!($name, $db, $env, forall $typekind $($types),+ . !($premise) || ($conclusion)); }; } mod stable { - use super::union; + use super::{ + setup::get_cached_db, + type_generation::{FullyStaticTy, Ty}, + union, + }; use crate::types::{CallableType, IntersectionBuilder, KnownClass, Type}; // Reflexivity: `T` is equivalent to itself. type_property_test!( - equivalent_to_is_reflexive, db, - forall types t. t.is_equivalent_to(db, t) + equivalent_to_is_reflexive, db, env, + forall types t. t.is_equivalent_to(db, env, 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) + equivalent_to_is_symmetric, db, env, + forall types s, t. s.is_equivalent_to(db, env, t) => t.is_equivalent_to(db, env, 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) + equivalent_to_is_transitive, db, env, + forall types s, t, u. s.is_equivalent_to(db, env, t) && t.is_equivalent_to(db, env, u) => s.is_equivalent_to(db, env, 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) + subtype_of_is_transitive, db, env, + forall types s, t, u. s.is_subtype_of(db, env, t) && t.is_subtype_of(db, env, u) => s.is_subtype_of(db, env, 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) + subtype_of_is_antisymmetric, db, env, + forall types s, t. s.is_subtype_of(db, env, t) && t.is_subtype_of(db, env, s) => s.is_equivalent_to(db, env, t) ); type_property_test!( - structural_negation_subtyping_matches_materialized_negation, db, + structural_negation_subtyping_matches_materialized_negation, db, env, forall types s, t. { let mut cache = None; - s.negation_is_subtype_of_cached(db, t, &mut cache) == s.negate(db).is_subtype_of(db, t) + s.negation_is_subtype_of_cached(db, env, t, &mut cache) == s.negate(db, env).is_subtype_of(db, env, 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() + disjoint_from_is_irreflexive, db, env, + forall types t. t.is_disjoint_from(db, env, 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) + disjoint_from_is_symmetric, db, env, + forall types s, t. s.is_disjoint_from(db, env, t) == t.is_disjoint_from(db, env, 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() + subtype_of_implies_not_disjoint_from, db, env, + forall types s, t. s.is_subtype_of(db, env, t) => !s.is_disjoint_from(db, env, 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) + subtype_of_implies_assignable_to, db, env, + forall types s, t. s.is_subtype_of(db, env, t) => s.is_assignable_to(db, env, t) ); // 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()) + all_types_assignable_to_object, db, env, + forall types t. t.is_assignable_to(db, env, Type::object()) ); // 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()) + all_types_subtype_of_object, db, env, + forall types t. t.is_subtype_of(db, env, Type::object()) ); // 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) + never_assignable_to_every_type, db, env, + forall types t. Type::Never.is_assignable_to(db, env, 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) + never_subtype_of_every_type, db, env, + forall types t. Type::Never.is_subtype_of(db, env, 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, + bottom_callable_is_subtype_of_all_callable, db, env, forall types t. t.is_callable_type() - => Type::Callable(CallableType::bottom(db)).is_subtype_of(db, t) + => Type::Callable(CallableType::bottom(db)).is_subtype_of(db, env, t) ); // `T` can be assigned to itself. type_property_test!( - assignable_to_is_reflexive, db, - forall types t. t.is_assignable_to(db, t) + assignable_to_is_reflexive, db, env, + forall types t. t.is_assignable_to(db, env, 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])) + all_type_pairs_are_assignable_to_their_union, db, env, + forall types s, t. s.is_assignable_to(db, env, union(db, env, [s, t])) && t.is_assignable_to(db, env, union(db, env, [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_never_is_subtype_of_any, db, env, + forall types s. !s.is_equivalent_to(db, env, Type::Never) => !s.is_subtype_of(db, env, 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()) => !Type::any().is_subtype_of(db, t) + only_object_is_supertype_of_any, db, env, + forall types t. !t.is_equivalent_to(db, env, Type::object()) => !Type::any().is_subtype_of(db, env, 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) + equivalent_to_is_commutative, db, env, + forall types s, t. s.is_equivalent_to(db, env, t) == t.is_equivalent_to(db, env, 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) + subtype_of_is_reflexive_for_fully_static_types, db, env, + forall fully_static_types t. t.is_subtype_of(db, env, 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])) + all_fully_static_type_pairs_are_subtype_of_their_union, db, env, + forall fully_static_types s, t. s.is_subtype_of(db, env, union(db, env, [s, t])) && t.is_subtype_of(db, env, union(db, env, [s, t])) ); // Any type assignable to `Iterable[object]` should be considered iterable. @@ -242,15 +242,15 @@ mod stable { // the Liskov violation). All you need to do is to create a class that subclasses // `Iterable` but assigns `__iter__ = None` in the class body (or similar). type_property_test!( - all_types_assignable_to_iterable_are_iterable, db, - forall types t. t.is_assignable_to(db, KnownClass::Iterable.to_specialized_instance(db, &[Type::object()])) => t.try_iterate(db).is_ok() + all_types_assignable_to_iterable_are_iterable, db, env, + forall types t. t.is_assignable_to(db, env, KnownClass::Iterable.to_specialized_instance(db, env, &[Type::object()])) => t.try_iterate(db, env).is_ok() ); // Our optimized `Type::negate()` function should always produce the exact same type // as going "the long way" via the `IntersectionBuilder`. type_property_test!( - all_negated_types_identical_to_intersection_with_single_negated_element, db, - forall types t. t.negate(db) == IntersectionBuilder::new(db).add_negative(t).build() + all_negated_types_identical_to_intersection_with_single_negated_element, db, env, + forall types t. t.negate(db, env) == IntersectionBuilder::new(db, env).add_negative(t).build() ); } @@ -264,68 +264,73 @@ mod stable { mod flaky { use itertools::Itertools; - use super::{intersection, union}; + use super::{ + intersection, + setup::get_cached_db, + type_generation::{FullyStaticTy, Ty}, + 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) + double_negation_is_identity, db, env, + forall types t. t.negate(db, env).negate(db, env).is_equivalent_to(db, env, 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) + negation_of_fully_static_types_is_disjoint, db, env, + forall fully_static_types t. t.negate(db, env).is_disjoint_from(db, env, 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, + all_type_pairs_are_supertypes_of_their_intersection, db, env, forall types s, t. - intersection(db, [s, t]).is_subtype_of(db, s) && intersection(db, [s, t]).is_subtype_of(db, t) + intersection(db, env, [s, t]).is_subtype_of(db, env, s) && intersection(db, env, [s, t]).is_subtype_of(db, env, 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) + all_type_pairs_can_be_assigned_from_their_intersection, db, env, + forall types s, t. intersection(db, env, [s, t]).is_assignable_to(db, env, s) && intersection(db, env, [s, t]).is_assignable_to(db, env, 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, + intersection_equivalence_not_order_dependent, db, env, forall types s, t, u. [s, t, u] .into_iter() .permutations(3) - .map(|trio_of_types| intersection(db, trio_of_types)) + .map(|trio_of_types| intersection(db, env, trio_of_types)) .permutations(2) - .all(|vec_of_intersections| vec_of_intersections[0].is_equivalent_to(db, vec_of_intersections[1])) + .all(|vec_of_intersections| vec_of_intersections[0].is_equivalent_to(db, env, 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, + union_equivalence_not_order_dependent, db, env, forall types s, t, u. [s, t, u] .into_iter() .permutations(3) - .map(|trio_of_types| union(db, trio_of_types)) + .map(|trio_of_types| union(db, env, trio_of_types)) .permutations(2) - .all(|vec_of_unions| vec_of_unions[0].is_equivalent_to(db, vec_of_unions[1])) + .all(|vec_of_unions| vec_of_unions[0].is_equivalent_to(db, env, 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, + constituent_members_of_union_is_not_disjoint_from_that_union, db, env, forall types s, t. - !s.is_disjoint_from(db, union(db, [s, t])) && !t.is_disjoint_from(db, union(db, [s, t])) + !s.is_disjoint_from(db, env, union(db, env, [s, t])) && !t.is_disjoint_from(db, env, union(db, env, [s, t])) ); // If `S <: T`, then `~T <: ~S`. @@ -339,8 +344,8 @@ mod flaky { // 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)) + negation_reverses_subtype_order, db, env, + forall types s, t. s.is_subtype_of(db, env, t) => t.negate(db, env).is_subtype_of(db, env, s.negate(db, env)) ); // Both the top and bottom materialization tests are flaky in part due to various failures that @@ -349,13 +354,13 @@ mod flaky { // `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) + top_materialization_of_type_is_assignable_to_type, db, env, + forall types t. t.top_materialization(db, env).is_assignable_to(db, env, t) ); // Similarly, `T'`, the bottom materialization of `T`, should also be assignable to `T`. type_property_test!( - bottom_materialization_of_type_is_assignable_to_type, db, - forall types t. t.bottom_materialization(db).is_assignable_to(db, t) + bottom_materialization_of_type_is_assignable_to_type, db, env, + forall types t. t.bottom_materialization(db, env).is_assignable_to(db, env, t) ); } 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 index 2fef4cc9c7..720710a510 100644 --- a/crates/ty_python_semantic/src/types/property_tests/type_generation.rs +++ b/crates/ty_python_semantic/src/types/property_tests/type_generation.rs @@ -1,5 +1,4 @@ use crate::Db; -use crate::db::tests::TestDb; use crate::place::{DefinedPlace, Place, builtins_symbol, global_symbol, known_module_symbol}; use crate::types::enums::is_single_member_enum; use crate::types::known_instance::KnownInstanceType; @@ -9,11 +8,14 @@ use crate::types::{ IntersectionType, KnownClass, MaterializationKind, Parameter, Parameters, Signature, SpecialFormType, SubclassOfType, Type, UnionType, }; +use crate::{Program, ProgramEnvironment}; +use itertools::Either; use quickcheck::{Arbitrary, Gen}; use ruff_db::files::system_path_to_file; use ruff_python_ast::name::Name; use rustc_hash::FxHashSet; use ty_module_resolver::KnownModule; +use ty_python_core::ProgramFile; /// A test representation of a type that can be transformed unambiguously into a real Type, /// given a db. @@ -90,11 +92,16 @@ pub(crate) enum CallableParams { } impl CallableParams { - pub(crate) fn into_parameters(self, db: &TestDb) -> Parameters<'_> { + fn into_parameters<'db>( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Parameters<'db> { match self { CallableParams::GradualForm => Parameters::gradual_form(), CallableParams::List(params) => Parameters::from_annotation( db, + env, params.into_iter().map(|param| { let parameter = match param.kind { ParamKind::PositionalOnly => Parameter::positional_only(param.name), @@ -108,12 +115,45 @@ impl CallableParams { } }; parameter - .with_annotated_type(param.annotated_ty.into_type(db)) - .with_optional_default_type(param.default_ty.map(|t| t.into_type(db))) + .with_annotated_type(param.annotated_ty.into_type(db, env)) + .with_optional_default_type(param.default_ty.map(|t| t.into_type(db, env))) }), ), } } + + fn shrink(self) -> impl Iterator { + match self { + // If the failure does not depend on accepting arbitrary arguments, replace `...` + // with the simplest concrete signature: one that accepts no arguments. + Self::GradualForm => Either::Left(std::iter::once(Self::List(Vec::new()))), + Self::List(params) => { + // Removing one parameter at a time preserves the ordering and names of all + // remaining parameters, so each candidate is still a valid signature. + let removed_parameters = (0..params.len()).map({ + let params = params.clone(); + move |index| { + let mut shrunk = params.clone(); + shrunk.remove(index); + Self::List(shrunk) + } + }); + + // If a parameter cannot be removed without losing the failure, try simplifying its + // name, default, or annotation while preserving the rest of the signature. + let shrunk_parameters = (0..params.len()).flat_map(move |index| { + let params = params.clone(); + params[index].clone().shrink().map(move |parameter| { + let mut shrunk = params.clone(); + shrunk[index] = parameter; + Self::List(shrunk) + }) + }); + + Either::Right(removed_parameters.chain(shrunk_parameters)) + } + } + } } #[derive(Debug, Clone, PartialEq)] @@ -124,6 +164,44 @@ pub(crate) struct Param { default_ty: Option, } +impl Param { + fn shrink(self) -> impl Iterator { + let without_name = + (self.kind == ParamKind::PositionalOnly && self.name.is_some()).then(|| Self { + name: None, + ..self.clone() + }); + + let shrunk_defaults = self.default_ty.shrink().map({ + let parameter = self.clone(); + move |default_ty| Self { + default_ty, + ..parameter.clone() + } + }); + + let shrunk_annotations = shrink_callable_component(&self.annotated_ty).map({ + let parameter = self.clone(); + move |annotated_ty| Self { + annotated_ty, + ..parameter.clone() + } + }); + + without_name + .into_iter() + .chain(shrunk_defaults) + .chain(shrunk_annotations) + } +} + +fn shrink_callable_component(ty: &Ty) -> impl Iterator + use<> { + let object = Ty::KnownClassInstance(KnownClass::Object); + let simplified = (ty != &object).then_some(object); + + simplified.into_iter().chain(ty.shrink()) +} + #[derive(Debug, Clone, Copy, PartialEq)] enum ParamKind { PositionalOnly, @@ -136,25 +214,31 @@ enum ParamKind { #[salsa::tracked(returns(copy), heap_size=ruff_memory_usage::heap_size)] fn create_bound_method<'db>( db: &'db dyn Db, + program: Program<'db>, function: Type<'db>, builtins_class: Type<'db>, ) -> Type<'db> { + let env = ProgramEnvironment::from_program(program); Type::BoundMethod(BoundMethodType::new( db, function.expect_function_literal(), - builtins_class.to_instance_approximation(db).unwrap(), + builtins_class.to_instance_approximation(db, &env).unwrap(), )) } impl Ty { - pub(crate) fn into_type(self, db: &TestDb) -> Type<'_> { + pub(crate) fn into_type<'db>( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { match self { Ty::Never => Type::Never, Ty::Unknown => Type::unknown(), - Ty::Divergent => divergent(db, 1, None), - Ty::TopDivergent => divergent(db, 2, Some(MaterializationKind::Top)), - Ty::BottomDivergent => divergent(db, 3, Some(MaterializationKind::Bottom)), - Ty::None => Type::none(db), + Ty::Divergent => divergent(db, env, 1, None), + Ty::TopDivergent => divergent(db, env, 2, Some(MaterializationKind::Top)), + Ty::BottomDivergent => divergent(db, env, 3, Some(MaterializationKind::Bottom)), + Ty::None => Type::none(db, env), Ty::Any => Type::any(), Ty::IntLiteral(n) => Type::int_literal(n), Ty::StringLiteral(s) => Type::string_literal(db, s), @@ -162,7 +246,7 @@ impl Ty { Ty::LiteralString => Type::literal_string(), Ty::BytesLiteral(s) => Type::bytes_literal(db, s.as_bytes()), Ty::EnumLiteral(name) => { - let enum_class = known_module_symbol(db, KnownModule::Uuid, "SafeUUID") + let enum_class = known_module_symbol(db, env, KnownModule::Uuid, "SafeUUID") .place .expect_type() .expect_class_literal() @@ -171,64 +255,67 @@ impl Ty { Type::enum_literal(EnumLiteralType::new(db, enum_class, Name::new(name))) } Ty::SingleMemberEnumLiteral => { - let ty = known_module_symbol(db, KnownModule::Dataclasses, "MISSING") + let ty = known_module_symbol(db, env, KnownModule::Dataclasses, "MISSING") .place .expect_type(); debug_assert!( - matches!(ty, Type::NominalInstance(instance) if is_single_member_enum(db, instance.class_literal(db))) + matches!(ty, Type::NominalInstance(instance) if is_single_member_enum(db, instance.class_literal(db, env))) ); ty } - Ty::BuiltinInstance(s) => builtins_symbol(db, s) + Ty::BuiltinInstance(s) => builtins_symbol(db, env, s) .place .expect_type() - .to_instance_approximation(db) + .to_instance_approximation(db, env) .unwrap(), - Ty::AbcInstance(s) => known_module_symbol(db, KnownModule::Abc, s) + Ty::AbcInstance(s) => known_module_symbol(db, env, KnownModule::Abc, s) .place .expect_type() - .to_instance_approximation(db) + .to_instance_approximation(db, env) .unwrap(), - Ty::AbcClassLiteral(s) => known_module_symbol(db, KnownModule::Abc, s) - .place - .expect_type(), - Ty::UnittestMockLiteral => known_module_symbol(db, KnownModule::UnittestMock, "Mock") + Ty::AbcClassLiteral(s) => known_module_symbol(db, env, KnownModule::Abc, s) .place .expect_type(), + Ty::UnittestMockLiteral => { + known_module_symbol(db, env, KnownModule::UnittestMock, "Mock") + .place + .expect_type() + } Ty::UnittestMockInstance => Ty::UnittestMockLiteral - .into_type(db) - .to_instance_approximation(db) + .into_type(db, env) + .to_instance_approximation(db, env) .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::BuiltinClassLiteral(s) => builtins_symbol(db, env, s).place.expect_type(), + Ty::KnownClassInstance(known_class) => known_class.to_instance(db, env), Ty::Union(tys) => { - UnionType::from_elements(db, tys.into_iter().map(|ty| ty.into_type(db))) + UnionType::from_elements(db, env, tys.into_iter().map(|ty| ty.into_type(db, env))) } Ty::Intersection { pos, neg } => { - let mut builder = IntersectionBuilder::new(db); + let mut builder = IntersectionBuilder::new(db, env); for p in pos { - builder = builder.add_positive(p.into_type(db)); + builder.add_positive_in_place(p.into_type(db, env)); } for n in neg { - builder = builder.add_negative(n.into_type(db)); + builder.add_negative_in_place(n.into_type(db, env)); } builder.build() } Ty::FixedLengthTuple(tys) => { - let elements = tys.into_iter().map(|ty| ty.into_type(db)); - Type::heterogeneous_tuple(db, elements) + let elements = tys.into_iter().map(|ty| ty.into_type(db, env)); + Type::heterogeneous_tuple(db, env, 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)); - Type::tuple(TupleType::mixed(db, prefix, variable, suffix)) + let prefix = prefix.into_iter().map(|ty| ty.into_type(db, env)); + let variable = variable.into_type(db, env); + let suffix = suffix.into_iter().map(|ty| ty.into_type(db, env)); + Type::tuple(TupleType::mixed(db, env, prefix, variable, suffix)) } Ty::SubclassOfAny => SubclassOfType::subclass_of_any(), Ty::SubclassOfBuiltinClass(s) => SubclassOfType::from( db, - builtins_symbol(db, s) + env, + builtins_symbol(db, env, s) .place .expect_type() .expect_class_literal() @@ -236,7 +323,8 @@ impl Ty { ), Ty::SubclassOfAbcClass(s) => SubclassOfType::from( db, - known_module_symbol(db, KnownModule::Abc, s) + env, + known_module_symbol(db, env, KnownModule::Abc, s) .place .expect_type() .expect_class_literal() @@ -244,44 +332,51 @@ impl Ty { ), Ty::AlwaysTruthy => Type::AlwaysTruthy, Ty::AlwaysFalsy => Type::AlwaysFalsy, - Ty::BuiltinsFunction(name) => builtins_symbol(db, name).place.expect_type(), + Ty::BuiltinsFunction(name) => builtins_symbol(db, env, 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(); + let builtins_class = builtins_symbol(db, env, class).place.expect_type(); + let function = builtins_class.member(db, env, method).place.expect_type(); - create_bound_method(db, function, builtins_class) + create_bound_method(db, env.program(db), function, builtins_class) } Ty::Callable { params, returns } => Type::single_callable( db, - Signature::new(params.into_parameters(db), returns.into_type(db)), + Signature::new(params.into_parameters(db, env), returns.into_type(db, env)), ), - Ty::FloatNewtypeInstance => newtype_instance(db, "NewTypeOfFloat"), - Ty::IntNewtypeInstance => newtype_instance(db, "NewTypeOfInt"), - Ty::StrNewtypeInstance => newtype_instance(db, "NewTypeOfStr"), - Ty::ComplexNewtypeInstance => newtype_instance(db, "NewTypeOfComplex"), - Ty::SubNewTypeOfIntInstance => newtype_instance(db, "SubNewTypeOfInt"), - Ty::SubSubNewTypeOfIntInstance => newtype_instance(db, "SubSubNewTypeOfInt"), - Ty::SubNewTypeOfFloatInstance => newtype_instance(db, "SubNewTypeOfFloat"), + Ty::FloatNewtypeInstance => newtype_instance(db, env, "NewTypeOfFloat"), + Ty::IntNewtypeInstance => newtype_instance(db, env, "NewTypeOfInt"), + Ty::StrNewtypeInstance => newtype_instance(db, env, "NewTypeOfStr"), + Ty::ComplexNewtypeInstance => newtype_instance(db, env, "NewTypeOfComplex"), + Ty::SubNewTypeOfIntInstance => newtype_instance(db, env, "SubNewTypeOfInt"), + Ty::SubSubNewTypeOfIntInstance => newtype_instance(db, env, "SubSubNewTypeOfInt"), + Ty::SubNewTypeOfFloatInstance => newtype_instance(db, env, "SubNewTypeOfFloat"), } } } -fn divergent(db: &TestDb, id_bits: u64, materialization: Option) -> Type<'_> { +fn divergent<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + id_bits: u64, + materialization: Option, +) -> Type<'db> { let divergent = Type::divergent(salsa::plumbing::Id::from_bits(id_bits)); match materialization { Some(materialization_kind) => divergent.materialize( db, + env, materialization_kind, - &ApplyTypeMappingVisitor::default(), + &ApplyTypeMappingVisitor::new(env), ), None => divergent, } } -fn newtype_instance<'db>(db: &'db dyn Db, name: &str) -> Type<'db> { +fn newtype_instance<'db>(db: &'db dyn Db, env: &ProgramEnvironment<'db>, name: &str) -> Type<'db> { let file = system_path_to_file(db, super::setup::PROPERTY_TEST_MODULE_PATH) .expect("Property-test module must exist"); + let file = ProgramFile::new(db, file, env.program(db)); let Place::Defined(DefinedPlace { ty, .. }) = global_symbol(db, file, name).place else { panic!( "Expected a global symbol for `{name}` in the property test module, but it was not found" @@ -297,8 +392,12 @@ fn newtype_instance<'db>(db: &'db dyn Db, name: &str) -> Type<'db> { pub(crate) struct FullyStaticTy(Ty); impl FullyStaticTy { - pub(crate) fn into_type(self, db: &TestDb) -> Type<'_> { - self.0.into_type(db) + pub(crate) fn into_type<'db>( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { + self.0.into_type(db, env) } } @@ -601,6 +700,23 @@ impl Arbitrary for Ty { }), ) } + Ty::Callable { params, returns } => { + let shrunk_parameters = params.clone().shrink().map({ + let returns = returns.clone(); + move |params| Ty::Callable { + params, + returns: returns.clone(), + } + }); + + let shrunk_return_type = + shrink_callable_component(&returns).map(move |returns| Ty::Callable { + params: params.clone(), + returns: Box::new(returns), + }); + + Box::new(shrunk_parameters.chain(shrunk_return_type)) + } _ => Box::new(std::iter::empty()), } } @@ -618,12 +734,89 @@ impl Arbitrary for FullyStaticTy { } pub(crate) fn intersection<'db>( - db: &'db TestDb, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, tys: impl IntoIterator>, ) -> Type<'db> { - IntersectionType::from_elements(db, tys) + IntersectionType::from_elements(db, env, tys) } -pub(crate) fn union<'db>(db: &'db TestDb, tys: impl IntoIterator>) -> Type<'db> { - UnionType::from_elements(db, tys) +pub(crate) fn union<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + tys: impl IntoIterator>, +) -> Type<'db> { + UnionType::from_elements(db, env, tys) +} + +mod tests { + use super::*; + use test_case::test_case; + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + enum CallableShrink { + Parameter, + ParameterName, + ParameterDefault, + ParameterAnnotation, + ReturnType, + } + + // Test each independently removable signature detail separately so a failure identifies the + // exact shrink candidate that is missing. + #[test_case(CallableShrink::Parameter; "removes a parameter")] + #[test_case(CallableShrink::ParameterName; "removes a positional only parameter name")] + #[test_case(CallableShrink::ParameterDefault; "removes a parameter default")] + #[test_case(CallableShrink::ParameterAnnotation; "simplifies a parameter annotation")] + #[test_case(CallableShrink::ReturnType; "simplifies the return type")] + fn callable_shrinks_parameters_and_return_type(shrink: CallableShrink) { + let parameter = Param { + kind: ParamKind::PositionalOnly, + name: Some(Name::new_static("argument")), + annotated_ty: Ty::Union(vec![Ty::KnownClassInstance(KnownClass::Int), Ty::None]), + default_ty: Some(Ty::IntLiteral(1)), + }; + let callable = Ty::Callable { + params: CallableParams::List(vec![parameter.clone()]), + returns: Box::new(Ty::FixedLengthTuple(vec![])), + }; + + let mut expected_parameters = vec![parameter]; + let mut expected_return = Ty::FixedLengthTuple(vec![]); + match shrink { + CallableShrink::Parameter => expected_parameters.clear(), + CallableShrink::ParameterName => expected_parameters[0].name = None, + CallableShrink::ParameterDefault => expected_parameters[0].default_ty = None, + CallableShrink::ParameterAnnotation => { + expected_parameters[0].annotated_ty = Ty::KnownClassInstance(KnownClass::Object); + } + CallableShrink::ReturnType => { + expected_return = Ty::KnownClassInstance(KnownClass::Object); + } + } + + let expected = Ty::Callable { + params: CallableParams::List(expected_parameters), + returns: Box::new(expected_return), + }; + assert!(callable.shrink().any(|candidate| candidate == expected)); + } + + // A gradual `...` parameter list can become an empty concrete signature when accepting + // arbitrary arguments is not essential to the failing property. + #[test] + fn gradual_callable_shrinks_to_empty_parameter_list() { + let callable = Ty::Callable { + params: CallableParams::GradualForm, + returns: Box::new(Ty::KnownClassInstance(KnownClass::Object)), + }; + + assert_eq!( + callable.shrink().collect::>(), + vec![Ty::Callable { + params: CallableParams::List(vec![]), + returns: Box::new(Ty::KnownClassInstance(KnownClass::Object)), + }] + ); + } } diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index c3424fc6a9..16a14f2b9f 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -1,3 +1,4 @@ +use crate::{Program, ProgramEnvironment}; use std::fmt::Write; use std::{collections::BTreeMap, ops::Deref}; @@ -14,6 +15,7 @@ use crate::types::attribute_write::{ use crate::types::call::{CallArguments, CallDunderError}; use crate::types::deferred::is_symbolic_operand; use crate::types::instance::Protocol; +use crate::types::overrides::{VariableKind, effective_superclass_variable_kind}; use crate::types::relation::{DisjointnessChecker, TypeRelationChecker}; use crate::types::visitor::any_over_type; use crate::types::{TypeContext, UpcastPolicy}; @@ -26,11 +28,11 @@ use crate::{ types::{ ApplyTypeMappingVisitor, BindingContext, BoundTypeVarIdentity, BoundTypeVarInstance, CallableType, ClassBase, ClassType, DeferredOperation, DeferredType, ErrorContext, - FindLegacyTypeVarsVisitor, GenericContext, InstanceFallbackShadowsNonDataDescriptor, - IntersectionType, KnownFunction, MemberLookupKey, MemberLookupPolicy, Parameter, - PropertyInstanceType, ProtocolInstanceType, SelfBinding, Signature, StaticClassLiteral, - Type, TypeMapping, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarVariance, UnionType, - VarianceInferable, + FindLegacyTypeVarsVisitor, GenericAlias, GenericContext, + InstanceFallbackShadowsNonDataDescriptor, IntersectionType, KnownFunction, + MaterializationKind, MemberLookupKey, MemberLookupPolicy, Parameter, PropertyInstanceType, + ProtocolInstanceType, SelfBinding, Signature, StaticClassLiteral, Type, TypeMapping, + TypeQualifiers, TypeVarBoundOrConstraints, TypeVarVariance, UnionType, VarianceInferable, constraints::{ConstraintSet, IteratorConstraintsExtension, OptionConstraintsExtension}, context::InferContext, diagnostic::report_undeclared_protocol_member, @@ -79,6 +81,30 @@ impl<'db> ProtocolClass<'db> { cached_protocol_interface(db, *self) } + /// Returns the interface before an invariant specialization is materialized. + /// + /// A materialized generic origin retains its specialization for nominal identity and display. + /// Building member requirements from that specialization, however, would first materialize an + /// invariant type variable as a read and then reuse that result as its write. Strip only the + /// pending marker while constructing the shared interface so reads and writes can each apply + /// the original materialization in their own variance position. + pub(super) fn unmaterialized_interface(self, db: &'db dyn Db) -> ProtocolInterface<'db> { + let ClassType::Generic(alias) = *self else { + return self.interface(db); + }; + let specialization = alias.specialization(db); + if specialization.materialization_kind(db).is_none() { + return self.interface(db); + } + + let alias = GenericAlias::new( + db, + alias.origin(db), + specialization.with_materialization_kind(db, None), + ); + ProtocolClass(ClassType::Generic(alias)).interface(db) + } + /// Walk the effective non-method member types declared by this protocol. /// /// Method relations have their own declaration-based recursion guard. Keeping them out of this @@ -91,13 +117,17 @@ impl<'db> ProtocolClass<'db> { ) { let mut seen_members = FxHashSet::default(); - self.for_each_member_candidate(db, |name, candidate, specialization| { - if !seen_members.insert(name.clone()) { - return; - } - let candidate = candidate.apply_specialization(db, specialization); - candidate.walk_recursive_member_types(db, visitor); - }); + self.for_each_member_candidate( + db, + visitor.program_environment(), + |name, candidate, specialization| { + if !seen_members.insert(name.clone()) { + return; + } + let candidate = candidate.apply_specialization(db, specialization); + candidate.walk_recursive_member_types(db, visitor); + }, + ); } /// Visits protocol member candidates in MRO order after applying declaration precedence. @@ -106,6 +136,7 @@ impl<'db> ProtocolClass<'db> { fn for_each_member_candidate( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, mut visit: impl FnMut(&Name, ProtocolMemberCandidate<'db>, Option>), ) { for (parent_scope, specialization) in self @@ -128,7 +159,7 @@ impl<'db> ProtocolClass<'db> { // runtime-checkable protocols still consider them members for `isinstance()` and // `issubclass()`. for (symbol_id, bindings) in use_def_map.all_end_of_scope_symbol_bindings() { - let place_and_definition = place_from_bindings(db, bindings); + let place_and_definition = place_from_bindings(db, env, bindings); if let Some(ty) = place_and_definition.place.ignore_possibly_undefined() { direct_members.insert( symbol_id, @@ -143,7 +174,7 @@ impl<'db> ProtocolClass<'db> { } for (symbol_id, declarations) in use_def_map.all_end_of_scope_symbol_declarations() { - let place_result = place_from_declarations(db, declarations); + let place_result = place_from_declarations(db, env, declarations); let first_declaration = place_result.first_declaration; let place = place_result.ignore_conflicting_declarations(); if let Some(ty) = place.place.ignore_possibly_undefined() { @@ -196,6 +227,10 @@ impl<'db> ProtocolClass<'db> { /// __doc__: str /// ``` pub(super) fn has_member_declaration(self, db: &'db dyn Db, name: &str) -> bool { + let Some((class, _)) = self.static_class_literal(db) else { + return false; + }; + let env = ProgramEnvironment::from_scope(class.body_scope(db)); self.iter_mro(db) .filter_map(ClassBase::into_class) .any(|superclass| { @@ -209,6 +244,7 @@ impl<'db> ProtocolClass<'db> { }; !place_from_declarations( db, + &env, use_def_map(db, superclass_scope) .end_of_scope_declarations(ScopedPlaceId::Symbol(scoped_symbol_id)), ) @@ -256,24 +292,27 @@ impl<'db> ProtocolClass<'db> { pub(super) fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { Self( self.0 - .apply_type_mapping_impl(db, type_mapping, tcx, visitor), + .apply_type_mapping_impl(db, env, type_mapping, tcx, visitor), ) } pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { Some(Self( - self.0.recursive_type_normalized_impl(db, div, nested)?, + self.0 + .recursive_type_normalized_impl(db, env, div, nested)?, )) } } @@ -295,6 +334,9 @@ impl<'db> From> for Type<'db> { /// The interface of a protocol: the members of that protocol, and the types of those members. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub(super) struct ProtocolInterface<'db> { + #[returns(copy)] + pub(super) program: Program<'db>, + #[returns(ref)] inner: BTreeMap>, @@ -339,6 +381,7 @@ pub(super) enum InlineProtocolMember<'db> { /// too, so both kinds of protocol already agree about it. pub(super) fn symbolic_method_member<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, name: &Name, receiver: Option>, @@ -347,11 +390,12 @@ pub(super) fn symbolic_method_member<'db>( if !is_symbolic_operand(receiver) { return None; } - structural_interface(db, ty)? + structural_interface(db, env, ty)? .is_instance_method_member(db, name) .then(|| { DeferredType::build( db, + env, &DeferredOperation::Attribute(name.clone()), Box::from([receiver]), ) @@ -360,14 +404,18 @@ pub(super) fn symbolic_method_member<'db>( /// The interface of `ty` when it is a structural protocol, looking through a type parameter to /// the bound that says what its members are. -fn structural_interface<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option> { +fn structural_interface<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Option> { match ty { Type::ProtocolInstance(ProtocolInstanceType { inner: Protocol::Synthesized(protocol), .. }) => Some(protocol.interface(db)), Type::TypeVar(bound_typevar) => { - structural_interface(db, bound_typevar.typevar(db).upper_bound(db)?) + structural_interface(db, env, bound_typevar.typevar(db).upper_bound(db, env)?) } _ => None, } @@ -385,15 +433,273 @@ pub(super) enum InlineProtocolMemberForm<'db> { impl get_size2::GetSize for ProtocolInterface<'_> {} +/// A protocol interface together with the materialization applied to its requirements. +/// +/// The original interface remains shared. A member's readable and writable types are +/// materialized only when that member is accessed or compared. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, get_size2::GetSize, salsa::SalsaValue)] +pub(super) struct ProtocolInterfaceView<'db> { + interface: ProtocolInterface<'db>, + materialization: Option, +} + +impl<'db> ProtocolInterfaceView<'db> { + pub(super) const fn new( + interface: ProtocolInterface<'db>, + materialization: Option, + ) -> Self { + Self { + interface, + materialization, + } + } + + pub(super) const fn base(self) -> ProtocolInterface<'db> { + self.interface + } + + pub(super) const fn materialization_kind(self) -> Option { + self.materialization + } + + pub(super) fn members<'a>( + self, + db: &'db dyn Db, + ) -> impl ExactSizeIterator> + where + 'db: 'a, + { + self.interface + .inner(db) + .iter() + .map(move |(name, data)| ProtocolMember { + name, + data, + materialization: self.materialization, + }) + } + + pub(super) fn member_count(self, db: &'db dyn Db) -> usize { + self.interface.member_count(db) + } + + /// Returns whether structural comparison can avoid recursive member expansion. + pub(super) fn has_only_finite_members(self, db: &'db dyn Db) -> bool { + let env = ProgramEnvironment::from_program(self.interface.program(db)); + self.members(db).all(|member| { + !matches!( + member.structural_member_priority(db, &env), + StructuralMemberPriority::Recursive + ) + }) + } + + fn member_by_name<'a>(self, db: &'db dyn Db, name: &'a str) -> Option> { + self.interface + .inner(db) + .get(name) + .map(|data| ProtocolMember { + name, + data, + materialization: self.materialization, + }) + } + + pub(super) fn includes_member(self, db: &'db dyn Db, name: &str) -> bool { + self.interface.includes_member(db, name) + } + + /// Includes inherited `object` members except `__hash__`, which subclasses can disable. + fn includes_member_or_object_fallback( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> bool { + self.includes_member(db, name) + || name != "__hash__" + && object_member_names(db, self.interface.program(db)).contains(name) + && matches!( + Type::object().member(db, env, name).place, + Place::Defined(place) if place.is_definitely_defined() + ) + } + + /// Compare the original and materialized forms of members required by `required`. + /// + /// An unrelated materialized member must not prevent a protocol from retaining its + /// nominal relationship to one of its bases. + pub(super) fn differs_for_members_required_by( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + required: Self, + ) -> bool { + required.members(db).any(|required_member| { + let Some(materialized) = self.member_by_name(db, required_member.name()) else { + return false; + }; + let original = ProtocolMember { + name: materialized.name, + data: materialized.data, + materialization: None, + }; + + if materialized + .access(db, env, ProtocolMemberAccessMode::Instance) + .resolved(db, env) + != original + .access(db, env, ProtocolMemberAccessMode::Instance) + .resolved(db, env) + { + return true; + } + + // Class access to an ordinary instance method requires only that the method + // exists. Its unbound `self` is not part of structural compatibility and can + // recursively refer to this protocol, so do not materialize that signature. + if materialized.is_instance_method() { + return false; + } + + materialized + .access(db, env, ProtocolMemberAccessMode::Class) + .resolved(db, env) + != original + .access(db, env, ProtocolMemberAccessMode::Class) + .resolved(db, env) + }) + } + + /// Returns the declared instance-write requirement for a protocol member. + /// + /// `None` means that the protocol does not declare `name`; `Some((None, _))` means that the + /// member exists but is read-only. A writable member's requirement is bound to `receiver_ty` + /// before it is returned. + pub(super) fn instance_write_requirement( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + receiver_ty: Type<'db>, + name: &str, + ) -> Option<(Option>, TypeQualifiers)> { + self.member_by_name(db, name).map(|member| { + ( + member + .access(db, env, ProtocolMemberAccessMode::Instance) + .write + .and_then(|write| write.bind_requirement(db, env, receiver_ty)), + member.qualifiers(), + ) + }) + } + + /// Returns the write requirement exposed through `type[Protocol]` lookup. + /// + /// Only members required on every class object that satisfies the meta-protocol are available. + /// Ordinary instance attributes are required on the constructed object instead. + pub(super) fn meta_write_requirement( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + receiver_ty: Type<'db>, + name: &str, + ) -> Option<(Option>, TypeQualifiers)> { + self.member_by_name(db, name).map(|member| { + ( + member + .access(db, env, ProtocolMemberAccessMode::Class) + .write + .and_then(|write| write.bind_compatibility_type(db, env, receiver_ty)), + member.qualifiers(), + ) + }) + } + + /// Returns the callable signature exposed by instance access to a protocol's `__call__` + /// method. + /// + /// The callable is already in its instance-bound form, so callers must not bind it again. + pub(super) fn call_method( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { + self.member_by_name(db, "__call__").and_then(|member| { + if !member.is_method() { + return None; + } + match member + .access(db, env, ProtocolMemberAccessMode::Instance) + .read + .and_then(|read| read.resolve(db, env)) + .map(ProtocolMemberType::ty) + { + Some(Type::Callable(callable)) => Some(callable), + _ => None, + } + }) + } + + pub(super) fn instance_member( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> PlaceAndQualifiers<'db> { + self.member_by_name(db, name) + .map(|member| PlaceAndQualifiers { + place: member + .access(db, env, ProtocolMemberAccessMode::Instance) + .read + .and_then(|read| read.resolve(db, env)) + .map(|read| Place::bound(read.ty())) + .unwrap_or(Place::Undefined) + .with_provenance(Provenance::from_definition(member.definition())), + qualifiers: member.qualifiers(), + }) + .unwrap_or_else(|| Type::object().member(db, env, name)) + } + + /// Looks up a member guaranteed to exist on every inhabitant of `type[Protocol]`. + /// + /// Methods retain their unbound signatures and `ClassVar`s retain their class-side types. + /// Properties are only required on the constructed instance, so they are undefined even when + /// the nominal protocol origin provides a property descriptor. + pub(super) fn meta_member( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> Option> { + self.member_by_name(db, name).map(|member| { + let read = member.access(db, env, ProtocolMemberAccessMode::Class).read; + PlaceAndQualifiers { + place: read + .and_then(|read| read.resolve(db, env)) + .map(|read| Place::bound(read.ty())) + .unwrap_or(Place::Undefined) + .with_provenance(Provenance::from_definition(member.definition())), + qualifiers: member.qualifiers(), + } + }) + } + + pub(super) fn member_is_property(self, db: &'db dyn Db, name: &str) -> bool { + self.member_by_name(db, name) + .is_some_and(|member| member.is_property()) + } +} + pub(super) fn walk_protocol_interface<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( db: &'db dyn Db, - interface: ProtocolInterface<'db>, + interface: ProtocolInterfaceView<'db>, visitor: &V, ) { for member in interface.members(db) { walk_protocol_member(db, &member, visitor); } - for pack in interface.pending_packs(db) { + for pack in interface.base().pending_packs(db) { visitor.visit_type(db, *pack); } } @@ -413,7 +719,7 @@ pub(super) fn walk_protocol_instance_interface< V: super::visitor::TypeVisitor<'db> + ?Sized, >( db: &'db dyn Db, - interface: ProtocolInterface<'db>, + interface: ProtocolInterfaceView<'db>, receiver_ty: Type<'db>, visitor: &V, ) { @@ -423,7 +729,7 @@ pub(super) fn walk_protocol_instance_interface< // a pending `protocol(**Kwargs)` pack contributes no member yet, but its // typevar is still part of the type — keep this in step with // `walk_protocol_interface` - for pack in interface.pending_packs(db) { + for pack in interface.base().pending_packs(db) { visitor.visit_type(db, *pack); } } @@ -435,37 +741,45 @@ pub(super) fn walk_protocol_instance_member<'db, V: super::visitor::TypeVisitor< receiver_ty: Type<'db>, visitor: &V, ) { + let env = visitor.program_environment(); match member.data.kind { ProtocolMemberKind::Method(method, _) => { + let method = member + .materialization + .map_or(method, |kind| method.materialize(db, env, kind)); let Type::Callable(callable) = method.ty() else { visitor.visit_type(db, method.ty()); return; }; for signature in callable.signatures(db) { if signature.has_implicit_positional_receiver_annotation() { - let signature = signature.bind_self(db, Some(receiver_ty)); + let signature = signature.bind_self(db, env, Some(receiver_ty)); walk_signature(db, &signature, visitor); } else { walk_signature(db, signature, visitor); } } } - ProtocolMemberKind::Property { read, write } => { + ProtocolMemberKind::Property { .. } => { + let access = member.access(db, env, ProtocolMemberAccessMode::Instance); for member_type in [ - read, - write.and_then(ProtocolMemberWrite::domain), - write.and_then(ProtocolMemberWrite::descriptor_type), + access.read, + access.write.and_then(ProtocolMemberWrite::domain), + access.write.and_then(ProtocolMemberWrite::descriptor_type), ] .into_iter() .flatten() { - if let Some(ty) = member_type.bind_self(db, receiver_ty) { + if let Some(ty) = member_type.bind_self(db, env, receiver_ty) { visitor.visit_type(db, ty); } } } ProtocolMemberKind::Attribute(attribute) => { - if let Some(ty) = attribute.bind_self(db, receiver_ty) { + let attribute = member + .materialization + .map_or(attribute, |kind| attribute.materialize(db, env, kind)); + if let Some(ty) = attribute.bind_self(db, env, receiver_ty) { visitor.visit_type(db, ty); } } @@ -477,7 +791,11 @@ impl<'db> ProtocolInterface<'db> { /// /// 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 + pub(super) fn with_property_members<'a, M>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + members: M, + ) -> Self where M: IntoIterator)>, { @@ -490,11 +808,15 @@ impl<'db> ProtocolInterface<'db> { ) }) .collect(); - Self::new(db, members, Box::default()) + Self::new(db, env.program(db), members, Box::default()) } /// Synthesize a new protocol interface with the given methods. - pub(super) fn with_methods<'a, M>(db: &'db dyn Db, members: M) -> Self + pub(super) fn with_methods<'a, M>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + members: M, + ) -> Self where M: IntoIterator)>, { @@ -503,11 +825,11 @@ impl<'db> ProtocolInterface<'db> { .map(|(name, callable)| { ( Name::new(name), - ProtocolMemberData::method(db, callable, None), + ProtocolMemberData::method(db, env, callable, None), ) }) .collect(); - Self::new(db, members, Box::default()) + Self::new(db, env.program(db), members, Box::default()) } /// basedpython: synthesize the interface of an inline `protocol(...)` type expression. @@ -516,6 +838,7 @@ impl<'db> ProtocolInterface<'db> { /// known yet; see [`ProtocolInterface::pending_packs`]. pub(super) fn with_inline_members( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, members: M, packs: Box<[Type<'db>]>, ) -> Self @@ -524,16 +847,22 @@ impl<'db> ProtocolInterface<'db> { { let members: BTreeMap<_, _> = members .into_iter() - .map(|(name, member)| (name, ProtocolMemberData::from_inline(db, member))) + .map(|(name, member)| (name, ProtocolMemberData::from_inline(db, env, member))) .collect(); - Self::new(db, members, packs) + Self::new(db, env.program(db), members, packs) } - fn empty(db: &'db dyn Db) -> Self { - Self::new(db, BTreeMap::default(), Box::default()) + fn empty(db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Self { + Self::new(db, env.program(db), BTreeMap::default(), Box::default()) } - fn cycle_normalized(self, db: &'db dyn Db, previous: Self, cycle: &salsa::Cycle) -> Self { + fn cycle_normalized( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + previous: Self, + cycle: &salsa::Cycle, + ) -> Self { let prev_inner = previous.inner(db); let curr_inner = self.inner(db); @@ -541,14 +870,14 @@ impl<'db> ProtocolInterface<'db> { .iter() .map(|(name, curr_data)| { let normalized = if let Some(prev_data) = prev_inner.get(name) { - curr_data.cycle_normalized(db, prev_data, cycle) + curr_data.cycle_normalized(db, env, prev_data, cycle) } else { curr_data.clone() }; (name.clone(), normalized) }) .collect(); - Self::new(db, members, self.pending_packs(db).clone()) + Self::new(db, env.program(db), members, self.pending_packs(db).clone()) } pub(super) fn members<'a>( @@ -558,9 +887,11 @@ impl<'db> ProtocolInterface<'db> { where 'db: 'a, { - self.inner(db) - .iter() - .map(|(name, data)| ProtocolMember { name, data }) + self.inner(db).iter().map(|(name, data)| ProtocolMember { + name, + data, + materialization: None, + }) } pub(super) fn filter_members( @@ -570,9 +901,16 @@ impl<'db> ProtocolInterface<'db> { ) -> Self { Self::new( db, + self.program(db), self.inner(db) .iter() - .filter(|&(name, data)| predicate(&ProtocolMember { name, data })) + .filter(|&(name, data)| { + predicate(&ProtocolMember { + name, + data, + materialization: None, + }) + }) .map(|(name, data)| (name.clone(), data.clone())) .collect::>(), self.pending_packs(db).clone(), @@ -585,16 +923,10 @@ impl<'db> ProtocolInterface<'db> { pub(super) fn non_method_members(self, db: &'db dyn Db) -> Vec> { self.members(db) - .filter(|member| !member.is_method() && !member.has_todo_type()) + .filter(|member| !member.is_method()) .collect() } - fn member_by_name<'a>(self, db: &'db dyn Db, name: &'a str) -> Option> { - self.inner(db) - .get(name) - .map(|data| ProtocolMember { name, data }) - } - pub(super) fn includes_member(self, db: &'db dyn Db, name: &str) -> bool { self.inner(db).contains_key(name) } @@ -602,7 +934,8 @@ impl<'db> ProtocolInterface<'db> { /// basedpython: whether `name` is an ordinary method member, the one kind whose access /// binds a receiver away. pub(super) fn is_instance_method_member(self, db: &'db dyn Db, name: &str) -> bool { - self.member_by_name(db, name) + ProtocolInterfaceView::new(self, None) + .member_by_name(db, name) .is_some_and(|member| member.is_instance_method()) } @@ -611,13 +944,15 @@ impl<'db> ProtocolInterface<'db> { pub(super) fn includes_generic_writable_instance_member( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, generic_context: GenericContext<'db>, ) -> bool { - self.member_by_name(db, name) - .and_then(|member| member.capabilities(db).instance.write) + self.inner(db) + .get(name) + .and_then(|data| data.capabilities(db, env).instance.write) .and_then(ProtocolMemberWrite::domain) - .and_then(|write| write.resolve(db)) + .and_then(|write| write.resolve(db, env)) .is_some_and(|write| { matches!( write.ty(), @@ -629,127 +964,31 @@ impl<'db> ProtocolInterface<'db> { }) } - /// Returns the declared instance-write requirement for a protocol member. - /// - /// `None` means that the protocol does not declare `name`; `Some((None, _))` means that the - /// member exists but is read-only. A writable member's requirement is bound to `receiver_ty` - /// before it is returned. - pub(super) fn instance_write_requirement( - self, - db: &'db dyn Db, - receiver_ty: Type<'db>, - name: &str, - ) -> Option<(Option>, TypeQualifiers)> { - self.member_by_name(db, name).map(|member| { - let capabilities = member.capabilities(db); - ( - capabilities - .instance - .write - .and_then(|write| write.bind_requirement(db, receiver_ty)), - member.qualifiers(), - ) - }) - } - - /// Returns the write requirement exposed through `type[Protocol]` lookup. - /// - /// Only members required on every class object that satisfies the meta-protocol are available. - /// Ordinary instance attributes are required on the constructed object instead. - pub(super) fn meta_write_requirement( - self, - db: &'db dyn Db, - receiver_ty: Type<'db>, - name: &str, - ) -> Option<(Option>, TypeQualifiers)> { - self.member_by_name(db, name).and_then(|member| { - Some(( - member - .meta_access(db)? - .write - .and_then(|write| write.bind_compatibility_type(db, receiver_ty)), - member.qualifiers(), - )) - }) - } - - /// Returns the callable signature exposed by instance access to a protocol's `__call__` - /// method. - /// - /// The callable is already in its instance-bound form, so callers must not bind it again. - pub(super) fn call_method(self, db: &'db dyn Db) -> Option> { - self.member_by_name(db, "__call__").and_then(|member| { - if !member.is_method() { - return None; - } - match member - .capabilities(db) - .instance - .read - .and_then(|read| read.resolve(db)) - .map(ProtocolMemberType::ty) - { - Some(Type::Callable(callable)) => Some(callable), - _ => None, - } - }) - } - - pub(super) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { - self.member_by_name(db, name) - .map(|member| { - let capabilities = member.capabilities(db); - PlaceAndQualifiers { - place: capabilities - .instance - .read - .and_then(|read| read.resolve(db)) - .map(|read| Place::bound(read.ty())) - .unwrap_or(Place::Undefined) - .with_provenance(Provenance::from_definition(member.definition())), - qualifiers: member.qualifiers(), - } - }) - .unwrap_or_else(|| Type::object().member(db, name)) - } - - /// Looks up a member guaranteed to exist on every inhabitant of `type[Protocol]`. - /// - /// Methods retain their unbound signatures and `ClassVar`s retain their class-side types. - /// Properties are only required on the constructed instance, so they are undefined even when - /// the nominal protocol origin provides a property descriptor. - pub(super) fn meta_member( + pub(super) fn instance_member( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, - ) -> Option> { - self.member_by_name(db, name).and_then(|member| { - let read = member.meta_access(db)?.read; - Some(PlaceAndQualifiers { - place: read - .and_then(|read| read.resolve(db)) - .map(|read| Place::bound(read.ty())) - .unwrap_or(Place::Undefined) - .with_provenance(Provenance::from_definition(member.definition())), - qualifiers: member.qualifiers(), - }) - }) + ) -> PlaceAndQualifiers<'db> { + ProtocolInterfaceView::new(self, None).instance_member(db, env, name) } pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { Some(Self::new( db, + env.program(db), self.inner(db) .iter() .map(|(name, data)| { Some(( name.clone(), - data.recursive_type_normalized_impl(db, div, nested)?, + data.recursive_type_normalized_impl(db, env, div, nested)?, )) }) .collect::>>()?, @@ -760,9 +999,10 @@ impl<'db> ProtocolInterface<'db> { pub(super) fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { let mut members: BTreeMap<_, _> = self .inner(db) @@ -770,7 +1010,7 @@ impl<'db> ProtocolInterface<'db> { .map(|(name, data)| { ( name.clone(), - data.apply_type_mapping_impl(db, type_mapping, tcx, visitor), + data.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor), ) }) .collect(); @@ -780,7 +1020,7 @@ impl<'db> ProtocolInterface<'db> { let packs = self.pending_packs(db); let mut pending = Vec::with_capacity(packs.len()); for pack in packs { - let pack = pack.apply_type_mapping_impl(db, type_mapping, tcx, visitor); + let pack = pack.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor); match pack.keyword_pack_fields(db) { Some(fields) => { for (name, field_ty) in fields { @@ -798,36 +1038,43 @@ impl<'db> ProtocolInterface<'db> { } } - Self::new(db, members, pending.into_boxed_slice()) + Self::new(db, env.program(db), members, pending.into_boxed_slice()) } pub(super) fn find_legacy_typevars_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, ) { for data in self.inner(db).values() { - data.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + data.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } for pack in self.pending_packs(db) { - pack.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + pack.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } } - pub(super) fn display(self, db: &'db dyn Db) -> impl std::fmt::Display { - struct ProtocolInterfaceDisplay<'db> { + pub(super) fn display<'env>( + self, + db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, + ) -> impl std::fmt::Display + 'env { + struct ProtocolInterfaceDisplay<'env, 'db> { db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, interface: ProtocolInterface<'db>, } - impl std::fmt::Display for ProtocolInterfaceDisplay<'_> { + impl std::fmt::Display for ProtocolInterfaceDisplay<'_, '_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let db = self.db; f.write_char('{')?; - for (i, (name, data)) in self.interface.inner(self.db).iter().enumerate() { - write!(f, "\"{name}\": {data}", data = data.display(self.db))?; - if i < self.interface.inner(self.db).len() - 1 { + for (i, (name, data)) in self.interface.inner(db).iter().enumerate() { + write!(f, "\"{name}\": {data}", data = data.display(db, self.env))?; + if i < self.interface.inner(db).len() - 1 { f.write_str(", ")?; } } @@ -837,6 +1084,7 @@ impl<'db> ProtocolInterface<'db> { ProtocolInterfaceDisplay { db, + env, interface: self, } } @@ -886,13 +1134,17 @@ impl<'db> ProtocolMemberWrite<'db> { } } - fn display_type(self, db: &'db dyn Db) -> Option> { + fn display_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { match self { - Self::Type(member) => member.resolve(db), + Self::Type(member) => member.resolve(db, env), Self::Descriptor { domain: Some(domain), .. - } => domain.resolve(db), + } => domain.resolve(db, env), Self::Descriptor { domain: None, .. } => Some(ProtocolMemberType::new(Type::unknown())), } } @@ -900,37 +1152,79 @@ impl<'db> ProtocolMemberWrite<'db> { fn bind_requirement( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, self_type: Type<'db>, ) -> Option> { match self { Self::Type(member) => Some(ProtocolMemberWriteRequirement::AssignableTo( - member.bind_self(db, self_type)?, + member.bind_self(db, env, self_type)?, )), Self::Descriptor { descriptor, domain } => { Some(ProtocolMemberWriteRequirement::Descriptor { - descriptor_ty: descriptor.bind_self(db, self_type)?, + descriptor_ty: descriptor.bind_self(db, env, self_type)?, receiver_ty: self_type, - domain: domain.and_then(|domain| domain.bind_self(db, self_type)), + domain: domain.and_then(|domain| domain.bind_self(db, env, self_type)), }) } } } - fn bind_compatibility_type(self, db: &'db dyn Db, self_type: Type<'db>) -> Option> { + fn bind_compatibility_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + self_type: Type<'db>, + ) -> Option> { match self { - Self::Type(member) => member.bind_self(db, self_type), + Self::Type(member) => member.bind_self(db, env, self_type), Self::Descriptor { domain, .. } => Some( domain - .and_then(|domain| domain.bind_self(db, self_type)) + .and_then(|domain| domain.bind_self(db, env, self_type)) .unwrap_or_else(Type::unknown), ), } } - fn cycle_normalized(self, db: &'db dyn Db, previous: Self, cycle: &salsa::Cycle) -> Self { + /// Materialize an exposed write domain in its contravariant position. + /// + /// The descriptor itself remains unchanged so normal descriptor dispatch and deletion + /// continue to use the declaration on the original protocol class. + fn materialize( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + kind: MaterializationKind, + ) -> Self { + match self { + Self::Type(member) => Self::Type(member.materialize(db, env, kind.flip())), + Self::Descriptor { descriptor, domain } => Self::Descriptor { + descriptor, + domain: domain.map(|domain| domain.materialize(db, env, kind.flip())), + }, + } + } + + /// Resolve accessor representations without changing descriptor identity. + fn resolved(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Self { + match self { + Self::Type(member) => Self::Type(member.resolve(db, env).unwrap_or(member)), + Self::Descriptor { descriptor, domain } => Self::Descriptor { + descriptor: descriptor.resolve(db, env).unwrap_or(descriptor), + domain: domain.map(|member| member.resolve(db, env).unwrap_or(member)), + }, + } + } + + fn cycle_normalized( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + previous: Self, + cycle: &salsa::Cycle, + ) -> Self { match (self, previous) { (Self::Type(current), Self::Type(previous)) => { - Self::Type(current.cycle_normalized(db, previous, cycle)) + Self::Type(current.cycle_normalized(db, env, previous, cycle)) } ( Self::Descriptor { @@ -942,16 +1236,32 @@ impl<'db> ProtocolMemberWrite<'db> { domain: previous_domain, }, ) => Self::Descriptor { - descriptor: current_descriptor.cycle_normalized(db, previous_descriptor, cycle), - domain: cycle_normalized_optional_type(db, current_domain, previous_domain, cycle), + descriptor: current_descriptor.cycle_normalized( + db, + env, + previous_descriptor, + cycle, + ), + domain: cycle_normalized_optional_type( + db, + env, + current_domain, + previous_domain, + cycle, + ), }, (current, _) => current, } } - fn cycle_normalized_without_previous(self, db: &'db dyn Db, cycle: &salsa::Cycle) -> Self { + fn cycle_normalized_without_previous( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + cycle: &salsa::Cycle, + ) -> Self { let normalize = |member: ProtocolMemberType<'db>| { - member.with_ty(member.ty().recursive_type_normalized(db, cycle)) + member.with_ty(member.ty().recursive_type_normalized(db, env, cycle)) }; match self { Self::Type(member) => Self::Type(normalize(member)), @@ -965,17 +1275,20 @@ impl<'db> ProtocolMemberWrite<'db> { fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { Some(match self { Self::Type(member) => { - Self::Type(member.recursive_type_normalized_impl(db, div, nested)?) + Self::Type(member.recursive_type_normalized_impl(db, env, div, nested)?) } Self::Descriptor { descriptor, domain } => Self::Descriptor { - descriptor: descriptor.recursive_type_normalized_impl(db, div, nested)?, + descriptor: descriptor.recursive_type_normalized_impl(db, env, div, nested)?, domain: match domain { - Some(domain) => Some(domain.recursive_type_normalized_impl(db, div, nested)?), + Some(domain) => { + Some(domain.recursive_type_normalized_impl(db, env, div, nested)?) + } None => None, }, }, @@ -985,33 +1298,40 @@ impl<'db> ProtocolMemberWrite<'db> { fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { match self { Self::Type(member) => { - Self::Type(member.apply_type_mapping_impl(db, type_mapping, tcx, visitor)) + Self::Type(member.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor)) } Self::Descriptor { descriptor, domain } => Self::Descriptor { - descriptor: descriptor.apply_type_mapping_impl(db, type_mapping, tcx, visitor), - domain: domain - .map(|domain| domain.apply_type_mapping_impl(db, type_mapping, tcx, visitor)), + descriptor: descriptor.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor), + domain: domain.map(|domain| { + domain.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor) + }), }, } } } impl<'db> VarianceInferable<'db> for ProtocolInterface<'db> { - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { + fn variance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { self.members(db) .flat_map(|member| { - let capabilities = member.capabilities(db); + let capabilities = member.capabilities(db, env); [capabilities.instance, capabilities.class] .into_iter() - .flat_map(|access| access.variances(db)) + .flat_map(|access| access.variances(db, env)) }) - .map(|(ty, variance)| ty.with_polarity(variance).variance_of(db, typevar)) + .map(|(ty, variance)| ty.with_polarity(variance).variance_of(db, env, typevar)) .collect() } } @@ -1081,50 +1401,84 @@ impl<'db> ProtocolMemberType<'db> { } /// Resolves a stored property accessor to the value type exposed by that access. - fn resolve(self, db: &'db dyn Db) -> Option { + fn resolve(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Option { match self { Self::Value { .. } => Some(self), - Self::PropertyGetter(getter) => property_get_member_type(db, getter), - Self::PropertySetter(setter) => property_set_member_type(db, setter), + Self::PropertyGetter(getter) => property_get_member_type(db, env, getter), + Self::PropertySetter(setter) => property_set_member_type(db, env, setter), } } + /// Materialize the value exposed by a member, not the accessor implementing it. + /// + /// In particular, resolving a property setter before materialization prevents its callable + /// parameter from introducing a second contravariant flip. + fn materialize( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + kind: MaterializationKind, + ) -> Self { + let Some(resolved) = self.resolve(db, env) else { + return self; + }; + let ty = match kind { + MaterializationKind::Top => resolved.ty().top_materialization(db, env), + MaterializationKind::Bottom => resolved.ty().bottom_materialization(db, env), + }; + resolved.with_ty(ty) + } + /// Resolves this member type and binds member-local `Self` occurrences to `self_type`. - fn bind_self(self, db: &'db dyn Db, self_type: Type<'db>) -> Option> { + fn bind_self( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + self_type: Type<'db>, + ) -> Option> { let Self::Value { ty, self_binding_context, - } = self.resolve(db)? + } = self.resolve(db, env)? else { return None; }; - if !ty.contains_self(db) { + if !ty.contains_self(db, env) { return Some(ty); } Some(ty.apply_type_mapping( db, - &TypeMapping::BindSelf(SelfBinding::new(db, self_type, self_binding_context)), + env, + &TypeMapping::BindSelf(SelfBinding::new(db, env, self_type, self_binding_context)), TypeContext::default(), )) } - fn cycle_normalized(self, db: &'db dyn Db, previous: Self, cycle: &salsa::Cycle) -> Self { - let ty = self.ty().cycle_normalized(db, previous.ty(), cycle); + fn cycle_normalized( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + previous: Self, + cycle: &salsa::Cycle, + ) -> Self { + let ty = self.ty().cycle_normalized(db, env, previous.ty(), cycle); self.with_ty(ty) } fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { let ty = if nested { - self.ty().recursive_type_normalized_impl(db, div, true)? + self.ty() + .recursive_type_normalized_impl(db, env, div, true)? } else { self.ty() - .recursive_type_normalized_impl(db, div, true) + .recursive_type_normalized_impl(db, env, div, true) .unwrap_or(div) }; Some(self.with_ty(ty)) @@ -1133,13 +1487,14 @@ impl<'db> ProtocolMemberType<'db> { fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { let ty = self .ty() - .apply_type_mapping_impl(db, type_mapping, tcx, visitor); + .apply_type_mapping_impl(db, env, type_mapping, tcx, visitor); self.with_ty(ty) } } @@ -1167,15 +1522,41 @@ impl<'db> ProtocolMemberAccess<'db> { Self { read, write } } - fn variances(self, db: &'db dyn Db) -> impl Iterator, TypeVarVariance)> { + fn materialize( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + kind: MaterializationKind, + ) -> Self { + Self { + read: self.read.map(|read| read.materialize(db, env, kind)), + write: self.write.map(|write| write.materialize(db, env, kind)), + } + } + + /// Resolve readable and writable accessor representations without losing descriptor identity. + fn resolved(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Self { + Self { + read: self + .read + .map(|member| member.resolve(db, env).unwrap_or(member)), + write: self.write.map(|write| write.resolved(db, env)), + } + } + + fn variances( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> impl Iterator, TypeVarVariance)> { self.read - .and_then(|member| member.resolve(db)) + .and_then(|member| member.resolve(db, env)) .map(|member| (member.ty(), TypeVarVariance::Covariant)) .into_iter() .chain( self.write .and_then(ProtocolMemberWrite::domain) - .and_then(|member| member.resolve(db)) + .and_then(|member| member.resolve(db, env)) .map(|member| (member.ty(), TypeVarVariance::Contravariant)), ) } @@ -1192,6 +1573,20 @@ struct ProtocolMemberCapabilities<'db> { class: ProtocolMemberAccess<'db>, } +impl<'db> ProtocolMemberCapabilities<'db> { + fn materialize( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + kind: MaterializationKind, + ) -> Self { + Self { + instance: self.instance.materialize(db, env, kind), + class: self.class.materialize(db, env, kind), + } + } +} + #[derive(Copy, Clone, Eq, PartialEq)] enum ProtocolMemberAccessMode { Instance, @@ -1200,14 +1595,15 @@ enum ProtocolMemberAccessMode { fn cycle_normalized_optional_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, current: Option>, previous: Option>, cycle: &salsa::Cycle, ) -> Option> { match (current, previous) { - (Some(current), Some(previous)) => Some(current.cycle_normalized(db, previous, cycle)), + (Some(current), Some(previous)) => Some(current.cycle_normalized(db, env, previous, cycle)), (Some(current), None) => { - Some(current.with_ty(current.ty().recursive_type_normalized(db, cycle))) + Some(current.with_ty(current.ty().recursive_type_normalized(db, env, cycle))) } (None, _) => None, } @@ -1223,13 +1619,14 @@ pub(super) struct ProtocolMemberData<'db> { impl<'db> ProtocolMemberData<'db> { fn method( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, callable: CallableType<'db>, definition: Option>, ) -> Self { let (method_kind, callable) = if callable.is_classmethod_like(db) { ( ProtocolMethodKind::Class, - protocol_bind_self(db, callable, None), + protocol_bind_self(db, env.program(db), callable, None), ) } else if callable.is_staticmethod_like(db) { (ProtocolMethodKind::Static, callable.into_regular(db)) @@ -1247,12 +1644,16 @@ impl<'db> ProtocolMemberData<'db> { } } - fn from_inline(db: &'db dyn Db, member: InlineProtocolMember<'db>) -> Self { + fn from_inline( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + member: InlineProtocolMember<'db>, + ) -> Self { match member { InlineProtocolMember::Attribute(ty) => { Self::attribute(ty, TypeQualifiers::default(), None) } - InlineProtocolMember::Method(callable) => Self::method(db, callable, None), + InlineProtocolMember::Method(callable) => Self::method(db, env, callable, None), InlineProtocolMember::ReadOnlyAttribute(ty) => { Self::property(Some(ProtocolMemberType::new(ty)), None, None) } @@ -1289,13 +1690,17 @@ impl<'db> ProtocolMemberData<'db> { /// /// These are views of the canonical method, property, or attribute representation below; /// keeping them derived prevents the stored member kind and its capabilities from diverging. - fn capabilities(&self, db: &'db dyn Db) -> ProtocolMemberCapabilities<'db> { + fn capabilities( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> ProtocolMemberCapabilities<'db> { match self.kind { ProtocolMemberKind::Method(member, kind) => { let instance_method = match (member.ty(), kind) { - (Type::Callable(callable), ProtocolMethodKind::Instance) => { - member.with_ty(Type::Callable(protocol_bind_self(db, callable, None))) - } + (Type::Callable(callable), ProtocolMethodKind::Instance) => member.with_ty( + Type::Callable(protocol_bind_self(db, env.program(db), callable, None)), + ), _ => member, }; ProtocolMemberCapabilities { @@ -1310,20 +1715,16 @@ impl<'db> ProtocolMemberData<'db> { ProtocolMemberKind::Attribute(member_ty) => { let is_class_var = self.qualifiers.contains(TypeQualifiers::CLASS_VAR); let is_final = self.qualifiers.contains(TypeQualifiers::FINAL); - // A `Todo` records a protocol member form that is not modeled yet; do not infer a - // write requirement from that temporary representation. - let is_todo = member_ty.ty().is_todo(); ProtocolMemberCapabilities { instance: ProtocolMemberAccess::new( Some(member_ty), - (!is_class_var && !is_final && !is_todo) + (!is_class_var && !is_final) .then_some(ProtocolMemberWrite::from_type(member_ty)), ), class: if is_class_var { ProtocolMemberAccess::new( Some(member_ty), - (!is_final && !is_todo) - .then_some(ProtocolMemberWrite::from_type(member_ty)), + (!is_final).then_some(ProtocolMemberWrite::from_type(member_ty)), ) } else { ProtocolMemberAccess::NONE @@ -1333,9 +1734,15 @@ impl<'db> ProtocolMemberData<'db> { } } - fn cycle_normalized(&self, db: &'db dyn Db, previous: &Self, cycle: &salsa::Cycle) -> Self { + fn cycle_normalized( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + previous: &Self, + cycle: &salsa::Cycle, + ) -> Self { Self { - kind: self.kind.cycle_normalized(db, previous.kind, cycle), + kind: self.kind.cycle_normalized(db, env, previous.kind, cycle), qualifiers: self.qualifiers, definition: self.definition, } @@ -1344,11 +1751,14 @@ impl<'db> ProtocolMemberData<'db> { fn recursive_type_normalized_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { Some(Self { - kind: self.kind.recursive_type_normalized_impl(db, div, nested)?, + kind: self + .kind + .recursive_type_normalized_impl(db, env, div, nested)?, qualifiers: self.qualifiers, definition: self.definition, }) @@ -1357,14 +1767,15 @@ impl<'db> ProtocolMemberData<'db> { fn apply_type_mapping_impl<'a>( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { Self { kind: self .kind - .apply_type_mapping_impl(db, type_mapping, tcx, visitor), + .apply_type_mapping_impl(db, env, type_mapping, tcx, visitor), qualifiers: self.qualifiers, definition: self.definition, } @@ -1373,6 +1784,7 @@ impl<'db> ProtocolMemberData<'db> { fn find_legacy_typevars_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, _visitor: &FindLegacyTypeVarsVisitor<'db>, @@ -1380,36 +1792,49 @@ impl<'db> ProtocolMemberData<'db> { for member_type in self.kind.member_types() { member_type .ty() - .find_legacy_typevars(db, binding_context, typevars); + .find_legacy_typevars(db, env, binding_context, typevars); } } - fn display(&self, db: &'db dyn Db) -> impl std::fmt::Display { - struct ProtocolMemberDataDisplay<'db> { + fn display<'env>( + &self, + db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, + ) -> impl std::fmt::Display + 'env { + struct ProtocolMemberDataDisplay<'env, 'db> { db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, kind: ProtocolMemberKind<'db>, qualifiers: TypeQualifiers, } - impl std::fmt::Display for ProtocolMemberDataDisplay<'_> { + impl std::fmt::Display for ProtocolMemberDataDisplay<'_, '_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let db = self.db; match self.kind { ProtocolMemberKind::Method(member, _) => { - write!(f, "MethodMember(`{}`)", member.ty().display(self.db)) + write!(f, "MethodMember(`{}`)", member.ty().display(db, self.env)) } ProtocolMemberKind::Property { read, write } => { + let env = self.env; let mut d = f.debug_struct("PropertyMember"); - if let Some(read) = read.and_then(|read| read.resolve(self.db)) { - d.field("read", &format_args!("`{}`", read.ty().display(self.db))); + if let Some(read) = read.and_then(|read| read.resolve(db, env)) { + d.field( + "read", + &format_args!("`{}`", read.ty().display(db, self.env)), + ); } - if let Some(write) = write.and_then(|write| write.display_type(self.db)) { - d.field("write", &format_args!("`{}`", write.ty().display(self.db))); + if let Some(write) = write.and_then(|write| write.display_type(db, env)) { + d.field( + "write", + &format_args!("`{}`", write.ty().display(db, self.env)), + ); } d.finish() } ProtocolMemberKind::Attribute(attribute) => { f.write_str("AttributeMember(")?; - write!(f, "`{}`", attribute.ty().display(self.db))?; + write!(f, "`{}`", attribute.ty().display(db, self.env))?; if self.qualifiers.contains(TypeQualifiers::CLASS_VAR) { f.write_str("; ClassVar")?; } @@ -1421,6 +1846,7 @@ impl<'db> ProtocolMemberData<'db> { ProtocolMemberDataDisplay { db, + env, kind: self.kind, qualifiers: self.qualifiers, } @@ -1459,17 +1885,24 @@ impl<'db> ProtocolMemberKind<'db> { .flatten() } - fn cycle_normalized(self, db: &'db dyn Db, previous: Self, cycle: &salsa::Cycle) -> Self { + fn cycle_normalized( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + previous: Self, + cycle: &salsa::Cycle, + ) -> Self { match (self, previous) { (Self::Method(current, kind), Self::Method(previous, _)) => { let (Type::Callable(current_callable), Type::Callable(previous_callable)) = (current.ty(), previous.ty()) else { - return Self::Method(current.cycle_normalized(db, previous, cycle), kind); + return Self::Method(current.cycle_normalized(db, env, previous, cycle), kind); }; debug_assert_eq!(current_callable.kind(db), previous_callable.kind(db)); let signatures = current_callable.signatures(db).cycle_normalized( db, + env, previous_callable.signatures(db), cycle, ); @@ -1493,19 +1926,19 @@ impl<'db> ProtocolMemberKind<'db> { write: previous_write, }, ) => Self::Property { - read: cycle_normalized_optional_type(db, current_read, previous_read, cycle), + read: cycle_normalized_optional_type(db, env, current_read, previous_read, cycle), write: match (current_write, previous_write) { (Some(current), Some(previous)) => { - Some(current.cycle_normalized(db, previous, cycle)) + Some(current.cycle_normalized(db, env, previous, cycle)) } (Some(current), None) => { - Some(current.cycle_normalized_without_previous(db, cycle)) + Some(current.cycle_normalized_without_previous(db, env, cycle)) } (None, _) => None, }, }, (Self::Attribute(current), Self::Attribute(previous)) => { - Self::Attribute(current.cycle_normalized(db, previous, cycle)) + Self::Attribute(current.cycle_normalized(db, env, previous, cycle)) } (current, _) => current, } @@ -1514,26 +1947,29 @@ impl<'db> ProtocolMemberKind<'db> { fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { Some(match self { Self::Method(member, kind) => Self::Method( - member.recursive_type_normalized_impl(db, div, nested)?, + member.recursive_type_normalized_impl(db, env, div, nested)?, kind, ), Self::Property { read, write } => Self::Property { read: match read { - Some(read) => Some(read.recursive_type_normalized_impl(db, div, nested)?), + Some(read) => Some(read.recursive_type_normalized_impl(db, env, div, nested)?), None => None, }, write: match write { - Some(write) => Some(write.recursive_type_normalized_impl(db, div, nested)?), + Some(write) => { + Some(write.recursive_type_normalized_impl(db, env, div, nested)?) + } None => None, }, }, Self::Attribute(attribute) => { - Self::Attribute(attribute.recursive_type_normalized_impl(db, div, nested)?) + Self::Attribute(attribute.recursive_type_normalized_impl(db, env, div, nested)?) } }) } @@ -1541,23 +1977,30 @@ impl<'db> ProtocolMemberKind<'db> { fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { match self { Self::Method(member, kind) => Self::Method( - member.apply_type_mapping_impl(db, type_mapping, tcx, visitor), + member.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor), kind, ), Self::Property { read, write } => Self::Property { - read: read.map(|read| read.apply_type_mapping_impl(db, type_mapping, tcx, visitor)), - write: write - .map(|write| write.apply_type_mapping_impl(db, type_mapping, tcx, visitor)), + read: read + .map(|read| read.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor)), + write: write.map(|write| { + write.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor) + }), }, - Self::Attribute(attribute) => { - Self::Attribute(attribute.apply_type_mapping_impl(db, type_mapping, tcx, visitor)) - } + Self::Attribute(attribute) => Self::Attribute(attribute.apply_type_mapping_impl( + db, + env, + type_mapping, + tcx, + visitor, + )), } } } @@ -1567,6 +2010,7 @@ impl<'db> ProtocolMemberKind<'db> { pub(super) struct ProtocolMember<'a, 'db> { name: &'a str, data: &'a ProtocolMemberData<'db>, + materialization: Option, } /// Orders protocol members so that finite constraints are established before recursive relations. @@ -1588,6 +2032,23 @@ fn walk_protocol_member<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( member: &ProtocolMember<'_, 'db>, visitor: &V, ) { + if member.materialization.is_some() { + let capabilities = member.capabilities(db, visitor.program_environment()); + for access in [capabilities.instance, capabilities.class] { + for member_type in [ + access.read, + access.write.and_then(ProtocolMemberWrite::domain), + access.write.and_then(ProtocolMemberWrite::descriptor_type), + ] + .into_iter() + .flatten() + { + visitor.visit_type(db, member_type.ty()); + } + } + return; + } + for member_type in member.data.kind.member_types() { visitor.visit_type(db, member_type.ty()); } @@ -1598,11 +2059,70 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { self.name } - pub(super) fn qualifiers(&self) -> TypeQualifiers { + fn qualifiers(&self) -> TypeQualifiers { self.data.qualifiers } - pub(super) fn is_method(&self) -> bool { + /// Returns whether an instance declaration conflicts with a required writable class variable. + /// + /// An unannotated assignment preserves an inherited `ClassVar`; an explicit instance + /// annotation does not: + /// + /// ```python + /// from typing import ClassVar + /// + /// class Base: + /// value: ClassVar[int] + /// + /// class Valid(Base): + /// value = 1 + /// + /// class Invalid(Base): + /// value: int = 1 + /// ``` + /// + /// Inspect declarations before descriptor binding, and ignore synthesized members without + /// source provenance. + pub(super) fn has_incompatible_class_variable_declaration( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + ) -> bool { + let qualifiers = self.qualifiers(); + qualifiers.contains(TypeQualifiers::CLASS_VAR) + && !qualifiers.contains(TypeQualifiers::FINAL) + && ty + .nominal_class(db, env) + .or_else(|| { + if !is_class_object_type(ty) { + return None; + } + + ty.to_meta_type(db, env) + .to_instance_approximation(db, env)? + .nominal_class(db, env) + }) + .is_some_and(|class| { + effective_superclass_variable_kind(db, class, Name::new(self.name)) + == Some(VariableKind::Instance) + && [ + class + .class_member(db, env, self.name, MemberLookupPolicy::default()) + .place, + class.instance_member(db, env, self.name).place, + ] + .into_iter() + .any(|place| { + matches!( + place, + Place::Defined(defined) if defined.provenance != Provenance::Unknown + ) + }) + }) + } + + fn is_method(&self) -> bool { matches!(self.data.kind, ProtocolMemberKind::Method(..)) } @@ -1634,9 +2154,13 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { /// /// Simple finite members are cheapest, followed by finite overloads. Recursive and /// alias-containing members are compared last because they can expand the same interface again. - fn structural_member_priority(&self, db: &'db dyn Db) -> StructuralMemberPriority { + fn structural_member_priority( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> StructuralMemberPriority { let is_recursive_type = |ty| { - any_over_type(db, ty, false, |nested| { + any_over_type(db, env, ty, false, |nested| { matches!(nested, Type::ProtocolInstance(_) | Type::TypeAlias(_)) }) }; @@ -1644,7 +2168,7 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { let ProtocolMemberKind::Method(member, _) = self.data.kind else { let is_finite = self.data.kind.member_types().all(|member| { member - .resolve(db) + .resolve(db, env) .is_some_and(|member| !is_recursive_type(member.ty())) }); return if is_finite { @@ -1822,54 +2346,64 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { self.data.definition } - fn capabilities(&self, db: &'db dyn Db) -> ProtocolMemberCapabilities<'db> { - self.data.capabilities(db) + fn capabilities( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> ProtocolMemberCapabilities<'db> { + let capabilities = self.data.capabilities(db, env); + self.materialization + .map_or(capabilities, |kind| capabilities.materialize(db, env, kind)) + } + + /// Materialize only the access that an operation actually observes. + /// + /// In particular, an instance-method relation must not materialize the class-side callable: + /// its unbound receiver can recursively refer to the very protocol being compared. + fn access( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + mode: ProtocolMemberAccessMode, + ) -> ProtocolMemberAccess<'db> { + let capabilities = self.data.capabilities(db, env); + let access = match mode { + ProtocolMemberAccessMode::Instance => capabilities.instance, + ProtocolMemberAccessMode::Class => capabilities.class, + }; + self.materialization + .map_or(access, |kind| access.materialize(db, env, kind)) } - /// Returns the accesses that a candidate value must provide for this member. + /// Returns the access that a candidate value must provide for this member. /// /// A module-level callable can satisfy an ordinary or static method through direct member /// access. A class object can likewise satisfy a class, static, or ordinary instance method; /// special instance methods instead use special-method lookup through the meta-type. Neither /// case needs a separate class-side check for the same member. - fn implementation_capabilities( + fn implementation_access( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, - ) -> ProtocolMemberCapabilities<'db> { - let capabilities = self.capabilities(db); - if matches!( - (ty, self.data.kind), - ( - Type::ModuleLiteral(_), - ProtocolMemberKind::Method( - _, - ProtocolMethodKind::Instance | ProtocolMethodKind::Static + mode: ProtocolMemberAccessMode, + ) -> ProtocolMemberAccess<'db> { + if mode == ProtocolMemberAccessMode::Class + && (matches!( + (ty, self.data.kind), + ( + Type::ModuleLiteral(_), + ProtocolMemberKind::Method( + _, + ProtocolMethodKind::Instance | ProtocolMethodKind::Static + ) ) - ) - ) || (is_class_object_type(ty) && self.is_method()) + ) || (is_class_object_type(ty) && self.is_method())) { - ProtocolMemberCapabilities { - class: ProtocolMemberAccess::NONE, - ..capabilities - } + ProtocolMemberAccess::NONE } else { - capabilities - } - } - - fn meta_access(&self, db: &'db dyn Db) -> Option> { - if self.has_todo_type() { - return None; + self.access(db, env, mode) } - Some(self.capabilities(db).class) - } - - fn has_todo_type(&self) -> bool { - self.data - .kind - .member_types() - .any(|ty| matches!(ty, ProtocolMemberType::Value { ty, .. } if ty.is_todo())) } /// basedpython: the shape a parametric protocol test (`x is A[int]`) checks @@ -1879,13 +2413,17 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { /// /// `None` for a member that can't be modeled this way: an overloaded method, /// or a method whose instance type isn't a plain callable. - pub(super) fn reified_member_shape(&self, db: &'db dyn Db) -> Option> { + pub(super) fn reified_member_shape( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { if self.is_method() { let Some(Type::Callable(callable)) = self - .capabilities(db) + .capabilities(db, env) .instance .read - .and_then(|member| member.resolve(db)) + .and_then(|member| member.resolve(db, env)) .map(ProtocolMemberType::ty) else { return None; @@ -1906,15 +2444,15 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { ret: signature.return_ty, }); } - let instance = self.capabilities(db).instance; + let instance = self.capabilities(db, env).instance; let read = instance .read - .and_then(|member| member.resolve(db)) + .and_then(|member| member.resolve(db, env)) .map(ProtocolMemberType::ty); let write = instance .write .and_then(ProtocolMemberWrite::domain) - .and_then(|member| member.resolve(db)) + .and_then(|member| member.resolve(db, env)) .map(ProtocolMemberType::ty); let ty = read.or(write)?; Some(ReifiedMember::Attribute { @@ -1947,36 +2485,38 @@ pub(super) enum ReifiedMember<'db> { fn property_get_member_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, getter: Type<'db>, ) -> Option> { let mut get_types = Vec::new(); let mut definition = None; - for callable in &getter.try_upcast_to_callable(db)? { + for callable in &getter.try_upcast_to_callable(db, env)? { for signature in callable.signatures(db) { get_types.push(signature.return_ty); definition = definition.or(signature.definition()); } } Some(ProtocolMemberType::with_definition( - UnionType::from_elements(db, get_types), + UnionType::from_elements(db, env, get_types), definition, )) } fn property_set_member_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, setter: Type<'db>, ) -> Option> { let mut set_types = Vec::new(); let mut definition = None; - for callable in &setter.try_upcast_to_callable(db)? { + for callable in &setter.try_upcast_to_callable(db, env)? { for signature in callable.signatures(db) { set_types.push(signature.parameters().get_positional(1)?.annotated_type()); definition = definition.or(signature.definition()); } } Some(ProtocolMemberType::with_definition( - UnionType::from_elements(db, set_types), + UnionType::from_elements(db, env, set_types), definition, )) } @@ -1984,6 +2524,7 @@ fn property_set_member_type<'db>( /// Derive the observable instance capabilities of a descriptor-decorated protocol member. fn descriptor_decorated_protocol_member<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, descriptor_ty: Type<'db>, protocol: ClassType<'db>, definition: Option>, @@ -1994,7 +2535,7 @@ fn descriptor_decorated_protocol_member<'db>( // variable can currently materialize that variable as `Unknown`. Reducing the descriptor to // its `__get__` result would then erase the remaining descriptor structure and weaken the // protocol member to a bare `Unknown`. - if super::visitor::any_over_type(db, descriptor_ty, false, |ty| ty.is_unknown()) { + if super::visitor::any_over_type(db, env, descriptor_ty, false, |ty| ty.is_unknown()) { return None; } @@ -2002,18 +2543,25 @@ fn descriptor_decorated_protocol_member<'db>( definedness: Definedness::AlwaysDefined, .. }) = descriptor_ty - .class_member_with_policy(db, "__get__", MemberLookupPolicy::REQUIRE_CONCRETE) + .class_member_with_policy(db, env, "__get__", MemberLookupPolicy::REQUIRE_CONCRETE) .place else { return None; }; - let receiver_ty = Type::instance(db, protocol); - let (read_ty, _) = - descriptor_ty.try_call_dunder_get(db, Some(receiver_ty), receiver_ty.to_meta_type(db))?; + let receiver_ty = Type::instance(db, env, protocol); + let read_ty = descriptor_ty + .try_call_dunder_get( + db, + env, + Some(receiver_ty), + receiver_ty.to_meta_type(db, env), + ) + .unwrap_or_else(|error| Some(error.fallback()))? + .return_type; let read = Some(ProtocolMemberType::with_definition(read_ty, definition)); - let write = match descriptor_setter_domain(db, descriptor_ty, receiver_ty) { + let write = match descriptor_setter_domain(db, env, descriptor_ty, receiver_ty) { DescriptorSetterDomain::Missing => None, DescriptorSetterDomain::Known(domain) => Some(ProtocolMemberWrite::descriptor( descriptor_ty, @@ -2040,6 +2588,7 @@ enum DescriptorSetterDomain<'db> { /// Derive the values accepted by every possible descriptor setter when they fit in [`Type`]. fn descriptor_setter_domain<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, descriptor_ty: Type<'db>, receiver_ty: Type<'db>, ) -> DescriptorSetterDomain<'db> { @@ -2047,24 +2596,25 @@ fn descriptor_setter_domain<'db>( Type::Union(union) => { let mut write_types = Vec::with_capacity(union.elements(db).len()); for descriptor_ty in union.elements(db) { - match single_descriptor_setter_domain(db, *descriptor_ty, receiver_ty) { + match single_descriptor_setter_domain(db, env, *descriptor_ty, receiver_ty) { DescriptorSetterDomain::Missing => return DescriptorSetterDomain::Missing, DescriptorSetterDomain::Known(write_ty) => write_types.push(write_ty), DescriptorSetterDomain::Deferred => return DescriptorSetterDomain::Deferred, } } - IntersectionType::bounded_from_elements(db, write_types).map_or( + IntersectionType::bounded_from_elements(db, env, write_types).map_or( DescriptorSetterDomain::Deferred, DescriptorSetterDomain::Known, ) } - _ => single_descriptor_setter_domain(db, descriptor_ty, receiver_ty), + _ => single_descriptor_setter_domain(db, env, descriptor_ty, receiver_ty), } } /// Derive the values accepted by one possible runtime descriptor. fn single_descriptor_setter_domain<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, descriptor_ty: Type<'db>, receiver_ty: Type<'db>, ) -> DescriptorSetterDomain<'db> { @@ -2075,6 +2625,7 @@ fn single_descriptor_setter_domain<'db>( }) = descriptor_ty .member_lookup_with_policy( db, + env, "__set__", MemberLookupPolicy::REQUIRE_CONCRETE | MemberLookupPolicy::NO_INSTANCE_FALLBACK, ) @@ -2083,14 +2634,15 @@ fn single_descriptor_setter_domain<'db>( return DescriptorSetterDomain::Missing; }; - let Some(callables) = setter_ty.try_upcast_to_callable(db) else { + let Some(callables) = setter_ty.try_upcast_to_callable(db, env) else { return DescriptorSetterDomain::Deferred; }; let mut callable_domains = Vec::with_capacity(callables.iter().len()); for callable in &callables { let mut write_types = Vec::new(); for signature in callable.signatures(db) { - match descriptor_setter_signature_domain(db, signature, descriptor_ty, receiver_ty) { + match descriptor_setter_signature_domain(db, env, signature, descriptor_ty, receiver_ty) + { DescriptorSetterSignatureDomain::Inapplicable => {} DescriptorSetterSignatureDomain::Known(write_ty) => write_types.push(write_ty), DescriptorSetterSignatureDomain::Deferred => { @@ -2098,9 +2650,9 @@ fn single_descriptor_setter_domain<'db>( } } } - callable_domains.push(UnionType::from_elements(db, write_types)); + callable_domains.push(UnionType::from_elements(db, env, write_types)); } - IntersectionType::bounded_from_elements(db, callable_domains).map_or( + IntersectionType::bounded_from_elements(db, env, callable_domains).map_or( DescriptorSetterDomain::Deferred, DescriptorSetterDomain::Known, ) @@ -2115,6 +2667,7 @@ enum DescriptorSetterSignatureDomain<'db> { /// Derive the values accepted by one `__set__` overload when they fit in [`Type`]. fn descriptor_setter_signature_domain<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, signature: &Signature<'db>, descriptor_ty: Type<'db>, receiver_ty: Type<'db>, @@ -2141,13 +2694,14 @@ fn descriptor_setter_signature_domain<'db>( let Some(receiver_parameter) = parameters.get_positional(0) else { return missing_required_parameter(); }; - let receiver_parameter = receiver_parameter - .annotated_type() - .bind_self_typevars(db, descriptor_ty); - if contains_signature_typevar(db, signature, receiver_parameter) { + let receiver_parameter = + receiver_parameter + .annotated_type() + .bind_self_typevars(db, env, descriptor_ty); + if contains_signature_typevar(db, env, signature, receiver_parameter) { return DescriptorSetterSignatureDomain::Deferred; } - if !receiver_ty.is_assignable_to(db, receiver_parameter) { + if !receiver_ty.is_assignable_to(db, env, receiver_parameter) { return DescriptorSetterSignatureDomain::Inapplicable; } @@ -2156,8 +2710,8 @@ fn descriptor_setter_signature_domain<'db>( }; let write_ty = write_parameter .annotated_type() - .bind_self_typevars(db, descriptor_ty); - if !contains_signature_typevar(db, signature, write_ty) { + .bind_self_typevars(db, env, descriptor_ty); + if !contains_signature_typevar(db, env, signature, write_ty) { return DescriptorSetterSignatureDomain::Known(write_ty); } @@ -2176,10 +2730,10 @@ fn descriptor_setter_signature_domain<'db>( return DescriptorSetterSignatureDomain::Deferred; } - match typevar.typevar(db).bound_or_constraints(db) { + match typevar.typevar(db).bound_or_constraints(db, env) { None => DescriptorSetterSignatureDomain::Known(Type::object()), Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - DescriptorSetterSignatureDomain::Known(bound.bind_self_typevars(db, descriptor_ty)) + DescriptorSetterSignatureDomain::Known(bound.bind_self_typevars(db, env, descriptor_ty)) } Some(TypeVarBoundOrConstraints::Constraints(_)) => { DescriptorSetterSignatureDomain::Deferred @@ -2189,11 +2743,12 @@ fn descriptor_setter_signature_domain<'db>( fn contains_signature_typevar<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, signature: &Signature<'db>, ty: Type<'db>, ) -> bool { signature.generic_context.is_some_and(|generic_context| { - super::visitor::any_over_type(db, ty, true, |ty| { + super::visitor::any_over_type(db, env, ty, true, |ty| { matches!(ty, Type::TypeVar(typevar) if generic_context.contains(db, typevar.identity(db))) }) }) @@ -2201,10 +2756,11 @@ fn contains_signature_typevar<'db>( fn property_set_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, property: PropertyInstanceType<'db>, receiver_ty: Type<'db>, ) -> Option> { - property_set_member_type(db, property.setter(db)?)?.bind_self(db, receiver_ty) + property_set_member_type(db, env, property.setter(db)?)?.bind_self(db, env, receiver_ty) } fn is_class_object_type(ty: Type<'_>) -> bool { @@ -2216,6 +2772,7 @@ fn is_class_object_type(ty: Type<'_>) -> bool { fn protocol_member_read_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, receiver_ty: Type<'db>, member: &ProtocolMember<'_, 'db>, @@ -2239,8 +2796,10 @@ fn protocol_member_read_type<'db>( { Type::invoke_descriptor_protocol( db, + env, MemberLookupKey::new( db, + env.program(db), ty, member.name, // The undefined fallback excludes instance members. Keep the class @@ -2251,9 +2810,10 @@ fn protocol_member_read_type<'db>( Place::Undefined.into(), InstanceFallbackShadowsNonDataDescriptor::No, ) + .unwrap_or_else(|error| error.fallback_member(db)) .place } else { - receiver_ty.member(db, member.name).place + receiver_ty.member(db, env, member.name).place }; match place { @@ -2279,30 +2839,35 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { member_name: &str, value_ty: Type<'db>, ) -> ConstraintSet<'db, 'c> { - let requirement = attribute_write_requirement(db, ty, member_name); - self.check_property_write_requirement(db, &requirement, member_name, value_ty) + let env = self.env; + let requirement = attribute_write_requirement(db, env, ty, member_name); + self.check_property_write_requirement(db, env, &requirement, member_name, value_ty) } fn check_property_write_requirement( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, requirement: &AttributeWriteRequirement<'db>, member_name: &str, value_ty: Type<'db>, ) -> ConstraintSet<'db, 'c> { match requirement { AttributeWriteRequirement::All { element_tys, .. } => { + let env = self.env; let mut result = self.always(); for element_ty in *element_tys { - let requirement = attribute_write_requirement(db, *element_ty, member_name); + let requirement = + attribute_write_requirement(db, env, *element_ty, member_name); let element_result = self.check_property_write_requirement( db, + env, &requirement, member_name, value_ty, ); result = result.and(db, self.constraints, || element_result); - if result.is_never_satisfied(db) { + if result.is_trivially_never_satisfied() { break; } } @@ -2311,15 +2876,17 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { AttributeWriteRequirement::Any { element_tys, .. } => { let mut result = self.never(); for element_ty in element_tys { - let requirement = attribute_write_requirement(db, *element_ty, member_name); + let requirement = + attribute_write_requirement(db, env, *element_ty, member_name); let element_result = self.check_property_write_requirement( db, + env, &requirement, member_name, value_ty, ); result = result.or(db, self.constraints, || element_result); - if result.is_always_satisfied(db) { + if result.is_trivially_always_satisfied() { break; } } @@ -2357,16 +2924,18 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { member_name: &str, value_ty: Type<'db>, ) -> ConstraintSet<'db, 'c> { + let env = self.env; let setattr_result = object_ty.try_call_dunder_with_policy( db, + env, "__setattr__", &mut CallArguments::positional([Type::string_literal(db, member_name), value_ty]), TypeContext::default(), MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, ); if match &setattr_result { - Ok(bindings) => bindings.return_type(db).is_never(), - Err(error) => error.return_type(db).is_some_and(|ty| ty.is_never()), + Ok(bindings) => bindings.return_type(db, env).is_never(), + Err(error) => error.return_type(db, env).is_some_and(|ty| ty.is_never()), } { return self.never(); } @@ -2410,7 +2979,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { ClassAttributeWriteMember::Explicit { member, fallback } => { let member_result = self.check_explicit_property_write(db, object_ty, member, value_ty); - if member_result.is_never_satisfied(db) { + if member_result.is_trivially_never_satisfied() { return member_result; } if let Some(fallback) = fallback { @@ -2445,7 +3014,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { .. } => { if let Some(property) = descriptor_ty.as_property_instance() - && let Some(set_type) = property_set_type(db, property, object_ty) + && let Some(set_type) = property_set_type(db, self.env, property, object_ty) { return self.check_type_pair(db, value_ty, set_type); } @@ -2471,9 +3040,11 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { object_ty: Type<'db>, value_ty: Type<'db>, ) -> ConstraintSet<'db, 'c> { + let env = self.env; if setter_ty .try_call( db, + env, &CallArguments::positional([descriptor_ty, object_ty, Type::unknown()]), ) .is_err() @@ -2490,9 +3061,11 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { object_ty: Type<'db>, value_ty: Type<'db>, ) -> ConstraintSet<'db, 'c> { + let env = self.env; let Place::Defined(DefinedPlace { ty: setattr_ty, .. }) = object_ty .member_lookup_with_policy( db, + env, "__setattr__", MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK | MemberLookupPolicy::NO_INSTANCE_FALLBACK, @@ -2528,8 +3101,9 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { }); } + let env = self.env; callable_ty - .try_upcast_to_callable_with_policy(db, UpcastPolicy::from(self.relation)) + .try_upcast_to_callable_with_policy(db, env, UpcastPolicy::from(self.relation)) .when_some_and(db, self.constraints, |callables| { callables.iter().when_all(db, self.constraints, |callable| { callable.signatures(db).into_iter().when_any( @@ -2545,7 +3119,9 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { }) }) .map(|parameter| { - parameter.annotated_type().bind_self_typevars(db, self_ty) + parameter + .annotated_type() + .bind_self_typevars(db, env, self_ty) }) .when_some_and(db, self.constraints, |write_ty| { self.check_type_pair(db, value_ty, write_ty) @@ -2583,7 +3159,9 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { required_ty: ProtocolMemberType<'db>, access: ProtocolMemberAccessMode, ) -> ConstraintSet<'db, 'c> { - let Some(attribute_type) = protocol_member_read_type(db, ty, receiver_ty, member, access) + let env = self.env; + let Some(attribute_type) = + protocol_member_read_type(db, env, ty, receiver_ty, member, access) else { return self.never(); }; @@ -2592,74 +3170,69 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // method on a class object names instances of that class: a `@classmethod` returning // `Self` returns `Factory`, not `type[Factory]`. Keep the bindings separate so a method // that returns an instance cannot satisfy a protocol that promises the class object. - let protocol_self_binding_ty = ty.literal_fallback_instance(db).unwrap_or(ty); + let protocol_self_binding_ty = ty.literal_fallback_instance(db, env).unwrap_or(ty); let implementation_self_binding_ty = ty - .to_instance_approximation(db) - .or_else(|| ty.literal_fallback_instance(db)) + .to_instance_approximation(db, env) + .or_else(|| ty.literal_fallback_instance(db, env)) .unwrap_or(ty); - let implementation_receiver_binding_ty = if member.is_class_method() { - implementation_self_binding_ty.to_meta_type(db) - } else { - implementation_self_binding_ty - }; - let protocol_receiver_binding_ty = if member.is_class_method() { - protocol_self_binding_ty.to_meta_type(db) - } else { - protocol_self_binding_ty - }; - - // Checking a class object against a protocol's instance capabilities can expose the - // property descriptor itself rather than the value returned by its getter. Compatibility - // for properties on class objects is not yet modeled; retain the previous name-only - // behavior until generic upper-bound solving can handle the large recursive unions this - // otherwise creates. - if member.is_property() && matches!(attribute_type, Type::PropertyInstance(_)) { - return self.always(); - } + let (implementation_receiver_binding_ty, protocol_receiver_binding_ty) = + if member.is_class_method() { + ( + implementation_self_binding_ty.to_meta_type(db, env), + protocol_self_binding_ty.to_meta_type(db, env), + ) + } else { + (implementation_self_binding_ty, protocol_self_binding_ty) + }; if member.is_method() && access == ProtocolMemberAccessMode::Instance { - let Some(required_ty) = required_ty.resolve(db) else { + let Some(required_ty) = required_ty.resolve(db, env) else { return self.never(); }; let Type::Callable(required_callable) = required_ty.ty() else { return self.never(); }; attribute_type - .try_upcast_to_callable_with_policy(db, UpcastPolicy::from(self.relation)) + .try_upcast_to_callable_with_policy(db, env, UpcastPolicy::from(self.relation)) .when_some_and(db, self.constraints, |callables| { self.check_callables_vs_callable( db, &callables.map(|callable| { - callable.apply_self_with_receiver( + protocol_apply_self_with_receiver( db, + env.program(db), + callable, implementation_receiver_binding_ty, implementation_self_binding_ty, ) }), - required_callable.apply_self_with_receiver( + protocol_apply_self_with_receiver( db, + env.program(db), + required_callable, protocol_receiver_binding_ty, protocol_self_binding_ty, ), ) }) } else if member.is_instance_method() { - let Some(required_ty) = required_ty.resolve(db) else { + let Some(required_ty) = required_ty.resolve(db, env) else { return self.never(); }; let Type::Callable(required_callable) = required_ty.ty() else { return self.never(); }; attribute_type - .try_upcast_to_callable_with_policy(db, UpcastPolicy::from(self.relation)) + .try_upcast_to_callable_with_policy(db, env, UpcastPolicy::from(self.relation)) .when_some_and(db, self.constraints, |callables| { callables.iter().when_all(db, self.constraints, |callable| { if callable.is_function_like(db) { self.check_callable_pair( db, - callable.bind_self(db, Some(implementation_self_binding_ty)), + callable.bind_self(db, env, Some(implementation_self_binding_ty)), protocol_bind_self( db, + env.program(db), required_callable, Some(protocol_self_binding_ty), ), @@ -2670,7 +3243,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { }) }) } else if member.is_method() { - let Some(required_ty) = required_ty.resolve(db) else { + let Some(required_ty) = required_ty.resolve(db, env) else { return self.never(); }; let Type::Callable(required_callable) = required_ty.ty() else { @@ -2679,19 +3252,21 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { self.check_type_pair( db, attribute_type, - Type::Callable(required_callable.apply_self_with_receiver( + Type::Callable(protocol_apply_self_with_receiver( db, + env.program(db), + required_callable, protocol_receiver_binding_ty, protocol_self_binding_ty, )), ) } else { required_ty - .bind_self(db, protocol_self_binding_ty) + .bind_self(db, env, protocol_self_binding_ty) .when_some_and(db, self.constraints, |required_ty| { let result = self.check_type_pair(db, attribute_type, required_ty); if let Some(context) = self.report_context() - && result.is_never_satisfied(db) + && result.is_never_satisfied(db, env) { context.push(ErrorContext::ProtocolMemberReadTypeIncompatible { source: attribute_type, @@ -2716,6 +3291,18 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { required: ProtocolMemberAccess<'db>, access: ProtocolMemberAccessMode, ) -> ConstraintSet<'db, 'c> { + if access == ProtocolMemberAccessMode::Class + && member.has_incompatible_class_variable_declaration(db, self.env, ty) + { + if let Some(context) = self.report_context() { + context.push(ErrorContext::ProtocolMemberClassVarMismatch { + member_name: member.name.into(), + ty, + }); + } + return self.never(); + } + if access == ProtocolMemberAccessMode::Class && member.is_instance_method() && required.read.is_some() @@ -2729,6 +3316,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { member.name == "__call__" || protocol_member_read_type( db, + self.env, ty, receiver_ty, member, @@ -2749,7 +3337,8 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { required.write.map_or_else( || self.always(), |write| { - let fallback_ty = ty.literal_fallback_instance(db).unwrap_or(ty); + let env = self.env; + let fallback_ty = ty.literal_fallback_instance(db, env).unwrap_or(ty); let receiver_ty = if access == ProtocolMemberAccessMode::Instance && matches!(ty, Type::LiteralValue(_)) { @@ -2758,12 +3347,12 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { receiver_ty }; write - .bind_compatibility_type(db, fallback_ty) + .bind_compatibility_type(db, env, fallback_ty) .when_some_and(db, self.constraints, |write_ty| { let result = self.check_property_write(db, receiver_ty, member.name, write_ty); if let Some(context) = self.report_context() - && result.is_never_satisfied(db) + && result.is_never_satisfied(db, env) { context.push(ErrorContext::ProtocolMemberWriteTypeIncompatible { target: write_ty, @@ -2783,23 +3372,40 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { ty: Type<'db>, member: &ProtocolMember<'_, 'db>, ) -> ConstraintSet<'db, 'c> { - let capabilities = member.implementation_capabilities(db, ty); + let env = self.env; + let instance_access = + member.implementation_access(db, env, ty, ProtocolMemberAccessMode::Instance); if let Some(context) = self.report_context() { - let instance_read_missing = capabilities.instance.read.is_some() + if member.has_incompatible_class_variable_declaration(db, env, ty) { + context.push(ErrorContext::ProtocolMemberClassVarMismatch { + member_name: member.name.into(), + ty, + }); + context.push(ErrorContext::ProtocolMemberIncompatible { + member_name: member.name.into(), + }); + return self.never(); + } + + let instance_read_missing = instance_access.read.is_some() && protocol_member_read_type( db, + env, ty, ty, member, ProtocolMemberAccessMode::Instance, ) .is_none(); - let class_read_missing = capabilities.class.read.is_some() + let class_access = + member.implementation_access(db, env, ty, ProtocolMemberAccessMode::Class); + let class_read_missing = class_access.read.is_some() && !(member.is_instance_method() && member.name == "__call__") && protocol_member_read_type( db, + env, ty, - ty.to_meta_type(db), + ty.to_meta_type(db, env), member, ProtocolMemberAccessMode::Class, ) @@ -2826,21 +3432,23 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { ty, ty, member, - capabilities.instance, + instance_access, ProtocolMemberAccessMode::Instance, ) .and(db, self.constraints, || { + let class_access = + member.implementation_access(db, env, ty, ProtocolMemberAccessMode::Class); self.type_satisfies_protocol_member_access( db, ty, - ty.to_meta_type(db), + ty.to_meta_type(db, env), member, - capabilities.class, + class_access, ProtocolMemberAccessMode::Class, ) }); if let Some(context) = self.report_context() - && result.is_never_satisfied(db) + && result.is_never_satisfied(db, env) { context.push(ErrorContext::ProtocolMemberIncompatible { member_name: member.name.into(), @@ -2862,11 +3470,12 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { meta_ty: Type<'db>, protocol: ProtocolInstanceType<'db>, ) -> ConstraintSet<'db, 'c> { + let env = self.env; protocol .interface(db) .members(db) .when_all(db, self.constraints, |member| { - let required = member.capabilities(db).class; + let required = member.access(db, env, ProtocolMemberAccessMode::Class); if required.read.is_none() && required.write.is_none() { return self.always(); } @@ -2897,7 +3506,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { }; if let Some(context) = self.report_context() - && result.is_never_satisfied(db) + && result.is_never_satisfied(db, env) { context.push(ErrorContext::ProtocolMemberIncompatible { member_name: member.name.into(), @@ -2919,8 +3528,8 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { target_member: &ProtocolMember<'_, 'db>, access: ProtocolMemberAccessMode, ) -> ConstraintSet<'db, 'c> { - let source_capabilities = source_member.capabilities(db); - let target_capabilities = target_member.capabilities(db); + let env = self.env; + let source = source_member.access(db, env, access); if access == ProtocolMemberAccessMode::Class && source_member.is_method() @@ -2928,20 +3537,9 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { { // The instance-side check is authoritative for an ordinary method's signature. Class // access only establishes that the source member is also present on the class. - return ConstraintSet::from_bool( - self.constraints, - source_capabilities.class.read.is_some(), - ); + return ConstraintSet::from_bool(self.constraints, source.read.is_some()); } - - let (source, target) = match access { - ProtocolMemberAccessMode::Instance => { - (source_capabilities.instance, target_capabilities.instance) - } - ProtocolMemberAccessMode::Class => { - (source_capabilities.class, target_capabilities.class) - } - }; + let target = target_member.access(db, env, access); let read_result = match (source.read, target.read) { (_, None) => self.always(), @@ -2949,13 +3547,19 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { (Some(source), Some(target)) => { let bind_read = |member_type: ProtocolMemberType<'db>, member: &ProtocolMember<'_, 'db>| { - let member_type = member_type.resolve(db)?; + let member_type = member_type.resolve(db, env)?; if member.is_method() && let Type::Callable(callable) = member_type.ty() { - Some(Type::Callable(callable.apply_self(db, source_type))) + Some(Type::Callable(protocol_apply_self_with_receiver( + db, + env.program(db), + callable, + source_type, + source_type, + ))) } else { - member_type.bind_self(db, source_type) + member_type.bind_self(db, env, source_type) } }; let (Some(source), Some(target)) = ( @@ -2967,7 +3571,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { let result = self.check_type_pair(db, source, target); if let Some(context) = self.report_context() && !target_member.is_method() - && result.is_never_satisfied(db) + && result.is_never_satisfied(db, env) { context .push(ErrorContext::ProtocolMemberReadTypeIncompatible { source, target }); @@ -2987,14 +3591,14 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } (Some(source), Some(target)) => { let (Some(target), Some(source)) = ( - target.bind_compatibility_type(db, source_type), - source.bind_compatibility_type(db, source_type), + target.bind_compatibility_type(db, env, source_type), + source.bind_compatibility_type(db, env, source_type), ) else { return self.never(); }; let result = self.check_type_pair(db, target, source); if let Some(context) = self.report_context() - && result.is_never_satisfied(db) + && result.is_never_satisfied(db, env) { context.push(ErrorContext::ProtocolMemberWriteTypeIncompatible { target }); } @@ -3008,21 +3612,29 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { &self, db: &'db dyn Db, source_type: Type<'db>, - source: ProtocolInterface<'db>, - target: ProtocolInterface<'db>, + source: ProtocolInterfaceView<'db>, + target: ProtocolInterfaceView<'db>, ) -> ConstraintSet<'db, 'c> { if source.member_count(db) < target.member_count(db) && !self.is_context_collection_enabled() + && source.member_count(db) < non_object_protocol_member_count(db, target.interface) { return self.never(); } + let env = self.env; target .members(db) - .sorted_by_cached_key(|member| member.structural_member_priority(db)) + .sorted_by_cached_key(|member| member.structural_member_priority(db, env)) .when_all(db, self.constraints, |target_member| { let source_member = source.member_by_name(db, target_member.name); + if source_member.is_none() + && source.includes_member_or_object_fallback(db, env, target_member.name) + { + return self.type_satisfies_protocol_member(db, source_type, &target_member); + } + if let Some(context) = self.report_context() && source_member.is_none() { @@ -3052,7 +3664,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { }) }); if let Some(context) = self.report_context() - && result.is_never_satisfied(db) + && result.is_never_satisfied(db, env) { context.push(ErrorContext::ProtocolMemberIncompatible { member_name: target_member.name.into(), @@ -3074,7 +3686,12 @@ impl<'c, 'db> DisjointnessChecker<'_, 'c, 'db> { member: &ProtocolMember<'_, 'db>, ty: Type<'db>, ) -> ConstraintSet<'db, 'c> { - if member.capabilities(db).instance.write.is_none() { + let env = self.env; + if member + .access(db, env, ProtocolMemberAccessMode::Instance) + .write + .is_none() + { return self.never(); } @@ -3082,7 +3699,7 @@ impl<'c, 'db> DisjointnessChecker<'_, 'c, 'db> { ty: Type::PropertyInstance(actual_property), definedness: Definedness::AlwaysDefined, .. - }) = ty.class_member(db, member.name()).place + }) = ty.class_member(db, env, member.name()).place else { return self.never(); }; @@ -3100,28 +3717,20 @@ impl<'c, 'db> DisjointnessChecker<'_, 'c, 'db> { member: &ProtocolMember<'_, 'db>, ty: Type<'db>, ) -> ConstraintSet<'db, 'c> { - // An unbound property descriptor does not establish that the value returned by its - // getter is disjoint from the required property type. - if member.is_property() && matches!(ty, Type::PropertyInstance(_)) { - return self.never(); - } - let capabilities = member.capabilities(db); + let env = self.env; + let access = member.access(db, env, ProtocolMemberAccessMode::Instance); if !member.is_method() { - capabilities - .instance - .read - .when_some_and(db, self.constraints, |read_ty| { - read_ty - .resolve(db) - .when_some_and(db, self.constraints, |read_ty| { - self.check_type_pair(db, ty, read_ty.ty()) - }) - }) + access.read.when_some_and(db, self.constraints, |read_ty| { + read_ty + .resolve(db, env) + .when_some_and(db, self.constraints, |read_ty| { + self.check_type_pair(db, ty, read_ty.ty()) + }) + }) } else { - let Some(Type::Callable(method)) = capabilities - .instance + let Some(Type::Callable(method)) = access .read - .and_then(|read| read.resolve(db)) + .and_then(|read| read.resolve(db, env)) .map(ProtocolMemberType::ty) else { return self.never(); @@ -3130,7 +3739,8 @@ impl<'c, 'db> DisjointnessChecker<'_, 'c, 'db> { return self.never(); } - let Some(callables) = ty.try_upcast_to_callable_with_policy(db, UpcastPolicy::Sound) + let Some(callables) = + ty.try_upcast_to_callable_with_policy(db, env, UpcastPolicy::Sound) else { return self.never(); }; @@ -3259,7 +3869,7 @@ impl<'db> ProtocolMemberCandidate<'db> { .into_iter() .flatten() { - if let Some(member) = member.resolve(db) { + if let Some(member) = member.resolve(db, visitor.program_environment()) { visitor.visit_type(db, member.ty()); } } @@ -3270,10 +3880,39 @@ impl<'db> ProtocolMemberCandidate<'db> { } } +/// Cache `object` member names so missing protocol members can be rejected without member lookup. +#[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)] +fn object_member_names<'db>(db: &'db dyn Db, program: Program<'db>) -> FxHashSet { + let env = ProgramEnvironment::from_program(program); + let Some((object, _)) = ClassType::object(db, &env).static_class_literal(db) else { + return FxHashSet::default(); + }; + + let mut names = place_table(db, object.body_scope(db)) + .symbols() + .map(|symbol| symbol.name().clone()) + .collect::>(); + names.shrink_to_fit(); + names +} + +/// Count protocol requirements that cannot be supplied by inherited `object` members. +#[salsa::tracked(returns(copy), heap_size=ruff_memory_usage::heap_size)] +fn non_object_protocol_member_count<'db>( + db: &'db dyn Db, + interface: ProtocolInterface<'db>, +) -> usize { + let inherited_member_count = object_member_names(db, interface.program(db)) + .iter() + .filter(|name| name.as_str() != "__hash__" && interface.includes_member(db, name)) + .count(); + interface.member_count(db) - inherited_member_count +} + /// Inner Salsa query for [`ProtocolClass::interface`]. #[salsa::tracked( returns(copy), - cycle_initial=|db, _, _| ProtocolInterface::empty(db), + cycle_initial=protocol_interface_cycle_initial, cycle_fn=proto_interface_cycle_recover, heap_size=ruff_memory_usage::heap_size, )] @@ -3281,9 +3920,10 @@ fn cached_protocol_interface<'db>( db: &'db dyn Db, class: ClassType<'db>, ) -> ProtocolInterface<'db> { + let env = ProgramEnvironment::from_file(class.class_literal(db).program_file(db)); let mut members = BTreeMap::default(); - ProtocolClass(class).for_each_member_candidate(db, |name, candidate, specialization| { + ProtocolClass(class).for_each_member_candidate(db, &env, |name, candidate, specialization| { if members.contains_key(name) { return; } @@ -3306,20 +3946,20 @@ fn cached_protocol_interface<'db>( definition, ), Type::Callable(callable) if bound_on_class.is_yes() && callable.is_method_like(db) => { - ProtocolMemberData::method(db, callable, definition) + ProtocolMemberData::method(db, &env, callable, definition) } Type::FunctionLiteral(function) if bound_on_class.is_yes() || function.is_staticmethod(db) || function.is_classmethod(db) => { - ProtocolMemberData::method(db, function.into_callable_type(db), definition) + ProtocolMemberData::method(db, &env, function.into_callable_type(db), definition) } _ if bound_on_class.is_yes() && definition.is_some_and(|definition| definition.kind(db).is_function_def()) => { if let Some(descriptor) = - descriptor_decorated_protocol_member(db, ty, class, definition) + descriptor_decorated_protocol_member(db, &env, ty, class, definition) { descriptor } else { @@ -3332,7 +3972,18 @@ fn cached_protocol_interface<'db>( members.insert(name.clone(), member); }); - ProtocolInterface::new(db, members, Box::default()) + ProtocolInterface::new(db, env.program(db), members, Box::default()) +} + +fn protocol_interface_cycle_initial<'db>( + db: &'db dyn Db, + _id: salsa::Id, + class: ClassType<'db>, +) -> ProtocolInterface<'db> { + ProtocolInterface::empty( + db, + &ProgramEnvironment::from_file(class.class_literal(db).program_file(db)), + ) } #[allow(clippy::trivially_copy_pass_by_ref)] @@ -3341,9 +3992,10 @@ fn proto_interface_cycle_recover<'db>( cycle: &salsa::Cycle, previous: &ProtocolInterface<'db>, value: ProtocolInterface<'db>, - _class: ClassType<'db>, + class: ClassType<'db>, ) -> ProtocolInterface<'db> { - value.cycle_normalized(db, *previous, cycle) + let env = ProgramEnvironment::from_file(class.class_literal(db).program_file(db)); + value.cycle_normalized(db, &env, *previous, cycle) } /// Bind `self` unless this is a `Callable[P, R]` dunder, and *also* discard the functionlike-ness @@ -3355,10 +4007,34 @@ fn proto_interface_cycle_recover<'db>( #[salsa::tracked(returns(copy), heap_size=ruff_memory_usage::heap_size)] fn protocol_bind_self<'db>( db: &'db dyn Db, + program: Program<'db>, callable: CallableType<'db>, self_type: Option>, ) -> CallableType<'db> { - callable.bind_self(db, self_type).into_regular(db) + let env = ProgramEnvironment::from_program(program); + callable.bind_self(db, &env, self_type).into_regular(db) +} + +/// Cache receiver and `Self` binding only for protocol-member compatibility checks. +#[salsa::tracked( + returns(copy), + cycle_initial=|db, _, _, _, _, _| CallableType::bottom(db), + heap_size=ruff_memory_usage::heap_size +)] +fn protocol_apply_self_with_receiver<'db>( + db: &'db dyn Db, + program: Program<'db>, + callable: CallableType<'db>, + receiver_type: Type<'db>, + self_type: Type<'db>, +) -> CallableType<'db> { + let env = ProgramEnvironment::from_program(program); + + if receiver_type == self_type { + callable.apply_self(db, &env, self_type) + } else { + callable.apply_self_with_receiver(db, &env, receiver_type, self_type) + } } /// Return `true` if a callable has at least one overload and none return `Never`. @@ -3382,6 +4058,7 @@ fn callable_has_only_non_never_returns<'db>(db: &'db dyn Db, callable: CallableT /// comparisons and generic protocol solving when the actual type is plainly missing a member. pub(super) fn has_all_protocol_members_defined<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, protocol: ProtocolInstanceType<'db>, ) -> bool { @@ -3391,14 +4068,16 @@ pub(super) fn has_all_protocol_members_defined<'db>( Type::ProtocolInstance(source_protocol) => { let source_interface = source_protocol.interface(db); - source_interface.member_count(db) >= target_interface.member_count(db) - && target_interface - .members(db) - .all(|member| source_interface.includes_member(db, member.name())) + (source_interface.member_count(db) >= target_interface.member_count(db) + || source_interface.member_count(db) + >= non_object_protocol_member_count(db, target_interface.interface)) + && target_interface.members(db).all(|member| { + source_interface.includes_member_or_object_fallback(db, env, member.name()) + }) } _ => target_interface.members(db).all(|member| { matches!( - ty.member(db, member.name()).place, + ty.member(db, env, member.name()).place, Place::Defined(DefinedPlace { definedness: Definedness::AlwaysDefined, .. diff --git a/crates/ty_python_semantic/src/types/receivers.rs b/crates/ty_python_semantic/src/types/receivers.rs index a2ed621989..500aae7c85 100644 --- a/crates/ty_python_semantic/src/types/receivers.rs +++ b/crates/ty_python_semantic/src/types/receivers.rs @@ -29,6 +29,7 @@ use ty_python_core::{place_table, semantic_index}; use crate::Db; use crate::place::{ConsideredDefinitions, symbol}; +use crate::types::ProgramEnvironment; use crate::types::name_fallback::claimed_by_name_resolution; use crate::types::signatures::{Parameters, Signature}; use crate::types::{Type, TypeContext, UnionType, infer_expression_types}; @@ -62,10 +63,15 @@ pub(crate) fn receiver_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option(db: &'db dyn Db, ty: Type<'db>, receiver_ty: Type<'db>) -> Option> { +fn bind_receiver<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + receiver_ty: Type<'db>, +) -> Option> { let signature = receiver_signature(db, ty)?; let receiver = signature.parameters().get_positional(0)?; - if !receiver_ty.is_assignable_to(db, receiver.annotated_type()) { + if !receiver_ty.is_assignable_to(db, env, receiver.annotated_type()) { return None; } let rest = signature.parameters().iter().skip(1).cloned(); @@ -73,7 +79,7 @@ fn bind_receiver<'db>(db: &'db dyn Db, ty: Type<'db>, receiver_ty: Type<'db>) -> db, Signature::new_generic( signature.generic_context, - Parameters::from_annotation(db, rest), + Parameters::from_annotation(db, env, rest), signature.return_ty, ), )) @@ -87,14 +93,15 @@ fn bind_receiver<'db>(db: &'db dyn Db, ty: Type<'db>, receiver_ty: Type<'db>) -> /// the name in an ordinary load pub(crate) fn resolve_receiver_attribute<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, scope: ScopeId<'db>, receiver_ty: Type<'db>, name: &str, ) -> Option> { - let index = semantic_index(db, file); + let index = semantic_index(db, db.program_file(file)); for (ancestor_id, _) in index.visible_ancestor_scopes(scope.file_scope_id(db)) { - let ancestor_scope = ancestor_id.to_scope_id(db, file); + let ancestor_scope = ancestor_id.to_scope_id(db, db.program_file(file)); let Some(place) = place_table(db, ancestor_scope).symbol_by_name(name) else { continue; }; @@ -115,7 +122,7 @@ pub(crate) fn resolve_receiver_attribute<'db>( ) .place .ignore_possibly_undefined()?; - return bind_receiver(db, declared, receiver_ty); + return bind_receiver(db, env, declared, receiver_ty); } None } @@ -127,6 +134,7 @@ pub(crate) fn resolve_receiver_attribute<'db>( /// applicable extension member both win over it pub(crate) fn is_implicit_receiver_attribute<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, scope: ScopeId<'db>, attribute: &ast::ExprAttribute, @@ -135,22 +143,24 @@ pub(crate) fn is_implicit_receiver_attribute<'db>( // an optional-chain link resolves against the chain's *present* type — the // `None` it short-circuits with is not part of the receiver let receiver_ty = if attribute.optional || spine_has_optional(&attribute.value) { - strip_none(db, receiver_ty) + strip_none(db, env, receiver_ty) } else { receiver_ty }; let name = attribute.attr.as_str(); - if !receiver_ty.member(db, name).place.is_undefined() { + if !receiver_ty.member(db, env, name).place.is_undefined() { return false; } // an extension member wins over a receiver callable, matching the order the // two fallbacks run in during inference. resolving again here is near-free // in a file with no extensions: the applicable-extension list is a cached // query that comes back empty - if crate::types::extensions::resolve_extension_member(db, file, receiver_ty, name).is_some() { + if crate::types::extensions::resolve_extension_member(db, env, file, receiver_ty, name) + .is_some() + { return false; } - resolve_receiver_attribute(db, file, scope, receiver_ty, name).is_some() + resolve_receiver_attribute(db, env, file, scope, receiver_ty, name).is_some() } /// whether any link of the attribute spine `expr` is an optional access @@ -164,12 +174,17 @@ pub(crate) fn spine_has_optional(expr: &Expr) -> bool { } /// basedpython: `ty` without the `None` an optional chain unions in -pub(crate) fn strip_none<'db>(db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { +pub(crate) fn strip_none<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Type<'db> { let Type::Union(union) = ty else { return ty; }; UnionType::from_elements( db, + env, union .elements(db) .iter() @@ -208,25 +223,31 @@ impl<'db> ImplicitReceiverName<'db> { /// captured (a method's own `self` keeps its meaning) pub(crate) fn implicit_receiver_name<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, scope: ScopeId<'db>, name: &str, ) -> Option> { let receiver = trailing_lambda_scope_receiver(db, file, scope)?; - if claimed_by_name_resolution(db, file, scope, name) { + if claimed_by_name_resolution(db, env, file, scope, name) { return None; } if name == "self" { return Some(ImplicitReceiverName::Receiver(receiver)); } - if let Some(member) = receiver.member(db, name).place.ignore_possibly_undefined() { + if let Some(member) = receiver + .member(db, env, name) + .place + .ignore_possibly_undefined() + { return Some(ImplicitReceiverName::Member(member)); } // an extension of the receiver's type supplies members too, and the block's // scope is the receiver's — so `p:` inside a `div:` block reaches an // `extension Tag: def p` exactly as `self.p:` does. reached last, after the // receiver's own members, like every other extension lookup - let resolution = crate::types::extensions::resolve_extension_member(db, file, receiver, name)?; + let resolution = + crate::types::extensions::resolve_extension_member(db, env, file, receiver, name)?; Some(ImplicitReceiverName::ExtensionMember { ty: resolution.ty, resolution, @@ -242,8 +263,8 @@ fn trailing_lambda_scope_receiver<'db>( file: File, scope: ScopeId<'db>, ) -> Option> { - let index = semantic_index(db, file); - let module = parsed_module(db, file).load(db); + let index = semantic_index(db, db.program_file(file)); + let module = parsed_module(db, db.program_file(file).python_file(db)).load(db); for (_, ancestor) in index.visible_ancestor_scopes(scope.file_scope_id(db)) { match ancestor.kind() { ScopeKind::Comprehension => continue, diff --git a/crates/ty_python_semantic/src/types/regex.rs b/crates/ty_python_semantic/src/types/regex.rs index cc73d12ad7..f50c328003 100644 --- a/crates/ty_python_semantic/src/types/regex.rs +++ b/crates/ty_python_semantic/src/types/regex.rs @@ -18,6 +18,7 @@ use ruff_python_ast::name::Name; use ty_module_resolver::{KnownModule, file_to_module}; use crate::Db; +use crate::types::ProgramEnvironment; use crate::types::instance::NominalInstanceType; use crate::types::typed_dict::{TypedDictFieldBuilder, TypedDictOpenness, TypedDictSchema}; use crate::types::{ @@ -69,6 +70,7 @@ impl<'db> RegexGroups<'db> { fn resolve( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, any_str: Type<'db>, unset: Type<'db>, key: GroupKey<'_>, @@ -88,7 +90,7 @@ impl<'db> RegexGroups<'db> { Ok(if group.definitely_set { any_str } else { - UnionType::from_two_elements(db, any_str, unset) + UnionType::from_two_elements(db, env, any_str, unset) }) } } @@ -173,31 +175,32 @@ impl MatchMember { /// the stubs gave the call. pub(crate) fn refined_return<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, call: RegexCall, groups: RegexGroups<'db>, any_str: Type<'db>, default: Type<'db>, ) -> Type<'db> { match call { - RegexCall::Compile | RegexCall::Match => attach_groups(db, default, groups), + RegexCall::Compile | RegexCall::Match => attach_groups(db, env, default, groups), RegexCall::Split => { // a split yields the text between matches plus every group; a group // that did not participate comes back as `None` let element = if groups.groups(db).iter().all(|group| group.definitely_set) { any_str } else { - UnionType::from_two_elements(db, any_str, Type::none(db)) + UnionType::from_two_elements(db, env, any_str, Type::none(db, env)) }; - KnownClass::List.to_specialized_instance(db, &[element]) + KnownClass::List.to_specialized_instance(db, env, &[element]) } RegexCall::FindAll => { // unlike everywhere else, `findall` reports a group that did not // participate as the empty string rather than `None` let element = match groups.groups(db).len() { 0 | 1 => any_str, - count => Type::heterogeneous_tuple(db, std::iter::repeat_n(any_str, count)), + count => Type::heterogeneous_tuple(db, env, std::iter::repeat_n(any_str, count)), }; - KnownClass::List.to_specialized_instance(db, &[element]) + KnownClass::List.to_specialized_instance(db, env, &[element]) } RegexCall::Substitute => default, } @@ -206,28 +209,31 @@ pub(crate) fn refined_return<'db>( /// the type of `m.group(key)` / `m[key]` pub(crate) fn group_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, groups: RegexGroups<'db>, any_str: Type<'db>, key: GroupKey<'_>, ) -> Result, NoSuchGroup> { - groups.resolve(db, any_str, Type::none(db), key) + groups.resolve(db, env, any_str, Type::none(db, env), key) } /// the type of `m.groups()`, or of `m.groups(default)` when `unset` is given pub(crate) fn groups_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, groups: RegexGroups<'db>, any_str: Type<'db>, unset: Option>, ) -> Type<'db> { - let unset = unset.unwrap_or_else(|| Type::none(db)); + let unset = unset.unwrap_or_else(|| Type::none(db, env)); Type::heterogeneous_tuple( db, + env, groups.groups(db).iter().map(|group| { if group.definitely_set { any_str } else { - UnionType::from_two_elements(db, any_str, unset) + UnionType::from_two_elements(db, env, any_str, unset) } }), ) @@ -240,6 +246,7 @@ pub(crate) fn groups_type<'db>( /// costing the caller everything a `dict` can be passed to pub(crate) fn group_dict_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, groups: RegexGroups<'db>, any_str: Type<'db>, unset: Option>, @@ -247,7 +254,7 @@ pub(crate) fn group_dict_type<'db>( if groups.groups(db).iter().all(|group| group.name.is_none()) { return None; } - let unset = unset.unwrap_or_else(|| Type::none(db)); + let unset = unset.unwrap_or_else(|| Type::none(db, env)); let items: TypedDictSchema<'db> = groups .groups(db) .iter() @@ -256,7 +263,7 @@ pub(crate) fn group_dict_type<'db>( let declared = if group.definitely_set { any_str } else { - UnionType::from_two_elements(db, any_str, unset) + UnionType::from_two_elements(db, env, any_str, unset) }; Some(( name, @@ -280,11 +287,13 @@ pub(crate) fn group_dict_type<'db>( /// (`Match[str] | None`), or nested in another generic (`Iterator[Match[str]]`) pub(crate) fn attach_groups<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, groups: RegexGroups<'db>, ) -> Type<'db> { ty.apply_type_mapping( db, + env, &TypeMapping::AttachRegexGroups(groups), TypeContext::default(), ) @@ -325,9 +334,13 @@ pub(crate) fn is_pattern<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { } /// the `str`/`bytes` a `re.Match` / `re.Pattern` instance is specialized over -pub(crate) fn any_str_of<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option> { +pub(crate) fn any_str_of<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Option> { regex_instance(db, ty)? - .class(db) + .class(db, env) .into_generic_alias()? .specialization(db) .types(db) @@ -342,17 +355,21 @@ pub(crate) fn is_regex_class(known: Option) -> bool { /// the pattern text of a literal `re` pattern argument, with the `str`/`bytes` /// type the resulting match is over -pub(crate) fn pattern_source<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<(String, Type<'db>)> { +pub(crate) fn pattern_source<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Option<(String, Type<'db>)> { match ty.as_literal_value_kind()? { LiteralValueTypeKind::String(literal) => Some(( literal.value(db).to_string(), - KnownClass::Str.to_instance(db), + KnownClass::Str.to_instance(db, env), )), LiteralValueTypeKind::Bytes(literal) => { // read the bytes as latin-1 so one byte stays one character, which // keeps the offsets in python's own error messages right let text = literal.value(db).iter().copied().map(char::from).collect(); - Some((text, KnownClass::Bytes.to_instance(db))) + Some((text, KnownClass::Bytes.to_instance(db, env))) } _ => None, } @@ -377,7 +394,8 @@ pub(crate) fn flag_is_verbose<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option(db: &'db dyn Db, class: ClassLiteral<'db>) -> bool { class.name(db) == "RegexFlag" - && file_to_module(db, class.file(db)).and_then(|module| module.known(db)) + && file_to_module(db, class.program_file(db).resolver_file(db)) + .and_then(|module| module.known(db)) == Some(KnownModule::Re) } @@ -389,6 +407,7 @@ fn is_regex_flag_class<'db>(db: &'db dyn Db, class: ClassLiteral<'db>) -> bool { /// collapse rather than accumulate two indistinguishable elements pub(crate) fn merge_differing_groups<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, left: Type<'db>, right: Type<'db>, ) -> Option> { @@ -396,6 +415,6 @@ pub(crate) fn merge_differing_groups<'db>( if left.regex_groups(db).is_none() && right.regex_groups(db).is_none() { return None; } - let class = left.class(db); - (class == right.class(db)).then(|| Type::instance(db, class)) + let class = left.class(db, env); + (class == right.class(db, env)).then(|| Type::instance(db, env, class)) } diff --git a/crates/ty_python_semantic/src/types/reified_infer.rs b/crates/ty_python_semantic/src/types/reified_infer.rs index 0308439d2d..49e92ca20a 100644 --- a/crates/ty_python_semantic/src/types/reified_infer.rs +++ b/crates/ty_python_semantic/src/types/reified_infer.rs @@ -25,10 +25,12 @@ use rustc_hash::FxHashMap; use crate::Db; use crate::place::{builtins_symbol, global_symbol}; +use crate::types::ProgramEnvironment; use crate::types::call::{Argument, CallArguments}; use crate::types::class::{ClassLiteral, ClassType, GenericAlias}; use crate::types::function::FunctionType; use crate::types::generics::{Specialization, combine_use_site_projections}; +use crate::types::instance::Protocol; use crate::types::literal::LiteralValueTypeKind; use crate::types::protocol_class::ReifiedMember; use crate::types::tuple::Tuple; @@ -54,21 +56,22 @@ pub(crate) enum ReifiedInferenceError<'db> { /// so `self` binding is accounted for. pub(crate) fn inferred_call_type_arguments<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, callee: Type<'db>, function: FunctionType<'db>, arguments: &CallArguments<'_, 'db>, ) -> Result, ReifiedInferenceError<'db>> { let bindings = callee - .try_call(db, arguments) + .try_call(db, env, arguments) .map_err(|_| ReifiedInferenceError::NoBinding)?; let specialization = bindings .single_element() .and_then(|callable| callable.matching_overloads().exactly_one().ok()) .ok_or(ReifiedInferenceError::NoBinding)? .1 - .specialization(db); - rendered_type_arguments(db, file, function, specialization) + .specialization(db, env); + rendered_type_arguments(db, env, file, function, specialization) } /// [`inferred_call_type_arguments`] for callers outside the `types` module: @@ -80,6 +83,7 @@ pub(crate) fn inferred_call_type_arguments<'db>( /// caller just skips injection pub(crate) fn injectable_call_specialization<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, callee: Type<'db>, function: FunctionType<'db>, @@ -95,7 +99,8 @@ pub(crate) fn injectable_call_specialization<'db>( .map(|(name, ty)| (Argument::Keyword(name), Some(ty))), ) .collect(); - let rendered = inferred_call_type_arguments(db, file, callee, function, &arguments).ok()?; + let rendered = + inferred_call_type_arguments(db, env, file, callee, function, &arguments).ok()?; // an empty prefix means everything defaults — the bare call is already // correct and nothing is injected if rendered.is_empty() { @@ -127,6 +132,7 @@ pub(crate) fn injectable_call_specialization<'db>( /// the bare call is legal exactly as written (everything defaults). fn rendered_type_arguments<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, function: FunctionType<'db>, specialization: Option>, @@ -153,13 +159,13 @@ fn rendered_type_arguments<'db>( let solution = solved .get(index) .copied() - .filter(|ty| kind.is_solution(db, *ty)); + .filter(|ty| kind.is_solution(db, env, *ty)); if solution.is_some() { last_solved = Some(index); } resolved.push(ResolvedParameter { name: typevar.name(db), - value: solution.or_else(|| typevar.default_type(db)), + value: solution.or_else(|| typevar.default_type(db, env)), kind, }); } @@ -187,10 +193,10 @@ fn rendered_type_arguments<'db>( let ty = parameter .value .ok_or_else(|| ReifiedInferenceError::Unsolved(parameter.name.clone()))?; - let promoted = ty.promote(db); + let promoted = ty.promote(db, env); parameter .kind - .spelling(db, file, promoted) + .spelling(db, env, file, promoted) .map(|text| TypeArgument { text, keyword: parameter.kind == ParameterKind::KeywordPack, @@ -245,23 +251,34 @@ impl ParameterKind { /// call site can be specialized with. a run or a pack whose shape is not /// statically known is what the solver leaves behind when it could not /// determine it at all, which is "unsolved", not "solved to anything" - fn is_solution<'db>(self, db: &'db dyn Db, ty: Type<'db>) -> bool { + fn is_solution<'db>( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + ) -> bool { match self { Self::Single => is_solution(db, ty), - Self::Variadic => variadic_elements(db, ty).is_some(), + Self::Variadic => variadic_elements(db, env, ty).is_some(), Self::KeywordPack => ty.keyword_pack_fields(db).is_some(), } } /// the source text this parameter's value spells as, or the empty string /// when it stands for no arguments at all - fn spelling<'db>(self, db: &'db dyn Db, file: File, ty: Type<'db>) -> Option { + fn spelling<'db>( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + file: File, + ty: Type<'db>, + ) -> Option { let fields = match self { - Self::Single => return runtime_spelling(db, file, ty), + Self::Single => return runtime_spelling(db, env, file, ty), Self::Variadic => { - let spellings = variadic_elements(db, ty)? + let spellings = variadic_elements(db, env, ty)? .into_iter() - .map(|element| runtime_spelling(db, file, element.promote(db))) + .map(|element| runtime_spelling(db, env, file, element.promote(db, env))) .collect::>>()?; return Some(spellings.join(", ")); } @@ -272,7 +289,7 @@ impl ParameterKind { .map(|(name, field)| { Some(format!( "{name}={}", - runtime_spelling(db, file, field.promote(db))? + runtime_spelling(db, env, file, field.promote(db, env))? )) }) .collect::>>()?; @@ -282,11 +299,15 @@ impl ParameterKind { /// the run of type arguments a `*Ts` parameter stands for — the elements of /// the tuple that is its value -fn variadic_elements<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option>> { +fn variadic_elements<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Option>> { let Type::NominalInstance(instance) = ty else { return None; }; - match instance.tuple_spec(db)?.into_owned() { + match instance.tuple_spec(db, env)?.into_owned() { Tuple::Fixed(elements) => Some(elements.elements_slice().to_vec()), Tuple::Variable(_) => None, } @@ -310,12 +331,17 @@ fn is_solution<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { /// A python expression that evaluates, in `file`'s module scope, to the /// runtime object denoted by `ty` — or `None` when there is no such spelling. -fn runtime_spelling<'db>(db: &'db dyn Db, file: File, ty: Type<'db>) -> Option { +fn runtime_spelling<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + file: File, + ty: Type<'db>, +) -> Option { if ty.is_none(db) { return Some("None".to_owned()); } match ty { - Type::NominalInstance(instance) => spell_class(db, file, instance.class(db)), + Type::NominalInstance(instance) => spell_class(db, env, file, instance.class(db, env)), // a reified type parameter spells as its own name: pep 695 compiles it // into the enclosing function's closure, and the `generic` wrapper fills // that cell with the type argument, so the name evaluates to the type @@ -326,7 +352,7 @@ fn runtime_spelling<'db>(db: &'db dyn Db, file: File, ty: Type<'db>) -> Option>>()? .join(" | "), ), @@ -334,14 +360,19 @@ fn runtime_spelling<'db>(db: &'db dyn Db, file: File, ty: Type<'db>) -> Option(db: &'db dyn Db, file: File, class: ClassType<'db>) -> Option { +fn spell_class<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + file: File, + class: ClassType<'db>, +) -> Option { match class { - ClassType::NonGeneric(literal) => spell_class_literal(db, file, literal), + ClassType::NonGeneric(literal) => spell_class_literal(db, env, file, literal), ClassType::Generic(alias) => { let origin = ClassLiteral::Static(alias.origin(db)); - let base = spell_class_literal(db, file, origin)?; + let base = spell_class_literal(db, env, file, origin)?; let arguments = - spell_specialization_arguments(db, file, origin, alias.specialization(db))?; + spell_specialization_arguments(db, env, file, origin, alias.specialization(db))?; Some(format!("{base}[{arguments}]")) } } @@ -352,6 +383,7 @@ fn spell_class<'db>(db: &'db dyn Db, file: File, class: ClassType<'db>) -> Optio /// shape out-of-band; spell it rather than the class's single typevar fn spell_specialization_arguments<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, origin: ClassLiteral<'db>, specialization: Specialization<'db>, @@ -362,7 +394,7 @@ fn spell_specialization_arguments<'db>( let elements = fixed .elements_slice() .iter() - .map(|element| runtime_spelling(db, file, element.promote(db))) + .map(|element| runtime_spelling(db, env, file, element.promote(db, env))) .collect::>>()?; if elements.is_empty() { Some("()".to_owned()) @@ -378,7 +410,7 @@ fn spell_specialization_arguments<'db>( let arguments = specialization .types(db) .iter() - .map(|argument| runtime_spelling(db, file, argument.promote(db))) + .map(|argument| runtime_spelling(db, env, file, argument.promote(db, env))) .collect::>>()?; Some(arguments.join(", ")) } @@ -394,21 +426,22 @@ fn spell_specialization_arguments<'db>( /// is never an error — the call simply stays bare. pub(crate) fn constructor_specialization_display<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, class_literal: ClassLiteral<'db>, constructed: Type<'db>, ) -> Option { - let Type::NominalInstance(instance) = constructed.promote(db) else { + let Type::NominalInstance(instance) = constructed.promote(db, env) else { return None; }; - let ClassType::Generic(alias) = instance.class(db) else { + let ClassType::Generic(alias) = instance.class(db, env) else { return None; }; let origin = ClassLiteral::Static(alias.origin(db)); if origin != class_literal { return None; } - spell_specialization_arguments(db, file, origin, alias.specialization(db)) + spell_specialization_arguments(db, env, file, origin, alias.specialization(db)) } /// The full runtime spelling (`list[int]`, `tuple[int, str]`) with which the @@ -424,14 +457,19 @@ pub(crate) fn constructor_specialization_display<'db>( /// evaluates to the intended type object fn spell_class_literal<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, literal: ClassLiteral<'db>, ) -> Option { let name = literal.name(db); - let resolved = global_symbol(db, file, name) + let resolved = global_symbol(db, db.program_file(file), name) .place .ignore_possibly_undefined() - .or_else(|| builtins_symbol(db, name).place.ignore_possibly_undefined())?; + .or_else(|| { + builtins_symbol(db, env, name) + .place + .ignore_possibly_undefined() + })?; let resolved_literal = resolved.as_class_literal()?; (resolved_literal == literal).then(|| name.to_string()) } @@ -534,7 +572,12 @@ pub struct ErasedUnion { /// and every argument must have a runtime spelling — an unspellable argument /// (a scope-local class, a dynamic type) disqualifies the whole union rather /// than producing a rewrite that cannot be spelled back out -pub(crate) fn erased_union<'db>(db: &'db dyn Db, file: File, ty: Type<'db>) -> Option { +pub(crate) fn erased_union<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + file: File, + ty: Type<'db>, +) -> Option { let Type::Union(union) = ty else { return None; }; @@ -544,7 +587,7 @@ pub(crate) fn erased_union<'db>(db: &'db dyn Db, file: File, ty: Type<'db>) -> O let Type::NominalInstance(instance) = element else { return None; }; - let ClassType::Generic(alias) = instance.class(db) else { + let ClassType::Generic(alias) = instance.class(db, env) else { return None; }; let arm_origin = alias.origin(db); @@ -593,16 +636,17 @@ pub(crate) fn erased_union<'db>(db: &'db dyn Db, file: File, ty: Type<'db>) -> O let arms = rows .iter() - .map(|row| runtime_spelling(db, file, row[position].promote(db))) + .map(|row| runtime_spelling(db, env, file, row[position].promote(db, env))) .collect::>>()?; let fixed = (0..width) .filter(|index| *index != position) .map(|index| { - runtime_spelling(db, file, rows[0][index].promote(db)).map(|text| (index, text)) + runtime_spelling(db, env, file, rows[0][index].promote(db, env)) + .map(|text| (index, text)) }) .collect::>>()?; Some(ErasedUnion { - origin: spell_class_literal(db, file, origin)?, + origin: spell_class_literal(db, env, file, origin)?, position, arms, fixed, @@ -659,6 +703,7 @@ fn erased_target_reason<'db>( /// keeps the ordinary `isinstance` lowering. pub(crate) fn parametric_is_target<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, rhs_ty: Type<'db>, ) -> Option> { match rhs_ty { @@ -667,7 +712,7 @@ pub(crate) fn parametric_is_target<'db>( let value = rhs_ty.as_type_alias()?.value_type(db); match value { Type::GenericAlias(alias) => Some(alias), - Type::NominalInstance(instance) => match instance.class(db) { + Type::NominalInstance(instance) => match instance.class(db, env) { ClassType::Generic(alias) => Some(alias), ClassType::NonGeneric(_) => None, }, @@ -683,14 +728,16 @@ pub(crate) fn parametric_is_target<'db>( /// the two forms; both then classify through [`classify_parametric_is`]. pub(crate) fn parametric_cast_target<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, target_ty: Type<'db>, ) -> Option> { match target_ty { - Type::NominalInstance(instance) => match instance.class(db) { + Type::NominalInstance(instance) => match instance.class(db, env) { ClassType::Generic(alias) => Some(alias), ClassType::NonGeneric(_) => None, }, Type::ProtocolInstance(instance) => match instance.inner { + Protocol::Materialized(_) => None, crate::types::instance::Protocol::FromClass(protocol_class) => match *protocol_class { ClassType::Generic(alias) => Some(alias), ClassType::NonGeneric(_) => None, @@ -698,7 +745,7 @@ pub(crate) fn parametric_cast_target<'db>( crate::types::instance::Protocol::Synthesized(_) => None, }, // an alias name still resolves through the value-position rules - _ => parametric_is_target(db, target_ty), + _ => parametric_is_target(db, env, target_ty), } } @@ -713,6 +760,7 @@ pub(crate) fn parametric_cast_target<'db>( /// back to the static fold or the runtime probe. pub(crate) fn classify_parametric_is<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, lhs_ty: Type<'db>, rhs_alias: crate::types::class::GenericAlias<'db>, @@ -728,7 +776,8 @@ pub(crate) fn classify_parametric_is<'db>( }; let plan = classify_value( db, - lhs_ty.promote(db), + env, + lhs_ty.promote(db, env), target_origin, rhs_alias, &target_args_ast, @@ -746,7 +795,7 @@ pub(crate) fn classify_parametric_is<'db>( if let ParametricIsPlan::Probe(_) = plan && let Some(ErasedTargetReason::Protocol) = erased_target_reason(db, target_origin) { - return protocol_structural_members(db, file, ClassType::Generic(rhs_alias)) + return protocol_structural_members(db, env, file, ClassType::Generic(rhs_alias)) .map(|checks| ParametricIsPlan::ProtocolStructural(checks.into_boxed_slice())) .unwrap_or(ParametricIsPlan::ErasedTarget(ErasedTargetReason::Protocol)); } @@ -764,6 +813,7 @@ pub(crate) fn classify_parametric_is<'db>( /// reified annotations, so both consult one source of truth. pub(crate) fn protocol_structural_members<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, class: ClassType<'db>, ) -> Option> { @@ -771,13 +821,13 @@ pub(crate) fn protocol_structural_members<'db>( let mut checks = Vec::new(); for member in protocol_class.interface(db).members(db) { let name = member.name().to_owned(); - let check = match member.reified_member_shape(db)? { + let check = match member.reified_member_shape(db, env)? { ReifiedMember::Attribute { ty, readable, writable, } => { - let expected = protocol_member_spelling(db, file, ty)?; + let expected = protocol_member_spelling(db, env, file, ty)?; let variance = match (readable, writable) { (true, true) => ArgVariance::Invariant, (true, false) => ArgVariance::Covariant, @@ -796,10 +846,10 @@ pub(crate) fn protocol_structural_members<'db>( // back to the erased-target error let mut param_checks = Vec::with_capacity(params.len()); for param_ty in params { - let expected = protocol_member_spelling(db, file, param_ty)?; + let expected = protocol_member_spelling(db, env, file, param_ty)?; param_checks.push((expected, ArgVariance::Contravariant)); } - let ret = match reified_return_check(db, file, ret) { + let ret = match reified_return_check(db, env, file, ret) { ReturnCheck::Skip => None, ReturnCheck::Check(expected) => Some((expected, ArgVariance::Covariant)), ReturnCheck::Unspellable => return None, @@ -828,7 +878,12 @@ pub(crate) fn protocol_structural_members<'db>( /// This deliberately does *not* widen [`runtime_spelling`] itself: that spelling /// is also injected into reified calls (`f[int](…)`) and constructor /// specializations (`A[int](1)`), where `_by_lit` is not in scope. -fn protocol_member_spelling<'db>(db: &'db dyn Db, file: File, ty: Type<'db>) -> Option { +fn protocol_member_spelling<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + file: File, + ty: Type<'db>, +) -> Option { if let Type::LiteralValue(literal) = ty { let value = match literal.kind() { LiteralValueTypeKind::Bool(boolean) => { @@ -849,7 +904,7 @@ fn protocol_member_spelling<'db>(db: &'db dyn Db, file: File, ty: Type<'db>) -> }; return Some(format!("_by_lit({value})")); } - runtime_spelling(db, file, ty) + runtime_spelling(db, env, file, ty) } /// the covariant/skip/unspellable classification of a protocol method's return @@ -864,23 +919,29 @@ enum ReturnCheck { Unspellable, } -fn reified_return_check<'db>(db: &'db dyn Db, file: File, ret: Type<'db>) -> ReturnCheck { - if ret.is_none(db) || ret.is_dynamic() || is_object_instance(db, ret) { +fn reified_return_check<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + file: File, + ret: Type<'db>, +) -> ReturnCheck { + if ret.is_none(db) || ret.is_dynamic() || is_object_instance(db, env, ret) { return ReturnCheck::Skip; } - match protocol_member_spelling(db, file, ret) { + match protocol_member_spelling(db, env, file, ret) { Some(expected) => ReturnCheck::Check(expected), None => ReturnCheck::Unspellable, } } -fn is_object_instance<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { +fn is_object_instance<'db>(db: &'db dyn Db, env: &ProgramEnvironment<'db>, ty: Type<'db>) -> bool { matches!(ty, Type::NominalInstance(instance) - if instance.class(db).class_literal(db).is_known(db, KnownClass::Object)) + if instance.class(db, env).class_literal(db).is_known(db, KnownClass::Object)) } fn classify_value<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, value_ty: Type<'db>, target_origin: ClassLiteral<'db>, rhs_alias: crate::types::class::GenericAlias<'db>, @@ -890,9 +951,10 @@ fn classify_value<'db>( // when the value's type is carried by a reified type parameter, the answer // lives in a runtime cell rather than the static type — extract the cell // comparisons before falling back to static subtyping - if value_ty.has_typevar(db) + if value_ty.has_typevar(db, env) && let Some(plan) = try_token_eq( db, + env, value_ty, target_origin, rhs_alias, @@ -905,10 +967,10 @@ fn classify_value<'db>( // `a is C[args]` means `type(a) <: C[args]`, so the static answer is a // subtype question — this respects `C`'s declared variance for free - let target_instance = Type::instance(db, ClassType::Generic(rhs_alias)); - if value_ty.is_subtype_of(db, target_instance) { + let target_instance = Type::instance(db, env, ClassType::Generic(rhs_alias)); + if value_ty.is_subtype_of(db, env, target_instance) { ParametricIsPlan::Fold(true) - } else if value_ty.is_disjoint_from(db, target_instance) { + } else if value_ty.is_disjoint_from(db, env, target_instance) { ParametricIsPlan::Fold(false) } else { // undecidable statically; `classify_parametric_is` turns this into a @@ -923,6 +985,7 @@ fn classify_value<'db>( /// not so shaped (the caller then resolves it statically). fn try_token_eq<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, value_ty: Type<'db>, target_origin: ClassLiteral<'db>, rhs_alias: crate::types::class::GenericAlias<'db>, @@ -945,7 +1008,7 @@ fn try_token_eq<'db>( )])) } Type::NominalInstance(instance) => { - let ClassType::Generic(alias) = instance.class(db) else { + let ClassType::Generic(alias) = instance.class(db, env) else { return None; }; if ClassLiteral::Static(alias.origin(db)) != target_origin { @@ -954,6 +1017,7 @@ fn try_token_eq<'db>( let mut tokens = Vec::new(); unify_specializations( db, + env, target_origin, alias.specialization(db), rhs_alias.specialization(db), @@ -981,13 +1045,14 @@ fn try_token_eq<'db>( /// type arguments against the value's reified `__orig_class__`. pub(crate) fn parametric_soundness_spelling<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, ty: Type<'db>, ) -> Option<(String, Box<[ArgVariance]>)> { let Type::NominalInstance(instance) = ty else { return None; }; - let ClassType::Generic(alias) = instance.class(db) else { + let ClassType::Generic(alias) = instance.class(db, env) else { return None; }; let origin = ClassLiteral::Static(alias.origin(db)); @@ -997,7 +1062,7 @@ pub(crate) fn parametric_soundness_spelling<'db>( if erased_target_reason(db, origin).is_some() { return None; } - let spelling = spell_class(db, file, ClassType::Generic(alias))?; + let spelling = spell_class(db, env, file, ClassType::Generic(alias))?; let variances = target_variances(db, alias); if variances.is_empty() { return None; @@ -1023,7 +1088,7 @@ fn target_variances<'db>(db: &'db dyn Db, alias: GenericAlias<'db>) -> Box<[ArgV generic_context .variables(db) .map(|bound_typevar| { - let declared = bound_typevar.variance(db); + let declared = bound_typevar.probe_variance(db); let effective = combine_use_site_projections( declared, None, @@ -1047,6 +1112,7 @@ fn target_variances<'db>(db: &'db dyn Db, alias: GenericAlias<'db>) -> Box<[ArgV /// a set of token comparisons (the caller then resolves the test statically). fn unify_specializations<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, origin: ClassLiteral<'db>, value_spec: Specialization<'db>, target_spec: Specialization<'db>, @@ -1066,6 +1132,7 @@ fn unify_specializations<'db>( { unify_argument( db, + env, *s, *t, target_args_ast.and_then(|args| args.get(index).copied()), @@ -1085,6 +1152,7 @@ fn unify_specializations<'db>( for (index, (s, t)) in value_types.iter().zip(target_types).enumerate() { unify_argument( db, + env, *s, *t, target_args_ast.and_then(|args| args.get(index).copied()), @@ -1096,12 +1164,13 @@ fn unify_specializations<'db>( fn unify_argument<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, value: Type<'db>, target: Type<'db>, target_ast: Option<&ast::Expr>, tokens: &mut Vec<(Name, TextRange)>, ) -> Result<(), ()> { - if value == target || value.is_equivalent_to(db, target) { + if value == target || value.is_equivalent_to(db, env, target) { return Ok(()); } if let Type::TypeVar(bound_typevar) = value { @@ -1118,8 +1187,10 @@ fn unify_argument<'db>( // (`list[T]` vs the `list[int]` written in the rhs) if let (Type::NominalInstance(value_instance), Type::NominalInstance(target_instance)) = (value, target) - && let (ClassType::Generic(value_alias), ClassType::Generic(target_alias)) = - (value_instance.class(db), target_instance.class(db)) + && let (ClassType::Generic(value_alias), ClassType::Generic(target_alias)) = ( + value_instance.class(db, env), + target_instance.class(db, env), + ) && value_alias.origin(db) == target_alias.origin(db) { let nested_ast: Option> = @@ -1133,6 +1204,7 @@ fn unify_argument<'db>( }; return unify_specializations( db, + env, ClassLiteral::Static(value_alias.origin(db)), value_alias.specialization(db), target_alias.specialization(db), @@ -1155,7 +1227,7 @@ fn is_reified_function_typevar<'db>( return false; }; let def_file = definition.file(db); - let module = parsed_module(db, def_file).load(db); + let module = parsed_module(db, db.program_file(def_file).python_file(db)).load(db); let ty_python_core::definition::DefinitionKind::Function(function) = definition.kind(db) else { return false; }; @@ -1201,6 +1273,7 @@ pub(crate) enum ReifiedOverrideError<'db> { /// check's scope — plain erased generics, overloads, `*Ts` / `**P` lists). pub(crate) fn reified_override_error<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, base: FunctionType<'db>, sub: FunctionType<'db>, ) -> Option> { @@ -1217,8 +1290,8 @@ pub(crate) fn reified_override_error<'db>( .then(|| ReifiedOverrideError::ReifiesErased(missing.iter().cloned().collect())) } (true, true) => { - let base_interface = type_param_interface(db, base)?; - let sub_interface = type_param_interface(db, sub)?; + let base_interface = type_param_interface(db, env, base)?; + let sub_interface = type_param_interface(db, env, sub)?; if sub_interface.required > base_interface.required || sub_interface.params.len() < base_interface.params.len() { @@ -1232,7 +1305,7 @@ pub(crate) fn reified_override_error<'db>( for ((base_name, base_admissible), (sub_name, sub_admissible)) in base_interface.params.iter().zip(&sub_interface.params) { - if !base_admissible.is_assignable_to(db, *sub_admissible) { + if !base_admissible.is_assignable_to(db, env, *sub_admissible) { return Some(ReifiedOverrideError::Bound { base_name: base_name.clone(), sub_name: sub_name.clone(), @@ -1257,6 +1330,7 @@ struct TypeParamInterface<'db> { fn type_param_interface<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, function: FunctionType<'db>, ) -> Option> { let signature = function.signature(db); @@ -1274,10 +1348,10 @@ fn type_param_interface<'db>( .variables(db) .map(|bound_typevar| { let typevar = bound_typevar.typevar(db); - let admissible = match typevar.bound_or_constraints(db) { + let admissible = match typevar.bound_or_constraints(db, env) { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound, Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - constraints.as_type(db) + constraints.as_type(db, env) } None => Type::object(), }; @@ -1287,7 +1361,7 @@ fn type_param_interface<'db>( }) .unwrap_or_default(); - let module = parsed_module(db, overload_literal.file(db)).load(db); + let module = parsed_module(db, overload_literal.program_file(db).python_file(db)).load(db); let node = overload_literal .body_scope(db) .node(db) diff --git a/crates/ty_python_semantic/src/types/relation.rs b/crates/ty_python_semantic/src/types/relation.rs index 267007bbb2..336d178b1f 100644 --- a/crates/ty_python_semantic/src/types/relation.rs +++ b/crates/ty_python_semantic/src/types/relation.rs @@ -1,3 +1,4 @@ +use crate::ProgramEnvironment; use std::borrow::Cow; use itertools::Itertools; @@ -40,7 +41,7 @@ use crate::{ Db, types::{ ErrorContext, ErrorContextTree, Type, TypePair, constraints::ConstraintSet, - generics::InferableTypeVars, + typevar::TypeVarSet, }, }; @@ -220,19 +221,6 @@ pub(crate) enum TypeRelation { SubtypingAssuming, } -/// Determines when comparisons involving type variables are evaluated. -#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] -pub(crate) enum TypeVarEvaluation { - /// Check immediately whether the relation holds for all or any valid specializations, - /// depending on whether the type variable is inferable. - Eager, - - /// Move comparisons involving a type variable into the constraint set for later evaluation. - /// - /// This is currently opt-in, but will eventually replace eager type-variable evaluation. - Lazy, -} - impl TypeRelation { pub(crate) const fn is_assignability(self) -> bool { matches!(self, TypeRelation::Assignability) @@ -242,7 +230,7 @@ impl TypeRelation { matches!(self, TypeRelation::Subtyping) } - pub(crate) const fn can_safely_assume_reflexivity(self, ty: Type) -> bool { + const fn can_safely_assume_reflexivity(self, ty: Type) -> bool { match self { TypeRelation::Assignability | TypeRelation::Redundancy { .. } => true, TypeRelation::Subtyping | TypeRelation::SubtypingAssuming => { @@ -250,6 +238,26 @@ impl TypeRelation { } } } + + pub(super) const fn description(self) -> &'static str { + match self { + TypeRelation::Assignability => "assignable to", + _ => "a subtype of", + } + } +} + +/// Determines when comparisons involving type variables are evaluated. +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] +pub(crate) enum TypeVarEvaluation { + /// Check immediately whether the relation holds for all or any valid specializations, + /// depending on whether the type variable is inferable. + Eager, + + /// Move comparisons involving a type variable into the constraint set for later evaluation. + /// + /// This is currently opt-in, but will eventually replace eager type-variable evaluation. + Lazy, } #[salsa::tracked] @@ -272,11 +280,15 @@ impl<'db> Type<'db> { KnownBoundMethodType::FunctionTypeDunderGet(_) | KnownBoundMethodType::FunctionTypeDunderCall(_) | KnownBoundMethodType::StrStartswith(_) + | KnownBoundMethodType::ConstraintSetLowerBound + | KnownBoundMethodType::ConstraintSetUpperBound + | KnownBoundMethodType::ConstraintSetEquality | KnownBoundMethodType::ConstraintSetRange | KnownBoundMethodType::ConstraintSetAlways | KnownBoundMethodType::ConstraintSetNever | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) | KnownBoundMethodType::ConstraintSetSatisfies(_) + | KnownBoundMethodType::ConstraintSetExists(_) | KnownBoundMethodType::ConstraintSetForAll(_) | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) | KnownBoundMethodType::ConstraintSetSolutionsFor(_) @@ -290,13 +302,15 @@ impl<'db> Type<'db> { | Type::SpecialForm(_) | Type::KnownInstance(_) | Type::AlwaysFalsy - | Type::AlwaysTruthy + | Type::AlwaysTruthy => true, + // `T` is always a subtype of itself, // and `T` is always a subtype of `T | None` - | Type::TypeVar(_) + Type::TypeVar(_) => true, + // might inherit `Any`, but subtyping is still reflexive - | Type::ClassLiteral(_) - => true, + Type::ClassLiteral(_) => true, + Type::Dynamic(_) | Type::Divergent(_) | Type::NominalInstance(_) @@ -330,20 +344,33 @@ impl<'db> Type<'db> { /// Return true if this type is a subtype of type `target`. /// /// See [`TypeRelation::Subtyping`] for more details. - pub(crate) fn is_subtype_of(self, db: &'db dyn Db, target: Type<'db>) -> bool { + pub(crate) fn is_subtype_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + target: Type<'db>, + ) -> bool { let constraints = ConstraintSetBuilder::new(); - self.when_subtype_of(db, target, &constraints, InferableTypeVars::None) - .is_always_satisfied(db) + self.when_subtype_of(db, env, target, &constraints, TypeVarSet::None) + .is_always_satisfied(db, env) } pub(super) fn when_subtype_of<'c>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, target: Type<'db>, constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, ) -> ConstraintSet<'db, 'c> { - self.has_relation_to(db, target, constraints, inferable, TypeRelation::Subtyping) + self.has_relation_to( + db, + env, + target, + constraints, + inferable, + TypeRelation::Subtyping, + ) } /// Return the constraints under which this type is a subtype of type `target`, assuming that @@ -353,22 +380,25 @@ impl<'db> Type<'db> { pub(super) fn when_subtype_of_assuming<'c>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, target: Type<'db>, assuming: ConstraintSet<'db, 'c>, constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, ) -> ConstraintSet<'db, 'c> { let relation_visitor = HasRelationToVisitor::default(constraints); let disjointness_visitor = IsDisjointVisitor::default(constraints); let signature_relation_visitor = SignatureRelationVisitor::default(); - let materialization_visitor = ApplyTypeMappingVisitor::default(); + let materialization_visitor = ApplyTypeMappingVisitor::new(env); let checker = TypeRelationChecker { + env, constraints, inferable, relation: TypeRelation::SubtypingAssuming, typevar_evaluation: TypeVarEvaluation::Eager, context_tree: None, given: assuming, + perform_expensive_checks: true, relation_visitor: &relation_visitor, disjointness_visitor: &disjointness_visitor, signature_relation_visitor: &signature_relation_visitor, @@ -380,10 +410,15 @@ impl<'db> Type<'db> { /// Return true if this type is assignable to type `target`. /// /// See `TypeRelation::Assignability` for more details. - pub fn is_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool { + pub fn is_assignable_to( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + target: Type<'db>, + ) -> bool { let constraints = ConstraintSetBuilder::new(); - self.when_assignable_to(db, target, &constraints, InferableTypeVars::None) - .is_always_satisfied(db) + self.when_assignable_to(db, env, target, &constraints, TypeVarSet::None) + .is_always_satisfied(db, env) } /// Re-run the assignability check with error context collection enabled. @@ -396,48 +431,94 @@ impl<'db> Type<'db> { pub(crate) fn assignability_error_context( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + target: Type<'db>, + ) -> ErrorContextTree<'db> { + self.relation_error_context(db, env, TypeRelation::Assignability, target) + } + + /// Re-run the pure redundancy check with error context collection enabled. + /// + /// This should normally be called when `is_pure_redundant_with` has returned `false` + /// and we are now about to emit a diagnostic where additional context could be + /// useful. + /// + /// This is a separate method so that we can skip this expensive check when diagnostics + /// are suppressed. + pub(crate) fn pure_redundancy_error_context( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + target: Type<'db>, + ) -> ErrorContextTree<'db> { + self.relation_error_context(db, env, TypeRelation::Redundancy { pure: true }, target) + } + + /// Re-run the relation check with error context collection enabled. + /// + /// This is a separate method so that we can skip this expensive check when diagnostics + /// are suppressed. + pub(crate) fn relation_error_context( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + relation: TypeRelation, target: Type<'db>, ) -> ErrorContextTree<'db> { let builder = ConstraintSetBuilder::new(); let checker = TypeRelationChecker { + env, constraints: &builder, - inferable: InferableTypeVars::None, - relation: TypeRelation::Assignability, + inferable: TypeVarSet::None, + relation, typevar_evaluation: TypeVarEvaluation::Eager, - context_tree: Some(ErrorContextTree::new()), + context_tree: Some(ErrorContextTree::new(relation)), given: ConstraintSet::from_bool(&builder, false), + perform_expensive_checks: true, relation_visitor: &HasRelationToVisitor::default(&builder), disjointness_visitor: &IsDisjointVisitor::default(&builder), signature_relation_visitor: &SignatureRelationVisitor::default(), - materialization_visitor: &ApplyTypeMappingVisitor::default(), + materialization_visitor: &ApplyTypeMappingVisitor::new(env), }; checker.check_type_pair(db, self, target); checker.into_error_context() } /// Return true if this type is assignable to type `target` using constraint-set typevar rules. - pub fn is_constraint_set_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool { + pub(crate) fn is_constraint_set_assignable_to( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + target: Type<'db>, + ) -> bool { let constraints = ConstraintSetBuilder::new(); - self.when_constraint_set_assignable_to(db, target, &constraints) - .is_always_satisfied(db) + self.when_constraint_set_assignable_to(db, env, target, &constraints) + .is_always_satisfied(db, env) } /// Return true if this type is a subtype of `target` using constraint-set typevar rules. - pub(super) fn is_constraint_set_subtype_of(self, db: &'db dyn Db, target: Type<'db>) -> bool { + pub(super) fn is_constraint_set_subtype_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + target: Type<'db>, + ) -> bool { let constraints = ConstraintSetBuilder::new(); - self.when_constraint_set_subtype_of(db, target, &constraints) - .is_always_satisfied(db) + self.when_constraint_set_subtype_of(db, env, target, &constraints) + .is_always_satisfied(db, env) } pub(super) fn when_assignable_to<'c>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, target: Type<'db>, constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, ) -> ConstraintSet<'db, 'c> { self.has_relation_to( db, + env, target, constraints, inferable, @@ -481,6 +562,7 @@ impl<'db> Type<'db> { pub(super) fn when_constraint_set_assignable_to_owned( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, target: Type<'db>, ) -> Cow<'db, OwnedConstraintSet<'db>> { #[salsa::tracked( @@ -492,6 +574,8 @@ impl<'db> Type<'db> { db: &'db dyn Db, types: TypePair<'db>, ) -> OwnedConstraintSet<'db> { + let program = types.program(db); + let env = ProgramEnvironment::from_program(program); let constraints = ConstraintSetBuilder::new(); constraints.into_owned(|constraints| { let source = types.first(db); @@ -499,9 +583,10 @@ impl<'db> Type<'db> { source.has_relation_to_with_typevar_evaluation( db, + &env, target, constraints, - InferableTypeVars::None, + TypeVarSet::None, TypeRelation::Assignability, TypeVarEvaluation::Lazy, ) @@ -512,39 +597,44 @@ impl<'db> Type<'db> { return Cow::Owned(OwnedConstraintSet::always()); } + let program = env.program(db); Cow::Borrowed(when_constraint_set_assignable_to_owned_impl( db, - TypePair::new(db, self, target), + TypePair::new(db, program, self, target), )) } pub(super) fn when_constraint_set_assignable_to<'c>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, target: Type<'db>, constraints: &'c ConstraintSetBuilder<'db>, ) -> ConstraintSet<'db, 'c> { self.has_relation_to_with_typevar_evaluation( db, + env, target, constraints, - InferableTypeVars::None, + TypeVarSet::None, TypeRelation::Assignability, TypeVarEvaluation::Lazy, ) } - pub(super) fn when_constraint_set_subtype_of<'c>( + fn when_constraint_set_subtype_of<'c>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, target: Type<'db>, constraints: &'c ConstraintSetBuilder<'db>, ) -> ConstraintSet<'db, 'c> { self.has_relation_to_with_typevar_evaluation( db, + env, target, constraints, - InferableTypeVars::None, + TypeVarSet::None, TypeRelation::Subtyping, TypeVarEvaluation::Lazy, ) @@ -553,38 +643,76 @@ impl<'db> Type<'db> { /// Return `true` if it would be redundant to add `self` to a union that already contains `other`. /// /// See [`TypeRelation::Redundancy`] for more details. - pub(super) fn is_redundant_with(self, db: &'db dyn Db, other: Type<'db>) -> bool { + pub(super) fn is_redundant_with( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + other: Type<'db>, + ) -> bool { #[salsa::tracked(returns(copy), cycle_initial=|_, _, _| true, heap_size=ruff_memory_usage::heap_size)] fn is_redundant_with_impl<'db>(db: &'db dyn Db, types: TypePair<'db>) -> bool { + let program = types.program(db); + let env = ProgramEnvironment::from_program(program); types .first(db) .has_relation_to( db, + &env, types.second(db), &ConstraintSetBuilder::new(), - InferableTypeVars::None, + TypeVarSet::None, TypeRelation::Redundancy { pure: false }, ) - .is_always_satisfied(db) + .is_always_satisfied(db, &env) } if self == other { return true; } - is_redundant_with_impl(db, TypePair::new(db, self, other)) + let program = env.program(db); + is_redundant_with_impl(db, TypePair::new(db, program, self, other)) + } + + /// Return `true` if `self` is redundant with `other` under the pure redundancy relation. + /// + /// Unlike [`Self::is_redundant_with`], this does not apply shortcuts intended for simplifying + /// unions. + pub(super) fn is_pure_redundant_with( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + other: Type<'db>, + ) -> bool { + if self == other { + return true; + } + + let program = env.program(db); + let env = ProgramEnvironment::from_program(program); + self.has_relation_to( + db, + &env, + other, + &ConstraintSetBuilder::new(), + TypeVarSet::None, + TypeRelation::Redundancy { pure: true }, + ) + .is_always_satisfied(db, &env) } pub(super) fn has_relation_to<'c>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, target: Type<'db>, constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, relation: TypeRelation, ) -> ConstraintSet<'db, 'c> { self.has_relation_to_with_typevar_evaluation( db, + env, target, constraints, inferable, @@ -593,26 +721,30 @@ impl<'db> Type<'db> { ) } + #[expect(clippy::too_many_arguments)] fn has_relation_to_with_typevar_evaluation<'c>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, target: Type<'db>, constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, relation: TypeRelation, typevar_evaluation: TypeVarEvaluation, ) -> ConstraintSet<'db, 'c> { let relation_visitor = HasRelationToVisitor::default(constraints); let disjointness_visitor = IsDisjointVisitor::default(constraints); let signature_relation_visitor = SignatureRelationVisitor::default(); - let materialization_visitor = ApplyTypeMappingVisitor::default(); + let materialization_visitor = ApplyTypeMappingVisitor::new(env); let checker = TypeRelationChecker { + env, constraints, inferable, relation, typevar_evaluation, context_tree: None, given: ConstraintSet::from_bool(constraints, false), + perform_expensive_checks: true, relation_visitor: &relation_visitor, disjointness_visitor: &disjointness_visitor, signature_relation_visitor: &signature_relation_visitor, @@ -633,54 +765,103 @@ impl<'db> Type<'db> { /// > — [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 { - self.when_equivalent_to(db, other, &ConstraintSetBuilder::new()) - .is_always_satisfied(db) + pub(crate) fn is_equivalent_to( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + other: Type<'db>, + ) -> bool { + self.when_equivalent_to(db, env, other, &ConstraintSetBuilder::new()) + .is_always_satisfied(db, env) } pub(crate) fn is_equivalent_to_with_materialization_visitor( self, db: &'db dyn Db, other: Type<'db>, - materialization_visitor: &ApplyTypeMappingVisitor<'db>, + materialization_visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> bool { self.when_equivalent_to_with_materialization_visitor( db, other, &ConstraintSetBuilder::new(), materialization_visitor, + TypeVarEvaluation::Eager, ) - .is_always_satisfied(db) + .is_always_satisfied(db, materialization_visitor.env) } pub(crate) fn when_equivalent_to<'c>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, other: Type<'db>, constraints: &'c ConstraintSetBuilder<'db>, ) -> ConstraintSet<'db, 'c> { - let materialization_visitor = ApplyTypeMappingVisitor::default(); + let materialization_visitor = ApplyTypeMappingVisitor::new(env); self.when_equivalent_to_with_materialization_visitor( db, other, constraints, &materialization_visitor, + TypeVarEvaluation::Eager, + ) + } + + /// Returns whether `self` and `other` can be equivalent under some typevar specialization. + pub(super) fn can_be_constraint_set_equivalent_to( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + other: Type<'db>, + ) -> bool { + #[salsa::tracked(returns(copy), cycle_initial=|_, _, _| true, heap_size=ruff_memory_usage::heap_size)] + fn can_be_constraint_set_equivalent_to_impl<'db>( + db: &'db dyn Db, + types: TypePair<'db>, + ) -> bool { + let env = ProgramEnvironment::from_program(types.program(db)); + let constraints = ConstraintSetBuilder::new(); + let materialization_visitor = ApplyTypeMappingVisitor::new(&env); + !types + .first(db) + .when_equivalent_to_with_materialization_visitor( + db, + types.second(db), + &constraints, + &materialization_visitor, + TypeVarEvaluation::Lazy, + ) + .is_never_satisfied(db, &env) + } + + if self == other { + return true; + } + + can_be_constraint_set_equivalent_to_impl( + db, + TypePair::new(db, env.program(db), self, other), ) } - pub(crate) fn when_equivalent_to_with_materialization_visitor<'c>( + fn when_equivalent_to_with_materialization_visitor<'c>( self, db: &'db dyn Db, other: Type<'db>, constraints: &'c ConstraintSetBuilder<'db>, - materialization_visitor: &ApplyTypeMappingVisitor<'db>, + materialization_visitor: &ApplyTypeMappingVisitor<'_, 'db>, + typevar_evaluation: TypeVarEvaluation, ) -> ConstraintSet<'db, 'c> { let relation_visitor = HasRelationToVisitor::default(constraints); let disjointness_visitor = IsDisjointVisitor::default(constraints); let signature_relation_visitor = SignatureRelationVisitor::default(); let checker = EquivalenceChecker { + env: materialization_visitor.env, constraints, given: ConstraintSet::from_bool(constraints, false), + perform_expensive_checks: true, + typevar_evaluation, relation_visitor: &relation_visitor, disjointness_visitor: &disjointness_visitor, signature_relation_visitor: &signature_relation_visitor, @@ -704,27 +885,64 @@ impl<'db> Type<'db> { /// /// 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 { + pub(crate) fn is_disjoint_from( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + other: Type<'db>, + ) -> bool { let constraints = ConstraintSetBuilder::new(); - self.when_disjoint_from(db, other, &constraints, InferableTypeVars::None) - .is_always_satisfied(db) + self.when_disjoint_from(db, env, other, &constraints, TypeVarSet::None) + .is_always_satisfied(db, env) } pub(crate) fn when_disjoint_from<'c>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + other: Type<'db>, + constraints: &'c ConstraintSetBuilder<'db>, + inferable: TypeVarSet<'db>, + ) -> ConstraintSet<'db, 'c> { + let relation_visitor = HasRelationToVisitor::default(constraints); + let disjointness_visitor = IsDisjointVisitor::default(constraints); + let signature_relation_visitor = SignatureRelationVisitor::default(); + let materialization_visitor = ApplyTypeMappingVisitor::new(env); + let checker = DisjointnessChecker { + env, + constraints, + inferable, + given: ConstraintSet::from_bool(constraints, false), + perform_expensive_checks: true, + disjointness_visitor: &disjointness_visitor, + relation_visitor: &relation_visitor, + signature_relation_visitor: &signature_relation_visitor, + materialization_visitor: &materialization_visitor, + }; + checker.check_type_pair(db, self, other) + } + + /// Checks whether `self` is disjoint from `other`, while being more accepting of false + /// negatives. Use this when you want to _quickly_ check whether two types are _definitely_ + /// disjoint, typically for engaging a fast path in some algorithm. + pub(crate) fn when_trivially_disjoint_from<'c>( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, other: Type<'db>, constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, ) -> ConstraintSet<'db, 'c> { let relation_visitor = HasRelationToVisitor::default(constraints); let disjointness_visitor = IsDisjointVisitor::default(constraints); let signature_relation_visitor = SignatureRelationVisitor::default(); - let materialization_visitor = ApplyTypeMappingVisitor::default(); + let materialization_visitor = ApplyTypeMappingVisitor::new(env); let checker = DisjointnessChecker { + env, constraints, inferable, given: ConstraintSet::from_bool(constraints, false), + perform_expensive_checks: false, disjointness_visitor: &disjointness_visitor, relation_visitor: &relation_visitor, signature_relation_visitor: &signature_relation_visitor, @@ -788,12 +1006,14 @@ impl<'db, 'c> IsDisjointVisitor<'db, 'c> { #[derive(Clone)] pub(super) struct TypeRelationChecker<'a, 'c, 'db> { + pub(super) env: &'a ProgramEnvironment<'db>, pub(super) constraints: &'c ConstraintSetBuilder<'db>, - pub(super) inferable: InferableTypeVars<'db>, + pub(super) inferable: TypeVarSet<'db>, pub(super) relation: TypeRelation, pub(super) typevar_evaluation: TypeVarEvaluation, context_tree: Option>, - pub(super) given: ConstraintSet<'db, 'c>, + given: ConstraintSet<'db, 'c>, + perform_expensive_checks: bool, // N.B. these fields are private to reduce the risk of // "double-visiting" a given pair of types. You should @@ -804,25 +1024,28 @@ pub(super) struct TypeRelationChecker<'a, 'c, 'db> { relation_visitor: &'a HasRelationToVisitor<'db, 'c>, disjointness_visitor: &'a IsDisjointVisitor<'db, 'c>, pub(super) signature_relation_visitor: &'a SignatureRelationVisitor<'db>, - pub(super) materialization_visitor: &'a ApplyTypeMappingVisitor<'db>, + pub(super) materialization_visitor: &'a ApplyTypeMappingVisitor<'a, 'db>, } impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { pub(super) fn subtyping( + env: &'a ProgramEnvironment<'db>, constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, relation_visitor: &'a HasRelationToVisitor<'db, 'c>, disjointness_visitor: &'a IsDisjointVisitor<'db, 'c>, signature_relation_visitor: &'a SignatureRelationVisitor<'db>, - materialization_visitor: &'a ApplyTypeMappingVisitor<'db>, + materialization_visitor: &'a ApplyTypeMappingVisitor<'a, 'db>, ) -> Self { Self { + env, constraints, inferable, relation: TypeRelation::Subtyping, typevar_evaluation: TypeVarEvaluation::Eager, context_tree: None, given: ConstraintSet::from_bool(constraints, false), + perform_expensive_checks: true, relation_visitor, disjointness_visitor, signature_relation_visitor, @@ -831,19 +1054,22 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { } pub(super) fn constraint_set_assignability( + env: &'a ProgramEnvironment<'db>, constraints: &'c ConstraintSetBuilder<'db>, relation_visitor: &'a HasRelationToVisitor<'db, 'c>, disjointness_visitor: &'a IsDisjointVisitor<'db, 'c>, signature_relation_visitor: &'a SignatureRelationVisitor<'db>, - materialization_visitor: &'a ApplyTypeMappingVisitor<'db>, + materialization_visitor: &'a ApplyTypeMappingVisitor<'a, 'db>, ) -> Self { Self { + env, constraints, - inferable: InferableTypeVars::None, + inferable: TypeVarSet::None, relation: TypeRelation::Assignability, typevar_evaluation: TypeVarEvaluation::Lazy, context_tree: None, given: ConstraintSet::from_bool(constraints, false), + perform_expensive_checks: true, relation_visitor, disjointness_visitor, signature_relation_visitor, @@ -852,19 +1078,22 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { } pub(super) fn constraint_set_assignability_with_context( + env: &'a ProgramEnvironment<'db>, constraints: &'c ConstraintSetBuilder<'db>, relation_visitor: &'a HasRelationToVisitor<'db, 'c>, disjointness_visitor: &'a IsDisjointVisitor<'db, 'c>, signature_relation_visitor: &'a SignatureRelationVisitor<'db>, - materialization_visitor: &'a ApplyTypeMappingVisitor<'db>, + materialization_visitor: &'a ApplyTypeMappingVisitor<'a, 'db>, ) -> Self { Self { + env, constraints, - inferable: InferableTypeVars::None, + inferable: TypeVarSet::None, relation: TypeRelation::Assignability, typevar_evaluation: TypeVarEvaluation::Lazy, - context_tree: Some(ErrorContextTree::new()), + context_tree: Some(ErrorContextTree::new(TypeRelation::Assignability)), given: ConstraintSet::from_bool(constraints, false), + perform_expensive_checks: true, relation_visitor, disjointness_visitor, signature_relation_visitor, @@ -873,19 +1102,22 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { } pub(super) fn assignability_with_context( + env: &'a ProgramEnvironment<'db>, constraints: &'c ConstraintSetBuilder<'db>, relation_visitor: &'a HasRelationToVisitor<'db, 'c>, disjointness_visitor: &'a IsDisjointVisitor<'db, 'c>, signature_relation_visitor: &'a SignatureRelationVisitor<'db>, - materialization_visitor: &'a ApplyTypeMappingVisitor<'db>, + materialization_visitor: &'a ApplyTypeMappingVisitor<'a, 'db>, ) -> Self { Self { + env, constraints, - inferable: InferableTypeVars::None, + inferable: TypeVarSet::None, relation: TypeRelation::Assignability, typevar_evaluation: TypeVarEvaluation::Eager, - context_tree: Some(ErrorContextTree::new()), + context_tree: Some(ErrorContextTree::new(TypeRelation::Assignability)), given: ConstraintSet::from_bool(constraints, false), + perform_expensive_checks: true, relation_visitor, disjointness_visitor, signature_relation_visitor, @@ -893,7 +1125,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { } } - pub(super) fn with_inferable_typevars(&self, inferable: InferableTypeVars<'db>) -> Self { + pub(super) fn with_inferable_typevars(&self, inferable: TypeVarSet<'db>) -> Self { Self { inferable, ..self.clone() @@ -907,16 +1139,18 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { source: ClassType<'db>, target: ClassType<'db>, ) -> bool { + let env = self.env; Self::subtyping( + env, self.constraints, - InferableTypeVars::None, + TypeVarSet::None, self.relation_visitor, self.disjointness_visitor, self.signature_relation_visitor, self.materialization_visitor, ) .check_class_pair(db, source, target) - .is_always_satisfied(db) + .is_always_satisfied(db, env) } pub(super) const fn is_eager_assignability(&self) -> bool { @@ -926,7 +1160,8 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { /// Return the collected error context, or an empty tree if collection was disabled. pub(super) fn into_error_context(self) -> ErrorContextTree<'db> { - self.context_tree.unwrap_or_else(ErrorContextTree::new) + self.context_tree + .unwrap_or_else(|| ErrorContextTree::new(self.relation)) } pub(super) fn always(&self) -> ConstraintSet<'db, 'c> { @@ -938,7 +1173,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { } /// Overwrite the error context tree with a new root context and child nodes. - pub(super) fn set_context( + fn set_context( &self, root: ErrorContext<'db>, children: impl IntoIterator>, @@ -1038,14 +1273,17 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { /// that are unrelated to each other in the regular-class domain (they do not inherit each /// other or any other common base), but they are all constrained to have a metaclass that /// inherits from `ABCMeta`. - fn is_metaclass_instance(db: &'db dyn Db, target: Type<'db>) -> bool { + fn is_metaclass_instance(&self, db: &'db dyn Db, target: Type<'db>) -> bool { target.as_nominal_instance().is_some_and(|instance| { + let env = self.env; KnownClass::Type - .try_to_class_literal(db) + .try_to_class_literal(db, env) .is_some_and(|type_class| { - instance - .class(db) - .is_subclass_of(db, ClassType::NonGeneric(ClassLiteral::Static(type_class))) + instance.class(db, env).is_subclass_of( + db, + env, + ClassType::NonGeneric(ClassLiteral::Static(type_class)), + ) }) }) } @@ -1112,17 +1350,19 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { target: Type<'db>, ) -> Option> { let source_i = source_subclass.into_type_var()?; - let is_exact_upper_bound = source_subclass.exact_typevar_upper_bound(db) == Some(target); + let env = self.env; + let is_exact_upper_bound = + source_subclass.exact_typevar_upper_bound(db, env) == Some(target); - if Self::is_metaclass_instance(db, target) { + if self.is_metaclass_instance(db, target) { return Some(self.check_type_pair( db, - source_subclass.to_metaclass_instance(db), + source_subclass.to_metaclass_instance(db, env), target, )); } - let projection = target.to_instance(db)?; + let projection = target.to_instance(db, env)?; if projection.is_exact() || is_exact_upper_bound { return Some(self.check_type_pair( db, @@ -1133,7 +1373,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { let source = source_subclass .subclass_of() - .with_transposed_type_var(db) + .with_transposed_type_var(db, env) .into_type_var()?; Some(self.check_type_pair(db, Type::TypeVar(source), target)) } @@ -1162,6 +1402,8 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { return self.always(); } + let env = self.env; + // Handle constraint implication first. If either `source` or `target` is a typevar, check // the constraint set to see if the corresponding constraint is satisfied. if self.relation == TypeRelation::SubtypingAssuming @@ -1169,7 +1411,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { { return self .given - .implies_subtype_of(db, self.constraints, source, target); + .implies_subtype_of(db, env, self.constraints, source, target); } // With lazy evaluation, comparisons with a type variable are translated directly into a @@ -1182,24 +1424,26 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { // satisfies the upper bound/constraints). if let Type::TypeVar(bound_typevar) = source { let upper = if self.relation.is_subtyping() { - target.bottom_materialization(db) + target.bottom_materialization(db, env) } else { target }; return ConstraintSet::constrain_typevar_upper_bound( db, + env, self.constraints, bound_typevar, upper, ); } else if let Type::TypeVar(bound_typevar) = target { let lower = if self.relation.is_subtyping() { - source.top_materialization(db) + source.top_materialization(db, env) } else { source }; return ConstraintSet::constrain_typevar_lower_bound( db, + env, self.constraints, bound_typevar, lower, @@ -1225,7 +1469,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { self.check_type_pair(db, source, element) }) }); - if distributed.is_always_satisfied(db) { + if distributed.is_always_satisfied(db, env) { return distributed; } } @@ -1306,7 +1550,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { // `final A` value is an `A` — so it is simply unwrapped (_, Type::Restricted(target_restricted)) => { let inner = target_restricted.type_argument(db); - if restriction_admits(db, target_restricted.modifier(db), inner, source) { + if restriction_admits(db, env, target_restricted.modifier(db), inner, source) { self.check_type_pair(db, source, inner) } else { self.never() @@ -1329,8 +1573,9 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { // exactly the interesting case, and reducing it first would compare `int` // against `int` and always agree (_, Type::Deferred(target_deferred)) if target_deferred.is_checked(db) => { - let names_another_value = !any_over_type(db, source, false, |ty| ty.is_dynamic()) - && LinearForm::same_value(db, source, target) == Some(false); + let names_another_value = + !any_over_type(db, env, source, false, |ty| ty.is_dynamic()) + && LinearForm::same_value(db, source, target) == Some(false); if names_another_value { self.never() } else { @@ -1339,8 +1584,8 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { // arm and ask it of a source that no longer mentions the parameter self.check_type_pair( db, - source.reduce_deferred(db), - target_deferred.reduced(db), + source.reduce_deferred(db, env), + target_deferred.reduced(db, env), ) } } @@ -1357,19 +1602,19 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { match LinearForm::same_value(db, source, target) { // the same value, so the target's own relation to itself is the answer Some(true) => self.check_type_pair(db, target, target), - Some(false) if !any_over_type(db, source, false, |ty| ty.is_dynamic()) => { + Some(false) if !any_over_type(db, env, source, false, |ty| ty.is_dynamic()) => { self.never() } - _ => self.check_type_pair(db, source_deferred.reduced(db), target), + _ => self.check_type_pair(db, source_deferred.reduced(db, env), target), } } (Type::Deferred(source_deferred), _) => { - self.check_type_pair(db, source_deferred.reduced(db), target) + self.check_type_pair(db, source_deferred.reduced(db, env), target) } (_, Type::Deferred(target_deferred)) => { - self.check_type_pair(db, source, target_deferred.reduced(db)) + self.check_type_pair(db, source, target_deferred.reduced(db, env)) } // Annotation unions retain type aliases so recursive aliases can be represented. @@ -1377,7 +1622,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { // that depend on multiple elements, such as all members of an enum, are visible. (_, Type::Union(union)) if union.has_aliases(db) => { self.with_recursion_guard(db, source, target, || { - self.check_type_pair(db, source, union.expand_aliases(db)) + self.check_type_pair(db, source, union.expand_aliases(db, env)) }) } @@ -1393,7 +1638,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { (Type::SubclassOf(source_subclass), Type::TypeForm(target_typeform)) => self .check_type_pair( db, - source_subclass.to_instance(db), + source_subclass.to_instance(db, env), target_typeform.type_argument(db), ), @@ -1406,25 +1651,25 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { (Type::ClassLiteral(source_class), Type::TypeForm(target_typeform)) => self .check_type_pair( db, - Type::instance(db, source_class.default_specialization(db)), + Type::instance(db, env, source_class.default_specialization(db)), target_typeform.type_argument(db), ), (Type::GenericAlias(source_alias), Type::TypeForm(target_typeform)) => self .check_type_pair( db, - Type::instance(db, ClassType::Generic(source_alias)), + Type::instance(db, env, ClassType::Generic(source_alias)), target_typeform.type_argument(db), ), (Type::KnownInstance(source_instance), Type::TypeForm(target_typeform)) - if let Some(source_argument) = source_instance.type_form_argument(db) => + if let Some(source_argument) = source_instance.type_form_argument(db, env) => { self.check_type_pair(db, source_argument, target_typeform.type_argument(db)) } (Type::SpecialForm(source_form), Type::TypeForm(target_typeform)) => source_form - .type_form_argument(db) + .type_form_argument(db, env) .when_some_and(db, self.constraints, |source_argument| { self.check_type_pair(db, source_argument, target_typeform.type_argument(db)) }), @@ -1436,15 +1681,15 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { } (Type::EnumComplement(complement), Type::LiteralValue(_) | Type::Union(_)) => { - self.check_type_pair(db, complement.remaining_literal_union(db), target) + self.check_type_pair(db, complement.remaining_literal_union(db, env), target) } (Type::EnumComplement(complement), _) => { - self.check_type_pair(db, complement.to_intersection(db), target) + self.check_type_pair(db, complement.to_intersection(db, env), target) } (_, Type::EnumComplement(complement)) => { - self.check_type_pair(db, source, complement.to_intersection(db)) + self.check_type_pair(db, source, complement.to_intersection(db, env)) } // Field definitions in dataclasses and dataclass-transformers can involve calls to @@ -1511,10 +1756,10 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { Type::KnownInstance(KnownInstanceType::FunctoolsPartial(partial)), Type::NominalInstance(target_instance), ) if target_instance - .class(db) + .class(db, env) .is_known(db, KnownClass::FunctoolsPartial) => { - let specialized = partial.partial(db).into_functools_partial_instance(db); + let specialized = partial.partial(db).into_functools_partial_instance(db, env); self.check_type_pair(db, specialized, target) } @@ -1593,7 +1838,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { (_, Type::UnsafeUnion(target_unsafe_union)) => match self.relation { TypeRelation::Subtyping | TypeRelation::SubtypingAssuming => self.never(), TypeRelation::Assignability => { - self.check_type_pair(db, source, target_unsafe_union.to_union(db)) + self.check_type_pair(db, source, target_unsafe_union.to_union(db, env)) } TypeRelation::Redundancy { .. } => self.never(), }, @@ -1651,7 +1896,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { // "collapse to 'object'" in this case is a sound over-approximation.) (_, Type::SubclassOf(subclass_of)) if let Some(type_var) = subclass_of.into_type_var() - && let Some(instance) = source.to_instance_approximation(db) => + && let Some(instance) = source.to_instance_approximation(db, env) => { self.check_type_pair(db, instance, Type::TypeVar(type_var)) } @@ -1666,7 +1911,11 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { { self.check_type_pair( db, - Type::tuple(Some(TupleType::unpacked_typevartuple(db, bound_typevar))), + Type::tuple(Some(TupleType::unpacked_typevartuple( + db, + env, + bound_typevar, + ))), target, ) } @@ -1678,7 +1927,11 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { self.check_type_pair( db, source, - Type::tuple(Some(TupleType::unpacked_typevartuple(db, bound_typevar))), + Type::tuple(Some(TupleType::unpacked_typevartuple( + db, + env, + bound_typevar, + ))), ) } @@ -1712,7 +1965,8 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { (_, Type::TypeVar(bound_typevar)) if self.is_eager_assignability() && !bound_typevar.is_inferable(db, self.inferable) - && crate::types::inferred_signature::gradual_hole(db, target).is_some() => + && crate::types::inferred_signature::gradual_hole(db, env, target) + .is_some() => { self.always() } @@ -1735,7 +1989,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { (Type::TypeVar(bound_typevar), _) if !bound_typevar.is_inferable(db, self.inferable) && let Some(bound_or_constraints) = - bound_typevar.typevar(db).bound_or_constraints(db) => + bound_typevar.typevar(db).bound_or_constraints(db, env) => { match bound_or_constraints { TypeVarBoundOrConstraints::UpperBound(bound) => { @@ -1758,13 +2012,13 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { if !bound_typevar.is_inferable(db, self.inferable) && let constraints = bound_typevar .typevar(db) - .constraints(db) + .constraints(db, env) .when_some_and(db, self.constraints, |constraints| { constraints.iter().when_all(db, self.constraints, |c| { self.check_type_pair(db, source, *c) }) }) - && !constraints.is_never_satisfied(db) => + && !constraints.is_never_satisfied(db, env) => { constraints } @@ -1778,17 +2032,21 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { self.always() } - // Fast path for various types that we know `object` is never a subtype of - // (`object` can be a subtype of some protocols, or of itself, but those cases are - // handled above). + // Fast path for various types that we know `object` is never a subtype of. ( Type::NominalInstance(source), - Type::NominalInstance(_) - | Type::SubclassOf(_) - | Type::Callable(_) - | Type::ProtocolInstance(_), + Type::NominalInstance(_) | Type::SubclassOf(_) | Type::Callable(_), ) if source.is_object() => self.never(), + // `object` is not a subtype of a non-universal protocol because some subclasses + // might not implement it. For assignability, still inspect its actual members: + // `object()` is hashable and commonly used as a sentinel for `Hashable` parameters. + (Type::NominalInstance(source), Type::ProtocolInstance(_)) + if source.is_object() && !self.relation.is_assignability() => + { + self.never() + } + // Fast path: `object` is not a subtype of any non-inferable type variable, since the // type variable could be specialized to a type smaller than `object`. (Type::NominalInstance(source), Type::TypeVar(typevar)) @@ -1802,13 +2060,13 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { } (Type::Union(union), _) => { - if let Some(supertype) = union.common_literal_supertype(db) { + if let Some(supertype) = union.common_literal_supertype(db, env) { // Use the broader supertype only as a positive proof. If it has the requested // relation to the target, then every literal in the union does too. Otherwise, // check each literal individually. let supertype_result = self .without_context_collection(|| self.check_type_pair(db, supertype, target)); - if supertype_result.is_always_satisfied(db) { + if supertype_result.is_trivially_always_satisfied() { return supertype_result; } } @@ -1819,7 +2077,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { .when_all(db, self.constraints, |&elem_ty| { let constraint_set = self.check_type_pair(db, elem_ty, target); if let Some(context) = self.report_context() - && constraint_set.is_never_satisfied(db) + && constraint_set.is_never_satisfied(db, env) { context.push(ErrorContext::NotAllUnionElementsAssignable { element: elem_ty, @@ -1833,7 +2091,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { (_, Type::Union(union)) => { if let Type::Intersection(intersection) = source - && let Some(alternatives) = intersection.finite_alternative_union(db) + && let Some(alternatives) = intersection.finite_alternative_union(db, env) { return self.check_type_pair(db, alternatives, target); } @@ -1850,7 +2108,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { { self.check_type_pair( db, - intersection.with_expanded_typevars_and_newtypes(db), + intersection.with_expanded_typevars_and_newtypes(db, env), target, ) } @@ -1875,9 +2133,9 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { .when_any(db, self.constraints, |&elem_ty| { let result = self.check_type_pair(db, source, elem_ty); if let Some(context_tree) = context_tree { - let ctx = context_tree.take(); - if !ctx.is_empty() { - elements_context.push(ctx); + let env = context_tree.take(); + if !env.is_empty() { + elements_context.push(env); } } result @@ -1886,16 +2144,16 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { if context_tree.is_some() && !elements_context.is_empty() - && result.is_never_satisfied(db) + && result.is_never_satisfied(db, env) { let elements_without_context = elements.len() - elements_context.len(); if elements_without_context > 0 && elements_without_context < elements.len() { - elements_context.push( + elements_context.push(ErrorContextTree::from_context( ErrorContext::NotAssignableToNOtherUnionElements { n: elements_without_context, - } - .into(), - ); + }, + self.relation, + )); } self.set_context( ErrorContext::NotAssignableToAnyUnionElement { @@ -1918,7 +2176,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { .when_all(db, self.constraints, |&pos_ty| { let constraint_set = self.check_type_pair(db, source, pos_ty); if let Some(context) = self.report_context() - && constraint_set.is_never_satisfied(db) + && constraint_set.is_never_satisfied(db, env) { context.push(ErrorContext::NotAssignableToIntersectionElement { source, @@ -1946,7 +2204,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { TypeRelation::Subtyping | TypeRelation::Redundancy { .. } | TypeRelation::SubtypingAssuming => source, - TypeRelation::Assignability => source.bottom_materialization(db), + TypeRelation::Assignability => source.bottom_materialization(db, env), }; intersection .negative(db) @@ -1956,7 +2214,9 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { TypeRelation::Subtyping | TypeRelation::Redundancy { .. } | TypeRelation::SubtypingAssuming => neg_ty, - TypeRelation::Assignability => neg_ty.bottom_materialization(db), + TypeRelation::Assignability => { + neg_ty.bottom_materialization(db, env) + } }; self.as_disjointness_checker() .check_type_pair(db, source_ty, neg_ty) @@ -1965,7 +2225,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { (Type::Intersection(intersection), _) => { if matches!(target, Type::LiteralValue(_)) - && let Some(alternatives) = intersection.finite_alternative_union(db) + && let Some(alternatives) = intersection.finite_alternative_union(db, env) { return self.check_type_pair(db, alternatives, target); } @@ -1983,9 +2243,9 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { .when_any(db, self.constraints, |elem_ty| { let result = self.check_type_pair(db, elem_ty, target); if let Some(context_tree) = context_tree { - let ctx = context_tree.take(); - if !ctx.is_empty() { - elements_context.push(ctx); + let env = context_tree.take(); + if !env.is_empty() { + elements_context.push(env); } } result @@ -1994,7 +2254,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { if should_expand_intersection(intersection) { self.check_type_pair( db, - intersection.with_expanded_typevars_and_newtypes(db), + intersection.with_expanded_typevars_and_newtypes(db, env), target, ) } else { @@ -2004,7 +2264,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { if context_tree.is_some() && !elements_context.is_empty() - && result.is_never_satisfied(db) + && result.is_never_satisfied(db, env) { self.set_context( ErrorContext::NoIntersectionElementAssignableToTarget { @@ -2036,7 +2296,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { (_, Type::TypeVar(typevar)) if typevar.is_inferable(db, self.inferable) => { if self.is_eager_assignability() { // TODO: record the unification constraints - typevar.typevar(db).upper_bound(db).when_none_or( + typevar.typevar(db).upper_bound(db, env).when_none_or( db, self.constraints, |bound| self.check_type_pair(db, source, bound), @@ -2045,6 +2305,23 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { self.never() } } + // An unbounded type variable ranges over every `object`, so the rule that applies + // to `object` itself applies here: for *subtyping* it says nothing, because the + // variable could be specialized to a type that does not implement the target, but + // for assignability the members are inspected rather than refused outright. Without + // this a `T` could not fill a `Hashable` parameter, which is what `dict[T, V]` asks + // of its key. + (Type::TypeVar(bound_typevar), _) + if self.relation.is_assignability() + && !bound_typevar.is_inferable(db, self.inferable) + && bound_typevar + .typevar(db) + .bound_or_constraints(db, env) + .is_none() => + { + self.check_type_pair(db, Type::object(), target) + } + (Type::TypeVar(bound_typevar), _) => { // All inferable cases should have been handled above assert!(!bound_typevar.is_inferable(db, self.inferable)); @@ -2062,10 +2339,10 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { // 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. (_, Type::AlwaysFalsy) => { - ConstraintSet::from_bool(self.constraints, source.bool(db).is_always_false()) + ConstraintSet::from_bool(self.constraints, source.bool(db, env).is_always_false()) } (_, Type::AlwaysTruthy) => { - ConstraintSet::from_bool(self.constraints, source.bool(db).is_always_true()) + ConstraintSet::from_bool(self.constraints, source.bool(db, env).is_always_true()) } // Currently, the only supertype of `AlwaysFalsy` and `AlwaysTruthy` is the universal set (object instance). (Type::AlwaysFalsy | Type::AlwaysTruthy, _) => { @@ -2166,9 +2443,11 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { (_, Type::Callable(target_callable)) => { self.with_recursion_guard(db, source, target, || { - let Some(callables) = source - .try_upcast_to_callable_with_policy(db, UpcastPolicy::from(self.relation)) - else { + let Some(callables) = source.try_upcast_to_callable_with_policy( + db, + env, + UpcastPolicy::from(self.relation), + ) else { return self.never(); }; @@ -2176,11 +2455,11 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { if let Some(context) = self.report_context() && self.should_provide_callable_upcast_context(source) - && result.is_never_satisfied(db) + && result.is_never_satisfied(db, env) { context.push(ErrorContext::InferredCallableType { source, - callable: callables.into_type(db), + callable: callables.into_type(db, env), }); } @@ -2198,7 +2477,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { if (source_subclass_ty.is_dynamic() || source_subclass_ty.is_type_var()) && !self.is_eager_assignability() => { - self.check_type_pair(db, KnownClass::Type.to_instance(db), target) + self.check_type_pair(db, KnownClass::Type.to_instance(db, env), target) } (_, Type::ProtocolInstance(target_proto)) => { @@ -2219,29 +2498,66 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { (Type::TypedDict(typed_dict), _) => { self.with_recursion_guard(db, source, target, || { let dict_value_type = if self.relation.is_assignability() { - typed_dict.assignable_dict_value_type(db) + typed_dict.assignable_dict_value_type(db, env) } else { - typed_dict.dict_value_type(db) + typed_dict.dict_value_type(db, env) }; let fallback = if let Some(value_ty) = dict_value_type { KnownClass::Dict.to_specialized_instance( db, - &[KnownClass::Str.to_instance(db), value_ty], + env, + &[KnownClass::Str.to_instance(db, env), value_ty], ) } else { KnownClass::Mapping.to_specialized_instance( db, - &[KnownClass::Str.to_instance(db), typed_dict.value_type(db)], + env, + &[ + KnownClass::Str.to_instance(db, env), + typed_dict.value_type(db, env), + ], ) }; let result = self.check_type_pair(db, fallback, target); + if let Some(context) = self.report_context() - && result.is_never_satisfied(db) + && result.is_never_satisfied(db, env) && let Type::NominalInstance(instance) = target - && instance.class(db).is_known(db, KnownClass::Dict) { - context.push(ErrorContext::TypedDictNotAssignableToDict(typed_dict)); + match instance.class(db, env).known(db) { + Some(KnownClass::Dict) => { + context + .push(ErrorContext::TypedDictNotAssignableToDict(typed_dict)); + } + Some(KnownClass::Mapping) + if typed_dict.openness(db).is_implicitly_open() => + { + let field_types = + typed_dict.items(db).values().map(|field| field.declared_ty); + let mapping_fallback_spec = &[ + KnownClass::Str.to_instance(db, env), + UnionType::from_elements(db, env, field_types), + ]; + + let closed_typeddict_fallback = KnownClass::Mapping + .to_specialized_instance(db, env, mapping_fallback_spec); + + if self + .check_type_pair(db, closed_typeddict_fallback, target) + .is_always_satisfied(db, env) + { + let context_element = + ErrorContext::OpenTypedDictNotAssignableToMapping { + source: typed_dict, + target, + }; + context.push(context_element); + } + } + _ => {} + } } + result }) } @@ -2254,7 +2570,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { (Type::LiteralValue(literal), Type::NominalInstance(instance)) if let Some(value) = literal.as_string() => { - let target_class = instance.class(db); + let target_class = instance.class(db, env); if target_class.is_known(db, KnownClass::Str) { return self.always(); @@ -2270,7 +2586,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { ); } - if let Some(sequence_class) = KnownClass::Sequence.try_to_class_literal(db) + if let Some(sequence_class) = KnownClass::Sequence.try_to_class_literal(db, env) && !sequence_class .iter_mro(db, None) .filter_map(ClassBase::into_class) @@ -2298,7 +2614,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { }; KnownClass::Sequence - .to_specialized_class_type(db, &[spec]) + .to_specialized_class_type(db, env, &[spec]) .when_some_and(db, self.constraints, |sequence| { self.check_class_pair(db, sequence, target_class) }) @@ -2311,13 +2627,13 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { (Type::LiteralValue(literal), Type::NominalInstance(instance)) if let Some(value) = literal.as_bytes() => { - let target_class = instance.class(db); + let target_class = instance.class(db, env); if target_class.is_known(db, KnownClass::Bytes) { return self.always(); } - if let Some(sequence_class) = KnownClass::Sequence.try_to_class_literal(db) + if let Some(sequence_class) = KnownClass::Sequence.try_to_class_literal(db, env) && !sequence_class .iter_mro(db, None) .filter_map(ClassBase::into_class) @@ -2344,7 +2660,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { }; KnownClass::Sequence - .to_specialized_class_type(db, &[spec]) + .to_specialized_class_type(db, env, &[spec]) .when_some_and(db, self.constraints, |sequence| { self.check_class_pair(db, sequence, target_class) }) @@ -2357,7 +2673,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { (Type::NominalInstance(_), Type::LiteralValue(literal)) if let Some(target_enum_literal) = literal.as_enum() => { - if target_enum_literal.enum_class_instance(db) != source { + if target_enum_literal.enum_class_instance(db, env) != source { self.never() } else { ConstraintSet::from_bool( @@ -2371,7 +2687,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { // most `Literal` types delegate to their instance fallbacks // unless `source` is exactly equivalent to `target` (handled above) (Type::ModuleLiteral(_) | Type::LiteralValue(_) | Type::FunctionLiteral(_), _) => { - source.literal_fallback_instance(db).when_some_and( + source.literal_fallback_instance(db, env).when_some_and( db, self.constraints, |source_instance| self.check_type_pair(db, source_instance, target), @@ -2380,14 +2696,14 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { // The same reasoning applies for these special callable types: (Type::BoundMethod(_), _) => { - self.check_type_pair(db, KnownClass::MethodType.to_instance(db), target) + self.check_type_pair(db, KnownClass::MethodType.to_instance(db, env), target) } (Type::KnownBoundMethod(method), _) => { - self.check_type_pair(db, method.class().to_instance(db), target) + self.check_type_pair(db, method.class().to_instance(db, env), target) } (Type::WrapperDescriptor(_), _) => self.check_type_pair( db, - KnownClass::WrapperDescriptorType.to_instance(db), + KnownClass::WrapperDescriptorType.to_instance(db, env), target, ), @@ -2413,12 +2729,12 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { // `TypeIs[T]` and `TypeGuard[T]` are subtypes of `bool`. (Type::TypeIs(_) | Type::TypeGuard(_), _) => { - self.check_type_pair(db, KnownClass::Bool.to_instance(db), target) + self.check_type_pair(db, KnownClass::Bool.to_instance(db, env), target) } // Function-like callables are subtypes of `FunctionType` (Type::Callable(callable), _) if callable.is_function_like(db) => { - self.check_type_pair(db, KnownClass::FunctionType.to_instance(db), target) + self.check_type_pair(db, KnownClass::FunctionType.to_instance(db, env), target) } (Type::Callable(_), _) => self.never(), @@ -2428,7 +2744,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { .check_bound_super_pair(db, source, target), (Type::BoundSuper(_), _) => { - self.check_type_pair(db, KnownClass::Super.to_instance(db), target) + self.check_type_pair(db, KnownClass::Super.to_instance(db, env), target) } (Type::SubclassOf(subclass_of), _) | (_, Type::SubclassOf(subclass_of)) @@ -2448,7 +2764,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { target_protocol, ), target => target - .into_class(db) + .into_class(db, env) .map(|target_cls| { self.check_class_pair( db, @@ -2459,7 +2775,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { .unwrap_or_else(|| { ConstraintSet::from_bool( self.constraints, - self.is_eager_assignability(), + self.relation.is_assignability(), ) }), } @@ -2493,14 +2809,14 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { target_protocol, ), target => target - .into_class(db) + .into_class(db, env) .map(|target_cls| { self.check_class_pair(db, ClassType::Generic(source_alias), target_cls) }) .unwrap_or_else(|| { ConstraintSet::from_bool( self.constraints, - self.is_eager_assignability(), + self.relation.is_assignability(), ) }), } @@ -2515,30 +2831,30 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { // `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(source_class), _) => { - self.check_type_pair(db, source_class.metaclass_instance_type(db), target) + self.check_type_pair(db, source_class.metaclass_instance_type(db, env), target) } (Type::GenericAlias(source_alias), _) => self.check_type_pair( db, - ClassType::Generic(source_alias).metaclass_instance_type(db), + ClassType::Generic(source_alias).metaclass_instance_type(db, env), target, ), // `type[Any]` is a subtype of `type[object]`, and is assignable to any `type[...]` - (Type::SubclassOf(subclass_of_ty), _) if subclass_of_ty.is_dynamic() => { - self.check_type_pair(db, KnownClass::Type.to_instance(db), target) - .or(db, self.constraints, || { - ConstraintSet::from_bool(self.constraints, self.is_eager_assignability()) - .and(db, self.constraints, || { - self.check_type_pair(db, target, KnownClass::Type.to_instance(db)) - }) - }) - } + (Type::SubclassOf(subclass_of_ty), _) if subclass_of_ty.is_dynamic() => self + .check_type_pair(db, KnownClass::Type.to_instance(db, env), target) + .or(db, self.constraints, || { + ConstraintSet::from_bool(self.constraints, self.is_eager_assignability()).and( + db, + self.constraints, + || self.check_type_pair(db, target, KnownClass::Type.to_instance(db, env)), + ) + }), // Any `type[...]` type is assignable to `type[Any]` (_, Type::SubclassOf(subclass_of_ty)) if subclass_of_ty.is_dynamic() && self.is_eager_assignability() => { - self.check_type_pair(db, source, KnownClass::Type.to_instance(db)) + self.check_type_pair(db, source, KnownClass::Type.to_instance(db, env)) } // `type[str]` (== `SubclassOf("str")` in ty) describes all possible runtime subclasses @@ -2552,9 +2868,9 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { db, subclass_of_ty .subclass_of() - .into_class(db) - .map(|source_class| source_class.metaclass_instance_type(db)) - .unwrap_or_else(|| KnownClass::Type.to_instance(db)), + .into_class(db, env) + .map(|source_class| source_class.metaclass_instance_type(db, env)) + .unwrap_or_else(|| KnownClass::Type.to_instance(db, env)), target, ), @@ -2564,7 +2880,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { // 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(source_form), _) => { - self.check_type_pair(db, source_form.instance_fallback(db), target) + self.check_type_pair(db, source_form.instance_fallback(db, env), target) } // basedpython: wrapped optionals are covariant in their inner type — @@ -2580,18 +2896,18 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { // covariance arm above) — a bare inner value is *not* one of them // (it carries no wrapper), so only `None` remains assignable here (_, Type::KnownInstance(KnownInstanceType::WrappedOptional(_))) => { - self.check_type_pair(db, source, Type::none(db)) + self.check_type_pair(db, source, Type::none(db, env)) } (Type::KnownInstance(source), _) => { - self.check_type_pair(db, source.instance_fallback(db), target) + self.check_type_pair(db, source.instance_fallback(db, env), target) } // `bool` is a subtype of `int`, because `bool` subclasses `int`, // which means that all instances of `bool` are also instances of `int` (Type::NominalInstance(source_i), Type::NominalInstance(target_i)) => self .with_recursion_guard(db, source, target, || { - self.check_nominal_instance_pair(db, source_i, target_i) + self.check_nominal_instance_pair(db, env, source_i, target_i) }), (Type::PropertyInstance(source_p), Type::PropertyInstance(target_p)) => self @@ -2600,10 +2916,10 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { }), (Type::PropertyInstance(property), _) => { - self.check_type_pair(db, property.instance_fallback(db), target) + self.check_type_pair(db, property.instance_fallback(db, env), target) } (_, Type::PropertyInstance(property)) => { - self.check_type_pair(db, source, property.instance_fallback(db)) + self.check_type_pair(db, source, property.instance_fallback(db, env)) } // Other than the special cases enumerated above, nominal-instance types are never // subtypes of any other variants @@ -2617,6 +2933,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { source: PropertyInstanceType<'db>, target: PropertyInstanceType<'db>, ) -> ConstraintSet<'db, 'c> { + let env = self.env; let check_optional_methods = |source, target| match (source, target) { (None, None) => self.always(), (Some(source), Some(target)) => self.check_type_pair(db, source, target), @@ -2625,8 +2942,8 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { self.check_type_pair( db, - source.instance_fallback(db), - target.instance_fallback(db), + source.instance_fallback(db, env), + target.instance_fallback(db, env), ) .and(db, self.constraints, || { check_optional_methods(source.getter(db), target.getter(db)).and( @@ -2643,10 +2960,13 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { }) } - pub(super) fn as_equivalence_checker(&self) -> EquivalenceChecker<'_, 'c, 'db> { + fn as_equivalence_checker(&self) -> EquivalenceChecker<'_, 'c, 'db> { EquivalenceChecker { + env: self.env, constraints: self.constraints, given: self.given, + perform_expensive_checks: self.perform_expensive_checks, + typevar_evaluation: TypeVarEvaluation::Eager, relation_visitor: self.relation_visitor, disjointness_visitor: self.disjointness_visitor, signature_relation_visitor: self.signature_relation_visitor, @@ -2656,9 +2976,11 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { pub(super) fn as_disjointness_checker(&self) -> DisjointnessChecker<'_, 'c, 'db> { DisjointnessChecker { + env: self.env, constraints: self.constraints, inferable: self.inferable, given: self.given, + perform_expensive_checks: self.perform_expensive_checks, relation_visitor: self.relation_visitor, disjointness_visitor: self.disjointness_visitor, signature_relation_visitor: self.signature_relation_visitor, @@ -2695,8 +3017,11 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { } pub(super) struct EquivalenceChecker<'a, 'c, 'db> { + pub(super) env: &'a ProgramEnvironment<'db>, pub(super) constraints: &'c ConstraintSetBuilder<'db>, given: ConstraintSet<'db, 'c>, + perform_expensive_checks: bool, + typevar_evaluation: TypeVarEvaluation, // N.B. these fields are private to reduce the risk of // "double-visiting" a given pair of types. You should @@ -2707,21 +3032,23 @@ pub(super) struct EquivalenceChecker<'a, 'c, 'db> { relation_visitor: &'a HasRelationToVisitor<'db, 'c>, disjointness_visitor: &'a IsDisjointVisitor<'db, 'c>, signature_relation_visitor: &'a SignatureRelationVisitor<'db>, - materialization_visitor: &'a ApplyTypeMappingVisitor<'db>, + materialization_visitor: &'a ApplyTypeMappingVisitor<'a, 'db>, } impl<'c, 'db> EquivalenceChecker<'_, 'c, 'db> { fn as_relation_checker<'a>( &'a self, - materialization_visitor: &'a ApplyTypeMappingVisitor<'db>, + materialization_visitor: &'a ApplyTypeMappingVisitor<'a, 'db>, ) -> TypeRelationChecker<'a, 'c, 'db> { TypeRelationChecker { + env: self.env, relation: TypeRelation::Redundancy { pure: true }, - typevar_evaluation: TypeVarEvaluation::Eager, + typevar_evaluation: self.typevar_evaluation, constraints: self.constraints, context_tree: None, given: self.given, - inferable: InferableTypeVars::None, + perform_expensive_checks: self.perform_expensive_checks, + inferable: TypeVarSet::None, relation_visitor: self.relation_visitor, disjointness_visitor: self.disjointness_visitor, signature_relation_visitor: self.signature_relation_visitor, @@ -2760,9 +3087,11 @@ impl<'c, 'db> EquivalenceChecker<'_, 'c, 'db> { } pub(super) struct DisjointnessChecker<'a, 'c, 'db> { + pub(super) env: &'a ProgramEnvironment<'db>, pub(super) constraints: &'c ConstraintSetBuilder<'db>, - pub(super) inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, given: ConstraintSet<'db, 'c>, + perform_expensive_checks: bool, // N.B. these fields are private to reduce the risk of // "double-visiting" a given pair of types. You should @@ -2773,22 +3102,25 @@ pub(super) struct DisjointnessChecker<'a, 'c, 'db> { disjointness_visitor: &'a IsDisjointVisitor<'db, 'c>, relation_visitor: &'a HasRelationToVisitor<'db, 'c>, signature_relation_visitor: &'a SignatureRelationVisitor<'db>, - materialization_visitor: &'a ApplyTypeMappingVisitor<'db>, + materialization_visitor: &'a ApplyTypeMappingVisitor<'a, 'db>, } impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { pub(super) fn new( + env: &'a ProgramEnvironment<'db>, constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, relation_visitor: &'a HasRelationToVisitor<'db, 'c>, disjointness_visitor: &'a IsDisjointVisitor<'db, 'c>, signature_relation_visitor: &'a SignatureRelationVisitor<'db>, - materialization_visitor: &'a ApplyTypeMappingVisitor<'db>, + materialization_visitor: &'a ApplyTypeMappingVisitor<'a, 'db>, ) -> Self { Self { + env, constraints, inferable, given: ConstraintSet::from_bool(constraints, false), + perform_expensive_checks: true, disjointness_visitor, relation_visitor, signature_relation_visitor, @@ -2801,12 +3133,14 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { relation: TypeRelation, ) -> TypeRelationChecker<'_, 'c, 'db> { TypeRelationChecker { + env: self.env, relation, typevar_evaluation: TypeVarEvaluation::Eager, constraints: self.constraints, inferable: self.inferable, context_tree: None, given: self.given, + perform_expensive_checks: self.perform_expensive_checks, relation_visitor: self.relation_visitor, disjointness_visitor: self.disjointness_visitor, signature_relation_visitor: self.signature_relation_visitor, @@ -2814,10 +3148,13 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { } } - pub(super) fn as_equivalence_checker(&self) -> EquivalenceChecker<'_, 'c, 'db> { + fn as_equivalence_checker(&self) -> EquivalenceChecker<'_, 'c, 'db> { EquivalenceChecker { + env: self.env, constraints: self.constraints, given: self.given, + perform_expensive_checks: self.perform_expensive_checks, + typevar_evaluation: TypeVarEvaluation::Eager, relation_visitor: self.relation_visitor, disjointness_visitor: self.disjointness_visitor, signature_relation_visitor: self.signature_relation_visitor, @@ -2863,12 +3200,13 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { protocol: ProtocolInstanceType<'db>, other: Type<'db>, ) -> ConstraintSet<'db, 'c> { + let env = self.env; protocol .interface(db) .members(db) .when_any(db, self.constraints, |member| { other - .member(db, member.name()) + .member(db, env, member.name()) .place .ignore_possibly_undefined() .when_none_or(db, self.constraints, |attribute_type| { @@ -2878,6 +3216,14 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { db, &member, other, ) }) + .or(db, self.constraints, || { + ConstraintSet::from_bool( + self.constraints, + member.has_incompatible_class_variable_declaration( + db, env, other, + ), + ) + }) }) }) } @@ -2928,6 +3274,21 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { left: Type<'db>, right: Type<'db>, ) -> ConstraintSet<'db, 'c> { + /// This lets us clearly mark below which match arms require a non-trivial amount of work + /// to calculate, without sacrificing match guard exhaustiveness checks. If we are not + /// performing expensive checks, then we will conservatively report that the two types are + /// not disjoint. + fn nontrivial_check<'db, 'c>( + checker: &DisjointnessChecker<'_, 'c, 'db>, + check: impl FnOnce() -> ConstraintSet<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { + if checker.perform_expensive_checks { + check() + } else { + checker.never() + } + } + if let Some(left) = left.materialized_divergent_fallback() { return self.check_type_pair(db, left, right); } @@ -2936,25 +3297,27 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { return self.check_type_pair(db, left, right); } + let env = self.env; + match (left, right) { (Type::Never, _) | (_, Type::Never) => self.always(), (Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => self.never(), (Type::Divergent(_), _) | (_, Type::Divergent(_)) => self.never(), - (Type::TypeAlias(alias), _) => { + (Type::TypeAlias(alias), _) => nontrivial_check(self, || { let left_alias_ty = alias.value_type(db); self.with_recursion_guard(db, left, right, || { self.check_type_pair(db, left_alias_ty, right) }) - } + }), - (_, Type::TypeAlias(alias)) => { + (_, Type::TypeAlias(alias)) => nontrivial_check(self, || { let right_alias_ty = alias.value_type(db); self.with_recursion_guard(db, left, right, || { self.check_type_pair(db, left, right_alias_ty) }) - } + }), (Type::Overlapping(overlapping), _) => { self.check_type_pair(db, overlapping.type_argument(db), right) @@ -2991,22 +3354,32 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { self.check_type_pair(db, left, restricted.value_type(db)) } - (Type::Deferred(deferred), _) => self.check_type_pair(db, deferred.reduced(db), right), - - (_, Type::Deferred(deferred)) => self.check_type_pair(db, left, deferred.reduced(db)), - - (Type::EnumComplement(complement), other) => { - self.check_type_pair(db, complement.remaining_literal_union(db), other) + (Type::Deferred(deferred), _) => { + self.check_type_pair(db, deferred.reduced(db, env), right) } - (other, Type::EnumComplement(complement)) => { - self.check_type_pair(db, other, complement.remaining_literal_union(db)) + (_, Type::Deferred(deferred)) => { + self.check_type_pair(db, left, deferred.reduced(db, env)) } + (Type::EnumComplement(complement), other) => nontrivial_check(self, || { + self.check_type_pair(db, complement.remaining_literal_union(db, env), other) + }), + + (other, Type::EnumComplement(complement)) => nontrivial_check(self, || { + self.check_type_pair(db, other, complement.remaining_literal_union(db, env)) + }), + // `type[T]` and `TypeForm[S]` overlap whenever their represented instance types do. (Type::SubclassOf(subclass_of), Type::TypeForm(typeform)) | (Type::TypeForm(typeform), Type::SubclassOf(subclass_of)) => { - self.check_type_pair(db, subclass_of.to_instance(db), typeform.type_argument(db)) + nontrivial_check(self, || { + self.check_type_pair( + db, + subclass_of.to_instance(db, env), + typeform.type_argument(db), + ) + }) } // `type[T]` is disjoint from a callable or protocol instance if its upper bound or constraints are. @@ -3019,18 +3392,22 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { Type::SubclassOf(subclass_of), ) if let Some(type_var) = subclass_of .subclass_of() - .with_transposed_type_var(db) + .with_transposed_type_var(db, env) .into_type_var() => { - self.check_type_pair(db, Type::TypeVar(type_var), other) + nontrivial_check(self, || { + self.check_type_pair(db, Type::TypeVar(type_var), other) + }) } // `type[T]` is disjoint from a class object `A` if every instance of `T` is disjoint from an instance of `A`. (Type::SubclassOf(subclass_of), other) | (other, Type::SubclassOf(subclass_of)) if let Some(type_var) = subclass_of.into_type_var() - && let Some(instance) = other.to_instance_approximation(db) => + && let Some(instance) = other.to_instance_approximation(db, env) => { - self.check_type_pair(db, Type::TypeVar(type_var), instance) + nontrivial_check(self, || { + self.check_type_pair(db, Type::TypeVar(type_var), instance) + }) } // A typevar is never disjoint from itself, since all occurrences of the typevar must @@ -3059,30 +3436,36 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { (Type::TypeVar(tvar), other) | (other, Type::TypeVar(tvar)) if !tvar.is_inferable(db, self.inferable) => { - match tvar.typevar(db).bound_or_constraints(db) { - None => self.never(), - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - self.check_type_pair(db, bound, other) - } - Some(TypeVarBoundOrConstraints::Constraints(typevar_constraints)) => { - typevar_constraints.elements(db).iter().when_all( - db, - self.constraints, - |constraint| self.check_type_pair(db, *constraint, other), - ) + nontrivial_check(self, || { + match tvar.typevar(db).bound_or_constraints(db, env) { + None => self.never(), + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + self.check_type_pair(db, bound, other) + } + Some(TypeVarBoundOrConstraints::Constraints(typevar_constraints)) => { + typevar_constraints.elements(db).iter().when_all( + db, + self.constraints, + |constraint| self.check_type_pair(db, *constraint, other), + ) + } } - } + }) } // TODO: Infer specializations here (Type::TypeVar(_), _) | (_, Type::TypeVar(_)) => self.never(), - (Type::Union(union), other) | (other, Type::Union(union)) => union - .elements(db) - .iter() - .when_all(db, self.constraints, |e| { - self.check_type_pair(db, *e, other) - }), + (Type::Union(union), other) | (other, Type::Union(union)) => { + nontrivial_check(self, || { + union + .elements(db) + .iter() + .when_all(db, self.constraints, |e| { + self.check_type_pair(db, *e, other) + }) + }) + } // An unsafe union overlaps a type as soon as *one* of its materializations does, // so it is disjoint only when every materialization is. @@ -3099,44 +3482,49 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { // 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(left_intersection), Type::Intersection(right_intersection)) => { - if let Some(alternatives) = left_intersection.finite_alternative_union(db) { - self.check_type_pair(db, alternatives, right) - } else if let Some(alternatives) = right_intersection.finite_alternative_union(db) { - self.check_type_pair(db, left, alternatives) - } else { - self.with_recursion_guard(db, left, right, || { - left_intersection - .positive(db) - .iter() - .when_any(db, self.constraints, |&pos_ty| { - self.check_type_pair(db, pos_ty, right) - }) - .or(db, self.constraints, || { - right_intersection.positive(db).iter().when_any( - db, - self.constraints, - |&pos_ty| self.check_type_pair(db, pos_ty, left), - ) - }) - }) - } + nontrivial_check(self, || { + if let Some(alternatives) = left_intersection.finite_alternative_union(db, env) + { + self.check_type_pair(db, alternatives, right) + } else if let Some(alternatives) = + right_intersection.finite_alternative_union(db, env) + { + self.check_type_pair(db, left, alternatives) + } else { + self.with_recursion_guard(db, left, right, || { + left_intersection + .positive(db) + .iter() + .when_any(db, self.constraints, |&pos_ty| { + self.check_type_pair(db, pos_ty, right) + }) + .or(db, self.constraints, || { + right_intersection.positive(db).iter().when_any( + db, + self.constraints, + |&pos_ty| self.check_type_pair(db, pos_ty, left), + ) + }) + }) + } + }) } - (Type::Intersection(intersection), other) => { - if let Some(alternatives) = intersection.finite_alternative_union(db) { + (Type::Intersection(intersection), other) => nontrivial_check(self, || { + if let Some(alternatives) = intersection.finite_alternative_union(db, env) { self.check_type_pair(db, alternatives, other) } else { self.check_intersection_pair_via_elements(db, left, right, intersection, other) } - } + }), - (other, Type::Intersection(intersection)) => { - if let Some(alternatives) = intersection.finite_alternative_union(db) { + (other, Type::Intersection(intersection)) => nontrivial_check(self, || { + if let Some(alternatives) = intersection.finite_alternative_union(db, env) { self.check_type_pair(db, other, alternatives) } else { self.check_intersection_pair_via_elements(db, left, right, intersection, other) } - } + }), (Type::LiteralValue(left), Type::LiteralValue(right)) if left.is_literal_string() && right.is_literal_string() @@ -3158,7 +3546,7 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { } (Type::PropertyInstance(left), Type::PropertyInstance(right)) => { - self.check_property_instance_pair(db, left, right) + nontrivial_check(self, || self.check_property_instance_pair(db, left, right)) } ( @@ -3172,7 +3560,7 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { | ( Type::KnownBoundMethod(KnownBoundMethodType::PropertyDunderDelete(left)), Type::KnownBoundMethod(KnownBoundMethodType::PropertyDunderDelete(right)), - ) => self.check_property_instance_pair(db, left, right), + ) => nontrivial_check(self, || self.check_property_instance_pair(db, left, right)), ( Type::KnownInstance(KnownInstanceType::Sentinel(left_sentinel)), @@ -3182,10 +3570,9 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { !left_sentinel.is_same_sentinel(db, right_sentinel), ), - // any single-valued type is disjoint from another single-valued type - // iff the two types are nonequal + // These types are disjoint whenever their represented objects differ. ( - // note `LiteralString` is not single-valued, but we handle the special case above + // `LiteralString` can represent different strings and is handled above. left @ (Type::FunctionLiteral(..) | Type::KnownBoundMethod(..) | Type::WrapperDescriptor(..) @@ -3224,37 +3611,50 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { (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`. - ConstraintSet::from_bool(self.constraints, ty.bool(db).is_always_false()) + nontrivial_check(self, || { + ConstraintSet::from_bool(self.constraints, ty.bool(db, env).is_always_false()) + }) } (Type::AlwaysFalsy, ty) | (ty, Type::AlwaysFalsy) => { // Similarly, they are only disjoint if `ty.bool() == AlwaysTrue`. - ConstraintSet::from_bool(self.constraints, ty.bool(db).is_always_true()) + nontrivial_check(self, || { + ConstraintSet::from_bool(self.constraints, ty.bool(db, env).is_always_true()) + }) } - (Type::ProtocolInstance(left_proto), Type::ProtocolInstance(right_proto)) => self - .with_recursion_guard(db, left, right, || { - self.check_protocol_instance_pair(db, left_proto, right_proto) - }), + (Type::ProtocolInstance(left_proto), Type::ProtocolInstance(right_proto)) => { + nontrivial_check(self, || { + self.with_recursion_guard(db, left, right, || { + self.check_protocol_instance_pair(db, left_proto, right_proto) + }) + }) + } (Type::ProtocolInstance(protocol), Type::SpecialForm(special_form)) - | (Type::SpecialForm(special_form), Type::ProtocolInstance(protocol)) => self - .with_recursion_guard(db, left, right, || { - self.any_protocol_members_absent_or_disjoint( - db, - protocol, - special_form.instance_fallback(db), - ) - }), + | (Type::SpecialForm(special_form), Type::ProtocolInstance(protocol)) => { + nontrivial_check(self, || { + self.with_recursion_guard(db, left, right, || { + self.any_protocol_members_absent_or_disjoint( + db, + protocol, + special_form.instance_fallback(db, env), + ) + }) + }) + } (Type::ProtocolInstance(protocol), Type::KnownInstance(known_instance)) - | (Type::KnownInstance(known_instance), Type::ProtocolInstance(protocol)) => self - .with_recursion_guard(db, left, right, || { - self.any_protocol_members_absent_or_disjoint( - db, - protocol, - known_instance.instance_fallback(db), - ) - }), + | (Type::KnownInstance(known_instance), Type::ProtocolInstance(protocol)) => { + nontrivial_check(self, || { + self.with_recursion_guard(db, left, right, || { + self.any_protocol_members_absent_or_disjoint( + db, + protocol, + known_instance.instance_fallback(db, env), + ) + }) + }) + } // The absence of a protocol member on one of these types guarantees // that the type will be disjoint from the protocol, @@ -3298,8 +3698,10 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { | Type::FunctionLiteral(..) | Type::ModuleLiteral(..) | Type::GenericAlias(..)), - ) => self.with_recursion_guard(db, left, right, || { - self.any_protocol_members_absent_or_disjoint(db, protocol, ty) + ) => nontrivial_check(self, || { + self.with_recursion_guard(db, left, right, || { + self.any_protocol_members_absent_or_disjoint(db, protocol, ty) + }) }), // This is the same as the branch above -- @@ -3307,25 +3709,27 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { // () (Type::ProtocolInstance(protocol), Type::NominalInstance(nominal)) | (Type::NominalInstance(nominal), Type::ProtocolInstance(protocol)) - if nominal.class(db).is_final(db) => + if self.perform_expensive_checks && nominal.class(db, env).is_final(db) => { - self.with_recursion_guard(db, left, right, || { - self.any_protocol_members_absent_or_disjoint( - db, - protocol, - Type::NominalInstance(nominal), - ) + nontrivial_check(self, || { + self.with_recursion_guard(db, left, right, || { + self.any_protocol_members_absent_or_disjoint( + db, + protocol, + Type::NominalInstance(nominal), + ) + }) }) } (Type::ProtocolInstance(protocol), other) - | (other, Type::ProtocolInstance(protocol)) => { + | (other, Type::ProtocolInstance(protocol)) => nontrivial_check(self, || { self.with_recursion_guard(db, left, right, || { protocol .interface(db) .members(db) .when_any(db, self.constraints, |member| { - match other.member(db, member.name()).place { + match other.member(db, env, member.name()).place { Place::Defined(DefinedPlace { ty: attribute_type, .. }) => self.protocol_member_has_disjoint_type_from_ty( @@ -3337,7 +3741,7 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { } }) }) - } + }), (Type::SubclassOf(subclass_of_ty), _) | (_, Type::SubclassOf(subclass_of_ty)) if subclass_of_ty.is_type_var() => @@ -3351,35 +3755,49 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { left_alias.origin(db) != right_alias.origin(db), ) .or(db, self.constraints, || { - self.check_specialization_pair( - db, - left_alias.specialization(db), - right_alias.specialization(db), - ) + nontrivial_check(self, || { + self.check_specialization_pair( + db, + env, + left_alias.specialization(db), + right_alias.specialization(db), + ) + }) }) } (Type::ClassLiteral(class), Type::GenericAlias(alias_b)) - | (Type::GenericAlias(alias_b), Type::ClassLiteral(class)) => class - .default_specialization(db) - .into_generic_alias() - .when_none_or(db, self.constraints, |alias| { - self.check_type_pair(db, Type::GenericAlias(alias_b), Type::GenericAlias(alias)) - }), + | (Type::GenericAlias(alias_b), Type::ClassLiteral(class)) => { + nontrivial_check(self, || { + class + .default_specialization(db) + .into_generic_alias() + .when_none_or(db, self.constraints, |alias| { + self.check_type_pair( + db, + Type::GenericAlias(alias_b), + Type::GenericAlias(alias), + ) + }) + }) + } (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(_) => self.never(), SubclassOfInner::Protocol(_) => self.never(), - SubclassOfInner::Class(class_a) => ConstraintSet::from_bool( - self.constraints, - !class_a.could_exist_in_mro_of_with_disjointness_checker( - db, - ClassType::NonGeneric(class_b), - self, - ), - ), + SubclassOfInner::Class(class_a) => nontrivial_check(self, || { + ConstraintSet::from_bool( + self.constraints, + !class_a.could_exist_in_mro_of_with_disjointness_checker( + db, + env, + ClassType::NonGeneric(class_b), + self, + ), + ) + }), SubclassOfInner::TypeVar(_) => unreachable!(), } } @@ -3389,110 +3807,149 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { match subclass_of_ty.subclass_of() { SubclassOfInner::Dynamic(_) => self.never(), SubclassOfInner::Protocol(_) => self.never(), - SubclassOfInner::Class(class_a) => ConstraintSet::from_bool( - self.constraints, - !class_a.could_exist_in_mro_of_with_disjointness_checker( - db, - ClassType::Generic(alias_b), - self, - ), - ), + SubclassOfInner::Class(class_a) => nontrivial_check(self, || { + ConstraintSet::from_bool( + self.constraints, + !class_a.could_exist_in_mro_of_with_disjointness_checker( + db, + env, + ClassType::Generic(alias_b), + self, + ), + ) + }), SubclassOfInner::TypeVar(_) => unreachable!(), } } (Type::SubclassOf(left), Type::SubclassOf(right)) => { - self.check_subclassof_pair(db, left, right) + nontrivial_check(self, || self.check_subclassof_pair(db, left, 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(_) => { - self.check_type_pair(db, KnownClass::Type.to_instance(db), other) - } - SubclassOfInner::Class(class) => { - self.check_type_pair(db, class.metaclass_instance_type(db), other) - } - SubclassOfInner::Protocol(_) => { - self.check_type_pair(db, KnownClass::Type.to_instance(db), other) - } - SubclassOfInner::TypeVar(_) => unreachable!(), - }, + | (other, Type::SubclassOf(subclass_of_ty)) => { + nontrivial_check(self, || match subclass_of_ty.subclass_of() { + SubclassOfInner::Dynamic(_) => { + self.check_type_pair(db, KnownClass::Type.to_instance(db, env), other) + } + SubclassOfInner::Class(class) => { + self.check_type_pair(db, class.metaclass_instance_type(db, env), other) + } + SubclassOfInner::Protocol(_) => { + self.check_type_pair(db, KnownClass::Type.to_instance(db, env), other) + } + SubclassOfInner::TypeVar(_) => unreachable!(), + }) + } (Type::SpecialForm(special_form), Type::NominalInstance(instance)) | (Type::NominalInstance(instance), Type::SpecialForm(special_form)) => { - ConstraintSet::from_bool( - self.constraints, - !special_form.is_instance_of(db, instance.class(db)), - ) + nontrivial_check(self, || { + ConstraintSet::from_bool( + self.constraints, + !special_form.is_instance_of(db, env, instance.class(db, env)), + ) + }) } (Type::KnownInstance(known_instance), Type::NominalInstance(instance)) | (Type::NominalInstance(instance), Type::KnownInstance(known_instance)) => { - ConstraintSet::from_bool( - self.constraints, - !known_instance.is_instance_of(db, instance.class(db)), - ) + nontrivial_check(self, || { + ConstraintSet::from_bool( + self.constraints, + !known_instance.is_instance_of(db, env, instance.class(db, env)), + ) + }) } (Type::LiteralValue(literal), Type::NominalInstance(instance)) | (Type::NominalInstance(instance), Type::LiteralValue(literal)) => { - let positive_relation_holds = match literal.kind() { - LiteralValueTypeKind::Int(_) => { - KnownClass::Int.when_subclass_of(db, instance.class(db), self.constraints) - } - LiteralValueTypeKind::Bool(_) => { - KnownClass::Bool.when_subclass_of(db, instance.class(db), self.constraints) - } - // basedpython: single-grapheme literals (and `LiteralString`, - // which includes them) also inhabit `Character`, a proper subclass - // of `str` - LiteralValueTypeKind::LiteralString => KnownClass::Character.when_subclass_of( - db, - instance.class(db), - self.constraints, - ), - LiteralValueTypeKind::String(value) => { - let literal_class = if is_single_grapheme(value.value(db)) { - KnownClass::Character - } else { - KnownClass::Str - }; - literal_class.when_subclass_of(db, instance.class(db), self.constraints) - } - LiteralValueTypeKind::Bytes(_) => { - KnownClass::Bytes.when_subclass_of(db, instance.class(db), self.constraints) - } - LiteralValueTypeKind::Enum(enum_literal) => self - .as_relation_checker(TypeRelation::Subtyping) - .check_type_pair( + nontrivial_check(self, || { + let positive_relation_holds = match literal.kind() { + LiteralValueTypeKind::Int(_) => KnownClass::Int.when_subclass_of( db, - enum_literal.enum_class_instance(db), - Type::NominalInstance(instance), + env, + instance.class(db, env), + self.constraints, ), - LiteralValueTypeKind::Float(_) => { - KnownClass::Float.when_subclass_of(db, instance.class(db), self.constraints) - } - LiteralValueTypeKind::Complex(_) => KnownClass::Complex.when_subclass_of( - db, - instance.class(db), - self.constraints, - ), - }; - positive_relation_holds.negate(db, self.constraints) + LiteralValueTypeKind::Float(_) => KnownClass::Float.when_subclass_of( + db, + env, + instance.class(db, env), + self.constraints, + ), + LiteralValueTypeKind::Complex(_) => KnownClass::Complex.when_subclass_of( + db, + env, + instance.class(db, env), + self.constraints, + ), + LiteralValueTypeKind::Bool(_) => KnownClass::Bool.when_subclass_of( + db, + env, + instance.class(db, env), + self.constraints, + ), + // basedpython: single-grapheme literals (and `LiteralString`, + // which includes them) also inhabit `Character`, a proper subclass + // of `str` + LiteralValueTypeKind::LiteralString => KnownClass::Character + .when_subclass_of(db, env, instance.class(db, env), self.constraints), + LiteralValueTypeKind::String(value) => { + let literal_class = if is_single_grapheme(value.value(db)) { + KnownClass::Character + } else { + KnownClass::Str + }; + literal_class.when_subclass_of( + db, + env, + instance.class(db, env), + self.constraints, + ) + } + LiteralValueTypeKind::Bytes(_) => KnownClass::Bytes.when_subclass_of( + db, + env, + instance.class(db, env), + self.constraints, + ), + LiteralValueTypeKind::Enum(enum_literal) => self + .as_relation_checker(TypeRelation::Subtyping) + .check_type_pair( + db, + enum_literal.enum_class_instance(db, env), + Type::NominalInstance(instance), + ), + }; + positive_relation_holds.negate(db, self.constraints) + }) } (Type::TypeIs(_) | Type::TypeGuard(_), Type::NominalInstance(instance)) | (Type::NominalInstance(instance), Type::TypeIs(_) | Type::TypeGuard(_)) => { // A boolean literal must be an instance of exactly `bool` // (it cannot be an instance of a `bool` subclass) - KnownClass::Bool - .when_subclass_of(db, instance.class(db), self.constraints) - .negate(db, self.constraints) + nontrivial_check(self, || { + KnownClass::Bool + .when_subclass_of(db, env, instance.class(db, env), self.constraints) + .negate(db, self.constraints) + }) } + ( + Type::NewTypeInstance(newtype), + other @ (Type::LiteralValue(_) | Type::TypeIs(_) | Type::TypeGuard(_)), + ) + | ( + other @ (Type::LiteralValue(_) | Type::TypeIs(_) | Type::TypeGuard(_)), + Type::NewTypeInstance(newtype), + ) => nontrivial_check(self, || { + self.check_type_pair(db, newtype.concrete_base_type(db), other) + }), + (Type::TypeIs(_) | Type::TypeGuard(_), _) | (_, Type::TypeIs(_) | Type::TypeGuard(_)) => self.always(), @@ -3502,33 +3959,43 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { // unless the type expressing "all instances of `Z`" is a subtype of of `Y`, // where `Z` is `X`'s metaclass. (Type::ClassLiteral(class), Type::NominalInstance(instance)) - | (Type::NominalInstance(instance), Type::ClassLiteral(class)) => class - .metaclass_instance_type(db) - .when_subtype_of( - db, - Type::NominalInstance(instance), - self.constraints, - self.inferable, - ) - .negate(db, self.constraints), + | (Type::NominalInstance(instance), Type::ClassLiteral(class)) => { + nontrivial_check(self, || { + class + .metaclass_instance_type(db, env) + .when_subtype_of( + db, + env, + Type::NominalInstance(instance), + self.constraints, + self.inferable, + ) + .negate(db, self.constraints) + }) + } (Type::GenericAlias(alias), Type::NominalInstance(instance)) - | (Type::NominalInstance(instance), Type::GenericAlias(alias)) => self - .as_relation_checker(TypeRelation::Subtyping) - .check_type_pair( - db, - ClassType::Generic(alias).metaclass_instance_type(db), - Type::NominalInstance(instance), - ) - .negate(db, self.constraints), + | (Type::NominalInstance(instance), Type::GenericAlias(alias)) => { + nontrivial_check(self, || { + self.as_relation_checker(TypeRelation::Subtyping) + .check_type_pair( + db, + ClassType::Generic(alias).metaclass_instance_type(db, env), + Type::NominalInstance(instance), + ) + .negate(db, self.constraints) + }) + } (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 - .when_subclass_of(db, instance.class(db), self.constraints) - .negate(db, self.constraints) + nontrivial_check(self, || { + KnownClass::FunctionType + .when_subclass_of(db, env, instance.class(db, env), self.constraints) + .negate(db, self.constraints) + }) } // A `BoundMethod` type includes instances of the same method bound to a @@ -3542,57 +4009,71 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { // method name would show up on both sides of this check. However for // completeness, if we're ever comparing `BoundMethod` types with different // method names, then they're clearly disjoint. - self.always() - } else if a_function != b_function - && a_function.has_known_decorator(db, FunctionDecorators::FINAL) - && b_function.has_known_decorator(db, FunctionDecorators::FINAL) - { - // If *both* methods are `@final` (and they're not literally the same - // definition), they must be disjoint. - // - // Note that we can't establish disjointness when only one side is `@final`, - // because we have to worry about cases like this: - // - // ``` - // class A: - // def f(self): ... - // class B: - // @final - // def f(self): ... - // # Valid in this order, though `C(A, B)` would be invalid. - // class C(B, A): ... - // ``` - self.always() - } else { - // The names match, so `BoundMethod` disjointness depends on whether the bound - // self types are disjoint. Note that this can produce confusing results in the - // face of Liskov violations. For example: - // ``` - // class A: - // def f(self) -> int: ... - // class B: - // def f(self) -> str: ... - // def _(x: Intersection[A, B]): - // x.f() - // ``` - // `class C(A, B)` could inhabit that intersection, but `int` and `str` are - // disjoint, so the type of `x.f()` there is going to be inferred as `Never`. - // That's probably not correct in practice, but the right way to address it is - // to emit a diagnostic on the definition of `C.f`. - self.check_type_pair(db, a.self_instance(db), b.self_instance(db)) + return self.always(); } + + nontrivial_check(self, || { + if a_function != b_function + && a_function.has_known_decorator(db, FunctionDecorators::FINAL) + && b_function.has_known_decorator(db, FunctionDecorators::FINAL) + { + // If *both* methods are `@final` (and they're not literally the same + // definition), they must be disjoint. + // + // Note that we can't establish disjointness when only one side is `@final`, + // because we have to worry about cases like this: + // + // ``` + // class A: + // def f(self): ... + // class B: + // @final + // def f(self): ... + // # Valid in this order, though `C(A, B)` would be invalid. + // class C(B, A): ... + // ``` + self.always() + } else { + // The names match, so `BoundMethod` disjointness depends on whether the bound + // self types are disjoint. Note that this can produce confusing results in the + // face of Liskov violations. For example: + // ``` + // class A: + // def f(self) -> int: ... + // class B: + // def f(self) -> str: ... + // def _(x: Intersection[A, B]): + // x.f() + // ``` + // `class C(A, B)` could inhabit that intersection, but `int` and `str` are + // disjoint, so the type of `x.f()` there is going to be inferred as `Never`. + // That's probably not correct in practice, but the right way to address it is + // to emit a diagnostic on the definition of `C.f`. + self.check_type_pair(db, a.self_instance(db), b.self_instance(db)) + } + }) } (Type::BoundMethod(_), other) | (other, Type::BoundMethod(_)) => { - self.check_type_pair(db, KnownClass::MethodType.to_instance(db), other) + nontrivial_check(self, || { + self.check_type_pair(db, KnownClass::MethodType.to_instance(db, env), other) + }) } (Type::KnownBoundMethod(method), other) | (other, Type::KnownBoundMethod(method)) => { - self.check_type_pair(db, method.class().to_instance(db), other) + nontrivial_check(self, || { + self.check_type_pair(db, method.class().to_instance(db, env), other) + }) } (Type::WrapperDescriptor(_), other) | (other, Type::WrapperDescriptor(_)) => { - self.check_type_pair(db, KnownClass::WrapperDescriptorType.to_instance(db), other) + nontrivial_check(self, || { + self.check_type_pair( + db, + KnownClass::WrapperDescriptorType.to_instance(db, env), + other, + ) + }) } (Type::Callable(_) | Type::FunctionLiteral(_), Type::Callable(_)) @@ -3619,15 +4100,28 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { | ( Type::NominalInstance(nominal), Type::Callable(_) | Type::DataclassDecorator(_) | Type::DataclassTransformer(_), - ) if nominal.class(db).is_final(db) => Type::NominalInstance(nominal) - .member_lookup_with_policy(db, "__call__", MemberLookupPolicy::NO_INSTANCE_FALLBACK) - .place - .ignore_possibly_undefined() - .when_none_or(db, self.constraints, |dunder_call| { - self.as_relation_checker(TypeRelation::Assignability) - .check_type_pair(db, dunder_call, Type::Callable(CallableType::unknown(db))) - .negate(db, self.constraints) - }), + ) if self.perform_expensive_checks && nominal.class(db, env).is_final(db) => { + nontrivial_check(self, || { + Type::NominalInstance(nominal) + .member_lookup_with_policy( + db, + env, + "__call__", + MemberLookupPolicy::NO_INSTANCE_FALLBACK, + ) + .place + .ignore_possibly_undefined() + .when_none_or(db, self.constraints, |dunder_call| { + self.as_relation_checker(TypeRelation::Assignability) + .check_type_pair( + db, + dunder_call, + Type::Callable(CallableType::unknown(db)), + ) + .negate(db, self.constraints) + }) + }) + } ( Type::Callable(_) | Type::DataclassDecorator(_) | Type::DataclassTransformer(_), @@ -3644,60 +4138,75 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { (Type::ModuleLiteral(..), Type::NominalInstance(instance)) | (Type::NominalInstance(instance), Type::ModuleLiteral(..)) => { // Modules *can* actually be instances of `ModuleType` subclasses - self.check_type_pair( - db, - Type::NominalInstance(instance), - KnownClass::ModuleType.to_instance(db), - ) + nontrivial_check(self, || { + self.check_type_pair( + db, + Type::NominalInstance(instance), + KnownClass::ModuleType.to_instance(db, env), + ) + }) } - (Type::NominalInstance(left_i), Type::NominalInstance(right_i)) => self - .with_recursion_guard(db, left, right, || { - self.check_nominal_instance_pair(db, left_i, right_i) - }), + (Type::NominalInstance(left_i), Type::NominalInstance(right_i)) => { + nontrivial_check(self, || { + self.with_recursion_guard(db, left, right, || { + self.check_nominal_instance_pair(db, left_i, right_i) + }) + }) + } (Type::NewTypeInstance(left), Type::NewTypeInstance(right)) => { - self.check_newtype_pair(db, left, right) + nontrivial_check(self, || self.check_newtype_pair(db, left, right)) } (Type::NewTypeInstance(newtype), other) | (other, Type::NewTypeInstance(newtype)) => { - self.check_type_pair(db, newtype.concrete_base_type(db), other) + nontrivial_check(self, || { + self.check_type_pair(db, newtype.concrete_base_type(db), other) + }) } (Type::PropertyInstance(property), other) - | (other, Type::PropertyInstance(property)) => { - self.check_type_pair(db, property.instance_fallback(db), other) - } + | (other, Type::PropertyInstance(property)) => nontrivial_check(self, || { + self.check_type_pair(db, property.instance_fallback(db, env), other) + }), - (Type::BoundSuper(left), Type::BoundSuper(right)) => self - .as_equivalence_checker() - .check_bound_super_pair(db, left, right) - .negate(db, self.constraints), + (Type::BoundSuper(left), Type::BoundSuper(right)) => nontrivial_check(self, || { + self.as_equivalence_checker() + .check_bound_super_pair(db, left, right) + .negate(db, self.constraints) + }), (Type::BoundSuper(_), other) | (other, Type::BoundSuper(_)) => { - self.check_type_pair(db, KnownClass::Super.to_instance(db), other) + nontrivial_check(self, || { + self.check_type_pair(db, KnownClass::Super.to_instance(db, env), other) + }) } (Type::TypeForm(_), _) | (_, Type::TypeForm(_)) => self.never(), (Type::GenericAlias(_), _) | (_, Type::GenericAlias(_)) => self.always(), - (Type::TypedDict(left_td), Type::TypedDict(right_td)) => { + (Type::TypedDict(left_td), Type::TypedDict(right_td)) => nontrivial_check(self, || { self.with_recursion_guard(db, left, right, || { self.check_typeddict_pair(db, left_td, right_td) }) - } + }), // For any type `T`, if `dict[str, Any]` is not assignable to `T`, then all `TypedDict` // types will always be disjoint from `T`. This doesn't cover all cases -- in fact // `dict` *itself* is almost always disjoint from `TypedDict` -- but it's a good // approximation, and some false negatives are acceptable. (Type::TypedDict(_), other) | (other, Type::TypedDict(_)) => { - let dict_str_any = KnownClass::Dict - .to_specialized_instance(db, &[KnownClass::Str.to_instance(db), Type::any()]); + nontrivial_check(self, || { + let dict_str_any = KnownClass::Dict.to_specialized_instance( + db, + env, + &[KnownClass::Str.to_instance(db, env), Type::any()], + ); - self.as_relation_checker(TypeRelation::Assignability) - .check_type_pair(db, dict_str_any, other) - .negate(db, self.constraints) + self.as_relation_checker(TypeRelation::Assignability) + .check_type_pair(db, dict_str_any, other) + .negate(db, self.constraints) + }) } } } diff --git a/crates/ty_python_semantic/src/types/relation_error.rs b/crates/ty_python_semantic/src/types/relation_error.rs index ce2602fdd6..06570acce6 100644 --- a/crates/ty_python_semantic/src/types/relation_error.rs +++ b/crates/ty_python_semantic/src/types/relation_error.rs @@ -1,3 +1,5 @@ +use crate::Db; +use crate::types::relation::TypeRelation; /// This module defines a tree structure for collecting contextual information about type relation errors /// ("why is this complex type not assignable to that other complex type?"). use std::cell::{Cell, RefCell}; @@ -7,8 +9,8 @@ use ruff_python_ast::name::Name; use crate::types::context::LintDiagnosticGuard; use crate::types::tuple::TupleLength; -use crate::types::{Type, TypedDictType}; -use crate::{Db, FxOrderSet}; +use crate::types::{DisplaySettings, Type, TypedDictType}; +use crate::{FxOrderSet, ProgramEnvironment}; /// Identifies a parameter, either by name or by position. #[derive(Clone, Debug, PartialEq, Eq)] @@ -91,6 +93,10 @@ pub(crate) enum ErrorContext<'db> { target_field: Type<'db>, }, TypedDictNotAssignableToDict(TypedDictType<'db>), + OpenTypedDictNotAssignableToMapping { + source: TypedDictType<'db>, + target: Type<'db>, + }, IncompatibleReturnTypes { source: Type<'db>, target: Type<'db>, @@ -107,6 +113,17 @@ pub(crate) enum ErrorContext<'db> { ExtraRequiredParameter { parameter: ParameterDescription, }, + MissingParameter { + parameter: ParameterDescription, + }, + RequiredParameterMustHaveDefault { + parameter: ParameterDescription, + }, + MissingVariadicPositionalParameter, + MissingVariadicKeywordParameter, + TopCallableAssignedToNonTop { + return_type: Type<'db>, + }, ParameterNameMismatch { source_name: Name, target_name: Name, @@ -136,6 +153,10 @@ pub(crate) enum ErrorContext<'db> { member_name: Name, ty: Type<'db>, }, + ProtocolMemberClassVarMismatch { + member_name: Name, + ty: Type<'db>, + }, ProtocolSpecialMethodNotDefinedOnMetaType, ProtocolMemberIncompatible { member_name: Name, @@ -154,11 +175,15 @@ impl<'db> ErrorContext<'db> { fn render( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + relation: TypeRelation, help_messages: &mut FxOrderSet, ) -> Option { let typed_dict_name = |typed_dict: &TypedDictType<'db>| match typed_dict { TypedDictType::Class(class) => format!("TypedDict `{}`", class.name(db)), - TypedDictType::Synthesized(_) => Type::TypedDict(*typed_dict).display(db).to_string(), + TypedDictType::Synthesized(_) => { + Type::TypedDict(*typed_dict).display(db, env).to_string() + } }; Some(match self { @@ -169,17 +194,30 @@ impl<'db> ErrorContext<'db> { element, union, target, - } => format!( - "element `{}` of union `{}` is not assignable to `{}`", - element.display(db), - union.display(db), - target.display(db), - ), - Self::NotAssignableToAnyUnionElement { source, union } => format!( - "type `{}` is not assignable to any element of the union `{}`", - source.display(db), - union.display(db), - ), + } => { + let settings = DisplaySettings::from_possibly_ambiguous_types( + db, + env, + [*element, *union, *target], + ); + format!( + "element `{}` of union `{}` is not {} `{}`", + element.display_with(db, env, settings.clone()), + union.display_with(db, env, settings.expand_numeric_tower_unions()), + relation.description(), + target.display_with(db, env, settings), + ) + } + Self::NotAssignableToAnyUnionElement { source, union } => { + let settings = + DisplaySettings::from_possibly_ambiguous_types(db, env, [*source, *union]); + format!( + "type `{}` is not {} any element of the union `{}`", + source.display_with(db, env, settings.clone()), + relation.description(), + union.display_with(db, env, settings.expand_numeric_tower_unions()), + ) + } Self::NotAssignableToNOtherUnionElements { n } => format!( "... omitted {n} union element{} without additional context", if *n == 1 { "" } else { "s" } @@ -189,18 +227,20 @@ impl<'db> ErrorContext<'db> { element, intersection, } => format!( - "type `{}` is not assignable to element `{}` of intersection `{}`", - source.display(db), - element.display(db), - intersection.display(db), + "type `{}` is not {} element `{}` of intersection `{}`", + source.display(db, env), + relation.description(), + element.display(db, env), + intersection.display(db, env), ), Self::NoIntersectionElementAssignableToTarget { intersection, target, } => format!( - "no element of intersection `{}` is assignable to `{}`", - intersection.display(db), - target.display(db), + "no element of intersection `{}` is {} `{}`", + intersection.display(db, env), + relation.description(), + target.display(db, env), ), Self::TypedDictFieldMissing { field_name, source } => { format!( @@ -226,7 +266,8 @@ impl<'db> ErrorContext<'db> { } => { help_messages.insert(HelpMessages::RequiredFieldCouldBeRemoved); format!( - "field \"{field_name}\" is required in {source} but not required and mutable in {target}", + "field \"{field_name}\" is required in {source} \ + but not required and mutable in {target}", source = typed_dict_name(source), target = typed_dict_name(target) ) @@ -249,25 +290,46 @@ impl<'db> ErrorContext<'db> { source_field, target_field, } => format!( - "field \"{field_name}\" on {source} has type `{source_field}` which is not assignable to type `{target_field}` expected by {target}", + "field \"{field_name}\" on {source} has type `{source_field}` \ + which is not {relation} type `{target_field}` expected by {target}", source = typed_dict_name(source), target = typed_dict_name(target), - source_field = source_field.display(db), - target_field = target_field.display(db), + relation = relation.description(), + source_field = source_field.display(db, env), + target_field = target_field.display(db, env), ), Self::TypedDictNotAssignableToDict(typed_dict) => { - help_messages.insert(HelpMessages::TypedDictNotAssignableToDict); + help_messages.insert(HelpMessages::TypedDictNotAssignableToDict(relation)); help_messages.insert(HelpMessages::ConsiderUsingMappingInsteadOfDict); format!( - "{source} is not assignable to `dict`", - source = typed_dict_name(typed_dict) + "{source} is not {relation} `dict`", + source = typed_dict_name(typed_dict), + relation = relation.description() + ) + } + Self::OpenTypedDictNotAssignableToMapping { source, target } => { + let name = source.defining_class().map(|class| class.name(db)); + help_messages.insert(HelpMessages::OpenTypedDictNotAssignableToMapping { + typed_dict_name: name.cloned(), + relation, + }); + help_messages.insert(HelpMessages::ExplainOpenTypedDictUnsoundness { + typed_dict_name: name.cloned(), + }); + + format!( + "{source} is not {relation} `{target}`", + source = typed_dict_name(source), + relation = relation.description(), + target = target.display(db, env) ) } Self::IncompatibleReturnTypes { source, target } => format!( - "incompatible return types: `{source}` is not assignable to `{target}`", - source = source.display(db), - target = target.display(db), + "incompatible return types: `{source}` is not {relation} `{target}`", + source = source.display(db, env), + relation = relation.description(), + target = target.display(db, env), ), Self::IncompatibleParameterTypes { source, @@ -276,25 +338,55 @@ impl<'db> ErrorContext<'db> { } => { // reversed order due to contravariance of parameter types format!( - "{parameter} has an incompatible type: `{target}` is not assignable to `{source}`", - source = source.display(db), - target = target.display(db), + "{parameter} has an incompatible type: `{target}` is not {relation} `{source}`", + source = source.display(db, env), + relation = relation.description(), + target = target.display(db, env), ) } Self::InferredCallableType { source, callable } => format!( "type `{}` has inferred callable type `{}`", - source.display(db), - callable.display(db), + source.display(db, env), + callable.display(db, env), ), Self::ExtraRequiredParameter { parameter } => match parameter { - ParameterDescription::Named(name) => format!("unexpected extra parameter `{name}`"), - ParameterDescription::Index(_) => "unexpected extra parameter".to_string(), + ParameterDescription::Named(name) => { + help_messages.insert(HelpMessages::ConsiderAddingADefaultValue { + parameter_name: Some(name.clone()), + }); + format!("unexpected extra parameter `{name}`") + } + ParameterDescription::Index(_) => { + help_messages.insert(HelpMessages::ConsiderAddingADefaultValue { + parameter_name: None, + }); + "unexpected extra parameter".to_string() + } }, + Self::MissingParameter { parameter } => format!("{parameter} is missing"), + Self::RequiredParameterMustHaveDefault { parameter } => { + format!("{parameter} must have a default value") + } + Self::MissingVariadicPositionalParameter => { + "the signature must accept arbitrary positional arguments".to_string() + } + Self::MissingVariadicKeywordParameter => { + "the signature must accept arbitrary keyword arguments".to_string() + } + Self::TopCallableAssignedToNonTop { return_type } => { + help_messages.insert(HelpMessages::TopCallableExplanation); + format!( + "Object of type `Top[(...) -> {}]` is not safe to call; \ + its signature is not known", + return_type.display(db, env) + ) + } Self::ParameterNameMismatch { source_name, target_name, } => format!( - "the parameter named `{source_name}` does not match `{target_name}` (and can be used as a keyword parameter)", + "the parameter named `{source_name}` does not match `{target_name}` \ + (and can be used as a keyword parameter)", ), Self::ParameterMustAcceptKeywordArguments { source_name, @@ -302,7 +394,8 @@ impl<'db> ErrorContext<'db> { } => { if let Some(source_name) = source_name { format!( - "parameter `{source_name}` is positional-only but must also accept keyword arguments", + "parameter `{source_name}` is positional-only \ + but must also accept keyword arguments", ) } else { format!("parameter `{target_name}` must accept keyword arguments") @@ -315,7 +408,8 @@ impl<'db> ErrorContext<'db> { source_len, target_len, } => format!( - "a tuple of length {source_len} is not assignable to a tuple of length {}", + "a tuple of length {source_len} is not {} a tuple of length {}", + relation.description(), target_len.display_minimum(), ), Self::TupleElementNotCompatible { @@ -332,29 +426,37 @@ impl<'db> ErrorContext<'db> { (n, c) => format!("tuple element {n} of {c}"), }; format!( - "{which} is not compatible: `{source}` is not assignable to `{target}`", - source = source.display(db), - target = target.display(db) + "{which} is not compatible: `{source}` is not {relation} `{target}`", + source = source.display(db, env), + relation = relation.description(), + target = target.display(db, env) ) } Self::TypeNotCompatibleWithProtocol { ty, protocol } => { if let Type::ProtocolInstance(_) = ty { format!( - "protocol `{}` is not assignable to protocol `{}`", - ty.display(db), - protocol.display(db), + "protocol `{}` is not {} protocol `{}`", + ty.display(db, env), + relation.description(), + protocol.display(db, env), ) } else { format!( - "type `{}` is not assignable to protocol `{}`", - ty.display(db), - protocol.display(db), + "type `{}` is not {} protocol `{}`", + ty.display(db, env), + relation.description(), + protocol.display(db, env), ) } } Self::ProtocolMemberNotDefined { member_name, ty } => format!( "protocol member `{member_name}` is not defined on type `{}`", - ty.display(db), + ty.display(db, env), + ), + Self::ProtocolMemberClassVarMismatch { member_name, ty } => format!( + "protocol member `{member_name}` is an instance variable on type `{}`, \ + but a class variable is required", + ty.display(db, env), ), Self::ProtocolSpecialMethodNotDefinedOnMetaType => { "special methods must be defined on the meta-type when matching a protocol" @@ -364,14 +466,15 @@ impl<'db> ErrorContext<'db> { format!("protocol member `{member_name}` is incompatible") } Self::ProtocolMemberReadTypeIncompatible { source, target } => format!( - "read type `{source}` is not assignable to `{target}`", - source = source.display(db), - target = target.display(db), + "read type `{source}` is not {relation} `{target}`", + source = source.display(db, env), + relation = relation.description(), + target = target.display(db, env), ), Self::ProtocolMemberNotWritable => "the member is not writable".to_string(), Self::ProtocolMemberWriteTypeIncompatible { target } => format!( "the member does not accept writes of type `{}`", - target.display(db), + target.display(db, env), ), }) } @@ -380,22 +483,75 @@ impl<'db> ErrorContext<'db> { #[derive(Clone, Debug, PartialEq, Eq, Hash)] enum HelpMessages { RequiredFieldCouldBeRemoved, - TypedDictNotAssignableToDict, + TypedDictNotAssignableToDict(TypeRelation), ConsiderUsingMappingInsteadOfDict, + TopCallableExplanation, + ConsiderAddingADefaultValue { + parameter_name: Option, + }, + OpenTypedDictNotAssignableToMapping { + typed_dict_name: Option, + relation: TypeRelation, + }, + ExplainOpenTypedDictUnsoundness { + typed_dict_name: Option, + }, } impl std::fmt::Display for HelpMessages { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - HelpMessages::RequiredFieldCouldBeRemoved => { - f.write_str("The required field could be removed through a destructive operation like `del` on the target.") - } - HelpMessages::TypedDictNotAssignableToDict => { - f.write_str("A TypedDict is not usually assignable to any `dict[..]` type; `dict` types allow destructive operations like `clear()`.") + HelpMessages::RequiredFieldCouldBeRemoved => f.write_str( + "The required field could be removed through a destructive operation \ + like `del` on the target.", + ), + HelpMessages::TypedDictNotAssignableToDict(relation) => { + write!( + f, + "A TypedDict is not usually {} any `dict[..]` type; \ + `dict` types allow destructive operations like `clear()`.", + relation.description() + ) } HelpMessages::ConsiderUsingMappingInsteadOfDict => { f.write_str("Consider using `Mapping[..]` instead of `dict[..]`.") } + HelpMessages::OpenTypedDictNotAssignableToMapping { + typed_dict_name, + relation, + } => { + let name = typed_dict_name + .as_ref() + .map(|name| format!("`{name}`")) + .unwrap_or_else(|| "this TypedDict".to_string()); + write!( + f, + "{name} would be {relation} this `Mapping` type \ + if it were declared with `closed=True`, \ + but TypedDicts are open by default.", + relation = relation.description() + ) + } + HelpMessages::ExplainOpenTypedDictUnsoundness { typed_dict_name } => { + let name = typed_dict_name + .as_ref() + .map(|name| format!("`{name}`")) + .unwrap_or_else(|| "this TypedDict".to_string()); + write!( + f, + "A subclass of {name} could validly add a new field \ + of an arbitrary type, violating subtyping with the `Mapping` type" + ) + } + HelpMessages::TopCallableExplanation => f.write_str( + "This type includes all possible parameter sets, \ + so it cannot safely be called \ + because there is no valid set of arguments for it", + ), + HelpMessages::ConsiderAddingADefaultValue { parameter_name } => match parameter_name { + Some(name) => write!(f, "Parameter `{name}` must have a default value"), + None => f.write_str("The parameter must have a default value"), + }, } } } @@ -421,15 +577,18 @@ impl<'db> ErrorContextNode<'db> { matches!(self.context, ErrorContext::Empty) && self.children.is_empty() } + #[expect(clippy::too_many_arguments)] fn render_tree( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + relation: TypeRelation, output_lines: &mut Vec, help_messages: &mut FxOrderSet, prefix: &str, continuation: &str, ) { - if let Some(line) = self.context.render(db, help_messages) { + if let Some(line) = self.context.render(db, env, relation, help_messages) { output_lines.push(format!("{prefix}{line}")); } @@ -443,6 +602,8 @@ impl<'db> ErrorContextNode<'db> { }; child.render_tree( db, + env, + relation, output_lines, help_messages, &child_prefix, @@ -456,34 +617,35 @@ impl<'db> ErrorContextNode<'db> { pub(crate) struct ErrorContextTree<'db> { root: Rc>>, enabled: Cell, + relation: TypeRelation, } impl PartialEq for ErrorContextTree<'_> { fn eq(&self, other: &Self) -> bool { - *self.root.borrow() == *other.root.borrow() + *self.root.borrow() == *other.root.borrow() && self.relation == other.relation } } impl Eq for ErrorContextTree<'_> {} -impl<'db> From> for ErrorContextTree<'db> { - fn from(context: ErrorContext<'db>) -> Self { +impl<'db> ErrorContextTree<'db> { + /// Create a new, empty error context tree with collection enabled. + pub(crate) fn new(relation: TypeRelation) -> Self { Self { - root: Rc::new(RefCell::new(ErrorContextNode { - context, - children: Vec::new(), - })), + root: Rc::default(), enabled: Cell::new(true), + relation, } } -} -impl<'db> ErrorContextTree<'db> { - /// Create a new, empty error context tree with collection enabled. - pub(crate) fn new() -> Self { + pub(crate) fn from_context(context: ErrorContext<'db>, relation: TypeRelation) -> Self { Self { - root: Rc::default(), + root: Rc::new(RefCell::new(ErrorContextNode { + context, + children: Vec::new(), + })), enabled: Cell::new(true), + relation, } } @@ -534,6 +696,7 @@ impl<'db> ErrorContextTree<'db> { ErrorContextTree { root: Rc::new(RefCell::new(std::mem::take(&mut *self.root.borrow_mut()))), enabled: Cell::new(self.enabled.get()), + relation: self.relation, } } @@ -541,13 +704,20 @@ impl<'db> ErrorContextTree<'db> { pub(in crate::types) fn attach_to( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, diag: &mut LintDiagnosticGuard<'_, '_>, ) { let mut output_lines = Vec::new(); let mut help_messages = FxOrderSet::default(); - self.root - .borrow() - .render_tree(db, &mut output_lines, &mut help_messages, "", ""); + self.root.borrow().render_tree( + db, + env, + self.relation, + &mut output_lines, + &mut help_messages, + "", + "", + ); for line in output_lines { diag.info(line); } diff --git a/crates/ty_python_semantic/src/types/restricted.rs b/crates/ty_python_semantic/src/types/restricted.rs index 8ffff8d3f0..6b237017c2 100644 --- a/crates/ty_python_semantic/src/types/restricted.rs +++ b/crates/ty_python_semantic/src/types/restricted.rs @@ -24,6 +24,7 @@ use super::class::ClassType; use super::variance::VarianceInferable; use super::{BoundTypeVarIdentity, KnownClass, Type, TypeVarVariance, visitor}; use crate::Db; +use crate::types::ProgramEnvironment; #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub struct RestrictedType<'db> { @@ -57,15 +58,16 @@ impl<'db> RestrictedType<'db> { /// - stacking the same modifier twice is idempotent pub(crate) fn from_type_expression( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, modifier: TypeModifier, ty: Type<'db>, ) -> Type<'db> { // `literal str` and `LiteralString` denote the same set of values, so // there is no reason to carry a second spelling of it around - if modifier == TypeModifier::Literal && ty == KnownClass::Str.to_instance(db) { + if modifier == TypeModifier::Literal && ty == KnownClass::Str.to_instance(db, env) { return Type::literal_string(); } - if restriction_holds(db, modifier, ty) { + if restriction_holds(db, env, modifier, ty) { return ty; } Type::Restricted(Self::new(db, modifier, ty)) @@ -103,38 +105,39 @@ impl<'db> Type<'db> { /// /// A dynamic type is literal, matching the way gradual types are admissible /// against every other restriction in the type system. - pub(crate) fn is_literal_type(self, db: &'db dyn Db) -> bool { + pub(crate) fn is_literal_type(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool { match self { Type::LiteralValue(_) => true, Type::Dynamic(_) | Type::Divergent(_) | Type::Never => true, Type::Restricted(restricted) => { restricted.modifier(db) == TypeModifier::Literal - || restricted.value_type(db).is_literal_type(db) + || restricted.value_type(db).is_literal_type(db, env) } - Type::TypeAlias(alias) => alias.value_type(db).is_literal_type(db), - Type::Deferred(deferred) => deferred.reduced(db).is_literal_type(db), + Type::TypeAlias(alias) => alias.value_type(db).is_literal_type(db, env), + Type::Deferred(deferred) => deferred.reduced(db, env).is_literal_type(db, env), // a union is literal when every member is; an intersection when any // positive member is (its values are drawn from that member) Type::Union(union) => union .elements(db) .iter() - .all(|element| element.is_literal_type(db)), + .all(|element| element.is_literal_type(db, env)), Type::Intersection(intersection) => intersection .positive(db) .iter() - .any(|element| element.is_literal_type(db)), + .any(|element| element.is_literal_type(db, env)), Type::TypeVar(bound_typevar) => bound_typevar .typevar(db) - .bound_or_constraints(db) - .is_some_and(|bound| bound.as_type(db).is_literal_type(db)), + .bound_or_constraints(db, env) + .is_some_and(|bound| bound.as_type(db, env).is_literal_type(db, env)), Type::NominalInstance(nominal) => { // `None` and `...` are singletons written as literals - self.is_singleton(db) || class_arguments_are_literal(db, nominal.class(db)) + self.is_singleton(db, env) + || class_arguments_are_literal(db, env, nominal.class(db, env)) } _ => false, @@ -145,7 +148,11 @@ impl<'db> Type<'db> { /// Whether every type argument of `class` is literal, and there is at least one. /// A bare class has no arguments to make literal, so it is not literal itself — /// only its literal value types are. -fn class_arguments_are_literal<'db>(db: &'db dyn Db, class: ClassType<'db>) -> bool { +fn class_arguments_are_literal<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + class: ClassType<'db>, +) -> bool { let (_, Some(specialization)) = class.class_literal_and_specialization(db) else { return false; }; @@ -153,7 +160,7 @@ fn class_arguments_are_literal<'db>(db: &'db dyn Db, class: ClassType<'db>) -> b !arguments.is_empty() && arguments .iter() - .all(|argument| argument.is_literal_type(db)) + .all(|argument| argument.is_literal_type(db, env)) } /// Whether a value of type `source` is admissible where ` ` is @@ -161,6 +168,7 @@ fn class_arguments_are_literal<'db>(db: &'db dyn Db, class: ClassType<'db>) -> b /// relation separately checks that `source` is assignable to `inner`. pub(crate) fn restriction_admits<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, modifier: TypeModifier, inner: Type<'db>, source: Type<'db>, @@ -169,24 +177,24 @@ pub(crate) fn restriction_admits<'db>( // same way it is assignable to every type. an unannotated parameter's hole // that nothing bounded is one of those wearing a name if matches!(source, Type::Dynamic(_) | Type::Divergent(_) | Type::Never) - || crate::types::inferred_signature::gradual_hole(db, source).is_some() + || crate::types::inferred_signature::gradual_hole(db, env, source).is_some() { return true; } match source { Type::TypeAlias(alias) => { - return restriction_admits(db, modifier, inner, alias.value_type(db)); + return restriction_admits(db, env, modifier, inner, alias.value_type(db)); } Type::Deferred(deferred) => { - return restriction_admits(db, modifier, inner, deferred.reduced(db)); + return restriction_admits(db, env, modifier, inner, deferred.reduced(db, env)); } // every member of a union has to fit, since the value may be any of them Type::Union(union) => { return union .elements(db) .iter() - .all(|element| restriction_admits(db, modifier, inner, *element)); + .all(|element| restriction_admits(db, env, modifier, inner, *element)); } // an intersection's values are drawn from every positive member, so one // admissible member is enough @@ -195,13 +203,13 @@ pub(crate) fn restriction_admits<'db>( return !positive.is_empty() && positive .iter() - .any(|element| restriction_admits(db, modifier, inner, *element)); + .any(|element| restriction_admits(db, env, modifier, inner, *element)); } _ => {} } match modifier { - TypeModifier::Literal => source.is_literal_type(db), + TypeModifier::Literal => source.is_literal_type(db, env), TypeModifier::Final => { // "the runtime class is exactly `inner`'s": promote a literal to the // class it is an instance of (`Literal[1]` → `int`, `True` → `bool`) @@ -215,16 +223,17 @@ pub(crate) fn restriction_admits<'db>( // relation in the system admits let promoted = source .erase_restriction(db) - .literal_fallback_instance(db) + .literal_fallback_instance(db, env) .unwrap_or_else(|| source.erase_restriction(db)); let inner = inner.erase_restriction(db); match (promoted, inner) { (Type::NominalInstance(source), Type::NominalInstance(inner)) => { - source.class(db).class_literal(db) == inner.class(db).class_literal(db) + source.class(db, env).class_literal(db) + == inner.class(db, env).class_literal(db) } // a type with no class behind it — a callable, a protocol — // degenerates to plain type equality - _ => promoted.is_equivalent_to(db, inner), + _ => promoted.is_equivalent_to(db, env, inner), } } } @@ -238,7 +247,12 @@ pub(crate) fn restriction_admits<'db>( /// gradual type is not *known* to satisfy the restriction, so dropping the /// modifier from `literal list[*]` would silently discard the check for every /// later assignment. -fn restriction_holds<'db>(db: &'db dyn Db, modifier: TypeModifier, ty: Type<'db>) -> bool { +fn restriction_holds<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + modifier: TypeModifier, + ty: Type<'db>, +) -> bool { match ty { Type::Never => return true, Type::Dynamic(_) | Type::Divergent(_) => return false, @@ -249,14 +263,19 @@ fn restriction_holds<'db>(db: &'db dyn Db, modifier: TypeModifier, ty: Type<'db> // a `@final` class has no subclasses, so every one of its instances is // already exactly it TypeModifier::Final => matches!(ty, Type::NominalInstance(nominal) - if nominal.class(db).class_literal(db).is_final(db)), + if nominal.class(db, env).class_literal(db).is_final(db)), } } impl<'db> VarianceInferable<'db> for RestrictedType<'db> { // a restriction narrows the set of values without reordering it, so it // inherits the variance of the type it wraps - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { - self.type_argument(db).variance_of(db, typevar) + fn variance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { + self.type_argument(db).variance_of(db, env, typevar) } } diff --git a/crates/ty_python_semantic/src/types/safe_variance.rs b/crates/ty_python_semantic/src/types/safe_variance.rs index 1edf2b9d80..6a9274f975 100644 --- a/crates/ty_python_semantic/src/types/safe_variance.rs +++ b/crates/ty_python_semantic/src/types/safe_variance.rs @@ -15,6 +15,7 @@ use super::{ is_private_member, }; use crate::Db; +use crate::types::ProgramEnvironment; /// basedpython safe variance: a private member seen through a view of its class that is /// not the class's own. @@ -35,11 +36,11 @@ impl<'db> PrivateMemberView<'db> { /// /// The receiver's own type argument says nothing about what the object holds — that is what /// its being widened means. - fn erased(&self, db: &'db dyn Db) -> Type<'db> { + fn erased(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { self.substituted .iter() .fold(self.declared_ty, |ty, typevar| { - ty.substitute_one_typevar(db, *typevar, Type::any()) + ty.substitute_one_typevar(db, env, *typevar, Type::any()) }) } @@ -49,8 +50,8 @@ impl<'db> PrivateMemberView<'db> { /// everything such a view can know: `t: T` reads as `object`. The value can be treated as its /// bound, but it is no longer a `T`, so it can never be funnelled back into the `T`-typed /// storage it came from. - pub(super) fn read_type(&self, db: &'db dyn Db) -> Type<'db> { - self.erased(db).top_materialization(db) + pub(super) fn read_type(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + self.erased(db, env).top_materialization(db, env) } /// The type a *write* through this view has to supply. @@ -58,8 +59,8 @@ impl<'db> PrivateMemberView<'db> { /// Storage is invariant in its own type, so a write has to be valid for every type the member /// could really have — the erasure's bottom materialization. For a plain `T` that is `Never`: /// a view that knows nothing about a member cannot write to it, whatever it holds. - fn write_type(&self, db: &'db dyn Db) -> Type<'db> { - self.erased(db).bottom_materialization(db) + fn write_type(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + self.erased(db, env).bottom_materialization(db, env) } } @@ -69,10 +70,11 @@ impl<'db> PrivateMemberView<'db> { /// `None` leaves the write to ordinary specialization. pub(super) fn private_member_write_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, object_ty: Type<'db>, attribute: &str, ) -> Option> { - Some(private_member_view(db, object_ty, attribute)?.write_type(db)) + Some(private_member_view(db, env, object_ty, attribute)?.write_type(db, env)) } /// basedpython safe variance: a private member does not specialize. @@ -86,13 +88,14 @@ pub(super) fn private_member_write_type<'db>( /// is already sound. pub(super) fn private_member_view<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, object_ty: Type<'db>, attribute: &str, ) -> Option> { // a use-site modifier restricts which values the receiver may hold, not which // specialization it is a view of let instance = object_ty.erase_restriction(db).as_nominal_instance()?; - let super::ClassType::Generic(alias) = instance.class(db) else { + let super::ClassType::Generic(alias) = instance.class(db, env) else { return None; }; let specialization = alias.specialization(db); @@ -116,9 +119,13 @@ pub(super) fn private_member_view<'db>( // still names the class's type parameters rather than the receiver's arguments. a // `__getattr__` result is not a declared member of anything, so it is never private // however its name is spelled - let own_view = Type::instance(db, class.identity_specialization(db)); - let member = - own_view.member_lookup_with_policy(db, attribute, MemberLookupPolicy::NO_GETATTR_LOOKUP); + let own_view = Type::instance(db, env, class.identity_specialization(db)); + let member = own_view.member_lookup_with_policy( + db, + env, + attribute, + MemberLookupPolicy::NO_GETATTR_LOOKUP, + ); let declared_ty = member.place.ignore_possibly_undefined()?; if !is_private_member(db, attribute, member.qualifiers, declared_ty) { return None; @@ -129,6 +136,7 @@ pub(super) fn private_member_view<'db>( let identity = typevar.identity(db); let mentions_typevar = any_over_type( db, + env, declared_ty, false, |ty| matches!(ty, Type::TypeVar(other) if other.identity(db) == identity), diff --git a/crates/ty_python_semantic/src/types/set_theoretic.rs b/crates/ty_python_semantic/src/types/set_theoretic.rs index 17c89643b4..dfed4e5d24 100644 --- a/crates/ty_python_semantic/src/types/set_theoretic.rs +++ b/crates/ty_python_semantic/src/types/set_theoretic.rs @@ -1,3 +1,4 @@ +use crate::ProgramEnvironment; use itertools::Either; use std::convert::Infallible; @@ -46,7 +47,11 @@ impl<'db> UnionType<'db> { /// /// For performance reasons, consider using [`UnionType::from_two_elements`] if /// the union is constructed from exactly two elements. - pub fn from_elements(db: &'db dyn Db, elements: I) -> Type<'db> + pub fn from_elements( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + elements: I, + ) -> Type<'db> where I: IntoIterator, T: Into>, @@ -55,10 +60,13 @@ impl<'db> UnionType<'db> { if let Some(first) = iter_elements.next() { if let Some(second) = iter_elements.next() { - let builder = UnionBuilder::new(db).add(first.into()).add(second.into()); - iter_elements - .fold(builder, |builder, element| builder.add(element.into())) - .build() + let mut builder = UnionBuilder::new(db, env); + builder.add_in_place(first.into()); + builder.add_in_place(second.into()); + for element in iter_elements { + builder.add_in_place(element.into()); + } + builder.build() } else { first.into() } @@ -68,38 +76,46 @@ impl<'db> UnionType<'db> { } /// Create a union type `A | B` from two elements `A` and `B`. - pub fn from_two_elements(db: &'db dyn Db, a: Type<'db>, b: Type<'db>) -> Type<'db> { + pub fn from_two_elements( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + a: Type<'db>, + b: Type<'db>, + ) -> Type<'db> { #[salsa::tracked( returns(copy), cycle_initial=|_, id, _| Type::divergent(id), - cycle_fn=|db, cycle, previous: &Type<'db>, result: Type<'db>, _| { - result.cycle_normalized(db, *previous, cycle) + cycle_fn=|db, cycle, previous: &Type<'db>, result: Type<'db>, types: TypePair<'db>| { + result.cycle_normalized(db, &ProgramEnvironment::from_program(types.program(db)), *previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] fn union_from_two_elements<'db>(db: &'db dyn Db, types: TypePair<'db>) -> Type<'db> { - UnionBuilder::new(db) + let env = ProgramEnvironment::from_program(types.program(db)); + UnionBuilder::new(db, &env) .add(types.first(db)) .add(types.second(db)) .build() } - union_from_two_elements(db, TypePair::new(db, a, b)) + union_from_two_elements(db, TypePair::new(db, env.program(db), a, b)) } /// Create a union from a list of elements without unpacking type aliases. - pub(crate) fn from_elements_leave_aliases(db: &'db dyn Db, elements: I) -> Type<'db> + pub(crate) fn from_elements_leave_aliases( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + elements: I, + ) -> Type<'db> where I: IntoIterator, T: Into>, { - elements - .into_iter() - .fold( - UnionBuilder::new(db).unpack_aliases(false), - |builder, element| builder.add(element.into()), - ) - .build() + let mut builder = UnionBuilder::new(db, env).unpack_aliases(false); + for element in elements { + builder.add_in_place(element.into()); + } + builder.build() } /// Returns `true` if any direct element of this union is a type alias. @@ -112,23 +128,29 @@ impl<'db> UnionType<'db> { /// Recursively expands aliases that expose top-level union elements. /// /// Aliases nested inside non-union elements remain part of those elements. - pub(crate) fn expand_aliases(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn expand_aliases( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { // Rebuild the union so that `UnionBuilder` simplifies any redundancies exposed. - Self::from_elements(db, self.elements(db).iter().copied()) + Self::from_elements(db, env, self.elements(db).iter().copied()) } - pub(crate) fn from_elements_cycle_recovery(db: &'db dyn Db, elements: I) -> Type<'db> + pub(crate) fn from_elements_cycle_recovery( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + elements: I, + ) -> Type<'db> where I: IntoIterator, T: Into>, { - elements - .into_iter() - .fold( - UnionBuilder::new(db).cycle_recovery(true), - |builder, element| builder.add(element.into()), - ) - .build() + let mut builder = UnionBuilder::new(db, env).cycle_recovery(true); + for element in elements { + builder.add_in_place(element.into()); + } + builder.build() } /// A fallible version of [`UnionType::from_elements`]. @@ -136,14 +158,18 @@ impl<'db> UnionType<'db> { /// 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> + pub(crate) fn try_from_elements( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + elements: I, + ) -> Option> where I: IntoIterator>, T: Into>, { - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); for element in elements { - builder = builder.add(element?.into()); + builder.add_in_place(element?.into()); } Some(builder.build()) } @@ -153,10 +179,12 @@ impl<'db> UnionType<'db> { pub(crate) fn map( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, mut transform_fn: impl FnMut(&Type<'db>) -> Type<'db>, ) -> Type<'db> { - let Ok(mapped) = - self.try_map_impl(db, |element| Ok::<_, Infallible>(transform_fn(element))); + let Ok(mapped) = self.try_map_impl(db, env, |element| { + Ok::<_, Infallible>(transform_fn(element)) + }); mapped } @@ -164,6 +192,7 @@ impl<'db> UnionType<'db> { pub(crate) fn map_leave_aliases( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, mut transform_fn: impl FnMut(&Type<'db>) -> Type<'db>, ) -> Type<'db> { let elements = self.elements(db); @@ -171,13 +200,13 @@ impl<'db> UnionType<'db> { while let Some((i, ty)) = iter.next() { let new_ty = transform_fn(ty); if &new_ty != ty { - let mut builder = UnionBuilder::new(db).unpack_aliases(false); + let mut builder = UnionBuilder::new(db, env).unpack_aliases(false); for prev in &elements[..i] { - builder = builder.add(*prev); + builder.add_in_place(*prev); } - builder = builder.add(new_ty); + builder.add_in_place(new_ty); for (_, element) in iter { - builder = builder.add(transform_fn(element)); + builder.add_in_place(transform_fn(element)); } return builder .recursively_defined(self.recursively_defined(db)) @@ -198,15 +227,17 @@ impl<'db> UnionType<'db> { pub(crate) fn try_map( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, mut transform_fn: impl FnMut(&Type<'db>) -> Option>, ) -> Option> { - self.try_map_impl(db, |element| transform_fn(element).ok_or(())) + self.try_map_impl(db, env, |element| transform_fn(element).ok_or(())) .ok() } fn try_map_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, mut transform_fn: impl FnMut(&Type<'db>) -> Result, E>, ) -> Result, E> { let elements = self.elements(db); @@ -214,13 +245,13 @@ impl<'db> UnionType<'db> { while let Some((i, ty)) = iter.next() { let new_ty = transform_fn(ty)?; if &new_ty != ty || matches!(new_ty, Type::TypeAlias(_)) { - let mut builder = elements[..i] - .iter() - .copied() - .fold(UnionBuilder::new(db), UnionBuilder::add); - builder = builder.add(new_ty); + let mut builder = UnionBuilder::new(db, env); + for prev in &elements[..i] { + builder.add_in_place(*prev); + } + builder.add_in_place(new_ty); for (_, element) in iter { - builder = builder.add(transform_fn(element)?); + builder.add_in_place(transform_fn(element)?); } return Ok(builder .recursively_defined(self.recursively_defined(db)) @@ -231,10 +262,14 @@ impl<'db> UnionType<'db> { Ok(Type::Union(self)) } - pub(crate) fn to_instance(self, db: &'db dyn Db) -> Option>> { + pub(crate) fn to_instance( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option>> { let mut is_exact = true; - let instance = self.try_map(db, |element| { - let projection = element.to_instance(db)?; + let instance = self.try_map(db, env, |element| { + let projection = element.to_instance(db, env)?; is_exact &= projection.is_exact(); Some(projection.into_inner()) })?; @@ -245,7 +280,11 @@ impl<'db> UnionType<'db> { /// /// The returned type is broader than the literal types themselves. For example, the /// supertype for `Literal["a"] | Literal["b"]` is `LiteralString`. - pub(crate) fn common_literal_supertype(self, db: &'db dyn Db) -> Option> { + pub(crate) fn common_literal_supertype( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { // Do not use `Type::literal_fallback_instance` here: it also falls back from function // literals to `FunctionType`. Since `FunctionType.__call__` is gradual, it can be // assignable to a callable that the function literal's precise signature is not. @@ -253,7 +292,7 @@ impl<'db> UnionType<'db> { // supertype proves the relation for every literal in the union. let supertype = |element: &Type<'db>| match element { Type::LiteralValue(literal) if literal.is_string() => Some(Type::literal_string()), - Type::LiteralValue(literal) => Some(literal.fallback_instance(db)), + Type::LiteralValue(literal) => Some(literal.fallback_instance(db, env)), _ => None, }; @@ -279,9 +318,10 @@ impl<'db> UnionType<'db> { pub(crate) fn map_with_boundness( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, mut transform_fn: impl FnMut(&Type<'db>) -> Place<'db>, ) -> Place<'db> { - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); let mut all_unbound = true; let mut possibly_unbound = false; @@ -307,7 +347,7 @@ impl<'db> UnionType<'db> { provenance = provenance.or(member_provenance); all_unbound = false; - builder = builder.add(ty_member); + builder.add_in_place(ty_member); } } } @@ -334,9 +374,10 @@ impl<'db> UnionType<'db> { pub(crate) fn map_with_boundness_and_qualifiers( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, mut transform_fn: impl FnMut(&Type<'db>) -> PlaceAndQualifiers<'db>, ) -> PlaceAndQualifiers<'db> { - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); let mut qualifiers = TypeQualifiers::empty(); let mut all_unbound = true; @@ -367,7 +408,7 @@ impl<'db> UnionType<'db> { provenance = provenance.or(member_provenance); all_unbound = false; - builder = builder.add(ty_member); + builder.add_in_place(ty_member); } } } @@ -396,10 +437,11 @@ impl<'db> UnionType<'db> { pub(crate) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option> { - let mut builder = UnionBuilder::new(db) + let mut builder = UnionBuilder::new(db, env) .unpack_aliases(false) .cycle_recovery(true) .recursively_defined(self.recursively_defined(db)); @@ -407,11 +449,11 @@ impl<'db> UnionType<'db> { for ty in self.elements(db) { if nested { // list[T | Divergent] => list[Divergent] - let ty = ty.recursive_type_normalized_impl(db, div, nested)?; + let ty = ty.recursive_type_normalized_impl(db, env, div, nested)?; if ty.same_divergent_marker(div) { return Some(ty); } - builder = builder.add(ty); + builder.add_in_place(ty); empty = false; } else { // `Divergent` in a union type does not mean true divergence, so we skip it if not nested. @@ -420,15 +462,15 @@ impl<'db> UnionType<'db> { builder = builder.recursively_defined(RecursivelyDefined::Yes); continue; } - builder = builder.add( - ty.recursive_type_normalized_impl(db, div, nested) + builder.add_in_place( + ty.recursive_type_normalized_impl(db, env, div, nested) .unwrap_or(div), ); empty = false; } } if empty { - builder = builder.add(div); + builder.add_in_place(div); } Some(builder.build()) } @@ -462,19 +504,40 @@ pub(crate) enum KnownUnion { } impl KnownUnion { - pub(crate) fn to_type(self, db: &dyn Db) -> Type<'_> { + /// Returns the class whose annotation denotes this numeric-tower union. + pub(crate) const fn annotation_class(self) -> KnownClass { + match self { + Self::Float => KnownClass::Float, + Self::Complex => KnownClass::Complex, + } + } + + /// Returns whether this union contains exact instances of `class`. + pub(crate) const fn contains(self, class: KnownClass) -> bool { + match self { + Self::Float => matches!(class, KnownClass::Int | KnownClass::Float), + Self::Complex => matches!( + class, + KnownClass::Int | KnownClass::Float | KnownClass::Complex + ), + } + } + + pub(crate) fn to_type<'db>(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match self { KnownUnion::Float => UnionType::from_two_elements( db, - KnownClass::Int.to_instance(db), - KnownClass::Float.to_instance(db), + env, + KnownClass::Int.to_instance(db, env), + KnownClass::Float.to_instance(db, env), ), KnownUnion::Complex => UnionType::from_elements( db, + env, [ - KnownClass::Int.to_instance(db), - KnownClass::Float.to_instance(db), - KnownClass::Complex.to_instance(db), + KnownClass::Int.to_instance(db, env), + KnownClass::Float.to_instance(db, env), + KnownClass::Complex.to_instance(db, env), ], ), } @@ -528,7 +591,7 @@ impl<'db> NegativeIntersectionElements<'db> { } } - pub(crate) fn len(&self) -> usize { + fn len(&self) -> usize { match self { Self::Empty => 0, Self::Single(_) => 1, @@ -573,7 +636,7 @@ impl<'db> NegativeIntersectionElements<'db> { } /// Shrink the capacity of the collection as much as possible. - pub(crate) fn shrink_to_fit(&mut self) { + fn shrink_to_fit(&mut self) { match self { Self::Empty | Self::Single(_) => {} Self::Multiple(set) => set.shrink_to_fit(), @@ -589,7 +652,7 @@ impl<'db> NegativeIntersectionElements<'db> { /// the last element in the collection is popped off the end of the collection /// and placed at the index where `ty` was previously, allowing this method to complete /// in O(1) time (average). - pub(crate) fn swap_remove(&mut self, ty: &Type<'db>) -> bool { + fn swap_remove(&mut self, ty: &Type<'db>) -> bool { match self { Self::Empty => false, Self::Single(existing) => { @@ -611,7 +674,7 @@ impl<'db> NegativeIntersectionElements<'db> { /// The element is removed by swapping it with the last element /// of the collection and popping it off, allowing this method to complete /// in O(1) time (average). - pub(crate) fn swap_remove_index(&mut self, index: usize) -> Option> { + fn swap_remove_index(&mut self, index: usize) -> Option> { match self { Self::Empty => None, Self::Single(existing) => { @@ -730,36 +793,56 @@ pub(crate) fn walk_intersection_type<'db, V: visitor::TypeVisitor<'db> + ?Sized> #[salsa::tracked] impl<'db> IntersectionType<'db> { /// Return the compact enum-complement view of this intersection, if it has one. - pub(crate) fn enum_complement(self, db: &'db dyn Db) -> Option> { - EnumComplement::from_intersection_parts(db, self.positive(db), self.negative(db)) + pub(crate) fn enum_complement( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { + EnumComplement::from_intersection_parts(db, env, self.positive(db), self.negative(db)) } /// Return the exact finite alternatives represented by this intersection, if available. - pub fn finite_alternatives(self, db: &'db dyn Db) -> Option>> { - self.enum_complement(db) - .map(|complement| complement.remaining_literal_types(db)) + pub fn finite_alternatives( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option>> { + self.enum_complement(db, env) + .map(|complement| complement.remaining_literal_types(db, env)) } /// Return the exact finite alternative union represented by this intersection, if available. - pub(crate) fn finite_alternative_union(self, db: &'db dyn Db) -> Option> { - Some(self.enum_complement(db)?.remaining_literal_union(db)) + pub(crate) fn finite_alternative_union( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { + Some( + self.enum_complement(db, env)? + .remaining_literal_union(db, env), + ) } /// Return the finite alternatives only if they remain concise enough for display. pub(crate) fn finite_alternatives_for_display( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, max_literals: usize, ) -> Option>> { - self.enum_complement(db)? - .remaining_literal_types_for_display(db, max_literals) + self.enum_complement(db, env)? + .remaining_literal_types_for_display(db, env, max_literals) } /// Create an intersection type `E1 & E2 & ... & En` from a list of (positive) elements. /// /// For performance reasons, consider using [`IntersectionType::from_two_elements`] if /// the intersection is constructed from exactly two elements. - pub(crate) fn from_elements(db: &'db dyn Db, elements: I) -> Type<'db> + pub(crate) fn from_elements( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + elements: I, + ) -> Type<'db> where I: IntoIterator, T: Into>, @@ -768,13 +851,12 @@ impl<'db> IntersectionType<'db> { if let Some(first) = elements_iter.next() { if let Some(second) = elements_iter.next() { - let builder = - IntersectionBuilder::new(db).positive_elements([first.into(), second.into()]); - elements_iter - .fold(builder, |builder, element| { - builder.add_positive(element.into()) - }) - .build() + let mut builder = IntersectionBuilder::new(db, env) + .positive_elements([first.into(), second.into()]); + for element in elements_iter { + builder.add_positive_in_place(element.into()); + } + builder.build() } else { first.into() } @@ -792,7 +874,11 @@ impl<'db> IntersectionType<'db> { /// work, and if so, returns `None`. (Redundant terms do not count toward the budget.) /// /// Like [`from_elements`][Self::from_elements], a successful result is exact. - pub(crate) fn bounded_from_elements(db: &'db dyn Db, elements: I) -> Option> + pub(crate) fn bounded_from_elements( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + elements: I, + ) -> Option> where I: IntoIterator, I::IntoIter: Clone, @@ -808,21 +894,21 @@ impl<'db> IntersectionType<'db> { // there is a single union, the product of all union counts should be reasonable, even // if it exceeds the budget below. In both cases, just return the precise answer // without considering the budget. - return Some(Self::from_elements(db, elements)); + return Some(Self::from_elements(db, env, elements)); } let non_union_elements = elements.clone().filter(|element| !element.is_union()); - let initial = Self::from_elements(db, non_union_elements); + let initial = Self::from_elements(db, env, non_union_elements); let insert_candidate = |candidates: &mut Vec>, new_ty: Type<'db>| -> Option<()> { if new_ty.is_never() || candidates .iter() - .any(|old| new_ty.is_redundant_with(db, *old)) + .any(|old| new_ty.is_redundant_with(db, env, *old)) { return Some(()); } - candidates.retain(|old| !old.is_redundant_with(db, new_ty)); + candidates.retain(|old| !old.is_redundant_with(db, env, new_ty)); if candidates.len() >= MAX_INTERSECTION_DNF_TERMS { return None; } @@ -845,7 +931,7 @@ impl<'db> IntersectionType<'db> { next.clear(); for candidate in &frontier { for alternative in clause.elements(db) { - let refined = Self::from_two_elements(db, *candidate, *alternative); + let refined = Self::from_two_elements(db, env, *candidate, *alternative); insert_candidate(&mut next, refined).or(skip_budget_check)?; } } @@ -857,44 +943,51 @@ impl<'db> IntersectionType<'db> { std::mem::swap(&mut frontier, &mut next); } - Some(UnionType::from_elements(db, frontier)) + Some(UnionType::from_elements(db, env, frontier)) } /// Create an intersection type `A & B` from two elements `A` and `B`. - pub(crate) fn from_two_elements(db: &'db dyn Db, a: Type<'db>, b: Type<'db>) -> Type<'db> { + pub(crate) fn from_two_elements( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + a: Type<'db>, + b: Type<'db>, + ) -> Type<'db> { #[salsa::tracked( returns(copy), cycle_initial=|_, id, _| Type::divergent(id), - cycle_fn=|db, cycle, previous: &Type<'db>, result: Type<'db>, _| { - result.cycle_normalized(db, *previous, cycle) + cycle_fn=|db, cycle, previous: &Type<'db>, result: Type<'db>, types: TypePair<'db>| { + result.cycle_normalized(db, &ProgramEnvironment::from_program(types.program(db)), *previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] fn intersection_from_two_elements<'db>(db: &'db dyn Db, types: TypePair<'db>) -> Type<'db> { - IntersectionBuilder::new(db) + let env = ProgramEnvironment::from_program(types.program(db)); + IntersectionBuilder::new(db, &env) .positive_elements([types.first(db), types.second(db)]) .build() } - intersection_from_two_elements(db, TypePair::new(db, a, b)) + intersection_from_two_elements(db, TypePair::new(db, env.program(db), a, b)) } pub(crate) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { let positive = if nested { self.positive(db) .iter() - .map(|ty| ty.recursive_type_normalized_impl(db, div, nested)) + .map(|ty| ty.recursive_type_normalized_impl(db, env, div, nested)) .collect::>>>()? } else { self.positive(db) .iter() .map(|ty| { - ty.recursive_type_normalized_impl(db, div, nested) + ty.recursive_type_normalized_impl(db, env, div, nested) .unwrap_or(div) }) .collect() @@ -902,10 +995,10 @@ impl<'db> IntersectionType<'db> { let negative = if nested { self.negative(db) - .try_map(|ty| ty.recursive_type_normalized_impl(db, div, nested))? + .try_map(|ty| ty.recursive_type_normalized_impl(db, env, div, nested))? } else { self.negative(db).map(|ty| { - ty.recursive_type_normalized_impl(db, div, nested) + ty.recursive_type_normalized_impl(db, env, div, nested) .unwrap_or(div) }) }; @@ -932,14 +1025,15 @@ impl<'db> IntersectionType<'db> { pub(crate) fn map_positive( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, mut transform_fn: impl FnMut(&Type<'db>) -> Type<'db>, ) -> Type<'db> { - let mut builder = IntersectionBuilder::new(db); + let mut builder = IntersectionBuilder::new(db, env); for ty in self.positive(db) { - builder = builder.add_positive(transform_fn(ty)); + builder.add_positive_in_place(transform_fn(ty)); } for ty in self.negative(db) { - builder = builder.add_negative(*ty); + builder.add_negative_in_place(*ty); } builder.build() } @@ -949,31 +1043,34 @@ impl<'db> IntersectionType<'db> { /// /// Negative instance constraints are not transferred: an object not satisfying `P` does not /// imply that other instances of its class cannot satisfy `P`. - pub(crate) fn try_dunder_class(self, db: &'db dyn Db) -> Option> { + pub(crate) fn try_dunder_class( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { if !self.iter_positive(db).any(|positive| { matches!( positive, - Type::ProtocolInstance(protocol) if protocol.class_origin().is_some() + Type::ProtocolInstance(protocol) if protocol.class_origin(db).is_some() ) }) { return None; } - Some( - self.iter_positive(db) - .fold(IntersectionBuilder::new(db), |builder, positive| { - builder.add_positive(positive.dunder_class(db)) - }) - .build(), - ) + let mut builder = IntersectionBuilder::new(db, env); + for positive in self.iter_positive(db) { + builder.add_positive_in_place(positive.dunder_class(db, env)); + } + Some(builder.build()) } pub(crate) fn map_with_boundness( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, mut transform_fn: impl FnMut(&Type<'db>) -> Place<'db>, ) -> Place<'db> { - let mut builder = IntersectionBuilder::new(db); + let mut builder = IntersectionBuilder::new(db, env); let mut all_unbound = true; let mut any_definitely_bound = false; @@ -997,7 +1094,7 @@ impl<'db> IntersectionType<'db> { } provenance = provenance.or(member_provenance); - builder = builder.add_positive(ty_member); + builder.add_positive_in_place(ty_member); } } } @@ -1022,9 +1119,10 @@ impl<'db> IntersectionType<'db> { pub(crate) fn map_with_boundness_and_qualifiers( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, mut transform_fn: impl FnMut(&Type<'db>) -> PlaceAndQualifiers<'db>, ) -> PlaceAndQualifiers<'db> { - let mut builder = IntersectionBuilder::new(db); + let mut builder = IntersectionBuilder::new(db, env); let mut qualifiers = TypeQualifiers::empty(); let mut all_unbound = true; @@ -1053,7 +1151,7 @@ impl<'db> IntersectionType<'db> { } provenance = provenance.or(member_provenance); - builder = builder.add_positive(ty_member); + builder.add_positive_in_place(ty_member); } } } @@ -1081,15 +1179,19 @@ impl<'db> IntersectionType<'db> { /// Return a version of this intersection type where any type variables in the positive elements /// have been replaced by their bounds or constraints, and where any newtypes in the positive elements /// have been replaced by their concrete base types. - pub(crate) fn with_expanded_typevars_and_newtypes(self, db: &'db dyn Db) -> Type<'db> { - expand_intersection_typevars_and_newtypes(db, self.positive(db), self.negative(db)) + pub(crate) fn with_expanded_typevars_and_newtypes( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { + expand_intersection_typevars_and_newtypes(db, env, self.positive(db), self.negative(db)) } pub fn iter_positive(self, db: &'db dyn Db) -> impl Iterator> { self.positive(db).iter().copied() } - pub fn iter_negative(self, db: &'db dyn Db) -> impl Iterator> { + pub(crate) fn iter_negative(self, db: &'db dyn Db) -> impl Iterator> { self.negative(db).iter().copied() } @@ -1119,15 +1221,19 @@ impl<'db> IntersectionType<'db> { /// Projecting only the positive `type[Base]` is an over-approximation, since we have no /// representation of an exact instance type excluding subclasses, and projecting the negative /// `~TypeOf[Base]` to `~Base` would incorrectly exclude `Child` instances too. - pub(crate) fn to_instance(self, db: &'db dyn Db) -> Option>> { - let mut builder = IntersectionBuilder::new(db); + pub(crate) fn to_instance( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option>> { + let mut builder = IntersectionBuilder::new(db, env); let mut has_projected_positive = false; let mut is_exact = self.negative(db).is_empty(); for positive in self.iter_positive(db) { - if let Some(projection) = positive.to_instance(db) { + if let Some(projection) = positive.to_instance(db, env) { has_projected_positive = true; is_exact &= projection.is_exact(); - builder = builder.add_positive(projection.into_inner()); + builder.add_positive_in_place(projection.into_inner()); } else { is_exact = false; } @@ -1150,19 +1256,20 @@ impl<'db> IntersectionType<'db> { fn expand_intersection_typevars_and_newtypes<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, positive: &FxOrderSet>, negative: &NegativeIntersectionElements<'db>, ) -> Type<'db> { - let mut builder = IntersectionBuilder::new(db); + let mut builder = IntersectionBuilder::new(db, env); for &element in positive { match element { Type::TypeVar(tvar) => { - match tvar.typevar(db).bound_or_constraints(db) { + match tvar.typevar(db).bound_or_constraints(db, env) { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - builder = builder.add_positive(bound); + builder.add_positive_in_place(bound); } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - builder = builder.add_positive(constraints.as_type(db)); + builder.add_positive_in_place(constraints.as_type(db, env)); } // Type variables without bounds or constraints implicitly have `object` // as their upper bound, and adding `object` to an intersection is always a no-op @@ -1170,14 +1277,14 @@ fn expand_intersection_typevars_and_newtypes<'db>( } } Type::NewTypeInstance(newtype) => { - builder = builder.add_positive(newtype.concrete_base_type(db)); + builder.add_positive_in_place(newtype.concrete_base_type(db)); } - _ => builder = builder.add_positive(element), + _ => builder.add_positive_in_place(element), } } for &element in negative { - builder = builder.add_negative(element); + builder.add_negative_in_place(element); } builder.build() diff --git a/crates/ty_python_semantic/src/types/set_theoretic/builder.rs b/crates/ty_python_semantic/src/types/set_theoretic/builder.rs index fc8d939b81..d80b97d412 100644 --- a/crates/ty_python_semantic/src/types/set_theoretic/builder.rs +++ b/crates/ty_python_semantic/src/types/set_theoretic/builder.rs @@ -36,7 +36,10 @@ //! 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 redundancy checks. +use std::hint::cold_path; + use super::RecursivelyDefined; + use crate::types::enums::EnumComplement; use crate::types::regex; use crate::types::set_theoretic::expand_intersection_typevars_and_newtypes; @@ -45,7 +48,7 @@ use crate::types::{ KnownInstanceType, LiteralValueType, LiteralValueTypeKind, NegativeIntersectionElements, StringLiteralType, SubclassOfType, Type, TypeVarBoundOrConstraints, TypeVarVariance, UnionType, }; -use crate::{Db, FxOrderMap, FxOrderSet}; +use crate::{Db, FxOrderMap, FxOrderSet, ProgramEnvironment}; use rustc_hash::FxHashSet; use smallvec::SmallVec; @@ -60,13 +63,14 @@ use smallvec::SmallVec; /// This only recognizes the "single truthiness guard" forms used by truthiness narrowing. fn split_truthiness_guarded_intersection<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, ) -> Option<(Type<'db>, Type<'db>)> { let Type::Intersection(intersection) = ty else { return None; }; - let falsy = Type::AlwaysTruthy.negate(db); - let truthy = Type::AlwaysFalsy.negate(db); + let falsy = Type::AlwaysTruthy.negate(db, env); + let truthy = Type::AlwaysFalsy.negate(db, env); let negative = intersection.negative(db); let has_not_truthy = negative.contains(&Type::AlwaysTruthy); @@ -77,9 +81,9 @@ fn split_truthiness_guarded_intersection<'db>( _ => return None, }; - let mut core = IntersectionBuilder::new(db); + let mut core = IntersectionBuilder::new(db, env); for positive in intersection.positive(db) { - core = core.add_positive(*positive); + core.add_positive_in_place(*positive); } for negative in negative { if (guard == falsy && *negative == Type::AlwaysTruthy) @@ -87,7 +91,7 @@ fn split_truthiness_guarded_intersection<'db>( { continue; } - core = core.add_negative(*negative); + core.add_negative_in_place(*negative); } Some((core.build(), guard)) } @@ -97,11 +101,12 @@ fn split_truthiness_guarded_intersection<'db>( /// `list[Any]` is an invariant-dynamic generalization of `list[int]`. fn is_invariant_dynamic_generalization_of<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, general: Type<'db>, specific: Type<'db>, ) -> bool { // Fast path to avoid performance regressions. - if !general.has_dynamic(db) { + if !general.has_dynamic(db, env) { return false; } @@ -113,8 +118,8 @@ fn is_invariant_dynamic_generalization_of<'db>( Some((general_class, general_specialization)), Some((specific_class, specific_specialization)), ) = ( - general.class_specialization(db), - specific.class_specialization(db), + general.class_specialization(db, env), + specific.class_specialization(db, env), ) else { return false; @@ -165,22 +170,23 @@ fn is_invariant_dynamic_generalization_of<'db>( /// Discussion: fn merge_truthiness_guarded_pair<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, left: Type<'db>, right: Type<'db>, ) -> Option> { - let (left_core, left_guard) = split_truthiness_guarded_intersection(db, left)?; - let (right_core, right_guard) = split_truthiness_guarded_intersection(db, right)?; + let (left_core, left_guard) = split_truthiness_guarded_intersection(db, env, left)?; + let (right_core, right_guard) = split_truthiness_guarded_intersection(db, env, right)?; if left_guard == right_guard { return None; } - if left_core.is_equivalent_to(db, right_core) { + if left_core.is_equivalent_to(db, env, right_core) { return Some(left_core); } - let candidate = UnionType::from_elements(db, [left_core, right_core]); - let left_reconstructed = IntersectionType::from_two_elements(db, candidate, left_guard); - let right_reconstructed = IntersectionType::from_two_elements(db, candidate, right_guard); + let candidate = UnionType::from_elements(db, env, [left_core, right_core]); + let left_reconstructed = IntersectionType::from_two_elements(db, env, candidate, left_guard); + let right_reconstructed = IntersectionType::from_two_elements(db, env, candidate, right_guard); if left_reconstructed == left && right_reconstructed == right { Some(candidate) } else { @@ -193,11 +199,16 @@ fn merge_truthiness_guarded_pair<'db>( /// /// Hashability does not obey normal inheritance rules: subclasses of hashable classes can be /// unhashable. Keeping the non-final type allows downstream checks to consider it independently. -fn should_preserve_hashable_union(db: &dyn Db, left: Type, right: Type) -> bool { +fn should_preserve_hashable_union( + db: &dyn Db, + env: &ProgramEnvironment<'_>, + left: Type, + right: Type, +) -> bool { let is_hashable = |ty| matches!(ty, Type::ProtocolInstance(protocol) if protocol.is_hashable(db)); let is_non_final_nominal_instance = - |ty| matches!(ty, Type::NominalInstance(instance) if !instance.class(db).is_final(db)); + |ty| matches!(ty, Type::NominalInstance(instance) if !instance.class(db, env).is_final(db)); (is_hashable(left) && is_non_final_nominal_instance(right)) || (is_hashable(right) && is_non_final_nominal_instance(left)) @@ -218,7 +229,11 @@ fn should_preserve_hashable_union(db: &dyn Db, left: Type, right: Type) -> bool /// /// # (Color excluding RED) | Literal[Color.RED] simplifies to Color. /// ``` -fn normalize_enum_complement_unions<'db>(db: &'db dyn Db, types: &mut Vec>) -> bool { +fn normalize_enum_complement_unions<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + types: &mut Vec>, +) -> bool { for complement_index in 0..types.len() { let Type::EnumComplement(complement) = types[complement_index] else { continue; @@ -265,16 +280,16 @@ fn normalize_enum_complement_unions<'db>(db: &'db dyn Db, types: &mut Vec UnionElement<'db> { fn try_reduce( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, other_type: Type<'db>, cycle_recovery: bool, ) -> ReduceResult<'db> { + if let UnionElement::Type(existing) = self { + return ReduceResult::Type(*existing); + } + if cycle_recovery { + cold_path(); + // A widened literal group must absorb matching literals from later iterations for // recovery to converge. Preserve that exact fallback reduction without relation queries. return match self { - UnionElement::Type(existing) => ReduceResult::Type(*existing), UnionElement::IntLiterals(_) => { ReduceResult::KeepIf(!other_type.is_instance_of(db, KnownClass::Int)) } @@ -399,8 +420,9 @@ impl<'db> UnionElement<'db> { UnionElement::EnumLiterals { enum_class, .. } => ReduceResult::KeepIf( other_type .as_nominal_instance() - .is_none_or(|instance| instance.class_literal(db) != *enum_class), + .is_none_or(|instance| instance.class_literal(db, env) != *enum_class), ), + UnionElement::Type(_) => unreachable!("ordinary types are handled before recovery"), }; } @@ -428,17 +450,22 @@ impl<'db> UnionElement<'db> { // both `ignore` and `collapse` are `false`. If either is `true`, // we skip the expensive redundancy check and return `true`. let mut should_retain_type = |ty| { - if ignore || other_type.is_redundant_with(db, ty) { + if ignore || other_type.is_redundant_with(db, env, ty) { ignore = true; return true; } if collapse - || other_type.negation_is_subtype_of_cached(db, ty, &mut other_type_negated_cache) + || other_type.negation_is_subtype_of_cached( + db, + env, + ty, + &mut other_type_negated_cache, + ) { collapse = true; return true; } - !ty.is_redundant_with(db, other_type) + !ty.is_redundant_with(db, env, other_type) }; let should_keep = match self { @@ -451,7 +478,7 @@ impl<'db> UnionElement<'db> { } else { let (literal, promotable) = literals.first().unwrap(); !Type::from(LiteralValueType::new(*literal, *promotable)) - .is_redundant_with(db, other_type) + .is_redundant_with(db, env, other_type) } } UnionElement::StringLiterals(literals) => { @@ -463,7 +490,7 @@ impl<'db> UnionElement<'db> { } else { let (literal, promotable) = literals.first().unwrap(); !Type::from(LiteralValueType::new(*literal, *promotable)) - .is_redundant_with(db, other_type) + .is_redundant_with(db, env, other_type) } } UnionElement::BytesLiterals(literals) => { @@ -475,7 +502,7 @@ impl<'db> UnionElement<'db> { } else { let (literal, promotable) = literals.first().unwrap(); !Type::from(LiteralValueType::new(*literal, *promotable)) - .is_redundant_with(db, other_type) + .is_redundant_with(db, env, other_type) } } UnionElement::EnumLiterals { @@ -493,10 +520,10 @@ impl<'db> UnionElement<'db> { } else { let (literal, promotable) = literals.first().unwrap(); !Type::from(LiteralValueType::new(*literal, *promotable)) - .is_redundant_with(db, other_type) + .is_redundant_with(db, env, other_type) } } - UnionElement::Type(existing) => return ReduceResult::Type(*existing), + UnionElement::Type(_) => unreachable!("ordinary types are handled before reduction"), }; if ignore { @@ -534,6 +561,7 @@ const MAX_NON_RECURSIVE_UNION_LITERALS: usize = 8192; pub(crate) struct UnionBuilder<'db> { elements: Vec>, db: &'db dyn Db, + env: ProgramEnvironment<'db>, unpack_aliases: bool, /// This is enabled when joining types in a `cycle_recovery` function. Because recovery cannot /// introduce a new cycle, relation-based union simplifications are skipped in this mode. @@ -556,13 +584,13 @@ impl<'db> UnionAccumulator<'db> { UnionAccumulator::One(ty) } - pub(crate) fn add(&mut self, db: &'db dyn Db, ty: Type<'db>) { + pub(crate) fn add(&mut self, db: &'db dyn Db, env: &ProgramEnvironment<'db>, ty: Type<'db>) { match self { UnionAccumulator::One(existing) => { *self = UnionAccumulator::Two(*existing, ty); } UnionAccumulator::Two(first, second) => { - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); builder.add_in_place(*first); builder.add_in_place(*second); builder.add_in_place(ty); @@ -572,35 +600,43 @@ impl<'db> UnionAccumulator<'db> { } } - pub(crate) fn get_or_build(&mut self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn get_or_build( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { match self { UnionAccumulator::One(ty) => *ty, UnionAccumulator::Two(first, second) => { - let ty = UnionType::from_two_elements(db, *first, *second); + let ty = UnionType::from_two_elements(db, env, *first, *second); *self = UnionAccumulator::One(ty); ty } UnionAccumulator::Deferred(_) => { - let ty = std::mem::replace(self, UnionAccumulator::new(Type::Never)).into_type(db); + let ty = + std::mem::replace(self, UnionAccumulator::new(Type::Never)).into_type(db, env); *self = UnionAccumulator::new(ty); ty } } } - pub(crate) fn into_type(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn into_type(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match self { UnionAccumulator::One(ty) => ty, - UnionAccumulator::Two(first, second) => UnionType::from_two_elements(db, first, second), + UnionAccumulator::Two(first, second) => { + UnionType::from_two_elements(db, env, first, second) + } UnionAccumulator::Deferred(builder) => builder.build(), } } } impl<'db> UnionBuilder<'db> { - pub(crate) fn new(db: &'db dyn Db) -> Self { + pub(crate) fn new(db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Self { Self { db, + env: env.clone(), elements: vec![], unpack_aliases: true, cycle_recovery: false, @@ -637,21 +673,22 @@ impl<'db> UnionBuilder<'db> { } fn widen_literal_types(&mut self, seen_aliases: &mut Vec>) { + let db = self.db; let mut replace_with = vec![]; for elem in &self.elements { match elem { UnionElement::IntLiterals(_) => { - replace_with.push(KnownClass::Int.to_instance(self.db)); + replace_with.push(KnownClass::Int.to_instance(db, &self.env)); } UnionElement::StringLiterals(_) => { - replace_with.push(KnownClass::Str.to_instance(self.db)); + replace_with.push(KnownClass::Str.to_instance(db, &self.env)); } UnionElement::BytesLiterals(_) => { - replace_with.push(KnownClass::Bytes.to_instance(self.db)); + replace_with.push(KnownClass::Bytes.to_instance(db, &self.env)); } UnionElement::EnumLiterals { literals, .. } => { let (enum_literal, _) = literals.first().unwrap(); - replace_with.push(enum_literal.enum_class_instance(self.db)); + replace_with.push(enum_literal.enum_class_instance(db, &self.env)); } UnionElement::Type(_) => {} } @@ -672,7 +709,8 @@ impl<'db> UnionBuilder<'db> { self.add_in_place_impl(ty, &mut vec![]); } - pub(crate) fn add_in_place_impl(&mut self, ty: Type<'db>, seen_aliases: &mut Vec>) { + fn add_in_place_impl(&mut self, ty: Type<'db>, seen_aliases: &mut Vec>) { + let db = self.db; let cycle_recovery = self.cycle_recovery; let should_widen = |literals, recursively_defined: RecursivelyDefined| { if recursively_defined.is_yes() && cycle_recovery { @@ -683,18 +721,17 @@ impl<'db> UnionBuilder<'db> { }; let mut ty_negated_cache = None; - let mut ty_negated = || *ty_negated_cache.get_or_insert_with(|| ty.negate(self.db)); + let mut ty_negated = || *ty_negated_cache.get_or_insert_with(|| ty.negate(db, &self.env)); match ty { Type::Union(union) => { - let new_elements = union.elements(self.db); + let new_elements = union.elements(db); self.elements.reserve(new_elements.len()); for element in new_elements { self.add_in_place_impl(*element, seen_aliases); } - self.recursively_defined = self - .recursively_defined - .or(union.recursively_defined(self.db)); + self.recursively_defined = + self.recursively_defined.or(union.recursively_defined(db)); if self.cycle_recovery && self.recursively_defined.is_yes() { let literals = self.elements.iter().fold(0, |acc, elem| match elem { UnionElement::IntLiterals(literals) => acc + literals.len(), @@ -716,7 +753,7 @@ impl<'db> UnionBuilder<'db> { // leave out the recursive alias. TODO surface this error. } else { seen_aliases.push(ty); - self.add_in_place_impl(alias.value_type(self.db), seen_aliases); + self.add_in_place_impl(alias.value_type(db), seen_aliases); } } Type::LiteralValue(literal) => { @@ -734,7 +771,8 @@ impl<'db> UnionBuilder<'db> { match element { UnionElement::StringLiterals(literals) => { if should_widen(literals.len(), self.recursively_defined) { - let replace_with = KnownClass::Str.to_instance(self.db); + let replace_with = + KnownClass::Str.to_instance(db, &self.env); self.add_in_place_impl(replace_with, seen_aliases); return; } @@ -743,21 +781,22 @@ impl<'db> UnionBuilder<'db> { } UnionElement::Type(existing) if cycle_recovery - && literal.fallback_instance(self.db) == *existing => + && literal.fallback_instance(db, &self.env) + == *existing => { return; } UnionElement::Type(existing) if !cycle_recovery => { // e.g. `existing` could be `Literal[""] & Any`, // and `ty` could be `Literal[""]` - if ty.is_redundant_with(self.db, *existing) { + if ty.is_redundant_with(db, &self.env, *existing) { return; } - if existing.is_redundant_with(self.db, ty) { + if existing.is_redundant_with(db, &self.env, ty) { to_remove = Some(index); continue; } - if ty_negated().is_subtype_of(self.db, *existing) { + if ty_negated().is_subtype_of(db, &self.env, *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(); @@ -787,7 +826,8 @@ impl<'db> UnionBuilder<'db> { match element { UnionElement::BytesLiterals(literals) => { if should_widen(literals.len(), self.recursively_defined) { - let replace_with = KnownClass::Bytes.to_instance(self.db); + let replace_with = + KnownClass::Bytes.to_instance(db, &self.env); self.add_in_place_impl(replace_with, seen_aliases); return; } @@ -796,21 +836,22 @@ impl<'db> UnionBuilder<'db> { } UnionElement::Type(existing) if cycle_recovery - && literal.fallback_instance(self.db) == *existing => + && literal.fallback_instance(db, &self.env) + == *existing => { return; } UnionElement::Type(existing) if !cycle_recovery => { - if ty.is_redundant_with(self.db, *existing) { + if ty.is_redundant_with(db, &self.env, *existing) { return; } // e.g. `existing` could be `Literal[b""] & Any`, // and `ty` could be `Literal[b""]` - if existing.is_redundant_with(self.db, ty) { + if existing.is_redundant_with(db, &self.env, ty) { to_remove = Some(index); continue; } - if ty_negated().is_subtype_of(self.db, *existing) { + if ty_negated().is_subtype_of(db, &self.env, *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(); @@ -842,7 +883,8 @@ impl<'db> UnionBuilder<'db> { match element { UnionElement::IntLiterals(literals) => { if should_widen(literals.len(), self.recursively_defined) { - let replace_with = KnownClass::Int.to_instance(self.db); + let replace_with = + KnownClass::Int.to_instance(db, &self.env); self.add_in_place_impl(replace_with, seen_aliases); return; } @@ -851,21 +893,22 @@ impl<'db> UnionBuilder<'db> { } UnionElement::Type(existing) if cycle_recovery - && literal.fallback_instance(self.db) == *existing => + && literal.fallback_instance(db, &self.env) + == *existing => { return; } UnionElement::Type(existing) if !cycle_recovery => { - if ty.is_redundant_with(self.db, *existing) { + if ty.is_redundant_with(db, &self.env, *existing) { return; } // e.g. `existing` could be `Literal[1] & Any`, // and `ty` could be `Literal[1]` - if existing.is_redundant_with(self.db, ty) { + if existing.is_redundant_with(db, &self.env, ty) { to_remove = Some(index); continue; } - if ty_negated().is_subtype_of(self.db, *existing) { + if ty_negated().is_subtype_of(db, &self.env, *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(); @@ -891,15 +934,14 @@ impl<'db> UnionBuilder<'db> { } } LiteralValueTypeKind::Enum(enum_member_to_add) => { - let enum_class_literal = enum_member_to_add.enum_class_literal(self.db); - let enum_class = enum_class_literal.class_literal(self.db); - let enum_member_count = enum_class_literal.member_count(self.db); - let members_are_exhaustive = - enum_class_literal.members_are_exhaustive(self.db); + let enum_class_literal = enum_member_to_add.enum_class_literal(db); + let enum_class = enum_class_literal.class_literal(db); + let enum_member_count = enum_class_literal.member_count(db); + let members_are_exhaustive = enum_class_literal.members_are_exhaustive(db); if members_are_exhaustive && enum_member_count == 1 { self.add_in_place_impl( - enum_member_to_add.enum_class_instance(self.db), + enum_member_to_add.enum_class_instance(db, &self.env), seen_aliases, ); return; @@ -918,7 +960,8 @@ impl<'db> UnionBuilder<'db> { } if should_widen(literals.len(), self.recursively_defined) { let (literal, _) = literals.first().unwrap(); - let replace_with = literal.enum_class_instance(self.db); + let replace_with = + literal.enum_class_instance(db, &self.env); self.add_in_place_impl(replace_with, seen_aliases); return; } @@ -927,21 +970,22 @@ impl<'db> UnionBuilder<'db> { } UnionElement::Type(existing) if cycle_recovery - && literal.fallback_instance(self.db) == *existing => + && literal.fallback_instance(db, &self.env) + == *existing => { return; } UnionElement::Type(existing) if !cycle_recovery => { - if ty.is_redundant_with(self.db, *existing) { + if ty.is_redundant_with(db, &self.env, *existing) { return; } // e.g. `existing` could be `Literal[Foo.X] & Any`, // and `ty` could be `Literal[Foo.X]` - if existing.is_redundant_with(self.db, ty) { + if existing.is_redundant_with(db, &self.env, ty) { to_remove = Some(index); continue; } - if ty_negated().is_subtype_of(self.db, *existing) { + if ty_negated().is_subtype_of(db, &self.env, *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(); @@ -958,7 +1002,7 @@ impl<'db> UnionBuilder<'db> { if members_are_exhaustive && found.len() == enum_member_count { self.add_in_place_impl( - enum_member_to_add.enum_class_instance(self.db), + enum_member_to_add.enum_class_instance(db, &self.env), seen_aliases, ); return; @@ -991,6 +1035,8 @@ impl<'db> UnionBuilder<'db> { } fn push_type(&mut self, ty: Type<'db>, seen_aliases: &mut Vec>) { + let env = &self.env; + let db = self.db; let mut ty = ty; let bool_pair = |ty: Type<'db>| { if let Some(LiteralValueTypeKind::Bool(b)) = ty.as_literal_value_kind() { @@ -1009,7 +1055,7 @@ impl<'db> UnionBuilder<'db> { let mut to_remove = SmallVec::<[usize; 2]>::new(); for (i, element) in self.elements.iter_mut().enumerate() { - let element_type = match element.try_reduce(self.db, ty, self.cycle_recovery) { + let element_type = match element.try_reduce(db, &self.env, ty, self.cycle_recovery) { ReduceResult::KeepIf(keep) => { if !keep { to_remove.push(i); @@ -1035,7 +1081,9 @@ impl<'db> UnionBuilder<'db> { return; } - if !self.cycle_recovery && should_preserve_hashable_union(self.db, ty, element_type) { + if !self.cycle_recovery + && should_preserve_hashable_union(db, &self.env, ty, element_type) + { continue; } @@ -1050,7 +1098,7 @@ impl<'db> UnionBuilder<'db> { && left != right { to_remove.push(i); - ty = KnownClass::Range.to_instance(self.db); + ty = KnownClass::Range.to_instance(db, &self.env); continue; } @@ -1058,7 +1106,8 @@ impl<'db> UnionBuilder<'db> { // have different capture groups describe the same set of objects; the // group refinement is what differs, and neither answer holds for the // union, so fall back to the unrefined instance. - if let Some(merged_type) = regex::merge_differing_groups(self.db, ty, element_type) { + if let Some(merged_type) = regex::merge_differing_groups(self.db, env, ty, element_type) + { to_remove.push(i); ty = merged_type; continue; @@ -1066,7 +1115,8 @@ impl<'db> UnionBuilder<'db> { // Fold `(T & ~AlwaysTruthy) | (T & ~AlwaysFalsy)` to `T`. if !self.cycle_recovery - && let Some(merged_type) = merge_truthiness_guarded_pair(self.db, ty, element_type) + && let Some(merged_type) = + merge_truthiness_guarded_pair(db, &self.env, ty, element_type) { to_remove.push(i); ty = merged_type; @@ -1079,7 +1129,7 @@ impl<'db> UnionBuilder<'db> { .zip(bool_pair(ty)) .is_some_and(|(element, pair)| element == pair) { - self.add_in_place_impl(KnownClass::Bool.to_instance(self.db), seen_aliases); + self.add_in_place_impl(KnownClass::Bool.to_instance(db, &self.env), seen_aliases); return; } @@ -1092,16 +1142,16 @@ impl<'db> UnionBuilder<'db> { } if should_simplify_full && !matches!(element_type, Type::TypeAlias(_)) { - if ty.is_redundant_with(self.db, element_type) { + if ty.is_redundant_with(db, &self.env, element_type) { return; } - if element_type.is_redundant_with(self.db, ty) { + if element_type.is_redundant_with(db, &self.env, ty) { to_remove.push(i); continue; } - if ty.negation_is_subtype_of_cached(self.db, element_type, &mut ty_negated) { + if ty.negation_is_subtype_of_cached(db, &self.env, element_type, &mut ty_negated) { // 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 @@ -1135,6 +1185,7 @@ impl<'db> UnionBuilder<'db> { pub(crate) fn try_build(self) -> Option> { let db = self.db; + let unpack_aliases = self.unpack_aliases; let cycle_recovery = self.cycle_recovery; let recursively_defined = self.recursively_defined; @@ -1179,8 +1230,8 @@ impl<'db> UnionBuilder<'db> { } } - if normalize_enum_complement_unions(db, &mut types) { - let builder = UnionBuilder::new(db) + if normalize_enum_complement_unions(db, &self.env, &mut types) { + let builder = UnionBuilder::new(db, &self.env) .unpack_aliases(unpack_aliases) .cycle_recovery(cycle_recovery) .recursively_defined(recursively_defined); @@ -1211,19 +1262,22 @@ pub(crate) struct IntersectionBuilder<'db> { // create a union of intersections. intersections: Vec>, db: &'db dyn Db, + env: ProgramEnvironment<'db>, } impl<'db> IntersectionBuilder<'db> { - pub(crate) fn new(db: &'db dyn Db) -> Self { + pub(crate) fn new(db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Self { Self { db, + env: env.clone(), intersections: vec![InnerIntersectionBuilder::default()], } } - fn empty(db: &'db dyn Db) -> Self { + fn empty(db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Self { Self { db, + env: env.clone(), intersections: vec![], } } @@ -1239,15 +1293,17 @@ impl<'db> IntersectionBuilder<'db> { ); } - pub(crate) fn add_positive(self, ty: Type<'db>) -> Self { - self.add_positive_impl(ty, &mut vec![]) + pub(crate) fn add_positive(mut self, ty: Type<'db>) -> Self { + self.add_positive_in_place(ty); + self + } + + pub(crate) fn add_positive_in_place(&mut self, ty: Type<'db>) { + self.add_positive_impl(ty, &mut vec![]); } - pub(crate) fn add_positive_impl( - mut self, - ty: Type<'db>, - seen_aliases: &mut Vec>, - ) -> Self { + fn add_positive_impl(&mut self, ty: Type<'db>, seen_aliases: &mut Vec>) { + let db = self.db; match ty { Type::TypeAlias(alias) => { if seen_aliases.contains(&ty) { @@ -1255,11 +1311,11 @@ impl<'db> IntersectionBuilder<'db> { for inner in &mut self.intersections { inner.positive.insert(ty); } - return self; + return; } seen_aliases.push(ty); - let value_type = alias.value_type(self.db); - self.add_positive_impl(value_type, seen_aliases) + let value_type = alias.value_type(db); + self.add_positive_impl(value_type, seen_aliases); } Type::Union(union) => { // Distribute ourself over this union: for each union element, clone ourself and @@ -1270,50 +1326,48 @@ impl<'db> IntersectionBuilder<'db> { // (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_impl(*elem, seen_aliases)) - .fold(IntersectionBuilder::empty(self.db), |mut builder, sub| { - builder.extend(sub); - builder - }) + let mut distributed = IntersectionBuilder::empty(db, &self.env); + for elem in union.elements(db) { + let mut branch = self.clone(); + branch.add_positive_impl(*elem, seen_aliases); + distributed.extend(branch); + } + self.intersections = distributed.intersections; } // `(A & B & ~C) & (D & E & ~F)` -> `A & B & D & E & ~C & ~F` Type::Intersection(other) => { - let db = self.db; for pos in other.positive(db) { - self = self.add_positive_impl(*pos, seen_aliases); + self.add_positive_impl(*pos, seen_aliases); } for neg in other.negative(db) { - self = self.add_negative_impl(*neg, seen_aliases); + self.add_negative_impl(*neg, seen_aliases); } - self } Type::EnumComplement(complement) => { - let db = self.db; - self.add_positive_impl(complement.to_intersection(db), seen_aliases) + let intersection = complement.to_intersection(db, &self.env); + self.add_positive_impl(intersection, seen_aliases); } _ => { // 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); + inner.add_positive(db, &self.env, ty); } - self } } } - pub(crate) fn add_negative(self, ty: Type<'db>) -> Self { - self.add_negative_impl(ty, &mut vec![]) + pub(crate) fn add_negative(mut self, ty: Type<'db>) -> Self { + self.add_negative_in_place(ty); + self + } + + pub(crate) fn add_negative_in_place(&mut self, ty: Type<'db>) { + self.add_negative_impl(ty, &mut vec![]); } - pub(crate) fn add_negative_impl( - mut self, - ty: Type<'db>, - seen_aliases: &mut Vec>, - ) -> Self { + fn add_negative_impl(&mut self, ty: Type<'db>, seen_aliases: &mut Vec>) { + let db = self.db; // See comments above in `add_positive`; this is just the negated version. match ty { Type::TypeAlias(alias) => { @@ -1322,17 +1376,16 @@ impl<'db> IntersectionBuilder<'db> { for inner in &mut self.intersections { inner.negative.insert(ty); } - return self; + return; } seen_aliases.push(ty); - let value_type = alias.value_type(self.db); - self.add_negative_impl(value_type, seen_aliases) + let value_type = alias.value_type(db); + self.add_negative_impl(value_type, seen_aliases); } Type::Union(union) => { - for elem in union.elements(self.db) { - self = self.add_negative_impl(*elem, seen_aliases); + for elem in union.elements(db) { + self.add_negative_impl(*elem, seen_aliases); } - self } Type::Intersection(intersection) => { // (A | B) & ~(C & ~D) @@ -1342,41 +1395,29 @@ impl<'db> IntersectionBuilder<'db> { // 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_impl(*elem, &mut seen_aliases.clone()) - }); - - let negative_side = intersection - .negative(self.db) - .iter() - // all negative constraints end up becoming positive constraints - .map(|elem| { - self.clone() - .add_positive_impl(*elem, &mut seen_aliases.clone()) - }); - - positive_side.chain(negative_side).fold( - IntersectionBuilder::empty(self.db), - |mut builder, sub| { - builder.extend(sub); - builder - }, - ) + let mut distributed = IntersectionBuilder::empty(db, &self.env); + // We negate all the positive constraints while distributing. + for elem in intersection.positive(db) { + let mut branch = self.clone(); + branch.add_negative_impl(*elem, &mut seen_aliases.clone()); + distributed.extend(branch); + } + // All negative constraints end up becoming positive constraints. + for elem in intersection.negative(db) { + let mut branch = self.clone(); + branch.add_positive_impl(*elem, &mut seen_aliases.clone()); + distributed.extend(branch); + } + self.intersections = distributed.intersections; } Type::EnumComplement(complement) => { - let db = self.db; - self.add_negative_impl(complement.to_intersection(db), seen_aliases) + let intersection = complement.to_intersection(db, &self.env); + self.add_negative_impl(intersection, seen_aliases); } _ => { for inner in &mut self.intersections { - inner.add_negative(self.db, ty); + inner.add_negative(db, &self.env, ty); } - self } } } @@ -1387,17 +1428,19 @@ impl<'db> IntersectionBuilder<'db> { T: Into>, { for element in elements { - self = self.add_positive(element.into()); + self.add_positive_in_place(element.into()); } self } pub(crate) fn build(self) -> Type<'db> { + let db = self.db; UnionType::from_elements( - self.db, + db, + &self.env, self.intersections .into_iter() - .map(|inner| inner.build(self.db)), + .map(|inner| inner.build(db, &self.env)), ) } } @@ -1429,13 +1472,14 @@ impl<'db> InnerIntersectionBuilder<'db> { /// if color is not Color.RED and color is not Color.BLUE: /// reveal_type(color) # Never /// ``` - fn has_empty_enum_complement(&self, db: &'db dyn Db) -> bool { + fn has_empty_enum_complement(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool { for positive in &self.positive { let Type::NominalInstance(instance) = positive else { continue; }; - let Some(enum_class_literal) = instance.class_literal(db).into_enum_class(db) else { + let Some(enum_class_literal) = instance.class_literal(db, env).into_enum_class(db) + else { continue; }; if !enum_class_literal.members_are_exhaustive(db) { @@ -1474,7 +1518,12 @@ 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>) { + fn add_positive( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + mut new_positive: Type<'db>, + ) { // `Never & T` -> `Never` if self.positive.contains(&Type::Never) { return; @@ -1506,8 +1555,11 @@ impl<'db> InnerIntersectionBuilder<'db> { Type::TypeForm(typeform) => { if let Some(narrowed) = SubclassOfType::try_from_instance( db, + env, typeform.type_argument(db).resolve_type_alias(db), - ) && self.positive.swap_remove(&KnownClass::Type.to_instance(db)) + ) && self + .positive + .swap_remove(&KnownClass::Type.to_instance(db, env)) { new_positive = narrowed; } @@ -1520,6 +1572,7 @@ impl<'db> InnerIntersectionBuilder<'db> { .find_map(|(index, positive)| match positive { Type::TypeForm(typeform) => SubclassOfType::try_from_instance( db, + env, typeform.type_argument(db).resolve_type_alias(db), ) .map(|narrowed| (index, narrowed)), @@ -1536,39 +1589,39 @@ impl<'db> InnerIntersectionBuilder<'db> { match new_positive { // `LiteralString & AlwaysTruthy` -> `LiteralString & ~Literal[""]` Type::AlwaysTruthy if self.positive.contains(&Type::literal_string()) => { - self.add_negative(db, Type::string_literal(db, "")); + self.add_negative(db, env, Type::string_literal(db, "")); } // `LiteralString & AlwaysFalsy` -> `Literal[""]` Type::AlwaysFalsy if self.positive.swap_remove(&Type::literal_string()) => { - self.add_positive(db, Type::string_literal(db, "")); + self.add_positive(db, env, Type::string_literal(db, "")); } // `AlwaysTruthy & LiteralString` -> `LiteralString & ~Literal[""]` Type::LiteralValue(literal) if literal.is_literal_string() && self.positive.swap_remove(&Type::AlwaysTruthy) => { - self.add_positive(db, Type::literal_string()); - self.add_negative(db, Type::string_literal(db, "")); + self.add_positive(db, env, Type::literal_string()); + self.add_negative(db, env, Type::string_literal(db, "")); } // `AlwaysFalsy & LiteralString` -> `Literal[""]` Type::LiteralValue(literal) if literal.is_literal_string() && self.positive.swap_remove(&Type::AlwaysFalsy) => { - self.add_positive(db, Type::string_literal(db, "")); + self.add_positive(db, env, Type::string_literal(db, "")); } // `LiteralString & ~AlwaysTruthy` -> `LiteralString & AlwaysFalsy` -> `Literal[""]` Type::LiteralValue(literal) if literal.is_literal_string() && self.negative.swap_remove(&Type::AlwaysTruthy) => { - self.add_positive(db, Type::string_literal(db, "")); + self.add_positive(db, env, Type::string_literal(db, "")); } // `LiteralString & ~AlwaysFalsy` -> `LiteralString & ~Literal[""]` Type::LiteralValue(literal) if literal.is_literal_string() && self.negative.swap_remove(&Type::AlwaysFalsy) => { - self.add_positive(db, Type::literal_string()); - self.add_negative(db, Type::string_literal(db, "")); + self.add_positive(db, env, Type::literal_string()); + self.add_negative(db, env, Type::string_literal(db, "")); } _ => { @@ -1644,9 +1697,10 @@ impl<'db> InnerIntersectionBuilder<'db> { let mut to_remove = SmallVec::<[usize; 1]>::new(); for (index, existing_positive) in self.positive.iter().enumerate() { // S & T = S if S <: T or T is an invariant-dynamic generalization of S. - if existing_positive.is_redundant_with(db, new_positive) + if existing_positive.is_redundant_with(db, env, new_positive) || is_invariant_dynamic_generalization_of( db, + env, new_positive, *existing_positive, ) @@ -1654,9 +1708,10 @@ impl<'db> InnerIntersectionBuilder<'db> { return; } // same rule, reverse order - if new_positive.is_redundant_with(db, *existing_positive) + if new_positive.is_redundant_with(db, env, *existing_positive) || is_invariant_dynamic_generalization_of( db, + env, *existing_positive, new_positive, ) @@ -1664,7 +1719,7 @@ impl<'db> InnerIntersectionBuilder<'db> { to_remove.push(index); } // A & B = Never if A and B are disjoint - if new_positive.is_disjoint_from(db, *existing_positive) { + if new_positive.is_disjoint_from(db, env, *existing_positive) { *self = Self::default(); self.positive.insert(Type::Never); return; @@ -1677,13 +1732,13 @@ impl<'db> InnerIntersectionBuilder<'db> { 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) { + if new_positive.is_subtype_of(db, env, *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) { + if existing_negative.is_disjoint_from(db, env, new_positive) { to_remove.push(index); } } @@ -1697,7 +1752,12 @@ impl<'db> InnerIntersectionBuilder<'db> { } /// Adds a negative type to this intersection. - fn add_negative(&mut self, db: &'db dyn Db, new_negative: Type<'db>) { + fn add_negative( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + new_negative: Type<'db>, + ) { // `Never & ~T` -> `Never`. if self.positive.contains(&Type::Never) { return; @@ -1726,10 +1786,10 @@ impl<'db> InnerIntersectionBuilder<'db> { match new_negative { Type::Intersection(inter) => { for pos in inter.positive(db) { - self.add_negative(db, *pos); + self.add_negative(db, env, *pos); } for neg in inter.negative(db) { - self.add_positive(db, *neg); + self.add_positive(db, env, *neg); } } Type::Never => { @@ -1744,31 +1804,31 @@ impl<'db> InnerIntersectionBuilder<'db> { // 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); + self.add_positive(db, env, ty); } // `bool & ~AlwaysTruthy` -> `bool & Literal[False]` Type::AlwaysTruthy if contains_bool() => { - self.add_positive(db, Type::bool_literal(false)); + self.add_positive(db, env, Type::bool_literal(false)); } // `bool & ~Literal[True]` -> `bool & Literal[False]` Type::LiteralValue(literal) if literal.as_bool() == Some(true) && contains_bool() => { - self.add_positive(db, Type::bool_literal(false)); + self.add_positive(db, env, Type::bool_literal(false)); } // `LiteralString & ~AlwaysTruthy` -> `LiteralString & Literal[""]` Type::AlwaysTruthy if self.positive.contains(&Type::literal_string()) => { - self.add_positive(db, Type::string_literal(db, "")); + self.add_positive(db, env, Type::string_literal(db, "")); } // `bool & ~AlwaysFalsy` -> `bool & Literal[True]` Type::AlwaysFalsy if contains_bool() => { - self.add_positive(db, Type::bool_literal(true)); + self.add_positive(db, env, Type::bool_literal(true)); } // `bool & ~Literal[False]` -> `bool & Literal[True]` Type::LiteralValue(literal) if literal.as_bool() == Some(false) && contains_bool() => { - self.add_positive(db, Type::bool_literal(true)); + self.add_positive(db, env, Type::bool_literal(true)); } // `LiteralString & ~AlwaysFalsy` -> `LiteralString & ~Literal[""]` Type::AlwaysFalsy if self.positive.contains(&Type::literal_string()) => { - self.add_negative(db, Type::string_literal(db, "")); + self.add_negative(db, env, Type::string_literal(db, "")); } _ => { let new_negative_enum = new_negative.as_enum_literal(); @@ -1788,11 +1848,11 @@ impl<'db> InnerIntersectionBuilder<'db> { } // ~S & ~T = ~T if S <: T - if existing_negative.is_redundant_with(db, new_negative) { + if existing_negative.is_redundant_with(db, env, new_negative) { to_remove.push(index); } // same rule, reverse order - if new_negative.is_subtype_of(db, *existing_negative) { + if new_negative.is_subtype_of(db, env, *existing_negative) { return; } } @@ -1815,7 +1875,7 @@ impl<'db> InnerIntersectionBuilder<'db> { if existing_positive .as_nominal_instance() .is_some_and(|instance| { - instance.class_literal(db) == new_enum.enum_class(db) + instance.class_literal(db, env) == new_enum.enum_class(db) }) { continue; @@ -1823,13 +1883,13 @@ impl<'db> InnerIntersectionBuilder<'db> { } // S & ~T = Never if S <: T - if existing_positive.is_subtype_of(db, new_negative) { + if existing_positive.is_subtype_of(db, env, 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) { + if existing_positive.is_disjoint_from(db, env, new_negative) { return; } } @@ -1850,7 +1910,7 @@ impl<'db> InnerIntersectionBuilder<'db> { /// /// - 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) { + fn simplify_constrained_typevars(&mut self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) { let mut to_add = SmallVec::<[Type<'db>; 1]>::new(); for ty in &self.positive { @@ -1858,7 +1918,7 @@ impl<'db> InnerIntersectionBuilder<'db> { continue; }; let Some(TypeVarBoundOrConstraints::Constraints(constraints)) = - bound_typevar.typevar(db).bound_or_constraints(db) + bound_typevar.typevar(db).bound_or_constraints(db, env) else { continue; }; @@ -1872,7 +1932,7 @@ impl<'db> InnerIntersectionBuilder<'db> { let matching_constraints = constraints .iter() .enumerate() - .filter(|(_, c)| c.is_subtype_of(db, *negative)); + .filter(|(_, c)| c.is_subtype_of(db, env, *negative)); for (constraint_index, _) in matching_constraints { remaining_constraints[constraint_index] = None; } @@ -1900,16 +1960,16 @@ impl<'db> InnerIntersectionBuilder<'db> { } for remaining_constraint in to_add { - self.add_positive(db, remaining_constraint); + self.add_positive(db, env, remaining_constraint); } } - fn build(mut self, db: &'db dyn Db) -> Type<'db> { - if self.has_empty_enum_complement(db) { + fn build(mut self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + if self.has_empty_enum_complement(db, env) { return Type::Never; } - self.simplify_constrained_typevars(db); + self.simplify_constrained_typevars(db, env); // If any typevars are in `self.positive`, speculatively solve all bounded type variables // to their upper bound and all constrained type variables to the union of their constraints. @@ -1921,14 +1981,25 @@ impl<'db> InnerIntersectionBuilder<'db> { .any(|ty| matches!(ty, Type::TypeVar(_) | Type::NewTypeInstance(_))) { let speculative = - expand_intersection_typevars_and_newtypes(db, &self.positive, &self.negative); + expand_intersection_typevars_and_newtypes(db, env, &self.positive, &self.negative); if speculative.is_never() { return Type::Never; } + + if let Type::EnumComplement(complement) = speculative + && complement.is_singleton(db) + && self + .positive + .iter() + .any(|positive| matches!(positive, Type::NewTypeInstance(_))) + { + // Preserve the NewType while making its remaining enum member explicit. + self.add_positive(db, env, complement.remaining_literal_union(db, env)); + } } if let Some(complement) = - EnumComplement::from_intersection_parts(db, &self.positive, &self.negative) + EnumComplement::from_intersection_parts(db, env, &self.positive, &self.negative) { return Type::EnumComplement(complement); } @@ -1960,53 +2031,62 @@ mod tests { use ruff_db::system::DbWithWritableSystem as _; use ty_module_resolver::KnownModule; + use ty_python_core::ProgramFile; #[test] fn build_union_no_elements() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); - let empty_union = UnionBuilder::new(&db).build(); + let empty_union = UnionBuilder::new(db, &env).build(); assert_eq!(empty_union, Type::Never); } #[test] fn build_union_single_element() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); let t0 = Type::int_literal(0); - let union = UnionType::from_elements(&db, [t0]); + let union = UnionType::from_elements(db, &env, [t0]); assert_eq!(union, t0); } #[test] fn build_union_two_elements() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); let t0 = Type::int_literal(0); let t1 = Type::int_literal(1); - let union = UnionType::from_elements(&db, [t0, t1]).expect_union(); + let union = UnionType::from_elements(db, &env, [t0, t1]).expect_union(); - assert_eq!(union.elements(&db), &[t0, t1]); + assert_eq!(union.elements(db), &[t0, t1]); } #[test] fn cycle_recovery_widens_recursive_literal_union() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); let literal_limit = i64::try_from(MAX_RECURSIVE_UNION_LITERALS).expect("literal limit fits in i64"); let union = (0..=literal_limit).map(Type::int_literal).fold( - UnionBuilder::new(&db) + UnionBuilder::new(db, &env) .cycle_recovery(true) .recursively_defined(RecursivelyDefined::Yes), UnionBuilder::add, ); - assert_eq!(union.build(), KnownClass::Int.to_instance(&db)); + assert_eq!(union.build(), KnownClass::Int.to_instance(db, &env)); let assert_widens = |literal, instance| { for (first, second) in [(literal, instance), (instance, literal)] { - let union = UnionBuilder::new(&db) + let union = UnionBuilder::new(db, &env) .cycle_recovery(true) .add(first) .add(second) @@ -2015,49 +2095,56 @@ mod tests { } }; - assert_widens(Type::int_literal(1), KnownClass::Int.to_instance(&db)); + assert_widens(Type::int_literal(1), KnownClass::Int.to_instance(db, &env)); assert_widens( - Type::string_literal(&db, "literal"), - KnownClass::Str.to_instance(&db), + Type::string_literal(db, "literal"), + KnownClass::Str.to_instance(db, &env), ); assert_widens( - Type::bytes_literal(&db, b"literal"), - KnownClass::Bytes.to_instance(&db), + Type::bytes_literal(db, b"literal"), + KnownClass::Bytes.to_instance(db, &env), ); - let safe_uuid_class = known_module_symbol(&db, KnownModule::Uuid, "SafeUUID") + let safe_uuid_class = known_module_symbol(db, &env, KnownModule::Uuid, "SafeUUID") .place .expect_type() .expect_class_literal(); - let enum_literal = enum_member_literals(&db, safe_uuid_class, None) + let enum_literal = enum_member_literals(db, safe_uuid_class, None) .expect("SafeUUID is an enum") .next() .expect("SafeUUID has members"); assert_widens( enum_literal, - enum_literal.expect_enum_literal().enum_class_instance(&db), + enum_literal + .expect_enum_literal() + .enum_class_instance(db, &env), ); } #[test] fn cycle_recovery_skips_other_redundancy_simplification() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); for (left, right) in [ - (Type::string_literal(&db, "literal"), Type::literal_string()), - (Type::bool_literal(true), KnownClass::Bool.to_instance(&db)), + (Type::string_literal(db, "literal"), Type::literal_string()), + ( + Type::bool_literal(true), + KnownClass::Bool.to_instance(db, &env), + ), (Type::int_literal(1), Type::object()), (Type::bool_literal(true), Type::bool_literal(false)), ] { for (first, second) in [(left, right), (right, left)] { - let union = UnionBuilder::new(&db) + let union = UnionBuilder::new(db, &env) .cycle_recovery(true) .add(first) .add(second) .build() .expect_union(); - assert!(union.elements(&db).contains(&left)); - assert!(union.elements(&db).contains(&right)); + assert!(union.elements(db).contains(&left)); + assert!(union.elements(db).contains(&right)); } } } @@ -2065,31 +2152,35 @@ mod tests { #[test] fn union_common_literal_supertype() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); let str_union = UnionType::from_elements( - &db, - [ - Type::string_literal(&db, "a"), - Type::string_literal(&db, "b"), - ], + db, + &env, + [Type::string_literal(db, "a"), Type::string_literal(db, "b")], ) .expect_union(); assert_eq!( - str_union.common_literal_supertype(&db), + str_union.common_literal_supertype(db, &env), Some(Type::literal_string()) ); - let int_union = UnionType::from_elements(&db, [Type::int_literal(1), Type::int_literal(2)]) - .expect_union(); + let int_union = + UnionType::from_elements(db, &env, [Type::int_literal(1), Type::int_literal(2)]) + .expect_union(); assert_eq!( - int_union.common_literal_supertype(&db), - Some(KnownClass::Int.to_instance(&db)) + int_union.common_literal_supertype(db, &env), + Some(KnownClass::Int.to_instance(db, &env)) ); - let mixed_union = - UnionType::from_elements(&db, [Type::string_literal(&db, "a"), Type::int_literal(1)]) - .expect_union(); - assert_eq!(mixed_union.common_literal_supertype(&db), None); + let mixed_union = UnionType::from_elements( + db, + &env, + [Type::string_literal(db, "a"), Type::int_literal(1)], + ) + .expect_union(); + assert_eq!(mixed_union.common_literal_supertype(db, &env), None); } fn map_marker<'db>(ty: &Type<'db>, marker: Type<'db>, replacement: Type<'db>) -> Type<'db> { @@ -2099,26 +2190,32 @@ mod tests { #[test] fn map_rebuilds_prefix_for_literal_widening() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); - let marker = KnownClass::Str.to_instance(&db); + let marker = KnownClass::Str.to_instance(db, &env); let literal_limit = i64::try_from(MAX_NON_RECURSIVE_UNION_LITERALS).expect("literal limit fits in i64"); let widening_literal = Type::int_literal(literal_limit); - let expected = KnownClass::Int.to_instance(&db); + let expected = KnownClass::Int.to_instance(db, &env); let elements = (0..literal_limit).map(Type::int_literal).chain([marker]); - let union = UnionType::from_elements(&db, elements).expect_union(); + let union = UnionType::from_elements(db, &env, elements).expect_union(); assert_eq!( - union.map(&db, |ty| map_marker(ty, marker, widening_literal)), + union.map(db, &env, |ty| map_marker(ty, marker, widening_literal)), expected ); assert_eq!( - union.map_leave_aliases(&db, |ty| map_marker(ty, marker, widening_literal)), + union.map_leave_aliases(db, &env, |ty| map_marker(ty, marker, widening_literal)), expected ); assert_eq!( - union.try_map(&db, |ty| Some(map_marker(ty, marker, widening_literal))), + union.try_map(db, &env, |ty| Some(map_marker( + ty, + marker, + widening_literal + ))), Some(expected) ); } @@ -2127,8 +2224,10 @@ mod tests { fn map_preserves_alias_unpacking_behavior() { let mut db = setup_db(); db.write_dedented("/src/a.py", "type Alias = int").unwrap(); + let env = db.program_environment(); let module = ruff_db::files::system_path_to_file(&db, "/src/a.py").unwrap(); + let module = ProgramFile::new(&db, module, db.program_environment().program(&db)); let alias_ty = global_symbol(&db, module, "Alias").place.expect_type(); let Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(alias))) = alias_ty @@ -2137,35 +2236,42 @@ mod tests { }; let alias = Type::TypeAlias(TypeAliasType::PEP695(alias)); - let str_instance = KnownClass::Str.to_instance(&db); - let union_ty = UnionType::from_elements_leave_aliases(&db, [alias, str_instance]); + let str_instance = KnownClass::Str.to_instance(&db, &env); + let union_ty = UnionType::from_elements_leave_aliases(&db, &env, [alias, str_instance]); let union = union_ty.expect_union(); - let unpacked = - UnionType::from_elements(&db, [KnownClass::Int.to_instance(&db), str_instance]); + let unpacked = UnionType::from_elements( + &db, + &env, + [KnownClass::Int.to_instance(&db, &env), str_instance], + ); - assert_eq!(union.map(&db, |ty| *ty), unpacked); - assert_eq!(union.try_map(&db, |ty| Some(*ty)), Some(unpacked)); - assert_eq!(union.map_leave_aliases(&db, |ty| *ty), union_ty); + assert_eq!(union.map(&db, &env, |ty| *ty), unpacked); + assert_eq!(union.try_map(&db, &env, |ty| Some(*ty)), Some(unpacked)); + assert_eq!(union.map_leave_aliases(&db, &env, |ty| *ty), union_ty); } #[test] fn build_intersection_empty_intersection_equals_object() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); - let intersection = IntersectionBuilder::new(&db).build(); + let intersection = IntersectionBuilder::new(db, &env).build(); assert_eq!(intersection, Type::object()); } #[test] fn build_intersection_discards_never_dnf_branches() { let db = setup_db(); - let int = KnownClass::Int.to_instance(&db); - let str = KnownClass::Str.to_instance(&db); - let bytes = KnownClass::Bytes.to_instance(&db); - - let int_or_str = UnionType::from_elements(&db, [int, str]); - let int_or_bytes = UnionType::from_elements(&db, [int, bytes]); - let intersection = IntersectionBuilder::new(&db) + let db = &db; + let env = db.program_environment(); + let int = KnownClass::Int.to_instance(db, &env); + let str = KnownClass::Str.to_instance(db, &env); + let bytes = KnownClass::Bytes.to_instance(db, &env); + + let int_or_str = UnionType::from_elements(db, &env, [int, str]); + let int_or_bytes = UnionType::from_elements(db, &env, [int, bytes]); + let intersection = IntersectionBuilder::new(db, &env) .add_positive(int_or_str) .add_positive(int_or_bytes); @@ -2184,36 +2290,37 @@ mod tests { } fn build_intersection_simplify_split_bool_impl(db: &TestDb, t_splitter: Type) { - let bool_value = t_splitter.bool(db) == Truthiness::AlwaysTrue; + let env = db.program_environment(); + let bool_value = t_splitter.bool(db, &env) == 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(); - let t_bool = KnownClass::Bool.to_instance(db); + let t_bool = KnownClass::Bool.to_instance(db, &env); - let ty = IntersectionBuilder::new(db) + let ty = IntersectionBuilder::new(db, &env) .add_positive(t_object) .add_positive(t_bool) .add_negative(t_splitter) .build(); assert_eq!(ty, Type::bool_literal(!bool_value)); - let ty = IntersectionBuilder::new(db) + let ty = IntersectionBuilder::new(db, &env) .add_positive(t_bool) .add_positive(t_object) .add_negative(t_splitter) .build(); assert_eq!(ty, Type::bool_literal(!bool_value)); - let ty = IntersectionBuilder::new(db) + let ty = IntersectionBuilder::new(db, &env) .add_positive(t_object) .add_negative(t_splitter) .add_positive(t_bool) .build(); assert_eq!(ty, Type::bool_literal(!bool_value)); - let ty = IntersectionBuilder::new(db) + let ty = IntersectionBuilder::new(db, &env) .add_negative(t_splitter) .add_positive(t_object) .add_positive(t_bool) @@ -2224,73 +2331,82 @@ mod tests { #[test] fn build_intersection_enums() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); - let safe_uuid_class = known_module_symbol(&db, KnownModule::Uuid, "SafeUUID") + let safe_uuid_class = known_module_symbol(db, &env, KnownModule::Uuid, "SafeUUID") .place .ignore_possibly_undefined() .unwrap(); - let literals = enum_member_literals(&db, safe_uuid_class.expect_class_literal(), None) + let literals = enum_member_literals(db, safe_uuid_class.expect_class_literal(), None) .unwrap() .collect::>(); assert_eq!(literals.len(), 3); // SafeUUID.safe let l_safe = literals[0]; - assert_eq!(l_safe.expect_enum_literal().name(&db), "safe"); + assert_eq!(l_safe.expect_enum_literal().name(db), "safe"); // SafeUUID.unsafe let l_unsafe = literals[1]; - assert_eq!(l_unsafe.expect_enum_literal().name(&db), "unsafe"); + assert_eq!(l_unsafe.expect_enum_literal().name(db), "unsafe"); // SafeUUID.unknown let l_unknown = literals[2]; - assert_eq!(l_unknown.expect_enum_literal().name(&db), "unknown"); + assert_eq!(l_unknown.expect_enum_literal().name(db), "unknown"); // The enum itself: SafeUUID - let safe_uuid = l_safe.expect_enum_literal().enum_class_instance(&db); + let safe_uuid = l_safe.expect_enum_literal().enum_class_instance(db, &env); { - let actual = IntersectionBuilder::new(&db) + let actual = IntersectionBuilder::new(db, &env) .add_positive(safe_uuid) .add_negative(l_safe) .build(); assert_eq!( - actual.display(&db).to_string(), + actual.display(db, &db.program_environment()).to_string(), "Literal[SafeUUID.unsafe, SafeUUID.unknown]" ); } { // Same as above, but with the order reversed - let actual = IntersectionBuilder::new(&db) + let actual = IntersectionBuilder::new(db, &env) .add_negative(l_safe) .add_positive(safe_uuid) .build(); assert_eq!( - actual.display(&db).to_string(), + actual.display(db, &db.program_environment()).to_string(), "Literal[SafeUUID.unsafe, SafeUUID.unknown]" ); } { // Also the same, but now with a nested intersection - let actual = IntersectionBuilder::new(&db) + let actual = IntersectionBuilder::new(db, &env) .add_positive(safe_uuid) - .add_positive(IntersectionBuilder::new(&db).add_negative(l_safe).build()) + .add_positive( + IntersectionBuilder::new(db, &env) + .add_negative(l_safe) + .build(), + ) .build(); assert_eq!( - actual.display(&db).to_string(), + actual.display(db, &db.program_environment()).to_string(), "Literal[SafeUUID.unsafe, SafeUUID.unknown]" ); } { - let actual = IntersectionBuilder::new(&db) + let actual = IntersectionBuilder::new(db, &env) .add_negative(l_safe) .add_positive(safe_uuid) .add_negative(l_unsafe) .build(); - assert_eq!(actual.display(&db).to_string(), "Literal[SafeUUID.unknown]"); + assert_eq!( + actual.display(db, &db.program_environment()).to_string(), + "Literal[SafeUUID.unknown]" + ); } } } diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index de905e3a69..b420c67760 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -10,8 +10,10 @@ //! argument types and return types. For each callable type in the union, the call expression's //! arguments must match _at least one_ overload. +use crate::ProgramEnvironment; use crate::types::any_over_type; use std::fmt; +use std::num::NonZeroU32; use std::slice::Iter; use std::sync::Arc; @@ -24,10 +26,11 @@ use crate::types::UnpackedKwargs; use crate::types::callable::{CallableFunctionProvenance, CallableTypeKind}; use crate::types::constraints::{ ConstraintSet, ConstraintSetBuilder, IteratorConstraintsExtension, OwnedConstraintSet, + PathBounds, Solutions, }; use crate::types::cyclic::ActiveRecursionDetector; use crate::types::generics::{ - ApplySpecialization, GenericContext, InferableTypeVars, Specialization, TypeVarInference, + ApplySpecialization, GenericContext, Specialization, SpecializationBuilder, TypeVarInference, walk_generic_context, }; use crate::types::infer::{TypeExpressionFlags, infer_deferred_types}; @@ -38,7 +41,9 @@ use crate::types::relation::{ }; use crate::types::tuple::{Tuple, TupleType, VariableSegment}; use crate::types::typed_dict::extract_unpacked_typed_dict_keys_from_kwargs_annotation; -use crate::types::typevar::{TypeVarKind, max_typevar_freshness_matching_generic_context}; +use crate::types::typevar::{ + TypeVarInstance, TypeVarKind, TypeVarSet, max_typevar_freshness_matching_generic_context, +}; use crate::types::{ ApplyTypeMappingVisitor, BindingContext, BoundTypeVarIdentity, BoundTypeVarInstance, CallableType, ErrorContext, ErrorContextTree, FindLegacyTypeVarsVisitor, KnownClass, @@ -74,7 +79,7 @@ fn function_signature_expression_type<'db>( definition: Definition<'db>, expression: &ast::Expr, ) -> Type<'db> { - let file = definition.file(db); + let file = definition.program_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); @@ -92,7 +97,7 @@ fn function_signature_type_expression_flags<'db>( definition: Definition<'db>, expression: &ast::Expr, ) -> TypeExpressionFlags { - let file = definition.file(db); + let file = definition.program_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); @@ -116,6 +121,7 @@ pub struct CallableSignature<'db> { fn merge_receiver_constraints<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, first: Option<&OwnedConstraintSet<'db>>, second: Option<&OwnedConstraintSet<'db>>, ) -> Option> { @@ -132,8 +138,8 @@ fn merge_receiver_constraints<'db>( let constraints = ConstraintSetBuilder::new(); Some(constraints.into_owned(|builder| { builder - .load(db, first) - .and(db, builder, || builder.load(db, second)) + .load(db, env, first) + .and(db, builder, || builder.load(db, env, second)) })) } } @@ -177,10 +183,14 @@ impl<'db> CallableSignature<'db> { Self::single(Signature::bottom()) } - pub(crate) fn cycle_initial(db: &'db dyn Db, id: salsa::Id) -> Self { + pub(crate) fn cycle_initial( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + id: salsa::Id, + ) -> Self { Self::single(Signature::new( Parameters::bottom(), - Type::divergent(id).bottom_materialization(db), + Type::divergent(id).bottom_materialization(db, env), )) } @@ -211,11 +221,17 @@ impl<'db> CallableSignature<'db> { } /// Returns the union of all overload return types, or `Unknown` if there are no overloads. - pub(crate) fn overload_return_type_or_unknown(&self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn overload_return_type_or_unknown( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { match self.overloads.as_slice() { [] => Type::unknown(), [signature] => signature.return_ty, - overloads => UnionType::from_elements(db, overloads.iter().map(|sig| sig.return_ty)), + overloads => { + UnionType::from_elements(db, env, overloads.iter().map(|sig| sig.return_ty)) + } } } @@ -234,6 +250,7 @@ impl<'db> CallableSignature<'db> { /// Returns the reduced overloaded signature exposed by a `functools.partial(...)` object. pub(crate) fn partially_apply( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, overloads: impl IntoIterator>, ) -> Option { let mut new_overloads = Vec::new(); @@ -242,11 +259,15 @@ impl<'db> CallableSignature<'db> { for overload in overloads { let signature = overload.signature.partially_apply( db, + env, &overload.partial_application, overload.inference, overload.unspecialized_return_ty, ); - let dedup_key = signature.clone().with_definition(None); + let dedup_key = signature + .clone() + .with_definition(None) + .with_source_overload_index(None); if seen_overloads.insert(dedup_key) { new_overloads.push(signature); } @@ -258,6 +279,7 @@ impl<'db> CallableSignature<'db> { pub(crate) fn cycle_normalized( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, previous: &Self, cycle: &salsa::Cycle, ) -> Self { @@ -267,7 +289,7 @@ impl<'db> CallableSignature<'db> { .overloads .iter() .zip(previous.overloads.iter()) - .map(|(curr, prev)| curr.cycle_normalized(db, prev, cycle)) + .map(|(curr, prev)| curr.cycle_normalized(db, env, prev, cycle)) .collect(), } } else { @@ -279,6 +301,7 @@ impl<'db> CallableSignature<'db> { pub(super) fn recursive_type_normalized_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -286,7 +309,7 @@ impl<'db> CallableSignature<'db> { overloads: self .overloads .iter() - .map(|signature| signature.recursive_type_normalized_impl(db, div, nested)) + .map(|signature| signature.recursive_type_normalized_impl(db, env, div, nested)) .collect::>>()?, }) } @@ -296,22 +319,26 @@ impl<'db> CallableSignature<'db> { db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { + #[expect(clippy::too_many_arguments)] fn try_apply_type_mapping_for_paramspec<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, self_signature: &Signature<'db>, prefix_parameters: &[Parameter<'db>], paramspec_value: Type<'db>, type_mapping: &TypeMapping<'_, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Option> { match paramspec_value { Type::TypeVar(typevar) if typevar.is_parameter_pack(db) => { let prefix_parameters = prefix_parameters .iter() - .map(|param| param.apply_type_mapping_impl(db, type_mapping, tcx, visitor)) + .map(|param| { + param.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor) + }) .collect::>(); let parameters = if prefix_parameters.is_empty() { Parameters::paramspec(db, typevar) @@ -323,13 +350,16 @@ impl<'db> CallableSignature<'db> { ) }; + let env = visitor.env; Some(CallableSignature::single(Signature { generic_context: self_signature.generic_context.map(|context| { - type_mapping.update_signature_generic_context(db, context) + type_mapping.update_signature_generic_context(db, env, context) }), definition: self_signature.definition, + source_overload_index: self_signature.source_overload_index, receiver_constraints: self_signature.map_receiver_constraints( db, + env, type_mapping, tcx, visitor, @@ -337,6 +367,7 @@ impl<'db> CallableSignature<'db> { parameters, return_ty: self_signature.return_ty.apply_type_mapping_impl( db, + env, type_mapping, tcx, visitor, @@ -347,36 +378,47 @@ impl<'db> CallableSignature<'db> { Type::Callable(callable) if matches!(callable.kind(db), CallableTypeKind::ParamSpecValue) => { + let env = visitor.env; Some(CallableSignature::from_overloads( callable.signatures(db).iter().map(|signature| Signature { generic_context: GenericContext::merge_optional( db, signature.generic_context, self_signature.generic_context.map(|context| { - type_mapping.update_signature_generic_context(db, context) + type_mapping.update_signature_generic_context(db, env, context) }), ), definition: signature.definition, + source_overload_index: signature.source_overload_index, receiver_constraints: { let mapped = self_signature.map_receiver_constraints( db, + env, type_mapping, tcx, visitor, ); merge_receiver_constraints( db, + env, signature.receiver_constraints.as_ref(), mapped.as_ref(), ) }, parameters: signature.parameters().with_prefix( prefix_parameters.iter().map(|param| { - param.apply_type_mapping_impl(db, type_mapping, tcx, visitor) + param.apply_type_mapping_impl( + db, + env, + type_mapping, + tcx, + visitor, + ) }), ), return_ty: self_signature.return_ty.apply_type_mapping_impl( db, + env, type_mapping, tcx, visitor, @@ -388,6 +430,7 @@ impl<'db> CallableSignature<'db> { _ => None, } } + let env = visitor.env; if let TypeMapping::ApplySpecialization(specialization) | TypeMapping::ApplySpecializationWithMaterialization { specialization, .. } = @@ -398,6 +441,7 @@ impl<'db> CallableSignature<'db> { && let Some(value) = specialization.get(db, paramspec) && let Some(result) = try_apply_type_mapping_for_paramspec( db, + env, signature, prefix, value, @@ -428,20 +472,26 @@ impl<'db> CallableSignature<'db> { pub(crate) fn find_legacy_typevars_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, ) { for signature in &self.overloads { - signature.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + signature.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } } /// Binds the first (presumably `self`) parameter of this signature. If a `self_type` is /// provided, we will replace any occurrences of `typing.Self` in the parameter and return /// annotations with that type. - pub(crate) fn bind_self(&self, db: &'db dyn Db, self_type: Option>) -> Self { - self.bind_self_with_receiver(db, self_type, self_type) + pub(crate) fn bind_self( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + self_type: Option>, + ) -> Self { + self.bind_self_with_receiver(db, env, self_type, self_type) } /// Binds the receiver using its runtime type while using `typing_self_type` to replace @@ -452,6 +502,7 @@ impl<'db> CallableSignature<'db> { pub(crate) fn bind_self_with_receiver( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, receiver_type: Option>, typing_self_type: Option>, ) -> Self { @@ -460,7 +511,7 @@ impl<'db> CallableSignature<'db> { .overloads .iter() .map(|signature| { - signature.bind_self_with_receiver(db, receiver_type, typing_self_type) + signature.bind_self_with_receiver(db, env, receiver_type, typing_self_type) }) .collect(), } @@ -477,6 +528,7 @@ impl<'db> CallableSignature<'db> { pub(crate) fn apply_self_with_receiver( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, receiver_type: Type<'db>, self_type: Type<'db>, ) -> Self { @@ -484,7 +536,9 @@ impl<'db> CallableSignature<'db> { overloads: self .overloads .iter() - .map(|signature| signature.apply_self_with_receiver(db, receiver_type, self_type)) + .map(|signature| { + signature.apply_self_with_receiver(db, env, receiver_type, self_type) + }) .collect(), } } @@ -512,14 +566,16 @@ impl<'db> CallableSignature<'db> { pub(crate) fn when_constraint_set_assignable_to<'c>( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, other: &Self, constraints: &'c ConstraintSetBuilder<'db>, ) -> ConstraintSet<'db, 'c> { let relation_visitor = HasRelationToVisitor::default(constraints); let disjointness_visitor = IsDisjointVisitor::default(constraints); let signature_relation_visitor = SignatureRelationVisitor::default(); - let materialization_visitor = ApplyTypeMappingVisitor::default(); + let materialization_visitor = ApplyTypeMappingVisitor::new(env); let checker = TypeRelationChecker::constraint_set_assignability( + env, constraints, &relation_visitor, &disjointness_visitor, @@ -541,10 +597,15 @@ impl<'a, 'db> IntoIterator for &'a CallableSignature<'db> { impl<'db> VarianceInferable<'db> for &CallableSignature<'db> { // TODO: possibly need to replace self - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { + fn variance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { self.overloads .iter() - .map(|signature| signature.variance_of(db, typevar)) + .map(|signature| signature.variance_of(db, env, typevar)) .collect() } } @@ -559,6 +620,12 @@ pub struct Signature<'db> { /// This is useful for locating and extracting docstring information for the signature. pub(crate) definition: Option>, + /// Position of this overload in the original function definition. + /// + /// Filtering, receiver binding, and partial application can leave a signature at a different + /// position in the active overload list. Preserve its source position for call diagnostics. + source_overload_index: Option, + /// The constraint introduced by binding an explicitly annotated receiver, if any. receiver_constraints: Option>, @@ -808,6 +875,7 @@ impl<'db> Signature<'db> { Self { generic_context: None, definition: None, + source_overload_index: None, receiver_constraints: None, parameters, return_ty, @@ -823,6 +891,7 @@ impl<'db> Signature<'db> { Self { generic_context, definition: None, + source_overload_index: None, receiver_constraints: None, parameters, return_ty, @@ -835,6 +904,7 @@ impl<'db> Signature<'db> { Signature { generic_context: None, definition: None, + source_overload_index: None, receiver_constraints: None, parameters: Parameters::gradual_form(), return_ty: signature_type, @@ -845,6 +915,7 @@ impl<'db> Signature<'db> { /// Return a typed signature from a function definition. pub(super) fn from_function( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, pep695_generic_context: Option>, definition: Definition<'db>, function_node: &ast::StmtFunctionDef, @@ -860,7 +931,7 @@ impl<'db> Signature<'db> { let return_ty = if function_node.is_asserts_return { // basedpython: `-> asserts x` names a place, not a type. such a function // returns `None` — it raises when the assertion doesn't hold - Type::none(db) + Type::none(db, env) } else { function_node .returns @@ -897,6 +968,7 @@ impl<'db> Signature<'db> { Self { generic_context, definition: Some(definition), + source_overload_index: None, receiver_constraints: None, parameters, return_ty, @@ -904,9 +976,33 @@ impl<'db> Signature<'db> { } } - pub(super) fn wrap_coroutine_return_type(self, db: &'db dyn Db) -> Self { - let return_ty = KnownClass::CoroutineType - .to_specialized_instance(db, &[Type::any(), Type::any(), self.return_ty]); + /// Returns the binding referenced by a direct `P.args` or `P.kwargs` variadic parameter. + /// + /// Returns `None` if this signature has no `ParamSpec` component parameters, or if none of + /// their `ParamSpec`s has the same identity as `typevar`. + /// + /// This also exposes captured bindings that are intentionally absent from the function's own + /// generic context. + pub(super) fn paramspec_component_binding( + &self, + db: &'db dyn Db, + typevar: TypeVarInstance<'db>, + ) -> Option> { + self.parameters + .paramspec_component_bindings(db) + .find(|bound| bound.typevar(db).identity(db) == typevar.identity(db)) + } + + pub(super) fn wrap_coroutine_return_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Self { + let return_ty = KnownClass::CoroutineType.to_specialized_instance( + db, + env, + &[Type::any(), Type::any(), self.return_ty], + ); Self { return_ty, ..self } } @@ -925,15 +1021,18 @@ impl<'db> Signature<'db> { /// `Self` is hidden if it does not appear in: /// 1. The return type /// 2. Any explicitly annotated parameter (not inferred) - pub(crate) fn should_hide_self_from_display(&self, db: &'db dyn Db) -> bool { - !self.return_ty.contains_self(db) - && !self - .parameters() - .iter() - .any(|p| p.should_annotation_be_displayed() && p.annotated_type().contains_self(db)) + pub(crate) fn should_hide_self_from_display( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> bool { + !self.return_ty.contains_self(db, env) + && !self.parameters().iter().any(|p| { + p.should_annotation_be_displayed() && p.annotated_type().contains_self(db, env) + }) } - pub(crate) fn with_inherited_generic_context( + fn with_inherited_generic_context( mut self, db: &'db dyn Db, inherited_generic_context: GenericContext<'db>, @@ -949,17 +1048,23 @@ impl<'db> Signature<'db> { self } - fn cycle_normalized(&self, db: &'db dyn Db, previous: &Self, cycle: &salsa::Cycle) -> Self { + fn cycle_normalized( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + previous: &Self, + cycle: &salsa::Cycle, + ) -> Self { let return_ty = self .return_ty - .cycle_normalized(db, previous.return_ty, cycle); + .cycle_normalized(db, env, previous.return_ty, cycle); let parameters = if self.parameters.len() == previous.parameters.len() { Parameters::new( self.parameters .iter() .zip(previous.parameters.iter()) - .map(|(curr, prev)| curr.cycle_normalized(db, prev, cycle)) + .map(|(curr, prev)| curr.cycle_normalized(db, env, prev, cycle)) .collect::>(), self.parameters.kind(), ) @@ -971,6 +1076,7 @@ impl<'db> Signature<'db> { Self { generic_context: self.generic_context, definition: self.definition, + source_overload_index: self.source_overload_index, receiver_constraints: self.receiver_constraints.clone(), parameters, return_ty, @@ -978,30 +1084,32 @@ impl<'db> Signature<'db> { } } - pub(super) fn recursive_type_normalized_impl( + fn recursive_type_normalized_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { let return_ty = if nested { self.return_ty - .recursive_type_normalized_impl(db, div, true)? + .recursive_type_normalized_impl(db, env, div, true)? } else { self.return_ty - .recursive_type_normalized_impl(db, div, true) + .recursive_type_normalized_impl(db, env, div, true) .unwrap_or(div) }; let parameters = { let mut parameters = Vec::with_capacity(self.parameters.len()); for param in &self.parameters { - parameters.push(param.recursive_type_normalized_impl(db, div, nested)?); + parameters.push(param.recursive_type_normalized_impl(db, env, div, nested)?); } Parameters::new(parameters, self.parameters.kind()) }; Some(Self { generic_context: self.generic_context, definition: self.definition, + source_overload_index: self.source_overload_index, receiver_constraints: self.receiver_constraints.clone(), parameters, return_ty, @@ -1009,30 +1117,47 @@ impl<'db> Signature<'db> { }) } - pub(crate) fn apply_type_mapping_impl<'a>( + pub(super) fn apply_type_mapping_impl<'a>( &self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { + let env = visitor.env; Self { generic_context: self .generic_context - .map(|context| type_mapping.update_signature_generic_context(db, context)), + .map(|context| type_mapping.update_signature_generic_context(db, env, context)), definition: self.definition, - receiver_constraints: self.map_receiver_constraints(db, type_mapping, tcx, visitor), - parameters: self - .parameters - .apply_type_mapping_impl(db, type_mapping, tcx, visitor), + source_overload_index: self.source_overload_index, + receiver_constraints: self.map_receiver_constraints( + db, + env, + type_mapping, + tcx, + visitor, + ), + parameters: self.parameters.apply_type_mapping_impl( + db, + env, + type_mapping, + tcx, + visitor, + ), return_ty: self .return_ty - .apply_type_mapping_impl(db, type_mapping, tcx, visitor), + .apply_type_mapping_impl(db, env, type_mapping, tcx, visitor), narrowing_guards: self.narrowing_guards.clone(), } } - pub(crate) fn freshen_bound_typevars(&self, db: &'db dyn Db, delta: u32) -> Self { + pub(crate) fn freshen_bound_typevars( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + delta: u32, + ) -> Self { let Some(generic_context) = self.generic_context else { return self.clone(); }; @@ -1044,11 +1169,11 @@ impl<'db> Signature<'db> { delta, }, TypeContext::default(), - &ApplyTypeMappingVisitor::default(), + &ApplyTypeMappingVisitor::new(env), ) } - pub(crate) fn max_typevar_freshness_matching_generic_context( + fn max_typevar_freshness_matching_generic_context( &self, db: &'db dyn Db, generic_context: GenericContext<'db>, @@ -1072,26 +1197,28 @@ impl<'db> Signature<'db> { pub(crate) fn find_legacy_typevars_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, ) { for ty in self.receiver_constraint_types() { - ty.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + ty.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } for param in &self.parameters { param.annotated_type().find_legacy_typevars_impl( db, + env, binding_context, typevars, visitor, ); if let Some(ty) = param.default_type() { - ty.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + ty.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } } self.return_ty - .find_legacy_typevars_impl(db, binding_context, typevars, visitor); + .find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } /// Return the parameters in this signature. @@ -1107,6 +1234,7 @@ impl<'db> Signature<'db> { pub(crate) fn add_implicit_self_annotation( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, self_type: impl FnOnce() -> Option>, ) { if let Some(first_parameter) = self.parameters.data.value.first() @@ -1138,12 +1266,14 @@ impl<'db> Signature<'db> { Some(generic_context) => { *generic_context = GenericContext::from_typevar_instances( db, + env, std::iter::once(self_typevar).chain(generic_context.variables(db)), ); } None => { self.generic_context = Some(GenericContext::from_typevar_instances( db, + env, std::iter::once(self_typevar), )); } @@ -1179,6 +1309,7 @@ impl<'db> Signature<'db> { pub(crate) fn inherit_unannotated_from_overloads( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, overload_signatures: &[Signature<'db>], inherit_return: bool, ) { @@ -1206,13 +1337,13 @@ impl<'db> Signature<'db> { } } if !tys.is_empty() { - impl_param.annotated_type = UnionType::from_elements(db, tys); + impl_param.annotated_type = UnionType::from_elements(db, env, tys); impl_param.inferred_annotation = false; } } if inherit_return && !overload_signatures.is_empty() { self.return_ty = - UnionType::from_elements(db, overload_signatures.iter().map(|s| s.return_ty)); + UnionType::from_elements(db, env, overload_signatures.iter().map(|s| s.return_ty)); } } @@ -1231,6 +1362,7 @@ impl<'db> Signature<'db> { pub(crate) fn open_unannotated_parameter_holes( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, function: Definition<'db>, binds_receiver: bool, ) { @@ -1265,9 +1397,10 @@ impl<'db> Signature<'db> { self.generic_context = Some(match self.generic_context { Some(generic_context) => GenericContext::from_typevar_instances( db, + env, generic_context.variables(db).chain(holes), ), - None => GenericContext::from_typevar_instances(db, holes), + None => GenericContext::from_typevar_instances(db, env, holes), }); } @@ -1276,8 +1409,13 @@ impl<'db> Signature<'db> { self.definition } - pub(crate) fn bind_self(&self, db: &'db dyn Db, self_type: Option>) -> Self { - self.bind_self_with_receiver(db, self_type, self_type) + pub(crate) fn bind_self( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + self_type: Option>, + ) -> Self { + self.bind_self_with_receiver(db, env, self_type, self_type) } /// Binds the receiver while preserving the relation between its runtime type and annotation. @@ -1287,6 +1425,7 @@ impl<'db> Signature<'db> { pub(crate) fn bind_self_with_receiver( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, receiver_type: Option>, typing_self_type: Option>, ) -> Self { @@ -1310,15 +1449,22 @@ impl<'db> Signature<'db> { Type::TypeVar(BoundTypeVarInstance::synthetic_self( db, Type::object(), - BindingContext::Synthetic, + BindingContext::Synthetic(env.program(db)), )) }); let annotation = if let Some(typing_self_type) = typing_self_type { - let mapping = - TypeMapping::BindSelf(SelfBinding::new(db, typing_self_type, binding_context)); - parameter - .annotated_type() - .apply_type_mapping(db, &mapping, TypeContext::default()) + let mapping = TypeMapping::BindSelf(SelfBinding::new( + db, + env, + typing_self_type, + binding_context, + )); + parameter.annotated_type().apply_type_mapping( + db, + env, + &mapping, + TypeContext::default(), + ) } else { parameter.annotated_type() }; @@ -1331,35 +1477,39 @@ impl<'db> Signature<'db> { _ => None, }; if receiver_typevar.is_some_and(|typevar| { - Self::receiver_violates_typevar_domain(db, receiver, typevar) + Self::receiver_violates_typevar_domain(db, env, receiver, typevar) }) { return std::borrow::Cow::Owned(OwnedConstraintSet::default()); } - receiver.when_constraint_set_assignable_to_owned(db, annotation) + receiver.when_constraint_set_assignable_to_owned(db, env, annotation) }); let receiver_constraints = merge_receiver_constraints( db, + env, self.receiver_constraints.as_ref(), receiver_constraint.as_deref(), ); if let Some(self_type) = typing_self_type - && self.needs_self_mapping(db, removed_receiver) + && self.needs_self_mapping(db, env, removed_receiver) { let self_mapping = - TypeMapping::BindSelf(SelfBinding::new(db, self_type, binding_context)); + TypeMapping::BindSelf(SelfBinding::new(db, env, self_type, binding_context)); parameters = parameters.apply_type_mapping_impl( db, + env, &self_mapping, TypeContext::default(), - &ApplyTypeMappingVisitor::default(), + &ApplyTypeMappingVisitor::new(env), ); - return_ty = return_ty.apply_type_mapping(db, &self_mapping, TypeContext::default()); + return_ty = + return_ty.apply_type_mapping(db, env, &self_mapping, TypeContext::default()); } Self { generic_context: self .generic_context .map(|generic_context| generic_context.remove_self(db, binding_context)), definition: self.definition, + source_overload_index: self.source_overload_index, receiver_constraints, parameters, return_ty, @@ -1379,33 +1529,115 @@ impl<'db> Signature<'db> { /// ``` fn receiver_violates_typevar_domain( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, receiver: Type<'db>, typevar: BoundTypeVarInstance<'db>, ) -> bool { - let Some(domain) = typevar.typevar(db).bound_or_constraints(db) else { + let Some(domain) = typevar.typevar(db).bound_or_constraints(db, env) else { return false; }; - if receiver.has_typevar(db) { + if receiver.has_typevar(db, env) { return false; } !match domain { TypeVarBoundOrConstraints::UpperBound(bound) => { - receiver.is_assignable_to(db, bound.top_materialization(db)) + receiver.is_assignable_to(db, env, bound.top_materialization(db, env)) } TypeVarBoundOrConstraints::Constraints(constraints) => { constraints.elements(db).iter().any(|constraint| { - receiver.is_assignable_to(db, constraint.top_materialization(db)) + receiver.is_assignable_to(db, env, constraint.top_materialization(db, env)) }) } } } + /// Returns this signature bound to `receiver_type` if its explicit receiver annotation is + /// compatible with the bound receiver. + /// + /// Matching the receiver can constrain type variables that occur elsewhere in the signature. + /// Exact bounds determine an unambiguous specialization; one-sided constraints remain attached + /// to the bound signature for later relation checks. + pub(crate) fn bind_self_if_compatible( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + receiver_type: Type<'db>, + typing_self_type: Type<'db>, + ) -> Option { + if !self.can_bind_self_to(db, env, receiver_type) { + return None; + } + + let bound_signature = + self.bind_self_with_receiver(db, env, Some(receiver_type), Some(typing_self_type)); + let Some(receiver_constraints) = bound_signature.receiver_constraints.as_ref() else { + return Some(bound_signature); + }; + + let constraints = ConstraintSetBuilder::new(); + let when = constraints.load(db, env, receiver_constraints); + let inferable = self.inferable_typevars(db); + + match when.solutions(db, env, &constraints, inferable) { + Solutions::Unsatisfiable => return None, + Solutions::Unconstrained => return Some(bound_signature), + // Each receiver path can leave a different type variable unconstrained. Preserve the + // original relation instead of combining those independent solutions. + Solutions::Constrained(solutions) if solutions.len() > 1 => { + return Some(bound_signature); + } + Solutions::Constrained(_) => {} + } + + let Some(generic_context) = self.generic_context else { + return Some(bound_signature); + }; + + let mut builder = SpecializationBuilder::new(db, env, &constraints, inferable); + builder.add_constraint_set(when).ok()?; + let concrete_class_receiver = + matches!(receiver_type, Type::ClassLiteral(_) | Type::GenericAlias(_)); + let specialization = builder.build_with(generic_context, |typevar, bounds| { + if let Some(bounds) = bounds + && let Some(lower) = bounds.lower + && let Some(upper) = bounds.upper.as_single_bound(db, env) + && lower.is_equivalent_to(db, env, upper) + && let Ok(Some(solution)) = PathBounds::default_solve(db, env, &constraints, bounds) + { + return Some(solution); + } + + if let Some(bounds) = bounds + && concrete_class_receiver + && bound_signature + .variance_of(db, env, typevar.identity(db)) + .is_covariant() + && bounds.lower.is_some_and(|lower| !lower.is_never()) + && let Ok(Some(solution)) = PathBounds::default_solve(db, env, &constraints, bounds) + { + return Some(solution); + } + + Some(Type::TypeVar(typevar)) + }); + + Some( + self.apply_specialization(db, specialization) + .bind_self_with_receiver(db, env, Some(receiver_type), Some(typing_self_type)), + ) + } + /// Returns `true` if this signature's first parameter can accept the bound `self` type. /// /// This is used to prune impossible overloads when a method is bound to a concrete receiver. /// If a signature has no positional first parameter, we conservatively keep it. - pub(crate) fn can_bind_self_to(&self, db: &'db dyn Db, self_type: Type<'db>) -> bool { + fn can_bind_self_to( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + self_type: Type<'db>, + ) -> bool { // A dynamic receiver might be compatible with any explicit receiver annotation. if self_type.is_dynamic() { return true; @@ -1439,7 +1671,7 @@ impl<'db> Signature<'db> { // TODO: Expand type aliases here so `type Alias = Self` in a class body // participates in receiver-specific overload pruning. - expected_self_ty = expected_self_ty.bind_self_typevars(db, self_type); + expected_self_ty = expected_self_ty.bind_self_typevars(db, env, self_type); // `Self` binding can make the receiver annotation trivially compatible. if accepts_any_or_exact_self(expected_self_ty) { @@ -1447,7 +1679,7 @@ impl<'db> Signature<'db> { } // A specialized receiver can make generic receiver annotations concrete enough to compare. - if let Some((_, self_specialization)) = self_type.class_specialization(db) { + if let Some((_, self_specialization)) = self_type.class_specialization(db, env) { expected_self_ty = expected_self_ty.apply_optional_specialization(db, Some(self_specialization)); @@ -1461,11 +1693,12 @@ impl<'db> Signature<'db> { self_type .when_assignable_to( db, + env, expected_self_ty, &constraints, self.inferable_typevars(db), ) - .is_always_satisfied(db) + .is_always_satisfied(db, env) } pub(crate) fn has_explicit_positional_receiver_annotation(&self) -> bool { @@ -1483,21 +1716,25 @@ impl<'db> Signature<'db> { fn apply_self_with_receiver( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, receiver_type: Type<'db>, self_type: Type<'db>, ) -> Self { let binding_context = self.definition.map(BindingContext::Definition); let receiver_mapping = TypeMapping::BindSelf(SelfBinding::new( db, + env, receiver_type, - Some(BindingContext::Synthetic), + Some(BindingContext::Synthetic(env.program(db))), )); - let self_mapping = TypeMapping::BindSelf(SelfBinding::new(db, self_type, binding_context)); - let receiver_visitor = ApplyTypeMappingVisitor::default(); - let self_visitor = ApplyTypeMappingVisitor::default(); + let self_mapping = + TypeMapping::BindSelf(SelfBinding::new(db, env, self_type, binding_context)); + let receiver_visitor = ApplyTypeMappingVisitor::new(env); + let self_visitor = ApplyTypeMappingVisitor::new(env); let receiver_constraints = self .map_receiver_constraints( db, + env, &receiver_mapping, TypeContext::default(), &receiver_visitor, @@ -1505,6 +1742,7 @@ impl<'db> Signature<'db> { .map(|constraints| { Self::map_constraints( db, + env, &constraints, &self_mapping, TypeContext::default(), @@ -1512,9 +1750,9 @@ impl<'db> Signature<'db> { ) }) .filter(|constraints| { - !constraints.query(|_builder, constraints| constraints.is_always_satisfied(db)) + !constraints.query(|_builder, constraints| constraints.is_always_satisfied(db, env)) }); - if !self.needs_self_mapping(db, false) { + if !self.needs_self_mapping(db, env, false) { return Self { receiver_constraints, ..self.clone() @@ -1523,12 +1761,14 @@ impl<'db> Signature<'db> { let parameters = self.parameters.apply_type_mapping_impl( db, + env, &self_mapping, TypeContext::default(), &self_visitor, ); let return_ty = self.return_ty.apply_type_mapping_impl( db, + env, &self_mapping, TypeContext::default(), &self_visitor, @@ -1536,6 +1776,7 @@ impl<'db> Signature<'db> { Self { generic_context: self.generic_context, definition: self.definition, + source_overload_index: self.source_overload_index, receiver_constraints, parameters, return_ty, @@ -1545,50 +1786,54 @@ impl<'db> Signature<'db> { fn receiver_constraints_when_satisfied<'c>( &self, - checker: &TypeRelationChecker<'_, 'c, 'db>, db: &'db dyn Db, + checker: &TypeRelationChecker<'_, 'c, 'db>, ) -> ConstraintSet<'db, 'c> { let Some(constraints) = self.receiver_constraints.as_ref() else { return checker.always(); }; - checker.constraints.load(db, constraints) + checker.constraints.load(db, checker.env, constraints) } fn map_receiver_constraints( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'_, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Option> { let constraints = Self::map_constraints( db, + env, self.receiver_constraints.as_ref()?, type_mapping, tcx, visitor, ); - (!constraints.query(|_builder, constraints| constraints.is_always_satisfied(db))) - .then_some(constraints) + (!constraints + .query(|_builder, constraints| constraints.is_always_satisfied(db, visitor.env))) + .then_some(constraints) } fn map_constraints( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, constraints: &OwnedConstraintSet<'db>, type_mapping: &TypeMapping<'_, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> OwnedConstraintSet<'db> { if !constraints .types() - .any(|ty| ty.apply_type_mapping_impl(db, type_mapping, tcx, visitor) != ty) + .any(|ty| ty.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor) != ty) { return constraints.clone(); } let builder = ConstraintSetBuilder::new(); builder.into_owned(|builder| { - let constraints = builder.load(db, constraints); + let constraints = builder.load(db, visitor.env, constraints); constraints.apply_type_mapping_impl(db, type_mapping, tcx, visitor) }) } @@ -1600,31 +1845,29 @@ impl<'db> Signature<'db> { } /// Returns this signature with the given specialization applied to parameters and return type. - pub(crate) fn apply_specialization( - &self, - db: &'db dyn Db, - specialization: Specialization<'db>, - ) -> Self { + fn apply_specialization(&self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self { + let env = &ProgramEnvironment::from_program(specialization.generic_context(db).program(db)); let type_mapping = TypeMapping::ApplySpecialization(ApplySpecialization::Specialization(specialization)); self.apply_type_mapping_impl( db, &type_mapping, TypeContext::default(), - &ApplyTypeMappingVisitor::default(), + &ApplyTypeMappingVisitor::new(env), ) } /// Returns the callable signature produced by partially applying this signature. - pub(crate) fn partially_apply( + fn partially_apply( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, partial_application: &PartialApplication<'db>, inference: Option>, unspecialized_return_ty: Type<'db>, ) -> Self { let signature_specialization = - self.partial_application_specialization(db, partial_application, inference); + self.partial_application_specialization(db, env, partial_application, inference); let signature = signature_specialization.map_or_else( || self.clone(), |specialization| self.apply_specialization(db, specialization), @@ -1704,6 +1947,7 @@ impl<'db> Signature<'db> { fn partial_application_specialization( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, partial_application: &PartialApplication<'db>, inference: Option>, ) -> Option> { @@ -1720,9 +1964,11 @@ impl<'db> Signature<'db> { .enumerate() .filter(|(index, _)| !partial_application.is_positionally_bound(*index)) .any(|(_, parameter)| { - parameter - .annotated_type() - .references_typevar(db, typevar.typevar(db).identity(db)) + parameter.annotated_type().references_typevar( + db, + env, + typevar.typevar(db).identity(db), + ) }) }) .map(|typevar| typevar.identity(db)) @@ -1735,34 +1981,39 @@ impl<'db> Signature<'db> { Some(inference.specialization_with(db, |typevar, inferred| { promoted_typevars .contains(&typevar.identity(db)) - .then(|| inferred.map_or(Type::TypeVar(typevar), |ty| ty.promote(db))) + .then(|| inferred.map_or(Type::TypeVar(typevar), |ty| ty.promote(db, env))) })) } - fn needs_self_mapping(&self, db: &'db dyn Db, receiver_is_removed: bool) -> bool { + fn needs_self_mapping( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + receiver_is_removed: bool, + ) -> bool { // TODO: Expand type aliases here so `type Alias = Self` in parameters or returns // triggers binding when a method is accessed on a concrete receiver. - self.return_ty.contains_self(db) + self.return_ty.contains_self(db, env) || self .parameters .iter() .enumerate() .skip(usize::from(receiver_is_removed)) - .any(|(_, parameter)| parameter.annotated_type().contains_self(db)) + .any(|(_, parameter)| parameter.annotated_type().contains_self(db, env)) // a type parameter of this signature can be bounded by `Self` // (`def method[T: Self]`). that bound is evaluated lazily, so it is invisible to // `contains_self`, but it still has to be rewritten for the bound to constrain anything || self.generic_context.is_some_and(|generic_context| { generic_context .variables(db) - .any(|typevar| typevar.bounds_mention_self(db)) + .any(|typevar| typevar.bounds_mention_self(db, env)) }) } - fn inferable_typevars(&self, db: &'db dyn Db) -> InferableTypeVars<'db> { + fn inferable_typevars(&self, db: &'db dyn Db) -> TypeVarSet<'db> { match self.generic_context { Some(generic_context) => generic_context.inferable_typevars(db), - None => InferableTypeVars::None, + None => TypeVarSet::None, } } @@ -1825,6 +2076,7 @@ impl<'db> Signature<'db> { pub(crate) fn non_generic_implementation_parameters_consistency_with( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, overload: &Self, ) -> ParameterConsistency<'db> { debug_assert!(self.is_non_generic()); @@ -1836,8 +2088,9 @@ impl<'db> Signature<'db> { let relation_visitor = HasRelationToVisitor::default(&constraints); let disjointness_visitor = IsDisjointVisitor::default(&constraints); let signature_relation_visitor = SignatureRelationVisitor::default(); - let materialization_visitor = ApplyTypeMappingVisitor::default(); + let materialization_visitor = ApplyTypeMappingVisitor::new(env); let checker = TypeRelationChecker::constraint_set_assignability_with_context( + env, &constraints, &relation_visitor, &disjointness_visitor, @@ -1847,7 +2100,7 @@ impl<'db> Signature<'db> { let is_consistent = checker .check_signature_pair(db, &implementation, &overload) - .is_always_satisfied(db); + .is_always_satisfied(db, env); if is_consistent { ParameterConsistency::Consistent @@ -1861,6 +2114,7 @@ impl<'db> Signature<'db> { pub(crate) fn non_generic_implementation_return_type_consistency_with( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, overload: &Self, ) -> ReturnTypeConsistency<'db> { debug_assert!(self.is_non_generic()); @@ -1870,8 +2124,9 @@ impl<'db> Signature<'db> { let relation_visitor = HasRelationToVisitor::default(&constraints); let disjointness_visitor = IsDisjointVisitor::default(&constraints); let signature_relation_visitor = SignatureRelationVisitor::default(); - let materialization_visitor = ApplyTypeMappingVisitor::default(); + let materialization_visitor = ApplyTypeMappingVisitor::new(env); let checker = TypeRelationChecker::assignability_with_context( + env, &constraints, &relation_visitor, &disjointness_visitor, @@ -1881,7 +2136,7 @@ impl<'db> Signature<'db> { let is_consistent = checker .check_type_pair(db, overload.return_ty, self.return_ty) - .is_always_satisfied(db); + .is_always_satisfied(db, env); if is_consistent { ReturnTypeConsistency::Consistent @@ -1893,6 +2148,7 @@ impl<'db> Signature<'db> { pub(crate) fn when_constraint_set_assignable_to_signatures<'c>( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, other: &CallableSignature<'db>, constraints: &'c ConstraintSetBuilder<'db>, ) -> ConstraintSet<'db, 'c> { @@ -1914,6 +2170,7 @@ impl<'db> Signature<'db> { )); let param_spec_matches = ConstraintSet::constrain_typevar_upper_bound( db, + env, constraints, self_bound_typevar, upper, @@ -1925,6 +2182,7 @@ impl<'db> Signature<'db> { .when_any(db, constraints, |other_return_type| { self.return_ty.when_constraint_set_assignable_to( db, + env, other_return_type, constraints, ) @@ -1936,21 +2194,23 @@ impl<'db> Signature<'db> { .overloads .iter() .when_all(db, constraints, |other_signature| { - self.when_constraint_set_assignable_to(db, other_signature, constraints) + self.when_constraint_set_assignable_to(db, env, other_signature, constraints) }) } fn when_constraint_set_assignable_to<'c>( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, other: &Self, constraints: &'c ConstraintSetBuilder<'db>, ) -> ConstraintSet<'db, 'c> { let relation_visitor = HasRelationToVisitor::default(constraints); let disjointness_visitor = IsDisjointVisitor::default(constraints); let signature_relation_visitor = SignatureRelationVisitor::default(); - let materialization_visitor = ApplyTypeMappingVisitor::default(); + let materialization_visitor = ApplyTypeMappingVisitor::new(env); let checker = TypeRelationChecker::constraint_set_assignability( + env, constraints, &relation_visitor, &disjointness_visitor, @@ -1965,8 +2225,23 @@ impl<'db> Signature<'db> { Self { definition, ..self } } + /// Records this signature's position in its defining function's overload list. + pub(crate) fn with_source_overload_index(mut self, index: Option) -> Self { + self.source_overload_index = index + .and_then(|index| u32::try_from(index).ok()) + .and_then(|index| index.checked_add(1)) + .and_then(NonZeroU32::new); + self + } + + /// Returns this signature's position in its defining function's overload list. + pub(crate) fn source_overload_index(&self) -> Option { + self.source_overload_index + .map(|index| index.get() as usize - 1) + } + /// Create a new signature with the given parameters. - pub(crate) fn with_parameters(self, parameters: Parameters<'db>) -> Self { + fn with_parameters(self, parameters: Parameters<'db>) -> Self { Self { parameters, ..self } } @@ -1977,7 +2252,12 @@ impl<'db> Signature<'db> { } impl<'db> VarianceInferable<'db> for &Signature<'db> { - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { + fn variance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { tracing::trace!( "Checking variance of `{tvar}` in `{self:?}`", tvar = typevar.identity.name(db) @@ -1987,7 +2267,7 @@ impl<'db> VarianceInferable<'db> for &Signature<'db> { parameter .annotated_type() .with_polarity(TypeVarVariance::Contravariant) - .variance_of(db, typevar) + .variance_of(db, env, typevar) }; let parameter_variances = if let Some((prefix_parameters, paramspec)) = @@ -2000,7 +2280,7 @@ impl<'db> VarianceInferable<'db> for &Signature<'db> { .chain(std::iter::once( Type::TypeVar(paramspec) .with_polarity(TypeVarVariance::Contravariant) - .variance_of(db, typevar), + .variance_of(db, env, typevar), )), ) } else { @@ -2009,7 +2289,7 @@ impl<'db> VarianceInferable<'db> for &Signature<'db> { itertools::chain( parameter_variances, - Some(self.return_ty.variance_of(db, typevar)), + Some(self.return_ty.variance_of(db, env, typevar)), ) .collect() } @@ -2055,27 +2335,31 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } }; - let is_unary_overload_aggregate_candidate_type = |ty: Type<'db>| { - // Keep aggregate probing away from inference-sensitive shapes and defer them to the - // legacy path, which already handles dynamic/typevar interactions. - !ty.has_dynamic(db) && !ty.has_typevar_or_typevar_instance(db) - }; - let other_parameter_type = single_required_positional_parameter_type(target_signature)?; // Keep this aggregate path narrowly scoped to unary target callables whose parameter // domain is an explicit union. // // Broader overload-set assignability (non-union unary domains, higher arity, // typevars/dynamic interactions) needs dedicated relation logic. - if !matches!(other_parameter_type, Type::Union(_)) - || !is_unary_overload_aggregate_candidate_type(other_parameter_type) + if !matches!(other_parameter_type, Type::Union(_)) { + return None; + } + + let env = self.env; + let is_unary_overload_aggregate_candidate_type = |ty: Type<'db>| { + // Keep aggregate probing away from inference-sensitive shapes and defer them to the + // legacy path, which already handles dynamic/typevar interactions. + !ty.has_dynamic(db, env) && !ty.has_typevar_or_typevar_instance(db, env) + }; + + if !is_unary_overload_aggregate_candidate_type(other_parameter_type) || !is_unary_overload_aggregate_candidate_type(target_signature.return_ty) { return None; } - let mut parameter_type_union = UnionBuilder::new(db); - let mut return_type_union = UnionBuilder::new(db); + let mut parameter_type_union = UnionBuilder::new(db, env); + let mut return_type_union = UnionBuilder::new(db, env); let mut has_overlapping_domain = false; for self_signature in source_signatures { @@ -2088,7 +2372,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { let signatures_are_disjoint = self .as_disjointness_checker() .check_type_pair(db, self_parameter_type, other_parameter_type) - .is_always_satisfied(db); + .is_always_satisfied(db, env); if signatures_are_disjoint { continue; @@ -2111,7 +2395,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { let aggregate_relation = parameters_cover_target.and(db, self.constraints, returns_match_target); aggregate_relation - .is_always_satisfied(db) + .is_always_satisfied(db, env) .then_some(aggregate_relation) } @@ -2148,6 +2432,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // instead. match (source_is_single_paramspec, target_is_single_paramspec) { (Some((source_tvar, source_return)), None) if target_overloads.len() > 1 => { + let env = self.env; let upper = Type::Callable(CallableType::new( db, CallableSignature::from_overloads(target_overloads.iter().map( @@ -2157,6 +2442,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { signature.parameters().clone(), Type::unknown(), ) + .with_source_overload_index(signature.source_overload_index()) }, )), CallableTypeKind::ParamSpecValue, @@ -2164,6 +2450,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { )); let param_spec_matches = ConstraintSet::constrain_typevar_upper_bound( db, + env, self.constraints, source_tvar, upper, @@ -2184,6 +2471,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } (None, Some((target_tvar, target_return))) if source_overloads.len() > 1 => { + let env = self.env; // TODO: Ideally, the constraint solver should use the return type constraint // to remove unmatched overloads from the `ParamSpec` specialization instead // of filtering them here. @@ -2201,7 +2489,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { target_return, ) }) - .is_never_satisfied(db) + .is_never_satisfied(db, env) }) .map(|signature| { Signature::new_generic( @@ -2209,6 +2497,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { signature.parameters().clone(), Type::unknown(), ) + .with_source_overload_index(signature.source_overload_index()) }), ), CallableTypeKind::ParamSpecValue, @@ -2216,6 +2505,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { )); let param_spec_matches = ConstraintSet::constrain_typevar_lower_bound( db, + env, self.constraints, target_tvar, lower, @@ -2316,6 +2606,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { source: &Signature<'db>, target: &Signature<'db>, ) -> ConstraintSet<'db, 'c> { + let env = self.env; // If either signature is generic, freshen that signature's typevars before considering // them inferable for this relation. The relation only needs to find one specialization of // each generic callable that causes the check to succeed, but those callable-local @@ -2327,7 +2618,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { .max_typevar_freshness_matching_generic_context(db, generic_context) .map(|freshness| freshness.increment().value()) { - freshened_source = source.freshen_bound_typevars(db, delta); + freshened_source = source.freshen_bound_typevars(db, env, delta); &freshened_source } else { source @@ -2339,14 +2630,38 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { .max_typevar_freshness_matching_generic_context(db, generic_context) .map(|freshness| freshness.increment().value()) { - freshened_target = target.freshen_bound_typevars(db, delta); + freshened_target = target.freshen_bound_typevars(db, env, delta); &freshened_target } else { target }; - let source_inferable = source.inferable_typevars(db); - let target_inferable = target.inferable_typevars(db); + // `inferable` has different roles in the two type-variable evaluation modes: + // + // * Eager comparisons decide whether the relation holds immediately. An unbound generic + // method's `Self` can have an upper bound such as `C[T]`, so `T` must also be + // inferable; otherwise, a concrete receiver such as `C[int]` is compared against a + // fixed, symbolic `T` and valid higher-order calls are rejected. + // * Lazy comparisons record constraints for every type variable, regardless of whether + // it is inferable. Here, `signature_inferable` also determines which type variables + // `reduce_inferable` existentially removes below, so it must contain only variables + // actually bound by these signatures. Including an enclosing class's `T` would turn a + // decorator's return constraint `T <= R` into `exists T. T <= R`, losing the + // relationship needed to infer `R = T`. + let include_bound_dependencies = self.typevar_evaluation == TypeVarEvaluation::Eager; + let signature_typevars = |signature: &Signature<'db>| { + signature + .generic_context + .map_or(TypeVarSet::None, |context| { + if include_bound_dependencies { + context.inferable_typevars(db) + } else { + TypeVarSet::from_typevars(db, context.variables(db)) + } + }) + }; + let source_inferable = signature_typevars(source); + let target_inferable = signature_typevars(target); let signature_inferable = source_inferable.merge(db, target_inferable); let inferable = self.inferable.merge(db, signature_inferable); @@ -2360,10 +2675,10 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } let when = checker.with_signature_recursion_guard(source, target, || { source - .receiver_constraints_when_satisfied(&checker, db) + .receiver_constraints_when_satisfied(db, &checker) .and(db, self.constraints, || { target - .receiver_constraints_when_satisfied(&checker, db) + .receiver_constraints_when_satisfied(db, &checker) .and(db, self.constraints, || { checker.check_signature_pair_inner(db, source, target) }) @@ -2374,7 +2689,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // we produce, we reduce it back down to the inferable set that the caller asked about. // If we introduced new inferable typevars, those will be existentially quantified away // before returning. - when.reduce_inferable(db, self.constraints, signature_inferable) + when.reduce_inferable(db, env, self.constraints, signature_inferable) } fn with_signature_recursion_guard( @@ -2483,8 +2798,47 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } } + let mut source_parameters = source.parameters.expand_starred_variadic_annotations(db); + let mut target_parameters = target.parameters.expand_starred_variadic_annotations(db); + + // Gradual variadics and TypeVarTuples need their original suffix boundaries for + // materialization and inference. Named source prefixes must also remain visible when a + // target keyword could fill the same parameter. + if let (Some((_, source_variadic)), Some((_, target_variadic))) = + (source_parameters.variadic(), target_parameters.variadic()) + && !source_variadic.has_starred_annotation() + && !target_variadic.has_starred_annotation() + && source_variadic.annotated_type().resolve_type_alias(db) + == target_variadic.annotated_type().resolve_type_alias(db) + && !source_variadic + .annotated_type() + .resolve_type_alias(db) + .is_dynamic() + && source_parameters.positional().all(|source_parameter| { + source_parameter.is_positional_only() + || source_parameter.name().is_none_or(|name| { + match target_parameters.keyword_by_name(name) { + Some((_, parameter)) => { + !parameter.is_keyword_only() + || parameter.annotated_type().resolve_type_alias(db).is_never() + } + None => { + target_parameters + .keyword_variadic() + .is_none_or(|(_, parameter)| { + parameter.annotated_type().resolve_type_alias(db).is_never() + }) + } + } + }) + }) + { + source_parameters = source_parameters.with_homogeneous_variadic_suffix_in_prefix(db); + target_parameters = target_parameters.with_homogeneous_variadic_suffix_in_prefix(db); + } + let target_typevartuple = if self.typevar_evaluation == TypeVarEvaluation::Lazy { - target.parameters.variadic().and_then(|(index, parameter)| { + target_parameters.variadic().and_then(|(index, parameter)| { if parameter.has_starred_annotation() && let Type::TypeVar(typevartuple) = parameter.annotated_type() && typevartuple.is_typevartuple(db) @@ -2503,34 +2857,82 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // comparison below reaches the same result, but only after doing work that is expensive for // large overload sets. An unpacked target TypeVarTuple bypasses this fast path so it can be // constrained from the source parameters. - if source.parameters.is_standard() - && target.parameters.is_standard() - && source.parameters.variadic().is_none() + if source_parameters.is_standard() + && target_parameters.is_standard() + && source_parameters.variadic().is_none() && target_typevartuple.is_none() { - let source_positional = source.parameters.positional().count(); - let target_positional = target.parameters.positional().count(); + let source_positional = source_parameters.positional().count(); + let target_positional = target_parameters.positional().count(); + let target_variadic = target_parameters.variadic(); + + // A subdiagnostic telling the user that `source` is missing a `*args` parameter + // is only guaranteed to be correct when `target` has a plain, open-ended variadic tail. + // (Well: we might be able to do better here in the future, but we simplify the logic here + // for now.) + // + // An unpacked annotation may represent a fixed-length tuple, and a variadic parameter + // followed by positional parameters may represent an unpacked tuple with a required suffix + // instead of an open-ended tail. + let target_has_open_ended_variadic = || { + target_variadic.is_some_and(|(index, parameter)| { + !parameter.has_starred_annotation() + && !target_parameters + .iter() + .skip(index) + .any(Parameter::is_positional) + }) + }; + let target_accepts_extra_positionals = - target_positional > source_positional || target.parameters.variadic().is_some(); + target_positional > source_positional || target_variadic.is_some(); if target_accepts_extra_positionals { if let Some(context) = self.report_context() - && target_positional > source_positional - && let Some(ParameterKind::KeywordOnly { name, .. }) = source - .parameters - .iter() - .nth(source_positional) - .map(Parameter::kind) + && (target_positional > source_positional || target_has_open_ended_variadic()) { - context.push(ErrorContext::ParameterMustAcceptPositionalArguments { - name: name.clone(), - }); + let error_context = if target_positional > source_positional { + let source_parameter_kind = source_parameters + .get(source_positional) + .map(Parameter::kind); + + match source_parameter_kind { + Some(ParameterKind::KeywordOnly { name, .. }) => { + ErrorContext::ParameterMustAcceptPositionalArguments { + name: name.clone(), + } + } + Some(ParameterKind::KeywordVariadic { .. }) | None => { + let parameter = target_parameters + .get_positional(source_positional) + .and_then(Parameter::name); + ErrorContext::MissingParameter { + parameter: ParameterDescription::new( + source_positional, + parameter, + ), + } + } + Some( + ParameterKind::PositionalOnly { .. } + | ParameterKind::PositionalOrKeyword { .. } + | ParameterKind::Variadic { .. }, + ) => unreachable!( + "the first parameter after the positional prefix \ + cannot be positional or variadic" + ), + } + } else { + ErrorContext::MissingVariadicPositionalParameter + }; + context.push(error_context); } return self.never(); } } + let env = self.env; let mut result = self.always(); // Avoid returning early after checking the return types in case there is a `ParamSpec` type @@ -2539,7 +2941,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { let return_type_constraints = self.check_type_pair(db, source.return_ty, target.return_ty); let return_type_checks = !result .intersect(db, self.constraints, return_type_constraints) - .is_never_satisfied(db); + .is_never_satisfied(db, env); if let Some(context) = self.report_context() && !return_type_checks { @@ -2578,7 +2980,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { let constraint_set = self.check_type_pair(db, target_ty, source_ty); if let Some(context) = self.report_context() - && constraint_set.is_never_satisfied(db) + && constraint_set.is_never_satisfied(db, env) { let parameter = ParameterDescription::new(target_index, target_name); context.push(ErrorContext::IncompatibleParameterTypes { @@ -2587,14 +2989,21 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { parameter, }); } + // Continuing past a nonterminal contradiction can bind later `ParamSpec`s or + // replace the diagnostic context that explains the incompatible parameter. !result .intersect(db, self.constraints, constraint_set) - .is_never_satisfied(db) + .is_never_satisfied(db, env) + }; + let parameter_must_have_default = |parameter: &Parameter<'db>, index: usize| { + ErrorContext::RequiredParameterMustHaveDefault { + parameter: ParameterDescription::new(index, parameter.name()), + } }; if self.typevar_evaluation == TypeVarEvaluation::Lazy { - let source_paramspec = source.parameters.as_paramspec_with_prefix(); - let target_paramspec = target.parameters.as_paramspec_with_prefix(); + let source_paramspec = source_parameters.as_paramspec_with_prefix(); + let target_paramspec = target_parameters.as_paramspec_with_prefix(); // If either signature is a ParamSpec, the constraint set should bind the ParamSpec to // the other signature before the return-type and gradual/top fast paths can return @@ -2606,6 +3015,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { (Some(([], source_bound_typevar)), Some(([], target_bound_typevar))) => { let param_spec_matches = ConstraintSet::constrain_typevar( db, + env, self.constraints, source_bound_typevar, Type::TypeVar(target_bound_typevar), @@ -2637,6 +3047,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { )); let param_spec_prefix_matches = ConstraintSet::constrain_typevar_lower_bound( db, + env, self.constraints, target_bound_typevar, lower, @@ -2667,6 +3078,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { )); let param_spec_matches = ConstraintSet::constrain_typevar_upper_bound( db, + env, self.constraints, source_bound_typevar, upper, @@ -2777,23 +3189,27 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { if let Some(source_param) = source_params.next() { let lower = Type::Callable(CallableType::new( db, - CallableSignature::single(Signature::new_generic( - source.generic_context, - Parameters::concatenate( - db, - std::iter::once(source_param.clone()) - .chain(source_params.cloned()) - .collect(), - ConcatenateTail::ParamSpec(source_bound_typevar), - ), - Type::unknown(), - )), + CallableSignature::single( + Signature::new_generic( + source.generic_context, + Parameters::concatenate( + db, + std::iter::once(source_param.clone()) + .chain(source_params.cloned()) + .collect(), + ConcatenateTail::ParamSpec(source_bound_typevar), + ), + Type::unknown(), + ) + .with_source_overload_index(source.source_overload_index()), + ), CallableTypeKind::ParamSpecValue, CallableFunctionProvenance::None, )); let param_spec_prefix_matches = ConstraintSet::constrain_typevar_lower_bound( db, + env, self.constraints, target_bound_typevar, lower, @@ -2802,23 +3218,27 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } else if let Some(target_param) = target_params.next() { let upper = Type::Callable(CallableType::new( db, - CallableSignature::single(Signature::new_generic( - target.generic_context, - Parameters::concatenate( - db, - std::iter::once(target_param.clone()) - .chain(target_params.cloned()) - .collect(), - ConcatenateTail::ParamSpec(target_bound_typevar), - ), - Type::unknown(), - )), + CallableSignature::single( + Signature::new_generic( + target.generic_context, + Parameters::concatenate( + db, + std::iter::once(target_param.clone()) + .chain(target_params.cloned()) + .collect(), + ConcatenateTail::ParamSpec(target_bound_typevar), + ), + Type::unknown(), + ) + .with_source_overload_index(target.source_overload_index()), + ), CallableTypeKind::ParamSpecValue, CallableFunctionProvenance::None, )); let param_spec_prefix_matches = ConstraintSet::constrain_typevar_upper_bound( db, + env, self.constraints, source_bound_typevar, upper, @@ -2828,6 +3248,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // When the prefixes match exactly, we just relate the remaining tails. let param_spec_matches = ConstraintSet::constrain_typevar( db, + env, self.constraints, source_bound_typevar, Type::TypeVar(target_bound_typevar), @@ -2843,16 +3264,20 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { (None, Some(([], target_bound_typevar))) => { let lower = Type::Callable(CallableType::new( db, - CallableSignature::single(Signature::new_generic( - source.generic_context, - source.parameters.clone(), - Type::unknown(), - )), + CallableSignature::single( + Signature::new_generic( + source.generic_context, + source_parameters.clone(), + Type::unknown(), + ) + .with_source_overload_index(source.source_overload_index()), + ), CallableTypeKind::ParamSpecValue, CallableFunctionProvenance::None, )); let param_spec_matches = ConstraintSet::constrain_typevar_lower_bound( db, + env, self.constraints, target_bound_typevar, lower, @@ -2864,8 +3289,6 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // self: callable without ParamSpec // other: `Concatenate[, P]` (None, Some((target_prefix_params, target_bound_typevar))) => { - let source_parameters = - source.parameters.expand_starred_variadic_annotations(db); // Loop over self parameters and target_prefix_params in a similar manner to the // above loop let mut parameters = ParametersZip { @@ -2984,21 +3407,24 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } let (source_params, _) = parameters.into_remaining(); - let source_params = source - .parameters - .with_transformed_parameters(source_params.cloned()); + let source_params = + source_parameters.with_transformed_parameters(source_params.cloned()); let lower = Type::Callable(CallableType::new( db, - CallableSignature::single(Signature::new_generic( - source.generic_context, - source_params, - Type::unknown(), - )), + CallableSignature::single( + Signature::new_generic( + source.generic_context, + source_params, + Type::unknown(), + ) + .with_source_overload_index(source.source_overload_index()), + ), CallableTypeKind::ParamSpecValue, CallableFunctionProvenance::None, )); let param_spec_prefix_matches = ConstraintSet::constrain_typevar_lower_bound( db, + env, self.constraints, target_bound_typevar, lower, @@ -3013,16 +3439,20 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { (Some(([], source_bound_typevar)), None) => { let upper = Type::Callable(CallableType::new( db, - CallableSignature::single(Signature::new_generic( - target.generic_context, - target.parameters.clone(), - Type::unknown(), - )), + CallableSignature::single( + Signature::new_generic( + target.generic_context, + target_parameters.clone(), + Type::unknown(), + ) + .with_source_overload_index(target.source_overload_index()), + ), CallableTypeKind::ParamSpecValue, CallableFunctionProvenance::None, )); let param_spec_matches = ConstraintSet::constrain_typevar_upper_bound( db, + env, self.constraints, source_bound_typevar, upper, @@ -3038,10 +3468,10 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { current_source: None, current_target: None, source_iter: source_prefix_params.iter(), - target_iter: target.parameters.iter(), + target_iter: target_parameters.iter(), }; - if target.parameters.kind() != ParametersKind::Gradual { + if target_parameters.kind() != ParametersKind::Gradual { let mut target_index = 0usize; while let Some(next_parameter) = parameters.next() { match next_parameter { @@ -3122,21 +3552,24 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } let (_, target_params) = parameters.into_remaining(); - let target_params = target - .parameters - .with_transformed_parameters(target_params.cloned()); + let target_params = + target_parameters.with_transformed_parameters(target_params.cloned()); let upper = Type::Callable(CallableType::new( db, - CallableSignature::single(Signature::new_generic( - target.generic_context, - target_params, - Type::unknown(), - )), + CallableSignature::single( + Signature::new_generic( + target.generic_context, + target_params, + Type::unknown(), + ) + .with_source_overload_index(target.source_overload_index()), + ), CallableTypeKind::ParamSpecValue, CallableFunctionProvenance::None, )); let param_spec_prefix_matches = ConstraintSet::constrain_typevar_upper_bound( db, + env, self.constraints, source_bound_typevar, upper, @@ -3157,25 +3590,30 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // A gradual parameter list is a supertype of the "bottom" parameter list (*args: object, // **kwargs: object). - if target.parameters.is_gradual() - && !source.parameters.is_top() - && source - .parameters + if target_parameters.is_gradual() + && (matches!(target_parameters.kind(), ParametersKind::Gradual) + || self.typevar_evaluation == TypeVarEvaluation::Lazy) + && !source_parameters.is_top() + && source_parameters .variadic() .is_some_and(|(_, param)| param.annotated_type().is_object()) - && source - .parameters + && source_parameters .keyword_variadic() .is_some_and(|(_, param)| param.annotated_type().is_object()) { - return self.always(); + return result; } // The top signature is supertype of (and assignable from) all other signatures. It is a // subtype of no signature except itself, and assignable only to the gradual signature. - if target.parameters.is_top() { - return self.always(); - } else if source.parameters.is_top() && !target.parameters.is_gradual() { + if target_parameters.is_top() { + return result; + } else if source_parameters.is_top() && !target_parameters.is_gradual() { + if let Some(context) = self.report_context() { + context.push(ErrorContext::TopCallableAssignedToNonTop { + return_type: source.return_ty, + }); + } return self.never(); } @@ -3184,9 +3622,9 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // unpacked target TypeVarTuple instead continues to the ordinary parameter comparison so it // can be constrained from the source parameters. if target_typevartuple.is_none() - && (source.parameters.is_gradual() || target.parameters.is_gradual()) + && (source_parameters.is_gradual() || target_parameters.is_gradual()) { - match (source.parameters.kind(), target.parameters.kind()) { + match (source_parameters.kind(), target_parameters.kind()) { // Both parameter lists are `Concatenate` with gradual forms. All prefix parameters // are going to be positional-only. ( @@ -3194,9 +3632,9 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { ParametersKind::Concatenate(ConcatenateTail::Gradual), ) => { let source_prefix_params = - &source.parameters.as_slice()[..source.parameters.len().saturating_sub(2)]; + &source_parameters.as_slice()[..source_parameters.len().saturating_sub(2)]; let target_prefix_params = - &target.parameters.as_slice()[..target.parameters.len().saturating_sub(2)]; + &target_parameters.as_slice()[..target_parameters.len().saturating_sub(2)]; for (target_index, (source_param, target_param)) in source_prefix_params .iter() @@ -3222,11 +3660,11 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { ParametersKind::Standard, ) => { let source_prefix_params = - &source.parameters.as_slice()[..source.parameters.len().saturating_sub(2)]; + &source_parameters.as_slice()[..source_parameters.len().saturating_sub(2)]; for (target_index, param) in source_prefix_params .iter() - .zip_longest(target.parameters.iter()) + .zip_longest(target_parameters.iter()) .enumerate() { match param { @@ -3279,12 +3717,12 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { ParametersKind::Concatenate(ConcatenateTail::Gradual), ) => { let target_prefix_params = - &target.parameters.as_slice()[..target.parameters.len().saturating_sub(2)]; + &target_parameters.as_slice()[..target_parameters.len().saturating_sub(2)]; let mut parameters = ParametersZip { current_source: None, current_target: None, - source_iter: source.parameters.iter(), + source_iter: source_parameters.iter(), target_iter: target_prefix_params.iter(), }; @@ -3362,6 +3800,21 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } target_index += 1; } + + // Once every fixed source parameter matches the target prefix, an + // object-variadic tail accepts every materialization of the gradual remainder. + // Reject additional fixed or keyword-only parameters: they would make the + // source more restrictive than at least one possible target signature. + if let [source_prefix @ .., variadic, keyword_variadic] = + source_parameters.as_slice() + && source_prefix.len() <= target_prefix_params.len() + && variadic.is_variadic() + && variadic.annotated_type().is_object() + && keyword_variadic.is_keyword_variadic() + && keyword_variadic.annotated_type().is_object() + { + return result; + } } _ => {} @@ -3374,21 +3827,13 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { self.constraints, ConstraintSet::from_bool( self.constraints, - source.parameters.is_gradual() && target.parameters.is_gradual(), + source_parameters.is_gradual() && target_parameters.is_gradual(), ), ), TypeRelation::Assignability => result, }; } - // TODO: Normalize starred variadic annotations for all signature comparisons. Restricting - // expansion to target TypeVarTuple inference means equivalent nested unpackings such as - // `*tuple[*tuple[str, ...], bytes]` and `*tuple[str, ...], bytes` are not related correctly. - let source_parameters = if target_typevartuple.is_some() { - source.parameters.expand_starred_variadic_annotations(db) - } else { - source.parameters.clone() - }; // Align the fixed target prefix and suffix before entering the parameter loop so that the // target TypeVarTuple captures only the source parameter entries between them. let typevartuple_source_parameter_count = if let Some((typevartuple_index, _)) = @@ -3398,7 +3843,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { .iter() .take_while(|parameter| parameter.is_positional() || parameter.is_variadic()) .count(); - let target_suffix_len = target.parameters.as_slice()[typevartuple_index + 1..] + let target_suffix_len = target_parameters.as_slice()[typevartuple_index + 1..] .iter() .take_while(|parameter| parameter.is_positional()) .count(); @@ -3447,7 +3892,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { current_source: None, current_target: None, source_iter: source_parameters.iter(), - target_iter: target.parameters.iter(), + target_iter: target_parameters.iter(), }; // Collect all the standard parameters that have only been matched against a variadic @@ -3520,13 +3965,15 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { EitherOrBoth::Right(target_parameter) => { if let Some(source_parameter_count) = as_target_typevartuple(target_parameter) { - if source_parameter_count > 0 { - return self.never(); - } + assert_eq!( + source_parameter_count, 0, + "an exhausted source signature cannot provide parameters \ + to a TypeVarTuple" + ); if !check_types( &mut result, target_parameter.annotated_type(), - Type::empty_tuple(db), + Type::empty_tuple(db, env), target_parameter.name(), target_index, ) { @@ -3538,6 +3985,27 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // If there are more parameters in `target` than in `source`, then `source` is // not a subtype of `target`. + if let Some(context) = self.report_context() + && target_parameters.as_paramspec_with_prefix().is_none() + { + let error_context = match target_parameter.kind() { + ParameterKind::PositionalOnly { .. } + | ParameterKind::PositionalOrKeyword { .. } + | ParameterKind::KeywordOnly { .. } => ErrorContext::MissingParameter { + parameter: ParameterDescription::new( + target_index, + target_parameter.name(), + ), + }, + ParameterKind::Variadic { .. } => { + ErrorContext::MissingVariadicPositionalParameter + } + ParameterKind::KeywordVariadic { .. } => { + ErrorContext::MissingVariadicKeywordParameter + } + }; + context.push(error_context); + } return self.never(); } @@ -3558,6 +4026,12 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { }, ) => { if source_default.is_none() && target_default.is_some() { + if let Some(context) = self.report_context() { + context.push(parameter_must_have_default( + source_param, + target_index, + )); + } return self.never(); } if !check_types( @@ -3592,6 +4066,12 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } // The following checks are the same as positional-only parameters. if source_default.is_none() && target_default.is_some() { + if let Some(context) = self.report_context() { + context.push(parameter_must_have_default( + source_param, + target_index, + )); + } return self.never(); } if !check_types( @@ -3692,6 +4172,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { }; Type::tuple(TupleType::mixed_with_segment( db, + env, captured_source_parameters() .take(source_variadic_index) .map(Parameter::annotated_type), @@ -3703,6 +4184,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } else { Type::heterogeneous_tuple( db, + env, captured_source_parameters() .map(Parameter::annotated_type), ) @@ -3724,7 +4206,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } target_index += 1; - if source.parameters.is_gradual() { + if source_parameters.is_gradual() { return match self.relation { TypeRelation::Assignability => result, TypeRelation::Subtyping @@ -3736,6 +4218,16 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } if !source_param.is_variadic() { + if let Some(context) = self.report_context() + && target_parameters.as_paramspec_with_prefix().is_none() + { + let parameter = ParameterDescription::new( + target_index, + source_param.name(), + ); + context + .push(ErrorContext::ExtraRequiredParameter { parameter }); + } return self.never(); } if !check_types( @@ -3747,6 +4239,37 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { ) { return result; } + + // Align fixed suffixes from the end, reusing the source variadic for + // any additional target suffix elements. + let source_suffix_len = parameters + .source_iter + .as_slice() + .iter() + .take_while(|parameter| parameter.is_positional()) + .count(); + let target_suffix_len = parameters + .target_iter + .as_slice() + .iter() + .take_while(|parameter| parameter.is_positional()) + .count(); + for _ in source_suffix_len..target_suffix_len { + let Some(target_parameter) = parameters.peek_target() else { + break; + }; + target_index += 1; + if !check_types( + &mut result, + target_parameter.annotated_type(), + source_param.annotated_type(), + target_parameter.name(), + target_index, + ) { + return result; + } + parameters.next_target(); + } } ( @@ -3825,6 +4348,22 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // only contains keyword-only and keyword-variadic parameters. However, if the // parameter has a default, it's valid because callers don't need to provide it. if default_type.is_none() { + if let Some(context) = self.report_context() { + if let Some(source_name) = source_param.name() + && target_parameters + .iter() + .any(|target_param| target_param.name() == Some(source_name)) + { + context.push(ErrorContext::ParameterMustAcceptKeywordArguments { + source_name: Some(source_name.clone()), + target_name: source_name.clone(), + }); + } else { + let parameter = + ParameterDescription::new(target_index, source_param.name()); + context.push(ErrorContext::ExtraRequiredParameter { parameter }); + } + } return self.never(); } } @@ -3853,6 +4392,12 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { .. } => { if source_default.is_none() && target_default.is_some() { + if let Some(context) = self.report_context() { + context.push(parameter_must_have_default( + source_param, + target_index, + )); + } return self.never(); } if !check_types( @@ -3880,6 +4425,11 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { return result; } } else { + if let Some(context) = self.report_context() { + let parameter = + ParameterDescription::new(target_index, target_param.name()); + context.push(ErrorContext::MissingParameter { parameter }); + } return self.never(); } } @@ -3887,6 +4437,11 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { let Some(source_keyword_variadic) = source_keyword_variadic else { // For a `source <: target` relationship, if `target` has a keyword variadic // parameter, `source` must also have a keyword variadic parameter. + if let Some(context) = self.report_context() + && target_parameters.as_paramspec_with_prefix().is_none() + { + context.push(ErrorContext::MissingVariadicKeywordParameter); + } return self.never(); }; if !check_types( @@ -3914,6 +4469,10 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { )] for (_, source_param) in source_keywords { if source_param.default_type().is_none() { + if let Some(context) = self.report_context() { + let parameter = ParameterDescription::new(target_index, source_param.name()); + context.push(ErrorContext::ExtraRequiredParameter { parameter }); + } return self.never(); } } @@ -4016,7 +4575,7 @@ impl<'db> Parameters<'db> { /// `TypedDict`. Use [`Self::standard`] for a known-standard list and /// [`Self::from_annotation`] when the kind should be inferred from annotations; preserve the /// existing kind when transforming a parameter list. - pub(crate) fn new(value: impl Into]>>, kind: ParametersKind<'db>) -> Self { + fn new(value: impl Into]>>, kind: ParametersKind<'db>) -> Self { Self { data: Arc::new(ParametersData { value: value.into(), @@ -4035,6 +4594,7 @@ impl<'db> Parameters<'db> { /// `**kwargs` parameter for explicit extra items or an open `TypedDict`. pub(crate) fn from_annotation( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, parameters: impl IntoIterator>, ) -> Self { let parameters: Vec> = parameters.into_iter().collect(); @@ -4047,15 +4607,17 @@ impl<'db> Parameters<'db> { // mentions `T`, that one decides `T` before these keywords are matched, and the // deferred parameter could never be re-checked against it. Drop the deferral so the // shape is reported as unsupported instead of silently going unchecked. - if parameter.annotated_type().is_typed_dict_bounded_typevar(db) - && Self::typevar_used_by_another_parameter(db, ¶meters, index) + if parameter + .annotated_type() + .is_typed_dict_bounded_typevar(db, env) + && Self::typevar_used_by_another_parameter(db, env, ¶meters, index) { parameter.annotation_kind = ParameterAnnotationKind::Normal; } if let Some(unpacked_typed_dict) = parameter.unpacked_typed_dict(db) { Self::push_unpacked_typed_dict(db, &mut value, ¶meter, unpacked_typed_dict); } else if let Some(unpacked_protocol) = parameter.unpacked_protocol(db) { - Self::push_unpacked_protocol(db, &mut value, unpacked_protocol); + Self::push_unpacked_protocol(db, env, &mut value, unpacked_protocol); } else { value.push(parameter); } @@ -4068,6 +4630,7 @@ impl<'db> Parameters<'db> { /// parameter's annotation. fn typevar_used_by_another_parameter( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, parameters: &[Parameter<'db>], index: usize, ) -> bool { @@ -4079,6 +4642,7 @@ impl<'db> Parameters<'db> { other_index != index && any_over_type( db, + env, other.annotated_type(), false, |ty| matches!(ty, Type::TypeVar(other) if other.identity(db) == identity), @@ -4109,14 +4673,16 @@ impl<'db> Parameters<'db> { Parameter::keyword_only(name.clone()) .with_annotated_type(field.declared_ty) .with_optional_default_type((!field.is_required()).then_some(Type::unknown())) - .with_definition(field.first_declaration()), + .with_definition(field.first_declaration()) + .with_source_parameter_index(parameter.source_parameter_index()), ); } if let Some(extra_items) = unpacked_typed_dict.openness(db).effective_extra_items() { value.push( Parameter::keyword_variadic(kwargs_name) - .with_annotated_type(extra_items.declared_ty), + .with_annotated_type(extra_items.declared_ty) + .with_source_parameter_index(parameter.source_parameter_index()), ); } } @@ -4128,11 +4694,12 @@ impl<'db> Parameters<'db> { /// that can only be passed by keyword fn push_unpacked_protocol( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, value: &mut Vec>, unpacked_protocol: ProtocolInstanceType<'db>, ) { let members = Type::ProtocolInstance(unpacked_protocol) - .protocol_data_members(db) + .protocol_data_members(db, env) .unwrap_or_default(); for (name, ty) in members { if value @@ -4494,7 +5061,7 @@ impl<'db> Parameters<'db> { /// Return parameters that represents `(*args: object, **kwargs: object)`, the bottom signature /// (accepts any call, so subtype of all other signatures.) - pub(crate) fn bottom() -> Self { + fn bottom() -> Self { Self::new( [ Parameter::variadic(Name::new_static("args")).with_annotated_type(Type::object()), @@ -4540,6 +5107,7 @@ impl<'db> Parameters<'db> { node_index: _, } = parameters; + let env = ProgramEnvironment::from_definition(definition); let default_type = |param: &ast::ParameterWithDefault| { param.default().map(|default| { // Use the same approach as function_signature_expression_type to avoid cycles. @@ -4547,13 +5115,14 @@ impl<'db> Parameters<'db> { // directly to infer_deferred_types without first checking infer_definition_types. infer_deferred_types(db, definition) .expression_type(default) - .replace_parameter_defaults(db) + .replace_parameter_defaults(db, &env) }) }; let pos_only_param = |param: &ast::ParameterWithDefault| { Parameter::from_node_and_kind( db, + &env, definition, ¶m.parameter, ParameterKind::PositionalOnly { @@ -4581,7 +5150,7 @@ impl<'db> Parameters<'db> { let args = match &args[..] { [only] if only.parameter.range.is_empty() - && semantic_index(db, definition.file(db)) + && semantic_index(db, definition.program_file(db)) .try_definition(&only.parameter) .is_none() => { @@ -4616,6 +5185,7 @@ impl<'db> Parameters<'db> { } Parameter::from_node_and_kind( db, + &env, definition, &arg.parameter, ParameterKind::PositionalOrKeyword { @@ -4628,6 +5198,7 @@ impl<'db> Parameters<'db> { let variadic = vararg.as_ref().map(|arg| { Parameter::from_node_and_kind( db, + &env, definition, arg, ParameterKind::Variadic { @@ -4639,6 +5210,7 @@ impl<'db> Parameters<'db> { let keyword_only = kwonlyargs.iter().map(|arg| { Parameter::from_node_and_kind( db, + &env, definition, &arg.parameter, ParameterKind::KeywordOnly { @@ -4651,6 +5223,7 @@ impl<'db> Parameters<'db> { let keywords = kwarg.as_ref().map(|arg| { Parameter::from_node_and_kind( db, + &env, definition, arg, ParameterKind::KeywordVariadic { @@ -4661,21 +5234,25 @@ impl<'db> Parameters<'db> { Self::from_annotation( db, + &env, positional_only .into_iter() .chain(positional_or_keyword) .chain(variadic) .chain(keyword_only) - .chain(keywords), + .chain(keywords) + .enumerate() + .map(|(index, parameter)| parameter.with_source_parameter_index(Some(index))), ) } fn apply_type_mapping_impl<'a>( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { if let TypeMapping::Materialize(materialization_kind) = type_mapping && matches!( @@ -4702,7 +5279,7 @@ impl<'db> Parameters<'db> { .data .value .iter() - .map(|param| param.apply_type_mapping_impl(db, &type_mapping, tcx, visitor)) + .map(|param| param.apply_type_mapping_impl(db, env, &type_mapping, tcx, visitor)) .collect(); Self::new(value, self.data.kind).expand_starred_variadic_annotations(db) @@ -4715,6 +5292,25 @@ impl<'db> Parameters<'db> { self.data.value.iter() } + /// Iterates over the `ParamSpec` bindings referenced by direct variadic component annotations. + /// + /// The returned bindings represent `P` itself, with the `args` or `kwargs` component removed. + fn paramspec_component_bindings( + &self, + db: &'db dyn Db, + ) -> impl Iterator> + '_ { + self.iter() + .filter(|parameter| parameter.is_variadic() || parameter.is_keyword_variadic()) + .filter_map(move |parameter| match parameter.annotated_type() { + Type::TypeVar(typevar) + if typevar.is_paramspec(db) && typevar.paramspec_attr(db).is_some() => + { + Some(typevar.without_paramspec_attr(db)) + } + _ => None, + }) + } + /// Iterate initial positional parameters, not including variadic parameter, if any. /// /// For a valid signature, this will be all positional parameters. In an invalid signature, @@ -4774,7 +5370,45 @@ impl<'db> Parameters<'db> { .rfind(|(_, parameter)| parameter.is_keyword_variadic()) } + /// Moves required suffix elements that match a homogeneous variadic into its prefix. + fn with_homogeneous_variadic_suffix_in_prefix(self, db: &'db dyn Db) -> Self { + let Some((variadic_index, variadic)) = self.variadic() else { + return self; + }; + + let matching_suffix_len = self.as_slice()[variadic_index + 1..] + .iter() + .take_while(|parameter| { + parameter.is_positional_only() + && parameter.annotated_type().resolve_type_alias(db) + == variadic.annotated_type().resolve_type_alias(db) + }) + .count(); + + if matching_suffix_len == 0 + || self + .as_slice() + .get(variadic_index + matching_suffix_len + 1) + .is_some_and(Parameter::is_positional) + { + return self; + } + + let mut parameters = self.as_slice().to_vec(); + parameters[variadic_index..=variadic_index + matching_suffix_len].rotate_left(1); + self.with_transformed_parameters(parameters) + } + /// Expands an unpacked `*args` annotation into its logical callable parameters. + /// + /// Preserve the original `*args` definition and source position on every expanded parameter + /// so diagnostics can identify its declaration after specialization or overload filtering. + /// + /// ```python + /// from typing import Unpack + /// + /// def callback(*args: Unpack[tuple[int, str]]) -> None: ... + /// ``` fn expand_starred_variadic_annotations(&self, db: &'db dyn Db) -> Self { if !self.data.value.iter().any(|parameter| { (parameter.is_variadic() || parameter.is_keyword_variadic()) @@ -4811,37 +5445,39 @@ impl<'db> Parameters<'db> { && let Some(tuple) = parameter.annotated_type().exact_tuple_instance_spec(db) { expanded = true; + let positional_parameter = |ty| { + Parameter::positional_only(None) + .with_annotated_type(ty) + .with_definition(parameter.definition()) + .with_source_parameter_index(parameter.source_parameter_index()) + }; match tuple.as_ref() { Tuple::Fixed(tuple) => { - parameters.extend( - tuple - .iter_all_elements() - .map(|ty| Parameter::positional_only(None).with_annotated_type(ty)), - ); + parameters.extend(tuple.iter_all_elements().map(positional_parameter)); } Tuple::Variable(variable) => { - parameters.extend( - variable - .iter_prefix_elements() - .map(|ty| Parameter::positional_only(None).with_annotated_type(ty)), - ); + parameters + .extend(variable.iter_prefix_elements().map(positional_parameter)); let name = parameter .name() .cloned() .unwrap_or_else(|| Name::new_static("args")); - parameters.push(Parameter::variadic(name).with_annotated_type( - match variable.variable() { - VariableSegment::Homogeneous(element) => element, - VariableSegment::TypeVarTuple(typevartuple) => { - Type::TypeVar(typevartuple) - } - }, - )); - parameters.extend( - variable - .iter_suffix_elements() - .map(|ty| Parameter::positional_only(None).with_annotated_type(ty)), + let variadic = Parameter::variadic(name); + let variadic = match variable.variable() { + VariableSegment::Homogeneous(element) => { + variadic.with_annotated_type(element) + } + VariableSegment::TypeVarTuple(typevartuple) => variadic + .with_annotated_type(Type::TypeVar(typevartuple)) + .with_starred_annotation(), + }; + parameters.push( + variadic + .with_definition(parameter.definition()) + .with_source_parameter_index(parameter.source_parameter_index()), ); + parameters + .extend(variable.iter_suffix_elements().map(positional_parameter)); } } } else { @@ -4850,14 +5486,14 @@ impl<'db> Parameters<'db> { } if expanded { - Self::from_annotation(db, parameters) + self.with_transformed_parameters(parameters) } else { self.clone() } } /// Expands adjacent `P.args`/`P.kwargs` placeholders into their mapped parameters. - pub(crate) fn expand_paramspec_variadics(&self, db: &'db dyn Db) -> Self { + fn expand_paramspec_variadics(&self, db: &'db dyn Db) -> Self { let mut variadic_index = None; let mut paramspec_callable = None; @@ -5019,6 +5655,13 @@ pub(crate) struct Parameter<'db> { /// relation machinery reads this. borrow: ParameterBorrow, + /// Position of the source parameter that owns this logical parameter. + /// + /// Expanded tuple and `TypedDict` parameters retain the position of their original `*args` or + /// `**kwargs` declaration. Store positions one-based so `None` does not increase the size of + /// this struct. + source_parameter_index: Option, + kind: ParameterKind<'db>, } @@ -5053,6 +5696,7 @@ impl<'db> Parameter<'db> { is_context: false, is_receiver: false, borrow: ParameterBorrow::None, + source_parameter_index: None, kind: ParameterKind::PositionalOnly { name, default_type: None, @@ -5069,6 +5713,7 @@ impl<'db> Parameter<'db> { is_context: false, is_receiver: false, borrow: ParameterBorrow::None, + source_parameter_index: None, kind: ParameterKind::PositionalOrKeyword { name, default_type: None, @@ -5085,6 +5730,7 @@ impl<'db> Parameter<'db> { is_context: false, is_receiver: false, borrow: ParameterBorrow::None, + source_parameter_index: None, kind: ParameterKind::Variadic { name }, } } @@ -5098,6 +5744,7 @@ impl<'db> Parameter<'db> { is_context: false, is_receiver: false, borrow: ParameterBorrow::None, + source_parameter_index: None, kind: ParameterKind::KeywordOnly { name, default_type: None, @@ -5114,6 +5761,7 @@ impl<'db> Parameter<'db> { is_context: false, is_receiver: false, borrow: ParameterBorrow::None, + source_parameter_index: None, kind: ParameterKind::KeywordVariadic { name }, } } @@ -5134,8 +5782,12 @@ impl<'db> Parameter<'db> { /// basedpython: mark a keyword-variadic parameter as unpacking its annotation's keys /// into keyword-only parameters. [`Parameters::from_annotation`] performs the expansion, /// the same way it does for a `def`'s `**kwargs: Unpack[TypedDict]` - pub(crate) fn with_unpacked_kwargs(mut self, db: &'db dyn Db) -> Self { - match self.annotated_type.unpacked_kwargs(db) { + pub(crate) fn with_unpacked_kwargs( + mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Self { + match self.annotated_type.unpacked_kwargs(db, env) { Some(UnpackedKwargs::TypedDict) => { self.annotation_kind = ParameterAnnotationKind::UnpackedTypedDictKwargs; } @@ -5189,16 +5841,36 @@ impl<'db> Parameter<'db> { self } + /// Records the source position without replacing a synthesized parameter's IDE definition. + /// + /// A `TypedDict` field can then keep its declaration for navigation while diagnostics refer + /// to the enclosing `**kwargs` parameter. + fn with_source_parameter_index(mut self, index: Option) -> Self { + self.source_parameter_index = index + .and_then(|index| u32::try_from(index).ok()) + .and_then(|index| index.checked_add(1)) + .and_then(NonZeroU32::new); + self + } + + /// Returns the original source parameter's position before variadic expansion. + pub(crate) fn source_parameter_index(&self) -> Option { + self.source_parameter_index + .map(|index| index.get() as usize - 1) + } + fn apply_type_mapping_impl<'a>( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { Self { annotated_type: self.annotated_type.apply_type_mapping_impl( db, + env, type_mapping, tcx, visitor, @@ -5206,21 +5878,28 @@ impl<'db> Parameter<'db> { definition: self.definition, kind: self .kind - .apply_type_mapping_impl(db, type_mapping, tcx, visitor), + .apply_type_mapping_impl(db, env, type_mapping, tcx, visitor), inferred_annotation: self.inferred_annotation, annotation_kind: self.annotation_kind, is_context: self.is_context, is_receiver: self.is_receiver, borrow: self.borrow, + source_parameter_index: self.source_parameter_index, } } - fn cycle_normalized(&self, db: &'db dyn Db, previous: &Self, cycle: &salsa::Cycle) -> Self { + fn cycle_normalized( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + previous: &Self, + cycle: &salsa::Cycle, + ) -> Self { let annotated_type = self.annotated_type - .cycle_normalized(db, previous.annotated_type, cycle); + .cycle_normalized(db, env, previous.annotated_type, cycle); - let kind = self.kind.cycle_normalized(db, &previous.kind, cycle); + let kind = self.kind.cycle_normalized(db, env, &previous.kind, cycle); Self { annotated_type, @@ -5230,13 +5909,15 @@ impl<'db> Parameter<'db> { is_context: self.is_context, is_receiver: self.is_receiver, borrow: self.borrow, + source_parameter_index: self.source_parameter_index, kind, } } - pub(super) fn recursive_type_normalized_impl( + fn recursive_type_normalized_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -5248,14 +5929,15 @@ impl<'db> Parameter<'db> { is_context, is_receiver, borrow, + source_parameter_index, kind, } = self; let annotated_type = if nested { - annotated_type.recursive_type_normalized_impl(db, div, true)? + annotated_type.recursive_type_normalized_impl(db, env, div, true)? } else { annotated_type - .recursive_type_normalized_impl(db, div, true) + .recursive_type_normalized_impl(db, env, div, true) .unwrap_or(div) }; @@ -5263,9 +5945,11 @@ impl<'db> Parameter<'db> { ParameterKind::PositionalOnly { name, default_type } => ParameterKind::PositionalOnly { name: name.clone(), default_type: match default_type { - Some(ty) if nested => Some(ty.recursive_type_normalized_impl(db, div, true)?), + Some(ty) if nested => { + Some(ty.recursive_type_normalized_impl(db, env, div, true)?) + } Some(ty) => Some( - ty.recursive_type_normalized_impl(db, div, true) + ty.recursive_type_normalized_impl(db, env, div, true) .unwrap_or(div), ), None => None, @@ -5276,10 +5960,10 @@ impl<'db> Parameter<'db> { name: name.clone(), default_type: match default_type { Some(ty) if nested => { - Some(ty.recursive_type_normalized_impl(db, div, true)?) + Some(ty.recursive_type_normalized_impl(db, env, div, true)?) } Some(ty) => Some( - ty.recursive_type_normalized_impl(db, div, true) + ty.recursive_type_normalized_impl(db, env, div, true) .unwrap_or(div), ), None => None, @@ -5289,9 +5973,11 @@ impl<'db> Parameter<'db> { ParameterKind::KeywordOnly { name, default_type } => ParameterKind::KeywordOnly { name: name.clone(), default_type: match default_type { - Some(ty) if nested => Some(ty.recursive_type_normalized_impl(db, div, true)?), + Some(ty) if nested => { + Some(ty.recursive_type_normalized_impl(db, env, div, true)?) + } Some(ty) => Some( - ty.recursive_type_normalized_impl(db, div, true) + ty.recursive_type_normalized_impl(db, env, div, true) .unwrap_or(div), ), None => None, @@ -5311,17 +5997,19 @@ impl<'db> Parameter<'db> { is_context: *is_context, is_receiver: *is_receiver, borrow: *borrow, + source_parameter_index: *source_parameter_index, kind, }) } fn from_node_and_kind( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, function_definition: Definition<'db>, parameter: &ast::Parameter, kind: ParameterKind<'db>, ) -> Self { - let index = semantic_index(db, function_definition.file(db)); + let index = semantic_index(db, function_definition.program_file(db)); let definition = Some(index.expect_single_definition(parameter)); let (annotated_type, inferred_annotation, annotation_flags, has_starred_annotation) = @@ -5347,7 +6035,7 @@ impl<'db> Parameter<'db> { // `**kwargs: Unpack[T]` where `T` is bounded by `TypedDict` is an unpacked // `TypedDict` too; which keys it contributes is only known once `T` is solved || (annotation_flags.contains(TypeExpressionFlags::UNPACK) - && annotated_type.is_typed_dict_bounded_typevar(db))); + && annotated_type.is_typed_dict_bounded_typevar(db, env))); let annotation_kind = if is_unpacked_typed_dict_kwargs { ParameterAnnotationKind::UnpackedTypedDictKwargs } else if has_starred_annotation || has_unpacked_variadic_annotation { @@ -5366,6 +6054,7 @@ impl<'db> Parameter<'db> { // span, not carried on the signature — only a callable type records // the modifier here borrow: ParameterBorrow::None, + source_parameter_index: None, kind, } } @@ -5408,7 +6097,7 @@ impl<'db> Parameter<'db> { } } - pub(crate) fn callable_by_name(&self, name: &str) -> bool { + fn callable_by_name(&self, name: &str) -> bool { match &self.kind { ParameterKind::PositionalOrKeyword { name: param_name, .. @@ -5516,7 +6205,7 @@ impl<'db> Parameter<'db> { } /// Rewrites a positional-or-keyword parameter as keyword-only while preserving its metadata. - pub(crate) fn positional_or_keyword_to_keyword_only(&self) -> Self { + fn positional_or_keyword_to_keyword_only(&self) -> Self { let mut result = self.clone(); if let ParameterKind::PositionalOrKeyword { name, default_type } = &self.kind { result.kind = ParameterKind::KeywordOnly { @@ -5580,18 +6269,25 @@ impl<'db> ParameterKind<'db> { #[expect(clippy::ref_option)] fn cycle_normalized_default( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, current: &Option>, previous: &Option>, cycle: &salsa::Cycle, ) -> Option> { match (current, previous) { - (Some(curr), Some(prev)) => Some(curr.cycle_normalized(db, *prev, cycle)), - (Some(curr), None) => Some(curr.recursive_type_normalized(db, cycle)), + (Some(curr), Some(prev)) => Some(curr.cycle_normalized(db, env, *prev, cycle)), + (Some(curr), None) => Some(curr.recursive_type_normalized(db, env, cycle)), (None, _) => *current, } } - fn cycle_normalized(&self, db: &'db dyn Db, previous: &Self, cycle: &salsa::Cycle) -> Self { + fn cycle_normalized( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + previous: &Self, + cycle: &salsa::Cycle, + ) -> Self { match (self, previous) { ( ParameterKind::PositionalOnly { name, default_type }, @@ -5601,7 +6297,13 @@ impl<'db> ParameterKind<'db> { }, ) => ParameterKind::PositionalOnly { name: name.clone(), - default_type: Self::cycle_normalized_default(db, default_type, prev_default, cycle), + default_type: Self::cycle_normalized_default( + db, + env, + default_type, + prev_default, + cycle, + ), }, ( ParameterKind::PositionalOrKeyword { name, default_type }, @@ -5611,7 +6313,13 @@ impl<'db> ParameterKind<'db> { }, ) => ParameterKind::PositionalOrKeyword { name: name.clone(), - default_type: Self::cycle_normalized_default(db, default_type, prev_default, cycle), + default_type: Self::cycle_normalized_default( + db, + env, + default_type, + prev_default, + cycle, + ), }, ( ParameterKind::KeywordOnly { name, default_type }, @@ -5621,7 +6329,13 @@ impl<'db> ParameterKind<'db> { }, ) => ParameterKind::KeywordOnly { name: name.clone(), - default_type: Self::cycle_normalized_default(db, default_type, prev_default, cycle), + default_type: Self::cycle_normalized_default( + db, + env, + default_type, + prev_default, + cycle, + ), }, // Variadic / KeywordVariadic have no types to normalize. // Also, if the current `ParameterKind` is different from `previous`, it means that `previous` is the cycle initial value, @@ -5633,9 +6347,10 @@ impl<'db> ParameterKind<'db> { fn apply_type_mapping_impl<'a>( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { let apply_to_default_type = |default_type: &Option>| { if type_mapping == &TypeMapping::ReplaceParameterDefaults && default_type.is_some() { @@ -5643,7 +6358,7 @@ impl<'db> ParameterKind<'db> { } else { default_type .as_ref() - .map(|ty| ty.apply_type_mapping_impl(db, type_mapping, tcx, visitor)) + .map(|ty| ty.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor)) } }; @@ -5672,10 +6387,12 @@ mod tests { use crate::place::global_symbol; use crate::types::{FunctionType, KnownClass, LiteralValueType}; use ruff_db::system::DbWithWritableSystem as _; + use ty_python_core::ProgramFile; #[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(); + let module = ProgramFile::new(db, module, db.program_environment().program(db)); global_symbol(db, module, "f") .place .expect_type() @@ -5717,6 +6434,7 @@ mod tests { is_context, is_receiver, borrow: _, + source_parameter_index: _, kind, } = parameter; @@ -5744,11 +6462,15 @@ mod tests { #[test] fn always_satisfied_receiver_constraints_are_discarded() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); assert!( - merge_receiver_constraints(&db, Some(&OwnedConstraintSet::always()), None).is_none() + merge_receiver_constraints(db, &env, Some(&OwnedConstraintSet::always()), None,) + .is_none() ); assert!( - merge_receiver_constraints(&db, None, Some(&OwnedConstraintSet::always())).is_none() + merge_receiver_constraints(db, &env, None, Some(&OwnedConstraintSet::always()),) + .is_none() ); } @@ -5787,18 +6509,26 @@ mod tests { let sig = func.signature(&db); - assert_eq!(sig.return_ty.display(&db).to_string(), "bytes"); + assert_eq!( + sig.return_ty + .display(&db, &db.program_environment()) + .to_string(), + "bytes" + ); assert_params_have_definitions(&sig); 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("b"))).with_annotated_type( + KnownClass::Int.to_instance(&db, &db.program_environment()), + ), Parameter::positional_only(Some(Name::new_static("c"))) .with_default_type(Type::int_literal(1)), Parameter::positional_only(Some(Name::new_static("d"))) - .with_annotated_type(KnownClass::Int.to_instance(&db)) + .with_annotated_type( + KnownClass::Int.to_instance(&db, &db.program_environment()), + ) .with_default_type(Type::int_literal(2)), Parameter::positional_or_keyword(Name::new_static("e")) .with_default_type(Type::int_literal(3)), @@ -5811,8 +6541,9 @@ mod tests { Parameter::keyword_only(Name::new_static("h")) .with_annotated_type(LiteralValueType::unpromotable(6).into()) .with_default_type(LiteralValueType::unpromotable(6).into()), - Parameter::keyword_variadic(Name::new_static("kwargs")) - .with_annotated_type(KnownClass::Str.to_instance(&db)), + Parameter::keyword_variadic(Name::new_static("kwargs")).with_annotated_type( + KnownClass::Str.to_instance(&db, &db.program_environment()), + ), ], ); } @@ -5852,7 +6583,12 @@ mod tests { }; assert_eq!(name, "a"); // Parameter resolution not deferred; we should see A not B - assert_eq!(annotated_type.display(&db).to_string(), "A"); + assert_eq!( + annotated_type + .display(&db, &db.program_environment()) + .to_string(), + "A" + ); } #[test] @@ -5890,7 +6626,12 @@ mod tests { }; assert_eq!(name, "a"); // Parameter resolution deferred: - assert_eq!(annotated_type.display(&db).to_string(), "A | B"); + assert_eq!( + annotated_type + .display(&db, &db.program_environment()) + .to_string(), + "A | B" + ); } #[test] @@ -5933,8 +6674,18 @@ mod tests { }; assert_eq!(a_name, "a"); assert_eq!(b_name, "b"); - assert_eq!(a_annotated_ty.display(&db).to_string(), "A"); - assert_eq!(b_annotated_ty.display(&db).to_string(), "T@f"); + assert_eq!( + a_annotated_ty + .display(&db, &db.program_environment()) + .to_string(), + "A" + ); + assert_eq!( + b_annotated_ty + .display(&db, &db.program_environment()) + .to_string(), + "T@f" + ); } #[test] @@ -5978,8 +6729,18 @@ mod tests { assert_eq!(a_name, "a"); assert_eq!(b_name, "b"); // Parameter resolution deferred: - assert_eq!(a_annotated_ty.display(&db).to_string(), "A | B"); - assert_eq!(b_annotated_ty.display(&db).to_string(), "T@f"); + assert_eq!( + a_annotated_ty + .display(&db, &db.program_environment()) + .to_string(), + "A | B" + ); + assert_eq!( + b_annotated_ty + .display(&db, &db.program_environment()) + .to_string(), + "T@f" + ); } #[test] diff --git a/crates/ty_python_semantic/src/types/soundness.rs b/crates/ty_python_semantic/src/types/soundness.rs index b0d026cda8..099d5a8af6 100644 --- a/crates/ty_python_semantic/src/types/soundness.rs +++ b/crates/ty_python_semantic/src/types/soundness.rs @@ -15,6 +15,7 @@ use ruff_db::files::File; use crate::Db; use crate::place::{Place, explicit_global_symbol}; +use crate::types::ProgramEnvironment; use crate::types::instance::Protocol; use crate::types::literal::LiteralValueTypeKind; use crate::types::reified_infer::{ @@ -144,10 +145,15 @@ fn python_string_literal(value: &str) -> String { /// protocol with no faithful check degrades to [`CastCheck::Unchecked`]. This /// keeps the emitted code from ever running `isinstance` against a subscripted /// or non-`@runtime_checkable` protocol, which raises at runtime. -pub fn cast_check_plan<'db>(db: &'db dyn Db, file: File, ty: Type<'db>) -> Option { +pub fn cast_check_plan<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + file: File, + ty: Type<'db>, +) -> Option { if let Type::ProtocolInstance(instance) = ty { if let Protocol::FromClass(protocol_class) = instance.inner - && protocol_structural_members(db, file, *protocol_class).is_some() + && protocol_structural_members(db, env, file, *protocol_class).is_some() { return Some(CastCheck::Protocol); } @@ -158,7 +164,7 @@ pub fn cast_check_plan<'db>(db: &'db dyn Db, file: File, ty: Type<'db>) -> Optio if let Some(members) = literal_members(db, ty) { return Some(CastCheck::Members(members)); } - runtime_check_plan(db, file, ty).map(CastCheck::Kind) + runtime_check_plan(db, env, file, ty).map(CastCheck::Kind) } /// whether a checked cast to `target` has no faithful runtime residue and must @@ -167,11 +173,12 @@ pub fn cast_check_plan<'db>(db: &'db dyn Db, file: File, ty: Type<'db>) -> Optio /// transpiler emitting `isinstance(value, )`, which raises at runtime pub fn cast_target_is_unverifiable_protocol<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, target: Type<'db>, ) -> bool { matches!( - cast_check_plan(db, file, target), + cast_check_plan(db, env, file, target), Some(CastCheck::Unchecked) ) } @@ -181,41 +188,56 @@ pub fn cast_target_is_unverifiable_protocol<'db>( /// (`def t[T]() -> T`), or the method is bound to a specialized generic /// instance (`dict[str, int].get`), where the specialization itself is an /// unverified annotation-level claim -pub fn call_result_is_typevar_derived<'db>(db: &'db dyn Db, callee: Type<'db>) -> bool { +pub fn call_result_is_typevar_derived<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + callee: Type<'db>, +) -> bool { match callee { - Type::FunctionLiteral(function) => return_mentions_type_var(db, function), + Type::FunctionLiteral(function) => return_mentions_type_var(db, env, function), Type::BoundMethod(method) => { - is_specialized_generic_instance(db, method.self_instance(db)) - || return_mentions_type_var(db, method.function(db)) + is_specialized_generic_instance(db, env, method.self_instance(db)) + || return_mentions_type_var(db, env, method.function(db)) } _ => false, } } -fn return_mentions_type_var<'db>(db: &'db dyn Db, function: FunctionType<'db>) -> bool { +fn return_mentions_type_var<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + function: FunctionType<'db>, +) -> bool { function .signature(db) .overloads .iter() - .any(|signature| mentions_type_var(db, signature.return_ty)) + .any(|signature| mentions_type_var(db, env, signature.return_ty)) } -fn mentions_type_var<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { - any_over_type(db, ty, false, |nested| matches!(nested, Type::TypeVar(_))) +fn mentions_type_var<'db>(db: &'db dyn Db, env: &ProgramEnvironment<'db>, ty: Type<'db>) -> bool { + any_over_type(db, env, ty, false, |nested| { + matches!(nested, Type::TypeVar(_)) + }) } /// whether `ty` is an instance whose static shape includes a generic /// specialization — the runtime-unverifiable part of an annotation like /// `list[str]`. element projections out of such values (subscripts, method /// results, iteration) are where the specialization's claim is consumed -pub fn is_specialized_generic_instance<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { +pub fn is_specialized_generic_instance<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> bool { match ty { Type::NominalInstance(instance) => { - matches!(instance.class(db), ClassType::Generic(_)) + matches!(instance.class(db, env), ClassType::Generic(_)) } // a generic protocol instance (`Iterator[str]`, `Sequence[int]`) // carries the same annotation-level specialization claim Type::ProtocolInstance(instance) => match instance.inner { + Protocol::Materialized(_) => false, Protocol::FromClass(class) => matches!(*class, ClassType::Generic(_)), Protocol::Synthesized(_) => false, }, @@ -225,7 +247,7 @@ pub fn is_specialized_generic_instance<'db>(db: &'db dyn Db, ty: Type<'db>) -> b Type::Union(union) => union .elements(db) .iter() - .any(|element| is_specialized_generic_instance(db, *element)), + .any(|element| is_specialized_generic_instance(db, env, *element)), _ => false, } } @@ -236,8 +258,13 @@ pub fn is_specialized_generic_instance<'db>(db: &'db dyn Db, ty: Type<'db>) -> b /// unsolved typevars) or its name cannot be resolved at module scope in /// `file`. the check is deliberately shallow: `list[str]` validates as /// `list` — the element claim is validated at its own projection sites -pub fn runtime_check_target<'db>(db: &'db dyn Db, file: File, ty: Type<'db>) -> Option { - target(db, file, ty, 0) +pub fn runtime_check_target<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + file: File, + ty: Type<'db>, +) -> Option { + target(db, env, file, ty, 0) } /// The runtime soundness check for a value whose declared type is `ty`. @@ -247,14 +274,19 @@ pub fn runtime_check_target<'db>(db: &'db dyn Db, file: File, ty: Type<'db>) -> /// arguments are checkable at runtime); otherwise falls back to the shallow /// [`CheckKind::Isinstance`] of [`runtime_check_target`]. `None` when neither /// applies (no faithful runtime test). -pub fn runtime_check_plan<'db>(db: &'db dyn Db, file: File, ty: Type<'db>) -> Option { - if let Some((alias, variances)) = parametric_soundness_spelling(db, file, ty) { +pub fn runtime_check_plan<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + file: File, + ty: Type<'db>, +) -> Option { + if let Some((alias, variances)) = parametric_soundness_spelling(db, env, file, ty) { return Some(CheckKind::Parametric { alias, variances: variances.iter().copied().map(variance_code).collect(), }); } - runtime_check_target(db, file, ty).map(CheckKind::Isinstance) + runtime_check_target(db, env, file, ty).map(CheckKind::Isinstance) } /// whether a runtime check against `ty` must silently drop a type-argument @@ -262,10 +294,15 @@ pub fn runtime_check_plan<'db>(db: &'db dyn Db, file: File, ty: Type<'db>) -> Op /// erased at runtime, so only the origin class can be tested. this is what /// separates `list[int]` (a builtin, erased — only `list` is checkable) from /// `A[int]` (a user generic, whose instances carry `__orig_class__`) -pub fn erases_type_arguments<'db>(db: &'db dyn Db, file: File, ty: Type<'db>) -> bool { +pub fn erases_type_arguments<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + file: File, + ty: Type<'db>, +) -> bool { match ty { Type::NominalInstance(instance) => { - let ClassType::Generic(alias) = instance.class(db) else { + let ClassType::Generic(alias) = instance.class(db, env) else { return false; }; // a bare `list` infers as `list[Unknown]`: no argument was written, @@ -275,12 +312,12 @@ pub fn erases_type_arguments<'db>(db: &'db dyn Db, file: File, ty: Type<'db>) -> .types(db) .iter() .any(|argument| !argument.is_dynamic()) - && parametric_soundness_spelling(db, file, ty).is_none() + && parametric_soundness_spelling(db, env, file, ty).is_none() } Type::Union(union) => union .elements(db) .iter() - .any(|element| erases_type_arguments(db, file, *element)), + .any(|element| erases_type_arguments(db, env, file, *element)), _ => false, } } @@ -292,8 +329,13 @@ pub fn erases_type_arguments<'db>(db: &'db dyn Db, file: File, ty: Type<'db>) -> /// arguments are erased, or a subscripted protocol (`Sequence[object]`) whose /// bare `isinstance` is itself a runtime error. gradual `Any`/`Unknown` values /// are *not* subtypes of a concrete target, so their checks are kept -pub fn cast_is_redundant<'db>(db: &'db dyn Db, value_ty: Type<'db>, target: Type<'db>) -> bool { - value_ty.is_subtype_of(db, target) +pub fn cast_is_redundant<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + value_ty: Type<'db>, + target: Type<'db>, +) -> bool { + value_ty.is_subtype_of(db, env, target) } /// the runtime variance code the `_parametric_is` probe expects @@ -306,19 +348,25 @@ fn variance_code(variance: ArgVariance) -> u8 { } } -fn target<'db>(db: &'db dyn Db, file: File, ty: Type<'db>, depth: u8) -> Option { +fn target<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + file: File, + ty: Type<'db>, + depth: u8, +) -> Option { if depth > 8 { return None; } // literals promote to their instance form (`Literal[3]` → `int`, // `LiteralString` → `str`, enum literals → the enum class) - let ty = ty.promote(db); + let ty = ty.promote(db, env); match ty { Type::NominalInstance(instance) => { if ty.is_none(db) { return Some("type(None)".to_owned()); } - let class = instance.class(db); + let class = instance.class(db, env); let literal = class.class_literal(db); // `object` always passes — a check would validate nothing if literal.is_known(db, KnownClass::Object) { @@ -329,7 +377,7 @@ fn target<'db>(db: &'db dyn Db, file: File, ty: Type<'db>, depth: u8) -> Option< Type::Union(union) => { let mut parts: Vec = Vec::new(); for element in union.elements(db) { - let part = target(db, file, *element, depth + 1)?; + let part = target(db, env, file, *element, depth + 1)?; if !parts.contains(&part) { parts.push(part); } @@ -349,7 +397,7 @@ fn target<'db>(db: &'db dyn Db, file: File, ty: Type<'db>, depth: u8) -> Option< builtin_target(db, file, "type") } Type::TypeIs(_) | Type::TypeGuard(_) => builtin_target(db, file, "bool"), - Type::TypeAlias(alias) => target(db, file, alias.value_type(db), depth + 1), + Type::TypeAlias(alias) => target(db, env, file, alias.value_type(db), depth + 1), _ => None, } } @@ -360,7 +408,7 @@ fn target<'db>(db: &'db dyn Db, file: File, ty: Type<'db>, depth: u8) -> Option< /// builtin so the bare name reaches it fn class_target<'db>(db: &'db dyn Db, file: File, literal: ClassLiteral<'db>) -> Option { let name = literal.name(db).as_str(); - match explicit_global_symbol(db, file, name).place { + match explicit_global_symbol(db, db.program_file(file), name).place { Place::Defined(defined) if defined.ty == Type::ClassLiteral(literal) => { Some(name.to_owned()) } @@ -370,14 +418,14 @@ fn class_target<'db>(db: &'db dyn Db, file: File, literal: ClassLiteral<'db>) -> } fn class_is_builtin<'db>(db: &'db dyn Db, literal: ClassLiteral<'db>) -> bool { - file_to_module(db, literal.file(db)) + file_to_module(db, literal.program_file(db).resolver_file(db)) .is_some_and(|module| module.is_known(db, KnownModule::Builtins)) } /// a builtin referenced by bare name is only trustworthy when the module /// does not rebind that name fn builtin_target(db: &dyn Db, file: File, name: &str) -> Option { - match explicit_global_symbol(db, file, name).place { + match explicit_global_symbol(db, db.program_file(file), name).place { Place::Undefined => Some(name.to_owned()), Place::Defined(_) => None, } @@ -403,6 +451,7 @@ pub enum ArgSelector<'a> { /// a missed check is a no-op, a wrong one changes semantics pub fn parameter_check_plan<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, file: File, callee: Type<'db>, selector: ArgSelector<'_>, @@ -419,7 +468,7 @@ pub fn parameter_check_plan<'db>( if parameter.is_variadic() || parameter.is_keyword_variadic() { return None; } - runtime_check_plan(db, file, parameter.annotated_type()) + runtime_check_plan(db, env, file, parameter.annotated_type()) } /// the sole overload of `callee`'s signature, or `None` if `callee` is not a diff --git a/crates/ty_python_semantic/src/types/special_form.rs b/crates/ty_python_semantic/src/types/special_form.rs index 30d000353e..63684a9bc3 100644 --- a/crates/ty_python_semantic/src/types/special_form.rs +++ b/crates/ty_python_semantic/src/types/special_form.rs @@ -2,6 +2,7 @@ //! Each of these is considered to inhabit a unique type in our model of the type system. use super::{ClassType, Type, TypeFormType, class::KnownClass}; +use crate::ProgramEnvironment; use crate::db::Db; use crate::types::IntersectionType; use crate::types::infer::InferenceFlags; @@ -13,11 +14,10 @@ use crate::types::{ enclosing_class_for_self, function_known_decorator_flags, is_class_type_parameters_scope, }, }; -use ruff_db::files::File; use strum_macros::EnumString; -use ty_module_resolver::{KnownModule, file_to_module, resolve_module_confident}; +use ty_module_resolver::{ImportingFile, KnownModule, file_to_module, resolve_module_confident}; use ty_python_core::{ - FileScopeId, + FileScopeId, ProgramFile, definition::{Definition, DefinitionKind}, place::ScopedPlaceId, place_table, @@ -78,7 +78,7 @@ pub enum SpecialFormType { NoReturn, /// The symbol `typing.Never` available since 3.11 (which can also be found as `typing_extensions.Never`) Never, - /// The symbol `ty_extensions.Unknown` + /// The symbol `ty_extensions._internal.Unknown` Unknown, /// The symbol `ty_extensions._internal.Divergent` Divergent, @@ -237,15 +237,35 @@ impl SpecialFormType { /// 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)` + /// so `SpecialFormType::Literal.instance_fallback(db, python_version)` /// returns `Type::NominalInstance(NominalInstanceType { class: })`. - pub(super) fn instance_fallback(self, db: &dyn Db) -> Type<'_> { - self.class().to_instance(db) + pub(super) fn instance_fallback<'db>( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { + self.class().to_instance(db, env) + } + + /// Return `true` if this special form is guaranteed to be a singleton at runtime. + /// + /// 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. + pub(super) const fn is_guaranteed_singleton(self) -> bool { + !(self.check_module(KnownModule::Typing) + && self.check_module(KnownModule::TypingExtensions)) } /// Return the type denoted by this retained special-form value when it is valid without /// parameters or a surrounding inference scope. - pub(crate) fn type_form_argument(self, db: &dyn Db) -> Option> { + pub(crate) fn type_form_argument<'db>( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { match self { Self::Never | Self::NoReturn => Some(Type::Never), Self::LiteralString => Some(Type::literal_string()), @@ -255,37 +275,45 @@ impl SpecialFormType { Self::AlwaysFalsy => Some(Type::AlwaysFalsy), Self::NamedTuple => Some(IntersectionType::from_two_elements( db, - Type::homogeneous_tuple(db, Type::object()), - KnownClass::NamedTupleLike.to_instance(db), + env, + Type::homogeneous_tuple(db, env, Type::object()), + KnownClass::NamedTupleLike.to_instance(db, env), )), - Self::Type => Some(KnownClass::Type.to_instance(db)), + Self::Type => Some(KnownClass::Type.to_instance(db, env)), Self::TypeForm => Some(TypeFormType::from_type_expression(db, Type::any())), - Self::Tuple => Some(Type::homogeneous_tuple(db, Type::unknown())), + Self::Tuple => Some(Type::homogeneous_tuple(db, env, Type::unknown())), Self::TypingCallable | Self::CollectionsAbcCallable => { Some(Type::Callable(CallableType::unknown(db))) } - Self::LegacyStdlibAlias(alias) => Some(alias.aliased_class().to_instance(db)), + Self::LegacyStdlibAlias(alias) => Some(alias.aliased_class().to_instance(db, env)), _ => None, } } /// 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 is_instance_of( + self, + db: &dyn Db, + env: &ProgramEnvironment<'_>, + class: ClassType, + ) -> bool { + self.class().is_subclass_of(db, env, class) } pub(super) fn try_from_file_and_name( db: &dyn Db, - file: File, + file: ImportingFile<'_>, symbol_name: &str, ) -> Option { - Self::candidates_from_name(symbol_name) + let candidates = Self::candidates_from_name(symbol_name); + if candidates.is_empty() { + return None; + } + + let known_module = file_to_module(db, file.resolver_file(db))?.known(db)?; + candidates .iter() - .find(|candidate| { - file_to_module(db, file) - .and_then(|module| module.known(db)) - .is_some_and(|known_module| candidate.check_module(known_module)) - }) + .find(|candidate| candidate.check_module(known_module)) .copied() } @@ -532,7 +560,7 @@ impl SpecialFormType { /// /// 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 { + const fn check_module(self, module: KnownModule) -> bool { match self { Self::TypeQualifier(qualifier) => qualifier.check_module(module), Self::LegacyStdlibAlias(_) @@ -562,8 +590,7 @@ impl SpecialFormType { matches!(module, KnownModule::Typing | KnownModule::TypingExtensions) } - Self::Unknown - | Self::AlwaysTruthy + Self::AlwaysTruthy | Self::AlwaysFalsy | Self::Not | Self::Top @@ -572,7 +599,8 @@ impl SpecialFormType { | Self::UnsafeUnion | Self::Overlapping => module.is_ty_extensions(), - Self::Divergent + Self::Unknown + | Self::Divergent | Self::Todo | Self::TypeOf | Self::CallableTypeOf @@ -587,8 +615,12 @@ impl SpecialFormType { } } - pub(super) fn to_meta_type(self, db: &dyn Db) -> Type<'_> { - self.class().to_class_literal(db) + pub(super) fn to_meta_type<'db>( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { + self.class().to_class_literal(db, env) } /// Return true if this special form is callable at runtime. @@ -597,16 +629,16 @@ impl SpecialFormType { pub(super) const fn is_callable(self) -> bool { match self { // TypedDict can be called as a constructor to create TypedDict types - Self::TypedDict(_) + Self::TypedDict(_) => true, // Collection constructors are callable // TODO actually implement support for calling them - | Self::LegacyStdlibAlias( + Self::LegacyStdlibAlias( LegacyStdlibAlias::ChainMap | LegacyStdlibAlias::Counter | LegacyStdlibAlias::DefaultDict | LegacyStdlibAlias::Deque - | LegacyStdlibAlias::OrderedDict + | LegacyStdlibAlias::OrderedDict, ) | Self::NamedTuple => true, Self::TypeForm => true, @@ -617,7 +649,7 @@ impl SpecialFormType { LegacyStdlibAlias::List | LegacyStdlibAlias::Dict | LegacyStdlibAlias::Set - | LegacyStdlibAlias::FrozenSet + | LegacyStdlibAlias::FrozenSet, ) | Self::Tuple | Self::Type => false, @@ -702,9 +734,11 @@ impl SpecialFormType { | Self::Divergent | Self::Todo | Self::TypeOf - | Self::Any // can be used in `issubclass()` but not `isinstance()`. - | Self::Unpack => false, - Self::TypeForm => false, + | Self::Unpack + | Self::TypeForm => false, + + // can be used in `issubclass()` but not `isinstance()`. + Self::Any => false, } } @@ -797,8 +831,7 @@ impl SpecialFormType { SpecialFormType::CollectionsAbcCallable => &[KnownModule::CollectionsAbc], - SpecialFormType::Unknown - | SpecialFormType::AlwaysTruthy + SpecialFormType::AlwaysTruthy | SpecialFormType::AlwaysFalsy | SpecialFormType::Not | SpecialFormType::Intersection @@ -807,7 +840,8 @@ impl SpecialFormType { | SpecialFormType::Top | SpecialFormType::Bottom => &[KnownModule::TyExtensions], - SpecialFormType::Divergent + SpecialFormType::Unknown + | SpecialFormType::Divergent | SpecialFormType::Todo | SpecialFormType::TypeOf | SpecialFormType::CallableTypeOf @@ -815,11 +849,17 @@ impl SpecialFormType { } } - pub(super) fn definition(self, db: &dyn Db) -> Option> { + pub(super) fn definition<'db>( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { self.definition_modules() .iter() .find_map(|module| { - let file = resolve_module_confident(db, &module.name())?.file(db)?; + let module = + resolve_module_confident(db, env.resolver_environment(db), &module.name())?; + let file = ProgramFile::new(db, module.file(db)?, env.program(db)); let scope = FileScopeId::global().to_scope_id(db, file); let symbol_id = place_table(db, scope).symbol_id(self.name())?; @@ -843,6 +883,7 @@ impl SpecialFormType { typevar_binding_context: Option>, inference_flags: InferenceFlags, ) -> Result, InvalidTypeExpression<'db>> { + let env = ProgramEnvironment::from_scope(scope_id); match self { Self::Never | Self::NoReturn => Ok(Type::Never), Self::LiteralString => Ok(Type::literal_string()), @@ -863,8 +904,9 @@ impl SpecialFormType { // See conversation in https://github.com/astral-sh/ruff/pull/19915. Self::NamedTuple => Ok(IntersectionType::from_two_elements( db, - Type::homogeneous_tuple(db, Type::object()), - KnownClass::NamedTupleLike.to_instance(db), + &env, + Type::homogeneous_tuple(db, &env, Type::object()), + KnownClass::NamedTupleLike.to_instance(db, &env), )), Self::TypingSelf => { @@ -872,7 +914,8 @@ impl SpecialFormType { return Err(InvalidTypeExpression::TypingSelfInTypeAlias); } - let index = semantic_index(db, scope_id.file(db)); + let program_file = scope_id.program_file(db); + let index = semantic_index(db, program_file); // In a class's own type parameter list, `Self` is meaningful only as a // *default*. As a *bound* it could never be checked: specializing the class // (`C[X]`) happens where there is no receiver for `Self` to denote. @@ -910,17 +953,28 @@ impl SpecialFormType { } let is_in_metaclass = KnownClass::Type - .to_class_literal(db) + .to_class_literal(db, &env) .to_class_type(db) .is_some_and(|type_class| { class .default_specialization(db) - .is_subclass_of(db, type_class) + .is_subclass_of(db, &env, type_class) }); if is_in_metaclass { return Err(InvalidTypeExpression::TypingSelfInMetaclass); } + if inference_flags.contains(InferenceFlags::HAS_INCOMPATIBLE_SELF_RECEIVER) + && inference_flags.intersects( + InferenceFlags::IN_RETURN_TYPE | InferenceFlags::IN_PARAMETER_ANNOTATION, + ) + && let Some(typing_self) = typing_self + { + return Err(InvalidTypeExpression::TypingSelfWithIncompatibleReceiver( + typing_self, + )); + } + Ok(typing_self .map(Type::TypeVar) .unwrap_or(Type::SpecialForm(self))) @@ -969,13 +1023,15 @@ impl SpecialFormType { | Self::RegularCallableTypeOf => Err(InvalidTypeExpression::RequiresOneArgument(self)), // We treat `typing.Type` exactly the same as `builtins.type`: - SpecialFormType::Type => Ok(KnownClass::Type.to_instance(db)), + SpecialFormType::Type => Ok(KnownClass::Type.to_instance(db, &env)), SpecialFormType::TypeForm => Ok(TypeFormType::from_type_expression(db, Type::any())), - SpecialFormType::Tuple => Ok(Type::homogeneous_tuple(db, Type::unknown())), + SpecialFormType::Tuple => Ok(Type::homogeneous_tuple(db, &env, Type::unknown())), SpecialFormType::TypingCallable | SpecialFormType::CollectionsAbcCallable => { Ok(Type::Callable(CallableType::unknown(db))) } - SpecialFormType::LegacyStdlibAlias(alias) => Ok(alias.aliased_class().to_instance(db)), + SpecialFormType::LegacyStdlibAlias(alias) => { + Ok(alias.aliased_class().to_instance(db, &env)) + } SpecialFormType::TypeQualifier(qualifier) => { Err(InvalidTypeExpression::TypeQualifier(qualifier)) } diff --git a/crates/ty_python_semantic/src/types/string_annotation.rs b/crates/ty_python_semantic/src/types/string_annotation.rs index f51d386e11..a0ceb3e2bd 100644 --- a/crates/ty_python_semantic/src/types/string_annotation.rs +++ b/crates/ty_python_semantic/src/types/string_annotation.rs @@ -82,7 +82,7 @@ pub(crate) fn parse_string_annotation( let mut diagnostic = builder.into_diagnostic("Syntax error in forward annotation"); - diagnostic.set_primary_message(&error); + diagnostic.set_primary_annotation_message(&error); let possible_secondary = string_literal .range() diff --git a/crates/ty_python_semantic/src/types/subclass_of.rs b/crates/ty_python_semantic/src/types/subclass_of.rs index bf2fd0e0c4..6fb9d63e41 100644 --- a/crates/ty_python_semantic/src/types/subclass_of.rs +++ b/crates/ty_python_semantic/src/types/subclass_of.rs @@ -1,3 +1,6 @@ +use crate::Db; +use crate::FxOrderSet; +use crate::ProgramEnvironment; use crate::place::PlaceAndQualifiers; use crate::types::class::DynamicClassLiteral; use crate::types::constraints::ConstraintSet; @@ -9,7 +12,6 @@ use crate::types::{ ProtocolInstanceType, SpecialFormType, Type, TypeContext, TypeMapping, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarVariance, TypedDictType, UnionType, todo_type, }; -use crate::{Db, FxOrderSet}; use ty_python_core::definition::Definition; /// A type that represents `type[C]`, i.e. the class object `C` and class objects that are subclasses of `C`. @@ -39,14 +41,18 @@ impl<'db> SubclassOfType<'db> { /// /// 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> { + pub(crate) fn from( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + subclass_of: impl Into>, + ) -> Type<'db> { let subclass_of = subclass_of.into(); match subclass_of { SubclassOfInner::Class(class) => { if class.is_final(db) { Type::from(class) } else if class.is_object(db) { - Self::subclass_of_object(db) + Self::subclass_of_object(db, env) } else { Type::SubclassOf(Self { subclass_of }) } @@ -65,7 +71,11 @@ impl<'db> SubclassOfType<'db> { } /// Given the class object `T`, returns a [`Type`] instance representing `type[T]`. - pub(crate) fn try_from_type(db: &'db dyn Db, ty: Type<'db>) -> Option> { + pub(crate) fn try_from_type( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + ) -> Option> { let subclass_of = match ty { Type::Dynamic(dynamic) => SubclassOfInner::Dynamic(dynamic), Type::ClassLiteral(literal) => { @@ -79,24 +89,29 @@ impl<'db> SubclassOfType<'db> { _ => return None, }; - Some(Self::from(db, subclass_of)) + Some(Self::from(db, env, subclass_of)) } /// Given an instance of the class or type variable `T`, returns a [`Type`] instance representing `type[T]`. - pub(crate) fn try_from_instance(db: &'db dyn Db, ty: Type<'db>) -> Option> { + pub(crate) fn try_from_instance( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + ) -> Option> { // Handle unions by distributing `type[]` over each element: // `type[A | B]` -> `type[A] | type[B]` match ty { Type::Union(union) => UnionType::try_from_elements( db, + env, union .elements(db) .iter() - .map(|element| Self::try_from_instance(db, *element)), + .map(|element| Self::try_from_instance(db, env, *element)), ), - Type::ProtocolInstance(protocol) => Some(protocol.to_meta_type(db)), - _ => SubclassOfInner::try_from_instance(db, ty) - .map(|subclass_of| Self::from(db, subclass_of)), + Type::ProtocolInstance(protocol) => Some(protocol.to_meta_type(db, env)), + _ => SubclassOfInner::try_from_instance(db, env, ty) + .map(|subclass_of| Self::from(db, env, subclass_of)), } } @@ -116,9 +131,9 @@ impl<'db> SubclassOfType<'db> { } /// Return a [`Type`] instance representing the type `type[object]`. - pub(crate) fn subclass_of_object(db: &'db dyn Db) -> Type<'db> { + fn subclass_of_object(db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { // See the documentation of `SubclassOfType::from` for details. - KnownClass::Type.to_instance(db) + KnownClass::Type.to_instance(db, env) } /// Return the inner [`SubclassOfInner`] value wrapped by this `SubclassOfType`. @@ -130,6 +145,7 @@ impl<'db> SubclassOfType<'db> { pub(super) fn meta_write_requirement( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, ) -> Option<(Option>, TypeQualifiers)> { let SubclassOfInner::Protocol(protocol) = self.subclass_of else { @@ -137,7 +153,7 @@ impl<'db> SubclassOfType<'db> { }; protocol .interface(db) - .meta_write_requirement(db, Type::ProtocolInstance(protocol), name) + .meta_write_requirement(db, env, Type::ProtocolInstance(protocol), name) .map(|(write_ty, mut qualifiers)| { // `ClassVar` prohibits instance writes, not writes through the class object. qualifiers.remove(TypeQualifiers::CLASS_VAR); @@ -163,11 +179,15 @@ impl<'db> SubclassOfType<'db> { /// Return the exact class-object type of this `type[T]` `TypeVar`'s upper bound, if it has one. /// /// This can only succeed when the upper bound normalizes to a final class. - pub(crate) fn exact_typevar_upper_bound(self, db: &'db dyn Db) -> Option> { + pub(crate) fn exact_typevar_upper_bound( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { self.into_type_var() - .and_then(|typevar| typevar.typevar(db).upper_bound(db)) + .and_then(|typevar| typevar.typevar(db).upper_bound(db, env)) .and_then(|bound| { - let bound = Self::try_from_instance(db, bound.resolve_type_alias(db))?; + let bound = Self::try_from_instance(db, env, bound.resolve_type_alias(db))?; matches!(bound, Type::ClassLiteral(_) | Type::GenericAlias(_)).then_some(bound) }) } @@ -175,32 +195,35 @@ impl<'db> SubclassOfType<'db> { pub(super) fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Type<'db> { match self.subclass_of { SubclassOfInner::Class(class) => Type::SubclassOf(Self { subclass_of: SubclassOfInner::Class(class.apply_type_mapping_impl( db, + env, type_mapping, tcx, visitor, )), }), SubclassOfInner::Protocol(protocol) => protocol - .apply_type_mapping_impl(db, type_mapping, tcx, visitor) - .to_meta_type(db), + .apply_type_mapping_impl(db, env, type_mapping, tcx, visitor) + .to_meta_type(db, visitor.env), SubclassOfInner::Dynamic(_) => match type_mapping { TypeMapping::Materialize(materialization_kind) => match materialization_kind { - MaterializationKind::Top => KnownClass::Type.to_instance(db), + MaterializationKind::Top => KnownClass::Type.to_instance(db, visitor.env), MaterializationKind::Bottom => Type::Never, }, _ => Type::SubclassOf(self), }, SubclassOfInner::TypeVar(typevar) => { - let mapped = typevar.apply_type_mapping_impl(db, type_mapping, visitor); - Self::try_from_instance(db, mapped).unwrap_or_else(|| mapped.to_meta_type(db)) + let mapped = typevar.apply_type_mapping_impl(db, env, type_mapping, visitor); + Self::try_from_instance(db, visitor.env, mapped) + .unwrap_or_else(|| mapped.to_meta_type(db, visitor.env)) } } } @@ -208,6 +231,7 @@ impl<'db> SubclassOfType<'db> { pub(super) fn find_legacy_typevars_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, @@ -215,14 +239,15 @@ impl<'db> SubclassOfType<'db> { match self.subclass_of { SubclassOfInner::Dynamic(_) => {} SubclassOfInner::Class(class) => { - class.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + class.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } SubclassOfInner::Protocol(protocol) => { - protocol.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + protocol.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } SubclassOfInner::TypeVar(typevar) => { Type::TypeVar(typevar).find_legacy_typevars_impl( db, + env, binding_context, typevars, visitor, @@ -234,49 +259,51 @@ impl<'db> SubclassOfType<'db> { pub(crate) fn find_name_in_mro_with_policy( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, policy: MemberLookupPolicy, ) -> Option> { if let SubclassOfInner::Protocol(protocol) = self.subclass_of - && let Some(member) = protocol.interface(db).meta_member(db, name) + && let Some(member) = protocol.interface(db).meta_member(db, env, name) { return Some(member); } - let class_like = match self.subclass_of.with_transposed_type_var(db) { + let class_like = match self.subclass_of.with_transposed_type_var(db, env) { SubclassOfInner::Class(class) => Type::from(class), SubclassOfInner::Dynamic(dynamic) => Type::Dynamic(dynamic), - SubclassOfInner::Protocol(protocol) => Type::from(*protocol.class_origin()?), + SubclassOfInner::Protocol(protocol) => Type::from(*protocol.class_origin(db)?), SubclassOfInner::TypeVar(bound_typevar) => { - match bound_typevar.typevar(db).bound_or_constraints(db) { + match bound_typevar.typevar(db).bound_or_constraints(db, env) { None => unreachable!(), Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound, Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - constraints.as_type(db) + constraints.as_type(db, env) } } } }; - class_like.find_name_in_mro_with_policy(db, name, policy) + class_like.find_name_in_mro_with_policy(db, env, name, policy) } pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { Some(Self { subclass_of: self .subclass_of - .recursive_type_normalized_impl(db, div, nested)?, + .recursive_type_normalized_impl(db, env, div, nested)?, }) } - pub(crate) fn to_instance(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn to_instance(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match self.subclass_of { - SubclassOfInner::Class(class) => Type::instance(db, class), + SubclassOfInner::Class(class) => Type::instance(db, env, class), SubclassOfInner::Dynamic(dynamic_type) => Type::Dynamic(dynamic_type), SubclassOfInner::Protocol(protocol) => Type::ProtocolInstance(protocol), SubclassOfInner::TypeVar(bound_typevar) => Type::TypeVar(bound_typevar), @@ -284,14 +311,18 @@ impl<'db> SubclassOfType<'db> { } /// Return a type representing "the set of all instances of the metaclass of this type". - pub(crate) fn to_metaclass_instance(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn to_metaclass_instance( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { // This kind of looks like a no-op, but it's not. For `type[C]` where `C` has metaclass // `M`, `to_meta_type` transforms `type[C]` to `type[M]`, and then `to_instance` makes it // just `M`. And `to_meta_type` will transpose `type[T: C]` into `T: type[C]`, collapse to // the upper bound `type[C]`, and transform that to the meta-type `type[M]`, which // `to_instance` then resolves to `M`. - self.to_meta_type(db) - .to_instance_approximation(db) + self.to_meta_type(db, env) + .to_instance_approximation(db, env) .expect("the meta-type of a SubclassOf type should always be instantiable") } @@ -300,46 +331,56 @@ impl<'db> SubclassOfType<'db> { /// For `type[C]` where `C` is a concrete class, this returns `type[metaclass(C)]`. /// For `type[T]` where `T` is a `TypeVar`, this computes the metatype based on the /// `TypeVar`'s bounds or constraints. - pub(crate) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { - match self.subclass_of.with_transposed_type_var(db) { + pub(crate) fn to_meta_type(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + match self.subclass_of.with_transposed_type_var(db, env) { SubclassOfInner::Dynamic(dynamic) => { - SubclassOfType::from(db, SubclassOfInner::Dynamic(dynamic)) + SubclassOfType::from(db, env, SubclassOfInner::Dynamic(dynamic)) + } + SubclassOfInner::Class(class) => { + SubclassOfType::try_from_type(db, env, class.metaclass(db)) + .unwrap_or(SubclassOfType::subclass_of_unknown()) } - SubclassOfInner::Class(class) => SubclassOfType::try_from_type(db, class.metaclass(db)) - .unwrap_or(SubclassOfType::subclass_of_unknown()), // Structural implementations of a protocol can have arbitrary metaclasses. The only // guaranteed upper bound is therefore `type`, not the protocol origin's metaclass. - SubclassOfInner::Protocol(_) => KnownClass::Type.to_subclass_of(db), + SubclassOfInner::Protocol(_) => KnownClass::Type.to_subclass_of(db, env), // For `type[T]` where `T` is a TypeVar, `with_transposed_type_var` transforms // the bounds from instance types to `type[]` types. For example, `type[T]` where // `T: A | B` becomes a TypeVar with bound `type[A] | type[B]`. The metatype is // then the metatype of that bound. SubclassOfInner::TypeVar(bound_typevar) => { - match bound_typevar.typevar(db).bound_or_constraints(db) { + match bound_typevar.typevar(db).bound_or_constraints(db, env) { // `with_transposed_type_var` always adds a bound for unbounded TypeVars None => unreachable!(), - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound.to_meta_type(db), + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + bound.to_meta_type(db, env) + } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - constraints.as_type(db).to_meta_type(db) + constraints.as_type(db, env).to_meta_type(db, env) } } } } } - pub(crate) fn is_typed_dict(self, db: &'db dyn Db) -> bool { + pub(crate) fn is_typed_dict(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool { self.subclass_of - .into_class(db) + .into_class(db, env) .is_some_and(|class| class.class_literal(db).is_typed_dict(db)) } } impl<'db> VarianceInferable<'db> for SubclassOfType<'db> { - fn variance_of(self, db: &dyn Db, typevar: BoundTypeVarIdentity<'_>) -> TypeVarVariance { + fn variance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'_>, + ) -> TypeVarVariance { match self.subclass_of { - SubclassOfInner::Class(class) => class.variance_of(db, typevar), - SubclassOfInner::Protocol(protocol) => protocol.variance_of(db, typevar), - SubclassOfInner::Dynamic(_) | SubclassOfInner::TypeVar(_) => TypeVarVariance::Bivariant, + SubclassOfInner::Class(class) => class.variance_of(db, env, typevar), + SubclassOfInner::Protocol(protocol) => protocol.variance_of(db, env, typevar), + SubclassOfInner::TypeVar(inner) => Type::TypeVar(inner).variance_of(db, env, typevar), + SubclassOfInner::Dynamic(_) => TypeVarVariance::Bivariant, } } } @@ -363,7 +404,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { return self.check_type_pair( db, Type::ProtocolInstance(source_protocol), - target.to_instance(db), + target.to_instance(db, self.env), ); } @@ -374,11 +415,11 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { (SubclassOfInner::Dynamic(_), SubclassOfInner::Class(target_class)) => { ConstraintSet::from_bool( self.constraints, - target_class.is_object(db) || self.is_eager_assignability(), + target_class.is_object(db) || self.relation.is_assignability(), ) } (SubclassOfInner::Class(_), SubclassOfInner::Dynamic(_)) => { - ConstraintSet::from_bool(self.constraints, self.is_eager_assignability()) + ConstraintSet::from_bool(self.constraints, self.relation.is_assignability()) } // For example, `type[bool]` describes all possible runtime subclasses of the class `bool`, @@ -423,7 +464,7 @@ impl<'c, 'db> DisjointnessChecker<'_, 'c, 'db> { (SubclassOfInner::Class(left), SubclassOfInner::Class(right)) => { ConstraintSet::from_bool( self.constraints, - !left.could_coexist_in_mro_with_disjointness_checker(db, right, self), + !left.could_coexist_in_mro_with_disjointness_checker(db, self.env, right, self), ) } (SubclassOfInner::TypeVar(_), _) | (_, SubclassOfInner::TypeVar(_)) => { @@ -461,31 +502,37 @@ pub(crate) enum SubclassOfInner<'db> { } impl<'db> SubclassOfInner<'db> { - pub(crate) const fn unknown() -> Self { + const fn unknown() -> Self { Self::Dynamic(DynamicType::Unknown) } - pub(crate) const fn is_dynamic(self) -> bool { + const fn is_dynamic(self) -> bool { matches!(self, Self::Dynamic(_)) } - pub(crate) const fn is_type_var(self) -> bool { + const fn is_type_var(self) -> bool { matches!(self, Self::TypeVar(_)) } - pub(crate) fn into_class(self, db: &'db dyn Db) -> Option> { + pub(crate) fn into_class( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { match self { Self::Dynamic(_) | Self::Protocol(_) => None, Self::Class(class) => Some(class), Self::TypeVar(bound_typevar) => { - match bound_typevar.typevar(db).bound_or_constraints(db) { - None => Some(ClassType::object(db)), + match bound_typevar.typevar(db).bound_or_constraints(db, env) { + None => Some(ClassType::object(db, env)), Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - Self::try_from_instance(db, bound) - .and_then(|subclass_of| subclass_of.into_class(db)) + Self::try_from_instance(db, env, bound) + .and_then(|subclass_of| subclass_of.into_class(db, env)) } // TODO this is quite imprecise - Some(TypeVarBoundOrConstraints::Constraints(_)) => Some(ClassType::object(db)), + Some(TypeVarBoundOrConstraints::Constraints(_)) => { + Some(ClassType::object(db, env)) + } } } } @@ -505,9 +552,13 @@ impl<'db> SubclassOfInner<'db> { } } - pub(crate) fn try_from_instance(db: &'db dyn Db, ty: Type<'db>) -> Option { + fn try_from_instance( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + ) -> Option { Some(match ty { - Type::NominalInstance(instance) => SubclassOfInner::Class(instance.class(db)), + Type::NominalInstance(instance) => SubclassOfInner::Class(instance.class(db, env)), Type::TypedDict(typed_dict) => match typed_dict { TypedDictType::Class(class) => SubclassOfInner::Class(class), TypedDictType::Synthesized(_) => SubclassOfInner::Dynamic( @@ -537,7 +588,11 @@ impl<'db> SubclassOfInner<'db> { /// - Otherwise, for an unbounded type variable, this returns `type[object]`. /// /// If this is type of a concrete type `C`, returns the type unchanged. - pub(crate) fn with_transposed_type_var(self, db: &'db dyn Db) -> Self { + pub(crate) fn with_transposed_type_var( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Self { let Some(bound_typevar) = self.into_type_var() else { return self; }; @@ -545,15 +600,15 @@ impl<'db> SubclassOfInner<'db> { let bound_typevar = bound_typevar.map_bound_or_constraints(db, |bound_or_constraints| { Some(match bound_or_constraints { None => TypeVarBoundOrConstraints::UpperBound( - SubclassOfType::try_from_instance(db, Type::object()) + SubclassOfType::try_from_instance(db, env, Type::object()) .unwrap_or(SubclassOfType::subclass_of_unknown()), ), Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - TypeVarBoundOrConstraints::UpperBound(bound.to_meta_type(db)) + TypeVarBoundOrConstraints::UpperBound(bound.to_meta_type(db, env)) } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { TypeVarBoundOrConstraints::Constraints( - constraints.map(db, |constraint| constraint.to_meta_type(db)), + constraints.map(db, |constraint| constraint.to_meta_type(db, env)), ) } }) @@ -562,19 +617,20 @@ impl<'db> SubclassOfInner<'db> { Self::TypeVar(bound_typevar) } - pub(super) fn recursive_type_normalized_impl( + fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { match self { Self::Class(class) => Some(Self::Class( - class.recursive_type_normalized_impl(db, div, nested)?, + class.recursive_type_normalized_impl(db, env, div, nested)?, )), Self::Dynamic(dynamic) => Some(Self::Dynamic(dynamic.recursive_type_normalized())), Self::Protocol(protocol) => Some(Self::Protocol( - protocol.recursive_type_normalized_impl(db, div, nested)?, + protocol.recursive_type_normalized_impl(db, env, div, nested)?, )), Self::TypeVar(_) => Some(self), } diff --git a/crates/ty_python_semantic/src/types/subscript.rs b/crates/ty_python_semantic/src/types/subscript.rs index ca92580294..feca40de34 100644 --- a/crates/ty_python_semantic/src/types/subscript.rs +++ b/crates/ty_python_semantic/src/types/subscript.rs @@ -1,12 +1,12 @@ //! Inference for subscript expressions (e.g., `x[0]`, `list[int]`). +use crate::Db; +use crate::ProgramEnvironment; use std::fmt::{self, Display}; use compact_str::{CompactString, ToCompactString}; -use itertools::Itertools; use ruff_python_ast as ast; -use crate::Db; use crate::subscript::{PyIndex, PySlice}; use crate::types::special_form::TypeQualifier; @@ -23,8 +23,8 @@ use super::infer::TypeContext; use super::instance::SliceLiteral; use super::special_form::SpecialFormType; use super::{ - IntersectionBuilder, IntersectionType, KnownInstanceType, Type, TypeAliasType, TypedDictType, - UnionBuilder, UnionType, UnsafeUnionType, todo_type, + ClassLiteral, IntersectionBuilder, IntersectionType, KnownInstanceType, Type, TypeAliasType, + TypeVarBoundOrConstraints, TypedDictType, UnionBuilder, UnsafeUnionType, todo_type, }; /// basedpython: whether subscripting a value of type `ty` is a runtime @@ -35,20 +35,28 @@ use super::{ /// checker could not resolve keeps the specialization reading, which is what a /// keyword subscript in an annotation (`A[T=int]`) needs — and the reading ty /// itself takes for those. -pub(crate) fn is_runtime_subscript<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { +pub(crate) fn is_runtime_subscript<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> bool { match ty { - Type::Restricted(restricted) => is_runtime_subscript(db, restricted.value_type(db)), - Type::Overlapping(overlapping) => is_runtime_subscript(db, overlapping.value_type(db)), - Type::TypeAlias(alias) => is_runtime_subscript(db, alias.value_type(db)), + Type::Restricted(restricted) => is_runtime_subscript(db, env, restricted.value_type(db)), + Type::Overlapping(overlapping) => { + is_runtime_subscript(db, env, overlapping.value_type(db, env)) + } + Type::TypeAlias(alias) => is_runtime_subscript(db, env, alias.value_type(db)), Type::Union(union) => union .elements(db) .iter() - .all(|element| is_runtime_subscript(db, *element)), + .all(|element| is_runtime_subscript(db, env, *element)), Type::NominalInstance(_) | Type::ProtocolInstance(_) | Type::TypedDict(_) | Type::NewTypeInstance(_) - | Type::LiteralValue(_) => !ty.is_assignable_to(db, KnownClass::Type.to_instance(db)), + | Type::LiteralValue(_) => { + !ty.is_assignable_to(db, env, KnownClass::Type.to_instance(db, env)) + } _ => false, } } @@ -62,7 +70,7 @@ pub(crate) enum SubscriptKind { } impl SubscriptKind { - pub(crate) const fn as_str(self) -> &'static str { + const fn as_str(self) -> &'static str { match self { Self::Tuple => "tuple", Self::String => "string", @@ -91,7 +99,7 @@ impl Display for DunderMethod { } impl DunderMethod { - pub(crate) const fn as_str(self) -> &'static str { + const fn as_str(self) -> &'static str { match self { Self::GetItem => "__getitem__", Self::ClassGetItem => "__class_getitem__", @@ -134,6 +142,8 @@ pub(crate) enum SubscriptErrorKind<'db> { SliceStepSizeZero, /// A non-generic PEP 695 type alias was subscripted. NonGenericTypeAlias { alias: TypeAliasType<'db> }, + /// A non-generic subclass of a generic class was subscripted. + NonGenericClass { class: ClassLiteral<'db> }, /// `__getitem__` or `__class_getitem__` exists but is possibly unbound. DunderPossiblyUnbound { method: DunderMethod, @@ -241,6 +251,7 @@ impl<'db> SubscriptErrorKind<'db> { slice_node: &ast::Expr, ) { let db = context.db(); + let env = context.program_environment(); match self { Self::IndexOutOfBounds { kind, @@ -271,19 +282,27 @@ impl<'db> SubscriptErrorKind<'db> { diagnostic.annotate(context.secondary(&*subscript.value).message( format_args!( "Alias to `{}`, which is already specialized", - value_type.display(db) + value_type.display(db, env) ), )); } } } + Self::NonGenericClass { class } => { + if let Some(builder) = context.report_lint(&NOT_SUBSCRIPTABLE, subscript) { + builder.into_diagnostic(format_args!( + "Cannot specialize non-generic class `{}`", + class.name(db) + )); + } + } Self::DunderPossiblyUnbound { method, value_ty } => { if let Some(builder) = context.report_lint(&POSSIBLY_MISSING_IMPLICIT_CALL, value_node) { builder.into_diagnostic(format_args!( "Method `{method}` of type `{}` may be missing", - value_ty.display(db), + value_ty.display(db, env), )); } } @@ -299,8 +318,8 @@ impl<'db> SubscriptErrorKind<'db> { builder.into_diagnostic(format_args!( "Method `{method}` of type `{}` is not callable \ on object of type `{}`", - bindings.callable_type().display(db), - value_ty.display(db), + bindings.callable_type().display(db, env), + value_ty.display(db, env), )); } } @@ -321,9 +340,9 @@ impl<'db> SubscriptErrorKind<'db> { builder.into_diagnostic(format_args!( "Method `{method}` of type `{}` cannot be called \ with key of type `{}` on object of type `{}`", - bindings.callable_type().display(db), - slice_ty.display(db), - value_ty.display(db), + bindings.callable_type().display(db, env), + slice_ty.display(db, env), + value_ty.display(db, env), )); } } @@ -332,8 +351,8 @@ impl<'db> SubscriptErrorKind<'db> { builder.into_diagnostic(format_args!( "Method `{method}` of type `{}` may not be callable \ on object of type `{}`", - bindings.callable_type().display(db), - value_ty.display(db), + bindings.callable_type().display(db, env), + value_ty.display(db, env), )); } } @@ -367,7 +386,7 @@ impl<'db> SubscriptErrorKind<'db> { if let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, value_node) { builder.into_diagnostic(format_args!( "`{}` is not a valid argument to `{origin}`", - argument_ty.display(db), + argument_ty.display(db, env), )); } } @@ -408,24 +427,27 @@ impl<'db> SubscriptErrorKind<'db> { } } -fn map_union_subscript<'db, F>( +/// Preserve a constrained type variable when each alternative's result matches that constraint. +fn map_subscript_alternatives<'db>( db: &'db dyn Db, - union: UnionType<'db>, - mut map_fn: F, -) -> Result, SubscriptError<'db>> -where - F: FnMut(Type<'db>) -> Result, SubscriptError<'db>>, -{ - let mut builder = UnionBuilder::new(db); + env: &ProgramEnvironment<'db>, + full_object_ty: Type<'db>, + alternatives: impl IntoIterator>, + mut map_fn: impl FnMut(Type<'db>) -> Result, SubscriptError<'db>>, +) -> Result, SubscriptError<'db>> { + let mut builder = UnionBuilder::new(db, env); let mut errors = Vec::new(); + let mut preserves_typevar = matches!(full_object_ty, Type::TypeVar(_)); - for element in union.elements(db) { - match map_fn(*element) { + for element in alternatives { + match map_fn(element) { Ok(result) => { + if preserves_typevar { + preserves_typevar = result.is_equivalent_to(db, env, element); + } builder = builder.add(result); } Err(error) => { - let full_object_ty = Type::Union(union); builder = builder.add(error.result_type()); errors.extend( error @@ -437,24 +459,30 @@ where } } - builder = builder.recursively_defined(union.recursively_defined(db)); - let result_ty = builder.build(); + if let Type::Union(union) = full_object_ty { + builder = builder.recursively_defined(union.recursively_defined(db)); + } if errors.is_empty() { - Ok(result_ty) + Ok(if preserves_typevar { + full_object_ty + } else { + builder.build() + }) } else { - Err(SubscriptError::with_errors(result_ty, errors)) + Err(SubscriptError::with_errors(builder.build(), errors)) } } fn map_intersection_subscript<'db, F>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, intersection: IntersectionType<'db>, map_fn: F, ) -> Result, SubscriptError<'db>> where F: FnMut(Type<'db>) -> Result, SubscriptError<'db>>, { - if let Some(alternatives) = intersection.finite_alternative_union(db) { + if let Some(alternatives) = intersection.finite_alternative_union(db, env) { let mut map_fn = map_fn; return map_fn(alternatives); } @@ -469,7 +497,7 @@ where |db, results| { results .into_iter() - .fold(IntersectionBuilder::new(db), |builder, result| { + .fold(IntersectionBuilder::new(db, env), |builder, result| { builder.add_positive(result) }) .build() @@ -560,11 +588,12 @@ where // `Unknown` otherwise. This is not naturally representable via synthesized `__getitem__` overloads. fn typed_dict_subscript<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typed_dict: TypedDictType<'db>, slice_ty: Type<'db>, ) -> Result, SubscriptError<'db>> { if let Some(fallback) = slice_ty.materialized_divergent_fallback() { - return typed_dict_subscript(db, typed_dict, fallback); + return typed_dict_subscript(db, env, typed_dict, fallback); } if slice_ty.is_dynamic() { @@ -576,14 +605,14 @@ fn typed_dict_subscript<'db>( .map(|literal| literal.value(db)) else { if typed_dict.explicit_extra_items(db).is_some() - && slice_ty.is_assignable_to(db, KnownClass::Str.to_instance(db)) + && slice_ty.is_assignable_to(db, env, KnownClass::Str.to_instance(db, env)) { - return Ok(typed_dict.value_type(db)); + return Ok(typed_dict.value_type(db, env)); } let result_ty = if typed_dict.openness(db).is_closed() - && slice_ty.is_assignable_to(db, KnownClass::Str.to_instance(db)) + && slice_ty.is_assignable_to(db, env, KnownClass::Str.to_instance(db, env)) { - typed_dict.value_type(db) + typed_dict.value_type(db, env) } else { Type::unknown() }; @@ -616,16 +645,17 @@ impl<'db> Type<'db> { pub(super) fn subscript( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, slice_ty: Type<'db>, expr_context: ast::ExprContext, tcx: TypeContext<'db>, ) -> Result, SubscriptError<'db>> { if let Some(fallback) = self.materialized_divergent_fallback() { - return fallback.subscript(db, slice_ty, expr_context, tcx); + return fallback.subscript(db, env, slice_ty, expr_context, tcx); } if let Some(fallback) = slice_ty.materialized_divergent_fallback() { - return self.subscript(db, fallback, expr_context, tcx); + return self.subscript(db, env, fallback, expr_context, tcx); } let value_ty = self; @@ -633,86 +663,118 @@ impl<'db> Type<'db> { let inferred = match (value_ty, slice_ty) { (Type::Dynamic(_) | Type::Divergent(_) | Type::Never, _) => Some(Ok(value_ty)), - (Type::Overlapping(overlapping), _) => Some(overlapping.value_type(db).subscript( + (Type::Overlapping(overlapping), _) => Some(overlapping.value_type(db, env).subscript( db, + env, slice_ty, expr_context, tcx, )), - (Type::Restricted(restricted), _) => Some(restricted.value_type(db).subscript( - db, - slice_ty, - expr_context, - tcx, - )), + (Type::Restricted(restricted), _) => { + Some( + restricted + .value_type(db) + .subscript(db, env, slice_ty, expr_context, tcx), + ) + } + + (Type::Deferred(deferred), _) => { + Some( + deferred + .reduced(db, env) + .subscript(db, env, slice_ty, expr_context, tcx), + ) + } - (Type::Deferred(deferred), _) => Some(deferred.reduced(db).subscript( + (Type::TypeAlias(alias), _) => { + Some( + alias + .value_type(db) + .subscript(db, env, slice_ty, expr_context, tcx), + ) + } + + (_, Type::TypeAlias(alias)) => { + Some(value_ty.subscript(db, env, alias.value_type(db), expr_context, tcx)) + } + + (Type::Union(union), _) => Some(map_subscript_alternatives( db, - slice_ty, - expr_context, - tcx, + env, + value_ty, + union.elements(db).iter().copied(), + |element| element.subscript(db, env, slice_ty, expr_context, tcx), )), - (Type::TypeAlias(alias), _) => Some(alias.value_type(db).subscript( + (_, Type::Union(union)) => Some(map_subscript_alternatives( db, + env, slice_ty, - expr_context, - tcx, + union.elements(db).iter().copied(), + |element| value_ty.subscript(db, env, element, expr_context, tcx), )), - (_, Type::TypeAlias(alias)) => { - Some(value_ty.subscript(db, alias.value_type(db), expr_context, tcx)) + (Type::EnumComplement(complement), _) => { + Some(complement.remaining_literal_union(db, env).subscript( + db, + env, + slice_ty, + expr_context, + tcx, + )) } - (Type::Union(union), _) => Some(map_union_subscript(db, union, |element| { - element.subscript(db, slice_ty, expr_context, tcx) - })), - - (_, Type::Union(union)) => Some(map_union_subscript(db, union, |element| { - value_ty.subscript(db, element, expr_context, tcx) - })), - - (Type::EnumComplement(complement), _) => Some( - complement - .remaining_literal_union(db) - .subscript(db, slice_ty, expr_context, tcx), - ), - (_, Type::EnumComplement(complement)) => Some(value_ty.subscript( db, - complement.remaining_literal_union(db), + env, + complement.remaining_literal_union(db, env), expr_context, tcx, )), - (Type::Intersection(intersection), _) => { - Some(map_intersection_subscript(db, intersection, |element| { - element.subscript(db, slice_ty, expr_context, tcx) - })) - } + (Type::Intersection(intersection), _) => Some(map_intersection_subscript( + db, + env, + intersection, + |element| element.subscript(db, env, slice_ty, expr_context, tcx), + )), - (_, Type::Intersection(intersection)) => { - Some(map_intersection_subscript(db, intersection, |element| { - value_ty.subscript(db, element, expr_context, tcx) - })) - } + (_, Type::Intersection(intersection)) => Some(map_intersection_subscript( + db, + env, + intersection, + |element| value_ty.subscript(db, env, element, expr_context, tcx), + )), (Type::UnsafeUnion(unsafe_union), _) => { Some(map_unsafe_union_subscript(db, unsafe_union, |element| { - element.subscript(db, slice_ty, expr_context, tcx) + element.subscript(db, env, slice_ty, expr_context, tcx) })) } (_, Type::UnsafeUnion(unsafe_union)) => { Some(map_unsafe_union_subscript(db, unsafe_union, |element| { - value_ty.subscript(db, element, expr_context, tcx) + value_ty.subscript(db, env, element, expr_context, tcx) })) } + (Type::TypeVar(typevar), _) + if let Some(TypeVarBoundOrConstraints::Constraints(constraints)) = + typevar.typevar(db).bound_or_constraints(db, env) => + { + Some(map_subscript_alternatives( + db, + env, + value_ty, + constraints.elements(db).iter().copied(), + |constraint| constraint.subscript(db, env, slice_ty, expr_context, tcx), + )) + } + // Ex) Given `person["name"]`, return `str` (Type::TypedDict(typed_dict), _) if expr_context != ast::ExprContext::Store => { - Some(typed_dict_subscript(db, typed_dict, slice_ty)) + Some(typed_dict_subscript(db, env, typed_dict, slice_ty)) } ( @@ -741,10 +803,10 @@ impl<'db> Type<'db> { // Ex) Given `("a", "b", "c", "d")[1]`, return `"b"` (Type::NominalInstance(nominal), Type::LiteralValue(literal)) if let Some(i64_int) = literal.as_int() - && let Some(tuple) = nominal.tuple_spec(db) + && let Some(tuple) = nominal.tuple_spec(db, env) && let Ok(i32_int) = i32::try_from(i64_int) => { - let result = tuple.py_index(db, i32_int).map_err(|_| { + let result = tuple.py_index(db, env, i32_int).map_err(|_| { SubscriptError::new( Type::unknown(), SubscriptErrorKind::IndexOutOfBounds { @@ -763,13 +825,20 @@ impl<'db> Type<'db> { ( Type::NominalInstance(maybe_tuple_nominal), Type::NominalInstance(maybe_slice_nominal), - ) if let Some(tuple) = maybe_tuple_nominal.tuple_spec(db) + ) if let Some(tuple) = maybe_tuple_nominal.tuple_spec(db, env) && let Some(SliceLiteral { start, stop, step }) = maybe_slice_nominal.slice_literal(db) => { - Some(tuple.py_slice_type(db, start, stop, step).map_err(|_| { - SubscriptError::new(Type::unknown(), SubscriptErrorKind::SliceStepSizeZero) - })) + Some( + tuple + .py_slice_type(db, env, start, stop, step) + .map_err(|_| { + SubscriptError::new( + Type::unknown(), + SubscriptErrorKind::SliceStepSizeZero, + ) + }), + ) } // Ex) Given `"value"[1]`, return `"a"` @@ -780,7 +849,7 @@ impl<'db> Type<'db> { { let literal_value = literal_ty.value(db); - let result = match (&mut literal_value.chars()).py_index(db, i32_int) { + let result = match (&mut literal_value.chars()).py_index(db, env, i32_int) { Ok(ch) => Ok(Type::string_literal(db, ch.to_compact_string())), Err(_) => Err(SubscriptError::new( Type::unknown(), @@ -839,7 +908,7 @@ impl<'db> Type<'db> { { let literal_value = literal_ty.value(db); - let result = match literal_value.py_index(db, i32_int) { + let result = match literal_value.py_index(db, env, i32_int) { Ok(byte) => Ok(Type::int_literal((*byte).into())), Err(_) => Err(SubscriptError::new( Type::unknown(), @@ -881,14 +950,26 @@ impl<'db> Type<'db> { if (lhs_literal.is_string() || lhs_literal.is_bytes()) && let Some(bool) = rhs_literal.as_bool() => { - Some(value_ty.subscript(db, Type::int_literal(i64::from(bool)), expr_context, tcx)) + Some(value_ty.subscript( + db, + env, + Type::int_literal(i64::from(bool)), + expr_context, + tcx, + )) } (Type::NominalInstance(nominal), Type::LiteralValue(literal)) if let Some(bool) = literal.as_bool() - && nominal.tuple_spec(db).is_some() => + && nominal.tuple_spec(db, env).is_some() => { - Some(value_ty.subscript(db, Type::int_literal(i64::from(bool)), expr_context, tcx)) + Some(value_ty.subscript( + db, + env, + Type::int_literal(i64::from(bool)), + expr_context, + tcx, + )) } (Type::KnownInstance(KnownInstanceType::SubscriptedProtocol(_)), _) => { @@ -896,16 +977,13 @@ impl<'db> Type<'db> { Some(Ok(todo_type!("doubly-specialized typing.Protocol"))) } - ( - Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(alias))), - _, - ) if alias.generic_context(db).is_none() => { + (Type::KnownInstance(KnownInstanceType::TypeAliasType(alias)), _) + if alias.generic_context(db).is_none() => + { debug_assert!(alias.specialization(db).is_none()); Some(Err(SubscriptError::new( Type::unknown(), - SubscriptErrorKind::NonGenericTypeAlias { - alias: TypeAliasType::PEP695(alias), - }, + SubscriptErrorKind::NonGenericTypeAlias { alias }, ))) } @@ -936,7 +1014,7 @@ impl<'db> Type<'db> { Some(Ok(todo_type!("Inference of subscript on special form"))) } - // TODO: more complex logic required for the `Type::TypeVar(_) branch! + // Upper-bounded and unconstrained type variables use ordinary method lookup. ( Type::FunctionLiteral(_) | Type::WrapperDescriptor(_) @@ -977,16 +1055,17 @@ impl<'db> Type<'db> { // See: https://docs.python.org/3/reference/datamodel.html#class-getitem-versus-getitem match value_ty.try_call_dunder( db, + env, "__getitem__", CallArguments::positional([slice_ty]), tcx, ) { Ok(outcome) => { - return Ok(outcome.return_type(db)); + return Ok(outcome.return_type(db, env)); } Err(CallDunderError::PossiblyUnbound { bindings, .. }) => { return Err(SubscriptError::new( - bindings.return_type(db), + bindings.return_type(db, env), SubscriptErrorKind::DunderPossiblyUnbound { method: DunderMethod::GetItem, value_ty, @@ -995,7 +1074,7 @@ impl<'db> Type<'db> { } Err(CallDunderError::CallError(call_error_kind, bindings, _)) => { return Err(SubscriptError::new( - bindings.return_type(db), + bindings.return_type(db, env), SubscriptErrorKind::DunderCallError { method: DunderMethod::GetItem, value_ty, @@ -1019,20 +1098,21 @@ impl<'db> Type<'db> { // 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(db, KnownClass::Type.to_instance(db)) { + if value_ty.is_subtype_of(db, env, KnownClass::Type.to_instance(db, env)) { let call_arguments = CallArguments::positional([slice_ty]); match value_ty.try_call_dunder_on_class( db, + env, "__class_getitem__", &call_arguments, TypeContext::default(), ) { Ok(bindings) => { - return Ok(bindings.return_type(db)); + return Ok(bindings.return_type(db, env)); } Err(CallDunderError::PossiblyUnbound { bindings, .. }) => { return Err(SubscriptError::new( - bindings.return_type(db), + bindings.return_type(db, env), SubscriptErrorKind::DunderPossiblyUnbound { method: DunderMethod::ClassGetItem, value_ty, @@ -1041,7 +1121,7 @@ impl<'db> Type<'db> { } Err(CallDunderError::CallError(call_error_kind, bindings, _)) => { return Err(SubscriptError::new( - bindings.return_type(db), + bindings.return_type(db, env), SubscriptErrorKind::DunderCallError { method: DunderMethod::ClassGetItem, value_ty, @@ -1058,7 +1138,7 @@ impl<'db> Type<'db> { if let Type::ClassLiteral(class) = value_ty { if class.is_known(db, KnownClass::Type) { - return Ok(KnownClass::GenericAlias.to_instance(db)); + return Ok(KnownClass::GenericAlias.to_instance(db, env)); } if class.generic_context(db).is_some() { @@ -1071,21 +1151,22 @@ impl<'db> Type<'db> { // expression. return Ok(value_ty); } - } - // TODO: properly handle old-style generics; get rid of this temporary hack - if !value_ty - .as_class_literal() - .is_some_and(|class| class.iter_mro(db).contains(&ClassBase::Generic)) - { - return Err(SubscriptError::new( - Type::unknown(), - SubscriptErrorKind::NotSubscriptable { - value_ty, - method: DunderMethod::ClassGetItem, - }, - )); + if class.iter_mro(db).any(|base| base == ClassBase::Generic) { + return Err(SubscriptError::new( + Type::unknown(), + SubscriptErrorKind::NonGenericClass { class }, + )); + } } + + return Err(SubscriptError::new( + Type::unknown(), + SubscriptErrorKind::NotSubscriptable { + value_ty, + method: DunderMethod::ClassGetItem, + }, + )); } else if expr_context != ast::ExprContext::Store { return Err(SubscriptError::new( Type::unknown(), diff --git a/crates/ty_python_semantic/src/types/tests.rs b/crates/ty_python_semantic/src/types/tests.rs index ec1125e821..e6d83c1640 100644 --- a/crates/ty_python_semantic/src/types/tests.rs +++ b/crates/ty_python_semantic/src/types/tests.rs @@ -2,10 +2,13 @@ use super::*; use crate::db::tests::{TestDbBuilder, setup_db}; use crate::place::{typing_extensions_symbol, typing_symbol}; use crate::types::type_alias::PEP695TypeAliasType; +use crate::{Db, ProgramEnvironment}; use ruff_db::system::DbWithWritableSystem as _; use ruff_python_ast as ast; use ruff_python_ast::PythonVersion; use test_case::test_case; +use ty_python_core::program::Program; +use ty_python_core::{ProgramFile, TestProgramDb as _}; /// Explicitly test for Python version <3.13 and >=3.13, to ensure that /// the fallback to `typing_extensions` is working correctly. @@ -18,9 +21,10 @@ fn no_default_type_is_singleton(python_version: PythonVersion) { .build() .unwrap(); - let no_default = KnownClass::NoDefaultType.to_instance(&db); + let env = db.program_environment(); + let no_default = KnownClass::NoDefaultType.to_instance(&db, &env); - assert!(no_default.is_singleton(&db)); + assert!(no_default.is_singleton(&db, &env)); } #[test] @@ -30,21 +34,35 @@ fn typing_vs_typeshed_no_default() { .build() .unwrap(); - let typing_no_default = typing_symbol(&db, "NoDefault").place.expect_type(); - let typing_extensions_no_default = typing_extensions_symbol(&db, "NoDefault") + let typing_no_default = typing_symbol(&db, &db.program_environment(), "NoDefault") .place .expect_type(); + let typing_extensions_no_default = + typing_extensions_symbol(&db, &db.program_environment(), "NoDefault") + .place + .expect_type(); - assert_eq!(typing_no_default.display(&db).to_string(), "NoDefault"); assert_eq!( - typing_extensions_no_default.display(&db).to_string(), + typing_no_default + .display(&db, &db.program_environment()) + .to_string(), + "NoDefault" + ); + assert_eq!( + typing_extensions_no_default + .display(&db, &db.program_environment()) + .to_string(), "NoDefault" ); } -fn list_alias<'db>(db: &'db dyn Db, argument: Type<'db>) -> GenericAlias<'db> { +fn list_alias<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + argument: Type<'db>, +) -> GenericAlias<'db> { KnownClass::List - .to_specialized_class_type(db, &[argument]) + .to_specialized_class_type(db, env, &[argument]) .expect("`list` should accept one type argument") .into_generic_alias() .expect("a specialized `list` should be a generic alias") @@ -55,32 +73,35 @@ fn oscillating_generic_alias_cycle_recover<'db>( cycle: &salsa::Cycle, previous: &Type<'db>, current: Type<'db>, + program: Program<'db>, ) -> Type<'db> { - current.cycle_normalized(db, *previous, cycle) + let env = ProgramEnvironment::from_program(program); + current.cycle_normalized(db, &env, *previous, cycle) } #[salsa::tracked( returns(copy), - cycle_initial=|_, id| Type::divergent(id), + cycle_initial=|_, id, _| Type::divergent(id), cycle_fn=oscillating_generic_alias_cycle_recover, )] -fn oscillating_generic_alias(db: &dyn Db) -> Type<'_> { - let previous = oscillating_generic_alias(db); +fn oscillating_generic_alias<'db>(db: &'db dyn Db, program: Program<'db>) -> Type<'db> { + let env = ProgramEnvironment::from_program(program); + let previous = oscillating_generic_alias(db, program); let argument = if let Type::GenericAlias(alias) = previous && alias.specialization(db).types(db) == [Type::unknown()] { - KnownClass::Int.to_instance(db) + KnownClass::Int.to_instance(db, &env) } else { Type::unknown() }; - list_alias(db, argument).into() + list_alias(db, &env, argument).into() } #[test] fn generic_alias_cycle_recovery_normalizes_same_origin_unknown_oscillation() { let db = setup_db(); - let Type::GenericAlias(alias) = oscillating_generic_alias(&db) else { + let Type::GenericAlias(alias) = oscillating_generic_alias(&db, db.program()) else { panic!("cycle recovery should preserve the generic alias"); }; @@ -90,14 +111,16 @@ fn generic_alias_cycle_recovery_normalizes_same_origin_unknown_oscillation() { #[test] fn generic_alias_cycle_recovery_rejects_unsafe_merges() { let db = setup_db(); - let int = list_alias(&db, KnownClass::Int.to_instance(&db)); - let str = list_alias(&db, KnownClass::Str.to_instance(&db)); - assert!(str.merge_cycle_recovery(&db, int).is_none()); + let db = &db; + let env = db.program_environment(); + let int = list_alias(db, &env, KnownClass::Int.to_instance(db, &env)); + let str = list_alias(db, &env, KnownClass::Str.to_instance(db, &env)); + assert!(str.merge_cycle_recovery(db, int).is_none()); - let generic_context = int.specialization(&db).generic_context(&db); + let generic_context = int.specialization(db).generic_context(db); let unknown_generic = Type::Dynamic(DynamicType::UnknownGeneric(generic_context)); assert!( - int.merge_cycle_recovery(&db, list_alias(&db, unknown_generic)) + int.merge_cycle_recovery(db, list_alias(db, &env, unknown_generic)) .is_none() ); } @@ -108,15 +131,17 @@ fn generic_alias_cycle_recovery_rejects_unsafe_merges() { #[test] fn todo_types() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); let todo1 = todo_type!("1"); let todo2 = todo_type!("2"); - let int = KnownClass::Int.to_instance(&db); + let int = KnownClass::Int.to_instance(db, &env); - assert!(int.is_assignable_to(&db, todo1)); + assert!(int.is_assignable_to(db, &env, todo1)); - assert!(todo1.is_assignable_to(&db, int)); + assert!(todo1.is_assignable_to(db, &env, int)); // We lose information when combining several `Todo` types. This is an // acknowledged limitation of the current implementation. We cannot @@ -128,12 +153,12 @@ fn todo_types() { // 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, [todo1, todo2]).is_todo()); + assert!(UnionType::from_elements(db, &env, [todo1, todo2]).is_todo()); // And similar for intersection types: - assert!(IntersectionType::from_elements(&db, [todo1, todo2]).is_todo()); + assert!(IntersectionType::from_elements(db, &env, [todo1, todo2]).is_todo()); assert!( - IntersectionBuilder::new(&db) + IntersectionBuilder::new(db, &env) .add_positive(todo1) .add_negative(todo2) .build() @@ -144,57 +169,64 @@ fn todo_types() { #[test] fn a_gradual_member_is_visible_through_a_union_or_an_intersection() { let db = setup_db(); - let int = KnownClass::Int.to_instance(&db); - let str_ = KnownClass::Str.to_instance(&db); + let int = KnownClass::Int.to_instance(&db, &db.program_environment()); + let str_ = KnownClass::Str.to_instance(&db, &db.program_environment()); let unknown = Type::unknown(); // a gradual type is assignable both ways to everything, so a set-theoretic type // holding one answers yes to every question about what it can hold. anything // deciding a representation from an assignability test has to see that first - assert!(UnionType::from_elements(&db, [unknown, int]).has_gradual_member(&db)); assert!( - IntersectionBuilder::new(&db) + UnionType::from_elements(&db, &db.program_environment(), [unknown, int]) + .has_gradual_member(&db, &db.program_environment()) + ); + assert!( + IntersectionBuilder::new(&db, &db.program_environment()) .add_positive(unknown) .add_negative(str_) .build() - .has_gradual_member(&db) + .has_gradual_member(&db, &db.program_environment()) ); // a set-theoretic type of ordinary members hides nothing - assert!(!UnionType::from_elements(&db, [int, str_]).has_gradual_member(&db)); assert!( - !IntersectionBuilder::new(&db) + !UnionType::from_elements(&db, &db.program_environment(), [int, str_]) + .has_gradual_member(&db, &db.program_environment()) + ); + assert!( + !IntersectionBuilder::new(&db, &db.program_environment()) .add_positive(int) .add_negative(str_) .build() - .has_gradual_member(&db) + .has_gradual_member(&db, &db.program_environment()) ); // a union of *narrowed* alternatives is the shape a `x if isinstance(x, C) else y` // over an unannotated parameter produces, and the gradual member is one level down let narrowed = UnionType::from_elements( &db, + &db.program_environment(), [ - IntersectionBuilder::new(&db) + IntersectionBuilder::new(&db, &db.program_environment()) .add_positive(unknown) .add_positive(int) .build(), - IntersectionBuilder::new(&db) + IntersectionBuilder::new(&db, &db.program_environment()) .add_positive(unknown) .add_negative(int) .build(), ], ); - assert!(narrowed.has_gradual_member(&db)); + assert!(narrowed.has_gradual_member(&db, &db.program_environment())); // and the predicate is about a *member*: a type that is itself gradual has none - assert!(!unknown.has_gradual_member(&db)); + assert!(!unknown.has_gradual_member(&db, &db.program_environment())); // a generic argument is not a member — `list[Unknown]` is still every bit a `list` assert!( !KnownClass::List - .to_specialized_instance(&db, &[unknown]) - .has_gradual_member(&db) + .to_specialized_instance(&db, &db.program_environment(), &[unknown]) + .has_gradual_member(&db, &db.program_environment()) ); // a type variable is answered by its upper bound, because that is what every @@ -204,121 +236,161 @@ fn a_gradual_member_is_visible_through_a_union_or_an_intersection() { Type::TypeVar(BoundTypeVarInstance::synthetic_self( &db, bound, - BindingContext::Synthetic, + BindingContext::Synthetic(db.program()), )) }; - assert!(bounded_by(unknown).has_gradual_member(&db)); - assert!(bounded_by(UnionType::from_elements(&db, [unknown, int])).has_gradual_member(&db)); - assert!(!bounded_by(int).has_gradual_member(&db)); + assert!(bounded_by(unknown).has_gradual_member(&db, &db.program_environment())); + assert!( + bounded_by(UnionType::from_elements( + &db, + &db.program_environment(), + [unknown, int] + )) + .has_gradual_member(&db, &db.program_environment()) + ); + assert!(!bounded_by(int).has_gradual_member(&db, &db.program_environment())); } #[test] fn divergent_type() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); let div = Type::divergent(salsa::plumbing::Id::from_bits(1)); assert!(div.is_dynamic()); - assert!(div.has_dynamic(&db)); - let visitor = ApplyTypeMappingVisitor::default(); - let top_div = div.materialize(&db, MaterializationKind::Top, &visitor); - let bottom_div = div.materialize(&db, MaterializationKind::Bottom, &visitor); + assert!(div.has_dynamic(db, &env)); + let visitor = ApplyTypeMappingVisitor::new(&env); + let top_div = div.materialize( + db, + &db.program_environment(), + MaterializationKind::Top, + &visitor, + ); + let bottom_div = div.materialize( + db, + &db.program_environment(), + MaterializationKind::Bottom, + &visitor, + ); assert!(top_div.is_divergent()); assert!(bottom_div.is_divergent()); assert!(!top_div.is_dynamic()); assert!(!bottom_div.is_dynamic()); - assert!(!top_div.has_dynamic(&db)); - assert!(!bottom_div.has_dynamic(&db)); + assert!(!top_div.has_dynamic(db, &env)); + assert!(!bottom_div.has_dynamic(db, &env)); assert!(top_div.is_object()); assert!(!top_div.is_never()); assert!(!bottom_div.is_object()); assert!(bottom_div.is_never()); - assert_eq!(top_div.negate(&db), bottom_div); - assert_eq!(bottom_div.negate(&db), top_div); - assert_eq!(IntersectionBuilder::new(&db).add_negative(div).build(), div); + assert_eq!(top_div.negate(db, &env), bottom_div); + assert_eq!(bottom_div.negate(db, &env), top_div); assert_eq!( - IntersectionBuilder::new(&db).add_negative(top_div).build(), + IntersectionBuilder::new(db, &env).add_negative(div).build(), + div + ); + assert_eq!( + IntersectionBuilder::new(db, &env) + .add_negative(top_div) + .build(), bottom_div ); assert_eq!( - IntersectionBuilder::new(&db) + IntersectionBuilder::new(db, &env) .add_negative(bottom_div) .build(), top_div ); assert!( KnownClass::Int - .to_instance(&db) - .is_assignable_to(&db, top_div) + .to_instance(db, &env) + .is_assignable_to(db, &env, top_div) ); - assert!(!top_div.is_assignable_to(&db, KnownClass::Int.to_instance(&db))); - assert!(bottom_div.is_assignable_to(&db, KnownClass::Int.to_instance(&db))); + assert!(!top_div.is_assignable_to(db, &env, KnownClass::Int.to_instance(db, &env))); + assert!(bottom_div.is_assignable_to(db, &env, KnownClass::Int.to_instance(db, &env))); assert!( !KnownClass::Int - .to_instance(&db) - .is_assignable_to(&db, bottom_div) + .to_instance(db, &env) + .is_assignable_to(db, &env, bottom_div) ); assert_eq!( - top_div.member(&db, "__str__").place.expect_type(), - Type::object().member(&db, "__str__").place.expect_type() + top_div.member(db, &env, "__str__").place.expect_type(), + Type::object() + .member(db, &env, "__str__") + .place + .expect_type() ); assert_eq!( - top_div.member(&db, "__class__").place.expect_type(), - Type::object().dunder_class(&db) + top_div.member(db, &env, "__class__",).place.expect_type(), + Type::object().dunder_class(db, &env) ); - assert!(top_div.try_upcast_to_callable(&db).is_none()); + assert!(top_div.try_upcast_to_callable(db, &env).is_none()); assert!( top_div .subscript( - &db, + db, + &env, Type::int_literal(0), ast::ExprContext::Load, TypeContext::default() ) .is_err() ); - assert_eq!(top_div.recursive_type_normalized_impl(&db, div, true), None); assert_eq!( - bottom_div.recursive_type_normalized_impl(&db, div, true), + top_div.recursive_type_normalized_impl(db, &env, div, true), + None + ); + assert_eq!( + bottom_div.recursive_type_normalized_impl(db, &env, div, true), None ); // The `Divergent` type must not be eliminated in union with other dynamic types, // as this would prevent detection of divergent type inference using `Divergent`. - let union = UnionType::from_elements(&db, [Type::unknown(), div]); - assert_eq!(union.display(&db).to_string(), "Unknown | Divergent"); + let union = UnionType::from_elements(db, &env, [Type::unknown(), div]); + assert_eq!( + union.display(db, &db.program_environment()).to_string(), + "Unknown | Divergent" + ); - let union = UnionType::from_elements(&db, [div, Type::unknown()]); - assert_eq!(union.display(&db).to_string(), "Divergent | Unknown"); + let union = UnionType::from_elements(db, &env, [div, Type::unknown()]); + assert_eq!( + union.display(db, &db.program_environment()).to_string(), + "Divergent | Unknown" + ); - let union = UnionType::from_elements(&db, [div, Type::unknown(), todo_type!("1")]); - assert_eq!(union.display(&db).to_string(), "Divergent | Unknown"); + let union = UnionType::from_elements(db, &env, [div, Type::unknown(), todo_type!("1")]); + assert_eq!( + union.display(db, &db.program_environment()).to_string(), + "Divergent | Unknown" + ); - assert!(div.is_equivalent_to(&db, div)); - assert!(!div.is_equivalent_to(&db, Type::unknown())); - assert!(!Type::unknown().is_equivalent_to(&db, div)); - assert!(!div.is_redundant_with(&db, Type::unknown())); - assert!(!Type::unknown().is_redundant_with(&db, div)); + assert!(div.is_equivalent_to(db, &env, div)); + assert!(!div.is_equivalent_to(db, &env, Type::unknown())); + assert!(!Type::unknown().is_equivalent_to(db, &env, div)); + assert!(!div.is_redundant_with(db, &env, Type::unknown())); + assert!(!Type::unknown().is_redundant_with(db, &env, div)); // `Divergent & T` and `Divergent & ~T` both simplify to `Divergent`, except for the // specific case of `Divergent & Never`, which simplifies to `Never`. - let divergent_intersection = IntersectionBuilder::new(&db) + let divergent_intersection = IntersectionBuilder::new(db, &env) .add_positive(div) .add_positive(todo_type!("2")) .add_negative(todo_type!("3")) .build(); assert_eq!(divergent_intersection, div); - let divergent_intersection = IntersectionBuilder::new(&db) + let divergent_intersection = IntersectionBuilder::new(db, &env) .add_positive(todo_type!("2")) .add_negative(todo_type!("3")) .add_positive(div) .build(); assert_eq!(divergent_intersection, div); - let divergent_never_intersection = IntersectionBuilder::new(&db) + let divergent_never_intersection = IntersectionBuilder::new(db, &env) .add_positive(div) .add_positive(Type::Never) .build(); assert_eq!(divergent_never_intersection, Type::Never); - let divergent_never_intersection = IntersectionBuilder::new(&db) + let divergent_never_intersection = IntersectionBuilder::new(db, &env) .add_positive(Type::Never) .add_positive(div) .build(); @@ -327,66 +399,99 @@ fn divergent_type() { // The `object` type has a good convergence property, that is, its union with all other types is `object`. // (e.g. `object | tuple[Divergent] == object`, `object | tuple[object] == object`) // So we can safely eliminate `Divergent`. - let union = UnionType::from_elements(&db, [div, KnownClass::Object.to_instance(&db)]); - assert_eq!(union.display(&db).to_string(), "object"); + let union = UnionType::from_elements(db, &env, [div, KnownClass::Object.to_instance(db, &env)]); + assert_eq!( + union.display(db, &db.program_environment()).to_string(), + "object" + ); - let union = UnionType::from_elements(&db, [KnownClass::Object.to_instance(&db), div]); - assert_eq!(union.display(&db).to_string(), "object"); + let union = UnionType::from_elements(db, &env, [KnownClass::Object.to_instance(db, &env), div]); + assert_eq!( + union.display(db, &db.program_environment()).to_string(), + "object" + ); let recursive = UnionType::from_elements( - &db, + db, + &env, [ - KnownClass::List.to_specialized_instance(&db, &[div]), - Type::none(&db), + KnownClass::List.to_specialized_instance(db, &env, &[div]), + Type::none(db, &env), ], ); - let nested_rec = KnownClass::List.to_specialized_instance(&db, &[recursive]); + let nested_rec = KnownClass::List.to_specialized_instance(db, &env, &[recursive]); assert_eq!( - nested_rec.display(&db).to_string(), + nested_rec + .display(db, &db.program_environment()) + .to_string(), "list[list[Divergent] | None]" ); let normalized = nested_rec - .recursive_type_normalized_impl(&db, div, false) + .recursive_type_normalized_impl(db, &env, div, false) .unwrap(); - assert_eq!(normalized.display(&db).to_string(), "list[Divergent]"); + assert_eq!( + normalized + .display(db, &db.program_environment()) + .to_string(), + "list[Divergent]" + ); let recursive_tuple = Type::heterogeneous_tuple( - &db, + db, + &env, [ UnionType::from_elements( - &db, + db, + &env, [ - KnownClass::Int.to_instance(&db), + KnownClass::Int.to_instance(db, &env), Type::heterogeneous_tuple( - &db, + db, + &env, [ - UnionType::from_elements(&db, [KnownClass::Int.to_instance(&db), div]), - KnownClass::Str.to_instance(&db), + UnionType::from_elements( + db, + &env, + [KnownClass::Int.to_instance(db, &env), div], + ), + KnownClass::Str.to_instance(db, &env), ], ), ], ), - KnownClass::Str.to_instance(&db), + KnownClass::Str.to_instance(db, &env), ], ); let normalized = recursive_tuple - .recursive_type_normalized_impl(&db, div, false) + .recursive_type_normalized_impl(db, &env, div, false) .unwrap(); - assert_eq!(normalized.display(&db).to_string(), "tuple[Divergent, str]"); + assert_eq!( + normalized + .display(db, &db.program_environment()) + .to_string(), + "tuple[Divergent, str]" + ); let recursive_dict = KnownClass::Dict.to_specialized_instance( - &db, + db, + &env, &[ - KnownClass::Str.to_instance(&db), + KnownClass::Str.to_instance(db, &env), UnionType::from_elements( - &db, + db, + &env, [ - KnownClass::Int.to_instance(&db), + KnownClass::Int.to_instance(db, &env), KnownClass::Dict.to_specialized_instance( - &db, + db, + &env, &[ - KnownClass::Str.to_instance(&db), - UnionType::from_elements(&db, [KnownClass::Int.to_instance(&db), div]), + KnownClass::Str.to_instance(db, &env), + UnionType::from_elements( + db, + &env, + [KnownClass::Int.to_instance(db, &env), div], + ), ], ), ], @@ -394,27 +499,50 @@ fn divergent_type() { ], ); let normalized = recursive_dict - .recursive_type_normalized_impl(&db, div, false) + .recursive_type_normalized_impl(db, &env, div, false) .unwrap(); - assert_eq!(normalized.display(&db).to_string(), "dict[str, Divergent]"); + assert_eq!( + normalized + .display(db, &db.program_environment()) + .to_string(), + "dict[str, Divergent]" + ); - let union = UnionType::from_elements(&db, [div, KnownClass::Int.to_instance(&db)]); - assert_eq!(union.display(&db).to_string(), "Divergent | int"); + let union = UnionType::from_elements(db, &env, [div, KnownClass::Int.to_instance(db, &env)]); + assert_eq!( + union.display(db, &db.program_environment()).to_string(), + "Divergent | int" + ); for (source, target) in [(div, union), (div, Type::unknown()), (Type::unknown(), div)] { - let when = source.when_constraint_set_assignable_to_owned(&db, target); - assert!(when.query(|_builder, when| when.is_always_satisfied(&db))); + let when = source.when_constraint_set_assignable_to_owned(db, &env, target); + assert!(when.query(|_builder, when| when.is_always_satisfied(db, &env))); } let normalized = union - .recursive_type_normalized_impl(&db, div, false) + .recursive_type_normalized_impl(db, &env, div, false) .unwrap(); - assert_eq!(normalized.display(&db).to_string(), "int"); + assert_eq!( + normalized + .display(db, &db.program_environment()) + .to_string(), + "int" + ); // The same can be said about intersections for the `Never` type. - let intersection = IntersectionType::from_elements(&db, [Type::Never, div]); - assert_eq!(intersection.display(&db).to_string(), "Never"); + let intersection = IntersectionType::from_elements(db, &env, [Type::Never, div]); + assert_eq!( + intersection + .display(db, &db.program_environment()) + .to_string(), + "Never" + ); - let intersection = IntersectionType::from_elements(&db, [div, Type::Never]); - assert_eq!(intersection.display(&db).to_string(), "Never"); + let intersection = IntersectionType::from_elements(db, &env, [div, Type::Never]); + assert_eq!( + intersection + .display(db, &db.program_environment()) + .to_string(), + "Never" + ); } #[test] @@ -424,6 +552,7 @@ fn type_alias_variance() { fn get_type_alias<'db>(db: &'db TestDb, name: &str) -> PEP695TypeAliasType<'db> { let module = ruff_db::files::system_path_to_file(db, "/src/a.py").unwrap(); + let module = ProgramFile::new(db, module, db.program_environment().program(db)); let ty = global_symbol(db, module, name).place.expect_type(); let Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::PEP695( type_alias, @@ -433,12 +562,28 @@ fn type_alias_variance() { }; type_alias } + fn get_bound_typevar_instance<'db>( + db: &'db TestDb, + type_alias: PEP695TypeAliasType<'db>, + ) -> BoundTypeVarInstance<'db> { + let generic_context = type_alias.generic_context(db).unwrap(); + generic_context.variables(db).next().unwrap() + } + fn get_bound_typevar<'db>( db: &'db TestDb, type_alias: PEP695TypeAliasType<'db>, ) -> BoundTypeVarIdentity<'db> { - let generic_context = type_alias.generic_context(db).unwrap(); - generic_context.variables(db).next().unwrap().identity(db) + get_bound_typevar_instance(db, type_alias).identity(db) + } + + fn assert_effective_variance<'db>( + db: &'db TestDb, + type_alias: PEP695TypeAliasType<'db>, + expected: TypeVarVariance, + ) { + let typevar = get_bound_typevar_instance(db, type_alias); + assert_eq!(typevar.variance(db), expected); } let mut db = setup_db(); @@ -473,6 +618,7 @@ type ContravariantAliasAlias[T] = ContravariantAlias[T] type InvariantAliasAlias[T] = InvariantAlias[T] type BivariantAliasAlias[T] = BivariantAlias[T] type ParamSpecContravariantAlias[**P] = Callable[P, None] +type ParamSpecDefaultContravariantAlias[**P = [int, str]] = Callable[P, None] type ParamSpecConcatenateAlias[**P] = Callable[Concatenate[int, P], None] type ParamSpecBivariantAlias[**P] = int @@ -481,96 +627,177 @@ type RecursiveAlias2[T] = None | list[T] | list[RecursiveAlias2[T]] "#, ) .unwrap(); - let covariant = get_type_alias(&db, "CovariantAlias"); + let db = &db; + let env = db.program_environment(); + let covariant = get_type_alias(db, "CovariantAlias"); assert_eq!( - KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(covariant)) - .variance_of(&db, get_bound_typevar(&db, covariant)), + KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(covariant)).variance_of( + db, + &env, + get_bound_typevar(db, covariant) + ), TypeVarVariance::Covariant ); - let contravariant = get_type_alias(&db, "ContravariantAlias"); + let contravariant = get_type_alias(db, "ContravariantAlias"); assert_eq!( - KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(contravariant)) - .variance_of(&db, get_bound_typevar(&db, contravariant)), + KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(contravariant)).variance_of( + db, + &env, + get_bound_typevar(db, contravariant) + ), TypeVarVariance::Contravariant ); - let invariant = get_type_alias(&db, "InvariantAlias"); + let invariant = get_type_alias(db, "InvariantAlias"); assert_eq!( - KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(invariant)) - .variance_of(&db, get_bound_typevar(&db, invariant)), + KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(invariant)).variance_of( + db, + &env, + get_bound_typevar(db, invariant) + ), TypeVarVariance::Invariant ); - let bivariant = get_type_alias(&db, "BivariantAlias"); + let bivariant = get_type_alias(db, "BivariantAlias"); assert_eq!( - KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(bivariant)) - .variance_of(&db, get_bound_typevar(&db, bivariant)), + KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(bivariant)).variance_of( + db, + &env, + get_bound_typevar(db, bivariant) + ), TypeVarVariance::Bivariant ); - let covariant_alias = get_type_alias(&db, "CovariantAliasAlias"); + let covariant_alias = get_type_alias(db, "CovariantAliasAlias"); assert_eq!( - KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(covariant_alias)) - .variance_of(&db, get_bound_typevar(&db, covariant_alias)), + KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(covariant_alias)).variance_of( + db, + &env, + get_bound_typevar(db, covariant_alias) + ), TypeVarVariance::Covariant ); - let contravariant_alias = get_type_alias(&db, "ContravariantAliasAlias"); + let contravariant_alias = get_type_alias(db, "ContravariantAliasAlias"); assert_eq!( - KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(contravariant_alias)) - .variance_of(&db, get_bound_typevar(&db, contravariant_alias)), + KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(contravariant_alias)).variance_of( + db, + &env, + get_bound_typevar(db, contravariant_alias) + ), TypeVarVariance::Contravariant ); - let invariant_alias = get_type_alias(&db, "InvariantAliasAlias"); + let invariant_alias = get_type_alias(db, "InvariantAliasAlias"); assert_eq!( - KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(invariant_alias)) - .variance_of(&db, get_bound_typevar(&db, invariant_alias)), + KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(invariant_alias)).variance_of( + db, + &env, + get_bound_typevar(db, invariant_alias) + ), TypeVarVariance::Invariant ); - let bivariant_alias = get_type_alias(&db, "BivariantAliasAlias"); + let bivariant_alias = get_type_alias(db, "BivariantAliasAlias"); assert_eq!( - KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(bivariant_alias)) - .variance_of(&db, get_bound_typevar(&db, bivariant_alias)), + KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(bivariant_alias)).variance_of( + db, + &env, + get_bound_typevar(db, bivariant_alias) + ), TypeVarVariance::Bivariant ); - let paramspec_contravariant = get_type_alias(&db, "ParamSpecContravariantAlias"); + let paramspec_contravariant = get_type_alias(db, "ParamSpecContravariantAlias"); assert_eq!( KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(paramspec_contravariant)) - .variance_of(&db, get_bound_typevar(&db, paramspec_contravariant)), + .variance_of(db, &env, get_bound_typevar(db, paramspec_contravariant)), TypeVarVariance::Contravariant ); - let paramspec_concatenate = get_type_alias(&db, "ParamSpecConcatenateAlias"); + let paramspec_default_contravariant = get_type_alias(db, "ParamSpecDefaultContravariantAlias"); assert_eq!( - KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(paramspec_concatenate)) - .variance_of(&db, get_bound_typevar(&db, paramspec_concatenate)), + KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(paramspec_default_contravariant)) + .variance_of( + db, + &env, + get_bound_typevar(db, paramspec_default_contravariant) + ), + TypeVarVariance::Contravariant + ); + + let paramspec_concatenate = get_type_alias(db, "ParamSpecConcatenateAlias"); + assert_eq!( + KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(paramspec_concatenate)).variance_of( + db, + &env, + get_bound_typevar(db, paramspec_concatenate) + ), TypeVarVariance::Contravariant ); - let paramspec_bivariant = get_type_alias(&db, "ParamSpecBivariantAlias"); + let paramspec_bivariant = get_type_alias(db, "ParamSpecBivariantAlias"); assert_eq!( - KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(paramspec_bivariant)) - .variance_of(&db, get_bound_typevar(&db, paramspec_bivariant)), + KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(paramspec_bivariant)).variance_of( + db, + &env, + get_bound_typevar(db, paramspec_bivariant) + ), TypeVarVariance::Bivariant ); - let recursive = get_type_alias(&db, "RecursiveAlias"); + let recursive = get_type_alias(db, "RecursiveAlias"); assert_eq!( - KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(recursive)) - .variance_of(&db, get_bound_typevar(&db, recursive)), + KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(recursive)).variance_of( + db, + &env, + get_bound_typevar(db, recursive) + ), TypeVarVariance::Bivariant ); - let recursive2 = get_type_alias(&db, "RecursiveAlias2"); + let recursive2 = get_type_alias(db, "RecursiveAlias2"); assert_eq!( - KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(recursive2)) - .variance_of(&db, get_bound_typevar(&db, recursive2)), + KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(recursive2)).variance_of( + db, + &env, + get_bound_typevar(db, recursive2) + ), TypeVarVariance::Invariant ); + + assert_effective_variance(db, covariant, TypeVarVariance::Covariant); + assert_effective_variance(db, contravariant, TypeVarVariance::Contravariant); + assert_effective_variance(db, invariant, TypeVarVariance::Invariant); + assert_effective_variance(db, bivariant, TypeVarVariance::Covariant); + assert_effective_variance(db, covariant_alias, TypeVarVariance::Covariant); + assert_effective_variance(db, contravariant_alias, TypeVarVariance::Contravariant); + assert_effective_variance(db, invariant_alias, TypeVarVariance::Invariant); + assert_effective_variance(db, bivariant_alias, TypeVarVariance::Covariant); + assert_effective_variance(db, paramspec_contravariant, TypeVarVariance::Contravariant); + assert_effective_variance( + db, + paramspec_default_contravariant, + TypeVarVariance::Contravariant, + ); + assert_effective_variance(db, paramspec_concatenate, TypeVarVariance::Contravariant); + assert_effective_variance(db, paramspec_bivariant, TypeVarVariance::Covariant); + assert_effective_variance(db, recursive, TypeVarVariance::Covariant); + assert_effective_variance(db, recursive2, TypeVarVariance::Invariant); + + let bivariant_typevar = get_bound_typevar_instance(db, bivariant); + for polarity in [ + TypeVarVariance::Covariant, + TypeVarVariance::Contravariant, + TypeVarVariance::Invariant, + TypeVarVariance::Bivariant, + ] { + assert_eq!( + bivariant_typevar.variance_with_polarity(db, polarity), + polarity + ); + } } #[test] @@ -580,6 +807,7 @@ fn eager_expansion() { fn get_type_alias<'db>(db: &'db TestDb, name: &str) -> Type<'db> { let module = ruff_db::files::system_path_to_file(db, "/src/a.py").unwrap(); + let module = ProgramFile::new(db, module, db.program_environment().program(db)); let ty = global_symbol(db, module, name).place.expect_type(); let Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::PEP695( type_alias, @@ -610,43 +838,78 @@ type H[T] = G[T] let int_str = get_type_alias(&db, "IntStr"); assert_eq!( - int_str.expand_eagerly(&db).display(&db).to_string(), + int_str + .expand_eagerly(&db, &db.program_environment()) + .display(&db, &db.program_environment()) + .to_string(), "int | str", ); let list_int_str = get_type_alias(&db, "ListIntStr"); assert_eq!( - list_int_str.expand_eagerly(&db).display(&db).to_string(), + list_int_str + .expand_eagerly(&db, &db.program_environment()) + .display(&db, &db.program_environment()) + .to_string(), "list[int | str]", ); let rec_list = get_type_alias(&db, "RecursiveList"); assert_eq!( - rec_list.expand_eagerly(&db).display(&db).to_string(), + rec_list + .expand_eagerly(&db, &db.program_environment()) + .display(&db, &db.program_environment()) + .to_string(), "list[Divergent]", ); let rec_int_list = get_type_alias(&db, "RecursiveIntList"); assert_eq!( - rec_int_list.expand_eagerly(&db).display(&db).to_string(), + rec_int_list + .expand_eagerly(&db, &db.program_environment()) + .display(&db, &db.program_environment()) + .to_string(), "list[Divergent]", ); let itself = get_type_alias(&db, "Itself"); assert_eq!( - itself.expand_eagerly(&db).display(&db).to_string(), + itself + .expand_eagerly(&db, &db.program_environment()) + .display(&db, &db.program_environment()) + .to_string(), "Divergent", ); let a = get_type_alias(&db, "A"); - assert_eq!(a.expand_eagerly(&db).display(&db).to_string(), "Divergent",); + assert_eq!( + a.expand_eagerly(&db, &db.program_environment()) + .display(&db, &db.program_environment()) + .to_string(), + "Divergent", + ); let b = get_type_alias(&db, "B"); - assert_eq!(b.expand_eagerly(&db).display(&db).to_string(), "Divergent",); + assert_eq!( + b.expand_eagerly(&db, &db.program_environment()) + .display(&db, &db.program_environment()) + .to_string(), + "Divergent", + ); let g = get_type_alias(&db, "G"); - assert_eq!(g.expand_eagerly(&db).display(&db).to_string(), "Divergent",); + assert_eq!( + g.expand_eagerly(&db, &db.program_environment()) + .display(&db, &db.program_environment()) + .to_string(), + "Divergent", + ); let h = get_type_alias(&db, "H"); - assert_eq!(h.expand_eagerly(&db).display(&db).to_string(), "Divergent",); + assert_eq!( + h.expand_eagerly(&db, &db.program_environment()) + .display(&db, &db.program_environment()) + .to_string(), + "Divergent", + ); } diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index cc3cd36bd3..3a9714f301 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -16,6 +16,7 @@ //! that adds that "collapse `Never`" behavior, whereas [`TupleSpec`] allows you to add any element //! types, including `Never`.) +use crate::{Program, ProgramEnvironment}; use std::cmp::Ordering; use std::hash::Hash; use std::num::{NonZeroI32, NonZeroUsize}; @@ -34,7 +35,7 @@ use crate::types::{ ApplyTypeMappingVisitor, BoundTypeVarInstance, ErrorContext, FindLegacyTypeVarsVisitor, IntersectionType, Type, TypeContext, TypeMapping, UnionBuilder, UnionType, }; -use crate::{Db, FxOrderSet, Program}; +use crate::{Db, FxOrderSet}; use ty_python_core::Truthiness; use ty_python_core::definition::Definition; @@ -57,11 +58,8 @@ 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), - } + fn size_hint(self) -> (usize, Option) { + (self.minimum(), self.maximum()) } /// Returns the minimum length of this tuple. @@ -132,6 +130,9 @@ impl TupleLength { #[salsa::interned(debug, constructor=new_internal, heap_size=ruff_memory_usage::heap_size)] pub struct TupleType<'db> { + #[returns(copy)] + pub(crate) program: Program<'db>, + #[returns(ref)] pub(crate) tuple: TupleSpec<'db>, } @@ -152,7 +153,9 @@ pub(super) fn walk_tuple_type<'db, V: super::visitor::TypeVisitor<'db> + ?Sized> visitor.visit_type(db, element); } match tuple.variable() { - VariableSegment::Homogeneous(element) => visitor.visit_type(db, element), + VariableSegment::Homogeneous(element) => { + visitor.visit_type(db, element); + } VariableSegment::TypeVarTuple(typevartuple) => { visitor.visit_type(db, Type::TypeVar(typevartuple)); } @@ -169,7 +172,11 @@ impl get_size2::GetSize for TupleType<'_> {} #[salsa::tracked] impl<'db> TupleType<'db> { - pub(crate) fn new(db: &'db dyn Db, spec: &TupleSpec<'db>) -> Option { + pub(crate) fn new( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + spec: &TupleSpec<'db>, + ) -> Option { // 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. if spec.fixed_elements().any(Type::is_never) { @@ -186,56 +193,79 @@ impl<'db> TupleType<'db> { .iter_prefix_elements() .chain(tuple.iter_suffix_elements()), )); - return Some(TupleType::new_internal::<_, TupleSpec<'db>>(db, tuple)); + return Some(TupleType::new_internal(db, env.program(db), tuple)); } - Some(TupleType::new_internal(db, spec)) + Some(TupleType::new_internal(db, env.program(db), spec)) } - pub(crate) fn empty(db: &'db dyn Db) -> Self { - TupleType::new_internal(db, TupleSpec::from(FixedLengthTuple::empty())) + pub(crate) fn empty(db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Self { + TupleType::new_internal( + db, + env.program(db), + TupleSpec::from(FixedLengthTuple::empty()), + ) } pub(crate) fn heterogeneous( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, types: impl IntoIterator>, ) -> Option { - TupleType::new(db, &TupleSpec::heterogeneous(types)) + TupleType::new(db, env, &TupleSpec::heterogeneous(types)) } pub(crate) fn mixed( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, prefix: impl IntoIterator>, variable: Type<'db>, suffix: impl IntoIterator>, ) -> Option { - Self::mixed_with_segment(db, prefix, VariableSegment::Homogeneous(variable), suffix) + Self::mixed_with_segment( + db, + env, + prefix, + VariableSegment::Homogeneous(variable), + suffix, + ) } pub(crate) fn mixed_with_segment( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, prefix: impl IntoIterator>, variable: VariableSegment<'db>, suffix: impl IntoIterator>, ) -> Option { - TupleType::new(db, &VariableLengthTuple::mixed(prefix, variable, suffix)) + TupleType::new( + db, + env, + &VariableLengthTuple::mixed(prefix, variable, suffix), + ) } - pub(crate) fn homogeneous(db: &'db dyn Db, element: Type<'db>) -> Self { + pub(crate) fn homogeneous( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + element: Type<'db>, + ) -> Self { match element { - Type::Never => TupleType::empty(db), - _ => TupleType::new_internal(db, TupleSpec::homogeneous(element)), + Type::Never => TupleType::empty(db, env), + _ => TupleType::new_internal(db, env.program(db), TupleSpec::homogeneous(element)), } } /// Packs a `TypeVarTuple` into the tuple value used for generic specialization relations. pub(crate) fn unpacked_typevartuple( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typevar: BoundTypeVarInstance<'db>, ) -> Self { debug_assert!(typevar.is_typevartuple(db)); TupleType::new_internal( db, + env.program(db), VariableLengthTuple::mixed([], VariableSegment::TypeVarTuple(typevar), []), ) } @@ -245,13 +275,14 @@ impl<'db> TupleType<'db> { // from `NominalInstanceType::class()`, which is a very hot method. #[salsa::tracked(returns(copy), cycle_initial=to_class_type_cycle_initial, heap_size=ruff_memory_usage::heap_size)] pub(crate) fn to_class_type(self, db: &'db dyn Db) -> ClassType<'db> { + let env = &ProgramEnvironment::from_program(self.program(db)); let tuple_class = KnownClass::Tuple - .try_to_class_literal(db) + .try_to_class_literal(db, env) .expect("Typeshed should always have a `tuple` class in `builtins.pyi`"); tuple_class.apply_specialization(db, |generic_context| { if generic_context.variables(db).len() == 1 { - let element_type = self.tuple(db).tuple_class_type(db); + let element_type = self.tuple(db).tuple_class_type(db, env); generic_context.specialize_tuple(db, element_type, self) } else { generic_context.default_specialization(db, Some(KnownClass::Tuple)) @@ -262,44 +293,45 @@ impl<'db> TupleType<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { Some(Self::new_internal( db, + env.program(db), self.tuple(db) - .recursive_type_normalized_impl(db, div, nested)?, + .recursive_type_normalized_impl(db, env, div, nested)?, )) } pub(crate) fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Option { TupleType::new( db, + visitor.env, &self .tuple(db) - .apply_type_mapping_impl(db, type_mapping, tcx, visitor), + .apply_type_mapping_impl(db, env, type_mapping, tcx, visitor), ) } pub(crate) fn find_legacy_typevars_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, ) { self.tuple(db) - .find_legacy_typevars_impl(db, binding_context, typevars, visitor); - } - - pub(crate) fn is_single_valued(self, db: &'db dyn Db) -> bool { - self.tuple(db).is_single_valued(db) + .find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } } @@ -356,7 +388,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { |(&source, &target)| { let constraint_set = self.check_type_pair(db, source, target); if let Some(context) = self.report_context() - && constraint_set.is_never_satisfied(db) + && constraint_set.is_never_satisfied(db, self.env) { context.push(ErrorContext::TupleElementNotCompatible { source, @@ -387,7 +419,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { let element_constraints = self.check_type_pair(db, source_ty, target_ty); if result .intersect(db, self.constraints, element_constraints) - .is_never_satisfied(db) + .is_trivially_never_satisfied() { return result; } @@ -399,7 +431,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { let element_constraints = self.check_type_pair(db, source_ty, target_ty); if result .intersect(db, self.constraints, element_constraints) - .is_never_satisfied(db) + .is_trivially_never_satisfied() { return result; } @@ -407,7 +439,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { match target.variable() { VariableSegment::TypeVarTuple(typevartuple) => { - let packed = Type::heterogeneous_tuple(db, source_iter.copied()); + let packed = Type::heterogeneous_tuple(db, self.env, source_iter.copied()); result.and(db, self.constraints, || { self.check_type_pair(db, packed, Type::TypeVar(typevartuple)) }) @@ -453,24 +485,28 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { return self.never(); } + let env = self.env; + // 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 result = self.always(); let mut target_iter = target.iter_all_elements(); - for source_ty in source.prenormalized_prefix_elements(db, None) { + for source_ty in source.prenormalized_prefix_elements(db, env, None) { let Some(target_ty) = target_iter.next() else { return self.never(); }; let element_constraints = self.check_type_pair(db, source_ty, target_ty); if result .intersect(db, self.constraints, element_constraints) - .is_never_satisfied(db) + .is_trivially_never_satisfied() { return result; } } - let suffix: Vec<_> = source.prenormalized_suffix_elements(db, None).collect(); + let suffix: Vec<_> = source + .prenormalized_suffix_elements(db, env, None) + .collect(); for &source_ty in suffix.iter().rev() { let Some(target_ty) = target_iter.next_back() else { return self.never(); @@ -478,7 +514,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { let element_constraints = self.check_type_pair(db, source_ty, target_ty); if result .intersect(db, self.constraints, element_constraints) - .is_never_satisfied(db) + .is_trivially_never_satisfied() { return result; } @@ -515,6 +551,8 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { }); } + let env = self.env; + if self.typevar_evaluation == TypeVarEvaluation::Lazy && let VariableSegment::TypeVarTuple(typevartuple) = target.variable() { @@ -543,6 +581,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { let packed = Type::tuple(TupleType::new( db, + env, &VariableLengthTuple::mixed( source_prefix[target_prefix.len()..].iter().copied(), source.variable(), @@ -576,10 +615,12 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // variable-length part. let mut result = self.always(); let pairwise = source - .prenormalized_prefix_elements(db, source_prenormalize_variable) - .zip_longest( - target.prenormalized_prefix_elements(db, target_prenormalize_variable), - ); + .prenormalized_prefix_elements(db, env, source_prenormalize_variable) + .zip_longest(target.prenormalized_prefix_elements( + db, + env, + target_prenormalize_variable, + )); for pair in pairwise { let pair_constraints = match pair { EitherOrBoth::Both(self_ty, other_ty) => { @@ -601,17 +642,17 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { }; if result .intersect(db, self.constraints, pair_constraints) - .is_never_satisfied(db) + .is_trivially_never_satisfied() { return result; } } let source_suffix: Vec<_> = source - .prenormalized_suffix_elements(db, source_prenormalize_variable) + .prenormalized_suffix_elements(db, env, source_prenormalize_variable) .collect(); let target_suffix: Vec<_> = target - .prenormalized_suffix_elements(db, target_prenormalize_variable) + .prenormalized_suffix_elements(db, env, target_prenormalize_variable) .collect(); let pairwise = source_suffix .iter() @@ -638,7 +679,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { }; if result .intersect(db, self.constraints, pair_constraints) - .is_never_satisfied(db) + .is_trivially_never_satisfied() { return result; } @@ -727,8 +768,9 @@ fn to_class_type_cycle_initial<'db>( id: salsa::Id, self_: TupleType<'db>, ) -> ClassType<'db> { + let env = &ProgramEnvironment::from_program(self_.program(db)); let tuple_class = KnownClass::Tuple - .try_to_class_literal(db) + .try_to_class_literal(db, env) .expect("Typeshed should always have a `tuple` class in `builtins.pyi`"); tuple_class.apply_specialization(db, |generic_context| { @@ -839,6 +881,7 @@ impl<'db> FixedLengthTuple> { fn resize( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, new_length: TupleLength, ) -> Result, ResizeTupleError> { match new_length { @@ -858,8 +901,11 @@ impl<'db> FixedLengthTuple> { // suffix. let mut elements = self.iter_all_elements(); let prefix: Vec<_> = elements.by_ref().take(prefix).collect(); - let variable = - UnionType::from_elements_leave_aliases(db, elements.by_ref().take(variable)); + let variable = UnionType::from_elements_leave_aliases( + db, + env, + elements.by_ref().take(variable), + ); let suffix = elements.by_ref().take(suffix); Ok(VariableLengthTuple::mixed( prefix, @@ -873,6 +919,7 @@ impl<'db> FixedLengthTuple> { fn recursive_type_normalized_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -880,7 +927,7 @@ impl<'db> FixedLengthTuple> { Some(Self::from_elements( self.0 .iter() - .map(|ty| ty.recursive_type_normalized_impl(db, div, true)) + .map(|ty| ty.recursive_type_normalized_impl(db, env, div, true)) .collect::>>()?, )) } else { @@ -888,7 +935,7 @@ impl<'db> FixedLengthTuple> { self.0 .iter() .map(|ty| { - ty.recursive_type_normalized_impl(db, div, true) + ty.recursive_type_normalized_impl(db, env, div, true) .unwrap_or(div) }) .collect::>(), @@ -899,18 +946,19 @@ impl<'db> FixedLengthTuple> { fn apply_type_mapping_impl<'a>( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { let tcx_tuple = tcx .annotation() - .and_then(|annotation| annotation.known_specialization(db, KnownClass::Tuple)) + .and_then(|annotation| annotation.known_specialization(db, env, KnownClass::Tuple)) .and_then(|specialization| { specialization .tuple(db) .expect("the specialization of `KnownClass::Tuple` must have a tuple spec") - .resize(db, TupleLength::Fixed(self.0.len())) + .resize(db, visitor.env, TupleLength::Fixed(self.0.len())) .ok() }); @@ -927,32 +975,34 @@ impl<'db> FixedLengthTuple> { self.0 .iter() .zip(tcx_elements) - .map(|(ty, tcx)| ty.apply_type_mapping_impl(db, type_mapping, tcx, visitor)), + .map(|(ty, tcx)| ty.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor)), ) } fn find_legacy_typevars_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, ) { for ty in &self.0 { - ty.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + ty.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } } - - fn is_single_valued(&self, db: &'db dyn Db) -> bool { - self.0.iter().all(|ty| ty.is_single_valued(db)) - } } impl<'db> PyIndex<'db> for &FixedLengthTuple> { type Item = Type<'db>; - fn py_index(self, db: &'db dyn Db, index: i32) -> Result { - self.0.py_index(db, index).copied() + fn py_index( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + index: i32, + ) -> Result { + self.0.py_index(db, env, index).copied() } } @@ -1077,7 +1127,7 @@ impl VariableLengthTuple { self.variable_segment } - pub(crate) fn variable_element_mut(&mut self) -> &mut V { + fn variable_element_mut(&mut self) -> &mut V { &mut self.variable_segment } @@ -1092,7 +1142,7 @@ impl VariableLengthTuple { self.prefix_elements().iter().copied() } - pub(crate) fn prefix_elements_mut(&mut self) -> &mut [T] { + fn prefix_elements_mut(&mut self) -> &mut [T] { &mut self.fixed_elements[..self.prefix_len] } @@ -1107,7 +1157,7 @@ impl VariableLengthTuple { self.suffix_elements().iter().copied() } - pub(crate) fn suffix_elements_mut(&mut self) -> &mut [T] { + fn suffix_elements_mut(&mut self) -> &mut [T] { &mut self.fixed_elements[self.prefix_len..] } @@ -1311,10 +1361,12 @@ impl VariableSlice { fn ty<'db>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, tuple: &VariableLengthTuple, VariableSegment<'db>>, ) -> Type<'db> { UnionType::from_elements_leave_aliases( db, + env, matches!( self.kind, VariableSliceKind::ElementType | VariableSliceKind::Preserved @@ -1335,15 +1387,16 @@ impl VariableTupleSlicePlan { fn into_type<'db>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, tuple: &VariableLengthTuple, VariableSegment<'db>>, ) -> Type<'db> { match self { VariableTupleSlicePlan::Empty => { - Type::heterogeneous_tuple(db, std::iter::empty::>()) + Type::heterogeneous_tuple(db, env, std::iter::empty::>()) } VariableTupleSlicePlan::Fixed(fixed) => { - Type::heterogeneous_tuple(db, tuple.slice_fixed_position(db, fixed)) + Type::heterogeneous_tuple(db, env, tuple.slice_fixed_position(db, env, fixed)) } VariableTupleSlicePlan::Mixed { @@ -1354,11 +1407,12 @@ impl VariableTupleSlicePlan { let variable_segment = match variable.kind { VariableSliceKind::Preserved => tuple.variable(), VariableSliceKind::Excluded | VariableSliceKind::ElementType => { - VariableSegment::Homogeneous(variable.ty(db, tuple)) + VariableSegment::Homogeneous(variable.ty(db, env, tuple)) } }; Type::tuple(TupleType::new( db, + env, &VariableLengthTuple::mixed( VariableLengthTuple::optional_fixed_slice( tuple.prefix_elements(), @@ -1373,7 +1427,7 @@ impl VariableTupleSlicePlan { )) } - VariableTupleSlicePlan::Homogeneous => tuple.homogeneous_type(db), + VariableTupleSlicePlan::Homogeneous => tuple.homogeneous_type(db, env), } } } @@ -1452,6 +1506,7 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { fn slice_fixed_position<'a>( &'a self, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, slice: FixedPositionSlice, ) -> impl Iterator> + 'a where @@ -1465,10 +1520,10 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { } = slice; match origin { FixedPositionOrigin::Front => { - Either::Left(self.slice_front_forward(db, start, exclusive_stop, step)) + Either::Left(self.slice_front_forward(db, env, start, exclusive_stop, step)) } FixedPositionOrigin::Back => { - Either::Right(self.slice_back(db, start, exclusive_stop, step)) + Either::Right(self.slice_back(db, env, start, exclusive_stop, step)) } } } @@ -1476,6 +1531,7 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { fn slice_front_forward<'a>( &'a self, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, start: usize, exclusive_stop: usize, step: NonZeroUsize, @@ -1486,17 +1542,20 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { (start..exclusive_stop) .step_by(step.get()) .map(move |index| { - self.type_at_nonnegative_index(db, index).unwrap_or_else(|| { - unreachable!( - "front-origin fixed slice positions are validated during plan construction" - ) - }) + self.type_at_nonnegative_index(db, env, index) + .unwrap_or_else(|| { + unreachable!( + "front-origin fixed slice positions are validated \ + during plan construction" + ) + }) }) } fn slice_back<'a>( &'a self, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, start: usize, exclusive_stop: usize, step: NonZeroUsize, @@ -1513,7 +1572,7 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { } let element = self - .type_at_negative_distance(db, distance) + .type_at_negative_distance(db, env, distance) .unwrap_or_else(|| { unreachable!( "back-origin fixed slice positions are validated during plan construction" @@ -1549,6 +1608,7 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { fn py_slice_type( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, start: Option, stop: Option, step: Option, @@ -1564,7 +1624,7 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { Ok(match direction { TupleSliceDirection::Forward => self .forward_slice_plan(start, stop, step) - .into_type(db, self), + .into_type(db, env, self), TupleSliceDirection::Backward => { let reversed = self.reversed(db); reversed @@ -1573,7 +1633,7 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { TupleSliceDirection::reverse_bound(stop), step, ) - .into_type(db, &reversed) + .into_type(db, env, &reversed) } }) } @@ -1881,20 +1941,36 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { start + ((stop - start - 1) / step) * step } - fn type_at_nonnegative_index(&self, db: &'db dyn Db, index: usize) -> Option> { - (index < self.len().minimum()).then(|| self.type_at_nonnegative_index_unbounded(db, index)) + fn type_at_nonnegative_index( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + index: usize, + ) -> Option> { + (index < self.len().minimum()) + .then(|| self.type_at_nonnegative_index_unbounded(db, env, index)) } - fn type_at_nonnegative_index_unbounded(&self, db: &'db dyn Db, index: usize) -> Type<'db> { + fn type_at_nonnegative_index_unbounded( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + index: usize, + ) -> Type<'db> { if let Some(element) = self.prefix_elements().get(index) { *element } else { let suffix_stop = index - self.prefix_len() + 1; - self.variable_and_suffix_type(db, Some(suffix_stop)) + self.variable_and_suffix_type(db, env, Some(suffix_stop)) } } - fn type_at_negative_distance(&self, db: &'db dyn Db, distance: usize) -> Option> { + fn type_at_negative_distance( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + distance: usize, + ) -> Option> { if distance == 0 || distance > self.len().minimum() { return None; } @@ -1909,6 +1985,7 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { let prefix_and_variable_len = distance - self.suffix_len(); Some(UnionType::from_elements_leave_aliases( db, + env, self.iter_prefix_elements() .skip(self.prefix_len() - prefix_and_variable_len) .chain(std::iter::once(self.variable().element_type(db))), @@ -1924,14 +2001,20 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { .chain(self.iter_suffix_elements()) } - fn homogeneous_type(&self, db: &'db dyn Db) -> Type<'db> { - let element = UnionType::from_elements_leave_aliases(db, self.iter_all_elements(db)); - Type::homogeneous_tuple(db, element) + fn homogeneous_type(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + let element = UnionType::from_elements_leave_aliases(db, env, self.iter_all_elements(db)); + Type::homogeneous_tuple(db, env, element) } - fn variable_and_suffix_type(&self, db: &'db dyn Db, suffix_stop: Option) -> Type<'db> { + fn variable_and_suffix_type( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + suffix_stop: Option, + ) -> Type<'db> { UnionType::from_elements_leave_aliases( db, + env, std::iter::once(self.variable().element_type(db)).chain( self.iter_suffix_elements() .take(suffix_stop.unwrap_or_else(|| self.suffix_len())), @@ -1961,12 +2044,13 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { fn prenormalized_prefix_elements<'a>( &'a self, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, variable: Option>, ) -> impl Iterator> + 'a { let variable = variable.unwrap_or_else(|| self.variable().element_type(db)); self.iter_prefix_elements().chain( self.iter_suffix_elements() - .take_while(move |element| element.is_equivalent_to(db, variable)), + .take_while(move |element| element.is_equivalent_to(db, env, variable)), ) } @@ -1992,16 +2076,18 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { fn prenormalized_suffix_elements<'a>( &'a self, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, variable: Option>, ) -> impl Iterator> + 'a { let variable = variable.unwrap_or_else(|| self.variable().element_type(db)); self.iter_suffix_elements() - .skip_while(move |element| element.is_equivalent_to(db, variable)) + .skip_while(move |element| element.is_equivalent_to(db, env, variable)) } fn resize( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, new_length: TupleLength, ) -> Result, ResizeTupleError> { match new_length { @@ -2037,6 +2123,7 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { // `I2` (variable empty, suffix shifts left), so it should be `I1 | I2`. let variable = UnionType::from_elements_leave_aliases( db, + env, self.iter_prefix_elements() .skip(prefix_length) .chain(std::iter::once(self.variable().element_type(db))) @@ -2058,6 +2145,7 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { fn recursive_type_normalized_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -2065,11 +2153,11 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { let prefix = self .prefix_elements() .iter() - .map(|ty| ty.recursive_type_normalized_impl(db, div, true)); + .map(|ty| ty.recursive_type_normalized_impl(db, env, div, true)); let variable_segment = match self.variable() { VariableSegment::Homogeneous(variable) => VariableSegment::Homogeneous( - variable.recursive_type_normalized_impl(db, div, true)?, + variable.recursive_type_normalized_impl(db, env, div, true)?, ), VariableSegment::TypeVarTuple(typevartuple) => { VariableSegment::TypeVarTuple(typevartuple) @@ -2079,19 +2167,19 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { let suffix = self .suffix_elements() .iter() - .map(|ty| ty.recursive_type_normalized_impl(db, div, true)); + .map(|ty| ty.recursive_type_normalized_impl(db, env, div, true)); Self::try_new(prefix, variable_segment, suffix) } else { let prefix = self.prefix_elements().iter().map(|ty| { - ty.recursive_type_normalized_impl(db, div, true) + ty.recursive_type_normalized_impl(db, env, div, true) .unwrap_or(div) }); let variable_segment = match self.variable() { VariableSegment::Homogeneous(variable) => VariableSegment::Homogeneous( variable - .recursive_type_normalized_impl(db, div, true) + .recursive_type_normalized_impl(db, env, div, true) .unwrap_or(div), ), VariableSegment::TypeVarTuple(typevartuple) => { @@ -2100,7 +2188,7 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { }; let suffix = self.suffix_elements().iter().map(|ty| { - ty.recursive_type_normalized_impl(db, div, true) + ty.recursive_type_normalized_impl(db, env, div, true) .unwrap_or(div) }); @@ -2111,24 +2199,26 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { fn apply_type_mapping_impl<'a>( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> TupleSpec<'db> { let prefix = self .prefix_elements() .iter() - .map(|ty| ty.apply_type_mapping_impl(db, type_mapping, tcx, visitor)); + .map(|ty| ty.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor)); let suffix = self .suffix_elements() .iter() - .map(|ty| ty.apply_type_mapping_impl(db, type_mapping, tcx, visitor)); + .map(|ty| ty.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor)); match self.variable() { VariableSegment::Homogeneous(variable) => Self::mixed( prefix, VariableSegment::Homogeneous(variable.apply_type_mapping_impl( db, + env, type_mapping, tcx, visitor, @@ -2138,6 +2228,7 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { VariableSegment::TypeVarTuple(typevartuple) => { let mapped = Type::TypeVar(typevartuple).apply_type_mapping_impl( db, + env, type_mapping, tcx, visitor, @@ -2163,7 +2254,7 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { for element in prefix { builder.push(element); } - builder = builder.concat(db, &mapped_tuple); + builder = builder.concat(db, visitor.env, &mapped_tuple); for element in suffix { builder.push(element); } @@ -2178,20 +2269,22 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { fn find_legacy_typevars_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, ) { for ty in self.prefix_elements() { - ty.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + ty.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } match self.variable() { VariableSegment::Homogeneous(variable) => { - variable.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + variable.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } VariableSegment::TypeVarTuple(typevartuple) => { Type::TypeVar(typevartuple).find_legacy_typevars_impl( db, + env, binding_context, typevars, visitor, @@ -2199,7 +2292,7 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { } } for ty in self.suffix_elements() { - ty.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + ty.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } } } @@ -2207,9 +2300,14 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { impl<'db> PyIndex<'db> for &VariableLengthTuple, VariableSegment<'db>> { type Item = Type<'db>; - fn py_index(self, db: &'db dyn Db, index: i32) -> Result { + fn py_index( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + index: i32, + ) -> Result { match Nth::from_index(index) { - Nth::FromStart(index) => Ok(self.type_at_nonnegative_index_unbounded(db, index)), + Nth::FromStart(index) => Ok(self.type_at_nonnegative_index_unbounded(db, env, index)), Nth::FromEnd(index_from_end) => { if index_from_end < self.suffix_elements().len() { @@ -2225,6 +2323,7 @@ impl<'db> PyIndex<'db> for &VariableLengthTuple, VariableSegment<'db>> let index_past_suffix = index_from_end - self.suffix_elements().len() + 1; Ok(UnionType::from_elements_leave_aliases( db, + env, (self.prefix_elements().iter().rev().copied()) .take(index_past_suffix) .rev() @@ -2270,7 +2369,7 @@ impl Tuple { } } - pub(crate) fn into_all_elements_with_kind(self) -> impl Iterator> { + fn into_all_elements_with_kind(self) -> impl Iterator> { match self { Tuple::Fixed(tuple) => { Either::Left(tuple.owned_elements().into_iter().map(TupleElement::Fixed)) @@ -2306,24 +2405,29 @@ impl<'db> Tuple, VariableSegment<'db>> { )) } - pub(crate) fn homogeneous_element_type(&self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn homogeneous_element_type( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { match self { Tuple::Fixed(tuple) => { - UnionType::from_elements_leave_aliases(db, tuple.iter_all_elements()) + UnionType::from_elements_leave_aliases(db, env, tuple.iter_all_elements()) } Tuple::Variable(tuple) => { - UnionType::from_elements_leave_aliases(db, tuple.iter_all_elements(db)) + UnionType::from_elements_leave_aliases(db, env, tuple.iter_all_elements(db)) } } } - fn tuple_class_type(&self, db: &'db dyn Db) -> Type<'db> { + fn tuple_class_type(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match self { Tuple::Fixed(tuple) => { - UnionType::from_elements_leave_aliases(db, tuple.iter_all_elements()) + UnionType::from_elements_leave_aliases(db, env, tuple.iter_all_elements()) } Tuple::Variable(tuple) => UnionType::from_elements_leave_aliases( db, + env, tuple .iter_prefix_elements() .chain(std::iter::once(tuple.variable().tuple_class_type())) @@ -2356,6 +2460,7 @@ impl<'db> Tuple, VariableSegment<'db>> { pub(crate) fn py_slice_type( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, start: Option, stop: Option, step: Option, @@ -2363,9 +2468,10 @@ impl<'db> Tuple, VariableSegment<'db>> { match self { Tuple::Fixed(tuple) => Ok(Type::heterogeneous_tuple( db, + env, tuple.py_slice(db, start, stop, step)?, )), - Tuple::Variable(tuple) => tuple.py_slice_type(db, start, stop, step), + Tuple::Variable(tuple) => tuple.py_slice_type(db, env, start, stop, step), } } @@ -2375,69 +2481,68 @@ impl<'db> Tuple, VariableSegment<'db>> { pub(crate) fn resize( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, new_length: TupleLength, ) -> Result { match self { - Tuple::Fixed(tuple) => tuple.resize(db, new_length), - Tuple::Variable(tuple) => tuple.resize(db, new_length), + Tuple::Fixed(tuple) => tuple.resize(db, env, new_length), + Tuple::Variable(tuple) => tuple.resize(db, env, new_length), } } - pub(super) fn recursive_type_normalized_impl( + fn recursive_type_normalized_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { match self { Tuple::Fixed(tuple) => Some(Tuple::Fixed( - tuple.recursive_type_normalized_impl(db, div, nested)?, + tuple.recursive_type_normalized_impl(db, env, div, nested)?, )), Tuple::Variable(tuple) => Some(Tuple::Variable( - tuple.recursive_type_normalized_impl(db, div, nested)?, + tuple.recursive_type_normalized_impl(db, env, div, nested)?, )), } } - pub(crate) fn apply_type_mapping_impl<'a>( + fn apply_type_mapping_impl<'a>( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { match self { Tuple::Fixed(tuple) => { - Tuple::Fixed(tuple.apply_type_mapping_impl(db, type_mapping, tcx, visitor)) + Tuple::Fixed(tuple.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor)) + } + Tuple::Variable(tuple) => { + tuple.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor) } - Tuple::Variable(tuple) => tuple.apply_type_mapping_impl(db, type_mapping, tcx, visitor), } } fn find_legacy_typevars_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, ) { match self { Tuple::Fixed(tuple) => { - tuple.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + tuple.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } Tuple::Variable(tuple) => { - tuple.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + tuple.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } } } - pub(crate) fn is_single_valued(&self, db: &'db dyn Db) -> bool { - match self { - Tuple::Fixed(tuple) => tuple.is_single_valued(db), - Tuple::Variable(_) => false, - } - } - /// Calls a closure for each pair of elements that could potentially be compared at runtime /// between `self` and `other`. /// @@ -2589,9 +2694,12 @@ impl<'db> Tuple, VariableSegment<'db>> { } /// Return the `TupleSpec` for the singleton `sys.version_info` - pub(crate) fn version_info_spec(db: &'db dyn Db) -> TupleSpec<'db> { - let python_version = Program::get(db).python_version(db); - let int_instance_ty = KnownClass::Int.to_instance(db); + pub(crate) fn version_info_spec( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> TupleSpec<'db> { + let python_version = env.python_version(db); + let int_instance_ty = KnownClass::Int.to_instance(db, env); // TODO: just grab this type from typeshed (it's a `sys._ReleaseLevel` type alias there) let release_level_ty = { @@ -2632,15 +2740,20 @@ impl From> for Tuple { impl<'db> PyIndex<'db> for &TupleSpec<'db> { type Item = Type<'db>; - fn py_index(self, db: &'db dyn Db, index: i32) -> Result { + fn py_index( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + index: i32, + ) -> Result { match self { - Tuple::Fixed(tuple) => tuple.py_index(db, index), - Tuple::Variable(tuple) => tuple.py_index(db, index), + Tuple::Fixed(tuple) => tuple.py_index(db, env, index), + Tuple::Variable(tuple) => tuple.py_index(db, env, index), } } } -pub(crate) enum TupleElement { +enum TupleElement { Fixed(T), Prefix(T), Variable(V), @@ -2656,23 +2769,29 @@ pub(crate) enum TupleElement { /// assigned to the starred target in `list`. pub(crate) struct TupleUnpacker<'db> { db: &'db dyn Db, + env: ProgramEnvironment<'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); + pub(crate) fn new(db: &'db dyn Db, env: &ProgramEnvironment<'db>, len: TupleLength) -> Self { + let new_builders = + |len: usize| std::iter::repeat_with(|| UnionBuilder::new(db, env)).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), + UnionBuilder::new(db, env), new_builders(suffix), ), }; - Self { db, targets } + Self { + db, + env: env.clone(), + targets, + } } /// Unpacks a single rhs tuple into the target tuple that we are building. If you want to @@ -2683,13 +2802,14 @@ impl<'db> TupleUnpacker<'db> { /// 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: &TupleSpec<'db>) -> Result<(), ResizeTupleError> { - let values = values.resize(self.db, self.targets.len())?; + let db = self.db; + let values = values.resize(db, &self.env, 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); + targets.unpack_tuple(db, &self.env, values); } _ => panic!("should have ensured that tuples are the same length"), } @@ -2701,11 +2821,12 @@ impl<'db> TupleUnpacker<'db> { /// 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 + let Self { db, env, targets } = self; + targets .into_all_elements_with_kind() - .map(|builder| match builder { + .map(move |builder| match builder { TupleElement::Variable(builder) => builder.try_build().unwrap_or_else(|| { - KnownClass::List.to_specialized_instance(self.db, &[Type::unknown()]) + KnownClass::List.to_specialized_instance(db, &env, &[Type::unknown()]) }), TupleElement::Fixed(builder) | TupleElement::Prefix(builder) @@ -2729,6 +2850,7 @@ impl<'db> VariableLengthTuple> { fn unpack_tuple( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, values: &VariableLengthTuple, VariableSegment<'db>>, ) { // We have already verified above that the two tuples have the same length. @@ -2737,9 +2859,12 @@ impl<'db> VariableLengthTuple> { { target.add_in_place(value); } - self.variable_element_mut().add_in_place( - KnownClass::List.to_specialized_instance(db, &[values.variable().element_type(db)]), - ); + self.variable_element_mut() + .add_in_place(KnownClass::List.to_specialized_instance( + db, + env, + &[values.variable().element_type(db)], + )); for (target, value) in (self.suffix_elements_mut().iter_mut()).zip(values.iter_suffix_elements()) { @@ -2781,15 +2906,21 @@ impl<'db> TupleSpecBuilder<'db> { pub(crate) fn concat_variadic_typevar( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typevar: BoundTypeVarInstance<'db>, ) -> Self { debug_assert!(typevar.is_typevartuple(db)); let other = VariableLengthTuple::mixed([], VariableSegment::TypeVarTuple(typevar), []); - self.concat(db, &other) + self.concat(db, env, &other) } /// Concatenates another tuple to the end of this tuple, returning a new tuple. - pub(crate) fn concat(mut self, db: &'db dyn Db, other: &TupleSpec<'db>) -> Self { + pub(crate) fn concat( + mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + other: &TupleSpec<'db>, + ) -> Self { match (&mut self, other) { (TupleSpecBuilder::Fixed(left_tuple), TupleSpec::Fixed(right_tuple)) => { left_tuple.extend_from_slice(&right_tuple.0); @@ -2827,6 +2958,7 @@ impl<'db> TupleSpecBuilder<'db> { ) => { let variable = UnionType::from_elements_leave_aliases( db, + env, left_suffix .iter() .copied() @@ -2872,13 +3004,18 @@ impl<'db> TupleSpecBuilder<'db> { /// `tuple[int, str, bytes]`, the result will be a tuple-spec builder for /// `tuple[int | str | bytes, ...]`. We could consider improving this in the future if real-world /// use cases arise. - pub(crate) fn union(mut self, db: &'db dyn Db, other: &TupleSpec<'db>) -> Self { + pub(crate) fn union( + mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + other: &TupleSpec<'db>, + ) -> Self { match (&mut self, other) { (TupleSpecBuilder::Fixed(our_elements), TupleSpec::Fixed(new_elements)) if our_elements.len() == new_elements.len() => { for (existing, new) in our_elements.iter_mut().zip(new_elements.all_elements()) { - *existing = UnionType::from_elements_leave_aliases(db, [*existing, *new]); + *existing = UnionType::from_elements_leave_aliases(db, env, [*existing, *new]); } self } @@ -2893,6 +3030,7 @@ impl<'db> TupleSpecBuilder<'db> { _ => { let unioned = UnionType::from_elements_leave_aliases( db, + env, self.iter_element_types(db) .chain(other.iter_element_types(db)), ); @@ -2912,14 +3050,19 @@ impl<'db> TupleSpecBuilder<'db> { /// For example, if `self` is a tuple-spec builder for `tuple[int, str]` and `other` is a /// tuple-spec for `tuple[object, object]`, the result will be a tuple-spec builder for /// `tuple[int, str]` (since `int & object` simplifies to `int`, and `str & object` to `str`). - pub(crate) fn intersect(mut self, db: &'db dyn Db, other: &TupleSpec<'db>) -> Option { + pub(crate) fn intersect( + mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + other: &TupleSpec<'db>, + ) -> Option { match (&mut self, other) { // Both fixed-length with the same length: element-wise intersection. (TupleSpecBuilder::Fixed(our_elements), TupleSpec::Fixed(new_elements)) if our_elements.len() == new_elements.len() => { for (existing, new) in our_elements.iter_mut().zip(new_elements.all_elements()) { - *existing = IntersectionType::from_elements(db, [*existing, *new]); + *existing = IntersectionType::from_elements(db, env, [*existing, *new]); } Some(self) } @@ -2928,16 +3071,16 @@ impl<'db> TupleSpecBuilder<'db> { (TupleSpecBuilder::Fixed(_), TupleSpec::Fixed(_)) => None, (TupleSpecBuilder::Fixed(our_elements), TupleSpec::Variable(var)) => var - .resize(db, TupleLength::Fixed(our_elements.len())) + .resize(db, env, TupleLength::Fixed(our_elements.len())) .ok() - .and_then(|tuple| self.intersect(db, &tuple)), + .and_then(|tuple| self.intersect(db, env, &tuple)), (TupleSpecBuilder::Variable { .. }, TupleSpec::Fixed(fixed)) => self .clone() .build() - .resize(db, TupleLength::Fixed(fixed.len())) + .resize(db, env, TupleLength::Fixed(fixed.len())) .ok() - .and_then(|tuple| TupleSpecBuilder::from(&tuple).intersect(db, other)), + .and_then(|tuple| TupleSpecBuilder::from(&tuple).intersect(db, env, other)), ( TupleSpecBuilder::Variable { @@ -2951,7 +3094,7 @@ impl<'db> TupleSpecBuilder<'db> { && suffix.len() == var.suffix_elements().len() { for (existing, new) in prefix.iter_mut().zip(var.prefix_elements()) { - *existing = IntersectionType::from_two_elements(db, *existing, *new); + *existing = IntersectionType::from_two_elements(db, env, *existing, *new); } *segment = match (*segment, var.variable()) { ( @@ -2961,26 +3104,30 @@ impl<'db> TupleSpecBuilder<'db> { (left, right) => { VariableSegment::Homogeneous(IntersectionType::from_two_elements( db, + env, left.element_type(db), right.element_type(db), )) } }; for (existing, new) in suffix.iter_mut().zip(var.suffix_elements()) { - *existing = IntersectionType::from_two_elements(db, *existing, *new); + *existing = IntersectionType::from_two_elements(db, env, *existing, *new); } return Some(self); } let self_built = self.clone().build(); let self_len = self_built.len(); - var.resize(db, self_len) + var.resize(db, env, self_len) .ok() - .and_then(|resized| self.intersect(db, &resized)) + .and_then(|resized| self.intersect(db, env, &resized)) .or_else(|| { - self_built.resize(db, var.len()).ok().and_then(|resized| { - TupleSpecBuilder::from(&resized).intersect(db, other) - }) + self_built + .resize(db, env, var.len()) + .ok() + .and_then(|resized| { + TupleSpecBuilder::from(&resized).intersect(db, env, other) + }) }) } } diff --git a/crates/ty_python_semantic/src/types/tuple/promotion.rs b/crates/ty_python_semantic/src/types/tuple/promotion.rs index b62cc3a336..018a6c4a71 100644 --- a/crates/ty_python_semantic/src/types/tuple/promotion.rs +++ b/crates/ty_python_semantic/src/types/tuple/promotion.rs @@ -1,8 +1,9 @@ +use crate::Db; +use crate::ProgramEnvironment; use rustc_hash::FxHashSet; -use ruff_python_ast as ast; +use ruff_python_ast::{self as ast}; -use crate::Db; use crate::types::tuple::TupleSpec; use crate::types::typevar::BoundTypeVarIdentity; use crate::types::visitor::any_over_type; @@ -24,12 +25,13 @@ impl<'db> TupleSizePromotionConstraints<'db> { pub(crate) fn record_inferred_expression_type( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typevar_identity: BoundTypeVarIdentity<'db>, expression: &ast::Expr, ty: Type<'db>, ) { - if !Self::is_promotable_tuple_literal(db, expression, ty) { - self.record_unpromotable_type(db, typevar_identity, ty); + if !Self::is_promotable_tuple_literal(db, env, expression, ty) { + self.record_unpromotable_type(db, env, typevar_identity, ty); } } @@ -38,10 +40,13 @@ impl<'db> TupleSizePromotionConstraints<'db> { pub(crate) fn record_unpromotable_type( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typevar_identity: BoundTypeVarIdentity<'db>, ty: Type<'db>, ) { - if any_over_type(db, ty, true, |ty| ty.tuple_instance_spec(db).is_some()) { + if any_over_type(db, env, ty, true, |ty| { + ty.tuple_instance_spec(db, env).is_some() + }) { self.blocked_typevars.insert(typevar_identity); } } @@ -54,9 +59,14 @@ impl<'db> TupleSizePromotionConstraints<'db> { /// Returns true if the given expression is either a non-starred homogeneous tuple literal or the /// empty tuple (and hence is eligible for tuple size promotion). - fn is_promotable_tuple_literal(db: &'db dyn Db, expression: &ast::Expr, ty: Type<'db>) -> bool { + fn is_promotable_tuple_literal( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + expression: &ast::Expr, + ty: Type<'db>, + ) -> bool { matches!(expression, ast::Expr::Tuple(tuple) if !tuple.iter().any(ast::Expr::is_starred_expr)) - && TupleSizePromotionCandidate::from_type(db, ty).is_some() + && TupleSizePromotionCandidate::from_type(db, env, ty).is_some() } } @@ -72,7 +82,7 @@ enum TupleSizePromotionCandidate<'db> { impl<'db> TupleSizePromotionCandidate<'db> { /// Returns an eligible candidate if the given type represents one (i.e., it is a /// fixed-length homogeneous tuple or the empty tuple). - fn from_type(db: &'db dyn Db, ty: Type<'db>) -> Option { + fn from_type(db: &'db dyn Db, env: &ProgramEnvironment<'db>, ty: Type<'db>) -> Option { let tuple_spec = ty.exact_tuple_instance_spec(db)?; let TupleSpec::Fixed(tuple) = tuple_spec.as_ref() else { return None; @@ -84,7 +94,7 @@ impl<'db> TupleSizePromotionCandidate<'db> { }; elements - .all(|element| element.is_equivalent_to(db, element_type)) + .all(|element| element.is_equivalent_to(db, env, element_type)) .then_some(Self::Homogeneous { element_type, length: tuple.len(), @@ -122,20 +132,21 @@ impl<'db> HomogeneousTupleUnionGroup<'db> { /// candidates for tuple size promotion, and another for groups of homogeneous tuple elements that are. fn partition_tuple_union_elements<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, elements: impl IntoIterator>, ) -> (Vec>, Vec>) { let mut other_union_elements = Vec::new(); let mut tuple_groups: Vec> = Vec::new(); for element in elements { - match TupleSizePromotionCandidate::from_type(db, element) { + match TupleSizePromotionCandidate::from_type(db, env, element) { Some(TupleSizePromotionCandidate::Homogeneous { element_type, length, }) => { if let Some(group) = tuple_groups .iter_mut() - .find(|group| group.element_type.is_equivalent_to(db, element_type)) + .find(|group| group.element_type.is_equivalent_to(db, env, element_type)) { group.add(element, length); } else { @@ -175,19 +186,23 @@ impl<'db> Type<'db> { /// reveal_type(languages) # revealed: dict[str, tuple[str, ...]] /// ``` /// - pub(crate) fn promote_tuple_size_in_union(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn promote_tuple_size_in_union( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { let Type::Union(union) = self else { return self; }; let (other_union_elements, tuple_groups) = - partition_tuple_union_elements(db, union.elements(db).iter().copied()); + partition_tuple_union_elements(db, env, union.elements(db).iter().copied()); if !tuple_groups.iter().any(|group| group.has_multiple_lengths) { return self; } - let mut builder = UnionBuilder::new(db) + let mut builder = UnionBuilder::new(db, env) .unpack_aliases(false) .recursively_defined(union.recursively_defined(db)); @@ -197,7 +212,7 @@ impl<'db> Type<'db> { for group in tuple_groups { if group.has_multiple_lengths { - builder = builder.add(Type::homogeneous_tuple(db, group.element_type)); + builder = builder.add(Type::homogeneous_tuple(db, env, group.element_type)); } else { for element in group.original_tuple_types { builder = builder.add(element); diff --git a/crates/ty_python_semantic/src/types/type_alias.rs b/crates/ty_python_semantic/src/types/type_alias.rs index 366520f27c..dcf588b207 100644 --- a/crates/ty_python_semantic/src/types/type_alias.rs +++ b/crates/ty_python_semantic/src/types/type_alias.rs @@ -1,12 +1,14 @@ +use crate::ProgramEnvironment; use std::fmt::Write; use crate::{ - Db, + Db, FxOrderSet, types::{ - ApplyTypeMappingVisitor, BoundTypeVarIdentity, GenericContext, Type, TypeContext, - TypeMapping, TypeVarVariance, definition_expression_type, + ApplyTypeMappingVisitor, BindingContext, BoundTypeVarIdentity, GenericContext, + KnownInstanceType, MaterializationKind, Type, TypeContext, TypeMapping, TypeVarVariance, + definition_expression_type, display::qualified_name_components_from_scope, - generics::{ApplySpecialization, Specialization}, + generics::{ApplySpecialization, Specialization, bind_typevar}, match_type::{MatchTypeOutcome, evaluate_match_type}, variance::VarianceInferable, visitor, @@ -19,8 +21,8 @@ use ty_python_core::{ }; use ruff_db::parsed::parsed_module; -use ruff_python_ast as ast; use ruff_python_ast::name::Name; +use ruff_python_ast::{self as ast}; #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub struct PEP695TypeAliasType<'db> { @@ -32,6 +34,10 @@ pub struct PEP695TypeAliasType<'db> { #[returns(copy)] pub(super) specialization: Option>, + + /// Keeps recursive references stable while their alias body is materialized lazily. + #[returns(copy)] + pub(super) materialization_kind: Option, } // The Salsa heap is tracked separately. @@ -42,7 +48,7 @@ pub(super) fn walk_pep_695_type_alias<'db, V: visitor::TypeVisitor<'db> + ?Sized type_alias: PEP695TypeAliasType<'db>, visitor: &V, ) { - visitor.visit_type(db, type_alias.value_type(db)); + visitor.visit_type(db, TypeAliasType::PEP695(type_alias).value_type(db)); } /// basedpython: whether the alias declared by `scope` is a match type. @@ -54,7 +60,7 @@ pub(super) fn walk_pep_695_type_alias<'db, V: visitor::TypeVisitor<'db> + ?Sized /// use of every type alias, and answering from the AST would put a module load on that path. #[salsa::tracked(returns(copy), heap_size = ruff_memory_usage::heap_size)] fn scope_declares_match_type<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> bool { - let module = parsed_module(db, scope.file(db)).load(db); + let module = parsed_module(db, scope.program_file(db).python_file(db)).load(db); !scope .node(db) .expect_type_alias() @@ -68,7 +74,7 @@ impl<'db> PEP695TypeAliasType<'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) + semantic_index(db, scope.program_file(db)).expect_single_definition(type_alias_stmt_node) } /// The RHS type of a PEP-695 style type alias with specialization applied. @@ -79,7 +85,12 @@ impl<'db> PEP695TypeAliasType<'db> { /// reported as `Unknown` while it waits to be specialized. pub(crate) fn value_type(self, db: &'db dyn Db) -> Type<'db> { if !self.is_match_type(db) { - return self.apply_function_specialization(db, self.raw_value_type(db)); + return apply_type_alias_specialization( + db, + self.raw_value_type(db), + self.generic_context(db), + self.specialization(db), + ); } match evaluate_match_type(db, self) { Some(MatchTypeOutcome::Matched(ty)) => *ty, @@ -101,7 +112,7 @@ impl<'db> PEP695TypeAliasType<'db> { /// Match-type evaluation needs this for the subject and for the winning case's body, /// both of which are written in terms of the alias's type parameters. pub(crate) fn apply_own_specialization(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { - self.apply_function_specialization(db, ty) + apply_type_alias_specialization(db, ty, self.generic_context(db), self.specialization(db)) } /// The RHS type of a PEP-695 style type alias with *no* specialization applied. @@ -109,46 +120,23 @@ impl<'db> PEP695TypeAliasType<'db> { #[salsa::tracked( returns(copy), cycle_initial=|_, id, _| Type::divergent(id), - cycle_fn=|db, cycle, previous: &Type<'db>, value: Type<'db>, _| { - value.cycle_normalized(db, *previous, cycle) + cycle_fn=|db: &'db dyn Db, cycle, previous: &Type<'db>, value: Type<'db>, alias: PEP695TypeAliasType<'db>| { + let env = ProgramEnvironment::from_scope(alias.rhs_scope(db)); + value.cycle_normalized(db, &env, *previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] pub(super) fn raw_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 program_file = scope.program_file(db); + let python_file = program_file.python_file(db); + let module = parsed_module(db, python_file).load(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.node(&module).value) } - fn apply_function_specialization(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { - if let Some(generic_context) = self.generic_context(db) { - let specialization = self - .specialization(db) - .unwrap_or_else(|| generic_context.default_specialization(db, None)); - let type_mapping = match specialization.materialization_kind(db) { - None => { - TypeMapping::ApplySpecialization(ApplySpecialization::TypeAlias(specialization)) - } - Some(materialization_kind) => TypeMapping::ApplySpecializationWithMaterialization { - specialization: ApplySpecialization::TypeAlias(specialization), - materialization_kind, - }, - }; - - ty.apply_type_mapping_impl( - db, - &type_mapping, - TypeContext::default(), - &ApplyTypeMappingVisitor::default(), - ) - } else { - ty - } - } - pub(crate) fn apply_specialization( self, db: &'db dyn Db, @@ -169,20 +157,18 @@ impl<'db> PEP695TypeAliasType<'db> { self.name(db), self.rhs_scope(db), Some(specialization), + self.materialization_kind(db), ) } } } - pub(crate) fn is_specialized(self, db: &'db dyn Db) -> bool { - self.specialization(db).is_some() - } - #[salsa::tracked(returns(copy), cycle_initial=|_, _, _| None, heap_size=ruff_memory_usage::heap_size)] pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option> { let scope = self.rhs_scope(db); - let file = scope.file(db); - let parsed = parsed_module(db, file).load(db); + let program_file = scope.program_file(db); + let python_file = program_file.python_file(db); + let parsed = parsed_module(db, python_file).load(db); let type_alias_stmt_node = scope.node(db).expect_type_alias(); type_alias_stmt_node @@ -190,7 +176,7 @@ impl<'db> PEP695TypeAliasType<'db> { .type_params .as_ref() .map(|type_params| { - let index = semantic_index(db, scope.file(db)); + let index = semantic_index(db, program_file); let definition = index.expect_single_definition(type_alias_stmt_node); GenericContext::from_type_params(db, index, definition, type_params) }) @@ -207,6 +193,13 @@ pub struct ManualPEP695TypeAliasType<'db> { pub name: Name, #[returns(copy)] pub definition: Definition<'db>, + + #[returns(copy)] + pub(super) specialization: Option>, + + /// Keeps recursive references stable while their alias body is materialized lazily. + #[returns(copy)] + pub(super) materialization_kind: Option, } // The Salsa heap is tracked separately. @@ -217,27 +210,39 @@ pub(super) fn walk_manual_pep_695_type_alias<'db, V: visitor::TypeVisitor<'db> + type_alias: ManualPEP695TypeAliasType<'db>, visitor: &V, ) { - visitor.visit_type(db, type_alias.value_type(db)); + visitor.visit_type(db, TypeAliasType::ManualPEP695(type_alias).value_type(db)); } #[salsa::tracked] impl<'db> ManualPEP695TypeAliasType<'db> { /// The value type of this manual type alias. /// + /// Computed lazily from the definition with specialization applied. + fn value_type(self, db: &'db dyn Db) -> Type<'db> { + apply_type_alias_specialization( + db, + self.raw_value_type(db), + self.generic_context(db), + self.specialization(db), + ) + } + + /// The value type of this manual type alias with no specialization applied. + /// /// Computed lazily from the definition to avoid including the value in the interned /// struct's identity. Returns `Divergent` if the type alias is defined cyclically. #[salsa::tracked( returns(copy), cycle_initial=|_, id, _| Type::divergent(id), - cycle_fn=|db, cycle, previous: &Type<'db>, value: Type<'db>, _| { - value.cycle_normalized(db, *previous, cycle) + cycle_fn=|db: &'db dyn Db, cycle, previous: &Type<'db>, value: Type<'db>, alias: ManualPEP695TypeAliasType<'db>| { + let env = ProgramEnvironment::from_definition(alias.definition(db)); + value.cycle_normalized(db, &env, *previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] - pub(crate) fn value_type(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn raw_value_type(self, db: &'db dyn Db) -> Type<'db> { let definition = self.definition(db); - let file = definition.file(db); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, definition.python_file(db)).load(db); let DefinitionKind::Assignment(assignment) = definition.kind(db) else { return Type::unknown(); }; @@ -251,6 +256,93 @@ impl<'db> ManualPEP695TypeAliasType<'db> { }; definition_expression_type(db, definition, value_arg) } + + fn apply_specialization( + self, + db: &'db dyn Db, + f: impl FnOnce(GenericContext<'db>) -> Specialization<'db>, + ) -> Self { + let Some(generic_context) = self.generic_context(db) else { + return self; + }; + + Self::new( + db, + self.name(db), + self.definition(db), + Some(f(generic_context)), + self.materialization_kind(db), + ) + } + + #[salsa::tracked(returns(copy), cycle_initial=|_, _, _| None, heap_size=ruff_memory_usage::heap_size)] + pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option> { + let definition = self.definition(db); + let file = definition.program_file(db); + let env = ProgramEnvironment::from_file(file); + let module = parsed_module(db, file.python_file(db)).load(db); + let DefinitionKind::Assignment(assignment) = definition.kind(db) else { + return None; + }; + let ast::Expr::Call(call) = assignment.value(&module) else { + return None; + }; + let type_params = call + .arguments + .find_argument_value("type_params", 2)? + .as_tuple_expr()?; + let index = semantic_index(db, file); + + let mut variables = FxOrderSet::default(); + for element in &type_params.elts { + let typevar = match definition_expression_type(db, definition, element) { + Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => bind_typevar( + db, + index, + definition.file_scope(db), + Some(definition), + typevar, + )?, + _ => return None, + }; + if typevar.binding_context(db) != BindingContext::Definition(definition) { + return None; + } + variables.insert(typevar); + } + + (!variables.is_empty()).then(|| GenericContext::from_typevar_instances(db, &env, variables)) + } +} + +fn apply_type_alias_specialization<'db>( + db: &'db dyn Db, + ty: Type<'db>, + generic_context: Option>, + specialization: Option>, +) -> Type<'db> { + let Some(generic_context) = generic_context else { + return ty; + }; + + let env = ProgramEnvironment::from_program(generic_context.program(db)); + let specialization = + specialization.unwrap_or_else(|| generic_context.default_specialization(db, None)); + let type_mapping = match specialization.materialization_kind(db) { + None => TypeMapping::ApplySpecialization(ApplySpecialization::TypeAlias(specialization)), + Some(materialization_kind) => TypeMapping::ApplySpecializationWithMaterialization { + specialization: ApplySpecialization::TypeAlias(specialization), + materialization_kind, + }, + }; + + ty.apply_type_mapping_impl( + db, + &env, + &type_mapping, + TypeContext::default(), + &ApplyTypeMappingVisitor::new(&env), + ) } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize, salsa::SalsaValue)] @@ -296,20 +388,49 @@ impl<'db> TypeAliasType<'db> { } pub fn value_type(self, db: &'db dyn Db) -> Type<'db> { + if let Some(materialization_kind) = self.materialization_kind(db) { + return self.materialized_value_type(db, materialization_kind); + } + match self { TypeAliasType::PEP695(type_alias) => type_alias.value_type(db), TypeAliasType::ManualPEP695(type_alias) => type_alias.value_type(db), } } + /// Materialize the alias body lazily, keeping this alias as the recursive fallback. + /// + /// Comparing a recursive specialization with its materialization can request this same body + /// before it has finished materializing. Returning the already-marked alias closes that cycle + /// without losing its materialization polarity. + #[salsa::tracked( + returns(copy), + cycle_initial=|_, _, alias: TypeAliasType<'db>, _| Type::TypeAlias(alias), + heap_size=ruff_memory_usage::heap_size + )] + fn materialized_value_type( + self, + db: &'db dyn Db, + materialization_kind: MaterializationKind, + ) -> Type<'db> { + let value_type = self.with_materialization_kind(db, None).value_type(db); + let env = ProgramEnvironment::from_definition(self.definition(db)); + value_type.materialize( + db, + &env, + materialization_kind, + &ApplyTypeMappingVisitor::new(&env), + ) + } + pub(crate) fn raw_value_type(self, db: &'db dyn Db) -> Type<'db> { match self { TypeAliasType::PEP695(type_alias) => type_alias.raw_value_type(db), - TypeAliasType::ManualPEP695(type_alias) => type_alias.value_type(db), + TypeAliasType::ManualPEP695(type_alias) => type_alias.raw_value_type(db), } } - /// Returns the alias without an applied specialization. + /// Returns the alias without an applied specialization or pending materialization. pub(super) fn unspecialized(self, db: &'db dyn Db) -> Self { match self { TypeAliasType::PEP695(alias) => TypeAliasType::PEP695(PEP695TypeAliasType::new( @@ -317,8 +438,53 @@ impl<'db> TypeAliasType<'db> { alias.name(db), alias.rhs_scope(db), None, + None, + )), + TypeAliasType::ManualPEP695(alias) => { + TypeAliasType::ManualPEP695(ManualPEP695TypeAliasType::new( + db, + alias.name(db), + alias.definition(db), + None, + None, + )) + } + } + } + + pub(super) fn materialization_kind(self, db: &'db dyn Db) -> Option { + match self { + TypeAliasType::PEP695(alias) => alias.materialization_kind(db), + TypeAliasType::ManualPEP695(alias) => alias.materialization_kind(db), + } + } + + pub(super) fn with_materialization_kind( + self, + db: &'db dyn Db, + materialization_kind: Option, + ) -> Self { + if self.materialization_kind(db) == materialization_kind { + return self; + } + + match self { + TypeAliasType::PEP695(alias) => TypeAliasType::PEP695(PEP695TypeAliasType::new( + db, + alias.name(db), + alias.rhs_scope(db), + alias.specialization(db), + materialization_kind, )), - TypeAliasType::ManualPEP695(_) => self, + TypeAliasType::ManualPEP695(alias) => { + TypeAliasType::ManualPEP695(ManualPEP695TypeAliasType::new( + db, + alias.name(db), + alias.definition(db), + alias.specialization(db), + materialization_kind, + )) + } } } @@ -330,17 +496,16 @@ impl<'db> TypeAliasType<'db> { } pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option> { - // TODO: Add support for generic non-PEP695 type aliases. match self { TypeAliasType::PEP695(type_alias) => type_alias.generic_context(db), - TypeAliasType::ManualPEP695(_) => None, + TypeAliasType::ManualPEP695(type_alias) => type_alias.generic_context(db), } } pub(crate) fn specialization(self, db: &'db dyn Db) -> Option> { match self { TypeAliasType::PEP695(type_alias) => type_alias.specialization(db), - TypeAliasType::ManualPEP695(_) => None, + TypeAliasType::ManualPEP695(type_alias) => type_alias.specialization(db), } } @@ -353,7 +518,9 @@ impl<'db> TypeAliasType<'db> { TypeAliasType::PEP695(type_alias) => { TypeAliasType::PEP695(type_alias.apply_specialization(db, f)) } - TypeAliasType::ManualPEP695(_) => self, + TypeAliasType::ManualPEP695(type_alias) => { + TypeAliasType::ManualPEP695(type_alias.apply_specialization(db, f)) + } } } @@ -363,16 +530,32 @@ impl<'db> TypeAliasType<'db> { } } -#[salsa::tracked] impl<'db> VarianceInferable<'db> for TypeAliasType<'db> { + fn variance_of( + self, + db: &'db dyn Db, + _: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { + self.variance_of_owner(db, typevar) + } +} + +#[salsa::tracked] +impl<'db> TypeAliasType<'db> { #[salsa::tracked( returns(copy), cycle_initial=|_, _, _, _| TypeVarVariance::Bivariant, heap_size=ruff_memory_usage::heap_size )] - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { + fn variance_of_owner( + self, + db: &'db dyn Db, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { + let env = ProgramEnvironment::from_definition(self.definition(db)); let Some(generic_context) = self.generic_context(db) else { - return self.value_type(db).variance_of(db, typevar); + return self.value_type(db).variance_of(db, &env, typevar); }; // Infer an alias's own type-parameter variance from the raw RHS. Applying specialization @@ -381,7 +564,7 @@ impl<'db> VarianceInferable<'db> for TypeAliasType<'db> { .variables(db) .any(|alias_typevar| alias_typevar.identity(db) == typevar) { - return self.raw_value_type(db).variance_of(db, typevar); + return self.raw_value_type(db).variance_of(db, &env, typevar); } let raw_value_type = self.raw_value_type(db); @@ -396,8 +579,8 @@ impl<'db> VarianceInferable<'db> for TypeAliasType<'db> { .zip(specialization.types(db)) .map(|(alias_typevar, argument_ty)| { raw_value_type - .variance_of(db, alias_typevar.identity(db)) - .compose_thunk(|| argument_ty.variance_of(db, typevar)) + .variance_of(db, &env, alias_typevar.identity(db)) + .compose_thunk(|| argument_ty.variance_of(db, &env, typevar)) }) .collect() } @@ -414,7 +597,7 @@ pub(crate) struct QualifiedTypeAliasName<'db> { } impl<'db> QualifiedTypeAliasName<'db> { - pub(crate) fn from_type_alias(db: &'db dyn Db, type_alias: TypeAliasType<'db>) -> Self { + fn from_type_alias(db: &'db dyn Db, type_alias: TypeAliasType<'db>) -> Self { Self { db, type_alias } } @@ -424,7 +607,7 @@ impl<'db> QualifiedTypeAliasName<'db> { /// would return `["a", "b", "C"]`. pub(crate) fn components_excluding_self(&self) -> Vec { let definition = self.type_alias.definition(self.db); - let file = definition.file(self.db); + let file = definition.program_file(self.db); let file_scope_id = definition.file_scope(self.db); // Type aliases are defined directly in their enclosing scope (no body scope like classes), diff --git a/crates/ty_python_semantic/src/types/type_expansion.rs b/crates/ty_python_semantic/src/types/type_expansion.rs index f19b145c76..4bb9546ec4 100644 --- a/crates/ty_python_semantic/src/types/type_expansion.rs +++ b/crates/ty_python_semantic/src/types/type_expansion.rs @@ -1,6 +1,7 @@ +use crate::Db; use itertools::Itertools; -use crate::Db; +use crate::ProgramEnvironment; use crate::types::enums::enum_member_literals; use crate::types::tuple::Tuple; use crate::types::typevar::TypeVarKind; @@ -17,12 +18,16 @@ const MAX_TUPLE_EXPANSION: usize = 64; /// Expands a type into its possible subtypes, if applicable. /// /// Returns [`None`] if the type cannot be expanded. -pub(crate) fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option>> { +pub(crate) fn expand_type<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Option>> { match ty { - Type::EnumComplement(complement) => Some(complement.remaining_literal_types(db)), - Type::Intersection(intersection) => intersection.finite_alternatives(db), + Type::EnumComplement(complement) => Some(complement.remaining_literal_types(db, env)), + Type::Intersection(intersection) => intersection.finite_alternatives(db, env), Type::NominalInstance(instance) => { - let class = instance.class(db); + let class = instance.class(db, env); if class.is_known(db, KnownClass::Bool) { return Some(vec![Type::bool_literal(true), Type::bool_literal(false)]); @@ -32,12 +37,13 @@ pub(crate) fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option union.elements(db).to_vec(), @@ -46,7 +52,7 @@ pub(crate) fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option { // Pre-expand each element and compute the total Cartesian product size. @@ -56,7 +62,7 @@ pub(crate) fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option = fixed_length_tuple .iter_all_elements() .map(|element| { - expand_type(db, element).unwrap_or_else(|| vec![element]) + expand_type(db, env, element).unwrap_or_else(|| vec![element]) }) .collect(); @@ -71,7 +77,7 @@ pub(crate) fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option>(); Some(expanded) } @@ -91,16 +97,16 @@ pub(crate) fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option complement.remaining_literal_types(db), + Type::EnumComplement(complement) => complement.remaining_literal_types(db, env), Type::Intersection(intersection) => intersection - .finite_alternatives(db) + .finite_alternatives(db, env) .unwrap_or_else(|| vec![*element]), _ => vec![*element], }) .collect(), ), // For type aliases, expand the underlying value type. - Type::TypeAlias(alias) => expand_type(db, alias.value_type(db)), + Type::TypeAlias(alias) => expand_type(db, env, alias.value_type(db)), // basedpython: `Self` in a payload-enum method is bounded by the enum's // closed variant union, so it expands to the variants — making // `match self` in an enum method exhaustive @@ -109,10 +115,10 @@ pub(crate) fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option| match ty { Type::NominalInstance(instance) => instance - .class_literal(db) + .class_literal(db, env) .as_static() .is_some_and(|class| class.is_enum_variant(db)), ty if ty.as_enum_literal().is_some() => true, @@ -142,13 +148,15 @@ mod tests { #[test] fn expand_union_type() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); let types = [ - KnownClass::Int.to_instance(&db), - KnownClass::Str.to_instance(&db), - KnownClass::Bytes.to_instance(&db), + KnownClass::Int.to_instance(db, &env), + KnownClass::Str.to_instance(db, &env), + KnownClass::Bytes.to_instance(db, &env), ]; - let union_type = UnionType::from_elements(&db, types); - let expanded = expand_type(&db, union_type).unwrap(); + let union_type = UnionType::from_elements(db, &env, types); + let expanded = expand_type(db, &env, union_type).unwrap(); assert_eq!(expanded.len(), types.len()); assert_eq!(expanded, types); } @@ -156,8 +164,10 @@ mod tests { #[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 db = &db; + let env = db.program_environment(); + let bool_instance = KnownClass::Bool.to_instance(db, &env); + let expanded = expand_type(db, &env, bool_instance).unwrap(); let expected_types = [Type::bool_literal(true), Type::bool_literal(false)]; assert_eq!(expanded.len(), expected_types.len()); assert_eq!(expanded, expected_types); @@ -166,70 +176,78 @@ mod tests { #[test] fn expand_tuple_type() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); - 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 int_ty = KnownClass::Int.to_instance(db, &env); + let str_ty = KnownClass::Str.to_instance(db, &env); + let bytes_ty = KnownClass::Bytes.to_instance(db, &env); + let bool_ty = KnownClass::Bool.to_instance(db, &env); let true_ty = Type::bool_literal(true); let false_ty = Type::bool_literal(false); // Empty tuple - let empty_tuple = Type::empty_tuple(&db); - let expanded = expand_type(&db, empty_tuple); + let empty_tuple = Type::empty_tuple(db, &env); + let expanded = expand_type(db, &env, empty_tuple); assert!(expanded.is_none()); // None of the elements can be expanded. - let tuple_type1 = Type::heterogeneous_tuple(&db, [int_ty, str_ty]); - let expanded = expand_type(&db, tuple_type1); + let tuple_type1 = Type::heterogeneous_tuple(db, &env, [int_ty, str_ty]); + let expanded = expand_type(db, &env, tuple_type1); assert!(expanded.is_none()); // All elements can be expanded. let tuple_type2 = Type::heterogeneous_tuple( - &db, + db, + &env, [ bool_ty, - UnionType::from_elements(&db, [int_ty, str_ty, bytes_ty]), + UnionType::from_elements(db, &env, [int_ty, str_ty, bytes_ty]), ], ); let expected_types = [ - Type::heterogeneous_tuple(&db, [true_ty, int_ty]), - Type::heterogeneous_tuple(&db, [true_ty, str_ty]), - Type::heterogeneous_tuple(&db, [true_ty, bytes_ty]), - Type::heterogeneous_tuple(&db, [false_ty, int_ty]), - Type::heterogeneous_tuple(&db, [false_ty, str_ty]), - Type::heterogeneous_tuple(&db, [false_ty, bytes_ty]), + Type::heterogeneous_tuple(db, &env, [true_ty, int_ty]), + Type::heterogeneous_tuple(db, &env, [true_ty, str_ty]), + Type::heterogeneous_tuple(db, &env, [true_ty, bytes_ty]), + Type::heterogeneous_tuple(db, &env, [false_ty, int_ty]), + Type::heterogeneous_tuple(db, &env, [false_ty, str_ty]), + Type::heterogeneous_tuple(db, &env, [false_ty, bytes_ty]), ]; - let expanded = expand_type(&db, tuple_type2).unwrap(); + let expanded = expand_type(db, &env, tuple_type2).unwrap(); assert_eq!(expanded, expected_types); // Mixed set of elements where some can be expanded while others cannot be. let tuple_type3 = Type::heterogeneous_tuple( - &db, + db, + &env, [ bool_ty, int_ty, - UnionType::from_elements(&db, [str_ty, bytes_ty]), + UnionType::from_elements(db, &env, [str_ty, bytes_ty]), str_ty, ], ); let expected_types = [ - Type::heterogeneous_tuple(&db, [true_ty, int_ty, str_ty, str_ty]), - Type::heterogeneous_tuple(&db, [true_ty, int_ty, bytes_ty, str_ty]), - Type::heterogeneous_tuple(&db, [false_ty, int_ty, str_ty, str_ty]), - Type::heterogeneous_tuple(&db, [false_ty, int_ty, bytes_ty, str_ty]), + Type::heterogeneous_tuple(db, &env, [true_ty, int_ty, str_ty, str_ty]), + Type::heterogeneous_tuple(db, &env, [true_ty, int_ty, bytes_ty, str_ty]), + Type::heterogeneous_tuple(db, &env, [false_ty, int_ty, str_ty, str_ty]), + Type::heterogeneous_tuple(db, &env, [false_ty, int_ty, bytes_ty, str_ty]), ]; - let expanded = expand_type(&db, tuple_type3).unwrap(); + let expanded = expand_type(db, &env, tuple_type3).unwrap(); assert_eq!(expanded, expected_types); // Variable-length tuples are not expanded. let variable_length_tuple = Type::tuple(TupleType::mixed( - &db, + db, + &env, [bool_ty], int_ty, - [UnionType::from_elements(&db, [str_ty, bytes_ty]), str_ty], + [ + UnionType::from_elements(db, &env, [str_ty, bytes_ty]), + str_ty, + ], )); - let expanded = expand_type(&db, variable_length_tuple); + let expanded = expand_type(db, &env, variable_length_tuple); assert!(expanded.is_none()); } } diff --git a/crates/ty_python_semantic/src/types/type_fn.rs b/crates/ty_python_semantic/src/types/type_fn.rs index 253807fb44..e61505586f 100644 --- a/crates/ty_python_semantic/src/types/type_fn.rs +++ b/crates/ty_python_semantic/src/types/type_fn.rs @@ -41,6 +41,7 @@ use ruff_text_size::Ranged; use crate::Db; use crate::place::{builtins_symbol, imported_symbol}; +use crate::types::ProgramEnvironment; use crate::types::function::FunctionType; use crate::types::literal::LiteralValueTypeKind; use crate::types::tuple::TupleType; @@ -85,6 +86,7 @@ pub(crate) fn evaluate_type_fn<'db>( function: FunctionType<'db>, arguments: TypeFnArguments<'db>, ) -> TypeFnOutcome<'db> { + let env = &ProgramEnvironment::from_definition(function.definition(db)); let arguments = arguments.arguments(db); let file = function.file(db); @@ -92,7 +94,7 @@ pub(crate) fn evaluate_type_fn<'db>( return TypeFnOutcome::Failed(refusal); } - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, db.program_file(file).python_file(db)).load(db); let Some(source) = type_fn_python_source(db, function, file, &module) else { return TypeFnOutcome::Failed("`type def` has no body to execute".to_string()); @@ -103,11 +105,11 @@ pub(crate) fn evaluate_type_fn<'db>( if i > 0 { argument_json.push(','); } - let Some(described) = describe_type(db, *argument, Some(i), 0) else { + let Some(described) = describe_type(db, env, *argument, Some(i), 0) else { return TypeFnOutcome::Failed(format!( "cannot describe `{}` to a type function; the proof of concept \ only handles class instances, literals, unions and `None`", - argument.display(db) + argument.display(db, env) )); }; argument_json.push_str(&described); @@ -118,7 +120,7 @@ pub(crate) fn evaluate_type_fn<'db>( match run_python(&script) { Err(error) => TypeFnOutcome::Failed(error), - Ok(output) => interpret_result(db, arguments, &output), + Ok(output) => interpret_result(db, env, arguments, &output), } } @@ -142,7 +144,7 @@ fn execution_is_permitted(db: &dyn Db, file: File) -> Result<(), String> { ); } - let is_first_party = file_to_module(db, file) + let is_first_party = file_to_module(db, db.program_file(file).resolver_file(db)) .and_then(|module| module.search_path(db).map(SearchPath::is_first_party)) .unwrap_or(false); @@ -199,6 +201,7 @@ pub(crate) fn arity_mismatch<'db>( /// outside its bound. pub(crate) fn first_bound_violation<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, function: FunctionType<'db>, arguments: &[Type<'db>], ) -> Option<(usize, Type<'db>, Type<'db>)> { @@ -208,13 +211,13 @@ pub(crate) fn first_bound_violation<'db>( .zip(arguments.iter()) .enumerate() { - let Some(bound) = typevar.typevar(db).upper_bound(db) else { + let Some(bound) = typevar.typevar(db).upper_bound(db, env) else { continue; }; // the argument reads as a type expression, so compare the *instance* it // denotes against the bound's instance — `F[bool]` under `X: int` asks // whether `bool` is assignable to `int` - if !argument.is_assignable_to(db, bound) { + if !argument.is_assignable_to(db, env, bound) { return Some((index, *argument, bound)); } } @@ -274,6 +277,7 @@ fn type_fn_python_source<'db>( /// nominal relations are answerable. fn describe_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, handle: Option, depth: u32, @@ -291,7 +295,7 @@ fn describe_type<'db>( Type::ClassLiteral(class) => { // `F[int]` passes the *instance* type, matching how `int` reads in a // type expression - Type::instance(db, class.default_specialization(db)) + Type::instance(db, env, class.default_specialization(db)) } Type::Union(union) => { let mut members = String::from("["); @@ -299,7 +303,7 @@ fn describe_type<'db>( if i > 0 { members.push(','); } - members.push_str(&describe_type(db, *member, None, depth + 1)?); + members.push_str(&describe_type(db, env, *member, None, depth + 1)?); } members.push(']'); return Some(format!( @@ -322,9 +326,9 @@ fn describe_type<'db>( // a literal is described by the class it falls back to (`Literal[9]` → `int`) // plus its value, so `X <= int` and `X.literal` both work let instance_class = match instance { - Type::NominalInstance(nominal) => nominal.class(db), - other => match other.literal_fallback_instance(db)? { - Type::NominalInstance(nominal) => nominal.class(db), + Type::NominalInstance(nominal) => nominal.class(db, env), + other => match other.literal_fallback_instance(db, env)? { + Type::NominalInstance(nominal) => nominal.class(db, env), _ => return None, }, }; @@ -363,6 +367,7 @@ const RESULT_SENTINEL: &str = "\u{1}by-type-fn\u{1}"; /// fail outright for anything that is not a builtin. fn interpret_result<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, arguments: &[Type<'db>], output: &str, ) -> TypeFnOutcome<'db> { @@ -378,7 +383,7 @@ fn interpret_result<'db>( let (tag, payload) = line.split_once(' ').unwrap_or((line, "")); match tag { - "TYPE" => match resolve_graph(db, arguments, payload) { + "TYPE" => match resolve_graph(db, env, arguments, payload) { Ok(ty) => TypeFnOutcome::Type(ty), Err(error) => { TypeFnOutcome::Failed(format!("type function returned an unusable type: {error}")) @@ -422,6 +427,7 @@ fn unescape(payload: &str) -> String { /// spelling that would have lost it. fn resolve_graph<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, arguments: &[Type<'db>], encoded: &str, ) -> Result, String> { @@ -436,7 +442,7 @@ fn resolve_graph<'db>( .and_then(|index| arguments.get(index).copied()) .ok_or_else(|| format!("unknown argument handle `{payload}`"))?, // a class object, resolved through ty's module resolver - "c" => resolve_qualified_name(db, payload) + "c" => resolve_qualified_name(db, env, payload) .ok_or_else(|| format!("`{payload}` does not name a type"))?, "i" => payload .parse::() @@ -458,7 +464,7 @@ fn resolve_graph<'db>( .next() .ok_or_else(|| "empty generic form".to_string())??; let arguments = parts.collect::, _>>()?; - specialize(db, origin, &arguments)? + specialize(db, env, origin, &arguments)? } "u" => { let members = payload @@ -471,7 +477,7 @@ fn resolve_graph<'db>( .ok_or_else(|| "malformed union".to_string()) }) .collect::, _>>()?; - UnionType::from_elements(db, members) + UnionType::from_elements(db, env, members) } other => return Err(format!("unknown type form `{other}`")), }; @@ -486,6 +492,7 @@ fn resolve_graph<'db>( /// expression would. fn specialize<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, origin: Type<'db>, arguments: &[Type<'db>], ) -> Result, String> { @@ -493,13 +500,13 @@ fn specialize<'db>( // (`list` → `list[Unknown]`), so specializing one means going back to the class // it came from and applying the arguments to that let class_literal = match origin { - Type::NominalInstance(nominal) => nominal.class(db).class_literal(db), + Type::NominalInstance(nominal) => nominal.class(db, env).class_literal(db), Type::ClassLiteral(class) => class, Type::GenericAlias(alias) => alias.origin(db).into(), _ => { return Err(format!( "`{}` cannot take type arguments", - origin.display(db) + origin.display(db, env) )); } }; @@ -509,6 +516,7 @@ fn specialize<'db>( if class_literal.is_known(db, KnownClass::Tuple) { return Ok(Type::tuple(TupleType::heterogeneous( db, + env, arguments.iter().copied(), ))); } @@ -516,29 +524,33 @@ fn specialize<'db>( let specialized = class_literal.apply_specialization(db, |generic_context| { generic_context.specialize_partial(db, arguments.iter().copied().map(Some)) }); - Ok(Type::instance(db, specialized)) + Ok(Type::instance(db, env, specialized)) } /// Resolves a dotted `module.Class` reference (or a bare builtin) to its instance /// type, through ty's module resolver. -fn resolve_qualified_name<'db>(db: &'db dyn Db, qualname: &str) -> Option> { +fn resolve_qualified_name<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + qualname: &str, +) -> Option> { let qualname = qualname.trim(); if matches!(qualname, "None" | "NoneType" | "builtins.NoneType") { - return Some(Type::none(db)); + return Some(Type::none(db, env)); } let place = match qualname.rsplit_once('.') { - None => builtins_symbol(db, qualname).place, - Some(("builtins", name)) => builtins_symbol(db, name).place, + None => builtins_symbol(db, env, qualname).place, + Some(("builtins", name)) => builtins_symbol(db, env, name).place, Some((module, name)) => { let module_name = ModuleName::new(module)?; - let module = resolve_module_confident(db, &module_name)?; - imported_symbol(db, Some(module.file(db)?), name, None).place + let module = resolve_module_confident(db, env.resolver_environment(db), &module_name)?; + imported_symbol(db, env, Some(db.program_file(module.file(db)?)), name, None).place } }; match place.ignore_possibly_undefined() { Some(Type::ClassLiteral(class)) => { - Some(Type::instance(db, class.default_specialization(db))) + Some(Type::instance(db, env, class.default_specialization(db))) } _ => None, } diff --git a/crates/ty_python_semantic/src/types/type_form.rs b/crates/ty_python_semantic/src/types/type_form.rs index 6d817b329e..b85763b23c 100644 --- a/crates/ty_python_semantic/src/types/type_form.rs +++ b/crates/ty_python_semantic/src/types/type_form.rs @@ -4,6 +4,7 @@ use super::{ visitor, }; use crate::Db; +use crate::ProgramEnvironment; #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub struct TypeFormType<'db> { @@ -36,64 +37,73 @@ impl<'db> Type<'db> { /// bounds or constraints, using cycle detection for recursive types. Union and intersection /// elements that do not represent type forms are ignored, as are negative intersection /// elements. If no type-form component can be projected, this returns the original type. - pub(crate) fn project_type_form(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn project_type_form( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { struct TypeFormArgument; type TypeFormArgumentVisitor<'db> = CycleDetector<'db, TypeFormArgument, Type<'db>, Option>, 3>; fn project<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, visitor: &TypeFormArgumentVisitor<'db>, ) -> Option> { match ty { Type::TypeForm(type_form) => Some(type_form.type_argument(db)), Type::TypeAlias(alias) => { - visitor.visit(db, ty, || project(db, alias.value_type(db), visitor)) + visitor.visit(db, ty, || project(db, env, alias.value_type(db), visitor)) } Type::Union(union) => { let mut elements = union .elements(db) .iter() - .filter_map(|element| project(db, *element, visitor)) + .filter_map(|element| project(db, env, *element, visitor)) .peekable(); elements.peek()?; - Some(UnionType::from_elements(db, elements)) + Some(UnionType::from_elements(db, env, elements)) } Type::Intersection(intersection) => { let mut elements = intersection .iter_positive(db) - .filter_map(|element| project(db, element, visitor)) + .filter_map(|element| project(db, env, element, visitor)) .peekable(); elements.peek()?; - Some(IntersectionType::from_elements(db, elements)) + Some(IntersectionType::from_elements(db, env, elements)) } Type::TypeVar(typevar) => visitor.visit(db, ty, || { - typevar - .typevar(db) - .bound_or_constraints(db) - .and_then(|bound_or_constraints| { - project(db, bound_or_constraints.as_type(db), visitor) - }) + typevar.typevar(db).bound_or_constraints(db, env).and_then( + |bound_or_constraints| { + project(db, env, bound_or_constraints.as_type(db, env), visitor) + }, + ) }), - Type::SpecialForm(special_form) => special_form.type_form_argument(db), + Type::SpecialForm(special_form) => special_form.type_form_argument(db, env), Type::KnownInstance(instance) if instance.is_type_form_value() => { - instance.type_form_argument(db) + instance.type_form_argument(db, env) } Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_) => { - ty.to_instance_approximation(db) + ty.to_instance_approximation(db, env) } _ => None, } } - project(db, self, &TypeFormArgumentVisitor::default()).unwrap_or(self) + project(db, env, self, &TypeFormArgumentVisitor::default()).unwrap_or(self) } } impl<'db> VarianceInferable<'db> for TypeFormType<'db> { // `TypeForm` is covariant in its type argument. - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { - self.type_argument(db).variance_of(db, typevar) + fn variance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { + self.type_argument(db).variance_of(db, env, typevar) } } diff --git a/crates/ty_python_semantic/src/types/typed_dict.rs b/crates/ty_python_semantic/src/types/typed_dict.rs index f7dd4b91f6..c30eb24040 100644 --- a/crates/ty_python_semantic/src/types/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/typed_dict.rs @@ -21,7 +21,6 @@ use super::{ ApplyTypeMappingVisitor, ErrorContext, IntersectionType, Type, TypeMapping, TypeQualifiers, UnionBuilder, definition_expression_annotation, definition_expression_type, visitor, }; -use crate::Db; use crate::types::TypeContext; use crate::types::TypeDefinition; use crate::types::class::FieldKind; @@ -29,6 +28,8 @@ use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension}; use crate::types::relation::{DisjointnessChecker, TypeRelation, TypeRelationChecker}; use crate::types::typevar::BoundTypeVarIdentity; use crate::types::variance::{TypeVarVariance, VarianceInferable}; +use crate::{Db, ProgramEnvironment}; +use ty_python_core::Truthiness; use ty_python_core::definition::Definition; bitflags! { @@ -127,17 +128,22 @@ impl<'db> TypedDictOpenness<'db> { fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { match self { Self::ImplicitlyOpen | Self::Closed => self, Self::Extra(extra_items) => Self::extra( db, - extra_items - .declared_ty - .apply_type_mapping_impl(db, type_mapping, tcx, visitor), + extra_items.declared_ty.apply_type_mapping_impl( + db, + env, + type_mapping, + tcx, + visitor, + ), extra_items.is_read_only, ), } @@ -146,6 +152,7 @@ impl<'db> TypedDictOpenness<'db> { pub(crate) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -154,7 +161,7 @@ impl<'db> TypedDictOpenness<'db> { Self::Extra(extra_items) => { let declared_ty = extra_items .declared_ty - .recursive_type_normalized_impl(db, div, true); + .recursive_type_normalized_impl(db, env, div, true); let declared_ty = if nested { declared_ty? } else { @@ -249,7 +256,9 @@ impl<'db> TypedDictType<'db> { let (class_literal, specialization) = class.class_literal_and_specialization(db); let static_class = match class_literal { ClassLiteral::Static(static_class) => static_class, - ClassLiteral::DynamicTypedDict(dynamic) => return dynamic.openness(db), + ClassLiteral::DynamicTypedDict(dynamic) => { + return dynamic.openness(db); + } ClassLiteral::Dynamic(_) | ClassLiteral::DynamicNamedTuple(_) | ClassLiteral::DynamicEnum(_) => { @@ -258,7 +267,10 @@ impl<'db> TypedDictType<'db> { } }; - let module = parsed_module(db, static_class.file(db)).load(db); + let program_file = static_class.program_file(db); + let python_file = program_file.python_file(db); + let env = ProgramEnvironment::from_file(program_file); + let module = parsed_module(db, python_file).load(db); let class_definition = static_class.definition(db); let class_stmt = class_definition .kind(db) @@ -280,7 +292,7 @@ impl<'db> TypedDictType<'db> { if let Some(closed) = arguments.find_keyword("closed") { let closed_ty = definition_expression_type(db, class_definition, &closed.value); - return if closed_ty.bool(db).is_always_true() { + return if closed_ty.bool(db, &env).is_always_true() { TypedDictOpenness::Closed } else { TypedDictOpenness::ImplicitlyOpen @@ -322,13 +334,13 @@ impl<'db> TypedDictType<'db> { /// /// An implicitly open `TypedDict` immediately returns `object` because hidden items may have /// any value type. This also avoids unnecessarily materializing its declared items. - pub(crate) fn value_type(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn value_type(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { let openness = self.openness(db); if openness.is_implicitly_open() { return Type::object(); } - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); for field in self.items(db).values() { builder = builder.add(field.declared_ty); } @@ -342,20 +354,34 @@ impl<'db> TypedDictType<'db> { /// /// A closed `TypedDict` has a finite set of literal keys. Open and extra-items `TypedDict`s may /// contain arbitrary string keys. - pub(crate) fn key_type(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn key_type(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { if !self.openness(db).is_closed() { - return KnownClass::Str.to_instance(db); + return KnownClass::Str.to_instance(db, env); } self.items(db) .iter() .filter(|(_, field)| field.may_be_present(db)) - .fold(UnionBuilder::new(db), |builder, (name, _)| { + .fold(UnionBuilder::new(db, env), |builder, (name, _)| { builder.add(Type::string_literal(db, name)) }) .build() } + /// Returns whether a literal string key must, cannot, or might be present. + /// + /// An undeclared key can still exist in an implicitly open `TypedDict` or one with explicit + /// extra items. An optional field with an uninhabited value type can never be present. + pub(crate) fn key_membership_truthiness(self, db: &'db dyn Db, key: &str) -> Truthiness { + match self.items(db).get(key) { + Some(field) if field.is_required() => Truthiness::AlwaysTrue, + Some(field) if field.may_be_present(db) => Truthiness::Ambiguous, + Some(_) => Truthiness::AlwaysFalse, + None if self.openness(db).is_closed() => Truthiness::AlwaysFalse, + None => Truthiness::Ambiguous, + } + } + /// Returns the field exposed by a literal key. /// /// Undeclared keys synthesize a field only for explicit extra items. Hidden items on an @@ -377,8 +403,12 @@ impl<'db> TypedDictType<'db> { /// The runtime key may name either an extra item or any declared item, so the result is the /// intersection of all possible destination item types. Returns `None` unless extra items are /// explicit. - pub(crate) fn arbitrary_key_initialization_type(self, db: &'db dyn Db) -> Option> { - self.arbitrary_key_initialization_type_excluding(db, &OrderSet::new()) + pub(crate) fn arbitrary_key_initialization_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { + self.arbitrary_key_initialization_type_excluding(db, env, &OrderSet::new()) } /// Returns the arbitrary-key initialization type after excluding keys that are known to be @@ -389,12 +419,14 @@ impl<'db> TypedDictType<'db> { fn arbitrary_key_initialization_type_excluding( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, excluded_keys: &OrderSet, ) -> Option> { let extra_items = self.explicit_extra_items(db)?; Some(IntersectionType::from_elements( db, + env, std::iter::once(extra_items.declared_ty).chain( self.items(db) .iter() @@ -408,7 +440,11 @@ impl<'db> TypedDictType<'db> { /// /// A mutation may target any declared or extra item, so no such mutation is allowed if any /// possible destination is read-only. - pub(crate) fn arbitrary_key_mutation_type(self, db: &'db dyn Db) -> Option> { + pub(crate) fn arbitrary_key_mutation_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { if self .explicit_extra_items(db) .is_some_and(TypedDictExtraItems::is_read_only) @@ -417,7 +453,7 @@ impl<'db> TypedDictType<'db> { return None; } - self.arbitrary_key_initialization_type(db) + self.arbitrary_key_initialization_type(db, env) } /// Returns whether operations that delete an arbitrary key are safe. @@ -442,7 +478,11 @@ impl<'db> TypedDictType<'db> { /// /// This requires mutable explicit extra items and optional, mutable declared items whose value /// types are equivalent to the extra-items type. - pub(crate) fn dict_value_type(self, db: &'db dyn Db) -> Option> { + pub(crate) fn dict_value_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { let extra_items = self.explicit_extra_items(db)?; if extra_items.is_read_only() || self.items(db).values().any(|field| { @@ -450,7 +490,7 @@ impl<'db> TypedDictType<'db> { || field.is_read_only() || !field .declared_ty - .is_equivalent_to(db, extra_items.declared_ty) + .is_equivalent_to(db, env, extra_items.declared_ty) }) { return None; @@ -462,7 +502,11 @@ impl<'db> TypedDictType<'db> { /// /// This uses mutual assignability rather than equivalence so gradual value types can satisfy /// the mutable `dict` contract. - pub(crate) fn assignable_dict_value_type(self, db: &'db dyn Db) -> Option> { + pub(crate) fn assignable_dict_value_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { let extra_items = self.explicit_extra_items(db)?; if extra_items.is_read_only() || self.items(db).values().any(|field| { @@ -470,10 +514,10 @@ impl<'db> TypedDictType<'db> { || field.is_read_only() || !field .declared_ty - .is_assignable_to(db, extra_items.declared_ty) + .is_assignable_to(db, env, extra_items.declared_ty) || !extra_items .declared_ty - .is_assignable_to(db, field.declared_ty) + .is_assignable_to(db, env, field.declared_ty) }) { return None; @@ -524,6 +568,7 @@ impl<'db> TypedDictType<'db> { if let ClassLiteral::DynamicTypedDict(class) = defining_class.class_literal(db) { return class.items(db); } + class_based_items(db, defining_class) } Self::Synthesized(synthesized) => synthesized.items(db), @@ -551,17 +596,22 @@ impl<'db> TypedDictType<'db> { pub(crate) fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { // TODO: Materialization of gradual TypedDicts needs more logic match self { - Self::Class(defining_class) => { - Self::Class(defining_class.apply_type_mapping_impl(db, type_mapping, tcx, visitor)) - } + Self::Class(defining_class) => Self::Class(defining_class.apply_type_mapping_impl( + db, + env, + type_mapping, + tcx, + visitor, + )), Self::Synthesized(synthesized) => Self::Synthesized( - synthesized.apply_type_mapping_impl(db, type_mapping, tcx, visitor), + synthesized.apply_type_mapping_impl(db, env, type_mapping, tcx, visitor), ), } } @@ -683,16 +733,16 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { let mut result = self.always(); for (source_item_name, source_item_field) in source_items { - let target_ty = if let Some(target_item_field) = target_items.get(source_item_name) - { - target_item_field.declared_ty - } else { - match target_openness { - TypedDictOpenness::ImplicitlyOpen => continue, - TypedDictOpenness::Closed => return self.never(), - TypedDictOpenness::Extra(extra_items) => extra_items.declared_ty, - } - }; + let target_ty = + if let Some(target_item_field) = target_items.get(source_item_name.as_str()) { + target_item_field.declared_ty + } else { + match target_openness { + TypedDictOpenness::ImplicitlyOpen => continue, + TypedDictOpenness::Closed => return self.never(), + TypedDictOpenness::Extra(extra_items) => extra_items.declared_ty, + } + }; result.intersect( db, @@ -700,7 +750,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { self.check_type_pair(db, source_item_field.declared_ty, target_ty), ); - if result.is_never_satisfied(db) { + if result.is_trivially_never_satisfied() { return result; } } @@ -724,7 +774,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { target_item_field.declared_ty, ), ); - if result.is_never_satisfied(db) { + if result.is_trivially_never_satisfied() { return result; } } @@ -769,7 +819,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { for (target_item_name, target_item_field) in target_items { let field_constraints = if target_item_field.is_required() { // required target fields - let Some(source_item_field) = source_items.get(target_item_name) else { + let Some(source_item_field) = source_items.get(target_item_name.as_str()) else { // Self is missing a required field. if let Some(context) = self.report_context() { context.push(ErrorContext::TypedDictFieldMissing { @@ -836,7 +886,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // A missing read-only field is checked against the source's effective extra // items. Missing mutable fields below require explicit mutable extra items and // a relation in both directions. - if let Some(source_item_field) = source_items.get(target_item_name) { + if let Some(source_item_field) = source_items.get(target_item_name.as_str()) { self.check_type_pair( db, source_item_field.declared_ty, @@ -854,7 +904,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } } } else { - if let Some(source_item_field) = source_items.get(target_item_name) { + if let Some(source_item_field) = source_items.get(target_item_name.as_str()) { if source_item_field.is_read_only() { // A read-only field can't be assigned to a mutable target. if let Some(context) = self.report_context() { @@ -919,9 +969,11 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } }; result.intersect(db, self.constraints, field_constraints); - if result.is_never_satisfied(db) { + if result.is_trivially_never_satisfied() + || (self.is_context_collection_enabled() && result.is_never_satisfied(db, self.env)) + { if let Some(context) = self.report_context() - && let Some(source_item_field) = source_items.get(target_item_name) + && let Some(source_item_field) = source_items.get(target_item_name.as_str()) { context.push(ErrorContext::TypedDictFieldIncompatible { field_name: target_item_name.clone(), @@ -1297,7 +1349,12 @@ impl<'db> VarianceInferable<'db> for TypedDictType<'db> { /// preference in `Bindings::infer_specialization`, which then adopts the declared pack without /// checking the arguments against it — `a: A[foo=int] = A(bar=1)` starts passing. Until a pack /// pinned by a type context also drives the call's arity, bivariant is the safe answer. - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { + fn variance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { let Some((schema, _packs)) = self.synthesized_shape(db) else { return TypeVarVariance::Bivariant; }; @@ -1307,7 +1364,7 @@ impl<'db> VarianceInferable<'db> for TypedDictType<'db> { field .declared_ty .with_polarity(TypeVarVariance::Invariant) - .variance_of(db, typevar) + .variance_of(db, env, typevar) }) .collect() } @@ -1318,29 +1375,29 @@ pub(crate) fn walk_typed_dict_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( typed_dict: TypedDictType<'db>, visitor: &V, ) { - match typed_dict { - TypedDictType::Class(defining_class) => { - visitor.visit_type(db, defining_class.into()); - // basedpython: a synthesized `{"key": T}` literal is a non-generic class, so its - // schema is not reachable through the class type itself - if let Some((schema, packs)) = typed_dict.synthesized_shape(db) { - for field in schema.values() { - visitor.visit_type(db, field.declared_ty); - } - for pack in packs { - visitor.visit_type(db, *pack); - } + if let TypedDictType::Class(defining_class) = typed_dict { + visitor.visit_type(db, defining_class.into()); + + // basedpython: a synthesized `{"key": T}` literal is a non-generic class, so its + // schema is not reachable through the class type itself + if let Some((_, packs)) = typed_dict.synthesized_shape(db) { + for pack in packs { + visitor.visit_type(db, *pack); } } - TypedDictType::Synthesized(synthesized) => { - for field in synthesized.items(db).values() { - visitor.visit_type(db, field.declared_ty); - } - if let Some(extra_items) = synthesized.openness(db).explicit_extra_items() { - visitor.visit_type(db, extra_items.declared_ty); - } + + if !visitor.should_visit_lazy_type_attributes() { + return; } } + + for field in typed_dict.items(db).values() { + visitor.visit_type(db, field.declared_ty); + } + + if let Some(extra_items) = typed_dict.explicit_extra_items(db) { + visitor.visit_type(db, extra_items.declared_ty); + } } #[salsa::tracked( @@ -1352,7 +1409,10 @@ pub(super) fn deferred_functional_typed_dict_schema<'db>( db: &'db dyn Db, definition: Definition<'db>, ) -> TypedDictSchema<'db> { - let module = parsed_module(db, definition.file(db)).load(db); + let program_file = definition.program_file(db); + let python_file = program_file.python_file(db); + let env = ProgramEnvironment::from_file(program_file); + let module = parsed_module(db, python_file).load(db); let node = definition .kind(db) .value(&module) @@ -1364,7 +1424,7 @@ pub(super) fn deferred_functional_typed_dict_schema<'db>( let total = node.arguments.find_keyword("total").is_none_or(|total_kw| { let total_ty = definition_expression_type(db, definition, &total_kw.value); - !total_ty.bool(db).is_always_false() + !total_ty.bool(db, &env).is_always_false() }); let mut schema = TypedDictSchema::default(); @@ -1414,7 +1474,10 @@ pub(super) fn deferred_functional_typed_dict_openness<'db>( db: &'db dyn Db, definition: Definition<'db>, ) -> TypedDictOpenness<'db> { - let module = parsed_module(db, definition.file(db)).load(db); + let program_file = definition.program_file(db); + let python_file = program_file.python_file(db); + let env = ProgramEnvironment::from_file(program_file); + let module = parsed_module(db, python_file).load(db); let node = definition .kind(db) .value(&module) @@ -1435,7 +1498,7 @@ pub(super) fn deferred_functional_typed_dict_openness<'db>( if let Some(closed) = node.arguments.find_keyword("closed") { let closed_ty = definition_expression_type(db, definition, &closed.value); - if closed_ty.bool(db).is_always_true() { + if closed_ty.bool(db, &env).is_always_true() { return TypedDictOpenness::Closed; } } @@ -1529,6 +1592,8 @@ impl<'db> TypedDictKeyAssignment<'_, 'db, '_> { return false; }; + let env = &self.context.program_environment(); + if self.assignment_kind.is_subscript() && item.is_read_only() { if self.emit_diagnostic && let Some(builder) = self @@ -1536,15 +1601,15 @@ impl<'db> TypedDictKeyAssignment<'_, 'db, '_> { .report_lint(self.assignment_kind.diagnostic_type(), self.key_node) { let typed_dict_ty = Type::TypedDict(self.typed_dict); - let typed_dict_d = typed_dict_ty.display(db); + let typed_dict_d = typed_dict_ty.display(db, env); let mut diagnostic = builder.into_diagnostic(format_args!( "Cannot assign to key \"{}\" on TypedDict `{typed_dict_d}`", self.key, )); - diagnostic.set_primary_message(format_args!("key is marked read-only")); - self.add_object_type_annotation(db, &mut diagnostic); + diagnostic.set_primary_annotation_message(format_args!("key is marked read-only")); + self.add_object_type_annotation(db, env, &mut diagnostic); Self::add_item_definition_subdiagnostic( db, &item, @@ -1557,7 +1622,7 @@ impl<'db> TypedDictKeyAssignment<'_, 'db, '_> { } // Key exists, check if value type is assignable to declared type - if self.value_ty.is_assignable_to(db, item.declared_ty) { + if self.value_ty.is_assignable_to(db, env, item.declared_ty) { return true; } @@ -1572,9 +1637,9 @@ impl<'db> TypedDictKeyAssignment<'_, 'db, '_> { .report_lint(self.assignment_kind.diagnostic_type(), self.value_node) { let typed_dict_ty = Type::TypedDict(self.typed_dict); - let typed_dict_d = typed_dict_ty.display(db); - let value_d = self.value_ty.display(db); - let item_type_d = item.declared_ty.display(db); + let typed_dict_d = typed_dict_ty.display(db, env); + let value_d = self.value_ty.display(db, env); + let item_type_d = item.declared_ty.display(db, env); let mut diagnostic = builder.into_diagnostic(format_args!( "Invalid {} to key \"{}\" with declared type `{item_type_d}` \ @@ -1583,7 +1648,7 @@ impl<'db> TypedDictKeyAssignment<'_, 'db, '_> { self.key, )); - diagnostic.set_primary_message(format_args!("value of type `{value_d}`")); + diagnostic.set_primary_annotation_message(format_args!("value of type `{value_d}`")); diagnostic.annotate( self.context @@ -1597,19 +1662,24 @@ impl<'db> TypedDictKeyAssignment<'_, 'db, '_> { &mut diagnostic, "Item declared here", ); - self.add_object_type_annotation(db, &mut diagnostic); + self.add_object_type_annotation(db, env, &mut diagnostic); } false } - fn add_object_type_annotation(&self, db: &'db dyn Db, diagnostic: &mut Diagnostic) { + fn add_object_type_annotation( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + diagnostic: &mut Diagnostic, + ) { if let Some(full_object_ty) = self.full_object_ty { diagnostic.annotate(self.context.secondary(self.typed_dict_node).message( format_args!( "TypedDict `{}` in {kind} type `{}`", - Type::TypedDict(self.typed_dict).display(db), - full_object_ty.display(db), + Type::TypedDict(self.typed_dict).display(db, env), + full_object_ty.display(db, env), kind = if full_object_ty.is_union() { "union" } else { @@ -1621,7 +1691,7 @@ impl<'db> TypedDictKeyAssignment<'_, 'db, '_> { diagnostic.annotate(self.context.secondary(self.typed_dict_node).message( format_args!( "TypedDict `{}`", - Type::TypedDict(self.typed_dict).display(db) + Type::TypedDict(self.typed_dict).display(db, env) ), )); } @@ -1635,7 +1705,7 @@ impl<'db> TypedDictKeyAssignment<'_, 'db, '_> { ) { if let Some(declaration) = item.first_declaration() { let file = declaration.file(db); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, declaration.python_file(db)).load(db); let mut sub = SubDiagnostic::new(SubDiagnosticSeverity::Info, "Item declaration"); sub.annotate( @@ -1654,7 +1724,7 @@ impl<'db> TypedDictKeyAssignment<'_, 'db, '_> { /// Reports errors for any keys that are required but not provided. /// /// Returns true if the assignment is valid, or false otherwise. -pub(super) fn validate_typed_dict_required_keys<'db, 'ast>( +fn validate_typed_dict_required_keys<'db, 'ast>( context: &InferContext<'db, 'ast>, typed_dict: TypedDictType<'db>, provided_keys: &OrderSet, @@ -1714,6 +1784,7 @@ pub(crate) struct UnpackedTypedDict<'db> { /// writes through the synthesized policy. fn intersect_unpacked_typed_dict_openness<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, openness: impl IntoIterator>, ) -> TypedDictOpenness<'db> { let mut explicit_value_types = Vec::new(); @@ -1733,7 +1804,7 @@ fn intersect_unpacked_typed_dict_openness<'db>( } else { TypedDictOpenness::extra( db, - IntersectionType::from_elements(db, explicit_value_types), + IntersectionType::from_elements(db, env, explicit_value_types), true, ) } @@ -1752,9 +1823,10 @@ fn intersect_unpacked_typed_dict_openness<'db>( /// observes its values. fn union_unpacked_typed_dict_openness<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, openness: impl IntoIterator>, ) -> TypedDictOpenness<'db> { - let mut value_types = UnionBuilder::new(db); + let mut value_types = UnionBuilder::new(db, env); let mut has_implicitly_open = false; let mut has_explicit_extra_items = false; @@ -1791,14 +1863,16 @@ fn union_unpacked_typed_dict_openness<'db>( /// and a key is only considered required if every arm requires it. pub(crate) fn extract_unpacked_typed_dict_keys_from_value_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, ) -> Option>> { - extract_unpacked_typed_dict_from_value_type(db, ty).map(|unpacked| unpacked.keys) + extract_unpacked_typed_dict_from_value_type(db, env, ty).map(|unpacked| unpacked.keys) } /// Extracts the declared keys and openness from a `TypedDict`-shaped value. pub(crate) fn extract_unpacked_typed_dict_from_value_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, ) -> Option> { match ty { @@ -1827,7 +1901,9 @@ pub(crate) fn extract_unpacked_typed_dict_from_value_type<'db>( let unpacked_elements: Vec<_> = intersection .positive(db) .iter() - .filter_map(|element| extract_unpacked_typed_dict_from_value_type(db, *element)) + .filter_map(|element| { + extract_unpacked_typed_dict_from_value_type(db, env, *element) + }) .collect(); if unpacked_elements.is_empty() { @@ -1844,6 +1920,7 @@ pub(crate) fn extract_unpacked_typed_dict_from_value_type<'db>( .and_modify(|existing| { existing.value_ty = IntersectionType::from_two_elements( db, + env, existing.value_ty, unpacked_key.value_ty, ); @@ -1865,6 +1942,7 @@ pub(crate) fn extract_unpacked_typed_dict_from_value_type<'db>( if let Some(extra_items) = unpacked.openness.effective_extra_items() { unpacked_key.value_ty = IntersectionType::from_two_elements( db, + env, unpacked_key.value_ty, extra_items.declared_ty, ); @@ -1875,6 +1953,7 @@ pub(crate) fn extract_unpacked_typed_dict_from_value_type<'db>( let openness = intersect_unpacked_typed_dict_openness( db, + env, unpacked_elements.iter().map(|unpacked| unpacked.openness), ); @@ -1887,7 +1966,7 @@ pub(crate) fn extract_unpacked_typed_dict_from_value_type<'db>( let unpacked_elements: Vec<_> = union .elements(db) .iter() - .map(|element| extract_unpacked_typed_dict_from_value_type(db, *element)) + .map(|element| extract_unpacked_typed_dict_from_value_type(db, env, *element)) .collect::>()?; let all_keys: OrderSet = unpacked_elements @@ -1897,15 +1976,15 @@ pub(crate) fn extract_unpacked_typed_dict_from_value_type<'db>( let mut result = BTreeMap::new(); for key in all_keys { - let mut value_ty = UnionBuilder::new(db); + let mut value_ty = UnionBuilder::new(db, env); let mut is_required = true; let mut definition = None; let mut saw_key = false; for unpacked in &unpacked_elements { - if let Some(unpacked_key) = unpacked.keys.get(&key) { + if let Some(unpacked_key) = unpacked.keys.get(key.as_str()) { saw_key = true; - value_ty = value_ty.add(unpacked_key.value_ty); + value_ty.add_in_place(unpacked_key.value_ty); is_required &= unpacked_key.is_required; definition = Some(if let Some(definition) = definition { merge_unpacked_key_definitions(definition, unpacked_key.definition) @@ -1914,7 +1993,7 @@ pub(crate) fn extract_unpacked_typed_dict_from_value_type<'db>( }); } else if let Some(extra_items) = unpacked.openness.effective_extra_items() { saw_key = true; - value_ty = value_ty.add(extra_items.declared_ty); + value_ty.add_in_place(extra_items.declared_ty); is_required = false; definition = Some(None); } else { @@ -1937,6 +2016,7 @@ pub(crate) fn extract_unpacked_typed_dict_from_value_type<'db>( let openness = union_unpacked_typed_dict_openness( db, + env, unpacked_elements.iter().map(|unpacked| unpacked.openness), ); @@ -1948,19 +2028,19 @@ pub(crate) fn extract_unpacked_typed_dict_from_value_type<'db>( // Unpacking has to be valid whichever materialization this turns out to be, so the // union face is the right shape here. Type::UnsafeUnion(unsafe_union) => { - extract_unpacked_typed_dict_from_value_type(db, unsafe_union.to_union(db)) + extract_unpacked_typed_dict_from_value_type(db, env, unsafe_union.to_union(db, env)) } Type::TypeAlias(alias) => { - extract_unpacked_typed_dict_from_value_type(db, alias.value_type(db)) + extract_unpacked_typed_dict_from_value_type(db, env, alias.value_type(db)) } Type::Overlapping(overlapping) => { - extract_unpacked_typed_dict_from_value_type(db, overlapping.value_type(db)) + extract_unpacked_typed_dict_from_value_type(db, env, overlapping.value_type(db, env)) } Type::Restricted(restricted) => { - extract_unpacked_typed_dict_from_value_type(db, restricted.value_type(db)) + extract_unpacked_typed_dict_from_value_type(db, env, restricted.value_type(db)) } Type::Deferred(deferred) => { - extract_unpacked_typed_dict_from_value_type(db, deferred.reduced(db)) + extract_unpacked_typed_dict_from_value_type(db, env, deferred.reduced(db, env)) } // All other types cannot contain a TypedDict Type::Dynamic(_) @@ -2054,7 +2134,7 @@ pub(super) fn infer_unpacked_keyword_types<'db>( .collect() } -pub(super) fn unpacked_keyword_is_gradual<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { +fn unpacked_keyword_is_gradual<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { match ty.resolve_type_alias(db) { ty if ty.is_never() || ty.is_dynamic() => true, Type::Union(union) => union @@ -2072,6 +2152,7 @@ pub(super) fn unpacked_keyword_is_gradual<'db>(db: &'db dyn Db, ty: Type<'db>) - /// key diagnostics for the positional mapping. pub(super) fn collect_guaranteed_keyword_keys<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typed_dict: TypedDictType<'db>, arguments: &Arguments, unpacked_keyword_types: &[Option>], @@ -2104,6 +2185,7 @@ pub(super) fn collect_guaranteed_keyword_keys<'db>( collect_guaranteed_keys_from_merged_unpacked_keyword( db, + env, typed_dict, &keyword.value, unpacked_type, @@ -2118,6 +2200,7 @@ pub(super) fn collect_guaranteed_keyword_keys<'db>( /// Collects keys guaranteed by one unpacked constructor argument. fn collect_guaranteed_keys_from_merged_unpacked_keyword<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typed_dict: TypedDictType<'db>, expr: &ast::Expr, unpacked_type: Type<'db>, @@ -2135,6 +2218,7 @@ fn collect_guaranteed_keys_from_merged_unpacked_keyword<'db>( let nested_ty = expression_type_fn(&item.value, TypeContext::default()); collect_guaranteed_keys_from_merged_unpacked_keyword( db, + env, typed_dict, &item.value, nested_ty, @@ -2149,7 +2233,7 @@ fn collect_guaranteed_keys_from_merged_unpacked_keyword<'db>( if unpacked_keyword_is_gradual(db, unpacked_type) { provided_keys.extend(typed_dict.items(db).keys().cloned()); } else if let Some(unpacked_keys) = - extract_unpacked_typed_dict_keys_from_value_type(db, unpacked_type) + extract_unpacked_typed_dict_keys_from_value_type(db, env, unpacked_type) { for (key, unpacked_key) in unpacked_keys { if unpacked_key.is_required { @@ -2163,7 +2247,7 @@ fn collect_guaranteed_keys_from_merged_unpacked_keyword<'db>( /// /// This is used for mixed positional-and-keyword constructor calls, where guaranteed keyword /// arguments override any same-named keys from the positional mapping. -pub(super) fn typed_dict_without_keys<'db>( +fn typed_dict_without_keys<'db>( db: &'db dyn Db, typed_dict: TypedDictType<'db>, excluded_keys: &OrderSet, @@ -2297,6 +2381,7 @@ fn validate_extracted_typed_dict_openness<'db, 'ast>( return true; }; let extra_items_ty = extra_items.declared_ty; + let env = context.program_environment(); let target_openness = typed_dict.openness(db); if target_openness.is_implicitly_open() && source_openness.is_implicitly_open() { @@ -2310,41 +2395,39 @@ fn validate_extracted_typed_dict_openness<'db, 'ast>( typed_dict.items(db).iter().find(|(name, field)| { !source_keys.contains_key(*name) && !ignored_keys.contains(*name) - && !extra_items_ty.is_assignable_to(db, field.declared_ty) + && !extra_items_ty.is_assignable_to(db, env, field.declared_ty) }) { if let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, nodes.value) { let mut diagnostic = builder.into_diagnostic(format_args!( "Unpacked argument has extra items of type `{}` that are not assignable to item `{target_name}` with type `{}` on TypedDict `{}`", - extra_items_ty.display(db), - target_field.declared_ty.display(db), - typed_dict_ty.display(db), + extra_items_ty.display(db, env), + target_field.declared_ty.display(db, env), + typed_dict_ty.display(db, env), )); - diagnostic.annotate( - context - .secondary(nodes.typed_dict) - .message(format_args!("TypedDict `{}`", typed_dict_ty.display(db))), - ); + diagnostic.annotate(context.secondary(nodes.typed_dict).message(format_args!( + "TypedDict `{}`", + typed_dict_ty.display(db, env) + ))); } return false; } - if extra_items_ty.is_assignable_to(db, target_extra_items.declared_ty) { + if extra_items_ty.is_assignable_to(db, env, target_extra_items.declared_ty) { return true; } if let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, nodes.value) { let mut diagnostic = builder.into_diagnostic(format_args!( "Unpacked argument has extra items of type `{}` that are not assignable to extra items type `{}` on TypedDict `{}`", - extra_items_ty.display(db), - target_extra_items.declared_ty.display(db), - typed_dict_ty.display(db), + extra_items_ty.display(db, env), + target_extra_items.declared_ty.display(db, env), + typed_dict_ty.display(db, env), )); - diagnostic.annotate( - context - .secondary(nodes.typed_dict) - .message(format_args!("TypedDict `{}`", typed_dict_ty.display(db))), - ); + diagnostic.annotate(context.secondary(nodes.typed_dict).message(format_args!( + "TypedDict `{}`", + typed_dict_ty.display(db, env) + ))); } return false; } @@ -2352,13 +2435,12 @@ fn validate_extracted_typed_dict_openness<'db, 'ast>( if let Some(builder) = context.report_lint(&INVALID_KEY, nodes.key) { let mut diagnostic = builder.into_diagnostic(format_args!( "Unpacked argument may contain unknown keys for TypedDict `{}`", - typed_dict_ty.display(db), + typed_dict_ty.display(db, env), )); - diagnostic.annotate( - context - .secondary(nodes.typed_dict) - .message(format_args!("TypedDict `{}`", typed_dict_ty.display(db))), - ); + diagnostic.annotate(context.secondary(nodes.typed_dict).message(format_args!( + "TypedDict `{}`", + typed_dict_ty.display(db, env) + ))); } false } @@ -2379,8 +2461,9 @@ fn validate_from_typed_dict_argument<'db, 'ast>( ignored_keys: &OrderSet, ) -> Option> { let db = context.db(); + let env = context.program_environment(); let typed_dict_items = typed_dict.items(db); - let unpacked = extract_unpacked_typed_dict_from_value_type(db, arg_ty)?; + let unpacked = extract_unpacked_typed_dict_from_value_type(db, env, arg_ty)?; let source_openness = unpacked.openness; let validate_extra_keys = !typed_dict.openness(db).is_implicitly_open(); let unpacked_keys = unpacked @@ -2422,11 +2505,13 @@ fn report_duplicate_typed_dict_constructor_key<'db, 'ast>( duplicate_node: AnyNodeRef<'ast>, original_node: AnyNodeRef<'ast>, ) { + let db = context.db(); let Some(builder) = context.report_lint(&PARAMETER_ALREADY_ASSIGNED, duplicate_node) else { return; }; - let typed_dict_display = Type::TypedDict(typed_dict).display(context.db()); + let env = context.program_environment(); + let typed_dict_display = Type::TypedDict(typed_dict).display(db, env); let mut diagnostic = builder.into_diagnostic(format_args!( "Multiple values provided for key \"{key}\" in TypedDict `{typed_dict_display}` constructor", )); @@ -2480,6 +2565,7 @@ pub(super) fn validate_typed_dict_constructor<'db, 'ast>( ) { let db = context.db(); let typed_dict_ty = Type::TypedDict(typed_dict); + let env = context.program_environment(); if arguments.args.len() > 1 { if let Some(builder) = @@ -2487,7 +2573,7 @@ pub(super) fn validate_typed_dict_constructor<'db, 'ast>( { builder.into_diagnostic(format_args!( "Too many positional arguments to TypedDict `{}` constructor: expected 1, got {}", - typed_dict_ty.display(db), + typed_dict_ty.display(db, env), arguments.args.len(), )); } @@ -2544,14 +2630,14 @@ pub(super) fn validate_typed_dict_constructor<'db, 'ast>( ) { provided_keys } else { - if !positional_target_is_unconstrained - && !arg_ty.is_assignable_to(db, positional_target_ty) - { - if let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, arg) { + if !positional_target_is_unconstrained { + if !arg_ty.is_assignable_to(db, env, positional_target_ty) + && let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, arg) + { builder.into_diagnostic(format_args!( "Argument of type `{}` is not assignable to `{}`", - arg_ty.display(db), - positional_target_ty.display(db), + arg_ty.display(db, env), + positional_target_ty.display(db, env), )); } } @@ -2586,12 +2672,12 @@ pub(super) fn validate_typed_dict_constructor<'db, 'ast>( let arg = &arguments.args[0]; let arg_ty = expression_type_fn(arg, TypeContext::new(Some(typed_dict_ty))); - if !arg_ty.is_assignable_to(db, typed_dict_ty) { + if !arg_ty.is_assignable_to(db, env, typed_dict_ty) { if let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, arg) { builder.into_diagnostic(format_args!( "Argument of type `{}` is not assignable to `{}`", - arg_ty.display(db), - typed_dict_ty.display(db), + arg_ty.display(db, env), + typed_dict_ty.display(db, env), )); } } @@ -2749,27 +2835,28 @@ fn validate_merged_dict_literal<'db, 'ast>( ) -> bool { let db = context.db(); let mut valid = true; + let env = &context.program_environment(); for item in dict_expr.items.iter().rev() { if let Some(key_expr) = &item.key { let key_ty = expression_type_fn(key_expr, TypeContext::default()); let Some(key_literal) = key_ty.as_string_literal() else { - if key_ty.is_assignable_to(db, KnownClass::Str.to_instance(db)) { - if let Some(expected_ty) = - typed_dict.arbitrary_key_initialization_type_excluding(db, shadowed_keys) + if key_ty.is_assignable_to(db, env, KnownClass::Str.to_instance(db, env)) { + if let Some(expected_ty) = typed_dict + .arbitrary_key_initialization_type_excluding(db, env, shadowed_keys) { let value_ty = expression_type_fn(&item.value, TypeContext::new(Some(expected_ty))); - if !value_ty.is_assignable_to(db, expected_ty) { + if !value_ty.is_assignable_to(db, env, expected_ty) { valid = false; if let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, &item.value) { builder.into_diagnostic(format_args!( "Value of type `{}` is not assignable to arbitrary key value type `{}` on TypedDict `{}`", - value_ty.display(db), - expected_ty.display(db), - Type::TypedDict(typed_dict).display(db), + value_ty.display(db, env), + expected_ty.display(db, env), + Type::TypedDict(typed_dict).display(db, env), )); } } @@ -2778,7 +2865,7 @@ fn validate_merged_dict_literal<'db, 'ast>( if let Some(builder) = context.report_lint(&INVALID_KEY, key_expr) { builder.into_diagnostic(format_args!( "Non-literal string key may be unknown for TypedDict `{}`", - Type::TypedDict(typed_dict).display(db), + Type::TypedDict(typed_dict).display(db, env), )); } } @@ -2787,8 +2874,8 @@ fn validate_merged_dict_literal<'db, 'ast>( if let Some(builder) = context.report_lint(&INVALID_KEY, key_expr) { builder.into_diagnostic(format_args!( "TypedDict `{}` requires string keys, got key of type `{}`", - Type::TypedDict(typed_dict).display(db), - key_ty.display(db), + Type::TypedDict(typed_dict).display(db, env), + key_ty.display(db, env), )); } } @@ -2858,6 +2945,7 @@ fn validate_merged_unpacked_keyword_argument<'db, 'ast>( expression_type_fn: &mut impl FnMut(&ast::Expr, TypeContext<'db>) -> Type<'db>, ) -> bool { let db = context.db(); + let env = context.program_environment(); let items = typed_dict.items(db); if let ast::Expr::Dict(dict_expr) = expr { @@ -2880,7 +2968,9 @@ fn validate_merged_unpacked_keyword_argument<'db, 'ast>( guaranteed_keys.entry(key_name.clone()).or_insert(None); } return true; - } else if let Some(unpacked) = extract_unpacked_typed_dict_from_value_type(db, unpacked_type) { + } + + if let Some(unpacked) = extract_unpacked_typed_dict_from_value_type(db, env, unpacked_type) { let ignored_keys = shadowed_keys.clone(); let (_, mut unpacked_valid) = validate_extracted_typed_dict_keys( context, @@ -2914,12 +3004,14 @@ fn validate_merged_unpacked_keyword_argument<'db, 'ast>( } return unpacked_valid; - } else if let Some((key_ty, value_ty)) = unpacked_type.unpack_keys_and_items(db) { - if !key_ty.is_assignable_to(db, KnownClass::Str.to_instance(db)) { + } + + if let Some((key_ty, value_ty)) = unpacked_type.unpack_keys_and_items(db, env) { + if !key_ty.is_assignable_to(db, env, KnownClass::Str.to_instance(db, env)) { if let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, nodes.value) { builder.into_diagnostic(format_args!( "Unpacked argument has key type `{}` that is not assignable to `str`", - key_ty.display(db), + key_ty.display(db, env), )); } return false; @@ -3013,28 +3105,30 @@ impl<'db> SynthesizedTypedDictType<'db> { self.kind(db) == SynthesizedTypedDictKind::Patch } - pub(super) fn apply_type_mapping_impl<'a>( + fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { let items = self .items(db) .iter() .map(|(name, field)| { - let field = field - .clone() - .apply_type_mapping_impl(db, type_mapping, tcx, visitor); + let field = + field + .clone() + .apply_type_mapping_impl(db, env, type_mapping, tcx, visitor); (name.clone(), field) }) .collect::>(); - let openness = self - .openness(db) - .apply_type_mapping_impl(db, type_mapping, tcx, visitor); + let openness = + self.openness(db) + .apply_type_mapping_impl(db, env, type_mapping, tcx, visitor); match self.kind(db) { SynthesizedTypedDictKind::Schema => Self::schema(db, items, openness), @@ -3050,6 +3144,7 @@ impl<'db> TypedDictSchema<'db> { pub(super) fn recursive_type_normalized_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -3057,7 +3152,7 @@ impl<'db> TypedDictSchema<'db> { .map(|(name, field)| { let declared_ty = field .declared_ty - .recursive_type_normalized_impl(db, div, true); + .recursive_type_normalized_impl(db, env, div, true); let declared_ty = if nested { declared_ty? } else { @@ -3137,14 +3232,19 @@ impl<'db> TypedDictField<'db> { pub(crate) fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { Self { - declared_ty: self - .declared_ty - .apply_type_mapping_impl(db, type_mapping, tcx, visitor), + declared_ty: self.declared_ty.apply_type_mapping_impl( + db, + env, + type_mapping, + tcx, + visitor, + ), flags: self.flags, first_declaration: self.first_declaration, } @@ -3181,7 +3281,7 @@ impl<'db> TypedDictFieldBuilder<'db> { self } - pub(crate) fn first_declaration(mut self, definition: Option>) -> Self { + fn first_declaration(mut self, definition: Option>) -> Self { self.first_declaration = definition; self } diff --git a/crates/ty_python_semantic/src/types/typevar.rs b/crates/ty_python_semantic/src/types/typevar.rs index 73d9be74b3..96c128a8ad 100644 --- a/crates/ty_python_semantic/src/types/typevar.rs +++ b/crates/ty_python_semantic/src/types/typevar.rs @@ -1,6 +1,8 @@ +use crate::ProgramEnvironment; use std::cell::{Cell, RefCell}; use std::rc::Rc; +use itertools::{Either, Itertools}; use ruff_db::parsed::parsed_module; use ruff_python_ast::name::Name; use ruff_python_ast::{self as ast, PySourceType}; @@ -8,19 +10,19 @@ use rustc_hash::FxHashSet; use smallvec::SmallVec; use crate::{ - Db, TypeQualifiers, + Db, FxOrderMap, TypeQualifiers, place::{ DefinedPlace, Definedness, Place, PlaceAndQualifiers, Provenance, PublicTypePolicy, TypeOrigin, }, types::{ - ApplySpecialization, ApplyTypeMappingVisitor, CycleDetector, DynamicType, GenericContext, - InstanceProjection, KnownClass, KnownInstanceType, LintDiagnosticGuard, - MaterializationKind, Parameter, Parameters, Type, TypeAliasType, TypeContext, TypeMapping, - TypeVarVariance, UnionBuilder, UnionType, any_over_type, binding_type, + ApplySpecialization, ApplyTypeMappingVisitor, ClassLiteral, CycleDetector, DynamicType, + GenericContext, InstanceProjection, IntersectionType, KnownClass, KnownInstanceType, + LintDiagnosticGuard, MaterializationKind, Parameter, Parameters, Type, TypeAliasType, + TypeContext, TypeMapping, TypeVarVariance, UnionBuilder, UnionType, any_over_type, + binding_type, constraints::ConstraintSetBuilder, definition_expression_type, - generics::InferableTypeVars, tuple::Tuple, variance::VarianceInferable, visitor::{ @@ -30,6 +32,7 @@ use crate::{ }, }; use ty_python_core::{ + Program, definition::{Definition, DefinitionKind}, semantic_index, }; @@ -56,16 +59,17 @@ impl<'db> Type<'db> { ) } - pub(crate) fn has_typevar(self, db: &'db dyn Db) -> bool { - any_over_type(db, self, false, |ty| matches!(ty, Type::TypeVar(_))) + pub(crate) fn has_typevar(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool { + any_over_type(db, env, self, false, |ty| matches!(ty, Type::TypeVar(_))) } pub(crate) fn references_typevar( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typevar_id: TypeVarIdentity<'db>, ) -> bool { - any_over_type(db, self, false, |ty| match ty { + any_over_type(db, env, self, false, |ty| match ty { Type::TypeVar(bound_typevar) => typevar_id == bound_typevar.typevar(db).identity(db), Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => { typevar_id == typevar.identity(db) @@ -74,17 +78,26 @@ impl<'db> Type<'db> { }) } - pub(crate) fn has_non_self_typevar(self, db: &'db dyn Db) -> bool { + pub(crate) fn has_non_self_typevar( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> bool { any_over_type( db, + env, self, false, |ty| matches!(ty, Type::TypeVar(tv) if !tv.typevar(db).is_self(db)), ) } - pub(crate) fn has_typevar_or_typevar_instance(self, db: &'db dyn Db) -> bool { - any_over_type(db, self, false, |ty| { + pub(crate) fn has_typevar_or_typevar_instance( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> bool { + any_over_type(db, env, self, false, |ty| { matches!( ty, Type::KnownInstance(KnownInstanceType::TypeVar(_)) | Type::TypeVar(_) @@ -96,8 +109,12 @@ impl<'db> Type<'db> { /// /// `Self` is bound by the enclosing class rather than by the generic context currently being /// defined, so a type mentioning only `Self` leaves nothing unsolved in that context. - pub(crate) fn has_non_self_typevar_or_typevar_instance(self, db: &'db dyn Db) -> bool { - any_over_type_with_opaque_self(db, self, |ty| match ty { + pub(crate) fn has_non_self_typevar_or_typevar_instance( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> bool { + any_over_type_with_opaque_self(db, env, self, |ty| match ty { Type::TypeVar(bound_typevar) => !bound_typevar.typevar(db).is_self(db), Type::KnownInstance(KnownInstanceType::TypeVar(_)) => true, _ => false, @@ -108,11 +125,15 @@ impl<'db> Type<'db> { /// /// Such a type variable is a stand-in for an as-yet-unknown `TypedDict`, so a construct that /// requires one (`**kwargs: Unpack[T]`) can accept it and defer until it is solved. - pub(crate) fn is_typed_dict_bounded_typevar(self, db: &'db dyn Db) -> bool { + pub(crate) fn is_typed_dict_bounded_typevar( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> bool { let Type::TypeVar(bound_typevar) = self else { return false; }; - match bound_typevar.typevar(db).bound_or_constraints(db) { + match bound_typevar.typevar(db).bound_or_constraints(db, env) { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { bound.resolve_type_alias(db).is_typed_dict() } @@ -124,8 +145,12 @@ impl<'db> Type<'db> { } } - pub(crate) fn has_unspecialized_type_var(self, db: &'db dyn Db) -> bool { - any_over_type(db, self, false, |ty| { + pub(crate) fn has_unspecialized_type_var( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> bool { + any_over_type(db, env, self, false, |ty| { matches!(ty, Type::Dynamic(DynamicType::UnspecializedTypeVar)) }) } @@ -200,10 +225,12 @@ pub(super) fn walk_type_var_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( visitor: &V, ) { if let Some(bound_or_constraints) = if visitor.should_visit_lazy_type_attributes() { - typevar.bound_or_constraints(db) + typevar.bound_or_constraints(db, visitor.program_environment()) } else { match typevar._bound_or_constraints(db) { - _ if visitor.should_visit_lazy_type_attributes() => typevar.bound_or_constraints(db), + _ if visitor.should_visit_lazy_type_attributes() => { + typevar.bound_or_constraints(db, visitor.program_environment()) + } Some(TypeVarBoundOrConstraintsEvaluation::Eager(bound_or_constraints)) => { Some(bound_or_constraints) } @@ -223,7 +250,7 @@ pub(super) fn walk_type_var_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( visitor.visit_type(db, lower_bound); } if let Some(default_type) = if visitor.should_visit_lazy_type_attributes() { - typevar.default_type(db) + typevar.default_type(db, visitor.program_environment()) } else { match typevar._default(db) { Some(TypeVarDefaultEvaluation::Eager(default_type)) => Some(default_type), @@ -315,7 +342,11 @@ impl<'db> TypeVarInstance<'db> { /// /// [`bound_or_constraints`](Self::bound_or_constraints) therefore hides it, and this is the /// only way to reach it. - pub(crate) fn pack_bound(self, db: &'db dyn Db) -> Option> { + pub(crate) fn pack_bound( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { if !self.is_pack(db) { return None; } @@ -323,7 +354,7 @@ impl<'db> TypeVarInstance<'db> { TypeVarBoundOrConstraintsEvaluation::Eager(TypeVarBoundOrConstraints::UpperBound( bound, )) => Some(bound), - TypeVarBoundOrConstraintsEvaluation::LazyUpperBound => self.lazy_bound(db), + TypeVarBoundOrConstraintsEvaluation::LazyUpperBound => self.lazy_bound(db, env), TypeVarBoundOrConstraintsEvaluation::Eager(TypeVarBoundOrConstraints::Constraints( _, )) @@ -331,8 +362,8 @@ impl<'db> TypeVarInstance<'db> { } } - pub(crate) fn has_pack_bound(self, db: &'db dyn Db) -> bool { - self.pack_bound(db).is_some() + pub(crate) fn has_pack_bound(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool { + self.pack_bound(db, env).is_some() } /// Whether this type variable stands for a run of types (`*Ts`) or a field mapping @@ -341,16 +372,39 @@ impl<'db> TypeVarInstance<'db> { self.is_typevartuple(db) || self.is_keyword_variadic(db) } - pub(crate) fn upper_bound(self, db: &'db dyn Db) -> Option> { - if let Some(TypeVarBoundOrConstraints::UpperBound(ty)) = self.bound_or_constraints(db) { + pub(crate) fn upper_bound( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { + if let Some(TypeVarBoundOrConstraints::UpperBound(ty)) = self.bound_or_constraints(db, env) + { 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) { + /// Returns whether this type variable has constraints without evaluating a lazy bound. + pub(super) fn is_constrained(self, db: &'db dyn Db) -> bool { + matches!( + self._bound_or_constraints(db), + Some( + TypeVarBoundOrConstraintsEvaluation::Eager(TypeVarBoundOrConstraints::Constraints( + _ + )) | TypeVarBoundOrConstraintsEvaluation::LazyConstraints + ) + ) + } + + pub(crate) fn constraints( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option<&'db [Type<'db>]> { + if let Some(TypeVarBoundOrConstraints::Constraints(tuple)) = + self.bound_or_constraints(db, env) + { Some(tuple.elements(db)) } else { None @@ -360,6 +414,7 @@ impl<'db> TypeVarInstance<'db> { pub(crate) fn bound_or_constraints( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ) -> Option> { // basedpython: a variadic pack's bound is never an upper bound on the pack's own value, // so it is kept out of the type lattice entirely — reach it through @@ -372,10 +427,10 @@ impl<'db> TypeVarInstance<'db> { Some(bound_or_constraints) } TypeVarBoundOrConstraintsEvaluation::LazyUpperBound => self - .lazy_bound(db) + .lazy_bound(db, env) .map(TypeVarBoundOrConstraints::UpperBound), TypeVarBoundOrConstraintsEvaluation::LazyConstraints => self - .lazy_constraints(db) + .lazy_constraints(db, env) .map(TypeVarBoundOrConstraints::Constraints), }) } @@ -396,8 +451,9 @@ impl<'db> TypeVarInstance<'db> { heap_size=ruff_memory_usage::heap_size )] fn lazy_lower_bound(self, db: &'db dyn Db) -> Option> { + let env = &ProgramEnvironment::from_definition(self.definition(db)?); let definition = self.definition(db)?; - let module = parsed_module(db, definition.file(db)).load(db); + let module = parsed_module(db, definition.program_file(db).python_file(db)).load(db); let DefinitionKind::TypeVar(typevar) = definition.kind(db) else { return None; }; @@ -405,7 +461,7 @@ impl<'db> TypeVarInstance<'db> { definition_expression_type(db, definition, typevar.node(&module).lower_bound.as_ref()?); // a generic lower bound is reported as an error and dropped, mirroring the upper bound - if lower.has_non_self_typevar_or_typevar_instance(db) { + if lower.has_non_self_typevar_or_typevar_instance(db, env) { return None; } @@ -417,25 +473,31 @@ impl<'db> TypeVarInstance<'db> { pub(crate) fn require_bound_or_constraints( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ) -> TypeVarBoundOrConstraints<'db> { - self.bound_or_constraints(db) + self.bound_or_constraints(db, env) .unwrap_or_else(|| TypeVarBoundOrConstraints::UpperBound(Type::object())) } - pub(crate) fn default_type(self, db: &'db dyn Db) -> Option> { + pub(crate) fn default_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { let visitor = TypeVarDefaultVisitor::new(None); - self.default_type_impl(db, &visitor) + self.default_type_impl(db, env, &visitor) } fn default_type_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, visitor: &TypeVarDefaultVisitor<'db>, ) -> Option> { visitor.visit(db, self, || { self._default(db).and_then(|default| match default { TypeVarDefaultEvaluation::Eager(ty) => Some(ty), - TypeVarDefaultEvaluation::Lazy => self.lazy_default_impl(db, visitor), + TypeVarDefaultEvaluation::Lazy => self.lazy_default_impl(db, env, visitor), }) }) } @@ -443,8 +505,9 @@ impl<'db> TypeVarInstance<'db> { fn materialize_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, materialization_kind: MaterializationKind, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { Self::new( db, @@ -453,56 +516,64 @@ impl<'db> TypeVarInstance<'db> { .and_then(|bound_or_constraints| match bound_or_constraints { TypeVarBoundOrConstraintsEvaluation::Eager(bound_or_constraints) => Some( bound_or_constraints - .materialize_impl(db, materialization_kind, visitor) + .materialize_impl(db, env, materialization_kind, visitor) .into(), ), TypeVarBoundOrConstraintsEvaluation::LazyUpperBound => { - self.lazy_bound(db).map(|bound| { + self.lazy_bound(db, visitor.env).map(|bound| { TypeVarBoundOrConstraints::UpperBound(bound) - .materialize_impl(db, materialization_kind, visitor) + .materialize_impl(db, env, materialization_kind, visitor) .into() }) } TypeVarBoundOrConstraintsEvaluation::LazyConstraints => { - self.lazy_constraints(db).map(|constraints| { + self.lazy_constraints(db, visitor.env).map(|constraints| { TypeVarBoundOrConstraints::Constraints(constraints) - .materialize_impl(db, materialization_kind, visitor) + .materialize_impl(db, env, materialization_kind, visitor) .into() }) } }), self._lower_bound(db) .and_then(|lower_bound| match lower_bound { - TypeVarLowerBoundEvaluation::Eager(ty) => { - Some(ty.materialize(db, materialization_kind, visitor).into()) - } - TypeVarLowerBoundEvaluation::Lazy => self - .lazy_lower_bound(db) - .map(|ty| ty.materialize(db, materialization_kind, visitor).into()), + TypeVarLowerBoundEvaluation::Eager(ty) => Some( + ty.materialize(db, env, materialization_kind, visitor) + .into(), + ), + TypeVarLowerBoundEvaluation::Lazy => self.lazy_lower_bound(db).map(|ty| { + ty.materialize(db, env, materialization_kind, visitor) + .into() + }), }), self.explicit_variance(db), self._default(db).and_then(|default| match default { - TypeVarDefaultEvaluation::Eager(ty) => { - Some(ty.materialize(db, materialization_kind, visitor).into()) - } - TypeVarDefaultEvaluation::Lazy => self - .lazy_default(db) - .map(|ty| ty.materialize(db, materialization_kind, visitor).into()), + TypeVarDefaultEvaluation::Eager(ty) => Some( + ty.materialize(db, env, materialization_kind, visitor) + .into(), + ), + TypeVarDefaultEvaluation::Lazy => self.lazy_default(db, visitor.env).map(|ty| { + ty.materialize(db, env, materialization_kind, visitor) + .into() + }), }), ) } - fn to_instance(self, db: &'db dyn Db) -> Option> { - let bound_or_constraints = match self.bound_or_constraints(db)? { + fn to_instance( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { + let bound_or_constraints = match self.bound_or_constraints(db, env)? { TypeVarBoundOrConstraints::UpperBound(upper_bound) => upper_bound - .to_instance(db)? + .to_instance(db, env)? .map(TypeVarBoundOrConstraints::UpperBound), TypeVarBoundOrConstraints::Constraints(constraints) => constraints - .to_instance(db)? + .to_instance(db, env)? .map(TypeVarBoundOrConstraints::Constraints), }; let lower_bound = match self.lower_bound(db) { - Some(lower_bound) => Some(lower_bound.to_instance(db)?), + Some(lower_bound) => Some(lower_bound.to_instance(db, env)?), None => None, }; let identity = TypeVarIdentity::new( @@ -532,6 +603,7 @@ impl<'db> TypeVarInstance<'db> { fn type_is_self_referential( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, visitor: &TypeVarDefaultVisitor<'db>, ) -> bool { @@ -540,6 +612,7 @@ impl<'db> TypeVarInstance<'db> { #[derive(Copy, Clone)] struct State<'db, 'a> { db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, visitor: &'a TypeVarDefaultVisitor<'db>, seen_typevars: &'a RefCell>>, seen_type_aliases: &'a RefCell>, @@ -547,10 +620,13 @@ impl<'db> TypeVarInstance<'db> { fn typevar_default_is_self_referential<'db>( state: State<'db, '_>, + env: &ProgramEnvironment<'db>, typevar: TypeVarInstance<'db>, self_identity: TypeVarIdentity<'db>, ) -> bool { - if typevar.identity(state.db) == self_identity { + let db = state.db; + + if typevar.identity(db) == self_identity { return true; } @@ -559,20 +635,22 @@ impl<'db> TypeVarInstance<'db> { } typevar - .default_type_impl(state.db, state.visitor) + .default_type_impl(db, state.env, state.visitor) .is_some_and(|default_ty| { - type_is_self_referential_impl(state, default_ty, self_identity) + type_is_self_referential_impl(state, env, default_ty, self_identity) }) } fn type_alias_is_self_referential<'db>( state: State<'db, '_>, + env: &ProgramEnvironment<'db>, type_alias: TypeAliasType<'db>, self_identity: TypeVarIdentity<'db>, ) -> bool { + let db = state.db; { let mut seen_type_aliases = state.seen_type_aliases.borrow_mut(); - let definition = type_alias.definition(state.db); + let definition = type_alias.definition(db); // A recursive alias can produce a new specialization every time its body is // expanded, so use its definition as the stable recursion key. if seen_type_aliases.contains(&definition) { @@ -581,54 +659,57 @@ impl<'db> TypeVarInstance<'db> { seen_type_aliases.push(definition); } - let value_type = if let Some(specialization) = type_alias.specialization(state.db) { + let value_type = if let Some(specialization) = type_alias.specialization(db) { if specialization - .types(state.db) + .types(db) .iter() - .any(|ty| type_is_self_referential_impl(state, *ty, self_identity)) + .any(|ty| type_is_self_referential_impl(state, env, *ty, self_identity)) { return true; } - type_alias.value_type(state.db) - } else if let Some(generic_context) = type_alias.generic_context(state.db) - && generic_context.variables(state.db).any(|typevar| { + type_alias.value_type(db) + } else if let Some(generic_context) = type_alias.generic_context(db) + && generic_context.variables(db).any(|typevar| { typevar_default_is_self_referential( state, - typevar.typevar(state.db), + env, + typevar.typevar(db), self_identity, ) }) { return true; } else { - type_alias.raw_value_type(state.db) + type_alias.raw_value_type(db) }; - type_is_self_referential_impl(state, value_type, self_identity) + type_is_self_referential_impl(state, env, value_type, self_identity) } fn type_is_self_referential_impl<'db>( state: State<'db, '_>, + env: &ProgramEnvironment<'db>, ty: Type<'db>, self_identity: TypeVarIdentity<'db>, ) -> bool { // `Self` is opaque here: its upper bound names the enclosing class's own type // parameters, so descending into it would make `class C[T = Self]` look like a // typevar whose default refers back to itself. - any_over_type_with_opaque_self(state.db, ty, |inner_ty| match inner_ty { + any_over_type_with_opaque_self(state.db, env, ty, |inner_ty| match inner_ty { Type::TypeVar(bound_typevar) => typevar_default_is_self_referential( state, + env, bound_typevar.typevar(state.db), self_identity, ), Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => { - typevar_default_is_self_referential(state, typevar, self_identity) + typevar_default_is_self_referential(state, env, typevar, self_identity) } Type::TypeAlias(alias) => { - type_alias_is_self_referential(state, alias, self_identity) + type_alias_is_self_referential(state, env, alias, self_identity) } Type::KnownInstance(KnownInstanceType::TypeAliasType(alias)) => { - type_alias_is_self_referential(state, alias, self_identity) + type_alias_is_self_referential(state, env, alias, self_identity) } _ => false, }) @@ -639,12 +720,13 @@ impl<'db> TypeVarInstance<'db> { let state = State { db, + env, visitor, seen_typevars: &seen_typevars, seen_type_aliases: &seen_type_aliases, }; - type_is_self_referential_impl(state, ty, self.identity(db)) + type_is_self_referential_impl(state, env, ty, self.identity(db)) } /// Returns the "unchecked" upper bound of a type variable instance. @@ -657,7 +739,9 @@ impl<'db> TypeVarInstance<'db> { )] fn lazy_bound_unchecked(self, db: &'db dyn Db) -> Option> { let definition = self.definition(db)?; - let module = parsed_module(db, definition.file(db)).load(db); + let program_file = definition.program_file(db); + let python_file = program_file.python_file(db); + let module = parsed_module(db, python_file).load(db); let ty = match definition.kind(db) { // PEP 695 typevar DefinitionKind::TypeVar(typevar) => { @@ -703,7 +787,7 @@ impl<'db> TypeVarInstance<'db> { let Some(definition) = self.definition(db) else { return false; }; - let module = parsed_module(db, definition.file(db)).load(db); + let module = parsed_module(db, definition.program_file(db).python_file(db)).load(db); let bound = match definition.kind(db) { DefinitionKind::TypeVarTuple(typevartuple) => &typevartuple.node(&module).bound, DefinitionKind::ParamSpec(paramspec) => ¶mspec.node(&module).bound, @@ -728,19 +812,19 @@ impl<'db> TypeVarInstance<'db> { let Some(definition) = self.definition(db) else { return false; }; - let module = parsed_module(db, definition.file(db)).load(db); + let module = parsed_module(db, definition.program_file(db).python_file(db)).load(db); match definition.kind(db) { DefinitionKind::TypeVar(typevar) => typevar.node(&module).is_some_hole, _ => false, } } - fn lazy_bound(self, db: &'db dyn Db) -> Option> { + fn lazy_bound(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Option> { let bound = self.lazy_bound_unchecked(db)?; // a generic bound is reported as an error and dropped, but `Self` is a legitimate bound: // it is bound by the enclosing class, and is substituted when the method binds its receiver - if bound.has_non_self_typevar_or_typevar_instance(db) { + if bound.has_non_self_typevar_or_typevar_instance(db, env) { return None; } @@ -757,14 +841,17 @@ impl<'db> TypeVarInstance<'db> { )] fn lazy_constraints_unchecked(self, db: &'db dyn Db) -> Option> { let definition = self.definition(db)?; - let module = parsed_module(db, definition.file(db)).load(db); + let program_file = definition.program_file(db); + let python_file = program_file.python_file(db); + let env = ProgramEnvironment::from_file(program_file); + let module = parsed_module(db, python_file).load(db); let constraints = match definition.kind(db) { // PEP 695 typevar DefinitionKind::TypeVar(typevar) => { let typevar_node = typevar.node(&module); let bound = definition_expression_type(db, definition, typevar_node.bound.as_ref()?); - if let Some(tuple) = bound.tuple_instance_spec(db) + if let Some(tuple) = bound.tuple_instance_spec(db, &env) && let Tuple::Fixed(tuple) = tuple.into_owned() { TypeVarConstraints::new(db, tuple.owned_elements()) @@ -792,13 +879,17 @@ impl<'db> TypeVarInstance<'db> { Some(constraints) } - fn lazy_constraints(self, db: &'db dyn Db) -> Option> { + fn lazy_constraints( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { let constraints = self.lazy_constraints_unchecked(db)?; if constraints .elements(db) .iter() - .any(|ty| ty.has_typevar_or_typevar_instance(db)) + .any(|ty| ty.has_typevar_or_typevar_instance(db, env)) { return None; } @@ -855,7 +946,9 @@ impl<'db> TypeVarInstance<'db> { } let definition = self.definition(db)?; - let module = parsed_module(db, definition.file(db)).load(db); + let program_file = definition.program_file(db); + let python_file = program_file.python_file(db); + let module = parsed_module(db, python_file).load(db); let ty = match definition.kind(db) { // PEP 695 typevar DefinitionKind::TypeVar(typevar) => { @@ -901,14 +994,15 @@ impl<'db> TypeVarInstance<'db> { Some(ty) } - fn lazy_default(self, db: &'db dyn Db) -> Option> { + fn lazy_default(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Option> { let visitor = TypeVarDefaultVisitor::new(None); - self.lazy_default_impl(db, &visitor) + self.lazy_default_impl(db, env, &visitor) } fn lazy_default_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, visitor: &TypeVarDefaultVisitor<'db>, ) -> Option> { let default = self.lazy_default_unchecked(db)?; @@ -917,7 +1011,7 @@ impl<'db> TypeVarInstance<'db> { // (https://typing.python.org/en/latest/spec/generics.html#defaults-for-type-parameters). // Here we simply check for non-self-referential. // TODO: We should also check for non-forward references. - if self.type_is_self_referential(db, default, visitor) { + if self.type_is_self_referential(db, env, default, visitor) { return None; } @@ -932,7 +1026,7 @@ impl<'db> TypeVarInstance<'db> { return None; } let typevar_definition = self.definition(db)?; - let index = semantic_index(db, typevar_definition.file(db)); + let index = semantic_index(db, typevar_definition.program_file(db)); let (_, child) = index .child_scopes(typevar_definition.file_scope(db)) .next()?; @@ -966,7 +1060,7 @@ impl TypeVarNonce { ) } - pub(crate) fn add(self, delta: u32) -> Self { + fn add(self, delta: u32) -> Self { Self( self.0 .checked_add(delta) @@ -1044,14 +1138,19 @@ pub(crate) fn max_typevar_freshness_matching_generic_context<'db>( types: impl IntoIterator>, generic_context: GenericContext<'db>, ) -> Option { - struct MatchingFreshnessCollector<'db> { + struct MatchingFreshnessCollector<'a, 'db> { + env: &'a ProgramEnvironment<'db>, base_identities: FxHashSet>, recursion_guard: TypeCollector<'db>, max_freshness: Cell>, } - impl<'db> MatchingFreshnessCollector<'db> { - fn new(db: &'db dyn Db, generic_context: GenericContext<'db>) -> Self { + impl<'a, 'db> MatchingFreshnessCollector<'a, 'db> { + fn new( + db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, + generic_context: GenericContext<'db>, + ) -> Self { let base_identities = generic_context .variables(db) .map(|typevar| { @@ -1061,6 +1160,7 @@ pub(crate) fn max_typevar_freshness_matching_generic_context<'db>( }) .collect(); Self { + env, base_identities, recursion_guard: TypeCollector::default(), max_freshness: Cell::default(), @@ -1068,7 +1168,11 @@ pub(crate) fn max_typevar_freshness_matching_generic_context<'db>( } } - impl<'db> TypeVisitor<'db> for MatchingFreshnessCollector<'db> { + impl<'db> TypeVisitor<'db> for MatchingFreshnessCollector<'_, 'db> { + fn program_environment(&self) -> &ProgramEnvironment<'db> { + self.env + } + fn should_visit_lazy_type_attributes(&self) -> bool { false } @@ -1094,7 +1198,8 @@ pub(crate) fn max_typevar_freshness_matching_generic_context<'db>( } } - let collector = MatchingFreshnessCollector::new(db, generic_context); + let env = ProgramEnvironment::from_program(generic_context.program(db)); + let collector = MatchingFreshnessCollector::new(db, &env, generic_context); for ty in types { collector.visit_type(db, ty); } @@ -1175,7 +1280,7 @@ impl<'db> BoundTypeVarInstance<'db> { } pub(crate) fn kind(self, db: &'db dyn Db) -> TypeVarKind { - self.typevar(db).kind(db) + self.identity(db).kind(db) } pub fn is_paramspec(self, db: &'db dyn Db) -> bool { @@ -1213,11 +1318,16 @@ impl<'db> BoundTypeVarInstance<'db> { self.kind(db) ); + let env = ProgramEnvironment::from_program(self.binding_context(db).program(db)); let upper_bound = TypeVarBoundOrConstraints::UpperBound(match kind { - ParamSpecAttrKind::Args => Type::homogeneous_tuple(db, Type::object()), + ParamSpecAttrKind::Args => Type::homogeneous_tuple(db, &env, Type::object()), ParamSpecAttrKind::Kwargs => KnownClass::Dict - .to_specialized_instance(db, &[KnownClass::Str.to_instance(db), Type::any()]) - .top_materialization(db), + .to_specialized_instance( + db, + &env, + &[KnownClass::Str.to_instance(db, &env), Type::any()], + ) + .top_materialization(db, &env), }); let typevar = self.typevar(db); @@ -1278,7 +1388,12 @@ impl<'db> BoundTypeVarInstance<'db> { /// Create a new PEP 695 type variable that can be used in signatures /// of synthetic generic functions. - pub(crate) fn synthetic(db: &'db dyn Db, name: Name, variance: TypeVarVariance) -> Self { + pub(crate) fn synthetic( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: Name, + variance: TypeVarVariance, + ) -> Self { let identity = TypeVarIdentity::new( db, name, @@ -1296,7 +1411,7 @@ impl<'db> BoundTypeVarInstance<'db> { Self::new( db, typevar, - BindingContext::Synthetic, + BindingContext::Synthetic(env.program(db)), None, TypeVarNonce::NONE, ) @@ -1332,8 +1447,9 @@ impl<'db> BoundTypeVarInstance<'db> { db: &'db dyn Db, f: impl FnOnce(Option>) -> Option>, ) -> Self { + let env = ProgramEnvironment::from_program(self.binding_context(db).program(db)); let typevar = self.typevar(db); - let bound_or_constraints = f(typevar.bound_or_constraints(db)); + let bound_or_constraints = f(typevar.bound_or_constraints(db, &env)); let typevar = TypeVarInstance::new( db, typevar.identity(db), @@ -1358,13 +1474,32 @@ impl<'db> BoundTypeVarInstance<'db> { polarity: TypeVarVariance, ) -> TypeVarVariance { let _span = tracing::trace_span!("variance_with_polarity").entered(); + match self.typevar(db).explicit_variance(db) { Some(explicit_variance) => explicit_variance.compose(polarity), None => match self.binding_context(db) { - BindingContext::Definition(definition) => binding_type(db, definition) - .with_polarity(polarity) - .variance_of(db, self.identity(db)), - BindingContext::Synthetic => TypeVarVariance::Invariant, + BindingContext::Definition(definition) => polarity.compose_thunk(|| { + let env = ProgramEnvironment::from_definition(definition); + let binding_ty = binding_type(db, definition); + match binding_ty.variance_of(db, &env, self.identity(db)) { + // When both directions are valid, the typing spec selects covariance. It + // says so of a parameter the class never mentions; basedpython also infers + // bivariance for one that only a private member mentions, and that + // parameter really is used, so its inferred variance stands. + TypeVarVariance::Bivariant + if binding_ty + .as_class_literal() + .and_then(ClassLiteral::as_static) + .is_none_or(|class| { + class.typevar_is_unused(db, self.identity(db)) + }) => + { + TypeVarVariance::Covariant + } + variance => variance, + } + }), + BindingContext::Synthetic(_) => TypeVarVariance::Invariant, }, } } @@ -1373,6 +1508,24 @@ impl<'db> BoundTypeVarInstance<'db> { self.variance_with_polarity(db, TypeVarVariance::Covariant) } + /// basedpython: the variance a *runtime* probe should test, which is the inferred + /// answer without the typing spec's rule that a bivariant class parameter is + /// reported covariant. A parameter no member mentions really does match either + /// way, and the parametric `is`-test skips comparing it rather than checking a + /// direction that cannot fail. + pub(crate) fn probe_variance(self, db: &'db dyn Db) -> TypeVarVariance { + match self.typevar(db).explicit_variance(db) { + Some(explicit_variance) => explicit_variance, + None => match self.binding_context(db) { + BindingContext::Definition(definition) => { + let env = ProgramEnvironment::from_definition(definition); + binding_type(db, definition).variance_of(db, &env, self.identity(db)) + } + BindingContext::Synthetic(_) => TypeVarVariance::Invariant, + }, + } + } + /// The variance of this type variable at the position it is bound. /// /// A declared variance only says something about a generic *class*: it fixes how two @@ -1381,7 +1534,11 @@ impl<'db> BoundTypeVarInstance<'db> { /// rules, so the declaration is read past and the type variable's position within the /// function's own signature answers instead. This keeps `def f[T]() -> T` and its legacy /// spelling saying the same thing. - pub(crate) fn positional_variance(self, db: &'db dyn Db) -> TypeVarVariance { + pub(crate) fn positional_variance( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> TypeVarVariance { let BindingContext::Definition(definition) = self.binding_context(db) else { return self.variance(db); }; @@ -1389,7 +1546,7 @@ impl<'db> BoundTypeVarInstance<'db> { if binding_ty.is_function_literal() { return binding_ty .with_polarity(TypeVarVariance::Covariant) - .variance_of(db, self.identity(db)); + .variance_of(db, env, self.identity(db)); } self.variance(db) } @@ -1413,7 +1570,11 @@ impl<'db> BoundTypeVarInstance<'db> { /// Confined to `.by`, since `in out` is its syntax. A legacy `TypeVar("T")` /// is invariant under python's own rules, and refining that would change /// what a `.py` file means. - pub(crate) fn is_declared_invariant_but_never_written(self, db: &'db dyn Db) -> bool { + pub(crate) fn is_declared_invariant_but_never_written( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> bool { if self.typevar(db).explicit_variance(db) != Some(TypeVarVariance::Invariant) { return false; } @@ -1425,7 +1586,7 @@ impl<'db> BoundTypeVarInstance<'db> { } binding_type(db, definition) .with_polarity(TypeVarVariance::Covariant) - .variance_of(db, self.identity(db)) + .variance_of(db, env, self.identity(db)) .is_covariant() } @@ -1443,8 +1604,9 @@ impl<'db> BoundTypeVarInstance<'db> { pub(super) fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'a, 'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Type<'db> { let mapped_specialization_type = |specialization: &ApplySpecialization<'a, 'db>| -> Option> { @@ -1527,21 +1689,41 @@ impl<'db> BoundTypeVarInstance<'db> { if mapped == Type::TypeVar(self) { mapped } else { + let env = visitor.env; // Materialization uses a different mapping mode. Reuse of the outer // visitor can incorrectly hit a cache entry from specialization. - let materialization_visitor = ApplyTypeMappingVisitor::default(); - mapped.materialize(db, *materialization_kind, &materialization_visitor) + let materialization_visitor = visitor.for_new_materialization_root(); + let materialized = mapped.materialize( + db, + env, + *materialization_kind, + &materialization_visitor, + ); + + if *materialization_kind == MaterializationKind::Top + && !materialization_visitor.is_equivalent_to_materialization( + db, + mapped, + materialized, + ) + && let Some(upper_bound) = self.top_materialized_upper_bound(db) + { + IntersectionType::from_two_elements(db, env, materialized, upper_bound) + } else { + materialized + } } }) .unwrap_or(Type::TypeVar(self)), TypeMapping::BindSelf(binding) => { - if binding.should_bind(db, self) { + if binding.should_bind(db, visitor.env, self) { binding.self_type() - } else if self.bounds_mention_self(db) { + } else if self.bounds_mention_self(db, env) { // a type variable can be bounded by `Self` (`def method[T: Self]`). that bound // only constrains anything once its `Self` has been bound to the receiver too Type::TypeVar(self.with_mapped_bound_and_default( db, + env, self.freshness(db), type_mapping, visitor, @@ -1568,6 +1750,7 @@ impl<'db> BoundTypeVarInstance<'db> { if generic_context.contains(db, self.identity(db)) && !self.is_parameter_pack(db) { Type::TypeVar(self.with_mapped_bound_and_default( db, + env, self.freshness(db).add(*delta), type_mapping, visitor, @@ -1583,10 +1766,42 @@ impl<'db> BoundTypeVarInstance<'db> { | TypeMapping::RescopeReturnCallables(_) | TypeMapping::AttachRegexGroups(_) => Type::TypeVar(self), TypeMapping::Materialize(materialization_kind) => { - Type::TypeVar(self.materialize_impl(db, *materialization_kind, visitor)) + Type::TypeVar(self.materialize_impl(db, env, *materialization_kind, visitor)) } } } + + /// Returns the static upper bound used when materializing a gradual type argument. + /// + /// Constraints are unioned only when materializing an exposed member, where their union is a + /// valid conservative upper bound. A bound may recursively refer to its own generic class, + /// either directly or through other bounds. Such a bound has no finite static top + /// materialization, so recover from its cycle without applying an upper bound. + pub(super) fn top_materialized_upper_bound(self, db: &'db dyn Db) -> Option> { + #[salsa::tracked( + returns(copy), + cycle_result=|_, _, _| None, + heap_size=ruff_memory_usage::heap_size + )] + fn top_materialized_upper_bound_inner<'db>( + db: &'db dyn Db, + bound_typevar: BoundTypeVarInstance<'db>, + ) -> Option> { + let env = + ProgramEnvironment::from_program(bound_typevar.binding_context(db).program(db)); + + bound_typevar + .typevar(db) + .bound_or_constraints(db, &env) + .map(|bound_or_constraints| { + bound_or_constraints + .as_type(db, &env) + .top_materialization(db, &env) + }) + } + + top_materialized_upper_bound_inner(db, self) + } } pub(super) fn walk_bound_type_var_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( @@ -1627,13 +1842,14 @@ impl<'db> BoundTypeVarInstance<'db> { fn materialize_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, materialization_kind: MaterializationKind, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { Self::new( db, self.typevar(db) - .materialize_impl(db, materialization_kind, visitor), + .materialize_impl(db, env, materialization_kind, visitor), self.binding_context(db), self.paramspec_attr(db), self.freshness(db), @@ -1645,21 +1861,25 @@ impl<'db> BoundTypeVarInstance<'db> { /// A lazily-evaluated bound is invisible to [`Type::contains_self`], so callers that need to /// know whether `Self` binding has any work to do must ask this separately. That covers the /// lower end of a basedpython bound range (`def method[T: Self..object]`), which is lazy too - pub(crate) fn bounds_mention_self(self, db: &'db dyn Db) -> bool { + pub(crate) fn bounds_mention_self( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> bool { let typevar = self.typevar(db); if typevar .lower_bound(db) - .is_some_and(|lower_bound| lower_bound.contains_self(db)) + .is_some_and(|lower_bound| lower_bound.contains_self(db, env)) { return true; } - match typevar.bound_or_constraints(db) { + match typevar.bound_or_constraints(db, env) { None => false, - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound.contains_self(db), + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound.contains_self(db, env), Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints .elements(db) .iter() - .any(|constraint| constraint.contains_self(db)), + .any(|constraint| constraint.contains_self(db, env)), } } @@ -1668,12 +1888,13 @@ impl<'db> BoundTypeVarInstance<'db> { fn with_mapped_bound_and_default( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, nonce: TypeVarNonce, type_mapping: &TypeMapping<'_, 'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { let typevar = self.typevar(db); - let bound_or_constraints = typevar.bound_or_constraints(db); + let bound_or_constraints = typevar.bound_or_constraints(db, env); let lower_bound = typevar.lower_bound(db); let default = self.default_type(db); @@ -1692,16 +1913,16 @@ impl<'db> BoundTypeVarInstance<'db> { typevar.identity(db), bound_or_constraints.map(|bound_or_constraints| { bound_or_constraints - .apply_type_mapping_impl(db, type_mapping, visitor) + .apply_type_mapping_impl(db, env, type_mapping, visitor) .into() }), lower_bound.map(|ty| { - ty.apply_type_mapping_impl(db, type_mapping, TypeContext::default(), visitor) + ty.apply_type_mapping_impl(db, env, type_mapping, TypeContext::default(), visitor) .into() }), typevar.explicit_variance(db), default.map(|ty| { - ty.apply_type_mapping_impl(db, type_mapping, TypeContext::default(), visitor) + ty.apply_type_mapping_impl(db, env, type_mapping, TypeContext::default(), visitor) .into() }), ); @@ -1715,8 +1936,12 @@ impl<'db> BoundTypeVarInstance<'db> { ) } - pub(super) fn to_instance(self, db: &'db dyn Db) -> Option> { - Some(self.typevar(db).to_instance(db)?.map(|typevar| { + pub(super) fn to_instance( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { + Some(self.typevar(db).to_instance(db, env)?.map(|typevar| { Self::new( db, typevar, @@ -1840,12 +2065,18 @@ fn lazy_lower_bound_cycle_recover<'db>( cycle: &salsa::Cycle, previous: &Option>, current: Option>, - _typevar: TypeVarInstance<'db>, + typevar: TypeVarInstance<'db>, ) -> Option> { // Normalize the bound to ensure cycle convergence. match (previous, current) { - (Some(prev), Some(current)) => Some(current.cycle_normalized(db, *prev, cycle)), - (None, Some(current)) => Some(current.recursive_type_normalized(db, cycle)), + (Some(prev), Some(current)) => { + let env = &ProgramEnvironment::from_definition(typevar.definition(db)?); + Some(current.cycle_normalized(db, env, *prev, cycle)) + } + (None, Some(current)) => { + let env = &ProgramEnvironment::from_definition(typevar.definition(db)?); + Some(current.recursive_type_normalized(db, env, cycle)) + } (_, None) => None, } } @@ -1856,14 +2087,19 @@ fn lazy_bound_cycle_recover<'db>( cycle: &salsa::Cycle, previous: &Option>, current: Option>, - _typevar: TypeVarInstance<'db>, + typevar: TypeVarInstance<'db>, ) -> Option> { // Normalize the bounds/constraints to ensure cycle convergence. - match (previous, current) { - (Some(prev), Some(current)) => Some(current.cycle_normalized(db, *prev, cycle)), - (None, Some(current)) => Some(current.recursive_type_normalized(db, cycle)), - (_, None) => None, - } + let current = current?; + let program_file = typevar + .definition(db) + .expect("a lazy TypeVar bound must have a source definition") + .program_file(db); + let env = ProgramEnvironment::from_file(program_file); + Some(match previous { + Some(prev) => current.cycle_normalized(db, &env, *prev, cycle), + None => current.recursive_type_normalized(db, &env, cycle), + }) } #[allow(clippy::trivially_copy_pass_by_ref)] @@ -1873,14 +2109,19 @@ fn lazy_constraints_cycle_recover<'db>( cycle: &salsa::Cycle, previous: &Option>, current: Option>, - _typevar: TypeVarInstance<'db>, + typevar: TypeVarInstance<'db>, ) -> Option> { // Normalize the bounds/constraints to ensure cycle convergence. - match (previous, current) { - (Some(prev), Some(constraints)) => Some(constraints.cycle_normalized(db, *prev, cycle)), - (None, Some(current)) => Some(current.recursive_type_normalized(db, cycle)), - (_, None) => None, - } + let current = current?; + let program_file = typevar + .definition(db) + .expect("lazy TypeVar constraints must have a source definition") + .program_file(db); + let env = ProgramEnvironment::from_file(program_file); + Some(match previous { + Some(prev) => current.cycle_normalized(db, &env, *prev, cycle), + None => current.recursive_type_normalized(db, &env, cycle), + }) } #[expect(clippy::ref_option)] @@ -1888,15 +2129,20 @@ fn lazy_default_cycle_recover<'db>( db: &'db dyn Db, cycle: &salsa::Cycle, previous_default: &Option>, - default: Option>, - _typevar: TypeVarInstance<'db>, + current: Option>, + typevar: TypeVarInstance<'db>, ) -> Option> { // Normalize the default to ensure cycle convergence. - match (previous_default, default) { - (Some(prev), Some(default)) => Some(default.cycle_normalized(db, *prev, cycle)), - (None, Some(default)) => Some(default.recursive_type_normalized(db, cycle)), - (_, None) => None, - } + let current = current?; + let program_file = typevar + .definition(db) + .expect("a lazy TypeVar default must have a source definition") + .program_file(db); + let env = ProgramEnvironment::from_file(program_file); + Some(match previous_default { + Some(prev) => current.cycle_normalized(db, &env, *prev, cycle), + None => current.recursive_type_normalized(db, &env, cycle), + }) } /// Where a type variable is bound and usable. @@ -1905,8 +2151,9 @@ pub enum BindingContext<'db> { /// The definition of the generic class, function, or type alias that binds this typevar. Definition(Definition<'db>), /// The typevar is synthesized internally, and is not associated with a particular definition - /// in the source, but is still bound and eligible for specialization inference. - Synthetic, + /// in the source, but is still bound and eligible for specialization inference. Its program + /// identifies the environment that cannot otherwise be recovered from a source definition. + Synthetic(Program<'db>), } impl<'db> From> for BindingContext<'db> { @@ -1919,7 +2166,14 @@ impl<'db> BindingContext<'db> { pub(crate) fn definition(self) -> Option> { match self { BindingContext::Definition(definition) => Some(definition), - BindingContext::Synthetic => None, + BindingContext::Synthetic(_) => None, + } + } + + pub(crate) fn program(self, db: &'db dyn Db) -> Program<'db> { + match self { + Self::Definition(definition) => definition.program(db), + Self::Synthetic(program) => program, } } @@ -1934,6 +2188,17 @@ pub enum ParamSpecAttrKind { Kwargs, } +impl ParamSpecAttrKind { + /// Returns the component represented by a `ParamSpec` attribute name. + pub(crate) fn from_name(name: &str) -> Option { + match name { + "args" => Some(Self::Args), + "kwargs" => Some(Self::Kwargs), + _ => None, + } + } +} + impl std::fmt::Display for ParamSpecAttrKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -1958,7 +2223,7 @@ pub struct BoundTypeVarIdentity<'db> { /// of a `ParamSpec` i.e., `P.args` or `P.kwargs`. pub(super) paramspec_attr: Option, /// The freshness nonce for this bound typevar occurrence; `0` is the source-level occurrence. - pub(super) freshness: TypeVarNonce, + freshness: TypeVarNonce, } impl<'db> BoundTypeVarIdentity<'db> { @@ -1986,6 +2251,110 @@ impl<'db> BoundTypeVarIdentity<'db> { } } +/// A set of bound typevar occurrences. +/// +/// Membership is keyed by [`BoundTypeVarIdentity`], including any freshness nonce, while the first +/// bound instance encountered for each identity is retained. This lets a fresh generic-callable +/// occurrence be inferable without making the surrounding source-level typevar inferable. +#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, get_size2::GetSize, salsa::SalsaValue)] +pub(crate) enum TypeVarSet<'db> { + None, + Some(TypeVarSetInner<'db>), +} + +impl<'db> TypeVarSet<'db> { + pub(crate) fn from_typevars( + db: &'db dyn Db, + typevars: impl IntoIterator>, + ) -> Self { + let mut typevars = typevars.into_iter().peekable(); + if typevars.peek().is_none() { + return TypeVarSet::None; + } + + let mut set = FxOrderMap::default(); + for typevar in typevars { + set.entry(typevar.identity(db)).or_insert(typevar); + } + set.shrink_to_fit(); + Self::Some(TypeVarSetInner::new_internal(db, set)) + } +} + +#[salsa::interned(debug, constructor=new_internal, heap_size=ruff_memory_usage::heap_size)] +pub(crate) struct TypeVarSetInner<'db> { + #[returns(ref)] + typevars: FxOrderMap, BoundTypeVarInstance<'db>>, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for TypeVarSetInner<'_> {} + +impl<'db> BoundTypeVarIdentity<'db> { + pub(crate) fn is_inferable(self, db: &'db dyn Db, inferable: TypeVarSet<'db>) -> bool { + match inferable { + TypeVarSet::None => false, + TypeVarSet::Some(inner) => inner.typevars(db).contains_key(&self), + } + } +} + +impl<'db> BoundTypeVarInstance<'db> { + pub(crate) fn is_inferable(self, db: &'db dyn Db, inferable: TypeVarSet<'db>) -> bool { + self.identity(db).is_inferable(db, inferable) + } +} + +impl<'db> TypeVarSet<'db> { + pub(crate) fn merge(self, db: &'db dyn Db, other: Self) -> Self { + #[salsa::tracked(returns(copy), heap_size=ruff_memory_usage::heap_size)] + fn merge_inner<'db>( + db: &'db dyn Db, + self_inner: TypeVarSetInner<'db>, + other_inner: TypeVarSetInner<'db>, + ) -> TypeVarSet<'db> { + TypeVarSet::from_typevars( + db, + self_inner + .typevars(db) + .values() + .chain(other_inner.typevars(db).values()) + .copied(), + ) + } + + match (self, other) { + (TypeVarSet::None, other) | (other, TypeVarSet::None) => other, + (TypeVarSet::Some(self_inner), TypeVarSet::Some(other_inner)) => { + merge_inner(db, self_inner, other_inner) + } + } + } + + // This is not an IntoIterator implementation because I have no desire to try to name the + // iterator type. + pub(crate) fn iter( + self, + db: &'db dyn Db, + ) -> impl Iterator> + 'db { + match self { + TypeVarSet::None => Either::Left(std::iter::empty()), + TypeVarSet::Some(inner) => Either::Right(inner.typevars(db).values().copied()), + } + } + + // Keep this around for debugging purposes + #[cfg_attr(not(test), expect(dead_code))] + fn display(self, db: &'db dyn Db) -> String { + format!( + "[{}]", + self.iter(db) + .map(|typevar| typevar.identity(db).display(db)) + .format(", ") + ) + } +} + #[salsa::tracked( returns(copy), cycle_initial=|_, id, _| Some(Type::divergent(id)), @@ -1996,14 +2365,21 @@ fn bound_typevar_default_type<'db>( db: &'db dyn Db, bound_typevar: BoundTypeVarInstance<'db>, ) -> Option> { + let typevar = bound_typevar.typevar(db); + typevar._default(db)?; + let definition = typevar + .definition(db) + .expect("a bound TypeVar with a default must have a source definition"); + let env = ProgramEnvironment::from_definition(definition); + let default = typevar.default_type(db, &env)?; let binding_context = bound_typevar.binding_context(db); - bound_typevar.typevar(db).default_type(db).map(|ty| { - ty.apply_type_mapping( - db, - &TypeMapping::BindLegacyTypevars(binding_context), - TypeContext::default(), - ) - }) + + Some(default.apply_type_mapping( + db, + &env, + &TypeMapping::BindLegacyTypevars(binding_context), + TypeContext::default(), + )) } #[expect(clippy::ref_option)] @@ -2012,13 +2388,19 @@ fn bound_typevar_default_type_cycle_recover<'db>( cycle: &salsa::Cycle, previous_default: &Option>, default: Option>, - _bound_typevar: BoundTypeVarInstance<'db>, + bound_typevar: BoundTypeVarInstance<'db>, ) -> Option> { - match (previous_default, default) { - (Some(previous), Some(default)) => Some(default.cycle_normalized(db, *previous, cycle)), - (None, Some(default)) => Some(default.recursive_type_normalized(db, cycle)), - (_, None) => None, - } + let default = default?; + let program_file = bound_typevar + .typevar(db) + .definition(db) + .expect("a bound TypeVar with a default must have a source definition") + .program_file(db); + let env = ProgramEnvironment::from_file(program_file); + Some(match previous_default { + Some(previous) => default.cycle_normalized(db, &env, *previous, cycle), + None => default.recursive_type_normalized(db, &env, cycle), + }) } /// Whether a typevar's basedpython lower bound is eagerly specified or lazily evaluated. @@ -2089,15 +2471,19 @@ fn walk_type_var_constraints<'db, V: visitor::TypeVisitor<'db> + ?Sized>( } impl<'db> TypeVarConstraints<'db> { - pub(super) fn as_type(self, db: &'db dyn Db) -> Type<'db> { - UnionType::from_elements(db, self.elements(db)) + pub(super) fn as_type(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + UnionType::from_elements(db, env, self.elements(db)) } - fn to_instance(self, db: &'db dyn Db) -> Option>> { + fn to_instance( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option>> { let mut instance_elements = Vec::new(); let mut is_exact = true; for ty in self.elements(db) { - let projection = ty.to_instance(db)?; + let projection = ty.to_instance(db, env)?; is_exact &= projection.is_exact(); instance_elements.push(projection.into_inner()); } @@ -2123,9 +2509,10 @@ impl<'db> TypeVarConstraints<'db> { pub(crate) fn map_with_boundness_and_qualifiers( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, mut transform_fn: impl FnMut(&Type<'db>) -> PlaceAndQualifiers<'db>, ) -> PlaceAndQualifiers<'db> { - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); let mut qualifiers = TypeQualifiers::empty(); let mut all_unbound = true; @@ -2180,13 +2567,14 @@ impl<'db> TypeVarConstraints<'db> { fn materialize_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, materialization_kind: MaterializationKind, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { let materialized = self .elements(db) .iter() - .map(|ty| ty.materialize(db, materialization_kind, visitor)) + .map(|ty| ty.materialize(db, env, materialization_kind, visitor)) .collect::>(); TypeVarConstraints::new(db, materialized) } @@ -2194,13 +2582,16 @@ impl<'db> TypeVarConstraints<'db> { fn apply_type_mapping_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'_, 'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { let mapped = self .elements(db) .iter() - .map(|ty| ty.apply_type_mapping_impl(db, type_mapping, TypeContext::default(), visitor)) + .map(|ty| { + ty.apply_type_mapping_impl(db, env, type_mapping, TypeContext::default(), visitor) + }) .collect::>(); TypeVarConstraints::new(db, mapped) } @@ -2209,7 +2600,13 @@ impl<'db> TypeVarConstraints<'db> { /// removing divergent types introduced by the cycle. /// /// See [`Type::cycle_normalized`] for more details on how this works. - fn cycle_normalized(self, db: &'db dyn Db, previous: Self, cycle: &salsa::Cycle) -> Self { + fn cycle_normalized( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + previous: Self, + cycle: &salsa::Cycle, + ) -> Self { let current_elements = self.elements(db); let prev_elements = previous.elements(db); TypeVarConstraints::new( @@ -2217,7 +2614,7 @@ impl<'db> TypeVarConstraints<'db> { current_elements .iter() .zip(prev_elements.iter()) - .map(|(ty, prev_ty)| ty.cycle_normalized(db, *prev_ty, cycle)) + .map(|(ty, prev_ty)| ty.cycle_normalized(db, env, *prev_ty, cycle)) .collect::>(), ) } @@ -2225,8 +2622,13 @@ impl<'db> TypeVarConstraints<'db> { /// Normalize recursive types for cycle recovery when there's no previous value. /// /// See [`Type::recursive_type_normalized`] for more details. - fn recursive_type_normalized(self, db: &'db dyn Db, cycle: &salsa::Cycle) -> Self { - self.map(db, |ty| ty.recursive_type_normalized(db, cycle)) + fn recursive_type_normalized( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + cycle: &salsa::Cycle, + ) -> Self { + self.map(db, |ty| ty.recursive_type_normalized(db, env, cycle)) } } @@ -2242,7 +2644,9 @@ pub(super) fn walk_type_var_bounds<'db, V: visitor::TypeVisitor<'db> + ?Sized>( visitor: &V, ) { match bounds { - TypeVarBoundOrConstraints::UpperBound(bound) => visitor.visit_type(db, bound), + TypeVarBoundOrConstraints::UpperBound(bound) => { + visitor.visit_type(db, bound); + } TypeVarBoundOrConstraints::Constraints(constraints) => { walk_type_var_constraints(db, constraints, visitor); } @@ -2253,16 +2657,18 @@ impl<'db> TypeVarBoundOrConstraints<'db> { fn materialize_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, materialization_kind: MaterializationKind, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { match self { TypeVarBoundOrConstraints::UpperBound(bound) => TypeVarBoundOrConstraints::UpperBound( - bound.materialize(db, materialization_kind, visitor), + bound.materialize(db, env, materialization_kind, visitor), ), TypeVarBoundOrConstraints::Constraints(constraints) => { TypeVarBoundOrConstraints::Constraints(constraints.materialize_impl( db, + env, materialization_kind, visitor, )) @@ -2273,16 +2679,24 @@ impl<'db> TypeVarBoundOrConstraints<'db> { fn apply_type_mapping_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'_, 'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { match self { - TypeVarBoundOrConstraints::UpperBound(bound) => TypeVarBoundOrConstraints::UpperBound( - bound.apply_type_mapping_impl(db, type_mapping, TypeContext::default(), visitor), - ), + TypeVarBoundOrConstraints::UpperBound(bound) => { + TypeVarBoundOrConstraints::UpperBound(bound.apply_type_mapping_impl( + db, + env, + type_mapping, + TypeContext::default(), + visitor, + )) + } TypeVarBoundOrConstraints::Constraints(constraints) => { TypeVarBoundOrConstraints::Constraints(constraints.apply_type_mapping_impl( db, + env, type_mapping, visitor, )) @@ -2296,10 +2710,10 @@ impl<'db> TypeVarBoundOrConstraints<'db> { /// constraints provides a conservative upper bound, but it loses precision. And for many use /// cases, it's more efficient to just map over the constraint types directly, rather than /// building a union out of them and mapping over that. - pub(crate) fn as_type(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn as_type(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match self { TypeVarBoundOrConstraints::UpperBound(bound) => bound, - TypeVarBoundOrConstraints::Constraints(constraints) => constraints.as_type(db), + TypeVarBoundOrConstraints::Constraints(constraints) => constraints.as_type(db, env), } } } @@ -2317,13 +2731,22 @@ pub(crate) enum PackBoundViolation<'db> { impl<'db> PackBoundViolation<'db> { /// The bound this violation was measured against. Only ever `None` for a pack with no bound, /// which cannot produce a violation in the first place. - fn bound(db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> Option> { - typevar.typevar(db).pack_bound(db) + fn bound( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarInstance<'db>, + ) -> Option> { + typevar.typevar(db).pack_bound(db, env) } - pub(crate) fn message(&self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> String { - let bound = Self::bound(db, typevar) - .map(|bound| bound.display(db).to_string()) + pub(crate) fn message( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarInstance<'db>, + ) -> String { + let bound = Self::bound(db, env, typevar) + .map(|bound| bound.display(db, env).to_string()) .unwrap_or_default(); let kind = if typevar.is_typevartuple(db) { "type variable tuple" @@ -2334,7 +2757,7 @@ impl<'db> PackBoundViolation<'db> { match self { Self::Member(member) => format!( "Type `{}` is not assignable to upper bound `{bound}` of {kind} `{name}`", - member.display(db), + member.display(db, env), ), Self::MissingField(field) => { format!("Upper bound `{bound}` of {kind} `{name}` requires a field `{field}`") @@ -2347,15 +2770,16 @@ impl<'db> PackBoundViolation<'db> { pub(in crate::types) fn attach_context( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typevar: BoundTypeVarInstance<'db>, diagnostic: &mut LintDiagnosticGuard<'_, '_>, ) { - let (Self::Member(member), Some(bound)) = (self, Self::bound(db, typevar)) else { + let (Self::Member(member), Some(bound)) = (self, Self::bound(db, env, typevar)) else { return; }; member - .assignability_error_context(db, bound) - .attach_to(db, diagnostic); + .assignability_error_context(db, env, bound) + .attach_to(db, env, diagnostic); } } @@ -2372,16 +2796,17 @@ impl<'db> PackBoundViolation<'db> { /// callable for a keyword-variadic pack. pub(crate) fn pack_bound_violation<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typevar: BoundTypeVarInstance<'db>, provided: Type<'db>, constraints: &ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, ) -> Option> { - let bound = typevar.typevar(db).pack_bound(db)?; + let bound = typevar.typevar(db).pack_bound(db, env)?; let outside = |member: Type<'db>, bound: Type<'db>| { member - .when_assignable_to(db, bound, constraints, inferable) - .is_never_satisfied(db) + .when_assignable_to(db, env, bound, constraints, inferable) + .is_never_satisfied(db, env) }; let whole_pack = typevar.typevar(db).has_whole_pack_bound(db); @@ -2430,3 +2855,170 @@ impl<'db> super::cyclic::HasIdentity<'db> for TypeVarInstance<'db> { *self } } + +#[cfg(test)] +mod tests { + use super::*; + + use ruff_db::testing::assert_function_query_was_not_run_by_name; + + use crate::db::tests::setup_db; + + fn bound_typevar<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &'static str, + kind: TypeVarKind, + bound_or_constraints: Option>, + freshness: TypeVarNonce, + ) -> BoundTypeVarInstance<'db> { + let identity = TypeVarIdentity::new(db, Name::new_static(name), None, kind); + let typevar = TypeVarInstance::new( + db, + identity, + bound_or_constraints, + None, + Some(TypeVarVariance::Invariant), + None, + ); + BoundTypeVarInstance::new( + db, + typevar, + BindingContext::Synthetic(env.program(db)), + None, + freshness, + ) + } + + #[test] + fn typevar_set_empty_set_is_none() { + let db = setup_db(); + let db = &db; + let env = db.program_environment(); + let typevar = BoundTypeVarInstance::synthetic( + db, + &env, + Name::new_static("T"), + TypeVarVariance::Invariant, + ); + let inferable = TypeVarSet::from_typevars(db, []); + + assert_eq!(inferable, TypeVarSet::None); + assert_eq!(inferable.iter(db).count(), 0); + assert!(!typevar.is_inferable(db, inferable)); + assert!(!typevar.identity(db).is_inferable(db, inferable)); + } + + #[test] + fn typevar_set_keeps_first_instance_for_each_identity() { + let mut db = setup_db(); + db.clear_salsa_events(); + let env = db.program_environment(); + + // The synthetic lazy bound has no definition, so it is equivalent to the implicit + // `object` upper bound represented eagerly below. + let lazy = bound_typevar( + &db, + &env, + "T", + TypeVarKind::Pep695TypeVar, + Some(TypeVarBoundOrConstraintsEvaluation::LazyUpperBound), + TypeVarNonce::NONE, + ); + let eager = bound_typevar( + &db, + &env, + "T", + TypeVarKind::Pep695TypeVar, + Some(TypeVarBoundOrConstraints::UpperBound(Type::object()).into()), + TypeVarNonce::NONE, + ); + let u = BoundTypeVarInstance::synthetic( + &db, + &env, + Name::new_static("U"), + TypeVarVariance::Invariant, + ); + let v = BoundTypeVarInstance::synthetic( + &db, + &env, + Name::new_static("V"), + TypeVarVariance::Invariant, + ); + + assert_ne!(lazy, eager); + assert_eq!(lazy.identity(&db), eager.identity(&db)); + + let left = TypeVarSet::from_typevars(&db, [lazy, u, eager]); + let right = TypeVarSet::from_typevars(&db, [eager, v, lazy]); + let merged = left.merge(&db, right); + + assert_eq!(left.iter(&db).collect::>(), [lazy, u]); + assert_eq!(right.iter(&db).collect::>(), [eager, v]); + assert_eq!(merged.iter(&db).collect::>(), [lazy, u, v]); + assert_eq!(merged, TypeVarSet::from_typevars(&db, [lazy, u, v])); + assert!(lazy.is_inferable(&db, merged)); + assert!(eager.is_inferable(&db, merged)); + assert_eq!(merged.display(&db), "[T, U, V]"); + + let events = db.take_salsa_events(); + assert_function_query_was_not_run_by_name(&db, "lazy_bound_unchecked", None, &events); + } + + #[test] + fn typevar_set_distinguishes_fresh_and_paramspec_identities() { + let db = setup_db(); + let db = &db; + let env = db.program_environment(); + let typevar = bound_typevar( + db, + &env, + "T", + TypeVarKind::Pep695TypeVar, + None, + TypeVarNonce::NONE, + ); + let fresh = bound_typevar( + db, + &env, + "T", + TypeVarKind::Pep695TypeVar, + None, + TypeVarNonce::NONE.increment(), + ); + let paramspec = bound_typevar( + db, + &env, + "P", + TypeVarKind::Pep695ParamSpec, + None, + TypeVarNonce::NONE, + ); + let args = paramspec.with_paramspec_attr(db, ParamSpecAttrKind::Args); + let kwargs = paramspec.with_paramspec_attr(db, ParamSpecAttrKind::Kwargs); + + let inferable = TypeVarSet::from_typevars(db, [typevar, fresh, args, kwargs]); + assert_eq!( + inferable.iter(db).collect::>(), + [typevar, fresh, args, kwargs] + ); + assert!(typevar.is_inferable(db, inferable)); + assert!(fresh.is_inferable(db, inferable)); + assert!(args.is_inferable(db, inferable)); + assert!(kwargs.is_inferable(db, inferable)); + assert!(!paramspec.is_inferable(db, inferable)); + + let paramspec_only = TypeVarSet::from_typevars(db, [paramspec]); + assert!( + args.identity(db) + .without_paramspec_attr(db) + .is_inferable(db, paramspec_only) + ); + assert!( + kwargs + .identity(db) + .without_paramspec_attr(db) + .is_inferable(db, paramspec_only) + ); + } +} diff --git a/crates/ty_python_semantic/src/types/unpacker.rs b/crates/ty_python_semantic/src/types/unpacker.rs index 880db4b3ce..81a11315a3 100644 --- a/crates/ty_python_semantic/src/types/unpacker.rs +++ b/crates/ty_python_semantic/src/types/unpacker.rs @@ -1,6 +1,8 @@ +use crate::ProgramEnvironment; use std::borrow::Cow; use ruff_db::parsed::ParsedModuleRef; + use rustc_hash::FxHashMap; use ruff_python_ast::visitor::{self, Visitor}; @@ -14,6 +16,7 @@ use crate::types::{ report_iteration_over_character, }; use ty_python_core::ExpressionNodeKey; +use ty_python_core::ProgramFile; use ty_python_core::scope::ScopeId; use ty_python_core::unpack::{UnpackKind, UnpackValue}; @@ -41,11 +44,20 @@ impl<'ast> Visitor<'ast> for UnknownTargetCollector<'_, '_> { impl<'db, 'ast> Unpacker<'db, 'ast> { pub(crate) fn new( db: &'db dyn Db, + env: &'ast ProgramEnvironment<'db>, target_scope: ScopeId<'db>, + program_file: ProgramFile<'db>, module: &'ast ParsedModuleRef, ) -> Self { Self { - context: InferContext::new(db, target_scope, module), + context: InferContext::new( + db, + env, + target_scope, + program_file.file(db), + program_file, + module, + ), targets: FxHashMap::default(), } } @@ -60,13 +72,17 @@ impl<'db, 'ast> Unpacker<'db, 'ast> { /// Unpack the value to the target expression. pub(crate) fn unpack(&mut self, target: &ast::Expr, value: UnpackValue<'db>) { + let db = self.db(); debug_assert!( matches!(target, ast::Expr::List(_) | ast::Expr::Tuple(_)), "Unpacking target must be a list or tuple expression" ); - let value_inference = - infer_expression_types(self.db(), value.expression(), TypeContext::default()); + let value_inference = infer_expression_types( + self.context.db(), + value.expression(), + TypeContext::default(), + ); let value_expr = value.expression().node_ref(self.db()).node(self.module()); if matches!(value.kind(), UnpackKind::Assign) @@ -86,33 +102,37 @@ impl<'db, 'ast> Unpacker<'db, 'ast> { } } UnpackKind::Iterable { mode } => { + let env = self.context.program_environment(); report_iteration_over_character( &self.context, value_type, value.as_any_node_ref(self.db(), self.module()), ); value_type - .try_iterate_with_mode(self.db(), mode) - .map(|tuple| tuple.homogeneous_element_type(self.db())) + .try_iterate_with_mode(db, env, mode) + .map(|tuple| tuple.homogeneous_element_type(db, env)) + .unwrap_or_else(|err| { + err.report_diagnostic( + &self.context, + value_type, + value.as_any_node_ref(self.db(), self.module()), + ); + err.fallback_element_type(db, env) + }) + } + UnpackKind::ContextManager { mode } => { + let env = self.context.program_environment(); + value_type + .try_enter_with_mode(db, env, mode) .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()) + err.fallback_enter_type(db, env) }) } - UnpackKind::ContextManager { mode } => value_type - .try_enter_with_mode(self.db(), mode) - .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_expr.into(), value_type); @@ -197,6 +217,7 @@ impl<'db, 'ast> Unpacker<'db, 'ast> { value_expr: AnyNodeRef<'_>, value_ty: Type<'db>, ) { + let db = self.db(); match target { ast::Expr::Name(_) | ast::Expr::Attribute(_) | ast::Expr::Subscript(_) => { self.targets.insert(target.into(), value_ty); @@ -212,7 +233,8 @@ impl<'db, 'ast> Unpacker<'db, 'ast> { } None => TupleLength::Fixed(elts.len()), }; - let mut unpacker = TupleUnpacker::new(self.db(), target_len); + let env = self.context.program_environment(); + let mut unpacker = TupleUnpacker::new(db, env, target_len); // N.B. `Type::try_iterate` internally handles unions, but in a lossy way. // For our purposes here, we get better error messages and more precise inference @@ -226,9 +248,9 @@ impl<'db, 'ast> Unpacker<'db, 'ast> { for ty in unpack_types.iter().copied() { report_iteration_over_character(&self.context, ty, value_expr); - let tuple = ty.try_iterate(self.db()).unwrap_or_else(|err| { + let tuple = ty.try_iterate(self.db(), env).unwrap_or_else(|err| { err.report_diagnostic(&self.context, ty, value_expr); - Cow::Owned(TupleSpec::homogeneous(err.fallback_element_type(self.db()))) + Cow::Owned(TupleSpec::homogeneous(err.fallback_element_type(db, env))) }); if let Err(err) = unpacker.unpack_tuple(tuple.as_ref()) { @@ -241,7 +263,7 @@ impl<'db, 'ast> Unpacker<'db, 'ast> { ResizeTupleError::TooManyValues => { let mut diag = builder.into_diagnostic("Too many values to unpack"); - diag.set_primary_message(format_args!( + diag.set_primary_annotation_message(format_args!( "Expected {}", target_len.display_minimum(), )); @@ -252,7 +274,7 @@ impl<'db, 'ast> Unpacker<'db, 'ast> { ResizeTupleError::TooFewValues => { let mut diag = builder.into_diagnostic("Not enough values to unpack"); - diag.set_primary_message(format_args!( + diag.set_primary_annotation_message(format_args!( "Expected {}", target_len.display_minimum(), )); @@ -316,10 +338,7 @@ impl<'db> UnpackResult<'db> { ) } - pub(crate) fn try_expression_type( - &self, - expr: impl Into, - ) -> Option> { + fn try_expression_type(&self, expr: impl Into) -> Option> { self.targets .get(&expr.into()) .copied() @@ -342,12 +361,13 @@ impl<'db> UnpackResult<'db> { pub(crate) fn cycle_normalized( mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, previous_cycle_result: &UnpackResult<'db>, cycle: &salsa::Cycle, ) -> Self { for (expr, ty) in &mut self.targets { let previous_ty = previous_cycle_result.expression_type(*expr); - *ty = ty.cycle_normalized(db, previous_ty, cycle); + *ty = ty.cycle_normalized(db, env, previous_ty, cycle); } self diff --git a/crates/ty_python_semantic/src/types/unsafe_union.rs b/crates/ty_python_semantic/src/types/unsafe_union.rs index 026dfb73a9..562a6baef7 100644 --- a/crates/ty_python_semantic/src/types/unsafe_union.rs +++ b/crates/ty_python_semantic/src/types/unsafe_union.rs @@ -26,6 +26,7 @@ use crate::Db; use crate::place::{ DefinedPlace, Definedness, Place, PlaceAndQualifiers, Provenance, PublicTypePolicy, TypeOrigin, }; +use crate::types::ProgramEnvironment; use crate::types::set_theoretic::UnionType; use crate::types::variance::VarianceInferable; use crate::types::{ @@ -98,8 +99,8 @@ impl<'db> UnsafeUnionType<'db> { /// /// This is the type an `UnsafeUnion` is narrowed to when it is used *safely*, and the /// face it presents to operations that must hold for every possible materialization. - pub(crate) fn to_union(self, db: &'db dyn Db) -> Type<'db> { - UnionType::from_elements(db, self.elements(db).iter().copied()) + pub(crate) fn to_union(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + UnionType::from_elements(db, env, self.elements(db).iter().copied()) } /// Apply `transform` to every element, rebuilding (and possibly collapsing) the menu. @@ -122,10 +123,14 @@ impl<'db> UnsafeUnionType<'db> { } /// Project every element from a class-object type into its instance type. - pub(crate) fn to_instance(self, db: &'db dyn Db) -> Option>> { + pub(crate) fn to_instance( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option>> { let mut is_exact = true; let instance = self.try_map_elements(db, |element| { - let projection = element.to_instance(db)?; + let projection = element.to_instance(db, env)?; is_exact &= projection.is_exact(); Some(projection.into_inner()) })?; @@ -200,10 +205,17 @@ impl<'db> VarianceInferable<'db> for UnsafeUnionType<'db> { /// An `UnsafeUnion` is invariant in its elements: each one is reachable in both /// directions (a value can be assigned *to* the type through it, and read *out* of the /// type as it), so neither polarity alone describes it. - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { + fn variance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { self.elements(db) .iter() - .map(|element| TypeVarVariance::Invariant.compose(element.variance_of(db, typevar))) + .map(|element| { + TypeVarVariance::Invariant.compose(element.variance_of(db, env, typevar)) + }) .collect() } } diff --git a/crates/ty_python_semantic/src/types/variance.rs b/crates/ty_python_semantic/src/types/variance.rs index 21a3253dbf..4e5ca4432b 100644 --- a/crates/ty_python_semantic/src/types/variance.rs +++ b/crates/ty_python_semantic/src/types/variance.rs @@ -1,4 +1,5 @@ -use crate::{Db, types::BoundTypeVarIdentity}; +use crate::Db; +use crate::{ProgramEnvironment, types::BoundTypeVarIdentity}; #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, get_size2::GetSize)] pub enum TypeVarVariance { @@ -9,14 +10,6 @@ pub enum TypeVarVariance { } impl TypeVarVariance { - pub const fn bottom() -> Self { - TypeVarVariance::Bivariant - } - - pub const fn top() -> Self { - TypeVarVariance::Invariant - } - // supremum #[must_use] pub(crate) const fn join(self, other: Self) -> Self { @@ -141,7 +134,12 @@ pub(crate) trait VarianceInferable<'db>: Sized { /// /// Sometimes the recursive calls will be in positions where you need to /// specify a non-covariant polarity. See `with_polarity` for more details. - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance; + fn variance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance; /// Creates a `VarianceInferable` that applies `polarity` (see /// `TypeVarVariance::compose`) to the result of variance inference on the @@ -173,12 +171,17 @@ impl<'db, T> VarianceInferable<'db> for WithPolarity where T: VarianceInferable<'db>, { - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { + fn variance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { let WithPolarity { variance_inferable, polarity, } = self; - polarity.compose_thunk(|| variance_inferable.variance_of(db, typevar)) + polarity.compose_thunk(|| variance_inferable.variance_of(db, env, typevar)) } } diff --git a/crates/ty_python_semantic/src/types/visibility.rs b/crates/ty_python_semantic/src/types/visibility.rs index 684539885e..9b9dbfae1a 100644 --- a/crates/ty_python_semantic/src/types/visibility.rs +++ b/crates/ty_python_semantic/src/types/visibility.rs @@ -28,7 +28,7 @@ use crate::Db; pub fn private_symbols(db: &dyn Db, file: File) -> FxHashSet { let _span = tracing::trace_span!("private_symbols", file=?file.path(db)).entered(); - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, db.program_file(file).python_file(db)).load(db); let source = source_text(db, file); let mut names = FxHashSet::default(); diff --git a/crates/ty_python_semantic/src/types/visitor.rs b/crates/ty_python_semantic/src/types/visitor.rs index 3bcb7243e8..9521e0cebd 100644 --- a/crates/ty_python_semantic/src/types/visitor.rs +++ b/crates/ty_python_semantic/src/types/visitor.rs @@ -1,40 +1,39 @@ +use crate::Db; +use crate::ProgramEnvironment; use std::cell::{Cell, RefCell}; use std::hash::Hash; use rustc_hash::{FxBuildHasher, FxHashSet}; use smallvec::SmallVec; - -use crate::{ - Db, - types::{ - BoundMethodType, BoundSuperType, BoundTypeVarInstance, CallableType, DeferredType, - EnumComplementType, GenericAlias, IntersectionType, KnownBoundMethodType, - KnownInstanceType, NominalInstanceType, OverlappingType, PropertyInstanceType, - ProtocolInstanceType, RestrictedType, StaticClassLiteral, SubclassOfType, Type, - TypeAliasType, TypeFormType, TypeGuardType, TypeIsType, TypedDictType, UnionType, - UnsafeUnionType, - bound_super::walk_bound_super_type, - callable::walk_callable_type, - class::walk_generic_alias, - cyclic::ActiveRecursionDetector, - deferred::walk_deferred_type, - function::{FunctionType, walk_function_type}, - instance::{walk_nominal_instance_type, walk_protocol_instance_type}, - known_instance::walk_known_instance_type, - method::{walk_bound_method_type, walk_method_wrapper_type}, - newtype::{NewType, walk_newtype_instance_type}, - overlapping::walk_overlapping_type, - protocol_class::walk_protocol_instance_interface, - restricted::walk_restricted_type, - set_theoretic::{walk_intersection_type, walk_union}, - subclass_of::walk_subclass_of_type, - type_alias::walk_type_alias_type, - type_form::walk_typeform_type, - typed_dict::walk_typed_dict_type, - typevar::{TypeVarInstance, walk_bound_type_var_type, walk_type_var_type}, - unsafe_union::walk_unsafe_union, - walk_property_instance_type, walk_typeguard_type, walk_typeis_type, - }, +use ty_python_core::definition::Definition; + +use crate::types::{ + BoundMethodType, BoundSuperType, BoundTypeVarInstance, CallableType, DeferredType, + EnumComplementType, GenericAlias, IntersectionType, KnownBoundMethodType, KnownInstanceType, + NominalInstanceType, OverlappingType, PropertyInstanceType, ProtocolInstanceType, + RestrictedType, StaticClassLiteral, SubclassOfType, Type, TypeAliasType, TypeFormType, + TypeGuardType, TypeIsType, TypedDictType, UnionType, UnsafeUnionType, + bound_super::walk_bound_super_type, + callable::walk_callable_type, + class::walk_generic_alias, + cyclic::ActiveRecursionDetector, + deferred::walk_deferred_type, + function::{FunctionType, walk_function_type}, + instance::{walk_nominal_instance_type, walk_protocol_instance_type}, + known_instance::walk_known_instance_type, + method::{walk_bound_method_type, walk_method_wrapper_type}, + newtype::{NewType, walk_newtype_instance_type}, + overlapping::walk_overlapping_type, + protocol_class::walk_protocol_instance_interface, + restricted::walk_restricted_type, + set_theoretic::{walk_intersection_type, walk_union}, + subclass_of::walk_subclass_of_type, + type_alias::walk_type_alias_type, + type_form::walk_typeform_type, + typed_dict::walk_typed_dict_type, + typevar::{TypeVarInstance, walk_bound_type_var_type, walk_type_var_type}, + unsafe_union::walk_unsafe_union, + walk_property_instance_type, walk_typeguard_type, walk_typeis_type, }; /// A visitor trait that recurses into nested types. @@ -43,6 +42,8 @@ use crate::{ /// 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 program_environment(&self) -> &ProgramEnvironment<'db>; + /// Should the visitor trigger inference of and visit lazily-inferred type attributes? fn should_visit_lazy_type_attributes(&self) -> bool; @@ -280,7 +281,9 @@ pub(super) fn walk_non_atomic_type<'db, V: TypeVisitor<'db> + ?Sized>( visitor: &V, ) { match non_atomic_type { - NonAtomicType::FunctionLiteral(function) => visitor.visit_function_type(db, function), + NonAtomicType::FunctionLiteral(function) => { + visitor.visit_function_type(db, function); + } NonAtomicType::Intersection(intersection) => { visitor.visit_intersection_type(db, intersection); } @@ -296,13 +299,21 @@ pub(super) fn walk_non_atomic_type<'db, V: TypeVisitor<'db> + ?Sized>( 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::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::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); } @@ -321,7 +332,9 @@ pub(super) fn walk_non_atomic_type<'db, V: TypeVisitor<'db> + ?Sized>( NonAtomicType::ProtocolInstance(protocol) => { visitor.visit_protocol_instance_type(db, protocol); } - NonAtomicType::TypedDict(typed_dict) => visitor.visit_typed_dict_type(db, typed_dict), + NonAtomicType::TypedDict(typed_dict) => { + visitor.visit_typed_dict_type(db, typed_dict); + } NonAtomicType::TypeAlias(alias) => { visitor.visit_type_alias_type(db, alias); } @@ -356,7 +369,7 @@ pub(crate) fn walk_type_with_recursion_guard<'db>( pub(crate) struct TypeCollector<'db>(RefCell>); impl<'db> TypeCollector<'db> { - pub(crate) fn type_was_already_seen(&self, ty: Type<'db>) -> bool { + fn type_was_already_seen(&self, ty: Type<'db>) -> bool { !self.0.borrow_mut().insert(ty) } } @@ -379,7 +392,7 @@ impl Default for SmallSet { impl SmallSet { #[inline] - pub(super) fn insert(&mut self, value: T) -> bool + fn insert(&mut self, value: T) -> bool where T: Hash + Eq, { @@ -414,17 +427,17 @@ impl SmallSet { } #[cfg(test)] - pub(super) const fn is_spilled(&self) -> bool { + const fn is_spilled(&self) -> bool { matches!(self, Self::Spilled(_)) } } -/// Whether a type contains a non-`Any` dynamic type. +/// Whether a type contains a dynamic type matching the requested filter. #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub(super) enum DynamicContent { - /// The type was fully inspected and contains no non-`Any` dynamic type. + /// The type was fully inspected and contains no matching dynamic type. Absent, - /// The type contains a non-`Any` dynamic type. + /// The type contains a matching dynamic type. Present, /// Recursive specialization prevented the type from being fully inspected. Indeterminate, @@ -436,6 +449,15 @@ impl DynamicContent { } } +/// Determine whether `ty` contains any dynamic type. +pub(super) fn dynamic_content<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> DynamicContent { + dynamic_content_impl(db, env, ty, true) +} + /// Determine whether `ty` contains a dynamic type other than `Any`. /// /// Class-based protocol interfaces can be recursively specialized. An exact recursive cycle adds @@ -452,14 +474,31 @@ impl DynamicContent { /// /// Walking `Exact[int]` can skip its exact back-edge. Walking `Growing[int]` is indeterminate /// because each recursive edge creates a new specialization. -pub(super) fn non_any_dynamic_content<'db>(db: &'db dyn Db, ty: Type<'db>) -> DynamicContent { - struct DynamicContentVisitor<'db> { +pub(super) fn non_any_dynamic_content<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> DynamicContent { + dynamic_content_impl(db, env, ty, false) +} + +fn dynamic_content_impl<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + include_any: bool, +) -> DynamicContent { + struct DynamicContentVisitor<'a, 'db> { + env: &'a ProgramEnvironment<'db>, recursion_guard: TypeCollector<'db>, active_class_protocols: ActiveRecursionDetector>, + active_class_typed_dicts: ActiveRecursionDetector>, + active_type_aliases: ActiveRecursionDetector>, content: Cell, + include_any: bool, } - impl DynamicContentVisitor<'_> { + impl DynamicContentVisitor<'_, '_> { fn record(&self, content: DynamicContent) { debug_assert!(self.content.get().is_absent()); debug_assert!(!content.is_absent()); @@ -467,7 +506,11 @@ pub(super) fn non_any_dynamic_content<'db>(db: &'db dyn Db, ty: Type<'db>) -> Dy } } - impl<'db> TypeVisitor<'db> for DynamicContentVisitor<'db> { + impl<'db> TypeVisitor<'db> for DynamicContentVisitor<'_, 'db> { + fn program_environment(&self) -> &ProgramEnvironment<'db> { + self.env + } + fn should_visit_lazy_type_attributes(&self) -> bool { true } @@ -477,7 +520,10 @@ pub(super) fn non_any_dynamic_content<'db>(db: &'db dyn Db, ty: Type<'db>) -> Dy return; } - if ty.is_dynamic() && !matches!(ty, Type::Dynamic(crate::types::DynamicType::Any)) { + if ty.is_dynamic() + && (self.include_any + || !matches!(ty, Type::Dynamic(crate::types::DynamicType::Any))) + { self.record(DynamicContent::Present); return; } @@ -485,13 +531,21 @@ pub(super) fn non_any_dynamic_content<'db>(db: &'db dyn Db, ty: Type<'db>) -> Dy walk_type_with_recursion_guard(db, ty, self, &self.recursion_guard); } + fn visit_type_alias_type(&self, db: &'db dyn Db, alias: TypeAliasType<'db>) { + self.active_type_aliases.visit( + &alias.definition(db), + || self.record(DynamicContent::Indeterminate), + || walk_type_alias_type(db, alias, self), + ); + } + fn visit_protocol_instance_type( &self, db: &'db dyn Db, protocol: ProtocolInstanceType<'db>, ) { let protocol_ty = Type::ProtocolInstance(protocol); - let Some(class) = protocol.as_class_based() else { + let Some(class) = protocol.class_origin(db) else { walk_protocol_instance_interface(db, protocol.interface(db), protocol_ty, self); return; }; @@ -514,15 +568,38 @@ pub(super) fn non_any_dynamic_content<'db>(db: &'db dyn Db, ty: Type<'db>) -> Dy self.active_class_protocols.visit( &origin, || self.record(DynamicContent::Indeterminate), - || walk_protocol_instance_interface(db, protocol.interface(db), protocol_ty, self), + || { + walk_protocol_instance_interface(db, protocol.interface(db), protocol_ty, self); + }, + ); + } + + fn visit_typed_dict_type(&self, db: &'db dyn Db, typed_dict: TypedDictType<'db>) { + let Some(class) = typed_dict.defining_class() else { + walk_typed_dict_type(db, typed_dict, self); + return; + }; + let Some((origin, _)) = class.static_class_literal(db) else { + walk_typed_dict_type(db, typed_dict, self); + return; + }; + + self.active_class_typed_dicts.visit( + &origin, + || self.record(DynamicContent::Indeterminate), + || walk_typed_dict_type(db, typed_dict, self), ); } } let visitor = DynamicContentVisitor { + env, recursion_guard: TypeCollector::default(), active_class_protocols: ActiveRecursionDetector::default(), + active_class_typed_dicts: ActiveRecursionDetector::default(), + active_type_aliases: ActiveRecursionDetector::default(), content: Cell::new(DynamicContent::Absent), + include_any, }; visitor.visit_type(db, ty); visitor.content.get() @@ -531,6 +608,7 @@ pub(super) fn non_any_dynamic_content<'db>(db: &'db dyn Db, ty: Type<'db>) -> Dy /// Implementation for `any_over_type` and `find_over_type`. fn any_over_type_impl<'db, F, T>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, should_visit_lazy_type_attributes: bool, query: F, @@ -540,6 +618,7 @@ where F: Fn(Type<'db>) -> T, { struct AnyOverTypeVisitor<'db, 'a, U> { + env: &'a ProgramEnvironment<'db>, query: &'a dyn Fn(Type<'db>) -> U, recursion_guard: TypeCollector<'db>, found_matching_type: Cell, @@ -550,6 +629,10 @@ where where U: Copy + Default + PartialEq, { + fn program_environment(&self) -> &ProgramEnvironment<'db> { + self.env + } + fn should_visit_lazy_type_attributes(&self) -> bool { self.should_visit_lazy_type_attributes } @@ -570,6 +653,7 @@ where } let visitor = AnyOverTypeVisitor { + env, query: &query, recursion_guard: TypeCollector::default(), found_matching_type: Cell::default(), @@ -589,11 +673,12 @@ where /// are visited or not. pub(super) fn any_over_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, should_visit_lazy_type_attributes: bool, query: impl Fn(Type<'db>) -> bool, ) -> bool { - any_over_type_impl(db, ty, should_visit_lazy_type_attributes, query) + any_over_type_impl(db, env, ty, should_visit_lazy_type_attributes, query) } /// Like [`any_over_type`], but treats `Self` as an atom: `query` still sees it, but its upper @@ -605,16 +690,22 @@ pub(super) fn any_over_type<'db>( /// asking "which type variables does this type leave unsolved?" want `Self` treated as one atom. pub(super) fn any_over_type_with_opaque_self<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, query: impl Fn(Type<'db>) -> bool, ) -> bool { struct OpaqueSelfVisitor<'db, 'a> { + env: &'a ProgramEnvironment<'db>, query: &'a dyn Fn(Type<'db>) -> bool, recursion_guard: TypeCollector<'db>, found: Cell, } impl<'db> TypeVisitor<'db> for OpaqueSelfVisitor<'db, '_> { + fn program_environment(&self) -> &ProgramEnvironment<'db> { + self.env + } + fn should_visit_lazy_type_attributes(&self) -> bool { false } @@ -635,6 +726,7 @@ pub(super) fn any_over_type_with_opaque_self<'db>( } let visitor = OpaqueSelfVisitor { + env, query: &query, recursion_guard: TypeCollector::default(), found: Cell::new(false), @@ -658,6 +750,7 @@ pub(super) fn any_over_type_with_opaque_self<'db>( /// are visited or not. pub(super) fn find_over_type<'db, T>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, should_visit_lazy_type_attributes: bool, query: impl Fn(Type<'db>) -> Option, @@ -665,7 +758,7 @@ pub(super) fn find_over_type<'db, T>( where T: Copy + PartialEq, { - any_over_type_impl(db, ty, should_visit_lazy_type_attributes, query) + any_over_type_impl(db, env, ty, should_visit_lazy_type_attributes, query) } #[cfg(test)] diff --git a/crates/ty_python_semantic/tests/corpus.rs b/crates/ty_python_semantic/tests/corpus.rs index 5d8109fd04..e85eb25720 100644 --- a/crates/ty_python_semantic/tests/corpus.rs +++ b/crates/ty_python_semantic/tests/corpus.rs @@ -1,37 +1,29 @@ use std::sync::Arc; 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_module_resolver::SearchPathSettings; -use ty_python_core::platform::PythonPlatform; -use ty_python_core::program::{FallibleStrategy, Program, ProgramSettings}; +use ty_python_core::program::ProgramSettings; use ty_python_semantic::lint::{LintRegistry, RuleSelection}; use ty_python_semantic::pull_types::pull_types; -use ty_python_semantic::{AnalysisSettings, check_file_unwrap, default_lint_registry}; -use ty_site_packages::{PythonVersionSource, PythonVersionWithSource}; +use ty_python_semantic::{ + AnalysisSettings, Db as _, PythonVersionWithSource, check_file_unwrap, default_lint_registry, +}; use ruff_db::diagnostic::Diagnostic; use test_case::test_case; -use ty_python_core::Db as _; - -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()) +use ty_python_core::{Db as _, ProgramFile, TestProgramDb}; + +fn get_cargo_workspace_root() -> anyhow::Result<&'static SystemPath> { + SystemPath::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(SystemPath::parent) + .context("Failed to determine the Cargo workspace root") } -/// Test that all snippets in testcorpus can be checked without panic (except for [`KNOWN_FAILURES`]) +/// Test that all snippets in testcorpus can be checked without panic. #[test] fn corpus_no_panic() -> anyhow::Result<()> { let crate_root = String::from(env!("CARGO_MANIFEST_DIR")); @@ -102,17 +94,6 @@ fn run_corpus_tests(pattern: &str) -> anyhow::Result<()> { 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(); @@ -127,29 +108,11 @@ fn run_corpus_tests(pattern: &str) -> anyhow::Result<()> { // (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" || e == "byi") - .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" - ); + if let Err(err) = std::panic::catch_unwind(|| { + pull_types(&db, db.program_file(file)); + }) { + println!("Check failed for {relative_path:?}."); + std::panic::resume_unwind(err); } db.memory_file_system().remove_file(path).unwrap(); @@ -174,11 +137,6 @@ fn run_corpus_tests(pattern: &str) -> anyhow::Result<()> { Ok(()) } -/// Whether or not the .py/.pyi version of this file is expected to fail -#[rustfmt::skip] -const KNOWN_FAILURES: &[(&str, bool, bool)] = &[ -]; - #[salsa::db] #[derive(Clone)] pub struct CorpusDb { @@ -188,35 +146,23 @@ pub struct CorpusDb { system: TestSystem, vendored: VendoredFileSystem, analysis_settings: Arc, + program_settings: ProgramSettings, } impl CorpusDb { #[expect(clippy::new_without_default)] pub fn new() -> Self { - let db = Self { + let vendored = ty_vendored::file_system().clone(); + let program_settings = ProgramSettings::empty(&vendored); + Self { storage: salsa::Storage::new(None), system: TestSystem::default(), - vendored: ty_vendored::file_system().clone(), + vendored, rule_selection: RuleSelection::from_registry(default_lint_registry()), files: Files::default(), analysis_settings: Arc::new(AnalysisSettings::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(), &FallibleStrategy) - .unwrap(), - }, - ); - - db + program_settings, + } } } @@ -243,18 +189,10 @@ impl ruff_db::Db for CorpusDb { fn files(&self) -> &Files { &self.files } - - fn python_version(&self) -> PythonVersion { - Program::get(self).python_version(self) - } } #[salsa::db] -impl ty_module_resolver::Db for CorpusDb { - fn search_paths(&self) -> &ty_module_resolver::SearchPaths { - Program::get(self).search_paths(self) - } -} +impl ty_module_resolver::Db for CorpusDb {} #[salsa::db] impl ty_python_core::Db for CorpusDb { @@ -263,16 +201,31 @@ impl ty_python_core::Db for CorpusDb { } } +#[salsa::db] +impl TestProgramDb for CorpusDb { + fn program_settings(&self) -> &ProgramSettings { + &self.program_settings + } +} + #[salsa::db] impl ty_python_semantic::Db for CorpusDb { fn check_file(&self, file: File) -> Vec { if self.should_check_file(file) { - check_file_unwrap(self, file) + check_file_unwrap(self, self.program_file(file)) } else { Vec::new() } } + fn program_file(&self, file: File) -> ProgramFile<'_> { + self.program().program_file(self, file) + } + + fn python_version_with_source(&self, _file: File) -> &PythonVersionWithSource { + &self.program_settings.python_version + } + fn rule_selection(&self, _file: File) -> &RuleSelection { &self.rule_selection } diff --git a/crates/ty_server/Cargo.toml b/crates/ty_server/Cargo.toml index d2887fbce5..7c423fb5bc 100644 --- a/crates/ty_server/Cargo.toml +++ b/crates/ty_server/Cargo.toml @@ -28,6 +28,7 @@ ty_combine = { workspace = true } ty_ide = { workspace = true } ty_module_resolver = { workspace = true } ty_project = { workspace = true } +ty_python_semantic = { workspace = true } anyhow = { workspace = true } bitflags = { workspace = true } diff --git a/crates/ty_server/src/capabilities.rs b/crates/ty_server/src/capabilities.rs index a7e413338b..19b1c9bb0f 100644 --- a/crates/ty_server/src/capabilities.rs +++ b/crates/ty_server/src/capabilities.rs @@ -36,6 +36,8 @@ bitflags::bitflags! { const PREFER_MARKDOWN_IN_COMPLETION = 1 << 18; const COMPLETION_ITEM_SNIPPET_SUPPORT = 1 << 19; const FULL_DIAGNOSTIC_OUTPUT = 1 << 20; + const IMPLEMENTATION_LINK_SUPPORT = 1 << 21; + const TRIGGER_SIGNATURE_HELP_COMMAND = 1 << 22; } } @@ -126,6 +128,11 @@ impl ResolvedClientCapabilities { self.contains(Self::DECLARATION_LINK_SUPPORT) } + /// Returns `true` if the client supports location links in goto implementation. + pub(crate) const fn supports_implementation_link(self) -> bool { + self.contains(Self::IMPLEMENTATION_LINK_SUPPORT) + } + /// Returns `true` if the client prefers markdown in hover responses. pub(crate) const fn prefers_markdown_in_hover(self) -> bool { self.contains(Self::PREFER_MARKDOWN_IN_HOVER) @@ -199,6 +206,11 @@ impl ResolvedClientCapabilities { self.contains(Self::PREFER_MARKDOWN_IN_COMPLETION) } + /// Returns `true` if the client supports the `ty.triggerParameterHints` completion command. + pub(crate) const fn supports_trigger_parameter_hints_command(self) -> bool { + self.contains(Self::TRIGGER_SIGNATURE_HELP_COMMAND) + } + pub(super) fn new(client_capabilities: &ClientCapabilities) -> Self { let mut flags = Self::empty(); @@ -270,6 +282,19 @@ impl ResolvedClientCapabilities { flags |= Self::FULL_DIAGNOSTIC_OUTPUT; } + if client_capabilities + .experimental + .as_ref() + .and_then(|experimental| experimental.get("commands")?.get("commands")?.as_array()) + .is_some_and(|commands| { + commands + .iter() + .any(|command| command.as_str() == Some("ty.triggerParameterHints")) + }) + { + flags |= Self::TRIGGER_SIGNATURE_HELP_COMMAND; + } + if text_document .and_then(|text_document| text_document.type_definition?.link_support) .unwrap_or_default() @@ -291,6 +316,13 @@ impl ResolvedClientCapabilities { flags |= Self::DECLARATION_LINK_SUPPORT; } + if text_document + .and_then(|text_document| text_document.implementation?.link_support) + .unwrap_or_default() + { + flags |= Self::IMPLEMENTATION_LINK_SUPPORT; + } + if text_document .and_then(|document| document.hover.as_ref()) .and_then(|hover| preferred_markup_kind(hover.content_format.as_deref()?)) @@ -447,6 +479,7 @@ pub(crate) fn server_capabilities( type_definition_provider: Some(true.into()), definition_provider: Some(true.into()), declaration_provider: Some(true.into()), + implementation_provider: Some(true.into()), references_provider: Some(true.into()), rename_provider: Some(server_rename_options().into()), document_highlight_provider: Some(true.into()), @@ -553,7 +586,7 @@ pub(crate) fn server_diagnostic_options(workspace_diagnostics: bool) -> Diagnost } } -pub(crate) fn server_rename_options() -> RenameOptions { +fn server_rename_options() -> RenameOptions { RenameOptions { prepare_provider: Some(true), work_done_progress_options: WorkDoneProgressOptions::default(), diff --git a/crates/ty_server/src/document/notebook.rs b/crates/ty_server/src/document/notebook.rs index c538a7b7c6..e160d422eb 100644 --- a/crates/ty_server/src/document/notebook.rs +++ b/crates/ty_server/src/document/notebook.rs @@ -40,7 +40,7 @@ struct NotebookCell { } impl NotebookDocument { - pub fn new( + pub(crate) fn new( uri: lsp_types::Uri, notebook_version: DocumentVersion, cells: Vec, @@ -72,7 +72,7 @@ impl NotebookDocument { let cells = self .cells .iter() - .map(|cell| { + .filter_map(|cell| { let cell_text = if let Ok(document) = index.document(&DocumentKey::from_uri(&cell.uri)) { if let Some(text_document) = document.as_text() { @@ -89,23 +89,30 @@ impl NotebookDocument { let source = ruff_notebook::SourceValue::String(cell_text); match cell.kind { - NotebookCellKind::Code => ruff_notebook::Cell::Code(ruff_notebook::CodeCell { - execution_count: cell - .execution_summary - .as_ref() - .map(|summary| i64::from(summary.execution_order)), - id: None, - metadata: CellMetadata::default(), - outputs: vec![], - source, - }), + NotebookCellKind::Code => { + Some(ruff_notebook::Cell::Code(ruff_notebook::CodeCell { + execution_count: cell + .execution_summary + .as_ref() + .map(|summary| i64::from(summary.execution_order)), + id: None, + metadata: CellMetadata::default(), + outputs: vec![], + source, + })) + } NotebookCellKind::Markup => { - ruff_notebook::Cell::Markdown(ruff_notebook::MarkdownCell { + Some(ruff_notebook::Cell::Markdown(ruff_notebook::MarkdownCell { attachments: None, id: None, metadata: CellMetadata::default(), source, - }) + })) + } + NotebookCellKind::Custom(_) => { + // Ignore unsupported cell kinds. This arm should never be reached unless a + // client sends a value which is not mentioned/supported in the LSP. + None } } }) @@ -117,8 +124,12 @@ impl NotebookDocument { nbformat_minor: 5, }; - ruff_notebook::Notebook::from_raw_notebook(raw_notebook, false) - .unwrap_or_else(|err| panic!("Server notebook document could not be converted to ty's notebook document format: {err}")) + ruff_notebook::Notebook::from_raw_notebook(raw_notebook, false).unwrap_or_else(|err| { + panic!( + "Server notebook document could not be converted to ty's \ + notebook document format: {err}" + ) + }) } pub(crate) fn update( @@ -190,7 +201,7 @@ impl NotebookDocument { } impl NotebookCell { - pub(crate) fn new(cell: lsp_types::NotebookCell) -> Self { + fn new(cell: lsp_types::NotebookCell) -> Self { Self { uri: cell.document, kind: cell.kind, diff --git a/crates/ty_server/src/document/range.rs b/crates/ty_server/src/document/range.rs index f98486064f..fb34bbe6ec 100644 --- a/crates/ty_server/src/document/range.rs +++ b/crates/ty_server/src/document/range.rs @@ -75,7 +75,7 @@ impl LspPosition { /// Returns the uri of the text document this position belongs to. #[expect(unused)] - pub(crate) fn uri(&self) -> Option<&lsp_types::Uri> { + fn uri(&self) -> Option<&lsp_types::Uri> { self.uri.as_ref() } } diff --git a/crates/ty_server/src/document/text_document.rs b/crates/ty_server/src/document/text_document.rs index d876d33aa2..1ff775a6c9 100644 --- a/crates/ty_server/src/document/text_document.rs +++ b/crates/ty_server/src/document/text_document.rs @@ -36,7 +36,7 @@ pub struct TextDocument { } #[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum LanguageId { +pub(crate) enum LanguageId { Python, /// A django template. Not python, but the server still has language services /// for it — see [`ty_ide::django_template_completions`]. @@ -89,7 +89,7 @@ impl LanguageId { } impl TextDocument { - pub fn new( + pub(crate) fn new( uri: Uri, contents: String, version: DocumentVersion, @@ -113,23 +113,19 @@ impl TextDocument { self } - pub fn into_contents(self) -> String { - self.contents - } - pub(crate) fn uri(&self) -> &Uri { &self.uri } - pub fn contents(&self) -> &str { + pub(crate) fn contents(&self) -> &str { &self.contents } - pub fn version(&self) -> DocumentVersion { + pub(crate) fn version(&self) -> DocumentVersion { self.version } - pub fn language_id(&self) -> LanguageId { + pub(crate) fn language_id(&self) -> LanguageId { self.language_id } @@ -137,7 +133,7 @@ impl TextDocument { self.notebook.as_ref() } - pub fn apply_changes( + pub(crate) fn apply_changes( &mut self, changes: Vec, new_version: DocumentVersion, @@ -187,7 +183,7 @@ impl TextDocument { }); } - pub fn update_version(&mut self, new_version: DocumentVersion) { + pub(crate) fn update_version(&mut self, new_version: DocumentVersion) { self.modify(|_, version| { *version = new_version; }); diff --git a/crates/ty_server/src/lib.rs b/crates/ty_server/src/lib.rs index 96e4ef5f7f..da4f138448 100644 --- a/crates/ty_server/src/lib.rs +++ b/crates/ty_server/src/lib.rs @@ -19,7 +19,7 @@ mod server; mod session; mod system; -pub(crate) const SERVER_NAME: &str = "ty"; +const SERVER_NAME: &str = "ty"; pub(crate) const DIAGNOSTIC_NAME: &str = "ty"; /// A common result type used in most cases where a diff --git a/crates/ty_server/src/server/api.rs b/crates/ty_server/src/server/api.rs index a615a147eb..5fb14e7013 100644 --- a/crates/ty_server/src/server/api.rs +++ b/crates/ty_server/src/server/api.rs @@ -55,6 +55,11 @@ pub(super) fn request(req: server::Request) -> Task { >( req, BackgroundSchedule::Worker ), + requests::GotoImplementationRequestHandler::METHOD => background_document_request_task::< + requests::GotoImplementationRequestHandler, + >( + req, BackgroundSchedule::Worker + ), requests::GotoDefinitionRequestHandler::METHOD => background_document_request_task::< requests::GotoDefinitionRequestHandler, >(req, BackgroundSchedule::Worker), @@ -491,8 +496,11 @@ where 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.") + 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::InvalidParams) @@ -553,8 +561,11 @@ where 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.") + 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::InvalidParams)?, @@ -578,7 +589,7 @@ impl> LSPResult for core::result::Result { } impl Error { - pub(crate) fn new(err: anyhow::Error, code: server::ErrorCode) -> Self { + fn new(err: anyhow::Error, code: server::ErrorCode) -> Self { Self { code, error: err } } } diff --git a/crates/ty_server/src/server/api/diagnostics.rs b/crates/ty_server/src/server/api/diagnostics.rs index 960070cb32..4472592957 100644 --- a/crates/ty_server/src/server/api/diagnostics.rs +++ b/crates/ty_server/src/server/api/diagnostics.rs @@ -19,7 +19,7 @@ use ruff_db::files::{File, FileRange}; use ruff_db::source::source_text; use ruff_db::system::SystemPathBuf; use serde::{Deserialize, Serialize}; -use ty_project::{Db as _, ProjectDatabase}; +use ty_project::{Db as _, ProjectDatabase, SemanticDb as _}; use crate::capabilities::ResolvedClientCapabilities; use crate::document::{FileRangeExt, ToRangeExt}; @@ -418,7 +418,7 @@ pub(super) fn compute_diagnostics( // one `by check` registers too let diagnostics = db.check_file(file); - let unnecessary_hints = hints(db, file); + let unnecessary_hints = hints(db, db.program_file(file)); Some(Diagnostics { items: diagnostics, @@ -557,9 +557,9 @@ pub(super) fn to_lsp_diagnostic( .primary_annotation() .and_then(|annotation| annotation.get_message()) { - format!("{}: {annotation_message}", diagnostic.primary_message()) + format!("{}: {annotation_message}", diagnostic.headline_message()) } else { - diagnostic.primary_message().to_string() + diagnostic.headline_message().to_string() } } else { diagnostic.concise_message().to_string() 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 index a04b19cdad..b0e866838d 100644 --- 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 @@ -59,6 +59,8 @@ impl SyncNotificationHandler for DidChangeWatchedFiles { path: system_path, kind: DeletedKind::Any, }, + // Custom file change types are not supported and should be ignored. + FileChangeType::Custom(_) => continue, }; changes.push(change_event); diff --git a/crates/ty_server/src/server/api/requests.rs b/crates/ty_server/src/server/api/requests.rs index 5d3d207892..4931d1224d 100644 --- a/crates/ty_server/src/server/api/requests.rs +++ b/crates/ty_server/src/server/api/requests.rs @@ -24,6 +24,7 @@ mod execute_command; mod folding_range; mod goto_declaration; mod goto_definition; +mod goto_implementation; mod goto_type_definition; mod hover; mod inlay_hints; @@ -54,6 +55,7 @@ pub(super) use execute_command::ExecuteCommand; pub(super) use folding_range::FoldingRangeRequestHandler; pub(super) use goto_declaration::GotoDeclarationRequestHandler; pub(super) use goto_definition::GotoDefinitionRequestHandler; +pub(super) use goto_implementation::GotoImplementationRequestHandler; pub(super) use goto_type_definition::GotoTypeDefinitionRequestHandler; pub(super) use hover::HoverRequestHandler; pub(super) use inlay_hints::InlayHintRequestHandler; diff --git a/crates/ty_server/src/server/api/requests/call_hierarchy_incoming_calls.rs b/crates/ty_server/src/server/api/requests/call_hierarchy_incoming_calls.rs index b5ea24c099..efdef9bff3 100644 --- a/crates/ty_server/src/server/api/requests/call_hierarchy_incoming_calls.rs +++ b/crates/ty_server/src/server/api/requests/call_hierarchy_incoming_calls.rs @@ -1,5 +1,6 @@ use lsp_types::CallHierarchyIncomingCallsRequest; use lsp_types::{CallHierarchyIncomingCall, CallHierarchyIncomingCallsParams}; +use ty_project::SemanticDb as _; use crate::document::{ToRangeExt as _, resolve_file_uri_range}; use crate::server::api::requests::prepare_call_hierarchy::convert_to_lsp_item; @@ -40,7 +41,7 @@ impl BackgroundRequestHandler for CallHierarchyIncomingCallsRequestHandler { continue; }; - for call in ty_ide::incoming_calls(db, file, offset) { + for call in ty_ide::incoming_calls(db, db.program_file(file), offset) { // `from_ranges` are byte offsets into `call.from.file` (the caller), // NOT into `file` (the prepared/queried symbol). Capture the caller // file before moving `call.from` into `convert_to_lsp_item`. diff --git a/crates/ty_server/src/server/api/requests/call_hierarchy_outgoing_calls.rs b/crates/ty_server/src/server/api/requests/call_hierarchy_outgoing_calls.rs index c8da7c21ed..8562786ad0 100644 --- a/crates/ty_server/src/server/api/requests/call_hierarchy_outgoing_calls.rs +++ b/crates/ty_server/src/server/api/requests/call_hierarchy_outgoing_calls.rs @@ -1,5 +1,6 @@ use lsp_types::CallHierarchyOutgoingCallsRequest; use lsp_types::{CallHierarchyOutgoingCall, CallHierarchyOutgoingCallsParams}; +use ty_project::SemanticDb as _; use crate::document::{ToRangeExt as _, resolve_file_uri_range}; use crate::server::api::requests::prepare_call_hierarchy::convert_to_lsp_item; @@ -40,7 +41,7 @@ impl BackgroundRequestHandler for CallHierarchyOutgoingCallsRequestHandler { continue; }; - for call in ty_ide::outgoing_calls(db, file, offset) { + for call in ty_ide::outgoing_calls(db, db.program_file(file), offset) { let Some(to) = convert_to_lsp_item(db, call.to, encoding) else { continue; }; diff --git a/crates/ty_server/src/server/api/requests/code_action.rs b/crates/ty_server/src/server/api/requests/code_action.rs index 513cb045d9..0ba9582123 100644 --- a/crates/ty_server/src/server/api/requests/code_action.rs +++ b/crates/ty_server/src/server/api/requests/code_action.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; use lsp_types::{self as types, Code, CodeActionRequest, CodeActionResponse, TextEdit, Uri}; use ruff_text_size::Ranged; use ty_ide::{FileEdit, code_actions}; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; use types::CodeActionKind; use crate::db::Db; @@ -39,6 +39,7 @@ impl BackgroundDocumentRequestHandler for CodeActionRequestHandler { let Some(file) = snapshot.to_notebook_or_file(db) else { return Ok(None); }; + let program_file = db.program_file(file); let mut actions = Vec::new(); for mut diagnostic in diagnostics.into_iter().filter(|diagnostic| { @@ -99,7 +100,7 @@ impl BackgroundDocumentRequestHandler for CodeActionRequestHandler { { for action in code_actions( db, - file, + program_file, range, &diagnostic_id, snapshot.is_django_template(), diff --git a/crates/ty_server/src/server/api/requests/completion.rs b/crates/ty_server/src/server/api/requests/completion.rs index c817a52daf..e4f0e97d44 100644 --- a/crates/ty_server/src/server/api/requests/completion.rs +++ b/crates/ty_server/src/server/api/requests/completion.rs @@ -2,15 +2,20 @@ use std::borrow::Cow; use std::time::Instant; use lsp_types::{ - CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionList, + Command, CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionList, CompletionParams, CompletionRequest, CompletionResponse, Documentation, InsertTextFormat, TextEdit, Uri, }; use ruff_source_file::OneIndexed; use ruff_text_size::Ranged; -use ty_ide::{CompletionCapabilities, CompletionInsertTextFormat, CompletionKind, completion}; -use ty_project::ProjectDatabase; +use ty_ide::{ + CompletionCapabilities, CompletionCommand, CompletionInsertTextFormat, CompletionKind, + completion, +}; +use ty_project::{ProjectDatabase, SemanticDb as _}; +use ty_python_semantic::ProgramEnvironment; +use crate::capabilities::ResolvedClientCapabilities; use crate::document::{PositionExt, ToRangeExt}; use crate::server::api::traits::{ BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler, @@ -69,12 +74,15 @@ impl BackgroundDocumentRequestHandler for CompletionRequestHandler { } let client_capabilities = snapshot.resolved_client_capabilities(); + let program_file = db.program_file(file); + let env = ProgramEnvironment::from_file(program_file); let completions = completion( db, + &env, snapshot.workspace_settings().completions(), CompletionCapabilities::default() .snippets(client_capabilities.supports_completion_item_snippets()), - file, + program_file, offset, ); if completions.is_empty() { @@ -92,7 +100,7 @@ impl BackgroundDocumentRequestHandler for CompletionRequestHandler { // own few words instead let type_display = comp .ty - .map(|ty| ty.display(db).to_string()) + .map(|ty| ty.display(db, &env).to_string()) .or_else(|| comp.detail.as_ref().map(ToString::to_string)); let import_edit = comp.import.as_ref().and_then(|edit| { let range = edit @@ -170,6 +178,9 @@ impl BackgroundDocumentRequestHandler for CompletionRequestHandler { text_edit, additional_text_edits: import_edit.map(|edit| vec![edit]), documentation, + command: comp + .command + .and_then(|command| to_lsp_command(command, client_capabilities)), ..Default::default() } }) @@ -223,7 +234,8 @@ fn django_template_completions( file: ruff_db::files::File, offset: ruff_text_size::TextSize, ) -> Option { - let completions = ty_ide::django_template_completions(db, file, offset); + let env = ProgramEnvironment::from_file(db.program_file(file)); + let completions = ty_ide::django_template_completions(db, &env, file, offset); if completions.is_empty() { return None; } @@ -303,6 +315,30 @@ fn django_template_completions( })) } +/// Maps an editor-neutral completion intent to the concrete LSP command the +/// client should run after applying the completion. +/// +/// The intent itself is decided in `ty_ide`; this is the single place that knows +/// any editor-specific command identifiers. +/// +/// Returns `None` when the client has not advertised support for the command, +/// so that clients without a handler never receive one. +fn to_lsp_command( + command: CompletionCommand, + client_capabilities: ResolvedClientCapabilities, +) -> Option { + match command { + CompletionCommand::TriggerSignatureHelp => client_capabilities + .supports_trigger_parameter_hints_command() + .then(|| Command { + title: "Trigger parameter hints".into(), + tooltip: None, + command: "ty.triggerParameterHints".into(), + arguments: None, + }), + } +} + fn ty_kind_to_lsp_kind(kind: CompletionKind) -> CompletionItemKind { // Gimme my dang globs in tight scopes! #[allow(clippy::enum_glob_use)] diff --git a/crates/ty_server/src/server/api/requests/doc_highlights.rs b/crates/ty_server/src/server/api/requests/doc_highlights.rs index 3ced9d9588..f9f5e3a202 100644 --- a/crates/ty_server/src/server/api/requests/doc_highlights.rs +++ b/crates/ty_server/src/server/api/requests/doc_highlights.rs @@ -3,7 +3,7 @@ use std::borrow::Cow; use lsp_types::DocumentHighlightRequest; use lsp_types::{DocumentHighlight, DocumentHighlightKind, DocumentHighlightParams, Uri}; use ty_ide::{ReferenceKind, document_highlights}; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; use crate::document::{PositionExt, ToRangeExt}; use crate::server::api::traits::{ @@ -49,7 +49,7 @@ impl BackgroundDocumentRequestHandler for DocumentHighlightRequestHandler { return Ok(None); }; - let Some(highlights_result) = document_highlights(db, file, offset) else { + let Some(highlights_result) = document_highlights(db, db.program_file(file), offset) else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/document_symbols.rs b/crates/ty_server/src/server/api/requests/document_symbols.rs index 578bf62885..aff278821a 100644 --- a/crates/ty_server/src/server/api/requests/document_symbols.rs +++ b/crates/ty_server/src/server/api/requests/document_symbols.rs @@ -7,7 +7,7 @@ use ty_ide::{ HierarchicalSymbols, SymbolId, SymbolInfo, TemplateSymbol, django_template_document_symbols, document_symbols, }; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; use crate::Db; use crate::document::{PositionEncoding, ToRangeExt}; @@ -60,7 +60,7 @@ impl BackgroundDocumentRequestHandler for DocumentSymbolRequestHandler { )); } - let symbols = document_symbols(db, file); + let symbols = document_symbols(db, db.program_file(file)); if symbols.is_empty() { return Ok(None); } diff --git a/crates/ty_server/src/server/api/requests/execute_command.rs b/crates/ty_server/src/server/api/requests/execute_command.rs index 6f4c9d2462..aa7ac45e54 100644 --- a/crates/ty_server/src/server/api/requests/execute_command.rs +++ b/crates/ty_server/src/server/api/requests/execute_command.rs @@ -14,7 +14,6 @@ use std::fmt::{self, Write}; use std::str::FromStr; use ty_module_resolver::ModuleResolveMode; use ty_project::{Db as _, ProjectDatabase}; -use ty_python_core::program::Program; pub(crate) struct ExecuteCommand; @@ -213,7 +212,8 @@ fn interpreter(db: &ProjectDatabase) -> Option { let system = db.system(); - Program::get(db) + db.project() + .program(db) .search_paths(db) .site_packages_paths() .flat_map(|site_packages| site_packages.ancestors().take(MAX_PREFIX_DEPTH + 1)) @@ -255,13 +255,9 @@ fn debug_information(session: &Session) -> crate::Result { for db in session.project_dbs() { writeln!(buffer, "Project at {}", db.project().root(db))?; - let program = Program::get(db); + let program = db.project().program(db); writeln!(buffer, "Program:")?; - writeln!( - buffer, - " python-version: {}", - program.python_version_with_source(db).version - )?; + writeln!(buffer, " python-version: {}", program.python_version(db))?; writeln!(buffer, " python-platform: {}", program.python_platform(db))?; let mut writer = IndentingWriter { inner: &mut buffer, @@ -272,8 +268,8 @@ fn debug_information(session: &Session) -> crate::Result { writer, " search-paths: {:#}", program - .search_paths(db) - .display(db, ModuleResolveMode::Typing) + .resolver_environment(db) + .display_search_paths(db, ModuleResolveMode::Typing) )?; writeln!(buffer, "Settings: {:#?}", db.project().settings(db))?; diff --git a/crates/ty_server/src/server/api/requests/folding_range.rs b/crates/ty_server/src/server/api/requests/folding_range.rs index 9ae0338ab6..6cbde44c63 100644 --- a/crates/ty_server/src/server/api/requests/folding_range.rs +++ b/crates/ty_server/src/server/api/requests/folding_range.rs @@ -5,7 +5,7 @@ use lsp_types::{FoldingRange, FoldingRangeKind, FoldingRangeParams, Uri}; use ruff_db::source::source_text; use ruff_text_size::TextRange; use ty_ide::{django_template_folding_ranges, folding_ranges}; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; use crate::db::Db; use crate::document::ToRangeExt; @@ -59,7 +59,7 @@ impl BackgroundDocumentRequestHandler for FoldingRangeRequestHandler { let ranges = if snapshot.is_django_template() { django_template_folding_ranges(db, file) } else { - folding_ranges(db, file, cell_range) + folding_ranges(db, db.program_file(file).python_file(db), cell_range) }; let results: Vec<_> = ranges diff --git a/crates/ty_server/src/server/api/requests/goto_declaration.rs b/crates/ty_server/src/server/api/requests/goto_declaration.rs index 2dbf8c60aa..95102bc07a 100644 --- a/crates/ty_server/src/server/api/requests/goto_declaration.rs +++ b/crates/ty_server/src/server/api/requests/goto_declaration.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use lsp_types::{DeclarationParams, DeclarationRequest, DeclarationResponse, Uri}; use ty_ide::goto_declaration; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; use crate::document::{PositionExt, ToLink}; use crate::server::api::traits::{ @@ -48,7 +48,7 @@ impl BackgroundDocumentRequestHandler for GotoDeclarationRequestHandler { return Ok(None); }; - let Some(ranged) = goto_declaration(db, file, offset) else { + let Some(ranged) = goto_declaration(db, db.program_file(file), offset) else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/goto_definition.rs b/crates/ty_server/src/server/api/requests/goto_definition.rs index f849d41d3d..56b4aafee1 100644 --- a/crates/ty_server/src/server/api/requests/goto_definition.rs +++ b/crates/ty_server/src/server/api/requests/goto_definition.rs @@ -3,7 +3,7 @@ use std::borrow::Cow; use lsp_types::DefinitionRequest; use lsp_types::{DefinitionParams, DefinitionResponse, Uri}; use ty_ide::{django_template_goto_definition, goto_definition}; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; use crate::document::{PositionExt, ToLink}; use crate::server::api::traits::{ @@ -52,7 +52,7 @@ impl BackgroundDocumentRequestHandler for GotoDefinitionRequestHandler { let definition = if snapshot.is_django_template() { django_template_goto_definition(db, file, offset) } else { - goto_definition(db, file, offset) + goto_definition(db, db.program_file(file), offset) }; let Some(ranged) = definition else { diff --git a/crates/ty_server/src/server/api/requests/goto_implementation.rs b/crates/ty_server/src/server/api/requests/goto_implementation.rs new file mode 100644 index 0000000000..c6f0c723af --- /dev/null +++ b/crates/ty_server/src/server/api/requests/goto_implementation.rs @@ -0,0 +1,77 @@ +use std::borrow::Cow; + +use lsp_types::{ImplementationParams, ImplementationRequest, ImplementationResponse, Uri}; +use ty_ide::goto_implementation; +use ty_project::{ProjectDatabase, SemanticDb as _}; + +use crate::document::{PositionExt, ToLink}; +use crate::server::api::traits::{ + BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler, +}; +use crate::session::DocumentSnapshot; +use crate::session::client::Client; + +pub(crate) struct GotoImplementationRequestHandler; + +impl RequestHandler for GotoImplementationRequestHandler { + type RequestType = ImplementationRequest; +} + +impl BackgroundDocumentRequestHandler for GotoImplementationRequestHandler { + fn document_uri(params: &ImplementationParams) -> Cow<'_, Uri> { + Cow::Borrowed(¶ms.text_document_position_params.text_document.uri) + } + + fn run_with_snapshot( + db: &ProjectDatabase, + snapshot: &DocumentSnapshot, + _client: &Client, + params: ImplementationParams, + ) -> crate::server::Result> { + if snapshot + .workspace_settings() + .is_language_services_disabled() + { + return Ok(None); + } + + let Some(file) = snapshot.to_notebook_or_file(db) else { + return Ok(None); + }; + + let Some(offset) = params.text_document_position_params.position.to_text_size( + db, + file, + snapshot.uri(), + snapshot.encoding(), + ) else { + return Ok(None); + }; + + let Some(ranged) = goto_implementation(db, db.program_file(file), offset) else { + return Ok(None); + }; + + if snapshot + .resolved_client_capabilities() + .supports_implementation_link() + { + let src = Some(ranged.range); + let links: Vec<_> = ranged + .into_iter() + .filter_map(|target| target.to_link(db, src, snapshot.encoding())) + .collect(); + + Ok(Some(links.into())) + } else { + let locations: Vec<_> = ranged + .into_iter() + .filter_map(|target| target.to_location(db, snapshot.encoding())) + .collect(); + + Ok(Some(ImplementationResponse::Definition(locations.into()))) + } + } +} + +impl RetriableRequestHandler for GotoImplementationRequestHandler {} 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 index be0b040ce3..e324327936 100644 --- a/crates/ty_server/src/server/api/requests/goto_type_definition.rs +++ b/crates/ty_server/src/server/api/requests/goto_type_definition.rs @@ -3,7 +3,7 @@ use std::borrow::Cow; use lsp_types::{TypeDefinitionParams, TypeDefinitionRequest}; use lsp_types::{TypeDefinitionResponse, Uri}; use ty_ide::goto_type_definition; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; use crate::document::{PositionExt, ToLink}; use crate::server::api::traits::{ @@ -49,7 +49,7 @@ impl BackgroundDocumentRequestHandler for GotoTypeDefinitionRequestHandler { return Ok(None); }; - let Some(ranged) = goto_type_definition(db, file, offset) else { + let Some(ranged) = goto_type_definition(db, db.program_file(file), offset) else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/hover.rs b/crates/ty_server/src/server/api/requests/hover.rs index 70f09cb0d2..ba1469a579 100644 --- a/crates/ty_server/src/server/api/requests/hover.rs +++ b/crates/ty_server/src/server/api/requests/hover.rs @@ -9,7 +9,7 @@ use crate::session::client::Client; use lsp_types::HoverRequest; use lsp_types::{HoverParams, MarkupContent, Uri}; use ty_ide::{MarkupKind, django_template_hover, hover}; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; pub(crate) struct HoverRequestHandler; @@ -65,7 +65,7 @@ impl BackgroundDocumentRequestHandler for HoverRequestHandler { ) }) } else { - hover(db, file, offset).map(|range_info| { + hover(db, db.program_file(file), offset).map(|range_info| { ( range_info.display(db, markup_kind).to_string(), range_info.range, diff --git a/crates/ty_server/src/server/api/requests/inlay_hints.rs b/crates/ty_server/src/server/api/requests/inlay_hints.rs index 1a7b84632c..684d77c6fe 100644 --- a/crates/ty_server/src/server/api/requests/inlay_hints.rs +++ b/crates/ty_server/src/server/api/requests/inlay_hints.rs @@ -8,7 +8,8 @@ use ty_ide::{ InlayHintKind, InlayHintLabel, InlayHintTextEdit, TemplateInlayHint, TemplateInlayHintKind, django_template_inlay_hints, inlay_hints, }; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; +use ty_python_semantic::ProgramEnvironment; use crate::PositionEncoding; use crate::document::{RangeExt, TextSizeExt, ToLink}; @@ -55,16 +56,26 @@ impl BackgroundDocumentRequestHandler for InlayHintRequestHandler { }; if snapshot.is_django_template() { - let hints = - django_template_inlay_hints(db, file, range, workspace_settings.inlay_hints()) - .into_iter() - .filter_map(|hint| template_inlay_hint(hint, db, file, snapshot.encoding())) - .collect(); + let hints = django_template_inlay_hints( + db, + &ProgramEnvironment::from_file(db.program_file(file)), + file, + range, + workspace_settings.inlay_hints(), + ) + .into_iter() + .filter_map(|hint| template_inlay_hint(hint, db, file, snapshot.encoding())) + .collect(); return Ok(Some(hints)); } - let inlay_hints = inlay_hints(db, file, range, workspace_settings.inlay_hints()); + let inlay_hints = inlay_hints( + db, + db.program_file(file), + range, + workspace_settings.inlay_hints(), + ); let inlay_hints: Vec = inlay_hints .into_iter() diff --git a/crates/ty_server/src/server/api/requests/prepare_call_hierarchy.rs b/crates/ty_server/src/server/api/requests/prepare_call_hierarchy.rs index 55b00ce963..e2029034cb 100644 --- a/crates/ty_server/src/server/api/requests/prepare_call_hierarchy.rs +++ b/crates/ty_server/src/server/api/requests/prepare_call_hierarchy.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use lsp_types::CallHierarchyPrepareRequest; use lsp_types::{CallHierarchyItem, CallHierarchyPrepareParams, Uri}; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; use crate::PositionEncoding; use crate::document::{PositionExt, ToRangeExt as _}; @@ -57,7 +57,7 @@ impl BackgroundDocumentRequestHandler for PrepareCallHierarchyRequestHandler { return Ok(None); }; - let Some(items) = ty_ide::prepare_call_hierarchy(db, file, offset) else { + let Some(items) = ty_ide::prepare_call_hierarchy(db, db.program_file(file), offset) else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/prepare_rename.rs b/crates/ty_server/src/server/api/requests/prepare_rename.rs index 08d92a306b..47867e10bd 100644 --- a/crates/ty_server/src/server/api/requests/prepare_rename.rs +++ b/crates/ty_server/src/server/api/requests/prepare_rename.rs @@ -6,7 +6,7 @@ use lsp_types::{ PrepareRenameParams, PrepareRenamePlaceholder, PrepareRenameRequest, PrepareRenameResult, Uri, }; use ty_ide::{PreparedTemplateRename, can_rename, django_prepare_rename}; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; use crate::document::{PositionExt, ToRangeExt}; use crate::server::api::Error; @@ -57,7 +57,7 @@ impl BackgroundDocumentRequestHandler for PrepareRenameRequestHandler { // a python symbol is answered exactly as it was; the django names a // module writes as plain strings are what is left over once it declines - if !template && let Some(range) = can_rename(db, file, offset) { + if !template && let Some(range) = can_rename(db, db.program_file(file), offset) { return Ok(range .to_lsp_range(db, file, snapshot.encoding()) .map(|lsp_range| lsp_range.local_range().into())); diff --git a/crates/ty_server/src/server/api/requests/prepare_type_hierarchy.rs b/crates/ty_server/src/server/api/requests/prepare_type_hierarchy.rs index c5417e7f49..c5e3289ba1 100644 --- a/crates/ty_server/src/server/api/requests/prepare_type_hierarchy.rs +++ b/crates/ty_server/src/server/api/requests/prepare_type_hierarchy.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use lsp_types::TypeHierarchyPrepareRequest; use lsp_types::{TypeHierarchyItem, TypeHierarchyPrepareParams, Uri}; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; use crate::document::PositionExt; use crate::server::api::traits::{ @@ -58,7 +58,7 @@ impl BackgroundDocumentRequestHandler for PrepareTypeHierarchyRequestHandler { return Ok(None); }; - let Some(item) = ty_ide::prepare_type_hierarchy(db, file, offset) else { + let Some(item) = ty_ide::prepare_type_hierarchy(db, db.program_file(file), offset) else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/references.rs b/crates/ty_server/src/server/api/requests/references.rs index dc7dc791d6..439d216ee0 100644 --- a/crates/ty_server/src/server/api/requests/references.rs +++ b/crates/ty_server/src/server/api/requests/references.rs @@ -3,7 +3,7 @@ use std::borrow::Cow; use lsp_types::ReferencesRequest; use lsp_types::{Location, ReferenceParams, Uri}; use ty_ide::{django_references, find_references}; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; use crate::document::{PositionExt, ToLink}; use crate::server::api::traits::{ @@ -56,7 +56,7 @@ impl BackgroundDocumentRequestHandler for ReferencesRequestHandler { // names a module writes as plain strings are what is left over once it // declines let found = (!template) - .then(|| find_references(db, file, offset, include_declaration)) + .then(|| find_references(db, db.program_file(file), offset, include_declaration)) .flatten() .or_else(|| django_references(db, file, offset, include_declaration, template)); diff --git a/crates/ty_server/src/server/api/requests/rename.rs b/crates/ty_server/src/server/api/requests/rename.rs index 4fcf54c9f1..c90e6ddcae 100644 --- a/crates/ty_server/src/server/api/requests/rename.rs +++ b/crates/ty_server/src/server/api/requests/rename.rs @@ -6,7 +6,7 @@ use lsp_server::ErrorCode; use lsp_types::RenameRequest; use lsp_types::{RenameParams, TextEdit, Uri, WorkspaceEdit}; use ty_ide::{TemplateRename, TemplateRenameOutcome, django_rename, rename}; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; use crate::document::{FileRangeExt, LspRange, PositionExt, ToLink}; use crate::server::api::Error; @@ -57,7 +57,10 @@ impl BackgroundDocumentRequestHandler for RenameRequestHandler { // a python symbol is renamed exactly as it was; the django names a module // writes as plain strings are what is left over once it declines - if !template && let Some(rename_results) = rename(db, file, offset, ¶ms.new_name) { + if !template + && let Some(rename_results) = + rename(db, db.program_file(file), offset, ¶ms.new_name) + { // Group text edits by file let mut changes: HashMap> = HashMap::new(); diff --git a/crates/ty_server/src/server/api/requests/selection_range.rs b/crates/ty_server/src/server/api/requests/selection_range.rs index 904be7ecb5..bfde25ff24 100644 --- a/crates/ty_server/src/server/api/requests/selection_range.rs +++ b/crates/ty_server/src/server/api/requests/selection_range.rs @@ -4,7 +4,7 @@ use lsp_types::{ SelectionRange as LspSelectionRange, SelectionRangeParams, SelectionRangeRequest, Uri, }; use ty_ide::selection_range; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; use crate::document::{PositionExt, ToRangeExt}; use crate::server::api::traits::{ @@ -40,6 +40,7 @@ impl BackgroundDocumentRequestHandler for SelectionRangeRequestHandler { let Some(file) = snapshot.to_notebook_or_file(db) else { return Ok(None); }; + let python_file = db.program_file(file).python_file(db); let mut results = Vec::new(); @@ -49,7 +50,7 @@ impl BackgroundDocumentRequestHandler for SelectionRangeRequestHandler { continue; }; - let ranges = selection_range(db, file, offset); + let ranges = selection_range(db, python_file, offset); if !ranges.is_empty() { // Convert ranges to nested LSP SelectionRange structure let mut lsp_range = None; diff --git a/crates/ty_server/src/server/api/requests/signature_help.rs b/crates/ty_server/src/server/api/requests/signature_help.rs index 2a200f7830..c74c95b0ed 100644 --- a/crates/ty_server/src/server/api/requests/signature_help.rs +++ b/crates/ty_server/src/server/api/requests/signature_help.rs @@ -12,7 +12,8 @@ use lsp_types::{ SignatureHelpParams, SignatureInformation, Uri, }; use ty_ide::{TemplateSignature, django_template_signature_help, signature_help}; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; +use ty_python_semantic::ProgramEnvironment; pub(crate) struct SignatureHelpRequestHandler; @@ -52,7 +53,13 @@ impl BackgroundDocumentRequestHandler for SignatureHelpRequestHandler { }; if snapshot.is_django_template() { - return Ok(django_template_signature_help(db, file, offset).map(template_signature)); + return Ok(django_template_signature_help( + db, + &ProgramEnvironment::from_file(db.program_file(file)), + file, + offset, + ) + .map(template_signature)); } if triggered_by_a_template_character(¶ms) { @@ -67,7 +74,7 @@ impl BackgroundDocumentRequestHandler for SignatureHelpRequestHandler { // 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 { + let Some(signature_help_info) = signature_help(db, db.program_file(file), offset) else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs b/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs index d64ec525f1..4bf6b9121f 100644 --- a/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs +++ b/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs @@ -5,11 +5,10 @@ use std::time::{Duration, Instant}; use lsp_server::RequestId; use lsp_types::WorkspaceDiagnosticRequest; use lsp_types::{ - FullDocumentDiagnosticReport, PreviousResultId, ProgressNotification, ProgressParams, - ProgressToken, UnchangedDocumentDiagnosticReport, Uri, WorkspaceDiagnosticParams, - WorkspaceDiagnosticReport, WorkspaceDiagnosticReportPartialResult, - WorkspaceDocumentDiagnosticReport, WorkspaceFullDocumentDiagnosticReport, - WorkspaceUnchangedDocumentDiagnosticReport, + FullDocumentDiagnosticReport, PreviousResultId, ProgressToken, + UnchangedDocumentDiagnosticReport, Uri, WorkspaceDiagnosticParams, WorkspaceDiagnosticReport, + WorkspaceDiagnosticReportPartialResult, WorkspaceDocumentDiagnosticReport, + WorkspaceFullDocumentDiagnosticReport, WorkspaceUnchangedDocumentDiagnosticReport, }; use ruff_db::diagnostic::Diagnostic; use ruff_db::files::File; @@ -18,7 +17,7 @@ use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use serde_json::json; use ty_ide::{Hint, hints}; -use ty_project::{ProgressReporter, ProjectDatabase}; +use ty_project::{ProgressReporter, ProjectDatabase, SemanticDb as _}; use crate::PositionEncoding; use crate::capabilities::ResolvedClientCapabilities; @@ -240,7 +239,7 @@ impl ProgressReporter for WorkspaceDiagnosticsProgressReporter<'_> { } fn report_checked_file(&self, db: &ProjectDatabase, file: File, diagnostics: &[Diagnostic]) { - let unnecessary_hints = hints(db, file); + let unnecessary_hints = hints(db, db.program_file(file)); // Another thread might have panicked at this point because of a salsa cancellation which // poisoned the result. If the response is poisoned, just don't report and wait for our thread @@ -280,7 +279,7 @@ impl ProgressReporter for WorkspaceDiagnosticsProgressReporter<'_> { } else { tracing::debug!( "Ignoring diagnostic without a file: {diagnostic}", - diagnostic = diagnostic.primary_message() + diagnostic = diagnostic.headline_message() ); } } @@ -288,7 +287,7 @@ impl ProgressReporter for WorkspaceDiagnosticsProgressReporter<'_> { let response = &mut self.state.get_mut().unwrap().response; for (file, diagnostics) in by_file { - let unnecessary_hints = hints(db, file); + let unnecessary_hints = hints(db, db.program_file(file)); response.write_diagnostics_for_file(db, file, &diagnostics, &unnecessary_hints); } response.maybe_flush(); @@ -624,12 +623,17 @@ impl Streaming { .map(WorkspaceDocumentDiagnosticReport::WorkspaceFullDocumentDiagnosticReport) .collect(); - let report = self.create_result(items); + let partial_result = match self.create_result(items) { + WorkspaceDiagnosticReportResult::PartialReport(partial_report) => partial_report, + WorkspaceDiagnosticReportResult::Report(WorkspaceDiagnosticReport { items }) => { + // WorkspaceDiagnosticReport and WorkspaceDiagnosticReportPartialResult have the + // same serialization in the LSP. + // https://github.com/microsoft/language-server-protocol/issues/2281 + WorkspaceDiagnosticReportPartialResult { items } + } + }; self.client - .send_notification::(ProgressParams { - token: self.token.clone(), - value: json!(report), - }); + .send_partial_result::(self.token.clone(), partial_result); self.last_flush = Instant::now(); } diff --git a/crates/ty_server/src/server/api/semantic_tokens.rs b/crates/ty_server/src/server/api/semantic_tokens.rs index 09ccfb04c5..62a8096894 100644 --- a/crates/ty_server/src/server/api/semantic_tokens.rs +++ b/crates/ty_server/src/server/api/semantic_tokens.rs @@ -5,7 +5,7 @@ use ruff_text_size::{Ranged, TextRange}; use ty_ide::{ SemanticTokenModifier, SemanticTokenType, django_template_semantic_tokens, semantic_tokens, }; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; use crate::document::{PositionEncoding, ToRangeExt}; @@ -27,7 +27,7 @@ pub(crate) fn generate_semantic_tokens( let semantic_token_data = if django_template { django_template_semantic_tokens(db, file, range) } else { - semantic_tokens(db, file, range) + semantic_tokens(db, db.program_file(file), range) }; let mut encoder = Encoder { diff --git a/crates/ty_server/src/server/api/type_hierarchy.rs b/crates/ty_server/src/server/api/type_hierarchy.rs index b228c31f25..2b0095a67e 100644 --- a/crates/ty_server/src/server/api/type_hierarchy.rs +++ b/crates/ty_server/src/server/api/type_hierarchy.rs @@ -1,5 +1,5 @@ use lsp_types::{SymbolKind, TypeHierarchyItem}; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; use crate::PositionEncoding; use crate::document::{ToRangeExt, resolve_file_uri_range}; @@ -33,6 +33,7 @@ pub(crate) fn hierarchy_handler( ) else { continue; }; + let file = db.program_file(file); let hierarchy_types = match hierarchy_kind { TypeHierarchyKind::Subtypes => ty_ide::type_hierarchy_subtypes(db, file, offset), TypeHierarchyKind::Supertypes => ty_ide::type_hierarchy_supertypes(db, file, offset), diff --git a/crates/ty_server/src/server/schedule/thread/pool.rs b/crates/ty_server/src/server/schedule/thread/pool.rs index a66ea88af3..0d9cf303d0 100644 --- a/crates/ty_server/src/server/schedule/thread/pool.rs +++ b/crates/ty_server/src/server/schedule/thread/pool.rs @@ -130,7 +130,7 @@ impl Pool { } #[expect(dead_code)] - pub(super) fn len(&self) -> usize { + fn len(&self) -> usize { self.extant_tasks.load(Ordering::SeqCst) } } diff --git a/crates/ty_server/src/session.rs b/crates/ty_server/src/session.rs index 6891d13201..e9a58ae2a9 100644 --- a/crates/ty_server/src/session.rs +++ b/crates/ty_server/src/session.rs @@ -187,7 +187,7 @@ impl Session { &mut self.request_queue } - pub(crate) fn initialization_options(&self) -> &InitializationOptions { + fn initialization_options(&self) -> &InitializationOptions { &self.initialization_options } @@ -230,7 +230,11 @@ impl Session { .and_then(|request| { if !self.request_queue.incoming().is_pending(&request.id) { // Clear out the suspended request if the request has been cancelled. - tracing::debug!("Skipping suspended workspace diagnostics request `{}` because it was cancelled", request.id); + tracing::debug!( + "Skipping suspended workspace diagnostics request `{}` \ + because it was cancelled", + request.id + ); return None; } @@ -325,7 +329,7 @@ impl Session { /// Refer to [`project_db`] for more details on how the project is selected. /// /// [`project_db`]: Session::project_db - pub(crate) fn project_db_mut(&mut self, path: &AnySystemPath) -> &mut ProjectDatabase { + fn project_db_mut(&mut self, path: &AnySystemPath) -> &mut ProjectDatabase { &mut self.project_state_mut(path).db } @@ -335,7 +339,7 @@ impl Session { /// given path, or the first project if no project is found for the path. /// /// If the path is a virtual path, it will return the first project database in the session. - pub(crate) fn project_state(&self, path: &AnySystemPath) -> &ProjectState { + fn project_state(&self, path: &AnySystemPath) -> &ProjectState { match path { AnySystemPath::System(system_path) => self .project_state_for_path(system_path) @@ -382,10 +386,7 @@ impl Session { /// Returns a reference to the project's [`ProjectState`] corresponding to the given path, if /// any. - pub(crate) fn project_state_for_path( - &self, - path: impl AsRef, - ) -> Option<&ProjectState> { + fn project_state_for_path(&self, path: impl AsRef) -> Option<&ProjectState> { let path = path.as_ref(); self.projects .range(..=path.to_path_buf()) @@ -424,7 +425,7 @@ impl Session { } /// Returns a mutable iterator over all projects. - pub(crate) fn project_states_mut(&mut self) -> impl Iterator + '_ { + fn project_states_mut(&mut self) -> impl Iterator + '_ { self.projects.values_mut() } @@ -531,7 +532,7 @@ impl Session { /// /// The client provided is used to show error messages and publish /// diagnostics related to configuration. - pub(crate) fn initialize_workspace_folder( + fn initialize_workspace_folder( &mut self, client: &Client, uri: &Uri, @@ -868,7 +869,7 @@ impl Session { /// 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(crate) fn clear_diagnostics(&self, client: &Client, uri: &Uri) { + fn clear_diagnostics(&self, client: &Client, uri: &Uri) { if self.global_settings().diagnostic_mode().is_off() { return; } @@ -926,12 +927,14 @@ impl Session { match diagnostic_mode { DiagnosticMode::Off => { tracing::debug!( - "Skipping registration of diagnostic capability because diagnostics are turned off" + "Skipping registration of diagnostic capability \ + because diagnostics are turned off" ); } DiagnosticMode::OpenFilesOnly | DiagnosticMode::Workspace => { tracing::debug!( - "Registering diagnostic capability with {diagnostic_mode:?} diagnostic mode" + "Registering diagnostic capability \ + with {diagnostic_mode:?} diagnostic mode" ); registrations.push(Registration { id: DIAGNOSTIC_REGISTRATION_ID.into(), @@ -1095,7 +1098,11 @@ impl Session { let paths = self .project_dbs() .flat_map(|db| { - ty_module_resolver::system_module_search_paths(db).map(move |path| (db, path)) + ty_module_resolver::system_module_search_paths( + db, + db.project().program(db).resolver_environment(db), + ) + .map(move |path| (db, path)) }) .filter(|(db, path)| !path.starts_with(db.project().root(*db))) .map(|(_, path)| path) @@ -1639,15 +1646,15 @@ impl Workspace { &self.settings } - pub(crate) fn settings_arc(&self) -> Arc { + fn settings_arc(&self) -> Arc { self.settings.clone() } - pub(crate) fn is_initialized(&self) -> bool { + fn is_initialized(&self) -> bool { self.initialized } - pub(crate) fn initialize(&mut self, settings: WorkspaceSettings) { + fn initialize(&mut self, settings: WorkspaceSettings) { self.settings = Arc::new(settings); self.initialized = true; } @@ -1784,7 +1791,7 @@ impl DocumentHandle { } #[expect(unused)] - pub(crate) fn file_path(&self) -> Option<&AnySystemPath> { + fn file_path(&self) -> Option<&AnySystemPath> { match self { Self::Text { path, .. } | Self::Notebook { path, .. } => Some(path), Self::Cell { .. } => None, @@ -1792,7 +1799,7 @@ impl DocumentHandle { } #[expect(unused)] - pub(crate) fn notebook_path(&self) -> Option<&AnySystemPath> { + fn notebook_path(&self) -> Option<&AnySystemPath> { match self { DocumentHandle::Notebook { path, .. } => Some(path), DocumentHandle::Cell { notebook_path, .. } => Some(notebook_path), @@ -2027,3 +2034,29 @@ pub(super) fn warn_about_unknown_options( tracing::warn!("{message}"); client.show_warning_message(message); } + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use ruff_db::system::{CommandExecutor, OsSystem, System as _}; + + use super::Index; + use crate::system::LSPSystem; + + /// Mutating the document index requires exclusive ownership after Salsa cancels the current + /// database snapshots. A background command executor must not retain an `LSPSystem`, because + /// that would keep the index alive and prevent the server from applying document changes. + #[test] + fn detached_command_executor_does_not_retain_document_index() { + let index = Arc::new(Index::new()); + let system = LSPSystem::new(index.clone(), Arc::new(OsSystem::default())); + let executor = system.command_executor().map(CommandExecutor::dyn_clone); + assert!(executor.is_some()); + drop(system); + + assert_eq!(Arc::strong_count(&index), 1); + + drop(executor); + } +} diff --git a/crates/ty_server/src/session/client.rs b/crates/ty_server/src/session/client.rs index fb16e2106f..2806110b2a 100644 --- a/crates/ty_server/src/session/client.rs +++ b/crates/ty_server/src/session/client.rs @@ -118,7 +118,7 @@ impl 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) { + fn send_notification_no_params(&self, method: &str) { if let Err(err) = self.client_sender .send(lsp_server::Message::Notification(Notification::new( @@ -194,6 +194,21 @@ impl Client { self.show_message(message, lsp_types::MessageType::Error); } + /// Sends a notification of partial result progress to the client, via a `$/progress` + /// notification. + pub(crate) fn send_partial_result( + &self, + token: lsp_types::ProgressToken, + partial_result: R::PartialResult, + ) where + R: lsp_types::RequestWithPartialResults, + { + self.send_notification::(lsp_types::ProgressParams { + token, + value: serde_json::to_value(partial_result).expect("Partial result to be serializable"), + }); + } + /// 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. diff --git a/crates/ty_server/src/session/index.rs b/crates/ty_server/src/session/index.rs index 0576a46ed7..107b978d8a 100644 --- a/crates/ty_server/src/session/index.rs +++ b/crates/ty_server/src/session/index.rs @@ -56,7 +56,7 @@ impl Index { } #[expect(dead_code)] - pub(super) fn notebook_document_keys(&self) -> impl Iterator + '_ { + fn notebook_document_keys(&self) -> impl Iterator + '_ { self.documents .iter() .filter(|(_, doc)| doc.as_notebook().is_some()) @@ -230,11 +230,11 @@ pub(crate) enum Document { } impl Document { - pub(super) fn new_text(document: TextDocument) -> Self { + fn new_text(document: TextDocument) -> Self { Self::Text(Arc::new(document)) } - pub(super) fn new_notebook(document: NotebookDocument) -> Self { + fn new_notebook(document: NotebookDocument) -> Self { Self::Notebook(Arc::new(document)) } @@ -252,7 +252,7 @@ impl Document { } } - pub(crate) fn as_notebook_mut(&mut self) -> Option<&mut NotebookDocument> { + fn as_notebook_mut(&mut self) -> Option<&mut NotebookDocument> { Some(match self { Self::Notebook(notebook) => Arc::make_mut(notebook), Self::Text(_) => return None, diff --git a/crates/ty_server/src/session/options.rs b/crates/ty_server/src/session/options.rs index 882ea505ee..e3044bfb72 100644 --- a/crates/ty_server/src/session/options.rs +++ b/crates/ty_server/src/session/options.rs @@ -515,43 +515,43 @@ impl Combine for PythonExtension { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub(crate) struct ActiveEnvironment { - pub(crate) executable: PythonExecutable, + executable: PythonExecutable, #[deprecated] - pub(crate) environment: Option, - pub(crate) version: Option, + environment: Option, + version: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub(crate) struct EnvironmentVersion { - pub(crate) major: i64, - pub(crate) minor: i64, + major: i64, + minor: i64, #[deprecated( note = "Not provided by all clients (Zed, VS Code when using the Python Environment extension). Use `major` and `minor` instead." )] - pub(crate) patch: Option, + patch: Option, #[deprecated( note = "Not provided by all clients (Zed, VS Code when using the Python Environment extension)." )] - pub(crate) sys_version: Option, + sys_version: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub(crate) struct PythonEnvironment { #[deprecated] - pub(crate) folder_uri: Option, + folder_uri: Option, #[deprecated] #[serde(rename = "type")] - pub(crate) kind: Option, + kind: Option, #[deprecated] - pub(crate) name: Option, + name: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub(crate) struct PythonExecutable { #[deprecated] - pub(crate) uri: Option, - pub(crate) sys_prefix: SystemPathBuf, + uri: Option, + sys_prefix: SystemPathBuf, } diff --git a/crates/ty_server/src/system.rs b/crates/ty_server/src/system.rs index 31ed446713..aaa156f156 100644 --- a/crates/ty_server/src/system.rs +++ b/crates/ty_server/src/system.rs @@ -13,7 +13,7 @@ use ruff_db::file_revision::FileRevision; use ruff_db::files::{File, FilePath}; use ruff_db::system::walk_directory::WalkDirectoryBuilder; use ruff_db::system::{ - DirectoryEntry, FileType, Metadata, Result, System, SystemPath, SystemPathBuf, + CommandExecutor, DirectoryEntry, FileType, Metadata, Result, System, SystemPath, SystemPathBuf, SystemVirtualPath, SystemVirtualPathBuf, WhichResult, WritableSystem, }; use ruff_notebook::{Notebook, NotebookError}; @@ -49,7 +49,7 @@ impl AnySystemPath { } #[expect(unused)] - pub(crate) const fn as_virtual(&self) -> Option<&SystemVirtualPath> { + const fn as_virtual(&self) -> Option<&SystemVirtualPath> { match self { AnySystemPath::SystemVirtual(path) => Some(path.as_path()), AnySystemPath::System(_) => None, @@ -281,6 +281,10 @@ impl System for LSPSystem { self.native_system.env_var(name) } + fn command_executor(&self) -> Option<&dyn CommandExecutor> { + self.native_system.command_executor() + } + fn dyn_clone(&self) -> Box { Box::new(self.clone()) } diff --git a/crates/ty_server/tests/e2e/commands.rs b/crates/ty_server/tests/e2e/commands.rs index 5725f6be6b..4f641562f3 100644 --- a/crates/ty_server/tests/e2e/commands.rs +++ b/crates/ty_server/tests/e2e/commands.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{Context, Result}; use lsp_types::{ExecuteCommandParams, ExecuteCommandRequest, WorkDoneProgressParams}; use ruff_db::system::SystemPath; @@ -49,6 +49,21 @@ python-platform = \"linux\" .as_str() .expect("debug command to return a string response"); + let (before_structs, salsa_structs) = response + .split_once("=======SALSA STRUCTS=======\n") + .context("debug response missing Salsa structs section")?; + let (salsa_structs, after_structs) = salsa_structs + .split_once("=======SALSA QUERIES=======\n") + .context("debug response missing Salsa queries section")?; + + // The production report orders structs by memory usage, which varies between platforms. + let mut salsa_structs = salsa_structs.lines().collect::>(); + salsa_structs.sort_unstable(); + let response = format!( + "{before_structs}=======SALSA STRUCTS=======\n{}\n=======SALSA QUERIES=======\n{after_structs}", + salsa_structs.join("\n") + ); + let mut settings = insta::Settings::clone_current(); settings.add_filter(r"\b[0-9]+.[0-9]+MB\b", "[X.XXMB]"); settings.add_filter(r"Workspace .+\)", "Workspace XXX"); diff --git a/crates/ty_server/tests/e2e/completions.rs b/crates/ty_server/tests/e2e/completions.rs index 14699eac6d..577f4b1bdc 100644 --- a/crates/ty_server/tests/e2e/completions.rs +++ b/crates/ty_server/tests/e2e/completions.rs @@ -121,6 +121,53 @@ fn complete_function_parentheses() -> Result<()> { let foo_content = "\ def complete_parentheses() -> None: ... +complete_parenth +"; + + let mut server = TestServerBuilder::new()? + .with_initialization_options( + ClientOptions::default().with_complete_function_parentheses(true), + ) + .enable_completion_snippets(true) + .with_trigger_parameter_hints_command() + .with_workspace(workspace_root, None)? + .with_file(foo, foo_content)? + .build() + .wait_until_workspaces_are_initialized(); + + server.open_text_document(foo, foo_content, 1); + + let completions = server.completion_request(&server.file_uri(foo), Position::new(2, 16)); + insta::assert_json_snapshot!(completions, @r#" + [ + { + "label": "complete_parentheses", + "kind": 3, + "detail": "def complete_parentheses()", + "sortText": "0", + "insertText": "complete_parentheses($0)", + "insertTextFormat": 2, + "command": { + "title": "Trigger parameter hints", + "command": "ty.triggerParameterHints" + } + } + ] + "#); + + Ok(()) +} + +/// Tests that the signature-help command is omitted when the client has not +/// advertised support for it (for example, editors that would surface an +/// "unsupported command" error). +#[test] +fn complete_function_parentheses_without_command_support() -> Result<()> { + let workspace_root = SystemPath::new("src"); + let foo = SystemPath::new("src/foo.py"); + let foo_content = "\ +def complete_parentheses() -> None: ... + complete_parenth "; @@ -205,6 +252,7 @@ is_typedd ClientOptions::default().with_complete_function_parentheses(true), ) .enable_completion_snippets(true) + .with_trigger_parameter_hints_command() .with_workspace(workspace_root, None)? .with_file(foo, foo_content)? .build() @@ -220,7 +268,37 @@ is_typedd "kind": 3, "sortText": "0", "insertText": "typing.is_typeddict($0)", - "insertTextFormat": 2 + "insertTextFormat": 2, + "command": { + "title": "Trigger parameter hints", + "command": "ty.triggerParameterHints" + } + }, + { + "label": "is_typeddict (import typing_extensions)", + "kind": 3, + "sortText": "1", + "insertText": "is_typeddict($0)", + "insertTextFormat": 2, + "additionalTextEdits": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 0 + } + }, + "newText": "from typing_extensions import is_typeddict\n" + } + ], + "command": { + "title": "Trigger parameter hints", + "command": "ty.triggerParameterHints" + } } ] "#); @@ -267,10 +345,73 @@ TypedDi "sortText": "1", "insertText": "typing.is_typeddict" }, + { + "label": "TypedDict (import typing_extensions)", + "kind": 6, + "sortText": "2", + "insertText": "TypedDict", + "additionalTextEdits": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 0 + } + }, + "newText": "from typing_extensions import TypedDict\n" + } + ] + }, + { + "label": "TypedDictFallback (import _typeshed._type_checker_internals)", + "kind": 7, + "sortText": "3", + "insertText": "TypedDictFallback", + "additionalTextEdits": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 0 + } + }, + "newText": "from _typeshed._type_checker_internals import TypedDictFallback\n" + } + ] + }, + { + "label": "is_typeddict (import typing_extensions)", + "kind": 3, + "sortText": "4", + "insertText": "is_typeddict", + "additionalTextEdits": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 0 + } + }, + "newText": "from typing_extensions import is_typeddict\n" + } + ] + }, { "label": "_FilterConfigurationTypedDict (import logging.config)", "kind": 7, - "sortText": "2", + "sortText": "5", "insertText": "_FilterConfigurationTypedDict", "additionalTextEdits": [ { @@ -291,7 +432,7 @@ TypedDi { "label": "_FormatterConfigurationTypedDict (import logging.config)", "kind": 6, - "sortText": "3", + "sortText": "6", "insertText": "_FormatterConfigurationTypedDict", "additionalTextEdits": [ { @@ -308,6 +449,27 @@ TypedDi "newText": "from logging.config import _FormatterConfigurationTypedDict\n" } ] + }, + { + "label": "_typeshed.dbapi (import _typeshed.dbapi)", + "kind": 9, + "sortText": "7", + "insertText": "_typeshed.dbapi", + "additionalTextEdits": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 0 + } + }, + "newText": "import _typeshed.dbapi\n" + } + ] } ] "#); @@ -381,10 +543,73 @@ TypedDi } ] }, + { + "label": "TypedDict (import typing_extensions)", + "kind": 6, + "sortText": "2", + "insertText": "TypedDict", + "additionalTextEdits": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 0 + } + }, + "newText": "from typing_extensions import TypedDict\n" + } + ] + }, + { + "label": "TypedDictFallback (import _typeshed._type_checker_internals)", + "kind": 7, + "sortText": "3", + "insertText": "TypedDictFallback", + "additionalTextEdits": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 0 + } + }, + "newText": "from _typeshed._type_checker_internals import TypedDictFallback\n" + } + ] + }, + { + "label": "is_typeddict (import typing_extensions)", + "kind": 3, + "sortText": "4", + "insertText": "is_typeddict", + "additionalTextEdits": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 0 + } + }, + "newText": "from typing_extensions import is_typeddict\n" + } + ] + }, { "label": "_FilterConfigurationTypedDict (import logging.config)", "kind": 7, - "sortText": "2", + "sortText": "5", "insertText": "_FilterConfigurationTypedDict", "additionalTextEdits": [ { @@ -405,7 +630,7 @@ TypedDi { "label": "_FormatterConfigurationTypedDict (import logging.config)", "kind": 6, - "sortText": "3", + "sortText": "6", "insertText": "_FormatterConfigurationTypedDict", "additionalTextEdits": [ { @@ -422,6 +647,27 @@ TypedDi "newText": "from logging.config import _FormatterConfigurationTypedDict\n" } ] + }, + { + "label": "_typeshed.dbapi (import _typeshed.dbapi)", + "kind": 9, + "sortText": "7", + "insertText": "_typeshed.dbapi", + "additionalTextEdits": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 0 + } + }, + "newText": "import _typeshed.dbapi\n" + } + ] } ] "#); diff --git a/crates/ty_server/tests/e2e/implementation.rs b/crates/ty_server/tests/e2e/implementation.rs new file mode 100644 index 0000000000..2fc6564c08 --- /dev/null +++ b/crates/ty_server/tests/e2e/implementation.rs @@ -0,0 +1,116 @@ +use anyhow::Result; +use lsp_types::{ + Definition, ImplementationParams, ImplementationProvider, ImplementationRequest, + ImplementationResponse, PartialResultParams, Position, Range, TextDocumentIdentifier, + TextDocumentPositionParams, WorkDoneProgressParams, +}; + +use crate::TestServerBuilder; + +const CONTENT: &str = r#"class Animal: + def speak(self): ... + +class Dog(Animal): + def speak(self): ... + +class Cat(Animal): + def speak(self): ... + +def f(animal: Animal): + animal.speak() +"#; + +#[test] +fn implementation_provider_is_advertised() -> Result<()> { + let server = TestServerBuilder::new()? + .build() + .wait_until_workspaces_are_initialized(); + + let initialization_result = server.initialization_result().unwrap(); + assert_eq!( + initialization_result.capabilities.implementation_provider, + Some(ImplementationProvider::Bool(true)) + ); + + Ok(()) +} + +#[test] +fn implementation_locations_without_link_support() -> Result<()> { + let mut server = TestServerBuilder::new()? + .with_file("foo.py", CONTENT)? + .enable_implementations_link_support(false) + .build() + .wait_until_workspaces_are_initialized(); + + server.open_text_document("foo.py", CONTENT, 1); + + let response = implementation(&mut server, "foo.py", Position::new(10, 13)).unwrap(); + let ImplementationResponse::Definition(Definition::LocationList(locations)) = response else { + panic!("Expected Location[] response, got {response:#?}"); + }; + + let ranges: Vec<_> = locations.iter().map(|location| location.range).collect(); + assert_eq!( + ranges, + vec![ + Range::new(Position::new(1, 8), Position::new(1, 13)), + Range::new(Position::new(4, 8), Position::new(4, 13)), + Range::new(Position::new(7, 8), Position::new(7, 13)), + ] + ); + + Ok(()) +} + +#[test] +fn implementation_location_links_with_link_support() -> Result<()> { + let mut server = TestServerBuilder::new()? + .with_file("foo.py", CONTENT)? + .enable_implementations_link_support(true) + .build() + .wait_until_workspaces_are_initialized(); + + server.open_text_document("foo.py", CONTENT, 1); + + let response = implementation(&mut server, "foo.py", Position::new(10, 13)).unwrap(); + let ImplementationResponse::DefinitionLinkList(links) = response else { + panic!("Expected LocationLink[] response, got {response:#?}"); + }; + + let selection_ranges: Vec<_> = links + .iter() + .map(|link| link.target_selection_range) + .collect(); + assert_eq!( + selection_ranges, + vec![ + Range::new(Position::new(1, 8), Position::new(1, 13)), + Range::new(Position::new(4, 8), Position::new(4, 13)), + Range::new(Position::new(7, 8), Position::new(7, 13)), + ] + ); + assert!(links.iter().all(|link| { + link.origin_selection_range + == Some(Range::new(Position::new(10, 11), Position::new(10, 16))) + })); + + Ok(()) +} + +fn implementation( + server: &mut crate::TestServer, + path: impl AsRef, + position: Position, +) -> Option { + server.send_request_await::(ImplementationParams { + text_document_position_params: TextDocumentPositionParams { + text_document: TextDocumentIdentifier { + uri: server.file_uri(path), + }, + position, + }, + work_done_progress_params: WorkDoneProgressParams::default(), + partial_result_params: PartialResultParams::default(), + }) +} diff --git a/crates/ty_server/tests/e2e/main.rs b/crates/ty_server/tests/e2e/main.rs index dcc8a2ef18..5cbbda952e 100644 --- a/crates/ty_server/tests/e2e/main.rs +++ b/crates/ty_server/tests/e2e/main.rs @@ -35,6 +35,7 @@ mod configuration; mod django_templates; mod folding_range; mod hover; +mod implementation; mod initialize; mod inlay_hints; mod notebook; @@ -1398,9 +1399,41 @@ impl TestServerBuilder { /// Advertise support for ty's fully rendered diagnostic output. pub(crate) fn with_full_diagnostic_output(mut self) -> Self { - self.client_capabilities.experimental = Some(serde_json::json!({ - "fullDiagnosticOutput": true, - })); + let experimental = self + .client_capabilities + .experimental + .get_or_insert_with(|| serde_json::json!({})); + experimental + .as_object_mut() + .expect("experimental capabilities must be a JSON object") + .insert("fullDiagnosticOutput".to_string(), serde_json::json!(true)); + self + } + + /// Advertise support for the `ty.triggerParameterHints` completion command. + pub(crate) fn with_trigger_parameter_hints_command(mut self) -> Self { + let experimental = self + .client_capabilities + .experimental + .get_or_insert_with(|| serde_json::json!({})); + experimental + .as_object_mut() + .expect("experimental capabilities must be a JSON object") + .insert( + "commands".to_string(), + serde_json::json!({ "commands": ["ty.triggerParameterHints"] }), + ); + self + } + + /// Enable or disable location link support for goto implementations + pub(crate) fn enable_implementations_link_support(mut self, enabled: bool) -> Self { + self.client_capabilities + .text_document + .get_or_insert_default() + .implementation + .get_or_insert_default() + .link_support = Some(enabled); self } diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_attribute_access_on_unimported.snap b/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_attribute_access_on_unimported.snap index 8baf89d666..18f08b423c 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_attribute_access_on_unimported.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_attribute_access_on_unimported.snap @@ -87,7 +87,7 @@ expression: code_actions "character": 24 } }, - "newText": " # ty:ignore[unresolved-reference]" + "newText": " # ty: ignore[unresolved-reference]" } ] } diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_existing_import_undefined_decorator.snap b/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_existing_import_undefined_decorator.snap index 7c41413d40..105a8654bc 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_existing_import_undefined_decorator.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_existing_import_undefined_decorator.snap @@ -48,6 +48,51 @@ expression: code_actions } } }, + { + "title": "import typing_extensions.deprecated", + "kind": "quickfix", + "diagnostics": [ + { + "range": { + "start": { + "line": 3, + "character": 1 + }, + "end": { + "line": 3, + "character": 11 + } + }, + "severity": 1, + "code": "unresolved-reference", + "codeDescription": { + "href": "https://ty.dev/rules#unresolved-reference" + }, + "source": "ty", + "message": "Name `deprecated` used when not defined" + } + ], + "isPreferred": true, + "edit": { + "changes": { + "file:///src/foo.py": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 0 + } + }, + "newText": "from typing_extensions import deprecated\n" + } + ] + } + } + }, { "title": "qualify warnings.deprecated", "kind": "quickfix", @@ -132,7 +177,7 @@ expression: code_actions "character": 28 } }, - "newText": " # ty:ignore[unresolved-reference]" + "newText": " # ty: ignore[unresolved-reference]" } ] } diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_invalid_string_annotations.snap b/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_invalid_string_annotations.snap index 3f6b711aad..d2acbb63dd 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_invalid_string_annotations.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_invalid_string_annotations.snap @@ -42,7 +42,7 @@ expression: code_actions "character": 12 } }, - "newText": " # ty:ignore[unresolved-reference]" + "newText": " # ty: ignore[unresolved-reference]" } ] } diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_possible_missing_submodule_attribute.snap b/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_possible_missing_submodule_attribute.snap index 709c85ce87..7310c18cff 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_possible_missing_submodule_attribute.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_possible_missing_submodule_attribute.snap @@ -42,7 +42,7 @@ expression: code_actions "character": 11 } }, - "newText": " # ty:ignore[possibly-missing-submodule]" + "newText": " # ty: ignore[possibly-missing-submodule]" } ] } diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_undefined_decorator.snap b/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_undefined_decorator.snap index a3fac399ac..2111a88479 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_undefined_decorator.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_undefined_decorator.snap @@ -48,6 +48,51 @@ expression: code_actions } } }, + { + "title": "import typing_extensions.deprecated", + "kind": "quickfix", + "diagnostics": [ + { + "range": { + "start": { + "line": 1, + "character": 1 + }, + "end": { + "line": 1, + "character": 11 + } + }, + "severity": 1, + "code": "unresolved-reference", + "codeDescription": { + "href": "https://ty.dev/rules#unresolved-reference" + }, + "source": "ty", + "message": "Name `deprecated` used when not defined" + } + ], + "isPreferred": true, + "edit": { + "changes": { + "file:///src/foo.py": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 0 + } + }, + "newText": "from typing_extensions import deprecated\n" + } + ] + } + } + }, { "title": "Ignore 'unresolved-reference' for this line", "kind": "quickfix", @@ -87,7 +132,7 @@ expression: code_actions "character": 28 } }, - "newText": " # ty:ignore[unresolved-reference]" + "newText": " # ty: ignore[unresolved-reference]" } ] } diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_undefined_reference_multi.snap b/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_undefined_reference_multi.snap index 58d613c0dc..ff9357c46e 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_undefined_reference_multi.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_undefined_reference_multi.snap @@ -48,6 +48,51 @@ expression: code_actions } } }, + { + "title": "import typing_extensions.Literal", + "kind": "quickfix", + "diagnostics": [ + { + "range": { + "start": { + "line": 0, + "character": 3 + }, + "end": { + "line": 0, + "character": 10 + } + }, + "severity": 1, + "code": "unresolved-reference", + "codeDescription": { + "href": "https://ty.dev/rules#unresolved-reference" + }, + "source": "ty", + "message": "Name `Literal` used when not defined" + } + ], + "isPreferred": true, + "edit": { + "changes": { + "file:///src/foo.py": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 0 + } + }, + "newText": "from typing_extensions import Literal\n" + } + ] + } + } + }, { "title": "Ignore 'unresolved-reference' for this line", "kind": "quickfix", @@ -87,7 +132,7 @@ expression: code_actions "character": 17 } }, - "newText": " # ty:ignore[unresolved-reference]" + "newText": " # ty: ignore[unresolved-reference]" } ] } diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_with_full_diagnostic_output_link.snap b/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_with_full_diagnostic_output_link.snap index e0f3c2ad2d..11cea2b4d8 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_with_full_diagnostic_output_link.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_with_full_diagnostic_output_link.snap @@ -48,6 +48,51 @@ expression: code_actions } } }, + { + "title": "import typing_extensions.Literal", + "kind": "quickfix", + "diagnostics": [ + { + "range": { + "start": { + "line": 0, + "character": 3 + }, + "end": { + "line": 0, + "character": 10 + } + }, + "severity": 1, + "code": "Click for full diagnostic", + "codeDescription": { + "href": "https://ty.dev/rules#unresolved-reference" + }, + "source": "ty", + "message": "Name `Literal` used when not defined" + } + ], + "isPreferred": true, + "edit": { + "changes": { + "file:///src/foo.py": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 0 + } + }, + "newText": "from typing_extensions import Literal\n" + } + ] + } + } + }, { "title": "Ignore 'unresolved-reference' for this line", "kind": "quickfix", @@ -87,7 +132,7 @@ expression: code_actions "character": 17 } }, - "newText": " # ty:ignore[unresolved-reference]" + "newText": " # ty: ignore[unresolved-reference]" } ] } diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap index 5507b5982c..1b6438ce78 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap @@ -60,6 +60,7 @@ Settings: Settings { }, src: SrcSettings { respect_ignore_files: true, + exclude_scripts: false, files: IncludeExcludeFilter { include: IncludeFilter( [ @@ -177,6 +178,7 @@ Settings: Settings { }, }, analysis: AnalysisSettings { + strict_generic_narrowing: false, strict_equality_semantics: false, respect_type_ignore_comments: true, allowed_unresolved_imports: ModuleGlobSet { @@ -234,13 +236,16 @@ Settings: Settings { Memory report: =======SALSA STRUCTS======= -`Program` metadata=[X.XXMB] fields=[X.XXMB] count=1 -`Project` metadata=[X.XXMB] fields=[X.XXMB] count=1 `FileRoot` metadata=[X.XXMB] fields=[X.XXMB] count=1 `ModuleResolveModeIngredient` metadata=[X.XXMB] fields=[X.XXMB] count=1 +`Program` metadata=[X.XXMB] fields=[X.XXMB] count=1 +`Project` metadata=[X.XXMB] fields=[X.XXMB] count=1 +`ResolverEnvironment` metadata=[X.XXMB] fields=[X.XXMB] count=1 =======SALSA QUERIES======= `dynamic_resolution_paths -> alloc::vec::Vec` metadata=[X.XXMB] fields=[X.XXMB] count=1 +`Project::program_ -> ty_python_core::program::Program<'_>` + metadata=[X.XXMB] fields=[X.XXMB] count=1 =======SALSA SUMMARY======= TOTAL MEMORY USAGE: [X.XXMB] struct metadata = [X.XXMB] diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization.snap b/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization.snap index 0ee8a9bf79..a74d9b051a 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization.snap @@ -48,6 +48,7 @@ expression: initialization_result "declarationProvider": true, "definitionProvider": true, "typeDefinitionProvider": true, + "implementationProvider": true, "referencesProvider": true, "documentHighlightProvider": true, "documentSymbolProvider": true, diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization_with_workspace.snap b/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization_with_workspace.snap index 0ee8a9bf79..a74d9b051a 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization_with_workspace.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization_with_workspace.snap @@ -48,6 +48,7 @@ expression: initialization_result "declarationProvider": true, "definitionProvider": true, "typeDefinitionProvider": true, + "implementationProvider": true, "referencesProvider": true, "documentHighlightProvider": true, "documentSymbolProvider": true, diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import.snap b/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import.snap index a9740b7b97..0c246de7f8 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import.snap @@ -44,5 +44,47 @@ expression: completions "newText": "from typing import LiteralString\n" } ] + }, + { + "label": "Literal (import typing_extensions)", + "kind": 6, + "sortText": "[RANKING]", + "insertText": "Literal", + "additionalTextEdits": [ + { + "range": { + "start": { + "line": 1, + "character": 0 + }, + "end": { + "line": 1, + "character": 0 + } + }, + "newText": "from typing_extensions import Literal\n" + } + ] + }, + { + "label": "LiteralString (import typing_extensions)", + "kind": 6, + "sortText": "[RANKING]", + "insertText": "LiteralString", + "additionalTextEdits": [ + { + "range": { + "start": { + "line": 1, + "character": 0 + }, + "end": { + "line": 1, + "character": 0 + } + }, + "newText": "from typing_extensions import LiteralString\n" + } + ] } ] diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import_docstring.snap b/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import_docstring.snap index a9740b7b97..0c246de7f8 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import_docstring.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import_docstring.snap @@ -44,5 +44,47 @@ expression: completions "newText": "from typing import LiteralString\n" } ] + }, + { + "label": "Literal (import typing_extensions)", + "kind": 6, + "sortText": "[RANKING]", + "insertText": "Literal", + "additionalTextEdits": [ + { + "range": { + "start": { + "line": 1, + "character": 0 + }, + "end": { + "line": 1, + "character": 0 + } + }, + "newText": "from typing_extensions import Literal\n" + } + ] + }, + { + "label": "LiteralString (import typing_extensions)", + "kind": 6, + "sortText": "[RANKING]", + "insertText": "LiteralString", + "additionalTextEdits": [ + { + "range": { + "start": { + "line": 1, + "character": 0 + }, + "end": { + "line": 1, + "character": 0 + } + }, + "newText": "from typing_extensions import LiteralString\n" + } + ] } ] diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import_from_future.snap b/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import_from_future.snap index a9740b7b97..0c246de7f8 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import_from_future.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import_from_future.snap @@ -44,5 +44,47 @@ expression: completions "newText": "from typing import LiteralString\n" } ] + }, + { + "label": "Literal (import typing_extensions)", + "kind": 6, + "sortText": "[RANKING]", + "insertText": "Literal", + "additionalTextEdits": [ + { + "range": { + "start": { + "line": 1, + "character": 0 + }, + "end": { + "line": 1, + "character": 0 + } + }, + "newText": "from typing_extensions import Literal\n" + } + ] + }, + { + "label": "LiteralString (import typing_extensions)", + "kind": 6, + "sortText": "[RANKING]", + "insertText": "LiteralString", + "additionalTextEdits": [ + { + "range": { + "start": { + "line": 1, + "character": 0 + }, + "end": { + "line": 1, + "character": 0 + } + }, + "newText": "from typing_extensions import LiteralString\n" + } + ] } ] diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import_same_cell.snap b/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import_same_cell.snap index 713c26841e..a3a9b5d385 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import_same_cell.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import_same_cell.snap @@ -44,5 +44,47 @@ expression: completions "newText": ", LiteralString" } ] + }, + { + "label": "Literal (import typing_extensions)", + "kind": 6, + "sortText": "[RANKING]", + "insertText": "Literal", + "additionalTextEdits": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 0 + } + }, + "newText": "from typing_extensions import Literal\n" + } + ] + }, + { + "label": "LiteralString (import typing_extensions)", + "kind": 6, + "sortText": "[RANKING]", + "insertText": "LiteralString", + "additionalTextEdits": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 0 + } + }, + "newText": "from typing_extensions import LiteralString\n" + } + ] } ] diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__publish_diagnostics__full_diagnostic_output.snap b/crates/ty_server/tests/e2e/snapshots/e2e__publish_diagnostics__full_diagnostic_output.snap index 02d34942a0..a5a61422da 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__publish_diagnostics__full_diagnostic_output.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__publish_diagnostics__full_diagnostic_output.snap @@ -69,7 +69,7 @@ PublishDiagnosticsParams { data: Some( Object { "diagnostic_id": String("invalid-return-type"), - "rendered": String("\u{1b}[1m\u{1b}[91merror[invalid-return-type]\u{1b}[0m: \u{1b}[1mReturn type does not match returned value\u{1b}[0m\n \u{1b}[1m\u{1b}[94m-->\u{1b}[0m src/foo.py:1:14\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m\n\u{1b}[1m\u{1b}[94m1 |\u{1b}[0m def foo() -> str:\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m \u{1b}[1m\u{1b}[33m---\u{1b}[0m \u{1b}[1m\u{1b}[33mExpected `str` because of return type\u{1b}[0m\n\u{1b}[1m\u{1b}[94m2 |\u{1b}[0m return 42\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m \u{1b}[1m\u{1b}[91m^^\u{1b}[0m \u{1b}[1m\u{1b}[91mexpected `str`, found `Literal[42]`\u{1b}[0m\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m\n\n"), + "rendered": String("\u{1b}[1m\u{1b}[91merror[invalid-return-type]\u{1b}[0m\u{1b}[1m: Return type does not match returned value\u{1b}[0m\n \u{1b}[1m\u{1b}[94m--> \u{1b}[0msrc/foo.py:2:12\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m\n\u{1b}[1m\u{1b}[94m1\u{1b}[0m \u{1b}[1m\u{1b}[94m|\u{1b}[0m def foo() -> str:\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m \u{1b}[1m\u{1b}[94m---\u{1b}[0m \u{1b}[1m\u{1b}[94mExpected `str` because of return type\u{1b}[0m\n\u{1b}[1m\u{1b}[94m2\u{1b}[0m \u{1b}[1m\u{1b}[94m|\u{1b}[0m return 42\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m \u{1b}[1m\u{1b}[91m^^\u{1b}[0m \u{1b}[1m\u{1b}[91mexpected `str`, found `Literal[42]`\u{1b}[0m\n\n"), }, ), }, diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__pull_diagnostics__document_diagnostic_caching_rendered_source_after.snap b/crates/ty_server/tests/e2e/snapshots/e2e__pull_diagnostics__document_diagnostic_caching_rendered_source_after.snap index ff9985b20d..62b4eacc15 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__pull_diagnostics__document_diagnostic_caching_rendered_source_after.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__pull_diagnostics__document_diagnostic_caching_rendered_source_after.snap @@ -61,7 +61,7 @@ RelatedFullDocumentDiagnosticReport( data: Some( Object { "diagnostic_id": String("invalid-return-type"), - "rendered": String("\u{1b}[1m\u{1b}[91merror[invalid-return-type]\u{1b}[0m: \u{1b}[1mReturn type does not match returned value\u{1b}[0m\n \u{1b}[1m\u{1b}[94m-->\u{1b}[0m src/foo.py:1:14\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m\n\u{1b}[1m\u{1b}[94m1 |\u{1b}[0m def foo() -> str:\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m \u{1b}[1m\u{1b}[33m---\u{1b}[0m \u{1b}[1m\u{1b}[33mExpected `str` because of return type\u{1b}[0m\n\u{1b}[1m\u{1b}[94m2 |\u{1b}[0m return 42 # after!\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m \u{1b}[1m\u{1b}[91m^^\u{1b}[0m \u{1b}[1m\u{1b}[91mexpected `str`, found `Literal[42]`\u{1b}[0m\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m\n\n"), + "rendered": String("\u{1b}[1m\u{1b}[91merror[invalid-return-type]\u{1b}[0m\u{1b}[1m: Return type does not match returned value\u{1b}[0m\n \u{1b}[1m\u{1b}[94m--> \u{1b}[0msrc/foo.py:2:12\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m\n\u{1b}[1m\u{1b}[94m1\u{1b}[0m \u{1b}[1m\u{1b}[94m|\u{1b}[0m def foo() -> str:\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m \u{1b}[1m\u{1b}[94m---\u{1b}[0m \u{1b}[1m\u{1b}[94mExpected `str` because of return type\u{1b}[0m\n\u{1b}[1m\u{1b}[94m2\u{1b}[0m \u{1b}[1m\u{1b}[94m|\u{1b}[0m return 42 # after!\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m \u{1b}[1m\u{1b}[91m^^\u{1b}[0m \u{1b}[1m\u{1b}[91mexpected `str`, found `Literal[42]`\u{1b}[0m\n\n"), }, ), }, diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__pull_diagnostics__document_diagnostic_caching_rendered_source_before.snap b/crates/ty_server/tests/e2e/snapshots/e2e__pull_diagnostics__document_diagnostic_caching_rendered_source_before.snap index 81151c4ae3..686521e8a5 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__pull_diagnostics__document_diagnostic_caching_rendered_source_before.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__pull_diagnostics__document_diagnostic_caching_rendered_source_before.snap @@ -61,7 +61,7 @@ RelatedFullDocumentDiagnosticReport( data: Some( Object { "diagnostic_id": String("invalid-return-type"), - "rendered": String("\u{1b}[1m\u{1b}[91merror[invalid-return-type]\u{1b}[0m: \u{1b}[1mReturn type does not match returned value\u{1b}[0m\n \u{1b}[1m\u{1b}[94m-->\u{1b}[0m src/foo.py:1:14\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m\n\u{1b}[1m\u{1b}[94m1 |\u{1b}[0m def foo() -> str:\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m \u{1b}[1m\u{1b}[33m---\u{1b}[0m \u{1b}[1m\u{1b}[33mExpected `str` because of return type\u{1b}[0m\n\u{1b}[1m\u{1b}[94m2 |\u{1b}[0m return 42 # before\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m \u{1b}[1m\u{1b}[91m^^\u{1b}[0m \u{1b}[1m\u{1b}[91mexpected `str`, found `Literal[42]`\u{1b}[0m\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m\n\n"), + "rendered": String("\u{1b}[1m\u{1b}[91merror[invalid-return-type]\u{1b}[0m\u{1b}[1m: Return type does not match returned value\u{1b}[0m\n \u{1b}[1m\u{1b}[94m--> \u{1b}[0msrc/foo.py:2:12\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m\n\u{1b}[1m\u{1b}[94m1\u{1b}[0m \u{1b}[1m\u{1b}[94m|\u{1b}[0m def foo() -> str:\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m \u{1b}[1m\u{1b}[94m---\u{1b}[0m \u{1b}[1m\u{1b}[94mExpected `str` because of return type\u{1b}[0m\n\u{1b}[1m\u{1b}[94m2\u{1b}[0m \u{1b}[1m\u{1b}[94m|\u{1b}[0m return 42 # before\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m \u{1b}[1m\u{1b}[91m^^\u{1b}[0m \u{1b}[1m\u{1b}[91mexpected `str`, found `Literal[42]`\u{1b}[0m\n\n"), }, ), }, diff --git a/crates/ty_server/tests/e2e/workspace_folders.rs b/crates/ty_server/tests/e2e/workspace_folders.rs index 0fa0fc9839..874e72a4ab 100644 --- a/crates/ty_server/tests/e2e/workspace_folders.rs +++ b/crates/ty_server/tests/e2e/workspace_folders.rs @@ -759,7 +759,7 @@ fn condensed_full_document_diagnostic_report(report: FullDocumentDiagnosticRepor Some(DiagnosticSeverity::Warning) => "WARNING", Some(DiagnosticSeverity::Information) => "INFORMATION", Some(DiagnosticSeverity::Hint) => "HINT", - None => "unknown", + Some(DiagnosticSeverity::Custom(_)) | None => "unknown", }; let Message::String(message) = d.message else { panic!( diff --git a/crates/ty_site_packages/Cargo.toml b/crates/ty_site_packages/Cargo.toml index 65dae5d5c4..cb95e5ffd3 100644 --- a/crates/ty_site_packages/Cargo.toml +++ b/crates/ty_site_packages/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_site_packages" -version = "0.0.5" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ty_site_packages/README.md b/crates/ty_site_packages/README.md index 935980dcbf..c2ce5db95e 100644 --- a/crates/ty_site_packages/README.md +++ b/crates/ty_site_packages/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ty_site_packages). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ty_site_packages). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_site_packages/src/lib.rs b/crates/ty_site_packages/src/lib.rs index 880797e1fb..7649164acf 100644 --- a/crates/ty_site_packages/src/lib.rs +++ b/crates/ty_site_packages/src/lib.rs @@ -17,9 +17,9 @@ use std::ops::Deref; use std::str::FromStr; use std::{fmt, sync::Arc}; +use annotate_snippets::{AnnotationKind, Level, Renderer, Snippet}; 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; @@ -137,8 +137,9 @@ impl SitePackagesPaths { 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`)" + (expected `site-packages` to be an absolute path \ + with symlinks resolved, located at \ + `/lib/pythonX.Y/site-packages`)" ); c.as_str() @@ -399,14 +400,6 @@ impl PythonEnvironment { } } - /// Returns the `pyvenv.cfg` path for virtual environments. - pub fn pyvenv_cfg_path(&self) -> Option { - match self { - Self::Virtual(env) => Some(env.root_path.join("pyvenv.cfg")), - Self::System(_) => None, - } - } - /// Returns `true` if this is a virtual environment (has a `pyvenv.cfg` file). pub fn is_virtual(&self) -> bool { matches!(self, Self::Virtual(_)) @@ -685,7 +678,7 @@ impl PythonBuildVariant { #[derive(Debug)] pub struct VirtualEnvironment { root_path: SysPrefixPath, - base_executable_home_path: PythonHomePath, + base_executable_home_path: Option, include_system_site_packages: bool, /// The version of the Python executable that was used to create this virtual environment. @@ -709,10 +702,7 @@ pub struct VirtualEnvironment { } impl VirtualEnvironment { - pub(crate) fn new( - path: &SysPrefixPath, - system: &dyn System, - ) -> SitePackagesDiscoveryResult { + 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}'"); @@ -746,10 +736,9 @@ impl VirtualEnvironment { 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. + // The `home` key is read by the standard library's `site.py` module, so a missing + // key indicates an irredeemably broken virtual environment. An unresolvable value + // can still be tolerated when the base interpreter is not needed. let Some(base_executable_home_path) = base_executable_home_path else { return Err(SitePackagesDiscoveryError::PyvenvCfgParseError( pyvenv_cfg_path, @@ -757,26 +746,45 @@ impl VirtualEnvironment { )); }; - let base_executable_home_path = PythonHomePath::new(base_executable_home_path, system) - .map_err(|io_err| { - SitePackagesDiscoveryError::PyvenvCfgParseError( + let base_executable_home_path = match PythonHomePath::new(base_executable_home_path, system) + { + Ok(home_path) => Some(home_path), + Err(io_err) if !include_system_site_packages => { + tracing::warn!( + "Failed to resolve the `home` value in the `pyvenv.cfg` file at \ + `{pyvenv_cfg_path}`. Goto-definition for stdlib-defined items will not \ + be able to jump to the real implementation. Underlying error: {io_err}" + ); + None + } + Err(io_err) => { + return 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) + 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}", + "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() @@ -818,7 +826,7 @@ impl VirtualEnvironment { /// Return a list of `site-packages` directories that are available from this virtual environment /// /// See the documentation for [`site_packages_directories_from_sys_prefix`] for more details. - pub(crate) fn site_packages_directories( + fn site_packages_directories( &self, system: &dyn System, ) -> SitePackagesDiscoveryResult { @@ -844,17 +852,20 @@ impl VirtualEnvironment { } 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}" + "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); + let system_sys_prefix = base_executable_home_path + .as_ref() + .and_then(SysPrefixPath::from_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, @@ -871,15 +882,16 @@ impl VirtualEnvironment { } 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.", + 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}" + "Resolved site-packages directories for this virtual environment are: \ + {site_packages_directories}" ); Ok(site_packages_directories) } @@ -887,10 +899,7 @@ System site-packages will not be used for module resolution.", /// Return the real stdlib path (containing actual .py files, and not some variation of typeshed). /// /// See the documentation for [`real_stdlib_directory_from_sys_prefix`] for more details. - pub(crate) fn real_stdlib_directory( - &self, - system: &dyn System, - ) -> StdlibDiscoveryResult { + fn real_stdlib_directory(&self, system: &dyn System) -> StdlibDiscoveryResult { let VirtualEnvironment { base_executable_home_path, implementation, @@ -909,8 +918,9 @@ System site-packages will not be used for module resolution.", // of the dir we're looking for. let version = version.as_ref().map(|v| v.version); let layout = PythonInterpreterLayout::unknown(*implementation, version); - if let Some(system_sys_prefix) = - SysPrefixPath::from_executable_home_path_real(system, base_executable_home_path) + if let Some(system_sys_prefix) = base_executable_home_path + .as_ref() + .and_then(|home_path| SysPrefixPath::from_executable_home_path_real(system, home_path)) { let real_stdlib_directory = real_stdlib_directory_from_sys_prefix(&system_sys_prefix, layout, system); @@ -926,9 +936,9 @@ System site-packages will not be used for module resolution.", } else { let cfg_path = root_path.join("pyvenv.cfg"); tracing::debug!( - "Failed to resolve `sys.prefix` of the system Python installation \ -from the `home` value in the `pyvenv.cfg` file at `{cfg_path}`. \ -System stdlib will not be used for module definitions.", + "Failed to resolve `sys.prefix` of the system Python installation from the `home` \ + value in the `pyvenv.cfg` file at `{cfg_path}`. System stdlib will not be used \ + for module definitions.", ); Err(StdlibDiscoveryError::NoSysPrefixFound(cfg_path)) } @@ -990,7 +1000,7 @@ impl CondaEnvironmentKind { } /// Read `CONDA_PREFIX` and confirm that it has the expected kind -pub(crate) fn conda_environment_from_env( +fn conda_environment_from_env( system: &dyn System, kind: CondaEnvironmentKind, ) -> Option { @@ -1007,10 +1017,7 @@ pub(crate) fn conda_environment_from_env( Some(path) } -pub(crate) fn environment_from_binary( - system: &dyn System, - binary: &str, -) -> Option { +fn environment_from_binary(system: &dyn System, binary: &str) -> Option { let binary = system.which(binary).ok()?; let env = PythonEnvironment::new(binary, SysPrefixPathOrigin::PythonBinary, system).ok()?; @@ -1147,7 +1154,7 @@ impl SystemEnvironment { /// Return a list of `site-packages` directories that are available from this environment. /// /// See the documentation for [`site_packages_directories_from_sys_prefix`] for more details. - pub(crate) fn site_packages_directories( + fn site_packages_directories( &self, system: &dyn System, ) -> SitePackagesDiscoveryResult { @@ -1158,7 +1165,8 @@ impl SystemEnvironment { )?; tracing::debug!( - "Resolved site-packages directories for this environment are: {site_packages_directories}" + "Resolved site-packages directories for this environment are: \ + {site_packages_directories}" ); Ok(site_packages_directories) } @@ -1166,10 +1174,7 @@ impl SystemEnvironment { /// Return a list of `site-packages` directories that are available from this environment. /// /// See the documentation for [`site_packages_directories_from_sys_prefix`] for more details. - pub(crate) fn real_stdlib_directory( - &self, - system: &dyn System, - ) -> StdlibDiscoveryResult { + fn real_stdlib_directory(&self, system: &dyn System) -> StdlibDiscoveryResult { let stdlib_directory = real_stdlib_directory_from_sys_prefix( self.path.sys_prefix(), self.path.interpreter_layout().unwrap_or_default(), @@ -1299,7 +1304,8 @@ impl std::fmt::Display for SitePackagesDiscoveryError { f, origin, inner, - "Failed to iterate over the contents of the `lib`/`lib64` directories of the Python installation", + "Failed to iterate over the contents \ + of the `lib`/`lib64` directories of the Python installation", None, &**system, ) @@ -1310,7 +1316,8 @@ impl std::fmt::Display for SitePackagesDiscoveryError { inner, &format!("Invalid {origin}"), Some( - "Could not find a `site-packages` directory for this Python installation/executable", + "Could not find a `site-packages` directory for this Python \ + installation/executable", ), &**system, ), @@ -1342,7 +1349,8 @@ impl std::fmt::Display for StdlibDiscoveryError { f, origin, inner, - "Failed to iterate over the contents of the `lib` directory of the Python installation", + "Failed to iterate over the contents \ + of the `lib` directory of the Python installation", None, &**system, ) @@ -1405,7 +1413,7 @@ fn display_error( 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()); + let mut annotation = AnnotationKind::Primary.span((setting_range - start_offset).into()); if let Some(secondary_message) = secondary_message { annotation = annotation.label(secondary_message); @@ -1416,7 +1424,10 @@ fn display_error( .line_start(start_index.get()) .fold(false); - let message = Level::None.title(&primary_message).snippet(snippet); + let message = Level::ERROR + .no_name() + .primary_title(&primary_message) + .element(snippet); let renderer = if colored::control::SHOULD_COLORIZE.should_colorize() { Renderer::styled() @@ -1425,7 +1436,7 @@ fn display_error( }; let renderer = renderer.cut_indicator("…"); - writeln!(f, "{}", renderer.render(message)) + writeln!(f, "{}", renderer.render(&[message])) } /// The various ways in which parsing a `pyvenv.cfg` file could fail @@ -1448,7 +1459,8 @@ impl fmt::Display for PyvenvCfgParseErrorKind { write!( f, "the following error was encountered \ -when trying to resolve the `home` value to a directory on disk: {io_err}" + when trying to resolve the `home` value \ + to a directory on disk: {io_err}" ) } } @@ -1562,7 +1574,10 @@ fn discover_package_dirs( } let path = entry.into_path(); let name = path.file_name().unwrap_or_else(|| { - panic!("File name should be non-null because path is guaranteed to be a child of `{prefix_dir}`") + panic!( + "File name should be non-null because path is guaranteed \ + to be a child of `{prefix_dir}`" + ) }); let matches_implementation = match implementation { @@ -2053,7 +2068,8 @@ impl SysPrefixPath { let path = entry.into_path(); let name = path.file_name().expect( - "File name should be non-null because path is guaranteed to be a child of `lib`", + "File name should be non-null \ + because path is guaranteed to be a child of `lib`", ); if !(name.starts_with("python3.") || name.starts_with("pypy3.")) { @@ -2107,6 +2123,8 @@ pub enum SysPrefixPathOrigin { PythonCliFlag, /// The selected interpreter in the user's editor. Editor, + /// The `sys.prefix` path was provided by `uv workspace metadata`. + UvWorkspace, /// The `sys.prefix` path came from the `VIRTUAL_ENV` environment variable VirtualEnvVar, /// The `sys.prefix` path came from the `CONDA_PREFIX` environment variable @@ -2127,7 +2145,7 @@ pub enum SysPrefixPathOrigin { 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 { + const fn must_be_virtual_env(&self) -> bool { match self { Self::LocalVenv | Self::VirtualEnvVar => true, Self::ConfigFileSetting(..) @@ -2136,6 +2154,7 @@ impl SysPrefixPathOrigin { | Self::DerivedFromPyvenvCfg | Self::CondaPrefixVar | Self::PythonBinary + | Self::UvWorkspace | Self::SelfEnvironment => false, } } @@ -2144,7 +2163,7 @@ impl SysPrefixPathOrigin { /// /// 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 { + const fn must_point_directly_to_sys_prefix(&self) -> bool { match self { Self::PythonCliFlag | Self::ConfigFileSetting(..) @@ -2154,7 +2173,8 @@ impl SysPrefixPathOrigin { Self::VirtualEnvVar | Self::CondaPrefixVar | Self::DerivedFromPyvenvCfg - | Self::LocalVenv => true, + | Self::LocalVenv + | Self::UvWorkspace => true, } } @@ -2169,7 +2189,8 @@ impl SysPrefixPathOrigin { | Self::DerivedFromPyvenvCfg | Self::ConfigFileSetting(..) | Self::PythonCliFlag - | Self::PythonBinary => false, + | Self::PythonBinary + | Self::UvWorkspace => false, Self::LocalVenv => true, } } @@ -2185,6 +2206,7 @@ impl std::fmt::Display for SysPrefixPathOrigin { Self::DerivedFromPyvenvCfg => f.write_str("derived `sys.prefix` path"), Self::LocalVenv => f.write_str("local virtual environment"), Self::Editor => f.write_str("selected interpreter in your editor"), + Self::UvWorkspace => f.write_str("uv workspace environment"), Self::SelfEnvironment => f.write_str("ty environment"), Self::PythonBinary => f.write_str("Python binary discovered in $PATH"), } @@ -2467,7 +2489,10 @@ mod tests { } else { SystemPathBuf::from(&*format!("/Python3.{}/bin", self.minor_version)) }; - assert_eq!(venv.base_executable_home_path, expected_home); + assert_eq!( + venv.base_executable_home_path.as_deref(), + Some(&*expected_home) + ); let site_packages_directories = venv.site_packages_directories(&self.system).unwrap(); let expected_venv_site_packages = if cfg!(target_os = "windows") { @@ -2519,7 +2544,8 @@ mod tests { ) { assert!( self.virtual_env.is_none(), - "`assert_system_environment` should only be used when `virtual_env` is not populated" + "`assert_system_environment` should only be used \ + when `virtual_env` is not populated" ); assert_eq!( @@ -2625,6 +2651,18 @@ mod tests { test.run(); } + #[test] + fn can_find_site_packages_directory_no_virtual_env_at_origin_uv_workspace() { + let test = PythonEnvironmentTestCase { + system: TestSystem::default(), + minor_version: 12, + free_threaded: false, + origin: SysPrefixPathOrigin::UvWorkspace, + 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 @@ -2972,15 +3010,47 @@ mod tests { } #[test] - fn parsing_pyvenv_cfg_with_invalid_home_key_fails() { + fn unresolved_pyvenv_cfg_home_is_nonfatal_without_system_site_packages() { 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 site_packages = if cfg!(target_os = "windows") { + SystemPathBuf::from(r"\.venv\Lib\site-packages") + } else { + SystemPathBuf::from("/.venv/lib/python3.13/site-packages") + }; + memory_fs.create_directory_all(&site_packages).unwrap(); + + let venv = PythonEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system) + .unwrap() + .expect_venv(); + + assert_eq!(venv.base_executable_home_path, None); + let expected = [site_packages]; + assert_eq!( + venv.site_packages_directories(&system).unwrap(), + &expected[..] + ); + } + + #[test] + fn unresolved_pyvenv_cfg_home_with_system_site_packages_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\ninclude-system-site-packages = true", + ) + .unwrap(); + let venv_result = PythonEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system); + assert!(matches!( venv_result, Err(SitePackagesDiscoveryError::PyvenvCfgParseError( @@ -3004,7 +3074,8 @@ mod tests { #[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 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, diff --git a/crates/ty_site_packages/src/version.rs b/crates/ty_site_packages/src/version.rs index b64df135e2..6832650350 100644 --- a/crates/ty_site_packages/src/version.rs +++ b/crates/ty_site_packages/src/version.rs @@ -10,7 +10,7 @@ use ruff_python_ast::PythonVersion; use ruff_text_size::TextRange; /// The source of the Python version. -#[derive(Clone, Debug, Eq, PartialEq, Default, get_size2::GetSize)] +#[derive(Clone, Debug, Eq, PartialEq, Hash, Default, get_size2::GetSize)] pub enum PythonVersionSource { /// Value loaded from a project's configuration file. ConfigFile(PythonVersionFileSource), @@ -39,6 +39,9 @@ pub enum PythonVersionSource { /// (e.g., the Python environment) Editor, + /// The value was provided by `uv workspace metadata`. + UvWorkspace, + /// We fell back to a default value because the value was not specified via the CLI or a config file. #[default] Default, @@ -46,7 +49,7 @@ pub enum PythonVersionSource { /// Information regarding the file and [`TextRange`] of the configuration /// from which we inferred the Python version. -#[derive(Debug, PartialEq, Eq, Clone, get_size2::GetSize)] +#[derive(Debug, PartialEq, Eq, Hash, Clone, get_size2::GetSize)] pub struct PythonVersionFileSource { path: Arc, range: Option, @@ -57,16 +60,6 @@ impl PythonVersionFileSource { Self { path, range } } - /// Returns the path to the configuration file. - pub fn path(&self) -> &SystemPathBuf { - &self.path - } - - /// Returns the range of the configuration setting. - pub fn range(&self) -> Option { - self.range - } - /// Attempt to resolve a [`Span`] that corresponds to the location of /// the configuration setting that specified the Python version. /// @@ -79,7 +72,7 @@ impl PythonVersionFileSource { } /// A Python version with its source. -#[derive(Eq, PartialEq, Debug, Clone, get_size2::GetSize)] +#[derive(Eq, PartialEq, Hash, Debug, Clone, get_size2::GetSize)] pub struct PythonVersionWithSource { pub version: PythonVersion, pub source: PythonVersionSource, diff --git a/crates/ty_static/Cargo.toml b/crates/ty_static/Cargo.toml index dd649170e8..365a9854c6 100644 --- a/crates/ty_static/Cargo.toml +++ b/crates/ty_static/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_static" -version = "0.0.5" +version = "0.0.8" description = "This is an internal component crate of Ruff" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/ty_static/README.md b/crates/ty_static/README.md index 34c43bf209..6952f9a0ff 100644 --- a/crates/ty_static/README.md +++ b/crates/ty_static/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ty_static). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ty_static). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_static/src/env_vars.rs b/crates/ty_static/src/env_vars.rs index 49701a39fd..e85a1dd379 100644 --- a/crates/ty_static/src/env_vars.rs +++ b/crates/ty_static/src/env_vars.rs @@ -61,6 +61,19 @@ impl EnvVars { /// Accepts the same values as the `--output-format` command-line argument. pub const TY_OUTPUT_FORMAT: &'static str = "TY_OUTPUT_FORMAT"; + /// Enable uv integration. + /// + /// When set to `"1"` or `"true"`, ty invokes `uv workspace metadata` to discover the workspace + /// root. + #[attr_hidden] + pub const TY_UV: &'static str = "TY_UV"; + + /// The path to the uv executable to use for workspace discovery. + /// + /// ty uses this path when uv integration is enabled by `TY_UV`. + #[attr_hidden] + pub const UV: &'static str = "UV"; + /// Used to detect an activated virtual environment. pub const VIRTUAL_ENV: &'static str = "VIRTUAL_ENV"; diff --git a/crates/ty_test/README.md b/crates/ty_test/README.md index 555222bd70..4b4e981ead 100644 --- a/crates/ty_test/README.md +++ b/crates/ty_test/README.md @@ -430,6 +430,15 @@ X = 1 ``` ```` +The same placeholder can be used in `environment.extra-paths`: + +````markdown +```toml +[environment] +extra-paths = ["/.venv/"] +``` +```` + ## Documentation of tests Arbitrary Markdown syntax (including of course normal prose paragraphs) is permitted (and ignored by diff --git a/crates/ty_test/src/config.rs b/crates/ty_test/src/config.rs index 55af7274ad..d6fe6304ab 100644 --- a/crates/ty_test/src/config.rs +++ b/crates/ty_test/src/config.rs @@ -182,16 +182,16 @@ pub(crate) struct Environment { /// stable version supported by ty is used (see `ty check --help` output). /// /// ty will not infer the Python version from the Python environment at this time. - pub(crate) python_version: Option, + python_version: Option, /// Target platform to assume when resolving types. - pub(crate) python_platform: Option, + python_platform: Option, /// Path to a custom typeshed directory. - pub(crate) typeshed: Option, + typeshed: Option, /// Additional search paths to consider when resolving modules. - pub(crate) extra_paths: Option>, + extra_paths: Option>, /// Path to the Python environment. /// @@ -205,12 +205,15 @@ pub(crate) struct Environment { /// 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, + python: Option, } #[derive(Deserialize, Default, Debug, Clone)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub(crate) struct Analysis { + /// Whether narrowing with generic classes uses the top materialization. + pub(crate) strict_generic_narrowing: Option, + /// Whether equality-based checks should preserve possible subclass behavior. #[serde(alias = "strict-literal-narrowing")] pub(crate) strict_equality_semantics: Option, @@ -283,5 +286,5 @@ pub(crate) struct Project { /// The site-packages directory will then be copied into the test's filesystem. /// /// Example: `dependencies = ["pydantic==2.12.2"]` - pub(crate) dependencies: Option>, + dependencies: Option>, } diff --git a/crates/ty_test/src/db.rs b/crates/ty_test/src/db.rs index ac0e4f5251..2c2362aa3d 100644 --- a/crates/ty_test/src/db.rs +++ b/crates/ty_test/src/db.rs @@ -14,13 +14,14 @@ use salsa::Setter as _; use std::borrow::Cow; use std::sync::Arc; use tempfile::TempDir; -use ty_module_resolver::{ModuleGlobSetBuilder, SearchPaths}; -use ty_python_core::Db as _; -use ty_python_core::program::Program; +use ty_module_resolver::ModuleGlobSetBuilder; +use ty_python_core::program::ProgramSettings; +use ty_python_core::{Db as _, ProgramFile, TestProgramDb}; use ty_python_semantic::dependencies::DependencyManifest; use ty_python_semantic::lint::{LintRegistry, RuleSelection}; use ty_python_semantic::{ - AnalysisSettings, Db as SemanticDb, check_file_unwrap, default_lint_registry, django_settings, + AnalysisSettings, Db as SemanticDb, PythonVersionWithSource, check_file_unwrap, + default_lint_registry, django_settings, }; #[salsa::db] @@ -35,6 +36,8 @@ pub(crate) struct Db { impl Db { pub(crate) fn setup() -> Self { + let vendored = ty_vendored::file_system().clone(); + let program_settings = ProgramSettings::empty(&vendored); let mut db = Self { system: MdtestSystem::in_memory(), storage: salsa::Storage::new(Some(Box::new({ @@ -42,12 +45,12 @@ impl Db { tracing::trace!("event: {:?}", event); } }))), - vendored: ty_vendored::file_system().clone(), + vendored, files: Files::default(), settings: None, }; - db.settings = Some(Settings::new(&db)); + db.settings = Some(Settings::new(&db, program_settings)); db } @@ -68,6 +71,14 @@ impl Db { } } + pub(crate) fn update_program(&mut self, settings: ProgramSettings) { + let db_settings = self.settings(); + if db_settings.program(self) != &settings { + settings.search_paths.try_register_static_roots(self); + db_settings.set_program(self).to(settings); + } + } + pub(crate) fn set_verbosity(&mut self, verbose: bool) { self.settings().set_verbose(self).to(verbose); } @@ -131,18 +142,10 @@ impl SourceDb for Db { fn files(&self) -> &Files { &self.files } - - fn python_version(&self) -> ruff_python_ast::PythonVersion { - Program::get(self).python_version(self) - } } #[salsa::db] -impl ty_module_resolver::Db for Db { - fn search_paths(&self) -> &SearchPaths { - Program::get(self).search_paths(self) - } -} +impl ty_module_resolver::Db for Db {} #[salsa::db] impl ty_python_core::Db for Db { @@ -158,7 +161,15 @@ impl SemanticDb for Db { return Vec::new(); } - check_file_unwrap(self, file) + check_file_unwrap(self, self.program_file(file)) + } + + fn program_file(&self, file: File) -> ProgramFile<'_> { + self.program().program_file(self, file) + } + + fn python_version_with_source(&self, _file: File) -> &PythonVersionWithSource { + &self.settings().program(self).python_version } fn rule_selection(&self, file: File) -> &RuleSelection { @@ -198,6 +209,13 @@ impl SemanticDb for Db { } } +#[salsa::db] +impl TestProgramDb for Db { + fn program_settings(&self) -> &ProgramSettings { + self.settings().program(self) + } +} + #[salsa::db] impl salsa::Database for Db {} @@ -256,6 +274,8 @@ impl FileSettings { #[salsa::input(debug)] struct Settings { + #[returns(ref)] + program: ProgramSettings, #[default] #[returns(ref)] analysis: AnalysisSettings, @@ -298,6 +318,7 @@ fn mdtest_analysis_settings(options: Option<&Analysis>) -> AnalysisSettings { }; let AnalysisSettings { + strict_generic_narrowing: strict_generic_narrowing_default, strict_equality_semantics: strict_equality_semantics_default, respect_type_ignore_comments: respect_type_ignore_comments_default, allowed_unresolved_imports: allowed_unresolved_imports_default, @@ -344,6 +365,9 @@ fn mdtest_analysis_settings(options: Option<&Analysis>) -> AnalysisSettings { }; AnalysisSettings { + strict_generic_narrowing: options + .strict_generic_narrowing + .unwrap_or(strict_generic_narrowing_default), strict_equality_semantics: options .strict_equality_semantics .unwrap_or(strict_equality_semantics_default), @@ -403,26 +427,32 @@ fn mdtest_analysis_settings(options: Option<&Analysis>) -> AnalysisSettings { } fn mdtest_rule_selection(rules: Option<&Rules>, required_rule: Option<&str>) -> RuleSelection { + // In general (as shown by the initialization of `selection` below), we enable even rules that + // are ignored by default in mdtests so that their behaviour is covered alongside the default + // rules. There are a few small exceptions to this, however: + static DISABLED_IN_MDTESTS: &[&str] = &[ + // `missing-override-decorator` is an exception: because it is extremely pedantic we have + // chosen to keep it opt-in to minimize churn in unrelated tests. + "missing-override-decorator", + // `experimental-syntax` is also an exception: we make use of `&` and `~` for intersection and + // negation types in our tests for better readability. + "experimental-syntax", + // The `unsound-*` rules are also exceptions because they are very strict, would + // result in lots of additional diagnostics in mdtests, and are not the default behaviour + // we'll show to our users. + "unsound-return-statement", + "unsound-yield", + ]; + let registry = default_lint_registry(); let mut selection = RuleSelection::all(registry, Severity::Info); - // In general (as shown by the initialization of `selection` above), we enable even rules that - // are ignored by default in mdtests so that their behaviour is covered alongside the default - // rules. - // - // `missing-override-decorator` is an exception: because it is extremely pedantic we have - // chosen to keep it opt-in to minimize churn in unrelated tests. - let missing_override_decorator = registry - .get("missing-override-decorator") - .expect("missing-override-decorator is a known lint rule"); - selection.disable(missing_override_decorator); - - // `experimental-syntax` is also an exception: we make use of `&` and `~` for intersection and - // negation types in our tests for better readability. - let experimental_syntax = registry - .get("experimental-syntax") - .expect("experimental-syntax is a known lint rule"); - selection.disable(experimental_syntax); + for rule in DISABLED_IN_MDTESTS { + let lint = registry + .get(rule) + .unwrap_or_else(|error| panic!("Unknown lint rule `{rule}`: {error}")); + selection.disable(lint); + } // `redundant-return-annotation` is a third exception: the corpus writes `-> None` out // thousands of times, and reporting all of them would bury every test that is about diff --git a/crates/ty_test/src/lib.rs b/crates/ty_test/src/lib.rs index 533ed90643..8ca7a56d42 100644 --- a/crates/ty_test/src/lib.rs +++ b/crates/ty_test/src/lib.rs @@ -14,17 +14,19 @@ use ruff_db::files::{FileRootKind, system_path_to_file}; use ruff_db::system::{DbWithWritableSystem as _, SystemPath, SystemPathBuf}; use ruff_db::testing::{setup_logging, setup_logging_with_filter}; use ruff_diagnostics::Applicability; +use ruff_python_ast::PythonVersion; use ruff_source_file::OneIndexed; use std::fmt::Write; use ty_module_resolver::{ Module, SearchPath, SearchPathSettings, list_modules, resolve_module_confident, }; +use ty_python_core::TestProgramDb as _; use ty_python_core::platform::PythonPlatform; -use ty_python_core::program::{FallibleStrategy, Program, ProgramSettings}; +use ty_python_core::program::{FallibleStrategy, ProgramSettings}; use ty_python_semantic::pull_types::pull_types; use ty_python_semantic::types::UNDEFINED_REVEAL; use ty_python_semantic::{ - PythonEnvironment, PythonVersionSource, PythonVersionWithSource, SysPrefixPathOrigin, + Db as _, PythonEnvironment, PythonVersionSource, PythonVersionWithSource, SysPrefixPathOrigin, fix_all_diagnostics, }; @@ -188,24 +190,10 @@ fn run_test( { 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() == "") + } else if let Some(site_packages_path) = + expand_site_packages_placeholder(&full_path, python_version) { - // 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; + full_path = site_packages_path; } let temp_string; @@ -291,11 +279,12 @@ fn run_test( .unwrap_or_default() .iter() .map(|path| { - if path.is_absolute() { + let path = if path.is_absolute() { path.clone() } else { src_path.join(path) - } + }; + expand_site_packages_placeholder(&path, python_version).unwrap_or(path) }) .collect(); @@ -318,7 +307,7 @@ fn run_test( .expect("Failed to resolve search path settings"), }; - Program::init_or_update(db, settings); + db.update_program(settings); db.update_analysis_options(configuration.analysis.as_ref()); db.update_dependency_manifest(configuration.dependency_manifest()); db.update_mdtest_rule_selection(configuration.rules.as_ref(), options.default_error_rule); @@ -351,16 +340,22 @@ fn run_test( } }; - let failure = match matcher::match_file(db, test_file.file, &diagnostics, options) - .and_then(|inline_diagnostics| { - mdtest::validate_inline_snapshot( - db, - "ty", - test_file, - &inline_diagnostics, - &mut markdown_edits, - ) - }) { + let failure = match matcher::match_file( + db, + test_file.file, + python_version, + &diagnostics, + options, + ) + .and_then(|inline_diagnostics| { + mdtest::validate_inline_snapshot( + db, + "ty", + test_file, + &inline_diagnostics, + &mut markdown_edits, + ) + }) { Ok(()) => None, Err(line_failures) => Some(FileFailures { backtick_offsets: test_file.to_code_block_backtick_offsets(), @@ -370,7 +365,8 @@ fn run_test( all_diagnostics.extend(diagnostics); - let pull_types_result = attempt_test(|file| pull_types(db, file), test_file); + let pull_types_result = + attempt_test(|file| pull_types(db, db.program_file(file)), test_file); match pull_types_result { Ok(()) => {} Err(failures) => { @@ -501,22 +497,25 @@ struct ModuleInconsistency<'db> { /// `list_module`. fn run_module_resolution_consistency_test(db: &db::Db) -> Result<(), Vec>> { let mut errs = vec![]; - for from_list in list_modules(db).iter().copied() { + let environment = db.program().resolver_environment(db); + for from_list in list_modules(db, environment).iter().copied() { // TODO: For now list_modules does not partake in desperate module resolution so // only compare against confident module resolution. - errs.push(match resolve_module_confident(db, from_list.name(db)) { - None => ModuleInconsistency { - db, - from_list, - from_resolve: None, + errs.push( + match resolve_module_confident(db, environment, from_list.name(db)) { + None => ModuleInconsistency { + db, + from_list, + from_resolve: None, + }, + Some(from_resolve) if from_list != from_resolve => ModuleInconsistency { + db, + from_list, + from_resolve: Some(from_resolve), + }, + _ => continue, }, - Some(from_resolve) if from_list != from_resolve => ModuleInconsistency { - db, - from_list, - from_resolve: Some(from_resolve), - }, - _ => continue, - }); + ); } if errs.is_empty() { Ok(()) } else { Err(errs) } } @@ -569,6 +568,28 @@ impl std::fmt::Display for ModuleInconsistency<'_> { } } +fn expand_site_packages_placeholder( + path: &SystemPath, + python_version: PythonVersion, +) -> Option { + let component_index = path + .components() + .position(|component| component.as_str() == "")?; + + let mut components = path.components(); + let mut expanded: SystemPathBuf = components.by_ref().take(component_index).collect(); + if cfg!(target_os = "windows") { + expanded.extend(["Lib", "site-packages"]); + } else { + expanded.push("lib"); + expanded.push(format!("python{python_version}")); + expanded.push("site-packages"); + } + expanded.extend(components.skip(1)); + + Some(expanded) +} + fn parse<'s>( short_title: &'s str, source: &'s str, diff --git a/crates/ty_vendored/Cargo.toml b/crates/ty_vendored/Cargo.toml index 2ba738ffc7..ef78599682 100644 --- a/crates/ty_vendored/Cargo.toml +++ b/crates/ty_vendored/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_vendored" -version = "0.0.5" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ty_vendored/build.rs b/crates/ty_vendored/build.rs index 14226f3f06..c77bcb9722 100644 --- a/crates/ty_vendored/build.rs +++ b/crates/ty_vendored/build.rs @@ -88,6 +88,7 @@ fn write_zipped_typeshed_to(writer: File) -> ZipResult { } // Patch typeshed and add the stubs for the `ty_extensions` package. + zip.add_directory("stdlib/ty_extensions/", options)?; for (source, destination) in TY_EXTENSIONS_STUBS { println!("adding file {source} as {destination} ..."); zip.start_file(destination, options)?; diff --git a/crates/ty_vendored/ty_extensions/__init__.pyi b/crates/ty_vendored/ty_extensions/__init__.pyi index a8705ec4ff..b7b3bfa750 100644 --- a/crates/ty_vendored/ty_extensions/__init__.pyi +++ b/crates/ty_vendored/ty_extensions/__init__.pyi @@ -1,4 +1,6 @@ # ruff: noqa: PYI021 +"""Experimental ty APIs intended to be exposed to end users.""" + import collections.abc import sys from typing import ( @@ -11,7 +13,11 @@ from typing import ( from typing_extensions import LiteralString, Self # noqa: UP035 -from ._internal import TypeOf as _TypeOf +# basedpython: `Unknown` is part of the language a user reads and writes, not an +# internal of the checker, so it stays on the public module. upstream moved its +# declaration to `_internal`, so the re-export below is what keeps it public +# rather than a second declaration. +from ._internal import TypeOf as _TypeOf, Unknown as Unknown # ------------------ # Special operations @@ -162,15 +168,6 @@ eagerly. # Types # ----- -Unknown: _SpecialForm -""" -`Unknown` is a dynamic type inferred due to missing type information or an inference error. - -ty infers `Unknown` for unannotated values with insufficient type information. It also uses it as a -fallback after certain type errors. This contrasts with `Any`, which represents an *explicitly* -annotated dynamic type. Like `Any`, however, it is a dynamic type, so ty allows any operation on it. -""" - AlwaysTruthy: _SpecialForm """ `AlwaysTruthy` represents the set of all objects that always evaluate to `True` in a boolean diff --git a/crates/ty_vendored/ty_extensions/_internal.pyi b/crates/ty_vendored/ty_extensions/_internal.pyi index 82e85e5364..a0807320b1 100644 --- a/crates/ty_vendored/ty_extensions/_internal.pyi +++ b/crates/ty_vendored/ty_extensions/_internal.pyi @@ -1,4 +1,11 @@ # ruff: noqa: PYI021 +""" +Internal-only symbols for special forms and type-system tests. + +Some symbols provide definitions and on-hover documentation for special forms. Others exist only as +helpers for ty's tests. None of these symbols are intended to be directly imported by end users. +""" + import types from collections.abc import Callable from enum import Enum @@ -50,6 +57,15 @@ ordinary `Callable[...]` types in type-theoretic tests. # Types # ----- +Unknown: _SpecialForm +""" +`Unknown` is a dynamic type inferred due to missing type information or an inference error. + +ty infers `Unknown` for unannotated values with insufficient type information. It also uses it as a +fallback after certain type errors. This contrasts with `Any`, which represents an *explicitly* +annotated dynamic type. Like `Any`, however, it is a dynamic type, so ty allows any operation on it. +""" + Todo: _SpecialForm """ `@Todo` is a dynamic type inferred due to a known missing feature or incomplete implementation in @@ -84,6 +100,27 @@ class ConstraintSetSolution: """One solution path for a constraint set.""" class ConstraintSet: + @staticmethod + def lower_bound( + lower_bound: TypeForm[object], + typevar: TypeForm[object], + ) -> ConstraintSet: + """Returns a constraint set requiring `typevar` to be a supertype of `lower_bound`.""" + + @staticmethod + def upper_bound( + typevar: TypeForm[object], + upper_bound: TypeForm[object], + ) -> ConstraintSet: + """Returns a constraint set requiring `typevar` to be a subtype of `upper_bound`.""" + + @staticmethod + def equality( + typevar: TypeForm[object], + value: TypeForm[object], + ) -> ConstraintSet: + """Returns a constraint set requiring `typevar` to specialize exactly to `value`.""" + @staticmethod def range( lower_bound: TypeForm[object], @@ -118,6 +155,11 @@ class ConstraintSet: `other`. """ + def exists(self, typevars: TypeForm[tuple[object, ...]]) -> Self: + """ + Existentially abstracts the given type variables from this constraint set. + """ + def for_all(self, typevars: TypeForm[tuple[object, ...]]) -> Self: """ Universally abstracts the given type variables from this constraint set. @@ -247,9 +289,6 @@ def is_disjoint_from( def is_singleton(ty: TypeForm[object]) -> bool: """Returns `True` if `ty` is a singleton type with exactly one inhabitant.""" -def is_single_valued(ty: TypeForm[object]) -> bool: - """Returns `True` if `ty` is non-empty and all inhabitants compare equal to each other.""" - # ------------------- # Operations on types # ------------------- diff --git a/crates/ty_vendored/typeshed_patches/0005-inspect-isawaitable-object.patch b/crates/ty_vendored/typeshed_patches/0005-inspect-isawaitable-object.patch new file mode 100644 index 0000000000..fe7242f7c0 --- /dev/null +++ b/crates/ty_vendored/typeshed_patches/0005-inspect-isawaitable-object.patch @@ -0,0 +1,26 @@ +--- a/stdlib/inspect.pyi ++++ b/stdlib/inspect.pyi +@@ -345,10 +345,10 @@ def isgenerator(object: object) -> TypeIs[GeneratorType[object, Never, object]]: + throw() used to raise an exception inside the generator + """ + +-def iscoroutine(object: object) -> TypeIs[CoroutineType[Any, Any, Any]]: ++def iscoroutine(object: object) -> TypeIs[CoroutineType[object, Never, object]]: + """Return true if the object is a coroutine.""" + +-def isawaitable(object: object) -> TypeIs[Awaitable[Any]]: ++def isawaitable(object: object) -> TypeIs[Awaitable[object]]: + """Return true if object can be passed to an ``await`` expression.""" + + @overload +--- a/stdlib/typing_extensions.pyi ++++ b/stdlib/typing_extensions.pyi +@@ -1337,7 +1337,7 @@ else: + + For example:: + +- def is_awaitable(val: object) -> TypeIs[Awaitable[Any]]: ++ def is_awaitable(val: object) -> TypeIs[Awaitable[object]]: + return hasattr(val, '__await__') + + def f(val: Union[int, Awaitable[int]]) -> int: diff --git a/crates/ty_vendored/typeshed_patches/0006-stdlib-typeis-static-types.patch b/crates/ty_vendored/typeshed_patches/0006-stdlib-typeis-static-types.patch new file mode 100644 index 0000000000..871d83a51f --- /dev/null +++ b/crates/ty_vendored/typeshed_patches/0006-stdlib-typeis-static-types.patch @@ -0,0 +1,58 @@ +--- a/stdlib/builtins.pyi ++++ b/stdlib/builtins.pyi +@@ -78,6 +78,7 @@ from typing import ( # noqa: Y022,UP035 + + # we can't import `Literal` from typing or mypy crashes: see #11247 + from typing_extensions import Literal, LiteralString, Self, TypeIs, TypeVarTuple, deprecated, disjoint_base # noqa: Y023, UP035 ++from ty_extensions import Top + + if sys.version_info >= (3, 14): + from _typeshed import AnnotateFunc +@@ -3667,7 +3668,7 @@ def breakpoint(*args: Any, **kws: Any) -> None: + By default, this drops you into the pdb debugger. + """ + +-def callable(obj: object, /) -> TypeIs[Callable[..., object]]: ++def callable(obj: object, /) -> TypeIs[Top[Callable[..., object]]]: + """Return whether the object is callable (i.e., some kind of function). + + Note that classes are callable, as are instances of classes with a +--- a/stdlib/asyncio/base_futures.pyi ++++ b/stdlib/asyncio/base_futures.pyi +@@ -3,6 +3,7 @@ from collections.abc import Callable, Sequence + from contextvars import Context + from typing import Any, Final + from typing_extensions import TypeIs ++from ty_extensions import Top + + from . import futures + +@@ -12,7 +13,7 @@ _PENDING: Final = "PENDING" # undocumented + _CANCELLED: Final = "CANCELLED" # undocumented + _FINISHED: Final = "FINISHED" # undocumented + +-def isfuture(obj: object) -> TypeIs[Future[Any]]: ++def isfuture(obj: object) -> TypeIs[Top[Future[Any]]]: + """Check for a Future. + + This returns True when obj is a Future instance or is advertising +--- a/stdlib/asyncio/coroutines.pyi ++++ b/stdlib/asyncio/coroutines.pyi +@@ -1,6 +1,6 @@ + import sys + from collections.abc import Awaitable, Callable, Coroutine + from typing import Any, ParamSpec, TypeGuard, TypeVar, overload +-from typing_extensions import TypeIs, deprecated ++from typing_extensions import Never, TypeIs, deprecated + + # Keep asyncio.__all__ updated with any changes to __all__ here +@@ -22,7 +22,7 @@ if sys.version_info < (3, 11): + If the coroutine is not yielded from before it is destroyed, + an error message is logged. + """ + +-def iscoroutine(obj: object) -> TypeIs[Coroutine[Any, Any, Any]]: ++def iscoroutine(obj: object) -> TypeIs[Coroutine[object, Never, object]]: + """Return True if obj is a coroutine object.""" + + if sys.version_info >= (3, 11): diff --git a/crates/ty_vendored/typeshed_patches/0007-dataclass-transform-unknown-arguments.patch b/crates/ty_vendored/typeshed_patches/0007-dataclass-transform-unknown-arguments.patch new file mode 100644 index 0000000000..3e6b7a8db8 --- /dev/null +++ b/crates/ty_vendored/typeshed_patches/0007-dataclass-transform-unknown-arguments.patch @@ -0,0 +1,22 @@ +--- a/stdlib/typing.pyi ++++ b/stdlib/typing.pyi +@@ -2280,7 +2280,6 @@ if sys.version_info >= (3, 11): + order_default: bool = False, + kw_only_default: bool = False, + frozen_default: bool = False, # on 3.11, runtime accepts it as part of kwargs + field_specifiers: tuple[type[Any] | Callable[..., Any], ...] = (), +- **kwargs: Any, + ) -> IdentityFunction: + """Decorator to mark an object as providing dataclass-like behavior. + +--- a/stdlib/typing_extensions.pyi ++++ b/stdlib/typing_extensions.pyi +@@ -804,8 +804,7 @@ else: + order_default: bool = False, + kw_only_default: bool = False, + frozen_default: bool = False, + field_specifiers: tuple[type[Any] | Callable[..., Any], ...] = (), +- **kwargs: object, + ) -> IdentityFunction: + """Decorator that marks a function, class, or metaclass as providing + dataclass-like behavior. diff --git a/crates/ty_vendored/typeshed_patches/0007-dataclasses-is-dataclass-top.patch b/crates/ty_vendored/typeshed_patches/0007-dataclasses-is-dataclass-top.patch new file mode 100644 index 0000000000..c0f47888b2 --- /dev/null +++ b/crates/ty_vendored/typeshed_patches/0007-dataclasses-is-dataclass-top.patch @@ -0,0 +1,30 @@ +diff --git a/stdlib/dataclasses.pyi b/stdlib/dataclasses.pyi +index d46b694a7e..1db97b1893 100644 +--- a/stdlib/dataclasses.pyi ++++ b/stdlib/dataclasses.pyi +@@ -7,6 +7,7 @@ from collections.abc import Callable, Iterable, Mapping + from types import GenericAlias + from typing import Any, Final, Generic, Literal, Protocol, TypeVar, overload, type_check_only + from typing_extensions import Never, TypeIs ++from ty_extensions import Top + + _T = TypeVar("_T") + _T_co = TypeVar("_T_co", covariant=True) +@@ -402,14 +403,14 @@ def fields(class_or_instance: DataclassInstance | type[DataclassInstance]) -> tu + + # 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] # ty:ignore[invalid-type-guard-definition] ++def is_dataclass(obj: Never) -> TypeIs[Top[DataclassInstance | type[DataclassInstance]]]: # type: ignore[narrowed-type-not-subtype] # pyright: ignore[reportGeneralTypeIssues] # ty:ignore[invalid-type-guard-definition] + """Returns True if obj is a dataclass or an instance of a + dataclass. + """ + @overload +-def is_dataclass(obj: type) -> TypeIs[type[DataclassInstance]]: ... ++def is_dataclass(obj: type) -> TypeIs[Top[type[DataclassInstance]]]: ... + @overload +-def is_dataclass(obj: object) -> TypeIs[DataclassInstance | type[DataclassInstance]]: ... ++def is_dataclass(obj: object) -> TypeIs[Top[DataclassInstance | type[DataclassInstance]]]: ... + + class FrozenInstanceError(AttributeError): ... + diff --git a/crates/ty_vendored/vendor/typeshed/source_commit.txt b/crates/ty_vendored/vendor/typeshed/source_commit.txt index 0d7e1d20a2..5ba2971442 100644 --- a/crates/ty_vendored/vendor/typeshed/source_commit.txt +++ b/crates/ty_vendored/vendor/typeshed/source_commit.txt @@ -1 +1 @@ -b00c387c669cb50d5d388d77b74c2e832e147fe8 +1b116673774d062a4af7b0a0b3d05533a6be55d0 diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/__main__.byi b/crates/ty_vendored/vendor/typeshed/stdlib/__main__.byi index 54847d1579..566677f0ec 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/__main__.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/__main__.byi @@ -1 +1 @@ -def __getattr__(name: str) # incomplete module +def __getattr__(name: str, /) # incomplete module diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_ast.byi b/crates/ty_vendored/vendor/typeshed/stdlib/_ast.byi index fd89973aef..0e59f61336 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/_ast.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_ast.byi @@ -1,137 +1,137 @@ import sys -from ast import ( - AST as AST, - Add as Add, - And as And, - AnnAssign as AnnAssign, - Assert as Assert, - Assign as Assign, - AsyncFor as AsyncFor, - AsyncFunctionDef as AsyncFunctionDef, - AsyncWith as AsyncWith, - Attribute as Attribute, - AugAssign as AugAssign, - Await as Await, - BinOp as BinOp, - BitAnd as BitAnd, - BitOr as BitOr, - BitXor as BitXor, - BoolOp as BoolOp, - Break as Break, - Call as Call, - ClassDef as ClassDef, - Compare as Compare, - Constant as Constant, - Continue as Continue, - Del as Del, - Delete as Delete, - Dict as Dict, - DictComp as DictComp, - Div as Div, - Eq as Eq, - ExceptHandler as ExceptHandler, - Expr as Expr, - Expression as Expression, - FloorDiv as FloorDiv, - For as For, - FormattedValue as FormattedValue, - FunctionDef as FunctionDef, - FunctionType as FunctionType, - GeneratorExp as GeneratorExp, - Global as Global, - Gt as Gt, - GtE as GtE, - If as If, - IfExp as IfExp, - Import as Import, - ImportFrom as ImportFrom, - In as In, - Interactive as Interactive, - Invert as Invert, - Is as Is, - IsNot as IsNot, - JoinedStr as JoinedStr, - Lambda as Lambda, - List as List, - ListComp as ListComp, - Load as Load, - LShift as LShift, - Lt as Lt, - LtE as LtE, - Match as Match, - MatchAs as MatchAs, - MatchClass as MatchClass, - MatchMapping as MatchMapping, - MatchOr as MatchOr, - MatchSequence as MatchSequence, - MatchSingleton as MatchSingleton, - MatchStar as MatchStar, - MatchValue as MatchValue, - MatMult as MatMult, - Mod as Mod, - Module as Module, - Mult as Mult, - Name as Name, - NamedExpr as NamedExpr, - Nonlocal as Nonlocal, - Not as Not, - NotEq as NotEq, - NotIn as NotIn, - Or as Or, - Pass as Pass, - Pow as Pow, - Raise as Raise, - Return as Return, - RShift as RShift, - Set as Set, - SetComp as SetComp, - Slice as Slice, - Starred as Starred, - Store as Store, - Sub as Sub, - Subscript as Subscript, - Try as Try, - Tuple as Tuple, - TypeIgnore as TypeIgnore, - UAdd as UAdd, - UnaryOp as UnaryOp, - USub as USub, - While as While, - With as With, - Yield as Yield, - YieldFrom as YieldFrom, - alias as alias, - arg as arg, - arguments as arguments, - boolop as boolop, - cmpop as cmpop, - comprehension as comprehension, - excepthandler as excepthandler, - expr as expr, - expr_context as expr_context, - keyword as keyword, - match_case as match_case, - mod as mod, - operator as operator, - pattern as pattern, - stmt as stmt, - type_ignore as type_ignore, - unaryop as unaryop, - withitem as withitem, +from ast export ( + AST, + Add, + And, + AnnAssign, + Assert, + Assign, + AsyncFor, + AsyncFunctionDef, + AsyncWith, + Attribute, + AugAssign, + Await, + BinOp, + BitAnd, + BitOr, + BitXor, + BoolOp, + Break, + Call, + ClassDef, + Compare, + Constant, + Continue, + Del, + Delete, + Dict, + DictComp, + Div, + Eq, + ExceptHandler, + Expr, + Expression, + FloorDiv, + For, + FormattedValue, + FunctionDef, + FunctionType, + GeneratorExp, + Global, + Gt, + GtE, + If, + IfExp, + Import, + ImportFrom, + In, + Interactive, + Invert, + Is, + IsNot, + JoinedStr, + Lambda, + List, + ListComp, + Load, + LShift, + Lt, + LtE, + Match, + MatchAs, + MatchClass, + MatchMapping, + MatchOr, + MatchSequence, + MatchSingleton, + MatchStar, + MatchValue, + MatMult, + Mod, + Module, + Mult, + Name, + NamedExpr, + Nonlocal, + Not, + NotEq, + NotIn, + Or, + Pass, + Pow, + Raise, + Return, + RShift, + Set, + SetComp, + Slice, + Starred, + Store, + Sub, + Subscript, + Try, + Tuple, + TypeIgnore, + UAdd, + UnaryOp, + USub, + While, + With, + Yield, + YieldFrom, + alias, + arg, + arguments, + boolop, + cmpop, + comprehension, + excepthandler, + expr, + expr_context, + keyword, + match_case, + mod, + operator, + pattern, + stmt, + type_ignore, + unaryop, + withitem, ) from typing import Final if sys.version_info >= (3, 12): - from ast import ( - ParamSpec as ParamSpec, - TypeAlias as TypeAlias, - TypeVar as TypeVar, - TypeVarTuple as TypeVarTuple, - type_param as type_param, + from ast export ( + ParamSpec, + TypeAlias, + TypeVar, + TypeVarTuple, + type_param, ) if sys.version_info >= (3, 11): - from ast import TryStar as TryStar + from ast export TryStar PyCF_ALLOW_TOP_LEVEL_AWAIT: Final = 8192 PyCF_ONLY_AST: Final = 1024 diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_ctypes.byi b/crates/ty_vendored/vendor/typeshed/stdlib/_ctypes.byi index 03e5df6523..11fefa736c 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/_ctypes.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_ctypes.byi @@ -314,7 +314,7 @@ class _UnionType(_CTypeBaseType): # At runtime, various attributes are created on a Union subclass based # on its _fields_. This method doesn't exist, but represents those # dynamically created attributes. - def __getattr__(self, name: str) -> _CField[dynamic, dynamic, dynamic] + def __getattr__(self, name: str, /) -> _CField[dynamic, dynamic, dynamic] if sys.version_info < (3, 13): # Inherited from CType_Type starting on 3.13 def __mul__[CT: _CData](cls: type[CT], other: int) -> type[Array[CT]] @@ -330,8 +330,8 @@ class Union(_CData, metaclass=_UnionType): _align_: ClassVar[int] init(self, *args: dynamic, **kw: dynamic) - def __getattr__(self, name: str) -> dynamic - override def __setattr__(self, name: str, value: dynamic) -> None + def __getattr__(self, name: str, /) -> dynamic + override def __setattr__(self, name: str, value: dynamic, /) # This class is not exposed. It calls itself _ctypes.PyCStructType. @type_check_only @@ -344,7 +344,7 @@ class _PyCStructType(_CTypeBaseType): # At runtime, various attributes are created on a Structure subclass based # on its _fields_. This method doesn't exist, but represents those # dynamically created attributes. - def __getattr__(self, name: str) -> _CField[dynamic, dynamic, dynamic] + def __getattr__(self, name: str, /) -> _CField[dynamic, dynamic, dynamic] if sys.version_info < (3, 13): # Inherited from CType_Type starting on 3.13 def __mul__[CT: _CData](cls: type[CT], other: int) -> type[Array[CT]] @@ -364,8 +364,8 @@ class Structure(_CData, metaclass=_PyCStructType): _layout_: ClassVar["ms" | "gcc-sysv"] init(self, *args: dynamic, **kw: dynamic) - def __getattr__(self, name: str) -> dynamic - override def __setattr__(self, name: str, value: dynamic) -> None + def __getattr__(self, name: str, /) -> dynamic + override def __setattr__(self, name: str, value: dynamic) # This class is not exposed. It calls itself _ctypes.PyCArrayType. @type_check_only diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_curses.byi b/crates/ty_vendored/vendor/typeshed/stdlib/_curses.byi index 9a50a7c67f..0e7b833722 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/_curses.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_curses.byi @@ -891,9 +891,16 @@ def use_env(flag: bool, /): and COLUMNS are not set). """ -class error(Exception) +class error(Exception): + """Exception raised when a curses library function returns an error.""" final class window: # undocumented + """A curses window. + + Window objects are returned by initscr() and newwin(), and by the + methods that create subwindows and pads. + """ + encoding: str """the typecode character used to create the array""" diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_curses_panel.byi b/crates/ty_vendored/vendor/typeshed/stdlib/_curses_panel.byi index 685303f0f8..e148487a27 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/_curses_panel.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_curses_panel.byi @@ -4,9 +4,15 @@ from typing import Final, final final __version__: str final version: str -class error(Exception) +class error(Exception): + """Exception raised when a curses panel library function returns an error.""" final class panel: + """A curses panel. + + Panel objects are returned by new_panel(). + """ + def above(self) -> panel: """Return the panel above the current panel.""" diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_frozen_importlib.byi b/crates/ty_vendored/vendor/typeshed/stdlib/_frozen_importlib.byi index 005b1edbe0..44015d1b6c 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/_frozen_importlib.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_frozen_importlib.byi @@ -140,10 +140,10 @@ class BuiltinImporter(importlib.abc.MetaPathFinder, importlib.abc.InspectLoader) """ - override class def get_code(cls, fullname: str) -> None: + override class def get_code(cls, fullname: str): """Return None as built-in modules do not have code objects.""" - override class def get_source(cls, fullname: str) -> None: + override class def get_source(cls, fullname: str): """Return None as built-in modules do not have source code.""" # Loader @@ -163,7 +163,7 @@ class BuiltinImporter(importlib.abc.MetaPathFinder, importlib.abc.InspectLoader) static def create_module(spec: ModuleSpec) -> types.ModuleType | None: """Create a built-in module""" - override static def exec_module(module: types.ModuleType) -> None: + override static def exec_module(module: types.ModuleType): """Exec a built-in module""" class FrozenImporter(importlib.abc.MetaPathFinder, importlib.abc.InspectLoader): @@ -199,10 +199,10 @@ class FrozenImporter(importlib.abc.MetaPathFinder, importlib.abc.InspectLoader): """ - override class def get_code(cls, fullname: str) -> None: + override class def get_code(cls, fullname: str): """Return the code object for the frozen module.""" - override class def get_source(cls, fullname: str) -> None: + override class def get_source(cls, fullname: str): """Return None as frozen modules do not have source code.""" # Loader @@ -222,4 +222,4 @@ class FrozenImporter(importlib.abc.MetaPathFinder, importlib.abc.InspectLoader): static def create_module(spec: ModuleSpec) -> types.ModuleType | None: """Set __file__, if able.""" - override static def exec_module(module: types.ModuleType) -> None + override static def exec_module(module: types.ModuleType) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_frozen_importlib_external.byi b/crates/ty_vendored/vendor/typeshed/stdlib/_frozen_importlib_external.byi index a442c83a45..d37e7a96f6 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/_frozen_importlib_external.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_frozen_importlib_external.byi @@ -313,7 +313,7 @@ class ExtensionFileLoader(FileLoader, _LoaderBasics, importlib.abc.ExecutionLoad override def create_module(self, spec: ModuleSpec) -> types.ModuleType: """Create an uninitialized extension module""" - override def exec_module(self, module: types.ModuleType) -> None: + override def exec_module(self, module: types.ModuleType): """Initialize an extension module""" def get_code(self, fullname: str): diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_io.byi b/crates/ty_vendored/vendor/typeshed/stdlib/_io.byi index 6a53a3b3a9..0df3538fb3 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/_io.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_io.byi @@ -702,7 +702,7 @@ class _TextIOBase(_IOBase): (which is always equal to the length of the string). """ - override def writelines(self, lines: Iterable[str], /) -> None: + override def writelines(self, lines: Iterable[str], /): """Write a list of lines to stream. Line separators are not added, so it is usual for each of the diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_operator.byi b/crates/ty_vendored/vendor/typeshed/stdlib/_operator.byi index ddd83c3b78..154da181db 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/_operator.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_operator.byi @@ -20,7 +20,7 @@ from _typeshed import ( SupportsSub, ) from collections.abc import Callable, Container, Iterable, MutableMapping, MutableSequence, Sequence -from operator import attrgetter as attrgetter, itemgetter as itemgetter, methodcaller as methodcaller +from operator export attrgetter, itemgetter, methodcaller from typing import ParamSpec, Protocol, TypeAlias, TypeVar, type_check_only diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_pickle.byi b/crates/ty_vendored/vendor/typeshed/stdlib/_pickle.byi index f5a9f05a01..aa506691b4 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/_pickle.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_pickle.byi @@ -2,7 +2,7 @@ from _typeshed import ReadableBuffer, SupportsWrite from collections.abc import Callable, Iterable, Iterator, Mapping -from pickle import PickleBuffer as PickleBuffer +from pickle export PickleBuffer from typing import Protocol, TypeAlias, type_check_only from typing_extensions import disjoint_base diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_sitebuiltins.byi b/crates/ty_vendored/vendor/typeshed/stdlib/_sitebuiltins.byi index b8d3f9d653..f3be122d5f 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/_sitebuiltins.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_sitebuiltins.byi @@ -5,12 +5,13 @@ The objects used by the site module to add custom builtins. import sys from collections.abc import Iterable from typing import ClassVar, Literal +from typing_extensions import Never class Quitter: name: str eof: str init(self, name: str, eof: str) - def __call__(self, code: sys._ExitCode = None) -> NoReturn + def __call__(self, code: sys._ExitCode = None) -> Never class _Printer: """interactive prompt objects for printing the license text, a list of diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_socket.byi b/crates/ty_vendored/vendor/typeshed/stdlib/_socket.byi index 2f53461395..447d6966e4 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/_socket.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_socket.byi @@ -6,7 +6,7 @@ See the socket module for documentation. import sys from _typeshed import ReadableBuffer, WriteableBuffer from collections.abc import Iterable -from socket import error as error, gaierror as gaierror, herror as herror, timeout as timeout +from socket export error, gaierror, herror, timeout from typing import Final, TypeAlias from typing_extensions import CapsuleType, disjoint_base diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_sqlite3.byi b/crates/ty_vendored/vendor/typeshed/stdlib/_sqlite3.byi index e95143700a..52129cb032 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/_sqlite3.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_sqlite3.byi @@ -22,7 +22,7 @@ from typing import Final, Literal, TypeAlias, TypeVar from typing_extensions import deprecated if sys.version_info >= (3, 11): - from sqlite3 import Blob as Blob + from sqlite3 export Blob _T = TypeVar("_T") private type SqliteData = str | ReadableBuffer | int | float | None diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_struct.byi b/crates/ty_vendored/vendor/typeshed/stdlib/_struct.byi index f26609cc35..66f024a25d 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/_struct.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_struct.byi @@ -43,8 +43,8 @@ def pack_into(fmt: str | bytes, buffer: WriteableBuffer, offset: int, /, *v: dyn Pack the provided values according to the format string and write the packed bytes into the writable buffer starting at offset. Note that the - offset is a required argument. See help(struct) for more on format - strings. + offset is a required argument. A negative offset counts from the end of + the buffer. See help(struct) for more on format strings. """ def unpack(format: str | bytes, buffer: ReadableBuffer, /) -> (*: dynamic): @@ -57,7 +57,8 @@ def unpack(format: str | bytes, buffer: ReadableBuffer, /) -> (*: dynamic): def unpack_from(format: str | bytes, /, buffer: ReadableBuffer, offset: int = 0) -> (*: dynamic): """Return a tuple containing values unpacked according to the format string. - The buffer's size, minus offset, must be at least calcsize(format). See + The buffer must contain at least calcsize(format) bytes starting at + offset. A negative offset counts from the end of the buffer. See help(struct) for more on format strings. """ @@ -98,8 +99,9 @@ class Struct: Pack the provided values according to the struct format string and write the packed bytes into the writable buffer starting at - offset. Note that the offset is a required argument. See - help(struct) for more on format strings. + offset. Note that the offset is a required argument. A negative + offset counts from the end of the buffer. See help(struct) for + more on format strings. """ def unpack(self, buffer: ReadableBuffer, /) -> (*: dynamic): @@ -115,8 +117,8 @@ class Struct: Values are unpacked according to the struct format string. The buffer's size in bytes, starting at position offset, must be at - least the struct size. See help(struct) for more on format - strings. + least the struct size. A negative offset counts from the end of + the buffer. See help(struct) for more on format strings. """ def iter_unpack(self, buffer: ReadableBuffer, /) -> Iterator[(*: dynamic)]: diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_thread.byi b/crates/ty_vendored/vendor/typeshed/stdlib/_thread.byi index 582b1f54fe..d70823a2c1 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/_thread.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_thread.byi @@ -9,7 +9,7 @@ from collections.abc import Callable from threading import Thread from types import TracebackType from typing import Final, final -from typing_extensions import TypeVarTuple, Unpack, deprecated, disjoint_base +from typing_extensions import Never, TypeVarTuple, Unpack, deprecated, disjoint_base error = RuntimeError @@ -266,13 +266,13 @@ def interrupt_main(signum: signal.Signals = signal.SIGINT, /): Note: the default signal handler for SIGINT raises ``KeyboardInterrupt``. """ -def exit() -> NoReturn: +def exit() -> Never: """This is synonymous to ``raise SystemExit''. It will cause the current thread to exit silently unless the exception is caught. """ @deprecated("Obsolete synonym. Use `exit()` instead.") -def exit_thread() -> NoReturn: # undocumented +def exit_thread() -> Never: # undocumented """An obsolete synonym of exit().""" def allocate_lock() -> LockType: @@ -355,5 +355,5 @@ class _local: """Thread-local data""" override def __getattribute__(self, name: str, /) -> dynamic - override def __setattr__(self, name: str, value: dynamic, /) -> None - override def __delattr__(self, name: str, /) -> None + override def __setattr__(self, name: str, value: dynamic, /) + override def __delattr__(self, name: str, /) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_threading_local.byi b/crates/ty_vendored/vendor/typeshed/stdlib/_threading_local.byi index 3e9eb06e51..cc602866ee 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/_threading_local.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_threading_local.byi @@ -35,5 +35,5 @@ class local: __slots__ = ("_local__impl", "__dict__") def __new__(cls, /, *args: dynamic, **kw: dynamic) -> Self override def __getattribute__(self, name: str) -> dynamic - override def __setattr__(self, name: str, value: dynamic) -> None - override def __delattr__(self, name: str) -> None + override def __setattr__(self, name: str, value: dynamic) + override def __delattr__(self, name: str) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/__init__.byi b/crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/__init__.byi index 0358c0aec7..0b70b964fb 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/__init__.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/__init__.byi @@ -36,6 +36,14 @@ type Unused = object # stable # for more information. type MaybeNone = dynamic # stable +# typeshed-internal type aliases to facilitate transition from +# `float` to either `float | int` or `float` (and similar for `complex`). +# When you encounter one of these type aliases, you are encouraged to +# replace them with the correct type. Please don't use them outside typeshed. +# See https://github.com/python/typeshed/issues/16059 for details. +type FloatInt = float | int +type ComplexInt = complex | float | int + # Used to mark arguments that default to a sentinel value. This prevents # stubtest from complaining about the default value not matching. # @@ -289,7 +297,7 @@ type ConvertibleToFloat = 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 + from enum export StrEnum else: from enum import Enum diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/_type_checker_internals.byi b/crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/_type_checker_internals.byi index 858345de63..4626c2222a 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/_type_checker_internals.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/_type_checker_internals.byi @@ -79,4 +79,4 @@ class NamedTupleFallback((*: dynamic)): # Non-default variations to accommodate couroutines, and `AwaitableGenerator` having a 4th type parameter. # The parameters correspond to Generator, but the 4th is the original type. -class AwaitableGenerator[out Yield, in Send, out Return, in out Other](Awaitable[Return], Generator[Yield, Send, Return], metaclass=ABCMeta) +class AwaitableGenerator[out Yield, in SendT_nd, out ReturnT_nd, in out Other](Awaitable[ReturnT_nd], Generator[Yield, SendT_nd, ReturnT_nd], metaclass=ABCMeta) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_weakref.byi b/crates/ty_vendored/vendor/typeshed/stdlib/_weakref.byi index 5c08f87433..56bd7ceb92 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/_weakref.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_weakref.byi @@ -2,7 +2,7 @@ from collections.abc import Callable from typing import TypeVar -from weakref import CallableProxyType as CallableProxyType, ProxyType as ProxyType, ReferenceType as ReferenceType, ref as ref +from weakref export CallableProxyType, ProxyType, ReferenceType, ref def getweakrefcount(object: dynamic, /) -> int: diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_weakrefset.byi b/crates/ty_vendored/vendor/typeshed/stdlib/_weakrefset.byi index e47eb8193c..4339c2cd00 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/_weakrefset.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_weakrefset.byi @@ -10,10 +10,10 @@ class WeakSet[in out Element](MutableSet[Element]): init(self, data: None = None) init(self, data: Iterable[Element]) - override def add(self, item: Element) -> None - override def discard(self, item: Element) -> None + override def add(self, item: Element) + override def discard(self, item: Element) def copy(self) -> Self - override def remove(self, item: Element) -> None + override def remove(self, item: Element) def update(self, other: Iterable[Element]) __hash__: ClassVar[None] override def __contains__(self, item: object) -> bool diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_winapi.byi b/crates/ty_vendored/vendor/typeshed/stdlib/_winapi.byi index ef78059b29..87a16f68a7 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/_winapi.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_winapi.byi @@ -2,6 +2,7 @@ import sys from _typeshed import ReadableBuffer from collections.abc import Sequence from typing import Final, Literal, final +from typing_extensions import Never if sys.platform == "win32": ABOVE_NORMAL_PRIORITY_CLASS: Final = 0x8000 @@ -264,7 +265,7 @@ if sys.platform == "win32": through both handles. """ - def ExitProcess(ExitCode: int, /) -> NoReturn + def ExitProcess(ExitCode: int, /) -> Never def GetACP() -> int: """Get the current Windows ANSI code page identifier.""" @@ -438,3 +439,7 @@ if sys.platform == "win32": """ def NeedCurrentDirectoryForExePath(exe_name: str, /) -> bool + + if sys.version_info >= (3, 15): + def GetTickCount64() -> int: + """Number of milliseconds that have elapsed since the system was started.""" diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/argparse.byi b/crates/ty_vendored/vendor/typeshed/stdlib/argparse.byi index 3df4978fb5..dbb1d61562 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/argparse.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/argparse.byi @@ -66,7 +66,7 @@ from _typeshed import SupportsWrite, sentinel from collections.abc import Callable, Generator, Iterable, Sequence from re import Pattern from typing import ClassVar, Final, Generic, Protocol, TypeAlias, TypeVar, type_check_only -from typing_extensions import Self, deprecated +from typing_extensions import Never, Self, deprecated __all__ = [ "ArgumentParser", @@ -178,7 +178,7 @@ class _ActionsContainer: argument_default: dynamic = ..., conflict_handler: str = ..., ) -> _ArgumentGroup - @deprecated("The `prefix_chars` parameter deprecated since Python 3.14.") + @deprecated("The `prefix_chars` parameter is deprecated.") def add_argument_group( self, title: str | None = None, @@ -198,7 +198,7 @@ class _ActionsContainer: def _pop_action_class(self, kwargs: dynamic, default: type[Action] | None = None) -> type[Action] def _get_handler(self) -> (Action, Iterable[(str, Action)]) -> dynamic def _check_conflict(self, action: Action) - def _handle_conflict_error(self, action: Action, conflicting_actions: Iterable[(str, Action)]) -> NoReturn + def _handle_conflict_error(self, action: Action, conflicting_actions: Iterable[(str, Action)]) -> Never def _handle_conflict_resolve(self, action: Action, conflicting_actions: Iterable[(str, Action)]) @type_check_only @@ -355,8 +355,8 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer): def parse_known_args[N](self, *, namespace: N) -> (N, list[str]) def convert_arg_line_to_args(self, arg_line: str) -> list[str] - def exit(self, status: int = 0, message: str | None = None) -> NoReturn - def error(self, message: str) -> NoReturn: + def exit(self, status: int = 0, message: str | None = None) -> Never + def error(self, message: str) -> Never: """error(message: string) Prints a usage message incorporating the message to stderr and @@ -698,12 +698,12 @@ class Namespace(_AttributeHolder): init(self, **kwargs: dynamic) def __getattr__(self, name: str) -> dynamic - override def __setattr__(self, name: str, value: dynamic, /) -> None + override def __setattr__(self, name: str, value: dynamic, /) def __contains__(self, key: str) -> bool override def __eq__(self, other: object) -> bool __hash__: ClassVar[None] -@deprecated("Deprecated since Python 3.14. Open files after parsing arguments instead.") +@deprecated("Deprecated; may leave files open. Open files after parsing arguments instead.") class FileType: """Deprecated factory for creating file object types @@ -743,7 +743,7 @@ class _ArgumentGroup(_ActionsContainer): argument_default: dynamic = ..., conflict_handler: str = ..., ) - @deprecated("Undocumented `prefix_chars` parameter is deprecated since Python 3.14.") + @deprecated("Undocumented `prefix_chars` parameter is deprecated.") def __init__( self, container: _ActionsContainer, diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/array.byi b/crates/ty_vendored/vendor/typeshed/stdlib/array.byi index 46a7bd9590..05b0523795 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/array.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/array.byi @@ -118,7 +118,7 @@ class array[in out Element in (int, float, str)](MutableSequence[Element]): def __new__(cls, typecode: str, initializer: Iterable[Element], /) -> Self def __new__(cls, typecode: str, initializer: bytes | bytearray = ..., /) -> Self - override def append(self, v: Element, /) -> None: + override def append(self, v: Element, /): """Append new value v to the end of the array.""" def buffer_info(self) -> (int, int): @@ -139,7 +139,7 @@ class array[in out Element in (int, float, str)](MutableSequence[Element]): override def count(self, v: Element, /) -> int: """Return number of occurrences of v in the array.""" - override def extend(self, bb: Iterable[Element], /) -> None: + override def extend(self, bb: Iterable[Element], /): """Append items to the end of the array.""" def frombytes(self, buffer: ReadableBuffer, /): @@ -165,7 +165,7 @@ class array[in out Element in (int, float, str)](MutableSequence[Element]): Raise ValueError if the value is not present. """ - override def insert(self, i: int, v: Element, /) -> None: + override def insert(self, i: int, v: Element, /): """Insert a new item v into the array before position i.""" override def pop(self, i: int = -1, /) -> Element: @@ -174,7 +174,7 @@ class array[in out Element in (int, float, str)](MutableSequence[Element]): i defaults to -1. """ - override def remove(self, v: Element, /) -> None: + override def remove(self, v: Element, /): """Remove the first occurrence of v in the array.""" def tobytes(self) -> bytes: @@ -210,7 +210,7 @@ class array[in out Element in (int, float, str)](MutableSequence[Element]): """Set self[key] to value.""" def __setitem__(self, key: slice[SupportsIndex | None], value: array[Element], /) -> None - override def __delitem__(self, key: SupportsIndex | slice[SupportsIndex | None], /) -> None: + override def __delitem__(self, key: SupportsIndex | slice[SupportsIndex | None], /): """Delete self[key].""" def __add__(self, value: array[Element], /) -> array[Element]: diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/ast.byi b/crates/ty_vendored/vendor/typeshed/stdlib/ast.byi index 77cdd22e27..f2cfef32f8 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/ast.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/ast.byi @@ -26,10 +26,10 @@ import builtins import os import sys import typing_extensions -from _ast import ( - PyCF_ALLOW_TOP_LEVEL_AWAIT as PyCF_ALLOW_TOP_LEVEL_AWAIT, - PyCF_ONLY_AST as PyCF_ONLY_AST, - PyCF_TYPE_COMMENTS as PyCF_TYPE_COMMENTS, +from _ast export ( + PyCF_ALLOW_TOP_LEVEL_AWAIT, + PyCF_ONLY_AST, + PyCF_TYPE_COMMENTS, ) from _typeshed import ReadableBuffer, Unused from collections.abc import Iterable, Iterator, Sequence @@ -38,7 +38,7 @@ from typing import ClassVar, Generic, Literal, TypedDict, TypeVar as _TypeVar, t from typing_extensions import Self, Unpack, deprecated, disjoint_base if sys.version_info >= (3, 13): - from _ast import PyCF_OPTIMIZED_AST as PyCF_OPTIMIZED_AST + from _ast export PyCF_OPTIMIZED_AST # Used for node end positions in constructor keyword arguments diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/base_events.byi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/base_events.byi index d900899c0c..95c6c26e7e 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/base_events.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/base_events.byi @@ -69,7 +69,7 @@ class Server(AbstractServer): override async def start_serving(self) -> None override async def serve_forever(self) -> None let sockets: (*: socket) - override def close(self) -> None + override def close(self) override async def wait_closed(self) -> None: """Wait until server is closed and all connections are dropped. @@ -89,7 +89,7 @@ class Server(AbstractServer): """ class BaseEventLoop(AbstractEventLoop): - override def run_forever(self) -> None: + override def run_forever(self): """Run until stop() is called.""" override def run_until_complete[Element](self, future: _AwaitableLike[Element]) -> Element: @@ -104,7 +104,7 @@ class BaseEventLoop(AbstractEventLoop): Return the Future's result, or raise its exception. """ - override def stop(self) -> None: + override def stop(self): """Stop running the event loop. Every callback already scheduled will still run. This simply @@ -117,7 +117,7 @@ class BaseEventLoop(AbstractEventLoop): override def is_closed(self) -> bool: """Returns True if the event loop was closed.""" - override def close(self) -> None: + override def close(self): """Close the event loop. This clears the queues and shuts down the executor, @@ -209,7 +209,7 @@ class BaseEventLoop(AbstractEventLoop): Return a task object. """ - override def set_task_factory(self, factory: _TaskFactory | None) -> None: + override def set_task_factory(self, factory: _TaskFactory | None): """Set a task factory that will be used by loop.create_task(). If factory is None the default task factory will be set. @@ -231,7 +231,7 @@ class BaseEventLoop(AbstractEventLoop): """Like call_soon(), but thread-safe.""" override def run_in_executor[*Args, Element](self, executor: Executor | None, func: (*Args) -> Element, *args: *Args) -> Future[Element] - override def set_default_executor(self, executor: ThreadPoolExecutor) -> None + override def set_default_executor(self, executor: ThreadPoolExecutor) # Network I/O methods returning Futures. override async def getaddrinfo( self, @@ -696,9 +696,9 @@ class BaseEventLoop(AbstractEventLoop): text: False | None = None, **kwargs: dynamic, ) -> (SubprocessTransport, ProtocolT) - override def add_reader[*Args](self, fd: FileDescriptorLike, callback: (*Args) -> dynamic, *args: *Args) -> None + override def add_reader[*Args](self, fd: FileDescriptorLike, callback: (*Args) -> dynamic, *args: *Args) override def remove_reader(self, fd: FileDescriptorLike) -> bool - override def add_writer[*Args](self, fd: FileDescriptorLike, callback: (*Args) -> dynamic, *args: *Args) -> None + override def add_writer[*Args](self, fd: FileDescriptorLike, callback: (*Args) -> dynamic, *args: *Args) override def remove_writer(self, fd: FileDescriptorLike) -> bool # The sock_* methods (and probably some others) are not actually implemented on # BaseEventLoop, only on subclasses. We list them here for now for convenience. @@ -712,10 +712,10 @@ class BaseEventLoop(AbstractEventLoop): async def sock_recvfrom_into(self, sock: socket, buf: WriteableBuffer, nbytes: int = 0) -> (int, _RetAddress) async def sock_sendto(self, sock: socket, data: ReadableBuffer, address: _Address) -> int # Signal handling. - override def add_signal_handler[*Args](self, sig: int, callback: (*Args) -> dynamic, *args: *Args) -> None + override def add_signal_handler[*Args](self, sig: int, callback: (*Args) -> dynamic, *args: *Args) override def remove_signal_handler(self, sig: int) -> bool # Error handlers. - override def set_exception_handler(self, handler: ExceptionHandler | None) -> None: + override def set_exception_handler(self, handler: ExceptionHandler | None): """Set handler as the new event loop exception handler. If handler is None, the default exception handler will @@ -731,7 +731,7 @@ class BaseEventLoop(AbstractEventLoop): override def get_exception_handler(self) -> ExceptionHandler | None: """Return an exception handler, or None if the default one is in use.""" - override def default_exception_handler(self, context: _Context) -> None: + override def default_exception_handler(self, context: _Context): """Default exception handler. This is called when an exception occurs and no exception @@ -747,7 +747,7 @@ class BaseEventLoop(AbstractEventLoop): `call_exception_handler()`. """ - override def call_exception_handler(self, context: _Context) -> None: + override def call_exception_handler(self, context: _Context): """Call the current event loop's exception handler. The context argument is a dict containing the following keys: @@ -774,7 +774,7 @@ class BaseEventLoop(AbstractEventLoop): # Debug flag management. override def get_debug(self) -> bool - override def set_debug(self, enabled: bool) -> None + override def set_debug(self, enabled: bool) if sys.version_info >= (3, 12): async def shutdown_default_executor(self, timeout: float | None = None) -> None: """Schedule the shutdown of the default executor. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/base_futures.byi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/base_futures.byi index 8de4047c6d..59b46691ed 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/base_futures.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/base_futures.byi @@ -2,6 +2,7 @@ from _asyncio import Future from collections.abc import Callable, Sequence from contextvars import Context from typing import Final +from ty_extensions import Top from . import futures @@ -11,7 +12,7 @@ _PENDING: Final = "PENDING" # undocumented _CANCELLED: Final = "CANCELLED" # undocumented _FINISHED: Final = "FINISHED" # undocumented -def isfuture(obj: object) -> obj is Future[dynamic]: +def isfuture(obj: object) -> obj is Top[Future[dynamic]]: """Check for a Future. This returns True when obj is a Future instance or is advertising diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/base_subprocess.byi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/base_subprocess.byi index f47b1ae13e..d9ada63434 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/base_subprocess.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/base_subprocess.byi @@ -45,7 +45,7 @@ class BaseSubprocessTransport(transports.SubprocessTransport): override def get_pid(self) -> int | None override def get_pipe_transport(self, fd: int) -> _File def _check_proc(self) # undocumented - override def send_signal(self, signal: int) -> None + override def send_signal(self, signal: int) async def _connect_pipes(self, waiter: futures.Future[dynamic] | None) -> None # undocumented def _call(self, cb: (...) -> object, *data: dynamic) # undocumented def _pipe_connection_lost(self, fd: int, exc: BaseException | None) # undocumented diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/coroutines.byi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/coroutines.byi index 28c7a8350b..19b8890b33 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/coroutines.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/coroutines.byi @@ -1,7 +1,7 @@ import sys from collections.abc import Awaitable, Callable, Coroutine from typing import ParamSpec, TypeVar -from typing_extensions import deprecated +from typing_extensions import Never, deprecated # Keep asyncio.__all__ updated with any changes to __all__ here if sys.version_info >= (3, 11): @@ -19,18 +19,18 @@ if sys.version_info < (3, 11): an error message is logged. """ -def iscoroutine(obj: object) -> obj is Coroutine[dynamic, dynamic, dynamic]: +def iscoroutine(obj: object) -> obj is Coroutine[object, Never, object]: """Return True if obj is a coroutine object.""" if sys.version_info >= (3, 11): - @deprecated("Deprecated since Python 3.14. Use `inspect.iscoroutinefunction()` instead.") + @deprecated("Deprecated; will be removed in Python 3.16. Use `inspect.iscoroutinefunction()` instead.") def iscoroutinefunction(func: (...) -> Coroutine[dynamic, dynamic, dynamic]) -> bool: """Return True if func is a decorated coroutine function.""" - @deprecated("Deprecated since Python 3.14. Use `inspect.iscoroutinefunction()` instead.") + @deprecated("Deprecated; will be removed in Python 3.16. Use `inspect.iscoroutinefunction()` instead.") def iscoroutinefunction[Parameters: (*: *, **: *), Element](func: (**Parameters) -> Awaitable[Element]) -> TypeGuard[(**Parameters) -> Coroutine[dynamic, dynamic, Element]] - @deprecated("Deprecated since Python 3.14. Use `inspect.iscoroutinefunction()` instead.") + @deprecated("Deprecated; will be removed in Python 3.16. Use `inspect.iscoroutinefunction()` instead.") def iscoroutinefunction[Parameters: (*: *, **: *)](func: (**Parameters) -> object) -> TypeGuard[(**Parameters) -> Coroutine[dynamic, dynamic, dynamic]] - @deprecated("Deprecated since Python 3.14. Use `inspect.iscoroutinefunction()` instead.") + @deprecated("Deprecated; will be removed in Python 3.16. Use `inspect.iscoroutinefunction()` instead.") def iscoroutinefunction(func: object) -> TypeGuard[(...) -> Coroutine[dynamic, dynamic, dynamic]] else: # Sometimes needed in Python < 3.11 due to the fact that it supports @coroutine diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/events.byi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/events.byi index 6653e428e7..d59b6090cb 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/events.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/events.byi @@ -2,11 +2,11 @@ import ssl import sys -from _asyncio import ( - _get_running_loop as _get_running_loop, - _set_running_loop as _set_running_loop, - get_event_loop as get_event_loop, - get_running_loop as get_running_loop, +from _asyncio export ( + _get_running_loop, + _set_running_loop, + get_event_loop, + get_running_loop, ) from _typeshed import FileDescriptorLike, ReadableBuffer, StrPath, Unused, WriteableBuffer from abc import ABCMeta, abstractmethod @@ -904,10 +904,10 @@ else: abstract def new_event_loop(self) -> AbstractEventLoop # Child processes handling (Unix only). @abstractmethod - @deprecated("Deprecated since Python 3.12; removed in Python 3.14.") + @deprecated("Deprecated; removed in Python 3.14.") def get_child_watcher(self) -> AbstractChildWatcher @abstractmethod - @deprecated("Deprecated since Python 3.12; removed in Python 3.14.") + @deprecated("Deprecated; removed in Python 3.14.") def set_child_watcher(self, watcher: AbstractChildWatcher) -> None AbstractEventLoopPolicy = _AbstractEventLoopPolicy @@ -962,7 +962,7 @@ else: Returns an instance of EventLoop or raises an exception. """ - override def set_event_loop(self, loop: AbstractEventLoop | None) -> None: + override def set_event_loop(self, loop: AbstractEventLoop | None): """Set the event loop.""" override def new_event_loop(self) -> AbstractEventLoop: @@ -982,11 +982,11 @@ if sys.version_info >= (3, 14): If policy is None, the default policy is restored. """ -@deprecated("Deprecated since Python 3.14; will be removed in Python 3.16.") +@deprecated("Deprecated; will be removed in Python 3.16.") def get_event_loop_policy() -> _AbstractEventLoopPolicy: """Get the current event loop policy.""" -@deprecated("Deprecated since Python 3.14; will be removed in Python 3.16.") +@deprecated("Deprecated; will be removed in Python 3.16.") def set_event_loop_policy(policy: _AbstractEventLoopPolicy | None) -> None: """Set the current event loop policy. @@ -1000,11 +1000,11 @@ def new_event_loop() -> AbstractEventLoop: """Equivalent to calling get_event_loop_policy().new_event_loop().""" if sys.version_info < (3, 14): - @deprecated("Deprecated since Python 3.12; removed in Python 3.14.") + @deprecated("Deprecated; removed in Python 3.14.") def get_child_watcher() -> AbstractChildWatcher: """Equivalent to calling get_event_loop_policy().get_child_watcher().""" - @deprecated("Deprecated since Python 3.12; removed in Python 3.14.") + @deprecated("Deprecated; removed in Python 3.14.") def set_child_watcher(watcher: AbstractChildWatcher) -> None: """Equivalent to calling get_event_loop_policy().set_child_watcher(watcher). diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/exceptions.byi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/exceptions.byi index 26943d6fff..52aed89364 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/exceptions.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/exceptions.byi @@ -27,7 +27,7 @@ class CancelledError(BaseException): """The Future or Task was cancelled.""" if sys.version_info >= (3, 11): - from builtins import TimeoutError as TimeoutError + from builtins export TimeoutError else: class TimeoutError(Exception): """The operation exceeded the given deadline.""" diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/futures.byi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/futures.byi index d53a0bf25f..ae83fd55ea 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/futures.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/futures.byi @@ -1,11 +1,11 @@ """A Future class similar to the one in PEP 3148.""" import sys -from _asyncio import Future as Future +from _asyncio export Future from concurrent.futures._base import Future as _ConcurrentFuture from typing import TypeVar -from .base_futures import isfuture as isfuture +from .base_futures export isfuture from .events import AbstractEventLoop # Keep asyncio.__all__ updated with any changes to __all__ here diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/protocols.byi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/protocols.byi index 9ac250dd50..1142394f02 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/protocols.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/protocols.byi @@ -158,7 +158,7 @@ class DatagramProtocol(BaseProtocol): """Interface for datagram protocol.""" __slots__ = () - override def connection_made(self, transport: transports.DatagramTransport) -> None: + override def connection_made(self, transport: transports.DatagramTransport): """Called when a connection is made. The argument is the transport representing the pipe connection. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/sslproto.byi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/sslproto.byi index 3678870f94..5d230bc914 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/sslproto.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/sslproto.byi @@ -157,7 +157,7 @@ class _SSLProtocolTransport(transports._FlowControlMixin, transports.Transport): """Get optional transport information.""" let _protocol_paused: bool - override def write(self, data: bytes | bytearray | memoryview[dynamic]) -> None: # any memoryview format or shape + override def write(self, data: bytes | bytearray | memoryview[dynamic]): # any memoryview format or shape """Write some data bytes to the transport. This does not block; it buffers the data and arranges for it @@ -258,7 +258,7 @@ class SSLProtocol(_SSLProtocolBase): def _set_app_protocol(self, app_protocol: protocols.BaseProtocol) def _wakeup_waiter(self, exc: BaseException | None = None) - override def connection_lost(self, exc: BaseException | None) -> None: + override def connection_lost(self, exc: BaseException | None): """Called when the low-level connection is lost or closed. The argument is an exception object or None (the latter @@ -266,7 +266,7 @@ class SSLProtocol(_SSLProtocolBase): aborted or closed). """ - override def eof_received(self) -> None: + override def eof_received(self): """Called when the other end of the low-level stream is half-closed. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/subprocess.byi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/subprocess.byi index 635517629c..2dc1b75395 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/subprocess.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/subprocess.byi @@ -19,7 +19,7 @@ class SubprocessStreamProtocol(streams.FlowControlMixin, protocols.SubprocessPro stdout: streams.StreamReader | None stderr: streams.StreamReader | None init(self, limit: int, loop: events.AbstractEventLoop) - override def pipe_data_received(self, fd: int, data: bytes | str) -> None + override def pipe_data_received(self, fd: int, data: bytes | str) class Process: stdin: streams.StreamWriter | None diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/tasks.byi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/tasks.byi index 24db129502..3902c61f47 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/tasks.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/tasks.byi @@ -2,12 +2,12 @@ import concurrent.futures import sys -from _asyncio import ( - Task as Task, - _enter_task as _enter_task, - _leave_task as _leave_task, - _register_task as _register_task, - _unregister_task as _unregister_task, +from _asyncio export ( + Task, + _enter_task, + _leave_task, + _register_task, + _unregister_task, ) from collections.abc import AsyncIterator, Awaitable, Coroutine, Generator, Iterable, Iterator from typing import Final, Literal, Protocol, TypeAlias, TypeVar, type_check_only @@ -426,7 +426,7 @@ else: """ if sys.version_info >= (3, 12): - from _asyncio import current_task as current_task + from _asyncio export current_task else: def current_task(loop: AbstractEventLoop | None = None) -> Task[dynamic] | None: """Return a currently executed task.""" diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/trsock.byi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/trsock.byi index b456020133..f125d58870 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/trsock.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/trsock.byi @@ -5,7 +5,7 @@ from builtins import type as Type # alias to avoid name clashes with property n from collections.abc import Iterable from types import TracebackType from typing import TypeAlias -from typing_extensions import deprecated +from typing_extensions import Never, deprecated # These are based in socket, maybe move them out into _typeshed.pyi or such type _Address = socket._Address @@ -26,7 +26,7 @@ class TransportSocket: let family: int let type: int let proto: int - def __getstate__(self) -> NoReturn + def __getstate__(self) -> Never def fileno(self) -> int def dup(self) -> socket.socket def get_inheritable(self) -> bool @@ -40,7 +40,7 @@ class TransportSocket: def getpeername(self) -> _RetAddress def getsockname(self) -> _RetAddress - def getsockbyname(self) -> NoReturn # This method doesn't exist on socket, yet is passed through? + def getsockbyname(self) -> Never # This method doesn't exist on socket, yet is passed through? def settimeout(self, value: float | None) def gettimeout(self) -> float | None def setblocking(self, flag: bool) @@ -60,7 +60,7 @@ class TransportSocket: def ioctl(self, control: int, option: int | (int, int, int) | bool) -> None else: @deprecated("Removed in Python 3.11") - def ioctl(self, control: int, option: int | (int, int, int) | bool) -> NoReturn + def ioctl(self, control: int, option: int | (int, int, int) | bool) -> Never @deprecated("Removed in Python 3.11") def listen(self, backlog: int = ..., /) -> None @@ -82,7 +82,7 @@ class TransportSocket: @deprecated("Removed in Python 3.11.") def sendmsg_afalg( self, msg: Iterable[ReadableBuffer] = ..., *, op: int, iv: dynamic = ..., assoclen: int = ..., flags: int = 0 - ) -> NoReturn + ) -> Never @deprecated("Removed in Python 3.11.") def sendmsg( @@ -111,7 +111,7 @@ class TransportSocket: def share(self, process_id: int) -> bytes else: @deprecated("Removed in Python 3.11.") - def share(self, process_id: int) -> NoReturn + def share(self, process_id: int) -> Never @deprecated("Removed in Python 3.11.") def recv_into(self, buffer: WriteBuffer, nbytes: int = 0, flags: int = 0) -> int diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/unix_events.byi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/unix_events.byi index 48c2059f72..95a99045a0 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/unix_events.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/unix_events.byi @@ -260,9 +260,9 @@ if sys.platform != "win32": # Doesn't actually have ABCMeta metaclass at runtime, but mypy complains if we don't have it in the stub. # See discussion in #7412 class BaseChildWatcher(AbstractChildWatcher, metaclass=ABCMeta): - override def close(self) -> None + override def close(self) override def is_active(self) -> bool - override def attach_loop(self, loop: events.AbstractEventLoop | None) -> None + override def attach_loop(self, loop: events.AbstractEventLoop | None) class SafeChildWatcher(BaseChildWatcher): """'Safe' child watcher implementation. @@ -278,10 +278,10 @@ if sys.platform != "win32": override def __enter__(self) -> Self override def __exit__( self, a: type[BaseException] | None, b: BaseException | None, c: types.TracebackType | None - ) -> None + ) override def add_child_handler[*Args]( self, pid: int, callback: (int, int, *Args) -> object, *args: *Args - ) -> None + ) override def remove_child_handler(self, pid: int) -> bool class FastChildWatcher(BaseChildWatcher): @@ -298,10 +298,10 @@ if sys.platform != "win32": override def __enter__(self) -> Self override def __exit__( self, a: type[BaseException] | None, b: BaseException | None, c: types.TracebackType | None - ) -> None + ) override def add_child_handler[*Args]( self, pid: int, callback: (int, int, *Args) -> object, *args: *Args - ) -> None + ) override def remove_child_handler(self, pid: int) -> bool class _UnixSelectorEventLoop(BaseSelectorEventLoop): @@ -459,16 +459,16 @@ if sys.platform != "win32": """ override def is_active(self) -> bool - override def close(self) -> None + override def close(self) override def __enter__(self) -> Self override def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: types.TracebackType | None - ) -> None + ) override def add_child_handler[*Args]( self, pid: int, callback: (int, int, *Args) -> object, *args: *Args - ) -> None + ) override def remove_child_handler(self, pid: int) -> bool - override def attach_loop(self, loop: events.AbstractEventLoop | None) -> None + override def attach_loop(self, loop: events.AbstractEventLoop | None) class ThreadedChildWatcher(AbstractChildWatcher): """Threaded child watcher implementation. @@ -484,17 +484,17 @@ if sys.platform != "win32": """ override def is_active(self) -> True - override def close(self) -> None + override def close(self) override def __enter__(self) -> Self override def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: types.TracebackType | None - ) -> None + ) def __del__(self) override def add_child_handler[*Args]( self, pid: int, callback: (int, int, *Args) -> object, *args: *Args - ) -> None + ) override def remove_child_handler(self, pid: int) -> bool - override def attach_loop(self, loop: events.AbstractEventLoop | None) -> None + override def attach_loop(self, loop: events.AbstractEventLoop | None) class PidfdChildWatcher(AbstractChildWatcher): """Child watcher implementation using Linux's pid file descriptors. @@ -511,11 +511,11 @@ if sys.platform != "win32": override def __enter__(self) -> Self override def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: types.TracebackType | None - ) -> None + ) override def is_active(self) -> bool - override def close(self) -> None - override def attach_loop(self, loop: events.AbstractEventLoop | None) -> None + override def close(self) + override def attach_loop(self, loop: events.AbstractEventLoop | None) override def add_child_handler[*Args]( self, pid: int, callback: (int, int, *Args) -> object, *args: *Args - ) -> None + ) override def remove_child_handler(self, pid: int) -> bool diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/windows_events.byi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/windows_events.byi index b3ef9eed32..eae574ea05 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/windows_events.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/windows_events.byi @@ -5,6 +5,7 @@ import sys from _typeshed import Incomplete, ReadableBuffer, WriteableBuffer from collections.abc import Callable from typing import ClassVar, Final +from typing_extensions import Never from . import events, futures, proactor_events, selector_events, streams, windows_utils @@ -123,18 +124,18 @@ if sys.platform == "win32": else: class WindowsSelectorEventLoopPolicy(events.BaseDefaultEventLoopPolicy): _loop_factory: ClassVar[type[SelectorEventLoop]] - def get_child_watcher(self) -> NoReturn: + def get_child_watcher(self) -> Never: """Get the watcher for child processes.""" - def set_child_watcher(self, watcher: dynamic) -> NoReturn: + def set_child_watcher(self, watcher: dynamic) -> Never: """Set the watcher for child processes.""" class WindowsProactorEventLoopPolicy(events.BaseDefaultEventLoopPolicy): _loop_factory: ClassVar[type[ProactorEventLoop]] - def get_child_watcher(self) -> NoReturn: + def get_child_watcher(self) -> Never: """Get the watcher for child processes.""" - def set_child_watcher(self, watcher: dynamic) -> NoReturn: + def set_child_watcher(self, watcher: dynamic) -> Never: """Set the watcher for child processes.""" if sys.version_info >= (3, 14): diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/builtins.byi b/crates/ty_vendored/vendor/typeshed/stdlib/builtins.byi index 3e0ca94ad0..c256e11ee4 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/builtins.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/builtins.byi @@ -42,7 +42,7 @@ from _typeshed import ( SupportsRichComparisonT, SupportsWrite, ) -from collections.abc import Awaitable, Callable, Iterable, Iterator, MutableSet, Reversible, Set as AbstractSet, Sized +from collections.abc import Awaitable, Callable, Iterable, Iterator, MutableSet, Set as AbstractSet, Sized from io import BufferedRandom, BufferedReader, BufferedWriter, FileIO, TextIOWrapper from os import PathLike from types import CellType, CodeType, EllipsisType, GenericAlias, NotImplementedType, TracebackType, UnionType @@ -52,7 +52,8 @@ from types import CellType, CodeType, EllipsisType, GenericAlias, NotImplemented from typing import ClassVar, Final, Generic, ParamSpec, Protocol, TypeAlias, TypeVar, final, type_check_only # we can't import `Literal` from typing or mypy crashes: see #11247 -from typing_extensions import Never, Literal, LiteralString, Self, TypeIs, TypeVarTuple, deprecated, disjoint_base # noqa: Y023, UP035 +from typing_extensions import Never, Literal, Self, TypeIs, TypeVarTuple, deprecated, disjoint_base # noqa: Y023, UP035 +from ty_extensions import Top if sys.version_info >= (3, 14): from _typeshed import AnnotateFunc @@ -835,7 +836,7 @@ class str(Sequence[str]): def __new__(cls, object: object = "") -> Self def __new__(cls, object: ReadableBuffer, encoding: str = "utf-8", errors: str = "strict") -> Self - def capitalize(self: LiteralString) -> LiteralString: + def capitalize(self: literal str) -> literal str: """Return a capitalized version of the string. More specifically, make the first character have upper case and the @@ -843,11 +844,11 @@ class str(Sequence[str]): """ def capitalize(self) -> str - def casefold(self: LiteralString) -> LiteralString: + def casefold(self: literal str) -> literal str: """Return a version of the string suitable for caseless comparisons.""" def casefold(self) -> str - def center(self: LiteralString, width: SupportsIndex, fillchar: LiteralString = " ", /) -> LiteralString: + def center(self: literal str, width: SupportsIndex, fillchar: literal str = " ", /) -> literal str: """Return a centered string of length width. Padding is done using the specified fill character (default is @@ -888,7 +889,7 @@ class str(Sequence[str]): Optional stop position. Default: end of the string. """ - def expandtabs(self: LiteralString, tabsize: SupportsIndex = 8) -> LiteralString: + def expandtabs(self: literal str, tabsize: SupportsIndex = 8) -> literal str: """Return a copy where all tab characters are expanded using spaces. If tabsize is not given, a tab size of 8 characters is assumed. @@ -902,7 +903,7 @@ class str(Sequence[str]): notation. Return -1 on failure. """ - def format(self: LiteralString, *args: LiteralString, **kwargs: LiteralString) -> LiteralString: + def format(self: literal str, *args: literal str, **kwargs: literal str) -> literal str: """Return a formatted version of the string, using substitutions from args and kwargs. The substitutions are identified by braces ('{' and '}'). """ @@ -1003,7 +1004,7 @@ class str(Sequence[str]): uppercase and there is at least one cased character in the string. """ - def join(self: LiteralString, iterable: Iterable[LiteralString], /) -> LiteralString: + def join(self: literal str, iterable: Iterable[literal str], /) -> literal str: """Concatenate any number of strings. The string whose method is called is inserted in between each given @@ -1013,7 +1014,7 @@ class str(Sequence[str]): """ def join(self, iterable: Iterable[str], /) -> str - def ljust(self: LiteralString, width: SupportsIndex, fillchar: LiteralString = " ", /) -> LiteralString: + def ljust(self: literal str, width: SupportsIndex, fillchar: literal str = " ", /) -> literal str: """Return a left-justified string of length width. Padding is done using the specified fill character (default is @@ -1021,18 +1022,18 @@ class str(Sequence[str]): """ def ljust(self, width: SupportsIndex, fillchar: str = " ", /) -> str - def lower(self: LiteralString) -> LiteralString: + def lower(self: literal str) -> literal str: """Return a copy of the string converted to lowercase.""" def lower(self) -> str - def lstrip(self: LiteralString, chars: LiteralString | None = None, /) -> LiteralString: + def lstrip(self: literal str, chars: literal str | None = None, /) -> literal str: """Return a copy of the string with leading whitespace removed. If chars is given and not None, remove characters in chars instead. """ def lstrip(self, chars: str | None = None, /) -> str - def partition(self: LiteralString, sep: LiteralString, /) -> (LiteralString, LiteralString, LiteralString): + def partition(self: literal str, sep: literal str, /) -> (literal str, literal str, literal str): """Partition the string into three parts using the given separator. This will search for the separator in the string. If the separator @@ -1045,7 +1046,7 @@ class str(Sequence[str]): def partition(self, sep: str, /) -> (str, str, str) if sys.version_info >= (3, 13): - def replace(self: LiteralString, old: LiteralString, new: LiteralString, /, count: SupportsIndex = -1) -> LiteralString: + def replace(self: literal str, old: literal str, new: literal str, /, count: SupportsIndex = -1) -> literal str: """Return a copy with all occurrences of substring old replaced by new. count @@ -1057,7 +1058,7 @@ class str(Sequence[str]): """ def replace(self, old: str, new: str, /, count: SupportsIndex = -1) -> str else: - def replace(self: LiteralString, old: LiteralString, new: LiteralString, count: SupportsIndex = -1, /) -> LiteralString: + def replace(self: literal str, old: literal str, new: literal str, count: SupportsIndex = -1, /) -> literal str: """Return a copy with all occurrences of substring old replaced by new. count @@ -1069,7 +1070,7 @@ class str(Sequence[str]): """ def replace(self, old: str, new: str, count: SupportsIndex = -1, /) -> str - def removeprefix(self: LiteralString, prefix: LiteralString, /) -> LiteralString: + def removeprefix(self: literal str, prefix: literal str, /) -> literal str: """Return a str with the given prefix string removed if present. If the string starts with the prefix string, return @@ -1078,7 +1079,7 @@ class str(Sequence[str]): """ def removeprefix(self, prefix: str, /) -> str - def removesuffix(self: LiteralString, suffix: LiteralString, /) -> LiteralString: + def removesuffix(self: literal str, suffix: literal str, /) -> literal str: """Return a str with the given suffix string removed if present. If the string ends with the suffix string and that suffix is not @@ -1101,7 +1102,7 @@ class str(Sequence[str]): notation. Raises ValueError when the substring is not found. """ - def rjust(self: LiteralString, width: SupportsIndex, fillchar: LiteralString = " ", /) -> LiteralString: + def rjust(self: literal str, width: SupportsIndex, fillchar: literal str = " ", /) -> literal str: """Return a right-justified string of length width. Padding is done using the specified fill character (default is @@ -1109,7 +1110,7 @@ class str(Sequence[str]): """ def rjust(self, width: SupportsIndex, fillchar: str = " ", /) -> str - def rpartition(self: LiteralString, sep: LiteralString, /) -> (LiteralString, LiteralString, LiteralString): + def rpartition(self: literal str, sep: literal str, /) -> (literal str, literal str, literal str): """Partition the string into three parts using the given separator. This will search for the separator in the string, starting at the @@ -1122,7 +1123,7 @@ class str(Sequence[str]): """ def rpartition(self, sep: str, /) -> (str, str, str) - def rsplit(self: LiteralString, sep: LiteralString | None = None, maxsplit: SupportsIndex = -1) -> list[LiteralString]: + def rsplit(self: literal str, sep: literal str | None = None, maxsplit: SupportsIndex = -1) -> list[literal str]: """Return a list of the substrings in the string, using sep as the separator string. sep @@ -1139,14 +1140,14 @@ class str(Sequence[str]): """ def rsplit(self, sep: str | None = None, maxsplit: SupportsIndex = -1) -> list[str] - def rstrip(self: LiteralString, chars: LiteralString | None = None, /) -> LiteralString: + def rstrip(self: literal str, chars: literal str | None = None, /) -> literal str: """Return a copy of the string with trailing whitespace removed. If chars is given and not None, remove characters in chars instead. """ def rstrip(self, chars: str | None = None, /) -> str - def split(self: LiteralString, sep: LiteralString | None = None, maxsplit: SupportsIndex = -1) -> list[LiteralString]: + def split(self: literal str, sep: literal str | None = None, maxsplit: SupportsIndex = -1) -> list[literal str]: """Return a list of the substrings in the string, using sep as the separator string. sep @@ -1167,7 +1168,7 @@ class str(Sequence[str]): """ def split(self, sep: str | None = None, maxsplit: SupportsIndex = -1) -> list[str] - def splitlines(self: LiteralString, keepends: bool = False) -> list[LiteralString]: + def splitlines(self: literal str, keepends: bool = False) -> list[literal str]: """Return a list of the lines in the string, breaking at line boundaries. Line breaks are not included in the resulting list unless keepends @@ -1188,18 +1189,18 @@ class str(Sequence[str]): Optional stop position. Default: end of the string. """ - def strip(self: LiteralString, chars: LiteralString | None = None, /) -> LiteralString: + def strip(self: literal str, chars: literal str | None = None, /) -> literal str: """Return a copy of the string with leading and trailing whitespace removed. If chars is given and not None, remove characters in chars instead. """ def strip(self, chars: str | None = None, /) -> str - def swapcase(self: LiteralString) -> LiteralString: + def swapcase(self: literal str) -> literal str: """Convert uppercase characters to lowercase and lowercase characters to uppercase.""" def swapcase(self) -> str - def title(self: LiteralString) -> LiteralString: + def title(self: literal str) -> literal str: """Return a version of the string where each word is titlecased. More specifically, words start with uppercased characters and all @@ -1220,11 +1221,11 @@ class str(Sequence[str]): None are deleted. """ - def upper(self: LiteralString) -> LiteralString: + def upper(self: literal str) -> literal str: """Return a copy of the string converted to uppercase.""" def upper(self) -> str - def zfill(self: LiteralString, width: SupportsIndex, /) -> LiteralString: + def zfill(self: literal str, width: SupportsIndex, /) -> literal str: """Pad a numeric string with zeros on the left, to fill a field of the given width. The string is never truncated. @@ -1286,7 +1287,7 @@ class str(Sequence[str]): @staticmethod def maketrans(x: str, y: str, z: str, /) -> dict[int, int | None] - def __add__(self: LiteralString, value: LiteralString, /) -> LiteralString: + def __add__(self: literal str, value: literal str, /) -> literal str: """Return self+value.""" def __add__(self, value: str, /) -> str @@ -1297,14 +1298,14 @@ class str(Sequence[str]): override def __eq__(self, value: object, /) -> bool def __ge__(self, value: str, /) -> bool - override def __getitem__(self: LiteralString, key: SupportsIndex | slice[SupportsIndex | None], /) -> LiteralString: + override def __getitem__(self: literal str, key: SupportsIndex | slice[SupportsIndex | None], /) -> literal str: """Return self[key].""" def __getitem__(self, key: SupportsIndex | slice[SupportsIndex | None], /) -> str def __gt__(self, value: str, /) -> bool override def __hash__(self) -> int - override def __iter__(self: LiteralString) -> Iterator[LiteralString]: + override def __iter__(self: literal str) -> Iterator[literal str]: """Implement iter(self).""" def __iter__(self) -> Iterator[str] @@ -1314,17 +1315,17 @@ class str(Sequence[str]): def __lt__(self, value: str, /) -> bool - def __mod__(self: LiteralString, value: LiteralString | (*: LiteralString), /) -> LiteralString: + def __mod__(self: literal str, value: literal str | (*: literal str), /) -> literal str: """Return self%value.""" def __mod__(self, value: dynamic, /) -> str - def __mul__(self: LiteralString, value: SupportsIndex, /) -> LiteralString: + def __mul__(self: literal str, value: SupportsIndex, /) -> literal str: """Return self*value.""" def __mul__(self, value: SupportsIndex, /) -> str override def __ne__(self, value: object, /) -> bool - def __rmul__(self: LiteralString, value: SupportsIndex, /) -> LiteralString: + def __rmul__(self: literal str, value: SupportsIndex, /) -> literal str: """Return value*self.""" def __rmul__(self, value: SupportsIndex, /) -> str @@ -1826,7 +1827,7 @@ class bytearray(MutableSequence[int]): init(self, ints: Iterable[SupportsIndex] | SupportsIndex | ReadableBuffer, /) init(self, string: str, /, encoding: str, errors: str = "strict") - override def append(self, item: SupportsIndex, /) -> None: + override def append(self, item: SupportsIndex, /): """Append a single item to the end of the bytearray. item @@ -1896,7 +1897,7 @@ class bytearray(MutableSequence[int]): If tabsize is not given, a tab size of 8 characters is assumed. """ - override def extend(self, iterable_of_ints: Iterable[SupportsIndex], /) -> None: + override def extend(self, iterable_of_ints: Iterable[SupportsIndex], /): """Append all the items from the iterator or sequence to the end of the bytearray. iterable_of_ints @@ -1950,7 +1951,7 @@ class bytearray(MutableSequence[int]): Raise ValueError if the subsection is not found. """ - override def insert(self, index: SupportsIndex, item: SupportsIndex, /) -> None: + override def insert(self, index: SupportsIndex, item: SupportsIndex, /): """Insert a single item into the bytearray before the given index. index @@ -2066,7 +2067,7 @@ class bytearray(MutableSequence[int]): If no index argument is given, will pop the last item. """ - override def remove(self, value: int, /) -> None: + override def remove(self, value: int, /): """Remove the first occurrence of a value in the bytearray. value @@ -2309,7 +2310,7 @@ class bytearray(MutableSequence[int]): """Set self[key] to value.""" def __setitem__(self, key: slice[SupportsIndex | None], value: Iterable[SupportsIndex] | bytes, /) -> None - override def __delitem__(self, key: SupportsIndex | slice[SupportsIndex | None], /) -> None: + override def __delitem__(self, key: SupportsIndex | slice[SupportsIndex | None], /): """Delete self[key].""" def __add__(self, value: ReadableBuffer, /) -> bytearray: @@ -2563,6 +2564,7 @@ final class slice[out StartT = dynamic, out StopT = StartT, out StepT = StartT | override def __eq__(self, value: object, /) -> bool if sys.version_info >= (3, 12): def __hash__(self) -> int + else: __hash__: ClassVar[None] @@ -2654,10 +2656,10 @@ class list[in out Element](MutableSequence[Element]): def copy[WidenElement = Never](self) -> list[Element | WidenElement]: """Return a shallow copy of the list.""" - override def append(self, object: Element, /) -> None: + override def append(self, object: Element, /): """Append object to the end of the list.""" - override def extend(self, iterable: Iterable[Element], /) -> None: + override def extend(self, iterable: Iterable[Element], /): """Extend list by appending elements from the iterable.""" override def pop(self, index: SupportsIndex = -1, /) -> Element: @@ -2677,10 +2679,10 @@ class list[in out Element](MutableSequence[Element]): override def count(self, value: Element, /) -> int: """Return number of occurrences of value.""" - override def insert(self, index: SupportsIndex, object: Element, /) -> None: + override def insert(self, index: SupportsIndex, object: Element, /): """Insert object before index.""" - override def remove(self, value: Element, /) -> None: + override def remove(self, value: Element, /): """Remove first occurrence of value. Raises ValueError if the value is not present. @@ -2720,7 +2722,7 @@ class list[in out Element](MutableSequence[Element]): """Set self[key] to value.""" def __setitem__(self, key: slice[SupportsIndex | None], value: Iterable[Element], /) -> None - override def __delitem__(self, key: SupportsIndex | slice[SupportsIndex | None], /) -> None: + override def __delitem__(self, key: SupportsIndex | slice[SupportsIndex | None], /): """Delete self[key].""" # Overloading looks unnecessary, but is needed to work around complex mypy problems @@ -2834,10 +2836,10 @@ class dict[in out Key: Hashable, in out Value](MutableMapping[Key, Value]): override def __getitem__(self, key: Overlapping[Key], /) -> Value: """Return self[key].""" - override def __setitem__(self, key: Key, value: Value, /) -> None: + override def __setitem__(self, key: Key, value: Value, /): """Set self[key] to value.""" - override def __delitem__(self, key: Key, /) -> None: + override def __delitem__(self, key: Key, /): """Delete self[key].""" override def __iter__(self) -> Iterator[Key]: @@ -2858,6 +2860,7 @@ class dict[in out Key: Hashable, in out Value](MutableMapping[Key, Value]): def __ror__[T1, T2, WidenKey = Never, WidenValue = Never](self, value: dict[T1, T2], /) -> dict[Key | T1 | WidenKey, Value | T2 | WidenValue]: """Return value|self.""" def __ror__[T1, T2](self, value: frozendict[T1, T2], /) -> frozendict[Key | T1, Value | T2] + else: def __or__[T1, T2, WidenKey = Never, WidenValue = Never](self, value: dict[T1, T2], /) -> dict[Key | T1 | WidenKey, Value | T2 | WidenValue]: """Return self|value.""" @@ -2946,7 +2949,7 @@ class set[in out Element: Hashable](MutableSet[Element]): init(self) init(self, iterable: Iterable[Element], /) - override def add(self, element: Element, /) -> None: + override def add(self, element: Element, /): """Add an element to a set. This has no effect if the element is already present. @@ -2961,7 +2964,7 @@ class set[in out Element: Hashable](MutableSet[Element]): def difference_update(self, *s: Iterable[object]): """Update the set, removing elements found in others.""" - override def discard(self, element: object, /) -> None: + override def discard(self, element: object, /): """Remove an element from a set if it is a member. Unlike set.remove(), the discard() method does not raise @@ -2983,7 +2986,7 @@ class set[in out Element: Hashable](MutableSet[Element]): def issuperset(self, s: Iterable[object], /) -> bool: """Report whether this set contains another set.""" - override def remove(self, element: Element, /) -> None: + override def remove(self, element: Element, /): """Remove an element from a set; it must be a member. If the element is not a member, raise a KeyError. @@ -3288,7 +3291,7 @@ def breakpoint(*args: dynamic, **kws: dynamic): By default, this drops you into the pdb debugger. """ -def callable(obj: object, /) -> obj is (...) -> object: +def callable(obj: object, /) -> obj is Top[(...) -> object]: """Return whether the object is callable (i.e., some kind of function). Note that classes are callable, as are instances of classes with a @@ -4147,7 +4150,7 @@ private type SupportsSomeKindOfPow = ( ) # TODO: `pow(int, int, Literal[0])` fails at runtime, -# but adding a `NoReturn` overload isn't a good solution for expressing that (see #8566). +# but adding a `Never` overload isn't a good solution for expressing that (see #8566). def pow(base: int, exp: int, mod: int) -> int: """Equivalent to base**exp with 2 arguments or base**exp % mod with 3 arguments @@ -4178,12 +4181,16 @@ def pow(base: SupportsSomeKindOfPow, exp: complex, mod: None = None) -> complex quit: _sitebuiltins.Quitter +@type_check_only +private protocol SupportsReversed[out Element]: + def __reversed__(self) -> Element + @disjoint_base -class reversed[in out Element]: +class reversed[out Element]: """Return a reverse iterator over the values of the given sequence.""" - def __new__(cls, sequence: Reversible[Element], /) -> Iterator[Element] - def __new__(cls, sequence: SupportsLenAndGetItem[Element], /) -> Iterator[Element] + def __new__[T](cls, sequence: SupportsReversed[T], /) -> T + def __new__(cls, sequence: SupportsLenAndGetItem[Element], /) -> Self def __iter__(self) -> Self: """Implement iter(self).""" @@ -4406,7 +4413,7 @@ class BaseException: __suppress_context__: bool __traceback__: TracebackType | None init(self, *args: object) - def __new__(cls, *args: dynamic, **kwds: dynamic) -> Self + def __new__(cls, /, *args: dynamic, **kwds: dynamic) -> Self def __setstate__(self, state: dict[str, dynamic] | None, /) def with_traceback(self, tb: TracebackType | None, /) -> Self: """Set self.__traceback__ to tb and return self.""" diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/bz2.byi b/crates/ty_vendored/vendor/typeshed/stdlib/bz2.byi index 2362a285ce..d590e595e0 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/bz2.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/bz2.byi @@ -5,7 +5,7 @@ This module provides a file interface, classes for incremental """ import sys -from _bz2 import BZ2Compressor as BZ2Compressor, BZ2Decompressor as BZ2Decompressor +from _bz2 export BZ2Compressor, BZ2Decompressor from _typeshed import ReadableBuffer, StrOrBytesPath, WriteableBuffer from collections.abc import Iterable from io import TextIOWrapper @@ -231,7 +231,7 @@ class BZ2File(BaseStream, IO[bytes]): is called. """ - override def writelines(self, seq: Iterable[ReadableBuffer]) -> None: + override def writelines(self, seq: Iterable[ReadableBuffer]): """Write a sequence of byte strings to the file. Returns the number of uncompressed bytes written. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/cmd.byi b/crates/ty_vendored/vendor/typeshed/stdlib/cmd.byi index 48849ef7a6..c7892f2c86 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/cmd.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/cmd.byi @@ -44,12 +44,11 @@ functions respectively. from collections.abc import Callable from typing import Final -from typing_extensions import LiteralString __all__ = ["Cmd"] PROMPT: Final = "(Cmd) " -final IDENTCHARS: LiteralString # Too big to be `Literal` +final IDENTCHARS: literal str # Too big to be `Literal` class Cmd: """A simple framework for writing line-oriented command interpreters. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/codecs.byi b/crates/ty_vendored/vendor/typeshed/stdlib/codecs.byi index b0af8b6f90..4b2f05aaac 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/codecs.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/codecs.byi @@ -220,7 +220,7 @@ def getwriter(encoding: str) -> _StreamWriter: """ -@deprecated("Deprecated since Python 3.14. Use `open()` instead.") +@deprecated("Deprecated. Use `open()` instead.") def open( filename: str, mode: str = "r", encoding: str | None = None, errors: str = "strict", buffering: int = -1 ) -> StreamReaderWriter: @@ -682,20 +682,20 @@ class StreamReaderWriter(TextIO): """Return the next decoded line from the input stream.""" override def __iter__(self) -> Self - override def write(self, data: str) -> None - override def writelines(self, list: Iterable[str]) -> None + override def write(self, data: str) + override def writelines(self, list: Iterable[str]) def reset(self) - override def seek(self, offset: int, whence: int = 0) -> None + override def seek(self, offset: int, whence: int = 0) override def __enter__(self) -> Self - override def __exit__(self, type: type[BaseException] | None, value: BaseException | None, tb: types.TracebackType | None) -> None + override def __exit__(self, type: type[BaseException] | None, value: BaseException | None, tb: types.TracebackType | None) def __getattr__(self, name: str) -> dynamic: """Inherit all other methods from the underlying stream.""" # These methods don't actually exist directly, but they are needed to satisfy the TextIO # interface. At runtime, they are delegated through __getattr__. - override def close(self) -> None + override def close(self) override def fileno(self) -> int - override def flush(self) -> None + override def flush(self) override def isatty(self) -> bool override def readable(self) -> bool override def truncate(self, size: int | None = ...) -> int @@ -757,20 +757,20 @@ class StreamRecoder(BinaryIO): override def __iter__(self) -> Self # Base class accepts more types than just bytes - override def write(self, data: bytes) -> None - override def writelines(self, list: Iterable[bytes]) -> None + override def write(self, data: bytes) + override def writelines(self, list: Iterable[bytes]) def reset(self) def __getattr__(self, name: str) -> dynamic: """Inherit all other methods from the underlying stream.""" override def __enter__(self) -> Self - override def __exit__(self, type: type[BaseException] | None, value: BaseException | None, tb: types.TracebackType | None) -> None - override def seek(self, offset: int, whence: int = 0) -> None + override def __exit__(self, type: type[BaseException] | None, value: BaseException | None, tb: types.TracebackType | None) + override def seek(self, offset: int, whence: int = 0) # These methods don't actually exist directly, but they are needed to satisfy the BinaryIO # interface. At runtime, they are delegated through __getattr__. - override def close(self) -> None + override def close(self) override def fileno(self) -> int - override def flush(self) -> None + override def flush(self) override def isatty(self) -> bool override def readable(self) -> bool override def truncate(self, size: int | None = ...) -> int diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/collections/__init__.byi b/crates/ty_vendored/vendor/typeshed/stdlib/collections/__init__.byi index 8deee2f951..bf9946e650 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/collections/__init__.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/collections/__init__.byi @@ -31,7 +31,7 @@ from collections.abc import ( ) from types import GenericAlias from typing import ClassVar, Generic, TypeVar, final, type_check_only -from typing_extensions import Self, disjoint_base +from typing_extensions import Never, Self, disjoint_base if sys.version_info >= (3, 15): from builtins import frozendict @@ -98,8 +98,8 @@ class UserDict[in out Key, in out Value](MutableMapping[Key, Value]): override def __len__(self) -> int override def __getitem__(self, key: Key) -> Value - override def __setitem__(self, key: Key, item: Value) -> None - override def __delitem__(self, key: Key) -> None + override def __setitem__(self, key: Key, item: Value) + override def __delitem__(self, key: Key) override def __iter__(self) -> Iterator[Key] override def __contains__(self, key: object) -> bool def copy(self) -> Self @@ -151,17 +151,17 @@ class UserList[in out Element](MutableSequence[Element]): override def __setitem__(self, i: SupportsIndex, item: Element) -> None def __setitem__(self, i: slice[SupportsIndex | None], item: Iterable[Element]) -> None - override def __delitem__(self, i: SupportsIndex | slice[SupportsIndex | None]) -> None + override def __delitem__(self, i: SupportsIndex | slice[SupportsIndex | None]) def __add__(self, other: Iterable[Element]) -> Self def __radd__(self, other: Iterable[Element]) -> Self override def __iadd__(self, other: Iterable[Element]) -> Self def __mul__(self, n: int) -> Self def __rmul__(self, n: int) -> Self def __imul__(self, n: int) -> Self - override def append(self, item: Element) -> None - override def insert(self, i: int, item: Element) -> None + override def append(self, item: Element) + override def insert(self, i: int, item: Element) override def pop(self, i: int = -1) -> Element - override def remove(self, item: Element) -> None + override def remove(self, item: Element) def copy(self) -> Self def __copy__(self) -> Self override def count(self, item: Element) -> int @@ -174,7 +174,7 @@ class UserList[in out Element](MutableSequence[Element]): def sort(self: UserList[SupportsRichComparisonT], *, key: None = None, reverse: bool = False) -> None def sort(self, *, key: (Element) -> SupportsRichComparison, reverse: bool = False) -> None - override def extend(self, other: Iterable[Element]) -> None + override def extend(self, other: Iterable[Element]) class UserString(Sequence[UserString]): data: str @@ -257,7 +257,7 @@ class deque[in out Element](MutableSequence[Element]): init(self, *, maxlen: int | None = None) init(self, iterable: Iterable[Element], maxlen: int | None = None) - override def append(self, x: Element, /) -> None: + override def append(self, x: Element, /): """Add an element to the right side of the deque.""" def appendleft(self, x: Element, /): @@ -269,13 +269,13 @@ class deque[in out Element](MutableSequence[Element]): override def count(self, x: Element, /) -> int: """Return number of occurrences of value.""" - override def extend(self, iterable: Iterable[Element], /) -> None: + override def extend(self, iterable: Iterable[Element], /): """Extend the right side of the deque with elements from the iterable.""" def extendleft(self, iterable: Iterable[Element], /): """Extend the left side of the deque with elements from the iterable.""" - override def insert(self, i: int, x: Element, /) -> None: + override def insert(self, i: int, x: Element, /): """Insert value before index.""" override def index(self, x: Element, start: int = 0, stop: int = ..., /) -> int: @@ -290,7 +290,7 @@ class deque[in out Element](MutableSequence[Element]): def popleft(self) -> Element: """Remove and return the leftmost element.""" - override def remove(self, value: Element, /) -> None: + override def remove(self, value: Element, /): """Remove first occurrence of value.""" def rotate(self, n: int = 1, /): @@ -307,10 +307,10 @@ class deque[in out Element](MutableSequence[Element]): override def __getitem__(self, key: SupportsIndex, /) -> Element: """Return self[key].""" - override def __setitem__(self, key: SupportsIndex, value: Element, /) -> None: + override def __setitem__(self, key: SupportsIndex, value: Element, /): """Set self[key] to value.""" - override def __delitem__(self, key: SupportsIndex, /) -> None: + override def __delitem__(self, key: SupportsIndex, /): """Delete self[key].""" override def __contains__(self, key: object, /) -> bool: @@ -328,6 +328,9 @@ class deque[in out Element](MutableSequence[Element]): def __mul__(self, value: int, /) -> Self: """Return self*value.""" + def __rmul__(self, value: int, /) -> Self: + """Return value*self.""" + def __imul__(self, value: int, /) -> Self: """Implement self*=value.""" @@ -430,7 +433,7 @@ class Counter[in out Element](dict[Element, int]): """ - override class def fromkeys(cls, iterable: dynamic, v: int | None = None) -> NoReturn + override class def fromkeys(cls, iterable: dynamic, v: int | None = None) -> Never def subtract(self, iterable: None = None, /) -> None: """Like dict.update() but subtracts counts instead of replacing them. @@ -479,7 +482,7 @@ class Counter[in out Element](dict[Element, int]): def __missing__(self, key: Element) -> int: """The count of elements not in the Counter is zero.""" - override def __delitem__(self, elem: object) -> None: + override def __delitem__(self, elem: object): """Like dict.__delitem__() but does not raise KeyError for missing values.""" override def __eq__(self, other: object) -> bool: @@ -804,8 +807,8 @@ class ChainMap[in out Key, in out Value](MutableMapping[Key, Value]): let parents: Self - override def __setitem__(self, key: Key, value: Value) -> None - override def __delitem__(self, key: Key) -> None + override def __setitem__(self, key: Key, value: Value) + override def __delitem__(self, key: Key) override def __getitem__(self, key: Key) -> Value override def __iter__(self) -> Iterator[Key] override def __len__(self) -> int diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/collections/abc.byi b/crates/ty_vendored/vendor/typeshed/stdlib/collections/abc.byi index 337264c60c..c6e746887c 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/collections/abc.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/collections/abc.byi @@ -4,4 +4,4 @@ Unit tests are in test_collections. """ from _collections_abc import * -from _collections_abc import __all__ as __all__ +from _collections_abc export __all__ diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/__init__.byi b/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/__init__.byi index e3d5d8aa96..10ae7a7b48 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/__init__.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/__init__.byi @@ -2,24 +2,24 @@ 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 ._base export ( + ALL_COMPLETED, + FIRST_COMPLETED, + FIRST_EXCEPTION, + BrokenExecutor, + CancelledError, + Executor, + Future, + InvalidStateError, + TimeoutError, + as_completed, + wait, ) -from .process import ProcessPoolExecutor as ProcessPoolExecutor -from .thread import ThreadPoolExecutor as ThreadPoolExecutor +from .process export ProcessPoolExecutor +from .thread export ThreadPoolExecutor if sys.version_info >= (3, 14): - from .interpreter import InterpreterPoolExecutor as InterpreterPoolExecutor + from .interpreter export InterpreterPoolExecutor __all__ = [ "FIRST_COMPLETED", diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/_base.byi b/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/_base.byi index f8a9985c83..10aadb3256 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/_base.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/_base.byi @@ -25,7 +25,7 @@ class CancelledError(Error): """The Future was cancelled.""" if sys.version_info >= (3, 11): - from builtins import TimeoutError as TimeoutError + from builtins export TimeoutError else: class TimeoutError(Error): """The operation exceeded the given deadline.""" diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/process.byi b/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/process.byi index 91aa10da0e..e4bab6568d 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/process.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/process.byi @@ -230,12 +230,17 @@ class _ExecutorManagerThread(Thread): work_ids_queue: Queue[int] pending_work_items: dict[int, _WorkItem[dynamic]] init(self, executor: ProcessPoolExecutor) - override def run(self) -> None + override def run(self) def add_call_item_to_queue(self) def wait_result_broken_or_wakeup(self) -> (dynamic, bool, str) def process_result_item(self, result_item: int | _ResultItem) def is_shutting_down(self) -> bool - def terminate_broken(self, cause: str) + + if sys.version_info >= (3, 15): + def terminate_broken(self, cause: str, bpe_message: str | None = None) -> None + else: + def terminate_broken(self, cause: str) -> None + def flag_executor_shutting_down(self) def shutdown_workers(self) def join_executor_internals(self) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/interpreters/_queues.byi b/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/interpreters/_queues.byi index e778f1318e..bae05425ff 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/interpreters/_queues.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/interpreters/_queues.byi @@ -6,7 +6,7 @@ from typing import Final from typing_extensions import Self if sys.version_info >= (3, 14): # needed to satisfy pyright checks for Python <= 3.13 - from _interpqueues import QueueError as QueueError, QueueNotFoundError as QueueNotFoundError + from _interpqueues export QueueError, QueueNotFoundError from . import _crossinterp from ._crossinterp import UNBOUND_ERROR as UNBOUND_ERROR, UNBOUND_REMOVE as UNBOUND_REMOVE, UnboundItem, _AnyUnbound diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/configparser.byi b/crates/ty_vendored/vendor/typeshed/stdlib/configparser.byi index 3ed94568ce..003ac1d927 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/configparser.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/configparser.byi @@ -423,8 +423,8 @@ class RawConfigParser(_Parser): override def __len__(self) -> int override def __getitem__(self, key: _SectionName) -> SectionProxy - override def __setitem__(self, key: _SectionName, value: _Section) -> None - override def __delitem__(self, key: _SectionName) -> None + override def __setitem__(self, key: _SectionName, value: _Section) + override def __delitem__(self, key: _SectionName) override def __iter__(self) -> Iterator[str] override def __contains__(self, key: object) -> bool def defaults(self) -> _Section @@ -618,8 +618,8 @@ class SectionProxy(MutableMapping[str, str]): """Creates a view on a section of the specified `name` in `parser`.""" override def __getitem__(self, key: str) -> str - override def __setitem__(self, key: str, value: str) -> None - override def __delitem__(self, key: str) -> None + override def __setitem__(self, key: str, value: str) + override def __delitem__(self, key: str) override def __contains__(self, key: object) -> bool override def __len__(self) -> int override def __iter__(self) -> Iterator[str] @@ -679,8 +679,8 @@ class ConverterMapping(MutableMapping[str, ConverterCallback | None]): GETTERCRE: ClassVar[Pattern[dynamic]] init(self, parser: RawConfigParser) override def __getitem__(self, key: str) -> ConverterCallback - override def __setitem__(self, key: str, value: ConverterCallback | None) -> None - override def __delitem__(self, key: str) -> None + override def __setitem__(self, key: str, value: ConverterCallback | None) + override def __delitem__(self, key: str) override def __iter__(self) -> Iterator[str] override def __len__(self) -> int diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/contextlib.byi b/crates/ty_vendored/vendor/typeshed/stdlib/contextlib.byi index 5c964c5acb..f4c2e94efc 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/contextlib.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/contextlib.byi @@ -205,7 +205,7 @@ class closing[in out SupportsCloseT: SupportsClose](AbstractContextManager[Suppo init(self, thing: SupportsCloseT) override def __enter__(self) -> SupportsCloseT - override def __exit__(self, *exc_info: Unused) -> None + override def __exit__(self, *exc_info: Unused) @type_check_only private protocol SupportsAclose: @@ -247,7 +247,7 @@ class suppress(AbstractContextManager[None, bool]): """ init(self, *exceptions: type[BaseException]) - override def __enter__(self) -> None + override def __enter__(self) override def __exit__( self, exctype: type[BaseException] | None, excinst: BaseException | None, exctb: TracebackType | None ) -> bool @@ -266,7 +266,7 @@ class _RedirectStream[in out SupportsRedirectT: SupportsRedirect | None](Abstrac override def __enter__(self) -> SupportsRedirectT override def __exit__( self, exctype: type[BaseException] | None, excinst: BaseException | None, exctb: TracebackType | None - ) -> None + ) class redirect_stdout[in out SupportsRedirectT: SupportsRedirect | None](_RedirectStream[SupportsRedirectT]): """Context manager for temporarily redirecting stdout to another file. @@ -404,7 +404,7 @@ class nullcontext[in out Element](AbstractContextManager[Element, None], Abstrac init(self: nullcontext[Element], enter_result: Element) override def __enter__(self) -> Element - override def __exit__(self, *exctype: Unused) -> None + override def __exit__(self, *exctype: Unused) override async def __aenter__(self) -> Element override async def __aexit__(self, *exctype: Unused) -> None diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/contextvars.byi b/crates/ty_vendored/vendor/typeshed/stdlib/contextvars.byi index 22dc33006e..a22dcb70b3 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/contextvars.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/contextvars.byi @@ -1,3 +1,3 @@ -from _contextvars import Context as Context, ContextVar as ContextVar, Token as Token, copy_context as copy_context +from _contextvars export Context, ContextVar, Token, copy_context __all__ = ("Context", "ContextVar", "Token", "copy_context") diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/csv.byi b/crates/ty_vendored/vendor/typeshed/stdlib/csv.byi index 7baeccbb55..16c4c2d830 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/csv.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/csv.byi @@ -82,7 +82,7 @@ from _csv import ( ) if sys.version_info >= (3, 12): - from _csv import QUOTE_NOTNULL as QUOTE_NOTNULL, QUOTE_STRINGS as QUOTE_STRINGS + from _csv export QUOTE_NOTNULL, QUOTE_STRINGS from _csv import Reader, Writer from _typeshed import SupportsWrite from collections.abc import Collection, Iterable, Mapping, Sequence diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/ctypes/__init__.byi b/crates/ty_vendored/vendor/typeshed/stdlib/ctypes/__init__.byi index 2e43e75814..3e98b2cda1 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/ctypes/__init__.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/ctypes/__init__.byi @@ -26,19 +26,19 @@ from _ctypes import ( sizeof as sizeof, ) from _typeshed import StrPath, SupportsBool, SupportsLen -from ctypes._endian import BigEndianStructure as BigEndianStructure, LittleEndianStructure as LittleEndianStructure +from ctypes._endian export BigEndianStructure, LittleEndianStructure from types import GenericAlias from typing import ClassVar, Final, Generic, Literal, TypeAlias, TypeVar, type_check_only from typing_extensions import Self, 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 + from _ctypes export FormatError, get_last_error, set_last_error if sys.version_info >= (3, 14): - from _ctypes import COMError as COMError, CopyComPointer as CopyComPointer + from _ctypes export COMError, CopyComPointer if sys.version_info >= (3, 11): - from ctypes._endian import BigEndianUnion as BigEndianUnion, LittleEndianUnion as LittleEndianUnion + from ctypes._endian export BigEndianUnion, LittleEndianUnion if sys.version_info >= (3, 14): @@ -61,7 +61,7 @@ if sys.version_info >= (3, 14): """ else: - from _ctypes import POINTER as POINTER, pointer as pointer + from _ctypes export POINTER, pointer if sys.version_info >= (3, 14): CField = _CField @@ -229,10 +229,10 @@ def create_unicode_buffer(init: int | str, size: int | None = None) -> Array[c_w """ if sys.version_info < (3, 15): - @deprecated("Deprecated since Python 3.13; will be removed in Python 3.15.") + @deprecated("Deprecated; will be removed in Python 3.15.") def SetPointerType(pointer: type[_Pointer[dynamic]], cls: _CTypeBaseType) -> None -@deprecated("Soft deprecated since Python 3.13. Use multiplication instead.") +@deprecated("Soft deprecated. Use multiplication instead.") def ARRAY[CT: _CData](typ: CT, len: int) -> Array[CT] if sys.platform == "win32": diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/ctypes/macholib/dyld.byi b/crates/ty_vendored/vendor/typeshed/stdlib/ctypes/macholib/dyld.byi index 37be9bd241..6b59ce1742 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/ctypes/macholib/dyld.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/ctypes/macholib/dyld.byi @@ -3,8 +3,8 @@ dyld emulation """ from collections.abc import Mapping -from ctypes.macholib.dylib import dylib_info as dylib_info -from ctypes.macholib.framework import framework_info as framework_info +from ctypes.macholib.dylib export dylib_info +from ctypes.macholib.framework export framework_info __all__ = ["dyld_find", "framework_find", "framework_info", "dylib_info"] diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/curses/__init__.byi b/crates/ty_vendored/vendor/typeshed/stdlib/curses/__init__.byi index 1e191b0248..9f9f3ee172 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/curses/__init__.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/curses/__init__.byi @@ -11,7 +11,7 @@ the package, and perhaps a particular module inside it. """ from _curses import * -from _curses import window as window +from _curses export window from _typeshed import structseq from collections.abc import Callable from typing import Final, ParamSpec, TypeVar, final, type_check_only diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/dataclasses.byi b/crates/ty_vendored/vendor/typeshed/stdlib/dataclasses.byi index ab418168d0..997bd8be19 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/dataclasses.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/dataclasses.byi @@ -7,6 +7,7 @@ from collections.abc import Callable, Iterable, Mapping from types import GenericAlias from typing import Final, Generic, Literal, Protocol, TypeVar, type_check_only from typing_extensions import Never +from ty_extensions import Top __all__ = [ @@ -384,22 +385,26 @@ def fields(class_or_instance: DataclassInstance | type[DataclassInstance]) -> (* """ # HACK: `obj: Never` typing matches if object argument is using `Any` type. -def is_dataclass(obj: Never) -> obj is DataclassInstance | type[DataclassInstance]: +def is_dataclass(obj: Never) -> obj is Top[DataclassInstance | type[DataclassInstance]]: """Returns True if obj is a dataclass or an instance of a dataclass. """ -def is_dataclass(obj: type) -> obj is type[DataclassInstance] -def is_dataclass(obj: object) -> obj is DataclassInstance | type[DataclassInstance] +def is_dataclass(obj: type) -> obj is Top[type[DataclassInstance]] +def is_dataclass(obj: object) -> obj is Top[DataclassInstance | type[DataclassInstance]] class FrozenInstanceError(AttributeError) class InitVar[in out Element]: __slots__ = ("type",) - type: Type[Element] + type: Type[Element] # ty:ignore[unbound-type-variable] init(self, type: Type[Element]) - def __class_getitem__(cls, type: Type[Element]) -> InitVar[Element] - def __class_getitem__(cls, type: dynamic) -> InitVar[dynamic] + def __class_getitem__( + cls, type: Type[Element] + ) -> InitVar[Element] + def __class_getitem__( + cls, type: dynamic + ) -> InitVar[dynamic] if sys.version_info >= (3, 14): def make_dataclass( diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/datetime.byi b/crates/ty_vendored/vendor/typeshed/stdlib/datetime.byi index afa3b5fa74..364c370748 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/datetime.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/datetime.byi @@ -7,7 +7,7 @@ time zone and DST data sources. import sys from time import struct_time from typing import ClassVar, Final, TypeAlias, final, type_check_only -from typing_extensions import CapsuleType, Self, deprecated, disjoint_base +from typing_extensions import CapsuleType, Never, Self, deprecated, disjoint_base if sys.version_info >= (3, 11): __all__ = ("date", "datetime", "time", "timedelta", "timezone", "tzinfo", "MINYEAR", "MAXYEAR", "UTC") @@ -51,7 +51,7 @@ final class timezone(tzinfo): override def utcoffset(self, dt: datetime | None, /) -> timedelta: """Return fixed offset.""" - override def dst(self, dt: datetime | None, /) -> None: + override def dst(self, dt: datetime | None, /): """Return None.""" override def __hash__(self) -> int @@ -174,7 +174,7 @@ class date: def __radd__(self, value: timedelta, /) -> Self: """Return value+self.""" - def __sub__(self, value: datetime, /) -> NoReturn: + def __sub__(self, value: datetime, /) -> Never: """Return self-value.""" def __sub__(self, value: Self, /) -> timedelta def __sub__(self, value: timedelta, /) -> Self diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/dbm/__init__.byi b/crates/ty_vendored/vendor/typeshed/stdlib/dbm/__init__.byi index 891176fbaa..7314960e42 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/dbm/__init__.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/dbm/__init__.byi @@ -44,8 +44,8 @@ type _TFlags = "r" | "w" | "c" | "n" | "rf" | "wf" | "cf" | "nf" | "rs" | "ws" | class _Database(MutableMapping[_KeyType, bytes]): def close(self) override def __getitem__(self, key: _KeyType) -> bytes - override def __setitem__(self, key: _KeyType, value: _ValueType) -> None - override def __delitem__(self, key: _KeyType) -> None + override def __setitem__(self, key: _KeyType, value: _ValueType) + override def __delitem__(self, key: _KeyType) override def __iter__(self) -> Iterator[bytes] override def __len__(self) -> int def __del__(self) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/dbm/dumb.byi b/crates/ty_vendored/vendor/typeshed/stdlib/dbm/dumb.byi index 3a1a63a024..e05fe835a6 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/dbm/dumb.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/dbm/dumb.byi @@ -45,8 +45,8 @@ class _Database(MutableMapping[_KeyType, bytes]): def iterkeys(self) -> Iterator[bytes] # undocumented def close(self) override def __getitem__(self, key: _KeyType) -> bytes - override def __setitem__(self, key: _KeyType, val: _ValueType) -> None - override def __delitem__(self, key: _KeyType) -> None + override def __setitem__(self, key: _KeyType, val: _ValueType) + override def __delitem__(self, key: _KeyType) override def __iter__(self) -> Iterator[bytes] override def __len__(self) -> int def __del__(self) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/dbm/sqlite3.byi b/crates/ty_vendored/vendor/typeshed/stdlib/dbm/sqlite3.byi index d4746bb696..d0b3f99d76 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/dbm/sqlite3.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/dbm/sqlite3.byi @@ -2,16 +2,16 @@ import sys from _typeshed import ReadableBuffer, StrOrBytesPath, Unused from collections.abc import Generator, MutableMapping from typing import Final, Literal, TypeAlias -from typing_extensions import LiteralString, Self - -final BUILD_TABLE: LiteralString -final GET_SIZE: LiteralString -final LOOKUP_KEY: LiteralString -final STORE_KV: LiteralString -final DELETE_KEY: LiteralString -final ITER_KEYS: LiteralString +from typing_extensions import Self + +final BUILD_TABLE: literal str +final GET_SIZE: literal str +final LOOKUP_KEY: literal str +final STORE_KV: literal str +final DELETE_KEY: literal str +final ITER_KEYS: literal str if sys.version_info >= (3, 15): - final REORGANIZE: LiteralString + final REORGANIZE: literal str private type SqliteData = str | ReadableBuffer | int | float @@ -21,8 +21,8 @@ class _Database(MutableMapping[bytes, bytes]): init(self, path: StrOrBytesPath, /, *, flag: "r" | "w" | "c" | "n", mode: int) override def __len__(self) -> int override def __getitem__(self, key: SqliteData) -> bytes - override def __setitem__(self, key: SqliteData, value: SqliteData) -> None - override def __delitem__(self, key: SqliteData) -> None + override def __setitem__(self, key: SqliteData, value: SqliteData) + override def __delitem__(self, key: SqliteData) override def __iter__(self) -> Generator[bytes] def close(self) override def keys(self) -> list[bytes] diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/decimal.byi b/crates/ty_vendored/vendor/typeshed/stdlib/decimal.byi index c81298747e..10a30d0789 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/decimal.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/decimal.byi @@ -100,29 +100,29 @@ NaN import numbers import sys -from _decimal import ( - HAVE_CONTEXTVAR as HAVE_CONTEXTVAR, - HAVE_THREADS as HAVE_THREADS, - MAX_EMAX as MAX_EMAX, - MAX_PREC as MAX_PREC, - MIN_EMIN as MIN_EMIN, - MIN_ETINY as MIN_ETINY, - ROUND_05UP as ROUND_05UP, - ROUND_CEILING as ROUND_CEILING, - ROUND_DOWN as ROUND_DOWN, - ROUND_FLOOR as ROUND_FLOOR, - ROUND_HALF_DOWN as ROUND_HALF_DOWN, - ROUND_HALF_EVEN as ROUND_HALF_EVEN, - ROUND_HALF_UP as ROUND_HALF_UP, - ROUND_UP as ROUND_UP, - BasicContext as BasicContext, - DefaultContext as DefaultContext, - ExtendedContext as ExtendedContext, - __libmpdec_version__ as __libmpdec_version__, - __version__ as __version__, - getcontext as getcontext, - localcontext as localcontext, - setcontext as setcontext, +from _decimal export ( + HAVE_CONTEXTVAR, + HAVE_THREADS, + MAX_EMAX, + MAX_PREC, + MIN_EMIN, + MIN_ETINY, + ROUND_05UP, + ROUND_CEILING, + ROUND_DOWN, + ROUND_FLOOR, + ROUND_HALF_DOWN, + ROUND_HALF_EVEN, + ROUND_HALF_UP, + ROUND_UP, + BasicContext, + DefaultContext, + ExtendedContext, + __libmpdec_version__, + __version__, + getcontext, + localcontext, + setcontext, ) from collections.abc import Container, Sequence from types import TracebackType @@ -130,9 +130,9 @@ from typing import ClassVar, Literal, NamedTuple, TypeAlias, final, type_check_o from typing_extensions import Self, disjoint_base if sys.version_info >= (3, 14): - from _decimal import IEEE_CONTEXT_MAX_BITS as IEEE_CONTEXT_MAX_BITS, IEEEContext as IEEEContext + from _decimal export IEEE_CONTEXT_MAX_BITS, IEEEContext if sys.version_info >= (3, 15): - from _decimal import SPEC_VERSION as SPEC_VERSION + from _decimal export SPEC_VERSION type _Decimal = Decimal | int private type DecimalNew = Decimal | float | str | (int, Sequence[int], int) @@ -727,7 +727,7 @@ class Context: # even settable attributes like `prec` and `rounding`, # but that's inexpressible in the stub. # Type checkers either ignore it or misinterpret it - # if you add a `def __delattr__(self, name: str, /) -> NoReturn` method to the stub + # if you add a `def __delattr__(self, name: str, /) -> Never` method to the stub prec: int rounding: str Emin: int diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/bdist.byi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/bdist.byi index 64388e6312..1977f8b3b3 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/bdist.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/bdist.byi @@ -29,6 +29,6 @@ class bdist(Command): skip_build: int group: Incomplete owner: Incomplete - override def initialize_options(self) -> None - override def finalize_options(self) -> None - override def run(self) -> None + override def initialize_options(self) + override def finalize_options(self) + override def run(self) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/bdist_dumb.byi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/bdist_dumb.byi index 4a8559aa26..e236ea56b7 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/bdist_dumb.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/bdist_dumb.byi @@ -24,6 +24,6 @@ class bdist_dumb(Command): relative: int owner: Incomplete group: Incomplete - override def initialize_options(self) -> None - override def finalize_options(self) -> None - override def run(self) -> None + override def initialize_options(self) + override def finalize_options(self) + override def run(self) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/bdist_rpm.byi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/bdist_rpm.byi index 92015d2972..d5cc56cf2f 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/bdist_rpm.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/bdist_rpm.byi @@ -53,7 +53,7 @@ class bdist_rpm(Command): no_autoreq: int force_arch: Incomplete quiet: int - override def initialize_options(self) -> None - override def finalize_options(self) -> None + override def initialize_options(self) + override def finalize_options(self) def finalize_package_data(self) - override def run(self) -> None + override def run(self) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build.byi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build.byi index e6a98bd570..2eb5c472ee 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build.byi @@ -28,9 +28,9 @@ class build(Command): force: int executable: Incomplete parallel: Incomplete - override def initialize_options(self) -> None - override def finalize_options(self) -> None - override def run(self) -> None + override def initialize_options(self) + override def finalize_options(self) + override def run(self) def has_pure_modules(self) def has_c_libraries(self) def has_ext_modules(self) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build_clib.byi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build_clib.byi index 840e5f71b9..b8cbc42feb 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build_clib.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build_clib.byi @@ -27,9 +27,9 @@ class build_clib(Command): debug: Incomplete force: int compiler: Incomplete - override def initialize_options(self) -> None - override def finalize_options(self) -> None - override def run(self) -> None + override def initialize_options(self) + override def finalize_options(self) + override def run(self) def check_library_list(self, libraries): """Ensure that the list of libraries is valid. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build_ext.byi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build_ext.byi index fd4fcf9e00..97892b8166 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build_ext.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build_ext.byi @@ -42,9 +42,9 @@ class build_ext(Command): swig_opts: Incomplete user: Incomplete parallel: Incomplete - override def initialize_options(self) -> None - override def finalize_options(self) -> None - override def run(self) -> None + override def initialize_options(self) + override def finalize_options(self) + override def run(self) def check_extensions_list(self, extensions): """Ensure that the list of extensions (presumably provided as a command option 'extensions') is valid, i.e. it is a list of diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build_py.byi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build_py.byi index 8651de1bbd..d606f3f0e4 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build_py.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build_py.byi @@ -7,7 +7,7 @@ from _typeshed import Incomplete from typing import ClassVar, Literal from ..cmd import Command -from ..util import Mixin2to3 as Mixin2to3 +from ..util export Mixin2to3 class build_py(Command): description: str @@ -22,11 +22,11 @@ class build_py(Command): compile: int optimize: int force: Incomplete - override def initialize_options(self) -> None + override def initialize_options(self) packages: Incomplete data_files: Incomplete - override def finalize_options(self) -> None - override def run(self) -> None + override def finalize_options(self) + override def run(self) def get_data_files(self): """Generate list of '(package,src_dir,build_dir,filenames)' tuples""" @@ -73,5 +73,5 @@ class build_py(Command): class build_py_2to3(build_py, Mixin2to3): updated_files: Incomplete - override def run(self) -> None + override def run(self) override def build_module(self, module, module_file, package) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build_scripts.byi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build_scripts.byi index 514267ffe3..4c2a141da6 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build_scripts.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build_scripts.byi @@ -7,7 +7,7 @@ from _typeshed import Incomplete from typing import ClassVar from ..cmd import Command -from ..util import Mixin2to3 as Mixin2to3 +from ..util export Mixin2to3 first_line_re: Incomplete @@ -20,10 +20,10 @@ class build_scripts(Command): force: Incomplete executable: Incomplete outfiles: Incomplete - override def initialize_options(self) -> None - override def finalize_options(self) -> None + override def initialize_options(self) + override def finalize_options(self) def get_source_files(self) - override def run(self) -> None + override def run(self) def copy_scripts(self): """Copy each script listed in 'self.scripts'; if it's marked as a Python script in the Unix way (first line matches 'first_line_re', diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/check.byi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/check.byi index 7ce02634fc..cdc02c9258 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/check.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/check.byi @@ -38,14 +38,14 @@ class check(Command): restructuredtext: int metadata: int strict: int - override def initialize_options(self) -> None: + override def initialize_options(self): """Sets default values for options.""" - override def finalize_options(self) -> None + override def finalize_options(self) override def warn(self, msg): """Counts the number of warnings that occurs.""" - override def run(self) -> None: + override def run(self): """Runs the command.""" def check_metadata(self): diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/clean.byi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/clean.byi index 669c2c6810..d73d5245d4 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/clean.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/clean.byi @@ -18,6 +18,6 @@ class clean(Command): build_scripts: Incomplete bdist_base: Incomplete all: Incomplete - override def initialize_options(self) -> None - override def finalize_options(self) -> None - override def run(self) -> None + override def initialize_options(self) + override def finalize_options(self) + override def run(self) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/config.byi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/config.byi index 47f460d859..60a47e8e97 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/config.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/config.byi @@ -31,9 +31,9 @@ class config(Command): noisy: int dump_source: int temp_files: Sequence[str] - override def initialize_options(self) -> None - override def finalize_options(self) -> None - override def run(self) -> None + override def initialize_options(self) + override def finalize_options(self) + override def run(self) def try_cpp( self, body: str | None = None, diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install.byi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install.byi index 067a106cfe..ee8ab4f939 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install.byi @@ -44,12 +44,12 @@ class install(Command): build_base: Incomplete build_lib: Incomplete record: Incomplete - override def initialize_options(self) -> None: + override def initialize_options(self): """Initializes options.""" config_vars: Incomplete install_libbase: Incomplete - override def finalize_options(self) -> None: + override def finalize_options(self): """Finalizes options.""" def dump_dirs(self, msg): @@ -86,7 +86,7 @@ class install(Command): def create_home_path(self): """Create directories under ~.""" - override def run(self) -> None: + override def run(self): """Runs the command.""" def create_path_file(self): diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_data.byi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_data.byi index ee16a28b9a..11fd6efaef 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_data.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_data.byi @@ -19,8 +19,8 @@ class install_data(Command): force: int data_files: Incomplete warn_dir: int - override def initialize_options(self) -> None - override def finalize_options(self) -> None - override def run(self) -> None + override def initialize_options(self) + override def finalize_options(self) + override def run(self) def get_inputs(self) def get_outputs(self) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_egg_info.byi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_egg_info.byi index 00eb33d281..857844a09d 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_egg_info.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_egg_info.byi @@ -15,11 +15,11 @@ class install_egg_info(Command): description: ClassVar[str] user_options: ClassVar[list[(str, str, str)]] install_dir: Incomplete - override def initialize_options(self) -> None + override def initialize_options(self) target: Incomplete outputs: Incomplete - override def finalize_options(self) -> None - override def run(self) -> None + override def finalize_options(self) + override def run(self) def get_outputs(self) -> list[str] def safe_name(name): diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_headers.byi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_headers.byi index 778df23a73..c5e97c921b 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_headers.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_headers.byi @@ -16,8 +16,8 @@ class install_headers(Command): install_dir: Incomplete force: int outfiles: Incomplete - override def initialize_options(self) -> None - override def finalize_options(self) -> None - override def run(self) -> None + override def initialize_options(self) + override def finalize_options(self) + override def run(self) def get_inputs(self) def get_outputs(self) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_lib.byi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_lib.byi index b48017e4d6..4bc5e5fce0 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_lib.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_lib.byi @@ -22,9 +22,9 @@ class install_lib(Command): compile: Incomplete optimize: Incomplete skip_build: Incomplete - override def initialize_options(self) -> None - override def finalize_options(self) -> None - override def run(self) -> None + override def initialize_options(self) + override def finalize_options(self) + override def run(self) def build(self) def install(self) def byte_compile(self, files) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_scripts.byi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_scripts.byi index cb0c2636f8..288b3655e6 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_scripts.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_scripts.byi @@ -17,9 +17,9 @@ class install_scripts(Command): force: int build_dir: Incomplete skip_build: Incomplete - override def initialize_options(self) -> None - override def finalize_options(self) -> None + override def initialize_options(self) + override def finalize_options(self) outfiles: Incomplete - override def run(self) -> None + override def run(self) def get_inputs(self) def get_outputs(self) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/register.byi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/register.byi index 668e75c0c6..7fd0665b3e 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/register.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/register.byi @@ -14,9 +14,9 @@ class register(PyPIRCCommand): sub_commands: ClassVar[list[(str, ((dynamic) -> bool) | None)]] list_classifiers: int strict: int - override def initialize_options(self) -> None - override def finalize_options(self) -> None - override def run(self) -> None + override def initialize_options(self) + override def finalize_options(self) + override def run(self) def check_metadata(self): """Deprecated API.""" diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/sdist.byi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/sdist.byi index 315543fb77..5a9f430627 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/sdist.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/sdist.byi @@ -42,10 +42,10 @@ class sdist(Command): metadata_check: int owner: Incomplete group: Incomplete - override def initialize_options(self) -> None - override def finalize_options(self) -> None + override def initialize_options(self) + override def finalize_options(self) filelist: Incomplete - override def run(self) -> None + override def run(self) def check_metadata(self): """Deprecated API.""" diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/upload.byi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/upload.byi index 4f186343f6..7722ed895a 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/upload.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/upload.byi @@ -17,9 +17,9 @@ class upload(PyPIRCCommand): show_response: int sign: bool identity: Incomplete - override def initialize_options(self) -> None + override def initialize_options(self) repository: Incomplete realm: Incomplete - override def finalize_options(self) -> None - override def run(self) -> None + override def finalize_options(self) + override def run(self) def upload_file(self, command: str, pyversion: str, filename: str) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/config.byi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/config.byi index fb3c03cf89..673ea3b5fe 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/config.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/config.byi @@ -18,13 +18,13 @@ class PyPIRCCommand(Command): realm: None user_options: ClassVar[list[(str, str | None, str)]] boolean_options: ClassVar[list[str]] - override def initialize_options(self) -> None: + override def initialize_options(self): """Initialize options.""" - override def finalize_options(self) -> None: + override def finalize_options(self): """Finalizes options.""" - override abstract def run(self) -> None: + override abstract def run(self): """A command's raison d'etre: carry out the action it exists to perform, controlled by the options initialized in 'initialize_options()', customized by other commands, the setup diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/core.byi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/core.byi index 45b964888e..ccc12e1dbc 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/distutils/core.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/core.byi @@ -8,9 +8,9 @@ really defined in distutils.dist and distutils.cmd. from _typeshed import Incomplete, StrOrBytesPath from collections.abc import Mapping -from distutils.cmd import Command as Command -from distutils.dist import Distribution as Distribution -from distutils.extension import Extension as Extension +from distutils.cmd export Command +from distutils.dist export Distribution +from distutils.extension export Extension from typing import Final, Literal final USAGE: str diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/email/message.byi b/crates/ty_vendored/vendor/typeshed/stdlib/email/message.byi index 03c47386af..4831d3ae13 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/email/message.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/email/message.byi @@ -497,7 +497,7 @@ class MIMEPart[out HeaderRegistryT = dynamic, in HeaderRegistryParamT = dynamic] match. Ignore parts with 'Content-Disposition: attachment'. """ - override def attach(self, payload: Self) -> None: + override def attach(self, payload: Self): """Add the given payload to the current payload. The current payload will always be a list of objects after this method diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/email/parser.byi b/crates/ty_vendored/vendor/typeshed/stdlib/email/parser.byi index 438ba027dd..67dba2984d 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/email/parser.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/email/parser.byi @@ -3,7 +3,7 @@ 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.feedparser export BytesFeedParser, FeedParser from email.message import Message from email.policy import Policy from io import _WrappedBuffer diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/encodings/__init__.byi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/__init__.byi index d026d79737..6f302bd455 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/encodings/__init__.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/__init__.byi @@ -31,7 +31,7 @@ Written by Marc-Andre Lemburg (mal@lemburg.com). import sys from codecs import CodecInfo -from . import aliases as aliases +from . export aliases class CodecRegistryError(LookupError, SystemError) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_8_sig.byi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_8_sig.byi index abda2c3dbd..c44671a066 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_8_sig.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_8_sig.byi @@ -15,7 +15,7 @@ class IncrementalEncoder(codecs.IncrementalEncoder): init(self, errors: str = "strict") override def encode(self, input: str, final: bool = False) -> bytes override def getstate(self) -> int - override def setstate(self, state: int) -> None + override def setstate(self, state: int) class IncrementalDecoder(codecs.BufferedIncrementalDecoder): init(self, errors: str = "strict") diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/enum.byi b/crates/ty_vendored/vendor/typeshed/stdlib/enum.byi index 98a6f64813..d01e505191 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/enum.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/enum.byi @@ -85,7 +85,7 @@ class _EnumDict(dict[str, dynamic]): else: def __init__(self) -> None - override def __setitem__(self, key: str, value: dynamic) -> None: + override def __setitem__(self, key: str, value: dynamic): """ Changes anything not dundered or not a descriptor. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/functools.byi b/crates/ty_vendored/vendor/typeshed/stdlib/functools.byi index a399e4f592..bb0f351c0d 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/functools.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/functools.byi @@ -92,7 +92,7 @@ final class _lru_cache_wrapper[Fn: (...) -> object]: """ __wrapped__: Fn - def __call__[Parameters: (*: *, **: *), R](self: _lru_cache_wrapper[(**Parameters) -> R], *args: *Parameters, **kwargs: **Parameters) -> R: + def __call__[Parameters: (*: *, **: *), R](self: _lru_cache_wrapper[(**Parameters) -> R], *args: Parameters.args, **kwargs: Parameters.kwargs) -> R: """Call self as a function.""" def __get__(self, instance: None, owner: type | None = None) -> Self diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/genericpath.byi b/crates/ty_vendored/vendor/typeshed/stdlib/genericpath.byi index da85d1ca9a..a4a33541e0 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/genericpath.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/genericpath.byi @@ -9,7 +9,7 @@ import sys from _typeshed import BytesPath, FileDescriptorOrPath, StrOrBytesPath, StrPath, SupportsRichComparisonT from collections.abc import Sequence from typing import Literal, NewType -from typing_extensions import LiteralString, deprecated +from typing_extensions import deprecated __all__ = [ "commonprefix", @@ -37,7 +37,7 @@ if sys.version_info >= (3, 15): # type. But because this only works when T is str, we need Sequence[T] instead. if sys.version_info >= (3, 15): @deprecated("Deprecated since Python 3.15; use os.path.commonpath() for path prefixes.") - def commonprefix(m: Sequence[LiteralString], /) -> LiteralString: + def commonprefix(m: Sequence[literal str], /) -> literal str: """Given a list of pathnames, returns the longest common leading component""" @deprecated("Deprecated since Python 3.15; use os.path.commonpath() for path prefixes.") def commonprefix(m: Sequence[StrPath], /) -> str @@ -48,7 +48,7 @@ if sys.version_info >= (3, 15): @deprecated("Deprecated since Python 3.15; use os.path.commonpath() for path prefixes.") def commonprefix(m: Sequence[(*: SupportsRichComparisonT)], /) -> Sequence[SupportsRichComparisonT] else: - def commonprefix(m: Sequence[LiteralString]) -> LiteralString: + def commonprefix(m: Sequence[literal str]) -> literal str: """Given a list of pathnames, returns the longest common leading component""" def commonprefix(m: Sequence[StrPath]) -> str def commonprefix(m: Sequence[BytesPath]) -> bytes | "" diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/glob.byi b/crates/ty_vendored/vendor/typeshed/stdlib/glob.byi index 24f4e6340e..ee8dad3ad1 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/glob.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/glob.byi @@ -48,8 +48,8 @@ if sys.version_info >= (3, 11): If `dir_fd` is not None, it should be a file descriptor referring to a directory, and paths will then be relative to that directory. - If `include_hidden` is true, the patterns '*', '?', '**' will match - hidden directories. + If `include_hidden` is true, wildcards can match path segments beginning + with a dot ('.'). If `recursive` is true, the pattern '**' will match any files and zero or more directories and subdirectories. @@ -82,8 +82,8 @@ if sys.version_info >= (3, 11): If `dir_fd` is not None, it should be a file descriptor referring to a directory, and paths will then be relative to that directory. - If `include_hidden` is true, the patterns '*', '?', '**' will match - hidden directories. + If `include_hidden` is true, wildcards can match path segments beginning + with a dot ('.'). If `recursive` is true, the pattern '**' will match any files and zero or more directories and subdirectories. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/gzip.byi b/crates/ty_vendored/vendor/typeshed/stdlib/gzip.byi index 4fb269b9bb..3650adf784 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/gzip.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/gzip.byi @@ -225,8 +225,8 @@ class GzipFile(BaseStream): """ def peek(self, n: int) -> bytes - override def close(self) -> None - override def flush(self, zlib_mode: int = 2) -> None + override def close(self) + override def flush(self, zlib_mode: int = 2) override def fileno(self) -> int: """Invoke the underlying file object's fileno() method. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/hashlib.byi b/crates/ty_vendored/vendor/typeshed/stdlib/hashlib.byi index cbe00292e4..568197464e 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/hashlib.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/hashlib.byi @@ -50,7 +50,7 @@ More condensed: """ import sys -from _blake2 import blake2b as blake2b, blake2s as blake2s +from _blake2 export blake2b, blake2s from _hashlib import ( HASH, _HashObject, diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/http/client.byi b/crates/ty_vendored/vendor/typeshed/stdlib/http/client.byi index 71342d7e3e..3f0b09ae3f 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/http/client.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/http/client.byi @@ -261,7 +261,7 @@ class HTTPResponse(io.BufferedIOBase, BinaryIO): override def __enter__(self) -> Self override def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: types.TracebackType | None - ) -> None + ) @deprecated("Deprecated since Python 3.9. Use `HTTPResponse.headers` attribute instead.") def info(self) -> HTTPMessage: """Returns an instance of the class mimetools.Message containing diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/http/cookiejar.byi b/crates/ty_vendored/vendor/typeshed/stdlib/http/cookiejar.byi index 92a3946937..d6d6f1a386 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/http/cookiejar.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/http/cookiejar.byi @@ -320,7 +320,7 @@ class Cookie: domain_initial_dot: bool init( self, - version: int | None, + version: int | str | None, name: str, value: str | None, # undocumented port: str | None, @@ -331,7 +331,7 @@ class Cookie: path: str, path_specified: bool, secure: bool, - expires: int | None, + expires: float | str | None, discard: bool, comment: str | None, comment_url: str | None, diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/http/cookies.byi b/crates/ty_vendored/vendor/typeshed/stdlib/http/cookies.byi index f47862bbd7..ef391f1f36 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/http/cookies.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/http/cookies.byi @@ -126,14 +126,14 @@ class Morsel[in out Element](dict[str, dynamic]): def set(self, key: str, val: str, coded_val: Element) override def setdefault(self, key: str, val: str | None = None) -> str # The dict update can also get a keywords argument so this is incompatible - override def update(self, values: Iterable[(str, str)] | SupportsKeysAndGetItem[str, str]) -> None + override def update(self, values: Iterable[(str, str)] | SupportsKeysAndGetItem[str, str]) def isReservedKey(self, K: str) -> bool def output(self, attrs: Container[str] | None = None, header: str = "Set-Cookie:") -> str __str__ = output def js_output(self, attrs: Container[str] | None = None) -> str def OutputString(self, attrs: Container[str] | None = None) -> str override def __eq__(self, morsel: object) -> bool - override def __setitem__(self, K: str, V: dynamic) -> None + override def __setitem__(self, K: str, V: dynamic) override def __class_getitem__(cls, item: dynamic, /) -> GenericAlias: """Represent a PEP 585 generic type @@ -174,7 +174,7 @@ class BaseCookie[in out Element](dict[str, Morsel[Element]]): map(Cookie.__setitem__, d.keys(), d.values()) """ - override def __setitem__(self, key: str, value: str | Morsel[Element]) -> None: + override def __setitem__(self, key: str, value: str | Morsel[Element]): """Dictionary style assignment.""" class SimpleCookie(BaseCookie[str]): diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/http/server.byi b/crates/ty_vendored/vendor/typeshed/stdlib/http/server.byi index 2d129816b2..cd5752eecd 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/http/server.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/http/server.byi @@ -421,7 +421,7 @@ def executable(path: StrPath) -> bool: # undocumented """Test for executable file.""" if sys.version_info < (3, 15): - @deprecated("Deprecated since Python 3.13; will be removed in Python 3.15.") + @deprecated("Deprecated and unsafe; will be removed in Python 3.15.") class CGIHTTPRequestHandler(SimpleHTTPRequestHandler): """Complete HTTP server with GET, HEAD and POST commands. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/imaplib.byi b/crates/ty_vendored/vendor/typeshed/stdlib/imaplib.byi index d20456b41f..262c57d3fc 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/imaplib.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/imaplib.byi @@ -1,6 +1,6 @@ """IMAP4 client. -Based on RFC 2060. +Based on RFC 3501. Public class: IMAP4 Public variable: Debug @@ -292,13 +292,23 @@ class IMAP4: Note: 'duration' requires a socket connection (not IMAP4_stream). """ - def list(self, directory: str = '""', pattern: str = "*") -> (str, AnyResponseData): - """List mailbox names in directory matching pattern. + if sys.version_info >= (3, 15): + def list(self, directory: str = "", pattern: str = "*") -> (str, AnyResponseData): + """List mailbox names in directory matching pattern. - (typ, [data]) = .list(directory='""', pattern='*') + (typ, [data]) = .list(directory='', pattern='*') - 'data' is list of LIST responses. - """ + 'data' is list of LIST responses. + """ + + else: + def list(self, directory: str = '""', pattern: str = "*") -> (str, AnyResponseData): + """List mailbox names in directory matching pattern. + + (typ, [data]) = .list(directory='""', pattern='*') + + 'data' is list of LIST responses. + """ def login(self, user: str, password: str) -> ("OK", _list[bytes]): """Identify client using plaintext password. @@ -322,13 +332,23 @@ class IMAP4: Returns server 'BYE' response. """ - def lsub(self, directory: str = '""', pattern: str = "*") -> CommandResults: - """List 'subscribed' mailbox names in directory matching pattern. + if sys.version_info >= (3, 15): + def lsub(self, directory: str = "", pattern: str = "*") -> CommandResults: + """List 'subscribed' mailbox names in directory matching pattern. - (typ, [data, ...]) = .lsub(directory='""', pattern='*') + (typ, [data, ...]) = .lsub(directory='', pattern='*') - 'data' are tuples of message part envelope and data. - """ + 'data' are tuples of message part envelope and data. + """ + + else: + def lsub(self, directory: str = '""', pattern: str = "*") -> CommandResults: + """List 'subscribed' mailbox names in directory matching pattern. + + (typ, [data, ...]) = .lsub(directory='""', pattern='*') + + 'data' are tuples of message part envelope and data. + """ def myrights(self, mailbox: str) -> CommandResults: """Show my ACLs for a mailbox (i.e. the rights that I have on mailbox). @@ -399,10 +419,17 @@ class IMAP4: (typ, [data]) = .setacl(mailbox, who, what) """ - def setannotation(self, *args: str) -> CommandResults: - """(typ, [data]) = .setannotation(mailbox[, entry, attribute]+) - Set ANNOTATIONs. - """ + if sys.version_info >= (3, 15): + def setannotation(self, mailbox: str | bytes, *args: str) -> CommandResults: + """(typ, [data]) = .setannotation(mailbox[, entry, attribute]+) + Set ANNOTATIONs. + """ + + else: + def setannotation(self, *args: str) -> CommandResults: + """(typ, [data]) = .setannotation(mailbox[, entry, attribute]+) + Set ANNOTATIONs. + """ def setquota(self, root: str, limits: str) -> CommandResults: """Set the quota root's resource limits. @@ -558,7 +585,7 @@ class IMAP4_SSL(IMAP4): else: file: IO[dynamic] - override def open(self, host: str = "", port: int | None = 993, timeout: float | None = None) -> None: + override def open(self, host: str = "", port: int | None = 993, timeout: float | None = None): """Setup connection to remote server on "host:port". (default: localhost:standard IMAP4 SSL port). This connection will be used by the routines: @@ -588,7 +615,7 @@ class IMAP4_stream(IMAP4): process: subprocess.Popen[bytes] writefile: IO[dynamic] readfile: IO[dynamic] - override def open(self, host: str | None = None, port: int | None = None, timeout: float | None = None) -> None: + override def open(self, host: str | None = None, port: int | None = None, timeout: float | None = None): """Setup a stream connection. This connection will be used by the routines: read, readline, send, shutdown. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/imp.byi b/crates/ty_vendored/vendor/typeshed/stdlib/imp.byi index d28e38b039..5ac351bc01 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/imp.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/imp.byi @@ -7,16 +7,16 @@ functionality over this module. """ import types -from _imp import ( - acquire_lock as acquire_lock, - create_dynamic as create_dynamic, - get_frozen_object as get_frozen_object, - init_frozen as init_frozen, - is_builtin as is_builtin, - is_frozen as is_frozen, - is_frozen_package as is_frozen_package, - lock_held as lock_held, - release_lock as release_lock, +from _imp export ( + acquire_lock, + create_dynamic, + get_frozen_object, + init_frozen, + is_builtin, + is_frozen, + is_frozen_package, + lock_held, + release_lock, ) from _typeshed import StrPath from os import PathLike diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/importlib/__init__.byi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/__init__.byi index 7aedeb5f66..5bb5030c27 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/importlib/__init__.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/__init__.byi @@ -1,7 +1,7 @@ """A pure Python implementation of import.""" import sys -from importlib._bootstrap import __import__ as __import__ +from importlib._bootstrap export __import__ from importlib.abc import Loader from types import ModuleType from typing_extensions import deprecated diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/importlib/_bootstrap.byi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/_bootstrap.byi index 116884f228..0009d1b86a 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/importlib/_bootstrap.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/_bootstrap.byi @@ -8,4 +8,4 @@ work. One should use importlib as the public-facing version of this module. """ from _frozen_importlib import * -from _frozen_importlib import __import__ as __import__, _init_module_attrs as _init_module_attrs +from _frozen_importlib export __import__, _init_module_attrs diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/importlib/_bootstrap_external.byi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/_bootstrap_external.byi index a4d2aeccd2..e28faf24bb 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/importlib/_bootstrap_external.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/_bootstrap_external.byi @@ -8,4 +8,4 @@ work. One should use importlib as the public-facing version of this module. """ from _frozen_importlib_external import * -from _frozen_importlib_external import _NamespaceLoader as _NamespaceLoader +from _frozen_importlib_external export _NamespaceLoader diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/importlib/abc.byi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/abc.byi index 61e5d04152..c83578b6b7 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/importlib/abc.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/abc.byi @@ -7,7 +7,7 @@ from _typeshed import ReadableBuffer, StrPath from abc import ABCMeta from collections.abc import Iterator, Mapping, Sequence from importlib import _bootstrap_external -from importlib._abc import Loader as Loader +from importlib._abc export Loader from importlib.machinery import ModuleSpec from io import BufferedReader from typing import Literal, Protocol, runtime_checkable @@ -136,7 +136,7 @@ class SourceLoader(_bootstrap_external.SourceLoader, ResourceLoader, ExecutionLo override def path_mtime(self, path: str) -> float: """Return the (int) modification time for the path (str).""" - override def set_data(self, path: str, data: bytes) -> None: + override def set_data(self, path: str, data: bytes): """Write the bytes to the path (if possible). Accepts a str path and data as bytes. @@ -355,8 +355,8 @@ if sys.version_info < (3, 11): override 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, + from importlib.resources.abc export ( + ResourceReader, + Traversable, + TraversableResources, ) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/importlib/machinery.byi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/machinery.byi index 9cbc94326f..94daab3387 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/importlib/machinery.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/machinery.byi @@ -1,25 +1,25 @@ """The machinery of importlib: finders, loaders, hooks, etc.""" 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, +from importlib._bootstrap export BuiltinImporter, FrozenImporter, ModuleSpec +from importlib._bootstrap_external export ( + BYTECODE_SUFFIXES, + DEBUG_BYTECODE_SUFFIXES, + EXTENSION_SUFFIXES, + OPTIMIZED_BYTECODE_SUFFIXES, + SOURCE_SUFFIXES, + ExtensionFileLoader, + FileFinder, + PathFinder, + SourceFileLoader, + SourcelessFileLoader, + WindowsRegistryFinder, ) if sys.version_info >= (3, 11): - from importlib._bootstrap_external import NamespaceLoader as NamespaceLoader + from importlib._bootstrap_external export NamespaceLoader if sys.version_info >= (3, 14): - from importlib._bootstrap_external import AppleFrameworkLoader as AppleFrameworkLoader + from importlib._bootstrap_external export AppleFrameworkLoader def all_suffixes() -> list[str]: """Returns a list of all recognized module suffixes for this process""" diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/importlib/readers.byi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/readers.byi index 561e004b37..8f4c1e7f70 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/importlib/readers.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/readers.byi @@ -67,8 +67,8 @@ class MultiplexedPath(abc.Traversable): init(self, *paths: abc.Traversable) override def iterdir(self) -> Iterator[abc.Traversable] - override def read_bytes(self) -> NoReturn - override def read_text(self, *args: Never, **kwargs: Never) -> NoReturn + override def read_bytes(self) -> Never + override def read_text(self, *args: Never, **kwargs: Never) -> Never override def is_dir(self) -> True override def is_file(self) -> False @@ -82,7 +82,7 @@ class MultiplexedPath(abc.Traversable): if sys.version_info < (3, 12): __truediv__ = joinpath - override def open(self, *args: Never, **kwargs: Never) -> NoReturn + override def open(self, *args: Never, **kwargs: Never) -> Never let name: str class NamespaceReader(abc.TraversableResources): diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/__init__.byi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/__init__.byi index fc440d7c28..33dee42853 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/__init__.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/__init__.byi @@ -22,7 +22,7 @@ else: from importlib.abc import Traversable if sys.version_info >= (3, 11): - from importlib.resources._common import Package as Package + from importlib.resources._common export Package else: type Package = str | ModuleType @@ -49,19 +49,19 @@ elif sys.version_info < (3, 13): type Resource = str if sys.version_info >= (3, 12): - from importlib.resources._common import Anchor as Anchor + from importlib.resources._common export 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, + from importlib.resources._functional export ( + contents, + is_resource, + open_binary, + open_text, + path, + read_binary, + read_text, ) else: @@ -97,7 +97,7 @@ else: Directories are *not* resources. """ - @deprecated("Deprecated since Python 3.11. Use `files(anchor).iterdir()`.") + @deprecated("Deprecated; limited resource support. Use `files(anchor).iterdir()`.") def contents(package: Package) -> Iterator[str]: """Return an iterable of entries in `package`. @@ -107,7 +107,7 @@ else: """ if sys.version_info >= (3, 11): - from importlib.resources._common import as_file as as_file + from importlib.resources._common export as_file else: def as_file(path: Traversable) -> AbstractContextManager[Path, False]: """ @@ -116,7 +116,7 @@ else: """ if sys.version_info >= (3, 11): - from importlib.resources._common import files as files + from importlib.resources._common export files else: def files(package: Package) -> Traversable: """ @@ -124,6 +124,6 @@ else: """ if sys.version_info >= (3, 11): - from importlib.resources.abc import ResourceReader as ResourceReader + from importlib.resources.abc export ResourceReader else: - from importlib.abc import ResourceReader as ResourceReader + from importlib.abc export ResourceReader diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/abc.byi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/abc.byi index 95769ec827..41387fc222 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/abc.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/abc.byi @@ -7,7 +7,7 @@ from typing import Literal, Protocol, runtime_checkable from typing_extensions import deprecated if sys.version_info >= (3, 11): - @deprecated("Deprecated since Python 3.12. Use `importlib.resources.abc.TraversableResources` instead.") + @deprecated("Deprecated. Use `importlib.resources.abc.TraversableResources` instead.") class ResourceReader(metaclass=ABCMeta): """Abstract base class for loaders to provide resource reading support.""" diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/simple.byi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/simple.byi index e7defbe203..0af7cfcfa0 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/simple.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/simple.byi @@ -69,7 +69,7 @@ if sys.version_info >= (3, 11): def open(self, mode: "rb") -> BinaryIO def open(self, mode: str) -> IO[dynamic] - def joinpath(self, name: Never) -> NoReturn + def joinpath(self, name: Never) -> Never class ResourceContainer(Traversable, metaclass=abc.ABCMeta): """ @@ -81,7 +81,7 @@ if sys.version_info >= (3, 11): def is_dir(self) -> True def is_file(self) -> False def iterdir(self) -> Iterator[ResourceHandle | ResourceContainer] - def open(self, *args: Never, **kwargs: Never) -> NoReturn + def open(self, *args: Never, **kwargs: Never) -> Never if sys.version_info < (3, 12): def joinpath(self, *descendants: StrPath) -> Traversable diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/importlib/simple.byi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/simple.byi index 4c6f230844..0c9559af85 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/importlib/simple.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/simple.byi @@ -8,11 +8,11 @@ module directly. import sys if sys.version_info >= (3, 11): - from .resources.simple import ( - ResourceContainer as ResourceContainer, - ResourceHandle as ResourceHandle, - SimpleReader as SimpleReader, - TraversableReader as TraversableReader, + from .resources.simple export ( + ResourceContainer, + ResourceHandle, + SimpleReader, + TraversableReader, ) __all__ = ["SimpleReader", "ResourceHandle", "ResourceContainer", "TraversableReader"] diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/importlib/util.byi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/util.byi index ebe38e4697..2c0f0a71fb 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/importlib/util.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/util.byi @@ -5,13 +5,13 @@ 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._bootstrap export module_from_spec, spec_from_loader +from importlib._bootstrap_external export ( + MAGIC_NUMBER, + cache_from_source, + decode_source, + source_from_cache, + spec_from_file_location, ) from importlib.abc import Loader from types import TracebackType diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/inspect.byi b/crates/ty_vendored/vendor/typeshed/stdlib/inspect.byi index faf8977345..5c7c2ce3a9 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/inspect.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/inspect.byi @@ -315,10 +315,10 @@ def isgenerator(object: object) -> object is GeneratorType[object, Never, object throw() used to raise an exception inside the generator """ -def iscoroutine(object: object) -> object is CoroutineType[dynamic, dynamic, dynamic]: +def iscoroutine(object: object) -> object is CoroutineType[object, Never, object]: """Return true if the object is a coroutine.""" -def isawaitable(object: object) -> object is Awaitable[dynamic]: +def isawaitable(object: object) -> object is Awaitable[object]: """Return true if object can be passed to an ``await`` expression.""" def isasyncgenfunction(obj: (...) -> AsyncGenerator[dynamic, dynamic]) -> bool: @@ -703,7 +703,7 @@ class Signature: override def __hash__(self) -> int if sys.version_info >= (3, 14): - from annotationlib import get_annotations as get_annotations + from annotationlib export get_annotations else: def get_annotations( obj: (...) -> object | type[object] | ModuleType, # any callable, class, or module diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/io.byi b/crates/ty_vendored/vendor/typeshed/stdlib/io.byi index dd7a912a05..23573b0105 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/io.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/io.byi @@ -83,7 +83,7 @@ if sys.version_info >= (3, 14): __all__ += ["Reader", "Writer"] if sys.version_info >= (3, 11): - from _io import text_encoding as text_encoding + from _io export text_encoding __all__ += ["DEFAULT_BUFFER_SIZE", "IncrementalNewlineDecoder", "text_encoding"] diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/json/__init__.byi b/crates/ty_vendored/vendor/typeshed/stdlib/json/__init__.byi index b72d8f515b..04c46270d2 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/json/__init__.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/json/__init__.byi @@ -101,8 +101,8 @@ from _typeshed import SupportsRead, SupportsWrite from collections.abc import Callable from typing import Literal -from .decoder import JSONDecodeError as JSONDecodeError, JSONDecoder as JSONDecoder -from .encoder import JSONEncoder as JSONEncoder +from .decoder export JSONDecodeError, JSONDecoder +from .encoder export JSONEncoder __all__ = ["dump", "dumps", "load", "loads", "JSONDecoder", "JSONDecodeError", "JSONEncoder"] diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/json/scanner.byi b/crates/ty_vendored/vendor/typeshed/stdlib/json/scanner.byi index 890502df26..8eeaba95ac 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/json/scanner.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/json/scanner.byi @@ -1,6 +1,6 @@ """JSON token scanner""" -from _json import make_scanner as make_scanner +from _json export make_scanner from re import Pattern from typing import Final diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixer_base.byi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixer_base.byi index 44a4e805f5..180c485e02 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixer_base.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixer_base.byi @@ -122,5 +122,5 @@ class ConditionalFix(BaseFix, metaclass=ABCMeta): """Base class for fixers which not execute if an import is found.""" skip_on: ClassVar[str | None] - override def start_tree(self, tree: Node, filename: StrPath, /) -> None + override def start_tree(self, tree: Node, filename: StrPath, /) def should_skip(self, node: Base) -> bool diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_asserts.byi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_asserts.byi index 128ea39c8c..611f6ab8c8 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_asserts.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_asserts.byi @@ -9,4 +9,4 @@ final NAMES: dict[str, str] class FixAsserts(BaseFix): BM_compatible: ClassVar[False] PATTERN: ClassVar[str] - override def transform(self, node, results) -> None + override def transform(self, node, results) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_buffer.byi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_buffer.byi index 18ac740191..49a67e0e94 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_buffer.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_buffer.byi @@ -7,4 +7,4 @@ from .. import fixer_base class FixBuffer(fixer_base.BaseFix): BM_compatible: ClassVar[True] PATTERN: ClassVar[str] - override def transform(self, node, results) -> None + override def transform(self, node, results) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_exitfunc.byi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_exitfunc.byi index a5bb4538ae..d8bf71f5ee 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_exitfunc.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_exitfunc.byi @@ -13,5 +13,5 @@ class FixExitfunc(fixer_base.BaseFix): PATTERN: ClassVar[str] init(self, *args) sys_import: Incomplete | None - override def start_tree(self, tree: Node, filename: StrPath) -> None - override def transform(self, node, results) -> None + override def start_tree(self, tree: Node, filename: StrPath) + override def transform(self, node, results) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_funcattrs.byi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_funcattrs.byi index 5053866ca5..95ce99f864 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_funcattrs.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_funcattrs.byi @@ -7,4 +7,4 @@ from .. import fixer_base class FixFuncattrs(fixer_base.BaseFix): BM_compatible: ClassVar[True] PATTERN: ClassVar[str] - override def transform(self, node, results) -> None + override def transform(self, node, results) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_getcwdu.byi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_getcwdu.byi index 22ef42718b..be93cd52d8 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_getcwdu.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_getcwdu.byi @@ -9,4 +9,4 @@ from .. import fixer_base class FixGetcwdu(fixer_base.BaseFix): BM_compatible: ClassVar[True] PATTERN: ClassVar[str] - override def transform(self, node, results) -> None + override def transform(self, node, results) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_import.byi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_import.byi index 19794b1fa1..37cdd5b4ba 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_import.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_import.byi @@ -26,6 +26,6 @@ class FixImport(fixer_base.BaseFix): BM_compatible: ClassVar[True] PATTERN: ClassVar[str] skip: bool - override def start_tree(self, tree: Node, name: StrPath) -> None + override def start_tree(self, tree: Node, name: StrPath) override def transform(self, node, results) def probably_a_local_import(self, imp_name) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_imports.byi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_imports.byi index 3d361d7050..c212ff221c 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_imports.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_imports.byi @@ -16,8 +16,8 @@ class FixImports(fixer_base.BaseFix): BM_compatible: ClassVar[True] mapping = MAPPING def build_pattern(self) - override def compile_pattern(self) -> None + override def compile_pattern(self) override def match(self, node) replace: dict[str, str] - override def start_tree(self, tree: Node, filename: StrPath) -> None - override def transform(self, node, results) -> None + override def start_tree(self, tree: Node, filename: StrPath) + override def transform(self, node, results) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_isinstance.byi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_isinstance.byi index 6fe355da5d..58747edae9 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_isinstance.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_isinstance.byi @@ -13,4 +13,4 @@ from .. import fixer_base class FixIsinstance(fixer_base.BaseFix): BM_compatible: ClassVar[True] PATTERN: ClassVar[str] - override def transform(self, node, results) -> None + override def transform(self, node, results) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_itertools.byi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_itertools.byi index 4af35f61e6..41b80e4593 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_itertools.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_itertools.byi @@ -15,4 +15,4 @@ class FixItertools(fixer_base.BaseFix): BM_compatible: ClassVar[True] it_funcs: str PATTERN: ClassVar[str] - override def transform(self, node, results) -> None + override def transform(self, node, results) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_long.byi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_long.byi index bb9dc83472..4789934294 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_long.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_long.byi @@ -6,4 +6,4 @@ from typing import ClassVar, Literal class FixLong(fixer_base.BaseFix): BM_compatible: ClassVar[True] PATTERN: ClassVar["'long'"] - override def transform(self, node, results) -> None + override def transform(self, node, results) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_metaclass.byi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_metaclass.byi index 1cdc263eaf..ce78e96e7b 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_metaclass.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_metaclass.byi @@ -50,4 +50,4 @@ def fixup_indent(suite): class FixMetaclass(fixer_base.BaseFix): BM_compatible: ClassVar[True] PATTERN: ClassVar[str] - override def transform(self, node, results) -> None + override def transform(self, node, results) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_methodattrs.byi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_methodattrs.byi index c40a097098..fb26209d54 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_methodattrs.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_methodattrs.byi @@ -9,4 +9,4 @@ final MAP: dict[str, str] class FixMethodattrs(fixer_base.BaseFix): BM_compatible: ClassVar[True] PATTERN: ClassVar[str] - override def transform(self, node, results) -> None + override def transform(self, node, results) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_next.byi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_next.byi index 94482b5973..1e1e9b3668 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_next.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_next.byi @@ -13,8 +13,8 @@ class FixNext(fixer_base.BaseFix): PATTERN: ClassVar[str] order: ClassVar["pre"] shadowed_next: bool - override def start_tree(self, tree: Node, filename: StrPath) -> None - override def transform(self, node, results) -> None + override def start_tree(self, tree: Node, filename: StrPath) + override def transform(self, node, results) def is_assign_target(node) def find_assign(node) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_nonzero.byi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_nonzero.byi index 34ad2f6869..3fb037d599 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_nonzero.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_nonzero.byi @@ -7,4 +7,4 @@ from .. import fixer_base class FixNonzero(fixer_base.BaseFix): BM_compatible: ClassVar[True] PATTERN: ClassVar[str] - override def transform(self, node, results) -> None + override def transform(self, node, results) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_paren.byi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_paren.byi index c0c2958d77..8031bad93b 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_paren.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_paren.byi @@ -10,4 +10,4 @@ from .. import fixer_base class FixParen(fixer_base.BaseFix): BM_compatible: ClassVar[True] PATTERN: ClassVar[str] - override def transform(self, node, results) -> None + override def transform(self, node, results) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_raw_input.byi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_raw_input.byi index a4e34a6531..c8d315ec7d 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_raw_input.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_raw_input.byi @@ -7,4 +7,4 @@ from .. import fixer_base class FixRawInput(fixer_base.BaseFix): BM_compatible: ClassVar[True] PATTERN: ClassVar[str] - override def transform(self, node, results) -> None + override def transform(self, node, results) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_reduce.byi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_reduce.byi index 23be772fee..b49aa2b475 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_reduce.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_reduce.byi @@ -11,4 +11,4 @@ class FixReduce(fixer_base.BaseFix): BM_compatible: ClassVar[True] order: ClassVar["pre"] PATTERN: ClassVar[str] - override def transform(self, node, results) -> None + override def transform(self, node, results) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_renames.byi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_renames.byi index 082aab6a41..adc58e6656 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_renames.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_renames.byi @@ -20,4 +20,4 @@ class FixRenames(fixer_base.BaseFix): order: ClassVar["pre"] PATTERN: ClassVar[str] override def match(self, node) - override def transform(self, node, results) -> None + override def transform(self, node, results) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_throw.byi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_throw.byi index 28df0b4704..6d640c779d 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_throw.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_throw.byi @@ -14,4 +14,4 @@ from .. import fixer_base class FixThrow(fixer_base.BaseFix): BM_compatible: ClassVar[True] PATTERN: ClassVar[str] - override def transform(self, node, results) -> None + override def transform(self, node, results) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_unicode.byi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_unicode.byi index 2aa90e40ee..fe02894304 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_unicode.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_unicode.byi @@ -18,5 +18,5 @@ class FixUnicode(fixer_base.BaseFix): BM_compatible: ClassVar[True] PATTERN: ClassVar[str] unicode_literals: bool - override def start_tree(self, tree: Node, filename: StrPath) -> None + override def start_tree(self, tree: Node, filename: StrPath) override def transform(self, node, results) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_urllib.byi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_urllib.byi index 583b6041bd..10886bdc5f 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_urllib.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_urllib.byi @@ -29,4 +29,4 @@ class FixUrllib(FixImports): def transform_dot(self, node, results): """Transform for calls to module members in code.""" - override def transform(self, node, results) -> None + override def transform(self, node, results) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_xrange.byi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_xrange.byi index db1352bfe9..8833e287aa 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_xrange.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_xrange.byi @@ -10,8 +10,8 @@ class FixXrange(fixer_base.BaseFix): BM_compatible: ClassVar[True] PATTERN: ClassVar[str] transformed_xranges: set[Incomplete] | None - override def start_tree(self, tree: Node, filename: StrPath) -> None - override def finish_tree(self, tree: Node, filename: StrPath) -> None + override def start_tree(self, tree: Node, filename: StrPath) + override def finish_tree(self, tree: Node, filename: StrPath) override def transform(self, node, results) def transform_xrange(self, node, results) def transform_range(self, node, results) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_xreadlines.byi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_xreadlines.byi index 309ed7afd0..9522d1388e 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_xreadlines.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_xreadlines.byi @@ -10,4 +10,4 @@ from .. import fixer_base class FixXreadlines(fixer_base.BaseFix): BM_compatible: ClassVar[True] PATTERN: ClassVar[str] - override def transform(self, node, results) -> None + override def transform(self, node, results) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/main.byi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/main.byi index ac3293ab78..947f03d476 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/main.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/main.byi @@ -7,7 +7,7 @@ from collections.abc import Container, Iterable, Iterator, Mapping, Sequence from logging import _ExcInfoType from typing import Literal -from . import refactor as refactor +from . export refactor def diff_texts(a: str, b: str, filename: str) -> Iterator[str]: """Return a unified diff of two strings.""" @@ -63,13 +63,13 @@ class StdoutRefactoringTool(refactor.MultiprocessRefactoringTool): stack_info: bool = False, stacklevel: int = 1, extra: Mapping[str, object] | None = None, - ) -> None + ) # Same as super.write_file but without default values override def write_file( self, new_text: str, filename: FileDescriptorOrPath, old_text: str, encoding: str | None - ) -> None + ) # filename has to be str - override def print_output(self, old: str, new: str, filename: str, equal: bool) -> None + override def print_output(self, old: str, new: str, filename: str, equal: bool) def warn(msg: object) def main(fixer_pkg: str, args: Sequence[AnyStr] | None = None) -> 0 | 1 | 2: diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/pgen.byi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/pgen.byi index 4dd8332a8a..ab15495954 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/pgen.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/pgen.byi @@ -1,6 +1,7 @@ from _typeshed import Incomplete, StrPath from collections.abc import Iterable, Iterator from typing import ClassVar +from typing_extensions import Never from . import grammar from .tokenize import _TokenInfo @@ -30,8 +31,8 @@ class ParserGenerator: def expect(self, type: int, value: str | None = None) -> str def gettoken(self) - def raise_error(self, msg: object) -> NoReturn - def raise_error(self, msg: str, *args: object) -> NoReturn + def raise_error(self, msg: object) -> Never + def raise_error(self, msg: str, *args: object) -> Never class NFAState: arcs: list[(str | None, NFAState)] diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pytree.byi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pytree.byi index 33c64698aa..09981eebc1 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pytree.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pytree.byi @@ -7,7 +7,7 @@ even the comments and whitespace between tokens. There's also a pattern matching implementation here. """ -from _typeshed import Incomplete, SupportsGetItem, SupportsLenAndGetItem, Unused +from _typeshed import SupportsGetItem, SupportsLenAndGetItem, Unused from collections.abc import Iterable, Iterator, MutableSequence from typing import ClassVar, Final, TypeAlias from typing_extensions import Self @@ -109,7 +109,7 @@ class Node(Base): fixers_applied: MutableSequence[BaseFix] | None # Is Unbound until set in refactor.RefactoringTool - future_features: frozenset[Incomplete] + future_features: frozenset[str] # Is Unbound until set in pgen2.parse.Parser.pop used_names: set[str] init( diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/refactor.byi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/refactor.byi index 70d5a3a939..3082c61ea3 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/refactor.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/refactor.byi @@ -11,6 +11,7 @@ from logging import Logger, _ExcInfoType from multiprocessing import JoinableQueue from multiprocessing.synchronize import Lock from typing import ClassVar, Final +from typing_extensions import Never from .btm_matcher import BottomMatcher from .fixer_base import BaseFix @@ -68,7 +69,7 @@ class RefactoringTool: post-order traversal. """ - def log_error(self, msg: str, *args: Iterable[str], **kwargs: _ExcInfoType) -> NoReturn: + def log_error(self, msg: str, *args: Iterable[str], **kwargs: _ExcInfoType) -> Never: """Called when an error occurs.""" def log_message(self, msg: object) -> None: @@ -209,4 +210,4 @@ class MultiprocessRefactoringTool(RefactoringTool): output_lock: Lock | None override def refactor( self, items: Iterable[str], write: bool = False, doctests_only: bool = False, num_processes: int = 1 - ) -> None + ) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/locale.byi b/crates/ty_vendored/vendor/typeshed/stdlib/locale.byi index 0c77a8b89d..c49c01f668 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/locale.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/locale.byi @@ -11,17 +11,17 @@ also includes default encodings for all supported locale names. """ import sys -from _locale import ( - CHAR_MAX as CHAR_MAX, - LC_ALL as LC_ALL, - LC_COLLATE as LC_COLLATE, - LC_CTYPE as LC_CTYPE, - LC_MONETARY as LC_MONETARY, - LC_NUMERIC as LC_NUMERIC, - LC_TIME as LC_TIME, - localeconv as localeconv, - strcoll as strcoll, - strxfrm as strxfrm, +from _locale export ( + CHAR_MAX, + LC_ALL, + LC_COLLATE, + LC_CTYPE, + LC_MONETARY, + LC_NUMERIC, + LC_TIME, + localeconv, + strcoll, + strxfrm, ) # This module defines a function "str()", which is why "str" can't be used @@ -32,74 +32,74 @@ from decimal import Decimal from typing_extensions import deprecated if sys.version_info >= (3, 11): - from _locale import getencoding as getencoding + from _locale export getencoding # Some parts of the `_locale` module are platform-specific: if sys.platform != "win32": - from _locale import ( - ABDAY_1 as ABDAY_1, - ABDAY_2 as ABDAY_2, - ABDAY_3 as ABDAY_3, - ABDAY_4 as ABDAY_4, - ABDAY_5 as ABDAY_5, - ABDAY_6 as ABDAY_6, - ABDAY_7 as ABDAY_7, - ABMON_1 as ABMON_1, - ABMON_2 as ABMON_2, - ABMON_3 as ABMON_3, - ABMON_4 as ABMON_4, - ABMON_5 as ABMON_5, - ABMON_6 as ABMON_6, - ABMON_7 as ABMON_7, - ABMON_8 as ABMON_8, - ABMON_9 as ABMON_9, - ABMON_10 as ABMON_10, - ABMON_11 as ABMON_11, - ABMON_12 as ABMON_12, - ALT_DIGITS as ALT_DIGITS, - AM_STR as AM_STR, - CODESET as CODESET, - CRNCYSTR as CRNCYSTR, - D_FMT as D_FMT, - D_T_FMT as D_T_FMT, - DAY_1 as DAY_1, - DAY_2 as DAY_2, - DAY_3 as DAY_3, - DAY_4 as DAY_4, - DAY_5 as DAY_5, - DAY_6 as DAY_6, - DAY_7 as DAY_7, - ERA as ERA, - ERA_D_FMT as ERA_D_FMT, - ERA_D_T_FMT as ERA_D_T_FMT, - ERA_T_FMT as ERA_T_FMT, - LC_MESSAGES as LC_MESSAGES, - MON_1 as MON_1, - MON_2 as MON_2, - MON_3 as MON_3, - MON_4 as MON_4, - MON_5 as MON_5, - MON_6 as MON_6, - MON_7 as MON_7, - MON_8 as MON_8, - MON_9 as MON_9, - MON_10 as MON_10, - MON_11 as MON_11, - MON_12 as MON_12, - NOEXPR as NOEXPR, - PM_STR as PM_STR, - RADIXCHAR as RADIXCHAR, - T_FMT as T_FMT, - T_FMT_AMPM as T_FMT_AMPM, - THOUSEP as THOUSEP, - YESEXPR as YESEXPR, - bind_textdomain_codeset as bind_textdomain_codeset, - bindtextdomain as bindtextdomain, - dcgettext as dcgettext, - dgettext as dgettext, - gettext as gettext, - nl_langinfo as nl_langinfo, - textdomain as textdomain, + from _locale export ( + ABDAY_1, + ABDAY_2, + ABDAY_3, + ABDAY_4, + ABDAY_5, + ABDAY_6, + ABDAY_7, + ABMON_1, + ABMON_2, + ABMON_3, + ABMON_4, + ABMON_5, + ABMON_6, + ABMON_7, + ABMON_8, + ABMON_9, + ABMON_10, + ABMON_11, + ABMON_12, + ALT_DIGITS, + AM_STR, + CODESET, + CRNCYSTR, + D_FMT, + D_T_FMT, + DAY_1, + DAY_2, + DAY_3, + DAY_4, + DAY_5, + DAY_6, + DAY_7, + ERA, + ERA_D_FMT, + ERA_D_T_FMT, + ERA_T_FMT, + LC_MESSAGES, + MON_1, + MON_2, + MON_3, + MON_4, + MON_5, + MON_6, + MON_7, + MON_8, + MON_9, + MON_10, + MON_11, + MON_12, + NOEXPR, + PM_STR, + RADIXCHAR, + T_FMT, + T_FMT_AMPM, + THOUSEP, + YESEXPR, + bind_textdomain_codeset, + bindtextdomain, + dcgettext, + dgettext, + gettext, + nl_langinfo, + textdomain, ) __all__ = [ @@ -210,7 +210,7 @@ def normalize(localename: _str) -> _str: """ if sys.version_info < (3, 13): - @deprecated("Deprecated since Python 3.11; removed in Python 3.13. Use `locale.setlocale(locale.LC_ALL, '')` instead.") + @deprecated("Deprecated; removed in Python 3.13. Use `locale.setlocale(locale.LC_ALL, '')` instead.") def resetlocale(category: int = ...) -> None: """Sets the locale for category to the default setting. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/logging/__init__.byi b/crates/ty_vendored/vendor/typeshed/stdlib/logging/__init__.byi index 8738c547ab..022c9da223 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/logging/__init__.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/logging/__init__.byi @@ -496,7 +496,7 @@ class Handler(Filterer): level: int # undocumented formatter: Formatter | None # undocumented - lock: threading.Lock | None # undocumented + lock: threading.RLock | None # undocumented name: str | None # undocumented init(self, level: _Level = 0): """ @@ -872,7 +872,7 @@ class LogRecord: """ # Allows setting contextual information on LogRecord objects as per the docs, see #7833 - override def __setattr__(self, name: str, value: dynamic, /) -> None + override def __setattr__(self, name: str, value: dynamic, /) class LoggerAdapter[in out L: Logger | LoggerAdapter[dynamic]]: diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/mailbox.byi b/crates/ty_vendored/vendor/typeshed/stdlib/mailbox.byi index 5e246623d9..7290227ecd 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/mailbox.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/mailbox.byi @@ -187,10 +187,10 @@ class Maildir(Mailbox[MaildirMessage]): override def add(self, message: MessageData | MaildirMessage) -> str: """Add message and return assigned key.""" - override def remove(self, key: str) -> None: + override def remove(self, key: str): """Remove the keyed message; raise KeyError if it doesn't exist.""" - override def __setitem__(self, key: str, message: MessageData | MaildirMessage) -> None: + override def __setitem__(self, key: str, message: MessageData | MaildirMessage): """Replace the keyed message; raise KeyError if it doesn't exist.""" override def get_message(self, key: str) -> MaildirMessage: @@ -230,16 +230,16 @@ class Maildir(Mailbox[MaildirMessage]): override def __len__(self) -> int: """Return a count of messages in the mailbox.""" - override def flush(self) -> None: + override def flush(self): """Write any pending changes to disk.""" - override def lock(self) -> None: + override def lock(self): """Lock the mailbox.""" - override def unlock(self) -> None: + override def unlock(self): """Unlock the mailbox if it is locked.""" - override def close(self) -> None: + override def close(self): """Flush and close the mailbox.""" def list_folders(self) -> list[str]: @@ -266,10 +266,10 @@ class _singlefileMailbox[out MessageT: Message = Message](Mailbox[MessageT], met override def add(self, message: MessageData) -> str: """Add message and return assigned key.""" - override def remove(self, key: str) -> None: + override def remove(self, key: str): """Remove the keyed message; raise KeyError if it doesn't exist.""" - override def __setitem__(self, key: str, message: MessageData) -> None: + override def __setitem__(self, key: str, message: MessageData): """Replace the keyed message; raise KeyError if it doesn't exist.""" override def iterkeys(self) -> Iterator[str]: @@ -281,16 +281,16 @@ class _singlefileMailbox[out MessageT: Message = Message](Mailbox[MessageT], met override def __len__(self) -> int: """Return a count of messages in the mailbox.""" - override def lock(self) -> None: + override def lock(self): """Lock the mailbox.""" - override def unlock(self) -> None: + override def unlock(self): """Unlock the mailbox if it is locked.""" - override def flush(self) -> None: + override def flush(self): """Write any pending changes to disk.""" - override def close(self) -> None: + override def close(self): """Flush and close the mailbox.""" class _mboxMMDF[out MessageT: Message = Message](_singlefileMailbox[MessageT]): @@ -333,10 +333,10 @@ class MH(Mailbox[MHMessage]): override def add(self, message: MessageData) -> str: """Add message and return assigned key.""" - override def remove(self, key: str) -> None: + override def remove(self, key: str): """Remove the keyed message; raise KeyError if it doesn't exist.""" - override def __setitem__(self, key: str, message: MessageData) -> None: + override def __setitem__(self, key: str, message: MessageData): """Replace the keyed message; raise KeyError if it doesn't exist.""" override def get_message(self, key: str) -> MHMessage: @@ -357,16 +357,16 @@ class MH(Mailbox[MHMessage]): override def __len__(self) -> int: """Return a count of messages in the mailbox.""" - override def flush(self) -> None: + override def flush(self): """Write any pending changes to the disk.""" - override def lock(self) -> None: + override def lock(self): """Lock the mailbox.""" - override def unlock(self) -> None: + override def unlock(self): """Unlock the mailbox if it is locked.""" - override def close(self) -> None: + override def close(self): """Flush and close the mailbox.""" def list_folders(self) -> list[str]: diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/math/__init__.byi b/crates/ty_vendored/vendor/typeshed/stdlib/math/__init__.byi index ee70a3f878..db242fdc98 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/math/__init__.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/math/__init__.byi @@ -244,7 +244,7 @@ def ldexp(x: _SupportsFloatOrIndex, i: int, /) -> float: def lgamma(x: _SupportsFloatOrIndex, /) -> float: """Natural logarithm of absolute value of Gamma function at x.""" -def log(x: _SupportsFloatOrIndex, base: _SupportsFloatOrIndex = ...) -> float: +def log(x: _SupportsFloatOrIndex, base: _SupportsFloatOrIndex = ..., /) -> float: """log(x, [base=math.e]) Return the logarithm of x to the given base. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/mmap.byi b/crates/ty_vendored/vendor/typeshed/stdlib/mmap.byi index c0b8049ac3..ad9c4b62e1 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/mmap.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/mmap.byi @@ -3,7 +3,7 @@ import sys from _typeshed import ReadableBuffer, Unused from collections.abc import Iterator from typing import Final, Literal -from typing_extensions import Self, disjoint_base +from typing_extensions import Never, Self, disjoint_base ACCESS_DEFAULT: Final = 0 ACCESS_READ: Final = 1 @@ -137,7 +137,7 @@ class mmap: """Return self[key].""" def __getitem__(self, key: slice[SupportsIndex | None], /) -> bytes - def __delitem__(self, key: SupportsIndex | slice[SupportsIndex | None], /) -> NoReturn: + def __delitem__(self, key: SupportsIndex | slice[SupportsIndex | None], /) -> Never: """Delete self[key].""" def __setitem__(self, key: SupportsIndex, value: int, /) -> None: diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/__init__.byi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/__init__.byi index b9a33fbd66..28bd27cc60 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/__init__.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/__init__.byi @@ -1,15 +1,15 @@ from multiprocessing import context, reduction as reducer -from multiprocessing.context import ( - AuthenticationError as AuthenticationError, - BufferTooShort as BufferTooShort, - Process as Process, - ProcessError as ProcessError, - TimeoutError as TimeoutError, +from multiprocessing.context export ( + AuthenticationError, + BufferTooShort, + Process, + ProcessError, + TimeoutError, ) -from multiprocessing.process import ( - active_children as active_children, - current_process as current_process, - parent_process as parent_process, +from multiprocessing.process export ( + active_children, + current_process, + parent_process, ) # These are technically functions that return instances of these Queue classes. @@ -19,8 +19,8 @@ from multiprocessing.process import ( # Avoid using `multiprocessing.Queue` as a type annotation; # use imports from multiprocessing.queues instead. # See #4266 and #8450 for discussion. -from multiprocessing.queues import JoinableQueue as JoinableQueue, Queue as Queue, SimpleQueue as SimpleQueue -from multiprocessing.spawn import freeze_support as freeze_support +from multiprocessing.queues export JoinableQueue, Queue, SimpleQueue +from multiprocessing.spawn export freeze_support __all__ = [ "Array", diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/dummy/__init__.byi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/dummy/__init__.byi index 20a5ae4c3a..263ec43b9c 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/dummy/__init__.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/dummy/__init__.byi @@ -3,19 +3,19 @@ import sys import threading import weakref from collections.abc import Iterable, Mapping, Sequence -from queue import Queue as Queue -from threading import ( - Barrier as Barrier, - BoundedSemaphore as BoundedSemaphore, - Condition as Condition, - Event as Event, - Lock as Lock, - RLock as RLock, - Semaphore as Semaphore, +from queue export Queue +from threading export ( + Barrier, + BoundedSemaphore, + Condition, + Event, + Lock, + RLock, + Semaphore, ) from typing import Literal -from .connection import Pipe as Pipe +from .connection export Pipe __all__ = [ "Process", @@ -69,7 +69,7 @@ Process = DummyProcess class Namespace: init(self, **kwds: dynamic) def __getattr__(self, name: str, /) -> dynamic - override def __setattr__(self, name: str, value: dynamic, /) -> None + override def __setattr__(self, name: str, value: dynamic, /) class Value: _typecode: dynamic diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/managers.byi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/managers.byi index 0b9c23fe46..75f3263ac1 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/managers.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/managers.byi @@ -30,7 +30,7 @@ __all__ = ["BaseManager", "SyncManager", "BaseProxy", "Token", "SharedMemoryMana class Namespace: init(self, **kwds: dynamic) def __getattr__(self, name: str, /) -> dynamic - override def __setattr__(self, name: str, value: dynamic, /) -> None + override def __setattr__(self, name: str, value: dynamic, /) type _Namespace = Namespace @@ -138,8 +138,8 @@ else: __builtins__: ClassVar[dict[str, dynamic]] override def __len__(self) -> int override def __getitem__(self, key: Key, /) -> Value - override def __setitem__(self, key: Key, value: Value, /) -> None - override def __delitem__(self, key: Key, /) -> None + override def __setitem__(self, key: Key, value: Value, /) + override def __delitem__(self, key: Key, /) override def __iter__(self) -> Iterator[Key] def copy(self) -> dict[Key, Value] @@ -204,7 +204,7 @@ class BaseListProxy[in out Element](BaseProxy, MutableSequence[Element]): __builtins__: ClassVar[dict[str, dynamic]] override def __len__(self) -> int def __add__(self, x: list[Element], /) -> list[Element] - override def __delitem__(self, i: SupportsIndex | slice[SupportsIndex | None], /) -> None + override def __delitem__(self, i: SupportsIndex | slice[SupportsIndex | None], /) override def __getitem__(self, i: SupportsIndex, /) -> Element def __getitem__(self, s: slice[SupportsIndex | None], /) -> list[Element] @@ -216,13 +216,13 @@ class BaseListProxy[in out Element](BaseProxy, MutableSequence[Element]): def __rmul__(self, n: SupportsIndex, /) -> list[Element] def __imul__(self, value: SupportsIndex, /) -> Self override def __reversed__(self) -> Iterator[Element] - override def append(self, object: Element, /) -> None - override def extend(self, iterable: Iterable[Element], /) -> None + override def append(self, object: Element, /) + override def extend(self, iterable: Iterable[Element], /) override def pop(self, index: SupportsIndex = ..., /) -> Element override def index(self, value: Element, start: SupportsIndex = ..., stop: SupportsIndex = ..., /) -> int override def count(self, value: Element, /) -> int - override def insert(self, index: SupportsIndex, object: Element, /) -> None - override def remove(self, value: Element, /) -> None + override def insert(self, index: SupportsIndex, object: Element, /) + override def remove(self, value: Element, /) if sys.version_info >= (3, 14): # Next methods are copied from builtins.list def clear(self) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/queues.byi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/queues.byi index 4ff3b51ae8..26820593cb 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/queues.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/queues.byi @@ -35,7 +35,7 @@ class Queue[in out Element]: class JoinableQueue[in out Element](Queue[Element]): override def __getstate__(self) -> _JoinableQueueState - override def __setstate__(self, state: _JoinableQueueState) -> None + override def __setstate__(self, state: _JoinableQueueState) def task_done(self) def join(self) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/sharedctypes.byi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/sharedctypes.byi index 9d7386a4a6..d3aa3ee737 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/sharedctypes.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/sharedctypes.byi @@ -119,7 +119,7 @@ class SynchronizedString(SynchronizedArray[bytes]): def __setitem__(self, i: SupportsIndex, value: bytes) -> None override def __getslice__(self, start: SupportsIndex, stop: SupportsIndex) -> bytes - override def __setslice__(self, start: SupportsIndex, stop: SupportsIndex, values: bytes) -> None + override def __setslice__(self, start: SupportsIndex, stop: SupportsIndex, values: bytes) value: bytes raw: bytes diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/nt.byi b/crates/ty_vendored/vendor/typeshed/stdlib/nt.byi index c10b791bd4..008ce4a212 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/nt.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/nt.byi @@ -9,114 +9,114 @@ import sys if sys.platform == "win32": # Actually defined here and re-exported from os at runtime, # but this leads to less code duplication - from os import ( - F_OK as F_OK, - O_APPEND as O_APPEND, - O_BINARY as O_BINARY, - O_CREAT as O_CREAT, - O_EXCL as O_EXCL, - O_NOINHERIT as O_NOINHERIT, - O_RANDOM as O_RANDOM, - O_RDONLY as O_RDONLY, - O_RDWR as O_RDWR, - O_SEQUENTIAL as O_SEQUENTIAL, - O_SHORT_LIVED as O_SHORT_LIVED, - O_TEMPORARY as O_TEMPORARY, - O_TEXT as O_TEXT, - O_TRUNC as O_TRUNC, - O_WRONLY as O_WRONLY, - P_DETACH as P_DETACH, - P_NOWAIT as P_NOWAIT, - P_NOWAITO as P_NOWAITO, - P_OVERLAY as P_OVERLAY, - P_WAIT as P_WAIT, - R_OK as R_OK, - TMP_MAX as TMP_MAX, - W_OK as W_OK, - X_OK as X_OK, - DirEntry as DirEntry, - abort as abort, - access as access, - chdir as chdir, - chmod as chmod, - close as close, - closerange as closerange, - cpu_count as cpu_count, - device_encoding as device_encoding, - dup as dup, - dup2 as dup2, - error as error, - execv as execv, - execve as execve, - fspath as fspath, - fstat as fstat, - fsync as fsync, - ftruncate as ftruncate, - get_handle_inheritable as get_handle_inheritable, - get_inheritable as get_inheritable, - get_terminal_size as get_terminal_size, - getcwd as getcwd, - getcwdb as getcwdb, - getlogin as getlogin, - getpid as getpid, - getppid as getppid, - isatty as isatty, - kill as kill, - link as link, - listdir as listdir, - lseek as lseek, - lstat as lstat, - mkdir as mkdir, - open as open, - pipe as pipe, - putenv as putenv, - read as read, - readlink as readlink, - remove as remove, - rename as rename, - replace as replace, - rmdir as rmdir, - scandir as scandir, - set_handle_inheritable as set_handle_inheritable, - set_inheritable as set_inheritable, - spawnv as spawnv, - spawnve as spawnve, - startfile as startfile, - stat as stat, - stat_result as stat_result, - statvfs_result as statvfs_result, - strerror as strerror, - symlink as symlink, - system as system, - terminal_size as terminal_size, - times as times, - times_result as times_result, - truncate as truncate, - umask as umask, - uname_result as uname_result, - unlink as unlink, - unsetenv as unsetenv, - urandom as urandom, - utime as utime, - waitpid as waitpid, - waitstatus_to_exitcode as waitstatus_to_exitcode, - write as write, + from os export ( + F_OK, + O_APPEND, + O_BINARY, + O_CREAT, + O_EXCL, + O_NOINHERIT, + O_RANDOM, + O_RDONLY, + O_RDWR, + O_SEQUENTIAL, + O_SHORT_LIVED, + O_TEMPORARY, + O_TEXT, + O_TRUNC, + O_WRONLY, + P_DETACH, + P_NOWAIT, + P_NOWAITO, + P_OVERLAY, + P_WAIT, + R_OK, + TMP_MAX, + W_OK, + X_OK, + DirEntry, + abort, + access, + chdir, + chmod, + close, + closerange, + cpu_count, + device_encoding, + dup, + dup2, + error, + execv, + execve, + fspath, + fstat, + fsync, + ftruncate, + get_handle_inheritable, + get_inheritable, + get_terminal_size, + getcwd, + getcwdb, + getlogin, + getpid, + getppid, + isatty, + kill, + link, + listdir, + lseek, + lstat, + mkdir, + open, + pipe, + putenv, + read, + readlink, + remove, + rename, + replace, + rmdir, + scandir, + set_handle_inheritable, + set_inheritable, + spawnv, + spawnve, + startfile, + stat, + stat_result, + statvfs_result, + strerror, + symlink, + system, + terminal_size, + times, + times_result, + truncate, + umask, + uname_result, + unlink, + unsetenv, + urandom, + utime, + waitpid, + waitstatus_to_exitcode, + write, ) if sys.version_info >= (3, 11): - from os import EX_OK as EX_OK + from os export EX_OK if sys.version_info >= (3, 12): - from os import ( - get_blocking as get_blocking, - listdrives as listdrives, - listmounts as listmounts, - listvolumes as listvolumes, - set_blocking as set_blocking, + from os export ( + get_blocking, + listdrives, + listmounts, + listvolumes, + set_blocking, ) if sys.version_info >= (3, 13): - from os import fchmod as fchmod, lchmod as lchmod + from os export fchmod, lchmod if sys.version_info >= (3, 14): - from os import readinto as readinto + from os export readinto environ: dict[str, str] diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/ntpath.byi b/crates/ty_vendored/vendor/typeshed/stdlib/ntpath.byi index f0313ba51d..b46b28aa4a 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/ntpath.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/ntpath.byi @@ -24,40 +24,39 @@ from genericpath import ( from os import PathLike # Re-export common definitions from posixpath to reduce duplication -from posixpath import ( - abspath as abspath, - basename as basename, - commonpath as commonpath, - curdir as curdir, - defpath as defpath, - devnull as devnull, - dirname as dirname, - expanduser as expanduser, - expandvars as expandvars, - extsep as extsep, - isabs as isabs, - islink as islink, - ismount as ismount, - lexists as lexists, - normcase as normcase, - normpath as normpath, - pardir as pardir, - pathsep as pathsep, - relpath as relpath, - sep as sep, - split as split, - splitdrive as splitdrive, - splitext as splitext, - supports_unicode_filenames as supports_unicode_filenames, +from posixpath export ( + abspath, + basename, + commonpath, + curdir, + defpath, + devnull, + dirname, + expanduser, + expandvars, + extsep, + isabs, + islink, + ismount, + lexists, + normcase, + normpath, + pardir, + pathsep, + relpath, + sep, + split, + splitdrive, + splitext, + supports_unicode_filenames, ) -from typing_extensions import LiteralString if sys.version_info >= (3, 12): - from posixpath import isjunction as isjunction, splitroot as splitroot + from posixpath export isjunction, splitroot if sys.version_info >= (3, 13): - from genericpath import isdevdrive as isdevdrive + from genericpath export isdevdrive if sys.version_info >= (3, 15): - from genericpath import ALL_BUT_LAST as ALL_BUT_LAST + from genericpath export ALL_BUT_LAST __all__ = [ "normcase", @@ -107,12 +106,12 @@ if sys.version_info >= (3, 13): if sys.version_info >= (3, 15): __all__ += ["ALL_BUT_LAST"] -altsep: LiteralString +altsep: literal str # First parameter is not actually pos-only, # but must be defined as pos-only in the stub or cross-platform code doesn't type-check, # as the parameter name is different in posixpath.join() -def join(path: LiteralString, /, *paths: LiteralString) -> LiteralString +def join(path: literal str, /, *paths: literal str) -> literal str def join(path: StrPath, /, *paths: StrPath) -> str def join(path: BytesPath, /, *paths: BytesPath) -> bytes diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/nturl2path.byi b/crates/ty_vendored/vendor/typeshed/stdlib/nturl2path.byi index d4b405903b..2f54e7353e 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/nturl2path.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/nturl2path.byi @@ -6,13 +6,13 @@ for urllib.requests, thus do not use directly. from typing_extensions import deprecated -@deprecated("The `nturl2path` module is deprecated since Python 3.14.") +@deprecated("Deprecated; use `urllib.request` file-URL helpers instead.") def url2pathname(url: str) -> str: """OS-specific conversion from a relative URL of the 'file' scheme to a file system path; not recommended for general use. """ -@deprecated("The `nturl2path` module is deprecated since Python 3.14.") +@deprecated("Deprecated; use `urllib.request` file-URL helpers instead.") def pathname2url(p: str) -> str: """OS-specific conversion from a file system path to a relative URL of the 'file' scheme; not recommended for general use. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/operator.byi b/crates/ty_vendored/vendor/typeshed/stdlib/operator.byi index 41fc838ca5..984087d5c5 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/operator.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/operator.byi @@ -8,58 +8,58 @@ used for special methods; variants without leading and trailing """ import sys -from _operator import ( - abs as abs, - add as add, - and_ as and_, - concat as concat, - contains as contains, - countOf as countOf, - delitem as delitem, - eq as eq, - floordiv as floordiv, - ge as ge, - getitem as getitem, - gt as gt, - iadd as iadd, - iand as iand, - iconcat as iconcat, - ifloordiv as ifloordiv, - ilshift as ilshift, - imatmul as imatmul, - imod as imod, - imul as imul, - index as index, - indexOf as indexOf, - inv as inv, - invert as invert, - ior as ior, - ipow as ipow, - irshift as irshift, - is_ as is_, - is_not as is_not, - isub as isub, - itruediv as itruediv, - ixor as ixor, - le as le, - length_hint as length_hint, - lshift as lshift, - lt as lt, - matmul as matmul, - mod as mod, - mul as mul, - ne as ne, - neg as neg, - not_ as not_, - or_ as or_, - pos as pos, - pow as pow, - rshift as rshift, - setitem as setitem, - sub as sub, - truediv as truediv, - truth as truth, - xor as xor, +from _operator export ( + abs, + add, + and_, + concat, + contains, + countOf, + delitem, + eq, + floordiv, + ge, + getitem, + gt, + iadd, + iand, + iconcat, + ifloordiv, + ilshift, + imatmul, + imod, + imul, + index, + indexOf, + inv, + invert, + ior, + ipow, + irshift, + is_, + is_not, + isub, + itruediv, + ixor, + le, + length_hint, + lshift, + lt, + matmul, + mod, + mul, + ne, + neg, + not_, + or_, + pos, + pow, + rshift, + setitem, + sub, + truediv, + truth, + xor, ) from _typeshed import SupportsGetItem from typing import Generic, TypeVar, final @@ -124,12 +124,12 @@ __all__ = [ ] if sys.version_info >= (3, 11): - from _operator import call as call + from _operator export call __all__ += ["call"] if sys.version_info >= (3, 14): - from _operator import is_none as is_none, is_not_none as is_not_none + from _operator export is_none, is_not_none __all__ += ["is_none", "is_not_none"] diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/optparse.byi b/crates/ty_vendored/vendor/typeshed/stdlib/optparse.byi index e1b3d12fb3..57d9030748 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/optparse.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/optparse.byi @@ -25,7 +25,7 @@ import builtins from _typeshed import MaybeNone, SupportsWrite from collections.abc import Callable, Iterable, Mapping, Sequence from typing import ClassVar, Final, Literal -from typing_extensions import Self +from typing_extensions import Never, Self __all__ = [ "Option", @@ -394,7 +394,7 @@ class Values: # is set on the instance. def __getattr__(self, name: str) -> dynamic # TODO: mypy infers -> object for __getattr__ if __setattr__ has `value: object` - override def __setattr__(self, name: str, value: dynamic, /) -> None + override def __setattr__(self, name: str, value: dynamic, /) override def __eq__(self, other: object) -> bool class OptionParser(OptionContainer): @@ -551,7 +551,7 @@ class OptionParser(OptionContainer): allow_interspersed_args. """ - def error(self, msg: str) -> NoReturn: + def error(self, msg: str) -> Never: """error(msg : string) Print a usage message incorporating 'msg' to stderr and exit. @@ -559,7 +559,7 @@ class OptionParser(OptionContainer): should either exit or raise an exception. """ - def exit(self, status: int = 0, msg: str | None = None) -> NoReturn + def exit(self, status: int = 0, msg: str | None = None) -> Never def expand_prog_name(self, s: str) -> str def format_epilog(self, formatter: HelpFormatter) -> str override def format_help(self, formatter: HelpFormatter | None = None) -> str diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/os/__init__.byi b/crates/ty_vendored/vendor/typeshed/stdlib/os/__init__.byi index 2a933d2116..5340c83c32 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/os/__init__.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/os/__init__.byi @@ -49,20 +49,20 @@ from io import BufferedRandom, BufferedReader, BufferedWriter, FileIO, TextIOWra from subprocess import Popen from types import GenericAlias, TracebackType from typing import Final, Generic, Literal, Protocol, TypeAlias, TypeVar, final, runtime_checkable, type_check_only -from typing_extensions import LiteralString, Self, Unpack, deprecated +from typing_extensions import Never, Self, Unpack, deprecated from . import path as _path # Re-export common definitions from os.path to reduce duplication -from .path import ( - altsep as altsep, - curdir as curdir, - defpath as defpath, - devnull as devnull, - extsep as extsep, - pardir as pardir, - pathsep as pathsep, - sep as sep, +from .path export ( + altsep, + curdir, + defpath, + devnull, + extsep, + pardir, + pathsep, + sep, ) __all__ = [ @@ -750,7 +750,7 @@ if sys.platform != "win32": final ST_RDONLY: int linesep: "\n" | "\r\n" -name: LiteralString +name: literal str F_OK: Final = 0 R_OK: Final = 4 @@ -788,9 +788,9 @@ class _Environ(MutableMapping[AnyStr, AnyStr], Generic[AnyStr]): override def setdefault(self, key: AnyStr, value: AnyStr) -> AnyStr def copy(self) -> dict[AnyStr, AnyStr] - override def __delitem__(self, key: AnyStr) -> None + override def __delitem__(self, key: AnyStr) override def __getitem__(self, key: AnyStr) -> AnyStr - override def __setitem__(self, key: AnyStr, value: AnyStr) -> None + override def __setitem__(self, key: AnyStr, value: AnyStr) override def __iter__(self) -> Iterator[AnyStr] override def __len__(self) -> int def __or__[T1, T2](self, other: Mapping[T1, T2]) -> dict[AnyStr | T1, AnyStr | T2] @@ -2357,7 +2357,7 @@ if sys.platform != "win32": of the file the link points to. """ -def abort() -> NoReturn: +def abort() -> Never: """Abort the interpreter immediately. This function 'dumps core' or otherwise fails in the hardest way @@ -2365,14 +2365,14 @@ def abort() -> NoReturn: """ # These are defined as execl(file, *args) but the first *arg is mandatory. -def execl(file: StrOrBytesPath, *args: *(StrOrBytesPath, Unpack[tuple[StrOrBytesPath, ...]])) -> NoReturn: +def execl(file: StrOrBytesPath, *args: *(StrOrBytesPath, Unpack[tuple[StrOrBytesPath, ...]])) -> Never: """execl(file, *args) Execute the executable file with argument list args, replacing the current process. """ -def execlp(file: StrOrBytesPath, *args: *(StrOrBytesPath, Unpack[tuple[StrOrBytesPath, ...]])) -> NoReturn: +def execlp(file: StrOrBytesPath, *args: *(StrOrBytesPath, Unpack[tuple[StrOrBytesPath, ...]])) -> Never: """execlp(file, *args) Execute the executable file (which is searched for along $PATH) @@ -2380,14 +2380,14 @@ def execlp(file: StrOrBytesPath, *args: *(StrOrBytesPath, Unpack[tuple[StrOrByte """ # These are: execle(file, *args, env) but env is pulled from the last element of the args. -def execle(file: StrOrBytesPath, *args: *(StrOrBytesPath, Unpack[tuple[StrOrBytesPath, ...]], ExecEnv)) -> NoReturn: +def execle(file: StrOrBytesPath, *args: *(StrOrBytesPath, Unpack[tuple[StrOrBytesPath, ...]], ExecEnv)) -> Never: """execle(file, *args, env) Execute the executable file with argument list args and environment env, replacing the current process. """ -def execlpe(file: StrOrBytesPath, *args: *(StrOrBytesPath, Unpack[tuple[StrOrBytesPath, ...]], ExecEnv)) -> NoReturn: +def execlpe(file: StrOrBytesPath, *args: *(StrOrBytesPath, Unpack[tuple[StrOrBytesPath, ...]], ExecEnv)) -> Never: """execlpe(file, *args, env) Execute the executable file (which is searched for along $PATH) @@ -2416,7 +2416,7 @@ private type ExecVArgs = ( # we limit to str | bytes. private type ExecEnv = Mapping[bytes, bytes | str] | Mapping[str, bytes | str] -def execv(path: StrOrBytesPath, argv: ExecVArgs, /) -> NoReturn: +def execv(path: StrOrBytesPath, argv: ExecVArgs, /) -> Never: """Execute an executable path with arguments, replacing current process. path @@ -2425,7 +2425,7 @@ def execv(path: StrOrBytesPath, argv: ExecVArgs, /) -> NoReturn: Tuple or list of strings. """ -def execve(path: FileDescriptorOrPath, argv: ExecVArgs, env: ExecEnv) -> NoReturn: +def execve(path: FileDescriptorOrPath, argv: ExecVArgs, env: ExecEnv) -> Never: """Execute an executable path with arguments, replacing current process. path @@ -2436,7 +2436,7 @@ def execve(path: FileDescriptorOrPath, argv: ExecVArgs, env: ExecEnv) -> NoRetur Dictionary of strings mapping to strings. """ -def execvp(file: StrOrBytesPath, args: ExecVArgs) -> NoReturn: +def execvp(file: StrOrBytesPath, args: ExecVArgs) -> Never: """execvp(file, args) Execute the executable file (which is searched for along $PATH) @@ -2444,7 +2444,7 @@ def execvp(file: StrOrBytesPath, args: ExecVArgs) -> NoReturn: args may be a list or tuple of strings. """ -def execvpe(file: StrOrBytesPath, args: ExecVArgs, env: ExecEnv) -> NoReturn: +def execvpe(file: StrOrBytesPath, args: ExecVArgs, env: ExecEnv) -> Never: """execvpe(file, args, env) Execute the executable file (which is searched for along $PATH) @@ -2453,7 +2453,7 @@ def execvpe(file: StrOrBytesPath, args: ExecVArgs, env: ExecEnv) -> NoReturn: args may be a list or tuple of strings. """ -def _exit(status: int) -> NoReturn: +def _exit(status: int) -> Never: """Exit to the system with specified status, without normal exit processing.""" def kill(pid: int, signal: int, /): @@ -3235,8 +3235,8 @@ if sys.platform == "linux": def pidfd_open(pid: int, flags: int = 0) -> int: """Return a file descriptor referring to the process *pid*. - The descriptor can be used to perform process management without races and - signals. + The descriptor can be used to perform process management without races + and signals. """ if sys.version_info >= (3, 12) and sys.platform == "linux": diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/os/path.byi b/crates/ty_vendored/vendor/typeshed/stdlib/os/path.byi index 4ba2953ca2..b74322a68e 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/os/path.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/os/path.byi @@ -14,7 +14,7 @@ import sys if sys.platform == "win32": from ntpath import * - from ntpath import __all__ as __all__ + from ntpath export __all__ else: from posixpath import * - from posixpath import __all__ as __all__ + from posixpath export __all__ diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/pathlib/__init__.byi b/crates/ty_vendored/vendor/typeshed/stdlib/pathlib/__init__.byi index 550db4491d..36c5438de9 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/pathlib/__init__.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/pathlib/__init__.byi @@ -139,7 +139,7 @@ class PurePath(PathLike[str]): slashes. """ - @deprecated("Deprecated since Python 3.14; will be removed in Python 3.19. Use `Path.as_uri()` instead.") + @deprecated("Deprecated; will be removed in Python 3.19. Use `Path.as_uri()` instead.") def as_uri(self) -> str: """Return the path as a URI.""" @@ -172,7 +172,7 @@ class PurePath(PathLike[str]): else: def is_relative_to(self, other: StrPath, /) -> bool: """Return True if the path is relative to another path or False.""" - @deprecated("Passing additional arguments is deprecated since Python 3.12; removed in Python 3.14.") + @deprecated("Passing additional arguments is deprecated; removed in Python 3.14.") def is_relative_to(self, other: StrPath, /, *_deprecated: StrPath) -> bool if sys.version_info >= (3, 12): diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/pkgutil.byi b/crates/ty_vendored/vendor/typeshed/stdlib/pkgutil.byi index d7291424d3..b64555dd93 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/pkgutil.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/pkgutil.byi @@ -85,7 +85,7 @@ if sys.version_info < (3, 12): init(self, fullname: str, file: IO[str], filename: StrOrBytesPath, etc: (str, str, int)) if sys.version_info < (3, 14): - @deprecated("Deprecated since Python 3.12; removed in Python 3.14. Use `importlib.util.find_spec()` instead.") + @deprecated("Deprecated; removed in Python 3.14. Use `importlib.util.find_spec()` instead.") def find_loader(fullname: str) -> LoaderProtocol | None: """Find a "loader" object for fullname @@ -94,7 +94,7 @@ if sys.version_info < (3, 14): and only returns the loader rather than the full spec """ - @deprecated("Deprecated since Python 3.12; removed in Python 3.14. Use `importlib.util.find_spec()` instead.") + @deprecated("Deprecated; removed in Python 3.14. Use `importlib.util.find_spec()` instead.") def get_loader(module_or_name: str) -> LoaderProtocol | None: """Get a "loader" object for module_or_name diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/platform.byi b/crates/ty_vendored/vendor/typeshed/stdlib/platform.byi index 9732592ab5..922548bb00 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/platform.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/platform.byi @@ -41,7 +41,7 @@ def mac_ver( """ if sys.version_info < (3, 15): - @deprecated("Deprecated since Python 3.13; will be removed in Python 3.15.") + @deprecated("Deprecated; will be removed in Python 3.15.") def java_ver( release: str = "", vendor: str = "", @@ -119,7 +119,7 @@ if sys.version_info >= (3, 12): def __new__(_cls, system: str, node: str, release: str, version: str, machine: str) -> Self: """Create new instance of uname_result_base(system, node, release, version, machine)""" - let processor: str + let processor: str # ty:ignore[invalid-named-tuple-override] else: @disjoint_base @@ -135,7 +135,7 @@ else: def __new__(_cls, system: str, node: str, release: str, version: str, machine: str) -> Self: """Create new instance of uname_result_base(system, node, release, version, machine)""" - let processor: str + let processor: str # ty:ignore[invalid-named-tuple-override] def uname() -> uname_result: """Fairly portable uname interface. Returns a tuple diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/poplib.byi b/crates/ty_vendored/vendor/typeshed/stdlib/poplib.byi index 0aa2690321..de80fc186a 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/poplib.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/poplib.byi @@ -10,7 +10,7 @@ from _typeshed import StrOrBytesPath from builtins import list as _list # conflicts with a method named "list" from re import Pattern from typing import Final, TypeAlias -from typing_extensions import deprecated +from typing_extensions import Never, deprecated __all__ = ["POP3", "error_proto", "POP3_SSL"] @@ -206,7 +206,7 @@ class POP3_SSL(POP3): def __init__( self, host: str, port: int = 995, *, timeout: float = ..., context: ssl.SSLContext | None = None ) -> None - def stls(self, context: dynamic = None) -> NoReturn: + def stls(self, context: dynamic = None) -> Never: """The method unconditionally raises an exception since the STLS command doesn't make any sense on an already established SSL/TLS session. @@ -240,7 +240,7 @@ class POP3_SSL(POP3): certfile: StrOrBytesPath | None # "context" is actually the last argument, # but that breaks LSP and it doesn't really matter because all the arguments are ignored - override def stls(self, context: dynamic = None, keyfile: dynamic = None, certfile: dynamic = None) -> NoReturn: + override def stls(self, context: dynamic = None, keyfile: dynamic = None, certfile: dynamic = None) -> Never: """The method unconditionally raises an exception since the STLS command doesn't make any sense on an already established SSL/TLS session. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/posix.byi b/crates/ty_vendored/vendor/typeshed/stdlib/posix.byi index 1bebc208eb..986d318d6d 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/posix.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/posix.byi @@ -8,424 +8,424 @@ import sys if sys.platform != "win32": # Actually defined here, but defining in os allows sharing code with windows - from os import ( - CLD_CONTINUED as CLD_CONTINUED, - CLD_DUMPED as CLD_DUMPED, - CLD_EXITED as CLD_EXITED, - CLD_KILLED as CLD_KILLED, - CLD_STOPPED as CLD_STOPPED, - CLD_TRAPPED as CLD_TRAPPED, - EX_CANTCREAT as EX_CANTCREAT, - EX_CONFIG as EX_CONFIG, - EX_DATAERR as EX_DATAERR, - EX_IOERR as EX_IOERR, - EX_NOHOST as EX_NOHOST, - EX_NOINPUT as EX_NOINPUT, - EX_NOPERM as EX_NOPERM, - EX_NOUSER as EX_NOUSER, - EX_OK as EX_OK, - EX_OSERR as EX_OSERR, - EX_OSFILE as EX_OSFILE, - EX_PROTOCOL as EX_PROTOCOL, - EX_SOFTWARE as EX_SOFTWARE, - EX_TEMPFAIL as EX_TEMPFAIL, - EX_UNAVAILABLE as EX_UNAVAILABLE, - EX_USAGE as EX_USAGE, - F_LOCK as F_LOCK, - F_OK as F_OK, - F_TEST as F_TEST, - F_TLOCK as F_TLOCK, - F_ULOCK as F_ULOCK, - NGROUPS_MAX as NGROUPS_MAX, - O_ACCMODE as O_ACCMODE, - O_APPEND as O_APPEND, - O_ASYNC as O_ASYNC, - O_CLOEXEC as O_CLOEXEC, - O_CREAT as O_CREAT, - O_DIRECTORY as O_DIRECTORY, - O_DSYNC as O_DSYNC, - O_EXCL as O_EXCL, - O_FSYNC as O_FSYNC, - O_NDELAY as O_NDELAY, - O_NOCTTY as O_NOCTTY, - O_NOFOLLOW as O_NOFOLLOW, - O_NONBLOCK as O_NONBLOCK, - O_RDONLY as O_RDONLY, - O_RDWR as O_RDWR, - O_SYNC as O_SYNC, - O_TRUNC as O_TRUNC, - O_WRONLY as O_WRONLY, - P_ALL as P_ALL, - P_PGID as P_PGID, - P_PID as P_PID, - POSIX_SPAWN_CLOSE as POSIX_SPAWN_CLOSE, - POSIX_SPAWN_DUP2 as POSIX_SPAWN_DUP2, - POSIX_SPAWN_OPEN as POSIX_SPAWN_OPEN, - PRIO_PGRP as PRIO_PGRP, - PRIO_PROCESS as PRIO_PROCESS, - PRIO_USER as PRIO_USER, - R_OK as R_OK, - RTLD_GLOBAL as RTLD_GLOBAL, - RTLD_LAZY as RTLD_LAZY, - RTLD_LOCAL as RTLD_LOCAL, - RTLD_NODELETE as RTLD_NODELETE, - RTLD_NOLOAD as RTLD_NOLOAD, - RTLD_NOW as RTLD_NOW, - SCHED_FIFO as SCHED_FIFO, - SCHED_OTHER as SCHED_OTHER, - SCHED_RR as SCHED_RR, - SEEK_DATA as SEEK_DATA, - SEEK_HOLE as SEEK_HOLE, - ST_NOSUID as ST_NOSUID, - ST_RDONLY as ST_RDONLY, - TMP_MAX as TMP_MAX, - W_OK as W_OK, - WCONTINUED as WCONTINUED, - WCOREDUMP as WCOREDUMP, - WEXITED as WEXITED, - WEXITSTATUS as WEXITSTATUS, - WIFCONTINUED as WIFCONTINUED, - WIFEXITED as WIFEXITED, - WIFSIGNALED as WIFSIGNALED, - WIFSTOPPED as WIFSTOPPED, - WNOHANG as WNOHANG, - WNOWAIT as WNOWAIT, - WSTOPPED as WSTOPPED, - WSTOPSIG as WSTOPSIG, - WTERMSIG as WTERMSIG, - WUNTRACED as WUNTRACED, - X_OK as X_OK, - DirEntry as DirEntry, - _exit as _exit, - abort as abort, - access as access, - chdir as chdir, - chmod as chmod, - chown as chown, - chroot as chroot, - close as close, - closerange as closerange, - confstr as confstr, - confstr_names as confstr_names, - cpu_count as cpu_count, - ctermid as ctermid, - device_encoding as device_encoding, - dup as dup, - dup2 as dup2, - error as error, - execv as execv, - execve as execve, - fchdir as fchdir, - fchmod as fchmod, - fchown as fchown, - fork as fork, - forkpty as forkpty, - fpathconf as fpathconf, - fspath as fspath, - fstat as fstat, - fstatvfs as fstatvfs, - fsync as fsync, - ftruncate as ftruncate, - get_blocking as get_blocking, - get_inheritable as get_inheritable, - get_terminal_size as get_terminal_size, - getcwd as getcwd, - getcwdb as getcwdb, - getegid as getegid, - geteuid as geteuid, - getgid as getgid, - getgrouplist as getgrouplist, - getgroups as getgroups, - getloadavg as getloadavg, - getlogin as getlogin, - getpgid as getpgid, - getpgrp as getpgrp, - getpid as getpid, - getppid as getppid, - getpriority as getpriority, - getsid as getsid, - getuid as getuid, - initgroups as initgroups, - isatty as isatty, - kill as kill, - killpg as killpg, - lchown as lchown, - link as link, - listdir as listdir, - lockf as lockf, - lseek as lseek, - lstat as lstat, - major as major, - makedev as makedev, - minor as minor, - mkdir as mkdir, - mkfifo as mkfifo, - mknod as mknod, - nice as nice, - open as open, - openpty as openpty, - pathconf as pathconf, - pathconf_names as pathconf_names, - pipe as pipe, - posix_spawn as posix_spawn, - posix_spawnp as posix_spawnp, - pread as pread, - preadv as preadv, - putenv as putenv, - pwrite as pwrite, - pwritev as pwritev, - read as read, - readlink as readlink, - readv as readv, - register_at_fork as register_at_fork, - remove as remove, - rename as rename, - replace as replace, - rmdir as rmdir, - scandir as scandir, - sched_get_priority_max as sched_get_priority_max, - sched_get_priority_min as sched_get_priority_min, - sched_param as sched_param, - sched_yield as sched_yield, - sendfile as sendfile, - set_blocking as set_blocking, - set_inheritable as set_inheritable, - setegid as setegid, - seteuid as seteuid, - setgid as setgid, - setgroups as setgroups, - setpgid as setpgid, - setpgrp as setpgrp, - setpriority as setpriority, - setregid as setregid, - setreuid as setreuid, - setsid as setsid, - setuid as setuid, - stat as stat, - stat_result as stat_result, - statvfs as statvfs, - statvfs_result as statvfs_result, - strerror as strerror, - symlink as symlink, - sync as sync, - sysconf as sysconf, - sysconf_names as sysconf_names, - system as system, - tcgetpgrp as tcgetpgrp, - tcsetpgrp as tcsetpgrp, - terminal_size as terminal_size, - times as times, - times_result as times_result, - truncate as truncate, - ttyname as ttyname, - umask as umask, - uname as uname, - uname_result as uname_result, - unlink as unlink, - unsetenv as unsetenv, - urandom as urandom, - utime as utime, - wait as wait, - wait3 as wait3, - wait4 as wait4, - waitpid as waitpid, - waitstatus_to_exitcode as waitstatus_to_exitcode, - write as write, - writev as writev, + from os export ( + 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_OK, + EX_OSERR, + EX_OSFILE, + EX_PROTOCOL, + EX_SOFTWARE, + EX_TEMPFAIL, + EX_UNAVAILABLE, + EX_USAGE, + F_LOCK, + F_OK, + F_TEST, + F_TLOCK, + F_ULOCK, + NGROUPS_MAX, + O_ACCMODE, + O_APPEND, + O_ASYNC, + O_CLOEXEC, + O_CREAT, + O_DIRECTORY, + O_DSYNC, + O_EXCL, + O_FSYNC, + O_NDELAY, + O_NOCTTY, + O_NOFOLLOW, + O_NONBLOCK, + O_RDONLY, + O_RDWR, + O_SYNC, + O_TRUNC, + O_WRONLY, + P_ALL, + P_PGID, + P_PID, + POSIX_SPAWN_CLOSE, + POSIX_SPAWN_DUP2, + POSIX_SPAWN_OPEN, + PRIO_PGRP, + PRIO_PROCESS, + PRIO_USER, + R_OK, + 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, + TMP_MAX, + W_OK, + WCONTINUED, + WCOREDUMP, + WEXITED, + WEXITSTATUS, + WIFCONTINUED, + WIFEXITED, + WIFSIGNALED, + WIFSTOPPED, + WNOHANG, + WNOWAIT, + WSTOPPED, + WSTOPSIG, + WTERMSIG, + WUNTRACED, + X_OK, + DirEntry, + _exit, + abort, + access, + chdir, + chmod, + chown, + chroot, + close, + closerange, + confstr, + confstr_names, + cpu_count, + ctermid, + device_encoding, + dup, + dup2, + error, + execv, + execve, + fchdir, + fchmod, + fchown, + fork, + forkpty, + fpathconf, + fspath, + fstat, + fstatvfs, + fsync, + ftruncate, + get_blocking, + get_inheritable, + get_terminal_size, + getcwd, + getcwdb, + getegid, + geteuid, + getgid, + getgrouplist, + getgroups, + getloadavg, + getlogin, + getpgid, + getpgrp, + getpid, + getppid, + getpriority, + getsid, + getuid, + initgroups, + isatty, + kill, + killpg, + lchown, + link, + listdir, + lockf, + lseek, + lstat, + major, + makedev, + minor, + mkdir, + mkfifo, + mknod, + nice, + open, + openpty, + pathconf, + pathconf_names, + pipe, + posix_spawn, + posix_spawnp, + pread, + preadv, + putenv, + pwrite, + pwritev, + read, + readlink, + readv, + register_at_fork, + remove, + rename, + replace, + rmdir, + scandir, + sched_get_priority_max, + sched_get_priority_min, + sched_param, + sched_yield, + sendfile, + set_blocking, + set_inheritable, + setegid, + seteuid, + setgid, + setgroups, + setpgid, + setpgrp, + setpriority, + setregid, + setreuid, + setsid, + setuid, + stat, + stat_result, + statvfs, + statvfs_result, + strerror, + symlink, + sync, + sysconf, + sysconf_names, + system, + tcgetpgrp, + tcsetpgrp, + terminal_size, + times, + times_result, + truncate, + ttyname, + umask, + uname, + uname_result, + unlink, + unsetenv, + urandom, + utime, + wait, + wait3, + wait4, + waitpid, + waitstatus_to_exitcode, + write, + writev, ) if sys.version_info >= (3, 11): - from os import login_tty as login_tty + from os export login_tty if sys.version_info >= (3, 13): - from os import grantpt as grantpt, posix_openpt as posix_openpt, ptsname as ptsname, unlockpt as unlockpt + from os export grantpt, posix_openpt, ptsname, unlockpt if sys.version_info >= (3, 13) and sys.platform == "linux": - from os import ( - POSIX_SPAWN_CLOSEFROM as POSIX_SPAWN_CLOSEFROM, - TFD_CLOEXEC as TFD_CLOEXEC, - TFD_NONBLOCK as TFD_NONBLOCK, - TFD_TIMER_ABSTIME as TFD_TIMER_ABSTIME, - TFD_TIMER_CANCEL_ON_SET as TFD_TIMER_CANCEL_ON_SET, - timerfd_create as timerfd_create, - timerfd_gettime as timerfd_gettime, - timerfd_gettime_ns as timerfd_gettime_ns, - timerfd_settime as timerfd_settime, - timerfd_settime_ns as timerfd_settime_ns, + from os export ( + 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.version_info >= (3, 14): - from os import readinto as readinto + from os export readinto if sys.version_info >= (3, 14) and sys.platform == "linux": - from os import SCHED_DEADLINE as SCHED_DEADLINE, SCHED_NORMAL as SCHED_NORMAL + from os export SCHED_DEADLINE, 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 + from os export O_EXLOCK, O_SHLOCK, chflags, lchflags, lchmod if sys.platform != "linux" and sys.platform != "darwin": - from os import EX_NOTFOUND as EX_NOTFOUND, SCHED_SPORADIC as SCHED_SPORADIC + from os export EX_NOTFOUND, SCHED_SPORADIC if sys.platform != "linux" and sys.version_info >= (3, 13): - from os import O_EXEC as O_EXEC, O_SEARCH as O_SEARCH + from os export O_EXEC, O_SEARCH if sys.version_info >= (3, 15): - from os import NODEV as NODEV + from os export NODEV if sys.version_info >= (3, 15) and sys.platform == "linux": - from os import ( - AT_NO_AUTOMOUNT as AT_NO_AUTOMOUNT, - AT_STATX_DONT_SYNC as AT_STATX_DONT_SYNC, - AT_STATX_FORCE_SYNC as AT_STATX_FORCE_SYNC, - AT_STATX_SYNC_AS_STAT as AT_STATX_SYNC_AS_STAT, - STATX_ATIME as STATX_ATIME, - STATX_BASIC_STATS as STATX_BASIC_STATS, - STATX_BLOCKS as STATX_BLOCKS, - STATX_BTIME as STATX_BTIME, - STATX_CTIME as STATX_CTIME, - STATX_DIOALIGN as STATX_DIOALIGN, - STATX_GID as STATX_GID, - STATX_INO as STATX_INO, - STATX_MNT_ID as STATX_MNT_ID, - STATX_MNT_ID_UNIQUE as STATX_MNT_ID_UNIQUE, - STATX_MODE as STATX_MODE, - STATX_MTIME as STATX_MTIME, - STATX_NLINK as STATX_NLINK, - STATX_SIZE as STATX_SIZE, - STATX_TYPE as STATX_TYPE, - STATX_UID as STATX_UID, - _clearenv as _clearenv, - statx as statx, - statx_result as statx_result, + from os export ( + AT_NO_AUTOMOUNT, + AT_STATX_DONT_SYNC, + AT_STATX_FORCE_SYNC, + AT_STATX_SYNC_AS_STAT, + STATX_ATIME, + STATX_BASIC_STATS, + STATX_BLOCKS, + STATX_BTIME, + STATX_CTIME, + STATX_DIOALIGN, + STATX_GID, + STATX_INO, + STATX_MNT_ID, + STATX_MNT_ID_UNIQUE, + STATX_MODE, + STATX_MTIME, + STATX_NLINK, + STATX_SIZE, + STATX_TYPE, + STATX_UID, + _clearenv, + statx, + statx_result, ) if sys.platform != "darwin": - from os import ( - POSIX_FADV_DONTNEED as POSIX_FADV_DONTNEED, - POSIX_FADV_NOREUSE as POSIX_FADV_NOREUSE, - POSIX_FADV_NORMAL as POSIX_FADV_NORMAL, - POSIX_FADV_RANDOM as POSIX_FADV_RANDOM, - POSIX_FADV_SEQUENTIAL as POSIX_FADV_SEQUENTIAL, - POSIX_FADV_WILLNEED as POSIX_FADV_WILLNEED, - RWF_APPEND as RWF_APPEND, - RWF_DSYNC as RWF_DSYNC, - RWF_HIPRI as RWF_HIPRI, - RWF_NOWAIT as RWF_NOWAIT, - RWF_SYNC as RWF_SYNC, - ST_APPEND as ST_APPEND, - ST_MANDLOCK as ST_MANDLOCK, - ST_NOATIME as ST_NOATIME, - ST_NODEV as ST_NODEV, - ST_NODIRATIME as ST_NODIRATIME, - ST_NOEXEC as ST_NOEXEC, - ST_RELATIME as ST_RELATIME, - ST_SYNCHRONOUS as ST_SYNCHRONOUS, - ST_WRITE as ST_WRITE, - fdatasync as fdatasync, - getresgid as getresgid, - getresuid as getresuid, - pipe2 as pipe2, - posix_fadvise as posix_fadvise, - posix_fallocate as posix_fallocate, - sched_getaffinity as sched_getaffinity, - sched_getparam as sched_getparam, - sched_getscheduler as sched_getscheduler, - sched_rr_get_interval as sched_rr_get_interval, - sched_setaffinity as sched_setaffinity, - sched_setparam as sched_setparam, - sched_setscheduler as sched_setscheduler, - setresgid as setresgid, - setresuid as setresuid, + from os export ( + POSIX_FADV_DONTNEED, + POSIX_FADV_NOREUSE, + POSIX_FADV_NORMAL, + POSIX_FADV_RANDOM, + POSIX_FADV_SEQUENTIAL, + POSIX_FADV_WILLNEED, + RWF_APPEND, + 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_rr_get_interval, + sched_setaffinity, + sched_setparam, + sched_setscheduler, + setresgid, + setresuid, ) if sys.platform != "darwin" or sys.version_info >= (3, 13): - from os import waitid as waitid, waitid_result as waitid_result + from os export waitid, waitid_result if sys.platform == "linux": - from os import ( - EFD_CLOEXEC as EFD_CLOEXEC, - EFD_NONBLOCK as EFD_NONBLOCK, - EFD_SEMAPHORE as EFD_SEMAPHORE, - GRND_NONBLOCK as GRND_NONBLOCK, - GRND_RANDOM as GRND_RANDOM, - MFD_ALLOW_SEALING as MFD_ALLOW_SEALING, - MFD_CLOEXEC as MFD_CLOEXEC, - MFD_HUGE_1GB as MFD_HUGE_1GB, - MFD_HUGE_1MB as MFD_HUGE_1MB, - MFD_HUGE_2GB as MFD_HUGE_2GB, - MFD_HUGE_2MB as MFD_HUGE_2MB, - MFD_HUGE_8MB as MFD_HUGE_8MB, - MFD_HUGE_16GB as MFD_HUGE_16GB, - MFD_HUGE_16MB as MFD_HUGE_16MB, - MFD_HUGE_32MB as MFD_HUGE_32MB, - MFD_HUGE_64KB as MFD_HUGE_64KB, - MFD_HUGE_256MB as MFD_HUGE_256MB, - MFD_HUGE_512KB as MFD_HUGE_512KB, - MFD_HUGE_512MB as MFD_HUGE_512MB, - MFD_HUGE_MASK as MFD_HUGE_MASK, - MFD_HUGE_SHIFT as MFD_HUGE_SHIFT, - MFD_HUGETLB as MFD_HUGETLB, - O_DIRECT as O_DIRECT, - O_LARGEFILE as O_LARGEFILE, - O_NOATIME as O_NOATIME, - O_PATH as O_PATH, - O_RSYNC as O_RSYNC, - O_TMPFILE as O_TMPFILE, - P_PIDFD as P_PIDFD, - RTLD_DEEPBIND as RTLD_DEEPBIND, - SCHED_BATCH as SCHED_BATCH, - SCHED_IDLE as SCHED_IDLE, - SCHED_RESET_ON_FORK as SCHED_RESET_ON_FORK, - SPLICE_F_MORE as SPLICE_F_MORE, - SPLICE_F_MOVE as SPLICE_F_MOVE, - SPLICE_F_NONBLOCK as SPLICE_F_NONBLOCK, - XATTR_CREATE as XATTR_CREATE, - XATTR_REPLACE as XATTR_REPLACE, - XATTR_SIZE_MAX as XATTR_SIZE_MAX, - copy_file_range as copy_file_range, - eventfd as eventfd, - eventfd_read as eventfd_read, - eventfd_write as eventfd_write, - getrandom as getrandom, - getxattr as getxattr, - listxattr as listxattr, - memfd_create as memfd_create, - pidfd_open as pidfd_open, - removexattr as removexattr, - setxattr as setxattr, - splice as splice, + from os export ( + EFD_CLOEXEC, + EFD_NONBLOCK, + EFD_SEMAPHORE, + GRND_NONBLOCK, + GRND_RANDOM, + MFD_ALLOW_SEALING, + MFD_CLOEXEC, + MFD_HUGE_1GB, + MFD_HUGE_1MB, + MFD_HUGE_2GB, + MFD_HUGE_2MB, + MFD_HUGE_8MB, + MFD_HUGE_16GB, + MFD_HUGE_16MB, + MFD_HUGE_32MB, + MFD_HUGE_64KB, + MFD_HUGE_256MB, + MFD_HUGE_512KB, + MFD_HUGE_512MB, + MFD_HUGE_MASK, + MFD_HUGE_SHIFT, + MFD_HUGETLB, + O_DIRECT, + O_LARGEFILE, + O_NOATIME, + O_PATH, + O_RSYNC, + O_TMPFILE, + P_PIDFD, + RTLD_DEEPBIND, + SCHED_BATCH, + SCHED_IDLE, + SCHED_RESET_ON_FORK, + SPLICE_F_MORE, + SPLICE_F_MOVE, + SPLICE_F_NONBLOCK, + XATTR_CREATE, + XATTR_REPLACE, + XATTR_SIZE_MAX, + copy_file_range, + eventfd, + eventfd_read, + eventfd_write, + getrandom, + getxattr, + listxattr, + memfd_create, + pidfd_open, + removexattr, + setxattr, + splice, ) if sys.version_info >= (3, 12): - from os import ( - CLONE_FILES as CLONE_FILES, - CLONE_FS as CLONE_FS, - CLONE_NEWCGROUP as CLONE_NEWCGROUP, - CLONE_NEWIPC as CLONE_NEWIPC, - CLONE_NEWNET as CLONE_NEWNET, - CLONE_NEWNS as CLONE_NEWNS, - CLONE_NEWPID as CLONE_NEWPID, - CLONE_NEWTIME as CLONE_NEWTIME, - CLONE_NEWUSER as CLONE_NEWUSER, - CLONE_NEWUTS as CLONE_NEWUTS, - CLONE_SIGHAND as CLONE_SIGHAND, - CLONE_SYSVSEM as CLONE_SYSVSEM, - CLONE_THREAD as CLONE_THREAD, - CLONE_VM as CLONE_VM, - PIDFD_NONBLOCK as PIDFD_NONBLOCK, - setns as setns, - unshare as unshare, + from os export ( + 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, + PIDFD_NONBLOCK, + setns, + unshare, ) if sys.platform == "darwin": - from os import O_EVTONLY as O_EVTONLY, O_NOFOLLOW_ANY as O_NOFOLLOW_ANY, O_SYMLINK as O_SYMLINK + from os export O_EVTONLY, O_NOFOLLOW_ANY, O_SYMLINK if sys.version_info >= (3, 12): - from os import ( - PRIO_DARWIN_BG as PRIO_DARWIN_BG, - PRIO_DARWIN_NONUI as PRIO_DARWIN_NONUI, - PRIO_DARWIN_PROCESS as PRIO_DARWIN_PROCESS, - PRIO_DARWIN_THREAD as PRIO_DARWIN_THREAD, + from os export ( + PRIO_DARWIN_BG, + PRIO_DARWIN_NONUI, + PRIO_DARWIN_PROCESS, + PRIO_DARWIN_THREAD, ) # Not same as os.environ or os.environb diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/posixpath.byi b/crates/ty_vendored/vendor/typeshed/stdlib/posixpath.byi index ef347820e9..86149f6792 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/posixpath.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/posixpath.byi @@ -30,12 +30,11 @@ from genericpath import ( ) if sys.version_info >= (3, 15): - from genericpath import ALL_BUT_LAST as ALL_BUT_LAST + from genericpath export ALL_BUT_LAST if sys.version_info >= (3, 13): - from genericpath import isdevdrive as isdevdrive + from genericpath export isdevdrive from os import PathLike -from typing_extensions import LiteralString __all__ = [ "normcase", @@ -87,14 +86,14 @@ if sys.version_info >= (3, 13): supports_unicode_filenames: bool # aliases (also in os) -curdir: LiteralString -pardir: LiteralString -sep: LiteralString -altsep: LiteralString | None -extsep: LiteralString -pathsep: LiteralString -defpath: LiteralString -devnull: LiteralString +curdir: literal str +pardir: literal str +sep: literal str +altsep: literal str | None +extsep: literal str +pathsep: literal str +defpath: literal str +devnull: literal str # Overloads are necessary to work around python/mypy#17952 & python/mypy#11880 def abspath(path: PathLike[AnyStr]) -> AnyStr: @@ -143,7 +142,7 @@ def normpath(path: PathLike[AnyStr]) -> AnyStr: """Normalize path, eliminating double slashes, etc.""" def normpath(path: AnyOrLiteralStr) -> AnyOrLiteralStr -def commonpath(paths: Iterable[LiteralString]) -> LiteralString: +def commonpath(paths: Iterable[literal str]) -> literal str: """Given a sequence of path names, returns the longest common sub-path.""" def commonpath(paths: Iterable[StrPath]) -> str def commonpath(paths: Iterable[BytesPath]) -> bytes @@ -151,7 +150,7 @@ def commonpath(paths: Iterable[BytesPath]) -> bytes # First parameter is not actually pos-only before Python 3.15, # but must be defined as pos-only in the stub or cross-platform code doesn't type-check, # as the parameter name is different in ntpath.join() -def join(a: LiteralString, /, *paths: LiteralString) -> LiteralString: +def join(a: literal str, /, *paths: literal str) -> literal str: """Join two or more pathname components, inserting '/' as needed. If any component is an absolute path, all previous path components will be discarded. An empty last part will result in a path that @@ -173,7 +172,7 @@ else: """ def realpath(filename: AnyStr, *, strict: bool | _AllowMissingType = False) -> AnyStr -def relpath(path: LiteralString, start: LiteralString | None = None) -> LiteralString: +def relpath(path: literal str, start: literal str | None = None) -> literal str: """Return a relative version of a path""" def relpath(path: BytesPath, start: BytesPath | None = None) -> bytes def relpath(path: StrPath, start: StrPath | None = None) -> str diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/profiling/__init__.byi b/crates/ty_vendored/vendor/typeshed/stdlib/profiling/__init__.byi index 82d583bdf8..e5684d8643 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/profiling/__init__.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/profiling/__init__.byi @@ -10,6 +10,6 @@ This package provides two types of profilers: the call stack. Low overhead and suitable for production use. """ -from . import sampling as sampling, tracing as tracing +from . export sampling, tracing __all__ = ("tracing", "sampling") diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/profiling/sampling/__init__.byi b/crates/ty_vendored/vendor/typeshed/stdlib/profiling/sampling/__init__.byi index bf32eeb0a5..8d5086e890 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/profiling/sampling/__init__.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/profiling/sampling/__init__.byi @@ -4,13 +4,13 @@ This module provides low-overhead profiling by periodically sampling the call stack rather than tracing every function call. """ -from .collector import Collector as Collector -from .gecko_collector import GeckoCollector as GeckoCollector -from .heatmap_collector import HeatmapCollector as HeatmapCollector -from .jsonl_collector import JsonlCollector as JsonlCollector -from .pstats_collector import PstatsCollector as PstatsCollector -from .stack_collector import CollapsedStackCollector as CollapsedStackCollector -from .string_table import StringTable as StringTable +from .collector export Collector +from .gecko_collector export GeckoCollector +from .heatmap_collector export HeatmapCollector +from .jsonl_collector export JsonlCollector +from .pstats_collector export PstatsCollector +from .stack_collector export CollapsedStackCollector +from .string_table export StringTable __all__ = ( "Collector", diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/profiling/sampling/collector.byi b/crates/ty_vendored/vendor/typeshed/stdlib/profiling/sampling/collector.byi index 0fe7de3fe6..1272e1e312 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/profiling/sampling/collector.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/profiling/sampling/collector.byi @@ -52,4 +52,8 @@ class Collector(ABC): """Collect data about a failed sample attempt.""" abstract def export(self, filename: StrOrBytesPath): - """Export collected data to a file.""" + """Export collected data. + + Returns: + bool: True if output was generated, False if there was no data to export. + """ diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/profiling/sampling/stack_collector.byi b/crates/ty_vendored/vendor/typeshed/stdlib/profiling/sampling/stack_collector.byi index 13665729fa..f65061fc37 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/profiling/sampling/stack_collector.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/profiling/sampling/stack_collector.byi @@ -15,12 +15,12 @@ class StackTraceCollector(Collector, metaclass=ABCMeta): class CollapsedStackCollector(StackTraceCollector): init(self, sample_interval_usec: int, *, skip_idle: bool = False) - override def process_frames(self, frames: Sequence[_Frame], thread_id: int, weight: int = 1) -> None + override def process_frames(self, frames: Sequence[_Frame], thread_id: int, weight: int = 1) def export(self, filename: StrOrBytesPath) class FlamegraphCollector(StackTraceCollector): init(self, sample_interval_usec: int, *, skip_idle: bool = False) - override def collect(self, stack_frames: Sequence[InterpreterInfo] | Sequence[AwaitedInfo], timestamps_us: _Timestamps = None) -> None: + override def collect(self, stack_frames: Sequence[InterpreterInfo] | Sequence[AwaitedInfo], timestamps_us: _Timestamps = None): """Override to track thread status statistics before processing frames.""" def set_stats( @@ -35,7 +35,7 @@ class FlamegraphCollector(StackTraceCollector): """Set profiling statistics to include in flamegraph data.""" def export(self, filename: StrOrBytesPath) - override def process_frames(self, frames: Sequence[_Frame], thread_id: int, weight: int = 1) -> None: + override def process_frames(self, frames: Sequence[_Frame], thread_id: int, weight: int = 1): """Process stack frames into flamegraph tree structure. Args: diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/profiling/tracing.byi b/crates/ty_vendored/vendor/typeshed/stdlib/profiling/tracing.byi index b734d9e253..b45b71639b 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/profiling/tracing.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/profiling/tracing.byi @@ -4,7 +4,7 @@ This module provides deterministic profiling of Python programs by tracing every function call and return. """ -from cProfile import Profile as Profile, run as run, runctx as runctx +from cProfile export Profile, run, runctx from types import CodeType from typing import TypeAlias diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/pty.byi b/crates/ty_vendored/vendor/typeshed/stdlib/pty.byi index 77de826519..1b1396cb7e 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/pty.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/pty.byi @@ -20,14 +20,14 @@ if sys.platform != "win32": """ if sys.version_info < (3, 14): - @deprecated("Deprecated since Python 3.12; removed in Python 3.14. Use `openpty()` instead.") + @deprecated("Deprecated; removed in Python 3.14. Use `openpty()` instead.") def master_open() -> (int, str): """master_open() -> (master_fd, slave_name) Open a pty master and return the fd, and the filename of the slave end. Deprecated, use openpty() instead. """ - @deprecated("Deprecated since Python 3.12; removed in Python 3.14. Use `openpty()` instead.") + @deprecated("Deprecated; removed in Python 3.14. Use `openpty()` instead.") def slave_open(tty_name: str) -> int: """slave_open(tty_name) -> slave_fd Open the pty slave and acquire the controlling terminal, returning diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/pydoc.byi b/crates/ty_vendored/vendor/typeshed/stdlib/pydoc.byi index 8ecbe25adf..fdc0febbe0 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/pydoc.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/pydoc.byi @@ -44,7 +44,7 @@ from collections.abc import Callable, Container, Mapping, MutableMapping from reprlib import Repr from types import MethodType, ModuleType, TracebackType from typing import Final, Protocol, TypeVar, type_check_only -from typing_extensions import deprecated +from typing_extensions import Never, deprecated __all__ = ["help"] @@ -89,7 +89,7 @@ def visiblename(name: str, all: Container[str] | None = None, obj: object = None def classify_class_attrs(object: object) -> list[(str, str, type, str)]: """Wrap inspect.classify_class_attrs, with fixup for data descriptors and bound methods.""" -@deprecated("Deprecated since Python 3.13.") +@deprecated("Deprecated.") def ispackage(path: StrPath) -> bool: # undocumented """Guess whether a path refers to a package directory.""" @@ -134,7 +134,7 @@ class Doc: def document(self, object: object, name: str | None = None, *args: dynamic) -> str: """Generate documentation for an object.""" - def fail(self, object: object, name: str | None = None, *args: dynamic) -> NoReturn: + def fail(self, object: object, name: str | None = None, *args: dynamic) -> Never: """Raise an exception for unimplemented types.""" abstract def docmodule(self, object: object, name: str | None = None, *args: dynamic) -> str: diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/pyexpat/__init__.byi b/crates/ty_vendored/vendor/typeshed/stdlib/pyexpat/__init__.byi index 93a6090f5d..d1d154abb9 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/pyexpat/__init__.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/pyexpat/__init__.byi @@ -3,10 +3,10 @@ import sys from _typeshed import ReadableBuffer, SupportsRead from collections.abc import Callable -from pyexpat import errors as errors, model as model +from pyexpat export errors, model from typing import Final, TypeAlias, final from typing_extensions import CapsuleType -from xml.parsers.expat import ExpatError as ExpatError +from xml.parsers.expat export ExpatError final EXPAT_VERSION: str # undocumented version_info: (int, int, int) # undocumented diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/pyexpat/errors.byi b/crates/ty_vendored/vendor/typeshed/stdlib/pyexpat/errors.byi index 7f10d26a26..915c6064b6 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/pyexpat/errors.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/pyexpat/errors.byi @@ -2,54 +2,53 @@ import sys from typing import Final -from typing_extensions import LiteralString codes: dict[str, int] messages: dict[int, str] -final XML_ERROR_ABORTED: LiteralString -final XML_ERROR_ASYNC_ENTITY: LiteralString -final XML_ERROR_ATTRIBUTE_EXTERNAL_ENTITY_REF: LiteralString -final XML_ERROR_BAD_CHAR_REF: LiteralString -final XML_ERROR_BINARY_ENTITY_REF: LiteralString -final XML_ERROR_CANT_CHANGE_FEATURE_ONCE_PARSING: LiteralString -final XML_ERROR_DUPLICATE_ATTRIBUTE: LiteralString -final XML_ERROR_ENTITY_DECLARED_IN_PE: LiteralString -final XML_ERROR_EXTERNAL_ENTITY_HANDLING: LiteralString -final XML_ERROR_FEATURE_REQUIRES_XML_DTD: LiteralString -final XML_ERROR_FINISHED: LiteralString -final XML_ERROR_INCOMPLETE_PE: LiteralString -final XML_ERROR_INCORRECT_ENCODING: LiteralString -final XML_ERROR_INVALID_TOKEN: LiteralString -final XML_ERROR_JUNK_AFTER_DOC_ELEMENT: LiteralString -final XML_ERROR_MISPLACED_XML_PI: LiteralString -final XML_ERROR_NOT_STANDALONE: LiteralString -final XML_ERROR_NOT_SUSPENDED: LiteralString -final XML_ERROR_NO_ELEMENTS: LiteralString -final XML_ERROR_NO_MEMORY: LiteralString -final XML_ERROR_PARAM_ENTITY_REF: LiteralString -final XML_ERROR_PARTIAL_CHAR: LiteralString -final XML_ERROR_PUBLICID: LiteralString -final XML_ERROR_RECURSIVE_ENTITY_REF: LiteralString -final XML_ERROR_SUSPENDED: LiteralString -final XML_ERROR_SUSPEND_PE: LiteralString -final XML_ERROR_SYNTAX: LiteralString -final XML_ERROR_TAG_MISMATCH: LiteralString -final XML_ERROR_TEXT_DECL: LiteralString -final XML_ERROR_UNBOUND_PREFIX: LiteralString -final XML_ERROR_UNCLOSED_CDATA_SECTION: LiteralString -final XML_ERROR_UNCLOSED_TOKEN: LiteralString -final XML_ERROR_UNDECLARING_PREFIX: LiteralString -final XML_ERROR_UNDEFINED_ENTITY: LiteralString -final XML_ERROR_UNEXPECTED_STATE: LiteralString -final XML_ERROR_UNKNOWN_ENCODING: LiteralString -final XML_ERROR_XML_DECL: LiteralString +final XML_ERROR_ABORTED: literal str +final XML_ERROR_ASYNC_ENTITY: literal str +final XML_ERROR_ATTRIBUTE_EXTERNAL_ENTITY_REF: literal str +final XML_ERROR_BAD_CHAR_REF: literal str +final XML_ERROR_BINARY_ENTITY_REF: literal str +final XML_ERROR_CANT_CHANGE_FEATURE_ONCE_PARSING: literal str +final XML_ERROR_DUPLICATE_ATTRIBUTE: literal str +final XML_ERROR_ENTITY_DECLARED_IN_PE: literal str +final XML_ERROR_EXTERNAL_ENTITY_HANDLING: literal str +final XML_ERROR_FEATURE_REQUIRES_XML_DTD: literal str +final XML_ERROR_FINISHED: literal str +final XML_ERROR_INCOMPLETE_PE: literal str +final XML_ERROR_INCORRECT_ENCODING: literal str +final XML_ERROR_INVALID_TOKEN: literal str +final XML_ERROR_JUNK_AFTER_DOC_ELEMENT: literal str +final XML_ERROR_MISPLACED_XML_PI: literal str +final XML_ERROR_NOT_STANDALONE: literal str +final XML_ERROR_NOT_SUSPENDED: literal str +final XML_ERROR_NO_ELEMENTS: literal str +final XML_ERROR_NO_MEMORY: literal str +final XML_ERROR_PARAM_ENTITY_REF: literal str +final XML_ERROR_PARTIAL_CHAR: literal str +final XML_ERROR_PUBLICID: literal str +final XML_ERROR_RECURSIVE_ENTITY_REF: literal str +final XML_ERROR_SUSPENDED: literal str +final XML_ERROR_SUSPEND_PE: literal str +final XML_ERROR_SYNTAX: literal str +final XML_ERROR_TAG_MISMATCH: literal str +final XML_ERROR_TEXT_DECL: literal str +final XML_ERROR_UNBOUND_PREFIX: literal str +final XML_ERROR_UNCLOSED_CDATA_SECTION: literal str +final XML_ERROR_UNCLOSED_TOKEN: literal str +final XML_ERROR_UNDECLARING_PREFIX: literal str +final XML_ERROR_UNDEFINED_ENTITY: literal str +final XML_ERROR_UNEXPECTED_STATE: literal str +final XML_ERROR_UNKNOWN_ENCODING: literal str +final XML_ERROR_XML_DECL: literal str if sys.version_info >= (3, 11): - final XML_ERROR_RESERVED_PREFIX_XML: LiteralString - final XML_ERROR_RESERVED_PREFIX_XMLNS: LiteralString - final XML_ERROR_RESERVED_NAMESPACE_URI: LiteralString - final XML_ERROR_INVALID_ARGUMENT: LiteralString - final XML_ERROR_NO_BUFFER: LiteralString - final XML_ERROR_AMPLIFICATION_LIMIT_BREACH: LiteralString + final XML_ERROR_RESERVED_PREFIX_XML: literal str + final XML_ERROR_RESERVED_PREFIX_XMLNS: literal str + final XML_ERROR_RESERVED_NAMESPACE_URI: literal str + final XML_ERROR_INVALID_ARGUMENT: literal str + final XML_ERROR_NO_BUFFER: literal str + final XML_ERROR_AMPLIFICATION_LIMIT_BREACH: literal str if sys.version_info >= (3, 14): - final XML_ERROR_NOT_STARTED: LiteralString + final XML_ERROR_NOT_STARTED: literal str diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/queue.byi b/crates/ty_vendored/vendor/typeshed/stdlib/queue.byi index 208856c796..633af18411 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/queue.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/queue.byi @@ -1,7 +1,7 @@ """A multi-producer, multi-consumer queue.""" import sys -from _queue import Empty as Empty, SimpleQueue as SimpleQueue +from _queue export Empty, SimpleQueue from _typeshed import SupportsRichComparisonT from threading import Condition, Lock from types import GenericAlias diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/random.byi b/crates/ty_vendored/vendor/typeshed/stdlib/random.byi index b92f8cc4c7..acec6e567b 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/random.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/random.byi @@ -52,7 +52,7 @@ from _typeshed import SupportsLenAndGetItem from collections.abc import Callable, Iterable, MutableSequence, Sequence, Set as AbstractSet from fractions import Fraction from typing import ClassVar, TypeVar -from typing_extensions import deprecated +from typing_extensions import Never, deprecated __all__ = [ "Random", @@ -111,7 +111,7 @@ class Random(_random.Random): # Ignore Y041, since random.seed doesn't treat int like a float subtype. Having an explicit # int better documents conventional usage of random.seed. - override def seed(self, a: int | float | str | bytes | bytearray | None = None, version: int = 2) -> None: + override def seed(self, a: int | float | str | bytes | bytearray | None = None, version: int = 2): """Initialize internal state from a seed. The only supported seed types are None, int, float, @@ -132,7 +132,7 @@ class Random(_random.Random): override def getstate(self) -> (*: dynamic): """Return internal state; can be passed to setstate() later.""" - override def setstate(self, state: (*: dynamic)) -> None: + override def setstate(self, state: (*: dynamic)): """Restore internal state from object returned by getstate().""" def randrange(self, start: int, stop: int | None = None, step: int = 1) -> int: @@ -432,10 +432,10 @@ class SystemRandom(Random): override def getrandbits(self, k: int) -> int: # k can be passed by keyword """getrandbits(k) -> x. Generates an int with k random bits.""" - override def getstate(self, *args: dynamic, **kwds: dynamic) -> NoReturn: + override def getstate(self, *args: dynamic, **kwds: dynamic) -> Never: """Method should not be called for a system random number generator.""" - override def setstate(self, *args: dynamic, **kwds: dynamic) -> NoReturn: + override def setstate(self, *args: dynamic, **kwds: dynamic) -> Never: """Method should not be called for a system random number generator.""" _inst: Random diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/re.byi b/crates/ty_vendored/vendor/typeshed/stdlib/re.byi index e7cf5e5d14..38c5df3bd9 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/re.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/re.byi @@ -299,7 +299,7 @@ final class Pattern(Generic[AnyStr]): def split(self: Pattern[bytes], string: ReadableBuffer, maxsplit: int = 0) -> list[bytes | None] def split(self, string: AnyStr, maxsplit: int = 0) -> list[AnyStr | None] - # return type depends on the number of groups in the pattern + # return type is either list[str/bytes] or list[tuple[str/bytes, ...]] def findall(self: Pattern[str], string: str, pos: int = 0, endpos: int = sys.maxsize) -> list[dynamic]: """Return a list of all non-overlapping matches of pattern in string.""" def findall(self: Pattern[bytes], string: ReadableBuffer, pos: int = 0, endpos: int = sys.maxsize) -> list[dynamic] @@ -439,6 +439,7 @@ def split( pattern: bytes | Pattern[bytes], string: ReadableBuffer, maxsplit: int = 0, flags: FlagsType = 0 ) -> list[bytes | None] +# return type is either list[str/bytes] or list[tuple[str/bytes, ...]] def findall(pattern: str | Pattern[str], string: str, flags: FlagsType = 0) -> list[dynamic]: """Return a list of all non-overlapping matches in the string. @@ -505,6 +506,6 @@ def purge(): """Clear the regular expression caches""" if sys.version_info < (3, 13): - @deprecated("Deprecated since Python 3.11; removed in Python 3.13. Use `re.compile()` instead.") + @deprecated("Deprecated; removed in Python 3.13. Use `re.compile()` instead.") def template(pattern: AnyStr | Pattern[AnyStr], flags: FlagsType = 0) -> Pattern[AnyStr]: # undocumented """Compile a template pattern, returning a Pattern object, deprecated""" diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/secrets.byi b/crates/ty_vendored/vendor/typeshed/stdlib/secrets.byi index b6632f7aba..ab73d7892f 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/secrets.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/secrets.byi @@ -7,8 +7,8 @@ https://peps.python.org/pep-0506/ """ from _typeshed import SupportsLenAndGetItem -from hmac import compare_digest as compare_digest -from random import SystemRandom as SystemRandom +from hmac export compare_digest +from random export SystemRandom from typing import Final, TypeVar __all__ = ["choice", "randbelow", "randbits", "SystemRandom", "token_bytes", "token_hex", "token_urlsafe", "compare_digest"] diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/shelve.byi b/crates/ty_vendored/vendor/typeshed/stdlib/shelve.byi index 7cfabaf035..e09b20a50a 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/shelve.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/shelve.byi @@ -108,8 +108,8 @@ class Shelf[in out Value](MutableMapping[str, Value]): def get[Element](self, key: str, default: Element) -> Value | Element override def __getitem__(self, key: str) -> Value - override def __setitem__(self, key: str, value: Value) -> None - override def __delitem__(self, key: str) -> None + override def __setitem__(self, key: str, value: Value) + override def __delitem__(self, key: str) override def __contains__(self, key: str) -> bool def __enter__(self) -> Self def __exit__( diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/shutil.byi b/crates/ty_vendored/vendor/typeshed/stdlib/shutil.byi index 26083835fc..cae7b15f40 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/shutil.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/shutil.byi @@ -10,7 +10,7 @@ from _typeshed import BytesPath, ExcInfo, FileDescriptorOrPath, MaybeNone, StrOr from collections.abc import Callable, Iterable, Sequence from tarfile import _TarfileFilter from typing import NamedTuple, Protocol, TypeAlias, TypeVar, type_check_only -from typing_extensions import deprecated +from typing_extensions import Never, deprecated __all__ = [ "copyfileobj", @@ -355,7 +355,7 @@ else: if sys.platform == "win32" and sys.version_info < (3, 12): @deprecated("On Windows before Python 3.12, using a PathLike as `cmd` would always fail or return `None`.") - def which(cmd: os.PathLike[str], mode: int = 1, path: StrPath | None = None) -> NoReturn: + def which(cmd: os.PathLike[str], mode: int = 1, path: StrPath | None = None) -> Never: """Given a command, mode, and a PATH string, return the path which conforms to the given mode on the PATH, or None if there is no such file. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/smtpd.byi b/crates/ty_vendored/vendor/typeshed/stdlib/smtpd.byi index d6546bdc1b..74e214a3ed 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/smtpd.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/smtpd.byi @@ -91,9 +91,9 @@ class SMTPChannel(asynchat.async_chat): decode_data: bool = False, ) # base asynchat.async_chat.push() accepts bytes - override def push(self, msg: str) -> None - override def collect_incoming_data(self, data: bytes) -> None - override def found_terminator(self) -> None + override def push(self, msg: str) + override def collect_incoming_data(self, data: bytes) + override def found_terminator(self) def smtp_HELO(self, arg: str) def smtp_NOOP(self, arg: str) def smtp_QUIT(self, arg: str) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/socket.byi b/crates/ty_vendored/vendor/typeshed/stdlib/socket.byi index cfe805c3a1..9c6fcc212f 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/socket.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/socket.byi @@ -50,135 +50,135 @@ the setsockopt() and getsockopt() methods. # prefers the definitions from _socket over those defined here. import _socket import sys -from _socket import ( - CAPI as CAPI, - EAI_AGAIN as EAI_AGAIN, - EAI_BADFLAGS as EAI_BADFLAGS, - EAI_FAIL as EAI_FAIL, - EAI_FAMILY as EAI_FAMILY, - EAI_MEMORY as EAI_MEMORY, - EAI_NODATA as EAI_NODATA, - EAI_NONAME as EAI_NONAME, - EAI_SERVICE as EAI_SERVICE, - EAI_SOCKTYPE as EAI_SOCKTYPE, - INADDR_ALLHOSTS_GROUP as INADDR_ALLHOSTS_GROUP, - INADDR_ANY as INADDR_ANY, - INADDR_BROADCAST as INADDR_BROADCAST, - INADDR_LOOPBACK as INADDR_LOOPBACK, - INADDR_MAX_LOCAL_GROUP as INADDR_MAX_LOCAL_GROUP, - INADDR_NONE as INADDR_NONE, - INADDR_UNSPEC_GROUP as INADDR_UNSPEC_GROUP, - IP_ADD_MEMBERSHIP as IP_ADD_MEMBERSHIP, - IP_DROP_MEMBERSHIP as IP_DROP_MEMBERSHIP, - IP_HDRINCL as IP_HDRINCL, - IP_MULTICAST_IF as IP_MULTICAST_IF, - IP_MULTICAST_LOOP as IP_MULTICAST_LOOP, - IP_MULTICAST_TTL as IP_MULTICAST_TTL, - IP_OPTIONS as IP_OPTIONS, - IP_RECVTOS as IP_RECVTOS, - IP_TOS as IP_TOS, - IP_TTL as IP_TTL, - IPPORT_RESERVED as IPPORT_RESERVED, - IPPORT_USERRESERVED as IPPORT_USERRESERVED, - IPPROTO_AH as IPPROTO_AH, - IPPROTO_DSTOPTS as IPPROTO_DSTOPTS, - IPPROTO_EGP as IPPROTO_EGP, - IPPROTO_ESP as IPPROTO_ESP, - IPPROTO_FRAGMENT as IPPROTO_FRAGMENT, - IPPROTO_HOPOPTS as IPPROTO_HOPOPTS, - IPPROTO_ICMP as IPPROTO_ICMP, - IPPROTO_ICMPV6 as IPPROTO_ICMPV6, - IPPROTO_IDP as IPPROTO_IDP, - IPPROTO_IGMP as IPPROTO_IGMP, - IPPROTO_IP as IPPROTO_IP, - IPPROTO_IPV6 as IPPROTO_IPV6, - IPPROTO_NONE as IPPROTO_NONE, - IPPROTO_PIM as IPPROTO_PIM, - IPPROTO_PUP as IPPROTO_PUP, - IPPROTO_RAW as IPPROTO_RAW, - IPPROTO_ROUTING as IPPROTO_ROUTING, - IPPROTO_SCTP as IPPROTO_SCTP, - IPPROTO_TCP as IPPROTO_TCP, - IPPROTO_UDP as IPPROTO_UDP, - IPV6_CHECKSUM as IPV6_CHECKSUM, - IPV6_DONTFRAG as IPV6_DONTFRAG, - IPV6_HOPLIMIT as IPV6_HOPLIMIT, - IPV6_HOPOPTS as IPV6_HOPOPTS, - IPV6_JOIN_GROUP as IPV6_JOIN_GROUP, - IPV6_LEAVE_GROUP as IPV6_LEAVE_GROUP, - IPV6_MULTICAST_HOPS as IPV6_MULTICAST_HOPS, - IPV6_MULTICAST_IF as IPV6_MULTICAST_IF, - IPV6_MULTICAST_LOOP as IPV6_MULTICAST_LOOP, - IPV6_PKTINFO as IPV6_PKTINFO, - IPV6_RECVRTHDR as IPV6_RECVRTHDR, - IPV6_RECVTCLASS as IPV6_RECVTCLASS, - IPV6_RTHDR as IPV6_RTHDR, - IPV6_TCLASS as IPV6_TCLASS, - IPV6_UNICAST_HOPS as IPV6_UNICAST_HOPS, - IPV6_V6ONLY as IPV6_V6ONLY, - NI_DGRAM as NI_DGRAM, - NI_MAXHOST as NI_MAXHOST, - NI_MAXSERV as NI_MAXSERV, - NI_NAMEREQD as NI_NAMEREQD, - NI_NOFQDN as NI_NOFQDN, - NI_NUMERICHOST as NI_NUMERICHOST, - NI_NUMERICSERV as NI_NUMERICSERV, - SHUT_RD as SHUT_RD, - SHUT_RDWR as SHUT_RDWR, - SHUT_WR as SHUT_WR, - SO_ACCEPTCONN as SO_ACCEPTCONN, - SO_BROADCAST as SO_BROADCAST, - SO_DEBUG as SO_DEBUG, - SO_DONTROUTE as SO_DONTROUTE, - SO_ERROR as SO_ERROR, - SO_KEEPALIVE as SO_KEEPALIVE, - SO_LINGER as SO_LINGER, - SO_OOBINLINE as SO_OOBINLINE, - SO_RCVBUF as SO_RCVBUF, - SO_RCVLOWAT as SO_RCVLOWAT, - SO_RCVTIMEO as SO_RCVTIMEO, - SO_REUSEADDR as SO_REUSEADDR, - SO_SNDBUF as SO_SNDBUF, - SO_SNDLOWAT as SO_SNDLOWAT, - SO_SNDTIMEO as SO_SNDTIMEO, - SO_TYPE as SO_TYPE, - SOL_IP as SOL_IP, - SOL_SOCKET as SOL_SOCKET, - SOL_TCP as SOL_TCP, - SOL_UDP as SOL_UDP, - SOMAXCONN as SOMAXCONN, - TCP_FASTOPEN as TCP_FASTOPEN, - TCP_KEEPCNT as TCP_KEEPCNT, - TCP_KEEPINTVL as TCP_KEEPINTVL, - TCP_MAXSEG as TCP_MAXSEG, - TCP_NODELAY as TCP_NODELAY, - SocketType as SocketType, - _Address as _Address, - _RetAddress as _RetAddress, - close as close, - dup as dup, - getdefaulttimeout as getdefaulttimeout, - gethostbyaddr as gethostbyaddr, - gethostbyname as gethostbyname, - gethostbyname_ex as gethostbyname_ex, - gethostname as gethostname, - getnameinfo as getnameinfo, - getprotobyname as getprotobyname, - getservbyname as getservbyname, - getservbyport as getservbyport, - has_ipv6 as has_ipv6, - htonl as htonl, - htons as htons, - if_indextoname as if_indextoname, - if_nameindex as if_nameindex, - if_nametoindex as if_nametoindex, - inet_aton as inet_aton, - inet_ntoa as inet_ntoa, - inet_ntop as inet_ntop, - inet_pton as inet_pton, - ntohl as ntohl, - ntohs as ntohs, - setdefaulttimeout as setdefaulttimeout, +from _socket export ( + CAPI, + EAI_AGAIN, + EAI_BADFLAGS, + EAI_FAIL, + EAI_FAMILY, + EAI_MEMORY, + EAI_NODATA, + EAI_NONAME, + EAI_SERVICE, + EAI_SOCKTYPE, + INADDR_ALLHOSTS_GROUP, + INADDR_ANY, + INADDR_BROADCAST, + INADDR_LOOPBACK, + INADDR_MAX_LOCAL_GROUP, + INADDR_NONE, + INADDR_UNSPEC_GROUP, + IP_ADD_MEMBERSHIP, + IP_DROP_MEMBERSHIP, + IP_HDRINCL, + IP_MULTICAST_IF, + IP_MULTICAST_LOOP, + IP_MULTICAST_TTL, + IP_OPTIONS, + IP_RECVTOS, + IP_TOS, + IP_TTL, + IPPORT_RESERVED, + IPPORT_USERRESERVED, + IPPROTO_AH, + IPPROTO_DSTOPTS, + IPPROTO_EGP, + IPPROTO_ESP, + IPPROTO_FRAGMENT, + IPPROTO_HOPOPTS, + IPPROTO_ICMP, + IPPROTO_ICMPV6, + IPPROTO_IDP, + IPPROTO_IGMP, + IPPROTO_IP, + IPPROTO_IPV6, + IPPROTO_NONE, + IPPROTO_PIM, + IPPROTO_PUP, + IPPROTO_RAW, + IPPROTO_ROUTING, + IPPROTO_SCTP, + IPPROTO_TCP, + IPPROTO_UDP, + IPV6_CHECKSUM, + IPV6_DONTFRAG, + IPV6_HOPLIMIT, + IPV6_HOPOPTS, + IPV6_JOIN_GROUP, + IPV6_LEAVE_GROUP, + IPV6_MULTICAST_HOPS, + IPV6_MULTICAST_IF, + IPV6_MULTICAST_LOOP, + IPV6_PKTINFO, + IPV6_RECVRTHDR, + IPV6_RECVTCLASS, + IPV6_RTHDR, + IPV6_TCLASS, + IPV6_UNICAST_HOPS, + IPV6_V6ONLY, + NI_DGRAM, + NI_MAXHOST, + NI_MAXSERV, + NI_NAMEREQD, + NI_NOFQDN, + NI_NUMERICHOST, + NI_NUMERICSERV, + SHUT_RD, + SHUT_RDWR, + SHUT_WR, + SO_ACCEPTCONN, + SO_BROADCAST, + SO_DEBUG, + SO_DONTROUTE, + SO_ERROR, + SO_KEEPALIVE, + SO_LINGER, + SO_OOBINLINE, + SO_RCVBUF, + SO_RCVLOWAT, + SO_RCVTIMEO, + SO_REUSEADDR, + SO_SNDBUF, + SO_SNDLOWAT, + SO_SNDTIMEO, + SO_TYPE, + SOL_IP, + SOL_SOCKET, + SOL_TCP, + SOL_UDP, + SOMAXCONN, + TCP_FASTOPEN, + TCP_KEEPCNT, + TCP_KEEPINTVL, + TCP_MAXSEG, + TCP_NODELAY, + SocketType, + _Address, + _RetAddress, + close, + dup, + getdefaulttimeout, + gethostbyaddr, + gethostbyname, + gethostbyname_ex, + gethostname, + getnameinfo, + getprotobyname, + getservbyname, + getservbyport, + has_ipv6, + htonl, + htons, + if_indextoname, + if_nameindex, + if_nametoindex, + inet_aton, + inet_ntoa, + inet_ntop, + inet_pton, + ntohl, + ntohs, + setdefaulttimeout, ) from _typeshed import ReadableBuffer, Unused, WriteableBuffer from collections.abc import Iterable @@ -356,22 +356,22 @@ __all__ = [ ] if sys.platform == "win32": - from _socket import ( - IPPROTO_CBT as IPPROTO_CBT, - IPPROTO_ICLFXBM as IPPROTO_ICLFXBM, - IPPROTO_IGP as IPPROTO_IGP, - IPPROTO_L2TP as IPPROTO_L2TP, - IPPROTO_PGM as IPPROTO_PGM, - IPPROTO_RDP as IPPROTO_RDP, - IPPROTO_ST as IPPROTO_ST, - RCVALL_MAX as RCVALL_MAX, - RCVALL_OFF as RCVALL_OFF, - RCVALL_ON as RCVALL_ON, - RCVALL_SOCKETLEVELONLY as RCVALL_SOCKETLEVELONLY, - SIO_KEEPALIVE_VALS as SIO_KEEPALIVE_VALS, - SIO_LOOPBACK_FAST_PATH as SIO_LOOPBACK_FAST_PATH, - SIO_RCVALL as SIO_RCVALL, - SO_EXCLUSIVEADDRUSE as SO_EXCLUSIVEADDRUSE, + from _socket export ( + IPPROTO_CBT, + IPPROTO_ICLFXBM, + IPPROTO_IGP, + IPPROTO_L2TP, + IPPROTO_PGM, + IPPROTO_RDP, + IPPROTO_ST, + RCVALL_MAX, + RCVALL_OFF, + RCVALL_ON, + RCVALL_SOCKETLEVELONLY, + SIO_KEEPALIVE_VALS, + SIO_LOOPBACK_FAST_PATH, + SIO_RCVALL, + SO_EXCLUSIVEADDRUSE, ) __all__ += [ @@ -397,43 +397,43 @@ if sys.platform == "win32": ] if sys.platform == "darwin": - from _socket import PF_SYSTEM as PF_SYSTEM, SYSPROTO_CONTROL as SYSPROTO_CONTROL + from _socket export PF_SYSTEM, SYSPROTO_CONTROL __all__ += ["PF_SYSTEM", "SYSPROTO_CONTROL", "AF_SYSTEM"] if sys.platform != "darwin": - from _socket import TCP_KEEPIDLE as TCP_KEEPIDLE + from _socket export TCP_KEEPIDLE __all__ += ["TCP_KEEPIDLE", "AF_IRDA", "MSG_ERRQUEUE"] if sys.platform != "win32" and sys.platform != "darwin": - from _socket import ( - IP_TRANSPARENT as IP_TRANSPARENT, - IPX_TYPE as IPX_TYPE, - SCM_CREDENTIALS as SCM_CREDENTIALS, - SO_DOMAIN as SO_DOMAIN, - SO_MARK as SO_MARK, - SO_PASSCRED as SO_PASSCRED, - SO_PASSSEC as SO_PASSSEC, - SO_PEERCRED as SO_PEERCRED, - SO_PEERSEC as SO_PEERSEC, - SO_PRIORITY as SO_PRIORITY, - SO_PROTOCOL as SO_PROTOCOL, - SOL_ATALK as SOL_ATALK, - SOL_AX25 as SOL_AX25, - SOL_HCI as SOL_HCI, - SOL_IPX as SOL_IPX, - SOL_NETROM as SOL_NETROM, - SOL_ROSE as SOL_ROSE, - TCP_CONGESTION as TCP_CONGESTION, - TCP_CORK as TCP_CORK, - TCP_DEFER_ACCEPT as TCP_DEFER_ACCEPT, - TCP_INFO as TCP_INFO, - TCP_LINGER2 as TCP_LINGER2, - TCP_QUICKACK as TCP_QUICKACK, - TCP_SYNCNT as TCP_SYNCNT, - TCP_USER_TIMEOUT as TCP_USER_TIMEOUT, - TCP_WINDOW_CLAMP as TCP_WINDOW_CLAMP, + from _socket export ( + IP_TRANSPARENT, + IPX_TYPE, + SCM_CREDENTIALS, + SO_DOMAIN, + SO_MARK, + SO_PASSCRED, + SO_PASSSEC, + SO_PEERCRED, + SO_PEERSEC, + SO_PRIORITY, + SO_PROTOCOL, + SOL_ATALK, + SOL_AX25, + SOL_HCI, + SOL_IPX, + SOL_NETROM, + SOL_ROSE, + TCP_CONGESTION, + TCP_CORK, + TCP_DEFER_ACCEPT, + TCP_INFO, + TCP_LINGER2, + TCP_QUICKACK, + TCP_SYNCNT, + TCP_USER_TIMEOUT, + TCP_WINDOW_CLAMP, ) __all__ += [ @@ -478,32 +478,32 @@ if sys.platform != "win32" and sys.platform != "darwin": ] if sys.platform != "win32" and sys.platform != "darwin" and sys.version_info >= (3, 11): - from _socket import IP_BIND_ADDRESS_NO_PORT as IP_BIND_ADDRESS_NO_PORT + from _socket export IP_BIND_ADDRESS_NO_PORT __all__ += ["IP_BIND_ADDRESS_NO_PORT"] if sys.platform != "win32": - from _socket import ( - CMSG_LEN as CMSG_LEN, - CMSG_SPACE as CMSG_SPACE, - EAI_ADDRFAMILY as EAI_ADDRFAMILY, - EAI_OVERFLOW as EAI_OVERFLOW, - EAI_SYSTEM as EAI_SYSTEM, - IP_DEFAULT_MULTICAST_LOOP as IP_DEFAULT_MULTICAST_LOOP, - IP_DEFAULT_MULTICAST_TTL as IP_DEFAULT_MULTICAST_TTL, - IP_MAX_MEMBERSHIPS as IP_MAX_MEMBERSHIPS, - IP_RECVOPTS as IP_RECVOPTS, - IP_RECVRETOPTS as IP_RECVRETOPTS, - IP_RETOPTS as IP_RETOPTS, - IPPROTO_GRE as IPPROTO_GRE, - IPPROTO_IPIP as IPPROTO_IPIP, - IPPROTO_RSVP as IPPROTO_RSVP, - IPPROTO_TP as IPPROTO_TP, - IPV6_RTHDR_TYPE_0 as IPV6_RTHDR_TYPE_0, - SCM_RIGHTS as SCM_RIGHTS, - SO_REUSEPORT as SO_REUSEPORT, - TCP_NOTSENT_LOWAT as TCP_NOTSENT_LOWAT, - sethostname as sethostname, + from _socket export ( + CMSG_LEN, + CMSG_SPACE, + EAI_ADDRFAMILY, + EAI_OVERFLOW, + EAI_SYSTEM, + IP_DEFAULT_MULTICAST_LOOP, + IP_DEFAULT_MULTICAST_TTL, + IP_MAX_MEMBERSHIPS, + IP_RECVOPTS, + IP_RECVRETOPTS, + IP_RETOPTS, + IPPROTO_GRE, + IPPROTO_IPIP, + IPPROTO_RSVP, + IPPROTO_TP, + IPV6_RTHDR_TYPE_0, + SCM_RIGHTS, + SO_REUSEPORT, + TCP_NOTSENT_LOWAT, + sethostname, ) __all__ += [ @@ -534,16 +534,16 @@ if sys.platform != "win32": "MSG_NOSIGNAL", ] - from _socket import ( - IPV6_DSTOPTS as IPV6_DSTOPTS, - IPV6_NEXTHOP as IPV6_NEXTHOP, - IPV6_PATHMTU as IPV6_PATHMTU, - IPV6_RECVDSTOPTS as IPV6_RECVDSTOPTS, - IPV6_RECVHOPLIMIT as IPV6_RECVHOPLIMIT, - IPV6_RECVHOPOPTS as IPV6_RECVHOPOPTS, - IPV6_RECVPATHMTU as IPV6_RECVPATHMTU, - IPV6_RECVPKTINFO as IPV6_RECVPKTINFO, - IPV6_RTHDRDSTOPTS as IPV6_RTHDRDSTOPTS, + from _socket export ( + IPV6_DSTOPTS, + IPV6_NEXTHOP, + IPV6_PATHMTU, + IPV6_RECVDSTOPTS, + IPV6_RECVHOPLIMIT, + IPV6_RECVHOPOPTS, + IPV6_RECVPATHMTU, + IPV6_RECVPKTINFO, + IPV6_RTHDRDSTOPTS, ) __all__ += [ @@ -559,146 +559,146 @@ if sys.platform != "win32": ] if sys.platform != "darwin" or sys.version_info >= (3, 13): - from _socket import SO_BINDTODEVICE as SO_BINDTODEVICE + from _socket export SO_BINDTODEVICE __all__ += ["SO_BINDTODEVICE"] if sys.platform != "darwin": - from _socket import BDADDR_ANY as BDADDR_ANY, BDADDR_LOCAL as BDADDR_LOCAL, BTPROTO_RFCOMM as BTPROTO_RFCOMM + from _socket export BDADDR_ANY, BDADDR_LOCAL, BTPROTO_RFCOMM if sys.platform != "darwin" and sys.platform != "linux": __all__ += ["BDADDR_ANY", "BDADDR_LOCAL", "BTPROTO_RFCOMM"] if sys.platform == "darwin": - from _socket import TCP_KEEPALIVE as TCP_KEEPALIVE + from _socket export TCP_KEEPALIVE __all__ += ["TCP_KEEPALIVE"] if sys.platform == "darwin" and sys.version_info >= (3, 11): - from _socket import TCP_CONNECTION_INFO as TCP_CONNECTION_INFO + from _socket export TCP_CONNECTION_INFO __all__ += ["TCP_CONNECTION_INFO"] if sys.platform == "linux": - from _socket import ( - ALG_OP_DECRYPT as ALG_OP_DECRYPT, - ALG_OP_ENCRYPT as ALG_OP_ENCRYPT, - ALG_OP_SIGN as ALG_OP_SIGN, - ALG_OP_VERIFY as ALG_OP_VERIFY, - ALG_SET_AEAD_ASSOCLEN as ALG_SET_AEAD_ASSOCLEN, - ALG_SET_AEAD_AUTHSIZE as ALG_SET_AEAD_AUTHSIZE, - ALG_SET_IV as ALG_SET_IV, - ALG_SET_KEY as ALG_SET_KEY, - ALG_SET_OP as ALG_SET_OP, - ALG_SET_PUBKEY as ALG_SET_PUBKEY, - CAN_BCM as CAN_BCM, - CAN_BCM_CAN_FD_FRAME as CAN_BCM_CAN_FD_FRAME, - CAN_BCM_RX_ANNOUNCE_RESUME as CAN_BCM_RX_ANNOUNCE_RESUME, - CAN_BCM_RX_CHANGED as CAN_BCM_RX_CHANGED, - CAN_BCM_RX_CHECK_DLC as CAN_BCM_RX_CHECK_DLC, - CAN_BCM_RX_DELETE as CAN_BCM_RX_DELETE, - CAN_BCM_RX_FILTER_ID as CAN_BCM_RX_FILTER_ID, - CAN_BCM_RX_NO_AUTOTIMER as CAN_BCM_RX_NO_AUTOTIMER, - CAN_BCM_RX_READ as CAN_BCM_RX_READ, - CAN_BCM_RX_RTR_FRAME as CAN_BCM_RX_RTR_FRAME, - CAN_BCM_RX_SETUP as CAN_BCM_RX_SETUP, - CAN_BCM_RX_STATUS as CAN_BCM_RX_STATUS, - CAN_BCM_RX_TIMEOUT as CAN_BCM_RX_TIMEOUT, - CAN_BCM_SETTIMER as CAN_BCM_SETTIMER, - CAN_BCM_STARTTIMER as CAN_BCM_STARTTIMER, - CAN_BCM_TX_ANNOUNCE as CAN_BCM_TX_ANNOUNCE, - CAN_BCM_TX_COUNTEVT as CAN_BCM_TX_COUNTEVT, - CAN_BCM_TX_CP_CAN_ID as CAN_BCM_TX_CP_CAN_ID, - CAN_BCM_TX_DELETE as CAN_BCM_TX_DELETE, - CAN_BCM_TX_EXPIRED as CAN_BCM_TX_EXPIRED, - CAN_BCM_TX_READ as CAN_BCM_TX_READ, - CAN_BCM_TX_RESET_MULTI_IDX as CAN_BCM_TX_RESET_MULTI_IDX, - CAN_BCM_TX_SEND as CAN_BCM_TX_SEND, - CAN_BCM_TX_SETUP as CAN_BCM_TX_SETUP, - CAN_BCM_TX_STATUS as CAN_BCM_TX_STATUS, - CAN_EFF_FLAG as CAN_EFF_FLAG, - CAN_EFF_MASK as CAN_EFF_MASK, - CAN_ERR_FLAG as CAN_ERR_FLAG, - CAN_ERR_MASK as CAN_ERR_MASK, - CAN_ISOTP as CAN_ISOTP, - CAN_RAW as CAN_RAW, - CAN_RAW_FD_FRAMES as CAN_RAW_FD_FRAMES, - CAN_RAW_FILTER as CAN_RAW_FILTER, - CAN_RAW_LOOPBACK as CAN_RAW_LOOPBACK, - CAN_RAW_RECV_OWN_MSGS as CAN_RAW_RECV_OWN_MSGS, - CAN_RTR_FLAG as CAN_RTR_FLAG, - CAN_SFF_MASK as CAN_SFF_MASK, - IOCTL_VM_SOCKETS_GET_LOCAL_CID as IOCTL_VM_SOCKETS_GET_LOCAL_CID, - NETLINK_CRYPTO as NETLINK_CRYPTO, - NETLINK_DNRTMSG as NETLINK_DNRTMSG, - NETLINK_FIREWALL as NETLINK_FIREWALL, - NETLINK_IP6_FW as NETLINK_IP6_FW, - NETLINK_NFLOG as NETLINK_NFLOG, - NETLINK_ROUTE as NETLINK_ROUTE, - NETLINK_USERSOCK as NETLINK_USERSOCK, - NETLINK_XFRM as NETLINK_XFRM, - PACKET_BROADCAST as PACKET_BROADCAST, - PACKET_FASTROUTE as PACKET_FASTROUTE, - PACKET_HOST as PACKET_HOST, - PACKET_LOOPBACK as PACKET_LOOPBACK, - PACKET_MULTICAST as PACKET_MULTICAST, - PACKET_OTHERHOST as PACKET_OTHERHOST, - PACKET_OUTGOING as PACKET_OUTGOING, - PF_CAN as PF_CAN, - PF_PACKET as PF_PACKET, - PF_RDS as PF_RDS, - RDS_CANCEL_SENT_TO as RDS_CANCEL_SENT_TO, - RDS_CMSG_RDMA_ARGS as RDS_CMSG_RDMA_ARGS, - RDS_CMSG_RDMA_DEST as RDS_CMSG_RDMA_DEST, - RDS_CMSG_RDMA_MAP as RDS_CMSG_RDMA_MAP, - RDS_CMSG_RDMA_STATUS as RDS_CMSG_RDMA_STATUS, - RDS_CONG_MONITOR as RDS_CONG_MONITOR, - RDS_FREE_MR as RDS_FREE_MR, - RDS_GET_MR as RDS_GET_MR, - RDS_GET_MR_FOR_DEST as RDS_GET_MR_FOR_DEST, - RDS_RDMA_DONTWAIT as RDS_RDMA_DONTWAIT, - RDS_RDMA_FENCE as RDS_RDMA_FENCE, - RDS_RDMA_INVALIDATE as RDS_RDMA_INVALIDATE, - RDS_RDMA_NOTIFY_ME as RDS_RDMA_NOTIFY_ME, - RDS_RDMA_READWRITE as RDS_RDMA_READWRITE, - RDS_RDMA_SILENT as RDS_RDMA_SILENT, - RDS_RDMA_USE_ONCE as RDS_RDMA_USE_ONCE, - RDS_RECVERR as RDS_RECVERR, - SO_VM_SOCKETS_BUFFER_MAX_SIZE as SO_VM_SOCKETS_BUFFER_MAX_SIZE, - SO_VM_SOCKETS_BUFFER_MIN_SIZE as SO_VM_SOCKETS_BUFFER_MIN_SIZE, - SO_VM_SOCKETS_BUFFER_SIZE as SO_VM_SOCKETS_BUFFER_SIZE, - SOL_ALG as SOL_ALG, - SOL_CAN_BASE as SOL_CAN_BASE, - SOL_CAN_RAW as SOL_CAN_RAW, - SOL_RDS as SOL_RDS, - SOL_TIPC as SOL_TIPC, - TIPC_ADDR_ID as TIPC_ADDR_ID, - TIPC_ADDR_NAME as TIPC_ADDR_NAME, - TIPC_ADDR_NAMESEQ as TIPC_ADDR_NAMESEQ, - TIPC_CFG_SRV as TIPC_CFG_SRV, - TIPC_CLUSTER_SCOPE as TIPC_CLUSTER_SCOPE, - TIPC_CONN_TIMEOUT as TIPC_CONN_TIMEOUT, - TIPC_CRITICAL_IMPORTANCE as TIPC_CRITICAL_IMPORTANCE, - TIPC_DEST_DROPPABLE as TIPC_DEST_DROPPABLE, - TIPC_HIGH_IMPORTANCE as TIPC_HIGH_IMPORTANCE, - TIPC_IMPORTANCE as TIPC_IMPORTANCE, - TIPC_LOW_IMPORTANCE as TIPC_LOW_IMPORTANCE, - TIPC_MEDIUM_IMPORTANCE as TIPC_MEDIUM_IMPORTANCE, - TIPC_NODE_SCOPE as TIPC_NODE_SCOPE, - TIPC_PUBLISHED as TIPC_PUBLISHED, - TIPC_SRC_DROPPABLE as TIPC_SRC_DROPPABLE, - TIPC_SUB_CANCEL as TIPC_SUB_CANCEL, - TIPC_SUB_PORTS as TIPC_SUB_PORTS, - TIPC_SUB_SERVICE as TIPC_SUB_SERVICE, - TIPC_SUBSCR_TIMEOUT as TIPC_SUBSCR_TIMEOUT, - TIPC_TOP_SRV as TIPC_TOP_SRV, - TIPC_WAIT_FOREVER as TIPC_WAIT_FOREVER, - TIPC_WITHDRAWN as TIPC_WITHDRAWN, - TIPC_ZONE_SCOPE as TIPC_ZONE_SCOPE, - VM_SOCKETS_INVALID_VERSION as VM_SOCKETS_INVALID_VERSION, - VMADDR_CID_ANY as VMADDR_CID_ANY, - VMADDR_CID_HOST as VMADDR_CID_HOST, - VMADDR_PORT_ANY as VMADDR_PORT_ANY, + from _socket export ( + ALG_OP_DECRYPT, + ALG_OP_ENCRYPT, + ALG_OP_SIGN, + ALG_OP_VERIFY, + ALG_SET_AEAD_ASSOCLEN, + ALG_SET_AEAD_AUTHSIZE, + ALG_SET_IV, + ALG_SET_KEY, + ALG_SET_OP, + ALG_SET_PUBKEY, + CAN_BCM, + CAN_BCM_CAN_FD_FRAME, + CAN_BCM_RX_ANNOUNCE_RESUME, + CAN_BCM_RX_CHANGED, + CAN_BCM_RX_CHECK_DLC, + CAN_BCM_RX_DELETE, + CAN_BCM_RX_FILTER_ID, + CAN_BCM_RX_NO_AUTOTIMER, + CAN_BCM_RX_READ, + CAN_BCM_RX_RTR_FRAME, + CAN_BCM_RX_SETUP, + CAN_BCM_RX_STATUS, + CAN_BCM_RX_TIMEOUT, + CAN_BCM_SETTIMER, + CAN_BCM_STARTTIMER, + CAN_BCM_TX_ANNOUNCE, + CAN_BCM_TX_COUNTEVT, + CAN_BCM_TX_CP_CAN_ID, + CAN_BCM_TX_DELETE, + CAN_BCM_TX_EXPIRED, + CAN_BCM_TX_READ, + CAN_BCM_TX_RESET_MULTI_IDX, + CAN_BCM_TX_SEND, + CAN_BCM_TX_SETUP, + CAN_BCM_TX_STATUS, + CAN_EFF_FLAG, + CAN_EFF_MASK, + CAN_ERR_FLAG, + CAN_ERR_MASK, + CAN_ISOTP, + CAN_RAW, + CAN_RAW_FD_FRAMES, + CAN_RAW_FILTER, + CAN_RAW_LOOPBACK, + CAN_RAW_RECV_OWN_MSGS, + CAN_RTR_FLAG, + CAN_SFF_MASK, + IOCTL_VM_SOCKETS_GET_LOCAL_CID, + NETLINK_CRYPTO, + NETLINK_DNRTMSG, + NETLINK_FIREWALL, + NETLINK_IP6_FW, + NETLINK_NFLOG, + NETLINK_ROUTE, + NETLINK_USERSOCK, + NETLINK_XFRM, + PACKET_BROADCAST, + PACKET_FASTROUTE, + PACKET_HOST, + PACKET_LOOPBACK, + PACKET_MULTICAST, + PACKET_OTHERHOST, + PACKET_OUTGOING, + PF_CAN, + PF_PACKET, + PF_RDS, + RDS_CANCEL_SENT_TO, + RDS_CMSG_RDMA_ARGS, + RDS_CMSG_RDMA_DEST, + RDS_CMSG_RDMA_MAP, + RDS_CMSG_RDMA_STATUS, + RDS_CONG_MONITOR, + RDS_FREE_MR, + RDS_GET_MR, + RDS_GET_MR_FOR_DEST, + RDS_RDMA_DONTWAIT, + RDS_RDMA_FENCE, + RDS_RDMA_INVALIDATE, + RDS_RDMA_NOTIFY_ME, + RDS_RDMA_READWRITE, + RDS_RDMA_SILENT, + RDS_RDMA_USE_ONCE, + RDS_RECVERR, + SO_VM_SOCKETS_BUFFER_MAX_SIZE, + SO_VM_SOCKETS_BUFFER_MIN_SIZE, + SO_VM_SOCKETS_BUFFER_SIZE, + SOL_ALG, + SOL_CAN_BASE, + SOL_CAN_RAW, + SOL_RDS, + SOL_TIPC, + TIPC_ADDR_ID, + TIPC_ADDR_NAME, + TIPC_ADDR_NAMESEQ, + TIPC_CFG_SRV, + TIPC_CLUSTER_SCOPE, + TIPC_CONN_TIMEOUT, + TIPC_CRITICAL_IMPORTANCE, + TIPC_DEST_DROPPABLE, + TIPC_HIGH_IMPORTANCE, + TIPC_IMPORTANCE, + TIPC_LOW_IMPORTANCE, + TIPC_MEDIUM_IMPORTANCE, + TIPC_NODE_SCOPE, + TIPC_PUBLISHED, + TIPC_SRC_DROPPABLE, + TIPC_SUB_CANCEL, + TIPC_SUB_PORTS, + TIPC_SUB_SERVICE, + TIPC_SUBSCR_TIMEOUT, + TIPC_TOP_SRV, + TIPC_WAIT_FOREVER, + TIPC_WITHDRAWN, + TIPC_ZONE_SCOPE, + VM_SOCKETS_INVALID_VERSION, + VMADDR_CID_ANY, + VMADDR_CID_HOST, + VMADDR_PORT_ANY, ) __all__ += [ @@ -816,43 +816,43 @@ if sys.platform == "linux": ] if sys.version_info < (3, 11): - from _socket import CAN_RAW_ERR_FILTER as CAN_RAW_ERR_FILTER + from _socket export 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 + from _socket export CAN_RAW_ERR_FILTER __all__ += ["CAN_RAW_ERR_FILTER"] if sys.version_info >= (3, 15): - from _socket import ( - CAN_ISOTP_CHK_PAD_DATA as CAN_ISOTP_CHK_PAD_DATA, - CAN_ISOTP_CHK_PAD_LEN as CAN_ISOTP_CHK_PAD_LEN, - CAN_ISOTP_DEFAULT_EXT_ADDRESS as CAN_ISOTP_DEFAULT_EXT_ADDRESS, - CAN_ISOTP_DEFAULT_FLAGS as CAN_ISOTP_DEFAULT_FLAGS, - CAN_ISOTP_DEFAULT_FRAME_TXTIME as CAN_ISOTP_DEFAULT_FRAME_TXTIME, - CAN_ISOTP_DEFAULT_LL_MTU as CAN_ISOTP_DEFAULT_LL_MTU, - CAN_ISOTP_DEFAULT_LL_TX_DL as CAN_ISOTP_DEFAULT_LL_TX_DL, - CAN_ISOTP_DEFAULT_LL_TX_FLAGS as CAN_ISOTP_DEFAULT_LL_TX_FLAGS, - CAN_ISOTP_DEFAULT_PAD_CONTENT as CAN_ISOTP_DEFAULT_PAD_CONTENT, - CAN_ISOTP_DEFAULT_RECV_BS as CAN_ISOTP_DEFAULT_RECV_BS, - CAN_ISOTP_DEFAULT_RECV_STMIN as CAN_ISOTP_DEFAULT_RECV_STMIN, - CAN_ISOTP_DEFAULT_RECV_WFTMAX as CAN_ISOTP_DEFAULT_RECV_WFTMAX, - CAN_ISOTP_EXTEND_ADDR as CAN_ISOTP_EXTEND_ADDR, - CAN_ISOTP_FORCE_RXSTMIN as CAN_ISOTP_FORCE_RXSTMIN, - CAN_ISOTP_FORCE_TXSTMIN as CAN_ISOTP_FORCE_TXSTMIN, - CAN_ISOTP_HALF_DUPLEX as CAN_ISOTP_HALF_DUPLEX, - CAN_ISOTP_LISTEN_MODE as CAN_ISOTP_LISTEN_MODE, - CAN_ISOTP_LL_OPTS as CAN_ISOTP_LL_OPTS, - CAN_ISOTP_OPTS as CAN_ISOTP_OPTS, - CAN_ISOTP_RECV_FC as CAN_ISOTP_RECV_FC, - CAN_ISOTP_RX_EXT_ADDR as CAN_ISOTP_RX_EXT_ADDR, - CAN_ISOTP_RX_PADDING as CAN_ISOTP_RX_PADDING, - CAN_ISOTP_RX_STMIN as CAN_ISOTP_RX_STMIN, - CAN_ISOTP_SF_BROADCAST as CAN_ISOTP_SF_BROADCAST, - CAN_ISOTP_TX_PADDING as CAN_ISOTP_TX_PADDING, - CAN_ISOTP_TX_STMIN as CAN_ISOTP_TX_STMIN, - CAN_ISOTP_WAIT_TX_DONE as CAN_ISOTP_WAIT_TX_DONE, - SOL_CAN_ISOTP as SOL_CAN_ISOTP, + from _socket export ( + CAN_ISOTP_CHK_PAD_DATA, + CAN_ISOTP_CHK_PAD_LEN, + CAN_ISOTP_DEFAULT_EXT_ADDRESS, + CAN_ISOTP_DEFAULT_FLAGS, + CAN_ISOTP_DEFAULT_FRAME_TXTIME, + CAN_ISOTP_DEFAULT_LL_MTU, + CAN_ISOTP_DEFAULT_LL_TX_DL, + CAN_ISOTP_DEFAULT_LL_TX_FLAGS, + CAN_ISOTP_DEFAULT_PAD_CONTENT, + CAN_ISOTP_DEFAULT_RECV_BS, + CAN_ISOTP_DEFAULT_RECV_STMIN, + CAN_ISOTP_DEFAULT_RECV_WFTMAX, + CAN_ISOTP_EXTEND_ADDR, + CAN_ISOTP_FORCE_RXSTMIN, + CAN_ISOTP_FORCE_TXSTMIN, + CAN_ISOTP_HALF_DUPLEX, + CAN_ISOTP_LISTEN_MODE, + CAN_ISOTP_LL_OPTS, + CAN_ISOTP_OPTS, + CAN_ISOTP_RECV_FC, + CAN_ISOTP_RX_EXT_ADDR, + CAN_ISOTP_RX_PADDING, + CAN_ISOTP_RX_STMIN, + CAN_ISOTP_SF_BROADCAST, + CAN_ISOTP_TX_PADDING, + CAN_ISOTP_TX_STMIN, + CAN_ISOTP_WAIT_TX_DONE, + SOL_CAN_ISOTP, ) __all__ += [ @@ -887,35 +887,35 @@ if sys.platform == "linux": ] if sys.platform == "linux": - from _socket import ( - CAN_J1939 as CAN_J1939, - CAN_RAW_JOIN_FILTERS as CAN_RAW_JOIN_FILTERS, - IPPROTO_UDPLITE as IPPROTO_UDPLITE, - J1939_EE_INFO_NONE as J1939_EE_INFO_NONE, - J1939_EE_INFO_TX_ABORT as J1939_EE_INFO_TX_ABORT, - J1939_FILTER_MAX as J1939_FILTER_MAX, - J1939_IDLE_ADDR as J1939_IDLE_ADDR, - J1939_MAX_UNICAST_ADDR as J1939_MAX_UNICAST_ADDR, - J1939_NLA_BYTES_ACKED as J1939_NLA_BYTES_ACKED, - J1939_NLA_PAD as J1939_NLA_PAD, - J1939_NO_ADDR as J1939_NO_ADDR, - J1939_NO_NAME as J1939_NO_NAME, - J1939_NO_PGN as J1939_NO_PGN, - J1939_PGN_ADDRESS_CLAIMED as J1939_PGN_ADDRESS_CLAIMED, - J1939_PGN_ADDRESS_COMMANDED as J1939_PGN_ADDRESS_COMMANDED, - J1939_PGN_MAX as J1939_PGN_MAX, - J1939_PGN_PDU1_MAX as J1939_PGN_PDU1_MAX, - J1939_PGN_REQUEST as J1939_PGN_REQUEST, - SCM_J1939_DEST_ADDR as SCM_J1939_DEST_ADDR, - SCM_J1939_DEST_NAME as SCM_J1939_DEST_NAME, - SCM_J1939_ERRQUEUE as SCM_J1939_ERRQUEUE, - SCM_J1939_PRIO as SCM_J1939_PRIO, - SO_J1939_ERRQUEUE as SO_J1939_ERRQUEUE, - SO_J1939_FILTER as SO_J1939_FILTER, - SO_J1939_PROMISC as SO_J1939_PROMISC, - SO_J1939_SEND_PRIO as SO_J1939_SEND_PRIO, - UDPLITE_RECV_CSCOV as UDPLITE_RECV_CSCOV, - UDPLITE_SEND_CSCOV as UDPLITE_SEND_CSCOV, + from _socket export ( + CAN_J1939, + CAN_RAW_JOIN_FILTERS, + IPPROTO_UDPLITE, + J1939_EE_INFO_NONE, + J1939_EE_INFO_TX_ABORT, + J1939_FILTER_MAX, + J1939_IDLE_ADDR, + J1939_MAX_UNICAST_ADDR, + J1939_NLA_BYTES_ACKED, + J1939_NLA_PAD, + J1939_NO_ADDR, + J1939_NO_NAME, + J1939_NO_PGN, + J1939_PGN_ADDRESS_CLAIMED, + J1939_PGN_ADDRESS_COMMANDED, + J1939_PGN_MAX, + J1939_PGN_PDU1_MAX, + J1939_PGN_REQUEST, + SCM_J1939_DEST_ADDR, + SCM_J1939_DEST_NAME, + SCM_J1939_ERRQUEUE, + SCM_J1939_PRIO, + SO_J1939_ERRQUEUE, + SO_J1939_FILTER, + SO_J1939_PROMISC, + SO_J1939_SEND_PRIO, + UDPLITE_RECV_CSCOV, + UDPLITE_SEND_CSCOV, ) __all__ += [ @@ -949,35 +949,35 @@ if sys.platform == "linux": "UDPLITE_SEND_CSCOV", ] if sys.platform == "linux": - from _socket import IPPROTO_MPTCP as IPPROTO_MPTCP + from _socket export IPPROTO_MPTCP __all__ += ["IPPROTO_MPTCP"] if sys.platform == "linux" and sys.version_info >= (3, 11): - from _socket import SO_INCOMING_CPU as SO_INCOMING_CPU + from _socket export SO_INCOMING_CPU __all__ += ["SO_INCOMING_CPU"] if sys.platform == "linux" and sys.version_info >= (3, 12): - from _socket import ( - TCP_CC_INFO as TCP_CC_INFO, - TCP_FASTOPEN_CONNECT as TCP_FASTOPEN_CONNECT, - TCP_FASTOPEN_KEY as TCP_FASTOPEN_KEY, - TCP_FASTOPEN_NO_COOKIE as TCP_FASTOPEN_NO_COOKIE, - TCP_INQ as TCP_INQ, - TCP_MD5SIG as TCP_MD5SIG, - TCP_MD5SIG_EXT as TCP_MD5SIG_EXT, - TCP_QUEUE_SEQ as TCP_QUEUE_SEQ, - TCP_REPAIR as TCP_REPAIR, - TCP_REPAIR_OPTIONS as TCP_REPAIR_OPTIONS, - TCP_REPAIR_QUEUE as TCP_REPAIR_QUEUE, - TCP_REPAIR_WINDOW as TCP_REPAIR_WINDOW, - TCP_SAVE_SYN as TCP_SAVE_SYN, - TCP_SAVED_SYN as TCP_SAVED_SYN, - TCP_THIN_DUPACK as TCP_THIN_DUPACK, - TCP_THIN_LINEAR_TIMEOUTS as TCP_THIN_LINEAR_TIMEOUTS, - TCP_TIMESTAMP as TCP_TIMESTAMP, - TCP_TX_DELAY as TCP_TX_DELAY, - TCP_ULP as TCP_ULP, - TCP_ZEROCOPY_RECEIVE as TCP_ZEROCOPY_RECEIVE, + from _socket export ( + TCP_CC_INFO, + TCP_FASTOPEN_CONNECT, + TCP_FASTOPEN_KEY, + TCP_FASTOPEN_NO_COOKIE, + TCP_INQ, + TCP_MD5SIG, + TCP_MD5SIG_EXT, + TCP_QUEUE_SEQ, + TCP_REPAIR, + TCP_REPAIR_OPTIONS, + TCP_REPAIR_QUEUE, + TCP_REPAIR_WINDOW, + TCP_SAVE_SYN, + TCP_SAVED_SYN, + TCP_THIN_DUPACK, + TCP_THIN_LINEAR_TIMEOUTS, + TCP_TIMESTAMP, + TCP_TX_DELAY, + TCP_ULP, + TCP_ZEROCOPY_RECEIVE, ) __all__ += [ @@ -1004,34 +1004,34 @@ if sys.platform == "linux" and sys.version_info >= (3, 12): ] if sys.platform == "linux" and sys.version_info >= (3, 13): - from _socket import NI_IDN as NI_IDN, SO_BINDTOIFINDEX as SO_BINDTOIFINDEX + from _socket export NI_IDN, SO_BINDTOIFINDEX __all__ += ["NI_IDN", "SO_BINDTOIFINDEX"] if sys.version_info >= (3, 12): - from _socket import ( - IP_ADD_SOURCE_MEMBERSHIP as IP_ADD_SOURCE_MEMBERSHIP, - IP_BLOCK_SOURCE as IP_BLOCK_SOURCE, - IP_DROP_SOURCE_MEMBERSHIP as IP_DROP_SOURCE_MEMBERSHIP, - IP_PKTINFO as IP_PKTINFO, - IP_UNBLOCK_SOURCE as IP_UNBLOCK_SOURCE, + from _socket export ( + IP_ADD_SOURCE_MEMBERSHIP, + IP_BLOCK_SOURCE, + IP_DROP_SOURCE_MEMBERSHIP, + IP_PKTINFO, + IP_UNBLOCK_SOURCE, ) __all__ += ["IP_ADD_SOURCE_MEMBERSHIP", "IP_BLOCK_SOURCE", "IP_DROP_SOURCE_MEMBERSHIP", "IP_PKTINFO", "IP_UNBLOCK_SOURCE"] if sys.platform == "win32": - from _socket import ( - HV_GUID_BROADCAST as HV_GUID_BROADCAST, - HV_GUID_CHILDREN as HV_GUID_CHILDREN, - HV_GUID_LOOPBACK as HV_GUID_LOOPBACK, - HV_GUID_PARENT as HV_GUID_PARENT, - HV_GUID_WILDCARD as HV_GUID_WILDCARD, - HV_GUID_ZERO as HV_GUID_ZERO, - HV_PROTOCOL_RAW as HV_PROTOCOL_RAW, - HVSOCKET_ADDRESS_FLAG_PASSTHRU as HVSOCKET_ADDRESS_FLAG_PASSTHRU, - HVSOCKET_CONNECT_TIMEOUT as HVSOCKET_CONNECT_TIMEOUT, - HVSOCKET_CONNECT_TIMEOUT_MAX as HVSOCKET_CONNECT_TIMEOUT_MAX, - HVSOCKET_CONNECTED_SUSPEND as HVSOCKET_CONNECTED_SUSPEND, + from _socket export ( + HV_GUID_BROADCAST, + HV_GUID_CHILDREN, + HV_GUID_LOOPBACK, + HV_GUID_PARENT, + HV_GUID_WILDCARD, + HV_GUID_ZERO, + HV_PROTOCOL_RAW, + HVSOCKET_ADDRESS_FLAG_PASSTHRU, + HVSOCKET_CONNECT_TIMEOUT, + HVSOCKET_CONNECT_TIMEOUT_MAX, + HVSOCKET_CONNECTED_SUSPEND, ) __all__ += [ @@ -1048,23 +1048,23 @@ if sys.version_info >= (3, 12): "HVSOCKET_CONNECTED_SUSPEND", ] else: - from _socket import ( - ETHERTYPE_ARP as ETHERTYPE_ARP, - ETHERTYPE_IP as ETHERTYPE_IP, - ETHERTYPE_IPV6 as ETHERTYPE_IPV6, - ETHERTYPE_VLAN as ETHERTYPE_VLAN, + from _socket export ( + ETHERTYPE_ARP, + ETHERTYPE_IP, + ETHERTYPE_IPV6, + ETHERTYPE_VLAN, ) __all__ += ["ETHERTYPE_ARP", "ETHERTYPE_IP", "ETHERTYPE_IPV6", "ETHERTYPE_VLAN"] if sys.platform == "linux": - from _socket import ETH_P_ALL as ETH_P_ALL + from _socket export ETH_P_ALL __all__ += ["ETH_P_ALL"] if sys.platform != "linux" and sys.platform != "win32" and sys.platform != "darwin": # FreeBSD >= 14.0 - from _socket import PF_DIVERT as PF_DIVERT + from _socket export PF_DIVERT __all__ += ["PF_DIVERT", "AF_DIVERT"] @@ -1077,18 +1077,18 @@ if sys.platform != "darwin" and sys.platform != "linux": __all__ += ["AF_BLUETOOTH"] if sys.platform != "win32" and sys.platform != "darwin": - from _socket import BTPROTO_HCI as BTPROTO_HCI, BTPROTO_L2CAP as BTPROTO_L2CAP, BTPROTO_SCO as BTPROTO_SCO + from _socket export BTPROTO_HCI, BTPROTO_L2CAP, BTPROTO_SCO if sys.platform != "win32" and sys.platform != "darwin" and sys.platform != "linux": __all__ += ["BTPROTO_HCI", "BTPROTO_L2CAP", "BTPROTO_SCO"] if sys.platform != "win32" and sys.platform != "darwin" and sys.platform != "linux": - from _socket import HCI_DATA_DIR as HCI_DATA_DIR, HCI_FILTER as HCI_FILTER, HCI_TIME_STAMP as HCI_TIME_STAMP + from _socket export HCI_DATA_DIR, HCI_FILTER, HCI_TIME_STAMP __all__ += ["HCI_FILTER", "HCI_TIME_STAMP", "HCI_DATA_DIR"] if sys.version_info >= (3, 11) and sys.platform != "linux" and sys.platform != "win32" and sys.platform != "darwin": - from _socket import LOCAL_CREDS as LOCAL_CREDS, LOCAL_CREDS_PERSISTENT as LOCAL_CREDS_PERSISTENT, SCM_CREDS2 as SCM_CREDS2 + from _socket export LOCAL_CREDS, LOCAL_CREDS_PERSISTENT, SCM_CREDS2 __all__ += ["SCM_CREDS2", "LOCAL_CREDS", "LOCAL_CREDS_PERSISTENT"] @@ -1096,17 +1096,17 @@ if sys.platform == "win32" and sys.version_info >= (3, 12): __all__ += ["AF_HYPERV"] if sys.platform != "win32" and sys.platform != "linux": - from _socket import ( - EAI_BADHINTS as EAI_BADHINTS, - EAI_MAX as EAI_MAX, - EAI_PROTOCOL as EAI_PROTOCOL, - IPPROTO_EON as IPPROTO_EON, - IPPROTO_HELLO as IPPROTO_HELLO, - IPPROTO_IPCOMP as IPPROTO_IPCOMP, - IPPROTO_XTP as IPPROTO_XTP, - IPV6_USE_MIN_MTU as IPV6_USE_MIN_MTU, - LOCAL_PEERCRED as LOCAL_PEERCRED, - SCM_CREDS as SCM_CREDS, + from _socket export ( + EAI_BADHINTS, + EAI_MAX, + EAI_PROTOCOL, + IPPROTO_EON, + IPPROTO_HELLO, + IPPROTO_IPCOMP, + IPPROTO_XTP, + IPV6_USE_MIN_MTU, + LOCAL_PEERCRED, + SCM_CREDS, ) __all__ += [ @@ -1127,129 +1127,129 @@ if sys.platform != "win32" and sys.platform != "linux": ] if sys.platform != "win32" and sys.platform != "darwin" and sys.platform != "linux": - from _socket import ( - IPPROTO_BIP as IPPROTO_BIP, - IPPROTO_MOBILE as IPPROTO_MOBILE, - IPPROTO_VRRP as IPPROTO_VRRP, - MSG_BTAG as MSG_BTAG, - MSG_ETAG as MSG_ETAG, - SO_SETFIB as SO_SETFIB, + from _socket export ( + IPPROTO_BIP, + IPPROTO_MOBILE, + IPPROTO_VRRP, + MSG_BTAG, + MSG_ETAG, + SO_SETFIB, ) __all__ += ["SO_SETFIB", "MSG_BTAG", "MSG_ETAG", "IPPROTO_BIP", "IPPROTO_MOBILE", "IPPROTO_VRRP", "MSG_NOTIFICATION"] if sys.platform != "linux": - from _socket import ( - IP_RECVDSTADDR as IP_RECVDSTADDR, - IPPROTO_GGP as IPPROTO_GGP, - IPPROTO_IPV4 as IPPROTO_IPV4, - IPPROTO_MAX as IPPROTO_MAX, - IPPROTO_ND as IPPROTO_ND, - SO_USELOOPBACK as SO_USELOOPBACK, + from _socket export ( + IP_RECVDSTADDR, + IPPROTO_GGP, + IPPROTO_IPV4, + IPPROTO_MAX, + IPPROTO_ND, + SO_USELOOPBACK, ) __all__ += ["IPPROTO_GGP", "IPPROTO_IPV4", "IPPROTO_MAX", "IPPROTO_ND", "IP_RECVDSTADDR", "SO_USELOOPBACK"] if sys.version_info >= (3, 15): if sys.platform == "win32" or sys.platform == "linux": - from _socket import IPV6_HDRINCL as IPV6_HDRINCL + from _socket export IPV6_HDRINCL __all__ += ["IPV6_HDRINCL"] if sys.version_info >= (3, 14): - from _socket import IP_RECVTTL as IP_RECVTTL + from _socket export 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 + from _socket export IP_RECVERR, IPV6_RECVERR, 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, + from _socket export ( + SO_BTH_ENCRYPT, + SO_BTH_MTU, + SO_BTH_MTU_MAX, + SO_BTH_MTU_MIN, + SOL_RFCOMM, + 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 ( - BDADDR_BREDR as BDADDR_BREDR, - BDADDR_LE_PUBLIC as BDADDR_LE_PUBLIC, - BDADDR_LE_RANDOM as BDADDR_LE_RANDOM, - BT_CHANNEL_POLICY as BT_CHANNEL_POLICY, - BT_CHANNEL_POLICY_BREDR_ONLY as BT_CHANNEL_POLICY_BREDR_ONLY, - BT_CHANNEL_POLICY_BREDR_PREFERRED as BT_CHANNEL_POLICY_BREDR_PREFERRED, - BT_CODEC as BT_CODEC, - BT_DEFER_SETUP as BT_DEFER_SETUP, - BT_FLUSHABLE as BT_FLUSHABLE, - BT_FLUSHABLE_OFF as BT_FLUSHABLE_OFF, - BT_FLUSHABLE_ON as BT_FLUSHABLE_ON, - BT_ISO_QOS as BT_ISO_QOS, - BT_MODE as BT_MODE, - BT_MODE_BASIC as BT_MODE_BASIC, - BT_MODE_ERTM as BT_MODE_ERTM, - BT_MODE_EXT_FLOWCTL as BT_MODE_EXT_FLOWCTL, - BT_MODE_LE_FLOWCTL as BT_MODE_LE_FLOWCTL, - BT_MODE_STREAMING as BT_MODE_STREAMING, - BT_PHY as BT_PHY, - BT_PHY_BR_1M_1SLOT as BT_PHY_BR_1M_1SLOT, - BT_PHY_BR_1M_3SLOT as BT_PHY_BR_1M_3SLOT, - BT_PHY_BR_1M_5SLOT as BT_PHY_BR_1M_5SLOT, - BT_PHY_EDR_2M_1SLOT as BT_PHY_EDR_2M_1SLOT, - BT_PHY_EDR_2M_3SLOT as BT_PHY_EDR_2M_3SLOT, - BT_PHY_EDR_2M_5SLOT as BT_PHY_EDR_2M_5SLOT, - BT_PHY_EDR_3M_1SLOT as BT_PHY_EDR_3M_1SLOT, - BT_PHY_EDR_3M_3SLOT as BT_PHY_EDR_3M_3SLOT, - BT_PHY_EDR_3M_5SLOT as BT_PHY_EDR_3M_5SLOT, - BT_PHY_LE_1M_RX as BT_PHY_LE_1M_RX, - BT_PHY_LE_1M_TX as BT_PHY_LE_1M_TX, - BT_PHY_LE_2M_RX as BT_PHY_LE_2M_RX, - BT_PHY_LE_2M_TX as BT_PHY_LE_2M_TX, - BT_PHY_LE_CODED_RX as BT_PHY_LE_CODED_RX, - BT_PHY_LE_CODED_TX as BT_PHY_LE_CODED_TX, - BT_PKT_STATUS as BT_PKT_STATUS, - BT_POWER as BT_POWER, - BT_POWER_FORCE_ACTIVE_OFF as BT_POWER_FORCE_ACTIVE_OFF, - BT_POWER_FORCE_ACTIVE_ON as BT_POWER_FORCE_ACTIVE_ON, - BT_RCVMTU as BT_RCVMTU, - BT_SECURITY as BT_SECURITY, - BT_SECURITY_FIPS as BT_SECURITY_FIPS, - BT_SECURITY_HIGH as BT_SECURITY_HIGH, - BT_SECURITY_LOW as BT_SECURITY_LOW, - BT_SECURITY_MEDIUM as BT_SECURITY_MEDIUM, - BT_SECURITY_SDP as BT_SECURITY_SDP, - BT_SNDMTU as BT_SNDMTU, - BT_VOICE as BT_VOICE, - BT_VOICE_CVSD_16BIT as BT_VOICE_CVSD_16BIT, - BT_VOICE_TRANSPARENT as BT_VOICE_TRANSPARENT, - BT_VOICE_TRANSPARENT_16BIT as BT_VOICE_TRANSPARENT_16BIT, - HCI_CHANNEL_CONTROL as HCI_CHANNEL_CONTROL, - HCI_CHANNEL_LOGGING as HCI_CHANNEL_LOGGING, - HCI_CHANNEL_MONITOR as HCI_CHANNEL_MONITOR, - HCI_CHANNEL_RAW as HCI_CHANNEL_RAW, - HCI_CHANNEL_USER as HCI_CHANNEL_USER, - HCI_DEV_NONE as HCI_DEV_NONE, - IP_FREEBIND as IP_FREEBIND, - IP_RECVORIGDSTADDR as IP_RECVORIGDSTADDR, - L2CAP_LM as L2CAP_LM, - L2CAP_LM_AUTH as L2CAP_LM_AUTH, - L2CAP_LM_ENCRYPT as L2CAP_LM_ENCRYPT, - L2CAP_LM_MASTER as L2CAP_LM_MASTER, - L2CAP_LM_RELIABLE as L2CAP_LM_RELIABLE, - L2CAP_LM_SECURE as L2CAP_LM_SECURE, - L2CAP_LM_TRUSTED as L2CAP_LM_TRUSTED, - SOL_BLUETOOTH as SOL_BLUETOOTH, - SOL_L2CAP as SOL_L2CAP, - SOL_RFCOMM as SOL_RFCOMM, - SOL_SCO as SOL_SCO, - VMADDR_CID_LOCAL as VMADDR_CID_LOCAL, + from _socket export ( + BDADDR_BREDR, + BDADDR_LE_PUBLIC, + BDADDR_LE_RANDOM, + BT_CHANNEL_POLICY, + BT_CHANNEL_POLICY_BREDR_ONLY, + BT_CHANNEL_POLICY_BREDR_PREFERRED, + BT_CODEC, + BT_DEFER_SETUP, + BT_FLUSHABLE, + BT_FLUSHABLE_OFF, + BT_FLUSHABLE_ON, + BT_ISO_QOS, + BT_MODE, + BT_MODE_BASIC, + BT_MODE_ERTM, + BT_MODE_EXT_FLOWCTL, + BT_MODE_LE_FLOWCTL, + BT_MODE_STREAMING, + BT_PHY, + BT_PHY_BR_1M_1SLOT, + BT_PHY_BR_1M_3SLOT, + BT_PHY_BR_1M_5SLOT, + BT_PHY_EDR_2M_1SLOT, + BT_PHY_EDR_2M_3SLOT, + BT_PHY_EDR_2M_5SLOT, + BT_PHY_EDR_3M_1SLOT, + BT_PHY_EDR_3M_3SLOT, + BT_PHY_EDR_3M_5SLOT, + BT_PHY_LE_1M_RX, + BT_PHY_LE_1M_TX, + BT_PHY_LE_2M_RX, + BT_PHY_LE_2M_TX, + BT_PHY_LE_CODED_RX, + BT_PHY_LE_CODED_TX, + BT_PKT_STATUS, + BT_POWER, + BT_POWER_FORCE_ACTIVE_OFF, + BT_POWER_FORCE_ACTIVE_ON, + BT_RCVMTU, + BT_SECURITY, + BT_SECURITY_FIPS, + BT_SECURITY_HIGH, + BT_SECURITY_LOW, + BT_SECURITY_MEDIUM, + BT_SECURITY_SDP, + BT_SNDMTU, + BT_VOICE, + BT_VOICE_CVSD_16BIT, + BT_VOICE_TRANSPARENT, + BT_VOICE_TRANSPARENT_16BIT, + HCI_CHANNEL_CONTROL, + HCI_CHANNEL_LOGGING, + HCI_CHANNEL_MONITOR, + HCI_CHANNEL_RAW, + HCI_CHANNEL_USER, + HCI_DEV_NONE, + IP_FREEBIND, + IP_RECVORIGDSTADDR, + L2CAP_LM, + L2CAP_LM_AUTH, + L2CAP_LM_ENCRYPT, + L2CAP_LM_MASTER, + L2CAP_LM_RELIABLE, + L2CAP_LM_SECURE, + L2CAP_LM_TRUSTED, + SOL_BLUETOOTH, + SOL_L2CAP, + SOL_RFCOMM, + SOL_SCO, + VMADDR_CID_LOCAL, ) __all__ += ["IP_FREEBIND", "IP_RECVORIGDSTADDR", "VMADDR_CID_LOCAL"] diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/sqlite3/__init__.byi b/crates/ty_vendored/vendor/typeshed/stdlib/sqlite3/__init__.byi index 549e30ab52..51faed1cf0 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/sqlite3/__init__.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/sqlite3/__init__.byi @@ -35,218 +35,218 @@ The sqlite3 module is written by Gerhard Häring . 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 sqlite3.dbapi2 export ( + PARSE_COLNAMES, + PARSE_DECLTYPES, + SQLITE_ALTER_TABLE, + SQLITE_ANALYZE, + SQLITE_ATTACH, + SQLITE_CREATE_INDEX, + SQLITE_CREATE_TABLE, + SQLITE_CREATE_TEMP_INDEX, + SQLITE_CREATE_TEMP_TABLE, + SQLITE_CREATE_TEMP_TRIGGER, + SQLITE_CREATE_TEMP_VIEW, + SQLITE_CREATE_TRIGGER, + SQLITE_CREATE_VIEW, + SQLITE_CREATE_VTABLE, + SQLITE_DELETE, + SQLITE_DENY, + SQLITE_DETACH, + SQLITE_DONE, + SQLITE_DROP_INDEX, + SQLITE_DROP_TABLE, + SQLITE_DROP_TEMP_INDEX, + SQLITE_DROP_TEMP_TABLE, + SQLITE_DROP_TEMP_TRIGGER, + SQLITE_DROP_TEMP_VIEW, + SQLITE_DROP_TRIGGER, + SQLITE_DROP_VIEW, + SQLITE_DROP_VTABLE, + SQLITE_FUNCTION, + SQLITE_IGNORE, + SQLITE_INSERT, + SQLITE_OK, + SQLITE_PRAGMA, + SQLITE_READ, + SQLITE_RECURSIVE, + SQLITE_REINDEX, + SQLITE_SAVEPOINT, + SQLITE_SELECT, + SQLITE_TRANSACTION, + SQLITE_UPDATE, + Binary, + Date, + DateFromTicks, + Time, + TimeFromTicks, + TimestampFromTicks, + adapt, + adapters, + apilevel, + complete_statement, + connect, + converters, + enable_callback_tracebacks, + paramstyle, + register_adapter, + register_converter, + sqlite_version, + sqlite_version_info, + threadsafety, ) from types import TracebackType from typing import Literal, Protocol, TypeAlias, TypeVar, final, type_check_only from typing_extensions import Self, disjoint_base if sys.version_info < (3, 14): - from sqlite3.dbapi2 import version_info as version_info + from sqlite3.dbapi2 export version_info if sys.version_info >= (3, 15): - from sqlite3.dbapi2 import SQLITE_KEYWORDS as SQLITE_KEYWORDS + from sqlite3.dbapi2 export SQLITE_KEYWORDS 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, + from sqlite3.dbapi2 export ( + LEGACY_TRANSACTION_CONTROL, + SQLITE_DBCONFIG_DEFENSIVE, + SQLITE_DBCONFIG_DQS_DDL, + SQLITE_DBCONFIG_DQS_DML, + SQLITE_DBCONFIG_ENABLE_FKEY, + SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER, + SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, + SQLITE_DBCONFIG_ENABLE_QPSG, + SQLITE_DBCONFIG_ENABLE_TRIGGER, + SQLITE_DBCONFIG_ENABLE_VIEW, + SQLITE_DBCONFIG_LEGACY_ALTER_TABLE, + SQLITE_DBCONFIG_LEGACY_FILE_FORMAT, + SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE, + SQLITE_DBCONFIG_RESET_DATABASE, + SQLITE_DBCONFIG_TRIGGER_EQP, + SQLITE_DBCONFIG_TRUSTED_SCHEMA, + 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, + from sqlite3.dbapi2 export ( + SQLITE_ABORT, + SQLITE_ABORT_ROLLBACK, + SQLITE_AUTH, + SQLITE_AUTH_USER, + SQLITE_BUSY, + SQLITE_BUSY_RECOVERY, + SQLITE_BUSY_SNAPSHOT, + SQLITE_BUSY_TIMEOUT, + SQLITE_CANTOPEN, + SQLITE_CANTOPEN_CONVPATH, + SQLITE_CANTOPEN_DIRTYWAL, + SQLITE_CANTOPEN_FULLPATH, + SQLITE_CANTOPEN_ISDIR, + SQLITE_CANTOPEN_NOTEMPDIR, + SQLITE_CANTOPEN_SYMLINK, + SQLITE_CONSTRAINT, + SQLITE_CONSTRAINT_CHECK, + SQLITE_CONSTRAINT_COMMITHOOK, + SQLITE_CONSTRAINT_FOREIGNKEY, + SQLITE_CONSTRAINT_FUNCTION, + SQLITE_CONSTRAINT_NOTNULL, + SQLITE_CONSTRAINT_PINNED, + SQLITE_CONSTRAINT_PRIMARYKEY, + SQLITE_CONSTRAINT_ROWID, + SQLITE_CONSTRAINT_TRIGGER, + SQLITE_CONSTRAINT_UNIQUE, + SQLITE_CONSTRAINT_VTAB, + SQLITE_CORRUPT, + SQLITE_CORRUPT_INDEX, + SQLITE_CORRUPT_SEQUENCE, + SQLITE_CORRUPT_VTAB, + SQLITE_EMPTY, + SQLITE_ERROR, + SQLITE_ERROR_MISSING_COLLSEQ, + SQLITE_ERROR_RETRY, + SQLITE_ERROR_SNAPSHOT, + SQLITE_FORMAT, + SQLITE_FULL, + SQLITE_INTERNAL, + SQLITE_INTERRUPT, + SQLITE_IOERR, + SQLITE_IOERR_ACCESS, + SQLITE_IOERR_AUTH, + SQLITE_IOERR_BEGIN_ATOMIC, + SQLITE_IOERR_BLOCKED, + SQLITE_IOERR_CHECKRESERVEDLOCK, + SQLITE_IOERR_CLOSE, + SQLITE_IOERR_COMMIT_ATOMIC, + SQLITE_IOERR_CONVPATH, + SQLITE_IOERR_CORRUPTFS, + SQLITE_IOERR_DATA, + SQLITE_IOERR_DELETE, + SQLITE_IOERR_DELETE_NOENT, + SQLITE_IOERR_DIR_CLOSE, + SQLITE_IOERR_DIR_FSYNC, + SQLITE_IOERR_FSTAT, + SQLITE_IOERR_FSYNC, + SQLITE_IOERR_GETTEMPPATH, + SQLITE_IOERR_LOCK, + SQLITE_IOERR_MMAP, + SQLITE_IOERR_NOMEM, + SQLITE_IOERR_RDLOCK, + SQLITE_IOERR_READ, + SQLITE_IOERR_ROLLBACK_ATOMIC, + SQLITE_IOERR_SEEK, + SQLITE_IOERR_SHMLOCK, + SQLITE_IOERR_SHMMAP, + SQLITE_IOERR_SHMOPEN, + SQLITE_IOERR_SHMSIZE, + SQLITE_IOERR_SHORT_READ, + SQLITE_IOERR_TRUNCATE, + SQLITE_IOERR_UNLOCK, + SQLITE_IOERR_VNODE, + SQLITE_IOERR_WRITE, + SQLITE_LIMIT_ATTACHED, + SQLITE_LIMIT_COLUMN, + SQLITE_LIMIT_COMPOUND_SELECT, + SQLITE_LIMIT_EXPR_DEPTH, + SQLITE_LIMIT_FUNCTION_ARG, + SQLITE_LIMIT_LENGTH, + SQLITE_LIMIT_LIKE_PATTERN_LENGTH, + SQLITE_LIMIT_SQL_LENGTH, + SQLITE_LIMIT_TRIGGER_DEPTH, + SQLITE_LIMIT_VARIABLE_NUMBER, + SQLITE_LIMIT_VDBE_OP, + SQLITE_LIMIT_WORKER_THREADS, + SQLITE_LOCKED, + SQLITE_LOCKED_SHAREDCACHE, + SQLITE_LOCKED_VTAB, + SQLITE_MISMATCH, + SQLITE_MISUSE, + SQLITE_NOLFS, + SQLITE_NOMEM, + SQLITE_NOTADB, + SQLITE_NOTFOUND, + SQLITE_NOTICE, + SQLITE_NOTICE_RECOVER_ROLLBACK, + SQLITE_NOTICE_RECOVER_WAL, + SQLITE_OK_LOAD_PERMANENTLY, + SQLITE_OK_SYMLINK, + SQLITE_PERM, + SQLITE_PROTOCOL, + SQLITE_RANGE, + SQLITE_READONLY, + SQLITE_READONLY_CANTINIT, + SQLITE_READONLY_CANTLOCK, + SQLITE_READONLY_DBMOVED, + SQLITE_READONLY_DIRECTORY, + SQLITE_READONLY_RECOVERY, + SQLITE_READONLY_ROLLBACK, + SQLITE_ROW, + SQLITE_SCHEMA, + SQLITE_TOOBIG, + SQLITE_WARNING, + SQLITE_WARNING_AUTOINDEX, ) if sys.version_info < (3, 12): - from sqlite3.dbapi2 import enable_shared_cache as enable_shared_cache, version as version + from sqlite3.dbapi2 export enable_shared_cache, version private type SqliteData = str | ReadableBuffer | int | float | None # Data that is passed through adapters can be of any type accepted by an adapter. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/sqlite3/dbapi2.byi b/crates/ty_vendored/vendor/typeshed/stdlib/sqlite3/dbapi2.byi index e1f9857c85..960e75d9c5 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/sqlite3/dbapi2.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/sqlite3/dbapi2.byi @@ -1,217 +1,217 @@ import sys -from _sqlite3 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, - adapt as adapt, - adapters as adapters, - complete_statement as complete_statement, - connect as connect, - converters as converters, - enable_callback_tracebacks as enable_callback_tracebacks, - register_adapter as register_adapter, - register_converter as register_converter, - sqlite_version as sqlite_version, +from _sqlite3 export ( + PARSE_COLNAMES, + PARSE_DECLTYPES, + SQLITE_ALTER_TABLE, + SQLITE_ANALYZE, + SQLITE_ATTACH, + SQLITE_CREATE_INDEX, + SQLITE_CREATE_TABLE, + SQLITE_CREATE_TEMP_INDEX, + SQLITE_CREATE_TEMP_TABLE, + SQLITE_CREATE_TEMP_TRIGGER, + SQLITE_CREATE_TEMP_VIEW, + SQLITE_CREATE_TRIGGER, + SQLITE_CREATE_VIEW, + SQLITE_CREATE_VTABLE, + SQLITE_DELETE, + SQLITE_DENY, + SQLITE_DETACH, + SQLITE_DONE, + SQLITE_DROP_INDEX, + SQLITE_DROP_TABLE, + SQLITE_DROP_TEMP_INDEX, + SQLITE_DROP_TEMP_TABLE, + SQLITE_DROP_TEMP_TRIGGER, + SQLITE_DROP_TEMP_VIEW, + SQLITE_DROP_TRIGGER, + SQLITE_DROP_VIEW, + SQLITE_DROP_VTABLE, + SQLITE_FUNCTION, + SQLITE_IGNORE, + SQLITE_INSERT, + SQLITE_OK, + SQLITE_PRAGMA, + SQLITE_READ, + SQLITE_RECURSIVE, + SQLITE_REINDEX, + SQLITE_SAVEPOINT, + SQLITE_SELECT, + SQLITE_TRANSACTION, + SQLITE_UPDATE, + adapt, + adapters, + complete_statement, + connect, + converters, + enable_callback_tracebacks, + register_adapter, + register_converter, + sqlite_version, ) from datetime import date, datetime, time -from sqlite3 import ( - Connection as Connection, - Cursor as Cursor, - DatabaseError as DatabaseError, - DataError as DataError, - Error as Error, - IntegrityError as IntegrityError, - InterfaceError as InterfaceError, - InternalError as InternalError, - NotSupportedError as NotSupportedError, - OperationalError as OperationalError, - PrepareProtocol as PrepareProtocol, - ProgrammingError as ProgrammingError, - Row as Row, - Warning as Warning, +from sqlite3 export ( + Connection, + Cursor, + DatabaseError, + DataError, + Error, + IntegrityError, + InterfaceError, + InternalError, + NotSupportedError, + OperationalError, + PrepareProtocol, + ProgrammingError, + Row, + Warning, ) from typing import Final, Literal from typing_extensions import deprecated if sys.version_info >= (3, 12): - from _sqlite3 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, + from _sqlite3 export ( + LEGACY_TRANSACTION_CONTROL, + SQLITE_DBCONFIG_DEFENSIVE, + SQLITE_DBCONFIG_DQS_DDL, + SQLITE_DBCONFIG_DQS_DML, + SQLITE_DBCONFIG_ENABLE_FKEY, + SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER, + SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, + SQLITE_DBCONFIG_ENABLE_QPSG, + SQLITE_DBCONFIG_ENABLE_TRIGGER, + SQLITE_DBCONFIG_ENABLE_VIEW, + SQLITE_DBCONFIG_LEGACY_ALTER_TABLE, + SQLITE_DBCONFIG_LEGACY_FILE_FORMAT, + SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE, + SQLITE_DBCONFIG_RESET_DATABASE, + SQLITE_DBCONFIG_TRIGGER_EQP, + SQLITE_DBCONFIG_TRUSTED_SCHEMA, + SQLITE_DBCONFIG_WRITABLE_SCHEMA, ) if sys.version_info >= (3, 15): - from _sqlite3 import SQLITE_KEYWORDS as SQLITE_KEYWORDS + from _sqlite3 export SQLITE_KEYWORDS if sys.version_info >= (3, 11): - from _sqlite3 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, + from _sqlite3 export ( + SQLITE_ABORT, + SQLITE_ABORT_ROLLBACK, + SQLITE_AUTH, + SQLITE_AUTH_USER, + SQLITE_BUSY, + SQLITE_BUSY_RECOVERY, + SQLITE_BUSY_SNAPSHOT, + SQLITE_BUSY_TIMEOUT, + SQLITE_CANTOPEN, + SQLITE_CANTOPEN_CONVPATH, + SQLITE_CANTOPEN_DIRTYWAL, + SQLITE_CANTOPEN_FULLPATH, + SQLITE_CANTOPEN_ISDIR, + SQLITE_CANTOPEN_NOTEMPDIR, + SQLITE_CANTOPEN_SYMLINK, + SQLITE_CONSTRAINT, + SQLITE_CONSTRAINT_CHECK, + SQLITE_CONSTRAINT_COMMITHOOK, + SQLITE_CONSTRAINT_FOREIGNKEY, + SQLITE_CONSTRAINT_FUNCTION, + SQLITE_CONSTRAINT_NOTNULL, + SQLITE_CONSTRAINT_PINNED, + SQLITE_CONSTRAINT_PRIMARYKEY, + SQLITE_CONSTRAINT_ROWID, + SQLITE_CONSTRAINT_TRIGGER, + SQLITE_CONSTRAINT_UNIQUE, + SQLITE_CONSTRAINT_VTAB, + SQLITE_CORRUPT, + SQLITE_CORRUPT_INDEX, + SQLITE_CORRUPT_SEQUENCE, + SQLITE_CORRUPT_VTAB, + SQLITE_EMPTY, + SQLITE_ERROR, + SQLITE_ERROR_MISSING_COLLSEQ, + SQLITE_ERROR_RETRY, + SQLITE_ERROR_SNAPSHOT, + SQLITE_FORMAT, + SQLITE_FULL, + SQLITE_INTERNAL, + SQLITE_INTERRUPT, + SQLITE_IOERR, + SQLITE_IOERR_ACCESS, + SQLITE_IOERR_AUTH, + SQLITE_IOERR_BEGIN_ATOMIC, + SQLITE_IOERR_BLOCKED, + SQLITE_IOERR_CHECKRESERVEDLOCK, + SQLITE_IOERR_CLOSE, + SQLITE_IOERR_COMMIT_ATOMIC, + SQLITE_IOERR_CONVPATH, + SQLITE_IOERR_CORRUPTFS, + SQLITE_IOERR_DATA, + SQLITE_IOERR_DELETE, + SQLITE_IOERR_DELETE_NOENT, + SQLITE_IOERR_DIR_CLOSE, + SQLITE_IOERR_DIR_FSYNC, + SQLITE_IOERR_FSTAT, + SQLITE_IOERR_FSYNC, + SQLITE_IOERR_GETTEMPPATH, + SQLITE_IOERR_LOCK, + SQLITE_IOERR_MMAP, + SQLITE_IOERR_NOMEM, + SQLITE_IOERR_RDLOCK, + SQLITE_IOERR_READ, + SQLITE_IOERR_ROLLBACK_ATOMIC, + SQLITE_IOERR_SEEK, + SQLITE_IOERR_SHMLOCK, + SQLITE_IOERR_SHMMAP, + SQLITE_IOERR_SHMOPEN, + SQLITE_IOERR_SHMSIZE, + SQLITE_IOERR_SHORT_READ, + SQLITE_IOERR_TRUNCATE, + SQLITE_IOERR_UNLOCK, + SQLITE_IOERR_VNODE, + SQLITE_IOERR_WRITE, + SQLITE_LIMIT_ATTACHED, + SQLITE_LIMIT_COLUMN, + SQLITE_LIMIT_COMPOUND_SELECT, + SQLITE_LIMIT_EXPR_DEPTH, + SQLITE_LIMIT_FUNCTION_ARG, + SQLITE_LIMIT_LENGTH, + SQLITE_LIMIT_LIKE_PATTERN_LENGTH, + SQLITE_LIMIT_SQL_LENGTH, + SQLITE_LIMIT_TRIGGER_DEPTH, + SQLITE_LIMIT_VARIABLE_NUMBER, + SQLITE_LIMIT_VDBE_OP, + SQLITE_LIMIT_WORKER_THREADS, + SQLITE_LOCKED, + SQLITE_LOCKED_SHAREDCACHE, + SQLITE_LOCKED_VTAB, + SQLITE_MISMATCH, + SQLITE_MISUSE, + SQLITE_NOLFS, + SQLITE_NOMEM, + SQLITE_NOTADB, + SQLITE_NOTFOUND, + SQLITE_NOTICE, + SQLITE_NOTICE_RECOVER_ROLLBACK, + SQLITE_NOTICE_RECOVER_WAL, + SQLITE_OK_LOAD_PERMANENTLY, + SQLITE_OK_SYMLINK, + SQLITE_PERM, + SQLITE_PROTOCOL, + SQLITE_RANGE, + SQLITE_READONLY, + SQLITE_READONLY_CANTINIT, + SQLITE_READONLY_CANTLOCK, + SQLITE_READONLY_DBMOVED, + SQLITE_READONLY_DIRECTORY, + SQLITE_READONLY_RECOVERY, + SQLITE_READONLY_ROLLBACK, + SQLITE_ROW, + SQLITE_SCHEMA, + SQLITE_TOOBIG, + SQLITE_WARNING, + SQLITE_WARNING_AUTOINDEX, ) - from sqlite3 import Blob as Blob + from sqlite3 export Blob if sys.version_info < (3, 14): # Deprecated and removed from _sqlite3 in 3.12, but removed from here in 3.14. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/sre_constants.byi b/crates/ty_vendored/vendor/typeshed/stdlib/sre_constants.byi index a556d404a3..a2c482a679 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/sre_constants.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/sre_constants.byi @@ -1,7 +1,7 @@ """Internal support module for sre""" import sys -from re import error as error +from re export error from typing import Final from typing_extensions import Self, disjoint_base diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/ssl.byi b/crates/ty_vendored/vendor/typeshed/stdlib/ssl.byi index 7788c0ec77..01f28792e8 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/ssl.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/ssl.byi @@ -124,19 +124,19 @@ from typing import Final, Literal, NamedTuple, TypeAlias, TypedDict, type_check_ from typing_extensions import Never, Self, deprecated if sys.version_info >= (3, 13): - from _ssl import HAS_PSK as HAS_PSK + from _ssl export HAS_PSK if sys.version_info >= (3, 15): - from _ssl import HAS_PSK_TLS13 as HAS_PSK_TLS13 + from _ssl export HAS_PSK_TLS13 if sys.version_info >= (3, 14): - from _ssl import HAS_PHA as HAS_PHA + from _ssl export HAS_PHA if sys.version_info < (3, 12): - from _ssl import RAND_pseudo_bytes as RAND_pseudo_bytes + from _ssl export RAND_pseudo_bytes if sys.platform == "win32": - from _ssl import enum_certificates as enum_certificates, enum_crls as enum_crls + from _ssl export enum_certificates, enum_crls private type PCTRTT = (*: (str, str)) private type PCTRTTT = (*: PCTRTT) @@ -390,8 +390,18 @@ class Purpose(_ASN1Object, enum.Enum): # because this is an enum, the inherited __new__ is replaced at runtime with # Enum.__new__. def __new__(cls, value: object) -> Self - SERVER_AUTH = (129, "serverAuth", "TLS Web Server Authentication", "1.3.6.1.5.5.7.3.2") - CLIENT_AUTH = (130, "clientAuth", "TLS Web Client Authentication", "1.3.6.1.5.5.7.3.1") + SERVER_AUTH = ( # ty:ignore[invalid-assignment] + 129, + "serverAuth", + "TLS Web Server Authentication", + "1.3.6.1.5.5.7.3.2", + ) + CLIENT_AUTH = ( # ty:ignore[invalid-assignment] + 130, + "clientAuth", + "TLS Web Client Authentication", + "1.3.6.1.5.5.7.3.1", + ) class SSLSocket(socket.socket): """This class implements a subtype of socket.socket that wraps @@ -410,7 +420,7 @@ class SSLSocket(socket.socket): let session_reused: bool | None init(self, *args: dynamic, **kwargs: dynamic) - override def connect(self, addr: socket._Address) -> None: + override def connect(self, addr: socket._Address): """Connects to remote ADDR, and then wraps the connection in an SSL channel. """ @@ -427,12 +437,12 @@ class SSLSocket(socket.socket): self, buffer: WriteableBuffer, nbytes: int | None = None, flags: int = 0 ) -> (int, socket._RetAddress) override def send(self, data: ReadableBuffer, flags: int = 0) -> int - override def sendall(self, data: ReadableBuffer, flags: int = 0) -> None + override def sendall(self, data: ReadableBuffer, flags: int = 0) override def sendto(self, data: ReadableBuffer, flags_or_addr: socket._Address, addr: None = None) -> int def sendto(self, data: ReadableBuffer, flags_or_addr: int, addr: socket._Address) -> int - override def shutdown(self, how: int) -> None + override def shutdown(self, how: int) @deprecated("Deprecated since Python 3.6. Use `SSLSocket.recv` method instead.") def read(self, len: int = 1024, buffer: WriteableBuffer | None = None) -> bytes: """Read up to LEN bytes and return them. @@ -641,7 +651,7 @@ class SSLContext(_SSLContext): cafile: StrOrBytesPath | None = None, capath: StrOrBytesPath | None = None, cadata: str | ReadableBuffer | None = None, - ) -> None + ) override def get_ca_certs(self, binary_form: False = False) -> list[PeerCertRetDictType]: """Returns a list of dicts with information of loaded CA certs. @@ -663,14 +673,14 @@ class SSLContext(_SSLContext): def set_client_sigalgs(self, sigalgs: str, /) def set_server_sigalgs(self, sigalgs: str, /) - override def set_default_verify_paths(self) -> None - override def set_ciphers(self, cipherlist: str, /) -> None + override def set_default_verify_paths(self) + override def set_ciphers(self, cipherlist: str, /) def set_alpn_protocols(self, alpn_protocols: Iterable[str]) @deprecated("Deprecated since Python 3.10. Use ALPN instead.") def set_npn_protocols(self, npn_protocols: Iterable[str]) -> None def set_servername_callback(self, server_name_callback: SrvnmeCbType | None) - override def load_dh_params(self, path: str, /) -> None - override def set_ecdh_curve(self, name: str, /) -> None + override def load_dh_params(self, path: str, /) + override def set_ecdh_curve(self, name: str, /) def wrap_socket( self, sock: socket.socket, diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/stat.byi b/crates/ty_vendored/vendor/typeshed/stdlib/stat.byi index 809da644e3..75132da320 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/stat.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/stat.byi @@ -4,93 +4,93 @@ Suggested usage: from stat import * """ import sys -from _stat import ( - S_ENFMT as S_ENFMT, - S_IEXEC as S_IEXEC, - S_IFBLK as S_IFBLK, - S_IFCHR as S_IFCHR, - S_IFDIR as S_IFDIR, - S_IFDOOR as S_IFDOOR, - S_IFIFO as S_IFIFO, - S_IFLNK as S_IFLNK, - S_IFMT as S_IFMT, - S_IFPORT as S_IFPORT, - S_IFREG as S_IFREG, - S_IFSOCK as S_IFSOCK, - S_IFWHT as S_IFWHT, - S_IMODE as S_IMODE, - S_IREAD as S_IREAD, - S_IRGRP as S_IRGRP, - S_IROTH as S_IROTH, - S_IRUSR as S_IRUSR, - S_IRWXG as S_IRWXG, - S_IRWXO as S_IRWXO, - S_IRWXU as S_IRWXU, - S_ISBLK as S_ISBLK, - S_ISCHR as S_ISCHR, - S_ISDIR as S_ISDIR, - S_ISDOOR as S_ISDOOR, - S_ISFIFO as S_ISFIFO, - S_ISGID as S_ISGID, - S_ISLNK as S_ISLNK, - S_ISPORT as S_ISPORT, - S_ISREG as S_ISREG, - S_ISSOCK as S_ISSOCK, - S_ISUID as S_ISUID, - S_ISVTX as S_ISVTX, - S_ISWHT as S_ISWHT, - S_IWGRP as S_IWGRP, - S_IWOTH as S_IWOTH, - S_IWRITE as S_IWRITE, - S_IWUSR as S_IWUSR, - S_IXGRP as S_IXGRP, - S_IXOTH as S_IXOTH, - S_IXUSR as S_IXUSR, - SF_APPEND as SF_APPEND, - SF_ARCHIVED as SF_ARCHIVED, - SF_IMMUTABLE as SF_IMMUTABLE, - SF_NOUNLINK as SF_NOUNLINK, - SF_SNAPSHOT as SF_SNAPSHOT, - ST_ATIME as ST_ATIME, - ST_CTIME as ST_CTIME, - ST_DEV as ST_DEV, - ST_GID as ST_GID, - ST_INO as ST_INO, - ST_MODE as ST_MODE, - ST_MTIME as ST_MTIME, - ST_NLINK as ST_NLINK, - ST_SIZE as ST_SIZE, - ST_UID as ST_UID, - UF_APPEND as UF_APPEND, - UF_COMPRESSED as UF_COMPRESSED, - UF_HIDDEN as UF_HIDDEN, - UF_IMMUTABLE as UF_IMMUTABLE, - UF_NODUMP as UF_NODUMP, - UF_NOUNLINK as UF_NOUNLINK, - UF_OPAQUE as UF_OPAQUE, - filemode as filemode, +from _stat export ( + S_ENFMT, + S_IEXEC, + S_IFBLK, + S_IFCHR, + S_IFDIR, + S_IFDOOR, + S_IFIFO, + S_IFLNK, + S_IFMT, + S_IFPORT, + S_IFREG, + S_IFSOCK, + S_IFWHT, + S_IMODE, + S_IREAD, + S_IRGRP, + S_IROTH, + S_IRUSR, + S_IRWXG, + S_IRWXO, + S_IRWXU, + S_ISBLK, + S_ISCHR, + S_ISDIR, + S_ISDOOR, + S_ISFIFO, + S_ISGID, + S_ISLNK, + S_ISPORT, + S_ISREG, + S_ISSOCK, + S_ISUID, + S_ISVTX, + S_ISWHT, + S_IWGRP, + S_IWOTH, + S_IWRITE, + S_IWUSR, + S_IXGRP, + S_IXOTH, + S_IXUSR, + SF_APPEND, + SF_ARCHIVED, + SF_IMMUTABLE, + SF_NOUNLINK, + SF_SNAPSHOT, + ST_ATIME, + ST_CTIME, + ST_DEV, + ST_GID, + ST_INO, + ST_MODE, + ST_MTIME, + ST_NLINK, + ST_SIZE, + ST_UID, + UF_APPEND, + UF_COMPRESSED, + UF_HIDDEN, + UF_IMMUTABLE, + UF_NODUMP, + UF_NOUNLINK, + UF_OPAQUE, + filemode, ) from typing import Final if sys.platform == "win32": - from _stat import ( - IO_REPARSE_TAG_APPEXECLINK as IO_REPARSE_TAG_APPEXECLINK, - IO_REPARSE_TAG_MOUNT_POINT as IO_REPARSE_TAG_MOUNT_POINT, - IO_REPARSE_TAG_SYMLINK as IO_REPARSE_TAG_SYMLINK, + from _stat export ( + IO_REPARSE_TAG_APPEXECLINK, + IO_REPARSE_TAG_MOUNT_POINT, + IO_REPARSE_TAG_SYMLINK, ) if sys.version_info >= (3, 13): - from _stat import ( - SF_DATALESS as SF_DATALESS, - SF_FIRMLINK as SF_FIRMLINK, - SF_SETTABLE as SF_SETTABLE, - UF_DATAVAULT as UF_DATAVAULT, - UF_SETTABLE as UF_SETTABLE, - UF_TRACKED as UF_TRACKED, + from _stat export ( + SF_DATALESS, + SF_FIRMLINK, + SF_SETTABLE, + UF_DATAVAULT, + UF_SETTABLE, + UF_TRACKED, ) if sys.platform == "darwin": - from _stat import SF_SUPPORTED as SF_SUPPORTED, SF_SYNTHETIC as SF_SYNTHETIC + from _stat export SF_SUPPORTED, SF_SYNTHETIC # _stat.c defines FILE_ATTRIBUTE_* constants conditionally, # making them available only at runtime on Windows. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/string/__init__.byi b/crates/ty_vendored/vendor/typeshed/stdlib/string/__init__.byi index 4394a7bbb0..80b3b8e116 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/string/__init__.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/string/__init__.byi @@ -19,7 +19,6 @@ from _typeshed import StrOrLiteralStr from collections.abc import Iterable, Mapping, Sequence from re import Pattern, RegexFlag from typing import ClassVar, Final -from typing_extensions import LiteralString __all__ = [ "ascii_letters", @@ -39,12 +38,12 @@ __all__ = [ whitespace: Final = " \t\n\r\v\f" ascii_lowercase: Final = "abcdefghijklmnopqrstuvwxyz" ascii_uppercase: Final = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" -final ascii_letters: LiteralString # string too long +final ascii_letters: literal str # string too long digits: Final = "0123456789" hexdigits: Final = "0123456789abcdefABCDEF" octdigits: Final = "01234567" punctuation: Final = r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~""" -final printable: LiteralString # string too long +final printable: literal str # string too long def capwords(s: StrOrLiteralStr, sep: StrOrLiteralStr | None = None) -> StrOrLiteralStr: """capwords(s [,sep]) -> string @@ -80,12 +79,12 @@ class Template: class Formatter: """See PEP 3101 for details and purpose of this class.""" - def format(self, format_string: LiteralString, /, *args: LiteralString, **kwargs: LiteralString) -> LiteralString + def format(self, format_string: literal str, /, *args: literal str, **kwargs: literal str) -> literal str def format(self, format_string: str, /, *args: dynamic, **kwargs: dynamic) -> str def vformat( - self, format_string: LiteralString, args: Sequence[LiteralString], kwargs: Mapping[LiteralString, LiteralString] - ) -> LiteralString + self, format_string: literal str, args: Sequence[literal str], kwargs: Mapping[literal str, literal str] + ) -> literal str def vformat(self, format_string: str, args: Sequence[dynamic], kwargs: Mapping[str, dynamic]) -> str def _vformat( # undocumented diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/subprocess.byi b/crates/ty_vendored/vendor/typeshed/stdlib/subprocess.byi index 0b64180931..05d1a3a966 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/subprocess.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/subprocess.byi @@ -1825,23 +1825,23 @@ if sys.platform == "win32": lpAttributeList: Mapping[str, dynamic] def copy(self) -> STARTUPINFO - from _winapi import ( - ABOVE_NORMAL_PRIORITY_CLASS as ABOVE_NORMAL_PRIORITY_CLASS, - BELOW_NORMAL_PRIORITY_CLASS as BELOW_NORMAL_PRIORITY_CLASS, - CREATE_BREAKAWAY_FROM_JOB as CREATE_BREAKAWAY_FROM_JOB, - CREATE_DEFAULT_ERROR_MODE as CREATE_DEFAULT_ERROR_MODE, - CREATE_NEW_CONSOLE as CREATE_NEW_CONSOLE, - CREATE_NEW_PROCESS_GROUP as CREATE_NEW_PROCESS_GROUP, - CREATE_NO_WINDOW as CREATE_NO_WINDOW, - DETACHED_PROCESS as DETACHED_PROCESS, - HIGH_PRIORITY_CLASS as HIGH_PRIORITY_CLASS, - IDLE_PRIORITY_CLASS as IDLE_PRIORITY_CLASS, - NORMAL_PRIORITY_CLASS as NORMAL_PRIORITY_CLASS, - REALTIME_PRIORITY_CLASS as REALTIME_PRIORITY_CLASS, - STARTF_USESHOWWINDOW as STARTF_USESHOWWINDOW, - STARTF_USESTDHANDLES as STARTF_USESTDHANDLES, - STD_ERROR_HANDLE as STD_ERROR_HANDLE, - STD_INPUT_HANDLE as STD_INPUT_HANDLE, - STD_OUTPUT_HANDLE as STD_OUTPUT_HANDLE, - SW_HIDE as SW_HIDE, + from _winapi export ( + ABOVE_NORMAL_PRIORITY_CLASS, + BELOW_NORMAL_PRIORITY_CLASS, + CREATE_BREAKAWAY_FROM_JOB, + CREATE_DEFAULT_ERROR_MODE, + CREATE_NEW_CONSOLE, + CREATE_NEW_PROCESS_GROUP, + CREATE_NO_WINDOW, + DETACHED_PROCESS, + HIGH_PRIORITY_CLASS, + IDLE_PRIORITY_CLASS, + NORMAL_PRIORITY_CLASS, + REALTIME_PRIORITY_CLASS, + STARTF_USESHOWWINDOW, + STARTF_USESTDHANDLES, + STD_ERROR_HANDLE, + STD_INPUT_HANDLE, + STD_OUTPUT_HANDLE, + SW_HIDE, ) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/sunau.byi b/crates/ty_vendored/vendor/typeshed/stdlib/sunau.byi index 70603d0854..2454d8d377 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/sunau.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/sunau.byi @@ -105,7 +105,7 @@ is destroyed. from _typeshed import Unused from typing import Final, Literal, NamedTuple, TypeAlias -from typing_extensions import Self +from typing_extensions import Never, Self type _File = str | IO[bytes] @@ -153,7 +153,7 @@ class Au_read: def getcompname(self) -> str def getparams(self) -> _sunau_params def getmarkers(self) - def getmark(self, id: dynamic) -> NoReturn + def getmark(self, id: dynamic) -> Never def setpos(self, pos: int) def readframes(self, nframes: int) -> bytes | None diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/symtable.byi b/crates/ty_vendored/vendor/typeshed/stdlib/symtable.byi index ec2e8c1ed0..7da9117f9c 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/symtable.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/symtable.byi @@ -123,7 +123,7 @@ class Function(SymbolTable): """Return a tuple of nonlocals in the function.""" class Class(SymbolTable): - @deprecated("Deprecated since Python 3.14; will be removed in Python 3.16.") + @deprecated("Deprecated; will be removed in Python 3.16.") def get_methods(self) -> (*: str): """Return a tuple of methods declared in the class.""" diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/sys/__init__.byi b/crates/ty_vendored/vendor/typeshed/stdlib/sys/__init__.byi index aba9fb9693..2472d8edaf 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/sys/__init__.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/sys/__init__.byi @@ -79,7 +79,7 @@ from collections.abc import AsyncGenerator, Callable, Sequence from io import TextIOWrapper from types import FrameType, ModuleType, SimpleNamespace, TracebackType from typing import Final, Literal, Protocol, TypeAlias, TypeVar, final, type_check_only -from typing_extensions import LiteralString, deprecated +from typing_extensions import Never, deprecated private type LazyImportMode = "normal" | "all" | "none" private type LazyImportFilter = (str | None, str, (*: str) | None) -> bool @@ -134,7 +134,7 @@ orig_argv: list[str] path: list[str] path_hooks: list[(str) -> PathEntryFinderProtocol] path_importer_cache: dict[str, PathEntryFinderProtocol | None] -platform: LiteralString +platform: literal str platlibdir: str prefix: str pycache_prefix: str | None @@ -572,7 +572,7 @@ if sys.version_info >= (3, 11): if no such exception exists. """ -def exit(status: _ExitCode = None, /) -> NoReturn: +def exit(status: _ExitCode = None, /) -> Never: """Exit the interpreter by raising SystemExit(status). If the status is omitted or None, it defaults to zero (i.e., success). @@ -597,10 +597,10 @@ if sys.platform != "win32": The flag constants are defined in the os module. """ -def getfilesystemencoding() -> LiteralString: +def getfilesystemencoding() -> literal str: """Return the encoding used to convert Unicode filenames to OS filenames.""" -def getfilesystemencodeerrors() -> LiteralString: +def getfilesystemencodeerrors() -> literal str: """Return the error mode used Unicode to OS filename conversion.""" if sys.version_info >= (3, 15): @@ -697,7 +697,7 @@ if sys.platform == "win32": intended for identifying the OS rather than feature detection. """ -def intern(string: LiteralString, /) -> LiteralString: +def intern(string: literal str, /) -> literal str: """``Intern'' the given string. This enters the string in the (global) table of interned strings whose @@ -917,7 +917,7 @@ if sys.version_info >= (3, 12): """Activate stack profiler trampoline *backend*.""" else: - def activate_stack_trampoline(backend: str, /) -> NoReturn: + def activate_stack_trampoline(backend: str, /) -> Never: """Activate stack profiler trampoline *backend*.""" from . import _monitoring diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/sysconfig.byi b/crates/ty_vendored/vendor/typeshed/stdlib/sysconfig.byi index 426723cae4..37c6c5a905 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/sysconfig.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/sysconfig.byi @@ -2,7 +2,7 @@ import sys from typing import Literal -from typing_extensions import LiteralString, deprecated +from typing_extensions import deprecated __all__ = [ "get_config_h_filename", @@ -42,11 +42,11 @@ def get_config_vars(arg: str, /, *args: str) -> list[dynamic] def get_scheme_names() -> (*: str): """Return a tuple containing the schemes names.""" -def get_default_scheme() -> LiteralString -def get_preferred_scheme(key: "prefix" | "home" | "user") -> LiteralString +def get_default_scheme() -> literal str +def get_preferred_scheme(key: "prefix" | "home" | "user") -> literal str # Documented -- see https://docs.python.org/3/library/sysconfig.html#sysconfig._get_preferred_schemes -def _get_preferred_schemes() -> dict["prefix" | "home" | "user", LiteralString] +def _get_preferred_schemes() -> dict["prefix" | "home" | "user", literal str] def get_path_names() -> (*: str): """Return a tuple containing the paths names.""" @@ -99,11 +99,11 @@ if sys.version_info >= (3, 15): def is_python_build() -> bool elif sys.version_info >= (3, 11): def is_python_build() -> bool - @deprecated("The `check_home` parameter is deprecated since Python 3.12; removed in Python 3.15.") + @deprecated("The `check_home` parameter is deprecated; removed in Python 3.15.") def is_python_build(check_home: object = None) -> bool else: def is_python_build() -> bool - @deprecated("The `check_home` parameter is deprecated since Python 3.12; removed in Python 3.15.") + @deprecated("The `check_home` parameter is deprecated; removed in Python 3.15.") def is_python_build(check_home: bool = False) -> bool def parse_config_h(fp: IO[dynamic], vars: dict[str, dynamic] | None = None) -> dict[str, dynamic]: diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/tarfile.byi b/crates/ty_vendored/vendor/typeshed/stdlib/tarfile.byi index d182db233f..ccfa32306b 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/tarfile.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/tarfile.byi @@ -1130,10 +1130,10 @@ class TarInfo: """ @property - @deprecated("Deprecated since Python 3.13; will be removed in Python 3.16.") + @deprecated("Deprecated; will be removed in Python 3.16.") def tarfile(self) -> TarFile | None @tarfile.setter - @deprecated("Deprecated since Python 3.13; will be removed in Python 3.16.") + @deprecated("Deprecated; will be removed in Python 3.16.") def tarfile(self, tarfile: TarFile | None) -> None class def frombuf(cls, buf: bytes | bytearray, encoding: str, errors: str) -> Self: diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/tempfile.byi b/crates/ty_vendored/vendor/typeshed/stdlib/tempfile.byi index 4c942b6e8f..bde1f9d866 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/tempfile.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/tempfile.byi @@ -293,9 +293,9 @@ class _TemporaryFileWrapper(IO[AnyStr]): def __init__(self, file: IO[AnyStr], name: str, delete: bool = True) -> None override def __enter__(self) -> Self - override def __exit__(self, exc: type[BaseException] | None, value: BaseException | None, tb: TracebackType | None) -> None + override def __exit__(self, exc: type[BaseException] | None, value: BaseException | None, tb: TracebackType | None) def __getattr__(self, name: str) -> dynamic - override def close(self) -> None: + override def close(self): """ Close the temporary file, possibly deleting it. """ @@ -314,7 +314,7 @@ class _TemporaryFileWrapper(IO[AnyStr]): # TypeError: '_TemporaryFileWrapper' object is not an iterator override def __next__(self) -> AnyStr override def fileno(self) -> int - override def flush(self) -> None + override def flush(self) override def isatty(self) -> bool override def read(self, n: int = ...) -> AnyStr override def readable(self) -> bool @@ -423,13 +423,13 @@ class SpooledTemporaryFile(IO[AnyStr], _SpooledTemporaryFileBase): let errors: str | None def rollover(self) override def __enter__(self) -> Self - override def __exit__(self, exc: type[BaseException] | None, value: BaseException | None, tb: TracebackType | None) -> None + override def __exit__(self, exc: type[BaseException] | None, value: BaseException | None, tb: TracebackType | None) # These methods are copied from the abstract methods of IO, because # SpooledTemporaryFile implements IO. # See also https://github.com/python/typeshed/pull/2452#issuecomment-420657918. - override def close(self) -> None + override def close(self) override def fileno(self) -> int - override def flush(self) -> None + override def flush(self) override def isatty(self) -> bool if sys.version_info >= (3, 11): # These three work only if the SpooledTemporaryFile is opened in binary mode, diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/__init__.byi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/__init__.byi index 35b87e2c39..78b9ad139a 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/__init__.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/__init__.byi @@ -3,7 +3,7 @@ Tkinter provides classes which allow the display, positioning and control of widgets. Toplevel widgets are Tk and Toplevel. Other widgets are Frame, Label, Entry, Text, Canvas, Button, Radiobutton, -Checkbutton, Scale, Listbox, Scrollbar, OptionMenu, Spinbox +Checkbutton, Scale, Listbox, Scrollbar, OptionMenu, Spinbox, LabelFrame and PanedWindow. Properties of the widgets are specified with keyword arguments. @@ -444,7 +444,7 @@ class Variable: def trace_info(self) -> list[((*: Literal["array", "read", "write", "unset"]), str)]: """Return all trace callback information.""" - @deprecated("Deprecated since Python 3.14. Use `trace_add()` instead.") + @deprecated("Deprecated; will be removed in Python 3.17. Use `trace_add()` instead.") def trace(self, mode, callback) -> str: """Define a trace callback for the variable. @@ -455,10 +455,11 @@ class Variable: Return the name of the callback. This deprecated method wraps a deprecated Tcl method removed - in Tcl 9.0. Use trace_add() instead. + in Tcl 9.0 and will be removed in Python 3.17. Use trace_add() + instead. """ - @deprecated("Deprecated since Python 3.14. Use `trace_add()` instead.") + @deprecated("Deprecated; will be removed in Python 3.17. Use `trace_add()` instead.") def trace_variable(self, mode, callback) -> str: """Define a trace callback for the variable. @@ -469,10 +470,11 @@ class Variable: Return the name of the callback. This deprecated method wraps a deprecated Tcl method removed - in Tcl 9.0. Use trace_add() instead. + in Tcl 9.0 and will be removed in Python 3.17. Use trace_add() + instead. """ - @deprecated("Deprecated since Python 3.14. Use `trace_remove()` instead.") + @deprecated("Deprecated; will be removed in Python 3.17. Use `trace_remove()` instead.") def trace_vdelete(self, mode, cbname) -> None: """Delete the trace callback for a variable. @@ -480,15 +482,17 @@ class Variable: CBNAME is the name of the callback returned from trace_variable or trace. This deprecated method wraps a deprecated Tcl method removed - in Tcl 9.0. Use trace_remove() instead. + in Tcl 9.0 and will be removed in Python 3.17. Use trace_remove() + instead. """ - @deprecated("Deprecated since Python 3.14. Use `trace_info()` instead.") + @deprecated("Deprecated; will be removed in Python 3.17. Use `trace_info()` instead.") def trace_vinfo(self) -> list[Incomplete]: """Return all trace callback information. This deprecated method wraps a deprecated Tcl method removed - in Tcl 9.0. Use trace_info() instead. + in Tcl 9.0 and will be removed in Python 3.17. Use trace_info() + instead. """ override def __eq__(self, other: object) -> bool @@ -511,7 +515,7 @@ class StringVar(Variable): then the existing value is retained. """ - override def set(self, value: str) -> None: + override def set(self, value: str): """Set the variable to VALUE.""" initialize = set @@ -532,7 +536,7 @@ class IntVar(Variable): then the existing value is retained. """ - override def set(self, value: int) -> None: + override def set(self, value: int): """Set the variable to VALUE.""" initialize = set @@ -553,7 +557,7 @@ class DoubleVar(Variable): then the existing value is retained. """ - override def set(self, value: float) -> None: + override def set(self, value: float): """Set the variable to VALUE.""" initialize = set @@ -574,7 +578,7 @@ class BooleanVar(Variable): then the existing value is retained. """ - override def set(self, value: bool) -> None: + override def set(self, value: bool): """Set the variable to VALUE.""" initialize = set @@ -2080,7 +2084,7 @@ class Wm: iconphoto = wm_iconphoto def wm_iconposition(self, x: int | None = None, y: int | None = None) -> (int, int) | None: """Set the position of the icon of this widget to X and Y. Return - a tuple of the current values of X and X if None is given. + a tuple of the current values of X and Y if None is given. """ iconposition = wm_iconposition @@ -2247,7 +2251,7 @@ class Tk(Misc, Wm): def configure(self, cnf: str) -> (str, str, str, dynamic, dynamic) config = configure - override def destroy(self) -> None: + override def destroy(self): """Destroy this and all descendants widgets. This will end the application of this Tcl interpreter. """ @@ -2526,7 +2530,7 @@ class BaseWidget(Misc): and appropriate options. """ - override def destroy(self) -> None: + override def destroy(self): """Destroy this and all descendants widgets.""" # This class represents any widget except Toplevel or Tk. @@ -2624,10 +2628,11 @@ class Toplevel(BaseWidget, Wm): ): """Construct a toplevel widget with the parent MASTER. - Valid option names: background, bd, bg, borderwidth, class, - colormap, container, cursor, height, highlightbackground, - highlightcolor, highlightthickness, menu, relief, screen, takefocus, - use, visual, width. + Valid option names: background, backgroundimage (Tk 9.0+), bd, bg, + bgimg (Tk 9.0+), borderwidth, class, colormap, container, + cursor, height, highlightbackground, highlightcolor, + highlightthickness, menu, padx, pady, relief, screen, + takefocus, tile (Tk 9.0+), use, visual, width. """ override def configure( @@ -3594,7 +3599,7 @@ class Canvas(Widget, XView, YView): (optional below another item). """ - override def lower(self, first: str | int, second: str | int | None = ..., /) -> None: + override def lower(self, first: str | int, second: str | int | None = ..., /): """Lower an item TAGORID given in ARGS (optional below another item). """ @@ -3604,12 +3609,12 @@ class Canvas(Widget, XView, YView): (optional above another item). """ - override def tkraise(self, first: str | int, second: str | int | None = ..., /) -> None: + override def tkraise(self, first: str | int, second: str | int | None = ..., /): """Raise an item TAGORID given in ARGS (optional above another item). """ - override def lift(self, first: str | int, second: str | int | None = ..., /) -> None: + override def lift(self, first: str | int, second: str | int | None = ..., /): """Raise an item TAGORID given in ARGS (optional above another item). """ @@ -3709,12 +3714,13 @@ class Checkbutton(Widget): """Construct a checkbutton widget with the parent MASTER. Valid option names: activebackground, activeforeground, anchor, - background, bd, bg, bitmap, borderwidth, command, cursor, - disabledforeground, fg, font, foreground, height, - highlightbackground, highlightcolor, highlightthickness, image, - indicatoron, justify, offvalue, onvalue, padx, pady, relief, - selectcolor, selectimage, state, takefocus, text, textvariable, - underline, variable, width, wraplength. + background, bd, bg, bitmap, borderwidth, command, compound, + cursor, disabledforeground, fg, font, foreground, height, + highlightbackground, highlightcolor, highlightthickness, + image, indicatoron, justify, offrelief, offvalue, onvalue, + overrelief, padx, pady, relief, selectcolor, selectimage, + state, takefocus, text, textvariable, tristateimage, + tristatevalue, underline, variable, width, wraplength. """ override def configure( @@ -3844,13 +3850,15 @@ class Entry(Widget, XView): """Construct an entry widget with the parent MASTER. Valid option names: background, bd, bg, borderwidth, cursor, - exportselection, fg, font, foreground, highlightbackground, - highlightcolor, highlightthickness, insertbackground, - insertborderwidth, insertofftime, insertontime, insertwidth, - invalidcommand, invcmd, justify, relief, selectbackground, - selectborderwidth, selectforeground, show, state, takefocus, - textvariable, validate, validatecommand, vcmd, width, - xscrollcommand. + disabledbackground, disabledforeground, exportselection, fg, + font, foreground, highlightbackground, highlightcolor, + highlightthickness, insertbackground, insertborderwidth, + insertofftime, insertontime, insertwidth, invalidcommand, + invcmd, justify, locale (Tk 9.1+), placeholder (Tk 9.0+), + placeholderforeground (Tk 9.0+), readonlybackground, relief, + selectbackground, selectborderwidth, selectforeground, show, + state, takefocus, textvariable, validate, validatecommand, + vcmd, width, xscrollcommand. """ override def configure( @@ -3937,7 +3945,7 @@ class Entry(Widget, XView): def selection_adjust(self, index: str | int): """Adjust the end of the selection near the cursor to INDEX.""" - override def selection_clear(self) -> None: + override def selection_clear(self): """Clear the selection if it is in this widget.""" def selection_from(self, index: str | int): @@ -3992,9 +4000,11 @@ class Frame(Widget): ): """Construct a frame widget with the parent MASTER. - Valid option names: background, bd, bg, borderwidth, class, - colormap, container, cursor, height, highlightbackground, - highlightcolor, highlightthickness, relief, takefocus, visual, width. + Valid option names: background, backgroundimage (Tk 9.0+), bd, bg, + bgimg (Tk 9.0+), borderwidth, class, colormap, container, + cursor, height, highlightbackground, highlightcolor, + highlightthickness, padx, pady, relief, takefocus, tile (Tk + 9.0+), visual, width. """ override def configure( @@ -4088,7 +4098,8 @@ class Label(Widget): WIDGET-SPECIFIC OPTIONS - height, state, width + compound, height, state, + textangle (Tk 9.1+), width """ @@ -4200,11 +4211,14 @@ class Listbox(Widget, XView, YView): ): """Construct a listbox widget with the parent MASTER. - Valid option names: background, bd, bg, borderwidth, cursor, - exportselection, fg, font, foreground, height, highlightbackground, - highlightcolor, highlightthickness, relief, selectbackground, - selectborderwidth, selectforeground, selectmode, setgrid, takefocus, - width, xscrollcommand, yscrollcommand, listvariable. + Valid option names: activestyle, background, bd, bg, borderwidth, + cursor, disabledforeground, exportselection, fg, font, + foreground, height, highlightbackground, highlightcolor, + highlightthickness, inactiveselectbackground (Tk 9.1+), + inactiveselectforeground (Tk 9.1+), justify, listvariable, + relief, selectbackground, selectborderwidth, selectforeground, + selectmode, setgrid, state, takefocus, width, xscrollcommand, + yscrollcommand. """ override def configure( @@ -4298,7 +4312,7 @@ class Listbox(Widget, XView, YView): """Set the fixed end oft the selection to INDEX.""" select_anchor = selection_anchor - override def selection_clear(self, first: str | int, last: str | int | None = None) -> None: + override def selection_clear(self, first: str | int, last: str | int | None = None): """Clear the selection from FIRST to LAST (included).""" select_clear = selection_clear @@ -4363,7 +4377,8 @@ class Menu(Widget): """Construct menu widget with the parent MASTER. Valid option names: activebackground, activeborderwidth, - activeforeground, background, bd, bg, borderwidth, cursor, + activeforeground, activerelief (Tk 9.0+), background, bd, bg, + borderwidth, cursor, disabledforeground, fg, font, foreground, postcommand, relief, selectcolor, takefocus, tearoff, tearoffcommand, title, type. """ @@ -4715,7 +4730,16 @@ class Menubutton(Widget): underline: int = -1, width: float | str = 0, wraplength: float | str = 0, - ) + ): + """Construct a menubutton widget with the parent MASTER. + + Valid option names: activebackground, activeforeground, anchor, + background, bd, bg, bitmap, borderwidth, compound, cursor, + direction, disabledforeground, fg, font, foreground, height, + highlightbackground, highlightcolor, highlightthickness, + image, indicatoron, justify, menu, padx, pady, relief, state, + takefocus, text, textvariable, underline, width, wraplength. + """ override def configure( self, @@ -4804,7 +4828,14 @@ class Message(Widget): textvariable: Variable = ..., # there's width but no height width: float | str = 0, - ) + ): + """Construct a message widget with the parent MASTER. + + Valid option names: anchor, aspect, background, bd, bg, borderwidth, + cursor, fg, font, foreground, highlightbackground, + highlightcolor, highlightthickness, justify, padx, pady, + relief, takefocus, text, textvariable, width. + """ override def configure( self, @@ -4903,12 +4934,13 @@ class Radiobutton(Widget): """Construct a radiobutton widget with the parent MASTER. Valid option names: activebackground, activeforeground, anchor, - background, bd, bg, bitmap, borderwidth, command, cursor, - disabledforeground, fg, font, foreground, height, - highlightbackground, highlightcolor, highlightthickness, image, - indicatoron, justify, padx, pady, relief, selectcolor, selectimage, - state, takefocus, text, textvariable, underline, value, variable, - width, wraplength. + background, bd, bg, bitmap, borderwidth, command, compound, + cursor, disabledforeground, fg, font, foreground, height, + highlightbackground, highlightcolor, highlightthickness, + image, indicatoron, justify, offrelief, overrelief, padx, + pady, relief, selectcolor, selectimage, state, takefocus, + text, textvariable, tristateimage, tristatevalue, underline, + value, variable, width, wraplength. """ override def configure( @@ -5308,9 +5340,11 @@ class Text(Widget, XView, YView): WIDGET-SPECIFIC OPTIONS - autoseparators, height, maxundo, - spacing1, spacing2, spacing3, - state, tabs, undo, width, wrap, + autoseparators, blockcursor, endline, + height, inactiveselectbackground, + insertunfocussed, locale (Tk 9.1+), maxundo, + spacing1, spacing2, spacing3, startline, + state, tabs, tabstyle, undo, width, wrap, """ @@ -6077,7 +6111,7 @@ class OptionMenu(Menubutton): variable: StringVar, value: str, *values: str, - command: ((StringVar) -> object) | None = ..., + command: ((str) -> object) | None = ..., name: str | None = None, ) -> None: """Construct an optionmenu widget with the parent MASTER, with @@ -6094,7 +6128,7 @@ class OptionMenu(Menubutton): variable: StringVar, value: str, *values: str, - command: ((StringVar) -> object) | None = ..., + command: ((str) -> object) | None = ..., ) -> None: """Construct an optionmenu widget with the parent MASTER, with the option textvariable set to VARIABLE, the initially selected @@ -6172,7 +6206,7 @@ class PhotoImage(Image, _PhotoImageLike): height: int = ..., palette: int | str = ..., width: int = ..., - ) -> None: + ): """Configure the image.""" config = configure @@ -6536,9 +6570,12 @@ class Spinbox(Widget, XView): buttondownrelief, buttonuprelief, command, disabledbackground, disabledforeground, format, from, - invalidcommand, increment, + invalidcommand, invcmd, increment, + locale (Tk 9.1+), + placeholder (Tk 9.0+), + placeholderforeground (Tk 9.0+), readonlybackground, state, to, - validate, validatecommand values, + validate, validatecommand, vcmd, values, width, wrap, """ @@ -6869,8 +6906,9 @@ class PanedWindow(Widget): WIDGET-SPECIFIC OPTIONS handlepad, handlesize, opaqueresize, - sashcursor, sashpad, sashrelief, - sashwidth, showhandle, + proxybackground, proxyborderwidth, + proxyrelief, sashcursor, sashpad, + sashrelief, sashwidth, showhandle, """ override def configure( diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/colorchooser.byi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/colorchooser.byi index d7f7750bef..d9cbfe7be1 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/colorchooser.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/colorchooser.byi @@ -1,3 +1,5 @@ +"""Interface to the native Tk color selection dialog.""" + from tkinter import Misc from tkinter.commondialog import Dialog from typing import ClassVar diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/commondialog.byi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/commondialog.byi index abd4f7f3b9..20b95d5e94 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/commondialog.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/commondialog.byi @@ -1,3 +1,5 @@ +"""Base class for the Tk common dialogs.""" + from collections.abc import Mapping from tkinter import Misc from typing import ClassVar diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/dialog.byi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/dialog.byi index 9aefbd87f0..20f5e7fdd4 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/dialog.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/dialog.byi @@ -1,3 +1,5 @@ +"""Classic Tk dialog box, wrapping the tk_dialog script.""" + from collections.abc import Mapping from tkinter import Widget from typing import Final @@ -7,7 +9,10 @@ __all__ = ["Dialog"] DIALOG_ICON: Final = "questhead" class Dialog(Widget): + """A modal dialog box built from the classic (non-themed) Tk widgets.""" + widgetName: str num: int init(self, master=None, cnf: Mapping[str, dynamic] = {}, **kw) - override def destroy(self) -> None + override def destroy(self): + """Do nothing; the dialog window is already destroyed.""" diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/filedialog.byi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/filedialog.byi index 60ca8153a3..3a1d75c399 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/filedialog.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/filedialog.byi @@ -95,13 +95,13 @@ class LoadFileDialog(FileDialog): """File selection dialog which checks that the file exists.""" title: str - override def ok_command(self) -> None + override def ok_command(self) class SaveFileDialog(FileDialog): """File selection dialog which checks that the file may be created.""" title: str - override def ok_command(self) -> None + override def ok_command(self) class _Dialog(commondialog.Dialog) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/font.byi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/font.byi index a286091e2e..e4ec7d4b30 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/font.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/font.byi @@ -1,3 +1,5 @@ +"""Utilities to help work with fonts in Tkinter.""" + import _tkinter import itertools import tkinter diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/messagebox.byi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/messagebox.byi index d27db7c539..1ece196b9c 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/messagebox.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/messagebox.byi @@ -1,3 +1,5 @@ +"""Interface to the standard Tk message boxes.""" + from tkinter import Misc from tkinter.commondialog import Dialog from typing import ClassVar, Final, Literal @@ -68,7 +70,7 @@ def askquestion( default: "yes" | "no" = ..., parent: Misc = ..., ) -> str: - """Ask a question""" + """Ask a question; return the symbolic name of the selected button""" def askokcancel( title: str | None = None, @@ -79,7 +81,7 @@ def askokcancel( default: "ok" | "cancel" = ..., parent: Misc = ..., ) -> bool: - """Ask if operation should proceed; return true if the answer is ok""" + """Ask if operation should proceed; return True if the answer is ok""" def askyesno( title: str | None = None, @@ -90,7 +92,7 @@ def askyesno( default: "yes" | "no" = ..., parent: Misc = ..., ) -> bool: - """Ask a question; return true if the answer is yes""" + """Ask a question; return True if the answer is yes""" def askyesnocancel( title: str | None = None, @@ -101,7 +103,7 @@ def askyesnocancel( default: "cancel" | "yes" | "no" = ..., parent: Misc = ..., ) -> bool | None: - """Ask a question; return true if the answer is yes, None if cancelled.""" + """Ask a question; return True if the answer is yes, None if cancelled""" def askretrycancel( title: str | None = None, @@ -112,4 +114,4 @@ def askretrycancel( default: "retry" | "cancel" = ..., parent: Misc = ..., ) -> bool: - """Ask if operation should be retried; return true if the answer is yes""" + """Ask if operation should be retried; return True if the answer is retry""" diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/simpledialog.byi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/simpledialog.byi index 080a88f349..1304072390 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/simpledialog.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/simpledialog.byi @@ -1,4 +1,4 @@ -"""This modules handles dialog boxes. +"""This module handles dialog boxes. It contains the following public symbols: diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/tix.byi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/tix.byi index ba86424fc8..94547e8dca 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/tix.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/tix.byi @@ -533,7 +533,7 @@ class HList(TixWidget, tkinter.XView, tkinter.YView): def entryconfigure(self, entry: str, cnf: dict[str, dynamic] = {}, **kw) -> Incomplete | None def nearest(self, y: int) -> str def see(self, entry: str) - override def selection_clear(self, cnf: dict[str, dynamic] = {}, **kw) -> None + override def selection_clear(self, cnf: dict[str, dynamic] = {}, **kw) def selection_includes(self, entry: str) -> bool def selection_set(self, first: str, last: str | None = None) def show_entry(self, entry: str) @@ -643,7 +643,7 @@ class TList(TixWidget, tkinter.XView, tkinter.YView): def info_up(self, index: int) -> int def nearest(self, x: int, y: int) -> int def see(self, index: int) - override def selection_clear(self, cnf: dict[str, dynamic] = {}, **kw) -> None + override def selection_clear(self, cnf: dict[str, dynamic] = {}, **kw) def selection_includes(self, index: int) -> bool def selection_set(self, first: int, last: int | None = None) @@ -662,7 +662,7 @@ class PanedWindow(TixWidget): init(self, master: tkinter.Widget | None, cnf: dict[str, dynamic] = {}, **kw) def add(self, name: str, cnf: dict[str, dynamic] = {}, **kw) def delete(self, name: str) - override def forget(self, name: str) -> None + override def forget(self, name: str) def panecget(self, entry: str, opt) def paneconfigure(self, entry: str, cnf: dict[str, dynamic] = {}, **kw) -> Incomplete | None def panes(self) -> list[tkinter.Widget] diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/ttk.byi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/ttk.byi index d96663b291..b30ab9d959 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/ttk.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/ttk.byi @@ -386,8 +386,8 @@ class Button(Widget): STANDARD OPTIONS - class, compound, cursor, image, state, style, takefocus, - text, textvariable, underline, width + class, compound, cursor, image, justify (Tk 9.0+), padding, + state, style, takefocus, text, textvariable, underline, width WIDGET-SPECIFIC OPTIONS @@ -462,8 +462,8 @@ class Checkbutton(Widget): STANDARD OPTIONS - class, compound, cursor, image, state, style, takefocus, - text, textvariable, underline, width + class, compound, cursor, image, justify (Tk 9.0+), padding, + state, style, takefocus, text, textvariable, underline, width WIDGET-SPECIFIC OPTIONS @@ -552,8 +552,10 @@ class Entry(Widget, tkinter.Entry): WIDGET-SPECIFIC OPTIONS - exportselection, invalidcommand, justify, show, state, - textvariable, validate, validatecommand, width + background, exportselection, font, foreground, invalidcommand, + justify, locale (Tk 9.1+), placeholder (Tk 9.0+), + placeholderforeground (Tk 9.0+), show, state, textvariable, + validate, validatecommand, width VALIDATION MODES @@ -682,12 +684,14 @@ class Combobox(Entry): STANDARD OPTIONS - class, cursor, style, takefocus + class, cursor, style, takefocus, xscrollcommand WIDGET-SPECIFIC OPTIONS - exportselection, justify, height, postcommand, state, - textvariable, values, width + background, exportselection, font, foreground, height, + invalidcommand, justify, locale (Tk 9.1+), placeholder (Tk 9.0+), + placeholderforeground (Tk 9.0+), postcommand, show, state, + textvariable, validate, validatecommand, values, width """ override def configure( @@ -875,13 +879,13 @@ class Label(Widget): STANDARD OPTIONS - class, compound, cursor, image, style, takefocus, text, - textvariable, underline, width + class, compound, cursor, image, state, style, takefocus, + text, textvariable, underline, width WIDGET-SPECIFIC OPTIONS - anchor, background, font, foreground, justify, padding, - relief, text, wraplength + anchor, background, borderwidth, font, foreground, justify, + padding, relief, text, textangle (Tk 9.1+), wraplength """ override def configure( @@ -958,8 +962,9 @@ class Labelframe(Widget): class, cursor, style, takefocus WIDGET-SPECIFIC OPTIONS - labelanchor, text, underline, padding, labelwidget, width, - height + + borderwidth, height, labelanchor, labelwidget, padding, + relief, text, underline, width """ override def configure( @@ -1027,8 +1032,8 @@ class Menubutton(Widget): STANDARD OPTIONS - class, compound, cursor, image, state, style, takefocus, - text, textvariable, underline, width + class, compound, cursor, image, justify (Tk 9.0+), padding, + state, style, takefocus, text, textvariable, underline, width WIDGET-SPECIFIC OPTIONS @@ -1163,7 +1168,7 @@ class Notebook(Widget): restored to its previous position. """ - override def forget(self, tab_id) -> None: + override def forget(self, tab_id): """Removes the tab specified by tab_id, unmaps and unmanages the associated window. """ @@ -1270,7 +1275,7 @@ class Panedwindow(Widget, tkinter.PanedWindow): weight """ - override def add(self, child: tkinter.Widget, *, weight: int = ..., **kw) -> None: + override def add(self, child: tkinter.Widget, *, weight: int = ..., **kw): """Add a child widget to the panedwindow in a new pane. The child argument is the name of the child widget @@ -1387,7 +1392,9 @@ class Progressbar(Widget): STANDARD OPTIONS - class, cursor, style, takefocus + anchor (Tk 9.0+), class, cursor, font (Tk 9.0+), + foreground (Tk 9.0+), justify (Tk 9.0+), style, takefocus, + text (Tk 9.0+), wraplength (Tk 9.0+) WIDGET-SPECIFIC OPTIONS @@ -1472,8 +1479,8 @@ class Radiobutton(Widget): STANDARD OPTIONS - class, compound, cursor, image, state, style, takefocus, - text, textvariable, underline, width + class, compound, cursor, image, justify (Tk 9.0+), padding, + state, style, takefocus, text, textvariable, underline, width WIDGET-SPECIFIC OPTIONS @@ -1550,7 +1557,7 @@ class Scale(Widget, tkinter.Scale): STANDARD OPTIONS - class, cursor, style, takefocus + class, cursor, state, style, takefocus WIDGET-SPECIFIC OPTIONS @@ -1839,7 +1846,10 @@ class Spinbox(Entry): WIDGET-SPECIFIC OPTIONS - to, from_, increment, values, wrap, format, command + background, command, exportselection, font, foreground, + format, from_, increment, justify, locale (Tk 9.1+), + placeholder (Tk 9.0+), placeholderforeground (Tk 9.0+), show, + state, textvariable, to, values, width, wrap """ override def configure( @@ -1959,7 +1969,10 @@ class Treeview(Widget, tkinter.XView, tkinter.YView): WIDGET-SPECIFIC OPTIONS - columns, displaycolumns, height, padding, selectmode, show + columns, displaycolumns, headingheight (Tk 9.1+), height, + padding, rowheight (Tk 9.1+), selectmode, selecttype (Tk 9.0+), + show, striped (Tk 9.0+), titlecolumns (Tk 9.0+), + titleitems (Tk 9.0+) ITEM OPTIONS @@ -2298,8 +2311,8 @@ class Treeview(Widget, tkinter.XView, tkinter.YView): ) -> _TreeviewTagDict | MaybeNone # can be None but annoying to check def tag_has(self, tagname: str, item: None = None) -> (*: str): - """If item is specified, returns 1 or 0 depending on whether the - specified item has the given tagname. Otherwise, returns a list of + """If item is specified, returns True if the specified item has the + given tagname, False otherwise. Otherwise, returns a list of all items which have the specified tag. * Availability: Tk 8.6 diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/tokenize.byi b/crates/ty_vendored/vendor/typeshed/stdlib/tokenize.byi index 1264ae4500..92355a86e7 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/tokenize.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/tokenize.byi @@ -32,7 +32,7 @@ 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 + from token export EXACT_TOKEN_TYPES __all__ = [ "AMPER", diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/turtle.byi b/crates/ty_vendored/vendor/typeshed/stdlib/turtle.byi index 25159d06ad..feb1368994 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/turtle.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/turtle.byi @@ -1566,7 +1566,7 @@ class RawTurtle(TPen, TNavigator): undobuffersize: int = 1000, visible: bool = True, ) - override def reset(self) -> None: + override def reset(self): """Delete the turtle's drawings and restore its default values. No argument. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/types.byi b/crates/ty_vendored/vendor/typeshed/stdlib/types.byi index 4fb055cdbe..5e3dcd2fb0 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/types.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/types.byi @@ -351,8 +351,8 @@ else: init(self, **kwargs: dynamic) override def __eq__(self, value: object, /) -> bool override def __getattribute__(self, name: str, /) -> dynamic - override def __setattr__(self, name: str, value: dynamic, /) -> None - override def __delattr__(self, name: str, /) -> None + override def __setattr__(self, name: str, value: dynamic, /) + override def __delattr__(self, name: str, /) @disjoint_base class ModuleType: @@ -495,7 +495,7 @@ final class AsyncGeneratorType[out Yield, in Send = None](AsyncGenerator[Yield, # Non-default variations to accommodate coroutines -final class CoroutineType[out Yield, in Send, out Return](Coroutine[Yield, Send, Return]): +final class CoroutineType[out Yield, in SendT_nd, out ReturnT_nd](Coroutine[Yield, SendT_nd, ReturnT_nd]): __name__: str """name of the coroutine""" @@ -516,13 +516,13 @@ final class CoroutineType[out Yield, in Send, out Return](Coroutine[Yield, Send, def cr_state(self) -> "CORO_CREATED" | "CORO_SUSPENDED" | "CORO_RUNNING" | "CORO_CLOSED": """state of the coroutine""" - override def close(self) -> None: + override def close(self): """close() -> raise GeneratorExit inside coroutine.""" - override def __await__(self) -> Generator[dynamic, None, Return]: + override def __await__(self) -> Generator[dynamic, None, ReturnT_nd]: """Return an iterator to be used in await expression.""" - override def send(self, arg: Send, /) -> Yield: + override def send(self, arg: SendT_nd, /) -> Yield: """send(arg) -> send 'arg' into coroutine, return next iterated value or raise StopIteration. """ @@ -816,8 +816,8 @@ class DynamicClassAttribute(property): doc: str | None = None, ) override def __get__(self, instance: dynamic, ownerclass: type | None = None) -> dynamic - override def __set__(self, instance: dynamic, value: dynamic) -> None - override def __delete__(self, instance: dynamic) -> None + override def __set__(self, instance: dynamic, value: dynamic) + override def __delete__(self, instance: dynamic) override def getter(self, fget: (dynamic) -> dynamic) -> DynamicClassAttribute override def setter(self, fset: (dynamic, dynamic) -> object) -> DynamicClassAttribute override def deleter(self, fdel: (dynamic) -> object) -> DynamicClassAttribute diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/typing.byi b/crates/ty_vendored/vendor/typeshed/stdlib/typing.byi index 231cba6574..12e2f59d4f 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/typing.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/typing.byi @@ -27,7 +27,7 @@ import typing_extensions from _collections_abc import dict_items, dict_keys, dict_values from _typeshed import IdentityFunction, ReadableBuffer, SupportsGetItem, SupportsGetItemViewable, SupportsKeysAndGetItem, Viewable from abc import ABCMeta, abstractmethod -from re import Match as Match, Pattern as Pattern +from re export Match, Pattern from types import ( BuiltinFunctionType, CodeType, @@ -1144,7 +1144,7 @@ def no_type_check[F: (...) -> dynamic](arg: F) -> F: """ if sys.version_info < (3, 15): - @deprecated("Deprecated since Python 3.13; removed in Python 3.15.") + @deprecated("Deprecated; removed in Python 3.15.") def no_type_check_decorator[Parameters: (*: *, **: *), Element](decorator: (**Parameters) -> Element) -> (**Parameters) -> Element: """Decorator to give another decorator the @no_type_check effect. @@ -1473,11 +1473,11 @@ protocol Awaitable[out Element]: # Non-default variations to accommodate coroutines, and `AwaitableGenerator` having a 4th type parameter. -class Coroutine[out Yield, in Send, out Return](Awaitable[Return]): +class Coroutine[out Yield, in SendT_nd, out ReturnT_nd](Awaitable[ReturnT_nd]): __name__: str __qualname__: str - abstract def send(self, value: Send, /) -> Yield: + abstract def send(self, value: SendT_nd, /) -> Yield: """Send a value into the coroutine. Return next yielded value or raise StopIteration. """ @@ -2120,7 +2120,6 @@ if sys.version_info >= (3, 11): kw_only_default: bool = False, frozen_default: bool = False, # on 3.11, runtime accepts it as part of kwargs field_specifiers: (*: type[dynamic] | (...) -> dynamic) = (), - **kwargs: dynamic, ) -> IdentityFunction: """Decorator to mark an object as providing dataclass-like behavior. @@ -2221,11 +2220,11 @@ class NamedTuple((*: dynamic)): @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: dynamic) -> None - final class def _make(cls, iterable: Iterable[dynamic]) -> typing_extensions.Self + final class def _make(cls, iterable: Iterable[dynamic]) -> typing_extensions.Self # ty:ignore[invalid-type-form] final def _asdict(self) -> dict[str, dynamic] - final def _replace(self, **kwargs: dynamic) -> typing_extensions.Self + final def _replace(self, **kwargs: dynamic) -> typing_extensions.Self # ty:ignore[invalid-type-form] if sys.version_info >= (3, 13): - def __replace__(self, **kwargs: dynamic) -> typing_extensions.Self + def __replace__(self, **kwargs: dynamic) -> typing_extensions.Self # ty:ignore[invalid-type-form] # 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 @@ -2271,7 +2270,7 @@ class _TypedDict(Mapping[str, object], metaclass=ABCMeta): def __ior__(self, value: typing_extensions.Self, /) -> typing_extensions.Self if sys.version_info >= (3, 14): - from annotationlib import ForwardRef as ForwardRef + from annotationlib export ForwardRef def evaluate_forward_ref( forward_ref: ForwardRef, diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/typing_extensions.byi b/crates/ty_vendored/vendor/typeshed/stdlib/typing_extensions.byi index 20967e3e18..5bac9c56be 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/typing_extensions.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/typing_extensions.byi @@ -3,32 +3,32 @@ 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 collections.abc export ( + AsyncGenerator, + AsyncIterable, + AsyncIterator, + Awaitable, + Collection, + Container, + Coroutine, + Generator, + Hashable, + ItemsView, + Iterable, + Iterator, + KeysView, + Mapping, + MappingView, + MutableMapping, + MutableSequence, + MutableSet, + Reversible, + Sequence, + Sized, + ValuesView, ) from contextlib import AbstractAsyncContextManager as AsyncContextManager, AbstractContextManager as ContextManager -from re import Match as Match, Pattern as Pattern +from re export Match, Pattern from types import GenericAlias, ModuleType, UnionType from typing import ( # noqa: Y022,Y037,Y038,Y039,UP035 IO as IO, @@ -463,7 +463,7 @@ OrderedDict = _Alias() """Deprecated alias to collections.OrderedDict.""" if sys.version_info >= (3, 13): - from typing import get_type_hints as get_type_hints + from typing export get_type_hints else: def get_type_hints( obj: dynamic, globalns: dict[str, dynamic] | None = None, localns: Mapping[str, dynamic] | None = None, include_extras: bool = False @@ -589,21 +589,21 @@ _AnnotatedAlias: dynamic # undocumented # 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, + from typing export ( + LiteralString, + NamedTuple, + Never, + NewType, + NotRequired, + Required, + Self, + Unpack, + assert_never, + assert_type, + clear_overloads, + dataclass_transform, + get_overloads, + reveal_type, ) else: Self: _SpecialForm @@ -797,7 +797,6 @@ else: kw_only_default: bool = False, frozen_default: bool = False, field_specifiers: (*: type[dynamic] | (...) -> dynamic) = (), - **kwargs: object, ) -> IdentityFunction: """Decorator that marks a function, class, or metaclass as providing dataclass-like behavior. @@ -889,9 +888,9 @@ else: init(self, typename: str, fields: Iterable[(str, dynamic)] = ...) init(self, typename: str, fields: None = None, **kwargs: dynamic) - class def _make(cls, iterable: Iterable[dynamic]) -> Self + class def _make(cls, iterable: Iterable[dynamic]) -> Self # ty:ignore[invalid-type-form] def _asdict(self) -> dict[str, dynamic] - def _replace(self, **kwargs: dynamic) -> Self + def _replace(self, **kwargs: dynamic) -> Self # ty:ignore[invalid-type-form] class NewType: """NewType creates simple unique types with almost zero @@ -915,17 +914,17 @@ else: __name__: str 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, + from collections.abc export Buffer + from types export get_original_bases + from typing export ( + SupportsAbs, + SupportsBytes, + SupportsComplex, + SupportsFloat, + SupportsIndex, + SupportsInt, + SupportsRound, + override, ) else: def override(arg: _F, /) -> _F: @@ -1067,7 +1066,7 @@ else: def __round__(self, ndigits: int, /) -> _T_co if sys.version_info >= (3, 14): - from io import Reader as Reader, Writer as Writer + from io export Reader, Writer else: @runtime_checkable class Reader(Protocol[_T_co]): @@ -1098,17 +1097,17 @@ else: """Write *data* to the output stream and return the number of items written.""" 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, - get_protocol_members as get_protocol_members, - is_protocol as is_protocol, + from types export CapsuleType + from typing export ( + NoDefault, + ParamSpec, + ReadOnly, + TypeIs, + TypeVar, + get_protocol_members, + is_protocol, ) - from warnings import deprecated as deprecated + from warnings export deprecated else: def is_protocol(tp: type, /) -> bool: """Return True if the given type is a Protocol. @@ -1198,10 +1197,10 @@ else: """ - message: LiteralString + message: literal str category: type[Warning] | None stacklevel: int - init(self, message: LiteralString, /, *, category: type[Warning] | None = ..., stacklevel: int = 1) + init(self, message: literal str, /, *, category: type[Warning] | None = ..., stacklevel: int = 1) def __call__(self, arg: _T, /) -> _T final class TypeVar: @@ -1309,7 +1308,7 @@ else: For example:: - def is_awaitable(val: object) -> TypeIs[Awaitable[Any]]: + def is_awaitable(val: object) -> TypeIs[Awaitable[object]]: return hasattr(val, '__await__') def f(val: Union[int, Awaitable[int]]) -> int: @@ -1323,7 +1322,7 @@ else: """ if sys.version_info >= (3, 15): - from typing import TypeVarTuple as TypeVarTuple + from typing export TypeVarTuple else: final class TypeVarTuple: """Type variable tuple.""" @@ -1369,7 +1368,7 @@ else: # 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 + from typing export TypeAliasType else: final class TypeAliasType: """Create named, parameterized type aliases. @@ -1410,7 +1409,7 @@ else: let __module__: str | None # Returns typing._GenericAlias, which isn't stubbed. def __getitem__(self, parameters: Incomplete | (*: Incomplete)) -> AnnotationForm - def __init_subclass__(cls, *args: Unused, **kwargs: Unused) -> NoReturn + def __init_subclass__(cls, *args: Unused, **kwargs: Unused) -> Never def __or__(self, right: dynamic, /) -> _SpecialForm def __ror__(self, left: dynamic, /) -> _SpecialForm @@ -1468,9 +1467,9 @@ See PEP 747 for more information. # PEP 649/749 if sys.version_info >= (3, 14): - from typing import evaluate_forward_ref as evaluate_forward_ref + from typing export evaluate_forward_ref - from annotationlib import Format as Format, get_annotations as get_annotations, type_repr as type_repr + from annotationlib export Format, get_annotations, type_repr else: class Format(enum.IntEnum): """An enumeration.""" @@ -1604,7 +1603,7 @@ else: # PEP 661 if sys.version_info >= (3, 15): - from builtins import sentinel as sentinel + from builtins export sentinel else: class sentinel: """Create a unique sentinel object. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/unittest/__init__.byi b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/__init__.byi index f208e20746..fced5faf78 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/unittest/__init__.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/__init__.byi @@ -47,30 +47,30 @@ SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. import sys from unittest.async_case import * -from .case import ( - FunctionTestCase as FunctionTestCase, - SkipTest as SkipTest, - TestCase as TestCase, - addModuleCleanup as addModuleCleanup, - expectedFailure as expectedFailure, - skip as skip, - skipIf as skipIf, - skipUnless as skipUnless, +from .case export ( + FunctionTestCase, + SkipTest, + TestCase, + addModuleCleanup, + expectedFailure, + skip, + skipIf, + skipUnless, ) -from .loader import TestLoader as TestLoader, defaultTestLoader as defaultTestLoader -from .main import TestProgram as TestProgram, main as main -from .result import TestResult as TestResult -from .runner import TextTestResult as TextTestResult, TextTestRunner as TextTestRunner -from .signals import ( - installHandler as installHandler, - registerResult as registerResult, - removeHandler as removeHandler, - removeResult as removeResult, +from .loader export TestLoader, defaultTestLoader +from .main export TestProgram, main +from .result export TestResult +from .runner export TextTestResult, TextTestRunner +from .signals export ( + installHandler, + registerResult, + removeHandler, + removeResult, ) -from .suite import BaseTestSuite as BaseTestSuite, TestSuite as TestSuite +from .suite export BaseTestSuite, TestSuite if sys.version_info >= (3, 11): - from .case import doModuleCleanups as doModuleCleanups, enterModuleContext as enterModuleContext + from .case export doModuleCleanups, enterModuleContext __all__ = [ "IsolatedAsyncioTestCase", @@ -96,7 +96,7 @@ __all__ = [ ] if sys.version_info < (3, 13): - from .loader import findTestCases as findTestCases, getTestCaseNames as getTestCaseNames, makeSuite as makeSuite + from .loader export findTestCases, getTestCaseNames, makeSuite __all__ += ["getTestCaseNames", "makeSuite", "findTestCases"] diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/unittest/case.byi b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/case.byi index be75e6242d..3e1df0daeb 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/unittest/case.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/case.byi @@ -149,7 +149,7 @@ class TestCase: def run(self, result: unittest.result.TestResult | None = None) -> unittest.result.TestResult | None def __call__(self, result: unittest.result.TestResult | None = ...) -> unittest.result.TestResult | None - def skipTest(self, reason: dynamic) -> NoReturn: + def skipTest(self, reason: dynamic) -> Never: """Skip this test.""" def subTest(self, msg: dynamic = ..., **params: dynamic) -> AbstractContextManager[None]: @@ -567,7 +567,7 @@ class TestCase: # assertDictEqual accepts only true dict instances. We can't use that here, since that would make # assertDictEqual incompatible with TypedDict. def assertDictEqual(self, d1: Mapping[dynamic, object], d2: Mapping[dynamic, object], msg: dynamic = None) - def fail(self, msg: dynamic = None) -> NoReturn: + def fail(self, msg: dynamic = None) -> Never: """Fail immediately, with the given message.""" def countTestCases(self) -> int diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/unittest/loader.byi b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/loader.byi index a567857d2a..097ffa9814 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/unittest/loader.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/loader.byi @@ -88,21 +88,21 @@ class TestLoader: defaultTestLoader: TestLoader if sys.version_info < (3, 13): - @deprecated("Deprecated since Python 3.11; removed in Python 3.13.") + @deprecated("Deprecated; removed in Python 3.13.") def getTestCaseNames( testCaseClass: type[unittest.case.TestCase], prefix: str, sortUsing: SortComparisonMethod = ..., testNamePatterns: list[str] | None = None, ) -> Sequence[str] - @deprecated("Deprecated since Python 3.11; removed in Python 3.13.") + @deprecated("Deprecated; removed in Python 3.13.") def makeSuite( testCaseClass: type[unittest.case.TestCase], prefix: str = "test", sortUsing: SortComparisonMethod = ..., suiteClass: SuiteClass = ..., ) -> unittest.suite.TestSuite - @deprecated("Deprecated since Python 3.11; removed in Python 3.13.") + @deprecated("Deprecated; removed in Python 3.13.") def findTestCases( module: ModuleType, prefix: str = "test", sortUsing: SortComparisonMethod = ..., suiteClass: SuiteClass = ... ) -> unittest.suite.TestSuite diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/unittest/main.byi b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/main.byi index 81f2549755..e8b3fb759c 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/unittest/main.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/main.byi @@ -70,7 +70,7 @@ class TestProgram: ) -> None if sys.version_info < (3, 13): - @deprecated("Deprecated since Python 3.11; removed in Python 3.13.") + @deprecated("Deprecated; removed in Python 3.13.") def usageExit(self, msg: dynamic = None) -> None def parseArgs(self, argv: list[str]) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/unittest/mock.byi b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/mock.byi index 7ad3f5aeeb..37204f6c32 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/unittest/mock.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/mock.byi @@ -217,32 +217,32 @@ class NonCallableMock(Base, dynamic): **kwargs: dynamic, ) def __getattr__(self, name: str) -> dynamic - override def __delattr__(self, name: str) -> None - override def __setattr__(self, name: str, value: dynamic) -> None + override def __delattr__(self, name: str) + override def __setattr__(self, name: str, value: dynamic) override def __dir__(self) -> list[str]: """Filter the output of `dir(mock)` to only useful members.""" def assert_called_with(self, *args: dynamic, **kwargs: dynamic): - """assert that the last call was made with the specified arguments. + """Assert that the last call was made with the specified arguments. Raises an AssertionError if the args and keyword args passed in are different to the last call to the mock. """ def assert_not_called(self): - """assert that the mock was never called.""" + """Assert that the mock was never called.""" def assert_called_once_with(self, *args: dynamic, **kwargs: dynamic): - """assert that the mock was called exactly once and that call was + """Assert that the mock was called exactly once and that call was with the specified arguments. """ def _format_mock_failure_message(self, args: dynamic, kwargs: dynamic, action: str = "call") -> str def assert_called(self): - """assert that the mock was called at least once""" + """Assert that the mock was called at least once.""" def assert_called_once(self): - """assert that the mock was called only once.""" + """Assert that the mock was called only once.""" def reset_mock(self, visited: dynamic = None, *, return_value: bool = False, side_effect: bool = False): """Restore the mock object to its initial state.""" @@ -261,7 +261,7 @@ class NonCallableMock(Base, dynamic): """ def assert_any_call(self, *args: dynamic, **kwargs: dynamic): - """assert the mock has been called with the specified arguments. + """Assert the mock has been called with the specified arguments. The assert passes if the mock has *ever* been called, unlike `assert_called_with` and `assert_called_once_with` that only pass if @@ -269,7 +269,7 @@ class NonCallableMock(Base, dynamic): """ def assert_has_calls(self, calls: Sequence[_Call], any_order: bool = False): - """assert the mock has been called with the specified calls. + """Assert the mock has been called with the specified calls. The `mock_calls` list is checked for the calls. If `any_order` is False (the default) then the calls must be @@ -880,7 +880,7 @@ class AsyncMock(AsyncMockMixin, AsyncMagicMixin, Mock): # Improving the `reset_mock` signature. # It is defined on `AsyncMockMixin` with `*args, **kwargs`, which is not ideal. # But, `NonCallableMock` super-class has the better version. - override def reset_mock(self, visited: dynamic = None, *, return_value: bool = False, side_effect: bool = False) -> None: + override def reset_mock(self, visited: dynamic = None, *, return_value: bool = False, side_effect: bool = False): """ See :func:`.Mock.reset_mock()` """ diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/urllib/request.byi b/crates/ty_vendored/vendor/typeshed/stdlib/urllib/request.byi index cbec0b2cba..6b4aaac875 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/urllib/request.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/urllib/request.byi @@ -76,8 +76,8 @@ from http.client import HTTPConnection, HTTPMessage, HTTPResponse from http.cookiejar import CookieJar from re import Pattern from typing import ClassVar, Literal, Protocol, TypeAlias, TypeVar, type_check_only -from typing_extensions import deprecated -from urllib.error import HTTPError as HTTPError +from typing_extensions import Never, deprecated +from urllib.error export HTTPError from urllib.response import addclosehook, addinfourl __all__ = [ @@ -278,7 +278,7 @@ if sys.version_info >= (3, 14): else: if sys.platform == "win32": - from nturl2path import pathname2url as pathname2url, url2pathname as url2pathname + from nturl2path export pathname2url, url2pathname else: def url2pathname(pathname: str) -> str: """OS-specific conversion from a relative URL of the 'file' scheme @@ -449,13 +449,13 @@ class HTTPPasswordMgr: """Accept authority or URI and extract only the authority and path.""" class HTTPPasswordMgrWithDefaultRealm(HTTPPasswordMgr): - override def add_password(self, realm: str | None, uri: str | Sequence[str], user: str, passwd: str) -> None + override def add_password(self, realm: str | None, uri: str | Sequence[str], user: str, passwd: str) override def find_user_password(self, realm: str | None, authuri: str) -> (str | None, str | None) class HTTPPasswordMgrWithPriorAuth(HTTPPasswordMgrWithDefaultRealm): override 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) def is_authenticated(self, authuri: str) -> bool | None @@ -582,7 +582,7 @@ class CacheFTPHandler(FTPHandler): def clear_cache(self) # undocumented class UnknownHandler(BaseHandler): - def unknown_open(self, req: Request) -> NoReturn + def unknown_open(self, req: Request) -> Never class HTTPErrorProcessor(BaseHandler): """Process HTTP error responses.""" diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/urllib/response.byi b/crates/ty_vendored/vendor/typeshed/stdlib/urllib/response.byi index f61499fb63..5cf0b189af 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/urllib/response.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/urllib/response.byi @@ -22,13 +22,13 @@ class addbase(tempfile._TemporaryFileWrapper[bytes]): init(self, fp: IO[bytes]) override def __exit__( self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None - ) -> None + ) # These methods don't actually exist, but the class inherits at runtime from # tempfile._TemporaryFileWrapper, which uses __getattr__ to delegate to the # underlying file object. To satisfy the BinaryIO interface, we pretend that this # class has these additional methods. override def write(self, s: ReadableBuffer) -> int - override def writelines(self, lines: Iterable[ReadableBuffer]) -> None + override def writelines(self, lines: Iterable[ReadableBuffer]) class addclosehook(addbase): """Class to add a close hook to an open file.""" diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/uuid.byi b/crates/ty_vendored/vendor/typeshed/stdlib/uuid.byi index 773269f757..afd3e47caa 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/uuid.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/uuid.byi @@ -61,7 +61,7 @@ import sys from _typeshed import Unused from enum import Enum from typing import Final, TypeAlias -from typing_extensions import LiteralString +from typing_extensions import Never private type FieldsType = (int, int, int, int, int, int) @@ -200,7 +200,7 @@ class UUID: def __gt__(self, other: UUID) -> bool def __ge__(self, other: UUID) -> bool override def __hash__(self) -> builtins.int - override def __setattr__(self, name: Unused, value: Unused) -> NoReturn + override def __setattr__(self, name: Unused, value: Unused) -> Never def getnode() -> int: """Get the hardware address as a 48-bit positive integer. @@ -272,10 +272,10 @@ final NAMESPACE_DNS: UUID final NAMESPACE_URL: UUID final NAMESPACE_OID: UUID final NAMESPACE_X500: UUID -final RESERVED_NCS: LiteralString -final RFC_4122: LiteralString -final RESERVED_MICROSOFT: LiteralString -final RESERVED_FUTURE: LiteralString +final RESERVED_NCS: literal str +final RFC_4122: literal str +final RESERVED_MICROSOFT: literal str +final RESERVED_FUTURE: literal str if sys.version_info >= (3, 12): def main(): diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/warnings.byi b/crates/ty_vendored/vendor/typeshed/stdlib/warnings.byi index e8cd518986..adad64bd2c 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/warnings.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/warnings.byi @@ -2,11 +2,11 @@ import re import sys -from _warnings import warn as warn, warn_explicit as warn_explicit +from _warnings export warn, warn_explicit from collections.abc import Sequence from types import ModuleType, TracebackType from typing import Generic, Literal, TypeAlias -from typing_extensions import LiteralString, TypeVar +from typing_extensions import TypeVar __all__ = [ "warn", @@ -229,8 +229,8 @@ if sys.version_info >= (3, 13): """ - message: LiteralString + message: literal str category: type[Warning] | None stacklevel: int - init(self, message: LiteralString, /, *, category: type[Warning] | None = ..., stacklevel: int = 1) + init(self, message: literal str, /, *, category: type[Warning] | None = ..., stacklevel: int = 1) def __call__[Element](self, arg: Element, /) -> Element diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/wave.byi b/crates/ty_vendored/vendor/typeshed/stdlib/wave.byi index f8ff5fa81e..691526b4b9 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/wave.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/wave.byi @@ -75,7 +75,7 @@ is destroyed. import sys from _typeshed import ReadableBuffer, StrOrBytesPath, Unused from typing import Final, Literal, NamedTuple, TypeAlias -from typing_extensions import Self, deprecated +from typing_extensions import Never, Self, deprecated __all__ = ["open", "Error", "Wave_read", "Wave_write"] if sys.version_info >= (3, 15): @@ -157,10 +157,10 @@ class Wave_read: def getcompname(self) -> str def getparams(self) -> _wave_params if sys.version_info < (3, 15): - @deprecated("Deprecated since Python 3.13; will be removed in Python 3.15.") + @deprecated("Deprecated; will be removed in Python 3.15.") def getmarkers(self) -> None - @deprecated("Deprecated since Python 3.13; will be removed in Python 3.15.") - def getmark(self, id: dynamic) -> NoReturn + @deprecated("Deprecated; will be removed in Python 3.15.") + def getmark(self, id: dynamic) -> Never def setpos(self, pos: int) def readframes(self, nframes: int) -> bytes @@ -222,11 +222,11 @@ class Wave_write: def getparams(self) -> _wave_params if sys.version_info < (3, 15): - @deprecated("Deprecated since Python 3.13; will be removed in Python 3.15.") - def setmark(self, id: dynamic, pos: dynamic, name: dynamic) -> NoReturn - @deprecated("Deprecated since Python 3.13; will be removed in Python 3.15.") - def getmark(self, id: dynamic) -> NoReturn - @deprecated("Deprecated since Python 3.13; will be removed in Python 3.15.") + @deprecated("Deprecated; will be removed in Python 3.15.") + def setmark(self, id: dynamic, pos: dynamic, name: dynamic) -> Never + @deprecated("Deprecated; will be removed in Python 3.15.") + def getmark(self, id: dynamic) -> Never + @deprecated("Deprecated; will be removed in Python 3.15.") def getmarkers(self) -> None def tell(self) -> int diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/weakref.byi b/crates/ty_vendored/vendor/typeshed/stdlib/weakref.byi index a66437da2a..6037931454 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/weakref.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/weakref.byi @@ -6,8 +6,8 @@ https://peps.python.org/pep-0205/ """ from _typeshed import SupportsKeysAndGetItem -from _weakref import getweakrefcount as getweakrefcount, getweakrefs as getweakrefs, proxy as proxy -from _weakrefset import WeakSet as WeakSet +from _weakref export getweakrefcount, getweakrefs, proxy +from _weakrefset export WeakSet from collections.abc import Callable, Iterable, Iterator, Mapping, MutableMapping from types import GenericAlias from typing import ClassVar, Generic, ParamSpec, TypeVar, final @@ -103,8 +103,8 @@ class WeakValueDictionary[in out Key, in out Value](MutableMapping[Key, Value]): override def __len__(self) -> int override def __getitem__(self, key: Key) -> Value - override def __setitem__(self, key: Key, value: Value) -> None - override def __delitem__(self, key: Key) -> None + override def __setitem__(self, key: Key, value: Value) + override def __delitem__(self, key: Key) override def __contains__(self, key: object) -> bool override def __iter__(self) -> Iterator[Key] def copy(self) -> WeakValueDictionary[Key, Value] @@ -189,8 +189,8 @@ class WeakKeyDictionary[in out Key, in out Value](MutableMapping[Key, Value]): override def __len__(self) -> int override def __getitem__(self, key: Key) -> Value - override def __setitem__(self, key: Key, value: Value) -> None - override def __delitem__(self, key: Key) -> None + override def __setitem__(self, key: Key, value: Value) + override def __delitem__(self, key: Key) override def __contains__(self, key: object) -> bool override def __iter__(self) -> Iterator[Key] def copy(self) -> WeakKeyDictionary[Key, Value] diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/webbrowser.byi b/crates/ty_vendored/vendor/typeshed/stdlib/webbrowser.byi index 77336a45c6..541ae37b53 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/webbrowser.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/webbrowser.byi @@ -114,7 +114,7 @@ if sys.platform == "win32": if sys.platform == "darwin": if sys.version_info < (3, 13): - @deprecated("Deprecated since Python 3.11; removed in Python 3.13.") + @deprecated("Deprecated; removed in Python 3.13.") class MacOSX(BaseBrowser): """Launcher class for Aqua browsers on Mac OS X diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/handlers.byi b/crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/handlers.byi index e83d87c32f..5fb71395f2 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/handlers.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/handlers.byi @@ -185,9 +185,9 @@ class SimpleHandler(BaseHandler): ) override def get_stdin(self) -> InputStream override def get_stderr(self) -> ErrorStream - override def add_cgi_vars(self) -> None - override def _write(self, data: bytes) -> None - override def _flush(self) -> None + override def add_cgi_vars(self) + override def _write(self, data: bytes) + override def _flush(self) class BaseCGIHandler(SimpleHandler): """CGI-like systems using input/output/error streams and environ mapping diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/validate.byi b/crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/validate.byi index 87a3272a2f..c2938c3075 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/validate.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/validate.byi @@ -108,6 +108,7 @@ Some of the things this checks: from _typeshed.wsgi import ErrorStream, InputStream, WSGIApplication from collections.abc import Callable, Iterable, Iterator from typing import TypeAlias +from typing_extensions import Never __all__ = ["validator"] @@ -134,7 +135,7 @@ class InputWrapper: def readline(self, size: int = ...) -> bytes def readlines(self, hint: int = ...) -> bytes def __iter__(self) -> Iterator[bytes] - def close(self) -> NoReturn + def close(self) -> Never class ErrorWrapper: errors: ErrorStream @@ -142,7 +143,7 @@ class ErrorWrapper: def write(self, s: str) def flush(self) def writelines(self, seq: Iterable[str]) - def close(self) -> NoReturn + def close(self) -> Never private type WriterCallback = (bytes) -> dynamic diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/xml/__init__.byi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/__init__.byi index 8aace71da1..8bd8f95e7c 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/xml/__init__.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/xml/__init__.byi @@ -24,4 +24,4 @@ __all__ = ["dom", "parsers", "sax", "etree"] # noqa: F822 # pyright: ignore[re if sys.version_info >= (3, 15): __all__ += ["is_valid_name"] - from xml.utils import is_valid_name as is_valid_name, is_valid_text as is_valid_text + from xml.utils export is_valid_name, is_valid_text diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/__init__.byi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/__init__.byi index 992e479c51..9527ab6953 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/__init__.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/__init__.byi @@ -16,7 +16,7 @@ pulldom -- DOM builder supporting on-demand tree-building for selected from typing import Final, Literal -from .domreg import getDOMImplementation as getDOMImplementation, registerDOMImplementation as registerDOMImplementation +from .domreg export getDOMImplementation, registerDOMImplementation class Node: """Class giving the NodeType constants.""" diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/expatbuilder.byi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/expatbuilder.byi index 412df8a0e7..9f2646b882 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/expatbuilder.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/expatbuilder.byi @@ -6,6 +6,7 @@ This avoids all the overhead of SAX and pulldom to gain performance. from _typeshed import ReadableBuffer, SupportsRead from typing import Final, TypeAlias +from typing_extensions import Never from xml.dom.minidom import Document, DocumentFragment, DOMImplementation, Element, Node, TypeInfo from xml.dom.xmlbuilder import DOMBuilderFilter, Options from xml.parsers.expat import XMLParserType @@ -127,7 +128,7 @@ class FragmentBuilder(ExpatBuilder): originalDocument: Document context: Node init(self, context: Node, options: Options | None = None) - override def reset(self) -> None + override def reset(self) override def parseFile(self, file: SupportsRead[ReadableBuffer | str]) -> DocumentFragment: """Parse a document fragment from a file object, returning the fragment node. @@ -171,13 +172,13 @@ class InternalSubsetExtractor(ExpatBuilder): def getSubset(self) -> str: """Return the internal subset as a string.""" - override def parseFile(self, file: SupportsRead[ReadableBuffer | str]) -> None - override def parseString(self, string: str | ReadableBuffer) -> None + override def parseFile(self, file: SupportsRead[ReadableBuffer | str]) + override def parseString(self, string: str | ReadableBuffer) override def start_doctype_decl_handler( self, name: str, publicId: str | None, systemId: str | None, has_internal_subset: bool - ) -> None - override def end_doctype_decl_handler(self) -> NoReturn - override def start_element_handler(self, name: str, attrs: list[str]) -> NoReturn + ) + override def end_doctype_decl_handler(self) -> Never + override def start_element_handler(self, name: str, attrs: list[str]) -> Never def parse(file: str | SupportsRead[ReadableBuffer | str], namespaces: bool = True) -> Document: """Parse a document, returning the resulting Document node. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/minidom.byi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/minidom.byi index 448ad6d600..dff7eaa0b7 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/minidom.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/minidom.byi @@ -21,7 +21,7 @@ from _typeshed import Incomplete, ReadableBuffer, SupportsRead, SupportsWrite from collections.abc import Iterable, Sequence from types import TracebackType from typing import ClassVar, Generic, Literal, Protocol, TypeAlias, TypeVar, type_check_only -from typing_extensions import Self +from typing_extensions import Never, Self from xml.dom.minicompat import EmptyNodeList, NodeList from xml.dom.xmlbuilder import DocumentLS, DOMImplementationLS from xml.sax.xmlreader import XMLReader @@ -209,7 +209,7 @@ class Attr(Node): init( self, qName: str, namespaceURI: str | None = None, localName: str | None = None, prefix: str | None = None ) - override def unlink(self) -> None + override def unlink(self) let isId: bool let schemaType: TypeInfo @@ -309,7 +309,7 @@ class Element(Node): init( self, tagName: str, namespaceURI: str | None = None, prefix: str | None = None, localName: str | None = None ) - override def unlink(self) -> None + override def unlink(self) def getAttribute(self, attname: str) -> str: """Returns the value of the specified attribute. @@ -380,14 +380,14 @@ class Childless: childNodes: EmptyNodeList let firstChild: None let lastChild: None - def appendChild(self, node: NodesThatAreChildren | DocumentFragment) -> NoReturn + def appendChild(self, node: NodesThatAreChildren | DocumentFragment) -> Never def hasChildNodes(self) -> False def insertBefore( self, newChild: NodesThatAreChildren | DocumentFragment, refChild: NodesThatAreChildren | None - ) -> NoReturn - def removeChild(self, oldChild: NodesThatAreChildren) -> NoReturn + ) -> Never + def removeChild(self, oldChild: NodesThatAreChildren) -> Never def normalize(self) - def replaceChild(self, newChild: NodesThatAreChildren | DocumentFragment, oldChild: NodesThatAreChildren) -> NoReturn + def replaceChild(self, newChild: NodesThatAreChildren | DocumentFragment, oldChild: NodesThatAreChildren) -> Never class ProcessingInstruction(Childless, Node): __slots__ = ("target", "data") @@ -494,7 +494,7 @@ class CDATASection(Text): nextSibling: DocumentFragmentChildren | ElementChildren | None previousSibling: DocumentFragmentChildren | ElementChildren | None - override def writexml(self, writer: SupportsWrite[str], indent: str = "", addindent: str = "", newl: str = "") -> None + override def writexml(self, writer: SupportsWrite[str], indent: str = "", addindent: str = "", newl: str = "") class ReadOnlySequentialNamedNodeMap[in out N: Node]: __slots__ = ("_seq",) @@ -504,10 +504,10 @@ class ReadOnlySequentialNamedNodeMap[in out N: Node]: def getNamedItemNS(self, namespaceURI: str | None, localName: str) -> N | None def __getitem__(self, name_or_tuple: str | NSName) -> N | None def item(self, index: int) -> N | None - def removeNamedItem(self, name: str) -> NoReturn - def removeNamedItemNS(self, namespaceURI: str | None, localName: str) -> NoReturn - def setNamedItem(self, node: Node) -> NoReturn - def setNamedItemNS(self, node: Node) -> NoReturn + def removeNamedItem(self, name: str) -> Never + def removeNamedItemNS(self, namespaceURI: str | None, localName: str) -> Never + def setNamedItem(self, node: Node) -> Never + def setNamedItemNS(self, node: Node) -> Never let length: int class Identified: @@ -567,10 +567,10 @@ class Entity(Identified, Node): notationName: str | None init(self, name: str, publicId: str | None, systemId: str | None, notation: str | None) - override def appendChild(self, newChild: EntityChildren) -> NoReturn - override def insertBefore(self, newChild: EntityChildren, refChild: EntityChildren | None) -> NoReturn - override def removeChild(self, oldChild: EntityChildren) -> NoReturn - override def replaceChild(self, newChild: EntityChildren, oldChild: EntityChildren) -> NoReturn + override def appendChild(self, newChild: EntityChildren) -> Never + override def insertBefore(self, newChild: EntityChildren, refChild: EntityChildren | None) -> Never + override def removeChild(self, oldChild: EntityChildren) -> Never + override def replaceChild(self, newChild: EntityChildren, oldChild: EntityChildren) -> Never class Notation(Identified, Childless, Node): nodeType: ClassVar[12] @@ -658,7 +658,7 @@ class Document(Node, DocumentLS): init(self) override def appendChild[DocumentChildrenVar: DocumentChildren](self, node: DocumentChildrenVar) -> DocumentChildrenVar override def removeChild[DocumentChildrenVar: DocumentChildren](self, oldChild: DocumentChildrenVar) -> DocumentChildrenVar - override def unlink(self) -> None + override def unlink(self) override def cloneNode(self, deep: bool) -> Document | None def createDocumentFragment(self) -> DocumentFragment def createElement(self, tagName: str) -> Element diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/pulldom.byi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/pulldom.byi index 2452ddcc76..6516056ae0 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/pulldom.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/pulldom.byi @@ -2,7 +2,7 @@ import sys from _typeshed import Incomplete, Unused from collections.abc import MutableSequence, Sequence from typing import Final, Literal, TypeAlias -from typing_extensions import Self +from typing_extensions import Never, Self from xml.dom.minidom import Comment, Document, DOMImplementation, Element, ProcessingInstruction, Text from xml.sax import _SupportsReadClose from xml.sax.handler import ContentHandler @@ -55,27 +55,27 @@ class PullDOM(ContentHandler): ) init(self, documentFactory: DocumentFactory = None) def pop(self) -> Element | Document - override def setDocumentLocator(self, locator: Locator) -> None - override def startPrefixMapping(self, prefix: str | None, uri: str) -> None - override def endPrefixMapping(self, prefix: str | None) -> None - override def startElementNS(self, name: NSName, tagName: str | None, attrs: AttributesNSImpl) -> None - override def endElementNS(self, name: NSName, tagName: str | None) -> None - override def startElement(self, name: str, attrs: AttributesImpl) -> None - override def endElement(self, name: str) -> None + override def setDocumentLocator(self, locator: Locator) + override def startPrefixMapping(self, prefix: str | None, uri: str) + override def endPrefixMapping(self, prefix: str | None) + override def startElementNS(self, name: NSName, tagName: str | None, attrs: AttributesNSImpl) + override def endElementNS(self, name: NSName, tagName: str | None) + override def startElement(self, name: str, attrs: AttributesImpl) + override def endElement(self, name: str) def comment(self, s: str) - override def processingInstruction(self, target: str, data: str) -> None - override def ignorableWhitespace(self, chars: str) -> None - override def characters(self, chars: str) -> None - override def startDocument(self) -> None + override def processingInstruction(self, target: str, data: str) + override def ignorableWhitespace(self, chars: str) + override def characters(self, chars: str) + override def startDocument(self) def buildDocument(self, uri: str | None, tagname: str | None) -> Element - override def endDocument(self) -> None + override def endDocument(self) def clear(self): """clear(): Explicitly release parsing structures""" class ErrorHandler: def warning(self, exception: BaseException) - def error(self, exception: BaseException) -> NoReturn - def fatalError(self, exception: BaseException) -> NoReturn + def error(self, exception: BaseException) -> Never + def fatalError(self, exception: BaseException) -> Never class DOMEventStream: stream: _SupportsReadClose[bytes] | _SupportsReadClose[str] @@ -95,11 +95,11 @@ class DOMEventStream: """clear(): Explicitly release parsing objects""" class SAX2DOM(PullDOM): - override def startElementNS(self, name: NSName, tagName: str | None, attrs: AttributesNSImpl) -> None - override def startElement(self, name: str, attrs: AttributesImpl) -> None - override def processingInstruction(self, target: str, data: str) -> None - override def ignorableWhitespace(self, chars: str) -> None - override def characters(self, chars: str) -> None + override def startElementNS(self, name: NSName, tagName: str | None, attrs: AttributesNSImpl) + override def startElement(self, name: str, attrs: AttributesImpl) + override def processingInstruction(self, target: str, data: str) + override def ignorableWhitespace(self, chars: str) + override def characters(self, chars: str) final default_bufsize: int diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/xmlbuilder.byi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/xmlbuilder.byi index b91805defb..6c41c2c827 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/xmlbuilder.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/xmlbuilder.byi @@ -2,6 +2,7 @@ from _typeshed import SupportsRead from typing import Final, Literal +from typing_extensions import Never from xml.dom.minidom import Document, Node, _DOMErrorHandler __all__ = ["DOMBuilder", "DOMEntityResolver", "DOMInputSource"] @@ -49,7 +50,7 @@ class DOMBuilder: def getFeature(self, name: str) -> dynamic def parseURI(self, uri: str) -> Document def parse(self, input: DOMInputSource) -> Document - def parseWithContext(self, input: DOMInputSource, cnode: Node, action: 1 | 2 | 3 | 4) -> NoReturn + def parseWithContext(self, input: DOMInputSource, cnode: Node, action: 1 | 2 | 3 | 4) -> Never class DOMEntityResolver: __slots__ = ("_opener",) @@ -82,14 +83,14 @@ class DocumentLS: """Mixin to create documents that conform to the load/save spec.""" async_: bool - def abort(self) -> NoReturn - def load(self, uri: str) -> NoReturn - def loadXML(self, source: str) -> NoReturn + def abort(self) -> Never + def load(self, uri: str) -> Never + def loadXML(self, source: str) -> Never def saveXML(self, snode: Node | None) -> str class DOMImplementationLS: MODE_SYNCHRONOUS: Final = 1 MODE_ASYNCHRONOUS: Final = 2 def createDOMBuilder(self, mode: 1, schemaType: None) -> DOMBuilder - def createDOMWriter(self) -> NoReturn + def createDOMWriter(self) -> Never def createDOMInputSource(self) -> DOMInputSource diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/xml/parsers/__init__.byi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/parsers/__init__.byi index 82eee29371..ce5d57f856 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/xml/parsers/__init__.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/xml/parsers/__init__.byi @@ -7,4 +7,4 @@ expat -- Python wrapper for James Clark's Expat parser, with namespace """ -from xml.parsers import expat as expat +from xml.parsers export expat diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/__init__.byi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/__init__.byi index d258946315..daa926eff1 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/__init__.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/__init__.byi @@ -23,14 +23,14 @@ import sys from _typeshed import ReadableBuffer, StrPath, SupportsRead, _T_co from collections.abc import Iterable from typing import Final, Protocol, TypeAlias, type_check_only -from xml.sax._exceptions import ( - SAXException as SAXException, - SAXNotRecognizedException as SAXNotRecognizedException, - SAXNotSupportedException as SAXNotSupportedException, - SAXParseException as SAXParseException, - SAXReaderNotAvailable as SAXReaderNotAvailable, +from xml.sax._exceptions export ( + SAXException, + SAXNotRecognizedException, + SAXNotSupportedException, + SAXParseException, + SAXReaderNotAvailable, ) -from xml.sax.handler import ContentHandler as ContentHandler, ErrorHandler as ErrorHandler +from xml.sax.handler export ContentHandler, ErrorHandler from xml.sax.xmlreader import InputSource as InputSource, XMLReader @type_check_only diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/_exceptions.byi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/_exceptions.byi index b8b1840fc4..38ce116dd8 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/_exceptions.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/_exceptions.byi @@ -1,5 +1,6 @@ """Different kinds of SAX Exceptions""" +from typing_extensions import Never from xml.sax.xmlreader import Locator class SAXException(Exception): @@ -24,7 +25,7 @@ class SAXException(Exception): def getException(self) -> Exception | None: """Return the embedded exception, or None if there was none.""" - def __getitem__(self, ix: object) -> NoReturn: + def __getitem__(self, ix: object) -> Never: """Avoids weird error messages if someone does exception[ix] by mistake, since Exception has __getitem__ defined. """ diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/expatreader.byi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/expatreader.byi index 50bb7c5e3b..cb239990e1 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/expatreader.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/expatreader.byi @@ -36,13 +36,13 @@ class ExpatParser(xmlreader.IncrementalParser, xmlreader.Locator): """SAX driver for the pyexpat C module.""" init(self, namespaceHandling: BoolType = 0, bufsize: int = 65516) - override def parse(self, source: xmlreader.InputSource | _Source) -> None: + override def parse(self, source: xmlreader.InputSource | _Source): """Parse an XML document from a URL or an InputSource.""" - override def prepareParser(self, source: xmlreader.InputSource) -> None - override def setContentHandler(self, handler: _ContentHandlerProtocol) -> None + override def prepareParser(self, source: xmlreader.InputSource) + override def setContentHandler(self, handler: _ContentHandlerProtocol) override def getFeature(self, name: str) -> BoolType - override def setFeature(self, name: str, state: BoolType) -> None + override def setFeature(self, name: str, state: BoolType) override def getProperty(self, name: "http://xml.org/sax/properties/lexical-handler") -> LexicalHandler | None def getProperty(self, name: "http://www.python.org/sax/properties/interning-dict") -> dict[str, dynamic] | None @@ -55,10 +55,10 @@ class ExpatParser(xmlreader.IncrementalParser, xmlreader.Locator): ) -> None def setProperty(self, name: str, value: object) -> None - override def feed(self, data: str | ReadableBuffer, isFinal: bool = False) -> None + override def feed(self, data: str | ReadableBuffer, isFinal: bool = False) def flush(self) - override def close(self) -> None - override def reset(self) -> None + override def close(self) + override def reset(self) override def getColumnNumber(self) -> int | None override def getLineNumber(self) -> int override def getPublicId(self) -> str | None diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/handler.byi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/handler.byi index 2d730e9b5e..c46872bfd3 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/handler.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/handler.byi @@ -10,14 +10,15 @@ $Id$ """ from typing import Final, Protocol, type_check_only +from typing_extensions import Never from xml.sax import xmlreader final version: str @type_check_only protocol _ErrorHandlerProtocol: # noqa: Y046 # Protocol is not used - def error(self, exception: BaseException) -> NoReturn - def fatalError(self, exception: BaseException) -> NoReturn + def error(self, exception: BaseException) -> Never + def fatalError(self, exception: BaseException) -> Never def warning(self, exception: BaseException) class ErrorHandler: @@ -31,10 +32,10 @@ class ErrorHandler: SAXParseException as the only parameter. """ - def error(self, exception: BaseException) -> NoReturn: + def error(self, exception: BaseException) -> Never: """Handle a recoverable error.""" - def fatalError(self, exception: BaseException) -> NoReturn: + def fatalError(self, exception: BaseException) -> Never: """Handle a non-recoverable error.""" def warning(self, exception: BaseException): diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/saxutils.byi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/saxutils.byi index 0ba7185fd1..4cc2bac576 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/saxutils.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/saxutils.byi @@ -7,6 +7,7 @@ from codecs import StreamReaderWriter, StreamWriter from collections.abc import Mapping from io import RawIOBase, TextIOBase from typing import Literal +from typing_extensions import Never from xml.sax import _Source, handler, xmlreader def escape(data: str, entities: Mapping[str, str] = {}) -> str: @@ -47,17 +48,17 @@ class XMLGenerator(handler.ContentHandler): def _qname(self, name: (str | None, str)) -> str: """Builds a qualified name from a (ns_url, localname) pair""" - override def startDocument(self) -> None - override def endDocument(self) -> None - override def startPrefixMapping(self, prefix: str | None, uri: str) -> None - override def endPrefixMapping(self, prefix: str | None) -> None - override def startElement(self, name: str, attrs: xmlreader.AttributesImpl) -> None - override def endElement(self, name: str) -> None - override def startElementNS(self, name: (str | None, str), qname: str | None, attrs: xmlreader.AttributesNSImpl) -> None - override def endElementNS(self, name: (str | None, str), qname: str | None) -> None - override def characters(self, content: str) -> None - override def ignorableWhitespace(self, content: str) -> None - override def processingInstruction(self, target: str, data: str) -> None + override def startDocument(self) + override def endDocument(self) + override def startPrefixMapping(self, prefix: str | None, uri: str) + override def endPrefixMapping(self, prefix: str | None) + override def startElement(self, name: str, attrs: xmlreader.AttributesImpl) + override def endElement(self, name: str) + override def startElementNS(self, name: (str | None, str), qname: str | None, attrs: xmlreader.AttributesNSImpl) + override def endElementNS(self, name: (str | None, str), qname: str | None) + override def characters(self, content: str) + override def ignorableWhitespace(self, content: str) + override def processingInstruction(self, target: str, data: str) class XMLFilterBase(xmlreader.XMLReader): """This class is designed to sit between an XMLReader and the @@ -70,8 +71,8 @@ class XMLFilterBase(xmlreader.XMLReader): init(self, parent: xmlreader.XMLReader | None = None) # ErrorHandler methods - def error(self, exception: BaseException) -> NoReturn - def fatalError(self, exception: BaseException) -> NoReturn + def error(self, exception: BaseException) -> Never + def fatalError(self, exception: BaseException) -> Never def warning(self, exception: BaseException) # ContentHandler methods def setDocumentLocator(self, locator: xmlreader.Locator) @@ -93,12 +94,12 @@ class XMLFilterBase(xmlreader.XMLReader): # EntityResolver methods def resolveEntity(self, publicId: str | None, systemId: str) -> str # XMLReader methods - override def parse(self, source: xmlreader.InputSource | _Source) -> None - override def setLocale(self, locale: str) -> None + override def parse(self, source: xmlreader.InputSource | _Source) + override def setLocale(self, locale: str) override def getFeature(self, name: str) -> 1 | 0 | bool - override def setFeature(self, name: str, state: 1 | 0 | bool) -> None + override def setFeature(self, name: str, state: 1 | 0 | bool) override def getProperty(self, name: str) -> object - override def setProperty(self, name: str, value: object) -> None + override def setProperty(self, name: str, value: object) # XMLFilter methods def getParent(self) -> xmlreader.XMLReader | None def setParent(self, parent: xmlreader.XMLReader) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/xmlreader.byi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/xmlreader.byi index 0437ece240..14aa153427 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/xmlreader.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/xmlreader.byi @@ -93,7 +93,7 @@ class IncrementalParser(XMLReader): """ init(self, bufsize: int = 65536) - override def parse(self, source: InputSource | _Source) -> None + override def parse(self, source: InputSource | _Source) def feed(self, data: str | ReadableBuffer): """This method gives the raw XML data in the data parameter to the parser and makes it parse the data, emitting the diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/zipfile/__init__.byi b/crates/ty_vendored/vendor/typeshed/stdlib/zipfile/__init__.byi index 6aa79bba50..f28e78c205 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/zipfile/__init__.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/zipfile/__init__.byi @@ -488,7 +488,7 @@ class ZipInfo: """ if sys.version_info >= (3, 12): - from zipfile._path import CompleteDirs as CompleteDirs, Path as Path + from zipfile._path export CompleteDirs, Path else: class CompleteDirs(ZipFile): diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/zipimport.byi b/crates/ty_vendored/vendor/typeshed/stdlib/zipimport.byi index c990d35675..b19363373b 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/zipimport.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/zipimport.byi @@ -128,10 +128,10 @@ class zipimporter(_LoaderBasics): Deprecated since Python 3.10. Use exec_module() instead. """ - override def exec_module(self, module: ModuleType) -> None: + override def exec_module(self, module: ModuleType): """Execute the module.""" - override def create_module(self, spec: ModuleSpec) -> None: + override def create_module(self, spec: ModuleSpec): """Use default semantics for module creation.""" def find_spec(self, fullname: str, target: ModuleType | None = None) -> ModuleSpec | None: diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/zoneinfo/__init__.byi b/crates/ty_vendored/vendor/typeshed/stdlib/zoneinfo/__init__.byi index 527daae828..82a3162ee6 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/zoneinfo/__init__.byi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/zoneinfo/__init__.byi @@ -3,11 +3,11 @@ from collections.abc import Iterable from datetime import datetime, timedelta, tzinfo from typing_extensions import Self, disjoint_base 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, +from zoneinfo._tzpath export ( + TZPATH, + InvalidTZPathWarning, + available_timezones, + reset_tzpath, ) __all__ = ["ZoneInfo", "reset_tzpath", "available_timezones", "TZPATH", "ZoneInfoNotFoundError", "InvalidTZPathWarning"] diff --git a/crates/ty_wasm/Cargo.toml b/crates/ty_wasm/Cargo.toml index 070981044a..bcb4b91fa3 100644 --- a/crates/ty_wasm/Cargo.toml +++ b/crates/ty_wasm/Cargo.toml @@ -29,6 +29,7 @@ ty_project = { workspace = true, default-features = false, features = [ "format", ] } ty_python_core = { workspace = true } +ty_python_semantic = { workspace = true } ruff_db = { workspace = true, default-features = false, features = [] } ruff_diagnostics = { workspace = true } diff --git a/crates/ty_wasm/src/lib.rs b/crates/ty_wasm/src/lib.rs index 95e6a36eec..921cd12a66 100644 --- a/crates/ty_wasm/src/lib.rs +++ b/crates/ty_wasm/src/lib.rs @@ -26,8 +26,9 @@ use ty_ide::{NavigationTarget, NavigationTargets, hints, signature_help}; use ty_project::metadata::options::Options; use ty_project::watch::{ChangeEvent, ChangedKind, CreatedKind, DeletedKind}; use ty_project::{CheckMode, ProjectMetadata}; -use ty_project::{Db, ProjectDatabase}; -use ty_python_core::program::{FallibleStrategy, Program}; +use ty_project::{Db, ProjectDatabase, SemanticDb as _}; +use ty_python_core::program::FallibleStrategy; +use ty_python_semantic::ProgramEnvironment; use wasm_bindgen::prelude::*; #[wasm_bindgen] @@ -169,20 +170,23 @@ impl Workspace { let (program_settings, program_settings_diagnostics) = merged_options .to_program_settings(&self.system, self.db.vendored(), &FallibleStrategy) .map_err(into_error)?; - Program::get(&self.db).update_from_settings(&mut self.db, program_settings); + self.db + .project() + .update_program(&mut self.db, program_settings); - let (settings, settings_diagnostics) = merged_options + let (settings, mut settings_diagnostics) = merged_options .to_settings(&self.db, &FallibleStrategy) .map_err(into_error)?; - - self.db.project().reload( - &mut self.db, - project, - Some(settings), - settings_diagnostics, - program_settings_diagnostics, + settings_diagnostics.extend( + program_settings_diagnostics + .into_iter() + .map(|diagnostic| diagnostic.into_diagnostic(&self.db)), ); + self.db + .project() + .reload(&mut self.db, project, Some(settings), settings_diagnostics); + Ok(()) } @@ -275,7 +279,7 @@ impl Workspace { #[wasm_bindgen(js_name = "hints")] pub fn hints(&self, file_id: &FileHandle) -> Result, Error> { - Ok(hints(&self.db, file_id.file) + Ok(hints(&self.db, self.db.program_file(file_id.file)) .into_iter() .map(|hint| Hint::from_ide_hint(&self.db, file_id.file, self.position_encoding, &hint)) .collect()) @@ -290,7 +294,11 @@ impl Workspace { /// 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); + let parsed = ruff_db::parsed::parsed_module( + &self.db, + self.db.program_file(file_id.file).python_file(&self.db), + ) + .load(&self.db); Ok(format!("{:#?}", parsed.syntax())) } @@ -301,7 +309,11 @@ impl Workspace { /// 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); + let parsed = ruff_db::parsed::parsed_module( + &self.db, + self.db.program_file(file_id.file).python_file(&self.db), + ) + .load(&self.db); Ok(format!("{:#?}", parsed.tokens())) } @@ -324,7 +336,9 @@ impl Workspace { let offset = position.to_text_size(&source, &index, self.position_encoding)?; - let Some(targets) = goto_type_definition(&self.db, file_id.file, offset) else { + let Some(targets) = + goto_type_definition(&self.db, self.db.program_file(file_id.file), offset) + else { return Ok(Vec::new()); }; @@ -348,7 +362,8 @@ impl Workspace { let offset = position.to_text_size(&source, &index, self.position_encoding)?; - let Some(targets) = goto_declaration(&self.db, file_id.file, offset) else { + let Some(targets) = goto_declaration(&self.db, self.db.program_file(file_id.file), offset) + else { return Ok(Vec::new()); }; @@ -372,7 +387,8 @@ impl Workspace { let offset = position.to_text_size(&source, &index, self.position_encoding)?; - let Some(targets) = goto_definition(&self.db, file_id.file, offset) else { + let Some(targets) = goto_definition(&self.db, self.db.program_file(file_id.file), offset) + else { return Ok(Vec::new()); }; @@ -396,7 +412,9 @@ impl Workspace { let offset = position.to_text_size(&source, &index, self.position_encoding)?; - let Some(targets) = find_references(&self.db, file_id.file, offset, true) else { + let Some(targets) = + find_references(&self.db, self.db.program_file(file_id.file), offset, true) + else { return Ok(Vec::new()); }; @@ -435,7 +453,7 @@ impl Workspace { let offset = position.to_text_size(&source, &index, self.position_encoding)?; - let Some(range) = can_rename(&self.db, file_id.file, offset) else { + let Some(range) = can_rename(&self.db, self.db.program_file(file_id.file), offset) else { return Ok(None); }; @@ -458,12 +476,13 @@ impl Workspace { let index = line_index(&self.db, file_id.file); let offset = position.to_text_size(&source, &index, self.position_encoding)?; + let program_file = self.db.program_file(file_id.file); - if can_rename(&self.db, file_id.file, offset).is_none() { + if can_rename(&self.db, program_file, offset).is_none() { return Ok(Vec::new()); } - let Some(rename_results) = rename(&self.db, file_id.file, offset, new_name) else { + let Some(rename_results) = rename(&self.db, program_file, offset, new_name) else { return Ok(Vec::new()); }; @@ -488,7 +507,7 @@ impl Workspace { let offset = position.to_text_size(&source, &index, self.position_encoding)?; - let Some(range_info) = hover(&self.db, file_id.file, offset) else { + let Some(range_info) = hover(&self.db, self.db.program_file(file_id.file), offset) else { return Ok(None); }; @@ -519,11 +538,14 @@ impl Workspace { let offset = position.to_text_size(&source, &index, self.position_encoding)?; let settings = ty_ide::CompletionSettings::default(); + let program_file = self.db.program_file(file_id.file); + let env = ProgramEnvironment::from_file(program_file); let completions = ty_ide::completion( &self.db, + &env, &settings, CompletionCapabilities::default(), - file_id.file, + program_file, offset, ); @@ -532,7 +554,7 @@ impl Workspace { .map(|comp| { let name = comp.label().to_string(); let kind = comp.kind.map(CompletionKind::from); - let type_display = comp.ty.map(|ty| ty.display(&self.db).to_string()); + let type_display = comp.ty.map(|ty| ty.display(&self.db, &env).to_string()); let import_edit = comp.import.as_ref().map(|edit| { let range = Range::from_text_range( edit.range(), @@ -567,7 +589,7 @@ impl Workspace { let result = inlay_hints( &self.db, - file_id.file, + self.db.program_file(file_id.file), range.to_text_range(&index, &source, self.position_encoding)?, // TODO: Provide a way to configure this &InlayHintSettings::default(), @@ -621,7 +643,8 @@ impl Workspace { 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 semantic_token = + ty_ide::semantic_tokens(&self.db, self.db.program_file(file_id.file), None); let result = semantic_token .iter() @@ -646,7 +669,7 @@ impl Workspace { let semantic_token = ty_ide::semantic_tokens( &self.db, - file_id.file, + self.db.program_file(file_id.file), Some(range.to_text_range(&index, &source, self.position_encoding)?), ); @@ -683,7 +706,7 @@ impl Workspace { actions.extend( ty_ide::code_actions( &self.db, - file_id.file, + self.db.program_file(file_id.file), range, diagnostic.inner.id().as_str(), // the playground has no django template documents @@ -720,7 +743,9 @@ impl Workspace { 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 { + let Some(signature_help_info) = + signature_help(&self.db, self.db.program_file(file_id.file), offset) + else { return Ok(None); }; @@ -767,7 +792,9 @@ impl Workspace { let offset = position.to_text_size(&source, &index, self.position_encoding)?; - let Some(targets) = document_highlights(&self.db, file_id.file, offset) else { + let Some(targets) = + document_highlights(&self.db, self.db.program_file(file_id.file), offset) + else { return Ok(Vec::new()); }; @@ -912,7 +939,7 @@ impl Diagnostic { SubDiagnostic { severity: sub_diagnostic.severity().into(), - message: sub_diagnostic.primary_message().to_string(), + message: sub_diagnostic.headline_message().to_string(), annotations, } }) diff --git a/docs/.gitignore b/docs/.gitignore index ec6ed14e2b..c782bce528 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1,4 +1,5 @@ /contributing.md +/default-rules.md /index.md /rules.md /rules/ diff --git a/docs/configuration.md b/docs/configuration.md index 64b5af9e02..1a7babb3e9 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -7,6 +7,8 @@ semantics are the same. For a complete enumeration of the available configuration options, see [_Settings_](settings.md). +For the complete list of enabled rules, see [_Default Rules_](default-rules.md). + If left unspecified, Ruff's default configuration is equivalent to: === "pyproject.toml" @@ -51,10 +53,7 @@ If left unspecified, Ruff's default configuration is equivalent to: target-version = "py310" [tool.ruff.lint] - # 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"] + # select = [...] # See the Default Rules page for the full listing. ignore = [] # Allow fix for all enabled rules (when `--fix`) is provided. @@ -133,10 +132,7 @@ If left unspecified, Ruff's default configuration is equivalent to: target-version = "py310" [lint] - # 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"] + # select = [...] # See the Default Rules page for the full listing. ignore = [] # Allow fix for all enabled rules (when `--fix`) is provided. @@ -180,8 +176,8 @@ As an example, the following would configure Ruff to: ```toml [tool.ruff.lint] - # 1. Enable flake8-bugbear (`B`) rules, in addition to the defaults. - select = ["E4", "E7", "E9", "F", "B"] + # 1. Enable all flake8-bugbear (`B`) rules, in addition to the defaults. + extend-select = ["B"] # 2. Avoid enforcing line-length violations (`E501`) ignore = ["E501"] @@ -203,8 +199,8 @@ As an example, the following would configure Ruff to: ```toml [lint] - # 1. Enable flake8-bugbear (`B`) rules, in addition to the defaults. - select = ["E4", "E7", "E9", "F", "B"] + # 1. Enable all flake8-bugbear (`B`) rules, in addition to the defaults. + extend-select = ["B"] # 2. Avoid enforcing line-length violations (`E501`) ignore = ["E501"] @@ -229,7 +225,7 @@ Linter plugin configurations are expressed as subsections, e.g.: ```toml [tool.ruff.lint] # Add "Q" to the list of enabled codes. - select = ["E4", "E7", "E9", "F", "Q"] + extend-select = ["Q"] [tool.ruff.lint.flake8-quotes] docstring-quotes = "double" @@ -240,7 +236,7 @@ Linter plugin configurations are expressed as subsections, e.g.: ```toml [lint] # Add "Q" to the list of enabled codes. - select = ["E4", "E7", "E9", "F", "Q"] + extend-select = ["Q"] [lint.flake8-quotes] docstring-quotes = "double" @@ -648,9 +644,9 @@ Options: Enable automatic additions of `noqa` directives to failing lines. Optionally provide a reason to append after the codes --add-ignore[=] - Enable automatic additions of `ruff:ignore` comments to failing - lines. Optionally provide a reason to append after the rule names. - Requires preview mode + Enable automatic additions of `ruff: ignore` comments to failing + lines. Optionally provide a reason to append after the codes. In + preview, add suppression comments with rule names instead --show-files See the files Ruff will be run against with the current settings --show-settings diff --git a/docs/editors/features.md b/docs/editors/features.md index ef95a70601..164d049594 100644 --- a/docs/editors/features.md +++ b/docs/editors/features.md @@ -41,9 +41,6 @@ alt="Formatting a document in VS Code" ### Markdown code blocks -*This feature is currently only available in -[preview mode](https://docs.astral.sh/ruff/preview/#preview).* - The Ruff formatter can also format Python code blocks in Markdown files. The Ruff VS Code extension provides the `Format Document` command for Markdown files, which will then format the code blocks with the same settings @@ -55,15 +52,6 @@ then you will need to set one as the default in VS Code, and manually run the `Format Document With...` (or `Ruff: Format document`) command to run any other formatters separately. -To enable preview mode for formatting in VS Code, add the following to your -`settings.json`: - -```json -{ - "ruff.format.preview": true, -} -``` - To set Ruff as the default formatter for Markdown files in VS Code, add the following to your `settings.json`: diff --git a/docs/formatter.md b/docs/formatter.md index 6f0f2dfd83..ce932d3ee6 100644 --- a/docs/formatter.md +++ b/docs/formatter.md @@ -227,17 +227,14 @@ def f(x): ## Markdown code formatting -*This feature is currently only available in [preview mode](preview.md#preview).* - The Ruff formatter can also format Python code blocks in Markdown files. In these files, Ruff will format any CommonMark [fenced code blocks][] with -the following info strings: `python`, `py`, `python3`, `py3`, or `pyi`. The -formatter will automatically skip a code block if the code does not parse as +the following info strings: `python`, `py`, `python3`, `py3`, `pyi`, or `pycon`. +The formatter will automatically skip a code block if the code does not parse as valid Python or if the reformatted code would produce an invalid Python program. -Code blocks marked as `python`, `py`, `python3`, or `py3` will be formatted with -the normal Python code formatting style, while any code blocks marked with -`pyi` will be formatted like Python type stub files: +Code blocks marked as `pyi` are formatted like stub files, `pycon` blocks as +REPL sessions, and the others use normal Python file formatting. For example: ````markdown ```py @@ -306,7 +303,7 @@ support needs to be explicitly included by adding it to `types_or`: ```yaml title=".pre-commit-config.yaml" repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.22 + rev: v0.16.2 hooks: - id: ruff-format types_or: [python, pyi, jupyter, markdown] diff --git a/docs/integrations.md b/docs/integrations.md index 2645d5ab7a..c4b1fdb036 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.15.22-alpine + name: ghcr.io/astral-sh/ruff:0.16.2-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.15.22 + rev: v0.16.2 hooks: # Run the linter. - id: ruff-check @@ -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.15.22 + rev: v0.16.2 hooks: # Run the linter. - id: ruff-check @@ -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.15.22 + rev: v0.16.2 hooks: # Run the linter. - id: ruff-check diff --git a/docs/linter.md b/docs/linter.md index 22ce06c4b6..bdf7783629 100644 --- a/docs/linter.md +++ b/docs/linter.md @@ -292,8 +292,12 @@ see the [`lint.per-file-ignores`](settings.md#lint_per-file-ignores) setting. ### Comments -Ruff supports multiple forms of suppression comments, including inline and file-level `noqa` -comments, and range suppressions. +Ruff supports multiple forms of suppression comments, including inline and file-level `noqa` and +`ruff: ignore` comments, and range suppressions. + +In [`preview`](preview.md) mode, rule names (e.g. `unused-import`) can be used in `ruff: ignore`, +`ruff: file-ignore`, `ruff: disable`, and `ruff: enable` comments instead of rule codes (e.g. +`F401`). #### Line-level @@ -344,20 +348,18 @@ The full inline comment specification is as follows: missing delimiter (e.g. `F401F841`), though a warning will be emitted in this case. -*The following is currently only available in [preview mode](`preview.md`).* - To cover an entire "logical" line (a multi-line statement or suite header), an "ignore" comment may be placed above the first line: ```python -# ruff: ignore[unused-function-argument] # Covers the entire function signature +# ruff: ignore[ARG001] # Covers the entire function signature def foo( arg1, arg2, ): pass -# ruff: ignore[line-too-long] # Covers the entire list literal +# ruff: ignore[E501] # Covers the entire list literal things = [ "really long string literal ...", "really long string literal ...", @@ -371,13 +373,13 @@ of the multi-line statement or header uncovered: ```python def foo( arg1, - # ruff: ignore[unused-function-argument] # Only covers `arg2` + # ruff: ignore[ARG001] # Only covers `arg2` arg2, ): pass things = [ - "really long string literal ...", # ruff: ignore[line-too-long] # Only covers this line + "really long string literal ...", # ruff: ignore[E501] # Only covers this line "really long string literal ...", ] ``` @@ -386,8 +388,8 @@ Ignore comments can also be "stacked" with other comments or pragmas, and will still cover the next logical line: ```python -# ruff: ignore[ambiguous-variable-name] -# ruff: ignore[unused-variable] +# ruff: ignore[E741] +# ruff: ignore[F841] # I definitely know what I'm doing. i = 1 ``` @@ -454,9 +456,6 @@ be used to terminate a preceding "disable" comment with identical codes. Unlike `noqa` suppressions, range suppressions do not support "blanket" suppression of all violations. At least one violation code must be listed. -In [`preview`](preview.md) mode, rule names (e.g. `unused-import`) can be used in these comments -instead of rule codes (e.g. `F401`). - The full range suppression comment specification is as follows: - An own-line comment starting with case sensitive `#ruff:`, with optional whitespace @@ -496,12 +495,11 @@ The file-level suppression comment specification is as follows: optional whitespace and a case-insensitive match for `noqa`. After this, the specification is as in the inline `noqa` suppressions above. -In [`preview`](preview.md) mode, one or more rules can be ignored across an -entire file with a `file-ignore` comment on its own line, at global module scope, -and preferably near the top of the file: +One or more rules can also be ignored across an entire file with a `file-ignore` comment on its own +line, at global module scope, and preferably near the top of the file: ```python -# ruff: file-ignore[unused-import, unused-function-argument] +# ruff: file-ignore[F401, ARG001] ``` The full-level suppression comment specification is as follows: @@ -534,15 +532,17 @@ $ ruff check /path/to/file.py --extend-select RUF100 --fix ### Inserting necessary suppression comments Ruff can _automatically add_ suppression comments to all lines that contain violations, which is -useful when migrating a new codebase to Ruff. To add the appropriate comments to all relevant -lines, run Ruff with `--add-noqa`: +useful when migrating a new codebase to Ruff. To add the appropriate comments to all relevant lines, +run Ruff with `--add-noqa` to add `noqa` comments or with `--add-ignore` to add `ruff: ignore` +comments: ```shell-session $ ruff check /path/to/file.py --add-noqa +$ ruff check /path/to/file.py --add-ignore ``` -The `--add-noqa` flag adds `noqa` directives with rule codes. To add `ruff:ignore` comments with -human-readable rule names instead, use `--add-ignore` with preview mode enabled. +Both of these flags use rule codes on stable. To add `ruff: ignore` comments with human-readable +rule names instead, use `--add-ignore` with preview mode enabled. ### isort action comments diff --git a/docs/requirements.txt b/docs/requirements.txt index a545725cd9..c26757440a 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,9 +1,9 @@ PyYAML==6.0.3 -ruff==0.15.22 +ruff==0.16.2 mkdocs==1.6.1 -mkdocs-material==9.7.6 +mkdocs-material==9.7.7 mkdocs-redirects==1.2.3 mdformat==1.0.0 -mdformat-mkdocs==5.2.1 +mdformat-mkdocs==5.3.0 mkdocs-github-admonitions-plugin @ git+https://github.com/PGijsbers/admonitions.git#7343d2f4a92e4d1491094530ef3d0d02d93afbb7 mkdocs-llmstxt==0.2.0 diff --git a/docs/tutorial.md b/docs/tutorial.md index 450dbc81df..c58e24ec8f 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -201,13 +201,13 @@ Ruff supports [over 900 lint rules](rules.md) split across over 50 built-in plug determining the right set of rules will depend on your project's needs: some rules may be too strict, some are framework-specific, and so on. -By default, Ruff enables Flake8's `F` rules, along with a subset of the `E` rules, omitting any -stylistic rules that overlap with the use of a formatter, like `ruff format` or +By default, Ruff enables rules from the `F`, `E`, `B`, `UP`, and `RUF` categories, as well as many +more, omitting any stylistic rules that overlap with the use of a formatter, like `ruff format` or [Black](https://github.com/psf/black). If you're introducing a linter for the first time, **the default rule set is a great place to -start**: it's narrow and focused while catching a wide variety of common errors (like unused -imports) with zero configuration. +start**: it catches a wide variety of common errors (like unused imports) with zero configuration. +See [_Default Rules_](default-rules.md) for the complete list. If you're migrating to Ruff from another linter, you can enable rules that are equivalent to those enforced in your previous configuration. For example, if we want to enforce the pyupgrade @@ -361,8 +361,8 @@ index 71fca60c8d..e92d839f1b 100644 +from typing import Iterable # noqa: UP035 ``` -To add `# ruff:ignore[...]` comments with human-readable rule names instead, use the -`--add-ignore` flag with preview mode enabled. +To add `# ruff: ignore[...]` comments instead, use the `--add-ignore` flag. In preview mode, +`--add-ignore` uses human-readable rule names in place of rule codes. ## Integrations @@ -372,7 +372,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.15.22 + rev: v0.16.2 hooks: # Run the linter. - id: ruff-check diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index a7e92f5efc..2288fb1b6e 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -115,7 +115,7 @@ dependencies = [ "manyhow", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -131,7 +131,7 @@ dependencies = [ "proc-macro2", "quote", "quote-use", - "syn", + "syn 2.0.119", ] [[package]] @@ -287,7 +287,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -422,7 +422,7 @@ dependencies = [ "defmt-parser", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -442,7 +442,7 @@ checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -453,7 +453,7 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -558,7 +558,7 @@ checksum = "c736d226c32e496b8377813b52269e11ad3a48d8373b68862d0364f04fd1229d" dependencies = [ "attribute-derive", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -816,7 +816,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -873,7 +873,7 @@ checksum = "d0879bd39df99c4c5e2c6615ccc026391a423dde10532c573e6086eb94a802cc" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -940,7 +940,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0903173ea316c34a44d0497161e04d9210af44f5f5e89bf2f55d9a254c9a0e8d" dependencies = [ "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -982,7 +982,7 @@ dependencies = [ "manyhow-macros", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1093,21 +1093,18 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "path-absolutize" -version = "3.1.1" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4af381fe79fa195b4909485d99f73a80792331df0625188e707854f0b3383f5" +checksum = "f808742975794703469f67a28dd14b1d1009a1743c18b0353b4b951dbb0068ad" dependencies = [ "path-dedot", ] [[package]] name = "path-dedot" -version = "3.1.1" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07ba0ad7e047712414213ff67533e6dd477af0a4e1d14fb52343e53d30ea9397" -dependencies = [ - "once_cell", -] +checksum = "03351d0f1c066c114015408dc6a3e101f080fc03ef9d8d799aa58ec760cac5a6" [[package]] name = "path-slash" @@ -1371,7 +1368,7 @@ dependencies = [ "proc-macro-utils", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1513,7 +1510,7 @@ dependencies = [ [[package]] name = "ruff_annotate_snippets" -version = "0.0.5" +version = "0.0.8" dependencies = [ "anstyle", "memchr", @@ -1522,7 +1519,7 @@ dependencies = [ [[package]] name = "ruff_cache" -version = "0.0.5" +version = "0.0.8" dependencies = [ "char_str", "filetime", @@ -1535,7 +1532,7 @@ dependencies = [ [[package]] name = "ruff_db" -version = "0.0.5" +version = "0.0.8" dependencies = [ "anstyle", "arc-swap", @@ -1573,7 +1570,7 @@ dependencies = [ [[package]] name = "ruff_diagnostics" -version = "0.0.5" +version = "0.0.8" dependencies = [ "get-size2", "is-macro", @@ -1583,7 +1580,7 @@ dependencies = [ [[package]] name = "ruff_formatter" -version = "0.0.5" +version = "0.0.8" dependencies = [ "drop_bomb", "ruff_cache", @@ -1598,7 +1595,7 @@ dependencies = [ [[package]] name = "ruff_index" -version = "0.0.5" +version = "0.0.8" dependencies = [ "get-size2", "ruff_macros", @@ -1607,7 +1604,7 @@ dependencies = [ [[package]] name = "ruff_linter" -version = "0.15.22" +version = "0.16.2" dependencies = [ "aho-corasick", "anyhow", @@ -1665,7 +1662,7 @@ dependencies = [ [[package]] name = "ruff_macros" -version = "0.0.5" +version = "0.0.8" dependencies = [ "heck", "itertools 0.15.0", @@ -1673,19 +1670,19 @@ dependencies = [ "quote", "regex", "ruff_python_trivia", - "syn", + "syn 3.0.3", ] [[package]] name = "ruff_memory_usage" -version = "0.0.5" +version = "0.0.8" dependencies = [ "get-size2", ] [[package]] name = "ruff_notebook" -version = "0.0.5" +version = "0.0.8" dependencies = [ "anyhow", "rand 0.10.2", @@ -1700,7 +1697,7 @@ dependencies = [ [[package]] name = "ruff_python_ast" -version = "0.0.5" +version = "0.0.8" dependencies = [ "aho-corasick", "arrayvec", @@ -1724,7 +1721,7 @@ dependencies = [ [[package]] name = "ruff_python_codegen" -version = "0.0.5" +version = "0.0.8" dependencies = [ "ruff_python_ast", "ruff_python_literal", @@ -1735,7 +1732,7 @@ dependencies = [ [[package]] name = "ruff_python_formatter" -version = "0.0.5" +version = "0.0.8" dependencies = [ "anyhow", "clap", @@ -1763,7 +1760,7 @@ dependencies = [ [[package]] name = "ruff_python_importer" -version = "0.0.5" +version = "0.0.8" dependencies = [ "anyhow", "ruff_diagnostics", @@ -1776,7 +1773,7 @@ dependencies = [ [[package]] name = "ruff_python_index" -version = "0.0.5" +version = "0.0.8" dependencies = [ "ruff_python_ast", "ruff_python_trivia", @@ -1786,7 +1783,7 @@ dependencies = [ [[package]] name = "ruff_python_literal" -version = "0.0.5" +version = "0.0.8" dependencies = [ "bitflags 2.13.0", "icu_properties", @@ -1796,7 +1793,7 @@ dependencies = [ [[package]] name = "ruff_python_parser" -version = "0.0.5" +version = "0.0.8" dependencies = [ "bitflags 2.13.0", "bstr", @@ -1816,7 +1813,7 @@ dependencies = [ [[package]] name = "ruff_python_semantic" -version = "0.0.5" +version = "0.0.8" dependencies = [ "bitflags 2.13.0", "is-macro", @@ -1833,7 +1830,7 @@ dependencies = [ [[package]] name = "ruff_python_stdlib" -version = "0.0.5" +version = "0.0.8" dependencies = [ "bitflags 2.13.0", "unicode-ident", @@ -1841,7 +1838,7 @@ dependencies = [ [[package]] name = "ruff_python_trivia" -version = "0.0.5" +version = "0.0.8" dependencies = [ "itertools 0.15.0", "ruff_source_file", @@ -1852,7 +1849,7 @@ dependencies = [ [[package]] name = "ruff_ranged_value" -version = "0.0.5" +version = "0.0.8" dependencies = [ "ruff_db", "ruff_text_size", @@ -1862,7 +1859,7 @@ dependencies = [ [[package]] name = "ruff_source_file" -version = "0.0.5" +version = "0.0.8" dependencies = [ "get-size2", "memchr", @@ -1872,7 +1869,7 @@ dependencies = [ [[package]] name = "ruff_text_size" -version = "0.0.5" +version = "0.0.8" dependencies = [ "get-size2", "serde", @@ -1908,9 +1905,9 @@ checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "salsa" -version = "0.28.1" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a14fdadbf856222e731756d7fdbdf193a7abf8fdab009bb45f48671a42719a84" +checksum = "cf0e374215cd2db2b5c75d7b3a99cb0cc052c0595335dfdefc03d4eb08f4aa81" dependencies = [ "boxcar", "compact_str", @@ -1935,19 +1932,19 @@ dependencies = [ [[package]] name = "salsa-macro-rules" -version = "0.28.1" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d7dc08ba69b9aedfa61dfc4d65548ae42c0d8b90bbd62cd121776920841bcf" +checksum = "85f4b7d4405540bbd6d4ffa52d4322d983f3781954d3073067ac1bdb028459b3" [[package]] name = "salsa-macros" -version = "0.28.1" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec5c48c5a4a53a6e2be9762f56566f1325d4394c011c00b3ea86ba2d13411e71" +checksum = "445be2bfbb2f67cb663225ecd7bc5a25370c0250fca30f9d8cbad9a913650370" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] @@ -1998,7 +1995,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2031,9 +2028,9 @@ checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "similar" -version = "3.1.1" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6505efef05804732ed8a3f2d4f279429eb485bd69d5b0cc6b19cc02005cda16" +checksum = "85ee016af5d736b69fc89e19254540fa4b5f5492853fb5503920f084011c78b6" dependencies = [ "bstr", ] @@ -2101,7 +2098,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2121,6 +2118,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -2129,7 +2137,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2173,7 +2181,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2184,7 +2192,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2294,7 +2302,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2308,7 +2316,7 @@ dependencies = [ [[package]] name = "ty_combine" -version = "0.0.5" +version = "0.0.8" dependencies = [ "ordermap", "ruff_db", @@ -2318,12 +2326,13 @@ dependencies = [ [[package]] name = "ty_module_resolver" -version = "0.0.5" +version = "0.0.8" dependencies = [ "anyhow", "camino", "compact_str", "get-size2", + "ordermap", "regex", "regex-syntax", "ruff_db", @@ -2340,7 +2349,7 @@ dependencies = [ [[package]] name = "ty_python_core" -version = "0.0.5" +version = "0.0.8" dependencies = [ "bitflags 2.13.0", "bitvec", @@ -2368,7 +2377,7 @@ dependencies = [ [[package]] name = "ty_python_semantic" -version = "0.0.5" +version = "0.0.8" dependencies = [ "bitflags 2.13.0", "char_str", @@ -2408,7 +2417,7 @@ dependencies = [ [[package]] name = "ty_site_packages" -version = "0.0.5" +version = "0.0.8" dependencies = [ "camino", "colored", @@ -2428,14 +2437,14 @@ dependencies = [ [[package]] name = "ty_static" -version = "0.0.5" +version = "0.0.8" dependencies = [ "ruff_macros", ] [[package]] name = "ty_vendored" -version = "0.0.5" +version = "0.0.8" dependencies = [ "path-slash", "ruff_db", @@ -2618,7 +2627,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.119", "wasm-bindgen-shared", ] @@ -2711,7 +2720,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] @@ -2732,7 +2741,7 @@ checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2752,7 +2761,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] @@ -2786,7 +2795,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 14cef7c28c..92c7b21e30 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -26,12 +26,12 @@ ruff_python_formatter = { path = "../crates/ruff_python_formatter" } ruff_text_size = { path = "../crates/ruff_text_size" } ty_module_resolver = { path = "../crates/ty_module_resolver" } -ty_python_semantic = { path = "../crates/ty_python_semantic" } +ty_python_semantic = { path = "../crates/ty_python_semantic", features = ["testing"] } ty_vendored = { path = "../crates/ty_vendored" } ty_python_core = { path = "../crates/ty_python_core" } libfuzzer-sys = { git = "https://github.com/rust-fuzz/libfuzzer", default-features = false } -salsa = { version = "0.28.1", default-features = false, features = [ +salsa = { version = "0.28.2", default-features = false, features = [ "compact_str", "macros", "salsa_unstable", diff --git a/fuzz/fuzz_targets/ty_check_invalid_syntax.rs b/fuzz/fuzz_targets/ty_check_invalid_syntax.rs index 579180f8d1..d86e988072 100644 --- a/fuzz/fuzz_targets/ty_check_invalid_syntax.rs +++ b/fuzz/fuzz_targets/ty_check_invalid_syntax.rs @@ -14,12 +14,11 @@ 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_module_resolver::{Db as ModuleResolverDb, SearchPathSettings}; -use ty_python_core::Db as _; use ty_python_core::platform::PythonPlatform; -use ty_python_core::program::{FallibleStrategy, Program, ProgramSettings}; +use ty_python_core::program::{FallibleStrategy, ProgramSettings}; +use ty_python_core::{Db as _, ProgramFile, TestProgramDb}; use ty_python_semantic::lint::LintRegistry; use ty_python_semantic::types::check_types; use ty_python_semantic::{ @@ -39,22 +38,43 @@ struct TestDb { vendored: VendoredFileSystem, rule_selection: Arc, analysis_settings: Arc, + program_settings: ProgramSettings, } impl TestDb { fn new() -> Self { - Self { + let vendored = ty_vendored::file_system().clone(); + let program_settings = ProgramSettings::empty(&vendored); + let mut db = Self { storage: salsa::Storage::new(Some(Box::new({ move |event| { tracing::trace!("event: {:?}", event); } }))), system: TestSystem::default(), - vendored: ty_vendored::file_system().clone(), + vendored, files: Files::default(), rule_selection: RuleSelection::from_registry(default_lint_registry()).into(), analysis_settings: AnalysisSettings::default().into(), - } + program_settings, + }; + + let src_root = SystemPathBuf::from("/src"); + db.memory_file_system() + .create_directory_all(&src_root) + .unwrap(); + + let program_settings = ProgramSettings { + python_version: PythonVersionWithSource::default(), + python_platform: PythonPlatform::default(), + search_paths: SearchPathSettings::new(vec![src_root]) + .to_search_paths(db.system(), db.vendored(), &FallibleStrategy) + .expect("Valid search path settings"), + }; + program_settings.search_paths.try_register_static_roots(&db); + db.program_settings = program_settings; + + db } } @@ -71,10 +91,6 @@ impl SourceDb for TestDb { fn files(&self) -> &Files { &self.files } - - fn python_version(&self) -> PythonVersion { - Program::get(self).python_version(self) - } } impl DbWithTestSystem for TestDb { @@ -88,11 +104,7 @@ impl DbWithTestSystem for TestDb { } #[salsa::db] -impl ModuleResolverDb for TestDb { - fn search_paths(&self) -> &ty_module_resolver::SearchPaths { - Program::get(self).search_paths(self) - } -} +impl ModuleResolverDb for TestDb {} #[salsa::db] impl ty_python_core::Db for TestDb { @@ -105,12 +117,20 @@ impl ty_python_core::Db for TestDb { impl SemanticDb for TestDb { fn check_file(&self, file: File) -> Vec { if self.should_check_file(file) { - ty_python_semantic::check_file_unwrap(self, file) + ty_python_semantic::check_file_unwrap(self, self.program_file(file)) } else { Vec::new() } } + fn program_file(&self, file: File) -> ProgramFile<'_> { + self.program().program_file(self, file) + } + + fn python_version_with_source(&self, _file: File) -> &PythonVersionWithSource { + &self.program_settings.python_version + } + fn rule_selection(&self, _file: File) -> &RuleSelection { &self.rule_selection } @@ -137,30 +157,15 @@ impl SemanticDb for TestDb { } #[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(), &FallibleStrategy) - .expect("Valid search path settings"), - }, - ); - - db +impl TestProgramDb for TestDb { + fn program_settings(&self) -> &ProgramSettings { + &self.program_settings + } } +#[salsa::db] +impl salsa::Database for TestDb {} + static TEST_DB: OnceLock> = OnceLock::new(); fn do_fuzz(case: &[u8]) -> Corpus { @@ -174,14 +179,14 @@ fn do_fuzz(case: &[u8]) -> Corpus { } let mut db = TEST_DB - .get_or_init(|| Mutex::new(setup_db())) + .get_or_init(|| Mutex::new(TestDb::new())) .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); + check_types(&*db, db.program_file(file)); db.memory_file_system().remove_file(path).unwrap(); file.sync(&mut *db); } diff --git a/playground/api/package-lock.json b/playground/api/package-lock.json index de40873be0..14ceceb5d6 100644 --- a/playground/api/package-lock.json +++ b/playground/api/package-lock.json @@ -46,9 +46,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20260710.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260710.1.tgz", - "integrity": "sha512-OqJl2eWF5+y9jarMm3YqqCTUe7Hd4ihogX5jyRU8iaAgOVyDr/Bk6aXpPCVUi1/MHzO93a18R/TmSTtzmB0sQw==", + "version": "1.20260730.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260730.1.tgz", + "integrity": "sha512-+MBHmPaiTe2KajryW0T24rZvWFxb41hD3d8anNzQqHzft6vSEb18+sp0znSwxgij7ApPhSM1+vhkNg4f3YMguA==", "cpu": [ "x64" ], @@ -63,9 +63,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20260710.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260710.1.tgz", - "integrity": "sha512-MYBqWgUblO+VlGvO73zYsH3hB9tdRj+yLyt5IHDFWryipb2l1efmNiWtAOkIhSRfypqLYGFrfpaDm2Hg00XVKw==", + "version": "1.20260730.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260730.1.tgz", + "integrity": "sha512-SBHKntPkKvNPgaCrTe99xC1CAl8ygJDzlYfK0LbuJ1muKadIw35WnhO0wu894fKBtllsVQdNzDLee+cm0ppLSQ==", "cpu": [ "arm64" ], @@ -80,9 +80,9 @@ } }, "node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20260710.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260710.1.tgz", - "integrity": "sha512-lVWUgqI8qrkqvaCBGElu1kdaUFdAvaS2RD8K4qkCFP9hI3f5TCXumEs5qWSeZkvKum0+X/uJZ5hBFWsYI5SmoQ==", + "version": "1.20260730.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260730.1.tgz", + "integrity": "sha512-ouyPOSMbiKPeSwUJUvxtMcxGAXs2J4aPE4T5ABIYX5ClcQx5j5bbHTmnqOQEY8sAuLTPjH7dY+iB6UI5ISlwwA==", "cpu": [ "x64" ], @@ -97,9 +97,9 @@ } }, "node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20260710.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260710.1.tgz", - "integrity": "sha512-kDwDPItBjAI4JL0df9Fma2N+Qggbm77IB/DnroAkEGQ79fpR80sYMyuB/ZQKyjEk9f48Ocq7HCCLq59qVSyNqA==", + "version": "1.20260730.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260730.1.tgz", + "integrity": "sha512-YQ+Mi78U3TPdgBPtwq+Sm6rJU+Ihl2y0pjYtuuKkdmUbYzL7oLR6Xqq9wljhasnuCFICssDJaqhMep5WizYoEQ==", "cpu": [ "arm64" ], @@ -114,9 +114,9 @@ } }, "node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20260710.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260710.1.tgz", - "integrity": "sha512-GcLHy1oN1dfK6g1Z7UDV9f5xMGyTfPwcjWQ0sfWKH31IsoEVCRapnj3IC0PoIrDbnoo6irGPP0CwVs3WzdTajw==", + "version": "1.20260730.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260730.1.tgz", + "integrity": "sha512-27fAN+vUECW1oYVc1KOcHYpkL8COM2Uxtxql7TL595kxbjoqS5yckw7NLz7bTf2pALFCZWjqXDjZGJ/xbG4ZKQ==", "cpu": [ "x64" ], @@ -131,9 +131,9 @@ } }, "node_modules/@cloudflare/workers-types": { - "version": "5.20260715.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-5.20260715.1.tgz", - "integrity": "sha512-saxo/nMqQJ1dKDUXp1a2y/+IjKENFVD9+QRefHg5EjJZY20OG3xcEge4PGljbqZiF3AiU4o5ZLS3Vm7cayQIxg==", + "version": "5.20260804.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-5.20260804.1.tgz", + "integrity": "sha512-B1dwxpN6e5RZXZkE5zpZj+ooNeNZ1mwavLIyHDYe10ojhlGTwDfe8sAl7R1mMXc1cyIsbr+jKVdvmMEyVcdTdg==", "dev": true, "license": "MIT OR Apache-2.0" }, @@ -150,9 +150,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", - "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", "dev": true, "license": "MIT", "optional": true, @@ -603,9 +603,9 @@ } }, "node_modules/@img/colour": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", - "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", "dev": true, "license": "MIT", "engines": { @@ -613,9 +613,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.2.tgz", + "integrity": "sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg==", "cpu": [ "arm64" ], @@ -626,19 +626,19 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" + "@img/sharp-libvips-darwin-arm64": "1.3.1" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.2.tgz", + "integrity": "sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw==", "cpu": [ "x64" ], @@ -649,19 +649,39 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" + "@img/sharp-libvips-darwin-x64": "1.3.1" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.2.tgz", + "integrity": "sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.2" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.1.tgz", + "integrity": "sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g==", "cpu": [ "arm64" ], @@ -676,9 +696,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.1.tgz", + "integrity": "sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ==", "cpu": [ "x64" ], @@ -693,13 +713,16 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.1.tgz", + "integrity": "sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -710,13 +733,16 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.1.tgz", + "integrity": "sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -727,13 +753,16 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.1.tgz", + "integrity": "sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -744,13 +773,16 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.1.tgz", + "integrity": "sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -761,13 +793,16 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.1.tgz", + "integrity": "sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -778,13 +813,16 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.1.tgz", + "integrity": "sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -795,13 +833,16 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.1.tgz", + "integrity": "sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -812,13 +853,16 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.1.tgz", + "integrity": "sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -829,213 +873,254 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.2.tgz", + "integrity": "sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" + "@img/sharp-libvips-linux-arm": "1.3.1" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.2.tgz", + "integrity": "sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" + "@img/sharp-libvips-linux-arm64": "1.3.1" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.2.tgz", + "integrity": "sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" + "@img/sharp-libvips-linux-ppc64": "1.3.1" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.2.tgz", + "integrity": "sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" + "@img/sharp-libvips-linux-riscv64": "1.3.1" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.2.tgz", + "integrity": "sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" + "@img/sharp-libvips-linux-s390x": "1.3.1" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.2.tgz", + "integrity": "sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" + "@img/sharp-libvips-linux-x64": "1.3.1" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.2.tgz", + "integrity": "sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.1" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.2.tgz", + "integrity": "sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + "@img/sharp-libvips-linuxmusl-x64": "1.3.1" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.2.tgz", + "integrity": "sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw==", + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.2.tgz", + "integrity": "sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g==", "cpu": [ "wasm32" ], "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "license": "Apache-2.0", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.7.0" + "@img/sharp-wasm32": "0.35.2" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.2.tgz", + "integrity": "sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ==", "cpu": [ "arm64" ], @@ -1046,16 +1131,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.2.tgz", + "integrity": "sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog==", "cpu": [ "ia32" ], @@ -1066,16 +1151,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": "^20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.2.tgz", + "integrity": "sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ==", "cpu": [ "x64" ], @@ -1086,7 +1171,7 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" @@ -1754,16 +1839,16 @@ } }, "node_modules/miniflare": { - "version": "4.20260710.0", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260710.0.tgz", - "integrity": "sha512-x1LLRkU6o1p7hiKrB0TRnL0MJn6xFOT+/vrlEQINz5cRDKLP8ru4hBqWTIvXAetzr1acKAnmAaG84pQ4W/K14g==", + "version": "4.20260730.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260730.0.tgz", + "integrity": "sha512-1Z9SB9r/o//80UA02Re3QhtcecSHAyAjf5EcKBfQVlQrCg7Miy79hl2PvtkwFLIaJ5rcrOPdDcRr577okwZPsg==", "dev": true, "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "0.8.1", - "sharp": "0.34.5", + "sharp": "0.35.2", "undici": "7.28.0", - "workerd": "1.20260710.1", + "workerd": "1.20260730.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" }, @@ -1863,9 +1948,9 @@ } }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1875,48 +1960,48 @@ } }, "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.2.tgz", + "integrity": "sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w==", "dev": true, - "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@img/colour": "^1.0.0", + "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", - "semver": "^7.7.3" + "semver": "^7.8.4" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" + "@img/sharp-darwin-arm64": "0.35.2", + "@img/sharp-darwin-x64": "0.35.2", + "@img/sharp-freebsd-wasm32": "0.35.2", + "@img/sharp-libvips-darwin-arm64": "1.3.1", + "@img/sharp-libvips-darwin-x64": "1.3.1", + "@img/sharp-libvips-linux-arm": "1.3.1", + "@img/sharp-libvips-linux-arm64": "1.3.1", + "@img/sharp-libvips-linux-ppc64": "1.3.1", + "@img/sharp-libvips-linux-riscv64": "1.3.1", + "@img/sharp-libvips-linux-s390x": "1.3.1", + "@img/sharp-libvips-linux-x64": "1.3.1", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.1", + "@img/sharp-libvips-linuxmusl-x64": "1.3.1", + "@img/sharp-linux-arm": "0.35.2", + "@img/sharp-linux-arm64": "0.35.2", + "@img/sharp-linux-ppc64": "0.35.2", + "@img/sharp-linux-riscv64": "0.35.2", + "@img/sharp-linux-s390x": "0.35.2", + "@img/sharp-linux-x64": "0.35.2", + "@img/sharp-linuxmusl-arm64": "0.35.2", + "@img/sharp-linuxmusl-x64": "0.35.2", + "@img/sharp-webcontainers-wasm32": "0.35.2", + "@img/sharp-win32-arm64": "0.35.2", + "@img/sharp-win32-ia32": "0.35.2", + "@img/sharp-win32-x64": "0.35.2" } }, "node_modules/shebang-command": { @@ -2069,9 +2154,9 @@ } }, "node_modules/workerd": { - "version": "1.20260710.1", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260710.1.tgz", - "integrity": "sha512-U2sBPPrb9U97sBKnnMN6Kv8p65903P35nwMkPE9vSH/bRuRqkZ3a1EjUw3jV28RhiyXpkLF77Evzw8XimFxyTw==", + "version": "1.20260730.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260730.1.tgz", + "integrity": "sha512-zmfNIjwYSWFY5chGBOjWtH3xAE7p97FTC6vR4Ep98290ho6AeAR/NVcBD274YCLEUYzqm8yxdtZlxMybU8a3jA==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -2082,17 +2167,17 @@ "node": ">=16" }, "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20260710.1", - "@cloudflare/workerd-darwin-arm64": "1.20260710.1", - "@cloudflare/workerd-linux-64": "1.20260710.1", - "@cloudflare/workerd-linux-arm64": "1.20260710.1", - "@cloudflare/workerd-windows-64": "1.20260710.1" + "@cloudflare/workerd-darwin-64": "1.20260730.1", + "@cloudflare/workerd-darwin-arm64": "1.20260730.1", + "@cloudflare/workerd-linux-64": "1.20260730.1", + "@cloudflare/workerd-linux-arm64": "1.20260730.1", + "@cloudflare/workerd-windows-64": "1.20260730.1" } }, "node_modules/wrangler": { - "version": "4.111.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.111.0.tgz", - "integrity": "sha512-bffpI9EyrnpKkF/1S+RaIv8oRD93GtbsA7TlfWwOsGJGB7VO3jVbdGzpC9TU7Bqom3z7jUxcte4Z9MPhaQ4HoQ==", + "version": "4.118.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.118.0.tgz", + "integrity": "sha512-9pkBw/b8zWqGx2S+oLhgHMR1M/4VOE8SynUFABnGWiSFGlcOQ4xiI/B71Xf66RYP2xzngU37IQFPtUruij3lYw==", "dev": true, "license": "MIT OR Apache-2.0", "dependencies": { @@ -2100,10 +2185,10 @@ "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", - "miniflare": "4.20260710.0", + "miniflare": "5.20260730.0-alpha", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", - "workerd": "1.20260710.1" + "workerd": "1.20260730.1" }, "bin": { "cf-wrangler": "bin/cf-wrangler.js", @@ -2117,7 +2202,7 @@ "fsevents": "2.3.3" }, "peerDependencies": { - "@cloudflare/workers-types": "^5.20260710.1" + "@cloudflare/workers-types": "^5.20260730.1" }, "peerDependenciesMeta": { "@cloudflare/workers-types": { @@ -2125,6 +2210,24 @@ } } }, + "node_modules/wrangler/node_modules/miniflare": { + "version": "5.20260730.0-alpha", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20260730.0-alpha.tgz", + "integrity": "sha512-8/dspSXDshP6nSkCpjKO7BYc2qZoYSXm7iM+QxY7qJyJpAB3onnQSaiu0cvKJlfuMGwULl55hG69FJCcCMXU1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "0.35.2", + "undici": "7.28.0", + "workerd": "1.20260730.1", + "ws": "8.21.0", + "youch": "4.1.0-beta.10" + }, + "engines": { + "node": ">=22.0.0" + } + }, "node_modules/ws": { "version": "8.21.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", diff --git a/playground/package-lock.json b/playground/package-lock.json index b8e9cb0329..ea9c77d9a5 100644 --- a/playground/package-lock.json +++ b/playground/package-lock.json @@ -284,40 +284,6 @@ "node": ">=6.9.0" } }, - "node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", - "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -712,29 +678,10 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", - "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.3" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, "node_modules/@oxc-project/types": { - "version": "0.137.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.137.0.tgz", - "integrity": "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==", + "version": "0.142.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.142.0.tgz", + "integrity": "sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==", "dev": true, "license": "MIT", "funding": { @@ -2540,9 +2487,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.3.tgz", - "integrity": "sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.2.tgz", + "integrity": "sha512-l7x215OGvo1s52JWmR8U/DAVzEDWBCIbTm28aeJV/WDTSHgcKXaZTuBT0hJMs5NggilfJTW3clZVvd24yfKJxA==", "cpu": [ "arm64" ], @@ -2557,9 +2504,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.3.tgz", - "integrity": "sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.2.tgz", + "integrity": "sha512-9u9Xv6c1AJZT0FfwH5vrMG5Jjcwhc1MlyrPu0XfTqkzsmqfks2M6W/o5XwAJgVVN/jHpqqngC1WevHKKTIUtIA==", "cpu": [ "arm64" ], @@ -2574,9 +2521,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.3.tgz", - "integrity": "sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.2.tgz", + "integrity": "sha512-9W1mbGZAfW3oqd85bhBkmpyHCCzL1TeG/zFFP3vg7b0rlly8cxOcre5nXwz+LHazCwac2MNWgPdPCHndABjpWQ==", "cpu": [ "x64" ], @@ -2591,9 +2538,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.3.tgz", - "integrity": "sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.2.tgz", + "integrity": "sha512-0p1lhiCSCyaerFwtrdZQUx7NqGk6LQnaRKWX7tFQqwQgvX0rjM15cIkm3pax1UpEakK14C4mOxx/jSqCBdBRqQ==", "cpu": [ "x64" ], @@ -2608,9 +2555,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.3.tgz", - "integrity": "sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.2.tgz", + "integrity": "sha512-e+cOJXrJ2L3zx6YzqPg+f6Wbk3V1cKB8bOhbaYdVYN3DdquzNdRAmrbETz1qnt5yp/c7JNlNjmITiA2cVneQ7w==", "cpu": [ "arm" ], @@ -2625,9 +2572,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.3.tgz", - "integrity": "sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.2.tgz", + "integrity": "sha512-JsSMsj6sNat/MuhG5fnBD7QgbtpHKVe30x5/bAVirDHdhoQRXJkF6xc0Jqk8O4fiCUQAzMOoH9wZi3m60c8wtg==", "cpu": [ "arm64" ], @@ -2645,9 +2592,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.3.tgz", - "integrity": "sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.2.tgz", + "integrity": "sha512-B5G/zJdHaoJn9vD50eGHWkiWfmq8Uhi3IiLPJTzmZTrAalk1bztUikSXo0qga18ibE0IXboyeMUnhPjhAJ45wQ==", "cpu": [ "arm64" ], @@ -2665,9 +2612,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.3.tgz", - "integrity": "sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.2.tgz", + "integrity": "sha512-6mC/awzKka8W6EoekjegpfGkjz8jXWDX63pqu/HYVpyKtZfu65Jsh4QAH3Kej3CAv/c1oGX7psTmFEbr0mDxLA==", "cpu": [ "ppc64" ], @@ -2685,9 +2632,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.3.tgz", - "integrity": "sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.2.tgz", + "integrity": "sha512-412MX9fJLdA1IK28EZnc8jYv2HRTleOZgfLQumJ5zy7OeJLZlg/CETwFaXjNmGVxG51cFHpKLqb5LKvBC+HsHA==", "cpu": [ "s390x" ], @@ -2705,9 +2652,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.3.tgz", - "integrity": "sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.2.tgz", + "integrity": "sha512-Q/+HI/ToJafZ1iCqGgVQXUEkIjufHCTF0gBQ2a5o3cg7GJ2h0qyq3nvvSmU+bGda2/7ygXpTY4TM6gO9OhQ0ZA==", "cpu": [ "x64" ], @@ -2725,9 +2672,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.3.tgz", - "integrity": "sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.2.tgz", + "integrity": "sha512-ZKp/w41n6wCvxzxQHtQSbuphfX3Y4cCvbjkKHusrLx4lh+JWLTU7StSltO/DKARISzbj368d+qUaCjI8K2wzXw==", "cpu": [ "x64" ], @@ -2745,9 +2692,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.3.tgz", - "integrity": "sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.2.tgz", + "integrity": "sha512-pxE6xD4KS3eAROkKK5yrhB9/3+vhlhVGMvlQLbdpzrBGDbKrnzx3RLwPaHvasLo6jgaiBLn7e04Df9C4tYhjmA==", "cpu": [ "arm64" ], @@ -2761,29 +2708,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.3.tgz", - "integrity": "sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.11.1", - "@emnapi/runtime": "1.11.1", - "@napi-rs/wasm-runtime": "^1.1.6" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.3.tgz", - "integrity": "sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.2.tgz", + "integrity": "sha512-4MqEue5re+xIZzAWsB8sj0P1kqZySWqIuN4t6QaIO/YA6SFwySOLruvWQFzfmqk8LBK2P30KCSJwf6mJCZZ5/A==", "cpu": [ "arm64" ], @@ -2798,9 +2726,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.3.tgz", - "integrity": "sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.2.tgz", + "integrity": "sha512-NweNxxD0Nf9t8v7kodun45Ijp3EIwYY+uydPP6qBEYvfBqhIjN6dZMzlQja3tqX/aLs3F3Uz+AxDpKgRhpOZQg==", "cpu": [ "x64" ], @@ -2838,49 +2766,49 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz", - "integrity": "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "5.21.6", + "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.3.2" + "tailwindcss": "4.3.3" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.2.tgz", - "integrity": "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", "dev": true, "license": "MIT", "engines": { "node": ">= 20" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.3.2", - "@tailwindcss/oxide-darwin-arm64": "4.3.2", - "@tailwindcss/oxide-darwin-x64": "4.3.2", - "@tailwindcss/oxide-freebsd-x64": "4.3.2", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2", - "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2", - "@tailwindcss/oxide-linux-arm64-musl": "4.3.2", - "@tailwindcss/oxide-linux-x64-gnu": "4.3.2", - "@tailwindcss/oxide-linux-x64-musl": "4.3.2", - "@tailwindcss/oxide-wasm32-wasi": "4.3.2", - "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2", - "@tailwindcss/oxide-win32-x64-msvc": "4.3.2" + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.2.tgz", - "integrity": "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", "cpu": [ "arm64" ], @@ -2895,9 +2823,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.2.tgz", - "integrity": "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", "cpu": [ "arm64" ], @@ -2912,9 +2840,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.2.tgz", - "integrity": "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", "cpu": [ "x64" ], @@ -2929,9 +2857,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.2.tgz", - "integrity": "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", "cpu": [ "x64" ], @@ -2946,9 +2874,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.2.tgz", - "integrity": "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", "cpu": [ "arm" ], @@ -2963,9 +2891,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.2.tgz", - "integrity": "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", "cpu": [ "arm64" ], @@ -2983,9 +2911,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.2.tgz", - "integrity": "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", "cpu": [ "arm64" ], @@ -3003,9 +2931,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.2.tgz", - "integrity": "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", "cpu": [ "x64" ], @@ -3023,9 +2951,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.2.tgz", - "integrity": "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", "cpu": [ "x64" ], @@ -3043,9 +2971,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.2.tgz", - "integrity": "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -3139,9 +3067,9 @@ "optional": true }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz", - "integrity": "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", "cpu": [ "arm64" ], @@ -3156,9 +3084,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.2.tgz", - "integrity": "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", "cpu": [ "x64" ], @@ -3173,31 +3101,20 @@ } }, "node_modules/@tailwindcss/vite": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.2.tgz", - "integrity": "sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", + "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", "dev": true, "license": "MIT", "dependencies": { - "@tailwindcss/node": "4.3.2", - "@tailwindcss/oxide": "4.3.2", - "tailwindcss": "4.3.2" + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "tailwindcss": "4.3.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", - "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@types/emscripten": { "version": "1.41.5", "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.41.5.tgz", @@ -3226,9 +3143,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.2.17", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", - "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", "dev": true, "license": "MIT", "dependencies": { @@ -3236,9 +3153,9 @@ } }, "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", "dev": true, "license": "MIT", "peerDependencies": { @@ -3253,17 +3170,17 @@ "optional": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.63.0.tgz", - "integrity": "sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.66.0.tgz", + "integrity": "sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.63.0", - "@typescript-eslint/type-utils": "8.63.0", - "@typescript-eslint/utils": "8.63.0", - "@typescript-eslint/visitor-keys": "8.63.0", + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/type-utils": "8.66.0", + "@typescript-eslint/utils": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -3276,7 +3193,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.63.0", + "@typescript-eslint/parser": "^8.66.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } @@ -3292,16 +3209,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.63.0.tgz", - "integrity": "sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.66.0.tgz", + "integrity": "sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.63.0", - "@typescript-eslint/types": "8.63.0", - "@typescript-eslint/typescript-estree": "8.63.0", - "@typescript-eslint/visitor-keys": "8.63.0", + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", "debug": "^4.4.3" }, "engines": { @@ -3317,14 +3234,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.63.0.tgz", - "integrity": "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.66.0.tgz", + "integrity": "sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.63.0", - "@typescript-eslint/types": "^8.63.0", + "@typescript-eslint/tsconfig-utils": "^8.66.0", + "@typescript-eslint/types": "^8.66.0", "debug": "^4.4.3" }, "engines": { @@ -3339,14 +3256,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz", - "integrity": "sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.66.0.tgz", + "integrity": "sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.63.0", - "@typescript-eslint/visitor-keys": "8.63.0" + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3357,9 +3274,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz", - "integrity": "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.66.0.tgz", + "integrity": "sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==", "dev": true, "license": "MIT", "engines": { @@ -3374,15 +3291,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.63.0.tgz", - "integrity": "sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.66.0.tgz", + "integrity": "sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.63.0", - "@typescript-eslint/typescript-estree": "8.63.0", - "@typescript-eslint/utils": "8.63.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0", + "@typescript-eslint/utils": "8.66.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -3399,9 +3316,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.63.0.tgz", - "integrity": "sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.66.0.tgz", + "integrity": "sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==", "dev": true, "license": "MIT", "engines": { @@ -3413,16 +3330,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz", - "integrity": "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.66.0.tgz", + "integrity": "sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.63.0", - "@typescript-eslint/tsconfig-utils": "8.63.0", - "@typescript-eslint/types": "8.63.0", - "@typescript-eslint/visitor-keys": "8.63.0", + "@typescript-eslint/project-service": "8.66.0", + "@typescript-eslint/tsconfig-utils": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -3451,26 +3368,26 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.5" + "brace-expansion": "^5.0.8" }, "engines": { "node": "18 || 20 || >=22" @@ -3493,16 +3410,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.63.0.tgz", - "integrity": "sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.66.0.tgz", + "integrity": "sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.63.0", - "@typescript-eslint/types": "8.63.0", - "@typescript-eslint/typescript-estree": "8.63.0" + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3517,13 +3434,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.63.0.tgz", - "integrity": "sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.66.0.tgz", + "integrity": "sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/types": "8.66.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -3939,9 +3856,9 @@ } }, "node_modules/@vitejs/plugin-react": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz", - "integrity": "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==", + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.5.tgz", + "integrity": "sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==", "dev": true, "license": "MIT", "dependencies": { @@ -4673,9 +4590,9 @@ } }, "node_modules/dompurify": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", - "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", + "version": "3.4.8", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.8.tgz", + "integrity": "sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -4704,9 +4621,9 @@ "license": "ISC" }, "node_modules/enhanced-resolve": { - "version": "5.21.6", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", - "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", + "version": "5.24.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.3.tgz", + "integrity": "sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6636,12 +6553,12 @@ } }, "node_modules/monaco-editor": { - "version": "0.55.1", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", - "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.56.0.tgz", + "integrity": "sha512-sXboRm3BeBeLm938eaiyLMe0OxzfXIlZvbv4ir/jVgQy1zDhWjgmny0WoN45fuDKhCCQsYMbBJrv/A6jd8aCUg==", "license": "MIT", "dependencies": { - "dompurify": "3.2.7", + "dompurify": "3.4.8", "marked": "14.0.0" } }, @@ -6653,9 +6570,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.15", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", - "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "version": "3.3.17", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz", + "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==", "dev": true, "funding": [ { @@ -6970,9 +6887,9 @@ } }, "node_modules/postcss": { - "version": "8.5.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", - "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "version": "8.5.25", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", + "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", "dev": true, "funding": [ { @@ -6990,7 +6907,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -7009,9 +6926,9 @@ } }, "node_modules/prettier": { - "version": "3.9.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.4.tgz", - "integrity": "sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==", + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", "dev": true, "license": "MIT", "bin": { @@ -7318,13 +7235,13 @@ } }, "node_modules/rolldown": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.3.tgz", - "integrity": "sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.2.tgz", + "integrity": "sha512-opwpo1tQBAcpSUJDt94B7hhLNGOKjCdE//XXjeLrnx9b83bjnw45tXdg1b09yEw/VLFBJGZpwRULMmOZo7ol+A==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.137.0", + "@oxc-project/types": "=0.142.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -7334,21 +7251,20 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.1.3", - "@rolldown/binding-darwin-arm64": "1.1.3", - "@rolldown/binding-darwin-x64": "1.1.3", - "@rolldown/binding-freebsd-x64": "1.1.3", - "@rolldown/binding-linux-arm-gnueabihf": "1.1.3", - "@rolldown/binding-linux-arm64-gnu": "1.1.3", - "@rolldown/binding-linux-arm64-musl": "1.1.3", - "@rolldown/binding-linux-ppc64-gnu": "1.1.3", - "@rolldown/binding-linux-s390x-gnu": "1.1.3", - "@rolldown/binding-linux-x64-gnu": "1.1.3", - "@rolldown/binding-linux-x64-musl": "1.1.3", - "@rolldown/binding-openharmony-arm64": "1.1.3", - "@rolldown/binding-wasm32-wasi": "1.1.3", - "@rolldown/binding-win32-arm64-msvc": "1.1.3", - "@rolldown/binding-win32-x64-msvc": "1.1.3" + "@rolldown/binding-android-arm64": "1.2.2", + "@rolldown/binding-darwin-arm64": "1.2.2", + "@rolldown/binding-darwin-x64": "1.2.2", + "@rolldown/binding-freebsd-x64": "1.2.2", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.2", + "@rolldown/binding-linux-arm64-gnu": "1.2.2", + "@rolldown/binding-linux-arm64-musl": "1.2.2", + "@rolldown/binding-linux-ppc64-gnu": "1.2.2", + "@rolldown/binding-linux-s390x-gnu": "1.2.2", + "@rolldown/binding-linux-x64-gnu": "1.2.2", + "@rolldown/binding-linux-x64-musl": "1.2.2", + "@rolldown/binding-openharmony-arm64": "1.2.2", + "@rolldown/binding-win32-arm64-msvc": "1.2.2", + "@rolldown/binding-win32-x64-msvc": "1.2.2" } }, "node_modules/ruff_wasm": { @@ -7772,9 +7688,9 @@ } }, "node_modules/tailwindcss": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz", - "integrity": "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", "dev": true, "license": "MIT" }, @@ -7999,16 +7915,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.63.0.tgz", - "integrity": "sha512-xgwXyzG4sK9ALkBxbyGkTMMOS+imnW65iPhxCQMK83KhxyoDNW7l+IDqEf9vMdoUidHpOoS967RCq4eMiTexwQ==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.66.0.tgz", + "integrity": "sha512-QlEbBPz/RuJ1XUHj29nm3t0F/O/cSlEnntozqPOYHnnTGAXFamnMBu5i9Vn6vhUPHGAjR+Vl+5J8vPN/BMUrJw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.63.0", - "@typescript-eslint/parser": "8.63.0", - "@typescript-eslint/typescript-estree": "8.63.0", - "@typescript-eslint/utils": "8.63.0" + "@typescript-eslint/eslint-plugin": "8.66.0", + "@typescript-eslint/parser": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0", + "@typescript-eslint/utils": "8.66.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -8092,16 +8008,16 @@ } }, "node_modules/vite": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", - "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.0.tgz", + "integrity": "sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==", "dev": true, "license": "MIT", "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.16", - "rolldown": "~1.1.3", + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.23", + "rolldown": "~1.2.0", "tinyglobby": "^0.2.17" }, "bin": { @@ -8118,7 +8034,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.3.0", + "@vitejs/devtools": "^0.4.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -8192,6 +8108,279 @@ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/vite/node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/vite/node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/vite/node_modules/picomatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", @@ -8461,7 +8650,7 @@ "@monaco-editor/react": "^4.4.6", "classnames": "^2.3.2", "lz-string": "^1.5.0", - "monaco-editor": "^0.55.0", + "monaco-editor": "^0.56.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-resizable-panels": "^4.0.0", @@ -8480,7 +8669,7 @@ "@monaco-editor/react": "^4.7.0", "classnames": "^2.3.2", "fflate": "^0.8.2", - "monaco-editor": "^0.55.0", + "monaco-editor": "^0.56.0", "react": "^19.0.0", "react-aria-components": "^1.16.0", "react-resizable-panels": "^4.0.0" @@ -8493,7 +8682,7 @@ "@monaco-editor/react": "^4.7.0", "classnames": "^2.5.1", "lz-string": "^1.5.0", - "monaco-editor": "^0.55.0", + "monaco-editor": "^0.56.0", "pyodide": "^314.0.0", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/playground/ruff/package.json b/playground/ruff/package.json index ebae6503ca..a89da89c4e 100644 --- a/playground/ruff/package.json +++ b/playground/ruff/package.json @@ -18,7 +18,7 @@ "@monaco-editor/react": "^4.4.6", "classnames": "^2.3.2", "lz-string": "^1.5.0", - "monaco-editor": "^0.55.0", + "monaco-editor": "^0.56.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-resizable-panels": "^4.0.0", diff --git a/playground/shared/package.json b/playground/shared/package.json index 7d9c0dd61c..4706fa58b4 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", "fflate": "^0.8.2", - "monaco-editor": "^0.55.0", + "monaco-editor": "^0.56.0", "react-aria-components": "^1.16.0", "react": "^19.0.0", "react-resizable-panels": "^4.0.0" diff --git a/playground/ty/package.json b/playground/ty/package.json index c1e586ea3e..e96e136b79 100644 --- a/playground/ty/package.json +++ b/playground/ty/package.json @@ -18,7 +18,7 @@ "@monaco-editor/react": "^4.7.0", "classnames": "^2.5.1", "lz-string": "^1.5.0", - "monaco-editor": "^0.55.0", + "monaco-editor": "^0.56.0", "pyodide": "^314.0.0", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/playground/ty/src/Editor/Chrome.tsx b/playground/ty/src/Editor/Chrome.tsx index b806cd18ff..8c3642bad5 100644 --- a/playground/ty/src/Editor/Chrome.tsx +++ b/playground/ty/src/Editor/Chrome.tsx @@ -171,7 +171,7 @@ export default function Chrome({ diff --git a/playground/ty/src/Playground.tsx b/playground/ty/src/Playground.tsx index 76b19e4021..5bdb44077a 100644 --- a/playground/ty/src/Playground.tsx +++ b/playground/ty/src/Playground.tsx @@ -292,6 +292,10 @@ export const DEFAULT_SETTINGS = JSON.stringify( environment: { "python-version": "3.14", }, + analysis: { + "strict-equality-semantics": false, + "strict-generic-narrowing": false, + }, rules: { "experimental-syntax": "ignore", "undefined-reveal": "ignore", diff --git a/pyproject.toml b/pyproject.toml index c42e18f4e5..db5f15cdf6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,7 @@ strip = true [dependency-groups] dev = [ - "prek==0.4.9", + "prek==0.4.12", ] docs = [ "basedpython-pygments", diff --git a/python/ruff-ecosystem/pyproject.toml b/python/ruff-ecosystem/pyproject.toml index 434b03dd0a..8b8d2deea0 100644 --- a/python/ruff-ecosystem/pyproject.toml +++ b/python/ruff-ecosystem/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" name = "ruff-ecosystem" version = "0.0.0" requires-python = ">=3.11" -dependencies = ["unidiff==0.7.5", "tomli_w==1.2.0", "tomli==2.4.1"] +dependencies = ["unidiff==1.0.0", "tomli_w==1.2.0", "tomli==2.4.1"] [project.scripts] ruff-ecosystem = "ruff_ecosystem.cli:entrypoint" diff --git a/python/ruff-ecosystem/ruff_ecosystem/check.py b/python/ruff-ecosystem/ruff_ecosystem/check.py index 78fae8f162..9d92fd4187 100644 --- a/python/ruff-ecosystem/ruff_ecosystem/check.py +++ b/python/ruff-ecosystem/ruff_ecosystem/check.py @@ -22,6 +22,7 @@ markdown_plus_minus, markdown_project_section, ) +from ruff_ecosystem.projects import rule_name_to_code from ruff_ecosystem.types import ( Comparison, Diff, @@ -508,7 +509,13 @@ async def compare_check( config_overrides: ConfigOverrides, cloned_repo: ClonedRepository, ) -> Comparison: - with config_overrides.patch_config(cloned_repo.path, options.preview): + # TODO(brent) Remove this workaround when human-readable rule names are stabilized. + rule_names = ( + rule_name_to_code(ruff_comparison_executable.resolve()) + if not options.preview + else {} + ) + with config_overrides.patch_config(cloned_repo.path, options.preview, rule_names): async with asyncio.TaskGroup() as tg: baseline_task = tg.create_task( ruff_check( diff --git a/python/ruff-ecosystem/ruff_ecosystem/defaults.py b/python/ruff-ecosystem/ruff_ecosystem/defaults.py index a5dc724b19..947fae01e9 100644 --- a/python/ruff-ecosystem/ruff_ecosystem/defaults.py +++ b/python/ruff-ecosystem/ruff_ecosystem/defaults.py @@ -23,12 +23,6 @@ Project( repo=Repository(owner="apache", name="airflow", ref="main"), check_options=CheckOptions(select="ALL"), - config_overrides={ - # Broken symlink - "exclude": [ - "task-sdk/src/airflow/sdk/_shared/AGENTS.md", - ] - }, ), Project( repo=Repository(owner="apache", name="superset", ref="master"), @@ -41,15 +35,7 @@ repo=Repository(owner="bokeh", name="bokeh", ref="branch-3.10"), check_options=CheckOptions(select="ALL"), ), - # Disabled due to use of explicit `select` with `E999`, which has been removed. - # See: https://github.com/astral-sh/ruff/pull/12129 - # Project( - # repo=Repository(owner="demisto", name="content", ref="master"), - # format_options=FormatOptions( - # # Syntax errors in this file - # exclude="Packs/ThreatQ/Integrations/ThreatQ/ThreatQ.py" - # ), - # ), + Project(repo=Repository(owner="demisto", name="content", ref="master")), Project(repo=Repository(owner="docker", name="docker-py", ref="main")), Project(repo=Repository(owner="facebookresearch", name="chameleon", ref="main")), Project(repo=Repository(owner="freedomofpress", name="securedrop", ref="develop")), @@ -60,6 +46,7 @@ Project(repo=Repository(owner="langchain-ai", name="langchain", ref="master")), Project(repo=Repository(owner="latchbio", name="latch", ref="main")), Project(repo=Repository(owner="lnbits", name="lnbits", ref="main")), + Project(repo=Repository(owner="mhammond", name="pywin32", ref="main")), Project(repo=Repository(owner="milvus-io", name="pymilvus", ref="master")), Project(repo=Repository(owner="mlflow", name="mlflow", ref="master")), Project(repo=Repository(owner="model-bakers", name="model_bakery", ref="main")), @@ -88,19 +75,17 @@ Project( repo=Repository(owner="scikit-build", name="scikit-build-core", ref="main") ), - # TODO(charlie): Ecosystem check fails in non-preview due to the direct - # selection of preview rules. - # Project( - # repo=Repository( - # owner="sphinx-doc", - # name="sphinx", - # ref="master", - # ), - # format_options=FormatOptions( - # # Does not contain valid UTF-8 - # exclude="tests/roots/test-pycode/cp_1251_coded.py" - # ), - # ), + Project( + repo=Repository( + owner="sphinx-doc", + name="sphinx", + ref="master", + ), + format_options=FormatOptions( + # Does not contain valid UTF-8 + exclude="tests/roots/test-pycode/cp_1251_coded.py" + ), + ), Project(repo=Repository(owner="spruceid", name="siwe-py", ref="main")), Project(repo=Repository(owner="tiangolo", name="fastapi", ref="master")), Project(repo=Repository(owner="yandex", name="ch-backup", ref="main")), diff --git a/python/ruff-ecosystem/ruff_ecosystem/format.py b/python/ruff-ecosystem/ruff_ecosystem/format.py index b503a51dc6..5415cecb77 100644 --- a/python/ruff-ecosystem/ruff_ecosystem/format.py +++ b/python/ruff-ecosystem/ruff_ecosystem/format.py @@ -16,6 +16,7 @@ from ruff_ecosystem import logger from ruff_ecosystem.markdown import markdown_project_section +from ruff_ecosystem.projects import rule_name_to_code from ruff_ecosystem.types import Comparison, Diff, Result, ToolError if TYPE_CHECKING: @@ -173,7 +174,13 @@ async def format_then_format( config_overrides: ConfigOverrides, cloned_repo: ClonedRepository, ) -> Sequence[str]: - with config_overrides.patch_config(cloned_repo.path, options.preview): + # TODO(brent) Remove this workaround when human-readable rule names are stabilized. + rule_names = ( + rule_name_to_code(ruff_comparison_executable.resolve()) + if not options.preview + else {} + ) + with config_overrides.patch_config(cloned_repo.path, options.preview, rule_names): # Run format to get the baseline await format( formatter=baseline_formatter, @@ -201,7 +208,13 @@ async def format_and_format( config_overrides: ConfigOverrides, cloned_repo: ClonedRepository, ) -> Sequence[str]: - with config_overrides.patch_config(cloned_repo.path, options.preview): + # TODO(brent) Remove this workaround when human-readable rule names are stabilized. + rule_names = ( + rule_name_to_code(ruff_comparison_executable.resolve()) + if not options.preview + else {} + ) + with config_overrides.patch_config(cloned_repo.path, options.preview, rule_names): # Run format without diff to get the baseline await format( formatter=baseline_formatter, @@ -218,7 +231,7 @@ async def format_and_format( # Then reset await cloned_repo.reset() - with config_overrides.patch_config(cloned_repo.path, options.preview): + with config_overrides.patch_config(cloned_repo.path, options.preview, rule_names): # Then run format again await format( formatter=Formatter.ruff, diff --git a/python/ruff-ecosystem/ruff_ecosystem/projects.py b/python/ruff-ecosystem/ruff_ecosystem/projects.py index e25d2fd10f..2e86360817 100644 --- a/python/ruff-ecosystem/ruff_ecosystem/projects.py +++ b/python/ruff-ecosystem/ruff_ecosystem/projects.py @@ -7,12 +7,13 @@ import abc import contextlib import dataclasses +import json from asyncio import create_subprocess_exec from dataclasses import dataclass, field from enum import Enum from functools import cache from pathlib import Path -from subprocess import DEVNULL, PIPE +from subprocess import DEVNULL, PIPE, check_output from typing import Any, Self import tomli @@ -55,6 +56,61 @@ def __post_init__(self): "required-version": None } +# TODO(brent) Remove selector normalization when human-readable rule names are stabilized. +RULE_SELECTOR_OPTIONS = ( + "select", + "extend-select", + "ignore", + "extend-ignore", + "fixable", + "extend-fixable", + "unfixable", + "extend-unfixable", + "extend-safe-fixes", + "extend-unsafe-fixes", +) + + +@cache +def rule_name_to_code(executable: Path) -> dict[str, str]: + rules = json.loads( + check_output( + [executable, "rule", "--all", "--output-format", "json"], + encoding="utf8", + ) + ) + return {rule["name"]: rule["code"] for rule in rules} + + +def normalize_rule_selectors( + config: dict[str, Any], rule_names: dict[str, str] +) -> None: + selector_lists: list[list[Any]] = [] + + for section in (config, config.get("lint")): + if not isinstance(section, dict): + continue + + for option in RULE_SELECTOR_OPTIONS: + if isinstance(selectors := section.get(option), list): + selector_lists.append(selectors) + + for option in ("per-file-ignores", "extend-per-file-ignores"): + if isinstance(per_file_ignores := section.get(option), dict): + selector_lists.extend( + selectors + for selectors in per_file_ignores.values() + if isinstance(selectors, list) + ) + + for selectors in selector_lists: + selectors[:] = [ + rule_names.get(selector, selector) + if isinstance(selector, str) + else selector + for selector in selectors + ] + @dataclass(frozen=True) class ConfigOverrides(Serializable): @@ -91,6 +147,7 @@ def patch_config( self, dirpath: Path, preview: bool, + rule_names: dict[str, str], ) -> None: """ Temporarily patch the Ruff configuration file in the given directory. @@ -153,6 +210,12 @@ def patch_config( else: target[names[-1]] = value + if not preview: + ruff_config = toml + for name in base: + ruff_config = ruff_config[name] + normalize_rule_selectors(ruff_config, rule_names) + tomli_w.dump(toml, path.open("wb")) try: diff --git a/ruff.schema.json b/ruff.schema.json index b24b9d0127..85a4b65733 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -145,7 +145,7 @@ } }, "extend-select": { - "description": "A list of rule codes or prefixes to enable, in addition to those\nspecified by [`select`](#lint_select).\n\nUnlike [`select`](#lint_select), which _replaces_ the default rule set\nwhen specified, `extend-select` _adds_ to whatever rules are already\nactive. This makes `extend-select` the preferred option when you want\nto enable additional rules on top of the defaults without having to\nenumerate them.\n\nFor example, to enable the defaults plus flake8-bugbear:\n\n```toml\n[tool.ruff.lint]\n# Adds flake8-bugbear on top of the default rules (E4, E7, E9, F).\nextend-select = [\"B\"]\n```\n\nUsing `select = [\"B\"]` instead would _replace_ the defaults, enabling\nonly flake8-bugbear.", + "description": "A list of rule codes or prefixes to enable, in addition to those\nspecified by [`select`](#lint_select).\n\nUnlike [`select`](#lint_select), which _replaces_ the default rule set\nwhen specified, `extend-select` _adds_ to whatever rules are already\nactive. This makes `extend-select` the preferred option when you want\nto enable additional rules on top of the defaults without having to\nenumerate them.\n\nFor example, to enable the defaults plus flake8-bugbear:\n\n```toml\n[tool.ruff.lint]\n# Adds flake8-bugbear on top of the default rules.\nextend-select = [\"B\"]\n```\n\nUsing `select = [\"B\"]` instead would _replace_ the defaults, enabling\nonly flake8-bugbear.", "type": [ "array", "null" @@ -562,6 +562,13 @@ } ] }, + "output-prefer-rule-codes": { + "description": "Whether to prefer rule codes over human-readable rule names in diagnostic output, even\nwhen preview mode is enabled.\n\nDiagnostics without rule codes, such as syntax errors and formatting diagnostics, will\ncontinue to use the human-readable name, but those corresponding to lint rules will use the\nrule's code. For example, the concise diagnostic for an unused import will use the code\n`F401` instead of the name `unused-import`:\n\n```console\n$ ruff check --preview --config 'output-prefer-rule-codes = true' --output-format=concise example.py\nexample.py:1:8: F401 [*] `math` imported but unused\n$ ruff check --preview --config 'output-prefer-rule-codes = false' --output-format=concise example.py\nexample.py:1:8: unused-import: [*] `math` imported but unused\n```", + "type": [ + "boolean", + "null" + ] + }, "pep8-naming": { "description": "Options for the `pep8-naming` plugin.", "anyOf": [ @@ -1269,7 +1276,7 @@ "uniqueItems": true }, "extend-aliases": { - "description": "A mapping from module to conventional import alias. These aliases will\nbe added to the [`aliases`](#lint_flake8-import-conventions_aliases) mapping.", + "description": "A mapping from module to conventional import alias. These aliases will\nbe added to the [`aliases`](#lint_flake8-import-conventions_aliases) mapping\nand will override any existing `aliases` if the two settings overlap.", "type": [ "object", "null" @@ -2227,7 +2234,7 @@ } }, "extend-select": { - "description": "A list of rule codes or prefixes to enable, in addition to those\nspecified by [`select`](#lint_select).\n\nUnlike [`select`](#lint_select), which _replaces_ the default rule set\nwhen specified, `extend-select` _adds_ to whatever rules are already\nactive. This makes `extend-select` the preferred option when you want\nto enable additional rules on top of the defaults without having to\nenumerate them.\n\nFor example, to enable the defaults plus flake8-bugbear:\n\n```toml\n[tool.ruff.lint]\n# Adds flake8-bugbear on top of the default rules (E4, E7, E9, F).\nextend-select = [\"B\"]\n```\n\nUsing `select = [\"B\"]` instead would _replace_ the defaults, enabling\nonly flake8-bugbear.", + "description": "A list of rule codes or prefixes to enable, in addition to those\nspecified by [`select`](#lint_select).\n\nUnlike [`select`](#lint_select), which _replaces_ the default rule set\nwhen specified, `extend-select` _adds_ to whatever rules are already\nactive. This makes `extend-select` the preferred option when you want\nto enable additional rules on top of the defaults without having to\nenumerate them.\n\nFor example, to enable the defaults plus flake8-bugbear:\n\n```toml\n[tool.ruff.lint]\n# Adds flake8-bugbear on top of the default rules.\nextend-select = [\"B\"]\n```\n\nUsing `select = [\"B\"]` instead would _replace_ the defaults, enabling\nonly flake8-bugbear.", "type": [ "array", "null" diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 398f3f015c..7243604323 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.97.0" +channel = "1.97.1" diff --git a/scripts/benchmarks/pyproject.toml b/scripts/benchmarks/pyproject.toml index 72d3f15a68..6a1171ac92 100644 --- a/scripts/benchmarks/pyproject.toml +++ b/scripts/benchmarks/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "scripts" -version = "0.15.22" +version = "0.16.2" description = "" authors = ["Charles Marsh "] diff --git a/scripts/build_ruff_pgo.py b/scripts/build_ruff_pgo.py new file mode 100644 index 0000000000..c9347aeff2 --- /dev/null +++ b/scripts/build_ruff_pgo.py @@ -0,0 +1,556 @@ +"""Build Ruff with profile-guided optimization using pinned ecosystem projects.""" + +# /// script +# requires-python = ">=3.11" +# dependencies = [] +# /// + +from __future__ import annotations + +import argparse +import os +import re +import shlex +import subprocess +import sys +import tempfile +import time +from dataclasses import dataclass +from pathlib import Path + +REPOSITORY_ROOT = Path(__file__).resolve().parent.parent +EXCLUDED_DIRECTORIES = frozenset({"_tests", "_vendor", "test", "tests"}) +# basedpython ships the linter as `buff`, so the `ruff` package's binary is not called `ruff` +BINARY_STEM = "buff" + + +@dataclass(frozen=True, slots=True) +class EcosystemProject: + name: str + repository: str + revision: str + source_directories: tuple[str, ...] + + def __post_init__(self): + if re.fullmatch(r"[0-9a-f]{40}", self.revision) is None: + raise ValueError( + f"{self.repository} must be pinned to a full Git commit SHA, " + f"got {self.revision!r}" + ) + + @property + def url(self) -> str: + return f"https://github.com/{self.repository}.git" + + +# Train on a subset of the pinned ecosystem projects that we already use for +# linting, formatting, or type checking. The goal is to create a representative +# corpus that includes scientific computing, synchronous and asynchronous code, +# applications, libraries, and type stubs. +# +# But it wasn't a highly optimized selection process. (For example, during +# development, we added Zulip and Warehouse, which reduced Ruff's CPU time by +# 0.35% while increasing its wheel size by 0.44%.) +CORPUS_PROJECTS = ( + EcosystemProject( + name="pytest", + repository="pytest-dev/pytest", + revision="28e86a6c2ae0173831e4925a4af89b02a2936d09", + source_directories=("src/_pytest",), + ), + EcosystemProject( + name="httpx", + repository="encode/httpx", + revision="b5addb64f0161ff6bfe94c124ef76f6a1fba5254", + source_directories=("httpx",), + ), + EcosystemProject( + name="fastapi", + repository="fastapi/fastapi", + revision="a375f6b948b99fa4260129856bbf11d037f363ef", + source_directories=("fastapi",), + ), + EcosystemProject( + name="anyio", + repository="agronholm/anyio", + revision="ffe91331adb912c5d150f5d373f7cd28a0e96a62", + source_directories=("src/anyio",), + ), + EcosystemProject( + name="zulip", + repository="zulip/zulip", + revision="ccddbba7a3074283ccaac3bde35fd32b19faf042", + source_directories=("zerver/views", "zerver/models"), + ), + EcosystemProject( + name="warehouse", + repository="pypi/warehouse", + revision="5a4d2cadec641b5d6a6847d0127940e0f532f184", + source_directories=( + "warehouse/accounts", + "warehouse/oidc", + "warehouse/forklift", + ), + ), + EcosystemProject( + name="pip", + repository="pypa/pip", + revision="d1fd55753405fd728a0751a578e27c1054acdf48", + source_directories=("src/pip/_internal",), + ), + EcosystemProject( + name="sphinx", + repository="sphinx-doc/sphinx", + revision="b06d92e80eed130e1dd4e67cac4afa1267424f1a", + source_directories=( + "sphinx/builders", + "sphinx/ext/autodoc", + "sphinx/domains/python", + ), + ), + EcosystemProject( + name="astropy", + repository="astropy/astropy", + revision="b779108c7cec25c840c0f744fdf2a1550441e309", + source_directories=("astropy/units",), + ), + EcosystemProject( + name="typeshed", + repository="python/typeshed", + revision="e0efbeef901e9b6998d016e1ab9352678f09ae77", + source_directories=( + "stdlib/asyncio", + "stdlib/collections", + "stubs/requests", + ), + ), +) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--target", help="Host-native Rust target triple") + parser.add_argument( + "--target-dir", + type=Path, + help="Cargo target directory (default: CARGO_TARGET_DIR or target/ruff-pgo)", + ) + parser.add_argument( + "--profile-dir", + type=Path, + help="Raw profile directory (default: /profiles)", + ) + parser.add_argument( + "--llvm-profdata", + type=Path, + help="Override the active Rust toolchain's llvm-profdata executable", + ) + mode = parser.add_mutually_exclusive_group() + mode.add_argument( + "--train-only", + action="store_true", + help="Only produce /ruff.profdata for a subsequent release build", + ) + mode.add_argument( + "--prepare-corpus", + action="store_true", + help="Only download and prepare the pinned ecosystem training corpus", + ) + args = parser.parse_args() + + target_dir = ( + args.target_dir + or Path( + os.environ.get("CARGO_TARGET_DIR", REPOSITORY_ROOT / "target" / "ruff-pgo") + ) + ).resolve() + profile_dir = (args.profile_dir or target_dir / "profiles").resolve() + merged_profile = target_dir / "ruff.profdata" + + environment = os.environ.copy() + if args.prepare_corpus: + corpus = ecosystem_python_files(target_dir / "corpus", environment=environment) + write_corpus_arguments(target_dir, corpus) + print(f"Prepared {len(corpus)} ecosystem Python files", flush=True) + return + + host = rustc_host() + target = args.target or host + if target != host: + parser.error( + f"PGO training requires the host-native target {host}, got {target}" + ) + + profiler = find_llvm_profdata(host, args.llvm_profdata) + corpus = ecosystem_python_files(target_dir / "corpus", environment=environment) + corpus_arguments = write_corpus_arguments(target_dir, corpus) + + profile_dir.mkdir(parents=True, exist_ok=True) + for profile in profile_dir.glob("ruff-*.profraw"): + profile.unlink() + + environment["CARGO_INCREMENTAL"] = "0" + if target.endswith("-apple-darwin"): + for variable in ("CFLAGS", "CXXFLAGS"): + environment[variable] = append_flags( + environment.get(variable), "-fno-profile-generate -fno-profile-use" + ) + + instrumented_target_dir = target_dir / "instrumented" + instrumented_environment = environment | { + "CARGO_TARGET_DIR": str(instrumented_target_dir), + "RUSTFLAGS": append_flags( + environment.get("RUSTFLAGS"), f"-Cprofile-generate={profile_dir}" + ), + } + print("Building instrumented release Ruff", flush=True) + run(cargo_command(target), environment=instrumented_environment) + + binary_name = f"{BINARY_STEM}.exe" if "windows" in target else BINARY_STEM + instrumented_binary = instrumented_target_dir / target / "release" / binary_name + if not instrumented_binary.is_file(): + raise RuntimeError(f"Instrumented Ruff binary not found: {instrumented_binary}") + + profiles = train_ruff( + instrumented_binary, + corpus_arguments, + profile_dir, + corpus_size=len(corpus), + environment=instrumented_environment, + ) + merge_profiles(profiler, profiles, merged_profile, environment=environment) + + if args.train_only: + return + + optimized_environment = environment | { + "CARGO_TARGET_DIR": str(target_dir), + "RUSTFLAGS": append_flags( + environment.get("RUSTFLAGS"), f"-Cprofile-use={merged_profile}" + ), + } + print("Building optimized release Ruff", flush=True) + run(cargo_command(target), environment=optimized_environment) + print( + f"Optimized Ruff: {target_dir / target / 'release' / binary_name}", flush=True + ) + + +def train_ruff( + binary: Path, + corpus_arguments: Path, + profile_directory: Path, + *, + corpus_size: int, + environment: dict[str, str], +) -> list[Path]: + common_arguments = [ + "--isolated", + "--target-version", + "py314", + "--no-cache", + "--silent", + ] + workloads = ( + ("check", "--exit-zero", (0,)), + ("format", "--check", (0, 1)), + ) + print(f"Training on {corpus_size} ecosystem Python files", flush=True) + profiles = [] + + for mode, mode_argument, allowed_exit_codes in workloads: + run( + [ + str(binary), + mode, + *common_arguments, + mode_argument, + f"@{corpus_arguments}", + ], + environment=environment + | { + "LLVM_PROFILE_FILE": str( + profile_directory / f"ruff-{mode}-%m-%p.profraw" + ) + }, + allowed_exit_codes=allowed_exit_codes, + ) + + workload_profiles = sorted(profile_directory.glob(f"ruff-{mode}-*.profraw")) + if not workload_profiles or any( + profile.stat().st_size == 0 for profile in workload_profiles + ): + raise RuntimeError( + f"No complete Ruff {mode} profiling data found in {profile_directory}" + ) + profiles.extend(workload_profiles) + + return profiles + + +def merge_profiles( + profiler: Path, + profiles: list[Path], + destination: Path, + *, + environment: dict[str, str], +): + profile_size = sum(profile.stat().st_size for profile in profiles) + + with tempfile.NamedTemporaryFile( + dir=destination.parent, prefix="ruff-", suffix=".profdata", delete=False + ) as temporary_file: + temporary_profile = Path(temporary_file.name) + try: + run( + [ + str(profiler), + "merge", + "--output", + str(temporary_profile), + *map(str, profiles), + ], + environment=environment, + ) + temporary_profile.replace(destination) + finally: + temporary_profile.unlink(missing_ok=True) + print( + f"Merged {len(profiles)} PGO profiles ({profile_size:,} bytes): {destination}", + flush=True, + ) + + +def rustc_host() -> str: + version = subprocess.run( + ["rustc", "--version", "--verbose"], + cwd=REPOSITORY_ROOT, + check=True, + capture_output=True, + text=True, + ).stdout + for line in version.splitlines(): + if line.startswith("host: "): + return line.removeprefix("host: ") + raise RuntimeError("Could not determine the active Rust compiler's host target") + + +def find_llvm_profdata(host: str, override: Path | None) -> Path: + if override is not None: + profiler = override.resolve() + else: + sysroot = subprocess.run( + ["rustc", "--print", "sysroot"], + cwd=REPOSITORY_ROOT, + check=True, + capture_output=True, + text=True, + ).stdout.strip() + binary_name = "llvm-profdata.exe" if "windows" in host else "llvm-profdata" + profiler = Path(sysroot) / "lib" / "rustlib" / host / "bin" / binary_name + + if not profiler.is_file() or not os.access(profiler, os.X_OK): + raise RuntimeError( + f"Rust toolchain llvm-profdata not found: {profiler}; " + "run `rustup component add llvm-tools-preview`" + ) + return profiler + + +def ecosystem_python_files( + corpus_directory: Path, *, environment: dict[str, str] +) -> list[str]: + corpus_directory.mkdir(parents=True, exist_ok=True) + git_environment = environment | { + "GIT_CONFIG_GLOBAL": os.devnull, + "GIT_TERMINAL_PROMPT": "0", + "GIT_LFS_SKIP_SMUDGE": "1", + } + paths: list[str] = [] + + for project in CORPUS_PROJECTS: + checkout = corpus_directory / project.name + checkout.mkdir(parents=True, exist_ok=True) + git = ["git", "-c", f"core.hooksPath={os.devnull}", "-C", str(checkout)] + + if not (checkout / ".git").is_dir(): + print(f"Preparing {project.repository}@{project.revision}", flush=True) + run([*git, "init", "--quiet"], environment=git_environment) + run( + [ + *git, + "remote", + "add", + "origin", + project.url, + ], + environment=git_environment, + ) + + remote = subprocess.run( + [*git, "config", "--local", "--get", "remote.origin.url"], + cwd=REPOSITORY_ROOT, + env=git_environment, + check=True, + capture_output=True, + text=True, + ).stdout.strip() + if remote != project.url: + raise RuntimeError( + f"Unexpected origin for cached {project.name} checkout: " + f"expected {project.url}, got {remote}" + ) + + run( + [*git, "sparse-checkout", "set", "--cone", *project.source_directories], + environment=git_environment, + ) + + current_revision = subprocess.run( + [*git, "rev-parse", "--verify", "HEAD"], + cwd=REPOSITORY_ROOT, + env=git_environment, + check=False, + capture_output=True, + text=True, + ) + if ( + current_revision.returncode != 0 + or current_revision.stdout.strip() != project.revision + ): + run_git_with_retry( + [ + *git, + "fetch", + "--quiet", + "--no-tags", + "--no-recurse-submodules", + "--depth=1", + "--filter=blob:none", + "origin", + project.revision, + ], + environment=git_environment, + ) + + run_git_with_retry( + [ + *git, + "checkout", + "--quiet", + "--detach", + "--force", + "--no-recurse-submodules", + project.revision, + ], + environment=git_environment, + ) + + for source_directory in project.source_directories: + source = checkout / source_directory + if not source.is_dir(): + raise RuntimeError( + f"Missing training source directory {source_directory!r} " + f"in {project.repository}@{project.revision}" + ) + + tracked_files = subprocess.run( + [*git, "ls-files", "-z", "--", *project.source_directories], + cwd=REPOSITORY_ROOT, + env=git_environment, + check=True, + capture_output=True, + ).stdout.split(b"\0") + project_paths = [ + str(path) + for tracked_file in tracked_files + if tracked_file + and (path := checkout / os.fsdecode(tracked_file)).suffix in {".py", ".pyi"} + and path.is_file() + and not path.is_symlink() + and not EXCLUDED_DIRECTORIES.intersection( + path.relative_to(checkout).parts[:-1] + ) + ] + + if not project_paths: + raise RuntimeError( + f"No Python training files found in {project.repository}" + ) + paths.extend(sorted(project_paths)) + print(f" {project.name}: {len(project_paths)} Python files", flush=True) + + return paths + + +def run_git_with_retry(command: list[str], *, environment: dict[str, str]): + for attempt in range(3): + try: + run(command, environment=environment) + return + except subprocess.CalledProcessError: + if attempt == 2: + raise + delay = 2**attempt + print( + f"Git command failed; retrying in {delay}s (attempt {attempt + 2} of 3)", + file=sys.stderr, + flush=True, + ) + time.sleep(delay) + + +def write_corpus_arguments(target_directory: Path, corpus: list[str]) -> Path: + arguments = target_directory / "ruff-pgo.args" + arguments.write_text("\n".join(corpus) + "\n", encoding="utf-8", newline="\n") + return arguments + + +def cargo_command(target: str) -> list[str]: + return [ + "cargo", + "rustc", + "--release", + "--locked", + "--package", + "ruff", + "--bin", + BINARY_STEM, + "--target", + target, + "--", + "-C", + "strip=symbols", + ] + + +def append_flags(existing: str | None, additional: str) -> str: + return " ".join(flag for flag in (existing, additional) if flag) + + +def run( + command: list[str], + *, + environment: dict[str, str], + allowed_exit_codes: tuple[int, ...] = (0,), +): + logged_arguments = 16 + displayed_command = shlex.join(command[:logged_arguments]) + if len(command) > logged_arguments: + displayed_command += ( + f" ... ({len(command) - logged_arguments} arguments omitted)" + ) + print(f"> {displayed_command}", flush=True) + completed = subprocess.run( + command, cwd=REPOSITORY_ROOT, env=environment, check=False + ) + if completed.returncode not in allowed_exit_codes: + raise subprocess.CalledProcessError(completed.returncode, command) + + +if __name__ == "__main__": + try: + main() + except (OSError, RuntimeError, subprocess.CalledProcessError) as error: + print(f"error: {error}", file=sys.stderr) + raise SystemExit(1) from error diff --git a/scripts/generate_mkdocs.py b/scripts/generate_mkdocs.py index 0d596c9b71..154551dac1 100644 --- a/scripts/generate_mkdocs.py +++ b/scripts/generate_mkdocs.py @@ -47,6 +47,7 @@ class Section(NamedTuple): Section("Configuring Ruff", "configuration.md", generated=False), Section("Preview", "preview.md", generated=False), Section("Rules", "rules.md", generated=True), + Section("Default Rules", "default-rules.md", generated=True), Section("Settings", "settings.md", generated=True), Section("Versioning", "versioning.md", generated=False), Section("Integrations", "integrations.md", generated=False), @@ -71,6 +72,7 @@ class Section(NamedTuple): # ), # "https://docs.astral.sh/ruff/installation/": "installation.md", # "https://docs.astral.sh/ruff/rules/": "rules.md", + # "https://docs.astral.sh/ruff/default-rules/": "default-rules.md", # "https://docs.astral.sh/ruff/settings/": "settings.md", # "#whos-using-ruff": "https://github.com/astral-sh/ruff#whos-using-ruff", # "https://docs.astral.sh/ruff/preview/": "preview.md", @@ -214,6 +216,11 @@ def main(): ["cargo", "dev", "generate-rules-table"], encoding="utf-8", ) + elif filename == "default-rules.md": + file_content = subprocess.check_output( + ["cargo", "dev", "generate-default-rules"], + encoding="utf-8", + ) else: block = content.split(f"\n\n") if len(block) != 2: diff --git a/scripts/memory_report.py b/scripts/memory_report.py index b9ef891824..a6e9756a8a 100644 --- a/scripts/memory_report.py +++ b/scripts/memory_report.py @@ -4,13 +4,13 @@ This script can be used in two modes: 1. Report comparison mode: Reads pre-generated JSON memory reports and compares them. -2. Full run mode: Clones projects, builds ty, runs memory tests, and generates comparison. +2. Full run mode: Sets up projects, runs memory tests, and generates comparison. Examples: # Compare pre-generated memory reports %(prog)s compare --old-dir old_reports/ --new-dir new_reports/ - # Full run: clone projects, build ty, run memory tests + # Full run: set up projects and their dependencies, then run memory tests %(prog)s run --old-ty ./ty-old --new-ty ./ty-new # Write output to a file @@ -22,21 +22,15 @@ import argparse import json import os +import shlex import subprocess import sys import tempfile -from collections.abc import Mapping from dataclasses import dataclass, field from pathlib import Path from typing import Any, Final, Self -# Known projects with their Git URLs for memory testing. -KNOWN_PROJECTS: Final[Mapping[str, str]] = { - "flake8": "https://github.com/PyCQA/flake8", - "sphinx": "https://github.com/sphinx-doc/sphinx", - "prefect": "https://github.com/PrefectHQ/prefect", - "trio": "https://github.com/python-trio/trio", -} +KNOWN_PROJECTS: Final = ("flake8", "sphinx", "prefect", "trio") @dataclass(slots=True, kw_only=True) @@ -251,25 +245,43 @@ def render_summary(projects: list[ProjectComparison]) -> str: return "\n".join(lines) -def clone_project(*, name: str, url: str, dest: Path) -> Path: - """Clone a project from Git. Returns the path to the cloned project.""" +def setup_project(*, name: str, dest: Path) -> tuple[Path, list[str]]: + """Clone a project and install its mypy-primer dependencies.""" project_path = dest / name + setup_script = Path(__file__).with_name("setup_primer_project.py") + setup_command = [ + "uv", + "run", + "--locked", + "--python", + sys.executable, + "--script", + str(setup_script), + ] + if project_path.exists(): print(f"Project {name} already exists at {project_path}", file=sys.stderr) - return project_path + else: + print(f"Setting up {name} and its dependencies...", file=sys.stderr) + subprocess.run( + [*setup_command, name, str(project_path)], + check=True, + stdout=sys.stderr, + ) - print(f"Cloning {name} from {url}...", file=sys.stderr) - subprocess.run( - ["git", "clone", "--depth=1", url, str(project_path)], + ty_command = subprocess.run( + [*setup_command, "--print-ty-command", name, str(project_path)], check=True, capture_output=True, + text=True, ) - return project_path + return project_path, shlex.split(ty_command.stdout) def run_ty_memory_check( *, ty_path: str, + ty_command: list[str], project_path: Path, output_path: Path, ): @@ -279,8 +291,14 @@ def run_ty_memory_check( env["TY_MAX_PARALLELISM"] = "1" # For deterministic memory numbers print(f"Running {ty_path} on {project_path.name}...", file=sys.stderr) + command = [ + str(Path(ty_path).resolve()) if argument == "{ty}" else argument + for argument in ty_command + ] result = subprocess.run( - [ty_path, "check", str(project_path), "--exit-zero"], + command, + cwd=project_path, + check=True, capture_output=True, text=True, env=env, @@ -301,19 +319,25 @@ def run_memory_tests( old_reports_dir.mkdir(parents=True, exist_ok=True) new_reports_dir.mkdir(parents=True, exist_ok=True) - for project_name, url in KNOWN_PROJECTS.items(): - project_path = clone_project(name=project_name, url=url, dest=projects_dir) + for project_name in KNOWN_PROJECTS: + project_path, ty_command = setup_project(name=project_name, dest=projects_dir) # Run old ty old_report_path = old_reports_dir / f"{project_name}.json" run_ty_memory_check( - ty_path=old_ty, project_path=project_path, output_path=old_report_path + ty_path=old_ty, + ty_command=ty_command, + project_path=project_path, + output_path=old_report_path, ) # Run new ty new_report_path = new_reports_dir / f"{project_name}.json" run_ty_memory_check( - ty_path=new_ty, project_path=project_path, output_path=new_report_path + ty_path=new_ty, + ty_command=ty_command, + project_path=project_path, + output_path=new_report_path, ) @@ -455,7 +479,7 @@ def parse_args() -> argparse.Namespace: # Run subcommand run_parser = subparsers.add_parser( "run", - help="Clone projects, run ty, and compare memory usage", + help="Set up projects, run ty, and compare memory usage", ) run_parser.add_argument( "--old-ty", diff --git a/scripts/setup_primer_project.py b/scripts/setup_primer_project.py index cede47b74c..49a8cdbd1d 100644 --- a/scripts/setup_primer_project.py +++ b/scripts/setup_primer_project.py @@ -12,7 +12,9 @@ # exclude-newer = "7 days" # # [tool.uv.sources] -# mypy-primer = { git = "https://github.com/hauntsaninja/mypy_primer" } +# # Keep this revision and the script's lockfile in sync with ecosystem-analyzer's +# # mypy-primer pin so memory reports and ecosystem jobs use the same project definitions. +# mypy-primer = { git = "https://github.com/hauntsaninja/mypy_primer", rev = "6d6eebd8d37c9b8931381e79aa99808d9378c988" } # /// """Clone a mypy-primer project and set up a virtualenv with its dependencies installed. @@ -91,6 +93,11 @@ def main(): "--exclude-newer", help="Limit dependency resolution to packages uploaded before this timestamp", ) + parser.add_argument( + "--print-ty-command", + action="store_true", + help="Print the project-specific ty command without setting up the project", + ) args = parser.parse_args() project = find_project(args.project) @@ -99,6 +106,9 @@ def main(): revision = args.revision or project.revision or "" target_dir = Path(args.directory or project.name).resolve() + if args.print_ty_command: + print(get_ty_command(project, ty_binary="{ty}", venv_dir=target_dir / ".venv")) + return # Use a full clone only when a historical ecosystem report revision must be checked out. clone_cmd = [ diff --git a/scripts/setup_primer_project.py.lock b/scripts/setup_primer_project.py.lock index 2034e8f7f5..feea69058f 100644 --- a/scripts/setup_primer_project.py.lock +++ b/scripts/setup_primer_project.py.lock @@ -7,9 +7,9 @@ exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for exclude-newer-span = "P7D" [manifest] -requirements = [{ name = "mypy-primer", git = "https://github.com/hauntsaninja/mypy_primer" }] +requirements = [{ name = "mypy-primer", git = "https://github.com/hauntsaninja/mypy_primer?rev=6d6eebd8d37c9b8931381e79aa99808d9378c988" }] [[package]] name = "mypy-primer" version = "0.1.0" -source = { git = "https://github.com/hauntsaninja/mypy_primer#05f73ec3d85bb4f55676f3c57f2c3e5136228977" } +source = { git = "https://github.com/hauntsaninja/mypy_primer?rev=6d6eebd8d37c9b8931381e79aa99808d9378c988#6d6eebd8d37c9b8931381e79aa99808d9378c988" } diff --git a/scripts/ty_benchmark/pyproject.toml b/scripts/ty_benchmark/pyproject.toml index b34da1e599..5bcb0a4ce9 100644 --- a/scripts/ty_benchmark/pyproject.toml +++ b/scripts/ty_benchmark/pyproject.toml @@ -9,7 +9,7 @@ dependencies = [ # Pyright is missing because we install it with `npm` to avoid measuring the overhead # of the Python wrapper script (that lazily installs Pyright). "mslex>=1.3.0", - "pyrefly==1.1.1", + "pyrefly==1.2.0", "pytest-benchmark>=4.0.0", "pytest>=8.0.0", "pygls>=2.0.0", diff --git a/scripts/ty_benchmark/src/benchmark/__init__.py b/scripts/ty_benchmark/src/benchmark/__init__.py index 904dac3aec..cdeebb1bd7 100644 --- a/scripts/ty_benchmark/src/benchmark/__init__.py +++ b/scripts/ty_benchmark/src/benchmark/__init__.py @@ -3,8 +3,9 @@ import logging import subprocess import sys +from collections.abc import Mapping from pathlib import Path -from typing import Mapping, NamedTuple +from typing import NamedTuple if sys.platform == "win32": import mslex as shlex @@ -12,6 +13,9 @@ import shlex +logger = logging.getLogger(__name__) + + class Command(NamedTuple): name: str """The name of the command to benchmark.""" @@ -76,6 +80,6 @@ def run(self, *, cwd: Path | None = None, env: Mapping[str, str]): for command in self.commands: args.append(shlex.join(command.command)) - logging.info(f"Running {args}") + logger.info(f"Running {args}") - subprocess.run(args, cwd=cwd, env=env) + subprocess.run(args, cwd=cwd, env=env, check=True) diff --git a/scripts/ty_benchmark/src/benchmark/lsp_client.py b/scripts/ty_benchmark/src/benchmark/lsp_client.py index d51f61fa09..9dc202f627 100644 --- a/scripts/ty_benchmark/src/benchmark/lsp_client.py +++ b/scripts/ty_benchmark/src/benchmark/lsp_client.py @@ -9,6 +9,8 @@ from lsprotocol import types as lsp from pygls.lsp.client import LanguageClient +logger = logging.getLogger(__name__) + def _register_notebook_structure_hooks(converter): """Register structure hooks for notebook document types to work around cattrs deserialization issues.""" @@ -64,7 +66,7 @@ def __init__( def publish_diagnostics( client: LSPClient, params: lsp.PublishDiagnosticsParams ): - logging.info( + logger.info( f"Received publish_diagnostics for {params.uri} with version={params.version}, diagnostics count={len(params.diagnostics)}" ) future = self.diagnostics.get(params.uri, None) @@ -78,11 +80,11 @@ def publish_diagnostics( @self.feature(lsp.WINDOW_LOG_MESSAGE) def log_message(client: LSPClient, params: lsp.LogMessageParams): if params.type == lsp.MessageType.Error: - logging.error(f"server error: {params.message}") + logger.error(f"server error: {params.message}") elif params.type == lsp.MessageType.Warning: - logging.warning(f"server warning: {params.message}") + logger.warning(f"server warning: {params.message}") else: - logging.info(f"server info: {params.message}") + logger.info(f"server info: {params.message}") @override async def initialize_async( @@ -92,9 +94,7 @@ async def initialize_async( self.server_capabilities = result.capabilities - logging.info( - f"Pull diagnostic support: {self.server_supports_pull_diagnostics}" - ) + logger.info(f"Pull diagnostic support: {self.server_supports_pull_diagnostics}") return result @@ -154,9 +154,9 @@ async def wait_for_push_diagnostics_async( self.diagnostics[path.as_uri()] = future try: - logging.info(f"Waiting for push diagnostics for {path}") + logger.info(f"Waiting for push diagnostics for {path}") result = await asyncio.wait_for(future, timeout) - logging.info(f"Awaited push diagnostics for {path}") + logger.info(f"Awaited push diagnostics for {path}") finally: self.diagnostics.pop(path.as_uri()) diff --git a/scripts/ty_benchmark/src/benchmark/projects.py b/scripts/ty_benchmark/src/benchmark/projects.py index 78aaf96119..18c394b5d6 100644 --- a/scripts/ty_benchmark/src/benchmark/projects.py +++ b/scripts/ty_benchmark/src/benchmark/projects.py @@ -1,9 +1,12 @@ import logging import subprocess import sys +from collections.abc import Sequence from pathlib import Path from typing import Final, Literal, NamedTuple +logger = logging.getLogger(__name__) + class Project(NamedTuple): name: str @@ -26,10 +29,10 @@ class Project(NamedTuple): skip: str | None = None """The project is skipped from benchmarking if not `None`.""" - include: list[str] = [] + include: Sequence[str] = () """The directories and files to check. If empty, checks the current directory""" - exclude: list[str] = [] + exclude: Sequence[str] = () """The directories and files to exclude from checks.""" edit: IncrementalEdit | None = None @@ -39,7 +42,7 @@ def clone(self, checkout_dir: Path): if (checkout_dir / ".git").exists(): return - logging.debug(f"Cloning {self.repository} to {checkout_dir}") + logger.debug(f"Cloning {self.repository} to {checkout_dir}") try: # git doesn't support cloning a specific revision. @@ -94,7 +97,7 @@ def clone(self, checkout_dir: Path): except subprocess.CalledProcessError as e: raise RuntimeError(f"Failed to clone {self.name}:\n\n{e.stderr}") from e - logging.info(f"Cloned {self.name} to {checkout_dir}.") + logger.info(f"Cloned {self.name} to {checkout_dir}.") class IncrementalEdit(NamedTuple): diff --git a/scripts/ty_benchmark/src/benchmark/run.py b/scripts/ty_benchmark/src/benchmark/run.py index 8facb7361b..bd8ad34454 100644 --- a/scripts/ty_benchmark/src/benchmark/run.py +++ b/scripts/ty_benchmark/src/benchmark/run.py @@ -87,7 +87,7 @@ def main(): args = parser.parse_args() logging.basicConfig( - level=logging.INFO if args.verbose else logging.WARN, + level=logging.INFO if args.verbose else logging.WARNING, format="%(asctime)s %(levelname)s %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) @@ -165,15 +165,15 @@ def main(): continue if not first: - print("") + print() print( "-------------------------------------------------------------------------------" ) - print("") + print() print(f"{project.name}") print("-" * len(project.name)) - print("") + print() if args.snapshot: # Get the directory where run.py is located to find snapshots directory. diff --git a/scripts/ty_benchmark/src/benchmark/snapshot.py b/scripts/ty_benchmark/src/benchmark/snapshot.py index e377b4d655..66c6395ce8 100644 --- a/scripts/ty_benchmark/src/benchmark/snapshot.py +++ b/scripts/ty_benchmark/src/benchmark/snapshot.py @@ -4,11 +4,14 @@ import logging import re import subprocess +from collections.abc import Mapping from pathlib import Path -from typing import Mapping, NamedTuple +from typing import NamedTuple from benchmark import Command +logger = logging.getLogger(__name__) + def normalize_output(output: str, cwd: Path) -> str: """Normalize output by replacing absolute paths with relative placeholders.""" @@ -66,7 +69,7 @@ def run(self, *, cwd: Path, env: Mapping[str, str]): # Run the prepare command if provided. if command.prepare: - logging.info(f"Running prepare: {command.prepare}") + logger.info(f"Running prepare: {command.prepare}") subprocess.run( command.prepare, cwd=cwd, @@ -76,13 +79,14 @@ def run(self, *, cwd: Path, env: Mapping[str, str]): ) # Run the actual command and capture output. - logging.info(f"Running {command.command}") + logger.info(f"Running {command.command}") result = subprocess.run( command.command, cwd=cwd, env=env, capture_output=True, text=True, + check=False, ) # Get the actual output and combine stdout and stderr for the snapshot. diff --git a/scripts/ty_benchmark/src/benchmark/test_lsp_diagnostics.py b/scripts/ty_benchmark/src/benchmark/test_lsp_diagnostics.py index 54c239df19..4c257cebf8 100644 --- a/scripts/ty_benchmark/src/benchmark/test_lsp_diagnostics.py +++ b/scripts/ty_benchmark/src/benchmark/test_lsp_diagnostics.py @@ -40,7 +40,7 @@ @pytest.fixture(scope="module", params=ALL_PROJECTS, ids=lambda p: p.name) def project_setup( request, -) -> Generator[tuple[Project, Venv], None, None]: +) -> Generator[tuple[Project, Venv]]: """Set up a project and its venv once per module (shared across all tests for this project).""" project: Project = request.param @@ -187,7 +187,7 @@ def edited_file_path(self) -> Path: def absolute_file_path(self, file_path: str) -> Path: return self.cwd / file_path - def files_to_check(self) -> Generator[Path, None, None]: + def files_to_check(self) -> Generator[Path]: yield self.edited_file_path for file in self.edit.affected_files: diff --git a/scripts/ty_benchmark/src/benchmark/venv.py b/scripts/ty_benchmark/src/benchmark/venv.py index 11299aa6ec..a467093fe9 100644 --- a/scripts/ty_benchmark/src/benchmark/venv.py +++ b/scripts/ty_benchmark/src/benchmark/venv.py @@ -6,6 +6,8 @@ from dataclasses import dataclass from pathlib import Path +logger = logging.getLogger(__name__) + @dataclass(frozen=True, kw_only=True, slots=True) class Venv: @@ -61,7 +63,7 @@ def create(*, project: str, parent: Path, python_version: str) -> Venv: def install(self, pip_install_args: list[str], *, include_mypy: bool = False): """Installs the dependencies required to type check the project.""" - logging.debug(f"Installing dependencies: {', '.join(pip_install_args)}") + logger.debug(f"Installing dependencies: {', '.join(pip_install_args)}") mypy_overrides = Path(__file__).with_name("mypy-overrides.txt") command = [ diff --git a/scripts/ty_benchmark/uv.lock b/scripts/ty_benchmark/uv.lock index f121b4c1d6..dd30a3fb58 100644 --- a/scripts/ty_benchmark/uv.lock +++ b/scripts/ty_benchmark/uv.lock @@ -116,21 +116,21 @@ wheels = [ [[package]] name = "pyrefly" -version = "1.1.1" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/20/976165fa4b1517a1a92f393b3f4d4badabfff1165eff09d4cd4908428183/pyrefly-1.1.1.tar.gz", hash = "sha256:6deda959f8603a7dbdf112c48983e2275b2903cf33c8c739ed65d7e71a4fd520", size = 5880491, upload-time = "2026-06-18T23:45:43.785Z" } +sdist = { url = "https://files.pythonhosted.org/packages/89/01/a86e9f24722b095c3f88e3616132b75a21b0df53804bdc6a45314dd4d93c/pyrefly-1.2.0.tar.gz", hash = "sha256:5485f960fc2481617068c918335c39ab1507ef90b6b5bd35bf57726e60e73185", size = 6243654, upload-time = "2026-08-01T02:56:27.592Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/d6/02ba666018c6a1cb4ddfa2db98ada721adddd374db5c29ba47a0bf2637fa/pyrefly-1.1.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f4b8595f91885bc8b5e3c282ab68d1df21201668a84e6508b1e15f2feec0bb8d", size = 13631867, upload-time = "2026-06-18T23:45:13.923Z" }, - { url = "https://files.pythonhosted.org/packages/71/47/7a3457dbbddb513a83cf4fe527d5d5ebda5201a1010ad2a6034030e3e358/pyrefly-1.1.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d6b238e1362622d47a6eb5af704fd8b613c94e8c303386efd6350e3da59fecc8", size = 13075304, upload-time = "2026-06-18T23:45:16.865Z" }, - { url = "https://files.pythonhosted.org/packages/84/df/70f4b3f42d58ed686a80df31e04eca54d88036cea4f9b96195c64ad0b2b5/pyrefly-1.1.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b50d4510e4f8aaea79e2c4b343a4d7a060c9451c0b2aa9bfe10d7ca1ef33d68d", size = 13446966, upload-time = "2026-06-18T23:45:19.644Z" }, - { url = "https://files.pythonhosted.org/packages/3c/53/12a19bd6c7af985bcbc13c6910d0f9f6684069ead2282a5c08c2bfbb5d03/pyrefly-1.1.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f330cf039ef3da3b910c84f3a7e431f0cf8d0c1d2dad26491d6cadf3c7cd4759", size = 14449222, upload-time = "2026-06-18T23:45:22.252Z" }, - { url = "https://files.pythonhosted.org/packages/93/f0/e55c48a50076fc0f9ecf4bdedec50456db383e01162f5e2121f8468be071/pyrefly-1.1.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6342d87c52b04f72156da04f554c4d57f3616f2b32d1763969efb22d05a1407", size = 14472947, upload-time = "2026-06-18T23:45:24.858Z" }, - { url = "https://files.pythonhosted.org/packages/b6/e7/30e085b31fed978ecb675bdbb54df566673ab550469e5af2d350f6af0be6/pyrefly-1.1.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c08b814ad03175e9cf47111390537161828b472044c39ab3320252b3ac6b2edd", size = 13975252, upload-time = "2026-06-18T23:45:27.247Z" }, - { url = "https://files.pythonhosted.org/packages/47/58/49c3e67641133d3fe5d8d9a660dc0826c6c37ca197d86cad05fa7dd8bfd6/pyrefly-1.1.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d50cad97f19fc893b04deff7239626cffff5dd27ffb29b7d303a1b770247b208", size = 13471780, upload-time = "2026-06-18T23:45:29.775Z" }, - { url = "https://files.pythonhosted.org/packages/71/1e/65a7ba8355e2c39d8331832905fb74dcc85fc122a3f1dfd6dbf2a88907ad/pyrefly-1.1.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2150b450ee6a6bcbe69b2d45d9a4ebc934a609e1abcf65e490433f38eb873d84", size = 13989306, upload-time = "2026-06-18T23:45:32.576Z" }, - { url = "https://files.pythonhosted.org/packages/37/de/b7ee1ab2392c36945738246fba7524439810befa3cfcc03cb6157567fc10/pyrefly-1.1.1-py3-none-win32.whl", hash = "sha256:5ffd8a8ed62fe4e6bf0afe1837d1bad149bb3b9f80e928ef248c96b836db3742", size = 12608469, upload-time = "2026-06-18T23:45:35.419Z" }, - { url = "https://files.pythonhosted.org/packages/a6/9c/a0f5b52934bf80e9c7eff08222e7caf318287b9aef76acb8d9ac5740581b/pyrefly-1.1.1-py3-none-win_amd64.whl", hash = "sha256:4e0430f3ef69c8ac73505fd6584db70ed504665a9f0816fef7f723de510f26cb", size = 13502172, upload-time = "2026-06-18T23:45:38.375Z" }, - { url = "https://files.pythonhosted.org/packages/42/3d/4c6bcb3d456835f51445d3662a428f56c3ea5643ec798c577030ae34298c/pyrefly-1.1.1-py3-none-win_arm64.whl", hash = "sha256:83baf0db71e172665db1fca0ced50b8f7773f5192ca57e8ac6773a772b6d2fc5", size = 12895979, upload-time = "2026-06-18T23:45:41.026Z" }, + { url = "https://files.pythonhosted.org/packages/7d/9d/3c0ef1d4843987b22f996ed381ec9cf5a3b1273e29804db276252e4c95eb/pyrefly-1.2.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7f46d983ac49ddd2b043694960a01dc6a19a5cfd8eec609d6bd9c42866f91b4e", size = 14026305, upload-time = "2026-08-01T02:56:02.611Z" }, + { url = "https://files.pythonhosted.org/packages/0a/06/03bbb78fbea54cdc65b626619f3597d5611aca4fdef11e72a4e8360e7e63/pyrefly-1.2.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:756f669b5555090f5c1a4fef30db1785fabe657764f7e4e6dc88994dfb8ca82d", size = 13463880, upload-time = "2026-08-01T02:56:04.93Z" }, + { url = "https://files.pythonhosted.org/packages/13/5a/7d8bc00a38e93bbc9c3e7bd14d305f7948717e667c9bcddeab9dd42fd255/pyrefly-1.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3465812ce5ef4781fb592edbf2724547296f0a3124be115d73c7e8b2401862d", size = 13907329, upload-time = "2026-08-01T02:56:07.104Z" }, + { url = "https://files.pythonhosted.org/packages/be/94/9e08b4bf799d0b8f36b55a2783c7ba5f51730cf0632a85a67b5b5ed876cd/pyrefly-1.2.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5de7b2ad2bba5c8055181681a84b74143eac2234a48ba5d1b7ed7e7a722b02bd", size = 15039020, upload-time = "2026-08-01T02:56:09.208Z" }, + { url = "https://files.pythonhosted.org/packages/5b/bd/bca5fd0c80f4daf8ee6903a29df9f3de1feb05ff0946b8f35ec8c5096b13/pyrefly-1.2.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25822ea9505f589ea8a725e4268b475132fb89e038fbf092e446510443ac142a", size = 14986199, upload-time = "2026-08-01T02:56:11.924Z" }, + { url = "https://files.pythonhosted.org/packages/97/f7/f07087f3d185ad2eced0c56cef89ca5474dfb4ff25f146cd50a861c97553/pyrefly-1.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90efe75e17491ef5d636e10469e9278d7d0256b3b4c5e1f4750069bf3ae0f5d1", size = 14393715, upload-time = "2026-08-01T02:56:14.143Z" }, + { url = "https://files.pythonhosted.org/packages/d3/70/0d142c320e284b9e3ce35e9b1e58b8ce2ee1f578f2a7234bc30e5022b94f/pyrefly-1.2.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:368aaf7eee4f511ddc0f8e564cf14e01ab2f10b0db9105c6d5b153bf498d07bf", size = 13933008, upload-time = "2026-08-01T02:56:16.525Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e8/e84f11b6e1f63fd453ad3654213b9a0f6f4de8cef6b58038eef2d0d5955d/pyrefly-1.2.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d52d5da7bc65fb7675fbaa80eda879d4f8787c494f04cac21603330d3abbdbbe", size = 14431827, upload-time = "2026-08-01T02:56:18.645Z" }, + { url = "https://files.pythonhosted.org/packages/0f/06/810d31380f66c75e1c0779a408d3b16117b1b368b57894f6aa66bef21686/pyrefly-1.2.0-py3-none-win32.whl", hash = "sha256:8c90751de8506d938e8f802659c74cf35bd7a0036510ee6c634a38eebb280bfa", size = 13229447, upload-time = "2026-08-01T02:56:20.921Z" }, + { url = "https://files.pythonhosted.org/packages/ed/98/4dafa3c7a1caed2dc8cc708dde09ba27963c7736508f55b626fff3024113/pyrefly-1.2.0-py3-none-win_amd64.whl", hash = "sha256:8a8964c224ccc4882730130955815de21ff443c1ac3f0b90685b19bf63848170", size = 14087387, upload-time = "2026-08-01T02:56:23.188Z" }, + { url = "https://files.pythonhosted.org/packages/1b/1c/df3cb0a2e5591660ded7a1836cd2f29dc48c91adb1c0a3a700a96f6d09e1/pyrefly-1.2.0-py3-none-win_arm64.whl", hash = "sha256:3a90bb8df39dfbac74b1f3b2e9d7c526b8f80568884c3944d955023a73ebf61e", size = 13430873, upload-time = "2026-08-01T02:56:25.425Z" }, ] [[package]] @@ -180,7 +180,7 @@ requires-dist = [ { name = "lsprotocol", specifier = ">=2025.0.0" }, { name = "mslex", specifier = ">=1.3.0" }, { name = "pygls", specifier = ">=2.0.0" }, - { name = "pyrefly", specifier = "==1.1.1" }, + { name = "pyrefly", specifier = "==1.2.0" }, { name = "pytest", specifier = ">=8.0.0" }, { name = "pytest-benchmark", specifier = ">=4.0.0" }, ] diff --git a/ty.schema.json b/ty.schema.json index d11b46912e..4382aa1d1c 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -215,6 +215,13 @@ "boolean", "null" ] + }, + "strict-generic-narrowing": { + "description": "Whether ty should use strict narrowing for unspecialized generic classes in\n`isinstance()` and `issubclass()` checks, as well as `match` class patterns.\n\nWhen enabled, ty narrows to the top materialization of the class. For example,\n`isinstance(value, list)` narrows a value of type `object` to `Top[list[Unknown]]`,\nrepresenting the (infinite) union of all possible `list` specializations. Iterating\nover the list would yield values of type `object`.\n\nWhen disabled, ty uses gradual generic narrowing, preserving compatible type\narguments from the original type where possible. For example,\n`isinstance(value, list)` narrows a value of type `Sequence[int]` to `list[int]`.\nIf no specialization is available, the same check narrows a value of type `object`\nto `list[Unknown]`; items of any type can then be appended to the list. Class\npatterns such as `case list():` follow the same behavior.\n\nDefaults to `false`.", + "type": [ + "boolean", + "null" + ] } }, "additionalProperties": false @@ -438,6 +445,16 @@ "Rules": { "type": "object", "properties": { + "abstract-and-final-method": { + "title": "detects methods that are both abstract and final", + "description": "## What it does\n\nChecks for methods decorated with both `@abstractmethod` and `@final`.\n\n## Why is this bad?\n\nAn abstract method must be overridden for a subclass to become concrete, but a final\nmethod cannot be overridden. Combining the decorators therefore makes it impossible\nfor a subclass to provide a concrete implementation.\n\n## Example\n\n```python\nfrom abc import ABC, abstractmethod\nfrom typing import final\n\n\nclass Base(ABC):\n @final\n @abstractmethod\n def method(self) -> None: ... # error\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, "abstract-method-in-final-class": { "title": "detects `@final` classes with unimplemented abstract methods", "description": "## What it does\n\nChecks for `@final` classes that have unimplemented abstract methods.\n\n## Why is this bad?\n\nA class decorated with `@final` cannot be subclassed. If such a class has abstract\nmethods that are not implemented, the class can never be properly instantiated, as\nthe abstract methods can never be implemented (since subclassing is prohibited).\n\nAt runtime, instantiation of classes with unimplemented abstract methods is only\nprevented for classes that have `ABCMeta` (or a subclass of it) as their metaclass.\nHowever, type checkers also enforce this for classes that do not use `ABCMeta`, since\nthe intent for the class to be abstract is clear from the use of `@abstractmethod`.\n\n## Example\n\n```python\nfrom abc import ABC, abstractmethod\nfrom typing import final\n\n\nclass Base(ABC):\n @abstractmethod\n def method(self) -> int: ...\n\n\n@final\n# `Derived` does not implement `method`\nclass Derived(Base): # error\n pass\n```", @@ -1109,7 +1126,7 @@ }, "invalid-named-tuple": { "title": "detects invalid `NamedTuple` class definitions", - "description": "## What it does\n\nChecks for invalidly defined `NamedTuple` classes.\n\n## Why is this bad?\n\nAn invalidly defined `NamedTuple` class may lead to the type checker\ndrawing incorrect conclusions. It may also lead to `TypeError`s or\n`AttributeError`s at runtime.\n\n## Examples\n\nA class definition cannot combine `NamedTuple` with other base classes\nin multiple inheritance; doing so raises a `TypeError` at runtime. The sole\nexception to this rule is `Generic[]`, which can be used alongside `NamedTuple`\nin a class's bases list.\n\n```pycon\n>>> from typing import NamedTuple\n>>> class Foo(NamedTuple, object): ...\nTypeError: can only inherit from a NamedTuple type and Generic\n```\n\nFurther, `NamedTuple` field names cannot start with an underscore:\n\n```pycon\n>>> from typing import NamedTuple\n>>> class Foo(NamedTuple):\n... _bar: int\nValueError: Field names cannot start with an underscore: '_bar'\n```\n\n`NamedTuple` classes also have certain synthesized attributes (like `_asdict`, `_make`,\n`_replace`, etc.) that cannot be overwritten. Attempting to assign to these attributes\nwithout a type annotation will raise an `AttributeError` at runtime.\n\n```pycon\n>>> from typing import NamedTuple\n>>> class Foo(NamedTuple):\n... x: int\n... _asdict = 42\nAttributeError: Cannot overwrite NamedTuple attribute _asdict\n```", + "description": "## What it does\n\nChecks for invalidly defined `NamedTuple` classes.\n\n## Why is this bad?\n\nAn invalidly defined `NamedTuple` class may lead to the type checker\ndrawing incorrect conclusions. It may also lead to `TypeError`s or\n`AttributeError`s at runtime.\n\n## Examples\n\nA class definition cannot combine `NamedTuple` with other base classes\nin multiple inheritance; doing so raises a `TypeError` at runtime. The sole\nexception to this rule is `Generic[]`, which can be used alongside `NamedTuple`\nin a class's bases list.\n\n```pycon\n>>> from typing import NamedTuple\n>>> class Foo(NamedTuple, object): ...\nTypeError: can only inherit from a NamedTuple type and Generic\n```\n\nFurther, `NamedTuple` field names cannot start with an underscore:\n\n```pycon\n>>> from typing import NamedTuple\n>>> class Foo(NamedTuple):\n... _bar: int\nValueError: Field names cannot start with an underscore: '_bar'\n```\n\n`NamedTuple` classes also have certain synthesized attributes (like `_asdict`, `_make`,\n`_replace`, etc.) that cannot be overwritten. Attempting to assign to these attributes\nwithout a type annotation will raise an `AttributeError` at runtime.\n\n```pycon\n>>> from typing import NamedTuple\n>>> class Foo(NamedTuple):\n... x: int\n... _asdict = 42\nAttributeError: Cannot overwrite NamedTuple attribute _asdict\n```\n\nFinally, `NamedTuple` field annotations cannot use the `ClassVar` or `Final` type\nqualifiers. These qualifiers also cause a runtime error when annotations are evaluated eagerly:\n\n```pycon\n>>> from typing import ClassVar, NamedTuple\n>>> class Foo(NamedTuple):\n... x: ClassVar[int]\nTypeError: typing.ClassVar[int] is not valid as type argument\n```", "default": "error", "oneOf": [ { @@ -1299,7 +1316,7 @@ }, "invalid-type-alias-type": { "title": "detects invalid TypeAliasType definitions", - "description": "## What it does\n\nChecks for the creation of invalid `TypeAliasType`s\n\n## Why is this bad?\n\nThere are several requirements that you must follow when creating a `TypeAliasType`.\n\n## Examples\n\n```toml\n[environment]\npython-version = \"3.12\"\n```\n\n```python\nfrom typing import TypeAliasType\n\n\ndef get_name() -> str:\n return \"NewAlias\"\n\n\nIntOrStr = TypeAliasType(\"IntOrStr\", int | str) # okay\n# TypeAliasType name must be a string literal\nNewAlias = TypeAliasType(get_name(), int) # error\n```", + "description": "## What it does\n\nChecks for the creation of invalid `TypeAliasType`s\n\n## Why is this bad?\n\nThere are several requirements that you must follow when creating a `TypeAliasType`.\n\n## Examples\n\n```toml\n[environment]\npython-version = \"3.12\"\n```\n\n```python\nfrom typing import TypeAliasType, TypeVar\n\n\ndef get_name() -> str:\n return \"NewAlias\"\n\n\nIntOrStr = TypeAliasType(\"IntOrStr\", int | str) # okay\n# TypeAliasType name must be a string literal\nNewAlias = TypeAliasType(get_name(), int) # error\n\nT = TypeVar(\"T\")\nGenericAlias = TypeAliasType(\"GenericAlias\", list[T], type_params=(T,)) # okay\n# TypeAliasType type parameters must be type variables\nInvalidAlias = TypeAliasType(\"InvalidAlias\", list[T], type_params=(list[T],)) # error\n```", "default": "error", "oneOf": [ { @@ -2227,6 +2244,26 @@ } ] }, + "unsound-return-statement": { + "title": "detects return statements that unsoundly return a type that is not a subtype of the function's annotated return type", + "description": "## What it does\n\nDetects `return` statements that unsoundly return a type that is not a [subtype] of the function's\nannotated return type.\n\nThis lint is a stricter version of `invalid-return-type`.\n\n## Why is this bad?\n\nBy default, type checkers consider a `return` statement valid if the inferred type of the object\nbeing returned is [assignable] to the annotated return type of the function it's in. However, this\nmakes it easy for incorrect types to percolate through your code unexpectedly due to a single\nexpression being inferred as `Any`. This can easily lead to runtime errors that are not caught by\nthe type checker:\n\n```py\nfrom typing import Any\n\n\ndef returns_any() -> Any:\n return \"foo\"\n\n\ndef returns_int() -> int:\n # error: \"Unsound return statement: `Any` is not a subtype of `int`\"\n return returns_any()\n\n\n# fails at runtime, even though the type checker infers both operands as being of type `int`!\nreturns_int() + 42\n```\n\nThis rule allows you to use [\"fully static\"][fully-static] return types as \"typed boundaries\" for\nyour code. With this rule enabled, ty would emit an error on the `return returns_any()` statement\nin `returns_int`, since the `returns_any()` call is inferred as having type `Any`, and `Any` is not\na subtype of `int`. This helps prevent the unsoundness from spreading far from its original source\n(in this case, the return type of the `returns_any` function).\n\nNote that this rule is only applied to functions annotated as returning\n[fully static][fully-static] types. It will not trigger if `Any` or `Unknown` appear anywhere in\nyour return type, either implicitly or explicitly:\n\n```py\nfrom typing import Any\n\n\ndef returns_any() -> Any:\n return \"foo\"\n\n\n# error: [missing-type-argument]\ndef returns_unparameterized_tuple() -> tuple:\n # no error, since the return type is implicitly `tuple[Unknown, ...]`\n # (which is what the `missing-type-argument` error is complaining about on the line above!)\n return returns_any()\n\n\ndef returns_list_of_any() -> list[Any]:\n # no error, since the return type is explicitly `list[Any]`\n return returns_any()\n```\n\nThis rule works especially well when combined with ty's\n`missing-type-argument` rule, and the Ruff rules [`ANN201`][ann201],\n[`ANN202`][ann202], [`ANN204`][ann204], [`ANN205`][ann205], and [`ANN206`][ann206]. Enabling all\nthese rules at once effectively makes it much less likely that a `return` statement can lead to\nunsoundness \"leaking\" out of a function unless that function has been *explicitly* annotated with\na dynamic type in some way (`-> Any` or `-> tuple[Any]`, for example).\n\nThis rule is analogous to mypy's [`no-any-return`][no-any-return] error code, which is enabled by\nmypy’s [`--strict`][mypy-strict] mode and can also be enabled on its own using mypy’s\n[`--warn-return-any`][warn-return-any] option.\n\n## Examples\n\n```py\nfrom typing import Any\n\n\ndef returns_any() -> Any:\n return 42\n\n\ndef returns_int() -> int:\n # error: \"Unsound return statement: `Any` is not a subtype of `int`\"\n return returns_any()\n```\n\nNarrow the type to a subtype of `int` to fix the diagnostic:\n\n```py\nfrom typing import Any\nfrom typing_extensions import reveal_type\n\n\ndef returns_any() -> Any:\n return 42\n\n\ndef returns_int() -> int:\n my_int = returns_any()\n assert isinstance(my_int, int)\n reveal_type(my_int) # revealed: Any & int\n return my_int # no error: `Any & int` is a subtype of `int`\n```\n\n## Default level\n\nThis rule is disabled by default. It is intended for advanced users wanting additional soundness\nchecks from their type checker, not for users who have just started to use type checkers on their\nPython code.\n\n## See also\n\n- `unsound-yield` is a similar rule that triggers on unsound `yield` expressions rather than unsound `return` statements\n\n[ann201]: https://docs.astral.sh/ruff/rules/missing-return-type-undocumented-public-function/\n[ann202]: https://docs.astral.sh/ruff/rules/missing-return-type-private-function/\n[ann204]: https://docs.astral.sh/ruff/rules/missing-return-type-special-method/\n[ann205]: https://docs.astral.sh/ruff/rules/missing-return-type-static-method/\n[ann206]: https://docs.astral.sh/ruff/rules/missing-return-type-class-method/\n[assignable]: https://typing.python.org/en/latest/spec/glossary.html#term-assignable\n[fully-static]: https://typing.python.org/en/latest/spec/glossary.html#term-fully-static-type\n[mypy-strict]: https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-strict\n[no-any-return]: https://mypy.readthedocs.io/en/stable/error_code_list2.html#code-no-any-return\n[subtype]: https://typing.python.org/en/latest/spec/glossary.html#term-subtype\n[warn-return-any]: https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-warn-return-any", + "default": "ignore", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "unsound-yield": { + "title": "detects yield expressions that unsoundly yield a type that is not a subtype of the generator's annotated yield type", + "description": "## What it does\n\nDetects `yield` and `yield from` expressions that unsoundly yield a type that is not a [subtype] of\nthe generator function's annotated yield type.\n\nThis lint is a stricter version of `invalid-yield`.\n\n## Why is this bad?\n\nBy default, type checkers consider a yielded value valid if its inferred type is [assignable] to the\ngenerator's annotated yield type. However, this\nmakes it easy for incorrect types to percolate through your code unexpectedly due to a single\nexpression being inferred as `Any`. This can easily lead to runtime errors that are not caught by\nthe type checker:\n\n```py\nfrom typing import Any, Generator\n\n\ndef returns_any() -> Any:\n return \"not an integer\"\n\n\ndef integers() -> Generator[int]:\n # error: \"Unsound `yield`: `Any` is not a subtype of `int`\"\n yield returns_any()\n\n\n# Fails at runtime, even though the type checker infers `integers` as yielding only `int`s!\nsum(integers())\n```\n\nThis rule treats [fully static][fully-static] yield types as \"typed boundaries\" for your code. With this rule enabled, ty would emit an error on the `yield returns_any()` statement\nin `integers`, since the `returns_any()` call is inferred as having type `Any`, and `Any` is not\na subtype of `int`. This helps prevent the unsoundness from spreading far from its original source\n(in this case, the return type of the `returns_any` function).\n\nNote that this rule is only applied to functions annotated as yielding\n[fully static][fully-static] types. It will not trigger if `Any` or `Unknown` appear anywhere in\nyour function's yield type, either implicitly or explicitly. It will still trigger on functions that have non-fully-static send and/or return types, however:\n\n```py\nfrom typing import Any, Generator\n\n\ndef returns_any() -> Any:\n return \"not an integer\"\n\n\ndef dynamic_yield_type() -> Generator[Any]:\n yield returns_any()\n\n\ndef static_yield_type() -> Generator[int, Any, Any]:\n # error: \"Unsound `yield`: `Any` is not a subtype of `int`\"\n yield returns_any()\n```\n\nThis rule works especially well when combined with ty's\n`missing-type-argument` rule, and the Ruff rules [`ANN201`][ann201],\n[`ANN202`][ann202], [`ANN204`][ann204], [`ANN205`][ann205], and [`ANN206`][ann206]. Enabling all\nthese rules at once effectively makes it much less likely that a `yield` expression can lead to\nunsoundness \"leaking\" out of a function unless that function has been *explicitly* annotated with\na dynamic type in some way (`-> Generator[Any]` or `-> Generator[tuple[Any]]`, for example).\n\n## Examples\n\n```py\nfrom typing import Any, Iterator\n\n\ndef returns_any() -> Any:\n return \"foo\"\n\n\ndef any_iterator() -> Iterator[Any]:\n yield \"foo\"\n\n\ndef integers() -> Iterator[int]:\n # error: \"Unsound `yield`: `Any` is not a subtype of `int`\"\n yield returns_any()\n # error: \"Unsound `yield from`: `Any` is not a subtype of `int`\"\n yield from any_iterator()\n```\n\nNarrow the value before yielding it to fix the diagnostics:\n\n```py\nfrom typing import Any, Iterator\n\n\ndef returns_any() -> Any:\n return 42\n\n\ndef any_iterator() -> Iterator[Any]:\n yield \"foo\"\n\n\ndef integers() -> Iterator[int]:\n value = returns_any()\n assert isinstance(value, int)\n yield value\n\n for value in any_iterator():\n assert isinstance(value, int)\n yield value\n```\n\n## Default level\n\nThis rule is disabled by default. It is intended for users who want stricter soundness checks at\ngenerator boundaries.\n\n## See also\n\n- `unsound-return-statement` is a similar rule that triggers on unsound `return` statements rather than unsound `yield` expressions\n\n[ann201]: https://docs.astral.sh/ruff/rules/missing-return-type-undocumented-public-function/\n[ann202]: https://docs.astral.sh/ruff/rules/missing-return-type-private-function/\n[ann204]: https://docs.astral.sh/ruff/rules/missing-return-type-special-method/\n[ann205]: https://docs.astral.sh/ruff/rules/missing-return-type-static-method/\n[ann206]: https://docs.astral.sh/ruff/rules/missing-return-type-class-method/\n[assignable]: https://typing.python.org/en/latest/spec/glossary.html#term-assignable\n[fully-static]: https://typing.python.org/en/latest/spec/glossary.html#term-fully-static-type\n[subtype]: https://typing.python.org/en/latest/spec/glossary.html#term-subtype", + "default": "ignore", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, "unspecialized-reified-generic": { "title": "detects calls to reified generic functions without explicit specialization", "description": "## What it does\nChecks for calls to a basedpython reified generic function whose\nspecialization is neither written explicitly nor inferable from the\narguments.\n\n## Why is this bad?\nA function whose type parameter is referenced in a value position is\n*reified*: the type parameter behaves like a positional parameter that\nis filled by the `[...]` specialization step. A bare call is legal only\nwhen the transpiler can inject that step — every type parameter must\nsolve, from the arguments or its PEP 696 default, to a type with a\nruntime spelling at the call site. Otherwise the parameter has no\nvalue at runtime.\n\n## Example\n\n```by\ndef f[T](t: object):\n print(T)\n\nf[int](1) # ok\nf(1) # error: `T` appears nowhere in the signature\n\ndef g[T](t: T):\n print(T)\n\ng(1) # ok — transpiles to g[int](1)\n```", @@ -2363,6 +2400,13 @@ } ] }, + "exclude-scripts": { + "description": "Whether to exclude files containing PEP 723 inline script metadata unless they are\nexplicitly passed on the command line.", + "type": [ + "boolean", + "null" + ] + }, "include": { "description": "A list of files and directories to check. The `include` option\nfollows a similar syntax to `.gitignore` but reversed:\nIncluding a file or directory will make it so that it (and its contents)\nare type checked.\n\n- `./src/` matches only a directory\n- `./src` matches both files and directories\n- `src` matches a file or directory named `src`\n- `*` matches any (possibly empty) sequence of characters (except `/`).\n- `**` matches zero or more path components.\n This sequence **must** form a single path component, so both `**a` and `b**` are invalid and will result in an error.\n A sequence of more than two consecutive `*` characters is also invalid.\n- `?` matches any single character except `/`\n- `[abc]` matches any character inside the brackets. Character sequences can also specify ranges of characters, as ordered by Unicode,\n 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\nmatches `/src` and not `/test/src`).\n\n`exclude` takes precedence over `include`.", "anyOf": [ @@ -2380,18 +2424,6 @@ "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.\nThe project root (`.`) is always included. Additionally, the following directories are included\nif they exist and are not packages (i.e. they do not contain `__init__.py` or `__init__.pyi` files):\n\n* `./src`\n* `./` (if a `.//` directory exists)\n* `./python`", - "anyOf": [ - { - "$ref": "#/definitions/RelativePathBuf" - }, - { - "type": "null" - } - ], - "deprecated": true } }, "additionalProperties": false diff --git a/uv.lock b/uv.lock index 7f8669c660..0051e47069 100644 --- a/uv.lock +++ b/uv.lock @@ -3,8 +3,7 @@ revision = 3 requires-python = ">=3.7" resolution-markers = [ "python_full_version >= '3.12'", - "python_full_version >= '3.10' and python_full_version < '3.12'", - "python_full_version >= '3.8' and python_full_version < '3.10'", + "python_full_version >= '3.8' and python_full_version < '3.12'", "python_full_version < '3.8'", ] @@ -71,7 +70,7 @@ release = [ [package.metadata] [package.metadata.requires-dev] -dev = [{ name = "prek", marker = "python_full_version >= '3.12'", specifier = "==0.4.9" }] +dev = [{ name = "prek", marker = "python_full_version >= '3.12'", specifier = "==0.4.12" }] docs = [ { name = "basedpython-pygments", marker = "python_full_version >= '3.12'", directory = "python/basedpython-pygments" }, { name = "zensical", marker = "python_full_version >= '3.12'" }, @@ -298,11 +297,11 @@ wheels = [ [[package]] name = "markdown" -version = "3.10.2" +version = "3.10.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +sdist = { url = "https://files.pythonhosted.org/packages/29/6f/da4c6aea59b3001f2e8c0ec7497475aadaf3b021c10cab5b2858f0f32b26/markdown-3.10.3.tar.gz", hash = "sha256:3589362618f743188b4d955b874402bc814f4f83f544dc207719f4baa7d9c45f", size = 372596, upload-time = "2026-07-30T19:05:29.005Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, + { url = "https://files.pythonhosted.org/packages/64/69/4a5af2bc115a9a33fefe51709749de8262be3f9ba063d1753a837cdbc49c/markdown-3.10.3-py3-none-any.whl", hash = "sha256:fa6c92a00a4a3c98b22728c64a935ae1928250ae65058a6ded814d2cc29a4cea", size = 110757, upload-time = "2026-07-30T19:05:27.883Z" }, ] [[package]] @@ -515,26 +514,26 @@ wheels = [ [[package]] name = "prek" -version = "0.4.9" +version = "0.4.12" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ea/df/94ed29398576e03494c5aacda8bfed9536edf348bed29cd09f382f2b9b23/prek-0.4.9.tar.gz", hash = "sha256:f8b86441484a5756f3fdb6f3b201d3d448f8845902a84653d78cbf6f875c424f", size = 492711, upload-time = "2026-07-11T11:04:04.332Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/5c/cb6e63f7e5a58a5313ddb70409174f4dc004e4b0910b8a8d3f59b2225a95/prek-0.4.12.tar.gz", hash = "sha256:04beeba7f40437cd2f36804b84101bd7f3c9fb40b52da46a25604642ab2bfb09", size = 519080, upload-time = "2026-08-03T11:28:33.147Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/02/632446b72103daf92526203bf83cb76043ebce70588632aa2aea3741da07/prek-0.4.9-py3-none-linux_armv6l.whl", hash = "sha256:7b240ad6f679104309a944c4dd427ccc46d9aaf4f53ee07379c02bf7578c2750", size = 5637604, upload-time = "2026-07-11T11:03:38.985Z" }, - { url = "https://files.pythonhosted.org/packages/57/bf/89daefc85a1db9b8c525731c77121de0a8f57731e81c99d823e919e1f6a5/prek-0.4.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5cbb220d6d77cb047747dcabf421375fdfdd958e01a998a854758698d38fb239", size = 5960396, upload-time = "2026-07-11T11:03:40.953Z" }, - { url = "https://files.pythonhosted.org/packages/42/39/0e448da00671e77740be32cca96530ef64bcdbed178acce7f155b87f6057/prek-0.4.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fd85df4c186becdc47b2e6155de8cace99133c7517387e30f08ca15b93ead11f", size = 5524982, upload-time = "2026-07-11T11:03:42.605Z" }, - { url = "https://files.pythonhosted.org/packages/71/e5/1beceff9cfcf02817c06f38e726c9875cd003f62cbfa516f282fb3c3109b/prek-0.4.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:ba40511145e948d461d6641b556b6ff671b186b0c90743600b9dcb788dd5eb5c", size = 5793691, upload-time = "2026-07-11T11:03:44.106Z" }, - { url = "https://files.pythonhosted.org/packages/7e/17/99e4884c45c46be90e81e220d2bdd5020716963707ef6702361cc398dd91/prek-0.4.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0cc5c06ae448568d076bb5e7ce4d630879d6467b2a89fd79061c349a47826efa", size = 5544549, upload-time = "2026-07-11T11:03:45.564Z" }, - { url = "https://files.pythonhosted.org/packages/0c/4b/63f5fe22d867a6e07b12e8a7887a0f3b193c2ef0fa9ed80649f2d257f4ac/prek-0.4.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31af616c216ec7e47913802b082ca816d707d24738fa58eae0463ae296cbe71e", size = 5948798, upload-time = "2026-07-11T11:03:47.424Z" }, - { url = "https://files.pythonhosted.org/packages/1f/89/90d5005436afb6ab99d3ba6599820fe0f0fcb776f5e9e362b5a19e10cbea/prek-0.4.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a1ef842d7f19879fac2135c42ba15508f2677346ba800bc8bb82e5f43fe6a6ec", size = 6696938, upload-time = "2026-07-11T11:03:49Z" }, - { url = "https://files.pythonhosted.org/packages/2a/62/068dd25e1106e262b0ce69ffcdc594cf52d85aa13f64628f851e458980d4/prek-0.4.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:442ecf6a454c692bc8a2d5a16936a0fe8e3419727ff208e15818932acca9923a", size = 6170870, upload-time = "2026-07-11T11:03:50.407Z" }, - { url = "https://files.pythonhosted.org/packages/8e/42/bb1f3f3d84af4d12bb675ff071305fa776d3525c10626465d9960ad93c21/prek-0.4.9-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:5b3c590252e3d5724ebed3774695712ff7cb554743b25184808aa5f42b06bd4c", size = 5800355, upload-time = "2026-07-11T11:03:51.882Z" }, - { url = "https://files.pythonhosted.org/packages/a5/ca/dfc8312b4a7ce8fa100ce37a843279d108eec7e7fe2ddbe069448acd1642/prek-0.4.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1dd283b15dec4da29caa3910bd72c8c9d7df93209770503465ad109d6370cb8e", size = 5655126, upload-time = "2026-07-11T11:03:53.388Z" }, - { url = "https://files.pythonhosted.org/packages/25/8a/b19413cb64b81c18502ea7bbef32897f9126b8e53b58cd656996573dc53c/prek-0.4.9-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:cc25e30e1700a5c7dd9bad665c321e5589b51502bb1bbc6ada45e326d08b428b", size = 5517839, upload-time = "2026-07-11T11:03:54.884Z" }, - { url = "https://files.pythonhosted.org/packages/94/af/900df3f7535e87045df331646c6a01bef6e77dba7f2bdb53483f30cbb988/prek-0.4.9-py3-none-musllinux_1_1_i686.whl", hash = "sha256:4ff5947deeb9a92e6508dfba8b27962dfb927bf60fd36472c9ae862df96fb38c", size = 5802556, upload-time = "2026-07-11T11:03:56.787Z" }, - { url = "https://files.pythonhosted.org/packages/e9/7b/80e560cbe396d0f8687012769cb2d7d7f3428dd019549225ebf205dea806/prek-0.4.9-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:8320ca167d41855d9c4fed66df599f31f96307cbb0da1311a9fe465152e20bd5", size = 6285747, upload-time = "2026-07-11T11:03:58.099Z" }, - { url = "https://files.pythonhosted.org/packages/a3/d4/01ae3b99d09559a69befd128859d0036c604608dd9c6c99986592dde3c21/prek-0.4.9-py3-none-win32.whl", hash = "sha256:b1e8d3bc88ddce6414853468ed8126f45d4ae20f2f4677801ade20ad67a826fa", size = 5320862, upload-time = "2026-07-11T11:03:59.803Z" }, - { url = "https://files.pythonhosted.org/packages/28/68/d038b14f0220fed197be8bc93229e6ea7ad460803ff8a26e4b14a8c81f66/prek-0.4.9-py3-none-win_amd64.whl", hash = "sha256:ed1b4f87a13d1565e8731c60db7fa058966049cbb4d8872d160add510a286558", size = 5706850, upload-time = "2026-07-11T11:04:01.207Z" }, - { url = "https://files.pythonhosted.org/packages/02/4a/57d04de49f591088901794cc22f36563a102681e3512ae17ee6085cd2f30/prek-0.4.9-py3-none-win_arm64.whl", hash = "sha256:7eab3900d9ea614c8ea0d0d55a8b708f0c88e43c966dc8b13a4e36c1e398dd16", size = 5540477, upload-time = "2026-07-11T11:04:02.815Z" }, + { url = "https://files.pythonhosted.org/packages/f3/23/5811a3161e072e5f93e4da01af611ee30c32922507b8ab4d9873df6affd3/prek-0.4.12-py3-none-linux_armv6l.whl", hash = "sha256:cd92000b051e433f26340821cf1cc8e6e3960f1275f3d516ca01f05905abba64", size = 5793226, upload-time = "2026-08-03T11:28:09.534Z" }, + { url = "https://files.pythonhosted.org/packages/a3/88/8607845d94eb1482e1bd335dadf098618f077a15775f7e98de99669052b4/prek-0.4.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5904fe6c6ab26e7d8792a3c7f1e3fc8d94fcfb63ad33b247c35f004b62cb6275", size = 6132269, upload-time = "2026-08-03T11:28:11.147Z" }, + { url = "https://files.pythonhosted.org/packages/ac/28/571d79ba457fbd9ecf40ae879c91952e12f5fa475306218c91139b86db7a/prek-0.4.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:df3eff1db9c24dc293010a07bc7a0ae0c541d55af828f5586405dedc28c4920d", size = 5614964, upload-time = "2026-08-03T11:28:12.983Z" }, + { url = "https://files.pythonhosted.org/packages/b0/a9/3f5cb79a73c764a8ac38d5bcd51e0df57239856eca7949b09bdac4338bf3/prek-0.4.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:c7733b44ca772ea32ec6a8bee669d0358bdf45873e79767afed196065084f31c", size = 5941047, upload-time = "2026-08-03T11:28:14.45Z" }, + { url = "https://files.pythonhosted.org/packages/8c/00/1dfed0ef8af10c5c32aa903486dccd33d2df171f3d945a037c5692f10760/prek-0.4.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87f170cf1ffd6e3a196f947b83dff1f6c2cd68635f8d49740278bebe7b682262", size = 5707994, upload-time = "2026-08-03T11:28:15.914Z" }, + { url = "https://files.pythonhosted.org/packages/c0/bd/5f388f6cbdc0445b850e7c1a160d0be67fcef8bf221e3c8141a1feccef17/prek-0.4.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57dad513831f060cf73808df8edec29d46ec311435aa69f21c80edebf23dc5e1", size = 6133784, upload-time = "2026-08-03T11:28:17.184Z" }, + { url = "https://files.pythonhosted.org/packages/ba/47/342091a987bf68a74acec6d226a40ce7d51faf0019aa4126cc7bc952f8a7/prek-0.4.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b204844abc7ded983471f576ae8dc13b99e9b8d022e4d4b46176c6654769c9d8", size = 6901589, upload-time = "2026-08-03T11:28:18.545Z" }, + { url = "https://files.pythonhosted.org/packages/5b/8a/3ef7bdc3c3441649ebc040b9e164a13163e1e5fabae23e7bbb901992f3de/prek-0.4.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43b0a5a9d3f2f77871fdcb7893bfc5c8fe7e44f4e603ce6e4712bfec96b2d6f2", size = 6342189, upload-time = "2026-08-03T11:28:20Z" }, + { url = "https://files.pythonhosted.org/packages/c4/da/6277908442301b1b92a2879f6b04aaa03accb900f80e42776fc28b8197ef/prek-0.4.12-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:0d188e572c306cc44b96e1bae5647e25b7bd311113f3f3f4a67320c257ee64a3", size = 5951250, upload-time = "2026-08-03T11:28:21.339Z" }, + { url = "https://files.pythonhosted.org/packages/a3/68/bff51a7332837edb1ecbe017325adb7fafd69b9c7828ddc81a1334b884af/prek-0.4.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:986f52d104b7066190f0f32aebe3467710356de265e9bfd892101ba99371db4d", size = 5804147, upload-time = "2026-08-03T11:28:22.656Z" }, + { url = "https://files.pythonhosted.org/packages/aa/de/b7f544971072ed7814125145dfeb1f7c15cce6b78ccea65a96298ff37838/prek-0.4.12-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:13e34d9e09bafcbf1f25a01cf86985e2c5e486591d3f45b2786ba3de82e5153a", size = 5680104, upload-time = "2026-08-03T11:28:24.271Z" }, + { url = "https://files.pythonhosted.org/packages/68/94/95942bcc20a6a91ec2989aa30fdeb00ad095be736ec48b4bbcf0376166b1/prek-0.4.12-py3-none-musllinux_1_1_i686.whl", hash = "sha256:3d0208370da73e8b5bc97f2492dc3975f8dd2c22f4bf6e1f2cf3342503764b52", size = 5975030, upload-time = "2026-08-03T11:28:25.683Z" }, + { url = "https://files.pythonhosted.org/packages/ef/6d/26e6497198d81cf9aa82495400aef46adea8df3e4a4efc5f00e3b6ab3292/prek-0.4.12-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:b1005f42920111bec1403c25e8f2f12ec7af0be06686cc3b8dcf85429af908a8", size = 6458532, upload-time = "2026-08-03T11:28:27.121Z" }, + { url = "https://files.pythonhosted.org/packages/44/02/ee140c2eb4701bd194db429d84630733492be94897d5f72b61d6f11e6619/prek-0.4.12-py3-none-win32.whl", hash = "sha256:afee229488dcceaea282288e4d7096a93da5a8b85649d9ef506dbdbcd78f38a7", size = 5502213, upload-time = "2026-08-03T11:28:28.691Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7b/744cff84def48c1ce38c0b4f643a3553c66976c5bb7869ab7317044870e4/prek-0.4.12-py3-none-win_amd64.whl", hash = "sha256:fdd27bad8adafea8fe77606950ca09200d59296a47ab131cfb88718d460949d7", size = 5868065, upload-time = "2026-08-03T11:28:30.377Z" }, + { url = "https://files.pythonhosted.org/packages/46/1d/e2c0fc222904ef73df1739b11a83edc29e38bc4bc61259f2ca6d2f15abb0/prek-0.4.12-py3-none-win_arm64.whl", hash = "sha256:45e34a24fba4a4e4568682477158591698efc2375b8d1d418ae424691c4bd01b", size = 5632819, upload-time = "2026-08-03T11:28:31.743Z" }, ] [[package]] @@ -1004,7 +1003,7 @@ wheels = [ [[package]] name = "zensical" -version = "0.0.51" +version = "0.0.53" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -1016,18 +1015,18 @@ dependencies = [ { name = "pyyaml" }, { name = "tomli" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b8/f7/d07ffb268ca86afb26b7f32dbabe25dec03d3aa63ba4d876720c84681d33/zensical-0.0.51.tar.gz", hash = "sha256:de25de067bedfa18f916d7f366fd64a7fbf09bfcc615b44d1ddbe3b5fe02ab49", size = 3979640, upload-time = "2026-07-17T18:08:03.445Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/8b/d916d8226738421a847f039f71278fd07789744c32e9b40abcfa8b849ad8/zensical-0.0.53.tar.gz", hash = "sha256:61672d3e6389822b5738e099816dbc07416ea84db67c2b1cb7e6ea977d2e04d7", size = 3988318, upload-time = "2026-08-04T14:08:54.721Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/21/02db3e1fb3904016bfac310037c95b9f1eaaf0ffe7b4a84f14263a7d95df/zensical-0.0.51-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:134d776afa526098e05e34713e2f577c075e57a232e01b97842bb0206716afce", size = 12791154, upload-time = "2026-07-17T18:07:20.748Z" }, - { url = "https://files.pythonhosted.org/packages/a2/35/b0d96f58253514cb3d08f5779020ab01ee5472334fb984b92e3fc9e9c9ac/zensical-0.0.51-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e97ab39668ae3b452c550634e921a0336443743aae5e1fe031c7bb57d049e535", size = 12692190, upload-time = "2026-07-17T18:07:24.553Z" }, - { url = "https://files.pythonhosted.org/packages/2e/90/7a60e126a10c37c6b789938ff17e73fe76bba707fa029cb40ac659aeaa82/zensical-0.0.51-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c9579809f88608e7aa2cff516fff9d267d74a843cf6088a5f4227de2f092bb5", size = 13139337, upload-time = "2026-07-17T18:07:27.885Z" }, - { url = "https://files.pythonhosted.org/packages/ae/c3/9101c97b90d4713ef2816db03366a45ae4762efebffd296737a2dd2df325/zensical-0.0.51-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:296dc7a14aa28b81a58eb57df2d5c9c9a4b0de7e90c11d99c943354287952925", size = 13069851, upload-time = "2026-07-17T18:07:31.814Z" }, - { url = "https://files.pythonhosted.org/packages/d2/79/0474df9e15a2c18f6281a786e10177c1b6e16feac1c568e7f36ad39b339c/zensical-0.0.51-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f779d2d87b4bf228cf2e279bc0ae6bcf3b36a9335ff283a317d01f7c15ae46b2", size = 13451083, upload-time = "2026-07-17T18:07:35.543Z" }, - { url = "https://files.pythonhosted.org/packages/fe/6f/91bbf78f704d5fd4c0c9be27d6bce3b6e4c2c339e4dcd6e7cf19ecda643c/zensical-0.0.51-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f813a1514a90890ca86248a8d54b81b2164bcbff11a6bcf11b01e1c01a1454", size = 13110446, upload-time = "2026-07-17T18:07:38.783Z" }, - { url = "https://files.pythonhosted.org/packages/d9/89/aa9a95f81771614c37bdc52b8ab21fcdef4c8de7c9cedf34e9bf62674281/zensical-0.0.51-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:186ef37e0eee0e969e2cfae47b1b97775e3164e2cba95c71faa4dd6ef47ed009", size = 13315871, upload-time = "2026-07-17T18:07:42.43Z" }, - { url = "https://files.pythonhosted.org/packages/08/11/1bf6e9ded29d376f8c12644cc4de04676b010fee8caa17f682606b1f16d5/zensical-0.0.51-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f5d91ce246ed930224603083cef02ae8947132fc7c52901d72015ea03526fa58", size = 13344382, upload-time = "2026-07-17T18:07:46.066Z" }, - { url = "https://files.pythonhosted.org/packages/d6/ec/663f16ff82d08b212e7c3236a88bd332f73331f94ddac1c91aaf882bbd1e/zensical-0.0.51-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:b1108eae82c6e8ffc33026f60b485c1512647a5333be4f547166b7c8877b98af", size = 13499628, upload-time = "2026-07-17T18:07:49.196Z" }, - { url = "https://files.pythonhosted.org/packages/60/b4/7f1b6c3cf06d9f6ff5216523168a5d6ccc693444d5ceeb911eca97b30d98/zensical-0.0.51-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6fa0ecaf14f56841bfc595fa141396350c72aafbec73a016ebe3c824ed21ac72", size = 13451420, upload-time = "2026-07-17T18:07:52.563Z" }, - { url = "https://files.pythonhosted.org/packages/0a/44/be4bc09ec8f69e7be1b07b875887961c4e0e478b03a10d2cc624ef28fbe6/zensical-0.0.51-cp310-abi3-win32.whl", hash = "sha256:fb7ff4946b72168759c6af0a29cf5de4c38aebe633a83292e8cd4145b5213cc2", size = 12375639, upload-time = "2026-07-17T18:07:56.081Z" }, - { url = "https://files.pythonhosted.org/packages/1f/85/aa827c244ed4f404e99a91c3ecf5e5adb62eca806a9e9c8e3333bbad8660/zensical-0.0.51-cp310-abi3-win_amd64.whl", hash = "sha256:12529d3d3991b63820952111dc1d1edc29b2c9b3a3abb16c243bcb649631ebf2", size = 12628965, upload-time = "2026-07-17T18:07:59.741Z" }, + { url = "https://files.pythonhosted.org/packages/9d/53/5db8c8e5a257db9a5fff0b77c8e05783283d6aaf42c05e34577f6b59f5d0/zensical-0.0.53-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:292cf9c7c323a50c6e3515d334ca08d9dcc517ce6d9d8ad1cd94d22befab1f56", size = 12835291, upload-time = "2026-08-04T14:08:16.746Z" }, + { url = "https://files.pythonhosted.org/packages/33/73/49a64c2c44aec251336a1cedcccbec7ba3d3eba9dd75d52ed24c09217d86/zensical-0.0.53-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:0f4c1219c534d3cccc0b86093748dc009e0e9d80d4dad8d65e2150c846aa1123", size = 12719959, upload-time = "2026-08-04T14:08:20.279Z" }, + { url = "https://files.pythonhosted.org/packages/d8/3a/2c08429f7c725d1a40d158b84d6aca4b5c4320d09a0a313e875d7dd3bfe5/zensical-0.0.53-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ca63b952b4961461b4376d61603adc2bf9d81b4df4946b2f27e20b2726f881f", size = 13169416, upload-time = "2026-08-04T14:08:23.474Z" }, + { url = "https://files.pythonhosted.org/packages/41/bc/ed057082989645d5ad3245bdf0b14c30334a315f866552c794c2413cf92f/zensical-0.0.53-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34f41b7f37a0430a1378ac13d9a72513fcc53db676c124378cf63cc6f6e22713", size = 13099720, upload-time = "2026-08-04T14:08:26.521Z" }, + { url = "https://files.pythonhosted.org/packages/a4/54/859cf2267ef853ff20eee2af37d898071f821bf30ec3df7d73061b391c78/zensical-0.0.53-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30ed22e9fcedda71888d9fe84f4fdb1aadd3b66cdb0223716f1eecce9ae22b07", size = 13482295, upload-time = "2026-08-04T14:08:29.618Z" }, + { url = "https://files.pythonhosted.org/packages/35/94/f73744d9f4b6107e2740aad58214285b84d4cf0997cde36bced43089b3d0/zensical-0.0.53-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10b0cb72861b14bd985bc5ad0203c35b1da7a19c87c194df3189fab7a910db04", size = 13140985, upload-time = "2026-08-04T14:08:32.731Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ef/7557d859e25e4a74214d718a1528f2a123ff9d84823b49b32df9bc41ef17/zensical-0.0.53-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:15e9813f0f59db6cf1316414301139d030f1690b68af645f1bf68d78bc3defe0", size = 13344554, upload-time = "2026-08-04T14:08:35.924Z" }, + { url = "https://files.pythonhosted.org/packages/0b/d9/3a1011bd4390e85a6f602afca6ff8b862454415a800e7b41471dadd9e6b1/zensical-0.0.53-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:c203493598d6cad890d7cb48f9d75693f648fe0d2347b2f147406a99fd7bb101", size = 13373180, upload-time = "2026-08-04T14:08:39.384Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ee/f4faf3d66d1e854afa43fa5354c1e0c8414af3fc5c563233a3ca7f10e494/zensical-0.0.53-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:ec34844b3bc1855f5c10b99efbeebd27abcd983a9144dbad965609e65915c050", size = 13531133, upload-time = "2026-08-04T14:08:42.679Z" }, + { url = "https://files.pythonhosted.org/packages/5e/98/4a0272bb79bdd326714e552f685d58f31a501ffd49bdc17ae187e92b2581/zensical-0.0.53-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e016062c3299c84be811848d1e81ad0f3f711615f0bed87bb0a1b47f6968a5a4", size = 13480141, upload-time = "2026-08-04T14:08:45.97Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ab/8cbceea1e7f4d6d2ac078a0c34ccd06f1419401248d22f6d6ded4ac9a443/zensical-0.0.53-cp310-abi3-win32.whl", hash = "sha256:abb0af33bb646f15224045baa6c4118b59a2c9c3f80d7cd48edd66ee961c1985", size = 12410234, upload-time = "2026-08-04T14:08:48.871Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ac/65f0ced38274b6c1073a4b1c52ea41b8b43e7e972f5e3979c2f2aca5cc46/zensical-0.0.53-cp310-abi3-win_amd64.whl", hash = "sha256:8b609bc89717b6f276774651ea3a41df21b4813929d2a206ee161a294dc28cd1", size = 12646224, upload-time = "2026-08-04T14:08:51.945Z" }, ]